- gpu: the test modules share one GPU_LOCK, and timing and vector_mask
take it too, so one process drives one wgpu device at a time.
- ui-egui: the wgpu tests in pattern_preview_canvas, adjust_preview_gpu,
marquee_autoscroll and layout_perf take gpu_lock().
- affinity compound_limits: the child process selects its test by the
module path (compound_limits::<name>); with --exact, the bare name
matches no test and the child passes without running the check.
- xtask scorecard reads crates/io/tests/it/corpus.rs.
- Docs and comments name the new test paths and the --test it filters.
Merges main (8f09d122d). Conflicts: the replay_save doc comment (main added
file.export.exportAs replay) and the status lines and history rows of
docs/file-format-parity.md and docs/gaps.md; both sides kept.
The docs called FILE-215-19 done and said only Save As and Save a Copy ask.
Photoshop's JPEG Options also has Matte, the progressive scan count
(Scans 3-5) and the size preview, none of which is offered, and Baseline
Optimized writes the same file as Baseline for RGB (4:2:0). The row is now
partial with those gaps named, and the docs say File > Save asks too.
docs/scorecard.md is regenerated.
Adds jpeg_options_ui::tests::the_answer_reaches_the_encoder. Every UI test
used a fake exporter, so dropping `opts.encode.jpeg_format` from
ExportSettings::io_options (the desktop and web save path) left them all
green; this test writes a real JPEG through io_options and fails without it.
Co-authored-by: Michael Higgins <55243596+Higgs32584@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
crates/engine/src/print_cmds.rs: kept main's result fields and the printServer/spooler/backend parameter text, added the bleed field and the bleed parameter text (two hunks).
crates/engine/src/print_cmds/tests.rs: both sides' appended tests kept whole.
docs/target-app-parity.md: header from main with the Bleed item added; change-log rows from both sides, newest first.
Conflicts only in the parity docs: status lines take main's text with the
JPEG Options entry (FILE-215-19) on top; scorecard takes main's rows with
FILE-215-19 done and File compatibility 10 / 12 / 5.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Save As and Save a Copy to a JPEG now ask for JPEG Options before writing,
as Photoshop does. Re-creates #4330 on current main and folds in the
unique parts of #5580 (#5563).
- codecs: `JpegFormat` (Baseline, Optimized, Progressive) on
`EncodeOptions`. Progressive writes SOF2. Baseline Optimized fits the
Huffman tables only at 4:4:4 (Gray, CMYK) or with Progressive: fitted
tables make the baseline scans non-interleaved, which zune-jpeg 0.5.15
decodes wrongly with 4:2:0 chroma (zune-image#419), as Save for Web
already works around.
- engine: a `jpegFormat` save param (file.saveACopy, Batch, Image
Processor, Layers to Files and every SaveOpts user; anything but the
three names is BadParams before writing) and a shared 0–12 -> 1–100
`jpeg_quality()` that maps NaN/inf to 100.
- ui-egui: `jpeg_options_ui` (Quality 0–12 with Low/Medium/High/Maximum
presets, Format Options), parked like Targa Options. It starts at the
opened JPEG's own quality (`file_encoding`, #4032), else the Export
preference; left there, OK writes that quality exactly and the step
records no `quality`. `SaveChoices` (Targa bits + JPEG) replaces the
bare `tga_bits` through file.save / file.saveAs / app.save, replay and
Save a Copy; automation never prompts. `write_answered_save` is shared
with Targa Options. `park_at` + `Start` let any save seed the prompt.
A File › Save that confirms the document's own flat JPEG is still
journalled as a plain `file.save` (#3531).
- CLI: `--jpeg-format`.
- i18n: five new strings in all 20 catalogs (#4330's duplicate de/es/zh
`Low`/`High` rows dropped: main has them since #4008).
- Docs: book raster-formats JPEG bullet, control protocol, file-format
parity, gaps G10, target-app parity; scorecard FILE-215-19 done.
Co-authored-by: SckraT <70698629+SckraT@users.noreply.github.com>
Co-authored-by: PierrunoYT <95778421+PierrunoYT@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copy refused fill and adjustment layers ("the active layer has no
pixels"). With no pixel selection they are now taken whole, like smart
objects, type, shape and group layers, and Paste recreates them in this
or another document. An adjustment layer's clip spans the canvas, so
paste placement and the OS-clipboard mirror stay consistent.
Scorecard UI-217-23 note, gaps.md G9 and target-app-parity.md updated;
the row stays partial (a single pixel layer still copies pixels only).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The live preview only covered selections up to 256x256 px; larger ones (the
command takes up to 16 M) errored, the UI remembered the failure and showed
nothing until release.
Engine: a preview whose selection (or landed area) is over 256x256 is now
solved on a scaled-down copy of the area around the selection and where it
lands (area-average resize of the target, or of the composite with Sample All
Layers, and of the selection), on a one-layer document of its own, then
enlarged back (bilinear, float samples, so any depth) over the selection's
shape where it lands and, for a move, where it left. The landed content is
sampled through the scaled placement, so rounding the scaled offset does not
shift it. ContentAwarePreview::refine gives the full-resolution job, with the
command's own margin, so the refined preview is the command's result bit for
bit. Every step checks the JobCtx; the session is never touched; bad params
and selections over the command's limit still error. Selections within
256x256 take the unchanged path (same output as before, checked by hash
against main). The bilinear resampler used by Transform On Drop now runs its
rows in parallel (same results).
Measured (release, 24 MP 8-bit document, Ryzen 9 7950X with other builds
running): coarse preview 600x400 0.26 s, 1500x1000 0.38 s, 3000x2000 0.69 s
(256x256 full-resolution preview: 0.21 s); the exact refine takes as long as
the command (0.8 s, 4.1 s, 9.4 s).
UI: cam_preview shows the coarse result, then starts the refine as a second
cancellable job and swaps it in; a failed refine leaves the coarse result
shown, and a failed first job is still not retried.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Photoshop's Print dialog has Functions › Bleed: crop marks print inside
the image by the bleed width, so trimming on them cuts through the
artwork and leaves no paper-white sliver. target-app-parity listed it as
missing ("No Separations/bleed").
- file.print / file.printOneCopy take "bleed" in inches (default 0),
report it back, and Print One Copy remembers it.
- Corner crop marks move onto the trim line, still starting 6 pt outside
the image. Centre and registration marks are unchanged (a symmetric
bleed doesn't move the midlines).
- One helper (corner_crop_segments) drives both the PDF writer and the
native Windows primitives; the dialog preview draws the marks there too.
- Negative, non-numeric or too-wide bleeds are BadParams, never a panic;
bleed without corner crop marks changes nothing and reports 0.
- With bleed absent or 0 the PDF is byte-identical (tested).
The Print dialog field is a follow-up: every tl! label needs all
translations.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A shape layer's path is its vector mask in Photoshop (Properties > Masks),
with its own vector-mask density and feather. PhotoCraft's ShapeLayer had
neither, so soft shapes could not be made, and stroked soft shapes from PSDs
lost both on import.
- doc: ShapeLayer gains `density` (0..1, default 1) and `feather` (px,
default 0).
- vector: the shape renderer applies them to the fill as a soft vector mask
(fill shows beyond the path at 1 - density; the feather is the vector
mask's Gaussian), so the shape's own pixels, the GPU (which samples them)
and the stroke/fill split all follow. The stroke stays crisp. The feather
blur moves from compose::masks to vector::feather and both share it and the
feathered area, so a shape and an equal vector mask render the same pixels.
- compose: soft shapes' bounds reach their fill, their layer effects follow
the soft alpha, and the split cache key includes density and feather.
- engine: `layer.vectorMask.edit` on a shape layer sets density/feather in one
history step (path/enabled/linked/invert are refused there); density is now
validated (finite 0..100) instead of clamped. `layer.vectorMask.info`
reports a shape's values.
- ui: Properties shows the Vector Mask Density and Feather sliders for a
selected shape layer in both themes (live drag, one undo per gesture).
- .pcraft: manifest and convert carry both with serde defaults (older files
load at 100 % and 0 px).
- PSD: export writes them as the vector density/feather mask parameters;
import reads them into the shape (re-rendered, as the stored pixels lack the
fill beyond the path). Soft unstroked solid/gradient shapes now import as
shapes instead of fill layers with a vector mask; pattern-filled ones stay
fill layers.
Photoshop's 10 px feather on a 100 px black square, row 100, x = 30..72:
ours is within 1 level of every measured value (8, 16 and 32 bits);
density 50 % shows 127/128 beyond the path and 0 inside.
Scorecard: TOOL-214-12 done.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The merge of main (5a4df1a3) committed unresolved conflict markers in the
status lines of gaps.md, target-app-parity.md and ui-parity.md and in the
Type/Vector rows of target-app-parity.md. Keep main's text and add this
PR's own items back: G8 Warp Text preview done (TYPE-218-7), the Type
row's evidence, and the selection-modifier and Alt-wheel rows.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Re-checked every row this PR flips against current main: the cited
tests still exist and none of the rows was flipped meanwhile.
BUG-208-4 is done too: shift_alt_before_the_drag_intersects_the_selection
(marquee_tests.rs) covers the marquee Shift+Alt intersect.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Shapes: on-canvas corner radius widgets for live rectangles (TOOL-214-12)
As in Photoshop, with Path Selection or the Rectangle tool the active shape
layer's live rectangle shows a round widget at the centre of each corner arc
(a few screen points inside a sharp corner). Dragging one rounds every corner
alike, Alt drags that corner alone; the radius follows the move along the
corner's diagonal from the press, within half the shorter side. The drag
previews the new outline and commits one shape.edit with radii on release,
one history step; a click changes nothing.
Docs: ui-parity row done, TOOL-214-12 note updated (still partial: shape
feather), target-app-parity, regenerated scorecard.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Corner radius widgets: every selected rectangle, Move tool, cursor and readout
As in Photoshop:
- every selected live rectangle shows its widgets, also with the Move tool
when Show Transform Controls is on;
- dragging any widget gives every selected rectangle the same radius (each
capped at half its own shorter side); Alt changes that corner on each;
all the shape.edit calls share one history step (coalesce);
- the pointer over a widget is the arrow with a rounded-corner badge, and a
drag shows an "R: 25.0 px" readout;
- while dragging, the transform box and the Move rollover highlight hide, and
the Move tool's snapping started by the press is dropped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
None of the five rows was flipped on main; every test the notes cite
exists on main. Main made TYPE-218-1 (type on a path) partial, so the
Type recount in gaps.md G8 and the target-app-parity Type row now say
5 done, 3 partial, 1 missing and name it partial. Header lines of
gaps.md and ui-parity.md keep main's entries plus this PR's.
docs/scorecard.md regenerated.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Background Eraser: main now samples and matches against the pre-stroke pixels (#4552); the
shared dab effect takes the pre-stroke surface so the live preview, whose working areas are
per dab, reads the same pixels as the commit.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Conflicts in docs/gaps.md, docs/target-app-parity.md and docs/ui-parity.md:
kept main's status lines and rows and this branch's Type, selection-modifier
and Alt-wheel evidence. docs/scorecard.md regenerated.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Third-party action sets that load since 3204231 now play. Four TrueGrit
Texture Supply sets (Debaser, Distress Press, Halftone Zine Machine,
Transparency) go from 223 to 3 unmapped steps of 533 and play end to end.
- .atn import, 108 -> 112 events: Select and Mask (refineSelectionEdge),
Filter Gallery Spatter (GEfc), Ripple, Crystallize; targeting the layer
mask or composite channel, loading the targeted channel, Step
Backward/Forward, removeFromSelection (it silently imported as a plain
select), Pattern Fill layers (by name), nudges in points, Layer From
Background, Grayscale/Bitmap mode objects, Image Size by resolution.
- Engine: layer.translate units pt; layer.select mode remove;
select.loadSelection channel targeted; image.imageSize keepPrintSize;
layer.new.layerFromBackground ifPresent/select; pattern.import data.
New from Clipboard keeps the copied document's resolution (was 72 ppi).
- Filters: Spatter's Smoothness smooths the displacement (torn but
attached edges, as Photoshop's); Crystallize fills a cell from its point
instead of the average, so masks keep hard edges.
- UI: .pat opens/drops into the Patterns panel (no UI import existed).
- Docs: gaps G11, file-format and target-app parity.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Free Transform: H and V skew fields in the options bar
Photoshop's Free Transform options bar has H and V skew fields after the
angle; ours stopped at the angle (ui-parity: "Options bar H/V skew
fields | missing", gaps G5).
The bar now reads the box as Photoshop does: the frame scaled (W, H),
skewed (H, V) and rotated about the reference point (`Parts`). Typing a
skew leans the box about the reference point and keeps W, H and the
angle. A box can be read with any V skew, so the session keeps the typed
one (`TransformSession::skew_v`, serde default). Skews stop at ±89°.
A sheared box used to read as taller (H = 100/cos h %); it now reads as
an H skew with H at 100 %. Readouts without skew are unchanged, flips
included.
Docs: ui-parity row done, G5 and target-app-parity notes updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n(sr): translate the Free Transform skew field tooltips
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
- image.imageSize takes percent (both sides; an explicit side wins).
- actions.conditional {if, then, else}: Photoshop's Insert Conditional as a step, 33 conditions
on the document (shape, mode, depth, selection, layers, alpha channels, unsaved) and the
active layer (kind, locks, visibility, masks, effects); a failing branch stops the caller.
- .atn events: placeEvent → file.placeEmbedded/placeLinked (path, scale), open → file.openAs,
imageSize in percent, fill from a point → paint.bucket (pixels), conditional. 105 → 108.
- Panel labels conditional steps as 'If cond → then / else'.
- edit.transform gains the relative form (scaleX/scaleY %, rotate, dx/dy about the frame
centre), as a Photoshop action records a Free Transform; image.applyImage takes a source
layer by name.
- .atn events: transform (layer → translate or relative transform; selection →
transformSelection), applyImageEvent, export (Save for Web, with the txtu path record
decoded and written back; kept verbatim without a destination), newPlacedLayer /
placedLayerEditContents / updatePlacedLayer, close with the Save answer → save + close,
delete of the target channel → layer mask delete, Quick Mask on/off, polygon selections →
select.lasso, Gradient Map (direct and as an adjustment layer). 92 → 105 events.
- Export tries every event listing a command id (Quick Mask on → set, off → clearEvent).
- Docs: event counts in the parity docs.
* 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.
paint.remove takes several strokes at once (`strokes`) and fills them as one job and one undo
step, so the fill sees every hole together; and `newLayer` writes the fill to a new layer above
the target, only where the hole is, leaving the target as it was (with Sample All Layers it
fills from the composite). The options bar shows Photoshop's three options; with Remove after
each stroke off, strokes collect as a tint on the canvas until Enter or the ✓ applies them and
Esc or ⊘ drops them. Strokes are dropped when the tool or document changes under them.
Pattern Stamp (aligned through LiveRetouch, Impressionist dab by dab) and
Background Eraser now paint during the drag; the release commits exactly the
pixels the live stroke showed (per-pixel tests at 8 and 16 bits). Spot
Healing keeps healing on release and shows a dark overlay while dragging.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Color Sampler tool (#1046)
Add the Color Sampler to the Eyedropper (I) flyout: click to pin numbered
sample points to canvas pixels, drag to move, Alt-click (or drag off-canvas)
to remove, Clear All in the options bar, at most 10 per document. Each point
reads one composite pixel (RGB 0-255 and naive device CMYK, same as the
pointer readout) and the Info panel lists them under their number with the
position in the ruler unit; the readouts refresh when the document changes.
Points are per-document engine view state (DocState.color_samplers): they
belong to their document, are discarded on close, and are not saved to
.pcraft or PSD. They are driven by commands shared by the UI, CLI, control
channel and MCP (view.colorSamplers.add/.move/.delete/.clear/.list); the
mutations change no pixels and add no History step.
Points follow the canvas geometry (crop, Canvas Size, Image Size, Image
rotation/flip, rotated crop) through a pending Session.sampler_map that
Session::edit applies before culling the points that land outside the new
canvas; the count removed is reported in the status bar.
- engine: new sampler_cmds.rs (commands + shared readout helpers) and the
sampler_map wiring in lib.rs, image_cmds.rs, mode_cmds.rs, commands.rs,
analysis_cmds.rs (compound rolls points back on error).
- ui-egui: Tool::ColorSampler + original sampler.svg icon, toolbar slot,
Info panel list, options bar, pointer handling, overlay markers, cursor.
- docs/scorecard.md regenerated (TOOL-1649-1 no longer says the tool is
missing). No menu item, so parity is unchanged (627/627).
* Color Sampler follow-up: tool count, README, i18n, roadmap
The full ui-egui suite (not just the filtered tests) surfaced these gaps:
- state.rs: Tool::ALL.len() assertion 52 -> 53.
- README.md: "52 tools" -> "53 tools" and add Color Sampler to the list,
so readme_tool_list_matches_tool_all passes.
- i18n: the options-bar hint, "{n} of {max} sample points" and
"No document open" need entries in every catalog that claims complete
menus (every_tl_literal_is_translated); added to all 16 (coverage
1584/1584). The translations were drafted alongside the tool.
- docs/roadmap.md: drop Color Sampler from the missing-tools list and note
it landed.
* Color Sampler: fit main's new tools and Image Size resample type
Reset Tool covers ColorSampler, the test ViewXform sets aspect, and
Image Size checks Method::None instead of an Option.
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>
Convert Point is a tool of its own, built on the existing path.convertPoint
edit that Pen + Alt already drives. A click makes a smooth anchor a corner, a
drag from an anchor pulls out smooth handles, a drag on a handle moves it
alone. The tool selects the anchor it converts so its handles show; a press on
empty canvas or a segment does nothing, and Cmd/Ctrl is Direct Selection, as
with the Pen. It shares the Pen's key (P).
Original glyph with an ATTRIBUTION.md row; tool name in all catalogs; README
tool list and the G6 / ui-parity / roadmap counts updated (tools 56 of 68, the
earlier 53 was stale).
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>