mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-28 10:11:58 +01: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>
141 lines
6.4 KiB
Bash
Executable File
141 lines
6.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Build and package PhotoCraft for Linux (<arch> is x86_64 or aarch64):
|
|
#
|
|
# $DIST/photocraft-<version>-linux-<arch>.AppImage any distro with glibc >= the build host's
|
|
# $DIST/photocraft-<version>-linux-<arch>.AppImage.zsync delta updates (needs zsyncmake)
|
|
# $DIST/photocraft-<version>-linux-<arch>.deb Debian, Ubuntu, Mint, Pop!_OS, ...
|
|
# $DIST/photocraft-<version>-linux-<arch>.rpm Fedora, openSUSE, RHEL, ...
|
|
# $DIST/photocraft-<version>-linux-<arch>.tar.gz plain FHS-style tree (bin/, share/)
|
|
#
|
|
# Usage: packaging/linux/package.sh [--skip-build] [--formats "appimage deb rpm tar"]
|
|
#
|
|
# Needs: cargo; nfpm for deb/rpm (https://nfpm.goreleaser.com); appimagetool for the AppImage
|
|
# (downloaded into $CARGO_TARGET_DIR if missing). Build on an old distro (CI: Ubuntu 22.04,
|
|
# glibc 2.35) so the binaries run on newer ones. Optional: desktop-file-validate, appstreamcli,
|
|
# zsyncmake (the zsync package) for the AppImage's .zsync.
|
|
set -euo pipefail
|
|
# shellcheck source=../env.sh
|
|
. "$(dirname "${BASH_SOURCE[0]}")/../env.sh"
|
|
HERE="$ROOT/packaging/linux"
|
|
APP_ID=ai.storyteller.photocraft
|
|
|
|
SKIP_BUILD=0
|
|
FORMATS="appimage deb rpm tar"
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--skip-build) SKIP_BUILD=1; shift ;;
|
|
--formats) FORMATS="$2"; shift 2 ;;
|
|
-h | --help) sed -n '2,15p' "$0"; exit 0 ;;
|
|
*) echo "unknown argument: $1" >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
ARCH="$(uname -m)"
|
|
case "$ARCH" in
|
|
x86_64) DEB_ARCH=amd64 ;;
|
|
aarch64 | arm64) ARCH=aarch64; DEB_ARCH=arm64 ;;
|
|
*) echo "unsupported architecture $ARCH" >&2; exit 2 ;;
|
|
esac
|
|
export PHOTOCRAFT_MAINTAINER="${PHOTOCRAFT_MAINTAINER:-PhotoCraft maintainers <photocraft@storyteller.ai>}"
|
|
BASENAME="photocraft-$VERSION-linux-$ARCH"
|
|
|
|
echo "==> PhotoCraft $VERSION for Linux $ARCH ($FORMATS)"
|
|
|
|
if [ "$SKIP_BUILD" = 0 ]; then
|
|
(cd "$ROOT" && cargo build --release --locked -p photocraft -p photocraft-cli --features heif)
|
|
fi
|
|
BIN="$CARGO_TARGET_DIR/release"
|
|
WORK="$CARGO_TARGET_DIR/linux-package"
|
|
STAGE="$WORK/root"
|
|
rm -rf "$WORK"
|
|
|
|
# ---- stage an FHS tree (shared by every format) -------------------------------------------------
|
|
install -Dm755 "$BIN/photocraft" "$STAGE/usr/bin/photocraft"
|
|
install -Dm755 "$BIN/photocraft-cli" "$STAGE/usr/bin/photocraft-cli"
|
|
strip "$STAGE/usr/bin/photocraft" "$STAGE/usr/bin/photocraft-cli" 2>/dev/null || true
|
|
install -Dm644 "$HERE/$APP_ID.desktop" "$STAGE/usr/share/applications/$APP_ID.desktop"
|
|
install -Dm644 "$HERE/$APP_ID.mime.xml" "$STAGE/usr/share/mime/packages/$APP_ID.xml"
|
|
mkdir -p "$STAGE/usr/share/metainfo"
|
|
sed -e "s/@VERSION@/$VERSION/g" -e "s/@DATE@/$PHOTOCRAFT_BUILD_DATE/g" \
|
|
"$HERE/$APP_ID.metainfo.xml.in" >"$STAGE/usr/share/metainfo/$APP_ID.metainfo.xml"
|
|
mkdir -p "$STAGE/usr/share/icons"
|
|
cp -R "$ROOT/assets/app-icon/hicolor" "$STAGE/usr/share/icons/"
|
|
mkdir -p "$STAGE/usr/share/doc/photocraft"
|
|
copy_docs "$STAGE/usr/share/doc/photocraft"
|
|
|
|
if command -v desktop-file-validate >/dev/null; then
|
|
desktop-file-validate "$STAGE/usr/share/applications/$APP_ID.desktop"
|
|
fi
|
|
if command -v appstreamcli >/dev/null; then
|
|
appstreamcli validate --no-net --explain "$STAGE/usr/share/metainfo/$APP_ID.metainfo.xml"
|
|
fi
|
|
|
|
has() { case " $FORMATS " in *" $1 "*) return 0 ;; *) return 1 ;; esac; }
|
|
|
|
# ---- .tar.gz ------------------------------------------------------------------------------------
|
|
if has tar; then
|
|
mkdir -p "$WORK/tar"
|
|
cp -R "$STAGE/usr" "$WORK/tar/$BASENAME"
|
|
tar -C "$WORK/tar" -czf "$DIST/$BASENAME.tar.gz" "$BASENAME"
|
|
echo "wrote $DIST/$BASENAME.tar.gz"
|
|
fi
|
|
|
|
# ---- .deb / .rpm --------------------------------------------------------------------------------
|
|
if has deb || has rpm; then
|
|
command -v nfpm >/dev/null || { echo "error: nfpm not found (https://nfpm.goreleaser.com/install/)" >&2; exit 1; }
|
|
export VERSION
|
|
export NFPM_ARCH="$DEB_ARCH"
|
|
# nfpm expands env vars in fields like `version` and `arch`, but not in `contents[].src`.
|
|
sed "s|\${STAGE}|$STAGE|g" "$HERE/nfpm.yaml" >"$WORK/nfpm.yaml"
|
|
for fmt in deb rpm; do
|
|
if has "$fmt"; then (cd "$ROOT" && nfpm package -f "$WORK/nfpm.yaml" -p "$fmt" -t "$DIST/$BASENAME.$fmt"); fi
|
|
done
|
|
fi
|
|
|
|
# ---- AppImage -----------------------------------------------------------------------------------
|
|
if has appimage; then
|
|
APPDIR="$WORK/PhotoCraft.AppDir"
|
|
cp -R "$STAGE" "$APPDIR"
|
|
mv "$APPDIR/usr/share/doc" "$WORK/doc-unused"
|
|
ln -s usr/bin/photocraft "$APPDIR/AppRun"
|
|
cp "$HERE/$APP_ID.desktop" "$APPDIR/$APP_ID.desktop"
|
|
cp "$ROOT/assets/app-icon/hicolor/256x256/apps/$APP_ID.png" "$APPDIR/$APP_ID.png"
|
|
ln -s "$APP_ID.png" "$APPDIR/.DirIcon"
|
|
|
|
TOOL="${APPIMAGETOOL:-$(command -v appimagetool || true)}"
|
|
if [ -z "$TOOL" ]; then
|
|
TOOL="$CARGO_TARGET_DIR/appimagetool-$ARCH.AppImage"
|
|
if [ ! -x "$TOOL" ]; then
|
|
curl -fsSL -o "$TOOL" "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-$ARCH.AppImage"
|
|
chmod +x "$TOOL"
|
|
fi
|
|
fi
|
|
# Absolute, because appimagetool runs in $DIST below (CARGO_TARGET_DIR or APPIMAGETOOL may be
|
|
# relative, e.g. target/agent-<name>).
|
|
OUT="$(cd "$DIST" && pwd)/$BASENAME.AppImage"
|
|
APPDIR="$(cd "$APPDIR" && pwd)"
|
|
TOOL="$(cd "$(dirname "$TOOL")" && pwd)/$(basename "$TOOL")"
|
|
# A .zsync left by an earlier run would hide a missing zsyncmake and describe another file.
|
|
rm -f "$OUT.zsync"
|
|
# Update information (#349): AppImageUpdate, AppImageLauncher and the like read it from the
|
|
# file and fetch only the blocks that changed in a newer release, through the .zsync published
|
|
# next to each AppImage on GitHub Releases. `latest` is the newest published release that is
|
|
# not a pre-release. A fork's builds point at its own releases through GITHUB_REPOSITORY.
|
|
REPO="${GITHUB_REPOSITORY:-storytold/photocraft}"
|
|
UPDATE_INFO="gh-releases-zsync|${REPO%%/*}|${REPO#*/}|latest|photocraft-*-linux-$ARCH.AppImage.zsync"
|
|
# Extract-and-run: works without FUSE (containers, CI). The output embeds the static runtime,
|
|
# so users don't need libfuse2 either. With zsyncmake on the host (CI installs the zsync
|
|
# package) appimagetool also writes the .zsync, into its working directory, hence the cd.
|
|
(cd "$DIST" && ARCH="$ARCH" APPIMAGE_EXTRACT_AND_RUN=1 "$TOOL" --no-appstream -u "$UPDATE_INFO" "$APPDIR" "$OUT")
|
|
echo "wrote $OUT"
|
|
if [ -s "$OUT.zsync" ]; then
|
|
echo "wrote $OUT.zsync"
|
|
else
|
|
warn "zsyncmake not found, so $OUT.zsync was not written; AppImage delta updates need it"
|
|
fi
|
|
fi
|
|
|
|
"$STAGE/usr/bin/photocraft-cli" --version
|
|
echo "==> done"
|
|
ls -lh "$DIST"
|