Apple Photos Export. TypeScript library and CLI tool to export and process images and albums from your macOS Photos.app library.
Warning
Aphex is still under development. It should not be considered suitable for general use until a 1.0 release.
This project is open-sourced as a curiosity and for my own convenience, but I suspect it's too niche to be of wide interest or utility. I don't currently plan to spend time adding features for more general use-cases.
It won't work in CI pipelines. It can only target the system's active Photos.app library. It requires an Apple Silicon (arm64) Mac. It has not been tested against iCloud Photos libraries, and assets kept only in the cloud (via "Optimize Mac Storage") are unlikely to export successfully.
If you are looking for a proper Apple Photos.app mass-export or backup solution, I highly recommend using osxphotos instead.
Aphex is a TypeScript library for exporting images and albums from your local macOS Photos.app library via a Node-compatible runtime.
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.
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 from the Photos.app library. (See the unplugin-aphex project for an additional layer of integration with various build tools, and the vscode-aphex plugin for hover previews of Aphex links in VS Code.)
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.
The name "Aphex" is a concatenation of Apple PHotos EXport.
Requires an Apple Silicon (arm64) Mac with Photos.app installed and Node 22.18.0 or newer. No Intel (x86_64) build of the bundled native binary is provided.
Full image processing functionality also requires a number of command-line tools available via Homebrew:
| Tool | Used for |
|---|---|
libavif |
AVIF encoding |
mozjpeg |
JPEG encoding |
webp |
WebP encoding (lossy, lossless, and near-lossless) |
oxipng |
PNG optimization |
guetzli |
High-quality JPEG recompression (used when size budgets require) |
imagemagick |
General-purpose conversion and color profile handling |
ffmpeg |
Media probing used by some conversion paths |
dssim |
Perceptual similarity metrics (only needed if logSimilarity is enabled) |
If you skip image processing (processOptions: 'disabled') you can omit the Homebrew dependencies.
brew install libavif mozjpeg imagemagick webp dssim ffmpeg guetzli oxipng
npm install @kitschpatrol/aphexIn most cases, the application invoking aphex should request permission to access your Photos.app library on first use.
In certain situations, like executing commands in a VS Code terminal, can fail to prompt for permission. You can work around this through some highly inadvisable direct manipulation of the permissions database:
For example, to grant photo library permission to VS Code:
sqlite3 ~/Library/Application\ Support/com.apple.TCC/TCC.db "INSERT OR REPLACE INTO access (service, client, client_type, auth_value, auth_reason, auth_version, indirect_object_identifier) VALUES ('kTCCServicePhotos', 'com.microsoft.VSCode', 0, 2, 3, 1, 'UNUSED');"To revoke photo library permissions:
tccutil reset Photos com.microsoft.VSCodeAdapt the application identifiers (e.g. com.microsoft.VSCode) as required to suit your situation.
To get an application's bundle identifier:
osascript -e 'id of app "Cursor"'Aphex tries to be generous in what it accepts as valid image identifiers.
It imagines the contents of your Photos.app library as a hierarchical file system of folders, albums, and photos, where the "name" of each photo is either its filename or, if set, its title.
This lets you access specific photos in specific albums via a path-like syntax.
Be warned that exporting unedited images is very fast, but exporting edited images can be very (very) slow, since an alternate AppleScript-based export strategy is enabled by default to ensure maximum quality.
By default, different export strategies are used for different types of images. The default configuration prioritizes image quality over export speed.
Aphex provides two main export functions, one for individual photos, and one that can take an arbitrary number of identifiers / albums:
function exportPhoto(
identifier: PhotoInfo | string,
destinationDirectory: string,
options?: PartialDeep<ExportOptions>,
): Promise<ExportResult>function exportPhotos(
identifiers: Array<AlbumInfo | PhotoInfo | string>,
destinationDirectory: string,
options?: PartialDeep<ExportOptions>,
): Promise<ExportResult[]>The options parameter accepts a deeply partial ExportOptions object. All fields have sensible defaults, so you only need to specify what you want to override.
| Option group | Description |
|---|---|
exportOptions |
Controls which export engine is used per image type, file naming conventions (sluggify, UUID fragment, extension normalization), and AppleScript GUI export settings. |
processOptions |
Controls image processing: color profile normalization (preserveColorProfiles, defaultColorProfile), compression format and quality (lossyFormat, lossyQuality, losslessFormat), max dimensions and file size (maxDimensionsPixels, maxFileSizeBytes), and format passthrough (passthroughFormats). Set to 'disabled' to skip processing. |
metadataOptions |
Controls metadata written to exported images (creator, credit, description, label). Set to 'disabled' to skip metadata management. |
syncOptions |
Controls incremental export behavior: diff strategies (diffStrategies), whether to delete stale files (deleteTarget, deleteOthers), and force re-export (forceUpdate). Set to 'disabled' to skip sync. |
See the ExportOptions type and the inline JSDoc on each field in src/pipeline/ for the full set of options.
Functions are also provided for querying the contents of the Photos.app library:
function getPhotoInfo(
identifiers: string | string[],
caseSensitive?: boolean, // Defaults to false
): Promise<PhotoInfo[]>const [info] = await getPhotoInfo('Pets/Tiny')
console.log(info.uuid, info.original.fileName, info.dateCreated)function getAlbumInfo(
identifiers: string | string[],
caseSensitive?: boolean, // Defaults to false
): Promise<AlbumInfo[]>const [album] = await getAlbumInfo('Pets')
console.log(album.uuid, album.estimatedAssetCount)Aphex logs progress and warnings via lognow for logging. You can inject your own logger:
import { setLogger } from '@kitschpatrol/aphex'
setLogger(console)Let's assume you have an album named "Trip" in your Photos.app library containing a photo with the filename "IMG_1922.jpeg":
const result = await exportPhoto('Trip/IMG_1922.jpeg', '~/Desktop')
// '/Users/$USER/Desktop/IMG_1922.jpeg'
console.log(result.path)Lookups are case-insensitive by default. Albums nested in folders are also supported, just add them to the identifier path:
const result = await exportPhoto('Astrophotography/Regulus/IMG_2036.jpeg', '~/Desktop')Let's assume you have an album named "Pets" containing a photo you've titled "Tiny":
const result = await exportPhoto('Pets/Tiny', '~/Desktop')
// '/Users/$USER/Desktop/Tiny.jpeg'
console.log(result.path)Let's assume you know the local UUID of the photo you want. (Maybe you looked it up using the getPhotoInfo function, or the aphex CLI command, or osxphotos.)
const result = await exportPhoto('3AFE81DB-6BDB-42AB-AD27-90EE3A85A404', '~/Desktop')Note that UUIDs are unique to each instance of your Photos.app library, so if you have the same library synced across several machines, you can't expect the identifiers to be consistent. Technically, they aren't universal. Apple uses the term "local identifier" internally for this reason. For the sake of concision and consistency with tools like osxphotos, this library uses the term "UUID" interchangeably with "local identifier".
Let's assume you have an album named "Pets" containing a number of photos. Export them all as follows:
const results = await exportPhotos(['Pets'], '~/Desktop')Like photos, albums also have local UUIDs in the Photos.app library.
Let's assume you know the local UUID of the photo you want. (Maybe you looked it up using the getAlbumInfo function.)
const results = await exportPhotos(['2768A20C-9BD0-42CB-B464-9D299952D389'], '~/Desktop')The same caveat about UUID consistency across library instances applies here.
You can mix and match all the identifier forms as you like. Aphex will return a flat list of all the exported photos:
const results = await exportPhotos(
[
// Photo title
'Pets/Tiny',
// Photo filename
'Trip/IMG_1922.jpeg',
// Photo UUID
'3AFE81DB-6BDB-42AB-AD27-90EE3A85A404',
// Album name
'Astrophotography/Regulus',
// Album UUID
'2768A20C-9BD0-42CB-B464-9D299952D389',
],
'~/Desktop',
)Aphex uses the bundled aphex-swift CLI tool to query the Photos.app library. The tool allows you to perform simple lookups of photos in your library using the same identifier logic enumerated above. (E.g. searching by file name, album name, title, etc.)
It's exposed as a binary as part of this package, so it's accessible on the command line via aphex within the scope of the @kitschpatrol/aphex package installation.
See the project's readme for additional details.
Currently, the TypeScript code bridges via simple CLI calls to the aphex-swift binary, which is a wrapper around parts of Apple's PhotoKit framework. This is flexible and fast enough for now, but projects like Kabir Oberai's node-swift could be a good alternative for tighter integration between native Swift code and the TypeScript API.
Also, this library bundles a bunch of generically useful image processing functionality, which should probably live in a separate package.
Thank you to Rhet Turnbull for creating osxphotos, which informed some of the export pipelines in this library.
Aphex borrows a technique from Andreas Bentele's PhotosExporter for extracting semi-private values from PHAssetResource objects.
Issues are welcome and appreciated.
Please open an issue to discuss changes before submitting a pull request. Unsolicited PRs (especially AI-generated ones) are unlikely to be merged.
This repository uses @kitschpatrol/shared-config (via its ksc CLI) for linting and formatting, plus MDAT for readme placeholder expansion.
This is an unofficial library and is not affiliated with or blessed by Apple Inc.
The core export commands maintain a "read only" relationship with your library.
None of the code paths should modify the contents of your Photos.app library. But regardless, strange things can happen — please back up your Photos.app library before using this tool.
This tool has not been tested with iCloud-based Photos libraries.