Assets in UI not rotated properly based on stored rotation value
I have searched the existing issues, both open and closed, to make sure this is not a duplicate report.
- [x] Yes
The bug
Imported images that have a stored rotation value are not being rotated properly in the timeline view or the asset view (web and mobile).
It appears that the orientation value is stored correctly in the metadata of the asset as you can see a value of 8 (ExifOrientation.Rotate270CW in the code) in the table. So the orientation seems to be read and stored properly, but the UI (web and mobile) are not rotating the image when it is displayed.
Below is a screenshot of the UI displaying the image as well as the output of exiftool (filtering for "orientation") on the file itself as well as the XMP sidecar. Note that there is no change in orientation in the file itself, but the sidecar XMP has the updated / corrected orientation value.
$ exiftool DSCF0010.JPG | grep -i orienta
Orientation : Horizontal (normal)
$ exiftool DSCF0010.JPG.xmp | grep -i orienta
Orientation : Rotate 270 CW
The OS that Immich Server is running on
Docker - Proxmox 8.3.5
Version of Immich Server
1.333.0
Version of Immich Mobile App
1.333.0
Platform with the issue
- [ ] Server
- [x] Web
- [x] Mobile
Your docker-compose.yml content
N/A
Your .env content
N/A
Reproduction steps
- Add image to library with stored 'Orientation' in metadata, tested specifically with value in XMP sidecar
- View asset in timeline as well as individual asset view
- Image is not rotated properly
Relevant log output
Additional information
No response
Can you share a file this happens to in a .zip? Have all the background jobs finished processing, and have you changed anything about the thumbnail generation settings?
Reproducible when Orientation is defined in sidecar only.
I have a repo to test orientations - https://github.com/skatsubo/exif-orientation-vs-face-regions/tree/main/images. It contains images having various orientations defined in various places:
- image
- sidecar
- image+sidecar
The issue occurs when Orientation is defined in sidecar only.
This is how it looks in v1.133.1:
UPD. The issue is not specific to v1.133. I observed it on previous versions too.
Send image example and sidecar via discord DM.
The orientation in the DB is only used when generating thumbnails through an embedded preview. The reason for this is that EXIF orientation for HEIC images is only informational and not supposed to be applied to the image. We can change it so it applies the DB orientation unless it's in HEIC format.
The code in question: https://github.com/immich-app/immich/blob/493b9b7a54deffbfeea6107d897e7d20765866f7/server/src/services/media.service.ts#L267-L279
The orientation in the DB is only used when generating thumbnails through an embedded preview. The reason for this is that EXIF orientation for HEIC images is only informational and not supposed to be applied to the image. We can change it so it applies the DB orientation unless it's in HEIC format.
That would be very helpful! It's really confusing that thumbnails are generated with correct orientation, but as soon as you start zooming in, the image flips in the web UI (but not in the Android app).
Related:
- https://github.com/immich-app/immich/issues/15549
- https://github.com/immich-app/immich/discussions/15550
Following the discussion in Possible to change image orientation (rotation) from XMP sidecar? (Reddit) I decided to check orientation handling: whether Immich reads Orientation in different scenarios. I did an experiment and tested a quick monkey patch. This is probably obvious to Immich devs, but nevertheless 😄 See below.
Posting it here because Reddit cannot save/digest my comments with code snippets and screenshots.
Experiment
From https://github.com/skatsubo/exif-orientation-vs-face-regions (shameless plug of my own repo) I took samples covering 3 cases:
- Case 1: media file has embedded Orientation + no sidecar file
- Case 2: both media and sidecar have Orientation, though in different exif/xmp tags
- Case 3: only sidecar has Orientation
In all cases Immich reads Orientation and saves it to asset_exif table in the database.
- Samples' Orientation tags.
Exiftool output for media/sidecar:
exiftool -G1 -Orientation photo.6*
# Case 1. Media has Orientation, no sidecar file
======== photo.6.embedded.jpg
[IFD0] Orientation : Rotate 90 CW
# Case 2. Media and sidecar both have Orientation
======== photo.6.embedded_sidecar.jpg
[IFD0] Orientation : Rotate 90 CW
======== photo.6.embedded_sidecar.jpg.xmp
[XMP-tiff] Orientation : Rotate 90 CW
# Case 3. Media and sidecar, but only sidecar has Orientation
======== photo.6.sidecar.jpg
======== photo.6.sidecar.jpg.xmp
[XMP-tiff] Orientation : Rotate 90 CW
-
Now import them into Immich (external library).
-
Immich logs.
Based on logs (IMMICH_LOG_LEVEL=verbose) Immich reads Orientation in all cases. Immich uses exiftool-vendored under the hood, therefore proper handling of various Orientation tags is expected.
# Case 1
[Nest] 7 - 08/24/2025, 3:21:41 PM VERBOSE [Microservices:MetadataService] Object(41) {
SourceFile: '/img/photo.6.embedded.jpg',
Orientation: 6,
FileSize: 101551,
# Case 2
[Nest] 7 - 08/24/2025, 3:26:16 PM VERBOSE [Microservices:MetadataService] Object(41) {
SourceFile: '/img/photo.6.embedded_sidecar.jpg.xmp',
Orientation: 6,
FileSize: 1904,
# Case 3
# orientation appears near the bottom of the exif tags list, perhaps due to being read from sidecar only
[Nest] 7 - 08/24/2025, 3:28:54 PM VERBOSE [Microservices:MetadataService] Object(40) {
SourceFile: '/img/photo.6.sidecar.jpg.xmp',
FileSize: 1904,
...
Orientation: 6,
- Records in the database.
select a."originalFileName", a."sidecarPath", ae.orientation
from asset a inner join asset_exif ae on a.id = ae."assetId"
where a."originalFileName" like 'photo.6%';
originalFileName | sidecarPath | orientation
------------------------------+---------------------------------------+-------------
photo.6.embedded.jpg | | 6 -- Case 1
photo.6.embedded_sidecar.jpg | /img/photo.6.embedded_sidecar.jpg.xmp | 6 -- Case 2
photo.6.sidecar.jpg | /img/photo.6.sidecar.jpg.xmp | 6 -- Case 3
- Functions relevant to the experiment Immich reads exif from media and from sidecar: https://github.com/immich-app/immich/blob/0729887c9c6da88e717b277ddecdf58a7c9df671/server/src/services/metadata.service.ts#L414-L453
In particular
const [mediaTags, sidecarTags, videoTags] = await Promise.all([
this.metadataRepository.readTags(asset.originalPath),
asset.sidecarPath ? this.metadataRepository.readTags(asset.sidecarPath) : null,
asset.type === AssetType.Video ? this.getVideoTags(asset.originalPath) : null,
]);
- How the images appear in UI. Case 3 "sidecar only" was not rotated according to its orientation.
Monkey patch
- Sources
Based on @ mertalev's comment and @ bo0tzz's comment from #18577, this code in the thumbnails generation ignores Orientation deliberately, in particular to handle HEIC where orientation is "informational".
// only specify orientation to extracted images which don't have EXIF orientation data
// or it can double rotate the image
extracted ? asset.exifInfo : { ...asset.exifInfo, orientation: null },
- Patch
So below I un-ignore orientation by quickly patching javascript in my running Immich container (without needing dev env and rebuilding docker images). To see effect of the patch I restart the container and regenerate thumbnails.
# patch
docker exec --user root immich_server sed -i 's/, orientation: null//' /usr/src/app/server/dist/services/media.service.js
# restart
docker restart immich_server
# then trigger the thumbnail generation job
Optional check of the modified file before and after patchings:
docker exec --user root immich_server grep 'asset.exifInfo ' /usr/src/app/server/dist/services/media.service.js
# before
# ... extracted ? asset.exifInfo : { ...asset.exifInfo, orientation: null } ...
# after
# ... extracted ? asset.exifInfo : { ...asset.exifInfo } ...
- Result Now case 3 "sidecar only" is properly rotated according to its orientation.
@skatsubo Thank you for the patch. Is there a way hwo to re-generate the thumbnails of not-rotated images, after the patch was applied?
a way hwo to re-generate the thumbnails of not-rotated images
@peter-sc You can select images and trigger "Refresh thumbnails" through the three-dots menu
Alternatively, Jobs > Generate Thumbnails > All (maybe overkill if your collection is large).
Or did you mean something different?
@skatsubo I ment rather if there is a way how to do it programatically. I have hunreds of images, which need to be re-generated. Is there a way how to select them on the DB level and run the "Refresh thumbnail" job via DB ?
@peter-sc You can use
- API (recommended and supported way)
- SQL (not supported)
Anyway we have to do 2 things:
- Identify assets with unexpected rotation.
- Trigger thumbnail refresh for them.
For (2), in SQL, we can remove thumbnail records from asset_file or nullify asset.thumbhash. Because these records/fields affect thumbs generation https://github.com/immich-app/immich/blob/e95096d14f826c5615f0f0583c2510085984f711/server/src/services/media.service.ts#L72
Written from my phone and and not tested:
delete from asset_file
where "assetId" in (
select asset.id from asset
join asset_exif
where orientation != "1"
and "sidecarPath" is not null -- assuming that only assets w sidecars are affected
)
Then trigger the Regenerate Thumbnails Missing job.
The (1) part is complicated. See the select part of the query I wrote above (adding here a few more columns)
select asset.id, asset."originalFileName", asset."fileCreatedAt"
from asset
join asset_exif
where orientation != "1"
and "sidecarPath" is not null
But, most likely, this query will not cover "all and only" assets with incorrectly rotated thumbnails. I suggest to run the select first and see whether its results match your expectations.
Linking another reported case for visibility
The 2.4 release fixes the issue with rotation for me. Thank you 😀