Skip to content

Commit 2e6bff1

Browse files
authored
Merge pull request #878 from grasdk/feature/metadata-extra-refactor
removed ts-node-iptc dependency.
2 parents 89caba6 + 28bf225 commit 2e6bff1

File tree

7 files changed

+194
-67
lines changed

7 files changed

+194
-67
lines changed

package-lock.json

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@
5353
"nodemailer": "6.9.4",
5454
"reflect-metadata": "0.1.13",
5555
"sharp": "0.31.3",
56-
"ts-node-iptc": "1.0.11",
5756
"typeconfig": "2.2.11",
5857
"typeorm": "0.3.12",
5958
"xml2js": "0.6.2"

src/backend/model/fileaccess/MetadataLoader.ts

Lines changed: 27 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { FfprobeData } from 'fluent-ffmpeg';
1212
import { FileHandle } from 'fs/promises';
1313
import * as util from 'node:util';
1414
import * as path from 'path';
15-
import { IptcParser } from 'ts-node-iptc';
1615
import { Utils } from '../../../common/Utils';
1716
import { FFmpegFactory } from '../FFmpegFactory';
1817
import { ExtensionDecorator } from '../extension/ExtensionDecorator';
@@ -181,7 +180,7 @@ export class MetadataLoader {
181180
icc: false,
182181
jfif: false, //not needed and not supported for png
183182
ihdr: true,
184-
iptc: false, //exifr reads UTF8-encoded data wrongly, using IptcParser instead
183+
iptc: true,
185184
exif: true,
186185
gps: true,
187186
reviveValues: false, //don't convert timestamps
@@ -221,46 +220,6 @@ export class MetadataLoader {
221220
await fileHandle.close();
222221
}
223222
try {
224-
225-
226-
try { //Parse iptc data using the IptcParser, which works correctly for both UTF-8 and ASCII
227-
const iptcData = IptcParser.parse(data);
228-
if (iptcData.country_or_primary_location_name) {
229-
metadata.positionData = metadata.positionData || {};
230-
metadata.positionData.country =
231-
iptcData.country_or_primary_location_name
232-
.replace(/\0/g, '')
233-
.trim();
234-
}
235-
if (iptcData.province_or_state) {
236-
metadata.positionData = metadata.positionData || {};
237-
metadata.positionData.state = iptcData.province_or_state
238-
.replace(/\0/g, '')
239-
.trim();
240-
}
241-
if (iptcData.city) {
242-
metadata.positionData = metadata.positionData || {};
243-
metadata.positionData.city = iptcData.city
244-
.replace(/\0/g, '')
245-
.trim();
246-
}
247-
if (iptcData.object_name) {
248-
metadata.title = iptcData.object_name.replace(/\0/g, '').trim();
249-
}
250-
if (iptcData.caption) {
251-
metadata.caption = iptcData.caption.replace(/\0/g, '').trim();
252-
}
253-
if (Array.isArray(iptcData.keywords)) {
254-
metadata.keywords = iptcData.keywords;
255-
}
256-
257-
if (iptcData.date_time) {
258-
metadata.creationDate = iptcData.date_time.getTime();
259-
}
260-
} catch (err) {
261-
// Logger.debug(LOG_TAG, 'Error parsing iptc data', fullPath, err);
262-
}
263-
264223
try {
265224
const exif = await exifr.parse(data, exifrOptions);
266225
MetadataLoader.mapMetadata(metadata, exif);
@@ -370,20 +329,35 @@ export class MetadataLoader {
370329
}
371330
}
372331
}
332+
if (exif.iptc &&
333+
exif.iptc.Keywords &&
334+
exif.iptc.Keywords.length > 0) {
335+
const subj = Array.isArray(exif.iptc.Keywords) ? exif.iptc.Keywords : [exif.iptc.Keywords];
336+
if (metadata.keywords === undefined) {
337+
metadata.keywords = [];
338+
}
339+
for (let kw of subj) {
340+
kw = Utils.asciiToUTF8(kw);
341+
if (metadata.keywords.indexOf(kw) === -1) {
342+
metadata.keywords.push(kw);
343+
}
344+
}
345+
}
373346
}
374347

375348
private static mapTitle(metadata: PhotoMetadata, exif: any) {
376-
metadata.title = exif.dc?.title?.value || metadata.title || exif.photoshop?.Headline || exif.acdsee?.caption; //acdsee caption holds the title when data is saved by digikam. Used as last resort if iptc and dc do not contain the data
349+
metadata.title = exif.dc?.title?.value || Utils.asciiToUTF8(exif.iptc?.ObjectName) || metadata.title || exif.photoshop?.Headline || exif.acdsee?.caption; //acdsee caption holds the title when data is saved by digikam. Used as last resort if iptc and dc do not contain the data
377350
}
378351

379352
private static mapCaption(metadata: PhotoMetadata, exif: any) {
380-
metadata.caption = exif.dc?.description?.value || metadata.caption || exif.ifd0?.ImageDescription || exif.exif?.UserComment?.value || exif.Iptc4xmpCore?.ExtDescrAccessibility?.value ||exif.acdsee?.notes;
353+
metadata.caption = exif.dc?.description?.value || Utils.asciiToUTF8(exif.iptc?.Caption) || metadata.caption || exif.ifd0?.ImageDescription || exif.exif?.UserComment?.value || exif.Iptc4xmpCore?.ExtDescrAccessibility?.value ||exif.acdsee?.notes;
381354
}
382355

383356
private static mapTimestampAndOffset(metadata: PhotoMetadata, exif: any) {
384357
metadata.creationDate = Utils.timestampToMS(exif?.photoshop?.DateCreated, null) ||
385358
Utils.timestampToMS(exif?.xmp?.CreateDate, null) ||
386359
Utils.timestampToMS(exif?.xmp?.ModifyDate, null) ||
360+
Utils.timestampToMS(Utils.toIsoTimestampString(exif?.iptc?.DateCreated, exif?.iptc?.TimeCreated), null) ||
387361
metadata.creationDate;
388362

389363
metadata.creationDateOffset = Utils.timestampToOffsetString(exif?.photoshop?.DateCreated) ||
@@ -490,24 +464,15 @@ export class MetadataLoader {
490464

491465
private static mapToponyms(metadata: PhotoMetadata, exif: any) {
492466
//Function to convert html code for special characters into their corresponding character (used in exif.photoshop-section)
493-
const unescape = (tag: string) => {
494-
return tag.replace(/&#([0-9]{1,3});/gi, function (match, numStr) {
495-
return String.fromCharCode(parseInt(numStr, 10));
496-
});
497-
}
498-
//photoshop section sometimes has City, Country and State
499-
if (exif.photoshop) {
500-
if (!metadata.positionData?.country && exif.photoshop.Country) {
501-
metadata.positionData = metadata.positionData || {};
502-
metadata.positionData.country = unescape(exif.photoshop.Country);
503-
}
504-
if (!metadata.positionData?.state && exif.photoshop.State) {
505-
metadata.positionData = metadata.positionData || {};
506-
metadata.positionData.state = unescape(exif.photoshop.State);
507-
}
508-
if (!metadata.positionData?.city && exif.photoshop.City) {
509-
metadata.positionData = metadata.positionData || {};
510-
metadata.positionData.city = unescape(exif.photoshop.City);
467+
468+
metadata.positionData = metadata.positionData || {};
469+
metadata.positionData.country = Utils.asciiToUTF8(exif.iptc?.Country) || Utils.decodeHTMLChars(exif.photoshop?.Country);
470+
metadata.positionData.state = Utils.asciiToUTF8(exif.iptc?.State) || Utils.decodeHTMLChars(exif.photoshop?.State);
471+
metadata.positionData.city = Utils.asciiToUTF8(exif.iptc?.City) || Utils.decodeHTMLChars(exif.photoshop?.City);
472+
if (metadata.positionData) {
473+
Utils.removeNullOrEmptyObj(metadata.positionData);
474+
if (Object.keys(metadata.positionData).length === 0) {
475+
delete metadata.positionData;
511476
}
512477
}
513478
}

src/common/HTMLCharCodes.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
interface HTMLCharDictionary {
2+
[key: string]: string;
3+
}
4+
5+
export const HTMLChar: HTMLCharDictionary = {
6+
""": "\"",
7+
"&": "&",
8+
"&lt;": "<",
9+
"&gt;": ">",
10+
"&nbsp;": " ",
11+
"&iexcl;": "¡",
12+
"&cent;": "¢",
13+
"&pound;": "£",
14+
"&curren;": "¤",
15+
"&yen;": "¥",
16+
"&brvbar;": "¦",
17+
"&sect;": "§",
18+
"&uml;": "¨",
19+
"&copy;": "©",
20+
"&reg;": "®",
21+
"&trade;": "™",
22+
"&ordf;": "ª",
23+
"&laquo;": "«",
24+
"&not;": "¬",
25+
"&shy;": "­",
26+
"&macr;": "¯",
27+
"&deg;": "°",
28+
"&plusmn;": "±",
29+
"&sup2;": "²",
30+
"&sup3;": "³",
31+
"&acute;": "´",
32+
"&micro;": "µ",
33+
"&para;": "¶",
34+
"&middot;": "·",
35+
"&cedil;": "¸",
36+
"&sup1;": "¹",
37+
"&ordm;": "º",
38+
"&raquo;": "»",
39+
"&frac14;": "¼",
40+
"&frac12;": "½",
41+
"&frac34;": "¾",
42+
"&iquest;": "¿",
43+
"&times;": "×",
44+
"&divide;": "÷",
45+
"&ETH;": "Ð",
46+
"&eth;": "ð",
47+
"&THORN;": "Þ",
48+
"&thorn;": "þ",
49+
"&AElig;": "Æ",
50+
"&aelig;": "æ",
51+
"&OElig;": "Œ",
52+
"&oelig;": "œ",
53+
"&Aring;": "Å",
54+
"&Oslash;": "Ø",
55+
"&Ccedil;": "Ç",
56+
"&ccedil;": "ç",
57+
"&szlig;": "ß",
58+
"&Ntilde;": "Ñ",
59+
"&ntilde;": "ñ",
60+
"&Aacute;": "Á",
61+
"&Agrave;": "À",
62+
"&Acirc;": "Â",
63+
"&Auml;": "Ä",
64+
"&Atilde;": "Ã",
65+
"&aacute;": "á",
66+
"&agrave;": "à",
67+
"&acirc;": "â",
68+
"&auml;": "ä",
69+
"&atilde;": "ã",
70+
"&aring;": "å",
71+
"&Eacute;": "É",
72+
"&Egrave;": "È",
73+
"&Ecirc;": "Ê",
74+
"&Euml;": "Ë",
75+
"&Etilde;": "Ẽ",
76+
"&eacute;": "é",
77+
"&egrave;": "è",
78+
"&ecirc;": "ê",
79+
"&euml;": "ë",
80+
"&Iacute;": "Í",
81+
"&Igrave;": "Ì",
82+
"&Icirc;": "Î",
83+
"&Iuml;": "Ï",
84+
"&Itilde;": "Ĩ",
85+
"&iacute;": "í",
86+
"&igrave;": "ì",
87+
"&icirc;": "î",
88+
"&iuml;": "ï",
89+
"&itilde;": "ĩ",
90+
"&Oacute;": "Ó",
91+
"&Ograve;": "Ò",
92+
"&Ocirc;": "Ô",
93+
"&Ouml;": "Ö",
94+
"&Otilde;": "Õ",
95+
"&oacute;": "ó",
96+
"&ograve;": "ò",
97+
"&ocirc;": "ô",
98+
"&ouml;": "ö",
99+
"&otilde;": "õ",
100+
"&Uacute;": "Ú",
101+
"&Ugrave;": "Ù",
102+
"&Ucirc;": "Û",
103+
"&Uuml;": "Ü",
104+
"&Utilde;": "Ũ",
105+
"&Uring;": "Ů",
106+
"&uacute;": "ú",
107+
"&ugrave;": "ù",
108+
"&ucirc;": "û",
109+
"&uuml;": "ü",
110+
"&utilde;": "ũ",
111+
"&uring;": "ů",
112+
"&Yacute;": "Ý",
113+
"&Ycirc;": "Ŷ",
114+
"&Yuml;": "Ÿ",
115+
"&yacute;": "ý",
116+
"&ycirc;": "ŷ",
117+
"&yuml;": "ÿ"
118+
};

src/common/Utils.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { HTMLChar } from './HTMLCharCodes';
2+
13
export class Utils {
24
static GUID(): string {
35
const s4 = (): string =>
@@ -97,6 +99,25 @@ export class Utils {
9799
return d.getUTCFullYear() + '-' + d.getUTCMonth() + '-' + d.getUTCDate();
98100
}
99101

102+
static toIsoTimestampString(YYYYMMDD: string, hhmmss: string): string {
103+
if (YYYYMMDD && hhmmss) {
104+
// Regular expression to match YYYYMMDD format
105+
const dateRegex = /^(\d{4})(\d{2})(\d{2})$/;
106+
// Regular expression to match hhmmss+/-ohom format
107+
const timeRegex = /^(\d{2})(\d{2})(\d{2})([+-]\d{2})?(\d{2})?$/;
108+
const [, year, month, day] = YYYYMMDD.match(dateRegex);
109+
const [, hour, minute, second, offsetHour, offsetMinute] = hhmmss.match(timeRegex);
110+
const isoTimestamp = `${year}-${month}-${day}T${hour}:${minute}:${second}`;
111+
if (offsetHour && offsetMinute) {
112+
return isoTimestamp + `${offsetHour}:${offsetMinute}`;
113+
} else {
114+
return isoTimestamp;
115+
}
116+
} else {
117+
return undefined;
118+
}
119+
}
120+
100121

101122
static makeUTCMidnight(d: number | Date) {
102123
if (!(d instanceof Date)) {
@@ -125,7 +146,7 @@ export class Utils {
125146
}
126147

127148
//function to convert timestamp into milliseconds taking offset into account
128-
static timestampToMS(timestamp: string, offset: string) {
149+
static timestampToMS(timestamp: string, offset: string): number {
129150
if (!timestamp) {
130151
return undefined;
131152
}
@@ -371,6 +392,31 @@ export class Utils {
371392
return curr;
372393
}
373394

395+
public static asciiToUTF8(text: string): string {
396+
if (text) {
397+
return Buffer.from(text, 'ascii').toString('utf-8');
398+
} else {
399+
return text;
400+
}
401+
}
402+
403+
404+
405+
public static decodeHTMLChars(text: string): string {
406+
if (text) {
407+
const newtext = text.replace(/&#([0-9]{1,3});/gi, function (match, numStr) {
408+
return String.fromCharCode(parseInt(numStr, 10));
409+
});
410+
return newtext.replace(/&[^;]+;/g, function (match) {
411+
const char = HTMLChar[match];
412+
return char ? char : match;
413+
});
414+
} else {
415+
return text;
416+
}
417+
}
418+
419+
374420
public static isUInt32(value: number, max = 4294967295): boolean {
375421
value = parseInt('' + value, 10);
376422
return !isNaN(value) && value >= 0 && value <= max;

test/backend/assets/sidecar/testimagedesc1.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@
4141
"latitude": 37.871093,
4242
"longitude": -122.25678
4343
},
44-
"city": "test city őúéáűóöí-.,)(=",
44+
"city": "test city őúéáűóöí-.,)(=/%!+\"'",
4545
"country": "test country őúéáűóöí-.,)(=/%!+\"'",
46-
"state": "test state őúéáűóöí-.,)("
46+
"state": "test state őúéáűóöí-.,)(=/%!+\"'"
4747
},
4848
"rating": 3,
4949
"size": {

test/tmp/sqlite.db-journal

-8.52 KB
Binary file not shown.

0 commit comments

Comments
 (0)