mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-15 03:36:48 -04:00
main
562
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
df055f3dcb |
CI: fix the tests that keep main red, and ship the wasm as compressed parts (#5926, #5302) (#5941)
* Fix two ui-egui tests broken on main by concurrent landings - profile_open_tests: #5614 renamed the built-in profile to "(PhotoCraft)" after #5208 hard-coded "(Photocraft)" in the Use-embedded button label. - smart_object_curves_preview_keeps_its_size_on_a_reduced_proxy (#5779): the reduced square is in the right place, but its corner pixels are 220/255 after the smart object's resampling; assert full coverage inside and over half at the corners. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Web: ship the wasm as compressed parts behind a boot module (#5302) The release site held the app's wasm as one file, which sits at the 25 MiB per-file cap of Cloudflare Pages, so the 24 MiB gate failed unrelated PRs. packaging/web/package.sh now gzips the wasm and cuts it into parts of at most 20 MiB under app/, and ships the page of a new crate, apps/photocraft-web-boot. That module fetches the parts in order, has the browser inflate them (DecompressionStream) and passes the stream to the app's own wasm-bindgen loader, so the browser still compiles while it downloads. A failed download is retried twice, then the page says so. The check is now that no file in the site is over 24 MiB. trunk build in apps/photocraft-web and the Docker image still give one .wasm. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Web boot: inflate the parts in the module where the browser can't (#5302) * Tests: the adjustment-layer drag preview test follows #5778 (#5926) #5778 caps a live adjustment-layer preview by the view's zoom, so at 100 % the drag shows full resolution even with Low Resolution Previews on. The test still expected the reduced copy there. It now checks that at 100 % every column shows by default, and moves the low-res on/off comparison to 50 %, where the reduced copy is still used: on, the drag shows only the (inverted) black columns; off, it matches the committed view. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Brandon Thomas <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]> |
||
|
|
ad96df566b |
File > Save As / Save a Copy / CLI can write PDF (#3401) (#3441)
* TIFF: read and write CIE L*a*b* (CIELab, ICCLab); Lab documents stay Lab Photoshop saves Lab documents as CIELab TIFFs (PhotometricInterpretation 8, a* and b* signed), which PhotoCraft could not open, and PhotoCraft saved Lab documents as sRGB TIFFs. - codecs: a Lab channel layout (Lab, LabA) holding a* and b* as PhotoCraft's Lab documents do ((a + 128) / 255; 16 bits like the PSD importer), with its own Lab <-> sRGB conversion (same formulas as photocraft-color; codecs stays standalone) for formats without Lab, which now warn LabConverted. - TIFF read: CIELab and ICCLab (8 and 16 bits, alpha) are handed to the tiff crate as RGB and labelled Lab; a* and b* are converted from the file's encoding (signed a / 256a, or offset). - TIFF write: Lab and LabA as CIELab, as Photoshop writes it; float Lab is written with 16 bits (DepthReduced). - io: a Lab TIFF opens as a Lab document; a Lab document with one plain layer is written to TIFF in Lab (other formats still use sRGB). Photoshop 2025's Lab TIFFs open with the colours Photoshop shows (its own RGB conversion matches within one level at 8 bits and exactly at 16 bits), and Photoshop reads PhotoCraft's Lab TIFFs back with the same L*a*b* values. Fixes #4936 * PSD: a smart object's filter mask shows when the file opens Photoshop stores a smart object's layer pixels with its filters applied but not its filter mask. When it composites, it mixes the unfiltered pixels (cached in FEid) back in through the mask. We showed the stored pixels as they were, so a PSD whose smart filter has a mask opened with the filter everywhere. Only after something re-rendered the smart object did the mask appear, because our re-render mixes it. - compose::filter_mask holds the mix, moved from the engine, and its inverse. - Import decodes the unfiltered pixels from FEid (smart_map::unfiltered_from_item) and keeps the mixed result as the smart object's pixels, as the engine's re-render does. - Export writes the layer record the way Photoshop does: unmixed with the unfiltered pixels it writes (or keeps) in FEid, so Photoshop mixes back to what the document showed. Transparent pixels stay empty. Photoshop oracles: sf-filter-mask-hard, sf-filter-mask-gradient and sf-stack-blend-and-mask pass (145 -> 148 of 258); floor 145 -> 148. psd-tools and io corpora unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]> * DNG: decode lossy (JPEG-compressed) DNG tiles (FILE-215-11) DNG 1.4 lossy JPEG (compression 34892) stores each tile or strip as a baseline-DCT JPEG of 8-bit samples: a gray stream for CFA data, a YCbCr stream for three-sample LinearRaw. The decoder arm from #2021 read the streams but widened every sample by 257, so the values no longer matched the file's levels: an 8-bit WhiteLevel (or its default, 255) clipped everything to white and a 256-entry LinearizationTable was indexed past its end. It also required the JPEG frame to match the clipped row count, which rejected the padded edge tiles real tiled files carry. The arm is now `lossy_jpeg()` in sensor.rs. The decoded 8-bit values are the stored samples, and BlackLevel, WhiteLevel (default 255 for 8-bit data) and the LinearizationTable apply to them exactly as for every other compression. A tile may be coded at the full tile size (edge tiles padded, as the spec says) or at the image's row count; a strip at its row count; the width must be the segment's. BitsPerSample must be 8 and the JPEG's component count must equal SamplesPerPixel (1 or 3); anything else is an error, never a silent conversion. The frame header is checked before any pixel is decoded and the JPEG decoder's own size cap is the segment the IFD declares (already charged against the limits), so a hostile SOF cannot size a buffer past the tile. testgen writes true 8-bit lossy fixtures (`LossyJpegTiles`, and strips via the new `LossyJpegStrips`), accepts pre-encoded `lossy_segments` for hostile-input tests, exposes `baseline_jpeg()`, and leaves WhiteLevel out when `white` is 0. The DNG tests cover CFA and LinearRaw RGB from tiles and strips at 37x23 (partial edge tiles, a short last strip), the default white level, black/white scaling and a LinearizationTable against the uncompressed path, and hostile inputs: a truncated tile, frame-size and component-count mismatches, a 65535x65535 SOF under default and tight limits, a tile over the allocation budget, and 12/16-bit or 4-sample files. The hostile mutation set gains a lossy CFA and a lossy LinearRaw file. The parity doc's DNG row is updated; the scorecard row is left for the coordinating branch. * Raw: measure the Nikon D90 image area (4288x2848 at (10, 10)) Aligning the decoded D90 sensor plane with the camera's own JPEG puts the image at (10, 10) in the 4352x2868 sensor plane, giving the camera's 4288x2848 frame (10-pixel margins; the right border reads the camera's flat 255 from column 4310). Add the measured ImageArea so a D90 NEF opens on the camera's frame instead of the uncropped plane, and make a measured profile leave a file that carries its own colour calibration (a DNG) untouched. * File > Save As / Save a Copy / CLI can write PDF: one flattened page with the embedded profile (#3401) * Codecs: open Cineon (.cin) film frames read-only Cineon is the 10-bit logarithmic film-scan format DPX grew from. Read it from the same Kodak header family: the generic header (big-endian, orientation at 192, channel count at 193, the channel-1 bits/dimensions), one or three channels, three 10-bit samples packed into each 32-bit word on a word boundary. The 10-bit code values open as U16 (round(c x 65535 / 1023)); the file's log black/white points are a display choice and are not applied. A depth other than 10 bits or a channel count other than 1/3 is Unsupported; truncation errors rather than panicking. Read-only (no Cineon writer), so .cin joins the open filters and Cineon read closes part of #1049's G10 gap. * Vector: bound flattening memory, keep over-budget dashes, cheaper edge build (#5195) - flatten: a path's flattened points are capped at 1 << 20; past it every curve keeps a proportional share of its pieces (at least one), so hostile handles from files (vmsk, SVG, effect outlines) cannot exhaust memory. fill_rasterizer flattens the whole path under one budget. - stroke: past the dash step budget (or below coordinate precision), dash_polyline keeps the dashes made so far and draws the rest solid instead of dropping the stroke. Open polylines are no longer copied. - raster: coordinates are clamped to +-1e15 and edges with a non-finite slope (vanishingly short) are skipped, so huge finite coordinates no longer produce NaN coverage. fill_rasterizer sorts edges once instead of after every component (stable sort, identical order). - Cleanups: dead miter `half`, dead loop-length check in trace_mask, shared mask render body, map_knots helper, shared straight-segment test. Co-Authored-By: Claude Opus 5.5 <[email protected]> * PSD: cap pattern memory across a parse, reject mixed-depth pattern channels (#5198) - Patterns: one PatternBudget (decoded bytes + zero padding) is shared by every pattern of a block, a .pat file and the pattern blocks of one document; .abr patt sections are charged to the samples' budget. - Patterns: a channel whose depth differs from the first is rejected (the parsed pattern could not be written back). - Slices: descriptor numbers no longer wrap; out-of-range ids are no id, coordinates clamp to i32, and a v6 trailing entry without a usable id attaches its outsets to no slice. - LayerInfo::unpadded_len is computed from field sizes instead of serializing every record into a scratch buffer. - abr desc presets are moved out instead of cloned; UnFl and shmd counts use the bounded preallocation; ImageData::decode_channel (decoded every plane to return one, unused outside its test) is removed. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Vector: bound stroke outline points (#5211) - stroke_polygons dashes all lines first, then counts round caps and joins. When their full-resolution tessellation would exceed 1 << 20 outline points, every round cap and join gets the same smaller segment count (down to 4 per circle, 1 per join; area-exact radius kept). Strokes under the budget are unchanged; no piece is dropped. - The dash step budget (100000) is now shared by all lines of a stroke instead of per polyline, so many subpaths cannot multiply the dash (and cap) count. Past it the rest is drawn solid, as before. Co-Authored-By: Claude Opus 5.5 <[email protected]> * PSD: write tiff blocks without copies, inflate merged ZIP once - ImageSourceData::to_bytes writes the global mask and global blocks straight from the model with 4-byte alignment instead of cloning each block (data included) to set its padding. Same bytes. - The tiff transcoder's nested regions and blocks write into the parent's buffer (taken and handed back, error or not) instead of building their own output and copying it up per nesting level. Bodies that fail or are not transcoded are truncated before being copied as stored or dropped. Same bytes. - PsdFile::from_bytes_and_merged decodes the merged image with the parse when asked: a ZIP stream is inflated once (the decode's stream check is the parse's); the parse-only check runs only when the decode fails, to keep from_bytes' errors. PSD import uses it with merged_needed. - PsdBuilder::composite documents that Gray/CMYK composites stay straight. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Content-Aware Fill: measure a thin hole by its thickness, not its bounds (#1366) Removing long, thin objects (coiled guitar strings, wires) left their outline as darker and lighter blotches and copied pieces of other strings into the fill. The pyramid shrank until the hole's bounding box was four patches across: for a string across the image that is a level where the hole has swallowed its surroundings, so the matches found there copy whatever looks like the blur, and the finer levels (refine-only for wide bounds) keep them. A hole at most a quarter as thick as its bounds are long now counts as thin: the pyramid stops while it is still about two patches thick, and that level starts from a membrane across it (over a few pixels, the surroundings themselves) instead of the onion peel, whose exhaustive search is what made a shallow pyramid unaffordable. Compact holes are unchanged bit for bit. Refs #1366 * perf(compose,gpu): effect caches reserve tile addresses instead of pinning layers Effect map cache entries kept a clone of their layer (a deep clone for groups) so the tile addresses the cache key hashes couldn't be reused. That kept an edited or deleted layer's pixels alive, uncounted, until the entry was evicted. Keep a Weak to each hashed tile instead: it reserves the address without keeping the pixels. The GPU compositor's styled-group pins and shape-source tile snapshots do the same. Part of #4530. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(history): count snapshot-only tiles against the history budget History panel snapshots outlived every budget: a Merged Layers snapshot is a full flattened copy that neither the history budget, the GPU budget's document bytes nor Purge accounted for. Trim now counts the tiles only snapshots hold as fixed cost, the GPU budget includes them, and Purge > Histories no longer reports tiles a kept snapshot still holds as freed. Part of #4530. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Content-Aware Fill: every part of a thin selection keeps its surroundings (#1366) Two more causes of ghosts when removing strings and the small knobs selected with them: * Auto sampling summarises the selection's surroundings by a few clusters. Around a long selection those come from the strings' surroundings, so a small part elsewhere (a tuning knob in a differently coloured corner) lost its own surroundings and was filled from far away in another colour. Around a thin selection the Auto area now always reaches as far as the selection is thick. * The membrane that starts a thin hole now starts a patch radius out, past the anti-aliased fringe or halo that a selection drawn close to the object leaves outside it. Refs #1366 * Content-Aware Fill: peel a thin hole where that stays affordable (#1366) Starting every thin hole from a membrane smoothed Spot Healing on film grain (spot_healing_keeps_film_grain_through_overlapping_strokes: grain 9.0 against 12.1 around). The onion peel copies the grain; it was only given up because its exhaustive search is too slow on the big level a long thin hole stops at. It now starts the thin hole whenever hole cells times sources stay within 2^28 comparisons, and the membrane only beyond. Refs #1366 * Select Subject: keep subject parts coloured like the backdrop, leave enclosed gaps open Users on #2788 reported Select Subject dropping clothes that are close in colour to a plain wall and bridging the gap between an arm and the torso. Both come from the same place: the saliency pass labels low-contrast parts of the subject as background, the background colour model then learns them and explains them away, and the final blind hole fill closes every enclosed gap up to 5 % of the mask. Two changes in crates/algo: - A second GrabCut pass whose background model only knows the backdrop. A colour model fitted to the image frame (minus a margin around the salient blob, in case the subject runs out of the picture) marks every pixel it explains as probable background; the blob and whatever the backdrop cannot account for start as probable foreground. Only components that touch the first pass's result survive. - Hole filling that consults the colour models: specks (<= 0.2 % of the mask) always fill, larger holes up to 5 % fill only when their pixels fit the foreground model better than the background's. Tests: two regressions (a cream-shouldered figure on a near-white wall keeps its shoulders; a ring keeps its backdrop-coloured hole open), plus ignored timing and picture-dump helpers. The 6 MP first run goes from about 100 ms to about 200 ms on one core; the P29 repeat path is untouched. * Keyboard shortcut sets: Set menu, New Set, Save, Delete, .kys Export and Summarize (K2) Edit › Keyboard Shortcuts gets Photoshop's set management. A set is a saved copy of the bindings (Preferences.shortcutSets, the one in use in shortcutSet; "" is PhotoCraft Defaults), and the Set menu shows "(modified)" when the bindings differ from it. New Set…, Save and Delete wait for OK like the rest of the dialog. Export… writes the dialog's bindings as a .kys file (commands then tools, Photoshop's macOS key spelling), each row carrying our id as pc-id so PhotoCraft imports its own sets back exactly, whatever the label; Summarize… writes Photoshop's HTML list of every bound command by menu. edit.keyboardShortcuts takes useSet (before the call's edits), saveSet, deleteSet, sets and activeSet (after them), exportKys and summarize; every reply reports the sets, the set in use and whether it is modified. Bad names, keys or counts are errors that change nothing. * .kys import: tool keys and sets saved on Windows (K3) Photoshop's <tool> rows now import: each matches our tool by name (our tool names are Photoshop's) or by pc-id in sets PhotoCraft exported, and a letter other than the tool's own becomes its override, while its own letter keeps the tool cycling with the others (#5036). A set saved on Windows spells the primary modifier Ctrl where a Mac set writes Cmd and keeps Control for the Control key. A set with no Cmd or Opt anywhere but some Ctrl is read as a Windows set, its Ctrl as our Cmd: Ctrl+L is the Cmd+L default, not a Control-key override that would also mean Control on a Mac. The import reports `windows`. * cargo fmt the extension lists Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Gert Pellin <[email protected]> Co-authored-by: timothydaly1 <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Fabio Lissi <[email protected]> Co-authored-by: vangel76 <[email protected]> Co-authored-by: Finn Newick <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> Co-authored-by: Diego Chagas <[email protected]> Co-authored-by: Tanish Manku <[email protected]> |
||
|
|
4e1e325b17 |
PSD: a smart object's filter mask shows when the file opens (#5005)
* TIFF: read and write CIE L*a*b* (CIELab, ICCLab); Lab documents stay Lab Photoshop saves Lab documents as CIELab TIFFs (PhotometricInterpretation 8, a* and b* signed), which PhotoCraft could not open, and PhotoCraft saved Lab documents as sRGB TIFFs. - codecs: a Lab channel layout (Lab, LabA) holding a* and b* as PhotoCraft's Lab documents do ((a + 128) / 255; 16 bits like the PSD importer), with its own Lab <-> sRGB conversion (same formulas as photocraft-color; codecs stays standalone) for formats without Lab, which now warn LabConverted. - TIFF read: CIELab and ICCLab (8 and 16 bits, alpha) are handed to the tiff crate as RGB and labelled Lab; a* and b* are converted from the file's encoding (signed a / 256a, or offset). - TIFF write: Lab and LabA as CIELab, as Photoshop writes it; float Lab is written with 16 bits (DepthReduced). - io: a Lab TIFF opens as a Lab document; a Lab document with one plain layer is written to TIFF in Lab (other formats still use sRGB). Photoshop 2025's Lab TIFFs open with the colours Photoshop shows (its own RGB conversion matches within one level at 8 bits and exactly at 16 bits), and Photoshop reads PhotoCraft's Lab TIFFs back with the same L*a*b* values. Fixes #4936 * PSD: a smart object's filter mask shows when the file opens Photoshop stores a smart object's layer pixels with its filters applied but not its filter mask. When it composites, it mixes the unfiltered pixels (cached in FEid) back in through the mask. We showed the stored pixels as they were, so a PSD whose smart filter has a mask opened with the filter everywhere. Only after something re-rendered the smart object did the mask appear, because our re-render mixes it. - compose::filter_mask holds the mix, moved from the engine, and its inverse. - Import decodes the unfiltered pixels from FEid (smart_map::unfiltered_from_item) and keeps the mixed result as the smart object's pixels, as the engine's re-render does. - Export writes the layer record the way Photoshop does: unmixed with the unfiltered pixels it writes (or keeps) in FEid, so Photoshop mixes back to what the document showed. Transparent pixels stay empty. Photoshop oracles: sf-filter-mask-hard, sf-filter-mask-gradient and sf-stack-blend-and-mask pass (145 -> 148 of 258); floor 145 -> 148. psd-tools and io corpora unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]> * DNG: decode lossy (JPEG-compressed) DNG tiles (FILE-215-11) DNG 1.4 lossy JPEG (compression 34892) stores each tile or strip as a baseline-DCT JPEG of 8-bit samples: a gray stream for CFA data, a YCbCr stream for three-sample LinearRaw. The decoder arm from #2021 read the streams but widened every sample by 257, so the values no longer matched the file's levels: an 8-bit WhiteLevel (or its default, 255) clipped everything to white and a 256-entry LinearizationTable was indexed past its end. It also required the JPEG frame to match the clipped row count, which rejected the padded edge tiles real tiled files carry. The arm is now `lossy_jpeg()` in sensor.rs. The decoded 8-bit values are the stored samples, and BlackLevel, WhiteLevel (default 255 for 8-bit data) and the LinearizationTable apply to them exactly as for every other compression. A tile may be coded at the full tile size (edge tiles padded, as the spec says) or at the image's row count; a strip at its row count; the width must be the segment's. BitsPerSample must be 8 and the JPEG's component count must equal SamplesPerPixel (1 or 3); anything else is an error, never a silent conversion. The frame header is checked before any pixel is decoded and the JPEG decoder's own size cap is the segment the IFD declares (already charged against the limits), so a hostile SOF cannot size a buffer past the tile. testgen writes true 8-bit lossy fixtures (`LossyJpegTiles`, and strips via the new `LossyJpegStrips`), accepts pre-encoded `lossy_segments` for hostile-input tests, exposes `baseline_jpeg()`, and leaves WhiteLevel out when `white` is 0. The DNG tests cover CFA and LinearRaw RGB from tiles and strips at 37x23 (partial edge tiles, a short last strip), the default white level, black/white scaling and a LinearizationTable against the uncompressed path, and hostile inputs: a truncated tile, frame-size and component-count mismatches, a 65535x65535 SOF under default and tight limits, a tile over the allocation budget, and 12/16-bit or 4-sample files. The hostile mutation set gains a lossy CFA and a lossy LinearRaw file. The parity doc's DNG row is updated; the scorecard row is left for the coordinating branch. * Raw: measure the Nikon D90 image area (4288x2848 at (10, 10)) Aligning the decoded D90 sensor plane with the camera's own JPEG puts the image at (10, 10) in the 4352x2868 sensor plane, giving the camera's 4288x2848 frame (10-pixel margins; the right border reads the camera's flat 255 from column 4310). Add the measured ImageArea so a D90 NEF opens on the camera's frame instead of the uncropped plane, and make a measured profile leave a file that carries its own colour calibration (a DNG) untouched. * Codecs: open Cineon (.cin) film frames read-only Cineon is the 10-bit logarithmic film-scan format DPX grew from. Read it from the same Kodak header family: the generic header (big-endian, orientation at 192, channel count at 193, the channel-1 bits/dimensions), one or three channels, three 10-bit samples packed into each 32-bit word on a word boundary. The 10-bit code values open as U16 (round(c x 65535 / 1023)); the file's log black/white points are a display choice and are not applied. A depth other than 10 bits or a channel count other than 1/3 is Unsupported; truncation errors rather than panicking. Read-only (no Cineon writer), so .cin joins the open filters and Cineon read closes part of #1049's G10 gap. * Vector: bound flattening memory, keep over-budget dashes, cheaper edge build (#5195) - flatten: a path's flattened points are capped at 1 << 20; past it every curve keeps a proportional share of its pieces (at least one), so hostile handles from files (vmsk, SVG, effect outlines) cannot exhaust memory. fill_rasterizer flattens the whole path under one budget. - stroke: past the dash step budget (or below coordinate precision), dash_polyline keeps the dashes made so far and draws the rest solid instead of dropping the stroke. Open polylines are no longer copied. - raster: coordinates are clamped to +-1e15 and edges with a non-finite slope (vanishingly short) are skipped, so huge finite coordinates no longer produce NaN coverage. fill_rasterizer sorts edges once instead of after every component (stable sort, identical order). - Cleanups: dead miter `half`, dead loop-length check in trace_mask, shared mask render body, map_knots helper, shared straight-segment test. Co-Authored-By: Claude Opus 5.5 <[email protected]> * PSD: cap pattern memory across a parse, reject mixed-depth pattern channels (#5198) - Patterns: one PatternBudget (decoded bytes + zero padding) is shared by every pattern of a block, a .pat file and the pattern blocks of one document; .abr patt sections are charged to the samples' budget. - Patterns: a channel whose depth differs from the first is rejected (the parsed pattern could not be written back). - Slices: descriptor numbers no longer wrap; out-of-range ids are no id, coordinates clamp to i32, and a v6 trailing entry without a usable id attaches its outsets to no slice. - LayerInfo::unpadded_len is computed from field sizes instead of serializing every record into a scratch buffer. - abr desc presets are moved out instead of cloned; UnFl and shmd counts use the bounded preallocation; ImageData::decode_channel (decoded every plane to return one, unused outside its test) is removed. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Vector: bound stroke outline points (#5211) - stroke_polygons dashes all lines first, then counts round caps and joins. When their full-resolution tessellation would exceed 1 << 20 outline points, every round cap and join gets the same smaller segment count (down to 4 per circle, 1 per join; area-exact radius kept). Strokes under the budget are unchanged; no piece is dropped. - The dash step budget (100000) is now shared by all lines of a stroke instead of per polyline, so many subpaths cannot multiply the dash (and cap) count. Past it the rest is drawn solid, as before. Co-Authored-By: Claude Opus 5.5 <[email protected]> * PSD: write tiff blocks without copies, inflate merged ZIP once - ImageSourceData::to_bytes writes the global mask and global blocks straight from the model with 4-byte alignment instead of cloning each block (data included) to set its padding. Same bytes. - The tiff transcoder's nested regions and blocks write into the parent's buffer (taken and handed back, error or not) instead of building their own output and copying it up per nesting level. Bodies that fail or are not transcoded are truncated before being copied as stored or dropped. Same bytes. - PsdFile::from_bytes_and_merged decodes the merged image with the parse when asked: a ZIP stream is inflated once (the decode's stream check is the parse's); the parse-only check runs only when the decode fails, to keep from_bytes' errors. PSD import uses it with merged_needed. - PsdBuilder::composite documents that Gray/CMYK composites stay straight. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Content-Aware Fill: measure a thin hole by its thickness, not its bounds (#1366) Removing long, thin objects (coiled guitar strings, wires) left their outline as darker and lighter blotches and copied pieces of other strings into the fill. The pyramid shrank until the hole's bounding box was four patches across: for a string across the image that is a level where the hole has swallowed its surroundings, so the matches found there copy whatever looks like the blur, and the finer levels (refine-only for wide bounds) keep them. A hole at most a quarter as thick as its bounds are long now counts as thin: the pyramid stops while it is still about two patches thick, and that level starts from a membrane across it (over a few pixels, the surroundings themselves) instead of the onion peel, whose exhaustive search is what made a shallow pyramid unaffordable. Compact holes are unchanged bit for bit. Refs #1366 * perf(compose,gpu): effect caches reserve tile addresses instead of pinning layers Effect map cache entries kept a clone of their layer (a deep clone for groups) so the tile addresses the cache key hashes couldn't be reused. That kept an edited or deleted layer's pixels alive, uncounted, until the entry was evicted. Keep a Weak to each hashed tile instead: it reserves the address without keeping the pixels. The GPU compositor's styled-group pins and shape-source tile snapshots do the same. Part of #4530. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(history): count snapshot-only tiles against the history budget History panel snapshots outlived every budget: a Merged Layers snapshot is a full flattened copy that neither the history budget, the GPU budget's document bytes nor Purge accounted for. Trim now counts the tiles only snapshots hold as fixed cost, the GPU budget includes them, and Purge > Histories no longer reports tiles a kept snapshot still holds as freed. Part of #4530. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Content-Aware Fill: every part of a thin selection keeps its surroundings (#1366) Two more causes of ghosts when removing strings and the small knobs selected with them: * Auto sampling summarises the selection's surroundings by a few clusters. Around a long selection those come from the strings' surroundings, so a small part elsewhere (a tuning knob in a differently coloured corner) lost its own surroundings and was filled from far away in another colour. Around a thin selection the Auto area now always reaches as far as the selection is thick. * The membrane that starts a thin hole now starts a patch radius out, past the anti-aliased fringe or halo that a selection drawn close to the object leaves outside it. Refs #1366 * Content-Aware Fill: peel a thin hole where that stays affordable (#1366) Starting every thin hole from a membrane smoothed Spot Healing on film grain (spot_healing_keeps_film_grain_through_overlapping_strokes: grain 9.0 against 12.1 around). The onion peel copies the grain; it was only given up because its exhaustive search is too slow on the big level a long thin hole stops at. It now starts the thin hole whenever hole cells times sources stay within 2^28 comparisons, and the membrane only beyond. Refs #1366 * Select Subject: keep subject parts coloured like the backdrop, leave enclosed gaps open Users on #2788 reported Select Subject dropping clothes that are close in colour to a plain wall and bridging the gap between an arm and the torso. Both come from the same place: the saliency pass labels low-contrast parts of the subject as background, the background colour model then learns them and explains them away, and the final blind hole fill closes every enclosed gap up to 5 % of the mask. Two changes in crates/algo: - A second GrabCut pass whose background model only knows the backdrop. A colour model fitted to the image frame (minus a margin around the salient blob, in case the subject runs out of the picture) marks every pixel it explains as probable background; the blob and whatever the backdrop cannot account for start as probable foreground. Only components that touch the first pass's result survive. - Hole filling that consults the colour models: specks (<= 0.2 % of the mask) always fill, larger holes up to 5 % fill only when their pixels fit the foreground model better than the background's. Tests: two regressions (a cream-shouldered figure on a near-white wall keeps its shoulders; a ring keeps its backdrop-coloured hole open), plus ignored timing and picture-dump helpers. The 6 MP first run goes from about 100 ms to about 200 ms on one core; the P29 repeat path is untouched. * Keyboard shortcut sets: Set menu, New Set, Save, Delete, .kys Export and Summarize (K2) Edit › Keyboard Shortcuts gets Photoshop's set management. A set is a saved copy of the bindings (Preferences.shortcutSets, the one in use in shortcutSet; "" is PhotoCraft Defaults), and the Set menu shows "(modified)" when the bindings differ from it. New Set…, Save and Delete wait for OK like the rest of the dialog. Export… writes the dialog's bindings as a .kys file (commands then tools, Photoshop's macOS key spelling), each row carrying our id as pc-id so PhotoCraft imports its own sets back exactly, whatever the label; Summarize… writes Photoshop's HTML list of every bound command by menu. edit.keyboardShortcuts takes useSet (before the call's edits), saveSet, deleteSet, sets and activeSet (after them), exportKys and summarize; every reply reports the sets, the set in use and whether it is modified. Bad names, keys or counts are errors that change nothing. * .kys import: tool keys and sets saved on Windows (K3) Photoshop's <tool> rows now import: each matches our tool by name (our tool names are Photoshop's) or by pc-id in sets PhotoCraft exported, and a letter other than the tool's own becomes its override, while its own letter keeps the tool cycling with the others (#5036). A set saved on Windows spells the primary modifier Ctrl where a Mac set writes Cmd and keeps Control for the Control key. A set with no Cmd or Opt anywhere but some Ctrl is read as a Windows set, its Ctrl as our Cmd: Ctrl+L is the Cmd+L default, not a Control-key override that would also mean Control on a Mac. The import reports `windows`. * cargo fmt the extension lists Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Gert Pellin <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Fabio Lissi <[email protected]> Co-authored-by: Vikram Bhaskaran <[email protected]> Co-authored-by: vangel76 <[email protected]> Co-authored-by: Finn Newick <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> Co-authored-by: Diego Chagas <[email protected]> Co-authored-by: Tanish Manku <[email protected]> |
||
|
|
8a6af66cf6 |
DNG: decode lossy (JPEG-compressed) DNG tiles (FILE-215-11) (#5112)
* TIFF: read and write CIE L*a*b* (CIELab, ICCLab); Lab documents stay Lab Photoshop saves Lab documents as CIELab TIFFs (PhotometricInterpretation 8, a* and b* signed), which PhotoCraft could not open, and PhotoCraft saved Lab documents as sRGB TIFFs. - codecs: a Lab channel layout (Lab, LabA) holding a* and b* as PhotoCraft's Lab documents do ((a + 128) / 255; 16 bits like the PSD importer), with its own Lab <-> sRGB conversion (same formulas as photocraft-color; codecs stays standalone) for formats without Lab, which now warn LabConverted. - TIFF read: CIELab and ICCLab (8 and 16 bits, alpha) are handed to the tiff crate as RGB and labelled Lab; a* and b* are converted from the file's encoding (signed a / 256a, or offset). - TIFF write: Lab and LabA as CIELab, as Photoshop writes it; float Lab is written with 16 bits (DepthReduced). - io: a Lab TIFF opens as a Lab document; a Lab document with one plain layer is written to TIFF in Lab (other formats still use sRGB). Photoshop 2025's Lab TIFFs open with the colours Photoshop shows (its own RGB conversion matches within one level at 8 bits and exactly at 16 bits), and Photoshop reads PhotoCraft's Lab TIFFs back with the same L*a*b* values. Fixes #4936 * DNG: decode lossy (JPEG-compressed) DNG tiles (FILE-215-11) DNG 1.4 lossy JPEG (compression 34892) stores each tile or strip as a baseline-DCT JPEG of 8-bit samples: a gray stream for CFA data, a YCbCr stream for three-sample LinearRaw. The decoder arm from #2021 read the streams but widened every sample by 257, so the values no longer matched the file's levels: an 8-bit WhiteLevel (or its default, 255) clipped everything to white and a 256-entry LinearizationTable was indexed past its end. It also required the JPEG frame to match the clipped row count, which rejected the padded edge tiles real tiled files carry. The arm is now `lossy_jpeg()` in sensor.rs. The decoded 8-bit values are the stored samples, and BlackLevel, WhiteLevel (default 255 for 8-bit data) and the LinearizationTable apply to them exactly as for every other compression. A tile may be coded at the full tile size (edge tiles padded, as the spec says) or at the image's row count; a strip at its row count; the width must be the segment's. BitsPerSample must be 8 and the JPEG's component count must equal SamplesPerPixel (1 or 3); anything else is an error, never a silent conversion. The frame header is checked before any pixel is decoded and the JPEG decoder's own size cap is the segment the IFD declares (already charged against the limits), so a hostile SOF cannot size a buffer past the tile. testgen writes true 8-bit lossy fixtures (`LossyJpegTiles`, and strips via the new `LossyJpegStrips`), accepts pre-encoded `lossy_segments` for hostile-input tests, exposes `baseline_jpeg()`, and leaves WhiteLevel out when `white` is 0. The DNG tests cover CFA and LinearRaw RGB from tiles and strips at 37x23 (partial edge tiles, a short last strip), the default white level, black/white scaling and a LinearizationTable against the uncompressed path, and hostile inputs: a truncated tile, frame-size and component-count mismatches, a 65535x65535 SOF under default and tight limits, a tile over the allocation budget, and 12/16-bit or 4-sample files. The hostile mutation set gains a lossy CFA and a lossy LinearRaw file. The parity doc's DNG row is updated; the scorecard row is left for the coordinating branch. * Raw: measure the Nikon D90 image area (4288x2848 at (10, 10)) Aligning the decoded D90 sensor plane with the camera's own JPEG puts the image at (10, 10) in the 4352x2868 sensor plane, giving the camera's 4288x2848 frame (10-pixel margins; the right border reads the camera's flat 255 from column 4310). Add the measured ImageArea so a D90 NEF opens on the camera's frame instead of the uncropped plane, and make a measured profile leave a file that carries its own colour calibration (a DNG) untouched. * Codecs: open Cineon (.cin) film frames read-only Cineon is the 10-bit logarithmic film-scan format DPX grew from. Read it from the same Kodak header family: the generic header (big-endian, orientation at 192, channel count at 193, the channel-1 bits/dimensions), one or three channels, three 10-bit samples packed into each 32-bit word on a word boundary. The 10-bit code values open as U16 (round(c x 65535 / 1023)); the file's log black/white points are a display choice and are not applied. A depth other than 10 bits or a channel count other than 1/3 is Unsupported; truncation errors rather than panicking. Read-only (no Cineon writer), so .cin joins the open filters and Cineon read closes part of #1049's G10 gap. * Vector: bound flattening memory, keep over-budget dashes, cheaper edge build (#5195) - flatten: a path's flattened points are capped at 1 << 20; past it every curve keeps a proportional share of its pieces (at least one), so hostile handles from files (vmsk, SVG, effect outlines) cannot exhaust memory. fill_rasterizer flattens the whole path under one budget. - stroke: past the dash step budget (or below coordinate precision), dash_polyline keeps the dashes made so far and draws the rest solid instead of dropping the stroke. Open polylines are no longer copied. - raster: coordinates are clamped to +-1e15 and edges with a non-finite slope (vanishingly short) are skipped, so huge finite coordinates no longer produce NaN coverage. fill_rasterizer sorts edges once instead of after every component (stable sort, identical order). - Cleanups: dead miter `half`, dead loop-length check in trace_mask, shared mask render body, map_knots helper, shared straight-segment test. Co-Authored-By: Claude Opus 5.5 <[email protected]> * PSD: cap pattern memory across a parse, reject mixed-depth pattern channels (#5198) - Patterns: one PatternBudget (decoded bytes + zero padding) is shared by every pattern of a block, a .pat file and the pattern blocks of one document; .abr patt sections are charged to the samples' budget. - Patterns: a channel whose depth differs from the first is rejected (the parsed pattern could not be written back). - Slices: descriptor numbers no longer wrap; out-of-range ids are no id, coordinates clamp to i32, and a v6 trailing entry without a usable id attaches its outsets to no slice. - LayerInfo::unpadded_len is computed from field sizes instead of serializing every record into a scratch buffer. - abr desc presets are moved out instead of cloned; UnFl and shmd counts use the bounded preallocation; ImageData::decode_channel (decoded every plane to return one, unused outside its test) is removed. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Vector: bound stroke outline points (#5211) - stroke_polygons dashes all lines first, then counts round caps and joins. When their full-resolution tessellation would exceed 1 << 20 outline points, every round cap and join gets the same smaller segment count (down to 4 per circle, 1 per join; area-exact radius kept). Strokes under the budget are unchanged; no piece is dropped. - The dash step budget (100000) is now shared by all lines of a stroke instead of per polyline, so many subpaths cannot multiply the dash (and cap) count. Past it the rest is drawn solid, as before. Co-Authored-By: Claude Opus 5.5 <[email protected]> * PSD: write tiff blocks without copies, inflate merged ZIP once - ImageSourceData::to_bytes writes the global mask and global blocks straight from the model with 4-byte alignment instead of cloning each block (data included) to set its padding. Same bytes. - The tiff transcoder's nested regions and blocks write into the parent's buffer (taken and handed back, error or not) instead of building their own output and copying it up per nesting level. Bodies that fail or are not transcoded are truncated before being copied as stored or dropped. Same bytes. - PsdFile::from_bytes_and_merged decodes the merged image with the parse when asked: a ZIP stream is inflated once (the decode's stream check is the parse's); the parse-only check runs only when the decode fails, to keep from_bytes' errors. PSD import uses it with merged_needed. - PsdBuilder::composite documents that Gray/CMYK composites stay straight. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Content-Aware Fill: measure a thin hole by its thickness, not its bounds (#1366) Removing long, thin objects (coiled guitar strings, wires) left their outline as darker and lighter blotches and copied pieces of other strings into the fill. The pyramid shrank until the hole's bounding box was four patches across: for a string across the image that is a level where the hole has swallowed its surroundings, so the matches found there copy whatever looks like the blur, and the finer levels (refine-only for wide bounds) keep them. A hole at most a quarter as thick as its bounds are long now counts as thin: the pyramid stops while it is still about two patches thick, and that level starts from a membrane across it (over a few pixels, the surroundings themselves) instead of the onion peel, whose exhaustive search is what made a shallow pyramid unaffordable. Compact holes are unchanged bit for bit. Refs #1366 * perf(compose,gpu): effect caches reserve tile addresses instead of pinning layers Effect map cache entries kept a clone of their layer (a deep clone for groups) so the tile addresses the cache key hashes couldn't be reused. That kept an edited or deleted layer's pixels alive, uncounted, until the entry was evicted. Keep a Weak to each hashed tile instead: it reserves the address without keeping the pixels. The GPU compositor's styled-group pins and shape-source tile snapshots do the same. Part of #4530. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(history): count snapshot-only tiles against the history budget History panel snapshots outlived every budget: a Merged Layers snapshot is a full flattened copy that neither the history budget, the GPU budget's document bytes nor Purge accounted for. Trim now counts the tiles only snapshots hold as fixed cost, the GPU budget includes them, and Purge > Histories no longer reports tiles a kept snapshot still holds as freed. Part of #4530. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Content-Aware Fill: every part of a thin selection keeps its surroundings (#1366) Two more causes of ghosts when removing strings and the small knobs selected with them: * Auto sampling summarises the selection's surroundings by a few clusters. Around a long selection those come from the strings' surroundings, so a small part elsewhere (a tuning knob in a differently coloured corner) lost its own surroundings and was filled from far away in another colour. Around a thin selection the Auto area now always reaches as far as the selection is thick. * The membrane that starts a thin hole now starts a patch radius out, past the anti-aliased fringe or halo that a selection drawn close to the object leaves outside it. Refs #1366 * Content-Aware Fill: peel a thin hole where that stays affordable (#1366) Starting every thin hole from a membrane smoothed Spot Healing on film grain (spot_healing_keeps_film_grain_through_overlapping_strokes: grain 9.0 against 12.1 around). The onion peel copies the grain; it was only given up because its exhaustive search is too slow on the big level a long thin hole stops at. It now starts the thin hole whenever hole cells times sources stay within 2^28 comparisons, and the membrane only beyond. Refs #1366 * Select Subject: keep subject parts coloured like the backdrop, leave enclosed gaps open Users on #2788 reported Select Subject dropping clothes that are close in colour to a plain wall and bridging the gap between an arm and the torso. Both come from the same place: the saliency pass labels low-contrast parts of the subject as background, the background colour model then learns them and explains them away, and the final blind hole fill closes every enclosed gap up to 5 % of the mask. Two changes in crates/algo: - A second GrabCut pass whose background model only knows the backdrop. A colour model fitted to the image frame (minus a margin around the salient blob, in case the subject runs out of the picture) marks every pixel it explains as probable background; the blob and whatever the backdrop cannot account for start as probable foreground. Only components that touch the first pass's result survive. - Hole filling that consults the colour models: specks (<= 0.2 % of the mask) always fill, larger holes up to 5 % fill only when their pixels fit the foreground model better than the background's. Tests: two regressions (a cream-shouldered figure on a near-white wall keeps its shoulders; a ring keeps its backdrop-coloured hole open), plus ignored timing and picture-dump helpers. The 6 MP first run goes from about 100 ms to about 200 ms on one core; the P29 repeat path is untouched. * Keyboard shortcut sets: Set menu, New Set, Save, Delete, .kys Export and Summarize (K2) Edit › Keyboard Shortcuts gets Photoshop's set management. A set is a saved copy of the bindings (Preferences.shortcutSets, the one in use in shortcutSet; "" is PhotoCraft Defaults), and the Set menu shows "(modified)" when the bindings differ from it. New Set…, Save and Delete wait for OK like the rest of the dialog. Export… writes the dialog's bindings as a .kys file (commands then tools, Photoshop's macOS key spelling), each row carrying our id as pc-id so PhotoCraft imports its own sets back exactly, whatever the label; Summarize… writes Photoshop's HTML list of every bound command by menu. edit.keyboardShortcuts takes useSet (before the call's edits), saveSet, deleteSet, sets and activeSet (after them), exportKys and summarize; every reply reports the sets, the set in use and whether it is modified. Bad names, keys or counts are errors that change nothing. * .kys import: tool keys and sets saved on Windows (K3) Photoshop's <tool> rows now import: each matches our tool by name (our tool names are Photoshop's) or by pc-id in sets PhotoCraft exported, and a letter other than the tool's own becomes its override, while its own letter keeps the tool cycling with the others (#5036). A set saved on Windows spells the primary modifier Ctrl where a Mac set writes Cmd and keeps Control for the Control key. A set with no Cmd or Opt anywhere but some Ctrl is read as a Windows set, its Ctrl as our Cmd: Ctrl+L is the Cmd+L default, not a Control-key override that would also mean Control on a Mac. The import reports `windows`. * cargo fmt the extension lists Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Gert Pellin <[email protected]> Co-authored-by: Vikram Bhaskaran <[email protected]> Co-authored-by: vangel76 <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Finn Newick <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> Co-authored-by: Diego Chagas <[email protected]> Co-authored-by: Tanish Manku <[email protected]> |
||
|
|
4446ec6d65 |
PSD: write tiff blocks without copies, inflate merged ZIP once (#5221)
* Raw: measure the Nikon D90 image area (4288x2848 at (10, 10)) Aligning the decoded D90 sensor plane with the camera's own JPEG puts the image at (10, 10) in the 4352x2868 sensor plane, giving the camera's 4288x2848 frame (10-pixel margins; the right border reads the camera's flat 255 from column 4310). Add the measured ImageArea so a D90 NEF opens on the camera's frame instead of the uncropped plane, and make a measured profile leave a file that carries its own colour calibration (a DNG) untouched. * Codecs: open Cineon (.cin) film frames read-only Cineon is the 10-bit logarithmic film-scan format DPX grew from. Read it from the same Kodak header family: the generic header (big-endian, orientation at 192, channel count at 193, the channel-1 bits/dimensions), one or three channels, three 10-bit samples packed into each 32-bit word on a word boundary. The 10-bit code values open as U16 (round(c x 65535 / 1023)); the file's log black/white points are a display choice and are not applied. A depth other than 10 bits or a channel count other than 1/3 is Unsupported; truncation errors rather than panicking. Read-only (no Cineon writer), so .cin joins the open filters and Cineon read closes part of #1049's G10 gap. * PSD: write tiff blocks without copies, inflate merged ZIP once - ImageSourceData::to_bytes writes the global mask and global blocks straight from the model with 4-byte alignment instead of cloning each block (data included) to set its padding. Same bytes. - The tiff transcoder's nested regions and blocks write into the parent's buffer (taken and handed back, error or not) instead of building their own output and copying it up per nesting level. Bodies that fail or are not transcoded are truncated before being copied as stored or dropped. Same bytes. - PsdFile::from_bytes_and_merged decodes the merged image with the parse when asked: a ZIP stream is inflated once (the decode's stream check is the parse's); the parse-only check runs only when the decode fails, to keep from_bytes' errors. PSD import uses it with merged_needed. - PsdBuilder::composite documents that Gray/CMYK composites stay straight. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Keyboard shortcut sets: Set menu, New Set, Save, Delete, .kys Export and Summarize (K2) Edit › Keyboard Shortcuts gets Photoshop's set management. A set is a saved copy of the bindings (Preferences.shortcutSets, the one in use in shortcutSet; "" is PhotoCraft Defaults), and the Set menu shows "(modified)" when the bindings differ from it. New Set…, Save and Delete wait for OK like the rest of the dialog. Export… writes the dialog's bindings as a .kys file (commands then tools, Photoshop's macOS key spelling), each row carrying our id as pc-id so PhotoCraft imports its own sets back exactly, whatever the label; Summarize… writes Photoshop's HTML list of every bound command by menu. edit.keyboardShortcuts takes useSet (before the call's edits), saveSet, deleteSet, sets and activeSet (after them), exportKys and summarize; every reply reports the sets, the set in use and whether it is modified. Bad names, keys or counts are errors that change nothing. * .kys import: tool keys and sets saved on Windows (K3) Photoshop's <tool> rows now import: each matches our tool by name (our tool names are Photoshop's) or by pc-id in sets PhotoCraft exported, and a letter other than the tool's own becomes its override, while its own letter keeps the tool cycling with the others (#5036). A set saved on Windows spells the primary modifier Ctrl where a Mac set writes Cmd and keeps Control for the Control key. A set with no Cmd or Opt anywhere but some Ctrl is read as a Windows set, its Ctrl as our Cmd: Ctrl+L is the Cmd+L default, not a Control-key override that would also mean Control on a Mac. The import reports `windows`. * cargo fmt the extension lists Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Vikram Bhaskaran <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> Co-authored-by: Tanish Manku <[email protected]> |
||
|
|
5dafbeeafc |
Codecs: open Cineon (.cin) film frames read-only (#4691)
* Raw: measure the Nikon D90 image area (4288x2848 at (10, 10)) Aligning the decoded D90 sensor plane with the camera's own JPEG puts the image at (10, 10) in the 4352x2868 sensor plane, giving the camera's 4288x2848 frame (10-pixel margins; the right border reads the camera's flat 255 from column 4310). Add the measured ImageArea so a D90 NEF opens on the camera's frame instead of the uncropped plane, and make a measured profile leave a file that carries its own colour calibration (a DNG) untouched. * Codecs: open Cineon (.cin) film frames read-only Cineon is the 10-bit logarithmic film-scan format DPX grew from. Read it from the same Kodak header family: the generic header (big-endian, orientation at 192, channel count at 193, the channel-1 bits/dimensions), one or three channels, three 10-bit samples packed into each 32-bit word on a word boundary. The 10-bit code values open as U16 (round(c x 65535 / 1023)); the file's log black/white points are a display choice and are not applied. A depth other than 10 bits or a channel count other than 1/3 is Unsupported; truncation errors rather than panicking. Read-only (no Cineon writer), so .cin joins the open filters and Cineon read closes part of #1049's G10 gap. * cargo fmt the extension lists Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Brandon Thomas <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]> |
||
|
|
7edc7aa03d |
Raw: measure the Nikon D90 image area (4288x2848 at (10, 10)) (#4323)
Aligning the decoded D90 sensor plane with the camera's own JPEG puts the image at (10, 10) in the 4352x2868 sensor plane, giving the camera's 4288x2848 frame (10-pixel margins; the right border reads the camera's flat 255 from column 4310). Add the measured ImageArea so a D90 NEF opens on the camera's frame instead of the uncropped plane, and make a measured profile leave a file that carries its own colour calibration (a DNG) untouched. Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
38b96f273a |
Move: Show Transform Controls frames every selected layer (#5917)
As in Photoshop, with several layers selected (Shift-click with Auto-Select, or the Move marquee) Show Transform Controls draws one frame around all of them (the Background excluded) and outlines every selected shape layer. Before, the frame showed only the active layer, so the other selected layers had no feedback on the canvas. The frame joins the boxes Free Transform would open on each layer (transform_frame), and it hides while a corner radius widget is dragged. Free Transform works on one layer, so the shared frame has no handles and a press on it doesn't start Free Transform; a drag inside still moves every selected layer. Co-authored-by: Claude Opus 5.5 <[email protected]> |
||
|
|
e47086717a |
Channels: load the targeted layer mask as selection (#5562)
* Load the targeted layer mask from Channels footer * Fix Channels test setup after mask reveal |
||
|
|
8d840eab59 |
i18n(es): complete dynamic UI translations (#4306)
* i18n(es): complete dynamic UI translations * es.tsv: keep Blur Angle once after merging main --------- Co-authored-by: gorevyoneticisi <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
7e0f48a290 |
Blur Gallery: Tilt-Shift and Spin Blur on the image (#3373) (#3829)
* Blur Gallery: Field Blur pins on the image (#3373) Field Blur opened a parameter dialog with one centre, though the engine already blends any number of pins. It now works as Photoshop's Blur Gallery workspace: the image under the dialog takes the pointer. A click adds a pin and selects it, a drag moves a pin, and a drag around the selected pin's ring turns its blur up or down (one turn is the 0-500 px range; the arc shows the amount). Delete or Backspace removes the selected pin. The dialog shows the selected pin's Blur and Preview, and opens at the right edge, beside the image, so the pins stay in view. The pins live in the dialog's fields (`pins`, `__selected`), so the existing filter preview follows every change, OK applies filter.blurGallery.fieldBlur with them in one step, and automation drives it with ui.dialog.set. Pins are fractions of the selection's bounds when there is one, else the canvas, as the engine reads them. Proxy previews now scale Blur Gallery pins' pixel blur like other pixel params. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Blur Gallery: Iris Blur's pins and handles on the image (#3373) Iris Blur opened a parameter dialog with one ellipse. It now uses the Field Blur workspace too, with Photoshop's handles on the selected pin: the four on its outline resize that axis and turn the shape, the square at its corner rounds it towards a rectangle, and the four dots inside set the sharp core, where the blur starts. A click adds a pin with the selected pin's shape and blur; pins move, their ring sets the blur, Delete removes them, and the preview follows. The pins keep the command's own keys (radiusX/radiusY as fractions of the bounds' shorter side, angle, roundness, feather), and the handles use the engine's superellipse geometry, so what the outline shows is where the filter blurs. Values from automation are cleaned before use. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Blur Gallery: Tilt-Shift and Spin Blur on the image (#3373) Tilt-Shift and Spin Blur opened parameter dialogs; they now use the Blur Gallery workspace with Photoshop's handles. Tilt-Shift: the solid lines bound the sharp band (drag to widen or narrow it), the dashed lines mark where the blur is full (drag to set the transition), the dots on the solid lines turn the band, the pin moves it and its ring sets the blur. The engine takes one band, so a click adds none and the dialog keeps the command's own centerX, centerY, angle, focus, transition and blur. Spin Blur: pins like Iris Blur's, with the outline handles resizing and turning the ellipse; the ring and the dialog set the blur angle (one turn of the ring is 360 degrees). Pins keep the command's keys (radiusX, radiusY, angle, blurAngle). Co-Authored-By: Claude Opus 5.5 <[email protected]> * i18n(vi): translate the Field Blur pin strings main added the Vietnamese catalog after this branch was cut, so every_tl_literal_is_translated failed on the merge for "No pin selected" and the pin hint. Terms follow the catalog's own: ghim (pin), Bấm (click), Delete kept as the key name. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018CiFWEfKibLnwb6oSy6ZBE * i18n(vi): translate the Iris Blur handle hint main added the Vietnamese catalog after this branch was cut; terms follow the catalog's own (tay nắm for handle, Kéo for drag). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018CiFWEfKibLnwb6oSy6ZBE * i18n(vi): translate the Tilt-Shift and Spin Blur hints main added the Vietnamese catalog after this branch was cut; terms follow the catalog's own (ghim, tay nắm, Chuyển tiếp, Góc làm mờ). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018CiFWEfKibLnwb6oSy6ZBE * Blur Gallery: Alt+Cancel Reset starts again from one pin main's Alt+Cancel → Reset (#3871) restores a filter dialog's schema defaults, and the schema has none for Field Blur's pins, so Reset left the workspace with no pins. blur_pins::reset now puts back the opening pin (in the middle, selected), and reset_fields runs it for the Blur Gallery. init is reset plus capturing the bounds, which Reset keeps. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018CiFWEfKibLnwb6oSy6ZBE * i18n(hu, tr): translate the Field Blur pin strings main added complete Hungarian and Turkish catalogs after this branch was cut. Terms follow each catalog: tű / Mezőelmosás (informal Hungarian), pim / Alan Bulanıklığı (formal Turkish). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018CiFWEfKibLnwb6oSy6ZBE * i18n(hu, tr): translate the Iris Blur handle hint Terms follow each catalog: fogantyú for handle (Hungarian), tutamaç for handle (Turkish). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018CiFWEfKibLnwb6oSy6ZBE * i18n(hu, tr): translate the Tilt-Shift and Spin Blur hints Terms follow each catalog: tű, fogantyú, Átmenet (Hungarian); pim, tutamaç, Geçiş (Turkish). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018CiFWEfKibLnwb6oSy6ZBE * i18n(sr): translate the Field Blur pin strings Main gained a complete Serbian catalogue (#4436); every_tl_literal_is_translated needs these strings there too. Co-Authored-By: Claude Opus 5.5 <[email protected]> * i18n(sr): translate the Iris Blur handle hint Main gained a complete Serbian catalogue (#4436); every_tl_literal_is_translated needs these strings there too. Co-Authored-By: Claude Opus 5.5 <[email protected]> * i18n(sr): translate the Tilt-Shift and Spin Blur hints Main gained a complete Serbian catalogue (#4436); every_tl_literal_is_translated needs these strings there too. Co-Authored-By: Claude Opus 5.5 <[email protected]> * i18n: keep main's catalog row where both added the same label Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs/gaps.md: the Tilt-Shift and Spin paragraph replaces the Field/Iris one --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
cd046f4a70 |
Arrow-key nudges: a run of nudges is one History state, with Tools preferences (#2259) (#2764)
* Arrow-key nudges: a run of nudges is one History state, with Tools preferences (#2259) Tapping or holding an arrow key nudged the selected layers one pixel per History state, so one Undo took back one pixel. Nudges of the same thing that follow each other now share one `coalesce` key (the engine already folds edits with the same key into one step), so a run is one state and one Undo takes it back. - nudge_bundle.rs decides which run a nudge belongs to: the previous nudge was of the same thing (layers, selection outline), came within the pause, and left the document as it is now (no other edit, Undo/Redo, layer selection or save in between). - move_mods.rs runs layer.translate and select.transformSelection through it; Alt-nudges (a copy each) never join a run. Selected pixels float until dropped, which is one state already. - Free Transform keeps its own session history; its box nudges fold into one session step (transform_tool.rs). - Tools preferences: Bundle Arrow-Key Nudges (default on; off gives a state per press as before) and Nudge Bundle Pause (ms) (default 1000, 100..10000, hidden while bundling is off). Labels translated in all catalogues. The two #1428 tests that asserted a state per press for the selection outline now cover both settings. * Regenerate docs/scorecard.md after merging main --------- Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
a61df64cc9 |
feat(ui): select guides, the guide context menu and Edit Selected Guides (#3734)
* feat(engine): view.editGuide changes a guide in one step Photoshop's Edit Selected Guides (the guide context menu) reopens New Guide on the selected guide. view.editGuide takes the guide by orientation and index and changes whatever is given: "newOrientation", "position" (pixels or a length with a unit, % of the new orientation's extent) and "color" (a name or "#rrggbb"), as one undoable "Edit Guide" step. A guide that keeps its orientation keeps its index; one that turns goes to the end of the other list. Without "color" the guide keeps its own colour, or none. Bad params are refused without a history step. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(ui): select guides, the guide context menu and Edit Selected Guides As in Photoshop, a click on a guide with the Move tool (or ⌘ with a selection tool) selects it, drawn in Photoshop's selected-guide blue (#0000ff); a press anywhere else deselects it, and Delete or Backspace deletes it, before Edit › Clear can reach the pixels. A guide that is dragged stays selected, and a click no longer nudges a guide to the pointer. One guide at a time. Right-click on a guide selects it and opens Photoshop's guide menu: Edit Selected Guides, Lock Guides, Clear Guides, Clear Canvas Guides, Clear Selected Guides and Clear Selected Artboard Guides. Edit Selected Guides opens the New Guide dialog on the guide (its orientation, its position in the ruler unit and its colour, Cyan for a guide dragged from a ruler) and OK runs view.editGuide, sending the colour only when it was changed. The selection is view state (UiState.selected_guide, readable over the control channel), keyed by document, orientation and position like guide colours, so it lapses by itself when its guide goes. The menu's two new labels are in every catalogue. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(ui): Alt-click turns a guide; double-click opens the guide preferences As in Photoshop, with the Move tool: - Alt-clicking a guide turns it: a vertical guide becomes horizontal through the point clicked, and the other way round. It keeps its colour and stays selected; one undoable Edit Guide step (view.editGuide). Locked or hidden guides can't be turned. - Double-clicking a guide opens Preferences › Guides, Grid & Slices. docs/ui-parity.md lists the guide interactions. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(i18n): Hungarian and Turkish strings for the guide menu Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(ui): the spelling rows call engine_call with the app main's spelling suggestions (choose_spelling) came in calling engine_call(menu, command); on this branch engine_call takes the app first (the guide menu needs it), so the merge didn't compile. Co-Authored-By: Claude Opus 5.5 <[email protected]> * i18n(sr): translate the guide context menu items Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(ui): give main's move-hover test guide drag a press point Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
9788e5ee5e |
Shortcuts: a command can answer a second key (F2/F3/F4, Shift+F7, Cmd++) (#3665) (#3746)
* Shortcuts: a command can answer a second key (F2/F3/F4, Shift+F7, Cmd++) (#3665) Photoshop's default set gives some commands a second key besides the one its menu shows: F2/F3/F4 Cut/Copy/Paste beside Cmd+X/C/V, Shift+F7 Inverse beside Cmd+Shift+I, Cmd++ beside Cmd+= for Zoom In, Shift+Backspace for Fill. A command could hold only one key here, so these couldn't exist and a .kys import kept only a row's first key (a set listing Copy as Cmd+C and F3 could leave Cmd+C unbound). - prefs::ALTERNATE_SHORTCUTS holds Photoshop's second keys; Preferences.alternate_shortcuts (alternateShortcuts) replaces a command's defaults, [] removes them. Old settings files load unchanged. - edit.keyboardShortcuts takes `alternates`; reset, moving a key (taken from another command's main key or alternates) and the conflict list count every key, and a call's changes now apply atomically. - .kys import keeps a row's other keys as the command's alternates. - Dispatch matches every key (a user's main key wins over a default alternate); the UI-only SECONDARY table is folded in, so Fill's Shift+Backspace survives an override of its main key. Menus still show only the main key. - Keyboard Shortcuts dialog shows each command's alternates, removable one by one, with Add Shortcut to add one; Use Default / Reset All restore them. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * Translate Add Shortcut into Hungarian; regenerate the scorecard Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * Scorecard: regenerate for the new alternate_shortcuts preference Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * docs: regenerate scorecard after merging main Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * Regenerate docs/scorecard.md after merging main Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * Regenerate docs/scorecard.md after merging main --------- Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
4cc0518169 |
Menu bar from the keyboard: Alt / F10 focus, Alt+letter, underlined mnemonics (#4988) (#5091)
As Photoshop's menus on Windows: a tap on Alt (or F10) puts the keyboard on the in-window menu bar, with File lit; ← and → move along it, ↓, ↩ and Space open the lit menu with its first row lit, Esc or a click leave. Alt+letter opens the menu with that mnemonic, open or not, and in an open menu a letter runs the row with that mnemonic or opens its submenu. Mnemonics are assigned per menu level from the labels as shown (so translated), unique within the level: Photoshop's F E I L Y S T V W H for the top menus where the label has the letter, else the first unused letter, word starts first; only ASCII letters and digits, since those are the keys the keyboard reports. They are underlined while Alt is held, the bar is focused or the menu was opened from the keyboard; a menu opened with the mouse shows none until Alt, as on Windows. The underline is a layout job with the placeholder colour, so disabled rows stay dim; without underlines the text path is unchanged. The shortcuts defer to the focused bar as they do to an open menu, so E opens Edit instead of picking the Eraser and the arrows don't nudge the layer. Rows carry their mnemonic in the navigation state. macOS keeps the native menu bar, where this doesn't run. Tests: two unit tests (assignment, key letters) and seven harness tests (Alt focus and arrows, second Alt / Esc / click, Alt+letter and Enter, letters in an open menu, underline conditions, the layout job, and the whole app's shortcut deferral). Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
5cb5ab52fc |
Crop tool: Content-Aware fill of the area a rotated or extended crop uncovers (#3823) (#3827)
* Crop tool: Content-Aware fill of the area a rotated or extended crop uncovers (#3823) image.crop gains contentAware (bool, default false). After a turned crop or one that extends past the canvas, the area the original image doesn't cover is filled from its surroundings on the Background (else the active pixel layer), in the crop's one undo step. Each connected piece (corner, edge strip) gets its own Content-Aware sampling window and samples only covered pixels; the patch search is Edit > Fill > Content-Aware's (fill_cmds::content_aware_native, shared). A crop that uncovers nothing is unchanged; one with nothing to sample from fails and rolls back. Crop options bar: Fill: Background (Default) / Content-Aware Fill (ToolOptions crop_content_aware, ui.set cropFill). Generative Expand is not offered. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * Crop: adapt to Document::background after main removed is_background Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * Crop: translate Background (Default) for Hungarian and Turkish Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * Fix the merge with main: UI_SET_FIELDS length, Serbian Background (Default) Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * Merge main: UI_SET_FIELDS counts both new fields --------- Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
9df067b297 |
Shapes: on-canvas corner radius widgets for live rectangles (TOOL-214-12) (#5276)
* 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 <[email protected]> * 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 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
d56aa35bce |
Blur Gallery: Iris Blur's pins and handles on the image (#3373) (#3822)
* Blur Gallery: Field Blur pins on the image (#3373) Field Blur opened a parameter dialog with one centre, though the engine already blends any number of pins. It now works as Photoshop's Blur Gallery workspace: the image under the dialog takes the pointer. A click adds a pin and selects it, a drag moves a pin, and a drag around the selected pin's ring turns its blur up or down (one turn is the 0-500 px range; the arc shows the amount). Delete or Backspace removes the selected pin. The dialog shows the selected pin's Blur and Preview, and opens at the right edge, beside the image, so the pins stay in view. The pins live in the dialog's fields (`pins`, `__selected`), so the existing filter preview follows every change, OK applies filter.blurGallery.fieldBlur with them in one step, and automation drives it with ui.dialog.set. Pins are fractions of the selection's bounds when there is one, else the canvas, as the engine reads them. Proxy previews now scale Blur Gallery pins' pixel blur like other pixel params. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Blur Gallery: Iris Blur's pins and handles on the image (#3373) Iris Blur opened a parameter dialog with one ellipse. It now uses the Field Blur workspace too, with Photoshop's handles on the selected pin: the four on its outline resize that axis and turn the shape, the square at its corner rounds it towards a rectangle, and the four dots inside set the sharp core, where the blur starts. A click adds a pin with the selected pin's shape and blur; pins move, their ring sets the blur, Delete removes them, and the preview follows. The pins keep the command's own keys (radiusX/radiusY as fractions of the bounds' shorter side, angle, roundness, feather), and the handles use the engine's superellipse geometry, so what the outline shows is where the filter blurs. Values from automation are cleaned before use. Co-Authored-By: Claude Opus 5.5 <[email protected]> * i18n(vi): translate the Field Blur pin strings main added the Vietnamese catalog after this branch was cut, so every_tl_literal_is_translated failed on the merge for "No pin selected" and the pin hint. Terms follow the catalog's own: ghim (pin), Bấm (click), Delete kept as the key name. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018CiFWEfKibLnwb6oSy6ZBE * i18n(vi): translate the Iris Blur handle hint main added the Vietnamese catalog after this branch was cut; terms follow the catalog's own (tay nắm for handle, Kéo for drag). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018CiFWEfKibLnwb6oSy6ZBE * Blur Gallery: Alt+Cancel Reset starts again from one pin main's Alt+Cancel → Reset (#3871) restores a filter dialog's schema defaults, and the schema has none for Field Blur's pins, so Reset left the workspace with no pins. blur_pins::reset now puts back the opening pin (in the middle, selected), and reset_fields runs it for the Blur Gallery. init is reset plus capturing the bounds, which Reset keeps. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018CiFWEfKibLnwb6oSy6ZBE * i18n(hu, tr): translate the Field Blur pin strings main added complete Hungarian and Turkish catalogs after this branch was cut. Terms follow each catalog: tű / Mezőelmosás (informal Hungarian), pim / Alan Bulanıklığı (formal Turkish). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018CiFWEfKibLnwb6oSy6ZBE * i18n(hu, tr): translate the Iris Blur handle hint Terms follow each catalog: fogantyú for handle (Hungarian), tutamaç for handle (Turkish). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018CiFWEfKibLnwb6oSy6ZBE * i18n(sr): translate the Field Blur pin strings Main gained a complete Serbian catalogue (#4436); every_tl_literal_is_translated needs these strings there too. Co-Authored-By: Claude Opus 5.5 <[email protected]> * i18n(sr): translate the Iris Blur handle hint Main gained a complete Serbian catalogue (#4436); every_tl_literal_is_translated needs these strings there too. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
21c7cf637a |
Curvature Pen: edit finished paths (drag, add, smooth/corner) with an adaptive cursor (#5197)
* Curvature Pen: drag an anchor of the active path without a modifier As in Photoshop, the Curvature Pen with no path being drawn shows the active path's anchors (work path, active shape layer or targeted vector mask), and a press-drag on one moves it with no Ctrl/Cmd. The curve reflows: every smooth knot of that subpath gets its handles recomputed with the Curvature Pen's own curvature_knots, so it keeps flowing through all its anchors; corners keep their handles. The drag previews live and commits one history step. - vector: edit::move_anchors_curvature - engine: path.moveAnchors takes "curvature": bool=false - ui-egui: direct_select routes the idle Curvature Pen press (Edit::Curvature) Co-Authored-By: Claude Opus 5.5 <[email protected]> * Curvature Pen: add anchors, toggle smooth/corner and adapt the cursor on finished paths As in Photoshop, with no path being drawn the Curvature Pen also: - adds an anchor where a segment of the active path is clicked (path.addAnchor), and a drag pulls it like any anchor; adding and dragging are one history step; - toggles an anchor between smooth and corner on a double-click (path.convertPoint with the new "curvature" param, vector edit::toggle_curvature_point): a Pen-drawn corner rounds off and the segments around it curve through it; a second double-click makes it a corner again; - shows what a press does in its cursor: + over a segment, four arrows over an anchor (also an anchor of the path being drawn). Co-Authored-By: Claude Opus 5.5 <[email protected]> * rustfmt --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
524d608b50 |
feat(psd): editable Filter Gallery smart filters (Chrome) (#3426)
* feat(psd): map Filter Gallery smart filters to filter.filterGallery (Chrome)
A PSD smart filter from Filter > Filter Gallery (`GEfc`) was kept verbatim
as psd.unsupportedFilter, so it could not be edited. Its effect stack
(`GEfs` list of `GEfc` effects with `GEfk` effect enum and `GELv`
visibility) now maps to `filter.filterGallery {effects, foreground,
background}` when every effect and setting is modelled, and is written
back as `GEfc` on export. Layout taken from a Photopea-authored PSD.
The table starts with Chrome (`Chrm`: `Dtl ` detail, `Smth` smoothness).
Stacks with other effects or unknown settings stay verbatim as before.
* feat(smart-filters): edit Filter Gallery smart filters in the gallery dialog
Double-clicking a Filter Gallery smart filter in the Layers panel did
nothing. It now opens the Filter Gallery dialog with the recorded effect
stack and colours, previewing on the pixels below that filter; OK updates
the filter in place with layer.smartFilter.setParams (one history step).
* fix(psd): read and write Photoshop's inline Filter Gallery effect
Photoshop 27 writes a Filter Gallery smart filter with a single effect
inline in `Fltr` (`GEfc { GEfk, <settings> }`) rather than as a `GEfs`
list, so such files still opened as psd.unsupportedFilter. Both layouts
are read now. A single visible effect is written inline as Photoshop does;
several or hidden effects keep the list, which is the only layout with a
visibility flag (`GELv`).
---------
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
|
||
|
|
28e58d20d9 |
fix(ui): snap distorted corners to visible guides (#3928)
Co-authored-by: zenjabba <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
a62ef68ca2 |
CMYK: Hue/Saturation and Color Balance keep the black ink (#5000)
* Modern Brightness/Contrast: Photoshop's brightness curve The modern brightness curve was a line rolled off by a v^P term. It sat up to 4 levels off Photoshop's in the highlights, and up to 2.3 levels off for lowered brightness. The curve is refit to the Photoshop ground truth we have: - the ±50 samples from 4759d12b; - the photoshop corpus's brightness +25 / contrast +30 files, inverted through the (exact) contrast curve. Raised, it is the line s*x (s = 1.375^(b/50)) up to a knee at 80/255, then a cubic Hermite to white with end slope s^-6.6. Lowered, it is the exact inverse of the curve raised by as much (the -50 samples confirm this to within a level). Every measured point is within about a level. For strong values the knee moves down so the curve stays monotone across -150..150. Brightness then contrast are evaluated through one 4096-entry table, built in 55 us. The GPU uses that same table through the Levels/Curves kernel instead of rebuilding the curves in WGSL, so GPU and CPU agree by construction. Photoshop oracles: rgb8/16/32 and gray8/16/32 brightness-contrast now pass (145 -> 151 of 258). The floor goes 133 -> 151. Co-Authored-By: Claude Opus 5.5 <[email protected]> * CMYK: Brightness/Contrast curves each ink, as Photoshop does Photoshop applies Brightness/Contrast in a CMYK document to each ink's stored value, 1 - ink, black included. Both the modern and the legacy curve do this; the cmyk8 brightness-contrast(-legacy) oracles match it within a level on every pixel of the files' own inks. Raising brightness takes ink away. We applied it to the screen colour instead. - cmyk_channel_adjustment gains Brightness/Contrast. apply_in builds the channel curve once (brightness_contrast_lut) and runs each separated ink through it (ink_through). - Destructive Image > Adjustments on a CMYK layer curves the stored inks directly, so it is exact; a selection mixes them. adjusted_channels now takes the per-pixel operation, and works out luma only for CMYK Threshold, which compares it. - The GPU leaves CMYK Brightness/Contrast to the CPU, like the other ink rules. cmyk8/16 brightness-contrast oracles, bad pixels: - modern: 72% -> 48-57%, max error 0.19 -> 0.09 - legacy: 71% -> 51-53% The rest is our separation of the RGB composite, not the rule. Co-Authored-By: Claude Opus 5.5 <[email protected]> * CMYK Threshold compares the inks' own luma, as Photoshop does Photoshop's CMYK Threshold splits on the Rec. 601 luma of the inks' plain complement, (1 - c)(1 - k) and so on, whatever the document's profile. On the files' own inks this puts every pixel of cmyk8 and cmyk16 threshold-128.psd on the right side of level 128. The profile's RGB luma missed 100. stored_channels works the luma out from the inks itself (ink_luma), so it no longer takes one; the engine's stored-channel path no longer converts each pixel to RGB at all. Through the separated RGB composite the cmyk threshold oracles drop from 1.6% to 0.2% bad pixels. Co-Authored-By: Claude Opus 5.5 <[email protected]> * CMYK legacy Brightness/Contrast pivots at 128 in ink Photoshop's legacy contrast pivots at level 128 of the ink, which is 127 of 255 of the stored value (1 - ink) that the curve runs on, not 128 as in RGB. On Photoshop's own 16-bit inks (cmyk16 brightness-contrast-legacy.psd) ours sat a constant 0.43 of a level above. That is exactly the one-level pivot shift times contrast +30's slope minus one; with the pivot moved, the inks match within 0.01. brightness_contrast_lut is now ink_brightness_contrast_lut, since its legacy curve is the CMYK one. Co-Authored-By: Claude Opus 5.5 <[email protected]> * CMYK: adjustment layers composite on the document's own inks The compositor works in display RGB. In a CMYK document, an adjustment layer that Photoshop evaluates on the inks could only act on our separation of the RGB composite, and that separation's black differs from the file's. So even exact ink rules missed: CMYK Invert was 10% off, Levels and Curves 50-65%. The ink-native compositor (compose::native, until now only for flattening plain raster stacks) now takes adjustment layers too: - Invert, Posterize, Threshold and Brightness/Contrast (modern and legacy), and Levels and Curves on CMYK channels, prepared once (InkStack, InkOp) and applied to the straight inks. - Opacity and an unfeathered layer mask mix the inks linearly, as in Photoshop; raster layers' masks scale their coverage the same way. - A CMYK stack of plain raster layers and such adjustments renders this way for display, export and reduced views (render_cmyk, tiles in parallel); flattening and merging keep the adjusted inks exactly. - Stacks of raster layers alone keep the RGB compositor, which the GPU matches. The GPU already leaves these adjustments to the CPU. Photoshop oracles: cmyk8 and cmyk16 invert, posterize-4, threshold-128, levels, curves, brightness-contrast and brightness-contrast-legacy all pass (14 files), 151 -> 165 of 258; floor 151 -> 165. psd-tools and io corpora unchanged. 24 MP CMYK8 with one adjustment renders in 286 ms on the inks, against 442 ms through the RGB compositor. Co-Authored-By: Claude Opus 5.5 <[email protected]> * CMYK ink compositing inlines open groups The ink compositor gave up on any group. Now a group at full opacity without a mask is inlined: - a pass-through group's children composite straight onto the backdrop, so an adjustment inside it reaches the layers below; - an isolated (Normal) group of raster layers alone composites as its children would, since Normal is associative. An isolated group with an adjustment, which only sees the group's content, keeps the RGB compositor. So do a translucent group and an artboard. Nesting is bounded. psd-tools' CMYK adjustment files keep their swatches in pass-through groups. With #4948's fair comparison, curves_cmyk drops from 37% to 0.3% bad pixels and levels_cmyk from 14% to 0.2%. The photoshop corpus is unchanged at 165. Co-Authored-By: Claude Opus 5.5 <[email protected]> * CMYK: Hue/Saturation and Color Balance keep the black ink In a CMYK document Photoshop applies Hue/Saturation (not Colorize) and Color Balance to C, M and Y as the complement of an RGB colour, and leaves the black ink alone. On the photoshop corpus's own inks, cmyk16 hue-saturation.psd follows this on every pixel within half a level (cmyk8 within two), and color-balance within our RGB Color Balance's own error. We applied both to the separated screen colour, which also rewrote K. - adjust::cmy_adjustment and apply_cmy run the RGB adjustment once per band on (1 - c, 1 - m, 1 - y). - The ink compositor (InkOp::Cmy), the RGB compositor's CMYK branch and destructive Image > Adjustments (pixels::adjusted_cmy) use them. The GPU leaves these to the CPU in CMYK documents, as it does the other ink rules. - Colorize separates its colour again, black included, so it keeps the screen-colour path. cmyk8/16 oracles, bad pixels: - hue-saturation: 100% -> 0.4-0.7% - color-balance: 74% -> 25% Neither passes yet. Half a level of cyan ink is three levels of red through the profile at saturated cyan, and our RGB models leave that much. The pass count stays at 165. Co-Authored-By: Claude Opus 5.5 <[email protected]> * 32-bit: legacy Brightness/Contrast, Channel Mixer, Photo Filter and B&W tint unclipped In 32-bit documents Photoshop keeps these adjustments' results above 1 and below 0. On the photoshop corpus's rgb32/gray32 files: - Legacy Brightness/Contrast pivots its contrast at 127 of 255 (8/16-bit: 128) and is exact on every pixel. - Channel Mixer cuts each source percentage to 1/1024 toward zero, counts 255/512 of the constant and is exact on pure red, green, blue, black and white. - Photo Filter works on the linear samples, negative results included. - The Black & White tint is the Color blend mode without the clip at 1, as the blend mode is there; white tints to (1.05, 1.00, 0.87). We clipped all four to 0..1. The GPU kernels follow suit under F_HDR (the legacy pivot and the mixer's steps come from the planner), and the HDR parity test covers them. Photoshop oracles: rgb32 and gray32 brightness-contrast-legacy, rgb32 channel-mixer and black-white-tint pass (145 -> 149 of 258); floor 145 -> 149. rgb32 photo-filter improves (0.19 -> 0.035 max); the rest is its colour (#3790). Co-Authored-By: Claude Opus 5.5 <[email protected]> * Modern brightness: knee at output 126, fit out to -95 and +150 psd-tools' brightnesscontrast_rgb.psd masks four Brightness/Contrast layers to their own columns. Read through the exact contrast curve, it gives Photoshop's brightness at -95, -31, +23 and +150, well past the +/-50 samples the first fit had. The +/-50 curve extrapolated badly there: 22 levels off at -95 and 25 at +150. Every setting puts the knee at about the same output level, not input. The roll-off's end slope falls as 1/(1 + 0.084 b). With the knee at 126/255 and that end slope, the cubic Hermite fits -95, -50, -31, +23, +25 and +50 within 1.4 levels. Past about +110 no such cubic stays below white, so there the roll-off is 1 - (1 - y0)(1 - u)^alpha, with the slope continuous at the knee. That gives +150 within 6 levels, where every curve family tried bottoms out. Lowered curves are still the inverse of raised ones. The photoshop corpus's six brightness-contrast oracles still pass. psd-tools brightnesscontrast_rgb goes from 0.13 to 0.020 max error and _grayscale from 0.098 to 0.016. The new measurements are pinned in modern_brightness_matches_photoshop. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Levels: whole levels at every step and Photoshop's shadow toe In 8 bits Photoshop rounds each of the three Levels steps to a whole level (half up): the input-range stretch (already modelled), the midtone gamma and the output range. A narrow output range therefore merges neighbouring gamma levels: psd-tools levels_rgb maps R 43..48 to 58 through a 0.47 channel gamma and the master's 48..255, where the unrounded curve gave 56..59 (3 levels off in blue). The gamma above 1 starts along a toe a*u - b*u^q of slope a = 2^gamma that joins the power curve with the same value and slope (q = 2.2, fitted). With the rounding, that is exact on 1116 of the 1118 levels of gamma 1.16, 1.22, 1.3, 1.78 and 2.0 oracles; the soft-min it replaces missed 52. Gamma below 1 stays the plain power curve (0.67 and 0.7: every level exact). Oracles: psd-tools levels_rgb 0.0118 -> 0.0039 (PASS); photoshop rgb8/stack-levels-curves-hue-posterize 0.2000 -> 0.0000 (PASS); rgb8 and gray8 levels.psd 0.0039 -> 0.0000; rgb16/gray16 0.0039 -> 0.0020. Floors: photoshop 145 -> 146, psd-tools 236 -> 238 (main is at 237). Co-Authored-By: Claude Opus 5.5 <[email protected]> * Masks: feathers blur with exactly the feather's variance A mask feather is a Gaussian with sigma = feather, which we approximate with three box passes per axis. Whole-pixel boxes rounded the variance: a 6 px feather blurred like 6.48 px and a 3 px one like 3.46 px, so feathered edges were too soft by up to 4 levels against Photoshop (psd-tools mask-parameters-no-real-channel.psd: a 6 px vector and 3 px pixel feather). Each pass is now an "extended box" (Gwosdek et al. 2011): weight 1 on the whole radius and a fractional weight on the two samples beyond it, so three passes have variance sigma^2 exactly. It stays O(n), reusing the window's leaving sample for the low end weight (50 MP at a 1000 px feather: 180 ms parallel, was 172). psd-tools: mask-parameters-no-real-channel 0.0168 -> 0.0060 (PASS), layer_mask_data.psd 26.9 % -> 3.9 % of pixels off (psb 25.9 % -> 1.8 %). Corpus test 239 -> 240; floor 236 -> 240. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Lab: adjustment layers composite on the stored L, a and b The compositor reads a Lab document's layers through display RGB, which clips colours outside its gamut, so the adjustments Photoshop evaluates on L, a and b (Invert, Posterize, Threshold, Brightness/Contrast, Levels and Curves with Lab records) only saw the clipped colours: the photoshop corpus's Lab oracles were up to 11 levels off on saturated colours. compose::native_lab works a Lab stack of plain layers out on the stored values instead, like the CMYK ink path: Normal mixes L, a and b linearly, an opaque layer keeps them exactly, a layer mask scales the mix, and adjustment layers run adjust::lab_stored_op on each pixel. Stacks with such adjustments render for display this way; flattening a plain Lab stack keeps its stored values (exports no longer clip). Anything else (other blend modes, effects, other adjustments) keeps the RGB compositor. Photoshop oracles 145 -> 152: lab16 invert, posterize-4, levels, curves and brightness-contrast-legacy; lab8 invert and posterize-4. lab8 levels and legacy B/C come within a level (their 8-bit rules are #5143 and #5132). psd-tools and io unchanged; every round trip unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Threshold: split on Photoshop's luminosity of whole samples Photoshop's Threshold splits on the luminosity 0.30 R + 0.59 G + 0.11 B (the blend modes' lum, as Gradient Map uses), not the Rec. 601 grey, taken over the document's whole sample values: - 8 bits: 30 R + 59 G + 11 B > 100 level - 50, i.e. the luminosity rounded to a level with halves down reaches the level; - 16 bits: 30 R + 59 G + 11 B >= 100 x the level in 16 bits (round(level x 32768 / 255): mid grey 16448 is level 128); - 32 bits: the luminosity reaches the level. Exact on every pixel of psd-tools threshold_rgb (36432 px; Rec. 601 split 28 the other way), threshold_16bits_rgb (100) and threshold_16bits_ grayscale, and the photoshop corpus's rgb16 threshold-128. The sums are exact in f32, on the CPU and in the GPU kernel, so ties (grey exactly on the level) split the same way on both. psd-tools: threshold_rgb and threshold_16bits_rgb pass; corpus test 239 -> 241, floor 236 -> 241. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Posterize: 16-bit documents bin the 16-bit sample, as Photoshop does At 16 bits Photoshop doesn't round to 8-bit levels before binning. It bins the 16-bit sample, floor(v16 * n / 32768), and lands on 8-bit levels through a 16-bit step: floor(b * 32768 / (n - 1)), converted with floor((o * 255 + 16384) / 32768). So the middle of 3 levels is 128 where 8 bits give 127, while 42.5 and 25.5 stay 42 and 25. Exact on every sample of psd-tools posterize_16bits_rgb (levels 3, 7, 13, 21) and posterize_16bits_grayscale (2, 6, 9, 11); the 8-bit rule missed up to half the range at bin edges. The same rule applies to CMYK and Lab channels at 16 bits: on the stored inks (#4945's native path) psd-tools posterize_16bits_cmyk goes from 4.26 % to 0.01 % of pixels off. 8-bit Posterize is unchanged. The GPU kernel runs the same rule; 16-bit layers reach it as half floats, so a sample within their precision of a bin edge can land in the neighbouring bin there (the CPU, which exports, is exact). psd-tools 239 -> 241; floor 236 -> 241. Co-Authored-By: Claude Opus 5.5 <[email protected]> * CMYK: Posterize works on the stored values (1 - ink) Photoshop posterizes a CMYK channel's stored value, 1 - ink, not the ink: with levels whose steps are uneven (3, 7, 13, 21) the two differ, e.g. 3 levels take an ink of 85 to 128, where posterizing the ink drops it to none. psd-tools posterize_cmyk.psd goes from 13.8 % of pixels off to exact; psd-tools 241 -> 242, floor 242. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Groups: a pass-through clipping base with an adjustment renders isolated A pass-through group composites its children straight into the backdrop, so an adjustment layer inside reaches the layers beneath. When the group is the base of a clipping group and holds an adjustment layer, Photoshop renders it isolated instead: in psd-tools passthrough_clipping_mask_adjustment an Exposure in such a group leaves the layers beneath alone (we darkened them: half the image 170 levels off). The same group with a Linear Dodge child and no adjustment keeps reaching the backdrop (passthrough_clipping_mask_blendmode), and so do clipping-mask3/4/5, so the rule is the adjustment, not the clipping alone (isolating every pass-through clipping base broke those two). CPU compositor and GPU plan alike. psd-tools 239 -> 240; floor 240. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Raise corpus floors to the results of the landed adjustment fixes Photoshop-authored 165 -> 185 of 258, io 146 -> 148 of 170, psd-tools 242 -> 249 of 309, measured with the CMYK, Lab, 32-bit, Levels, Threshold, Posterize, feather and pass-through PRs merged together. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
1e6f565a7b |
CMYK: adjustment layers composite on the document's own inks (14 oracles pass) (#4945)
* Modern Brightness/Contrast: Photoshop's brightness curve The modern brightness curve was a line rolled off by a v^P term. It sat up to 4 levels off Photoshop's in the highlights, and up to 2.3 levels off for lowered brightness. The curve is refit to the Photoshop ground truth we have: - the ±50 samples from 4759d12b; - the photoshop corpus's brightness +25 / contrast +30 files, inverted through the (exact) contrast curve. Raised, it is the line s*x (s = 1.375^(b/50)) up to a knee at 80/255, then a cubic Hermite to white with end slope s^-6.6. Lowered, it is the exact inverse of the curve raised by as much (the -50 samples confirm this to within a level). Every measured point is within about a level. For strong values the knee moves down so the curve stays monotone across -150..150. Brightness then contrast are evaluated through one 4096-entry table, built in 55 us. The GPU uses that same table through the Levels/Curves kernel instead of rebuilding the curves in WGSL, so GPU and CPU agree by construction. Photoshop oracles: rgb8/16/32 and gray8/16/32 brightness-contrast now pass (145 -> 151 of 258). The floor goes 133 -> 151. Co-Authored-By: Claude Opus 5.5 <[email protected]> * CMYK: Brightness/Contrast curves each ink, as Photoshop does Photoshop applies Brightness/Contrast in a CMYK document to each ink's stored value, 1 - ink, black included. Both the modern and the legacy curve do this; the cmyk8 brightness-contrast(-legacy) oracles match it within a level on every pixel of the files' own inks. Raising brightness takes ink away. We applied it to the screen colour instead. - cmyk_channel_adjustment gains Brightness/Contrast. apply_in builds the channel curve once (brightness_contrast_lut) and runs each separated ink through it (ink_through). - Destructive Image > Adjustments on a CMYK layer curves the stored inks directly, so it is exact; a selection mixes them. adjusted_channels now takes the per-pixel operation, and works out luma only for CMYK Threshold, which compares it. - The GPU leaves CMYK Brightness/Contrast to the CPU, like the other ink rules. cmyk8/16 brightness-contrast oracles, bad pixels: - modern: 72% -> 48-57%, max error 0.19 -> 0.09 - legacy: 71% -> 51-53% The rest is our separation of the RGB composite, not the rule. Co-Authored-By: Claude Opus 5.5 <[email protected]> * CMYK Threshold compares the inks' own luma, as Photoshop does Photoshop's CMYK Threshold splits on the Rec. 601 luma of the inks' plain complement, (1 - c)(1 - k) and so on, whatever the document's profile. On the files' own inks this puts every pixel of cmyk8 and cmyk16 threshold-128.psd on the right side of level 128. The profile's RGB luma missed 100. stored_channels works the luma out from the inks itself (ink_luma), so it no longer takes one; the engine's stored-channel path no longer converts each pixel to RGB at all. Through the separated RGB composite the cmyk threshold oracles drop from 1.6% to 0.2% bad pixels. Co-Authored-By: Claude Opus 5.5 <[email protected]> * CMYK legacy Brightness/Contrast pivots at 128 in ink Photoshop's legacy contrast pivots at level 128 of the ink, which is 127 of 255 of the stored value (1 - ink) that the curve runs on, not 128 as in RGB. On Photoshop's own 16-bit inks (cmyk16 brightness-contrast-legacy.psd) ours sat a constant 0.43 of a level above. That is exactly the one-level pivot shift times contrast +30's slope minus one; with the pivot moved, the inks match within 0.01. brightness_contrast_lut is now ink_brightness_contrast_lut, since its legacy curve is the CMYK one. Co-Authored-By: Claude Opus 5.5 <[email protected]> * CMYK: adjustment layers composite on the document's own inks The compositor works in display RGB. In a CMYK document, an adjustment layer that Photoshop evaluates on the inks could only act on our separation of the RGB composite, and that separation's black differs from the file's. So even exact ink rules missed: CMYK Invert was 10% off, Levels and Curves 50-65%. The ink-native compositor (compose::native, until now only for flattening plain raster stacks) now takes adjustment layers too: - Invert, Posterize, Threshold and Brightness/Contrast (modern and legacy), and Levels and Curves on CMYK channels, prepared once (InkStack, InkOp) and applied to the straight inks. - Opacity and an unfeathered layer mask mix the inks linearly, as in Photoshop; raster layers' masks scale their coverage the same way. - A CMYK stack of plain raster layers and such adjustments renders this way for display, export and reduced views (render_cmyk, tiles in parallel); flattening and merging keep the adjusted inks exactly. - Stacks of raster layers alone keep the RGB compositor, which the GPU matches. The GPU already leaves these adjustments to the CPU. Photoshop oracles: cmyk8 and cmyk16 invert, posterize-4, threshold-128, levels, curves, brightness-contrast and brightness-contrast-legacy all pass (14 files), 151 -> 165 of 258; floor 151 -> 165. psd-tools and io corpora unchanged. 24 MP CMYK8 with one adjustment renders in 286 ms on the inks, against 442 ms through the RGB compositor. Co-Authored-By: Claude Opus 5.5 <[email protected]> * CMYK ink compositing inlines open groups The ink compositor gave up on any group. Now a group at full opacity without a mask is inlined: - a pass-through group's children composite straight onto the backdrop, so an adjustment inside it reaches the layers below; - an isolated (Normal) group of raster layers alone composites as its children would, since Normal is associative. An isolated group with an adjustment, which only sees the group's content, keeps the RGB compositor. So do a translucent group and an artboard. Nesting is bounded. psd-tools' CMYK adjustment files keep their swatches in pass-through groups. With #4948's fair comparison, curves_cmyk drops from 37% to 0.3% bad pixels and levels_cmyk from 14% to 0.2%. The photoshop corpus is unchanged at 165. Co-Authored-By: Claude Opus 5.5 <[email protected]> * 32-bit: legacy Brightness/Contrast, Channel Mixer, Photo Filter and B&W tint unclipped In 32-bit documents Photoshop keeps these adjustments' results above 1 and below 0. On the photoshop corpus's rgb32/gray32 files: - Legacy Brightness/Contrast pivots its contrast at 127 of 255 (8/16-bit: 128) and is exact on every pixel. - Channel Mixer cuts each source percentage to 1/1024 toward zero, counts 255/512 of the constant and is exact on pure red, green, blue, black and white. - Photo Filter works on the linear samples, negative results included. - The Black & White tint is the Color blend mode without the clip at 1, as the blend mode is there; white tints to (1.05, 1.00, 0.87). We clipped all four to 0..1. The GPU kernels follow suit under F_HDR (the legacy pivot and the mixer's steps come from the planner), and the HDR parity test covers them. Photoshop oracles: rgb32 and gray32 brightness-contrast-legacy, rgb32 channel-mixer and black-white-tint pass (145 -> 149 of 258); floor 145 -> 149. rgb32 photo-filter improves (0.19 -> 0.035 max); the rest is its colour (#3790). Co-Authored-By: Claude Opus 5.5 <[email protected]> * Modern brightness: knee at output 126, fit out to -95 and +150 psd-tools' brightnesscontrast_rgb.psd masks four Brightness/Contrast layers to their own columns. Read through the exact contrast curve, it gives Photoshop's brightness at -95, -31, +23 and +150, well past the +/-50 samples the first fit had. The +/-50 curve extrapolated badly there: 22 levels off at -95 and 25 at +150. Every setting puts the knee at about the same output level, not input. The roll-off's end slope falls as 1/(1 + 0.084 b). With the knee at 126/255 and that end slope, the cubic Hermite fits -95, -50, -31, +23, +25 and +50 within 1.4 levels. Past about +110 no such cubic stays below white, so there the roll-off is 1 - (1 - y0)(1 - u)^alpha, with the slope continuous at the knee. That gives +150 within 6 levels, where every curve family tried bottoms out. Lowered curves are still the inverse of raised ones. The photoshop corpus's six brightness-contrast oracles still pass. psd-tools brightnesscontrast_rgb goes from 0.13 to 0.020 max error and _grayscale from 0.098 to 0.016. The new measurements are pinned in modern_brightness_matches_photoshop. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Levels: whole levels at every step and Photoshop's shadow toe In 8 bits Photoshop rounds each of the three Levels steps to a whole level (half up): the input-range stretch (already modelled), the midtone gamma and the output range. A narrow output range therefore merges neighbouring gamma levels: psd-tools levels_rgb maps R 43..48 to 58 through a 0.47 channel gamma and the master's 48..255, where the unrounded curve gave 56..59 (3 levels off in blue). The gamma above 1 starts along a toe a*u - b*u^q of slope a = 2^gamma that joins the power curve with the same value and slope (q = 2.2, fitted). With the rounding, that is exact on 1116 of the 1118 levels of gamma 1.16, 1.22, 1.3, 1.78 and 2.0 oracles; the soft-min it replaces missed 52. Gamma below 1 stays the plain power curve (0.67 and 0.7: every level exact). Oracles: psd-tools levels_rgb 0.0118 -> 0.0039 (PASS); photoshop rgb8/stack-levels-curves-hue-posterize 0.2000 -> 0.0000 (PASS); rgb8 and gray8 levels.psd 0.0039 -> 0.0000; rgb16/gray16 0.0039 -> 0.0020. Floors: photoshop 145 -> 146, psd-tools 236 -> 238 (main is at 237). Co-Authored-By: Claude Opus 5.5 <[email protected]> * Masks: feathers blur with exactly the feather's variance A mask feather is a Gaussian with sigma = feather, which we approximate with three box passes per axis. Whole-pixel boxes rounded the variance: a 6 px feather blurred like 6.48 px and a 3 px one like 3.46 px, so feathered edges were too soft by up to 4 levels against Photoshop (psd-tools mask-parameters-no-real-channel.psd: a 6 px vector and 3 px pixel feather). Each pass is now an "extended box" (Gwosdek et al. 2011): weight 1 on the whole radius and a fractional weight on the two samples beyond it, so three passes have variance sigma^2 exactly. It stays O(n), reusing the window's leaving sample for the low end weight (50 MP at a 1000 px feather: 180 ms parallel, was 172). psd-tools: mask-parameters-no-real-channel 0.0168 -> 0.0060 (PASS), layer_mask_data.psd 26.9 % -> 3.9 % of pixels off (psb 25.9 % -> 1.8 %). Corpus test 239 -> 240; floor 236 -> 240. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Lab: adjustment layers composite on the stored L, a and b The compositor reads a Lab document's layers through display RGB, which clips colours outside its gamut, so the adjustments Photoshop evaluates on L, a and b (Invert, Posterize, Threshold, Brightness/Contrast, Levels and Curves with Lab records) only saw the clipped colours: the photoshop corpus's Lab oracles were up to 11 levels off on saturated colours. compose::native_lab works a Lab stack of plain layers out on the stored values instead, like the CMYK ink path: Normal mixes L, a and b linearly, an opaque layer keeps them exactly, a layer mask scales the mix, and adjustment layers run adjust::lab_stored_op on each pixel. Stacks with such adjustments render for display this way; flattening a plain Lab stack keeps its stored values (exports no longer clip). Anything else (other blend modes, effects, other adjustments) keeps the RGB compositor. Photoshop oracles 145 -> 152: lab16 invert, posterize-4, levels, curves and brightness-contrast-legacy; lab8 invert and posterize-4. lab8 levels and legacy B/C come within a level (their 8-bit rules are #5143 and #5132). psd-tools and io unchanged; every round trip unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Threshold: split on Photoshop's luminosity of whole samples Photoshop's Threshold splits on the luminosity 0.30 R + 0.59 G + 0.11 B (the blend modes' lum, as Gradient Map uses), not the Rec. 601 grey, taken over the document's whole sample values: - 8 bits: 30 R + 59 G + 11 B > 100 level - 50, i.e. the luminosity rounded to a level with halves down reaches the level; - 16 bits: 30 R + 59 G + 11 B >= 100 x the level in 16 bits (round(level x 32768 / 255): mid grey 16448 is level 128); - 32 bits: the luminosity reaches the level. Exact on every pixel of psd-tools threshold_rgb (36432 px; Rec. 601 split 28 the other way), threshold_16bits_rgb (100) and threshold_16bits_ grayscale, and the photoshop corpus's rgb16 threshold-128. The sums are exact in f32, on the CPU and in the GPU kernel, so ties (grey exactly on the level) split the same way on both. psd-tools: threshold_rgb and threshold_16bits_rgb pass; corpus test 239 -> 241, floor 236 -> 241. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Posterize: 16-bit documents bin the 16-bit sample, as Photoshop does At 16 bits Photoshop doesn't round to 8-bit levels before binning. It bins the 16-bit sample, floor(v16 * n / 32768), and lands on 8-bit levels through a 16-bit step: floor(b * 32768 / (n - 1)), converted with floor((o * 255 + 16384) / 32768). So the middle of 3 levels is 128 where 8 bits give 127, while 42.5 and 25.5 stay 42 and 25. Exact on every sample of psd-tools posterize_16bits_rgb (levels 3, 7, 13, 21) and posterize_16bits_grayscale (2, 6, 9, 11); the 8-bit rule missed up to half the range at bin edges. The same rule applies to CMYK and Lab channels at 16 bits: on the stored inks (#4945's native path) psd-tools posterize_16bits_cmyk goes from 4.26 % to 0.01 % of pixels off. 8-bit Posterize is unchanged. The GPU kernel runs the same rule; 16-bit layers reach it as half floats, so a sample within their precision of a bin edge can land in the neighbouring bin there (the CPU, which exports, is exact). psd-tools 239 -> 241; floor 236 -> 241. Co-Authored-By: Claude Opus 5.5 <[email protected]> * CMYK: Posterize works on the stored values (1 - ink) Photoshop posterizes a CMYK channel's stored value, 1 - ink, not the ink: with levels whose steps are uneven (3, 7, 13, 21) the two differ, e.g. 3 levels take an ink of 85 to 128, where posterizing the ink drops it to none. psd-tools posterize_cmyk.psd goes from 13.8 % of pixels off to exact; psd-tools 241 -> 242, floor 242. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Groups: a pass-through clipping base with an adjustment renders isolated A pass-through group composites its children straight into the backdrop, so an adjustment layer inside reaches the layers beneath. When the group is the base of a clipping group and holds an adjustment layer, Photoshop renders it isolated instead: in psd-tools passthrough_clipping_mask_adjustment an Exposure in such a group leaves the layers beneath alone (we darkened them: half the image 170 levels off). The same group with a Linear Dodge child and no adjustment keeps reaching the backdrop (passthrough_clipping_mask_blendmode), and so do clipping-mask3/4/5, so the rule is the adjustment, not the clipping alone (isolating every pass-through clipping base broke those two). CPU compositor and GPU plan alike. psd-tools 239 -> 240; floor 240. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
92c7922230 |
CMYK: Brightness/Contrast curves each ink, as Photoshop does (#4930)
* Modern Brightness/Contrast: Photoshop's brightness curve The modern brightness curve was a line rolled off by a v^P term. It sat up to 4 levels off Photoshop's in the highlights, and up to 2.3 levels off for lowered brightness. The curve is refit to the Photoshop ground truth we have: - the ±50 samples from 4759d12b; - the photoshop corpus's brightness +25 / contrast +30 files, inverted through the (exact) contrast curve. Raised, it is the line s*x (s = 1.375^(b/50)) up to a knee at 80/255, then a cubic Hermite to white with end slope s^-6.6. Lowered, it is the exact inverse of the curve raised by as much (the -50 samples confirm this to within a level). Every measured point is within about a level. For strong values the knee moves down so the curve stays monotone across -150..150. Brightness then contrast are evaluated through one 4096-entry table, built in 55 us. The GPU uses that same table through the Levels/Curves kernel instead of rebuilding the curves in WGSL, so GPU and CPU agree by construction. Photoshop oracles: rgb8/16/32 and gray8/16/32 brightness-contrast now pass (145 -> 151 of 258). The floor goes 133 -> 151. Co-Authored-By: Claude Opus 5.5 <[email protected]> * CMYK: Brightness/Contrast curves each ink, as Photoshop does Photoshop applies Brightness/Contrast in a CMYK document to each ink's stored value, 1 - ink, black included. Both the modern and the legacy curve do this; the cmyk8 brightness-contrast(-legacy) oracles match it within a level on every pixel of the files' own inks. Raising brightness takes ink away. We applied it to the screen colour instead. - cmyk_channel_adjustment gains Brightness/Contrast. apply_in builds the channel curve once (brightness_contrast_lut) and runs each separated ink through it (ink_through). - Destructive Image > Adjustments on a CMYK layer curves the stored inks directly, so it is exact; a selection mixes them. adjusted_channels now takes the per-pixel operation, and works out luma only for CMYK Threshold, which compares it. - The GPU leaves CMYK Brightness/Contrast to the CPU, like the other ink rules. cmyk8/16 brightness-contrast oracles, bad pixels: - modern: 72% -> 48-57%, max error 0.19 -> 0.09 - legacy: 71% -> 51-53% The rest is our separation of the RGB composite, not the rule. Co-Authored-By: Claude Opus 5.5 <[email protected]> * CMYK Threshold compares the inks' own luma, as Photoshop does Photoshop's CMYK Threshold splits on the Rec. 601 luma of the inks' plain complement, (1 - c)(1 - k) and so on, whatever the document's profile. On the files' own inks this puts every pixel of cmyk8 and cmyk16 threshold-128.psd on the right side of level 128. The profile's RGB luma missed 100. stored_channels works the luma out from the inks itself (ink_luma), so it no longer takes one; the engine's stored-channel path no longer converts each pixel to RGB at all. Through the separated RGB composite the cmyk threshold oracles drop from 1.6% to 0.2% bad pixels. Co-Authored-By: Claude Opus 5.5 <[email protected]> * CMYK legacy Brightness/Contrast pivots at 128 in ink Photoshop's legacy contrast pivots at level 128 of the ink, which is 127 of 255 of the stored value (1 - ink) that the curve runs on, not 128 as in RGB. On Photoshop's own 16-bit inks (cmyk16 brightness-contrast-legacy.psd) ours sat a constant 0.43 of a level above. That is exactly the one-level pivot shift times contrast +30's slope minus one; with the pivot moved, the inks match within 0.01. brightness_contrast_lut is now ink_brightness_contrast_lut, since its legacy curve is the CMYK one. Co-Authored-By: Claude Opus 5.5 <[email protected]> * 32-bit: legacy Brightness/Contrast, Channel Mixer, Photo Filter and B&W tint unclipped In 32-bit documents Photoshop keeps these adjustments' results above 1 and below 0. On the photoshop corpus's rgb32/gray32 files: - Legacy Brightness/Contrast pivots its contrast at 127 of 255 (8/16-bit: 128) and is exact on every pixel. - Channel Mixer cuts each source percentage to 1/1024 toward zero, counts 255/512 of the constant and is exact on pure red, green, blue, black and white. - Photo Filter works on the linear samples, negative results included. - The Black & White tint is the Color blend mode without the clip at 1, as the blend mode is there; white tints to (1.05, 1.00, 0.87). We clipped all four to 0..1. The GPU kernels follow suit under F_HDR (the legacy pivot and the mixer's steps come from the planner), and the HDR parity test covers them. Photoshop oracles: rgb32 and gray32 brightness-contrast-legacy, rgb32 channel-mixer and black-white-tint pass (145 -> 149 of 258); floor 145 -> 149. rgb32 photo-filter improves (0.19 -> 0.035 max); the rest is its colour (#3790). Co-Authored-By: Claude Opus 5.5 <[email protected]> * Modern brightness: knee at output 126, fit out to -95 and +150 psd-tools' brightnesscontrast_rgb.psd masks four Brightness/Contrast layers to their own columns. Read through the exact contrast curve, it gives Photoshop's brightness at -95, -31, +23 and +150, well past the +/-50 samples the first fit had. The +/-50 curve extrapolated badly there: 22 levels off at -95 and 25 at +150. Every setting puts the knee at about the same output level, not input. The roll-off's end slope falls as 1/(1 + 0.084 b). With the knee at 126/255 and that end slope, the cubic Hermite fits -95, -50, -31, +23, +25 and +50 within 1.4 levels. Past about +110 no such cubic stays below white, so there the roll-off is 1 - (1 - y0)(1 - u)^alpha, with the slope continuous at the knee. That gives +150 within 6 levels, where every curve family tried bottoms out. Lowered curves are still the inverse of raised ones. The photoshop corpus's six brightness-contrast oracles still pass. psd-tools brightnesscontrast_rgb goes from 0.13 to 0.020 max error and _grayscale from 0.098 to 0.016. The new measurements are pinned in modern_brightness_matches_photoshop. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Levels: whole levels at every step and Photoshop's shadow toe In 8 bits Photoshop rounds each of the three Levels steps to a whole level (half up): the input-range stretch (already modelled), the midtone gamma and the output range. A narrow output range therefore merges neighbouring gamma levels: psd-tools levels_rgb maps R 43..48 to 58 through a 0.47 channel gamma and the master's 48..255, where the unrounded curve gave 56..59 (3 levels off in blue). The gamma above 1 starts along a toe a*u - b*u^q of slope a = 2^gamma that joins the power curve with the same value and slope (q = 2.2, fitted). With the rounding, that is exact on 1116 of the 1118 levels of gamma 1.16, 1.22, 1.3, 1.78 and 2.0 oracles; the soft-min it replaces missed 52. Gamma below 1 stays the plain power curve (0.67 and 0.7: every level exact). Oracles: psd-tools levels_rgb 0.0118 -> 0.0039 (PASS); photoshop rgb8/stack-levels-curves-hue-posterize 0.2000 -> 0.0000 (PASS); rgb8 and gray8 levels.psd 0.0039 -> 0.0000; rgb16/gray16 0.0039 -> 0.0020. Floors: photoshop 145 -> 146, psd-tools 236 -> 238 (main is at 237). Co-Authored-By: Claude Opus 5.5 <[email protected]> * Masks: feathers blur with exactly the feather's variance A mask feather is a Gaussian with sigma = feather, which we approximate with three box passes per axis. Whole-pixel boxes rounded the variance: a 6 px feather blurred like 6.48 px and a 3 px one like 3.46 px, so feathered edges were too soft by up to 4 levels against Photoshop (psd-tools mask-parameters-no-real-channel.psd: a 6 px vector and 3 px pixel feather). Each pass is now an "extended box" (Gwosdek et al. 2011): weight 1 on the whole radius and a fractional weight on the two samples beyond it, so three passes have variance sigma^2 exactly. It stays O(n), reusing the window's leaving sample for the low end weight (50 MP at a 1000 px feather: 180 ms parallel, was 172). psd-tools: mask-parameters-no-real-channel 0.0168 -> 0.0060 (PASS), layer_mask_data.psd 26.9 % -> 3.9 % of pixels off (psb 25.9 % -> 1.8 %). Corpus test 239 -> 240; floor 236 -> 240. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Lab: adjustment layers composite on the stored L, a and b The compositor reads a Lab document's layers through display RGB, which clips colours outside its gamut, so the adjustments Photoshop evaluates on L, a and b (Invert, Posterize, Threshold, Brightness/Contrast, Levels and Curves with Lab records) only saw the clipped colours: the photoshop corpus's Lab oracles were up to 11 levels off on saturated colours. compose::native_lab works a Lab stack of plain layers out on the stored values instead, like the CMYK ink path: Normal mixes L, a and b linearly, an opaque layer keeps them exactly, a layer mask scales the mix, and adjustment layers run adjust::lab_stored_op on each pixel. Stacks with such adjustments render for display this way; flattening a plain Lab stack keeps its stored values (exports no longer clip). Anything else (other blend modes, effects, other adjustments) keeps the RGB compositor. Photoshop oracles 145 -> 152: lab16 invert, posterize-4, levels, curves and brightness-contrast-legacy; lab8 invert and posterize-4. lab8 levels and legacy B/C come within a level (their 8-bit rules are #5143 and #5132). psd-tools and io unchanged; every round trip unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Threshold: split on Photoshop's luminosity of whole samples Photoshop's Threshold splits on the luminosity 0.30 R + 0.59 G + 0.11 B (the blend modes' lum, as Gradient Map uses), not the Rec. 601 grey, taken over the document's whole sample values: - 8 bits: 30 R + 59 G + 11 B > 100 level - 50, i.e. the luminosity rounded to a level with halves down reaches the level; - 16 bits: 30 R + 59 G + 11 B >= 100 x the level in 16 bits (round(level x 32768 / 255): mid grey 16448 is level 128); - 32 bits: the luminosity reaches the level. Exact on every pixel of psd-tools threshold_rgb (36432 px; Rec. 601 split 28 the other way), threshold_16bits_rgb (100) and threshold_16bits_ grayscale, and the photoshop corpus's rgb16 threshold-128. The sums are exact in f32, on the CPU and in the GPU kernel, so ties (grey exactly on the level) split the same way on both. psd-tools: threshold_rgb and threshold_16bits_rgb pass; corpus test 239 -> 241, floor 236 -> 241. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Posterize: 16-bit documents bin the 16-bit sample, as Photoshop does At 16 bits Photoshop doesn't round to 8-bit levels before binning. It bins the 16-bit sample, floor(v16 * n / 32768), and lands on 8-bit levels through a 16-bit step: floor(b * 32768 / (n - 1)), converted with floor((o * 255 + 16384) / 32768). So the middle of 3 levels is 128 where 8 bits give 127, while 42.5 and 25.5 stay 42 and 25. Exact on every sample of psd-tools posterize_16bits_rgb (levels 3, 7, 13, 21) and posterize_16bits_grayscale (2, 6, 9, 11); the 8-bit rule missed up to half the range at bin edges. The same rule applies to CMYK and Lab channels at 16 bits: on the stored inks (#4945's native path) psd-tools posterize_16bits_cmyk goes from 4.26 % to 0.01 % of pixels off. 8-bit Posterize is unchanged. The GPU kernel runs the same rule; 16-bit layers reach it as half floats, so a sample within their precision of a bin edge can land in the neighbouring bin there (the CPU, which exports, is exact). psd-tools 239 -> 241; floor 236 -> 241. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Groups: a pass-through clipping base with an adjustment renders isolated A pass-through group composites its children straight into the backdrop, so an adjustment layer inside reaches the layers beneath. When the group is the base of a clipping group and holds an adjustment layer, Photoshop renders it isolated instead: in psd-tools passthrough_clipping_mask_adjustment an Exposure in such a group leaves the layers beneath alone (we darkened them: half the image 170 levels off). The same group with a Linear Dodge child and no adjustment keeps reaching the backdrop (passthrough_clipping_mask_blendmode), and so do clipping-mask3/4/5, so the rule is the adjustment, not the clipping alone (isolating every pass-through clipping base broke those two). CPU compositor and GPU plan alike. psd-tools 239 -> 240; floor 240. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
00c16f2c6d |
Modern Brightness/Contrast: Photoshop's brightness curve (six oracles pass) (#4722)
* Modern Brightness/Contrast: Photoshop's brightness curve The modern brightness curve was a line rolled off by a v^P term. It sat up to 4 levels off Photoshop's in the highlights, and up to 2.3 levels off for lowered brightness. The curve is refit to the Photoshop ground truth we have: - the ±50 samples from 4759d12b; - the photoshop corpus's brightness +25 / contrast +30 files, inverted through the (exact) contrast curve. Raised, it is the line s*x (s = 1.375^(b/50)) up to a knee at 80/255, then a cubic Hermite to white with end slope s^-6.6. Lowered, it is the exact inverse of the curve raised by as much (the -50 samples confirm this to within a level). Every measured point is within about a level. For strong values the knee moves down so the curve stays monotone across -150..150. Brightness then contrast are evaluated through one 4096-entry table, built in 55 us. The GPU uses that same table through the Levels/Curves kernel instead of rebuilding the curves in WGSL, so GPU and CPU agree by construction. Photoshop oracles: rgb8/16/32 and gray8/16/32 brightness-contrast now pass (145 -> 151 of 258). The floor goes 133 -> 151. Co-Authored-By: Claude Opus 5.5 <[email protected]> * 32-bit: legacy Brightness/Contrast, Channel Mixer, Photo Filter and B&W tint unclipped In 32-bit documents Photoshop keeps these adjustments' results above 1 and below 0. On the photoshop corpus's rgb32/gray32 files: - Legacy Brightness/Contrast pivots its contrast at 127 of 255 (8/16-bit: 128) and is exact on every pixel. - Channel Mixer cuts each source percentage to 1/1024 toward zero, counts 255/512 of the constant and is exact on pure red, green, blue, black and white. - Photo Filter works on the linear samples, negative results included. - The Black & White tint is the Color blend mode without the clip at 1, as the blend mode is there; white tints to (1.05, 1.00, 0.87). We clipped all four to 0..1. The GPU kernels follow suit under F_HDR (the legacy pivot and the mixer's steps come from the planner), and the HDR parity test covers them. Photoshop oracles: rgb32 and gray32 brightness-contrast-legacy, rgb32 channel-mixer and black-white-tint pass (145 -> 149 of 258); floor 145 -> 149. rgb32 photo-filter improves (0.19 -> 0.035 max); the rest is its colour (#3790). Co-Authored-By: Claude Opus 5.5 <[email protected]> * Modern brightness: knee at output 126, fit out to -95 and +150 psd-tools' brightnesscontrast_rgb.psd masks four Brightness/Contrast layers to their own columns. Read through the exact contrast curve, it gives Photoshop's brightness at -95, -31, +23 and +150, well past the +/-50 samples the first fit had. The +/-50 curve extrapolated badly there: 22 levels off at -95 and 25 at +150. Every setting puts the knee at about the same output level, not input. The roll-off's end slope falls as 1/(1 + 0.084 b). With the knee at 126/255 and that end slope, the cubic Hermite fits -95, -50, -31, +23, +25 and +50 within 1.4 levels. Past about +110 no such cubic stays below white, so there the roll-off is 1 - (1 - y0)(1 - u)^alpha, with the slope continuous at the knee. That gives +150 within 6 levels, where every curve family tried bottoms out. Lowered curves are still the inverse of raised ones. The photoshop corpus's six brightness-contrast oracles still pass. psd-tools brightnesscontrast_rgb goes from 0.13 to 0.020 max error and _grayscale from 0.098 to 0.016. The new measurements are pinned in modern_brightness_matches_photoshop. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Levels: whole levels at every step and Photoshop's shadow toe In 8 bits Photoshop rounds each of the three Levels steps to a whole level (half up): the input-range stretch (already modelled), the midtone gamma and the output range. A narrow output range therefore merges neighbouring gamma levels: psd-tools levels_rgb maps R 43..48 to 58 through a 0.47 channel gamma and the master's 48..255, where the unrounded curve gave 56..59 (3 levels off in blue). The gamma above 1 starts along a toe a*u - b*u^q of slope a = 2^gamma that joins the power curve with the same value and slope (q = 2.2, fitted). With the rounding, that is exact on 1116 of the 1118 levels of gamma 1.16, 1.22, 1.3, 1.78 and 2.0 oracles; the soft-min it replaces missed 52. Gamma below 1 stays the plain power curve (0.67 and 0.7: every level exact). Oracles: psd-tools levels_rgb 0.0118 -> 0.0039 (PASS); photoshop rgb8/stack-levels-curves-hue-posterize 0.2000 -> 0.0000 (PASS); rgb8 and gray8 levels.psd 0.0039 -> 0.0000; rgb16/gray16 0.0039 -> 0.0020. Floors: photoshop 145 -> 146, psd-tools 236 -> 238 (main is at 237). Co-Authored-By: Claude Opus 5.5 <[email protected]> * Masks: feathers blur with exactly the feather's variance A mask feather is a Gaussian with sigma = feather, which we approximate with three box passes per axis. Whole-pixel boxes rounded the variance: a 6 px feather blurred like 6.48 px and a 3 px one like 3.46 px, so feathered edges were too soft by up to 4 levels against Photoshop (psd-tools mask-parameters-no-real-channel.psd: a 6 px vector and 3 px pixel feather). Each pass is now an "extended box" (Gwosdek et al. 2011): weight 1 on the whole radius and a fractional weight on the two samples beyond it, so three passes have variance sigma^2 exactly. It stays O(n), reusing the window's leaving sample for the low end weight (50 MP at a 1000 px feather: 180 ms parallel, was 172). psd-tools: mask-parameters-no-real-channel 0.0168 -> 0.0060 (PASS), layer_mask_data.psd 26.9 % -> 3.9 % of pixels off (psb 25.9 % -> 1.8 %). Corpus test 239 -> 240; floor 236 -> 240. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Lab: adjustment layers composite on the stored L, a and b The compositor reads a Lab document's layers through display RGB, which clips colours outside its gamut, so the adjustments Photoshop evaluates on L, a and b (Invert, Posterize, Threshold, Brightness/Contrast, Levels and Curves with Lab records) only saw the clipped colours: the photoshop corpus's Lab oracles were up to 11 levels off on saturated colours. compose::native_lab works a Lab stack of plain layers out on the stored values instead, like the CMYK ink path: Normal mixes L, a and b linearly, an opaque layer keeps them exactly, a layer mask scales the mix, and adjustment layers run adjust::lab_stored_op on each pixel. Stacks with such adjustments render for display this way; flattening a plain Lab stack keeps its stored values (exports no longer clip). Anything else (other blend modes, effects, other adjustments) keeps the RGB compositor. Photoshop oracles 145 -> 152: lab16 invert, posterize-4, levels, curves and brightness-contrast-legacy; lab8 invert and posterize-4. lab8 levels and legacy B/C come within a level (their 8-bit rules are #5143 and #5132). psd-tools and io unchanged; every round trip unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Threshold: split on Photoshop's luminosity of whole samples Photoshop's Threshold splits on the luminosity 0.30 R + 0.59 G + 0.11 B (the blend modes' lum, as Gradient Map uses), not the Rec. 601 grey, taken over the document's whole sample values: - 8 bits: 30 R + 59 G + 11 B > 100 level - 50, i.e. the luminosity rounded to a level with halves down reaches the level; - 16 bits: 30 R + 59 G + 11 B >= 100 x the level in 16 bits (round(level x 32768 / 255): mid grey 16448 is level 128); - 32 bits: the luminosity reaches the level. Exact on every pixel of psd-tools threshold_rgb (36432 px; Rec. 601 split 28 the other way), threshold_16bits_rgb (100) and threshold_16bits_ grayscale, and the photoshop corpus's rgb16 threshold-128. The sums are exact in f32, on the CPU and in the GPU kernel, so ties (grey exactly on the level) split the same way on both. psd-tools: threshold_rgb and threshold_16bits_rgb pass; corpus test 239 -> 241, floor 236 -> 241. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Posterize: 16-bit documents bin the 16-bit sample, as Photoshop does At 16 bits Photoshop doesn't round to 8-bit levels before binning. It bins the 16-bit sample, floor(v16 * n / 32768), and lands on 8-bit levels through a 16-bit step: floor(b * 32768 / (n - 1)), converted with floor((o * 255 + 16384) / 32768). So the middle of 3 levels is 128 where 8 bits give 127, while 42.5 and 25.5 stay 42 and 25. Exact on every sample of psd-tools posterize_16bits_rgb (levels 3, 7, 13, 21) and posterize_16bits_grayscale (2, 6, 9, 11); the 8-bit rule missed up to half the range at bin edges. The same rule applies to CMYK and Lab channels at 16 bits: on the stored inks (#4945's native path) psd-tools posterize_16bits_cmyk goes from 4.26 % to 0.01 % of pixels off. 8-bit Posterize is unchanged. The GPU kernel runs the same rule; 16-bit layers reach it as half floats, so a sample within their precision of a bin edge can land in the neighbouring bin there (the CPU, which exports, is exact). psd-tools 239 -> 241; floor 236 -> 241. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Groups: a pass-through clipping base with an adjustment renders isolated A pass-through group composites its children straight into the backdrop, so an adjustment layer inside reaches the layers beneath. When the group is the base of a clipping group and holds an adjustment layer, Photoshop renders it isolated instead: in psd-tools passthrough_clipping_mask_adjustment an Exposure in such a group leaves the layers beneath alone (we darkened them: half the image 170 levels off). The same group with a Linear Dodge child and no adjustment keeps reaching the backdrop (passthrough_clipping_mask_blendmode), and so do clipping-mask3/4/5, so the rule is the adjustment, not the clipping alone (isolating every pass-through clipping base broke those two). CPU compositor and GPU plan alike. psd-tools 239 -> 240; floor 240. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
4c450b3879 |
Lab: adjustment layers composite on the stored L, a and b (seven Lab oracles pass) (#5171)
* 32-bit: legacy Brightness/Contrast, Channel Mixer, Photo Filter and B&W tint unclipped In 32-bit documents Photoshop keeps these adjustments' results above 1 and below 0. On the photoshop corpus's rgb32/gray32 files: - Legacy Brightness/Contrast pivots its contrast at 127 of 255 (8/16-bit: 128) and is exact on every pixel. - Channel Mixer cuts each source percentage to 1/1024 toward zero, counts 255/512 of the constant and is exact on pure red, green, blue, black and white. - Photo Filter works on the linear samples, negative results included. - The Black & White tint is the Color blend mode without the clip at 1, as the blend mode is there; white tints to (1.05, 1.00, 0.87). We clipped all four to 0..1. The GPU kernels follow suit under F_HDR (the legacy pivot and the mixer's steps come from the planner), and the HDR parity test covers them. Photoshop oracles: rgb32 and gray32 brightness-contrast-legacy, rgb32 channel-mixer and black-white-tint pass (145 -> 149 of 258); floor 145 -> 149. rgb32 photo-filter improves (0.19 -> 0.035 max); the rest is its colour (#3790). Co-Authored-By: Claude Opus 5.5 <[email protected]> * Levels: whole levels at every step and Photoshop's shadow toe In 8 bits Photoshop rounds each of the three Levels steps to a whole level (half up): the input-range stretch (already modelled), the midtone gamma and the output range. A narrow output range therefore merges neighbouring gamma levels: psd-tools levels_rgb maps R 43..48 to 58 through a 0.47 channel gamma and the master's 48..255, where the unrounded curve gave 56..59 (3 levels off in blue). The gamma above 1 starts along a toe a*u - b*u^q of slope a = 2^gamma that joins the power curve with the same value and slope (q = 2.2, fitted). With the rounding, that is exact on 1116 of the 1118 levels of gamma 1.16, 1.22, 1.3, 1.78 and 2.0 oracles; the soft-min it replaces missed 52. Gamma below 1 stays the plain power curve (0.67 and 0.7: every level exact). Oracles: psd-tools levels_rgb 0.0118 -> 0.0039 (PASS); photoshop rgb8/stack-levels-curves-hue-posterize 0.2000 -> 0.0000 (PASS); rgb8 and gray8 levels.psd 0.0039 -> 0.0000; rgb16/gray16 0.0039 -> 0.0020. Floors: photoshop 145 -> 146, psd-tools 236 -> 238 (main is at 237). Co-Authored-By: Claude Opus 5.5 <[email protected]> * Masks: feathers blur with exactly the feather's variance A mask feather is a Gaussian with sigma = feather, which we approximate with three box passes per axis. Whole-pixel boxes rounded the variance: a 6 px feather blurred like 6.48 px and a 3 px one like 3.46 px, so feathered edges were too soft by up to 4 levels against Photoshop (psd-tools mask-parameters-no-real-channel.psd: a 6 px vector and 3 px pixel feather). Each pass is now an "extended box" (Gwosdek et al. 2011): weight 1 on the whole radius and a fractional weight on the two samples beyond it, so three passes have variance sigma^2 exactly. It stays O(n), reusing the window's leaving sample for the low end weight (50 MP at a 1000 px feather: 180 ms parallel, was 172). psd-tools: mask-parameters-no-real-channel 0.0168 -> 0.0060 (PASS), layer_mask_data.psd 26.9 % -> 3.9 % of pixels off (psb 25.9 % -> 1.8 %). Corpus test 239 -> 240; floor 236 -> 240. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Lab: adjustment layers composite on the stored L, a and b The compositor reads a Lab document's layers through display RGB, which clips colours outside its gamut, so the adjustments Photoshop evaluates on L, a and b (Invert, Posterize, Threshold, Brightness/Contrast, Levels and Curves with Lab records) only saw the clipped colours: the photoshop corpus's Lab oracles were up to 11 levels off on saturated colours. compose::native_lab works a Lab stack of plain layers out on the stored values instead, like the CMYK ink path: Normal mixes L, a and b linearly, an opaque layer keeps them exactly, a layer mask scales the mix, and adjustment layers run adjust::lab_stored_op on each pixel. Stacks with such adjustments render for display this way; flattening a plain Lab stack keeps its stored values (exports no longer clip). Anything else (other blend modes, effects, other adjustments) keeps the RGB compositor. Photoshop oracles 145 -> 152: lab16 invert, posterize-4, levels, curves and brightness-contrast-legacy; lab8 invert and posterize-4. lab8 levels and legacy B/C come within a level (their 8-bit rules are #5143 and #5132). psd-tools and io unchanged; every round trip unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Threshold: split on Photoshop's luminosity of whole samples Photoshop's Threshold splits on the luminosity 0.30 R + 0.59 G + 0.11 B (the blend modes' lum, as Gradient Map uses), not the Rec. 601 grey, taken over the document's whole sample values: - 8 bits: 30 R + 59 G + 11 B > 100 level - 50, i.e. the luminosity rounded to a level with halves down reaches the level; - 16 bits: 30 R + 59 G + 11 B >= 100 x the level in 16 bits (round(level x 32768 / 255): mid grey 16448 is level 128); - 32 bits: the luminosity reaches the level. Exact on every pixel of psd-tools threshold_rgb (36432 px; Rec. 601 split 28 the other way), threshold_16bits_rgb (100) and threshold_16bits_ grayscale, and the photoshop corpus's rgb16 threshold-128. The sums are exact in f32, on the CPU and in the GPU kernel, so ties (grey exactly on the level) split the same way on both. psd-tools: threshold_rgb and threshold_16bits_rgb pass; corpus test 239 -> 241, floor 236 -> 241. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Posterize: 16-bit documents bin the 16-bit sample, as Photoshop does At 16 bits Photoshop doesn't round to 8-bit levels before binning. It bins the 16-bit sample, floor(v16 * n / 32768), and lands on 8-bit levels through a 16-bit step: floor(b * 32768 / (n - 1)), converted with floor((o * 255 + 16384) / 32768). So the middle of 3 levels is 128 where 8 bits give 127, while 42.5 and 25.5 stay 42 and 25. Exact on every sample of psd-tools posterize_16bits_rgb (levels 3, 7, 13, 21) and posterize_16bits_grayscale (2, 6, 9, 11); the 8-bit rule missed up to half the range at bin edges. The same rule applies to CMYK and Lab channels at 16 bits: on the stored inks (#4945's native path) psd-tools posterize_16bits_cmyk goes from 4.26 % to 0.01 % of pixels off. 8-bit Posterize is unchanged. The GPU kernel runs the same rule; 16-bit layers reach it as half floats, so a sample within their precision of a bin edge can land in the neighbouring bin there (the CPU, which exports, is exact). psd-tools 239 -> 241; floor 236 -> 241. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Groups: a pass-through clipping base with an adjustment renders isolated A pass-through group composites its children straight into the backdrop, so an adjustment layer inside reaches the layers beneath. When the group is the base of a clipping group and holds an adjustment layer, Photoshop renders it isolated instead: in psd-tools passthrough_clipping_mask_adjustment an Exposure in such a group leaves the layers beneath alone (we darkened them: half the image 170 levels off). The same group with a Linear Dodge child and no adjustment keeps reaching the backdrop (passthrough_clipping_mask_blendmode), and so do clipping-mask3/4/5, so the rule is the adjustment, not the clipping alone (isolating every pass-through clipping base broke those two). CPU compositor and GPU plan alike. psd-tools 239 -> 240; floor 240. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
4fb866942c |
Layers panel: Clip Thumbnails to Layer Bounds (#4281) (#4312)
* Layers panel: Clip Thumbnails to Layer Bounds (#4281) Add interface.clipThumbnailsToLayerBounds (off by default), toggled from the Layers panel menu or Preferences > Interface. When on, a pixel layer's thumbnail frames the layer's own pixels (cut to the canvas) instead of the whole canvas, so small objects in a large document stay visible. Empty or fully off-canvas layers keep the canvas frame. The frame is computed only when the thumbnail is rebuilt (tile-cached content bounds) and is part of the cache key, so toggling rebuilds it. Closes #4281 * Regenerate docs/scorecard.md after merging main * Regenerate docs/scorecard.md after merging main --------- Co-authored-by: badaaif <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
d24f6e9d5f |
Levels: whole levels at every step and Photoshop's shadow toe (levels_rgb + stack oracle pass) (#5143)
* 32-bit: legacy Brightness/Contrast, Channel Mixer, Photo Filter and B&W tint unclipped In 32-bit documents Photoshop keeps these adjustments' results above 1 and below 0. On the photoshop corpus's rgb32/gray32 files: - Legacy Brightness/Contrast pivots its contrast at 127 of 255 (8/16-bit: 128) and is exact on every pixel. - Channel Mixer cuts each source percentage to 1/1024 toward zero, counts 255/512 of the constant and is exact on pure red, green, blue, black and white. - Photo Filter works on the linear samples, negative results included. - The Black & White tint is the Color blend mode without the clip at 1, as the blend mode is there; white tints to (1.05, 1.00, 0.87). We clipped all four to 0..1. The GPU kernels follow suit under F_HDR (the legacy pivot and the mixer's steps come from the planner), and the HDR parity test covers them. Photoshop oracles: rgb32 and gray32 brightness-contrast-legacy, rgb32 channel-mixer and black-white-tint pass (145 -> 149 of 258); floor 145 -> 149. rgb32 photo-filter improves (0.19 -> 0.035 max); the rest is its colour (#3790). Co-Authored-By: Claude Opus 5.5 <[email protected]> * Levels: whole levels at every step and Photoshop's shadow toe In 8 bits Photoshop rounds each of the three Levels steps to a whole level (half up): the input-range stretch (already modelled), the midtone gamma and the output range. A narrow output range therefore merges neighbouring gamma levels: psd-tools levels_rgb maps R 43..48 to 58 through a 0.47 channel gamma and the master's 48..255, where the unrounded curve gave 56..59 (3 levels off in blue). The gamma above 1 starts along a toe a*u - b*u^q of slope a = 2^gamma that joins the power curve with the same value and slope (q = 2.2, fitted). With the rounding, that is exact on 1116 of the 1118 levels of gamma 1.16, 1.22, 1.3, 1.78 and 2.0 oracles; the soft-min it replaces missed 52. Gamma below 1 stays the plain power curve (0.67 and 0.7: every level exact). Oracles: psd-tools levels_rgb 0.0118 -> 0.0039 (PASS); photoshop rgb8/stack-levels-curves-hue-posterize 0.2000 -> 0.0000 (PASS); rgb8 and gray8 levels.psd 0.0039 -> 0.0000; rgb16/gray16 0.0039 -> 0.0020. Floors: photoshop 145 -> 146, psd-tools 236 -> 238 (main is at 237). Co-Authored-By: Claude Opus 5.5 <[email protected]> * Masks: feathers blur with exactly the feather's variance A mask feather is a Gaussian with sigma = feather, which we approximate with three box passes per axis. Whole-pixel boxes rounded the variance: a 6 px feather blurred like 6.48 px and a 3 px one like 3.46 px, so feathered edges were too soft by up to 4 levels against Photoshop (psd-tools mask-parameters-no-real-channel.psd: a 6 px vector and 3 px pixel feather). Each pass is now an "extended box" (Gwosdek et al. 2011): weight 1 on the whole radius and a fractional weight on the two samples beyond it, so three passes have variance sigma^2 exactly. It stays O(n), reusing the window's leaving sample for the low end weight (50 MP at a 1000 px feather: 180 ms parallel, was 172). psd-tools: mask-parameters-no-real-channel 0.0168 -> 0.0060 (PASS), layer_mask_data.psd 26.9 % -> 3.9 % of pixels off (psb 25.9 % -> 1.8 %). Corpus test 239 -> 240; floor 236 -> 240. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Threshold: split on Photoshop's luminosity of whole samples Photoshop's Threshold splits on the luminosity 0.30 R + 0.59 G + 0.11 B (the blend modes' lum, as Gradient Map uses), not the Rec. 601 grey, taken over the document's whole sample values: - 8 bits: 30 R + 59 G + 11 B > 100 level - 50, i.e. the luminosity rounded to a level with halves down reaches the level; - 16 bits: 30 R + 59 G + 11 B >= 100 x the level in 16 bits (round(level x 32768 / 255): mid grey 16448 is level 128); - 32 bits: the luminosity reaches the level. Exact on every pixel of psd-tools threshold_rgb (36432 px; Rec. 601 split 28 the other way), threshold_16bits_rgb (100) and threshold_16bits_ grayscale, and the photoshop corpus's rgb16 threshold-128. The sums are exact in f32, on the CPU and in the GPU kernel, so ties (grey exactly on the level) split the same way on both. psd-tools: threshold_rgb and threshold_16bits_rgb pass; corpus test 239 -> 241, floor 236 -> 241. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Posterize: 16-bit documents bin the 16-bit sample, as Photoshop does At 16 bits Photoshop doesn't round to 8-bit levels before binning. It bins the 16-bit sample, floor(v16 * n / 32768), and lands on 8-bit levels through a 16-bit step: floor(b * 32768 / (n - 1)), converted with floor((o * 255 + 16384) / 32768). So the middle of 3 levels is 128 where 8 bits give 127, while 42.5 and 25.5 stay 42 and 25. Exact on every sample of psd-tools posterize_16bits_rgb (levels 3, 7, 13, 21) and posterize_16bits_grayscale (2, 6, 9, 11); the 8-bit rule missed up to half the range at bin edges. The same rule applies to CMYK and Lab channels at 16 bits: on the stored inks (#4945's native path) psd-tools posterize_16bits_cmyk goes from 4.26 % to 0.01 % of pixels off. 8-bit Posterize is unchanged. The GPU kernel runs the same rule; 16-bit layers reach it as half floats, so a sample within their precision of a bin edge can land in the neighbouring bin there (the CPU, which exports, is exact). psd-tools 239 -> 241; floor 236 -> 241. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Groups: a pass-through clipping base with an adjustment renders isolated A pass-through group composites its children straight into the backdrop, so an adjustment layer inside reaches the layers beneath. When the group is the base of a clipping group and holds an adjustment layer, Photoshop renders it isolated instead: in psd-tools passthrough_clipping_mask_adjustment an Exposure in such a group leaves the layers beneath alone (we darkened them: half the image 170 levels off). The same group with a Linear Dodge child and no adjustment keeps reaching the backdrop (passthrough_clipping_mask_blendmode), and so do clipping-mask3/4/5, so the rule is the adjustment, not the clipping alone (isolating every pass-through clipping base broke those two). CPU compositor and GPU plan alike. psd-tools 239 -> 240; floor 240. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Empty commit to resync the pull request head --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
835f33a476 |
Pen tool: Shift constrains segments and handles to 45° (TOOL-214-10, #3751) (#3756)
* Pen tool: Shift constrains segments and handles to 45° (TOOL-214-10, #3751) Shift-click puts the next Pen anchor on the nearest 45-degree ray from the previous anchor (at the pointer's projection onto it); Shift held while dragging out direction handles turns them to 45-degree steps, read live per move. The first anchor, closing on the first anchor and the last-anchor break are unaffected; after Alt breaks the pair Shift constrains the outgoing handle alone. The rubber band previews the constrained point. Reuses stroke_constraint::snap45. Flips scorecard TOOL-214-10 to done. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * Regenerate docs/scorecard.md after merging main Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * Regenerate docs/scorecard.md after merging main Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * Regenerate docs/scorecard.md after merging main * Regenerate docs/scorecard.md after merging main --------- Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
f5606093cd |
Brush Settings: a response curve per dynamic control (#3294) (#3302)
* Brush Settings: a response curve per dynamic control (#3294) Every brush Dynamic (Size, Opacity, Flow, Roundness, Scatter, Count, Depth, Foreground/Background...) now carries its own response curve for its control (Fade, Pen Pressure, Pen Tilt, Stylus Wheel, Rotation), applied after the global Preferences pen curve. It is part of the brush JSON, so tools.setBrush sets it (e.g. shapeDynamics.size.curve), actions record it and presets keep it; linear by default, so existing brushes paint unchanged. - paint: ResponseCurve, the global pen curve's monotone cubic moved down from the engine as a fixed-size Copy type (no allocation per dab); serialises as its point list. dynamics::dynamic_value applies it. - engine: prefs::PressureCurve is now that type. - ui-egui: each Control row gets a curve button (lit when the curve isn't linear) that shows the same editor as Preferences > Tools. - scorecard: TOOL-213-11 done. Co-Authored-By: Claude Opus 5.5 <[email protected]> * i18n: Response curve for hu, tr Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: regenerate scorecard after merging main Co-Authored-By: Claude Opus 5.5 <[email protected]> * Fix i18n after merge: Serbian Response curve, drop duplicate hu row; regenerate scorecard Co-Authored-By: Claude Opus 5.5 <[email protected]> * Regenerate docs/scorecard.md after merging main * Regenerate docs/scorecard.md after merging main --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
973e868173 |
Brushes: import an image file as a brush tip (#3279) (#3292)
* Brushes: import an image file as a brush tip (#3279) `brush.presets.importImage` reads an image (path, or base64 data with an optional fileName) through the normal importer, so any format and bit depth File > Open reads works. Its composite becomes a sampled tip the way Define Brush Preset reads a selection: darkness x alpha, cropped to the painted extent, at most 2500 px a side. The bounds are found band by band first, so a large mostly-white image never needs a full-size buffer. The preset is named after the file (never replacing an existing one) and becomes the current brush unless select is false. Import Brushes... in the Brushes panel and the brush preset picker now opens a picker for .abr and image files; images go to the new command, .abr files to brush.presets.importAbr as before. Automation refuses an ambient `path` for it like the other import commands. Define Brush Preset and the new command share `sampled_brush`. Scorecard TOOL-213-12 -> done. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Regenerate scorecard Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: regenerate scorecard after merging main Co-Authored-By: Claude Opus 5.5 <[email protected]> * Regenerate docs/scorecard.md after merging main * Regenerate docs/scorecard.md after merging main --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
03aa7e4962 |
fix(ui): remember the selected tool between launches (Photoshop parity) (#5043)
* fix(ui): remember the selected tool between launches * fix(ui): preserve explicit Eraser selection during temporary tool overrides Clear temporary pen and spring state for explicit toolbar, menu, preset and control selections. Keep spring-loaded key behavior; only permanent key choices clear the pen marker. Add exit/relaunch coverage for explicit Eraser with pen and spring overrides. * Regenerate docs/scorecard.md after merging main --------- Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
aad56285ad |
Batch: Include All Subfolders (AUTO-219-5, #3753) (#3758)
* Batch: Include All Subfolders (AUTO-219-5, #3753) File > Automate > Batch and File > Scripts > Image Processor take an optional "includeSubfolders" (default false): a folder input also yields the images in its subfolders, walked depth first in name order, bounded at 32 levels and 100000 files, with a seen-set of canonical folders against symlink loops and the output folder left out. Each result keeps its relative subfolder under the output folder, so same-named files in different subfolders no longer collide. A non-boolean value is BadParams. Both dialogs gain an "Include All Subfolders" checkbox (translated in every complete catalog). Scorecard AUTO-219-5 stays partial (naming template, progress/cancel, error log file); gaps.md G11 updated. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * Batch: translate Include All Subfolders for Hungarian and Turkish Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
7833a6cad4 |
Layers panel: filter-type dropdown with Kind, Name and Mode (#5048) (#5268)
The filter row's "Kind" was painted text. It is now a dropdown, as in
Photoshop: Kind keeps the five layer-kind toggles, Name lists layers whose
name contains the typed text (ignoring case), and Mode lists layers with
one blend mode (Pass Through included), filtering by Normal at first.
The criteria live in UiState (layer_filter_by, layer_filter_name,
layer_filter_mode; serde defaults, so saved settings still load) and are
drivable with ui.set {layerFilter: {by, kinds, name, mode}}, validated
before anything changes. ui-parity.md gets a partial row: Effect,
Attribute, Color, Smart Object, Selected and Artboard are still missing.
Co-authored-by: Brandon Thomas <[email protected]>
|
||
|
|
3ee78a368b |
Free Transform: the box is the selection, and the marching ants follow it (#5297) (#5337)
With a selection, ⌘T and the Move tool's Show Transform Controls framed the layer's content clipped to the selection, so the box was smaller than the marching ants wherever the layer did not fill the selection. The ants then stayed where the selection started until the transform was applied. - transform_frame / target_frame (engine): the box is the selection's own bounds when a selection limits the transform. transform_bounds is unchanged, so snapping and the command defaults are as before. - The selection outline is drawn through the open box (homography, or the warp in Warp mode), so it follows move, scale, rotate, skew, distort, perspective and warp before the transform is applied. The whole outline is traced while such a box is open, not only the part in view where it started. - marching_ants_lines: ants along screen-space lines; non-finite lines are skipped and very long ones are clipped to the view. Part of #3402. Claude-Session: https://claude.ai/code/session_015vKPoMXFhSvWmX5D9XM646 Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
620bccef42 |
32-bit: legacy Brightness/Contrast, Channel Mixer, Photo Filter and B&W tint unclipped, as in Photoshop (#5011)
* 32-bit: legacy Brightness/Contrast, Channel Mixer, Photo Filter and B&W tint unclipped In 32-bit documents Photoshop keeps these adjustments' results above 1 and below 0. On the photoshop corpus's rgb32/gray32 files: - Legacy Brightness/Contrast pivots its contrast at 127 of 255 (8/16-bit: 128) and is exact on every pixel. - Channel Mixer cuts each source percentage to 1/1024 toward zero, counts 255/512 of the constant and is exact on pure red, green, blue, black and white. - Photo Filter works on the linear samples, negative results included. - The Black & White tint is the Color blend mode without the clip at 1, as the blend mode is there; white tints to (1.05, 1.00, 0.87). We clipped all four to 0..1. The GPU kernels follow suit under F_HDR (the legacy pivot and the mixer's steps come from the planner), and the HDR parity test covers them. Photoshop oracles: rgb32 and gray32 brightness-contrast-legacy, rgb32 channel-mixer and black-white-tint pass (145 -> 149 of 258); floor 145 -> 149. rgb32 photo-filter improves (0.19 -> 0.035 max); the rest is its colour (#3790). Co-Authored-By: Claude Opus 5.5 <[email protected]> * Masks: feathers blur with exactly the feather's variance A mask feather is a Gaussian with sigma = feather, which we approximate with three box passes per axis. Whole-pixel boxes rounded the variance: a 6 px feather blurred like 6.48 px and a 3 px one like 3.46 px, so feathered edges were too soft by up to 4 levels against Photoshop (psd-tools mask-parameters-no-real-channel.psd: a 6 px vector and 3 px pixel feather). Each pass is now an "extended box" (Gwosdek et al. 2011): weight 1 on the whole radius and a fractional weight on the two samples beyond it, so three passes have variance sigma^2 exactly. It stays O(n), reusing the window's leaving sample for the low end weight (50 MP at a 1000 px feather: 180 ms parallel, was 172). psd-tools: mask-parameters-no-real-channel 0.0168 -> 0.0060 (PASS), layer_mask_data.psd 26.9 % -> 3.9 % of pixels off (psb 25.9 % -> 1.8 %). Corpus test 239 -> 240; floor 236 -> 240. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Threshold: split on Photoshop's luminosity of whole samples Photoshop's Threshold splits on the luminosity 0.30 R + 0.59 G + 0.11 B (the blend modes' lum, as Gradient Map uses), not the Rec. 601 grey, taken over the document's whole sample values: - 8 bits: 30 R + 59 G + 11 B > 100 level - 50, i.e. the luminosity rounded to a level with halves down reaches the level; - 16 bits: 30 R + 59 G + 11 B >= 100 x the level in 16 bits (round(level x 32768 / 255): mid grey 16448 is level 128); - 32 bits: the luminosity reaches the level. Exact on every pixel of psd-tools threshold_rgb (36432 px; Rec. 601 split 28 the other way), threshold_16bits_rgb (100) and threshold_16bits_ grayscale, and the photoshop corpus's rgb16 threshold-128. The sums are exact in f32, on the CPU and in the GPU kernel, so ties (grey exactly on the level) split the same way on both. psd-tools: threshold_rgb and threshold_16bits_rgb pass; corpus test 239 -> 241, floor 236 -> 241. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Posterize: 16-bit documents bin the 16-bit sample, as Photoshop does At 16 bits Photoshop doesn't round to 8-bit levels before binning. It bins the 16-bit sample, floor(v16 * n / 32768), and lands on 8-bit levels through a 16-bit step: floor(b * 32768 / (n - 1)), converted with floor((o * 255 + 16384) / 32768). So the middle of 3 levels is 128 where 8 bits give 127, while 42.5 and 25.5 stay 42 and 25. Exact on every sample of psd-tools posterize_16bits_rgb (levels 3, 7, 13, 21) and posterize_16bits_grayscale (2, 6, 9, 11); the 8-bit rule missed up to half the range at bin edges. The same rule applies to CMYK and Lab channels at 16 bits: on the stored inks (#4945's native path) psd-tools posterize_16bits_cmyk goes from 4.26 % to 0.01 % of pixels off. 8-bit Posterize is unchanged. The GPU kernel runs the same rule; 16-bit layers reach it as half floats, so a sample within their precision of a bin edge can land in the neighbouring bin there (the CPU, which exports, is exact). psd-tools 239 -> 241; floor 236 -> 241. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Groups: a pass-through clipping base with an adjustment renders isolated A pass-through group composites its children straight into the backdrop, so an adjustment layer inside reaches the layers beneath. When the group is the base of a clipping group and holds an adjustment layer, Photoshop renders it isolated instead: in psd-tools passthrough_clipping_mask_adjustment an Exposure in such a group leaves the layers beneath alone (we darkened them: half the image 170 levels off). The same group with a Linear Dodge child and no adjustment keeps reaching the backdrop (passthrough_clipping_mask_blendmode), and so do clipping-mask3/4/5, so the rule is the adjustment, not the clipping alone (isolating every pass-through clipping base broke those two). CPU compositor and GPU plan alike. psd-tools 239 -> 240; floor 240. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
eb26e8f0d7 |
Masks: feathers blur with exactly the feather's variance (mask-parameters-no-real-channel passes) (#5163)
* Masks: feathers blur with exactly the feather's variance A mask feather is a Gaussian with sigma = feather, which we approximate with three box passes per axis. Whole-pixel boxes rounded the variance: a 6 px feather blurred like 6.48 px and a 3 px one like 3.46 px, so feathered edges were too soft by up to 4 levels against Photoshop (psd-tools mask-parameters-no-real-channel.psd: a 6 px vector and 3 px pixel feather). Each pass is now an "extended box" (Gwosdek et al. 2011): weight 1 on the whole radius and a fractional weight on the two samples beyond it, so three passes have variance sigma^2 exactly. It stays O(n), reusing the window's leaving sample for the low end weight (50 MP at a 1000 px feather: 180 ms parallel, was 172). psd-tools: mask-parameters-no-real-channel 0.0168 -> 0.0060 (PASS), layer_mask_data.psd 26.9 % -> 3.9 % of pixels off (psb 25.9 % -> 1.8 %). Corpus test 239 -> 240; floor 236 -> 240. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Threshold: split on Photoshop's luminosity of whole samples Photoshop's Threshold splits on the luminosity 0.30 R + 0.59 G + 0.11 B (the blend modes' lum, as Gradient Map uses), not the Rec. 601 grey, taken over the document's whole sample values: - 8 bits: 30 R + 59 G + 11 B > 100 level - 50, i.e. the luminosity rounded to a level with halves down reaches the level; - 16 bits: 30 R + 59 G + 11 B >= 100 x the level in 16 bits (round(level x 32768 / 255): mid grey 16448 is level 128); - 32 bits: the luminosity reaches the level. Exact on every pixel of psd-tools threshold_rgb (36432 px; Rec. 601 split 28 the other way), threshold_16bits_rgb (100) and threshold_16bits_ grayscale, and the photoshop corpus's rgb16 threshold-128. The sums are exact in f32, on the CPU and in the GPU kernel, so ties (grey exactly on the level) split the same way on both. psd-tools: threshold_rgb and threshold_16bits_rgb pass; corpus test 239 -> 241, floor 236 -> 241. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Posterize: 16-bit documents bin the 16-bit sample, as Photoshop does At 16 bits Photoshop doesn't round to 8-bit levels before binning. It bins the 16-bit sample, floor(v16 * n / 32768), and lands on 8-bit levels through a 16-bit step: floor(b * 32768 / (n - 1)), converted with floor((o * 255 + 16384) / 32768). So the middle of 3 levels is 128 where 8 bits give 127, while 42.5 and 25.5 stay 42 and 25. Exact on every sample of psd-tools posterize_16bits_rgb (levels 3, 7, 13, 21) and posterize_16bits_grayscale (2, 6, 9, 11); the 8-bit rule missed up to half the range at bin edges. The same rule applies to CMYK and Lab channels at 16 bits: on the stored inks (#4945's native path) psd-tools posterize_16bits_cmyk goes from 4.26 % to 0.01 % of pixels off. 8-bit Posterize is unchanged. The GPU kernel runs the same rule; 16-bit layers reach it as half floats, so a sample within their precision of a bin edge can land in the neighbouring bin there (the CPU, which exports, is exact). psd-tools 239 -> 241; floor 236 -> 241. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Groups: a pass-through clipping base with an adjustment renders isolated A pass-through group composites its children straight into the backdrop, so an adjustment layer inside reaches the layers beneath. When the group is the base of a clipping group and holds an adjustment layer, Photoshop renders it isolated instead: in psd-tools passthrough_clipping_mask_adjustment an Exposure in such a group leaves the layers beneath alone (we darkened them: half the image 170 levels off). The same group with a Linear Dodge child and no adjustment keeps reaching the backdrop (passthrough_clipping_mask_blendmode), and so do clipping-mask3/4/5, so the rule is the adjustment, not the clipping alone (isolating every pass-through clipping base broke those two). CPU compositor and GPU plan alike. psd-tools 239 -> 240; floor 240. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
9954c2935e |
Threshold: split on Photoshop's luminosity of whole samples (threshold_rgb, threshold_16bits_rgb pass) (#5263)
* Threshold: split on Photoshop's luminosity of whole samples Photoshop's Threshold splits on the luminosity 0.30 R + 0.59 G + 0.11 B (the blend modes' lum, as Gradient Map uses), not the Rec. 601 grey, taken over the document's whole sample values: - 8 bits: 30 R + 59 G + 11 B > 100 level - 50, i.e. the luminosity rounded to a level with halves down reaches the level; - 16 bits: 30 R + 59 G + 11 B >= 100 x the level in 16 bits (round(level x 32768 / 255): mid grey 16448 is level 128); - 32 bits: the luminosity reaches the level. Exact on every pixel of psd-tools threshold_rgb (36432 px; Rec. 601 split 28 the other way), threshold_16bits_rgb (100) and threshold_16bits_ grayscale, and the photoshop corpus's rgb16 threshold-128. The sums are exact in f32, on the CPU and in the GPU kernel, so ties (grey exactly on the level) split the same way on both. psd-tools: threshold_rgb and threshold_16bits_rgb pass; corpus test 239 -> 241, floor 236 -> 241. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Posterize: 16-bit documents bin the 16-bit sample, as Photoshop does At 16 bits Photoshop doesn't round to 8-bit levels before binning. It bins the 16-bit sample, floor(v16 * n / 32768), and lands on 8-bit levels through a 16-bit step: floor(b * 32768 / (n - 1)), converted with floor((o * 255 + 16384) / 32768). So the middle of 3 levels is 128 where 8 bits give 127, while 42.5 and 25.5 stay 42 and 25. Exact on every sample of psd-tools posterize_16bits_rgb (levels 3, 7, 13, 21) and posterize_16bits_grayscale (2, 6, 9, 11); the 8-bit rule missed up to half the range at bin edges. The same rule applies to CMYK and Lab channels at 16 bits: on the stored inks (#4945's native path) psd-tools posterize_16bits_cmyk goes from 4.26 % to 0.01 % of pixels off. 8-bit Posterize is unchanged. The GPU kernel runs the same rule; 16-bit layers reach it as half floats, so a sample within their precision of a bin edge can land in the neighbouring bin there (the CPU, which exports, is exact). psd-tools 239 -> 241; floor 236 -> 241. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Groups: a pass-through clipping base with an adjustment renders isolated A pass-through group composites its children straight into the backdrop, so an adjustment layer inside reaches the layers beneath. When the group is the base of a clipping group and holds an adjustment layer, Photoshop renders it isolated instead: in psd-tools passthrough_clipping_mask_adjustment an Exposure in such a group leaves the layers beneath alone (we darkened them: half the image 170 levels off). The same group with a Linear Dodge child and no adjustment keeps reaching the backdrop (passthrough_clipping_mask_blendmode), and so do clipping-mask3/4/5, so the rule is the adjustment, not the clipping alone (isolating every pass-through clipping base broke those two). CPU compositor and GPU plan alike. psd-tools 239 -> 240; floor 240. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
3feebb9b13 |
Groups: a pass-through clipping base with an adjustment renders isolated (passthrough_clipping_mask_adjustment passes) (#5272)
* Posterize: 16-bit documents bin the 16-bit sample, as Photoshop does At 16 bits Photoshop doesn't round to 8-bit levels before binning. It bins the 16-bit sample, floor(v16 * n / 32768), and lands on 8-bit levels through a 16-bit step: floor(b * 32768 / (n - 1)), converted with floor((o * 255 + 16384) / 32768). So the middle of 3 levels is 128 where 8 bits give 127, while 42.5 and 25.5 stay 42 and 25. Exact on every sample of psd-tools posterize_16bits_rgb (levels 3, 7, 13, 21) and posterize_16bits_grayscale (2, 6, 9, 11); the 8-bit rule missed up to half the range at bin edges. The same rule applies to CMYK and Lab channels at 16 bits: on the stored inks (#4945's native path) psd-tools posterize_16bits_cmyk goes from 4.26 % to 0.01 % of pixels off. 8-bit Posterize is unchanged. The GPU kernel runs the same rule; 16-bit layers reach it as half floats, so a sample within their precision of a bin edge can land in the neighbouring bin there (the CPU, which exports, is exact). psd-tools 239 -> 241; floor 236 -> 241. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Groups: a pass-through clipping base with an adjustment renders isolated A pass-through group composites its children straight into the backdrop, so an adjustment layer inside reaches the layers beneath. When the group is the base of a clipping group and holds an adjustment layer, Photoshop renders it isolated instead: in psd-tools passthrough_clipping_mask_adjustment an Exposure in such a group leaves the layers beneath alone (we darkened them: half the image 170 levels off). The same group with a Linear Dodge child and no adjustment keeps reaching the backdrop (passthrough_clipping_mask_blendmode), and so do clipping-mask3/4/5, so the rule is the adjustment, not the clipping alone (isolating every pass-through clipping base broke those two). CPU compositor and GPU plan alike. psd-tools 239 -> 240; floor 240. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
8504dc615e |
Posterize: 16-bit documents bin the 16-bit sample, as Photoshop does (#5267)
At 16 bits Photoshop doesn't round to 8-bit levels before binning. It bins the 16-bit sample, floor(v16 * n / 32768), and lands on 8-bit levels through a 16-bit step: floor(b * 32768 / (n - 1)), converted with floor((o * 255 + 16384) / 32768). So the middle of 3 levels is 128 where 8 bits give 127, while 42.5 and 25.5 stay 42 and 25. Exact on every sample of psd-tools posterize_16bits_rgb (levels 3, 7, 13, 21) and posterize_16bits_grayscale (2, 6, 9, 11); the 8-bit rule missed up to half the range at bin edges. The same rule applies to CMYK and Lab channels at 16 bits: on the stored inks (#4945's native path) psd-tools posterize_16bits_cmyk goes from 4.26 % to 0.01 % of pixels off. 8-bit Posterize is unchanged. The GPU kernel runs the same rule; 16-bit layers reach it as half floats, so a sample within their precision of a bin edge can land in the neighbouring bin there (the CPU, which exports, is exact). psd-tools 239 -> 241; floor 236 -> 241. Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
aa44eafe2c |
ACEScg: File Handling option opens colour-less EXR renders in the ACES working space (#4908)
* ACEScg: File Handling option opens colour-less EXR renders in the ACES working space Our render pipelines write ACEScg (AP1 primaries, ACES white ~D60, linear) and — like most — state no colour space in the EXR header, so io's linear-sRGB default misrepresented them (saturated hues shifted). - cms: AcesCg joins the built-in profiles, generated from the published AP1 primaries and ACES white point with a linear TRC (id "aces-cg", aliases acescg). - engine: File Handling gets "Open color-less EXR as ACEScg". When on, an EXR that io tagged with the linear-sRGB default opens as ACEScg instead; embedded or EXIF-declared profiles stay untouched. Translated in every catalog. ACES view transforms (OCIO-based display) remain future work; this covers the document's source colours. * scorecard: 26 of 158 (main's #3472 preference added one too) * Regenerate docs/scorecard.md after merging main --------- Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
a3a47e9cb0 |
History panel: the trash deletes the selected snapshot (#5324) (#5325)
Clicking a snapshot now selects it (highlighted in place of the current state) until the history moves on. With a snapshot selected the footer's trash runs history.deleteSnapshot instead of history.deleteState, as in Photoshop. Snapshot rows and past state rows can also be dragged onto the trash to delete them. Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> |
||
|
|
32fdc4e402 |
i18n(it): translate the 103 labels other catalogs added since #3742 (#4860)
* i18n(it): translate the 103 labels other catalogs added since the last sync * i18n(it): translate the 53 labels main added |
||
|
|
12a474d550 |
Interface: hide the Home screen's recent files (#4382) (#4387)
* Interface: a preference to hide the Home screen's recent files (#4382) Co-Authored-By: Claude Opus 5.5 <[email protected]> * i18n: drop the hu/tr 'Enable animations' lines, #4362 adds them Both PRs added the same two lines; whichever merged second would leave a duplicate key (bundled_catalogs_are_consistent rejects it). Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: regenerate scorecard after merging main Co-Authored-By: Claude Opus 5.5 <[email protected]> * Merge main; fix i18n (duplicate ja row, missing sr row) and regenerate scorecard Co-Authored-By: Claude Opus 5.5 <[email protected]> * Drop the duplicate hu row Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
5ae3ee0ae5 |
fix(ui): show brush tip thumbnails while dragging presets (#5203)
* fix(ui): show brush tip thumbnails while dragging presets * test(app): wait for autosave sidecar revision before recovery assertions |
||
|
|
b37a148759 |
ui: remove the title-bar Discord button (#1737)
Co-authored-by: highperfocused <[email protected]> |
||
|
|
0eb008ce5d |
Actions: record layer selections by name and display the target (#2441)
* Record action layer selections by name and show their targets * Use document pixel format in grouped-layer regression * Document the named layer action UI with a synthetic capture |
||
|
|
7b08b56531 |
feat(engine): layer.list, a read-only listing of the layer tree (#5059) (#5148)
The layers were only in document.inspect, together with history, channels and much more, so agents looked for a layer.* command that didn't exist. layer.list returns the layers top-down with id, name, kind, visibility, bounds, selected and active; groups list their children, or with "flat": true every layer is one row with its depth and parent group. It takes the same optional "document" index as document.inspect, is not journaled and bad params are errors. |
||
|
|
8d2806f66c |
Expose shared Smart Object instances in inspection and replacement results (#4832)
* Smart Objects: inspect and Replace Contents show shared instances (#4075) Placed layers that share a source have imported as one set of contents since #2222, and Replace Contents changes every instance, but nothing reported it: `linkGroup` is Link Layers, and `smartSource` can't tell copies of one contents from independent copies of the same file. document.inspect now gives each smart object `smartContents`: the contents `id` its copies share and how many `instances` there are. Replace Contents, Relink to File and Convert to Embedded/Linked list the `instances` they changed. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Test rendered caches after shared Smart Object replacement --------- Co-authored-by: jonathnwxh <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]> |
||
|
|
887dcd7ef9 |
Linux: a virtual keyboard claiming BTN_TOOL_PEN no longer sends the window through Xwayland (#5284) (#5295)
* Linux: a virtual keyboard claiming BTN_TOOL_PEN no longer sends the window through Xwayland (#5284) On Wayland the app opens its window through Xwayland when a pen is attached (#639), and it took any input device whose key bitmap has BTN_TOOL_PEN for a pen. A virtual keyboard made with uinput (RustDesk's, for one) claims every key code, that one included, so the window went through Xwayland on machines with no pen at all. On a fractionally scaled desktop (GNOME 46 at 125%) the X11 window is drawn at the logical size and upscaled by the compositor, so the UI came out blurred even though Auto UI Scale already followed the system's 1.25 (#1237): native Wayland, where winit negotiates wp_fractional_scale_v1 and renders at the physical 3840 px, was never used. A pen now also needs ABS_X and ABS_Y in its absolute-axis bitmap, as libinput asks of a tablet tool; a keyboard has no axes. Regression tests with the virtual keyboard's own bitmaps, a pen beside it, and a pen display. Co-Authored-By: Claude Fable 5.1 <[email protected]> * Docs: on a fractionally scaled Wayland desktop, Xwayland is blurred and native Wayland has no file drops (#5284) Say where the trade-off is: a native Wayland window is sharp at 125%, 150% and 175% but takes no dropped files (#386); an Xwayland window takes drops and pen input (#639) but is enlarged by the compositor. List the ways to open through Xwayland and what counts as a pen. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Fable 5.1 <[email protected]> |