The file-open path rejects PSDs nested past MAX_GROUP_DEPTH before
conversion, but the public psd_to_document built file.layer_tree() to the
file's full depth (~16000 levels) before Ctx::build capped it, and layered
TIFF reached it with no preflight at all.
- photocraft-psd: PsdFile::layer_tree_max_depth(n) builds the tree
iteratively and drops the children of groups nested n deep, so walking
or dropping it recursively is bounded. layer_tree() is the uncapped case.
- psd_to_document uses it with MAX_GROUP_DEPTH, the same cap Ctx::build
and apply_link_groups apply, so imports are unchanged.
- Layered TIFF rejects nesting past the cap with the same error as a PSD
(shared psd_import::check_group_depth).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
validate_planes bounded its zlib loop by the uncapped declared size, so a
PSD/PSB declaring more than MAX_DECODED_BYTES of merged image could make
PsdFile::from_bytes decompress far past the decode budget before any
error. decode_planes refuses such a layout with LimitExceeded anyway, so
the stream is now kept verbatim without inflating it, like an unknown
compression method. A layered file whose merged composite is too large
still opens (the importer only warns when the composite can't be decoded).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Every effect with a contour (Drop/Inner Shadow, Outer/Inner Glow, Satin,
Bevel & Emboss Gloss Contour and Contour element) now shows a contour
thumbnail with a preset picker and an Anti-aliased checkbox. Clicking the
thumbnail opens the Contour Editor: add/drag/delete points (the shared
point-curve editor), Corner per point, Input/Output, Preset, and Load…/Save…
of .shc contour files, read and written from Adobe's public File Formats
Specification (Contours). Edits go to the dialog's pending params, so the
preview follows and OK applies them in the one layer.layerStyle.replace.
- doc: Contour::Custom gains per-point corner flags (serde default).
- compose: contour_curve_lut splits the spline at corner points; CPU and
GPU effects use it.
- engine: the 12 standard presets with our own control points (older names
still accepted); `contour` params also take a curve object
{name, points, corners}; antiAlias params; Satin contour; the bevel
Contour element edits in place; contours_from_shc / contour_to_shc.
- io: PSD contour points read and write the Cnty (continuity) flag.
- psd: shc.rs parser/writer.
- scorecard FILE-215-15 missing -> partial.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(psd): Align global tagged blocks to four bytes
Use four-byte defaults for global blocks and streamed layer information.
Read global payloads without full copies while preserving block order,
explicit padding and the first layer candidate. Keep two-byte layer
alignment; test canonical padding and lossless PSD/PSB parsing.
* fix(psd): Preserve transparent composite colours and precision
* fix(psd): Check ZIP integrity and bound layer previews
* fix(io): Use document CMYK profiles for PSD fill pixels
Generated fill pixels used the default CMYK separation even when the PSD
embedded a different profile. Scope layer export to the document colour
space so cached pixels and the composite use the same profile.
Check native cached samples against an explicit ICC transform at 8 and
16 bits.
* io: refuse an empty RGB composite before splitting it into planes
chunks_exact(0) panics; an empty decoded composite now returns an error.
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>
A 16/32-bit file whose global Lr16/Lr32/Layr block failed to parse opened
as a single Background layer from the merged image with no warning: the
parse error was discarded in PsdFile::from_bytes. The raw block stayed in
the preserved global blocks, so a PSD save wrote the document's fresh Lr16
plus the unreadable original: two layer lists in one file.
- psd: PsdFile::unreadable_layer_block() reports the kept block and its
parse error.
- io import: a warning names the block and the error and says only the
merged image was opened.
- io export: a preserved Lr16/Lr32/Layr block is not re-emitted when the
document's own layers are written.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Brushes: keep the folder hierarchy of imported .abr files (#1851)
Photoshop CC brush files keep their Brushes panel folders in a `phry`
(preset hierarchy) section; the importer skipped it, so every preset of an
.abr landed in one flat group named after the file.
- photocraft-psd: `abr::parse` reads `phry` (a token stream of `Grup`
{Nm, zuid}, `preset` and `groupEnd` objects) into `AbrFile::folders`, one
folder path per preset. A damaged hierarchy never fails the parse: a stray
groupEnd, more or fewer preset tokens than presets, folders nested deeper
than MAX_FOLDER_DEPTH (32) or an unreadable section become warnings and the
affected presets land at the top level. `write_v6_folders` writes `phry`
back (`write_v6` is unchanged).
- photocraft-paint: `BrushPreset::folder`, the nested folder path inside its
group (serde default, so older JSON loads unchanged).
- photocraft-io: the file's folders nest inside the group named after the
file, as Photoshop's Import Brushes does.
- Preset store: each stored preset records its `folder`; group files written
before this change load with every preset directly in its group (no
rewrite). Paths from disk and from imported .pcpresets files are bounded.
- Commands: brush.presets.move, moveGroup, renameGroup and deleteGroup take
an optional `folder` path to move a preset into a folder, or to move a
folder among its siblings, rename or delete it. importAbr reports the
number of folders kept.
- Brushes panel and Brush Preset picker: nested folders are drawn indented
under their group (list and grid), open and close like groups, take
dropped presets, reorder among their siblings by drag and drop, and have
Rename/Delete in their context menu.
Checked against the 4-folder ABR Photoshop 25.1 exports of its default
brushes (8/6/6/8 presets): all 28 land in their folders, and a write/parse
round trip keeps them.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Brushes: bound phry folder allocation; reword the move folder param
Review on #2001:
- read_phry now runs after every section is read, so it builds folder
paths only for the presets in desc (a phry may precede desc), and the
paths it holds are capped at 16 MB; presets past the cap import at the
top level with a warning.
- brush.presets.move documents `folder` as folder names, so the
automation filesystem-path classifier no longer flags it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Photoshop writes the per-channel blend-if ranges byte for byte in either byte
order (an II TIFF holds the defaults as 00 00 ff ff). The transcoder swapped
them as u32s, turning every range into ff ff 00 00, which hides every pixel of
the layer: a Photoshop layered TIFF opened with an empty canvas.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Saving a 16- or 32-bit PSD built the Lr16/Lr32 layer payload in a temporary buffer and cloned
every global tagged block before copying them into the file. The payload is now written
straight into the output with its length patched afterwards, and the neighbouring blocks are
borrowed. Output is byte-identical: all 735 corpus PSD/PSBs re-save to the same bytes. A 24 MP
layer saves in 66 ms instead of 75 ms at 16-bit and 132 ms instead of 164 ms at 32-bit, and the
temporary 144-288 MB copy is gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- photocraft-psd: clean-room .aco (v1/v2, UTF-16 names, RGB/HSB/CMYK/Lab/Gray at 16 bits,
other spaces kept verbatim) and .ase (groups, global/spot/process, RGB/CMYK/LAB/Gray floats,
unknown models kept) readers and writers; byte-stable round trips, hostile input -> Err.
- engine: presets::swatches (colours in their own model, display/foreground conversion through
photocraft-cms working spaces, persisted with the prefs, default 40 colours in 4 groups) and
swatch_cmds (swatches.list/add/delete/rename/move/newGroup/renameGroup/deleteGroup/use/reset/
import/export) with graceful-failure tests.
- ui: swatches_ui.rs replaces the const grid (groups, tooltips, click = foreground and selected
type, Alt/Ctrl-click = background, rename/delete/move, panel menu Import/Replace/Export/Reset),
Color Picker "Add to Swatches", .aco/.ase via File > Open and drag-and-drop; strings translated
in every complete catalog.
- Scorecard FILE-215-14 -> done.
Co-authored-by: leon <claude.udder842@passmail.net>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Advanced Blending: knockout, blend as group, transparency shapes, masks hide effects (part of #215)
Blending Options > Advanced Blending now works end to end:
- doc: AdvancedBlending on every layer (knockout none/shallow/deep, blend
interior effects as group, blend clipped layers as group, transparency
shapes layer, layer/vector mask hides effects), saved in .pcraft.
- PSD and layered TIFF: knko, infx, clbl, tsly, lmgm, vmgm read into the
model and written back (in place, or added when not at the defaults);
the TIFF byte-order transcoder handles lmgm/vmgm as byte flags.
- CPU compositor (compose/src/advanced.rs): shallow and deep knockouts
(isolated groups and clipping groups stop a deep knockout), clipped
layers blended individually, effects over the whole layer when
Transparency Shapes is off, effects built from an unmasked copy when a
mask hides them, interior effects under fill opacity.
- GPU: documents using Advanced Blending fall back to the CPU compositor.
- engine: layer.layerStyle.blendingOptions takes channels, knockout and the
five switches; inspect reports them; Copy/Paste Layer Style carries them.
- UI: the Blending Options page laid out like Photoshop's (General,
Advanced Blending, Blend If with split sliders), translated.
psd-tools corpus oracle 229 -> 236 (seven knockout files now pass; floor
raised); other corpora and all round trips unchanged. Scorecard
FILE-215-16 -> partial.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Polish catalog rows for Advanced Blending; regenerate docs/scorecard.md
pl.tsv landed on main and the coverage test needs every catalog to translate new UI strings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Undo damage: compare Layer.advanced in layer_pixels_equal; regenerate scorecard
#810's exhaustive Layer destructure must classify the new advanced-blending
field; it changes the composite, so it is compared.
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 Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Review: keep unknown blocks with their byte order, io behind the engine, fixtures documented
Addresses the review on #672:
* Unknown layer blocks read from a little-endian TIFF are no longer dropped. Their bytes are
kept verbatim with the signature `MIB8` (`46B8` for `8B64`) marking the little-endian
layout (`TaggedBlock::is_foreign_order`), so a save back to TIFF restores them exactly. A
malformed known block is kept the same way. Only a save to the other byte order has to drop
such a block, with a warning: a PSD export strips them (`strip_foreign_order_blocks`), and a
big-endian block of unknown layout still cannot go into a little-endian TIFF.
* The UI no longer depends on `photocraft-io`: the layer predicate is exposed by the engine
(`file_cmds::tiff_would_write_layers`); io is a dev-dependency again.
* The layered TIFF path ends in the same `encode_image` as every flat export, so export-wide
metadata policies (such as #733's XMP choice) apply to its composite too.
* Scorecard FILE-215-7 is `partial`: extra alpha channels and a Quick Mask are not written.
* The committed fixtures get an ATTRIBUTION.md row and a README describing their contents and
how they were generated (psdtags + tifffile); the Python generator script is removed.
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
* tiff_corpus: ask for layers explicitly (headless default is flat since #825)
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
* i18n: drop the duplicate Korean "Ambient" entry that fails bundled_catalogs_are_consistent
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
* i18n: drop the duplicate Korean "Warmth" entry as well
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
* agent_tasks: tolerate Windows holding the test directory open during teardown
The three tests that remove their directory right after cancelling the client failed on Windows (CI and locally) with os error 32: the spawned server task is still closing its files. The cleanup now retries briefly and otherwise leaves the directory for the next run's tmp() to clear, as it already did at the start.
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
* preset store: a missing directory is an empty store on Windows too
DirBackend::read passed the OS message through, and Windows words a missing directory as "cannot find the path specified (os error 3)", which missing_file() did not recognise, so attaching a store on a path that does not exist yet (first launch, and two engine unit tests) reported actions.json as skipped. The backend now reports every NotFound as "<name>: not found", the wording the predicate and the memory backend already use. Regression test: opening a store on a nested missing directory warns about nothing and writes nothing.
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
* Review: keep scripts/layered_tiff_fixtures.py, drop the fixtures README and attribution rows
The four .tif fixtures those described were never committed (binary test data stays out of the repo; the tiff_layers tests read them from the gitignored corpus/layered-tiff/, written by the script that tiff_layers.rs:181 still points to).
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
* automation: the Direct Selection path commands edit vector paths, not files
#865 added path.moveAnchors, path.moveHandle, path.bendSegment and path.convertPoint, whose registry docs mention the vector path they return, so registry_filesystem_path_params_are_classified fails on main (Linux and Windows): they join the list of commands whose "path" is a document path.
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
* i18n(cs): the lower-case "{n} layer|{n} layers" plural (main red since #829)
xtask's source_key_set_is_covered_by_complete_catalogs fails on main: the key reached every catalog but the Czech one. Same forms as the capitalised twin already in cs.tsv.
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
* Drop the duplicate path.* entries from the workspace test (main already lists them)
main already has path.moveAnchors/moveHandle/bendSegment/convertPoint in this match; the
duplicates trip unreachable_patterns.
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>
real_before_parameters picked Photoshop's order (real mask first) whenever
the record length fit the parameter-flags byte at offset 36, even when that
byte was just part of the real rectangle. A density-only record in the
spec's order then read its density as the real mask's background and lost
the parameter. When both orders fit, Photoshop's reading now has to be
plausible (background 0 or 255, known parameter flag bits) or the spec's
reading with a real mask wins.
Co-authored-by: wstock <1394687+wstock@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* PSD: decode planes up to 8 GiB, so large PSBs keep their pixels (#375)
MAX_DECODED_BYTES capped a single decode at 2 GiB. Real PSBs go past
that in one plane set: the merged image of a 30000x30000 RGB file is
2.7 GB, a 33000x33000 16-bit layer channel 2.2 GB. Opening such a file
gave a document with no layers (flattened files) or layers whose
channels were "treated as empty" (layered files). Saving hit the same
cap on the encode side and silently fell back to uncompressed data.
The cap is now 8 GiB on 64-bit targets and 2 GiB elsewhere, the budget
photocraft-codecs already uses for Limits::max_alloc. It stays a
backstop: the decoders bound their output by their input on their own
(RLE expands at most 64x, Raw needs every byte present). The Raw and
RLE output buffers are reserved with try_reserve_exact, so a machine
without that much memory gets an error instead of an abort.
generated with Claude Code
* PSD import: a channel that can't be decoded leaves its layer empty (#375)
The warning says the channel is "treated as empty", but the importer
filled a missing transparency channel with opaque alpha, so a layer
whose channels all failed became an opaque black rectangle over the
document. A channel that is present but undecodable now leaves the
whole layer empty (transparent), as the warning says.
tests/large_psb.rs: the damaged-channel case (PSD and PSB), and an
opt-in release test that opens a flattened 29000x29000 PSB whose
merged image decodes to 2.52 GB (16 s, 11.6 GB peak here).
generated with Claude Code
* Large-file fixtures: generate and check 4-5 GB PSBs (#375, #216)
photocraft-psd example big_psb writes five PSBs straight from the spec,
independent of this crate's writer and its limits: flattened and
layered, raw (4.4 and 4.8 GB files, data past 4 GiB) and RLE (small
files whose planes decode past 2 GiB, up to a 33000x33000 16-bit
layer). Every sample follows one formula, so photocraft-io example
big_psb_check opens a file through photocraft_io::import and checks
pixels without a reference; with SAVE set it saves as PSB and checks
the re-opened file.
Release build, this machine: all five open with correct pixels and
survive a PSB save and re-open (5-14 s per import). Before the 8 GiB
cap, three of the five opened empty or black.
Scorecard FILE-216-2 missing -> partial: no 2 GB PSD, 150-layer or
20 MP fixture yet, and nothing runs in CI.
generated with Claude Code
* psd: ZIP channels reserve fallibly and read in bounded chunks
A small ZIP channel declaring a huge decoded size could make
zip_decompress try a multi-GiB with_capacity/read_to_end and abort on
allocation failure. Reserve with try_reserve_exact/try_reserve, read
through take(expected) in 1 MiB chunks, and report LimitExceeded
instead. Tests: absurd declared sizes, past MAX_DECODED_BYTES, garbage.
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>
* 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: read smart-filter masks (FEid) with one section per smart object
Photoshop writes the global Filter Effects block (`FEid`/`FXid`) as a
version followed by one length-prefixed section per filtered smart
object, each padded to 4 bytes outside its length. The reader took the
first section's length as the whole body, so any document with two or
more filtered smart objects failed with "filter effects: N unexpected
bytes after the data": the smart filter masks were not imported and the
open reported a warning. The 28 FEid files in the public corpora each
hold a single section, which is why the corpus never hit it.
The reader now walks every section (still accepting several items in one
section, as earlier PhotoCraft versions wrote), and the writer emits one
padded section per item, as Photoshop does.
Tests: reads_one_section_per_item (fails before: parse error) and
reads_several_items_in_one_section; malformed_data_is_an_error now cuts
only before the end of the section.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PSD FEid: an empty item list still writes one section, so it reads back
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: EthanSuquet <292885762+EthanSuquet@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.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>
* 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>
- codecs: bump tiff 0.10 -> 0.11. tiff 0.10 indexed an empty SampleFormat
list and panicked on a crafted TIFF (found by mutation fuzzing);
regression test tiff_empty_sample_format_is_an_error.
- codecs: Image::new returns Result instead of panicking on size overflow.
- format: par_compress returns an error if a compression worker panics.
- io: par_map joins every worker before relaying a worker panic, so the
thread scope can't panic on an unjoined thread.
- All four crates now deny unwrap/expect/panic/unimplemented/todo/
unreachable outside tests.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Rust 1.99's clippy adds chunks_exact_to_as_chunks and some_filter, which
broke the Clippy step on every CI runner (it uses the latest stable). All
sites are mechanical: as_chunks drops the remainder exactly like
chunks_exact.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI's `cargo fmt --all -- --check` has failed on every run because the
repo had no rustfmt config and was never formatted. Use the same settings
as EffectCraft/FilmCraft (max_width 160, use_small_heuristics Max), which
also keeps the churn small. Formatting only; no code changes.
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>