exiftool-vendored 36.0.0 → 37.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +64 -5
- package/CLAUDE.md +2 -1
- package/README.md +128 -35
- package/RELEASE.md +1 -1
- package/data/TagMetadata.json +10 -3
- package/dist/DateTime.d.ts +2 -2
- package/dist/DateTime.js +1 -1
- package/dist/DateTime.js.map +1 -1
- package/dist/ExifTool.d.ts +45 -8
- package/dist/ExifTool.dispose.spec.js +3 -3
- package/dist/ExifTool.dispose.spec.js.map +1 -1
- package/dist/ExifTool.js +54 -10
- package/dist/ExifTool.js.map +1 -1
- package/dist/ExifToolOptions.d.ts +15 -2
- package/dist/ExifToolOptions.js.map +1 -1
- package/dist/ExifToolVendoredTags.d.ts +11 -1
- package/dist/ExifToolVendoredTags.js +1 -1
- package/dist/ExifToolVendoredTags.js.map +1 -1
- package/dist/ExiftoolPath.js +1 -1
- package/dist/ExiftoolPath.js.map +1 -1
- package/dist/InvalidUtf8Bytes.d.ts +20 -0
- package/dist/InvalidUtf8Bytes.js +106 -0
- package/dist/InvalidUtf8Bytes.js.map +1 -0
- package/dist/InvalidUtf8Bytes.spec.d.ts +1 -0
- package/dist/InvalidUtf8Bytes.spec.js +75 -0
- package/dist/InvalidUtf8Bytes.spec.js.map +1 -0
- package/dist/InvalidUtf8Xml.spec.d.ts +1 -0
- package/dist/InvalidUtf8Xml.spec.js +156 -0
- package/dist/InvalidUtf8Xml.spec.js.map +1 -0
- package/dist/JSON.d.ts +1 -0
- package/dist/JSON.js +6 -1
- package/dist/JSON.js.map +1 -1
- package/dist/RawTags.d.ts +9 -2
- package/dist/ReadRawTask.d.ts +1 -0
- package/dist/ReadRawTask.js +16 -3
- package/dist/ReadRawTask.js.map +1 -1
- package/dist/ReadRawTask.spec.js +191 -0
- package/dist/ReadRawTask.spec.js.map +1 -1
- package/dist/ReadTask.js +24 -2
- package/dist/ReadTask.js.map +1 -1
- package/dist/ReadTask.spec.js +52 -0
- package/dist/ReadTask.spec.js.map +1 -1
- package/dist/TagEdit.d.ts +62 -0
- package/dist/TagEdit.js +224 -0
- package/dist/TagEdit.js.map +1 -0
- package/dist/TagEdit.spec.d.ts +1 -0
- package/dist/TagEdit.spec.js +863 -0
- package/dist/TagEdit.spec.js.map +1 -0
- package/dist/Tags.d.ts +56 -38
- package/dist/Tags.js +3 -3
- package/dist/Tags.js.map +1 -1
- package/dist/Tags.spec.js +8 -0
- package/dist/Tags.spec.js.map +1 -1
- package/dist/Timezones.js.map +1 -1
- package/dist/Utf8JsonFilter.d.ts +7 -0
- package/dist/Utf8JsonFilter.js +85 -0
- package/dist/Utf8JsonFilter.js.map +1 -0
- package/dist/WriteTask.d.ts +6 -0
- package/dist/WriteTask.js +58 -18
- package/dist/WriteTask.js.map +1 -1
- package/dist/WriteTask.spec.js +17 -0
- package/dist/WriteTask.spec.js.map +1 -1
- package/dist/update/mktags.js +19 -1
- package/dist/update/mktags.js.map +1 -1
- package/package.json +15 -47
|
@@ -0,0 +1,863 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const node_fs_1 = require("node:fs");
|
|
4
|
+
const _chai_spec_1 = require("./_chai.spec");
|
|
5
|
+
const ExifTool_1 = require("./ExifTool");
|
|
6
|
+
const TagEdit_1 = require("./TagEdit");
|
|
7
|
+
const WriteTask_1 = require("./WriteTask");
|
|
8
|
+
function regionList(tags) {
|
|
9
|
+
return tags.RegionInfo.RegionList;
|
|
10
|
+
}
|
|
11
|
+
async function readQualifiedTag(exiftool, file, tag) {
|
|
12
|
+
const raw = (await exiftool.readRaw(file, {
|
|
13
|
+
readArgs: ["-G1", "-struct", `-${tag}`],
|
|
14
|
+
}));
|
|
15
|
+
return raw[tag];
|
|
16
|
+
}
|
|
17
|
+
describe("ExifTool.editTags()", () => {
|
|
18
|
+
const exiftool = new ExifTool_1.ExifTool();
|
|
19
|
+
after(() => (0, _chai_spec_1.end)(exiftool));
|
|
20
|
+
async function seedCollections(file) {
|
|
21
|
+
await exiftool.write(file, {
|
|
22
|
+
Collections: [
|
|
23
|
+
{
|
|
24
|
+
CollectionName: "Vacation",
|
|
25
|
+
CollectionURI: "urn:vacation",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
CollectionName: "Portfolio",
|
|
29
|
+
CollectionURI: "urn:portfolio",
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
Subject: ["before"],
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
async function expectNoModification(file, edits, message) {
|
|
36
|
+
const before = await (0, _chai_spec_1.sha1)(file);
|
|
37
|
+
await (0, _chai_spec_1.expect)(exiftool.editTags(file, edits)).to.be.rejectedWith(message);
|
|
38
|
+
(0, _chai_spec_1.expect)(await (0, _chai_spec_1.sha1)(file)).to.eql(before);
|
|
39
|
+
}
|
|
40
|
+
it("preserves operation order in generated ExifTool arguments", () => {
|
|
41
|
+
const edits = [
|
|
42
|
+
{
|
|
43
|
+
tag: "XMP-dc:Subject",
|
|
44
|
+
operation: "remove",
|
|
45
|
+
value: "beach",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
tag: "XMP-dc:Subject",
|
|
49
|
+
operation: "add",
|
|
50
|
+
value: "forest",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
tag: "XMP-dc:Subject",
|
|
54
|
+
operation: "add",
|
|
55
|
+
value: "forest",
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
const task = WriteTask_1.WriteTask.forTagEdits("image.jpg", edits, ExifTool_1.DefaultWriteTaskOptions);
|
|
59
|
+
(0, _chai_spec_1.expect)(task.args.filter((arg) => arg.startsWith("-XMP-dc:Subject"))).to.eql([
|
|
60
|
+
"-XMP-dc:Subject-=beach",
|
|
61
|
+
"-XMP-dc:Subject+=forest",
|
|
62
|
+
"-XMP-dc:Subject+=forest",
|
|
63
|
+
]);
|
|
64
|
+
});
|
|
65
|
+
it("accepts every audited add-capable string-list tag", () => {
|
|
66
|
+
const edits = TagEdit_1.TagEditAddTagNames.values.map((tag) => ({
|
|
67
|
+
tag,
|
|
68
|
+
operation: "add",
|
|
69
|
+
value: "value",
|
|
70
|
+
}));
|
|
71
|
+
const task = WriteTask_1.WriteTask.forTagEdits("image.jpg", edits, ExifTool_1.DefaultWriteTaskOptions);
|
|
72
|
+
for (const tag of TagEdit_1.TagEditAddTagNames) {
|
|
73
|
+
(0, _chai_spec_1.expect)(task.args).to.include(`-${tag}+=value`);
|
|
74
|
+
}
|
|
75
|
+
(0, _chai_spec_1.expect)(TagEdit_1.TagEditTagNames.includes("XMP-mwg-coll:Collections")).to.eql(true);
|
|
76
|
+
});
|
|
77
|
+
it("accepts every audited remove-capable primitive tag", () => {
|
|
78
|
+
const edits = TagEdit_1.TagEditValueTagNames.values.map((tag) => ({
|
|
79
|
+
tag,
|
|
80
|
+
operation: "remove",
|
|
81
|
+
value: "value",
|
|
82
|
+
}));
|
|
83
|
+
const task = WriteTask_1.WriteTask.forTagEdits("image.jpg", edits, ExifTool_1.DefaultWriteTaskOptions);
|
|
84
|
+
for (const tag of TagEdit_1.TagEditValueTagNames) {
|
|
85
|
+
(0, _chai_spec_1.expect)(task.args).to.include(`-${tag}-=value`);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
it("removes every exact match and preserves duplicate additions", async () => {
|
|
89
|
+
const file = await (0, _chai_spec_1.testFile)("keywords.xmp");
|
|
90
|
+
await exiftool.write(file, {
|
|
91
|
+
Subject: ["remove", "keep", "remove"],
|
|
92
|
+
});
|
|
93
|
+
await exiftool.editTags(file, [
|
|
94
|
+
{
|
|
95
|
+
tag: "XMP-dc:Subject",
|
|
96
|
+
operation: "remove",
|
|
97
|
+
value: "remove",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
tag: "XMP-dc:Subject",
|
|
101
|
+
operation: "add",
|
|
102
|
+
value: "forest",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
tag: "XMP-dc:Subject",
|
|
106
|
+
operation: "add",
|
|
107
|
+
value: "forest",
|
|
108
|
+
},
|
|
109
|
+
]);
|
|
110
|
+
const raw = (await exiftool.readRaw(file, {
|
|
111
|
+
readArgs: ["-G1", "-XMP-dc:Subject"],
|
|
112
|
+
}));
|
|
113
|
+
const subject = raw["XMP-dc:Subject"];
|
|
114
|
+
(0, _chai_spec_1.expect)(subject).to.be.an("array");
|
|
115
|
+
(0, _chai_spec_1.expect)([...subject].sort()).to.eql(["forest", "forest", "keep"].sort());
|
|
116
|
+
});
|
|
117
|
+
it("targets only the qualified metadata group", async () => {
|
|
118
|
+
const file = await (0, _chai_spec_1.testImg)();
|
|
119
|
+
const before = (await exiftool.readRaw(file, {
|
|
120
|
+
readArgs: ["-G1", "-XMP-dc:Subject", "-IPTC:Keywords"],
|
|
121
|
+
}));
|
|
122
|
+
await exiftool.editTags(file, [
|
|
123
|
+
{
|
|
124
|
+
tag: "XMP-dc:Subject",
|
|
125
|
+
operation: "remove",
|
|
126
|
+
value: "beach",
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
tag: "XMP-dc:Subject",
|
|
130
|
+
operation: "add",
|
|
131
|
+
value: "forest",
|
|
132
|
+
},
|
|
133
|
+
]);
|
|
134
|
+
const after = (await exiftool.readRaw(file, {
|
|
135
|
+
readArgs: ["-G1", "-XMP-dc:Subject", "-IPTC:Keywords"],
|
|
136
|
+
}));
|
|
137
|
+
(0, _chai_spec_1.expect)(after["XMP-dc:Subject"]).to.include("forest");
|
|
138
|
+
(0, _chai_spec_1.expect)(after["XMP-dc:Subject"]).to.not.include("beach");
|
|
139
|
+
(0, _chai_spec_1.expect)(after["IPTC:Keywords"]).to.eql(before["IPTC:Keywords"]);
|
|
140
|
+
});
|
|
141
|
+
it("edits exact hierarchical keyword values", async () => {
|
|
142
|
+
const file = await (0, _chai_spec_1.testFile)("hierarchical-keywords.xmp");
|
|
143
|
+
await exiftool.write(file, {
|
|
144
|
+
"XMP-lr:HierarchicalSubject": ["Places|Beach", "Places|Forest"],
|
|
145
|
+
});
|
|
146
|
+
await exiftool.editTags(file, [
|
|
147
|
+
{
|
|
148
|
+
tag: "XMP-lr:HierarchicalSubject",
|
|
149
|
+
operation: "remove",
|
|
150
|
+
value: "Places|Beach",
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
tag: "XMP-lr:HierarchicalSubject",
|
|
154
|
+
operation: "add",
|
|
155
|
+
value: "Places|Mountain",
|
|
156
|
+
},
|
|
157
|
+
]);
|
|
158
|
+
const raw = (await exiftool.readRaw(file, {
|
|
159
|
+
readArgs: ["-G1", "-XMP-lr:HierarchicalSubject"],
|
|
160
|
+
}));
|
|
161
|
+
(0, _chai_spec_1.expect)([...raw["XMP-lr:HierarchicalSubject"]].sort()).to.eql(["Places|Forest", "Places|Mountain"].sort());
|
|
162
|
+
});
|
|
163
|
+
it("removes a Collection matching a strict non-empty predicate", async () => {
|
|
164
|
+
const file = await (0, _chai_spec_1.testFile)("collections.xmp");
|
|
165
|
+
await seedCollections(file);
|
|
166
|
+
await exiftool.editTags(file, [
|
|
167
|
+
{
|
|
168
|
+
tag: "XMP-mwg-coll:Collections",
|
|
169
|
+
operation: "remove",
|
|
170
|
+
predicate: { CollectionName: "Vacation" },
|
|
171
|
+
},
|
|
172
|
+
]);
|
|
173
|
+
(0, _chai_spec_1.expect)((await exiftool.read(file)).Collections).to.eql([
|
|
174
|
+
{
|
|
175
|
+
CollectionName: "Portfolio",
|
|
176
|
+
CollectionURI: "urn:portfolio",
|
|
177
|
+
},
|
|
178
|
+
]);
|
|
179
|
+
});
|
|
180
|
+
it("encodes only the validated snapshot of a Collection predicate", async () => {
|
|
181
|
+
const file = await (0, _chai_spec_1.testFile)("collections.xmp");
|
|
182
|
+
await exiftool.write(file, {
|
|
183
|
+
Collections: [
|
|
184
|
+
{
|
|
185
|
+
CollectionName: "Vacation",
|
|
186
|
+
CollectionURI: "urn:vacation",
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
CollectionName: "Vacation",
|
|
190
|
+
CollectionURI: "urn:other-vacation",
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
});
|
|
194
|
+
let predicateReads = 0;
|
|
195
|
+
const edit = {
|
|
196
|
+
tag: "XMP-mwg-coll:Collections",
|
|
197
|
+
operation: "remove",
|
|
198
|
+
get predicate() {
|
|
199
|
+
predicateReads++;
|
|
200
|
+
return predicateReads === 1
|
|
201
|
+
? {
|
|
202
|
+
CollectionName: "Vacation",
|
|
203
|
+
CollectionURI: "urn:vacation",
|
|
204
|
+
}
|
|
205
|
+
: { CollectionName: "Vacation", Typo: "x" };
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
await exiftool.editTags(file, [edit]);
|
|
209
|
+
(0, _chai_spec_1.expect)(predicateReads).to.eql(1);
|
|
210
|
+
(0, _chai_spec_1.expect)((await exiftool.read(file)).Collections).to.eql([
|
|
211
|
+
{
|
|
212
|
+
CollectionName: "Vacation",
|
|
213
|
+
CollectionURI: "urn:other-vacation",
|
|
214
|
+
},
|
|
215
|
+
]);
|
|
216
|
+
});
|
|
217
|
+
it("includes non-enumerable own fields in a Collection predicate", async () => {
|
|
218
|
+
const file = await (0, _chai_spec_1.testFile)("collections.xmp");
|
|
219
|
+
await exiftool.write(file, {
|
|
220
|
+
Collections: [
|
|
221
|
+
{
|
|
222
|
+
CollectionName: "Vacation",
|
|
223
|
+
CollectionURI: "urn:vacation",
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
CollectionName: "Vacation",
|
|
227
|
+
CollectionURI: "urn:other-vacation",
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
});
|
|
231
|
+
const predicate = { CollectionName: "Vacation" };
|
|
232
|
+
Object.defineProperty(predicate, "CollectionURI", {
|
|
233
|
+
value: "urn:vacation",
|
|
234
|
+
});
|
|
235
|
+
await exiftool.editTags(file, [
|
|
236
|
+
{
|
|
237
|
+
tag: "XMP-mwg-coll:Collections",
|
|
238
|
+
operation: "remove",
|
|
239
|
+
predicate,
|
|
240
|
+
},
|
|
241
|
+
]);
|
|
242
|
+
(0, _chai_spec_1.expect)((await exiftool.read(file)).Collections).to.eql([
|
|
243
|
+
{
|
|
244
|
+
CollectionName: "Vacation",
|
|
245
|
+
CollectionURI: "urn:other-vacation",
|
|
246
|
+
},
|
|
247
|
+
]);
|
|
248
|
+
});
|
|
249
|
+
it("rejects accessor fields in Collection values without modifying metadata", async () => {
|
|
250
|
+
const file = await (0, _chai_spec_1.testFile)("collections.xmp");
|
|
251
|
+
await seedCollections(file);
|
|
252
|
+
const value = { CollectionURI: "urn:new" };
|
|
253
|
+
Object.defineProperty(value, "CollectionName", {
|
|
254
|
+
enumerable: true,
|
|
255
|
+
get() {
|
|
256
|
+
return "Dynamic";
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
await expectNoModification(file, [
|
|
260
|
+
{
|
|
261
|
+
tag: "XMP-mwg-coll:Collections",
|
|
262
|
+
operation: "add",
|
|
263
|
+
value,
|
|
264
|
+
},
|
|
265
|
+
], /data properties/i);
|
|
266
|
+
});
|
|
267
|
+
it("matches leading whitespace in Collection predicates exactly", async () => {
|
|
268
|
+
const file = await (0, _chai_spec_1.testFile)("collections.xmp");
|
|
269
|
+
await exiftool.editTags(file, [
|
|
270
|
+
{
|
|
271
|
+
tag: "XMP-mwg-coll:Collections",
|
|
272
|
+
operation: "add",
|
|
273
|
+
value: {
|
|
274
|
+
CollectionName: " Vacation",
|
|
275
|
+
CollectionURI: "urn:leading",
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
tag: "XMP-mwg-coll:Collections",
|
|
280
|
+
operation: "add",
|
|
281
|
+
value: {
|
|
282
|
+
CollectionName: "Vacation",
|
|
283
|
+
CollectionURI: "urn:plain",
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
]);
|
|
287
|
+
await exiftool.editTags(file, [
|
|
288
|
+
{
|
|
289
|
+
tag: "XMP-mwg-coll:Collections",
|
|
290
|
+
operation: "remove",
|
|
291
|
+
predicate: { CollectionName: " Vacation" },
|
|
292
|
+
},
|
|
293
|
+
]);
|
|
294
|
+
(0, _chai_spec_1.expect)((await exiftool.read(file)).Collections).to.eql([
|
|
295
|
+
{
|
|
296
|
+
CollectionName: "Vacation",
|
|
297
|
+
CollectionURI: "urn:plain",
|
|
298
|
+
},
|
|
299
|
+
]);
|
|
300
|
+
});
|
|
301
|
+
it("round-trips structural delimiters inside Collection fields", async () => {
|
|
302
|
+
const file = await (0, _chai_spec_1.testFile)("collections.xmp");
|
|
303
|
+
await exiftool.editTags(file, [
|
|
304
|
+
{
|
|
305
|
+
tag: "XMP-mwg-coll:Collections",
|
|
306
|
+
operation: "add",
|
|
307
|
+
value: {
|
|
308
|
+
CollectionName: "{Vacation}",
|
|
309
|
+
CollectionURI: "urn:[vacation]",
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
]);
|
|
313
|
+
(0, _chai_spec_1.expect)((await exiftool.read(file)).Collections).to.eql([
|
|
314
|
+
{
|
|
315
|
+
CollectionName: "{Vacation}",
|
|
316
|
+
CollectionURI: "urn:[vacation]",
|
|
317
|
+
},
|
|
318
|
+
]);
|
|
319
|
+
});
|
|
320
|
+
it("distinguishes HTML entities from decoded text in Collection fields", async () => {
|
|
321
|
+
const file = await (0, _chai_spec_1.testFile)("collections.xmp");
|
|
322
|
+
await exiftool.editTags(file, [
|
|
323
|
+
{
|
|
324
|
+
tag: "XMP-mwg-coll:Collections",
|
|
325
|
+
operation: "add",
|
|
326
|
+
value: {
|
|
327
|
+
CollectionName: "A & B",
|
|
328
|
+
CollectionURI: "urn:entity",
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
tag: "XMP-mwg-coll:Collections",
|
|
333
|
+
operation: "add",
|
|
334
|
+
value: {
|
|
335
|
+
CollectionName: "A & B",
|
|
336
|
+
CollectionURI: "urn:ampersand",
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
]);
|
|
340
|
+
(0, _chai_spec_1.expect)((await exiftool.read(file)).Collections).to.eql([
|
|
341
|
+
{
|
|
342
|
+
CollectionName: "A & B",
|
|
343
|
+
CollectionURI: "urn:entity",
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
CollectionName: "A & B",
|
|
347
|
+
CollectionURI: "urn:ampersand",
|
|
348
|
+
},
|
|
349
|
+
]);
|
|
350
|
+
await exiftool.editTags(file, [
|
|
351
|
+
{
|
|
352
|
+
tag: "XMP-mwg-coll:Collections",
|
|
353
|
+
operation: "remove",
|
|
354
|
+
predicate: { CollectionName: "A & B" },
|
|
355
|
+
},
|
|
356
|
+
]);
|
|
357
|
+
(0, _chai_spec_1.expect)((await exiftool.read(file)).Collections).to.eql([
|
|
358
|
+
{
|
|
359
|
+
CollectionName: "A & B",
|
|
360
|
+
CollectionURI: "urn:ampersand",
|
|
361
|
+
},
|
|
362
|
+
]);
|
|
363
|
+
});
|
|
364
|
+
it("adds one validated Collection structure", async () => {
|
|
365
|
+
const file = await (0, _chai_spec_1.testFile)("collections.xmp");
|
|
366
|
+
await exiftool.editTags(file, [
|
|
367
|
+
{
|
|
368
|
+
tag: "XMP-mwg-coll:Collections",
|
|
369
|
+
operation: "add",
|
|
370
|
+
value: {
|
|
371
|
+
CollectionName: "Vacation",
|
|
372
|
+
CollectionURI: "urn:vacation",
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
]);
|
|
376
|
+
(0, _chai_spec_1.expect)((await exiftool.read(file)).Collections).to.eql([
|
|
377
|
+
{
|
|
378
|
+
CollectionName: "Vacation",
|
|
379
|
+
CollectionURI: "urn:vacation",
|
|
380
|
+
},
|
|
381
|
+
]);
|
|
382
|
+
});
|
|
383
|
+
it("removes a flattened face name without deleting its geometry", async () => {
|
|
384
|
+
const file = await (0, _chai_spec_1.testImg)({ srcBasename: "with_faces.jpg" });
|
|
385
|
+
const before = regionList(await exiftool.read(file));
|
|
386
|
+
const bobBefore = before.find((region) => region.Name === "Bob Jones");
|
|
387
|
+
await exiftool.editTags(file, [
|
|
388
|
+
{
|
|
389
|
+
tag: "XMP-mwg-rs:RegionName",
|
|
390
|
+
operation: "remove",
|
|
391
|
+
value: "Bob Jones",
|
|
392
|
+
},
|
|
393
|
+
]);
|
|
394
|
+
const after = regionList(await exiftool.read(file));
|
|
395
|
+
(0, _chai_spec_1.expect)(after).to.have.length(before.length);
|
|
396
|
+
(0, _chai_spec_1.expect)(after.find((region) => region.Name === "Alice Smith")).to.eql(before.find((region) => region.Name === "Alice Smith"));
|
|
397
|
+
const unnamedBob = after.find((region) => region.Type === "Face" && region.Name == null);
|
|
398
|
+
(0, _chai_spec_1.expect)(unnamedBob?.Area).to.eql(bobBefore.Area);
|
|
399
|
+
});
|
|
400
|
+
it("removes an exact XMP album scalar", async () => {
|
|
401
|
+
const file = await (0, _chai_spec_1.testFile)("album.xmp");
|
|
402
|
+
await exiftool.write(file, { "XMP-xmpDM:Album": "Vacation" });
|
|
403
|
+
await exiftool.editTags(file, [
|
|
404
|
+
{ tag: "XMP-xmpDM:Album", operation: "remove", value: "Other" },
|
|
405
|
+
]);
|
|
406
|
+
(0, _chai_spec_1.expect)(await readQualifiedTag(exiftool, file, "XMP-xmpDM:Album")).to.eql("Vacation");
|
|
407
|
+
await exiftool.editTags(file, [
|
|
408
|
+
{ tag: "XMP-xmpDM:Album", operation: "remove", value: "Vacation" },
|
|
409
|
+
]);
|
|
410
|
+
(0, _chai_spec_1.expect)(await readQualifiedTag(exiftool, file, "XMP-xmpDM:Album")).to.eql(undefined);
|
|
411
|
+
});
|
|
412
|
+
it("removes audited flattened region names without deleting sibling fields", async () => {
|
|
413
|
+
const file = await (0, _chai_spec_1.testFile)("person-structures.xmp");
|
|
414
|
+
await exiftool.write(file, {
|
|
415
|
+
"XMP-acdsee-rs:RegionInfoACDSee": {
|
|
416
|
+
AppliedToDimensions: { H: 800, Unit: "pixel", W: 1200 },
|
|
417
|
+
RegionList: [
|
|
418
|
+
{
|
|
419
|
+
ALGArea: { H: 0.2, W: 0.2, X: 0.3, Y: 0.4 },
|
|
420
|
+
Name: "Jane",
|
|
421
|
+
NameAssignType: "manual",
|
|
422
|
+
Type: "Face",
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
DLYArea: { H: 0.4, W: 0.3, X: 0.2, Y: 0.1 },
|
|
426
|
+
Name: "Keep",
|
|
427
|
+
NameAssignType: "manual",
|
|
428
|
+
Type: "Object",
|
|
429
|
+
},
|
|
430
|
+
],
|
|
431
|
+
},
|
|
432
|
+
"XMP-MP:RegionInfoMP": {
|
|
433
|
+
Regions: [
|
|
434
|
+
{
|
|
435
|
+
PersonDisplayName: "Jane",
|
|
436
|
+
PersonSourceID: "id-jane",
|
|
437
|
+
Rectangle: "0.1, 0.2, 0.3, 0.4",
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
PersonDisplayName: "Keep",
|
|
441
|
+
PersonSourceID: "id-keep",
|
|
442
|
+
Rectangle: "0.5, 0.6, 0.2, 0.2",
|
|
443
|
+
},
|
|
444
|
+
],
|
|
445
|
+
},
|
|
446
|
+
});
|
|
447
|
+
const beforeNonmatch = await (0, _chai_spec_1.sha1)(file);
|
|
448
|
+
await exiftool.editTags(file, [
|
|
449
|
+
{
|
|
450
|
+
tag: "XMP-acdsee-rs:ACDSeeRegionName",
|
|
451
|
+
operation: "remove",
|
|
452
|
+
value: "Other",
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
tag: "XMP-MP:RegionPersonDisplayName",
|
|
456
|
+
operation: "remove",
|
|
457
|
+
value: "Other",
|
|
458
|
+
},
|
|
459
|
+
]);
|
|
460
|
+
(0, _chai_spec_1.expect)(await (0, _chai_spec_1.sha1)(file)).to.eql(beforeNonmatch);
|
|
461
|
+
await exiftool.editTags(file, [
|
|
462
|
+
{
|
|
463
|
+
tag: "XMP-acdsee-rs:ACDSeeRegionName",
|
|
464
|
+
operation: "remove",
|
|
465
|
+
value: "Jane",
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
tag: "XMP-MP:RegionPersonDisplayName",
|
|
469
|
+
operation: "remove",
|
|
470
|
+
value: "Jane",
|
|
471
|
+
},
|
|
472
|
+
]);
|
|
473
|
+
(0, _chai_spec_1.expect)(await readQualifiedTag(exiftool, file, "XMP-acdsee-rs:RegionInfoACDSee")).to.eql({
|
|
474
|
+
AppliedToDimensions: { H: 800, Unit: "pixel", W: 1200 },
|
|
475
|
+
RegionList: [
|
|
476
|
+
{
|
|
477
|
+
ALGArea: { H: 0.2, W: 0.2, X: 0.3, Y: 0.4 },
|
|
478
|
+
NameAssignType: "manual",
|
|
479
|
+
Type: "Face",
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
DLYArea: { H: 0.4, W: 0.3, X: 0.2, Y: 0.1 },
|
|
483
|
+
Name: "Keep",
|
|
484
|
+
NameAssignType: "manual",
|
|
485
|
+
Type: "Object",
|
|
486
|
+
},
|
|
487
|
+
],
|
|
488
|
+
});
|
|
489
|
+
(0, _chai_spec_1.expect)(await readQualifiedTag(exiftool, file, "XMP-MP:RegionInfoMP")).to.eql({
|
|
490
|
+
Regions: [
|
|
491
|
+
{
|
|
492
|
+
PersonSourceID: "id-jane",
|
|
493
|
+
Rectangle: "0.1, 0.2, 0.3, 0.4",
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
PersonDisplayName: "Keep",
|
|
497
|
+
PersonSourceID: "id-keep",
|
|
498
|
+
Rectangle: "0.5, 0.6, 0.2, 0.2",
|
|
499
|
+
},
|
|
500
|
+
],
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
it("prunes flattened region structures left empty after removal", async () => {
|
|
504
|
+
const file = await (0, _chai_spec_1.testFile)("empty-person-structures.xmp");
|
|
505
|
+
await exiftool.write(file, {
|
|
506
|
+
"XMP-acdsee-rs:RegionInfoACDSee": {
|
|
507
|
+
RegionList: [{ Name: "Jane" }],
|
|
508
|
+
},
|
|
509
|
+
"XMP-MP:RegionInfoMP": {
|
|
510
|
+
Regions: [{ PersonDisplayName: "Jane" }],
|
|
511
|
+
},
|
|
512
|
+
});
|
|
513
|
+
await exiftool.editTags(file, [
|
|
514
|
+
{
|
|
515
|
+
tag: "XMP-acdsee-rs:ACDSeeRegionName",
|
|
516
|
+
operation: "remove",
|
|
517
|
+
value: "Jane",
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
tag: "XMP-MP:RegionPersonDisplayName",
|
|
521
|
+
operation: "remove",
|
|
522
|
+
value: "Jane",
|
|
523
|
+
},
|
|
524
|
+
]);
|
|
525
|
+
(0, _chai_spec_1.expect)(await readQualifiedTag(exiftool, file, "XMP-acdsee-rs:RegionInfoACDSee")).to.eql(undefined);
|
|
526
|
+
(0, _chai_spec_1.expect)(await readQualifiedTag(exiftool, file, "XMP-MP:RegionInfoMP")).to.eql(undefined);
|
|
527
|
+
});
|
|
528
|
+
it("rejects adding a flattened RegionName without modifying metadata", async () => {
|
|
529
|
+
const file = await (0, _chai_spec_1.testImg)({ srcBasename: "with_faces.jpg" });
|
|
530
|
+
await expectNoModification(file, [
|
|
531
|
+
{
|
|
532
|
+
tag: "XMP-mwg-rs:RegionName",
|
|
533
|
+
operation: "add",
|
|
534
|
+
value: "Ghost",
|
|
535
|
+
},
|
|
536
|
+
], /RegionName.*remove-only/i);
|
|
537
|
+
});
|
|
538
|
+
it("rejects adding every remove-only primitive tag", async () => {
|
|
539
|
+
const file = await (0, _chai_spec_1.testImg)({ srcBasename: "with_faces.jpg" });
|
|
540
|
+
for (const tag of TagEdit_1.TagEditRemoveOnlyTagNames) {
|
|
541
|
+
await expectNoModification(file, [{ tag, operation: "add", value: "Ghost" }], new RegExp(`${tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}.*remove-only`, "i"));
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
it("rejects an empty edit list before invoking ExifTool", async () => {
|
|
545
|
+
await (0, _chai_spec_1.expect)(exiftool.editTags(await (0, _chai_spec_1.testFile)("missing.jpg"), [])).to.be.rejectedWith(/at least one tag edit/i);
|
|
546
|
+
});
|
|
547
|
+
for (const edit of [
|
|
548
|
+
{ tag: "Subject", operation: "remove", value: "beach" },
|
|
549
|
+
{ tag: "XMP-dc:Subject", operation: "remove", value: null },
|
|
550
|
+
{ tag: "XMP-dc:Subject", operation: "add", value: undefined },
|
|
551
|
+
{ tag: "XMP-dc:Subject", operation: "add", value: ["a", "b"] },
|
|
552
|
+
{ tag: "XMP-dc:Subject", operation: "add", value: 42 },
|
|
553
|
+
{ tag: "XMP-dc:Subject", operation: "add", value: true },
|
|
554
|
+
{ tag: "XMP-dc:Subject", operation: "add", value: "" },
|
|
555
|
+
{ tag: "XMP-dc:Subject", operation: "set", value: "forest" },
|
|
556
|
+
]) {
|
|
557
|
+
it(`rejects invalid edit ${JSON.stringify(edit)}`, async () => {
|
|
558
|
+
await (0, _chai_spec_1.expect)(exiftool.editTags(await (0, _chai_spec_1.testFile)("missing.jpg"), [edit])).to.be.rejectedWith(/edit|group-qualified|operation|value|string|supported/i);
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
for (const tag of [
|
|
562
|
+
"XMP-xmp:CreateDate",
|
|
563
|
+
"XMP-iptcExt:PersonInImageName",
|
|
564
|
+
"XMP:All",
|
|
565
|
+
"XMP:Subject",
|
|
566
|
+
"XMP-custom:People",
|
|
567
|
+
]) {
|
|
568
|
+
it(`rejects unsupported or alias tag ${tag} before writing`, async () => {
|
|
569
|
+
const file = await (0, _chai_spec_1.testImg)();
|
|
570
|
+
await expectNoModification(file, [
|
|
571
|
+
{
|
|
572
|
+
tag,
|
|
573
|
+
operation: "remove",
|
|
574
|
+
value: tag === "XMP-xmp:CreateDate" ? "0:0:0 0:0:1" : "Prior Title",
|
|
575
|
+
},
|
|
576
|
+
], /not supported|canonical/i);
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
it("rejects an empty Collection predicate without modifying metadata", async () => {
|
|
580
|
+
const file = await (0, _chai_spec_1.testFile)("collections.xmp");
|
|
581
|
+
await seedCollections(file);
|
|
582
|
+
await expectNoModification(file, [
|
|
583
|
+
{
|
|
584
|
+
tag: "XMP-mwg-coll:Collections",
|
|
585
|
+
operation: "remove",
|
|
586
|
+
predicate: {},
|
|
587
|
+
},
|
|
588
|
+
], /non-empty predicate/i);
|
|
589
|
+
});
|
|
590
|
+
for (const { predicate, message } of [
|
|
591
|
+
{
|
|
592
|
+
predicate: { CollectionName: "Vacation", Typo: "x" },
|
|
593
|
+
message: /unknown Collection predicate field.*Typo/i,
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
predicate: { CollectionName: { Value: "Vacation" } },
|
|
597
|
+
message: /CollectionName.*string/i,
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
predicate: { CollectionURI: 42 },
|
|
601
|
+
message: /CollectionURI.*string/i,
|
|
602
|
+
},
|
|
603
|
+
]) {
|
|
604
|
+
it(`rejects invalid Collection predicate ${JSON.stringify(predicate)} without modifying metadata`, async () => {
|
|
605
|
+
const file = await (0, _chai_spec_1.testFile)("collections.xmp");
|
|
606
|
+
await seedCollections(file);
|
|
607
|
+
await expectNoModification(file, [
|
|
608
|
+
{
|
|
609
|
+
tag: "XMP-mwg-coll:Collections",
|
|
610
|
+
operation: "remove",
|
|
611
|
+
predicate,
|
|
612
|
+
},
|
|
613
|
+
], message);
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
it("validates every predicate before writing any edit", async () => {
|
|
617
|
+
const file = await (0, _chai_spec_1.testFile)("collections.xmp");
|
|
618
|
+
await seedCollections(file);
|
|
619
|
+
await expectNoModification(file, [
|
|
620
|
+
{
|
|
621
|
+
tag: "XMP-dc:Subject",
|
|
622
|
+
operation: "add",
|
|
623
|
+
value: "should-not-be-written",
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
tag: "XMP-mwg-coll:Collections",
|
|
627
|
+
operation: "remove",
|
|
628
|
+
predicate: { CollectionName: "Vacation", Typo: "x" },
|
|
629
|
+
},
|
|
630
|
+
], /unknown Collection predicate field.*Typo/i);
|
|
631
|
+
});
|
|
632
|
+
it("rejects predicates for unsupported structured tags", async () => {
|
|
633
|
+
const file = await (0, _chai_spec_1.testImg)({ srcBasename: "with_faces.jpg" });
|
|
634
|
+
await expectNoModification(file, [
|
|
635
|
+
{
|
|
636
|
+
tag: "XMP-mwg-rs:RegionList",
|
|
637
|
+
operation: "remove",
|
|
638
|
+
predicate: { Name: "Bob Jones", Type: "Face" },
|
|
639
|
+
},
|
|
640
|
+
], /not supported/i);
|
|
641
|
+
});
|
|
642
|
+
for (const value of [
|
|
643
|
+
"{Name=Bob Jones,Type=Face}",
|
|
644
|
+
" {Name=Bob Jones,Type=Face}",
|
|
645
|
+
]) {
|
|
646
|
+
it(`rejects unsupported structured tag with value ${JSON.stringify(value)} without modifying metadata`, async () => {
|
|
647
|
+
const file = await (0, _chai_spec_1.testImg)({ srcBasename: "with_faces.jpg" });
|
|
648
|
+
await expectNoModification(file, [
|
|
649
|
+
{
|
|
650
|
+
tag: "XMP-mwg-rs:RegionList",
|
|
651
|
+
operation: "remove",
|
|
652
|
+
value,
|
|
653
|
+
},
|
|
654
|
+
], /not supported/i);
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
it("rejects operations ExifTool cannot order without modifying metadata", async () => {
|
|
658
|
+
const file = await (0, _chai_spec_1.testFile)("keywords.xmp");
|
|
659
|
+
await exiftool.write(file, { Subject: ["seed"] });
|
|
660
|
+
await expectNoModification(file, [
|
|
661
|
+
{
|
|
662
|
+
tag: "XMP-dc:Subject",
|
|
663
|
+
operation: "add",
|
|
664
|
+
value: "conflict",
|
|
665
|
+
},
|
|
666
|
+
{
|
|
667
|
+
tag: "XMP-dc:Subject",
|
|
668
|
+
operation: "remove",
|
|
669
|
+
value: "conflict",
|
|
670
|
+
},
|
|
671
|
+
], /cannot preserve add-then-remove/i);
|
|
672
|
+
});
|
|
673
|
+
it("allows remove-then-add to normalize duplicate values", async () => {
|
|
674
|
+
const file = await (0, _chai_spec_1.testFile)("keywords.xmp");
|
|
675
|
+
await exiftool.write(file, { Subject: ["forest", "keep", "forest"] });
|
|
676
|
+
await exiftool.editTags(file, [
|
|
677
|
+
{
|
|
678
|
+
tag: "XMP-dc:Subject",
|
|
679
|
+
operation: "remove",
|
|
680
|
+
value: "forest",
|
|
681
|
+
},
|
|
682
|
+
{
|
|
683
|
+
tag: "XMP-dc:Subject",
|
|
684
|
+
operation: "add",
|
|
685
|
+
value: "forest",
|
|
686
|
+
},
|
|
687
|
+
]);
|
|
688
|
+
const raw = (await exiftool.readRaw(file, {
|
|
689
|
+
readArgs: ["-G1", "-XMP-dc:Subject"],
|
|
690
|
+
}));
|
|
691
|
+
(0, _chai_spec_1.expect)(raw["XMP-dc:Subject"]).to.be.an("array");
|
|
692
|
+
(0, _chai_spec_1.expect)([...raw["XMP-dc:Subject"]].sort()).to.eql([
|
|
693
|
+
"forest",
|
|
694
|
+
"keep",
|
|
695
|
+
]);
|
|
696
|
+
});
|
|
697
|
+
it("normalizes duplicate IPTC values in one write", async () => {
|
|
698
|
+
const file = await (0, _chai_spec_1.testImg)({ srcBasename: "iptc.jpg" });
|
|
699
|
+
await exiftool.editTags(file, [
|
|
700
|
+
{
|
|
701
|
+
tag: "IPTC:Keywords",
|
|
702
|
+
operation: "remove",
|
|
703
|
+
value: "IPTC CORE : KEYWORDS",
|
|
704
|
+
},
|
|
705
|
+
{ tag: "IPTC:Keywords", operation: "add", value: "forest" },
|
|
706
|
+
{ tag: "IPTC:Keywords", operation: "add", value: "keep" },
|
|
707
|
+
{ tag: "IPTC:Keywords", operation: "add", value: "forest" },
|
|
708
|
+
]);
|
|
709
|
+
await exiftool.editTags(file, [
|
|
710
|
+
{
|
|
711
|
+
tag: "IPTC:Keywords",
|
|
712
|
+
operation: "remove",
|
|
713
|
+
value: "forest",
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
tag: "IPTC:Keywords",
|
|
717
|
+
operation: "add",
|
|
718
|
+
value: "forest",
|
|
719
|
+
},
|
|
720
|
+
]);
|
|
721
|
+
// Bundled ExifTool 13.59 ground truth:
|
|
722
|
+
// exiftool -IPTC:Keywords-=forest -IPTC:Keywords+=forest file.jpg
|
|
723
|
+
// transforms [forest, keep, forest] to [forest, keep], not [keep, forest].
|
|
724
|
+
const raw = (await exiftool.readRaw(file, {
|
|
725
|
+
readArgs: ["-G1", "-IPTC:Keywords"],
|
|
726
|
+
}));
|
|
727
|
+
(0, _chai_spec_1.expect)(raw["IPTC:Keywords"]).to.eql(["forest", "keep"]);
|
|
728
|
+
});
|
|
729
|
+
it("rejects ExifTool's list separator without modifying metadata", async () => {
|
|
730
|
+
const file = await (0, _chai_spec_1.testFile)("keywords.xmp");
|
|
731
|
+
await exiftool.write(file, { Subject: ["a", "b", "keep"] });
|
|
732
|
+
await expectNoModification(file, [
|
|
733
|
+
{
|
|
734
|
+
tag: "XMP-dc:Subject",
|
|
735
|
+
operation: "remove",
|
|
736
|
+
value: "a\u001fb",
|
|
737
|
+
},
|
|
738
|
+
], /list separator/i);
|
|
739
|
+
});
|
|
740
|
+
it("rejects XML-invalid control characters without modifying metadata", async () => {
|
|
741
|
+
const file = await (0, _chai_spec_1.testFile)("keywords.xmp");
|
|
742
|
+
await exiftool.write(file, { Subject: ["a.b", "keep"] });
|
|
743
|
+
await expectNoModification(file, [
|
|
744
|
+
{
|
|
745
|
+
tag: "XMP-dc:Subject",
|
|
746
|
+
operation: "add",
|
|
747
|
+
value: "a\u0001b",
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
tag: "XMP-dc:Subject",
|
|
751
|
+
operation: "remove",
|
|
752
|
+
value: "a.b",
|
|
753
|
+
},
|
|
754
|
+
], /control character/i);
|
|
755
|
+
});
|
|
756
|
+
for (const value of ["a\ud800b", "a\ufffeb", "a\u{1ffff}b"]) {
|
|
757
|
+
it(`rejects non-round-tripping Unicode ${JSON.stringify(value)} without modifying metadata`, async () => {
|
|
758
|
+
const file = await (0, _chai_spec_1.testFile)("keywords.xmp");
|
|
759
|
+
await exiftool.write(file, { Subject: ["a???b", "keep"] });
|
|
760
|
+
await expectNoModification(file, [
|
|
761
|
+
{
|
|
762
|
+
tag: "XMP-dc:Subject",
|
|
763
|
+
operation: "remove",
|
|
764
|
+
value,
|
|
765
|
+
},
|
|
766
|
+
], /Unicode|character/i);
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
// ExifTool writes a literal CR byte, and its own reader hands it back
|
|
770
|
+
// unchanged, so only a conformant XML parser reveals the corruption.
|
|
771
|
+
for (const value of ["a\rb", "a\r\nb"]) {
|
|
772
|
+
it(`rejects the carriage return in ${JSON.stringify(value)} without modifying metadata`, async () => {
|
|
773
|
+
const file = await (0, _chai_spec_1.testFile)("keywords.xmp");
|
|
774
|
+
await exiftool.write(file, { Subject: ["keep"] });
|
|
775
|
+
await expectNoModification(file, [
|
|
776
|
+
{
|
|
777
|
+
tag: "XMP-dc:Subject",
|
|
778
|
+
operation: "add",
|
|
779
|
+
value,
|
|
780
|
+
},
|
|
781
|
+
], /carriage return/i);
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
it("preserves a leading ASCII space in primitive values", async () => {
|
|
785
|
+
const file = await (0, _chai_spec_1.testFile)("keywords.xmp");
|
|
786
|
+
await exiftool.editTags(file, [
|
|
787
|
+
{
|
|
788
|
+
tag: "XMP-dc:Subject",
|
|
789
|
+
operation: "add",
|
|
790
|
+
value: " leading",
|
|
791
|
+
},
|
|
792
|
+
]);
|
|
793
|
+
const raw = (await exiftool.readRaw(file, {
|
|
794
|
+
readArgs: ["-G1", "-XMP-dc:Subject"],
|
|
795
|
+
}));
|
|
796
|
+
(0, _chai_spec_1.expect)(raw["XMP-dc:Subject"]).to.include(" leading");
|
|
797
|
+
});
|
|
798
|
+
for (const writeArgs of [
|
|
799
|
+
["-sep", ","],
|
|
800
|
+
["-separator", ","],
|
|
801
|
+
["-api", "ListSplit=,"],
|
|
802
|
+
["-api", "NoDups=1"],
|
|
803
|
+
]) {
|
|
804
|
+
it(`rejects edit-incompatible writeArgs ${JSON.stringify(writeArgs)} without modifying metadata`, async () => {
|
|
805
|
+
const file = await (0, _chai_spec_1.testFile)("keywords.xmp");
|
|
806
|
+
await exiftool.write(file, { Subject: ["a", "b", "keep", "a,b"] });
|
|
807
|
+
const before = await (0, _chai_spec_1.sha1)(file);
|
|
808
|
+
await (0, _chai_spec_1.expect)(exiftool.editTags(file, [
|
|
809
|
+
{
|
|
810
|
+
tag: "XMP-dc:Subject",
|
|
811
|
+
operation: "remove",
|
|
812
|
+
value: "a,b",
|
|
813
|
+
},
|
|
814
|
+
], { writeArgs })).to.be.rejectedWith(/writeArgs.*not compatible/i);
|
|
815
|
+
(0, _chai_spec_1.expect)(await (0, _chai_spec_1.sha1)(file)).to.eql(before);
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
it("preserves literal HTML entities in primitive values", async () => {
|
|
819
|
+
const file = await (0, _chai_spec_1.testFile)("keywords.xmp");
|
|
820
|
+
const values = ["A & B", "ab"];
|
|
821
|
+
await exiftool.editTags(file, values.map((value) => ({
|
|
822
|
+
tag: "XMP-dc:Subject",
|
|
823
|
+
operation: "add",
|
|
824
|
+
value,
|
|
825
|
+
})));
|
|
826
|
+
const raw = (await exiftool.readRaw(file, {
|
|
827
|
+
readArgs: ["-G1", "-XMP-dc:Subject"],
|
|
828
|
+
}));
|
|
829
|
+
for (const value of values) {
|
|
830
|
+
(0, _chai_spec_1.expect)(raw["XMP-dc:Subject"]).to.include(value);
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
it("requires canonical casing for a registered structured tag", async () => {
|
|
834
|
+
await (0, _chai_spec_1.expect)(exiftool.editTags(await (0, _chai_spec_1.testFile)("missing.jpg"), [
|
|
835
|
+
{
|
|
836
|
+
tag: "XMP-mwg-coll:collections",
|
|
837
|
+
operation: "remove",
|
|
838
|
+
value: "Vacation",
|
|
839
|
+
},
|
|
840
|
+
])).to.be.rejectedWith(/canonical tag casing/i);
|
|
841
|
+
});
|
|
842
|
+
it("rejects an injected tag before accessing the target file", async () => {
|
|
843
|
+
await (0, _chai_spec_1.expect)(exiftool.editTags(await (0, _chai_spec_1.testFile)("missing.jpg"), [
|
|
844
|
+
{
|
|
845
|
+
tag: "XMP-dc:Subject\n-o\n../exploit",
|
|
846
|
+
operation: "remove",
|
|
847
|
+
value: "beach",
|
|
848
|
+
},
|
|
849
|
+
])).to.be.rejectedWith(/invalid tag edit.*tag name|control character/i);
|
|
850
|
+
});
|
|
851
|
+
it("preserves WriteTaskOptions", async () => {
|
|
852
|
+
const file = await (0, _chai_spec_1.testImg)();
|
|
853
|
+
await exiftool.editTags(file, [
|
|
854
|
+
{
|
|
855
|
+
tag: "XMP-dc:Subject",
|
|
856
|
+
operation: "add",
|
|
857
|
+
value: "forest",
|
|
858
|
+
},
|
|
859
|
+
], { writeArgs: ["-overwrite_original"] });
|
|
860
|
+
(0, _chai_spec_1.expect)((0, node_fs_1.existsSync)(file + "_original")).to.eql(false);
|
|
861
|
+
});
|
|
862
|
+
});
|
|
863
|
+
//# sourceMappingURL=TagEdit.spec.js.map
|