mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-27 11:27:21 -06:00
* Open HEIC/HEIF photos (#355) iPhones save HEIC by default and Photoshop opens it; PhotoCraft had no HEIF code, so .heic was missing from File > Open and failed to decode. Add Format::Heif to photocraft-codecs, decoded by heic-rs (pure Rust, forbid(unsafe), MIT OR Apache-2.0, wasm-ready): single pictures and grid-tiled photos, 8-bit to U8 and 10/12-bit to U16, auxiliary alpha, ICC, EXIF and XMP. The container's irot/imir/clap are applied and EXIF Orientation is rewritten to 1, so photos are never turned twice. Limits are checked against the declared size before decoding. Read-only (no pure-Rust HEVC encoder), listed in ASYMMETRIC_EXCEPTIONS. File > Open (desktop, web) and the batch list accept heic/heif/hif. Tests compare against Apple's own decode and a 16-bit source, cover orientation, metadata, limits and every truncation; fixtures from heic-rs and pillow-heif are attributed in ATTRIBUTION.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * HEIF: pin heic-rs exactly and add a decode_heif fuzz target Review follow-up: heic-rs is young and parses untrusted files, so pin it (=0.1.1) and fuzz the HEIF path. The target covers both orientation paths and is meant to run with -timeout so a hang is reported, not only a panic. It is added to the CI fuzz matrix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * HEIF: a heic-rs panic is a malformed-file error, not a crash Fuzzing decode_heif found two out-of-range slices in heic-rs 0.1.1 on malformed files: - in its box parser (boxes.rs:130), reachable from a 72-byte file, even through the metadata-only probe; - during HEVC reconstruction (hevc/decode/recon.rs:70). The background job path catches panics, but the synchronous ones (jobs off, the CLI) did not. The HEIF codec now runs the decoder under catch_unwind and reports a panic as CodecError::Malformed, as gpu/health.rs already does around wgpu. The fuzz target also calls heic-rs directly, so its panics still reach the fuzzer. The 72-byte input is a regression test, which fails without the guard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * HEIF: move the decoder into an optional photocraft-heif crate The heic-rs decoding from #372 now lives in `crates/heif` (photocraft-heif, standalone, depends only on heic-rs =0.1.1): probe, decode with a pixel limit, container transforms, alpha, 8/10-bit, ICC/EXIF/XMP and the catch_unwind guard, behind a small API of its own (Info, Options, Decoded, Error). photocraft-codecs uses it behind a new non-default `heif` feature; the apps and photocraft-io forward it. HEIF detection, extensions and mime stay compiled in, so without the feature a HEIC file is still recognised and opening it is a clear "HEIC/HEIF support isn't included in this build of PhotoCraft" error. `cargo xtask layers` allows exactly codecs -> heif between standalone crates. The fixtures are no longer committed: `cargo xtask corpus --heif` fetches them from heic-rs (tests/fixtures, v0.1.1) and pillow-heif (tests/images) at pinned commits, verified against xtask/heif-corpus.sha256 (same bytes as the PR's copies). The fixture tests run with `--features corpus,heif` (`cargo xtask test-corpus` enables both). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Build CI and official binaries with HEIC support (--features heif) CI test and clippy, the FreeBSD job, and the macOS, Linux (and Flatpak), FreeBSD, Windows and web packaging all build with the `heif` feature; CI also tests codecs/io without it on Linux so the "not in this build" path stays covered. `cargo xtask wasm` also checks photocraft-codecs with `--features heif`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Docs: HEIC open is an optional build feature, on in official builds Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
171 lines
7.3 KiB
Bash
Executable File
171 lines
7.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Build, sign and (optionally) notarize the macOS release artifacts:
|
|
#
|
|
# $DIST/photocraft-<version>-macos-<arch>.dmg PhotoCraft.app on a drag-to-Applications DMG
|
|
# $DIST/photocraft-cli-<version>-macos-<arch>.zip the headless CLI
|
|
#
|
|
# Usage: packaging/macos/package.sh [--arch universal|aarch64|x86_64] [--skip-build]
|
|
#
|
|
# Signing (env):
|
|
# MACOS_SIGN_IDENTITY codesign identity (name or SHA-1). Default "-" = ad-hoc (local testing;
|
|
# Gatekeeper will reject the result on other Macs). In CI, import-cert.sh sets it.
|
|
# MACOS_KEYCHAIN keychain holding the identity (optional)
|
|
# Notarization (env; all three needed, and a real identity):
|
|
# APPLE_ID, APPLE_PASSWORD (app-specific password), APPLE_TEAM_ID
|
|
set -euo pipefail
|
|
# shellcheck source=../env.sh
|
|
. "$(dirname "${BASH_SOURCE[0]}")/../env.sh"
|
|
HERE="$ROOT/packaging/macos"
|
|
|
|
ARCH=universal
|
|
SKIP_BUILD=0
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--arch) ARCH="$2"; shift 2 ;;
|
|
--skip-build) SKIP_BUILD=1; shift ;;
|
|
-h | --help) sed -n '2,16p' "$0"; exit 0 ;;
|
|
*) echo "unknown argument: $1" >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
case "$ARCH" in
|
|
universal) TARGETS=(aarch64-apple-darwin x86_64-apple-darwin) ;;
|
|
aarch64) TARGETS=(aarch64-apple-darwin) ;;
|
|
x86_64) TARGETS=(x86_64-apple-darwin) ;;
|
|
*) echo "unknown --arch $ARCH" >&2; exit 2 ;;
|
|
esac
|
|
|
|
# Keep in sync with LSMinimumSystemVersion in Info.plist.in.
|
|
export MACOSX_DEPLOYMENT_TARGET=11.0
|
|
IDENTITY="${MACOS_SIGN_IDENTITY:--}"
|
|
SHORT_VERSION="${VERSION%%-*}"
|
|
WORK="$CARGO_TARGET_DIR/macos-package"
|
|
APP="$WORK/PhotoCraft.app"
|
|
DMG="$DIST/photocraft-$VERSION-macos-$ARCH.dmg"
|
|
CLI_ZIP="$DIST/photocraft-cli-$VERSION-macos-$ARCH.zip"
|
|
|
|
NOTARIZE=0
|
|
if [ "$IDENTITY" = "-" ]; then
|
|
warn "macOS: ad-hoc signing (no MACOS_SIGN_IDENTITY); artifacts are not notarized"
|
|
elif [ -n "${APPLE_ID:-}" ] && [ -n "${APPLE_PASSWORD:-}" ] && [ -n "${APPLE_TEAM_ID:-}" ]; then
|
|
NOTARIZE=1
|
|
else
|
|
warn "macOS: APPLE_ID / APPLE_PASSWORD / APPLE_TEAM_ID incomplete; signed but not notarized"
|
|
fi
|
|
|
|
echo "==> PhotoCraft $VERSION for macOS ($ARCH), identity: $IDENTITY, notarize: $NOTARIZE"
|
|
|
|
# ---- build -------------------------------------------------------------------------------------
|
|
if [ "$SKIP_BUILD" = 0 ]; then
|
|
args=()
|
|
for t in "${TARGETS[@]}"; do args+=(--target "$t"); done
|
|
(cd "$ROOT" && cargo build --release --locked -p photocraft -p photocraft-cli --features heif "${args[@]}")
|
|
fi
|
|
|
|
rm -rf "$WORK"
|
|
mkdir -p "$WORK/bin"
|
|
for bin in photocraft photocraft-cli; do
|
|
inputs=()
|
|
for t in "${TARGETS[@]}"; do inputs+=("$CARGO_TARGET_DIR/$t/release/$bin"); done
|
|
lipo -create -output "$WORK/bin/$bin" "${inputs[@]}"
|
|
lipo -info "$WORK/bin/$bin"
|
|
done
|
|
|
|
# ---- signing helpers ---------------------------------------------------------------------------
|
|
sign() {
|
|
# Hardened runtime + secure timestamp for a real identity; ad-hoc can't be timestamped.
|
|
local ts=(--timestamp)
|
|
[ "$IDENTITY" = "-" ] && ts=(--timestamp=none)
|
|
local kc=()
|
|
[ -n "${MACOS_KEYCHAIN:-}" ] && kc=(--keychain "$MACOS_KEYCHAIN")
|
|
codesign --force --sign "$IDENTITY" ${kc[@]+"${kc[@]}"} "${ts[@]}" "$@"
|
|
}
|
|
|
|
notarize() {
|
|
local file="$1" out id status
|
|
echo "==> notarizing $(basename "$file") (this can take a few minutes)"
|
|
out="$(xcrun notarytool submit "$file" --apple-id "$APPLE_ID" --password "$APPLE_PASSWORD" \
|
|
--team-id "$APPLE_TEAM_ID" --wait --timeout 1h --output-format json)" || true
|
|
echo "$out"
|
|
id="$(printf '%s' "$out" | plutil -extract id raw -o - - 2>/dev/null || true)"
|
|
status="$(printf '%s' "$out" | plutil -extract status raw -o - - 2>/dev/null || true)"
|
|
if [ "$status" != "Accepted" ]; then
|
|
if [ -n "$id" ]; then
|
|
xcrun notarytool log "$id" --apple-id "$APPLE_ID" --password "$APPLE_PASSWORD" --team-id "$APPLE_TEAM_ID" || true
|
|
fi
|
|
echo "error: notarization of $(basename "$file") failed (status: ${status:-unknown})" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# ---- PhotoCraft.app ----------------------------------------------------------------------------
|
|
echo "==> assembling $APP"
|
|
mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources"
|
|
# Executable and icon carry the display name (CFBundleExecutable / CFBundleIconFile).
|
|
cp "$WORK/bin/photocraft" "$APP/Contents/MacOS/PhotoCraft"
|
|
cp "$ROOT/assets/app-icon/photocraft.icns" "$APP/Contents/Resources/PhotoCraft.icns"
|
|
# Licences of the embedded craft-fonts fonts (only when built with CRAFT_FONTS_DIR).
|
|
if [ -n "${CRAFT_FONTS_DIR:-}" ]; then
|
|
mkdir -p "$APP/Contents/Resources/Licenses"
|
|
copy_font_licences "$APP/Contents/Resources/Licenses"
|
|
fi
|
|
sed -e "s/@VERSION@/$VERSION/g" -e "s/@SHORT_VERSION@/$SHORT_VERSION/g" \
|
|
-e "s/@BUILD_SHA@/${PHOTOCRAFT_BUILD_SHA:-unknown}/g" \
|
|
"$HERE/Info.plist.in" >"$APP/Contents/Info.plist"
|
|
plutil -lint "$APP/Contents/Info.plist"
|
|
printf 'APPL????' >"$APP/Contents/PkgInfo"
|
|
|
|
# Sign inside-out: nested code first, then the bundle itself (no --deep on the final signature).
|
|
# Today the only nested code is the main executable; frameworks/helpers would be signed here too.
|
|
sign --options runtime --entitlements "$HERE/entitlements.plist" "$APP/Contents/MacOS/PhotoCraft"
|
|
sign --options runtime --entitlements "$HERE/entitlements.plist" "$APP"
|
|
codesign --verify --strict --deep --verbose=2 "$APP"
|
|
|
|
if [ "$NOTARIZE" = 1 ]; then
|
|
ditto -c -k --keepParent "$APP" "$WORK/PhotoCraft-notarize.zip"
|
|
notarize "$WORK/PhotoCraft-notarize.zip"
|
|
xcrun stapler staple "$APP"
|
|
xcrun stapler validate "$APP"
|
|
spctl --assess --type execute -vvv "$APP"
|
|
fi
|
|
|
|
# ---- DMG ---------------------------------------------------------------------------------------
|
|
echo "==> building $DMG"
|
|
STAGE="$WORK/dmg"
|
|
mkdir -p "$STAGE"
|
|
ditto "$APP" "$STAGE/PhotoCraft.app"
|
|
ln -s /Applications "$STAGE/Applications"
|
|
rm -f "$DMG" "$WORK/raw.dmg"
|
|
# makehybrid + convert builds the image without attaching a device, unlike `create -srcfolder`,
|
|
# which is flaky on CI runners ("Resource busy") and hangs in sandboxed sessions.
|
|
hdiutil makehybrid -hfs -hfs-volume-name "PhotoCraft $VERSION" -hfs-openfolder "$STAGE" -o "$WORK/raw.dmg" "$STAGE"
|
|
hdiutil convert "$WORK/raw.dmg" -format UDZO -imagekey zlib-level=9 -o "$DMG"
|
|
rm -f "$WORK/raw.dmg"
|
|
sign "$DMG"
|
|
codesign --verify --strict --verbose=2 "$DMG"
|
|
if [ "$NOTARIZE" = 1 ]; then
|
|
notarize "$DMG"
|
|
xcrun stapler staple "$DMG"
|
|
xcrun stapler validate "$DMG"
|
|
spctl --assess --type open --context context:primary-signature -vvv "$DMG"
|
|
fi
|
|
|
|
# ---- CLI ---------------------------------------------------------------------------------------
|
|
echo "==> building $CLI_ZIP"
|
|
CLI_DIR="$WORK/photocraft-cli-$VERSION-macos-$ARCH"
|
|
mkdir -p "$CLI_DIR"
|
|
cp "$WORK/bin/photocraft-cli" "$CLI_DIR/"
|
|
copy_docs "$CLI_DIR"
|
|
# Same Developer ID and hardened runtime as the app, with an explicit reverse-DNS identifier
|
|
# (codesign would otherwise use the bare file name).
|
|
sign --options runtime --identifier ai.storyteller.photocraft-cli "$CLI_DIR/photocraft-cli"
|
|
codesign --verify --strict --verbose=2 "$CLI_DIR/photocraft-cli"
|
|
rm -f "$CLI_ZIP"
|
|
ditto -c -k --keepParent "$CLI_DIR" "$CLI_ZIP"
|
|
# notarytool accepts a zip of the bare binary. A bare Mach-O can't carry a stapled ticket, so
|
|
# Gatekeeper looks the notarization up online when a quarantined copy first runs.
|
|
if [ "$NOTARIZE" = 1 ]; then notarize "$CLI_ZIP"; fi
|
|
|
|
"$WORK/bin/photocraft-cli" --version
|
|
echo "==> done (check the artifacts as shipped with packaging/macos/verify.sh --arch $ARCH)"
|
|
ls -lh "$DMG" "$CLI_ZIP"
|