Commit Graph
943 Commits
Author SHA1 Message Date
Vikram BhaskaranandBrandon Thomas 964f9fb9d0 Sample every visible layer in Content-Aware Fill by default. (#4133)
* Sample every visible layer in Content-Aware Fill by default.

An empty layer above the photo used to report a successful fill and stay empty, because only the active layer was sampled. The result is still written to the active layer. sampleAllLayers can be set to false to keep the old behaviour.

Fixes #4026

* Sample other layers in Content-Aware Fill only when asked or the layer is empty.

Reuse the Remove tool's composite, which writes the active layer's format, and leave Photoshop's Sample All Layers checkbox off unless the window has no pixels on this layer. An explicit false still reads only this layer.

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 23:35:21 -04:00
Fabio Lissi 70e182b08f panic_hunt: don't fuzz the print commands into a real printer (#4467)
* panic_hunt: don't fuzz the print commands into a real printer

file.print and file.printOneCopy spool to CUPS, so every run of the adversarial-params test
printed a blank page on the default printer. The hunt now skips those two ids (their params
are validated like everyone else's; spooling is not what the test is for).

* Guard the print spooler itself and the CI environment

- print_cmds: disable_printing() (a process-wide flag the panic hunt sets) and the
  PHOTOCRAFT_NO_PRINT environment variable make spool_pdf refuse before running lp; dryRun
  still renders and reports the command. Test.
- CI: PHOTOCRAFT_NO_PRINT=1 and LPDEST=photocraft-no-such-printer, so a regression fails
  loudly instead of printing. Documented in docs/development.md › Environment variables.
2026-10-12 23:13:42 -04:00
15aac8d7b6 Type: type on a path, glyphs follow the curve (#4365) (#4377)
* Type: type on a path, glyphs follow the curve (#4365)

Add TextShape::Path (path, start offset, flip). The text is laid out as one
point-text line and each glyph is placed rigidly on the flattened path by arc
length, turned to the tangent at its centre; glyphs past the end are hidden and
a degenerate path falls back to straight text. New commands type.attachToPath
(work path or shape layer subpath) and type.detachFromPath. TextShape is no
longer Copy. .pcraft round-trips the new variant.

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

* Automation policy: type.attachToPath/detachFromPath take document vector paths, not host files (#4365)

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

* docs: regenerate scorecard after merging main

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>
2026-10-12 23:11:09 -04:00
e8af9d592f Themes: Soft Dark and Soft Light (#4368) (#4378)
* Themes: Soft Dark and Soft Light (#4368)

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

* docs: regenerate scorecard after merging main

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>
2026-10-12 23:06:37 -04:00
0a32b3593d Add Krita (.kra) import (#4388)
Open .kra and .krz with paint layers (8/16-bit RGBA and gray, any offset), nested
and pass-through groups, names, visibility, opacity, locks, blend modes and
transparency masks, plus the image's ICC profile and resolution. Vector, filter,
fill, file and clone layers and the other mask kinds are left out with a warning;
float, CMYK and Lab documents open as Krita's merged image (preview). Import only:
Save suggests .ora. Checked against 35 Krita 5.2.9-authored files.

Co-authored-by: Scott Campbell <251750033+scottcampbelldata@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:41:38 -04:00
F4LCON 05e98e7ade Smart Filters: select a filter, drag or ⌥-drag it onto another smart object (#4271) (#4371) 2026-10-12 22:35:54 -04:00
F4LCON b4aea45e19 Actions: record File › Save a Copy and play it back as a copy (#4373) (#4390) 2026-10-12 22:24:18 -04:00
FredApps 78c61e6df5 Swatches: an RGB group with the exact primaries and secondaries (#4302) (#4409)
The built-in "Pure" group's hues are vivid but not pure (its green is
#28C850), so picking it for text looked dull next to Photoshop's #00FF00.
Add a Photoshop-style "RGB" group (#FF0000, #FFFF00, #00FF00, #00FFFF,
#0000FF, #FF00FF) right after Grays. Pure is kept unchanged so existing
documents, actions and saved swatches that use it keep their colours.
2026-10-12 22:24:07 -04:00
Valentin KleinandClaude Opus 5.5 5aa6e33adf Content-Aware Move: live preview of the result (#4385) (#4393)
* Content-Aware Move: live preview of the result while the pointer rests (#4385)

A cancellable, debounced background job shows the move/extend result during the drag and in the Transform On Drop box.

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

* docs: regenerate scorecard after merging main

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:24:03 -04:00
Valentin KleinandClaude Opus 5.5 ee8aca562b Engine: reject unknown params for layer.hideLayers/showLayers via an opt-in registry check (#4336) (#4364)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:23:46 -04:00
cee010ef5a Replace Color: a canvas eyedropper picks the From colour (#4143) (#4247)
The dialog had no colour control at all: the engine defaulted the From
colour to the foreground and the spec's `color` param rendered nothing.
It now shows a Color row (swatch opens the Color Picker, seeded from the
foreground) plus a pipette that arms a canvas eyedropper: the next click
on the image samples the merged composite into the field, like the
Curves picker and Photoshop's Replace Color.

Co-authored-by: highperfocused <highperfocused@pm.me>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:03:15 -04:00
1cb3eaa5d2 Free Transform: apply a large layer's warp in the background, with progress and Esc to cancel (#1015) (#4218)
Committing Free Transform on a 300-550 MP layer ran the bicubic warp on the UI
thread: several seconds of a frozen window with no feedback. On a 20000x16000
layer the warp alone took about 6.6 s.

edit.transform is now a background job (edit_job): the warp reports progress per
tile and stops when cancelled. The transform session stays open while it runs,
showing the moving pixels without the box, so the layer doesn't jump back to its
old place until the result lands. The modal progress dialog shows after the
usual delay, and Esc or Cancel cancels the job. A copy or placed layer the
session made is folded into the step, or taken back, in its own document.
Undo and Redo are refused while the commit runs.

Tests and scripts run inline as before.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 21:57:47 -04:00
e90dfdcf10 feat(ui): Photoshop's type formatting keys while editing text (#4239)
* feat(engine): relative leading, baseline shift and tracking steps in type.setStyle

`leadingBy`, `baselineShiftBy` and `trackingBy` move every character in the
range from its own value, so mixed values keep their differences. Auto
leading starts from the value layout uses (the paragraph's auto-leading
percentage of the character's size), results clamp to Photoshop's ranges,
and non-finite or oversized steps are refused as bad params. `leadingBy`
follows `metricsAsShown` like `leading`.

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

* feat(ui): Photoshop's type formatting keys while editing text

While a Type tool edit is active, Shift+Cmd+K/U/B/I and Shift+Cmd+/ toggle
All Caps, Underline, Faux Bold, Faux Italic and Strikethrough on the
selection (off when every selected character has it, else on);
Alt+Up/Down step leading by 2 pt and Shift+Alt+Up/Down baseline shift by
2 pt (10 with Cmd); Alt+Left/Right over a selection track it by 20 (100
with Cmd), a collapsed caret still kerning; Cmd+Alt+Shift+H toggles
Hyphenate for the paragraphs touched. Each press is one type.setStyle
history step, and the keys no longer fall through to Color Settings,
Desaturate, Auto Color or Inverse while typing; outside an edit those
shortcuts work as before.

A collapsed caret in non-empty text changes nothing: Photoshop styles the
next characters typed, which the document model can't hold apart from the
text. In empty text the style becomes the typing style.

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>
2026-10-12 21:41:23 -04:00
8157d1a851 Free Transform: refuse outputs over a pixel budget instead of building them (#4293)
A large scale (e.g. 200x on a thin column) made the warp fill every tile of
a result up to 2^21 px on a side, enough to exhaust memory. Free Transform
now prices each warp before building anything and refuses with an
allocation-style error ("...; the document was not changed") once a result
would exceed MAX_TRANSFORM_PIXELS (2^30, the same bound as Puppet Warp's
MAX_WARP_PIXELS). Only the result's size counts, so moving content far off
the canvas still works, and nothing is clipped.

The check covers the layer, a smart object's cache and filter mask, linked
masks, a lone mask/channel target and the selection outline, and runs over
the whole layer tree before any layer is changed. Transform Selection gets
the same check. The transform box now stays open when a commit is refused.

Part of #3773

Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 21:27:12 -04:00
Valentin KleinandClaude Opus 5.5 f6db7a8115 Smart Objects Warp: mesh knots span the contents, not the skewed bounding frame (#4113) (#4341)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 21:27:03 -04:00
FredApps 96f4256ced fix(type): the Type tool's canvas pick skips text in hidden groups (#4320) (#4332) 2026-10-12 21:26:59 -04:00
MrCloudy2andClaude Opus 5.5 b79d0a0bcf feat(gradients): Photoshop's interpolation method for Gradient Overlay, gradient Stroke and the Gradient tool (#4292)
layer.layerStyle.gradientOverlay, layer.layerStyle.stroke (gradient) and
paint.gradient take "method" (classic|perceptual|linear|smooth) and
"smoothness" (0..100, default 100). The stops are baked with the same
model PSD import uses (io::gradient_bake, now public as bake_gradient and
GradientMethod), so the compositors' linear interpolation reproduces
Photoshop's curve. Without a method the stops stay as they were.

Measured in Photoshop 27.11 (max level error vs main -> this change):
Gradient Overlay linear/reflected styles, any angle/scale/reverse 12 -> 0-1,
radial/diamond 16 -> 4-5; gradient Stroke 12 -> 2-4; Gradient tool
black -> white Classic 13-14 -> 2-3; red -> blue Classic 12 -> 1,
Perceptual 83 -> 2, Linear 64 -> 4, Smooth 83 -> 3. A 3-stop Classic
gradient improves only 14 -> 10 (the multi-stop smoothness spline is a
separate follow-up).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 21:26:53 -04:00
Nigel Ihza Firdausy ab5a6c5ed2 fix(layer-picker): include text bounds in right-click layer list (#4288) 2026-10-12 21:26:49 -04:00
1412ac26e2 Save for Web and Export As remember their settings across restarts (FILE-215-18) (#4184)
* engine: remember the last Save for Web settings in the preferences

* engine: test that a complete export remembers its settings for the next launch

* ui: seed the Save for Web dialog from the remembered preferences

* ui: Export As opens with the settings it was used with last

* scorecard: FILE-215-18 done, the export dialogs remember their settings

* docs: scorecard, File compatibility 7 done of 27

* Regenerate the scorecard

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-12 21:26:47 -04:00
David MannandClaude Opus 5.5 468d1c622a Bitmap mode: validate the halftone screen angle (#4214) (#4215)
Image > Mode > Bitmap (halftone) passed its angle through unchecked: 1e39
became inf as f32, the screen rotation turned it into NaN and an almost
white bitmap was recorded in history; 500 was accepted although the
params doc (and Photoshop's Halftone Screen dialog) says -180..180.

The angle must now be a finite number in -180..=180 and the frequency a
finite number (still clamped to 1..999 lines/inch); otherwise the command
returns BadParams before touching the document.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 21:17:49 -04:00
David MannandClaude Opus 5.5 8029b061af EXR: an unnamed part can be opened by the name the part list gives it (#4210) (#4211)
file.exrParts lists a part without a name attribute as "part <index>", but
file.openExrParts only matched parts whose name attribute equalled the
requested name, so "part/part 0" failed with "no flat part named part 0".
One helper now gives the display name for the listing, the lookup and the
layer name; a named part still wins over an unnamed one with the same
fallback name.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 21:17:45 -04:00
David MannandClaude Opus 5.5 72c8f58e11 Shapes: a rectangle with a negative size extends from its origin like the other shapes (#4203) (#4204)
rounded_rect took the absolute width/height without moving the origin, so
shape.create {"kind":"rect","rect":[50,10,-20,30]} covered x 50..70 while an
ellipse or polygon with the same rect covered 30..50. Normalise (x, w) and
(y, h) for negative sizes instead. The Shape tool drag already normalises its
rect, so interactive drawing is unchanged.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 21:17:38 -04:00
David MannandClaude Opus 5.5 e4f15eecdf Vector masks: validate feather like pixel masks (#4201) (#4202)
layer.vectorMask.edit stored `feather` as f.max(0.0) as f32, so 1e39 became
inf (hard edge, null in info, .pcraft save refused) and any huge value was
accepted. It now takes the same finite 0..=1000 px range and error as
layer.layerMask.edit, checked before the edit so a bad value leaves the mask
and history untouched. The params doc says so.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 21:17:14 -04:00
David MannandClaude Opus 5.5 56869d12e3 Layer Comps: the warning icon clears only its own comp (#4197) (#4198)
The warning triangle on a comp row sent layerComp.updateWarnings
{"clear":true}, which dropped the stale states of every comp. The
command now takes an optional "comp" (id or name) that limits both the
report and the clear to that comp; an unknown comp is an error and
changes nothing. Without "comp" it still acts on every comp (Clear All
Layer Comp Warnings). The row icon sends its own comp id.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 21:17:06 -04:00
David MannandClaude Opus 5.5 71827a614c Color Sampler: keep the CMYK readout in range on 32-bit documents (#4191) (#4192)
32-bit samples can lie outside 0..1. The RGB readout clamped them, but
device_cmyk did not, so a pixel at 1.5 read K:-50% and negative values
gave C/M/Y above 100%. Clamp each channel to 0..1 (NaN reads as 0) before
the conversion; this fixes both the Color Sampler and the Info panel.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 21:17:01 -04:00
David MannandClaude Opus 5.5 60c825513c Type: every text edit respects Lock All (#4188) (#4189)
#3132 refused type.edit on a Lock All layer, but type.insertText,
edit.checkSpelling "change", Paste Lorem Ipsum (and the other
type_extra_cmds::with_text edits) and Find and Replace Text "change"
on a layer locked after the find still changed its text and recorded
history. The check is now one pub(crate) helper,
type_cmds::check_text_unlocked, called by every one of these paths
with the same "...is locked" error as type.edit.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 21:16:56 -04:00
858fbcf1ad Object Selection: hover preview of the object it would select (Object Finder) (#4087) (#4095)
* WIP: Object Selection hover preview (#4087)

* Object Selection: Object Finder hover preview and click-to-select (#4087)

Hovering with Object Selection highlights the object a click selects,
computed off the UI thread after a short rest and cached per document
pixels; a click commits it via select.object {point} honouring
add/subtract/intersect; dragging keeps the rectangle mode. Adds an
'Object Finder' options-bar toggle (default on, persisted) with
translations in every catalog.

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 21:06:58 -04:00
skylartaylor d9bc5a8cd5 Use shared native print transport and keep page composition in PhotoCraft 2026-10-13 02:56:11 +02:00
e627b01169 feat: resize Save for Web previews and custom palette editing for Gif export (#4129)
* feat: resize Save for Web previews and added palette editing for Gif/PNG8

* Save for Web: add Turkish and Hungarian rows for the preview navigation hint

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-12 20:50:00 -04:00
David MannandClaude Opus 5.5 2f0979f61f Global Light: reject angles beyond f32 range instead of storing NaN (#4181) (#4182)
layer.layerStyle.globalLight cast its params straight to f32, so an angle
of 1e39 became inf and rem_euclid turned it into NaN, which was stored,
recorded as a history step and fed to every effect using global light.
Angle and altitude are now read as f64 and rejected with BadParams when
non-finite or beyond f32 range, before the edit. In-range wrapping and the
altitude clamp are unchanged.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 20:38:05 -04:00
David MannandClaude Opus 5.5 43c7584d5d Warp: move only the selected part of a linked mask (#4175) (#4176)
With a selection, Edit > Transform > Warp warped only the selected pixels
but the whole linked layer mask, so the unselected mask no longer lined up
with the unselected pixels. Warp now limits the linked mask to the selection
through transform_cmds::move_gray_selected, as Free Transform and
Perspective Warp already do (#3083); layers that move whole still take their
whole mask along. Adds a regression test driven by edit.transform.warp.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 20:37:56 -04:00
David MannandClaude Opus 5.5 e4bfa7907f Video layers: edits keep the timeline work area (#4170) (#4171)
Every video-layer edit ran `work_end = work_end.max(duration)`, so
Show Altered Video, Restore Frame, Interpret Footage and the other frame
edits reset a narrowed work area to the full duration. A new
`grow_duration` helper, used by every video edit and by New Video Layer
from File, grows the timeline and moves the work-area end only when it
reached the old end; a narrowed work area stays where the user put it.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 20:37:52 -04:00
David MannandClaude Opus 5.5 a256f1c579 Paint Bucket and Gradient: validate opacity like the Magic Eraser (#4167) (#4168)
paint.bucket and paint.gradient read `opacity` as f(p,"opacity",100)/100
with no range check, so 200 became a coverage of 2 and overshot the blend
(#808080 on white painted near-black). Both now use the Magic Eraser's
param reader: the value is clamped to 1..100, and a non-numeric opacity is
BadParams before any edit or history step.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 20:37:47 -04:00
David MannandClaude Opus 5.5 cb3aef5753 Count tool: Marker Size and Label Size fields take effect (#4156) (#4124)
Root cause: the options bar sent markerSize/labelSize as f32 JSON floats
(8.0) and count.setGroup read them with as_u64, which is None for floats,
so the command returned Ok and changed nothing.

Fix: count.setGroup accepts whole numbers sent as floats (UIs, agents and
actions send them), rejects fractions, negatives and non-numbers with
BadParams, and clamps in u64/f64 so huge values no longer wrap through
`as u32`. The options bar now sends integers.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 20:37:29 -04:00
David MannandClaude Opus 5.5 991ebe16d5 Liquify, Puppet Warp and Perspective Warp: refuse locked smart objects (#4155) (#4123)
Root cause: run_on_layer in distort_cmds.rs added the smart filter and
returned for a smart object before it called check_locks, so a smart
object with Lock All or Lock Pixels (its own or a parent group's) took
the distortion and a history step.

Fix: check the pixel and full locks (not position) before recording the
smart filter, as regular filters do, and use the filters' error wording.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 20:37:24 -04:00
David MannandClaude Opus 5.5 1014c37eaa Find and Replace Text: backward Change/Find moves past the replaced text (#4154) (#4122)
After Replace Text the find cursor collapsed to the end of the replacement.
A backward search takes the last match ending at or before the cursor, so
when the replacement still matched (e.g. "cat" -> "Cat", case-insensitive)
it found the replaced text again and never reached earlier matches.

Collapse the cursor at the start of the replacement when searching backward
(the end when forward), mirroring how forward search skips it.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 20:37:20 -04:00
David MannandClaude Opus 5.5 426747d7d2 Auto-Select: honour layer mask density when picking layers (#4153) (#4121)
layer.pickAt (Auto-Select, the canvas right-click layer list and the Move
tool's rollover highlight) read the raw layer-mask pixel, while the
compositor uses LayerMask::value, which applies density (1 - d*(1 - v)).
A hide-all mask at density 0 shows the layer in full, yet the click went
through to the layer below.

Read the mask through LayerMask::value, as the compositor does (it also
returns 1 for a disabled mask). Regression test covers the click and the
list form.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 20:37:15 -04:00
David MannandClaude Opus 5.5 b87180e14e Swatches: reject colour values beyond f32 range so presets survive a restart (#4150) (#4118)
Root cause: SwatchColor::from_param checked that each value was finite as
an f64, then cast it to f32. A value like 1e39 became inf, and an HSB hue
then wrapped (rem_euclid) to NaN. swatches.add stored Hsb([NaN, ..]), the
prefs file wrote it as null, and on the next launch load_presets_json
failed to parse the whole presets blob, so every user gradient, style,
shape, tool/document preset and swatch fell back to the defaults.

Fix: reject values whose magnitude exceeds f32::MAX with BadParams, for
every colour model.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 20:37:07 -04:00
xsvm 776b9dddd0 Clone Stamp and History Brush: releasing a stroke commits the live pixels (#4019)
Releasing a Clone Stamp or History Brush stroke rendered the whole
stroke again, although the live stroke had already drawn exactly those
pixels. The canvas now offers the live stroke to its commit as the Brush
does (#2131), through the same LiveCommit: a commit with the same
command, document revision, params and points takes the live surface
and reports fromLive; anything else renders as before.

LiveRetouch renders only the position and pressure of its points, which
is all the commands parse, and the offer is refused when the brush, layer
or clone source changed during the stroke (the revision doesn't cover
them). The Healing Brush, which blends on release, is left out.
2026-10-12 20:05:09 -04:00
skylartaylor 3702555aa9 Make native RGB color policy explicit and preserve embedded profiles 2026-10-13 02:02:13 +02:00
SckraTandClaude 1e9f1ae486 perf(engine): read selection coverage per row when mixing edits back (#4424)
Destructive adjustments (pixels::adjusted) and the RGBA filter helper (adjust_cmds
process_surface) each had the same loop mixing the edited pixels back over the original by
selection coverage, looking the selection's tile up for every pixel (sample_channel, a
BTreeMap lookup per pixel). They now share pixels::mix_through_selection, which reads the
coverage one row at a time with sample_row_strided: one tile lookup per tile per row, and a
row-sized buffer.

Results are unchanged: a new test compares the helper with the per-pixel sample_channel mix,
bit for bit, for 8-bit, 8-bit-with-alpha and 32-bit selections over 8- and 16-bit layers,
across tile edges and untouched (default) selection tiles.


Claude-Session: https://claude.ai/code/session_01QUda3K786uop8kvEQoUjSf

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-12 19:55:21 -04:00
SckraTandClaude 3c469bfd05 perf(engine): move layers without a second document clone (#4422)
move_layers cloned the document on every layer.translate, Align and Distribute only to read
the pre-move state while type and shapes re-render. Session::edit already clones the
document for the edit, and the callers hold the starting state (an Arc), so they pass it in.

The clone copies the layer tree and every surface's tile map (pixel tiles themselves are
shared), so its cost grows with the layer and tile count: a nudge in a document with many
layers or very large layers goes from two such clones per command to one.


Claude-Session: https://claude.ai/code/session_01QUda3K786uop8kvEQoUjSf

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-12 19:55:05 -04:00
Valentin KleinandClaude Opus 5.5 dc8a3f0b35 Paint Bucket and Edit > Stroke: a CMYK foreground writes the picked inks (#4351) (#4352)
* Color Picker: CMYK foreground fills a CMYK document with the picked inks (#4196)

The tool colours are stored as sRGB, so a colour picked as CMYK 100/0/0/0
(outside sRGB) came back as 100/28/0/17 when a CMYK document was filled.
tools.setColors now takes foregroundCmyk/backgroundCmyk, the picker sends
the entered inks, and Edit > Fill (and the fill keys) write them as they
are into a CMYK target. Fill also enters the document's CMYK profile.

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

* Brush and Pencil paint a CMYK foreground with the picked inks in a CMYK document (#4344)

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

* Paint Bucket and Edit > Stroke: a CMYK foreground writes the picked inks (#4351)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:54:16 -04:00
skylartaylor 4a730912c0 Preserve PDF text and CMYK paths and reject native layout mismatches 2026-10-13 01:53:16 +02:00
skylartaylor 3c4e1536a0 Merge current PhotoCraft and preserve asynchronous native printing 2026-10-13 01:48:54 +02:00
Valentin KleinandClaude Opus 5.5 46740ad4bc Brush: CMYK foreground paints the picked inks into a CMYK document (#4344) (#4347)
* Color Picker: CMYK foreground fills a CMYK document with the picked inks (#4196)

The tool colours are stored as sRGB, so a colour picked as CMYK 100/0/0/0
(outside sRGB) came back as 100/28/0/17 when a CMYK document was filled.
tools.setColors now takes foregroundCmyk/backgroundCmyk, the picker sends
the entered inks, and Edit > Fill (and the fill keys) write them as they
are into a CMYK target. Fill also enters the document's CMYK profile.

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

* Brush and Pencil paint a CMYK foreground with the picked inks in a CMYK document (#4344)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:47:14 -04:00
f48a72cbe3 Codecs: open and save DPX frames (#1049) (#4104)
Read SMPTE 268M DPX in both byte orders (SDPX, XPDS): the first image
element, descriptor 6 (luma), 50 (RGB) or 51 (RGBA), at 8, 10, 12 or 16
bits, with 10-bit packing 0, 1 (method A) or 2 (method B) and 12-bit
packing 0 or 1. 10- and 12-bit code values open as U16
round(c * 65535 / (2^b - 1)) and come back exactly when written at their
depth. No colour transform: the transfer characteristic and colorimetric
specification are reported as import notes; extra image elements are
reported; the orientation is applied on import.

Write big-endian, one unencoded element, at 8, 10, 12 or 16 bits
(EncodeOptions::dpx_bits; `dpxBits` on the save commands, MCP and RPC;
`--dpx-bits` in the CLI), following the document depth by default. Gray
with alpha is written as luma. Creator, project, copyright, creation
time, transfer, colorimetric, frame position, frame rate and time code
travel as DPX:* text metadata and are written back.

Unsupported descriptors, signed or run-length-encoded data and other bit
depths return CodecError::Unsupported; malformed and truncated files
return errors.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:44:04 -04:00
Valentin KleinandClaude Opus 5.5 3ba0e9419c Color Picker: CMYK foreground fills a CMYK document with the picked inks (#4196) (#4339)
The tool colours are stored as sRGB, so a colour picked as CMYK 100/0/0/0
(outside sRGB) came back as 100/28/0/17 when a CMYK document was filled.
tools.setColors now takes foregroundCmyk/backgroundCmyk, the picker sends
the entered inks, and Edit > Fill (and the fill keys) write them as they
are into a CMYK target. Fill also enters the document's CMYK profile.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:39:55 -04:00
Marco Arnulfo a1fbfd9a6c Filters on a type layer: ask to rasterize or convert to a Smart Object instead of greying out (#4333)
* Filters on a type layer: ask to rasterize or convert to a Smart Object instead of greying out (fixes #4303)

* Filters on a type layer: Liquify, Filter Gallery, Camera Raw, Adaptive Wide Angle, Vanishing Point and Last Filter ask too (fixes #4303)
2026-10-12 19:30:58 -04:00
David MannandClaude Opus 5.5 6c0dcf428b Color Table: keep the transparent index inside the palette (#4186) (#4187)
Replacing the colours could shrink the table while the transparent index
still pointed past its end, so the palette PNG export refused the table.
A stale index is now dropped (pixels end up as with "transparent": null),
and an explicit invalid index (out of range, negative, non-integer) is a
BadParams error before any edit instead of silently keeping the old one.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 19:24:17 -04:00