* OpenEXR/HDR export: linearize grayscale documents too
Fixes#2386. The EXR/HDR branch of export_flat converted RGB images to
linear sRGB but left gray ones as stored, so an sGray #808080 was written
as 0.502 instead of 0.216. Gray images are now converted from their
profile (sGray when untagged) to linear gray. An untagged grayscale
OpenEXR opens tagged linear gray (an EXR Y channel is linear luminance),
and the open policy keeps that profile without a mismatch prompt, as it
already does for linear sRGB.
* contributors: add itsF4LCON
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Save As to a flat format always made the written file the document's
file: a layered or PSD-backed document saved as JPEG was renamed to the
JPEG and marked saved, so Ctrl+S then opened Save As with JPEG chosen,
and closing no longer asked about the unsaved layers. app.save on the
desktop did the same for any format, unlike the headless server (#1547).
Save As now writes a copy when the flat file can't hold the document
(anything beyond one plain raster layer, or the document already lives in a layered
PSD/PSB/.pcraft/OpenRaster/TIFF file), as Photoshop does. A flat
document not in a layered file still becomes the flat file. app.save
only retargets for layered formats, through a helper shared with the
headless server (file_cmds::layered_extension).
Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Close each document as it is answered when closing several (#2547)
Quit, Close All and Close Others asked about each unsaved document but
only removed it from the prompt's list; nothing closed until every
document was answered. After "No" the answered tab stayed open (and
often active), so it looked like the same file was being asked about
again.
Now each answered document closes right away (after its save, for Yes)
and the next one asked about becomes the active tab, as Photoshop does.
The parked command still runs at the end: quitting is allowed and Close
All closes the clean rest even if its original target already closed.
Cancel stops there and keeps the unanswered documents open. Single-file
Close and Revert are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Show the first document asked about when closing several (#2547)
The first question of a Quit, Close All or Close Others prompt now
switches to the tab it asks about too, so every question shows its own
document. Before, the tab active when the command ran stayed up for the
first question, often a different file with the same name.
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>
* fix(document): keep layer comps valid across document duplication
Remap saved comp and Last Document State references when cloned layers receive new IDs, including historical states and nested groups. Drop obsolete comp metadata in merged-only copies instead of carrying references to removed layers.
Add focused engine regressions for #2382 and related copy workflows.
* style(test): apply rustfmt layout to layer-comp regression tests
Fill Path painted the whole work path even when a selection excluded
part of it. The path's coverage (after anti-aliasing and feather) is now
multiplied by the selection mask, so rectangular, elliptical and
feathered selections all limit the fill. With no selection the whole
path fills, as before.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Exports copied the source EXIF with only Orientation and resolution
patched, so a camera JPEG's embedded thumbnail (IFD1) came along. Finder,
Android and upload pickers show that thumbnail instead of decoding the
image, so an edited photo kept its original preview.
export_exif now clears IFD0's next-IFD pointer, unlinking IFD1. Nothing
else moves, so every other offset stays valid; JPEG, PNG, WebP and the PSD
EXIF resource all go through it.
Co-authored-by: vivekprojects-GIT <179340152+vivekprojects-GIT@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(compose): Preserve native CMYK fills through PSD round trips
Share depth-quantized ink samples between rendering and PSD caches.
Use the document profile for RGB fill colours and preserve valid imported
caches. Uncached CMYK fills use CPU rendering.
Cover all sample depths, gradient styles and tiled renders. Fix the real
16-bit gradient round trip and raise the psd-tools floor from 307 to 308.
* perf(raster): Reuse exact 8-bit CMYK conversions within rows
Cache the previous ink bytes and ICC result per row, with alpha decoded
independently. Keep the 16/32-bit conversion loops unchanged.
Matched 1 MP reads improve by 77% for smooth ramps, 33% with dithering
and 10% for random colours. Bitwise regression coverage includes all
depths, alpha and tile boundaries.
Stroke Path only honoured the transparency lock, so Brush, Pencil and
Eraser painted on a layer whose pixels (or everything) were locked,
directly or through a parent group. It now rejects those destinations
with the same error as Fill Path and ordinary painting, leaving the
layer unchanged.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(psd): an unedited save writes Photoshop's Txt2 back unchanged (#2374)
Since #1517 every PSD save with type layers rebuilt the document's Txt2
block: it replaced everything under /1 with the text-object list, emptied
objects no layer names, and cut each kept object's style runs down to the
auto-kern mode alone. Photoshop then refused to open an unedited re-save
("Could not complete your request because of a program error").
build_txt2 now starts from the file's own block: the other /1 keys and
unnamed objects are kept, a kept object's style runs keep every style key
and only get /11 set (split where a mode changes inside a run), and a block
whose content comes out unchanged is returned byte for byte.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(psd): Photoshop type files keep Txt2 byte-for-byte on an unedited save (#2374)
Corpus test over corpus/photoshop/text: import, export, re-import, and
compare the Txt2 global block. All 56 Photoshop-authored type files pass;
the previous build_txt2 fails on the first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: vivekprojects-GIT <179340152+vivekprojects-GIT@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Session::jobs_with_recent is documented as "running jobs followed by the
last few that ended", but chained the ended jobs first, so jobs.list and
MCP jobs_list returned a finished job ahead of a running one. Running
jobs now come first, as documented.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Border built a hard band r pixels wide (expand r/2 minus contract r/2).
Photoshop's Border is soft and twice as wide: measured on Photoshop
27.11 (scripted), Border 4 on a 32 px square gives 63, 127, 191, 255 |
255, 191, 127, 63 across the edge, and the same along the inside of a
corner; PhotoCraft gave 0, 0, 255, 255 | 255, 255, 0, 0.
A pixel D away (centre to centre) from the nearest pixel on the other
side of the edge is now selected 1 - (D - 1) / r, using the existing
exact distance transform. That matches every sampled pixel within 1.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Smooth box-averaged the selection and then applied a contrast curve
((v - 1/2) * 4 + 1/2). That left a band of half-selected pixels along
every edge: a straight edge of a 32 px square came out 158 instead of
255 after Smooth 8, and a fill showed a soft grey border.
Measured on Photoshop 27.11 (scripted): Smooth 8 on the same square
gives only 0 and 255; straight edges stay exactly in place and the
corner is cut where the 17x17 box average crosses 1/2. Smooth now
thresholds the box average at 1/2, which reproduces every sampled
pixel.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Select › Modify › Feather (and the marquee, lasso and path feather
options) blurred with sigma = radius / 2, so a feather came out half as
wide as Photoshop's. Measured on Photoshop 27.11 (scripted): a 32×32
rectangle feathered 4 px matches sigma = radius within 2 levels along
the edge; sigma = radius / 2 was up to 71 levels off. The mask feather
(Properties › Feather) already uses sigma = feather, fitted on the
psd-tools corpus, so both feathers now agree.
The two margins sized for the old reach grow with it: Modify's
canvas-bounds padding (3 sigma = 3 r) and Fill Path's render area.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The Spot Healing Brush's drag preview stamped a hard round footprint at the
base brush size, so a soft brush and pressure/shape dynamics did not show,
while the heal itself uses the brush engine's dab coverage. The trail now
renders the same coverage (`stroke_coverage`'s StrokeRenderer, brush built by
the new shared `retouch_cmds::stroke_brush`), including the press's lone
first dab via `tail_preview`.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Right-clicking the pasteboard (outside the document bounds) now opens
Photoshop's pasteboard menu: Default, Black, Dark Gray, Medium Gray,
Light Gray, Custom and Select Custom Color…. The rows set the existing
Preferences › Interface canvas colour through `prefs.set`; Select Custom
Color… opens the Color Picker and OK stores and uses the custom colour.
Painting tools keep their Brush Preset picker / right-button erase, the
Pen keeps its path menu, text editing and Free Transform keep theirs;
the Move tool falls back to the pasteboard menu where no layer is hit.
The control channel's `ui.pointer` secondary click opens it the same
way, `ui.context.choose` picks rows (authorised as `prefs.set`).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Zoom tool: double-click zooms to 100% (#2508)
Double-clicking the Zoom tool in the toolbar now selects it and runs
view.actualPixels, as in Photoshop, next to the existing Hand tool
double-click (fit on screen). With no document open it only picks the tool.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* contributors: add vangel76 (sync GitHub name)
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Pressing a tool shortcut from another group always picked the group's
first tool (L gave the Lasso even after the Polygonal Lasso was used).
The UI state now remembers the last tool used in each shortcut group,
and the key picks it, as in Photoshop. Applies to every group.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
layer.pickAt hit-tested every layer by the alpha of the pixel under the
pointer, so a click in the gap between a type layer's letters picked the
layer under it. Like Photoshop, Auto-Select (and Cmd-click) now hits a
type layer anywhere inside the bounds of its rendered text. The
right-click layer list still lists only layers with pixels there.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
In Photoshop, holding Ctrl (Cmd on macOS) is the temporary Move tool for
most tools, so Ctrl/Cmd+arrow nudges the selected layer 1 px, with Shift
10 px, and with Alt duplicates first. PhotoCraft only nudged with the Move
tool itself, and egui's consume_key never ignores an extra Ctrl/Cmd, so
Ctrl/Cmd+arrow did nothing anywhere.
- hold_keys::cmd_nudges: Ctrl/Cmd+arrow nudges wherever Ctrl/Cmd is the
temporary Move tool, with the Move tool, and with the selection tools
(moves the selected pixels). Not during Free Transform, text editing, or
an in-progress polygon or Magnetic Lasso border.
- move_mods::arrow_keys adds Command to its patterns in that case.
- nudge decides on floating the selected pixels as the Move tool.
- Direct Selection keeps Ctrl/Cmd for itself, like Path Selection.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
View > Show > Layer Edges was painted before the frame's tool events
were read, from the session document. Two bugs came from that:
- The frame in which Auto-Select picks a layer on the press still
outlined the previously selected layer, because the pick ran after
the outline had been drawn.
- During a live Move drag the moving pixels come from the preview
document at the drag offset, but the outline used the committed,
unmoved bounds. It stayed where the layer started until the release,
so it trailed behind and was visibly off on fast drags.
The canvas now reserves the outline's z-slot where it was drawn and
fills it after the tool events, so it outlines the newly picked layer.
The rect is offset by move_ui::shown_offset, which is the offset the
image was drawn with this frame. That keeps the outline in lockstep
with the pixels. The moving set (the move targets plus their
descendants) is computed once per preview revision, so there is no
per-frame scan.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The live preview (ants and W/H readout) showed the whole-pixel rectangle
from marquee_px, but finish_gesture deselected any rectangle narrower or
shorter than 2 px. That threshold stood in for "was this a click?" because
floor/ceil turns a click between pixel edges into a 1x1 rectangle, so a
deliberate one-pixel column or row, and a thin drag replacing an existing
selection, ended with no selection at all.
Release now commits exactly marquee_preview_px, which decides a click by
screen distance (shape_dialog::is_click at the view zoom) and drops only
zero-area rectangles; the preview draws no ants when the release would
deselect. A plain click still deselects, as in Photoshop.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(selection): constrain Polygonal Lasso segments with Shift (#2352)
* Polygonal Lasso: make the Shift constraint helper visible to its tests; 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>
* Free Transform: the Hand tool no longer steals a resize drag (#2302)
* Free Transform + Hand: only the selected Hand yields, and only in the primary canvas (#2302)
* cargo fmt
Interface > UI Scale is egui's zoom_factor, folded into pixels_per_point
with the display scale. The canvas divides View::zoom by the full
pixels_per_point (PhotocraftApp::point_zoom) and the GPU shader
multiplies it back, so 100% is one document pixel per physical display
pixel at every UI scale -- fixed by #1978 (Canvas: zoom is physical at
any display scale), which the report predates (v0.5.0). No behaviour
change; the report is stale.
Lock it down with a rendered-output regression test
(tests/ui_scale_canvas.rs): a black document is drawn through the real
app on the GPU and CPU canvas at UI Scale 100/125/150/200% and zoom
100/200%, and its on-screen size is measured in the pixels. It stays
60x60/120x120 physical px at every UI scale, and fails when the canvas
stops using point_zoom (75x75 at 125%) or when the GPU shader stops
multiplying by pixels_per_point (48x48 at 125%). The comments on
View::zoom, PhotocraftApp::ppp and canvas_view now name UI Scale too.
Open and save .ora with pixel layers (any offset), nested groups, pass-through
groups, names, visibility, opacity, edit/alpha locks and blend modes (svg:* plus
Krita's krita:* names), at 8 and 16 bit. Save applies pixel masks, renders
text/shape/fill/smart layers to pixels and reports what OpenRaster can't hold.
Wired into File > Open/Save, the CLI, automation/MCP, the web picker and the
desktop file associations. Checked against 34 Krita 5.2.9-authored files
(opt-in krita_corpus test) and Krita re-opening our exports.
Co-authored-by: Scott Campbell <251750033+scottcampbelldata@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
- The live set found beside Photoshop is offered, never applied unasked: an
"Import keyboard shortcuts?" prompt names the set and how many keys it
changes, with Import / Don't Import. Either answer is recorded in
dialogs.photoshopShortcuts, so it is asked once; ui.inspect shows the
pending offer as kysOffer.
- Parsing and label matching moved to the engine (photocraft_engine::kys), so
edit.keyboardShortcuts {"importKys": <xml>} imports a set from agents, the
CLI and MCP; the app's dialog uses the same plan with its menus' labels.
- "The keyboard shortcut set was not imported: {error}" is translated.
- .kys input over 4 MiB is refused before it is read as text.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* 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>
* EXR: Cryptomatte layers list, decode and hash to the specification
Cryptomatte (specification 1.2) carries per-pixel object and material
IDs with coverage in <layer>NN.red/.green channel pairs, announced by
cryptomatte/<key>/name|hash|conversion|manifest header attributes - the
ID channels Arnold, V-Ray, Redshift, Mantra/Karma and Cycles write next
to the beauty pass.
cryptomatte_layers lists every layer from the headers alone (name, key,
channels, the embedded manifest parsed and capped at 16 MiB, a sidecar
pointer); decode_cryptomatte returns the per-pixel (ID, coverage) pairs
above zero, sorted by descending coverage; cryptomatte_id hashes names
the reference way (MurmurHash3 x86-32, the exponent fixed away from
denormals/inf/NaN, read as f32 bits) - verified against the official
test vectors - and cryptomatte_key/cryptomatte_preview_color expose the
metadata key and preview colour. Files with deep parts are refused, like
the other flat part decoders.
* chore(ui): format prefs_ui (the workspace fmt gate is red on main until #2417 lands)
* codecs: Cryptomatte decode checks the layer size without truncating casts
Count each pixel's pair list in the allocation check too.
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>
* Distance fields: the Euclidean passes run on threads, results unchanged (#2119)
The chiselled bevel fields (#2006) and the precise glow fields are built
from an exact Euclidean distance transform, a few sweeps over the whole
map that all ran on one thread. They now run on scoped OS threads, in
groups of rows or columns handed out from a queue: edt_nearest (the
column pass, then the row pass), sharp_edges (rows), refine_edge_normals
(one edge pixel each) and the final per-pixel maps of the bevel and plain
Euclidean fields. Not rayon, as for the blurs (rows_par): maps are built
from inside rayon tiles, where a waiting worker steals other tiles (#276).
Maps under 65536 pixels stay on the calling thread, wasm always does.
Every unit is computed from its own inputs, in the same arithmetic as
before, so the fields are bit for bit the old ones whatever the thread
count: 216 fingerprints (bevel in and out, plain in and out, bevel_maps on
bars at 0 to 150 degrees, thin bars, discs, rounded rectangles, soft,
patchy and half-opaque masks, 40 stars, tiny and 1 pixel maps) are equal
at 1, 7, 16 and the default threads, and equal to the code before.
edt_nearest returns the nearest pixel as u32 (it was usize), which takes its
working memory from 20 to 16 bytes per pixel; a map past 2^32 pixels or a slice that does
not match its size gets "far" distances instead of a panic.
CPU compositor, 12 MP, bevel size change (ms, bevel_bench), main -> this:
ellipse 474 -> 217 (size 8), 990 -> 318 (250); text 747 -> 214 (8),
1110 -> 267 (250). A distance_field call on a 256x256 map goes from 2.2 to
1.8 ms, on 512x512 from 8.0 to 2.8, on 2048x2048 from 99 to 15.
Tests: par_units hands every unit to the closure once with its own
slices (short last unit, no second slice, units of different sizes, 1 to
64 threads); the nearest-pixel transform equals itself on 1, 2, 3 and 8
threads on awkward sizes and returns a seed at the distance it reports;
degenerate sizes; sharp edges and the four Euclidean fields are bit-equal
on 1, 2, 5 and 16 threads on discs, rounded rectangles, bars at several
angles, patchy opacity and an edge on the border. The nested-rayon
deadlock test (#276) now has a chiselled bevel layer in its document.
* GPU effect maps: the Euclidean fields are one window, not bands (#2119)
A bevel size change rebuilds the distance fields over the whole effect
region (the region grows with the size). The GPU path cut that into bands
of max(128, halo) rows, each computed from a window grown by the halo,
which for the bevel fields is reach + 10: at size 250, 262-row bands from
windows of about 786 rows. Every pixel went through the transform and the
edge refinement about three times, a 12 MP region gave about 12 tasks for
32 threads, and the two bevel fields ran one after the other.
The Euclidean fields (chiselled bevels, precise glows) now come from one
window, and distance_field spreads its own passes over the threads. A
rebuild is the whole region, which is also what the CPU compositor
computes, so the GPU canvas gets exactly the CPU's field: bands resolved
ties between equidistant edge pixels per window, and on a rebuild they
differed from the whole region by up to 0.07 px (ellipse) and 1.3 px (600
discs) at size 250, on a few dozen pixels in 12 MP. A partial update (a
brush dab) still computes the damage grown by the halo, from one window.
The chamfer fields (shadows, strokes, spread) are a sequential sweep and
keep the bands, and so does wasm, where there are no threads to spread
over and bands keep the memory small.
RTX 4080 SUPER, release, 12 MP, ms per size change on the GPU canvas
(bevel_bench from #2118, p50, median of 3 runs, before #2006 / main /
this), ellipse: 90 / 103 / 98 (8), 162 / 188 / 119 (50), 286 / 446 / 219
(250); text: 83 / 139 / 113, 140 / 215 / 130, 222 / 489 / 165. Alone in the
process (--no-cpu) ellipse at 250 is 407 -> 157 and text 468 -> 169.
36 MP ellipse at 250: 779 -> 382. Peak RSS: 12 MP at 250 1604 -> 862 MiB,
36 MP at 250 2987 -> 1726 MiB; at size 8 on 36 MP it is 163 MiB more
(1380 -> 1543). Precise glow fields on 12 MP: 46-84 ms -> 26 ms at reach
52-252 (32 ms at 12).
The window test now checks that the whole region is bit for bit the CPU's
field, and that a too small halo is noticed on a part of the region.
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Content-Aware tools: Photoshop's behaviour, measured
Photoshop 25.4 was measured as a black box (synthetic and public-domain
images, results read back exactly) and the content-aware family follows it:
- Fill (inpaint): coarse to fine with the coarser level's matches scaled up
and voted with the finer level's own pixels, the coarsest level filled
from the hole's edge inwards, a shallower pyramid. The fill is pieces of
the image copied pixel for pixel with narrow seams, as Photoshop's;
before, it was visibly blurred. Pixels have roles (hole, context,
source, void), so excluded areas stay context instead of being filled.
- Colour adaptation: Image Melding's per-patch gain and bias, limited per
level, instead of a low-frequency membrane pull.
- Sampling window: Photoshop's square of side 4·√(max(w,50)·max(h,50))
centred on the selection, slid inside the canvas; Edit › Fill,
Rectangular, Auto, Delete and Fill and Spot Healing use it.
- Auto sampling area: what looks like the selection's surroundings (local
colour and contrast, one min cut), fitted to Photoshop's overlay.
- Patch Tool: a Content-Aware mode (Structure, Color, Sample All Layers).
- Content-Aware Move: Structure 3..7 alike, a band about a patch wide;
Color as a gain/bias limited to about 0.04 per step.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Remove tool: keep its own sampling margin (fill_cmds::sampling_margin is gone)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The filter dialog previewed large documents on a 1/k proxy and divided
pixel-sized params by k. Filters whose features have a fixed size in
pixels (Ripple, Diffuse, Oil Paint, Flame, Fibers, Mezzotint) came out
k times larger in the preview than in the result, and params divided
below their minimum were clamped (Pointillize 5 px cells -> 12 px).
filter_dialog::preview_factor picks the proxy factor per command: 1 for
the fixed-size filters, otherwise the largest factor up to k at which no
pixel-sized param falls below its range minimum.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
doc_open, doc_save and the other automation file paths only took
forward-slash relative paths, so an agent passing C:/Users/me/a.png with
a C:/ root was refused. An absolute path is now compared with the
root's absolute path as text: if it lies beneath the root, the root's
components are stripped and the rest goes through the same checks as a
relative path and is opened through the same directory capability.
Nothing is resolved on the filesystem, so traversal, prefixes, device
names and link escapes are refused as before; absolute paths outside
the root, or with no root granted, are still refused.
Fixes#2176.
* Expand arithmetic expressions across numeric inputs
* Test arithmetic and scientific notation in indexed color counts
* Drop the numeric_fields example (it wrote PNGs to /tmp; the widget tests cover the behaviour)
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
On a smart-object layer, Image > Adjustments now records an editable
smart filter instead of committing disaster.
- new engine helpers:
- adjust_cmds::has_adjustable / adjust_as_smart_filter route
image.adjustments.<kind> onto the source, with the selection as the
filter mask, through smart_cmds::add_smart_filter
- brush/eraser/pencil/live preview strokes now render inside
with_cmyk_space(doc_cmyk_space(session)), so painted colours follow the
document profile the same way the compositor does
- docs/scorecard.md: FILE-215-4 partial -> done
- tests: adjust_cmds/tests + brush_cmds/tests, plus panic_hunt and
the lib suite all green on the committed tree
Known follow-up, already filed:
- FILE-215-5 rows: double-clicking an adjustment smart-filter row is still
a no-op. smart_ui::open_editor only proceeds when
filter_dialog::has_dialog returns true, and has_dialog accepts
filter.*, a fixed non-adjustment set and PREVIEWED - but not
image.adjustments.* (those open through adjust_dialog::open, which isn't
wired into the smart-filter row).
This is one commit on top of the latest upstream/main (rebased to fix the
stale fork ancestry that was blocking PR creation).
Co-authored-by: Brandon Thomas <echelon@gmail.com>