Commit Graph
27 Commits
Author SHA1 Message Date
9441484d16 Quick Mask: an empty start and Select All stay apart (#2743) (#2922)
* Keep Quick Mask's empty start and Select All apart (#2743)

Quick Mask stored "no selection" as an all-white mask, the same as Select
All. With Color Indicates Selected Areas that white was coloured over the
whole canvas, and leaving Quick Mask turned any all-white mask (a real
Select All too) into no selection.

The document now records that Quick Mask was entered with no selection.
The empty mask is white for Masked Areas and black for Selected Areas, so
neither shows colour; switching the option on an untouched empty mask
keeps it empty. On exit only that untouched white start becomes no
selection, so Select All survives the round trip. The flag is saved with
the document.

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

* Quick Mask tests: pass the active layer to channel_view::render (new main signature)

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

---------

Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 12:51:18 -04:00
60339cf675 app.open: open directory .pcraft through the automation read root (#2490) (#2581)
* app.open: open directory .pcraft through the automation read root (#2490)

The native on-disk format is a directory, but the automation read path
serves regular files only, so agents could not open the files the app
itself writes. The format crate's storage seam is now public (ByteSource
+ load_bytes_source); the workspace opens the .pcraft subdirectory through
the held capability and loads through it. app.open tries the directory
form for .pcraft paths and falls back to the bytes path for the
single-file (zip) form; every other format is unchanged.

* app.open .pcraft: review fixes (#2581)

read_entry now bounds the read itself (the stat length is only a buffer
hint) and both directory sources reject non-regular entries, so a crafted
bundle with a FIFO entry can no longer block the open and stream unbounded.
The desktop opener runs under the crash guard like its import sibling, the
zip fallback is structural (Ok(None) instead of an error-message match),
a directory-form document no longer arms a save-back target the file-only
automation writer could never replace, and the open status is translated
(Opened {name}, 16 locales).

* read_entry: saturating max+1 so a usize::MAX budget can't overflow

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-11 09:12:59 -04:00
09d079278e fix(video): retain original footage for Restore Frame and Show Altered Video (#3192)
* fix(video): retain original footage for restore and comparison (#3059, #3060)

* Clamp original-frame inserts to the original stack; cargo fmt

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-11 05:02:29 -04:00
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
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
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
shmuelbiller1andGitHub 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
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
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 GorevskiandGitHub 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
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
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
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 ThomasandGitHub c08322b7bc PSD manual and optical kerning: import, export, render, edit (#206) (#259) (#259) 2026-10-05 19:31:53 -04:00
Brandon ThomasandGitHub 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
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 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 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 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 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