@kitschpatrol/aphex 0.1.1 → 0.1.3
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/dist/aphex-swift +0 -0
- package/dist/index.d.ts +34 -18
- package/dist/index.js +136 -141
- package/dist/workers/process-image-worker.js +2 -3
- package/package.json +100 -98
- package/readme.md +5 -5
package/dist/aphex-swift
CHANGED
|
Binary file
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { ExifTool } from "exiftool-vendored";
|
|
2
2
|
import { ILogBasic, ILogLayer } from "lognow";
|
|
3
3
|
import { OmitDeep, PartialDeep, Simplify } from "type-fest";
|
|
4
|
-
|
|
5
4
|
//#region src/aphex-swift/cli-bridge.d.ts
|
|
6
5
|
/**
|
|
7
6
|
* Start an interactive session with aphex-swift. Commands will be sent to this
|
|
@@ -76,9 +75,13 @@ type ImageMimeType = (typeof IMAGE_MIME_TYPES)[number];
|
|
|
76
75
|
* options in the GUI.
|
|
77
76
|
*/
|
|
78
77
|
type ExportViaAppleScriptGuiOptions = {
|
|
79
|
-
/** "Color Profile" Drop Down */
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
/** "Color Profile" Drop Down */
|
|
79
|
+
colorProfile: 'AdobeRGB' | 'Display P3' | 'Most Compatible' | 'Original' | 'sRGB';
|
|
80
|
+
/** "File Name" Drop Down */
|
|
81
|
+
fileName: 'Album Name With Number' | 'Sequential' | 'Use File Name' | 'Use Title';
|
|
82
|
+
/** "Include Location Information" Checkbox */
|
|
83
|
+
includeLocation: boolean;
|
|
84
|
+
/** "Include Title, Keywords, and Caption" Checkbox */
|
|
82
85
|
includeMetadata: boolean;
|
|
83
86
|
/**
|
|
84
87
|
* "JPEG Quality" Drop Down
|
|
@@ -97,15 +100,18 @@ type ExportViaAppleScriptGuiOptions = {
|
|
|
97
100
|
*
|
|
98
101
|
* Only applies if `photoSize` is `Custom`
|
|
99
102
|
*/
|
|
100
|
-
maxSizeValue: number;
|
|
101
|
-
|
|
103
|
+
maxSizeValue: number;
|
|
104
|
+
/** "Photo Kind" Drop Down */
|
|
105
|
+
photoKind: 'HEIC' | 'JPEG' | 'PNG' | 'TIFF';
|
|
106
|
+
/** "Size" Drop Down */
|
|
102
107
|
photoSize: 'Custom' | 'Full Size' | 'Large' | 'Medium' | 'Small';
|
|
103
108
|
/**
|
|
104
109
|
* "Sequential Prefix" Text Field
|
|
105
110
|
*
|
|
106
111
|
* Only applies if `fileName` is `Sequential`
|
|
107
112
|
*/
|
|
108
|
-
sequentialPrefix: string;
|
|
113
|
+
sequentialPrefix: string;
|
|
114
|
+
/** "Subfolder Format" Drop Down */
|
|
109
115
|
subfolderFormat: 'Moment Name' | 'None';
|
|
110
116
|
/**
|
|
111
117
|
* "16 Bit" Checkbox
|
|
@@ -142,7 +148,6 @@ type ExportApplePhotoResult = {
|
|
|
142
148
|
photoInfo: PhotoInfo;
|
|
143
149
|
};
|
|
144
150
|
type FileNameOptions = 'fileName' | 'title' | 'uuid';
|
|
145
|
-
/** Also normalizes */
|
|
146
151
|
//#endregion
|
|
147
152
|
//#region src/utilities/image/tags.d.ts
|
|
148
153
|
declare const VALID_LABELS: readonly ["animation", "diagram", "illustration", "image", "photo", "render", "screenshot", "video"];
|
|
@@ -156,9 +161,12 @@ type AphexMetadata = {
|
|
|
156
161
|
photoInfo: PhotoInfo;
|
|
157
162
|
};
|
|
158
163
|
type ImageTags = {
|
|
159
|
-
aphexMetadata?: AphexMetadata | undefined;
|
|
160
|
-
|
|
161
|
-
|
|
164
|
+
aphexMetadata?: AphexMetadata | undefined;
|
|
165
|
+
/** Human Name */
|
|
166
|
+
creator?: string | undefined;
|
|
167
|
+
/** Organization */
|
|
168
|
+
credit?: string | undefined;
|
|
169
|
+
/** Description, used as alt text in HTML content */
|
|
162
170
|
description?: string | undefined;
|
|
163
171
|
/**
|
|
164
172
|
* Image Type, e.g. 'animation', 'diagram', 'illustration', 'screenshot',
|
|
@@ -278,16 +286,24 @@ type ImageInfo = {
|
|
|
278
286
|
//#endregion
|
|
279
287
|
//#region src/pipeline/image-process.d.ts
|
|
280
288
|
type ProcessImageOptions = CompressImageOptions & {
|
|
281
|
-
/** Fallback color profile applied when source profile is not in preserve list */
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
289
|
+
/** Fallback color profile applied when source profile is not in preserve list */
|
|
290
|
+
defaultColorProfile: ColorProfile;
|
|
291
|
+
/** Calculate SSIM/PSNR/DSSIM metrics comparing original vs processed images */
|
|
292
|
+
logSimilarity: boolean;
|
|
293
|
+
/** Lossless format used specifically for images with alpha channels */
|
|
294
|
+
losslessFormatAlpha: LosslessFormat;
|
|
295
|
+
/** Lossy format used specifically for images with alpha channels */
|
|
296
|
+
lossyFormatAlpha: LossyFormat;
|
|
297
|
+
/** Maximum pixel dimensions before triggering resize (width OR height) */
|
|
285
298
|
maxDimensionsPixels: {
|
|
286
299
|
width: number;
|
|
287
300
|
height: number;
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
|
|
301
|
+
};
|
|
302
|
+
/** Near-lossless format used specifically for images with alpha channels */
|
|
303
|
+
nearLosslessFormatAlpha: NearLosslessFormat;
|
|
304
|
+
/** Image formats that bypass conversion and compression entirely */
|
|
305
|
+
passthroughFormats: ImageMimeType[];
|
|
306
|
+
/** Color profiles that are kept unchanged instead of being normalized */
|
|
291
307
|
preserveColorProfiles: ColorProfile[];
|
|
292
308
|
};
|
|
293
309
|
type ProcessImageResult = {
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import fse from "fs-extra";
|
|
2
|
-
import path
|
|
2
|
+
import path from "node:path";
|
|
3
3
|
import is, { assert, isNonEmptyStringAndNotWhitespace } from "@sindresorhus/is";
|
|
4
4
|
import { execa } from "execa";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
@@ -58,11 +58,10 @@ function getExiftool() {
|
|
|
58
58
|
* processes. It's safe to call multiple times.
|
|
59
59
|
*/
|
|
60
60
|
async function endExiftool() {
|
|
61
|
-
if (exiftoolInstance.pids.length
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
61
|
+
if (exiftoolInstance.pids.length === 0) return;
|
|
62
|
+
log.debug(`Ending exiftool processes: ${exiftoolInstance.pids.join(", ")}`);
|
|
63
|
+
await exiftoolInstance.end();
|
|
64
|
+
log.debug("Exiftool processes ended");
|
|
66
65
|
}
|
|
67
66
|
/**
|
|
68
67
|
* Install process exit handlers for automatic cleanup
|
|
@@ -73,24 +72,27 @@ async function endExiftool() {
|
|
|
73
72
|
function scheduleProcessCleanup() {
|
|
74
73
|
if (processExitHandlerInstalled) return;
|
|
75
74
|
processExitHandlerInstalled = true;
|
|
76
|
-
process.on("beforeExit",
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
75
|
+
process.on("beforeExit", () => {
|
|
76
|
+
if (cleanupScheduled) return;
|
|
77
|
+
cleanupScheduled = true;
|
|
78
|
+
endExiftool();
|
|
81
79
|
});
|
|
82
80
|
process.on("exit", () => {
|
|
83
81
|
if (exiftoolInstance.pids.length > 0) log.debug(`Exiftool processes still running at exit: ${exiftoolInstance.pids.join(", ")}`);
|
|
84
82
|
});
|
|
85
|
-
process.on("SIGINT",
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
process.on("SIGINT", () => {
|
|
84
|
+
(async () => {
|
|
85
|
+
log.debug("Received SIGINT, cleaning up exiftool...");
|
|
86
|
+
await endExiftool();
|
|
87
|
+
process.exit(130);
|
|
88
|
+
})();
|
|
89
89
|
});
|
|
90
|
-
process.on("SIGTERM",
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
process.on("SIGTERM", () => {
|
|
91
|
+
(async () => {
|
|
92
|
+
log.debug("Received SIGTERM, cleaning up exiftool...");
|
|
93
|
+
await endExiftool();
|
|
94
|
+
process.exit(143);
|
|
95
|
+
})();
|
|
94
96
|
});
|
|
95
97
|
}
|
|
96
98
|
//#endregion
|
|
@@ -155,7 +157,7 @@ async function getTempDirectory(...labels) {
|
|
|
155
157
|
}
|
|
156
158
|
//#endregion
|
|
157
159
|
//#region src/utilities/general.ts
|
|
158
|
-
const UUID_REGEX = /^[\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12}$/
|
|
160
|
+
const UUID_REGEX = /^[\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12}$/iv;
|
|
159
161
|
/**
|
|
160
162
|
* Sips makes a mess of the temp folder and does not respect TMPDIR, so we need
|
|
161
163
|
* to clean up after it ourselves
|
|
@@ -200,7 +202,7 @@ function assertSingleElement(array) {
|
|
|
200
202
|
* getDirname(import.meta)
|
|
201
203
|
*/
|
|
202
204
|
function getDirname(meta) {
|
|
203
|
-
return dirname(fileURLToPath(process.env.PISCINA_WORKER_META_URL ?? meta.url));
|
|
205
|
+
return path.dirname(fileURLToPath(process.env.PISCINA_WORKER_META_URL ?? meta.url));
|
|
204
206
|
}
|
|
205
207
|
/**
|
|
206
208
|
* Asset directory path of package or distribution folder (Copied by tsdown)
|
|
@@ -226,7 +228,7 @@ function getPackageBinPath(meta) {
|
|
|
226
228
|
}
|
|
227
229
|
//#endregion
|
|
228
230
|
//#region src/aphex-swift/cli-bridge.ts
|
|
229
|
-
const SHELL_SPECIAL_CHARS_REGEX = /[\s'"\\]
|
|
231
|
+
const SHELL_SPECIAL_CHARS_REGEX = /[\s'"\\]/v;
|
|
230
232
|
let interactiveProcess;
|
|
231
233
|
let currentRequest;
|
|
232
234
|
/**
|
|
@@ -237,8 +239,9 @@ let currentRequest;
|
|
|
237
239
|
*/
|
|
238
240
|
async function interactiveSessionStart() {
|
|
239
241
|
if (interactiveProcess !== void 0) throw new Error("Interactive session is already active");
|
|
242
|
+
const binPath = getPackageBinPath(import.meta);
|
|
240
243
|
interactiveProcess = spawn("./aphex-swift", ["interactive"], {
|
|
241
|
-
cwd:
|
|
244
|
+
cwd: binPath,
|
|
242
245
|
stdio: [
|
|
243
246
|
"pipe",
|
|
244
247
|
"pipe",
|
|
@@ -258,13 +261,12 @@ async function interactiveSessionStart() {
|
|
|
258
261
|
} catch {}
|
|
259
262
|
});
|
|
260
263
|
interactiveProcess.stderr?.on("data", (data) => {
|
|
261
|
-
if (currentRequest
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
264
|
+
if (currentRequest === void 0) return;
|
|
265
|
+
const error = new Error(data.toString().trim());
|
|
266
|
+
const request = currentRequest;
|
|
267
|
+
currentRequest = void 0;
|
|
268
|
+
request.reject(error);
|
|
269
|
+
processNextCommand();
|
|
268
270
|
});
|
|
269
271
|
interactiveProcess.on("close", (code) => {
|
|
270
272
|
const error = /* @__PURE__ */ new Error(`Interactive session exited with code ${code}`);
|
|
@@ -310,7 +312,7 @@ function isInteractiveSessionActive() {
|
|
|
310
312
|
}
|
|
311
313
|
let commandQueue = [];
|
|
312
314
|
function processNextCommand() {
|
|
313
|
-
if (currentRequest !== void 0 ||
|
|
315
|
+
if (currentRequest !== void 0 || interactiveProcess === void 0 || commandQueue.length === 0) return;
|
|
314
316
|
const next = commandQueue.shift();
|
|
315
317
|
currentRequest = {
|
|
316
318
|
buffer: "",
|
|
@@ -472,7 +474,12 @@ async function aphexExport(identifiers, destination, caseSensitive = false, orig
|
|
|
472
474
|
}
|
|
473
475
|
}
|
|
474
476
|
function dateReviver$1(key, value) {
|
|
475
|
-
if (
|
|
477
|
+
if (typeof value === "string" && [
|
|
478
|
+
"dateCreated",
|
|
479
|
+
"dateEnd",
|
|
480
|
+
"dateModified",
|
|
481
|
+
"dateStart"
|
|
482
|
+
].includes(key)) return value.length > 0 ? new Date(value) : void 0;
|
|
476
483
|
return value;
|
|
477
484
|
}
|
|
478
485
|
//#endregion
|
|
@@ -500,15 +507,16 @@ async function resolveIdentifiers(identifiers) {
|
|
|
500
507
|
async function resolvePhotoIdentifier(identifier) {
|
|
501
508
|
if (isPhotoInfo(identifier)) return identifier;
|
|
502
509
|
const aphexPhotoInfoResult = await aphexPhotoInfo(identifier);
|
|
503
|
-
|
|
510
|
+
const [photoInfo] = aphexPhotoInfoResult;
|
|
511
|
+
if (photoInfo === void 0) throw new Error(`No photo asset found for identifier "${identifier}"`);
|
|
504
512
|
if (aphexPhotoInfoResult.length > 1) throw new Error(`Multiple photo assets found for identifier "${identifier} — is it an album?"`);
|
|
505
|
-
return
|
|
513
|
+
return photoInfo;
|
|
506
514
|
}
|
|
507
515
|
//#endregion
|
|
508
516
|
//#region src/utilities/defaults.ts
|
|
509
517
|
const mergeDefaultsInternal = createDefu((object, key, value) => {
|
|
510
518
|
if (Array.isArray(object[key]) && Array.isArray(value)) {
|
|
511
|
-
const merged = [
|
|
519
|
+
const merged = [.../* @__PURE__ */ new Set([...object[key], ...value])];
|
|
512
520
|
Object.assign(object, { [key]: merged });
|
|
513
521
|
return true;
|
|
514
522
|
}
|
|
@@ -574,7 +582,7 @@ function lookupImageMimeType(imagePath, validate = false) {
|
|
|
574
582
|
}
|
|
575
583
|
//#endregion
|
|
576
584
|
//#region src/utilities/image/color.ts
|
|
577
|
-
const SIPS_PROFILE_REGEX = /profile: (.+)
|
|
585
|
+
const SIPS_PROFILE_REGEX = /profile: (.+)/v;
|
|
578
586
|
/**
|
|
579
587
|
* Get the color profile of an image
|
|
580
588
|
* https://github.com/saucecontrol/Compact-ICC-Profiles?tab=readme-ov-file
|
|
@@ -777,7 +785,12 @@ async function getTags(imagePath) {
|
|
|
777
785
|
};
|
|
778
786
|
}
|
|
779
787
|
function dateReviver(key, value) {
|
|
780
|
-
if (
|
|
788
|
+
if (typeof value === "string" && [
|
|
789
|
+
"dateCreated",
|
|
790
|
+
"dateEnd",
|
|
791
|
+
"dateModified",
|
|
792
|
+
"dateStart"
|
|
793
|
+
].includes(key)) return value.length > 0 ? new Date(value) : void 0;
|
|
781
794
|
return value;
|
|
782
795
|
}
|
|
783
796
|
function isAphexMetadata(value) {
|
|
@@ -806,12 +819,12 @@ async function setTags(imagePath, imageTags) {
|
|
|
806
819
|
const { aphexMetadata, creator, credit, description, label, preservedFileName } = imageTags;
|
|
807
820
|
const userComment = aphexMetadata ? JSON.stringify(aphexMetadata) : void 0;
|
|
808
821
|
const tags = {
|
|
809
|
-
..."creator" in imageTags
|
|
810
|
-
..."description" in imageTags
|
|
811
|
-
..."credit" in imageTags
|
|
812
|
-
..."label" in imageTags
|
|
813
|
-
..."preservedFileName" in imageTags
|
|
814
|
-
..."aphexMetadata" in imageTags
|
|
822
|
+
..."creator" in imageTags && { "XMP:Creator": creator ?? "" },
|
|
823
|
+
..."description" in imageTags && { "XMP:Description": description ?? "" },
|
|
824
|
+
..."credit" in imageTags && { "XMP:Credit": credit ?? "" },
|
|
825
|
+
..."label" in imageTags && { "XMP:Label": label ?? "" },
|
|
826
|
+
..."preservedFileName" in imageTags && { "XMP:PreservedFileName": preservedFileName ?? "" },
|
|
827
|
+
..."aphexMetadata" in imageTags && { "XMP:UserComment": userComment ?? "" }
|
|
815
828
|
};
|
|
816
829
|
await getExiftool().write(imagePath, tags, { writeArgs: ["-overwrite_original_in_place"] });
|
|
817
830
|
}
|
|
@@ -831,8 +844,8 @@ async function cloneTags(sourceImagePath, destinationImagePath, includeKeys, for
|
|
|
831
844
|
];
|
|
832
845
|
let tagsToAssign = {};
|
|
833
846
|
for (const key of keys) {
|
|
834
|
-
const sourceValue =
|
|
835
|
-
if (sourceValue !==
|
|
847
|
+
const sourceValue = sourceTags[key];
|
|
848
|
+
if (sourceValue !== destinationTags[key] || force) tagsToAssign = {
|
|
836
849
|
...tagsToAssign,
|
|
837
850
|
[key]: sourceValue
|
|
838
851
|
};
|
|
@@ -896,7 +909,7 @@ async function hasAlpha(imagePath) {
|
|
|
896
909
|
]);
|
|
897
910
|
return Number(stdout) < 1;
|
|
898
911
|
} catch (error) {
|
|
899
|
-
throw new Error(`Error checking file: ${String(error)}
|
|
912
|
+
throw new Error(`Error checking file: ${String(error)}`, { cause: error });
|
|
900
913
|
}
|
|
901
914
|
}
|
|
902
915
|
//#endregion
|
|
@@ -937,7 +950,7 @@ async function exportViaAppleScript(photoUuid, forceOriginal = false) {
|
|
|
937
950
|
return path.join(tempDirectory, exportedFileName);
|
|
938
951
|
} catch (error) {
|
|
939
952
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
940
|
-
throw new Error(`Failed to export photo via AppleScript: ${errorMessage}
|
|
953
|
+
throw new Error(`Failed to export photo via AppleScript: ${errorMessage}`, { cause: error });
|
|
941
954
|
}
|
|
942
955
|
}
|
|
943
956
|
//#endregion
|
|
@@ -964,8 +977,9 @@ const defaultExportViaAppleScriptGuiOptions = {
|
|
|
964
977
|
async function exportViaAppleScriptGui(uuid, options) {
|
|
965
978
|
const tempDirectory = await getTempDirectory("engine", "applescript-gui");
|
|
966
979
|
const { colorProfile, fileName, includeLocation, includeMetadata, jpegQuality, maxSizeType, maxSizeValue, photoKind, photoSize, sequentialPrefix, subfolderFormat, tiffBitDepth } = mergeDefaults(options, defaultExportViaAppleScriptGuiOptions);
|
|
980
|
+
const appleScriptPath = path.join(getPackageAssetsPath(import.meta), "applescript-gui.applescript");
|
|
967
981
|
const { failed, stderr } = await execa("osascript", [
|
|
968
|
-
|
|
982
|
+
appleScriptPath,
|
|
969
983
|
uuid,
|
|
970
984
|
tempDirectory,
|
|
971
985
|
photoKind,
|
|
@@ -994,7 +1008,9 @@ async function exportViaAppleScriptGui(uuid, options) {
|
|
|
994
1008
|
* a temporary directory Clean up and move the file as needed afterwards
|
|
995
1009
|
*/
|
|
996
1010
|
async function exportViaFileSystem(photoUuid, forceOriginal = false) {
|
|
997
|
-
const [
|
|
1011
|
+
const [photoInfo] = isPhotoInfo(photoUuid) ? [photoUuid] : await aphexPhotoInfo(photoUuid);
|
|
1012
|
+
if (photoInfo === void 0) throw new Error(`No photo info found for UUID "${isPhotoInfo(photoUuid) ? photoUuid.uuid : photoUuid}"`);
|
|
1013
|
+
const { edited, original } = photoInfo;
|
|
998
1014
|
const tempDirectory = await getTempDirectory("engine", "file-system");
|
|
999
1015
|
if (forceOriginal || edited === void 0) {
|
|
1000
1016
|
const destinationPath = path.join(tempDirectory, original.fileName);
|
|
@@ -1012,6 +1028,7 @@ async function exportViaFileSystem(photoUuid, forceOriginal = false) {
|
|
|
1012
1028
|
*/
|
|
1013
1029
|
async function exportViaSwiftPhotoKit(photoUuid) {
|
|
1014
1030
|
const [result] = await aphexExport(photoUuid, await getTempDirectory("engine", "swift-photokit"));
|
|
1031
|
+
if (result === void 0) throw new Error(`Export via PhotoKit returned no files for photo "${photoUuid}"`);
|
|
1015
1032
|
return result;
|
|
1016
1033
|
}
|
|
1017
1034
|
//#endregion
|
|
@@ -1073,25 +1090,9 @@ async function exportApplePhotos(identifiers, destinationDirectory, options) {
|
|
|
1073
1090
|
const exportedPhoto = {
|
|
1074
1091
|
exportEngine: engine,
|
|
1075
1092
|
exportOptions: resolvedOptions,
|
|
1076
|
-
path:
|
|
1093
|
+
path: await exportPhotoWithEngine(photoInfo, engine, resolvedOptions),
|
|
1077
1094
|
photoInfo
|
|
1078
1095
|
};
|
|
1079
|
-
switch (engine) {
|
|
1080
|
-
case "applescript":
|
|
1081
|
-
exportedPhoto.path = await exportViaAppleScript(photoInfo.uuid);
|
|
1082
|
-
break;
|
|
1083
|
-
case "file-system":
|
|
1084
|
-
exportedPhoto.path = await exportViaFileSystem(photoInfo);
|
|
1085
|
-
break;
|
|
1086
|
-
case "photos-gui": {
|
|
1087
|
-
const [exportedPath] = await exportViaAppleScriptGui(photoInfo.uuid, resolvedOptions.appleScriptGuiOptions);
|
|
1088
|
-
exportedPhoto.path = exportedPath;
|
|
1089
|
-
break;
|
|
1090
|
-
}
|
|
1091
|
-
case "swift-photokit":
|
|
1092
|
-
exportedPhoto.path = await exportViaSwiftPhotoKit(photoInfo.uuid);
|
|
1093
|
-
break;
|
|
1094
|
-
}
|
|
1095
1096
|
const resolvedDestinationDirectory = await ensureDirectoryExists(destinationDirectory);
|
|
1096
1097
|
const normalizedDestinationPath = getImagePathWithFileName(photoInfo, path.join(resolvedDestinationDirectory, path.basename(exportedPhoto.path)), resolvedOptions.fileNameSluggify, resolvedOptions.fileNameAppendUuidFragment, resolvedOptions.fileNameNormalizeExtensions, resolvedOptions.fileNamePrecedence);
|
|
1097
1098
|
await fse.rename(exportedPhoto.path, normalizedDestinationPath);
|
|
@@ -1105,6 +1106,18 @@ async function exportApplePhotos(identifiers, destinationDirectory, options) {
|
|
|
1105
1106
|
}
|
|
1106
1107
|
return exportedPhotos;
|
|
1107
1108
|
}
|
|
1109
|
+
async function exportPhotoWithEngine(photoInfo, engine, options) {
|
|
1110
|
+
switch (engine) {
|
|
1111
|
+
case "applescript": return exportViaAppleScript(photoInfo.uuid);
|
|
1112
|
+
case "file-system": return exportViaFileSystem(photoInfo);
|
|
1113
|
+
case "photos-gui": {
|
|
1114
|
+
const [exportedPath] = await exportViaAppleScriptGui(photoInfo.uuid, options.appleScriptGuiOptions);
|
|
1115
|
+
if (exportedPath === void 0) throw new Error(`Photos GUI export returned no files for photo "${photoInfo.uuid}"`);
|
|
1116
|
+
return exportedPath;
|
|
1117
|
+
}
|
|
1118
|
+
case "swift-photokit": return exportViaSwiftPhotoKit(photoInfo.uuid);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1108
1121
|
/** Also normalizes */
|
|
1109
1122
|
function getImagePathWithFileName(photoInfo, filePath, sluggify = true, fileNameAppendUuidFragment = false, normalizeExtensions = true, namingStrategyPrecedence = [
|
|
1110
1123
|
"title",
|
|
@@ -1114,27 +1127,28 @@ function getImagePathWithFileName(photoInfo, filePath, sluggify = true, fileName
|
|
|
1114
1127
|
const normalizedPath = normalizeExtensions ? normalizeExtension(filePath) : filePath;
|
|
1115
1128
|
const basePath = path.dirname(normalizedPath);
|
|
1116
1129
|
const extension = path.extname(normalizedPath);
|
|
1117
|
-
for (const option of namingStrategyPrecedence)
|
|
1130
|
+
for (const option of namingStrategyPrecedence) {
|
|
1131
|
+
const fileName = getFileNameForStrategy(option, photoInfo, sluggify, fileNameAppendUuidFragment);
|
|
1132
|
+
if (fileName !== void 0) return path.join(basePath, `${fileName}${extension}`);
|
|
1133
|
+
}
|
|
1134
|
+
throw new Error("No valid filename option found, can't name image");
|
|
1135
|
+
}
|
|
1136
|
+
function getFileNameForStrategy(option, photoInfo, sluggify, fileNameAppendUuidFragment) {
|
|
1137
|
+
switch (option) {
|
|
1118
1138
|
case "fileName": {
|
|
1119
1139
|
const nameWithoutExtension = path.basename(photoInfo.original.fileName, path.extname(photoInfo.original.fileName));
|
|
1120
1140
|
const uuidFragment = fileNameAppendUuidFragment ? `-${photoInfo.uuid.slice(-8)}` : "";
|
|
1121
|
-
|
|
1122
|
-
return path.join(basePath, `${processedName}${extension}`);
|
|
1141
|
+
return sluggify ? slug(`${nameWithoutExtension}${uuidFragment}`) : `${nameWithoutExtension}${uuidFragment}`;
|
|
1123
1142
|
}
|
|
1124
1143
|
case "title":
|
|
1125
1144
|
if (isNonEmptyStringAndNotWhitespace(photoInfo.title)) {
|
|
1126
1145
|
const title = sluggify ? slug(photoInfo.title) : photoInfo.title;
|
|
1127
1146
|
const uuidFragment = fileNameAppendUuidFragment ? `-${photoInfo.uuid.slice(-8)}` : "";
|
|
1128
|
-
|
|
1129
|
-
return path.join(basePath, `${processedTitle}${extension}`);
|
|
1147
|
+
return sluggify ? slug(`${title}${uuidFragment}`) : `${title}${uuidFragment}`;
|
|
1130
1148
|
}
|
|
1131
|
-
|
|
1132
|
-
case "uuid":
|
|
1133
|
-
const processedUuid = sluggify ? slug(photoInfo.uuid) : photoInfo.uuid;
|
|
1134
|
-
return path.join(basePath, `${processedUuid}${extension}`);
|
|
1135
|
-
}
|
|
1149
|
+
return;
|
|
1150
|
+
case "uuid": return sluggify ? slug(photoInfo.uuid) : photoInfo.uuid;
|
|
1136
1151
|
}
|
|
1137
|
-
throw new Error("No valid filename option found, can't name image");
|
|
1138
1152
|
}
|
|
1139
1153
|
async function getEngineForPhoto(photoInfo, options) {
|
|
1140
1154
|
const { engineEdited, engineEditedAlpha, engineOriginal, engineOriginalAlpha } = mergeDefaults(options, defaultExportApplePhotoOptions);
|
|
@@ -1178,7 +1192,11 @@ const defaultManageMetadataOptions = {
|
|
|
1178
1192
|
async function manageMetadataBatch(identifiers, targetFilePaths, options, exportOptions) {
|
|
1179
1193
|
const photoInfos = await resolveIdentifiers(identifiers);
|
|
1180
1194
|
if (photoInfos.length !== targetFilePaths.length) throw new Error("Photo infos and target files length mismatch");
|
|
1181
|
-
return Promise.all(photoInfos.map(async (photoInfo, index) =>
|
|
1195
|
+
return Promise.all(photoInfos.map(async (photoInfo, index) => {
|
|
1196
|
+
const targetFilePath = targetFilePaths[index];
|
|
1197
|
+
if (targetFilePath === void 0) throw new Error(`Missing target file path for photo "${photoInfo.uuid}"`);
|
|
1198
|
+
return manageMetadata(photoInfo, targetFilePath, options, exportOptions);
|
|
1199
|
+
}));
|
|
1182
1200
|
}
|
|
1183
1201
|
/**
|
|
1184
1202
|
* Manage metadata...
|
|
@@ -1272,22 +1290,14 @@ async function convertToJpeg(sourceImagePath, destinationDirectory, quality, for
|
|
|
1272
1290
|
case "mozjpeg": {
|
|
1273
1291
|
const tgaTempDirectory = await fse.mkdtemp(path.join(os.tmpdir(), `com.kitschpatrol.aphex.${getSlugFilename(sourceImagePath)}.${engine}.`));
|
|
1274
1292
|
const tgaImagePath = await convertToTga(sourceImagePath, tgaTempDirectory);
|
|
1275
|
-
await
|
|
1276
|
-
"--lossless",
|
|
1293
|
+
await execa("cjpeg", [
|
|
1294
|
+
...jpegQuality === "lossless" ? ["--lossless"] : ["-quality", jpegQuality.toString()],
|
|
1277
1295
|
"-optimize",
|
|
1278
1296
|
"-progressive",
|
|
1279
1297
|
"-outfile",
|
|
1280
1298
|
destinationImagePath,
|
|
1281
1299
|
tgaImagePath
|
|
1282
|
-
])
|
|
1283
|
-
"-quality",
|
|
1284
|
-
jpegQuality.toString(),
|
|
1285
|
-
"-optimize",
|
|
1286
|
-
"-progressive",
|
|
1287
|
-
"-outfile",
|
|
1288
|
-
destinationImagePath,
|
|
1289
|
-
tgaImagePath
|
|
1290
|
-
]));
|
|
1300
|
+
]);
|
|
1291
1301
|
await fse.rm(tgaTempDirectory, { recursive: true });
|
|
1292
1302
|
break;
|
|
1293
1303
|
}
|
|
@@ -1304,7 +1314,6 @@ async function convertToJpeg(sourceImagePath, destinationDirectory, quality, for
|
|
|
1304
1314
|
"--out",
|
|
1305
1315
|
destinationImagePath
|
|
1306
1316
|
]);
|
|
1307
|
-
break;
|
|
1308
1317
|
}
|
|
1309
1318
|
}
|
|
1310
1319
|
return destinationImagePath;
|
|
@@ -1351,7 +1360,7 @@ async function convertToPng(sourceImagePath, destinationDirectory, force = false
|
|
|
1351
1360
|
"--setProperty",
|
|
1352
1361
|
"format",
|
|
1353
1362
|
"png",
|
|
1354
|
-
...restoreColorProfile && restoreColorProfile !== "None" ? [
|
|
1363
|
+
...restoreColorProfile !== void 0 && restoreColorProfile !== "None" ? [
|
|
1355
1364
|
"--setProperty",
|
|
1356
1365
|
"profile",
|
|
1357
1366
|
getPathToColorProfile(restoreColorProfile)
|
|
@@ -1407,7 +1416,7 @@ async function convertToAvif(sourceImagePath, destinationDirectory, quality, for
|
|
|
1407
1416
|
await execa("avifenc", [
|
|
1408
1417
|
...qualityArgs,
|
|
1409
1418
|
"--icc",
|
|
1410
|
-
profile === "None" ?
|
|
1419
|
+
getPathToColorProfile(profile === "None" ? "sRGB IEC61966-2.1" : profile),
|
|
1411
1420
|
"--speed",
|
|
1412
1421
|
"default",
|
|
1413
1422
|
sourceImagePath,
|
|
@@ -1444,9 +1453,7 @@ async function compressImage(sourceImagePath, destinationDirectory, options) {
|
|
|
1444
1453
|
workingImagePath = lookupImageMimeType(sourceImagePath) === "png" ? sourceImagePath : await convertToPng(sourceImagePath, tempDirectory);
|
|
1445
1454
|
workingImagePath = await optimizePng(workingImagePath, tempDirectory);
|
|
1446
1455
|
break;
|
|
1447
|
-
case "webp":
|
|
1448
|
-
workingImagePath = await convertToWebp(sourceImagePath, tempDirectory, "lossless");
|
|
1449
|
-
break;
|
|
1456
|
+
case "webp": workingImagePath = await convertToWebp(sourceImagePath, tempDirectory, "lossless");
|
|
1450
1457
|
}
|
|
1451
1458
|
if (await getSizeBytes(workingImagePath) <= maxFileSizeBytes) {
|
|
1452
1459
|
const destinationImagePath = path.join(destinationDirectory, path.basename(workingImagePath));
|
|
@@ -1465,9 +1472,7 @@ async function compressImage(sourceImagePath, destinationDirectory, options) {
|
|
|
1465
1472
|
case "none":
|
|
1466
1473
|
workingImagePath = sourceImagePath;
|
|
1467
1474
|
break;
|
|
1468
|
-
case "webp":
|
|
1469
|
-
workingImagePath = await convertToWebp(sourceImagePath, tempDirectory, "near-lossless");
|
|
1470
|
-
break;
|
|
1475
|
+
case "webp": workingImagePath = await convertToWebp(sourceImagePath, tempDirectory, "near-lossless");
|
|
1471
1476
|
}
|
|
1472
1477
|
if (await getSizeBytes(workingImagePath) <= maxFileSizeBytes) {
|
|
1473
1478
|
const destinationImagePath = path.join(destinationDirectory, path.basename(workingImagePath));
|
|
@@ -1492,9 +1497,7 @@ async function compressImage(sourceImagePath, destinationDirectory, options) {
|
|
|
1492
1497
|
case "none":
|
|
1493
1498
|
workingImagePath = sourceImagePath;
|
|
1494
1499
|
break;
|
|
1495
|
-
case "webp":
|
|
1496
|
-
workingImagePath = await convertToWebp(sourceImagePath, tempDirectory, lossyQuality, true);
|
|
1497
|
-
break;
|
|
1500
|
+
case "webp": workingImagePath = await convertToWebp(sourceImagePath, tempDirectory, lossyQuality, true);
|
|
1498
1501
|
}
|
|
1499
1502
|
const destinationImagePath = path.join(destinationDirectory, path.basename(workingImagePath));
|
|
1500
1503
|
await fse.copyFile(workingImagePath, destinationImagePath);
|
|
@@ -1509,8 +1512,8 @@ async function compressImage(sourceImagePath, destinationDirectory, options) {
|
|
|
1509
1512
|
}
|
|
1510
1513
|
//#endregion
|
|
1511
1514
|
//#region src/utilities/image/compare.ts
|
|
1512
|
-
const SSIM_REGEX = /All:\s*([\d.]+)
|
|
1513
|
-
const PSNR_REGEX = /average:\s*([\d.]+)
|
|
1515
|
+
const SSIM_REGEX = /All:\s*([\d.]+)/v;
|
|
1516
|
+
const PSNR_REGEX = /average:\s*([\d.]+)/v;
|
|
1514
1517
|
/**
|
|
1515
1518
|
* Get two identically sized PNGs from two images. Uses the smaller image size
|
|
1516
1519
|
* as the target size. Remember to clean up temp
|
|
@@ -1572,9 +1575,9 @@ async function calculateDSSIMInternal(image1, image2) {
|
|
|
1572
1575
|
return -1;
|
|
1573
1576
|
}
|
|
1574
1577
|
const { stdout } = await execa("dssim", [image1, image2]);
|
|
1575
|
-
const [dssimIndex] = stdout.split(" ");
|
|
1578
|
+
const [dssimIndex] = stdout.split(" ", 1);
|
|
1576
1579
|
if (dssimIndex === void 0) throw new Error("DSSIM index not found in dssim output.");
|
|
1577
|
-
return Number
|
|
1580
|
+
return Number(dssimIndex);
|
|
1578
1581
|
}
|
|
1579
1582
|
async function calculateSSIMInternal(image1, image2) {
|
|
1580
1583
|
lookupImageMimeType(image1, ["png"]);
|
|
@@ -1594,8 +1597,8 @@ async function calculateSSIMInternal(image1, image2) {
|
|
|
1594
1597
|
"null",
|
|
1595
1598
|
"-"
|
|
1596
1599
|
]);
|
|
1597
|
-
const
|
|
1598
|
-
return
|
|
1600
|
+
const ssimValue = SSIM_REGEX.exec(result.stderr)?.[1];
|
|
1601
|
+
return ssimValue === void 0 ? 0 : Number(ssimValue);
|
|
1599
1602
|
}
|
|
1600
1603
|
/**
|
|
1601
1604
|
* Calculates the PSNR between two images using ffmpeg.
|
|
@@ -1618,8 +1621,8 @@ async function calculatePSNRInternal(image1, image2) {
|
|
|
1618
1621
|
"null",
|
|
1619
1622
|
"-"
|
|
1620
1623
|
]);
|
|
1621
|
-
const
|
|
1622
|
-
return
|
|
1624
|
+
const psnrValue = PSNR_REGEX.exec(result.stderr)?.[1];
|
|
1625
|
+
return psnrValue === void 0 ? 0 : Number(psnrValue);
|
|
1623
1626
|
}
|
|
1624
1627
|
//#endregion
|
|
1625
1628
|
//#region src/pipeline/image-process.ts
|
|
@@ -1649,8 +1652,9 @@ async function processPhotos(imagePaths, destinationDirectory, options) {
|
|
|
1649
1652
|
const resolvedOptions = mergeDefaults(options, defaultProcessImageOptions);
|
|
1650
1653
|
const tempProcessOutputDirectory = await getTempDirectory("process", "images");
|
|
1651
1654
|
let processImageResults;
|
|
1655
|
+
const [singleImagePath] = imagePaths;
|
|
1652
1656
|
try {
|
|
1653
|
-
if (imagePaths.length === 1) processImageResults = [await processImage(
|
|
1657
|
+
if (singleImagePath !== void 0 && imagePaths.length === 1) processImageResults = [await processImage(singleImagePath, tempProcessOutputDirectory, resolvedOptions)];
|
|
1654
1658
|
else {
|
|
1655
1659
|
const threads = Math.floor(os.availableParallelism() * .5);
|
|
1656
1660
|
log.debug(`Using ${threads} threads for processing`);
|
|
@@ -1664,10 +1668,10 @@ async function processPhotos(imagePaths, destinationDirectory, options) {
|
|
|
1664
1668
|
minThreads: threads
|
|
1665
1669
|
});
|
|
1666
1670
|
try {
|
|
1667
|
-
processImageResults = await Promise.all(imagePaths.map(async (
|
|
1671
|
+
processImageResults = await Promise.all(imagePaths.map(async (imagePath) => piscina.run({
|
|
1668
1672
|
destinationDirectory: tempProcessOutputDirectory,
|
|
1669
1673
|
options: resolvedOptions,
|
|
1670
|
-
sourceImagePath:
|
|
1674
|
+
sourceImagePath: imagePath,
|
|
1671
1675
|
verbose: log.isLevelEnabled("debug")
|
|
1672
1676
|
})));
|
|
1673
1677
|
} finally {
|
|
@@ -1809,7 +1813,7 @@ async function getSyncPlanForImages(identifiers, destinationDirectory, options,
|
|
|
1809
1813
|
}
|
|
1810
1814
|
const sourcePhotoFileBaseName = stripExtension(getImagePathWithFileName(sourcePhotoInfo, "", resolvedExportOptions.exportOptions.fileNameSluggify, resolvedExportOptions.exportOptions.fileNameAppendUuidFragment, resolvedExportOptions.exportOptions.fileNameNormalizeExtensions, resolvedExportOptions.exportOptions.fileNamePrecedence));
|
|
1811
1815
|
const differenceFound = await isDifferent(resolvedOptions.diffStrategies, sourcePhotoInfo, sourcePhotoFileBaseName, matchingDestinationFile.filePath, matchingDestinationFile.tags, resolvedExportOptions, resolvedOptions.forceUpdate);
|
|
1812
|
-
if (differenceFound) {
|
|
1816
|
+
if (differenceFound !== false) {
|
|
1813
1817
|
if (resolvedOptions.deleteTarget) syncResult.toDelete.push(matchingDestinationFile.filePath);
|
|
1814
1818
|
syncResult.plan.push({
|
|
1815
1819
|
diffedVia: differenceFound,
|
|
@@ -1826,7 +1830,7 @@ async function getSyncPlanForImages(identifiers, destinationDirectory, options,
|
|
|
1826
1830
|
status: "unchanged"
|
|
1827
1831
|
});
|
|
1828
1832
|
}
|
|
1829
|
-
if (resolvedOptions.deleteOthers) syncResult.toDelete = [...syncResult.toDelete, ...destinationFiles.filter((file) =>
|
|
1833
|
+
if (resolvedOptions.deleteOthers) syncResult.toDelete = [...syncResult.toDelete, ...destinationFiles.filter((file) => syncResult.plan.every((plan) => plan.matchFilePath !== file.filePath)).map((file) => file.filePath)];
|
|
1830
1834
|
syncResult.toDelete = [...new Set(syncResult.toDelete)];
|
|
1831
1835
|
return syncResult;
|
|
1832
1836
|
}
|
|
@@ -1836,31 +1840,24 @@ async function getSyncPlanForImages(identifiers, destinationDirectory, options,
|
|
|
1836
1840
|
* @returns Undefined if the, or the reason for the difference if found
|
|
1837
1841
|
*/
|
|
1838
1842
|
async function isDifferent(diffStrategies, sourcePhotoInfo, sourcePhotoFileBaseName, matchPhotoFilePath, matchPhotoTags, exportOptions, forceUpdate) {
|
|
1839
|
-
let sourceTags;
|
|
1840
1843
|
if (forceUpdate) return "force-update";
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
break;
|
|
1857
|
-
case "photo-info":
|
|
1858
|
-
if (matchPhotoTags === void 0 || !deepEqual(sourcePhotoInfo, matchPhotoTags.aphexMetadata?.photoInfo)) return diffStrategy;
|
|
1859
|
-
break;
|
|
1860
|
-
case "process-options":
|
|
1861
|
-
if (matchPhotoTags === void 0 || !deepEqual(exportOptions.processOptions, matchPhotoTags.aphexMetadata?.exportOptions.processOptions)) return diffStrategy;
|
|
1862
|
-
break;
|
|
1844
|
+
let sourceTags;
|
|
1845
|
+
async function strategyFindsDifference(diffStrategy) {
|
|
1846
|
+
switch (diffStrategy) {
|
|
1847
|
+
case "exif-tags":
|
|
1848
|
+
sourceTags ??= await getTags(sourcePhotoInfo.original.filePath);
|
|
1849
|
+
return matchPhotoTags === void 0 || sourceTags.credit !== matchPhotoTags.credit || sourceTags.creator !== matchPhotoTags.creator || sourceTags.description !== matchPhotoTags.description || sourceTags.preservedFileName !== matchPhotoTags.preservedFileName || sourceTags.label !== matchPhotoTags.label;
|
|
1850
|
+
case "export-options": return matchPhotoTags === void 0 || !deepEqual(exportOptions.exportOptions, matchPhotoTags.aphexMetadata?.exportOptions.exportOptions);
|
|
1851
|
+
case "file-name": return stripExtension(path.basename(matchPhotoFilePath)) !== sourcePhotoFileBaseName;
|
|
1852
|
+
case "force-update": return true;
|
|
1853
|
+
case "metadata-options":
|
|
1854
|
+
sourceTags ??= await getTags(sourcePhotoInfo.original.filePath);
|
|
1855
|
+
return matchPhotoTags === void 0 || !deepEqual(exportOptions.metadataOptions, matchPhotoTags.aphexMetadata?.exportOptions.metadataOptions);
|
|
1856
|
+
case "photo-info": return matchPhotoTags === void 0 || !deepEqual(sourcePhotoInfo, matchPhotoTags.aphexMetadata?.photoInfo);
|
|
1857
|
+
case "process-options": return matchPhotoTags === void 0 || !deepEqual(exportOptions.processOptions, matchPhotoTags.aphexMetadata?.exportOptions.processOptions);
|
|
1858
|
+
}
|
|
1863
1859
|
}
|
|
1860
|
+
for (const diffStrategy of diffStrategies) if (await strategyFindsDifference(diffStrategy)) return diffStrategy;
|
|
1864
1861
|
return false;
|
|
1865
1862
|
}
|
|
1866
1863
|
//#endregion
|
|
@@ -1953,9 +1950,8 @@ async function exportPhotos(identifiers, destinationDirectory, options) {
|
|
|
1953
1950
|
exportResult.results.exportResult = cleanExportResults(matchingExportResult);
|
|
1954
1951
|
} else log.warn(`No export result found for photo "${exportResult.photoInfo.title ?? exportResult.photoInfo.uuid}"`);
|
|
1955
1952
|
}
|
|
1956
|
-
let processResults;
|
|
1957
1953
|
if (processOptions !== "disabled") {
|
|
1958
|
-
processResults = await processPhotos(exportResults.filter((exportResult) => exportResult.results.syncResult?.status !== "unchanged").map((exportResult) => exportResult.path), destinationDirectory, processOptions);
|
|
1954
|
+
const processResults = await processPhotos(exportResults.filter((exportResult) => exportResult.results.syncResult?.status !== "unchanged").map((exportResult) => exportResult.path), destinationDirectory, processOptions);
|
|
1959
1955
|
await fse.rm(exportDirectory, {
|
|
1960
1956
|
force: true,
|
|
1961
1957
|
recursive: true
|
|
@@ -1969,10 +1965,9 @@ async function exportPhotos(identifiers, destinationDirectory, options) {
|
|
|
1969
1965
|
} else log.warn(`No process result found for photo "${exportResult.photoInfo.title ?? exportResult.photoInfo.uuid}"`);
|
|
1970
1966
|
}
|
|
1971
1967
|
}
|
|
1972
|
-
let metadataResults;
|
|
1973
1968
|
if (metadataOptions !== "disabled") {
|
|
1974
1969
|
const targetPhotos = exportResults.filter((exportResult) => exportResult.results.syncResult?.status !== "unchanged");
|
|
1975
|
-
metadataResults = await manageMetadataBatch(targetPhotos.map((exportResult) => exportResult.photoInfo), targetPhotos.map((exportResult) => exportResult.path), metadataOptions, resolvedOptions);
|
|
1970
|
+
const metadataResults = await manageMetadataBatch(targetPhotos.map((exportResult) => exportResult.photoInfo), targetPhotos.map((exportResult) => exportResult.path), metadataOptions, resolvedOptions);
|
|
1976
1971
|
for (const exportResult of exportResults) {
|
|
1977
1972
|
const matchingMetadataResult = metadataResults.find((photoExportResult) => photoExportResult.photoInfo.uuid === exportResult.photoInfo.uuid);
|
|
1978
1973
|
if (matchingMetadataResult) exportResult.results.metadataResult = cleanMetadataResults(matchingMetadataResult);
|
|
@@ -27,9 +27,8 @@
|
|
|
27
27
|
export default async function worker({ destinationDirectory, options, sourceImagePath, verbose }) {
|
|
28
28
|
// Weird workaround after issues with ESM imports in worker threads and more recent versions of Node / TSX / Piscina / etc?
|
|
29
29
|
// ts-node didn't work
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
)
|
|
30
|
+
const importx = await import('importx')
|
|
31
|
+
const { processImage } = await importx.import('../index', import.meta.url)
|
|
33
32
|
|
|
34
33
|
if (verbose) {
|
|
35
34
|
console.log(`Processing image on worker thread:\n${sourceImagePath}`)
|
package/package.json
CHANGED
|
@@ -1,99 +1,101 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
2
|
+
"name": "@kitschpatrol/aphex",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Apple Photos Export. TypeScript library and CLI tool to export and process images and albums from your macOS Photos.app library.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"apple",
|
|
7
|
+
"apple-photos",
|
|
8
|
+
"photokit",
|
|
9
|
+
"photos",
|
|
10
|
+
"npm-package"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/kitschpatrol/aphex",
|
|
13
|
+
"bugs": "https://github.com/kitschpatrol/aphex/issues",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/kitschpatrol/aphex.git"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"author": {
|
|
20
|
+
"name": "Eric Mika",
|
|
21
|
+
"email": "eric@ericmika.com",
|
|
22
|
+
"url": "https://ericmika.com"
|
|
23
|
+
},
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"type": "module",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"import": "./dist/index.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"main": "./dist/index.js",
|
|
33
|
+
"module": "./dist/index.js",
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"bin": {
|
|
36
|
+
"aphex": "./dist/aphex-swift"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist/*"
|
|
40
|
+
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "pnpm --sequential /^build:/",
|
|
43
|
+
"build:1-lib": "tsdown",
|
|
44
|
+
"build:2-native": "cd native/aphex-swift && ./build.sh",
|
|
45
|
+
"build:3-publint": "publint",
|
|
46
|
+
"clean": "git rm -f pnpm-lock.yaml && git clean -fdX -e !.claude/",
|
|
47
|
+
"fix": "ksc fix && mdat ./native/aphex-swift/readme.md",
|
|
48
|
+
"lint": "ksc lint",
|
|
49
|
+
"release": "bumpp --commit 'Release: %s' && pnpm build && pnpm publish --otp $(op item get npm --otp)",
|
|
50
|
+
"test": "vitest run",
|
|
51
|
+
"test:coverage": "vitest run --coverage"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@sindresorhus/is": "^8.1.0",
|
|
55
|
+
"defu": "^6.1.7",
|
|
56
|
+
"execa": "^10.0.1",
|
|
57
|
+
"exiftool-vendored": "^37.2.0",
|
|
58
|
+
"fast-equals": "^6.0.2",
|
|
59
|
+
"fs-extra": "^11.4.0",
|
|
60
|
+
"github-slugger": "^2.0.0",
|
|
61
|
+
"image-size": "^2.0.2",
|
|
62
|
+
"importx": "^0.5.2",
|
|
63
|
+
"lognow": "^0.8.0",
|
|
64
|
+
"mrmime": "^2.0.1",
|
|
65
|
+
"piscina": "^5.3.1",
|
|
66
|
+
"type-fest": "^5.8.0"
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@arethetypeswrong/core": "^0.18.5",
|
|
70
|
+
"@clack/prompts": "^1.7.0",
|
|
71
|
+
"@kitschpatrol/shared-config": "^8.6.0",
|
|
72
|
+
"@types/fs-extra": "^11.0.4",
|
|
73
|
+
"@types/node": "~24.13.3",
|
|
74
|
+
"@vitest/coverage-v8": "^4.1.11",
|
|
75
|
+
"bumpp": "^12.2.1",
|
|
76
|
+
"globby": "^16.2.4",
|
|
77
|
+
"markdown-table": "^3.0.4",
|
|
78
|
+
"mdat-plugin-cli-help": "^3.1.0",
|
|
79
|
+
"open": "^11.0.1",
|
|
80
|
+
"pretty-bytes": "^7.1.1",
|
|
81
|
+
"pretty-ms": "^9.3.0",
|
|
82
|
+
"publint": "^0.3.24",
|
|
83
|
+
"tsdown": "^0.22.14",
|
|
84
|
+
"tsx": "^4.23.12",
|
|
85
|
+
"typescript": "~6.0.3",
|
|
86
|
+
"vitest": "^4.1.11"
|
|
87
|
+
},
|
|
88
|
+
"packageManager": "pnpm@11.22.0",
|
|
89
|
+
"engines": {
|
|
90
|
+
"node": ">=24.16.0"
|
|
91
|
+
},
|
|
92
|
+
"os": [
|
|
93
|
+
"darwin"
|
|
94
|
+
],
|
|
95
|
+
"cpu": [
|
|
96
|
+
"arm64"
|
|
97
|
+
],
|
|
98
|
+
"publishConfig": {
|
|
99
|
+
"access": "public"
|
|
100
|
+
}
|
|
101
|
+
}
|
package/readme.md
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
<!-- badges -->
|
|
10
10
|
|
|
11
|
-
[](https://npmjs.com/package/@kitschpatrol/aphex)
|
|
12
|
-
[](https://opensource.org/license/mit
|
|
11
|
+
[](https://www.npmjs.com/package/@kitschpatrol/aphex)
|
|
12
|
+
[](https://opensource.org/license/mit)
|
|
13
13
|
|
|
14
14
|
<!-- /badges -->
|
|
15
15
|
|
|
@@ -35,7 +35,7 @@ Aphex is a TypeScript library for exporting images and albums from your local ma
|
|
|
35
35
|
|
|
36
36
|
It makes it simple to export high-quality versions of specific photos or albums from your Photos.app library via a path-like syntax. It can also (optionally) perform image resizing, compression, metadata migration, metadata validation, and color space normalization.
|
|
37
37
|
|
|
38
|
-
I created this library for integration in static website content management asset pipelines, and to attempt to work around some issues related to [exporting high-quality versions of edited images](https://github.com/RhetTbull/osxphotos/discussions/1522) from the Photos.app library. (See the [unplugin-aphex](https://github.com/kitschpatrol/unplugin-aphex) project for an additional layer of integration with various build tools, and the [vscode-aphex](https://github.com/kitschpatrol/vscode-aphex) plugin for hover previews of Aphex links in VS Code.)
|
|
38
|
+
I created this library for integration in static website content management asset pipelines, and to attempt to work around some issues related to [exporting high-quality versions of edited images](https://github.com/RhetTbull/osxphotos/discussions/1522) from the Photos.app library. (See the [unplugin-aphex](https://github.com/kitschpatrol/unplugin-aphex) project for an additional layer of integration with various build tools, and the [vscode-aphex-preview](https://github.com/kitschpatrol/vscode-aphex-preview) plugin for hover previews of Aphex links in VS Code.)
|
|
39
39
|
|
|
40
40
|
This repository also embeds the `aphex-swift` CLI project, which provides a minimal and performant wrapper around parts of Apple's PhotoKit framework. It's not intended for direct use, instead it provides just enough functionality to support the parts of the methods provided by the `aphex` TypeScript library that can only be implemented natively.
|
|
41
41
|
|
|
@@ -77,7 +77,7 @@ npm install @kitschpatrol/aphex
|
|
|
77
77
|
|
|
78
78
|
In most cases, the application invoking aphex should request permission to access your Photos.app library on first use.
|
|
79
79
|
|
|
80
|
-
In certain situations, like executing commands in a VS Code terminal, can [fail to prompt for permission](https://errorism.dev/issues/microsoft-vscode-vscode-terminal-doesnt-allowrequest-permissions-to-access-media-devices). You can work around this through some _highly inadvisable_ direct manipulation of the [permissions database](https://www.rainforestqa.com/blog/macos-tcc-db-deep-dive):
|
|
80
|
+
In certain situations, like executing commands in a VS Code terminal, can [fail to prompt for permission](https://errorism.dev/issues/microsoft-vscode-vscode-terminal-doesnt-allowrequest-permissions-to-access-media-devices). You can work around this through some _highly inadvisable_ direct manipulation of the [permissions database](https://web.archive.org/web/https://www.rainforestqa.com/blog/macos-tcc-db-deep-dive):
|
|
81
81
|
|
|
82
82
|
For example, to grant photo library permission to VS Code:
|
|
83
83
|
|
|
@@ -192,7 +192,7 @@ setLogger(console)
|
|
|
192
192
|
|
|
193
193
|
##### Exporting a photo by filename
|
|
194
194
|
|
|
195
|
-
Let's assume you have an album named "Trip" in your Photos.app library containing a photo with the filename "
|
|
195
|
+
Let's assume you have an album named "Trip" in your Photos.app library containing a photo with the filename "IMG\_1922.jpeg":
|
|
196
196
|
|
|
197
197
|
```ts
|
|
198
198
|
const result = await exportPhoto('Trip/IMG_1922.jpeg', '~/Desktop')
|