editmamei 1.4.0 → 1.5.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/README.md +6 -6
- package/dist/bin/editmamei-core-darwin-arm64 +0 -0
- package/dist/bin/editmamei-core-darwin-x64 +0 -0
- package/dist/bin/editmamei-core-win-x64.exe +0 -0
- package/dist/cli/help.js +1 -1
- package/dist/core/server.js +110 -13
- package/dist/core/tool-groups.js +1 -0
- package/dist/core/tool-tiers.js +1 -0
- package/dist/install-channel.js +52 -2
- package/dist/kernel/module-lifecycle.js +44 -17
- package/dist/skills/editmamei-skill.zip +0 -0
- package/dist/telemetry/activity.js +149 -0
- package/dist/telemetry/client.js +115 -5
- package/dist/telemetry/events.js +43 -1
- package/dist/tools/document-tools.js +23 -1
- package/dist/update/check.js +24 -10
- package/dist/utils/session-log.js +3 -3
- package/dist/utils/xmp-crs.js +682 -0
- package/dist/version.js +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
export const RAW_EXTENSIONS = [
|
|
2
|
+
'heic',
|
|
3
|
+
'heif',
|
|
4
|
+
'raw',
|
|
5
|
+
'cr2',
|
|
6
|
+
'cr3',
|
|
7
|
+
'nef',
|
|
8
|
+
'arw',
|
|
9
|
+
'orf',
|
|
10
|
+
'rw2',
|
|
11
|
+
'dng',
|
|
12
|
+
'raf',
|
|
13
|
+
'pef',
|
|
14
|
+
'srw',
|
|
15
|
+
];
|
|
16
|
+
export const SIDECAR_DEVELOPABLE_EXTENSIONS = RAW_EXTENSIONS.filter((e) => e !== 'heic' && e !== 'heif');
|
|
17
|
+
const HSL_COLORS = [
|
|
18
|
+
'Red',
|
|
19
|
+
'Orange',
|
|
20
|
+
'Yellow',
|
|
21
|
+
'Green',
|
|
22
|
+
'Aqua',
|
|
23
|
+
'Blue',
|
|
24
|
+
'Purple',
|
|
25
|
+
'Magenta',
|
|
26
|
+
];
|
|
27
|
+
function hslFields() {
|
|
28
|
+
const out = {};
|
|
29
|
+
for (const axis of ['Hue', 'Saturation', 'Luminance']) {
|
|
30
|
+
for (const color of HSL_COLORS) {
|
|
31
|
+
out[`${axis.toLowerCase()}_${color.toLowerCase()}`] = {
|
|
32
|
+
key: `${axis}Adjustment${color}`,
|
|
33
|
+
group: 'hsl',
|
|
34
|
+
format: 'signedInt',
|
|
35
|
+
min: -100,
|
|
36
|
+
max: 100,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
for (const color of HSL_COLORS) {
|
|
41
|
+
out[`bw_mix_${color.toLowerCase()}`] = {
|
|
42
|
+
key: `GrayMixer${color}`,
|
|
43
|
+
group: 'bw',
|
|
44
|
+
format: 'signedInt',
|
|
45
|
+
min: -100,
|
|
46
|
+
max: 100,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
export const CRS_FIELDS = Object.freeze({
|
|
52
|
+
exposure: { key: 'Exposure2012', group: 'basic', format: 'signed2', min: -5, max: 5 },
|
|
53
|
+
contrast: { key: 'Contrast2012', group: 'basic', format: 'signedInt', min: -100, max: 100 },
|
|
54
|
+
highlights: { key: 'Highlights2012', group: 'basic', format: 'signedInt', min: -100, max: 100 },
|
|
55
|
+
shadows: { key: 'Shadows2012', group: 'basic', format: 'signedInt', min: -100, max: 100 },
|
|
56
|
+
whites: { key: 'Whites2012', group: 'basic', format: 'signedInt', min: -100, max: 100 },
|
|
57
|
+
blacks: { key: 'Blacks2012', group: 'basic', format: 'signedInt', min: -100, max: 100 },
|
|
58
|
+
texture: { key: 'Texture', group: 'basic', format: 'signedInt', min: -100, max: 100 },
|
|
59
|
+
clarity: { key: 'Clarity2012', group: 'basic', format: 'signedInt', min: -100, max: 100 },
|
|
60
|
+
dehaze: { key: 'Dehaze', group: 'basic', format: 'signedInt', min: -100, max: 100 },
|
|
61
|
+
vibrance: { key: 'Vibrance', group: 'basic', format: 'signedInt', min: -100, max: 100 },
|
|
62
|
+
saturation: { key: 'Saturation', group: 'basic', format: 'signedInt', min: -100, max: 100 },
|
|
63
|
+
temperature: {
|
|
64
|
+
key: 'Temperature',
|
|
65
|
+
group: 'basic',
|
|
66
|
+
format: 'plainInt',
|
|
67
|
+
min: 2000,
|
|
68
|
+
max: 50000,
|
|
69
|
+
note: 'Kelvin, raw files only. Requires white_balance="Custom" to take effect.',
|
|
70
|
+
},
|
|
71
|
+
tint: { key: 'Tint', group: 'basic', format: 'signedInt', min: -150, max: 150 },
|
|
72
|
+
shadow_tint: { key: 'ShadowTint', group: 'basic', format: 'signedInt', min: -100, max: 100 },
|
|
73
|
+
white_balance: {
|
|
74
|
+
key: 'WhiteBalance',
|
|
75
|
+
group: 'basic',
|
|
76
|
+
format: 'text',
|
|
77
|
+
values: [
|
|
78
|
+
'As Shot',
|
|
79
|
+
'Auto',
|
|
80
|
+
'Daylight',
|
|
81
|
+
'Cloudy',
|
|
82
|
+
'Shade',
|
|
83
|
+
'Tungsten',
|
|
84
|
+
'Fluorescent',
|
|
85
|
+
'Flash',
|
|
86
|
+
'Custom',
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
auto_tone: {
|
|
90
|
+
key: 'AutoTone',
|
|
91
|
+
group: 'auto',
|
|
92
|
+
format: 'bool',
|
|
93
|
+
note: "Camera Raw's real Auto button. Overrides tone sliders it decides to set.",
|
|
94
|
+
},
|
|
95
|
+
upright: {
|
|
96
|
+
key: 'PerspectiveUpright',
|
|
97
|
+
group: 'geometry',
|
|
98
|
+
format: 'plainInt',
|
|
99
|
+
min: 0,
|
|
100
|
+
max: 5,
|
|
101
|
+
note: '0=Off 1=Auto 2=Level 3=Vertical 4=Full 5=Guided. Content-dependent: a frame with no detectable geometry is legitimately left unchanged.',
|
|
102
|
+
},
|
|
103
|
+
rotate: {
|
|
104
|
+
key: 'PerspectiveRotate',
|
|
105
|
+
group: 'geometry',
|
|
106
|
+
format: 'signed1',
|
|
107
|
+
min: -10,
|
|
108
|
+
max: 10,
|
|
109
|
+
verified: true,
|
|
110
|
+
note: 'MEASURED: 5.0 applies, 30.0 is silently discarded.',
|
|
111
|
+
},
|
|
112
|
+
perspective_vertical: {
|
|
113
|
+
key: 'PerspectiveVertical',
|
|
114
|
+
group: 'geometry',
|
|
115
|
+
format: 'signedInt',
|
|
116
|
+
min: -100,
|
|
117
|
+
max: 100,
|
|
118
|
+
},
|
|
119
|
+
perspective_horizontal: {
|
|
120
|
+
key: 'PerspectiveHorizontal',
|
|
121
|
+
group: 'geometry',
|
|
122
|
+
format: 'signedInt',
|
|
123
|
+
min: -100,
|
|
124
|
+
max: 100,
|
|
125
|
+
},
|
|
126
|
+
perspective_scale: {
|
|
127
|
+
key: 'PerspectiveScale',
|
|
128
|
+
group: 'geometry',
|
|
129
|
+
format: 'plainInt',
|
|
130
|
+
min: 50,
|
|
131
|
+
max: 150,
|
|
132
|
+
},
|
|
133
|
+
perspective_aspect: {
|
|
134
|
+
key: 'PerspectiveAspect',
|
|
135
|
+
group: 'geometry',
|
|
136
|
+
format: 'signedInt',
|
|
137
|
+
min: -100,
|
|
138
|
+
max: 100,
|
|
139
|
+
},
|
|
140
|
+
perspective_x: { key: 'PerspectiveX', group: 'geometry', format: 'signed2', min: -100, max: 100 },
|
|
141
|
+
perspective_y: { key: 'PerspectiveY', group: 'geometry', format: 'signed2', min: -100, max: 100 },
|
|
142
|
+
has_crop: { key: 'HasCrop', group: 'crop', format: 'bool' },
|
|
143
|
+
crop_top: { key: 'CropTop', group: 'crop', format: 'decimal6', min: 0, max: 1 },
|
|
144
|
+
crop_left: { key: 'CropLeft', group: 'crop', format: 'decimal6', min: 0, max: 1 },
|
|
145
|
+
crop_bottom: { key: 'CropBottom', group: 'crop', format: 'decimal6', min: 0, max: 1 },
|
|
146
|
+
crop_right: { key: 'CropRight', group: 'crop', format: 'decimal6', min: 0, max: 1 },
|
|
147
|
+
crop_angle: { key: 'CropAngle', group: 'crop', format: 'signed1', min: -45, max: 45 },
|
|
148
|
+
lens_profile_enable: {
|
|
149
|
+
key: 'LensProfileEnable',
|
|
150
|
+
group: 'lens',
|
|
151
|
+
format: 'plainInt',
|
|
152
|
+
min: 0,
|
|
153
|
+
max: 1,
|
|
154
|
+
},
|
|
155
|
+
manual_distortion: {
|
|
156
|
+
key: 'LensManualDistortionAmount',
|
|
157
|
+
group: 'lens',
|
|
158
|
+
format: 'signedInt',
|
|
159
|
+
min: -100,
|
|
160
|
+
max: 100,
|
|
161
|
+
},
|
|
162
|
+
defringe_purple: {
|
|
163
|
+
key: 'DefringePurpleAmount',
|
|
164
|
+
group: 'lens',
|
|
165
|
+
format: 'plainInt',
|
|
166
|
+
min: 0,
|
|
167
|
+
max: 20,
|
|
168
|
+
},
|
|
169
|
+
defringe_green: {
|
|
170
|
+
key: 'DefringeGreenAmount',
|
|
171
|
+
group: 'lens',
|
|
172
|
+
format: 'plainInt',
|
|
173
|
+
min: 0,
|
|
174
|
+
max: 20,
|
|
175
|
+
},
|
|
176
|
+
sharpness: { key: 'Sharpness', group: 'detail', format: 'plainInt', min: 0, max: 150 },
|
|
177
|
+
sharpen_radius: { key: 'SharpenRadius', group: 'detail', format: 'signed1', min: 0.5, max: 3 },
|
|
178
|
+
sharpen_detail: { key: 'SharpenDetail', group: 'detail', format: 'plainInt', min: 0, max: 100 },
|
|
179
|
+
sharpen_masking: {
|
|
180
|
+
key: 'SharpenEdgeMasking',
|
|
181
|
+
group: 'detail',
|
|
182
|
+
format: 'plainInt',
|
|
183
|
+
min: 0,
|
|
184
|
+
max: 100,
|
|
185
|
+
},
|
|
186
|
+
luminance_noise_reduction: {
|
|
187
|
+
key: 'LuminanceSmoothing',
|
|
188
|
+
group: 'detail',
|
|
189
|
+
format: 'plainInt',
|
|
190
|
+
min: 0,
|
|
191
|
+
max: 100,
|
|
192
|
+
},
|
|
193
|
+
color_noise_reduction: {
|
|
194
|
+
key: 'ColorNoiseReduction',
|
|
195
|
+
group: 'detail',
|
|
196
|
+
format: 'plainInt',
|
|
197
|
+
min: 0,
|
|
198
|
+
max: 100,
|
|
199
|
+
},
|
|
200
|
+
split_shadow_hue: {
|
|
201
|
+
key: 'SplitToningShadowHue',
|
|
202
|
+
group: 'split_tone',
|
|
203
|
+
format: 'plainInt',
|
|
204
|
+
min: 0,
|
|
205
|
+
max: 360,
|
|
206
|
+
},
|
|
207
|
+
split_shadow_saturation: {
|
|
208
|
+
key: 'SplitToningShadowSaturation',
|
|
209
|
+
group: 'split_tone',
|
|
210
|
+
format: 'plainInt',
|
|
211
|
+
min: 0,
|
|
212
|
+
max: 100,
|
|
213
|
+
},
|
|
214
|
+
split_highlight_hue: {
|
|
215
|
+
key: 'SplitToningHighlightHue',
|
|
216
|
+
group: 'split_tone',
|
|
217
|
+
format: 'plainInt',
|
|
218
|
+
min: 0,
|
|
219
|
+
max: 360,
|
|
220
|
+
},
|
|
221
|
+
split_highlight_saturation: {
|
|
222
|
+
key: 'SplitToningHighlightSaturation',
|
|
223
|
+
group: 'split_tone',
|
|
224
|
+
format: 'plainInt',
|
|
225
|
+
min: 0,
|
|
226
|
+
max: 100,
|
|
227
|
+
},
|
|
228
|
+
split_balance: {
|
|
229
|
+
key: 'SplitToningBalance',
|
|
230
|
+
group: 'split_tone',
|
|
231
|
+
format: 'signedInt',
|
|
232
|
+
min: -100,
|
|
233
|
+
max: 100,
|
|
234
|
+
},
|
|
235
|
+
post_crop_vignette: {
|
|
236
|
+
key: 'PostCropVignetteAmount',
|
|
237
|
+
group: 'effects',
|
|
238
|
+
format: 'signedInt',
|
|
239
|
+
min: -100,
|
|
240
|
+
max: 100,
|
|
241
|
+
},
|
|
242
|
+
grain_amount: { key: 'GrainAmount', group: 'effects', format: 'plainInt', min: 0, max: 100 },
|
|
243
|
+
grain_size: { key: 'GrainSize', group: 'effects', format: 'plainInt', min: 0, max: 100 },
|
|
244
|
+
convert_to_grayscale: { key: 'ConvertToGrayscale', group: 'bw', format: 'bool' },
|
|
245
|
+
camera_profile: {
|
|
246
|
+
key: 'CameraProfile',
|
|
247
|
+
group: 'profile',
|
|
248
|
+
format: 'text',
|
|
249
|
+
note: 'Resolves BY NAME (e.g. "Camera Landscape", "Adobe Standard"). Creative Looks do NOT resolve by name and are not exposed.',
|
|
250
|
+
},
|
|
251
|
+
...hslFields(),
|
|
252
|
+
});
|
|
253
|
+
export const CRS_CURVES = Object.freeze({
|
|
254
|
+
curve: 'ToneCurvePV2012',
|
|
255
|
+
curve_red: 'ToneCurvePV2012Red',
|
|
256
|
+
curve_green: 'ToneCurvePV2012Green',
|
|
257
|
+
curve_blue: 'ToneCurvePV2012Blue',
|
|
258
|
+
});
|
|
259
|
+
export function isCrsField(name) {
|
|
260
|
+
return Object.prototype.hasOwnProperty.call(CRS_FIELDS, name);
|
|
261
|
+
}
|
|
262
|
+
export function isCrsCurve(name) {
|
|
263
|
+
return Object.prototype.hasOwnProperty.call(CRS_CURVES, name);
|
|
264
|
+
}
|
|
265
|
+
export function validateCrsChanges(changes) {
|
|
266
|
+
const errors = [];
|
|
267
|
+
for (const [name, raw] of Object.entries(changes.fields ?? {})) {
|
|
268
|
+
if (!isCrsField(name)) {
|
|
269
|
+
errors.push(`Unknown develop field '${name}'.`);
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
const spec = CRS_FIELDS[name];
|
|
273
|
+
if (spec.format === 'bool') {
|
|
274
|
+
if (typeof raw !== 'boolean')
|
|
275
|
+
errors.push(`'${name}' must be true or false, got ${JSON.stringify(raw)}.`);
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (spec.format === 'text') {
|
|
279
|
+
if (typeof raw !== 'string' || raw.length === 0) {
|
|
280
|
+
errors.push(`'${name}' must be a non-empty string, got ${JSON.stringify(raw)}.`);
|
|
281
|
+
}
|
|
282
|
+
else if (spec.values && !spec.values.includes(raw)) {
|
|
283
|
+
errors.push(`'${name}' must be one of ${spec.values.join(', ')}; got '${raw}'.`);
|
|
284
|
+
}
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
|
288
|
+
errors.push(`'${name}' must be a number, got ${JSON.stringify(raw)}.`);
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (spec.min !== undefined && raw < spec.min) {
|
|
292
|
+
errors.push(rangeError(name, spec, raw));
|
|
293
|
+
}
|
|
294
|
+
else if (spec.max !== undefined && raw > spec.max) {
|
|
295
|
+
errors.push(rangeError(name, spec, raw));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
for (const [name, points] of Object.entries(changes.curves ?? {})) {
|
|
299
|
+
if (!isCrsCurve(name)) {
|
|
300
|
+
errors.push(`Unknown curve '${name}'. Valid: ${Object.keys(CRS_CURVES).join(', ')}.`);
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (!Array.isArray(points) || points.length < 2) {
|
|
304
|
+
errors.push(`'${name}' needs at least 2 [input, output] points.`);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
for (const p of points) {
|
|
308
|
+
if (!Array.isArray(p) ||
|
|
309
|
+
p.length !== 2 ||
|
|
310
|
+
p.some((n) => !Number.isFinite(n) || n < 0 || n > 255)) {
|
|
311
|
+
errors.push(`'${name}' points must be [input, output] pairs in 0-255; got ${JSON.stringify(p)}.`);
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return errors;
|
|
317
|
+
}
|
|
318
|
+
export function applyCrsCoherence(changes, callerFields) {
|
|
319
|
+
const fields = { ...(changes.fields ?? {}) };
|
|
320
|
+
const notes = [];
|
|
321
|
+
const cropEdges = ['crop_top', 'crop_left', 'crop_bottom', 'crop_right', 'crop_angle'];
|
|
322
|
+
const anyCropEdge = cropEdges.some((k) => k in fields);
|
|
323
|
+
const callerTypedCropEdge = cropEdges.some((k) => callerFields.has(k));
|
|
324
|
+
if (anyCropEdge) {
|
|
325
|
+
if (fields.has_crop === undefined) {
|
|
326
|
+
fields.has_crop = true;
|
|
327
|
+
notes.push('Set has_crop=true: Camera Raw ignores a crop box without it.');
|
|
328
|
+
}
|
|
329
|
+
else if (fields.has_crop === false) {
|
|
330
|
+
if (callerTypedCropEdge && callerFields.has('has_crop')) {
|
|
331
|
+
throw new Error('Crop edges only take effect with has_crop=true, but has_crop=false was given. ' +
|
|
332
|
+
'Pass has_crop=true, or drop the crop edge values.');
|
|
333
|
+
}
|
|
334
|
+
if (callerTypedCropEdge) {
|
|
335
|
+
fields.has_crop = true;
|
|
336
|
+
notes.push('Set has_crop=true: the file had has_crop=false, which would have made the crop values you passed do nothing.');
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
const wbDriven = 'temperature' in fields || 'tint' in fields;
|
|
341
|
+
const callerTypedWbValue = callerFields.has('temperature') || callerFields.has('tint');
|
|
342
|
+
if (wbDriven) {
|
|
343
|
+
const wb = fields.white_balance;
|
|
344
|
+
if (wb === undefined) {
|
|
345
|
+
fields.white_balance = 'Custom';
|
|
346
|
+
notes.push('Set white_balance="Custom": Camera Raw ignores temperature/tint under any other white-balance mode.');
|
|
347
|
+
}
|
|
348
|
+
else if (wb !== 'Custom') {
|
|
349
|
+
if (callerTypedWbValue && callerFields.has('white_balance')) {
|
|
350
|
+
throw new Error(`temperature/tint only take effect with white_balance="Custom", but white_balance="${String(wb)}" was given. ` +
|
|
351
|
+
`Pass white_balance="Custom", or drop the temperature/tint values.`);
|
|
352
|
+
}
|
|
353
|
+
if (callerTypedWbValue) {
|
|
354
|
+
fields.white_balance = 'Custom';
|
|
355
|
+
notes.push(`Set white_balance="Custom": the file had white_balance="${String(wb)}", under which the temperature/tint you passed would be ignored.`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return { changes: { ...changes, fields }, notes };
|
|
360
|
+
}
|
|
361
|
+
function rangeError(name, spec, got) {
|
|
362
|
+
const measured = spec.verified
|
|
363
|
+
? ' (boundary measured against live Camera Raw — out-of-range values are discarded silently)'
|
|
364
|
+
: '';
|
|
365
|
+
return `'${name}' must be between ${spec.min} and ${spec.max}; got ${got}${measured}.`;
|
|
366
|
+
}
|
|
367
|
+
export function formatCrsValue(spec, value) {
|
|
368
|
+
switch (spec.format) {
|
|
369
|
+
case 'bool':
|
|
370
|
+
return value ? 'True' : 'False';
|
|
371
|
+
case 'text':
|
|
372
|
+
return escapeXmlAttr(String(value));
|
|
373
|
+
case 'plainInt':
|
|
374
|
+
return String(Math.round(Number(value)));
|
|
375
|
+
case 'decimal6':
|
|
376
|
+
return Number(value).toFixed(6);
|
|
377
|
+
case 'signed2':
|
|
378
|
+
return signed(Number(value).toFixed(2), Number(value));
|
|
379
|
+
case 'signed1':
|
|
380
|
+
return signed(Number(value).toFixed(1), Number(value));
|
|
381
|
+
case 'signedInt':
|
|
382
|
+
return signed(String(Math.round(Number(value))), Number(value));
|
|
383
|
+
default:
|
|
384
|
+
return String(value);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
function signed(rendered, value) {
|
|
388
|
+
return value > 0 ? `+${rendered}` : rendered;
|
|
389
|
+
}
|
|
390
|
+
function unescapeXmlAttr(s) {
|
|
391
|
+
return s
|
|
392
|
+
.replace(/"/g, '"')
|
|
393
|
+
.replace(/'/g, "'")
|
|
394
|
+
.replace(/</g, '<')
|
|
395
|
+
.replace(/>/g, '>')
|
|
396
|
+
.replace(/&/g, '&');
|
|
397
|
+
}
|
|
398
|
+
function escapeXmlAttr(s) {
|
|
399
|
+
return s
|
|
400
|
+
.replace(/&/g, '&')
|
|
401
|
+
.replace(/</g, '<')
|
|
402
|
+
.replace(/>/g, '>')
|
|
403
|
+
.replace(/"/g, '"');
|
|
404
|
+
}
|
|
405
|
+
function scanDescriptionAt(xmp, open) {
|
|
406
|
+
const attrsStart = open + '<rdf:Description'.length;
|
|
407
|
+
let i = attrsStart;
|
|
408
|
+
let quote = null;
|
|
409
|
+
while (i < xmp.length) {
|
|
410
|
+
const c = xmp[i];
|
|
411
|
+
if (quote) {
|
|
412
|
+
if (c === quote)
|
|
413
|
+
quote = null;
|
|
414
|
+
}
|
|
415
|
+
else if (c === '"' || c === "'") {
|
|
416
|
+
quote = c;
|
|
417
|
+
}
|
|
418
|
+
else if (c === '>') {
|
|
419
|
+
const selfClosing = xmp[i - 1] === '/';
|
|
420
|
+
return {
|
|
421
|
+
attrsStart,
|
|
422
|
+
attrsEnd: selfClosing ? i - 1 : i,
|
|
423
|
+
selfClosing,
|
|
424
|
+
tagEnd: i + 1,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
i++;
|
|
428
|
+
}
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
export function findTopLevelDescription(xmp) {
|
|
432
|
+
let first = null;
|
|
433
|
+
const re = /<(\/?)(crs:[A-Za-z0-9_]+|rdf:Description)([^>]*?)(\/?)>/g;
|
|
434
|
+
let crsDepth = 0;
|
|
435
|
+
let m;
|
|
436
|
+
while ((m = re.exec(xmp)) !== null) {
|
|
437
|
+
const [, closing, qname, , selfClosing] = m;
|
|
438
|
+
if (qname.startsWith('crs:')) {
|
|
439
|
+
if (closing === '/')
|
|
440
|
+
crsDepth--;
|
|
441
|
+
else if (selfClosing !== '/')
|
|
442
|
+
crsDepth++;
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (closing === '/')
|
|
446
|
+
continue;
|
|
447
|
+
if (crsDepth > 0)
|
|
448
|
+
continue;
|
|
449
|
+
const tag = scanDescriptionAt(xmp, m.index);
|
|
450
|
+
if (!tag)
|
|
451
|
+
break;
|
|
452
|
+
if (!first)
|
|
453
|
+
first = tag;
|
|
454
|
+
const attrs = xmp.slice(tag.attrsStart, tag.attrsEnd);
|
|
455
|
+
if (/\scrs:[A-Za-z0-9_]+=/.test(attrs) || /xmlns:crs=/.test(attrs))
|
|
456
|
+
return tag;
|
|
457
|
+
re.lastIndex = Math.max(re.lastIndex, tag.tagEnd);
|
|
458
|
+
}
|
|
459
|
+
return first;
|
|
460
|
+
}
|
|
461
|
+
function emptySidecar() {
|
|
462
|
+
return ('<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 7.0">\n' +
|
|
463
|
+
' <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">\n' +
|
|
464
|
+
' <rdf:Description rdf:about=""\n' +
|
|
465
|
+
' xmlns:crs="http://ns.adobe.com/camera-raw-settings/1.0/"\n' +
|
|
466
|
+
' crs:Version="18.2.2"\n' +
|
|
467
|
+
' crs:ProcessVersion="15.4"\n' +
|
|
468
|
+
' crs:HasSettings="True"/>\n' +
|
|
469
|
+
' </rdf:RDF>\n' +
|
|
470
|
+
'</x:xmpmeta>\n');
|
|
471
|
+
}
|
|
472
|
+
function renderCurve(key, points) {
|
|
473
|
+
const items = points
|
|
474
|
+
.map(([i, o]) => ` <rdf:li>${Math.round(i)}, ${Math.round(o)}</rdf:li>`)
|
|
475
|
+
.join('\n');
|
|
476
|
+
return ` <crs:${key}>\n <rdf:Seq>\n${items}\n </rdf:Seq>\n </crs:${key}>`;
|
|
477
|
+
}
|
|
478
|
+
function indentBlock(block) {
|
|
479
|
+
const lines = block.trim().split('\n');
|
|
480
|
+
const lead = /^\s*/.exec(lines[0])?.[0].length ?? 0;
|
|
481
|
+
return lines
|
|
482
|
+
.map((l) => ' ' + l.slice(Math.min(lead, /^\s*/.exec(l)?.[0].length ?? 0)))
|
|
483
|
+
.join('\n');
|
|
484
|
+
}
|
|
485
|
+
const XML_NAME = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
486
|
+
function topLevelChildBlocks(xmp) {
|
|
487
|
+
const tag = findTopLevelDescription(xmp);
|
|
488
|
+
if (!tag || tag.selfClosing)
|
|
489
|
+
return [];
|
|
490
|
+
const spans = [];
|
|
491
|
+
const re = /<(\/?)(crs:[A-Za-z0-9_]+|rdf:Description)([^>]*?)(\/?)>/g;
|
|
492
|
+
re.lastIndex = tag.tagEnd;
|
|
493
|
+
let depth = 0;
|
|
494
|
+
let openStart = -1;
|
|
495
|
+
let openTag = '';
|
|
496
|
+
let m;
|
|
497
|
+
while ((m = re.exec(xmp)) !== null) {
|
|
498
|
+
const [full, closing, qname, , selfClosing] = m;
|
|
499
|
+
const isCrs = qname.startsWith('crs:');
|
|
500
|
+
const name = isCrs ? qname.slice(4) : qname;
|
|
501
|
+
if (closing === '/') {
|
|
502
|
+
if (depth === 0)
|
|
503
|
+
break;
|
|
504
|
+
depth--;
|
|
505
|
+
if (depth === 0 && isCrs && name === openTag) {
|
|
506
|
+
spans.push({ tag: openTag, start: openStart, end: m.index + full.length });
|
|
507
|
+
openStart = -1;
|
|
508
|
+
openTag = '';
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
else if (selfClosing === '/') {
|
|
512
|
+
if (depth === 0 && isCrs) {
|
|
513
|
+
spans.push({ tag: name, start: m.index, end: m.index + full.length });
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
if (depth === 0 && isCrs) {
|
|
518
|
+
openStart = m.index;
|
|
519
|
+
openTag = name;
|
|
520
|
+
}
|
|
521
|
+
depth++;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return spans;
|
|
525
|
+
}
|
|
526
|
+
export function extractChildBlock(xmp, tagName) {
|
|
527
|
+
if (!XML_NAME.test(tagName)) {
|
|
528
|
+
throw new Error(`Refusing to read a block with a non-identifier tag name: '${tagName}'.`);
|
|
529
|
+
}
|
|
530
|
+
const span = topLevelChildBlocks(xmp).find((s) => s.tag === tagName);
|
|
531
|
+
return span ? xmp.slice(span.start, span.end) : null;
|
|
532
|
+
}
|
|
533
|
+
function upsertChildBlock(xmp, tagName, blockText) {
|
|
534
|
+
if (!XML_NAME.test(tagName)) {
|
|
535
|
+
throw new Error(`Refusing to write a block with a non-identifier tag name: '${tagName}'.`);
|
|
536
|
+
}
|
|
537
|
+
const existing = topLevelChildBlocks(xmp).find((s) => s.tag === tagName);
|
|
538
|
+
if (existing) {
|
|
539
|
+
return (xmp.slice(0, existing.start) + blockText.replace(/^[ \t]+/, '') + xmp.slice(existing.end));
|
|
540
|
+
}
|
|
541
|
+
const tag = findTopLevelDescription(xmp);
|
|
542
|
+
if (!tag)
|
|
543
|
+
throw new Error('Sidecar structure changed unexpectedly during merge.');
|
|
544
|
+
if (tag.selfClosing) {
|
|
545
|
+
return (xmp.slice(0, tag.attrsEnd) +
|
|
546
|
+
'>\n' +
|
|
547
|
+
blockText +
|
|
548
|
+
'\n </rdf:Description>' +
|
|
549
|
+
xmp.slice(tag.tagEnd));
|
|
550
|
+
}
|
|
551
|
+
return xmp.slice(0, tag.tagEnd) + '\n' + blockText + xmp.slice(tag.tagEnd);
|
|
552
|
+
}
|
|
553
|
+
const CARRYABLE_BLOCKS = [
|
|
554
|
+
'Look',
|
|
555
|
+
'ToneCurvePV2012',
|
|
556
|
+
'ToneCurvePV2012Red',
|
|
557
|
+
'ToneCurvePV2012Green',
|
|
558
|
+
'ToneCurvePV2012Blue',
|
|
559
|
+
];
|
|
560
|
+
const UNCARRYABLE_BLOCKS = [
|
|
561
|
+
[
|
|
562
|
+
'MaskGroupBasedCorrections',
|
|
563
|
+
'local/AI-mask adjustments — hand-moved mask blocks are discarded by Camera Raw, so carrying them would promise an edit that silently would not happen',
|
|
564
|
+
],
|
|
565
|
+
['PointColors', 'point-colour adjustments — untested through a written sidecar'],
|
|
566
|
+
['RetouchAreas', 'healing/spot removal — untested through a written sidecar'],
|
|
567
|
+
];
|
|
568
|
+
export function readPresetChanges(presetXmp) {
|
|
569
|
+
const raw = readTopLevelCrs(presetXmp);
|
|
570
|
+
const byKey = new Map(Object.entries(CRS_FIELDS).map(([name, spec]) => [spec.key, name]));
|
|
571
|
+
const fields = {};
|
|
572
|
+
const applied = [];
|
|
573
|
+
const rangeSkips = [];
|
|
574
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
575
|
+
const name = byKey.get(key);
|
|
576
|
+
if (!name)
|
|
577
|
+
continue;
|
|
578
|
+
const spec = CRS_FIELDS[name];
|
|
579
|
+
let candidate;
|
|
580
|
+
if (spec.format === 'bool')
|
|
581
|
+
candidate = value === 'True';
|
|
582
|
+
else if (spec.format === 'text')
|
|
583
|
+
candidate = value;
|
|
584
|
+
else {
|
|
585
|
+
const n = Number(value);
|
|
586
|
+
if (!Number.isFinite(n))
|
|
587
|
+
continue;
|
|
588
|
+
candidate = n;
|
|
589
|
+
}
|
|
590
|
+
const problems = validateCrsChanges({ fields: { [name]: candidate } });
|
|
591
|
+
if (problems.length > 0) {
|
|
592
|
+
rangeSkips.push(`${name}: ${problems[0]}`);
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
fields[name] = candidate;
|
|
596
|
+
applied.push(name);
|
|
597
|
+
}
|
|
598
|
+
const blocks = {};
|
|
599
|
+
const carried = [];
|
|
600
|
+
for (const tagName of CARRYABLE_BLOCKS) {
|
|
601
|
+
const block = extractChildBlock(presetXmp, tagName);
|
|
602
|
+
if (block) {
|
|
603
|
+
blocks[tagName] = block;
|
|
604
|
+
carried.push(tagName);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
const skipped = [...rangeSkips];
|
|
608
|
+
for (const [tagName, why] of UNCARRYABLE_BLOCKS) {
|
|
609
|
+
if (extractChildBlock(presetXmp, tagName))
|
|
610
|
+
skipped.push(`${tagName}: ${why}`);
|
|
611
|
+
}
|
|
612
|
+
return { changes: { fields, blocks }, applied: applied.sort(), carried: carried.sort(), skipped };
|
|
613
|
+
}
|
|
614
|
+
export function mergeCrsIntoSidecar(original, changes) {
|
|
615
|
+
const errors = validateCrsChanges(changes);
|
|
616
|
+
if (errors.length > 0) {
|
|
617
|
+
throw new Error(`Invalid develop settings:\n- ${errors.join('\n- ')}`);
|
|
618
|
+
}
|
|
619
|
+
let xmp = original && original.includes('<rdf:Description') ? original : emptySidecar();
|
|
620
|
+
const tag = findTopLevelDescription(xmp);
|
|
621
|
+
if (!tag) {
|
|
622
|
+
throw new Error('Sidecar has no <rdf:Description> element; refusing to guess its structure.');
|
|
623
|
+
}
|
|
624
|
+
const attrs = [['HasSettings', 'True']];
|
|
625
|
+
for (const [name, value] of Object.entries(changes.fields ?? {})) {
|
|
626
|
+
const spec = CRS_FIELDS[name];
|
|
627
|
+
attrs.push([spec.key, formatCrsValue(spec, value)]);
|
|
628
|
+
}
|
|
629
|
+
let region = xmp.slice(tag.attrsStart, tag.attrsEnd);
|
|
630
|
+
for (const [key, rendered] of attrs) {
|
|
631
|
+
const existing = new RegExp(`(\\scrs:${key}=")[^"]*(")`);
|
|
632
|
+
if (existing.test(region)) {
|
|
633
|
+
region = region.replace(existing, (_m, open, close) => open + rendered + close);
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
region = `${region}\n crs:${key}="${rendered}"`;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
if (!/xmlns:crs=/.test(region) && !/xmlns:crs=/.test(xmp.slice(0, tag.attrsStart))) {
|
|
640
|
+
region = `\n xmlns:crs="http://ns.adobe.com/camera-raw-settings/1.0/"${region}`;
|
|
641
|
+
}
|
|
642
|
+
xmp = xmp.slice(0, tag.attrsStart) + region + xmp.slice(tag.attrsEnd);
|
|
643
|
+
for (const [tagName, blockText] of Object.entries(changes.blocks ?? {})) {
|
|
644
|
+
xmp = upsertChildBlock(xmp, tagName, indentBlock(blockText));
|
|
645
|
+
}
|
|
646
|
+
for (const [name, points] of Object.entries(changes.curves ?? {})) {
|
|
647
|
+
xmp = upsertChildBlock(xmp, CRS_CURVES[name], renderCurve(CRS_CURVES[name], points));
|
|
648
|
+
}
|
|
649
|
+
return xmp;
|
|
650
|
+
}
|
|
651
|
+
export function readTopLevelCrs(xmp) {
|
|
652
|
+
const tag = findTopLevelDescription(xmp);
|
|
653
|
+
if (!tag)
|
|
654
|
+
return {};
|
|
655
|
+
const region = xmp.slice(tag.attrsStart, tag.attrsEnd);
|
|
656
|
+
const out = {};
|
|
657
|
+
const re = /crs:([A-Za-z0-9_]+)="([^"]*)"/g;
|
|
658
|
+
let m;
|
|
659
|
+
while ((m = re.exec(region)))
|
|
660
|
+
out[m[1]] = unescapeXmlAttr(m[2]);
|
|
661
|
+
return out;
|
|
662
|
+
}
|
|
663
|
+
export function describeSidecar(xmp) {
|
|
664
|
+
const raw = readTopLevelCrs(xmp);
|
|
665
|
+
const byKey = new Map(Object.entries(CRS_FIELDS).map(([name, spec]) => [spec.key, name]));
|
|
666
|
+
const out = {};
|
|
667
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
668
|
+
const name = byKey.get(key);
|
|
669
|
+
if (!name)
|
|
670
|
+
continue;
|
|
671
|
+
const spec = CRS_FIELDS[name];
|
|
672
|
+
if (spec.format === 'bool')
|
|
673
|
+
out[name] = value === 'True';
|
|
674
|
+
else if (spec.format === 'text')
|
|
675
|
+
out[name] = value;
|
|
676
|
+
else {
|
|
677
|
+
const n = Number(value);
|
|
678
|
+
out[name] = Number.isFinite(n) ? n : value;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return out;
|
|
682
|
+
}
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '1.
|
|
1
|
+
export const VERSION = '1.5.0';
|