1
1
import { Injectable } from '@nestjs/common' ;
2
2
import { BinaryField , DefaultReadTaskOptions , ExifTool , Tags } from 'exiftool-vendored' ;
3
3
import geotz from 'geo-tz' ;
4
+ import fs from 'node:fs/promises' ;
5
+ import path from 'node:path' ;
4
6
import { LoggingRepository } from 'src/repositories/logging.repository' ;
5
7
6
8
interface ExifDuration {
@@ -71,6 +73,8 @@ export interface ImmichTags extends Omit<Tags, TagsWithWrongTypes> {
71
73
72
74
AndroidMake ?: string ;
73
75
AndroidModel ?: string ;
76
+
77
+ MIMEEncoding ?: string ;
74
78
}
75
79
76
80
@Injectable ( )
@@ -84,6 +88,7 @@ export class MetadataRepository {
84
88
numericTags : [ ...DefaultReadTaskOptions . numericTags , 'FocalLength' , 'FileSize' ] ,
85
89
/* eslint unicorn/no-array-callback-reference: off, unicorn/no-array-method-this-argument: off */
86
90
geoTz : ( lat , lon ) => geotz . find ( lat , lon ) [ 0 ] ,
91
+ geolocation : true ,
87
92
// Enable exiftool LFS to parse metadata for files larger than 2GB.
88
93
readArgs : [ '-api' , 'largefilesupport=1' ] ,
89
94
writeArgs : [ '-api' , 'largefilesupport=1' , '-overwrite_original' ] ,
@@ -101,11 +106,39 @@ export class MetadataRepository {
101
106
await this . exiftool . end ( ) ;
102
107
}
103
108
104
- readTags ( path : string ) : Promise < ImmichTags > {
105
- return this . exiftool . read ( path ) . catch ( ( error ) => {
106
- this . logger . warn ( `Error reading exif data (${ path } ): ${ error } ` , error ?. stack ) ;
109
+ async readTags ( filePath : string ) : Promise < ImmichTags > {
110
+ try {
111
+ const tags = ( await this . exiftool . read ( filePath ) ) as ImmichTags ;
112
+
113
+ // exiftool 13.25+ may skip XMP payload parsing for UTF-16LE XMP
114
+ // (https://github.com/exiftool/exiftool/issues/348)
115
+ const needsUtf8Normalization = tags . FileType === 'XMP' && tags . MIMEEncoding === 'utf-16le' ;
116
+
117
+ if ( ! needsUtf8Normalization ) {
118
+ return tags ;
119
+ }
120
+
121
+ try {
122
+ // Create a temporary UTF-8 copy
123
+ const xmpContent = await fs . readFile ( filePath , 'utf-16le' ) ;
124
+ const tmpDir = await fs . mkdtemp ( 'immich-metadata-' ) ;
125
+ const tmpFile = path . join ( tmpDir , path . basename ( filePath ) ) ;
126
+ await fs . writeFile ( tmpFile , xmpContent , 'utf8' ) ;
127
+
128
+ // Try parsing the converted XMP file using exiftool
129
+ try {
130
+ return ( await this . exiftool . read ( tmpFile ) ) as ImmichTags ;
131
+ } finally {
132
+ await fs . rm ( tmpDir , { recursive : true , force : true } ) . catch ( ( ) => { } ) ;
133
+ }
134
+ } catch ( fallbackError ) {
135
+ this . logger . warn ( `UTF-8 normalization failed (${ filePath } ): ${ fallbackError } ` ) ;
136
+ return tags ;
137
+ }
138
+ } catch ( error ) {
139
+ this . logger . warn ( `Error reading exif data (${ filePath } ): ${ error } ` , ( error as Error ) . stack ) ;
107
140
return { } ;
108
- } ) as Promise < ImmichTags > ;
141
+ }
109
142
}
110
143
111
144
extractBinaryTag ( path : string , tagName : string ) : Promise < Buffer > {
0 commit comments