Commit Graph
3466 Commits
Author SHA1 Message Date
David MannandClaude Opus 5.5 aa8621788e Minimum / Maximum: CMYK spreads paper white with Maximum, ink with Minimum (#7074) (#7075)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 13:04:02 -04:00
UnemarsandClaude Fable 5.1 5071472e4d Layers: ⌘-click on the Background thumbnail is a plain click, as in Photoshop (#7165)
* Layers: ⌘-click on the Background thumbnail is a plain click, as in Photoshop

⌘-clicking a layer thumbnail loads its transparency as a selection (⇧ adds,
⌥ subtracts, ⇧⌥ intersects) without changing the selected layer. Measured on
Photoshop 25.4: the Background, which has no transparency, is the exception —
the click loads nothing and selects the row like a plain click, where
PhotoCraft selected the whole canvas. A hidden layer still loads.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Layers: the Background thumbnail's ⌘-click selects the Background alone

Review: the fallback to the row handler kept ⌘ set, so `select_mode` toggled
the Background into (or out of) the selected set instead of the plain click's
replace. The Background thumbnail's ⌘-click now selects it alone; the test
asserts the selected set, including when the Background was already part of a
multi-selection.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-19 13:03:56 -04:00
Omar Saidahmed 6385fced3d refactor: use photocraft-color's srgb transfer functions in algo and proof simulation (#7604)
* share the srgb transfer functions from photocraft-color

* document the touched srgb helpers
2026-10-19 13:03:48 -04:00
KnottyDyes c9d7f1d984 Docs: install the CLI and register it with MCP clients (#7663) 2026-10-19 13:03:40 -04:00
David MannandClaude Opus 5.5 baa005d476 Type: formatting commands respect Lock All (#7718) (#7720)
Character (type.setStyle), Warp Text, Anti-Alias, Orientation, OpenType
and Load Default Type Styles all run through with_text_targets, which
never checked Lock All, so they changed a locked type layer and added a
history step. Photoshop refuses all of them ("the layer is locked").
Check every target with check_text_unlocked before the edit, as
type.edit already does.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 13:03:32 -04:00
David MannandClaude Opus 5.5 94a135bc6b Free Transform: show a flip as -100 % on the flipped axis (#7749) (#7750)
The options bar read the box's X scale as never negative and dropped
the sign of the Y scale, so Flip Horizontal read W 100 %, H 100 %, 180°
(typing 0° then flipped the layer vertically) and Flip Vertical read
the same as an untouched box. A mirrored box now reads as a negative W
or H, whichever keeps the angle nearer 0, as in Photoshop (Flip
Horizontal: W -100 % at 0°, Flip Vertical: H -100 % at 0°). Typed W/H
divide by a sign-preserving guard instead of max(1e-9).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 13:03:26 -04:00
adbd40ef55 Actions: Photoshop Shadows/Highlights steps play (#5830) (#5854)
Imported actions stopped at Shadows/Highlights ("adaptCorrect has no
PhotoCraft equivalent yet"), although Image > Adjustments > Shadows/Highlights
exists. The step now maps to it both ways: shadowMode/highlightMode (amount,
width, radius), black/white clip, midtone contrast and colour correction,
each read as its char ID or string ID.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-19 13:03:20 -04:00
96c151525a feat(prefs): Show File Name In Title keeps the title bar to the app name (#5037) (#5129)
Co-authored-by: highperfocused <highperfocused@pm.me>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-19 13:02:53 -04:00
Tarek MohamedandBrandon Thomas 5c513b8c88 fix(actions): flush pending recorded steps on save (#7193) (#7244)
When actions.save was called while recording was active, it exported only
already-committed action steps, silently omitting any pending steps shown
in the Actions panel.

Call flush_recording at the beginning of actions.save to commit pending
journal entries to the action and advance the recording pointer, ensuring
all displayed recorded steps are saved while allowing ongoing recording to
continue uninterrupted.

Fixes #7193

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-19 13:02:37 -04:00
640873de7b PNG: keep a stereo file's sTER pairing on save (#6063) (#6285)
* PNG: keep a stereo file's sTER pairing on save (#6063)

Read the sTER chunk (PNG Extensions 1.4.0 §3.6) when a PNG opens, store it
on the document's FileEncoding, and write it back when saving, so a stereo
PNG still says it is a stereo pair. Covers the ordinary and the palette
PNG encoders, including interlaced output.

* io tests: pass the new sTER argument to encode_png_indexed

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

---------

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-19 13:02:18 -04:00
aiongg 3544721eb0 Merge PR #7692: perf(raster): Avoid redundant default pixel work 2026-10-19 16:40:42 +00:00
Elvis33LE 716d1e24c9 fix(io): keep variable noise GdFl fills as fill layers
constant_rgb_noise_color's None propagated out of fill_from_desc for every
noise gradient that is not provably constant - Photoshop's default Noise
gradient included - so the layer imported as pixels and the export dropped
its GdFl (RASTER_STALE_KEYS): an open-and-save data loss. The exact colour
now applies only when provable; everything else keeps the placeholder ramp,
the fill_cache pixels and the raw GdFl block, which the re-save writes back
byte for byte. Round-trip test included (fails on the old head: the layer
came back as pixels without a GdFl).
2026-10-19 17:24:19 +02:00
Rene Leonhardt 83aff80f35 fix(raster): support sample counts beyond stack capacity 2026-10-19 17:19:18 +02:00
Akos Arinton dfeba9e974 test(compose): compare NaN as NaN in the vector span test, not by its bits 2026-10-19 18:11:13 +03:00
David Mann c30f9180e8 Merge PR #7692: perf(raster): Avoid redundant default pixel work 2026-10-19 16:51:48 +02:00
David Mann c0072ca2da Merge PR #7691: perf(io): Interleave PSD pixels in the final buffer 2026-10-19 16:51:45 +02:00
David Mann b2269e3bdb Merge PR #7690: perf(codecs): Avoid redundant PNG filter passes 2026-10-19 16:51:43 +02:00
David Mann 8e54897d43 Merge PR #7022: perf(text): rasterize a type layer's pixels on all cores, bit for bit the same 2026-10-19 16:51:41 +02:00
David Mann ca915bc096 Merge PR #6950: fix(psd): make packbits::decode_into crate-private and overflow-safe 2026-10-19 16:51:39 +02:00
David Mann cfece486fc Merge PR #6797: Fix live gradient fill scale limits to match OG program 2026-10-19 16:51:36 +02:00
David Mann 10d012b843 Merge PR #6193: fix(perf): validate regression minimum threshold 2026-10-19 16:51:34 +02:00
David Mann 3c9f825269 Merge PR #5998: PSD save: check the Background's opacity without copying the canvas 2026-10-19 16:51:32 +02:00
David Mann b107b172e3 Merge PR #5984: Brushes: importing an .abr file numbers repeated brush names in linear time 2026-10-19 16:51:29 +02:00
David Mann 284ee60705 Merge PR #5976: PSD: keep guide resources 1092/1097 only while they match the guides (#5966) 2026-10-19 16:51:27 +02:00
David Mann 06445a21b8 Merge PR #5880: refactor(engine): drop the second damage pass after single-edit duplicates and pastes 2026-10-19 16:51:25 +02:00
David Mann c2f3e23a40 Merge PR #5477: perf: chisel bevel size changes, CPU passes on threads and GPU fields share their sharp edges (results unchanged) 2026-10-19 16:51:22 +02:00
David Mann 676761c435 Merge PR #5413: Limit blend-mode hover refreshes to the visible canvas region 2026-10-19 16:51:20 +02:00
Gert Pellin bfffc365c6 Merge branch 'main' into feat/4814-psd-palette-modes 2026-10-19 16:33:16 +02:00
David MannandClaude Opus 5.5 ebfe3d98da Save for Web: later slices cover earlier ones, covered slices split into pieces (#7770)
`slices::resolve()` only clipped each stored slice to the canvas, so a slice under a later
one was numbered and exported in full and Save for Web wrote the overlap twice (the HTML
path cut pieces itself, named `<name>_01`, `_02`). As Photoshop does, resolve() now cuts a
covered slice into its visible pieces: the first piece in reading order keeps the name,
every further piece is `<name>-NN` with its own slice number, and auto slices count the
pieces in their numbering (measured in Photoshop: sliceA, <doc>_02, sliceA-03, sliceB,
<doc>_05).

- Save for Web exports resolve()'s pieces as they are, with or without HTML; a no-image
  cell's text goes into its first piece only.
- Slice Options and slice.list show the slice's own name for every piece; slice.set accepts
  a slice that ends up wholly under later ones (number null) instead of calling it off the
  canvas.
- Slice Select drags the whole slice, not the piece that was hit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:02:32 +02:00
NetterSilverandClaude Opus 5.5 604007490b fix(psd): replace an earlier save's Txt2 stub also when its text is edited
The stub filter applied only to the kept object, so an edited layer whose
slot held a stub from an earlier save went through edited_object and kept
the bare stub. The slot is now filtered before both lookups.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 15:33:05 +02:00
David MannandClaude Opus 5.5 41fb9f986c Layer Style: a new Stroke is 1 px inside black, as in Photoshop (#7765)
`layer.layerStyle.stroke {}` built a red 3 px outside stroke, while the
dialog's factory default (defaultFor / Reset to Default) was black 3 px
outside. Photoshop's Reset to Default on the Stroke page is 1 px,
Inside, Normal, 100 %, black; both tables now give that, and a test
checks that every effect kind's bare build equals its dialog default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 15:14:51 +02:00
NetterSilverandClaude Opus 5.5 198fa59c7d fix(psd): a new type layer gets a full Txt2 object and its own frame (#7698)
A type layer the file's Txt2 has no object for (one added in PhotoCraft)
was written as a bare stub with no view, no paragraph runs and no frame
in /0 /8 /0. Photopea and Photoshop refused the file once the objects
outnumbered the frames.

build_txt2 now gives such a layer an object shaped like one of the
file's own (refitted with edited_object from #7220), with its view
naming its own frame, and pads the frame list to one per object. A stub
written by an earlier save is replaced the same way, so re-saving an
affected file repairs it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 14:56:15 +02:00
David MannandClaude Opus 5.5 745f535fda Perspective Crop: Color Sampler points move with the image (#7760)
Perspective Crop set no sampler map, so the edit only dropped the
points now off the smaller canvas and left the rest at their old
coordinates, reading unrelated pixels. As in Photoshop, each point now
follows its pixel through the crop's homography (fractional positions
kept), and the points outside the quad are deleted and counted for the
status message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:51:25 +02:00
David MannandClaude Opus 5.5 e8a9f579f7 Crop: a typed ratio fits from the frame the edit started with (#7752)
The options-bar ratio fields apply every keystroke, and each ratio was
fitted inside the frame the previous keystroke left: typing 16 over 4 in
a 400 x 300 frame went 1:3 (100 x 300), then 16:3 inside that
(100 x 18.75) instead of 400 x 75. The bar now keeps the frame from when
the W/H edit (typing, arrows or scrubbing) started and fits each new
ratio from it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:44:02 +02:00
David MannandClaude Opus 5.5 de2cb1f0c0 Compose: a clipped adjustment inside a pass-through clip base damages the whole group (#6535)
A pass-through group that is the base of a clipping group renders isolated while it holds a
visible adjustment (`clip_base_adjusts`), so showing, hiding, adding or removing a clipped
adjustment inside it can switch the group's other children (a Multiply layer beside the base)
between isolated and pass-through rendering. `change_bounds_in` now takes the list holding the
parent group and reports that group's change bounds in this case; `layers_damage`, `step_damage`
and `one_sided_bounds` pass it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:14:06 +02:00
David MannandClaude Opus 5.5 d67a949a54 Crop: a lost Straighten release no longer hijacks the next drag (#7745)
A Straighten line whose release went to Space's temporary Hand stayed
set, so the next plain drag fed its moves and release to the old line:
the frame turned instead of moving and the crop gesture stayed stuck.
A new press now drops any leftover line first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:10:07 +02:00
David MannandClaude Opus 5.5 c4e8eb2b86 Brush smoothing: catch-up compares the pose too, not only the position (#7747)
Since #5726 the smoothed brush pulls its pressure, tilt, rotation and wheel
towards the pointer like its position, but Stroke Catch-Up (`lagging`) and
Catch-Up On Stroke End (`finish`) still compared x/y only. A pen that changed
only its pressure in place got no catch-up time and no end point, so the
stroke ended at an in-between pressure (1.0 -> 0.0 ended at 0.64).

Both checks now also compare the pose (rotation on the circle).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:07:15 +02:00
Rene Leonhardt 4d50e31efd perf(codecs): stop PNG candidates at the winning cost 2026-10-19 13:43:19 +02:00
David MannandClaude Opus 5.5 b61a6124d9 Crop: Undo/Redo grey out when the frame has no step left (#7730)
While either crop-box stack has a step, intercept takes both Undo and
Redo, but is_enabled reported only the stack it would pop and otherwise
fell back to the document's can-undo: Undo looked enabled, popped
nothing and left the document's history unreachable. Grey the empty
direction instead, as the Free Transform box does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 13:40:23 +02:00
David MannandClaude Opus 5.5 c7c2c31416 Painting symmetry: canvas edits move the axis with the pixels (#7732)
Crop, Trim, Canvas Size, Image Size, Flip and Rotate Canvas mapped only the
Color Sampler points; the painting symmetry kept its old document
coordinates, so strokes mirrored about a different line (and a path axis
left its own moved path). Session::edit now maps the symmetry through the
same edit's geometry map: preset centre and angle (a flip mirrors it), or
the sampled path segments. Flips and rotations pass their edge-coordinate
map separately from the samplers' pixel-centre map.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 13:40:05 +02:00
Hector VillarrealandClaude Opus 5.5 b862323a8e Justify buttons: grey when any formatted type layer is point text
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 05:39:46 -06:00
Rene Leonhardt dd552f76b1 docs(raster): document touched read helpers 2026-10-19 13:33:01 +02:00
David MannandClaude Opus 5.5 d20a0782da Crop: a W x H crop on an Indexed Color document resamples with Nearest Neighbor (#7722)
The Crop tool's W x H x Resolution mode resampled with the chosen filter
(Bicubic by default) even on an Indexed Color document, so the result held
blends outside the colour table. Like Image Size (#6019), it now falls back
to Nearest Neighbor there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 13:28:32 +02:00
Hector VillarrealandClaude Opus 5.5 c8907e2cd2 Paragraph panel: grey the justify buttons on point text (#7713)
Justification needs a paragraph box: Photoshop greys the four justify buttons
for point text, which lays out left whatever its alignment. PhotoCraft offered
them, and choosing one did nothing visible.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 05:23:03 -06:00
Rene Leonhardt b5b5776a30 perf(io): remove the PSD export inlining hint 2026-10-19 13:15:06 +02:00
Rene Leonhardt 78a9982eec perf(raster): share contiguous region reads 2026-10-19 13:15:06 +02:00
Rene Leonhardt 6b1aa81561 docs(perf): measure PNG filtering on real images 2026-10-19 13:15:05 +02:00
Akos Arinton 80f896210a perf(compose): write band tiles as they finish, not after the band
Parallel bands each collected their finished tiles until the band's last
one was done, so with work stealing many bands could hold most of their
tiles at once, close to a second copy of the output. Each tile is now put
into its band behind a lock as soon as it is rendered; only the tiles in
flight are held, as before the bands ran in parallel.
2026-10-19 13:34:32 +03:00
Korg1ch af0857372b test(web): the boot module tells gzip from wasm the browser already inflated (#7639) 2026-10-19 12:37:55 +03:00
Korg1ch cb1b479452 fix(web): start the app when the server already inflated its gzip part (#7639) 2026-10-19 12:37:54 +03:00