* Load and save Photoshop Actions (.atn) (#219)
- photocraft-psd: `atn` parser and writer for version-16 action sets, exact round trip,
truncation/garbage safe, fuzz target and `atn_dump` example. Descriptor fix: `indx` and
`Idnt` reference items carry a class before the number (real action files failed to parse).
- photocraft-engine: `atn_cmds` maps 92 Photoshop events to commands and back (filters,
adjustments per channel, image/canvas/crop/trim/rotate/flip/mode, make/set/select/delete/
hide/show/move/duplicate, fill/stroke, selection, clipboard, purge, profiles, play, stop).
Other steps load as `actions.photoshopStep` (descriptor kept, written back by save, fails at
playback by name); tool selections load switched off. Commands `actions.load`,
`actions.save`, `actions.stopMessage`, `actions.photoshopStep`. `Action` gains `dialogs`
and `set`. `layer` params accept a layer name, so loaded steps replay in any document.
- UI: Actions panel menu › Load Actions… / Save Actions…; Photoshop steps labelled.
- CLI: `batch --actions set.atn`. Automation policy covers the new path params.
- Docs: file-format parity (.atn R/W), gaps G11, target-app parity, scorecard AUTO-219-2
partial, development guide.
* Translate the Actions panel's Load/Save strings in every catalog
The eight new panel strings (Load/Save Actions…, the loaded/saved counts, the kept-step label)
were missing from all 19 catalogs; the languages that were complete are back at 100%.
* Show the Actions entries in the default dock's panel menu
The legacy dock renders its own group menu and only knew the Layers and Swatches entries, so
Load Actions… and Save Actions… were missing there.
crates/raw/fuzz had no .gitignore, crates/psd/fuzz didn't ignore
Cargo.lock and crates/format/fuzz didn't ignore coverage/, so running
those fuzz targets left untracked files in the checkout. All five fuzz
crates now ignore target/, corpus/, artifacts/, coverage/ and Cargo.lock,
as codecs and affinity already did.
* psd: harden the standalone binary parsers (gradients, swatches, patterns, filter effects, metadata, resources, tagged blocks)
Correctness and robustness work on the psd crate's auxiliary parsers, with
fuzz targets and property tests so the invariants are enforced, not just asserted.
grd (gradients):
- Split num()/finite() into a single guard with a saturating f64->f32 cast: only
NaN has no value, everything else (including +/-inf and 1e300) saturates to the
representable range so the mapping stays monotonic instead of snapping to a field
default and inverting a hostile value.
- Hue now wraps with rem_euclid(360) instead of clamping (370 -> 10, not 370 -> 360,
which would be a different colour at the boundary).
- Every colour model is bounded: RGB/CMYK/Gray to 0..=1, L* to 0..=100 and a*/b* to
the CIELAB gamut (-128..=127), so a downstream converter never sees out-of-gamut input.
aco/ase (swatches):
- Share one io::utf16_units() helper that caps a name by UTF-16 code units without
ever splitting a surrogate pair; keeping it in one place stops the fix from drifting
between the two formats.
Coverage:
- New fuzz targets for grd, aco, ase, patterns, filter_effects, metadata, tagged and
resources (wired into the fuzz CI matrix).
- Property tests mirroring each fuzz invariant (round-trip stability, no panic on
arbitrary bytes, documented colour/number ranges) so normal CI covers them; the
fuzz jobs run on demand.
369 psd tests pass; clippy -D warnings and rustfmt clean; layering and wasm check pass;
all psd fuzz targets build and run clean.
* psd: address Gemini/Claude review of the parser hardening
Follow-up to the previous commit, applying the substantive points from two
independent code reviews (Gemini 3.8: ship; Claude: ship after small fixes).
Production:
- grd hue: an in-range value (0..=360, including exactly 360) is now kept verbatim;
only an out-of-range hue wraps with rem_euclid. Wrapping unconditionally altered a
legal 360 to 0 (and f32 round-off on a tiny negative could itself land on 360),
changing the colour at the boundary for anything that interpolates in HSB.
- ase: compile-time assert that MAX_NAME_UNITS < u16::MAX, since put_name writes the
length as `units.len() as u16 + 1`; guards the invariant if the cap is ever raised.
Stronger test/fuzz invariants (byte stability alone would pass even if the writer
silently dropped a field; these assert semantic equality):
- metadata fuzz: assert the parsed-then-written block re-parses to the same items
(the weakened byte-only check was the fix the first fuzz run surfaced).
- patterns fuzz: assert parse(write(parse(x))) == parse(x); channel offsets/padding
are wire artifacts, not fields, so the round trip is lossless (confirmed, 2M execs).
- filter_effects fuzz: assert back == fx as well as byte stability; also decode every
cached slot, not just the mask.
- resources fuzz: round-trip ResolutionInfo through to_bytes/from_bytes.
Coverage:
- tagged fuzz: select the block key from a table of every typed/strict key via the
first byte, so the fuzzer reaches each branch instead of guessing a 4-byte key.
- new property tests: damage-a-valid-block for metadata/patterns/filter_effects (pure
random bytes rarely pass a header), and check_structure over every structural key.
- grd_every_colour_model_is_clamped_and_finite now pins exact saturation values
(a*/b* to +/-128, L* to 0/100, units to 0/1), so a weaker clamp is caught, not just
an out-of-range value; grd_hue_wraps now pins the exact-360 and 0 boundaries.
Verified: 371 psd tests pass; clippy -D warnings, rustfmt, layers and wasm clean; all
13 fuzz targets build and run clean (metadata 11.5M, patterns 2M, filter_effects 5.9M,
resources 3.5M, tagged 5.4M execs); the a*/b* clamp-weakening and hue-guard-removal
mutants are both killed by the strengthened tests.
---------
Co-authored-by: photocraft-agent UNCONFIGURED <unconfigured@invalid.local>
* 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>
* psd: .abr brush (v1/v2/v6+) and .grd gradient (v5) parsers and writers
Clean-room from public format descriptions: legacy computed/sampled brushes,
8BIM-sectioned samp/patt/desc files, 8/16-bit raw and PackBits tips. Bounded
(MAX_EDGE, MAX_TOTAL_BYTES), damaged brushes are skipped with warnings, hostile
input returns Err. Adds an abr fuzz target.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* paint: 36 grouped built-in brush presets and new procedural tips
General / Dry Media / Wet Media / Special Effects groups (pressure size/opacity
variants, airbrush, pencils, charcoal, pastel, crayon, rake, watercolour, ink,
oil, foliage, grass, stipple, sparkle, halftone…). Leaf, grass, charcoal,
sponge, star and rake tips are generated in code. BrushPreset gains a group.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* io: map .abr brushes onto BrushSettings
Tip shape, shape dynamics, scattering, texture (embedded patt patterns),
dual brush, colour dynamics, transfer, pose, noise, wet edges, build-up and
captured tool options; unmapped settings are reported as warnings. Includes
bench_abr (100-brush import, 5000-dab stroke).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* engine: brush.presets.importAbr and gradient.presets.importGrd
Path or base64 data, group per file (re-import replaces), unique names next to
the built-ins; graceful-failure tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ui: real stylus pressure/tilt, grouped Brushes panel, Import Brushes
Pen force from egui Touch events (Windows WM_POINTER via winit), Pointer Events
pressure/tilt/twist on the web, tilt via ui.pointer; mouse stays 1.0. .abr/.grd
open from File > Open, drag-and-drop, Brushes panel and Preset Manager. Preset
previews keyed on a cheap signature instead of the full brush JSON.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Adopt the Crafting Apps licensing standard (craftrules standards/licensing.md):
- LICENSE-MIT and LICENSE-APACHE (the repo had neither), NOTICE with the
third-party list, docs/brand/LICENSE-brand.txt for the ArtCraft logos.
- ATTRIBUTION.md: every bundled asset with path, title, author, source and
license (Inter, JetBrains Mono, Lucide icons, SCOWL, the CMYK ICC profile,
the app icon, the original slice-knife glyph, brand files, screenshots).
- README: the standard "License and credits" section; badge alt text.
- Fuzz crates (outside the workspace) declare license = "MIT OR Apache-2.0".
- AGENTS.md and docs/contributing.md point new assets at ATTRIBUTION.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>