Commit Graph
40 Commits
Author SHA1 Message Date
ae96037672 Expose native shape stroke controls with staged previews (#2218)
* Expose native shape stroke controls with staged previews

* Document shape stroke defaults in MCP control schema

* Regenerate docs/scorecard.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: optimalbytee <optimalbytee@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 06:42:52 -04:00
Harrison O'Connor-HooverandBrandon Thomas 607a8542f5 Keep duplicated Smart Objects linked to shared contents (#2222)
* Keep duplicated Smart Objects linked to shared contents

* Share Smart Object source update code

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 05:05:56 -04:00
shmuelbiller1andClaude Opus 5.5 3e9359071c Native format: deflated ZIP entries read with an unbounded budget, no huge preallocation (#1956)
Reading a deflated .pcraft entry with max = usize::MAX no longer overflows the take() limit, and a
forged size in the ZIP header no longer reserves gigabytes up front (preallocation is now capped).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 15:19:43 -04:00
Max MurphyandClaude Opus 5.5 0050f29a85 Native format: reject a leading '+' in hex pairs (#1832)
u8::from_str_radix accepts a leading '+', so "+5" decoded as 0x05; unhex now requires
two ASCII hex digits per pair and fails as corrupt otherwise. Fixes #1818.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 12:03:52 -04:00
Elvis33LEandClaude Opus 5.5 7533254791 Keep PNG text metadata through document saves (#1684)
PNG tEXt/zTXt/iTXt entries (and TIFF ASCII tags) were dropped on open with a warning. The
document now keeps them in Metadata.text, the .pcraft manifest saves them (older files still
load), and PNG/TIFF saves write them back; Metadata: None and embed_metadata=false strip them,
and PSD/PSB and palette PNG exports warn that they are dropped. XMP in a tEXt or zTXt chunk is
read as XMP too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:53:37 -04:00
Brennan StehlingandClaude Opus 5.5 9fee51c308 macOS: save to SMB shares that refuse a full flush (#1639)
sync_all on Apple platforms asks for F_FULLFSYNC, which SMB shares (ENOTSUP) and some virtual
file systems (ENOTTY) refuse, so every save there failed. atomic::sync_file falls back to a
plain fsync for exactly those errors, as Apple's removefile does; the automation workspace
writes use it too. rustix is already in the lock on Apple targets. Fixes #1336.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:36:03 -04:00
shmuelbiller1 83d5c5436d fix(autosave): report background write failures and retry unchanged revisions (#1577)
* fix(format): expose asynchronous autosave completion and enqueue errors

* feat(ui): accept nonblocking background autosave outcomes

* fix(desktop): pass worker autosave results to shell

* fix(ui): confirm autosaves only after successful writes and retry failures

* test(desktop): retry failed background autosave with unchanged revision

* test(format): report async autosave write failure and successful retry

* style(ui): compact autosave iterator chains under project rustfmt width

* style(format): apply rustfmt for autosave completion iterator

* fix(format): simplify async autosave completion types for Clippy

* test(format): allow slow Windows autosave worker time to finish

* test(desktop): use bounded wall-clock deadlines for background autosave checks

* style(format): match rustfmt for autosave worker signature
2026-10-09 13:24:30 +07:00
kayrockscreenprintingandClaude Opus 5.5 b2003a4329 .pcraft: a tile index outside the i32 pixel range is a corrupt file (#938) (#1483)
The loader accepted a manifest tile at any i32 index. TileCoord::rect
multiplies the index by the tile size, so an index past ±2²³ loaded fine
and then panicked ("attempt to multiply with overflow", geom/src/lib.rs)
wherever the layer's bounds were asked for: the status bar, Properties,
edit.transform, the compositor. Release builds wrapped to a rectangle
unrelated to the tile.

Loading now refuses a tile whose rectangle doesn't fit in i32 on both edges
(indices -8388608 ..= 8388606) with FormatError::Corrupt, as it refuses a
duplicate tile. In-app editing can't produce such an index; only a damaged
or crafted manifest can.

The test moves a raster layer's tile to just past each end of the range and
to i32::MAX / i32::MIN (refused), and to the extreme valid indices (loaded,
bounds computed). Before the fix the out-of-range bundles loaded, and asking
the layer for its content bounds panicked at the multiply.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:01:51 +07:00
f5c3d37c5a Add layer and group color labels (#1283) (#1289)
* Add layer and group color labels (#1283)

* i18n(pl): layer colour label rows for the Polish catalog now on main

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>
2026-10-08 20:32:09 -04:00
b0680fd421 Advanced Blending: knockout, blend as group, transparency shapes, masks hide effects (FILE-215-16, part of #215) (#903)
* 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>
2026-10-08 20:08:03 -04:00
9e6bfdc76c fix(format): refuse to save a document holding a NaN or infinite float (#1101) (#1172)
serde_json writes a non-finite float as `null`, and the manifest's plain
`f32`/`f64` fields refuse `null` on load. So a document that picked one up
(a PSD `expA` block, a descriptor double) saved and autosaved with a success
report, and every later open failed with `manifest JSON: invalid type:
null, expected f32`. JSON has no spelling for these values, so the save is
refused instead of writing a file that can never be read back.

`finite::check` walks the manifest with a serde `Serializer` that produces
nothing and fails on the first non-finite float, tracking struct fields,
sequence indices, variants and map keys so the error names the value
(`document.layers[2].content.adjustment.exposure`). `PcraftWriter::prepare`
runs it before serialising, so one-shot, incremental, directory and
autosave saves all share it. New `FormatError::NonFinite { path }` with an
actionable message.

Tests: unit (`finite_values_pass_and_each_position_is_named`: each
container kind reports its path) and integration
(`a_non_finite_float_is_refused_at_save_rather_than_breaking_every_later_load`:
NaN/-inf in an Exposure layer and +inf in `resolution_dpi` are refused with
the field named, through both `save_to_bytes` and `PcraftWriter`, and the
finite document round-trips as before). The integration case was the
issue's repro: on main it saved and failed to load.

Co-authored-by: wstock <1394687+wstock@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 17:05:04 -04:00
Elvis33LE 3ef631619e Autosave: a thread that fails to start is reported, not a silent no-op (#1124)
Autosaver::new discarded the spawn result with .ok(): under thread or
resource exhaustion the recovery channel pointed at a dead receiver,
every send failed silently and last_result stayed None - crash
recovery was off while looking on. The failure is now recorded where
last_result/flush report it (via the same note helper the worker uses),
so health checks and flush surface it.
2026-10-08 16:09:27 -04:00
57452bfd22 Bound layer-group nesting at import and Group Layers (stack-overflow guard) (#796)
* Bound layer-group nesting at import and Group Layers (stack-overflow guard)

Everything that walks the layer tree (Document::walk behind nearly every
command, the layers panel, PSD export, .pcraft save/load) recurses once per
level, but nothing bounded how deep a document could get: a PSD whose records
nest groups a few thousand deep (each level costs only a divider + folder
record pair) overflowed the stack in the importer's build recursion, in
apply_link_groups, and again in every consumer - an abort no panic hook or
fuzz run catches. The one cap that existed lived in the .pcraft save path
(MAX_GROUP_DEPTH = 100), so such a file even imported 'fine' and then bricked
saving.

- photocraft-doc now owns the limit (MAX_GROUP_DEPTH = 100, the value the
  format crate already enforced) plus Document::max_group_depth(), an
  iterative depth measure that cannot itself overflow.
- PSD/PSB import rejects files nested deeper than the cap with an actionable
  Unsupported error, mirroring what the .pcraft loader already does; the
  never-fail psd_to_document keeps working on such files by capping the
  recursion and warning about the unimported remainder (this was the overflow).
- apply_link_groups (resource 1026) recurses over the same tree and is
  bounded the same way.
- layer.groupLayers refuses to nest past the cap (checked inside the edit, so
  a rejected call records no history step and leaves the document untouched).

* Enforce the group nesting cap in layer.moveTo and Artboard from Layers

Moving a group beside or into a deeply nested layer, or wrapping a deep
group in an artboard, could still build a tree deeper than MAX_GROUP_DEPTH.
A shared check_group_depth helper now rejects those edits with an error and
records no history step, like layer.groupLayers.

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>
2026-10-07 18:46:45 -04:00
Tanish MankuandClaude Opus 5.5 b6f5df3b3f pcraft directory saves: re-verify a rolling share of cached objects; stronger file signature (#821)
Follow-up to #752. After a directory save verifies an object, later saves trust it while its
file signature is unchanged. A file damaged in place with the same length and modification
time (bit rot, or a tool that restores timestamps) was trusted until the writer was recreated.

- The signature also carries inode, device and change time on Unix, which an in-place rewrite
  changes even when the modification time is restored.
- Each directory save re-reads and re-checks cached objects, oldest-checked first, up to
  DEFAULT_REVERIFY_BUDGET (16 MiB of compressed data; at least one object), so over enough
  saves every object is read again and a damaged one is rewritten. Repeat saves of small
  documents stay a full check; large ones pay a bounded cost per save.
  PcraftWriter::set_reverify_budget tunes it (0 = trust signatures, u64::MAX = check all).
- PcraftWriter's doc comment states what the cache trusts.
- Tests: a same-size, same-mtime tamper is repaired on the next save (fails with the budget
  at 0 on Windows, where there is no change time), and with a 1-byte budget the rolling
  re-check reaches the damaged object within one pass over the objects.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:20:36 -07:00
Alex GorevskiandCopilot 1b24b5212b Repair corrupt directory-save objects (#752)
* Fix directory save reuse of corrupt objects

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: e77cd34a-ce3e-4981-996f-4d37d49df90d

* Avoid revalidating unchanged directory-save objects

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: e85c024b-ace5-4415-9208-b8d4e4684505

---------

Copilot-Session: e77cd34a-ce3e-4981-996f-4d37d49df90d
Copilot-Session: e85c024b-ace5-4415-9208-b8d4e4684505
2026-10-07 17:13:09 -04:00
3ad26e3b51 Fix document run and ID overflow (#684, #685) (#749)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Co-authored-by: Tanish Manku <tmanku.dev@gmail.com>
Copilot-Session: e77cd34a-ce3e-4981-996f-4d37d49df90d
2026-10-07 13:49:46 -07:00
ljkxandClaude Opus 5.5 a8afec4455 Open documents in bounded reads, with a clear out-of-memory error (#375) (#401)
Every open path read the whole file with std::fs::read, which asks
Windows for up to 4 GiB in a single ReadFile. Some file systems (exFAT,
network redirectors) reject a read that large with
ERROR_INVALID_PARAMETER, "The parameter is incorrect" (os error 87),
as reported for a 4.75 GB PSB in #375.

Add photocraft_format::read_file and read_all beside atomic_write:
64 MiB reads into a buffer reserved up front, and an OutOfMemory error
that says how much the file needed. Route every document read through
them:
- the background open job (engine jobs.rs read_path);
- the engine's read_file;
- Open Recent and OS open events;
- desktop drag and drop, which now goes through open_path (and so
  through the background job when jobs are on);
- the File > Open dialog;
- .pcraft loading;
- the automation workspace read and automation::files::open;
- smart object Replace Contents and linked files.

The dialog no longer fails silently: PickOpenFn returns the picked name
with the read result, and a failure is reported through open_failed and
the notices like any other open failure.

Speed is unchanged (1.11 s vs 1.13 s for a warm 4.75 GB file).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 07:40:35 -04:00
5856866f89 Smart objects keep their four-corner (Distort / Perspective) placement (#426)
* Smart objects keep their four-corner (Distort / Perspective) placement

A smart object's placement was an affine transform, so a perspective
placement lost its fourth corner: a mockup PSD's distorted screen looked
right on open (Photoshop's cached pixels) but re-rendered as a
parallelogram after Edit Contents → Save.

- SmartObject gains `perspective`, the full projective map (source px →
  document px); `transform` keeps its affine approximation
- Re-renders go through it (place_source_projective), as do moves,
  canvas rotations and edit.transform, so Distort / Perspective now work
  on smart objects instead of being refused
- PSD import reads the quad (nonAffineTransform / Trnf) and export writes
  it back; .pcraft stores the field (absent for affine placements)
- Test: a distorted smart object survives Edit Contents → Save

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Smart objects: warp maps through a distorted placement; perspective round-trip tests

Warp handles and stored warps on a Distort / Perspective smart object go through its full
projective placement (smart_cmds::placement) instead of the affine approximation, so they sit
on the distorted corners. Tests: SoLd perspective round trip and quad_moved (io), a perspective
smart layer in the .pcraft rich_doc, warp on a distorted smart object (engine). Comments no
longer name another product.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Jason Short <3344856+jason4short@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-07 06:58:01 -04:00
BFlatandClaude Opus 5.5 2868220032 Formats: deep .pcraft groups reopen; zero-size PSD rejected; JPEG mattes over white; no-ICC formats get sRGB (#517, #519, #521, #522) (#555)
* 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>
2026-10-07 15:06:55 +07:00
BFlatandClaude Opus 5.5 bd1322a5e4 Recovered documents keep their recovery data until saved or closed (#444) (#549)
Launch no longer deletes recovery entries right after loading them. A
recovered document adopts the entry it came from: its next autosave
replaces it in place (atomically) and saving or closing the document
removes it, so a second crash before the next autosave loses nothing.
New documents autosave under per-launch keys, so a document id reused
from an earlier launch never overwrites a surviving entry, and sidecars
whose key doesn't match their file name are ignored.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:53:48 +07:00
Brandon Thomas c08322b7bc PSD manual and optical kerning: import, export, render, edit (#206) (#259) (#259) 2026-10-05 19:31:53 -04:00
Brandon Thomas dbb8911f2a Live gradients: Gradient tool makes editable Gradient Fill layers (on-canvas handles, stops, midpoints) + Classic mode (#180) (#197) (#197) 2026-10-05 12:04:52 -04:00
Brandon Thomas 82270711de Atomic saves: one crash-safe atomic_write for every document write (#230) (#230) 2026-10-05 11:16:13 -04:00
MikeeI 28a63fe27e fix(io): recognize reordered PhotoCraft archives (#137) 2026-10-05 01:37:25 -04:00
Brandon ThomasandClaude Opus 5.5 37c8f81032 Image › Adjustments overhaul: full params, one editor per kind, live-preview dialogs, Curves point deletion (#68)
* Adjustments: full parameter sets for every kind, Hue/Sat ranges, CMYK/Lab tone channels

- doc: Hue/Saturation gains six colour ranges (HueRange), Gradient Map a dither flag,
  Levels/Curves a ToneSpace (RGB, CMYK inks + black, Lab) for mode-native channels.
- engine: new adjust_params module (from_params over a base / to_params) used by
  image.adjustments.*, layer.newAdjustmentLayer.* and layer.setAdjustment. Curves,
  Color Balance, Channel Mixer, Black & White, Gradient Map and Photo Filter now honour
  their params; bad params are BadParams errors; setAdjustment merges (empty = reset).
- compose/gpu: hue-range tables, CMYK/Lab tone channels (CPU; GPU falls back), dithered
  Gradient Map, Black & White from grey/secondary/primary parts; GPU parity extended.
- io: hue2 ranges, grdm dither, CMYK/Lab Levels/Curves records round-trip.
- destructive adjustments use the document's tone transfer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Adjustments UI: one editor per kind for Properties and Image › Adjustments dialogs

- adjust_editors: Brightness/Contrast (legacy), Levels and Curves (per channel in RGB, Gray,
  CMYK and Lab, histogram behind), Exposure, Vibrance, Hue/Saturation (master + six ranges with
  draggable range sliders, colorize, before/after spectrum), Color Balance (tones, preserve
  luminosity), Black & White (six weights, tint colour), Photo Filter (presets or colour,
  density), Channel Mixer (outputs, constant, monochrome, total), Posterize, Threshold
  (histogram), Gradient Map (stops, presets, reverse, dither). Replaces the M7 placeholder.
- Properties previews live (GPU) while a control moves and commits one setAdjustment per gesture.
- adjust_dialog: Image › Adjustments opens the same editor modally; the canvas previews the
  real command on the proxy, Preview checkbox, OK = one history step, Cancel leaves the doc.
- Curves (#43): hit-test at the press origin, Delete/Backspace removes the selected point when
  the graph has focus, Cmd/Ctrl-click removes, dragging off removes (and back on restores),
  arrows nudge; endpoints and the last two points stay; pressing the gradient bars does nothing.
- tone: histograms per channel space, of the image below a layer or of the target layer alone.
- kittests: every adjustment layer kind edited through the Properties panel with real input at
  8/16/32-bit RGB, Gray, CMYK and Lab (#12); Curves point interactions; dialog preview/OK/Cancel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* io tests: adjustment round trip covers the new Levels/Curves/Hue-Sat/Gradient Map fields

#60's round-trip tests predate #68's doc-model additions (tone space and black channel,
Hue/Saturation ranges, Gradient Map dither); fill them with their defaults.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 16:08:40 -04:00
Brandon ThomasandClaude Opus 5.5 69f0e25d5f Never crash: psd, codecs, format, io are panic-free (Rule 9) (#34)
- 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>
2026-10-04 13:25:17 -04:00
Brandon ThomasandClaude Opus 5.5 1c09936154 Clippy 1.99: chunks_exact with a constant size -> as_chunks; drop a no-op Option filter
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>
2026-10-04 11:33:14 -04:00
Brandon ThomasandClaude Opus 5.5 f7eb6ea9e7 Format: adopt the craft-family rustfmt.toml and format the workspace
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>
2026-10-04 11:31:17 -04:00
11e5af23ad Layer Style › Blending Options › Blend If (#26)
Model Blend If (This Layer / Underlying Layer ranges, split points) as
Layer::blend_if, read and write it as the PSD layer-record blending
ranges (previously parsed but dropped on export), save it in .pcraft,
render it in the CPU compositor (premultiplied mix = compositing at
k × alpha), fall back to the CPU on the GPU path, and drive it from
layer.layerStyle.blendingOptions {"blendIf": …}; document.inspect
reports it.

Co-authored-by: storybored379 <storybored379@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 10:13:27 -04:00
Brandon ThomasandClaude Opus 5.5 cf93edc99c Licensing: MIT OR Apache-2.0 files, NOTICE, ArtCraft brand terms, attribution
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>
2026-10-04 10:06:17 -04:00
Brandon ThomasandClaude Opus 4.8 d549c60a24 Video layers persist in .pcraft (frame stacks round-trip)
VideoDataM stores each frame via the surface blob store (surface_m /
self.surface), with source/fps/show_altered; threaded both directions in
layer_m and the loader. A saved video layer reloads its full frame stack,
timeline and footage source. Round-trip test covers pixels + metadata.
format 16 tests, clippy + wasm green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-03 03:05:01 -04:00
Brandon ThomasandClaude Opus 4.8 9cfbe11bf1 Layer > Video Layers: frame stacks + Rasterize Video (parity 618)
Layer.video (VideoData: frame surfaces + fps + source); the layer's
displayed content is the frame at the timeline playhead, re-synced after
any edit/navigation. Commands: New Blank Video Layer, Insert Blank /
Duplicate / Delete / Restore Frame, Restore All Frames, Show Altered
Video, Rasterize (Layer > Video Layers > Rasterize and Rasterize > Video).
Timeline duration grows to fit. 4 tests; engine green, clippy clean, wasm
OK. Parity 609 -> 618 (98.9%); FLOOR 613. (File-based footage + Render
Video + Video Frames to Layers + .pcraft frame persistence are follow-ups.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-03 02:43:52 -04:00
Brandon ThomasandClaude Opus 4.8 39a014d7fc Timeline foundation: document timeline model + engine commands
Document.timeline (fps/duration/playhead/work-area), threaded through
.pcraft. Engine timeline.{create,delete,setFrame,next/previousFrame,
setProps,info} (navigation is non-history, scriptable for frame-range
rendering); reported in document.inspect. Foundation for Layer > Video
Layers + the Timeline panel (follow-up arcs). doc 4 + engine 2 tests;
format/doc/engine green, clippy clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-03 00:55:04 -04:00
Brandon ThomasandClaude Opus 4.8 79fd79c2ae Variables and Data Sets: data-driven graphics (Image > Variables, data-set import/export)
Clean-room implementation of Photoshop's data-driven graphics, headless
and scriptable for CLI/MCP batch generation:
- doc: Variables model (visibility/text/pixel-replacement bindings + named
  data sets), threaded through .pcraft (manifest + convert).
- engine variables_cmds.rs: image.variables.define, image.variables.dataSets,
  image.applyDataSet (visibility/text/pixel replace w/ fit/fill/conform/as-is
  + 9 alignments, one history step), file.import.variableDataSets (CSV),
  file.export.dataSetsAsFiles (apply each set and export), variables.list.
- Pixel replacement loads+scales+aligns an image into the layer bounds via
  the existing resample helpers.
- 7 tests (doc round-trip + lookups; engine define/apply, CSV import, export,
  pixel replace, bad params). Parity 600 -> 605; FLOOR 600.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-02 16:43:22 -04:00
Brandon ThomasandClaude Opus 5.5 42266291d5 Fidelity, GPU fallbacks, interactive performance; print spool-file fix
PSD oracle 111 -> 113 (tent effect blurs, bevel height/shading, glow
range, spread rounding, text gamma, gradient span, Lab blending, colour
burn/dodge edges), channel restrictions (brst), bevel contour/texture,
chisel techniques. Vector masks, pass-through clips, artboards, pattern
fills and bevel textures render on the GPU (parity <=1/255). PNG export
7x, PSD save 4x, .pcraft save 3.6x faster; CPU effects refresh 11x;
deadlock in the effect-map blur fixed. interactive_bench example.

Print: unique spool file per call (concurrent prints of same-named
documents overwrote each other).

Workspace: 2,040 tests pass, clippy clean, layers + wasm OK, parity 600/625.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 01:26:34 -04:00
Brandon ThomasandClaude Opus 5.5 ceb2667ac4 Preset panels, character/paragraph styles, Glyphs, Check Spelling
Gradients/Patterns/Styles/Shapes/Tool Presets/Clone Source panels with
headless preset commands and a Custom Shape tool; Color card tabs match
Essentials. Character and Paragraph Styles (doc model, .pcraft, PSD style
sheets), Glyphs panel + type.insertText, Check Spelling (SCOWL-50 list).

Checkpoint also includes compiling in-progress work of the remaining agents.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 23:02:34 -04:00
Brandon ThomasandClaude Opus 5.5 aa87012aab Analysis, notes, proof views, workspaces (+ in-progress work of other agents)
Image > Analysis (ruler, count, measurement scale/log/record, scale
marker), notes (PSD Anno round trip, Notes panel, import), Modifier Keys
panel, Proof Setup plates / Legacy Mac RGB / colour blindness (Vienot),
32-bit preview options, custom pixel aspect, extras options, New/Delete/
Lock Workspace. Parity 600/625, FLOOR 595.

Checkpoint: also contains compiling in-progress work from the parallel
panels, type styles, File menu, photo, filters, UI-parity and fidelity
agents, plus photocraft-cli serve.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 22:49:07 -04:00
Brandon Thomas 6b44c393fa continued work 2026-10-01 09:16:17 -04:00
Brandon Thomas 536a7346fb more algorithms, progress 2026-10-01 08:25:06 -04:00
Brandon Thomas ae92ac20bf continued wip 2026-09-30 09:37:18 -04:00