* 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>
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>
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>
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>
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>
* 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>
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).
`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>
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>
`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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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.