* Lossy WebP: a pure-Rust VP8 encoder with quality and lossless controls (#648)
crates/codecs gains codecs/vp8: a clean-room VP8 key-frame encoder from RFC 6386
(boolean coder, 16x16 and 4x4 intra prediction with per-macroblock mode decision,
forward DCT/WHT paired with the bit-exact inverse transforms, dc/ac quantiser tables,
token coding with per-frame probability updates, skipped macroblocks). webp.rs wraps
the frame in the RIFF container (bare VP8, or VP8X with ALPH alpha and ICCP/EXIF/XMP).
EncodeOptions: webp_lossless now picks the encoder instead of erroring, webp_quality
(0-100) sets the quantiser; a lossy WebP reports a LossyCompression fidelity warning.
Export As: the WebP entry gets Quality and a Lossless switch (new tl! string in all
catalogs). CLI --quality, MCP/RPC doc_save quality and the engine's JPEG-style
quality turn a WebP lossy; without a quality WebP stays lossless as before.
Tests: tests/webp_lossy.rs decodes every file with image-webp (PSNR by quality,
sizes fall with quality and are >= 4x below lossless on noisy photos, flat frames
under 1.2 KB, alpha exact, metadata round trip, odd/tiny sizes, 16-bit input, the
16383 px limit as an error, text edges), plus unit tests for the coder, transforms,
prediction and header.
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
* i18n(it): Lossless (Export As WebP switch) in the Italian catalog that landed on main
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
---------
Co-authored-by: leon <claude.udder842@passmail.net>
Co-authored-by: Claude Mythos 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Maintainer decision on #672: the app's Save As keeps TIFF layers as Photoshop does (it sets
ExportOptions.tiff_layers from its Layers option), but scripted and agent saves stay flat by
default so conversions keep predictable sizes. #672 merged with the default on, so every
headless TIFF save became layered.
- photocraft-io: ExportOptions::default() has tiff_layers: false. The desktop and web Save As
already set it explicitly from their settings, so the app is unchanged.
- photocraft-cli: --tiff-layers on convert, run and batch (listed in the usage text).
- engine: file.saveACopy, Batch, Image Processor, the export-to-files commands and the lens
batch take "tiffLayers": bool=false, through one SaveOpts { quality, tiff_layers }.
- automation: serve doc.save and MCP doc_save take tiffLayers (default false).
- Tests: default ExportOptions write a flat TIFF; the CLI writes flat without the flag and
layered with it (run and convert); #672's layered-TIFF tests ask for layers explicitly.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Layered TIFF: open and save Photoshop layer data in TIFF files, in either byte order (part of #215)
Photoshop keeps a layered TIFF's layers in two private tags: 37724 (ImageSourceData, the same
layer section a PSD has) and 34377 (the image resources). PhotoCraft now reads and writes both,
so a TIFF opens with its groups, masks, layer styles, type, shapes, smart objects and adjustment
layers exactly as the PSD path would, and saves them back.
* photocraft-psd: a `tiff` module with `ImageSourceData` (parse/write, PSD- and PSB-sized),
the 34377 resource helpers, and a field-by-field byte-order transcoder. In Intel-order TIFFs
Photoshop byte-swaps the whole block structure (reversed keys, little-endian integers and
doubles) while pixel samples stay big-endian; the transcoder knows the layer-info structure,
masks, blending ranges and every block photocraft-io maps (descriptors, TySh, lrFX, the
binary adjustments, paths, patterns, filter effects, metadata settings, linked layers,
annotations...), recognizes descriptor-shaped unknown blocks, and drops the rest with a
warning instead of passing byte-swapped data to other readers. The reader gained an optional
little-endian mode. 23 unit tests; a fuzz target in the CI matrix.
* photocraft-codecs: the two tags travel as opaque bytes on `Metadata`, read by a
bounds-checked walk of IFD0 (classic and BigTIFF, both byte orders) and written back as
BYTE / UNDEFINED tags. `tiff_writes_little_endian()` tells which order the encoder produces.
* photocraft-io: `tiff_layers` rebuilds a PSD model from the TIFF's pixels plus the tags on
import, and on export runs the PSD exporter (new `merged_matte` / `merged_compression`
options), interleaves its merged planes for the TIFF image and stores the layer section in
the encoder's byte order. `ExportOptions.tiff_layers` (default on). Lab and Multichannel
documents save flat; extra alpha channels are not written (one transparency sample).
* UI: the TIFF Options prompt ("Including layers will increase file size." / "Discard Layers
and Save a Copy" / "Don't show again") behind Preferences > File Handling > Ask Before
Saving Layered TIFF Files; `ExportSettings.tiff_layers`; catalogs updated.
* Tests: io round trips at 8/16/32 bits in Gray/RGB/CMYK, equivalence with the PSD path, every
TIFF compression, the flat fall-backs, malformed input, and files written by psdtags in both
byte orders (fixtures from scripts/layered_tiff_fixtures.py); a corpus test (feature
`corpus`) writes every corpus PSD as a layered TIFF and compares the result with the PSD
round trip (700 files, 0 differences, 0 crashes). Codecs tests cover the tag walk on
hand-made files.
* Scorecard FILE-215-7 done, BUG-204-3 partial; README and book updated.
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
* Layered TIFF: de and pt-br strings; neutral wording in the psd tiff docs
The four psdtags fixtures stay out of the repository (no binary test data here): the
generator now writes them to the gitignored corpus/layered-tiff/, and the tests that read
them skip with a message when absent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Regenerate docs/scorecard.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: leon <claude.udder842@passmail.net>
Co-authored-by: Claude Mythos 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* 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>
Decoders now report what the decoded image doesn't show in a new
`Image::warnings` (`photocraft_codecs::DecodeWarning`), and `image_to_document`
turns them into import warnings, so they reach `photocraft-cli info` /
`convert` stderr, `doc.open` over MCP and serve, and the app's open notice
through the existing import-warnings path.
* #523: APNG (acTL frames, plus a default image outside the animation),
animated GIF (image descriptors counted by walking the blocks), animated
WebP and multi-page TIFF (IFDs walked without decoding; reduced-resolution
copies and transparency masks are not pages) say "only the first of N
frames/pages was imported". Single images stay quiet; the kept frame's
pixels are unchanged.
* #518: a JPEG whose data runs out before its end-of-image marker (marker
segments skipped by length, scan data by byte stuffing) still decodes
leniently but warns "JPEG data ends early (the file is truncated or
damaged); part of the image is missing". A JPEG that ends before any scan
data is an error.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* PSD: reject a header with zero width or height (#519)
A 0xN header opened as an empty document and saved back as another
zero-sized PSD or .pcraft. Header::validate now requires at least 1x1
(the spec range starts at 1), and PSD export validates the header so a
zero-sized canvas is never written.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* .pcraft: documents with deeply nested groups load again (#517)
serde_json's 128-level recursion limit made bundles with 40 or more
nested groups unreadable after a successful save. The manifest is now
parsed with that limit disabled behind a linear JSON depth check, layers
are decoded and converted one level at a time with small stack frames,
and groups may nest up to MAX_GROUP_DEPTH (100) levels, which load and
save within a few hundred KiB of stack. Saving a deeper document fails
instead of writing a bundle that can't be opened.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Export: convert non-sRGB RGB to sRGB for formats without a profile (#522)
GIF, BMP, QOI, TGA, ICO and Netpbm can't embed ICC, so a linear-light
or Display P3 document was written with values that only meant something
under the dropped profile (a linear EXR came out dark). export_flat now
converts such RGB to sRGB through photocraft-cms (perceptual, as Quick
Export does) for every photocraft_io::export route. The EXR/HDR linear
conversion shares the same banded helper.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Export: composite transparency over white for formats without alpha (#521)
The CLI, MCP, Save As, Layers to Files and Layer > Export As dropped the
alpha channel when writing JPEG (or Radiance HDR), exposing the colours
stored under transparent pixels, while Export As and Quick Export
composite over white. export_flat now flattens over white too (no ink
for CMYK) and says so in its warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Command params that are not a JSON object are an error (#506)
`Session::dispatch` now rejects array, string, number and boolean params
with "invalid parameters for `<id>`: params must be a JSON object" instead
of running the command with every default; `null` still means no params.
The one check covers the control channel, `serve`, MCP (headless and
bridge) and the CLI, and batch steps fail per step. `photocraft-cli run
--new` prefixes its error with `--new`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Headless requests apply finished background jobs first (#503)
A `wait: false` job was applied to its document only by a later command
or `jobs.list`, so `doc.save`, export, `doc.inspect`, `doc.render` and
`session.list` (and their MCP tools) still read and wrote the document
without it. `Headless::sync_jobs` now runs at the top of every JSON-lines
request and before every headless MCP tool call.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* `serve` stdio keeps the session after a rejected request line (#505)
An over-long request line (or one that is not UTF-8) ended the stdio
session, losing every open document. It now gets one error reply, the
rest of that line is skipped without being dispatched, and the next
request is served. `read_bounded_line` reads bytes, so a cut inside a
multi-byte character is reported as too long instead of an I/O error.
TCP connections still close after an oversized line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* MCP `command_batch` keeps its step results when the reply budget stops it (#504)
The batch budget charged each step's plain JSON size, but MCP sends the
batch reply as escaped text content, so a batch the budget had already
stopped could still exceed the tool-result ceiling and come back as a
bare "response exceeds" error. MCP (headless and bridge) now uses
`BatchReplyBudget::escaped`, which charges the escaped size, through the
new `Headless::batch_with_budget`; `serve` keeps the plain budget.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: the 1 MiB request-line limit does not apply to MCP stdio (#529)
The book stated the 1 MiB request-line limit without naming a transport.
It applies to desktop and headless TCP and to the headless JSON-lines
stdio server; MCP over stdio reads its own lines and only its tool
results are size-checked. Say so in the automation security pages and in
the control protocol's transport limits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* CLI --quality refuses every value outside 1-100 (#491)
--quality was parsed as a u8 and clamped, so 0 and 101-255 were silently
accepted while 256 and above were rejected. Every value outside 1-100 is now
a bad --quality error, checked before any file is read or written.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* CLI batch: --format with a leading dot names outputs <stem>.<ext> (#490)
batch built output names from the raw --format value, so --format .jpg wrote
a..jpg. One leading dot is now dropped, as the save path and
file.automate.batch already do.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* CLI batch --actions takes recorded-action and droplet steps (#489)
batch read only {"command", "params"} objects, so the [id, params] pairs a
recorded action or droplet stores failed with 'missing "command"'. It now
uses the engine's action parser (automate_cmds::parse_action, split out of
parse_script), which takes [id, params], objects and bare ids, bare or
wrapped in {"steps": ...} or a droplet; {"actions": ...} still works.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* CLI batch refuses an --out folder that is the --in folder (#492)
Results are saved as <out>/<stem>.<ext>, so --out naming the input folder
silently replaced the originals. batch now compares the canonical folders
(relative spellings, trailing separators, symbolic links) and fails before
writing anything, naming the folder; the new --in-place flag opts in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- Roadmap (updated 2026-10-07): the M11 row notes the stricter CLI,
batch collision reports, per-step `wait` in MCP batches and safe
headless saves; the UI / UX row lists the keyboard-only shortcuts
(layer navigation, number keys, hardness, step and repeat), Alt-click
sampling, double-click for Layer Style, New from Clipboard, the
centred window and remembered Liquify settings.
- Scorecard: UI-217-1 is partial (Fill and Liquify settings persist);
UI-217-2's note points at the new `native_options()`. docs/scorecard.md
regenerated; docs/parity.md is unchanged (no new catalogue items).
- README: `<subcommand> --help` in the CLI snippet.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Any `--flag` a subcommand didn't know was recorded and ignored, so
`batch --fromat jpg` wrote the source formats and exited 0, `run
--param '{…}'` turned the JSON into a stray file name, and
`<subcommand> --help` ran the subcommand (`mcp --help` started a
server). One table, `SUBCOMMANDS`, now lists each subcommand's value
flags, bare flags and handler, replacing the shared VALUE_FLAGS list:
an unknown flag in either form (`--flag value` or `--flag=value`) is a
usage error naming it (exit 2) before any work, a value on a bare flag
is an error, and `--help` or `-h` after a subcommand prints usage and
exits 0. Every documented flag keeps working in both forms.
The usage text now lists `--quality` for `run` and `batch`, which
already honoured it, and the book's CLI page describes the stricter
parsing.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Rebased onto main (#58/#61/#63/#66/#70/#74) by the maintainers: automation
open/save return and surface import/export warnings (app.open/app.save
reply with `warnings`), automation opens route .abr/.grd to the preset
libraries with script events suppressed, interactive opens keep open_file's
path/recent/notice handling.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* docs: add mdBook and security documentation foundation
* docs: point book links at storytold/photocraft; scope the credits entry
The book's repository/edit URLs and source links pointed at the contributor's
fork. The credits entry now describes what this PR contributed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>