@kitschpatrol/aphex 0.1.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/dist/aphex-swift +0 -0
- package/dist/assets/applescript-gui.applescript +236 -0
- package/dist/assets/profiles/AdobeRGB1998.icc +0 -0
- package/dist/assets/profiles/Display P3.icc +0 -0
- package/dist/assets/profiles/ProPhoto.icm +0 -0
- package/dist/assets/profiles/sRGB Profile.icc +0 -0
- package/dist/index.d.ts +373 -0
- package/dist/index.js +1989 -0
- package/dist/workers/process-image-worker.js +36 -0
- package/license.txt +21 -0
- package/package.json +99 -0
- package/readme.md +329 -0
package/dist/aphex-swift
ADDED
|
Binary file
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
-- Exports images from Photos via automated GUI manipulation
|
|
2
|
+
-- It's an unsavory approach, but no other means of automation seems to yield higher quality edited image exports (so far)
|
|
3
|
+
-- Arguments are passed in their order of appearance in the GUI, and expect values exactly matching the UI strings in Photos.app
|
|
4
|
+
-- Accepts an individual photo UUID, or an album UUID (use aphex-swift to look up identifiers / UUIDs)
|
|
5
|
+
on run { uuid, exportDirectory, photoKind, jpegQuality, tiffBitDepth, colorProfile, photoSize, maxSizeType, maxSizeValue, includeMetadata, includeLocation , fileName, sequentialPrefix, subfolderFormat}
|
|
6
|
+
|
|
7
|
+
tell application "System Events"
|
|
8
|
+
set wasRunning to (name of processes) contains "Photos"
|
|
9
|
+
end tell
|
|
10
|
+
|
|
11
|
+
tell application "Photos"
|
|
12
|
+
activate
|
|
13
|
+
|
|
14
|
+
-- Reset selection and view
|
|
15
|
+
tell application "System Events"
|
|
16
|
+
tell process "Photos"
|
|
17
|
+
repeat until exists menu bar 1
|
|
18
|
+
delay 0.25
|
|
19
|
+
end repeat
|
|
20
|
+
click menu item "Library" of menu 1 of menu item "Photos" of menu 1 of menu bar item "View" of menu bar 1
|
|
21
|
+
delay 0.1
|
|
22
|
+
click menu item "Deselect All" of menu 1 of menu bar item "Edit" of menu bar 1
|
|
23
|
+
end tell
|
|
24
|
+
end tell
|
|
25
|
+
|
|
26
|
+
-- Select photo or album for export by UUID
|
|
27
|
+
try
|
|
28
|
+
spotlight media item id (uuid)
|
|
29
|
+
on error
|
|
30
|
+
try
|
|
31
|
+
spotlight album id (uuid)
|
|
32
|
+
end try
|
|
33
|
+
end try
|
|
34
|
+
|
|
35
|
+
if (count of selection) is 0 then
|
|
36
|
+
log "No photos to export"
|
|
37
|
+
if not wasRunning then
|
|
38
|
+
tell application "Photos" to quit
|
|
39
|
+
end if
|
|
40
|
+
return
|
|
41
|
+
end if
|
|
42
|
+
end tell
|
|
43
|
+
|
|
44
|
+
-- Create export destination directory if necessary
|
|
45
|
+
-- try
|
|
46
|
+
-- do shell script "mkdir -p " & quoted form of exportDirectory
|
|
47
|
+
-- on error errMsg number errNum
|
|
48
|
+
-- display alert "Error creating directory" message errMsg & " (Error " & errNum & ")"
|
|
49
|
+
-- end try
|
|
50
|
+
|
|
51
|
+
-- Store initial state for logging
|
|
52
|
+
set initialFiles to filesInExportDirectory(exportDirectory)
|
|
53
|
+
|
|
54
|
+
tell application "System Events"
|
|
55
|
+
tell process "Photos"
|
|
56
|
+
|
|
57
|
+
-- Export command
|
|
58
|
+
repeat until exists menu bar 1
|
|
59
|
+
delay 0.1
|
|
60
|
+
end repeat
|
|
61
|
+
click menu item 1 of menu "Export" of menu item "Export" of menu "File" of menu bar item "File" of menu bar 1
|
|
62
|
+
|
|
63
|
+
repeat until exists sheet 1 of window 1
|
|
64
|
+
delay 0.1
|
|
65
|
+
end repeat
|
|
66
|
+
|
|
67
|
+
-- Set export preferences
|
|
68
|
+
tell sheet 1 of window 1
|
|
69
|
+
tell group "Photos"
|
|
70
|
+
|
|
71
|
+
-- Ensure the disclosure is open
|
|
72
|
+
if not (exists pop up button "Color Profile:") then
|
|
73
|
+
click UI element 3
|
|
74
|
+
delay 0.5
|
|
75
|
+
end if
|
|
76
|
+
|
|
77
|
+
tell pop up button "Photo Kind:"
|
|
78
|
+
if value is not photoKind then
|
|
79
|
+
click
|
|
80
|
+
click menu item photoKind of menu 1
|
|
81
|
+
delay 0.5
|
|
82
|
+
end if
|
|
83
|
+
end tell
|
|
84
|
+
|
|
85
|
+
if photoKind is "JPEG" then
|
|
86
|
+
tell pop up button "JPEG Quality:"
|
|
87
|
+
if value is not jpegQuality then
|
|
88
|
+
click
|
|
89
|
+
click menu item jpegQuality of menu 1
|
|
90
|
+
delay 0.5
|
|
91
|
+
end if
|
|
92
|
+
end tell
|
|
93
|
+
end if
|
|
94
|
+
|
|
95
|
+
if photoKind is "TIFF" then
|
|
96
|
+
tell checkbox "16 Bit"
|
|
97
|
+
if (value as boolean) is not (tiffBitDepth is 16) then
|
|
98
|
+
click
|
|
99
|
+
end if
|
|
100
|
+
end tell
|
|
101
|
+
end if
|
|
102
|
+
|
|
103
|
+
tell pop up button "Color Profile:"
|
|
104
|
+
if value is not colorProfile then
|
|
105
|
+
click
|
|
106
|
+
click menu item colorProfile of menu 1
|
|
107
|
+
delay 0.5
|
|
108
|
+
end if
|
|
109
|
+
end tell
|
|
110
|
+
|
|
111
|
+
tell pop up button "Size:"
|
|
112
|
+
if value is not photoSize then
|
|
113
|
+
click
|
|
114
|
+
click menu item photoSize of menu 1
|
|
115
|
+
delay 0.5
|
|
116
|
+
end if
|
|
117
|
+
end tell
|
|
118
|
+
|
|
119
|
+
if photoSize is "Custom" then
|
|
120
|
+
tell group 1
|
|
121
|
+
|
|
122
|
+
tell pop up button "Max"
|
|
123
|
+
if value is not maxSizeType then
|
|
124
|
+
click
|
|
125
|
+
click menu item maxSizeType of menu 1
|
|
126
|
+
delay 0.5
|
|
127
|
+
end if
|
|
128
|
+
end tell
|
|
129
|
+
|
|
130
|
+
tell text field 1
|
|
131
|
+
if value is not maxSizeValue then
|
|
132
|
+
set value of attribute "AXFocused" to true
|
|
133
|
+
set value of attribute "AXValue" to maxSizeValue as string
|
|
134
|
+
perform action "AXConfirm"
|
|
135
|
+
end if
|
|
136
|
+
end tell
|
|
137
|
+
|
|
138
|
+
end tell
|
|
139
|
+
end if
|
|
140
|
+
|
|
141
|
+
end tell
|
|
142
|
+
|
|
143
|
+
tell group "Info"
|
|
144
|
+
tell checkbox "Title, Keywords, and Caption"
|
|
145
|
+
if (value as boolean) is not (includeMetadata as boolean) then
|
|
146
|
+
click
|
|
147
|
+
end if
|
|
148
|
+
end tell
|
|
149
|
+
tell checkbox "Location Information"
|
|
150
|
+
if (value as boolean) is not (includeLocation as boolean) then
|
|
151
|
+
click
|
|
152
|
+
end if
|
|
153
|
+
end tell
|
|
154
|
+
end tell
|
|
155
|
+
|
|
156
|
+
tell group "File Naming"
|
|
157
|
+
tell pop up button "File Name:"
|
|
158
|
+
if value is not fileName then
|
|
159
|
+
click
|
|
160
|
+
click menu item fileName of menu 1
|
|
161
|
+
delay 0.5
|
|
162
|
+
end if
|
|
163
|
+
end tell
|
|
164
|
+
if fileName is "Sequential" then
|
|
165
|
+
-- why not "Sequential Prefix:"?
|
|
166
|
+
tell text field 1
|
|
167
|
+
if value is not sequentialPrefix then
|
|
168
|
+
set value of attribute "AXFocused" to true
|
|
169
|
+
set value of attribute "AXValue" to sequentialPrefix
|
|
170
|
+
perform action "AXConfirm"
|
|
171
|
+
end if
|
|
172
|
+
end tell
|
|
173
|
+
end if
|
|
174
|
+
tell pop up button "Subfolder Format:"
|
|
175
|
+
if value is not subfolderFormat then
|
|
176
|
+
click
|
|
177
|
+
click menu item subfolderFormat of menu 1
|
|
178
|
+
delay 0.5
|
|
179
|
+
end if
|
|
180
|
+
end tell
|
|
181
|
+
end tell
|
|
182
|
+
|
|
183
|
+
delay 0.5
|
|
184
|
+
click button "Export"
|
|
185
|
+
|
|
186
|
+
end tell
|
|
187
|
+
|
|
188
|
+
-- Set export location
|
|
189
|
+
repeat until exists sheet 1 of window 1
|
|
190
|
+
delay 0.1
|
|
191
|
+
end repeat
|
|
192
|
+
|
|
193
|
+
tell sheet 1 of window 1
|
|
194
|
+
keystroke "g" using {command down, shift down}
|
|
195
|
+
repeat until exists sheet 1
|
|
196
|
+
delay 0.1
|
|
197
|
+
end repeat
|
|
198
|
+
tell sheet 1
|
|
199
|
+
keystroke exportDirectory
|
|
200
|
+
key code 76
|
|
201
|
+
end tell
|
|
202
|
+
|
|
203
|
+
-- Getting stuck here...
|
|
204
|
+
delay 1
|
|
205
|
+
click button "Export"
|
|
206
|
+
end tell
|
|
207
|
+
|
|
208
|
+
-- Wait for the export to finish, but allow a few second for the progress indicator to appear
|
|
209
|
+
set startTime to (current date)
|
|
210
|
+
repeat until (not (exists progress indicator 1 of group 2 of toolbar 1 of window 1)) and ((current date) - startTime) > 2
|
|
211
|
+
delay 0.25
|
|
212
|
+
end repeat
|
|
213
|
+
|
|
214
|
+
end tell
|
|
215
|
+
end tell
|
|
216
|
+
|
|
217
|
+
-- Clean up
|
|
218
|
+
if not wasRunning then
|
|
219
|
+
tell application "Photos" to quit
|
|
220
|
+
end if
|
|
221
|
+
|
|
222
|
+
-- Log the exported files
|
|
223
|
+
set currentFiles to filesInExportDirectory(exportDirectory)
|
|
224
|
+
repeat with aFile in currentFiles
|
|
225
|
+
if (initialFiles does not contain aFile) then
|
|
226
|
+
set end of initialFiles to aFile
|
|
227
|
+
log aFile
|
|
228
|
+
end if
|
|
229
|
+
end repeat
|
|
230
|
+
|
|
231
|
+
end run
|
|
232
|
+
|
|
233
|
+
-- Handler to get list of files excluding hidden ones
|
|
234
|
+
on filesInExportDirectory(exportDirectory)
|
|
235
|
+
return paragraphs of (do shell script "find " & quoted form of exportDirectory & " -type f \\! -name \".*\"")
|
|
236
|
+
end filesInExportDirectory
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { ExifTool } from "exiftool-vendored";
|
|
2
|
+
import { ILogBasic, ILogLayer } from "lognow";
|
|
3
|
+
import { OmitDeep, PartialDeep, Simplify } from "type-fest";
|
|
4
|
+
|
|
5
|
+
//#region src/aphex-swift/cli-bridge.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Start an interactive session with aphex-swift.
|
|
8
|
+
* Commands will be sent to this persistent process instead of spawning new ones.
|
|
9
|
+
* @throws {Error} If the session is already active or fails to start
|
|
10
|
+
*/
|
|
11
|
+
declare function interactiveSessionStart(): Promise<void>;
|
|
12
|
+
/**
|
|
13
|
+
* Stop the interactive session if one is active.
|
|
14
|
+
*/
|
|
15
|
+
declare function interactiveSessionStop(): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* TypeScript type definition for ResourceInfo from the Swift implementation
|
|
18
|
+
*/
|
|
19
|
+
type ResourceInfo = {
|
|
20
|
+
contentType: string;
|
|
21
|
+
fileName: string;
|
|
22
|
+
filePath: string;
|
|
23
|
+
fileSize: number;
|
|
24
|
+
height: number;
|
|
25
|
+
width: number;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* TypeScript type definition for the JSON representation of a PHAsset
|
|
29
|
+
* from the iOS Photos framework (CodablePHAsset)
|
|
30
|
+
*/
|
|
31
|
+
type PhotoInfo = {
|
|
32
|
+
dateCreated: Date;
|
|
33
|
+
dateModified: Date;
|
|
34
|
+
edited?: ResourceInfo;
|
|
35
|
+
favorite: boolean;
|
|
36
|
+
hidden: boolean;
|
|
37
|
+
original: ResourceInfo;
|
|
38
|
+
title?: string;
|
|
39
|
+
uuid: string;
|
|
40
|
+
};
|
|
41
|
+
type AlbumInfo = {
|
|
42
|
+
dateEnd?: Date;
|
|
43
|
+
dateStart?: Date;
|
|
44
|
+
estimatedAssetCount: number;
|
|
45
|
+
path: string;
|
|
46
|
+
subtype: number;
|
|
47
|
+
title: string;
|
|
48
|
+
type: number;
|
|
49
|
+
uuid: string;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Get photo asset information for given identifiers (ID, filename, album name, or photo path)
|
|
53
|
+
* @throws {Error} If the command fails
|
|
54
|
+
*/
|
|
55
|
+
declare function aphexPhotoInfo(identifiers: string | string[], caseSensitive?: boolean): Promise<PhotoInfo[]>;
|
|
56
|
+
/**
|
|
57
|
+
* Get album info
|
|
58
|
+
* @throws {Error} If the command fails
|
|
59
|
+
*/
|
|
60
|
+
declare function aphexAlbumInfo(identifiers: string | string[], caseSensitive?: boolean): Promise<AlbumInfo[]>;
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region src/utilities/image/mime.d.ts
|
|
63
|
+
declare const IMAGE_MIME_TYPES: readonly ["arw", "avif", "bmp", "cr2", "cr3", "crw", "tga", "dng", "gif", "heic", "heif", "jpeg", "nef", "pef", "png", "psd", "svg+xml", "tiff", "webp"];
|
|
64
|
+
type ImageMimeType = (typeof IMAGE_MIME_TYPES)[number];
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region src/pipeline/engines/applescript-gui.d.ts
|
|
67
|
+
/**
|
|
68
|
+
* Mirrors the export UI in Note that note that some options are contingent on others.
|
|
69
|
+
*
|
|
70
|
+
* The AppleScript implementation is smart about only "clicking" the necessary
|
|
71
|
+
* options in the GUI.
|
|
72
|
+
*/
|
|
73
|
+
type ExportViaAppleScriptGuiOptions = {
|
|
74
|
+
/** "Color Profile" Drop Down */colorProfile: 'AdobeRGB' | 'Display P3' | 'Most Compatible' | 'Original' | 'sRGB'; /** "File Name" Drop Down */
|
|
75
|
+
fileName: 'Album Name With Number' | 'Sequential' | 'Use File Name' | 'Use Title'; /** "Include Location Information" Checkbox */
|
|
76
|
+
includeLocation: boolean; /** "Include Title, Keywords, and Caption" Checkbox */
|
|
77
|
+
includeMetadata: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* "JPEG Quality" Drop Down
|
|
80
|
+
*
|
|
81
|
+
* Only applies if `photoKind` is `JPEG`
|
|
82
|
+
*/
|
|
83
|
+
jpegQuality: 'High' | 'Low (smallest Size)' | 'Maximum' | 'Medium';
|
|
84
|
+
/**
|
|
85
|
+
* "Size Max" Drop Down
|
|
86
|
+
*
|
|
87
|
+
* Only applies if `photoSize` is `Custom`
|
|
88
|
+
*/
|
|
89
|
+
maxSizeType: 'Dimension' | 'Height' | 'Width';
|
|
90
|
+
/**
|
|
91
|
+
* "Size Max of" text field
|
|
92
|
+
*
|
|
93
|
+
* Only applies if `photoSize` is `Custom`
|
|
94
|
+
*/
|
|
95
|
+
maxSizeValue: number; /** "Photo Kind" Drop Down */
|
|
96
|
+
photoKind: 'HEIC' | 'JPEG' | 'PNG' | 'TIFF'; /** "Size" Drop Down */
|
|
97
|
+
photoSize: 'Custom' | 'Full Size' | 'Large' | 'Medium' | 'Small';
|
|
98
|
+
/**
|
|
99
|
+
* "Sequential Prefix" Text Field
|
|
100
|
+
*
|
|
101
|
+
* Only applies if `fileName` is `Sequential`
|
|
102
|
+
*/
|
|
103
|
+
sequentialPrefix: string; /** "Subfolder Format" Drop Down */
|
|
104
|
+
subfolderFormat: 'Moment Name' | 'None';
|
|
105
|
+
/**
|
|
106
|
+
* "16 Bit" Checkbox
|
|
107
|
+
*
|
|
108
|
+
* Only applies if `photoKind` is `TIFF`
|
|
109
|
+
*/
|
|
110
|
+
tiffBitDepth: 8 | 16;
|
|
111
|
+
};
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/pipeline/image-export.d.ts
|
|
114
|
+
type ExportEngine = 'applescript' | 'file-system' | 'photos-gui' | 'swift-photokit';
|
|
115
|
+
type ExportEngineOptions = ExportEngine | Partial<Record<ImageMimeType, ExportEngine>>;
|
|
116
|
+
type ExportApplePhotoOptions = {
|
|
117
|
+
appleScriptGuiOptions: ExportViaAppleScriptGuiOptions;
|
|
118
|
+
engineEdited: ExportEngineOptions;
|
|
119
|
+
engineEditedAlpha: ExportEngineOptions;
|
|
120
|
+
engineOriginal: ExportEngineOptions;
|
|
121
|
+
engineOriginalAlpha: ExportEngineOptions;
|
|
122
|
+
/**
|
|
123
|
+
* Append a few digits from the image's local identifier in Photos.app, useful to avoid name collisions if exporting multiple album-worth of photos.
|
|
124
|
+
* Only applies to `FileNameOptions` `title` or `uuid`. This is NOT the same as a content hash.
|
|
125
|
+
*/
|
|
126
|
+
fileNameAppendUuidFragment: boolean;
|
|
127
|
+
fileNameNormalizeExtensions: boolean;
|
|
128
|
+
fileNamePrecedence: FileNameOptions[];
|
|
129
|
+
fileNameSluggify: boolean;
|
|
130
|
+
};
|
|
131
|
+
type ExportApplePhotoResult = {
|
|
132
|
+
exportEngine: ExportEngine;
|
|
133
|
+
exportOptions: ExportApplePhotoOptions;
|
|
134
|
+
path: string;
|
|
135
|
+
photoInfo: PhotoInfo;
|
|
136
|
+
};
|
|
137
|
+
type FileNameOptions = 'fileName' | 'title' | 'uuid';
|
|
138
|
+
/** Also normalizes */
|
|
139
|
+
//#endregion
|
|
140
|
+
//#region src/utilities/image/tags.d.ts
|
|
141
|
+
declare const VALID_LABELS: readonly ["animation", "diagram", "illustration", "image", "photo", "render", "screenshot", "video"];
|
|
142
|
+
type Label = (typeof VALID_LABELS)[number];
|
|
143
|
+
type ValidateTagsResult = {
|
|
144
|
+
issues: string[];
|
|
145
|
+
valid: boolean;
|
|
146
|
+
};
|
|
147
|
+
type AphexMetadata = {
|
|
148
|
+
exportOptions: ExportOptions;
|
|
149
|
+
photoInfo: PhotoInfo;
|
|
150
|
+
};
|
|
151
|
+
type ImageTags = {
|
|
152
|
+
aphexMetadata?: AphexMetadata | undefined; /** Human Name */
|
|
153
|
+
creator?: string | undefined; /** Organization */
|
|
154
|
+
credit?: string | undefined; /** Description, used as alt text in HTML content */
|
|
155
|
+
description?: string | undefined;
|
|
156
|
+
/**
|
|
157
|
+
Image Type, e.g. 'animation', 'diagram', 'illustration', 'screenshot',
|
|
158
|
+
'image', 'photo', 'render', 'video'
|
|
159
|
+
*/
|
|
160
|
+
label?: Label | undefined;
|
|
161
|
+
preservedFileName?: string | undefined;
|
|
162
|
+
};
|
|
163
|
+
//#endregion
|
|
164
|
+
//#region src/pipeline/image-metadata.d.ts
|
|
165
|
+
type ManageMetadataOptions = {
|
|
166
|
+
syncToEdited: boolean;
|
|
167
|
+
syncToTarget: boolean;
|
|
168
|
+
tagsToSync: Array<keyof ImageTags>;
|
|
169
|
+
validate: boolean;
|
|
170
|
+
writeAphexMetadata: boolean;
|
|
171
|
+
};
|
|
172
|
+
type ManageMetadataResult = {
|
|
173
|
+
photoInfo: PhotoInfo;
|
|
174
|
+
tagsToEdited: Array<keyof ImageTags> | undefined;
|
|
175
|
+
tagsToTarget: Array<keyof ImageTags> | undefined;
|
|
176
|
+
validationResult: undefined | ValidateTagsResult;
|
|
177
|
+
};
|
|
178
|
+
//#endregion
|
|
179
|
+
//#region src/utilities/image/color.d.ts
|
|
180
|
+
declare const validColorProfiles: readonly ["Adobe RGB (1998)", "Apple Wide Color Sharing Profile", "Display P3", "None", "ProPhoto RGB", "sRGB IEC61966-2.1", "Unsupported"];
|
|
181
|
+
type ColorProfile = (typeof validColorProfiles)[number];
|
|
182
|
+
/**
|
|
183
|
+
* Normalize the color profile of an image
|
|
184
|
+
*/
|
|
185
|
+
declare function normalizeColorProfile(imagePath: string, preserveColorProfiles?: ColorProfile[], defaultColorProfile?: ColorProfile): Promise<'assigned' | 'converted' | 'no-action'>;
|
|
186
|
+
//#endregion
|
|
187
|
+
//#region src/utilities/image/convert.d.ts
|
|
188
|
+
type LosslessFormat = 'avif' | 'none' | 'png' | 'webp';
|
|
189
|
+
type NearLosslessFormat = 'none' | 'webp';
|
|
190
|
+
type LossyFormat = 'avif' | 'jpeg' | 'none' | 'webp';
|
|
191
|
+
/**
|
|
192
|
+
* Configuration options for image compression.
|
|
193
|
+
*
|
|
194
|
+
* The compression process follows a three-tier approach:
|
|
195
|
+
* 1. First attempts lossless compression if enabled
|
|
196
|
+
* 2. Falls back to near-lossless compression if lossless exceeds size limit
|
|
197
|
+
* 3. Finally uses lossy compression if other methods fail to meet size requirements
|
|
198
|
+
*
|
|
199
|
+
* Set format to 'none' to disable that compression tier.
|
|
200
|
+
*/
|
|
201
|
+
type CompressImageOptions = {
|
|
202
|
+
/**
|
|
203
|
+
* Force compression even if the original image is already within the size limit.
|
|
204
|
+
* When false, images smaller than maxFileSizeBytes are copied without modification.
|
|
205
|
+
*/
|
|
206
|
+
forceCompression: boolean;
|
|
207
|
+
/**
|
|
208
|
+
* Format to use for lossless compression (first attempt).
|
|
209
|
+
* - 'avif': Best compression but slower encoding
|
|
210
|
+
* - 'png': Good compatibility, optimized with oxipng
|
|
211
|
+
* - 'webp': Good balance of compression and speed
|
|
212
|
+
* - 'none': Skip lossless compression
|
|
213
|
+
*/
|
|
214
|
+
losslessFormat: LosslessFormat;
|
|
215
|
+
/**
|
|
216
|
+
* Format to use for lossy compression (final fallback).
|
|
217
|
+
* - 'avif': Best compression but very slow encoding
|
|
218
|
+
* - 'jpeg': Good compatibility, uses mozjpeg encoder
|
|
219
|
+
* - 'webp': Good balance of compression and speed
|
|
220
|
+
* - 'none': Skip lossy compression (may result in oversized files)
|
|
221
|
+
*/
|
|
222
|
+
lossyFormat: LossyFormat;
|
|
223
|
+
/**
|
|
224
|
+
* Quality level for lossy compression (0-1 scale).
|
|
225
|
+
* - 0: Lowest quality, smallest file size
|
|
226
|
+
* - 1: Highest quality, largest file size
|
|
227
|
+
* - Recommended range: 0.85-0.96 for high-quality images
|
|
228
|
+
*/
|
|
229
|
+
lossyQuality: number;
|
|
230
|
+
/**
|
|
231
|
+
* Maximum allowed file size in bytes.
|
|
232
|
+
* Images exceeding this size will be compressed using the configured formats.
|
|
233
|
+
* The compression process stops when this target is met or all options are exhausted.
|
|
234
|
+
*/
|
|
235
|
+
maxFileSizeBytes: number;
|
|
236
|
+
/**
|
|
237
|
+
* Format to use for near-lossless compression (second attempt).
|
|
238
|
+
* - 'webp': Only format currently supporting near-lossless mode
|
|
239
|
+
* - 'none': Skip near-lossless compression
|
|
240
|
+
*/
|
|
241
|
+
nearLosslessFormat: NearLosslessFormat;
|
|
242
|
+
};
|
|
243
|
+
/**
|
|
244
|
+
* Compress an image to a target size
|
|
245
|
+
*/
|
|
246
|
+
declare function compressImage(sourceImagePath: string, destinationDirectory: string, options: CompressImageOptions): Promise<{
|
|
247
|
+
compression: 'lossless' | 'lossy' | 'near-lossless' | 'none';
|
|
248
|
+
path: string;
|
|
249
|
+
}>;
|
|
250
|
+
//#endregion
|
|
251
|
+
//#region src/utilities/image/image.d.ts
|
|
252
|
+
type ImageInfo = {
|
|
253
|
+
alpha: boolean;
|
|
254
|
+
colorProfile: string;
|
|
255
|
+
dimensionsPixels: {
|
|
256
|
+
width: number;
|
|
257
|
+
height: number;
|
|
258
|
+
};
|
|
259
|
+
mime: ImageMimeType;
|
|
260
|
+
path: string;
|
|
261
|
+
sizeBytes: number;
|
|
262
|
+
tags: ImageTags;
|
|
263
|
+
};
|
|
264
|
+
//#endregion
|
|
265
|
+
//#region src/pipeline/image-process.d.ts
|
|
266
|
+
type ProcessImageOptions = CompressImageOptions & {
|
|
267
|
+
/** Fallback color profile applied when source profile is not in preserve list */defaultColorProfile: ColorProfile; /** Calculate SSIM/PSNR/DSSIM metrics comparing original vs processed images */
|
|
268
|
+
logSimilarity: boolean; /** Lossless format used specifically for images with alpha channels */
|
|
269
|
+
losslessFormatAlpha: LosslessFormat; /** Lossy format used specifically for images with alpha channels */
|
|
270
|
+
lossyFormatAlpha: LossyFormat; /** Maximum pixel dimensions before triggering resize (width OR height) */
|
|
271
|
+
maxDimensionsPixels: {
|
|
272
|
+
width: number;
|
|
273
|
+
height: number;
|
|
274
|
+
}; /** Near-lossless format used specifically for images with alpha channels */
|
|
275
|
+
nearLosslessFormatAlpha: NearLosslessFormat; /** Image formats that bypass conversion and compression entirely */
|
|
276
|
+
passthroughFormats: ImageMimeType[]; /** Color profiles that are kept unchanged instead of being normalized */
|
|
277
|
+
preserveColorProfiles: ColorProfile[];
|
|
278
|
+
};
|
|
279
|
+
type ProcessImageResult = {
|
|
280
|
+
input: ImageInfo;
|
|
281
|
+
output: ImageInfo;
|
|
282
|
+
path: string;
|
|
283
|
+
report: {
|
|
284
|
+
color: Awaited<ReturnType<typeof normalizeColorProfile>>;
|
|
285
|
+
compression: Awaited<ReturnType<typeof compressImage>>['compression'];
|
|
286
|
+
date: Date;
|
|
287
|
+
durationMs: number;
|
|
288
|
+
similarity?: {
|
|
289
|
+
dssim: number;
|
|
290
|
+
psnr: number;
|
|
291
|
+
ssim: number;
|
|
292
|
+
};
|
|
293
|
+
};
|
|
294
|
+
};
|
|
295
|
+
/**
|
|
296
|
+
* Process an image
|
|
297
|
+
*
|
|
298
|
+
* Run in parallel through a worker for album processing
|
|
299
|
+
*/
|
|
300
|
+
declare function processImage(sourceImagePath: string, destinationDirectory: string, options: ProcessImageOptions): Promise<ProcessImageResult>;
|
|
301
|
+
//#endregion
|
|
302
|
+
//#region src/pipeline/image-sync.d.ts
|
|
303
|
+
type DiffStrategy = 'exif-tags' | 'export-options' | 'file-name' | 'force-update' | 'metadata-options' | 'photo-info' | 'process-options';
|
|
304
|
+
type SyncOptions = {
|
|
305
|
+
deleteOthers: boolean;
|
|
306
|
+
deleteTarget: boolean;
|
|
307
|
+
diffStrategies: DiffStrategy[];
|
|
308
|
+
forceUpdate: boolean;
|
|
309
|
+
};
|
|
310
|
+
type SyncResult = {
|
|
311
|
+
plan: Array<{
|
|
312
|
+
diffedVia: DiffStrategy | undefined;
|
|
313
|
+
matchFilePath: string | undefined;
|
|
314
|
+
photoInfo: PhotoInfo;
|
|
315
|
+
status: 'changed' | 'new' | 'unchanged';
|
|
316
|
+
}>;
|
|
317
|
+
toDelete: string[];
|
|
318
|
+
};
|
|
319
|
+
//#endregion
|
|
320
|
+
//#region src/utilities/exiftool.d.ts
|
|
321
|
+
/**
|
|
322
|
+
* End the exiftool process gracefully
|
|
323
|
+
*
|
|
324
|
+
* This should be called when you're done using exiftool to prevent
|
|
325
|
+
* hanging processes. It's safe to call multiple times.
|
|
326
|
+
*/
|
|
327
|
+
declare function endExiftool(): Promise<void>;
|
|
328
|
+
//#endregion
|
|
329
|
+
//#region src/utilities/log.d.ts
|
|
330
|
+
/**
|
|
331
|
+
* Set the logger instance for the module.
|
|
332
|
+
* Export this for library consumers to inject their own logger.
|
|
333
|
+
* @param logger - Accepts either a LogLayer instance or a Console- or Stream-like log target
|
|
334
|
+
*/
|
|
335
|
+
declare function setLogger(logger?: ILogBasic | ILogLayer<unknown>): void;
|
|
336
|
+
//#endregion
|
|
337
|
+
//#region src/index.d.ts
|
|
338
|
+
/**
|
|
339
|
+
* Helper for deep merging ExportOptions object against library defaults.
|
|
340
|
+
* Exported for unplugin-aphex.
|
|
341
|
+
*/
|
|
342
|
+
declare function mergeDefaultExportOptions(options: PartialDeep<ExportOptions> | undefined): ExportOptions;
|
|
343
|
+
type ExportOptions = {
|
|
344
|
+
exportOptions: ExportApplePhotoOptions;
|
|
345
|
+
metadataOptions: 'disabled' | ManageMetadataOptions;
|
|
346
|
+
processOptions: 'disabled' | ProcessImageOptions;
|
|
347
|
+
syncOptions: 'disabled' | SyncOptions;
|
|
348
|
+
};
|
|
349
|
+
declare const defaultSyncOptions: SyncOptions;
|
|
350
|
+
declare const defaultExportOptions: ExportOptions;
|
|
351
|
+
/** Some fields omitted for relevance... */
|
|
352
|
+
type ExportResults = {
|
|
353
|
+
exportResult: Simplify<Omit<ExportApplePhotoResult, 'exportOptions' | 'path' | 'photoInfo'>> | undefined;
|
|
354
|
+
metadataResult: Simplify<Omit<ManageMetadataResult, 'photoInfo'>> | undefined;
|
|
355
|
+
processResult: Simplify<OmitDeep<ProcessImageResult, 'input.path' | 'output.path' | 'path'>> | undefined;
|
|
356
|
+
syncResult: Simplify<Omit<SyncResult['plan'][number], 'photoInfo'>> | undefined;
|
|
357
|
+
};
|
|
358
|
+
type ExportResult = {
|
|
359
|
+
options: ExportOptions;
|
|
360
|
+
path: string;
|
|
361
|
+
photoInfo: PhotoInfo;
|
|
362
|
+
results: ExportResults;
|
|
363
|
+
};
|
|
364
|
+
/**
|
|
365
|
+
* Export a single photo
|
|
366
|
+
*/
|
|
367
|
+
declare function exportPhoto(identifier: PhotoInfo | string, destinationDirectory: string, options?: PartialDeep<ExportOptions>): Promise<ExportResult>;
|
|
368
|
+
/**
|
|
369
|
+
* Export photos
|
|
370
|
+
*/
|
|
371
|
+
declare function exportPhotos(identifiers: Array<AlbumInfo | PhotoInfo | string>, destinationDirectory: string, options?: PartialDeep<ExportOptions>): Promise<ExportResult[]>;
|
|
372
|
+
//#endregion
|
|
373
|
+
export { ExportOptions, ExportResult, IMAGE_MIME_TYPES, type ImageMimeType, defaultExportOptions, defaultSyncOptions, endExiftool, exportPhoto, exportPhotos, aphexAlbumInfo as getAlbumInfo, aphexPhotoInfo as getPhotoInfo, interactiveSessionStart, interactiveSessionStop, mergeDefaultExportOptions, processImage, setLogger };
|