mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-02 14:27:35 -06:00
2ed222416d45d710d09ca08573258a02bbfd39a9
54
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
33dd5f0103 |
perf(gpu): blend plain layers several to a pass, P60/P61 111 -> 28 ms
- LayerBlend: a plain layer is blended straight from its texture in one pass instead of a Content pass to a scratch chunk and a Blend pass. - fs_layerbatch: runs of up to four plain layers that only feed each other are drawn as one pass, reading and writing the chunk once (the uniform record grows to its 256-byte stride to hold the extra layers). - Drop shadows and outer glows alone: the merge builds the layer from its shape (F_LAYER_SHAPE), and nothing is copied back when the effect region covers the canvas. Such a layer takes 3 passes instead of 6. - fs_layerbatch_normal: runs of Normal layers use a shader with Normal's code alone. Per-pass timestamps showed the batch spent 80% of its time on the register cost of carrying every blend mode, not on the arithmetic; Normal goes through the same function in every shader, so a layer gives the same bits whichever pass draws it. P60 / P61 with the GPU compositor (8 GB budget, M4 Pro): 111 / 113 ms -> 28.2 / 28.1 ms. With the app's default Memory Usage these documents still fall back to the CPU compositor on unified memory, so the budgets stay unenforced. Output: over the GPU parity suite (1620 renders) 1038 are bit-identical to before and the rest differ by at most 1e-4/255, except one pixel one 8-bit step apart where an adjustment rounds at .5; the mean difference from the CPU reference is unchanged. New parity tests cover runs of every mode, Normal runs at 8/16/32-bit and Lab, and exterior effects at any fill. |
||
|
|
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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
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 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
c7366b8938 |
Drop shadow: knocked out beneath the whole shape when the layer's mode blends with it (#5304)
Layer Knocks Out Drop Shadow only knocked the shadow out through see-through fill: at 100 % fill the layer was taken to cover it. That holds in Normal mode, but a layer in Exclusion, Difference, Multiply, Screen and the rest blends with what's beneath it, shadow and all: a white Exclusion bar with a drop shadow over black type came out as a blank white bar instead of white type on black (Photoshop's merged image of the same file shows the shadow kept out from under the bar), and a Multiply layer was darkened by its own shadow. shadow_see_through() now knocks it out wholly in those modes, for the CPU compositor and the GPU plan alike. Co-authored-by: Daniel van Lith <deeev@Daniels-MacBook-Pro.local> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
753b610105 |
Keep gradient stops closer than one LUT step on the CPU. (#4712)
The GPU ramp is 4096 entries, so stops at 0.4999 and 0.5 interpolated to gray while the CPU kept the step. Stops that share an entry now use the CPU composite. Fixes #3053. Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
f57db2b5bb |
feat(gpu): composite Blend If on the GPU (#4395) (#4541)
A layer with Blending Options › Blend If made the planner refuse the whole document, so every refresh of it ran the CPU compositor: on the #209 layered document an opacity edit took 140 ms instead of 6 ms. Blend If is compose::apply_blend_if: the composite with the layer mixed back towards the backdrop by a per-pixel weight from the layer's own content with its masks ("This Layer"; an adjustment's result) and the backdrop ("Underlying Layer"). The planner now keeps the backdrop, composites the layer as before (channel restrictions included, as on the CPU), renders its own content and emits one Lerp pass with the new F_BLEND_IF flag and the ranges packed exactly in its parameters. Base and clipped layers both go through it; Advanced Blending still falls back to the CPU. Hard points: compositing can leave an 8-bit level a few millionths below itself (a level-20 backdrop at partial alpha reads 19.999998), which put it on the wrong side of a hard point at that level on the CPU, and the GPU's arithmetic differs there. Both now treat a value within 1e-4 of a level as that level; 8/16-bit values off a level are at least 1/257 away. On 16/32-bit documents the GPU reads layers as RGBA16F, so a value within about a tenth of a level of a hard point can still land on the other side, as with the GPU Threshold adjustment. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
becc27ac1c |
CMYK: Invert, Posterize and Threshold on the inks; 16-bit Lab Invert keeps greys neutral (#3930)
* Lab and CMYK: Invert, Posterize and Threshold work on the document's channels Photoshop applies these to each stored channel, not to RGB: - Lab: Invert and Posterize work on L, a and b, and Threshold cuts on L to a neutral black or white. - CMYK: Invert and Posterize work on each ink, black included, so paper inverts to 400% ink. Threshold gives paper or 100% black ink. Adjustment layers now do the same: - compose::adjust::apply_mode goes through the document's Lab or CMYK channels. - The GPU falls back to the CPU for these, as it already does for Lab/CMYK Levels and Curves. - The destructive Image > Adjustments path works on the stored samples directly, so it is exact. Photoshop oracles (lab/cmyk 8 and 16 bit), bad pixels before -> after: - lab threshold: PASS (was 12%) - lab invert: 72-99% -> 1% - lab posterize: 89% -> 0.1% - cmyk invert: 77% -> 10% - cmyk posterize: 71% -> 9% - cmyk threshold: 37% -> 1.6% Floor 133 -> 139 (137 already passed on main). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * CMYK: Invert, Posterize and Threshold on the inks; 16-bit Lab Invert keeps greys neutral Builds on main's adjust::apply_in (#4038): - apply_in's mode dispatch gains CMYK. Invert and Posterize work on each ink, black included, so paper inverts to 400% ink. Threshold gives paper or 100% black ink alone. cmyk_channel_adjustment says which adjustments do this. - Lab Invert mirrors a and b around neutral at 16 bits; 8-bit bytes still become 255 - v. stored_channels holds both rules, and apply_lab uses it. - Destructive Image > Adjustments on a Lab or CMYK layer works on its stored channels directly, so it is exact; a selection mixes the stored values. - The GPU leaves CMYK Invert, Posterize and Threshold to the CPU. Photoshop oracles, bad pixels: - cmyk invert: 77% -> 10% - cmyk posterize: 71% -> 9% - cmyk threshold: 37% -> 1.6% - lab16 invert: 52% -> 1.2% The count stays at 145/258, which main already passes, so the floor goes 133 -> 145. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
ab82871271 |
fix(compose): paint interior effects over the layer's blend mode (#3996) (#4231)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
fd21e73411 |
Format: check pattern sizes on load (#3478) (#3484)
* Format: check pattern sizes on load (#3478) A .pcraft pattern's width/height came straight from the manifest, so a 1 KB file could declare a 65535x65535 pattern without tiles; painting it allocated width*height*16 bytes (abort) and the engine's u32 size math overflowed. - format: reject patterns 0 px on a side (Corrupt) or above the PSD reader's 30 000 px edge (LimitExceeded). - doc: Pattern::pixel_count / is_paintable with MAX_PAINT_PIXELS (2^26, a 1 GiB f32 RGBA working copy). - compose/gpu: patterns too large to paint paint nothing instead of allocating (Tile::new_in, pattern_rgba, PreparedPatterns, GPU plan). - engine: brush.texturePattern refuses them; average_color uses u64 math and a bounded corner. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * gpu: skip unpaintable patterns in the Bevel texture too The Bevel & Emboss texture element uploaded the pattern through pattern_rgba, which now returns no pixels for a pattern too large to paint; filter it like the other GPU pattern lookups. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
27f56eb762 |
feat(compose): Lab documents evaluate Invert, Posterize, Threshold, Brightness/Contrast and Gradient Map on L, a, b (#4038)
In Lab documents Photoshop evaluates these adjustments on the Lab channels, not on the screen RGB colour: - Invert and Posterize act on L, a and b alike (normalised L/100, (a+128)/255, (b+128)/255; Invert is n -> 1 - n). - Brightness/Contrast (modern and legacy) applies its tone curve to L only; a and b are unchanged. - Threshold splits on L alone and outputs neutral black or white. - Gradient Map is driven by L (Reverse uses 1 - L). Adjustment layers (CPU compositor) and destructive Image > Adjustments both go through the new `adjust::apply_in`; the GPU compositor defers these adjustments to the CPU in Lab documents. Levels and Curves keep their Lab records; other adjustments stay on the screen colour. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3794ad471f |
Color Balance: Photoshop's per-channel levels and gamma curves (#3443)
Color Balance added a share of each slider weighted by the pixel's lightness, then (Preserve Luminosity) rescaled to the old luma. On the corpus's Photoshop-made file (photoshop/adjustments/rgb8/color-balance.psd, all three tones moved, Preserve Luminosity on) that was up to 43 levels off on 99% of the pixels. Measured on Photoshop 27.11 (scripted: a gray ramp and 16 pure colours through each slider at ±25, ±50 and ±100, with Preserve Luminosity on and off, plus two and three tones and two sliders at once), every slider is a levels-and-gamma curve on each channel: - Preserve Luminosity off: midtones is gamma 2^-a on the slider's own channel; shadows gamma 2^(-a/2), after a black point at 0.39·|a| when lowered; highlights gamma 2^(-a/2), after a white point at 1 - 0.39·a when raised. - Preserve Luminosity on: midtones gamma 2^(-a/2) on its channel and 2^(a/2) on the other two; shadows and highlights go by the differences between a tone's sliders: a black point at 0.39·(max - own) and a white point at 1 - 0.39·(own - min) per channel. - Tones apply highlights, shadows, then midtones. All 88 measured cases match within 1 level, except midtones +100 below about 28 of 255 (Photoshop flattens the steepest gamma near black, by up to 9 levels). The corpus file is now within 4 levels (mean 1.4, was 20.9). The GPU builds the same curves into per-channel tables and uses the Levels/Curves kernel; the old Color Balance shader case is gone. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
53d09f5850 |
Preserve HDR range in float adjustments and stack statistics (#2592)
* Preserve HDR range in float adjustments and stack statistics * Cover HDR exposure round trips and Levels record order * Report the backend used by GPU parity tests * Compare float PSD oracles at native depth and preserve signed Exposure * Retain merged alpha metadata when decoding float references --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
a8ad82dd46 |
GPU composite: reuse unchanged lower-stack chunks for property edits (#1169)
* perf(gpu): reuse unchanged lower-stack composite chunks * test(gpu): verify half-float composite checkpoint reuse * docs: record measured GPU composite reuse gains and limits * perf(gpu): admit whole grid chunks, serve partial damage as sub-rect copies Cache entries were keyed by the exact damage rectangle, so brush strokes (a new partial rect every frame) filled the budget with entries that never hit again. Entries are now whole chunk-aligned, chunk-sized grid cells: a capture merges into the cell it overlaps at the rectangle's offset, a read is served as a sub-rect copy of the covered part, and coverage is tracked per cell so a resume only happens where every pixel of the requested rectangle is cached. After a full render, any in-bounds partial rectangle hits. Regression test: two series of disjoint strokes inside the same chunks leave composite_cache_bytes unchanged (no entry-per-rectangle growth), a full refresh still finds its chunks and a top-layer edit afterwards reuses them. The old partial-damage test now expects the covered-chunk hit and checks outside-the-document renders admit nothing. * docs: record the GPU reuse measurement in target-app-parity.md The honest assessment moved from roadmap.md to target-app-parity.md on main; keep main's roadmap and add only the dated measurement. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
96ec16ed7d |
fix(compose, gpu): round dithered gradient fills to the document depth (#2755) (#2781)
A live gradient's dither adds a fraction of a level of noise, but gradient fill layers kept it in float. Color Dodge divides by 1 - cs, so under a bright dodge layer that sub-level noise came out as speckle several levels high, and channels at 0 were lifted off black (coloured fringes). Dithered gradient pixels are now rounded to the document depth's levels (8/16-bit; float documents unchanged), as a painted gradient stores them. Same rounding in the GPU shader. Regression tests in compose and a GPU parity case for stacked dithered gradients under Color Dodge. Claude-Session: https://claude.ai/code/session_01Qpa5z8GksXAFiDavFkmh4R Co-authored-by: Leel Wasanatha <260480244+kavishkashehan726-sudo@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4760f75b4d |
Blend modes: Fill below 100% works inside the blend for Photoshop's special eight (#2552)
* Blend modes: Fill below 100% works inside the blend for Photoshop's special eight In Photoshop, Fill and Opacity are the same for most blend modes, but not for eight: Color Burn, Linear Burn, Color Dodge, Linear Dodge (Add), Vivid Light, Linear Light, Hard Mix and Difference. PhotoCraft treated Fill as Opacity for all of them, so a Difference layer at 50% Fill gave (150,90,110) where Photoshop gives (150,60,10). Measured on Photoshop 27.11 (scripted, 24 cases at Fill 25/50/75% with two colour pairs): below 100% Fill, Photoshop pulls the layer's colour toward the mode's neutral colour (black for Color Dodge, Linear Dodge, Difference; white for Color Burn, Linear Burn; 50% gray for Vivid and Linear Light) and blends that at full strength; Hard Mix becomes clamp((b - f(1 - t)) / (1 - f)). Opacity still mixes the result in. That reproduces 31 of 32 values within 1 level (Hard Mix at 75%: 3). CPU (psblend::composite_fill, the final blend of plain pixel, shape and type layers) and GPU (Kernel::Blend takes Fill in the spare uniform slot) agree; Dissolve keeps Fill as coverage. Layers with effects, adjustment layers and groups keep the old behaviour for now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * GPU: composite_fill with a single blend_rgb call, so fs_blend stays small The first version added a third inlined copy of blend_rgb (every blend mode) to fs_blend: composite_g's two plus blend_rgb_fill's. Windows CI (software WARP adapter) then timed out rendering the UI in arbitrary_rotation_preview, which passes on main; a larger shader compiles noticeably slower there. composite_fill now handles every case itself with one blend_rgb call (Fill as coverage for normal modes, the special-eight blend, Lab Normal mixing, the text gamma space) and is fs_blend's only composite path, leaving it with fewer inlined copies than on main. Same results: the 35 GPU parity tests pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2c163bf73d |
Legacy Brightness/Contrast: brightness first, then contrast around 128, as in Photoshop (#2615)
* Legacy Brightness/Contrast: brightness first, then contrast around 128, as in Photoshop With Use Legacy on, PhotoCraft applied contrast first and then added the brightness, so any setting that moves both sliders came out wrong: brightness +40 with contrast +30 mapped mid-grey 128 to 168, where Photoshop gives 185 (up to 17 levels darker across the midtones). Measured on Photoshop 27.11 (scripted, a gray ramp through seven settings): Photoshop adds the brightness, then scales contrast around 128/255. Contrast 80 keeps 128 at 128; the 0.5 pivot moved it to 130. With both changes every measured value matches within 1 level, on the CPU and in the GPU adjustment kernel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Legacy B/C: brightness after contrast when contrast is lowered The corpus caught it: psd-tools' brightnesscontrast_legacy_grayscale.psd (brightness -23, contrast -26) matched Photoshop with the old order and was 6 levels off with brightness first. Photoshop adds the brightness before contrast when contrast is raised (measured: +40/+30 maps 128 to 185) and after it when contrast is lowered. The measured cases with lowered contrast had brightness 0, so they could not tell. CPU and GPU kernel (brightness after contrast goes in p0.w). The io corpus oracle is back at its floor; the 34 GPU parity tests pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
5b051861bc |
fix(blend): keep 32-bit Hue/Saturation/Color/Luminosity above 1 (#1735)
- Hue, Saturation, Color and Luminosity no longer clip 32-bit documents to 0..1, so values above 1 survive in the CPU compositor (blend_rgb_hdr now uses blend_rgb_within at f32::MAX) and in the GPU shader (under the existing F_HDR flag). 8/16-bit results are unchanged apart from rounding at white. - ClipColor returns the grey when the luminosity is outside the in-gamut range, instead of dividing by max − lum rounding noise. The old formula turned colours inside out (Luminosity of a grey of 2 over (0.5, 0.25, 0.125) gave (1.0, 2.32, 2.99)), and CPU and GPU disagreed (Hue over a grey of 2: CPU 1.0, GPU 2.82). - Tests: 8/16-bit inputs match the ISO 32000 formula, HDR cases in compose, psblend::with_hdr nesting, and a CPU/GPU parity test across depths. Co-authored-by: hideoneric <hideoneric@users.noreply.github.com> |
||
|
|
5290930e4a |
fix: Preserve CMYK solid and gradient fills through PSD round trips (#2628)
* fix(compose): Preserve native CMYK fills through PSD round trips Share depth-quantized ink samples between rendering and PSD caches. Use the document profile for RGB fill colours and preserve valid imported caches. Uncached CMYK fills use CPU rendering. Cover all sample depths, gradient styles and tiled renders. Fix the real 16-bit gradient round trip and raise the psd-tools floor from 307 to 308. * perf(raster): Reuse exact 8-bit CMYK conversions within rows Cache the previous ink bytes and ICC result per row, with alpha decoded independently. Keep the 16/32-bit conversion loops unchanged. Matched 1 MP reads improve by 77% for smooth ramps, 33% with dithering and 10% for random colours. Bitwise regression coverage includes all depths, alpha and tile boundaries. |
||
|
|
db0fb07862 |
Add native layered Paint.NET (.pdn) import (#1998)
Squashed onto main, with the maintainer's asks applied: - Photoshop's blend menus (layers, brush, pencil, gradient, layer style) stay as they are; the Layers panel and Blending Options list a Paint.NET mode only for a layer that uses one. - The six Paint.NET mode labels are translated in every catalog, and the blend-mode i18n test iterates layer_modes(). - pdn::pixels rejects truncated uncompressed (version 1) data before allocating its buffer. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
45b22e7403 |
GPU: gradients with coincident stops fall back to the CPU compositor (#2005)
The 4096-entry ramp LUT interpolated across a hard stop, showing grey where the CPU export is white. Gradient fills, Gradient Maps and gradient effects with coincident colour or opacity stops now return Unsupported so the canvas uses the CPU oracle. Fixes #974. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f1413ea12e |
Outer Glow stays behind a see-through fill (#1964)
Outer Glow now gets the same fill-dependent knockout as Drop Shadow, on CPU and GPU, so a Fill 0% shape with a glow no longer paints the glow across its interior. Fixes #1953. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
60018dea69 |
fix(compose): don't clip Add and Divide at 1 in 32-bit documents (#1722)
- Linear Dodge (Add) and Divide keep values above 1 in 32-bit float documents, clipped only at f32::MAX. Integer depths still clip at 1. - CPU: add psblend::HDR and generic::blend_rgb_hdr / blend_channel_hdr; render sets the flag per document depth. - GPU: add the F_HDR pass flag, and compose.wgsl uses it for the same clip bound. - Non-separable modes are unchanged. - Tests: colour unit tests, compose depth tests, and the GPU parity test float_documents_blend_past_white. Co-authored-by: hideoneric <hideoneric@users.noreply.github.com> |
||
|
|
5355221f81 | Preserve document depth when splitting vector shapes (#1495) | ||
|
|
b0680fd421 |
Advanced Blending: knockout, blend as group, transparency shapes, masks hide effects (FILE-215-16, part of #215) (#903)
* Advanced Blending: knockout, blend as group, transparency shapes, masks hide effects (part of #215) Blending Options > Advanced Blending now works end to end: - doc: AdvancedBlending on every layer (knockout none/shallow/deep, blend interior effects as group, blend clipped layers as group, transparency shapes layer, layer/vector mask hides effects), saved in .pcraft. - PSD and layered TIFF: knko, infx, clbl, tsly, lmgm, vmgm read into the model and written back (in place, or added when not at the defaults); the TIFF byte-order transcoder handles lmgm/vmgm as byte flags. - CPU compositor (compose/src/advanced.rs): shallow and deep knockouts (isolated groups and clipping groups stop a deep knockout), clipped layers blended individually, effects over the whole layer when Transparency Shapes is off, effects built from an unmasked copy when a mask hides them, interior effects under fill opacity. - GPU: documents using Advanced Blending fall back to the CPU compositor. - engine: layer.layerStyle.blendingOptions takes channels, knockout and the five switches; inspect reports them; Copy/Paste Layer Style carries them. - UI: the Blending Options page laid out like Photoshop's (General, Advanced Blending, Blend If with split sliders), translated. psd-tools corpus oracle 229 -> 236 (seven knockout files now pass; floor raised); other corpora and all round trips unchanged. Scorecard FILE-215-16 -> partial. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Polish catalog rows for Advanced Blending; regenerate docs/scorecard.md pl.tsv landed on main and the coverage test needs every catalog to translate new UI strings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Undo damage: compare Layer.advanced in layer_pixels_equal; regenerate scorecard #810's exhaustive Layer destructure must classify the new advanced-blending field; it changes the composite, so it is compared. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Regenerate docs/scorecard.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: leon <claude.udder842@passmail.net> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
4649946900 |
Vibrance: Photoshop's Saturation and Vibrance, measured (linear light, 0.288 R + 0.712 G grey) (#1418)
Saturation −100 greyed to the HSL lightness, so a yellow (255, 255, 0) became grey 128 where Photoshop shows near-white: a yellow logo with Brightness/Contrast +150 and Vibrance › Saturation −100 clipped to it stayed yellow under Lighter Color. Measured black-box on Photoshop 25.4 (16-bit runs of 7,400-colour charts and HSV sweeps; sRGB, Adobe RGB and ProPhoto documents), both sliders work in linear light through the document tone curve, Vibrance first, then Saturation: - Saturation moves each colour from the grey 0.288 R + 0.712 G (blue has no weight, in every profile) by 1 + s, clipping per channel: within 0.07/255. - Negative Vibrance is an HSV change in linear light keeping the hue, in closed form: 0.2/255 on average, the deepest shadows (V < 0.05) drift. - Positive Vibrance: the brightness term is exact; the saturation boost (stronger for dull and dark colours, damped on reds and skin) is a fit, 0.4 (+10) to 5 (+100) /255 on average. CPU (compose::adjust::vibrance_px) and GPU (compose.wgsl) share the constants; the parity test gains a negative Vibrance and Saturation −100. photocraft-corpus adjustments/rgb8|rgb16/ vibrance.psd (Vibrance +60, Saturation −15): 25.6/255 mean, 89 max → 0.38 mean, 5 max. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
f985b81300 |
Layer style: an Outside stroke at Fill below 100 % leaves the interior clear (#906)
Fill 0 % + an Outside stroke is Photoshop's "outline only" look, but on pixel layers the stroke lay beneath the whole interior at full coverage, relying on the layer composited on top to hide it. With the fill lowered the interior showed the stroke colour (a solid disc instead of a ring). The share beneath an interior pixel is now (1 - a) / (1 - c), with a the shape's coverage and c the layer's alpha after fill: 1 under an opaque layer (100 % fill renders exactly as before, so PSD oracles don't move), 1 - a at 0 % fill. CPU compositor and the wgpu shader agree (the layer's alpha now rides along the coverage seed for pixel layers too). Tests: compose regression at 8/16/32-bit (0 %, 50 %, 100 % fill), and a GPU parity case at fill 0 and 0.5 (fails without the shader change). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
5049a1d062 |
Never crash on hostile numbers: LUT sizes, Quick Selection points, link group ids (#869)
- Color Lookup: a stored document's `size` is untrusted. `size³ × 3` overflowed for sizes of
2²² and up: debug builds panicked, release builds wrapped past the table-length guard and
indexed out of bounds. One checked `photocraft_doc::adjust::lut3d_len` now backs the CPU
compositor, the GPU planner and the PSD writer.
- Quick Selection: a stroke point far off the canvas (`select.quick {"points":[[3e9,0]]}`)
saturated to i32::MAX and then overflowed on `+ 1`.
- Link Layers: `max + 1` over stored link group ids overflowed at u64::MAX (debug panic, release
wrap that could merge unrelated layers). A fresh id is now the smallest unused one when the
largest is taken; a selection's first group is read with `first()`.
- Vanishing Point clone footprint: the same `as i32 + 1` saturates (hardening; no reproducer).
|
||
|
|
452672765d |
Photo Filter and gradient glows render like Photoshop (#442, #443) (#850)
* Photo Filter and gradient glows render like Photoshop (#442, #443) Photo Filter multiplied encoded RGB by the filter colour and rescaled to Rec.601 luma. Photoshop multiplies the pixel's D50 XYZ (relative to white) by the filter colour's, mixed by density, and Preserve Luminosity restores the encoded luminosity like the Luminosity blend mode (SetLum + ClipColor); 32-bit documents keep luminance Y. rgb16 photo-filter.psd now matches within 0.6/255 (was 125/255); rgb8 mean error 0.3/255 (was 20.8). Outer and Inner Glow gradients were laid out across the canvas (a vertical linear gradient), so the colour bands ran top to bottom. Photoshop colours the glow at strength v with the gradient at 1 - v (first stop at the edge) and the glow is opaque from v = range^2. outer-glow-gradient.psd: mean 1.4/255, max 9.9 (was 252). CPU and GPU paths updated together; photoshop oracle floor 132 -> 133. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Merge origin/main; regenerate docs/scorecard.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
7313e08141 |
32-bit documents: Levels don't clip the input and output ranges (#403)
* 32-bit documents: Levels don't clip the input and output ranges Photoshop's float Levels keep the curve going past the black and white points and use a plain power for the gamma. rgb32 and gray32 levels.psd now match within 0.5/255 (was 10/255). * Raise the Photoshop oracle floor to 132 * Levels: destructive Image › Adjustments › Levels doesn't clip in 32-bit either; neutral comment wording Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8a5e89faa6 |
32-bit documents: Exposure works on the stored linear values (#400)
* 32-bit documents: Exposure works on the stored linear values Their samples are linear already, so going through the 2.2 curve applied the gamma twice. rgb32 and gray32 exposure.psd now match Photoshop within 0.5/255 (was 69/255 and 44/255). * Raise the Photoshop oracle floor to 130 |
||
|
|
dbb8911f2a | Live gradients: Gradient tool makes editable Gradient Fill layers (on-canvas handles, stops, midpoints) + Classic mode (#180) (#197) (#197) | ||
|
|
8bac8926a3 | PSD stroke effects: outlines of filled shapes, vector stroke above interior effects, joined subpaths (#233) (#233) | ||
|
|
f114f62179 |
Layout documents: fix GPU device loss, live Move tool, local damage, no per-frame work (#125, #128) (#226) (#226)
Merged with #241 (menu_bar returns the menus' right edge) and #240 (stroke trail field). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
1d9d92341f | PSD effects fidelity: spread metric, emboss, gradient geometry, text gamma (113 -> 118 oracle passes) (#224) (#224) | ||
|
|
4832e08aad | PSD fidelity: 16/32-bit colour modes, Lab, Levels, Exposure, Dissolve oracle (corpus 133 → 140, psd-tools 212 → 222) (#202) (#202) | ||
|
|
574d0d3d53 | PSD corpus: psd-tools set (pinned, verified), an oracle for every file, and the fixes it found (#195) (#195) | ||
|
|
37c8f81032 |
Image › Adjustments overhaul: full params, one editor per kind, live-preview dialogs, Curves point deletion (#68)
* Adjustments: full parameter sets for every kind, Hue/Sat ranges, CMYK/Lab tone channels - doc: Hue/Saturation gains six colour ranges (HueRange), Gradient Map a dither flag, Levels/Curves a ToneSpace (RGB, CMYK inks + black, Lab) for mode-native channels. - engine: new adjust_params module (from_params over a base / to_params) used by image.adjustments.*, layer.newAdjustmentLayer.* and layer.setAdjustment. Curves, Color Balance, Channel Mixer, Black & White, Gradient Map and Photo Filter now honour their params; bad params are BadParams errors; setAdjustment merges (empty = reset). - compose/gpu: hue-range tables, CMYK/Lab tone channels (CPU; GPU falls back), dithered Gradient Map, Black & White from grey/secondary/primary parts; GPU parity extended. - io: hue2 ranges, grdm dither, CMYK/Lab Levels/Curves records round-trip. - destructive adjustments use the document's tone transfer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Adjustments UI: one editor per kind for Properties and Image › Adjustments dialogs - adjust_editors: Brightness/Contrast (legacy), Levels and Curves (per channel in RGB, Gray, CMYK and Lab, histogram behind), Exposure, Vibrance, Hue/Saturation (master + six ranges with draggable range sliders, colorize, before/after spectrum), Color Balance (tones, preserve luminosity), Black & White (six weights, tint colour), Photo Filter (presets or colour, density), Channel Mixer (outputs, constant, monochrome, total), Posterize, Threshold (histogram), Gradient Map (stops, presets, reverse, dither). Replaces the M7 placeholder. - Properties previews live (GPU) while a control moves and commits one setAdjustment per gesture. - adjust_dialog: Image › Adjustments opens the same editor modally; the canvas previews the real command on the proxy, Preview checkbox, OK = one history step, Cancel leaves the doc. - Curves (#43): hit-test at the press origin, Delete/Backspace removes the selected point when the graph has focus, Cmd/Ctrl-click removes, dragging off removes (and back on restores), arrows nudge; endpoints and the last two points stay; pressing the gradient bars does nothing. - tone: histograms per channel space, of the image below a layer or of the target layer alone. - kittests: every adjustment layer kind edited through the Properties panel with real input at 8/16/32-bit RGB, Gray, CMYK and Lab (#12); Curves point interactions; dialog preview/OK/Cancel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * io tests: adjustment round trip covers the new Levels/Curves/Hue-Sat/Gradient Map fields #60's round-trip tests predate #68's doc-model additions (tone space and black channel, Hue/Saturation ranges, Gradient Map dither); fill them with their defaults. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f8f48db78a |
Never crash: the GPU compositor is panic-free (Rule 9) (#55)
- A failed readback mapping (e.g. device lost) now returns Unsupported,
so the caller falls back to the CPU compositor, instead of
expect("mapped readback buffer").
- Pipeline creation skips kernels without an entry point instead of
expect; resident/effect entries are looked up with let-else after the
insert; effect-program targets are Option instead of indexing +
expect; copies without a source are skipped; the planner returns
Unsupported instead of unreachable! for an adjustment layer.
- gpu denies unwrap/expect/panic/unimplemented/todo/unreachable.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
f7eb6ea9e7 |
Format: adopt the craft-family rustfmt.toml and format the workspace
CI's `cargo fmt --all -- --check` has failed on every run because the repo had no rustfmt config and was never formatted. Use the same settings as EffectCraft/FilmCraft (max_width 160, use_small_heuristics Max), which also keeps the churn small. Formatting only; no code changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
11e5af23ad |
Layer Style › Blending Options › Blend If (#26)
Model Blend If (This Layer / Underlying Layer ranges, split points) as
Layer::blend_if, read and write it as the PSD layer-record blending
ranges (previously parsed but dropped on export), save it in .pcraft,
render it in the CPU compositor (premultiplied mix = compositing at
k × alpha), fall back to the CPU on the GPU path, and drive it from
layer.layerStyle.blendingOptions {"blendIf": …}; document.inspect
reports it.
Co-authored-by: storybored379 <storybored379@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
4759d12b57 |
Modern Brightness/Contrast: real Photoshop curves (CPU + GPU)
The non-legacy B/C was a linear scale that hard-clipped; Photoshop's modern
B/C is a pair of smooth curves that pin pure black and white. Reverse-
engineered from Photoshop 2026 ground truth (a 0..255 ramp pushed through the
app via executeAction BrgC, useLegacy=false):
- Contrast: symmetric cubic-Hermite S pivoting at 0.5 (end slope 1-c/128,
centre 1+c/128) -- near-exact, <=0.5/255 vs Photoshop.
- Brightness: line of slope 1.375^(b/50) rolled off to (1,1) by a v^P white
anchor -- close, approximate at the extremes (the real roll-off is a spline).
Applied brightness-then-contrast; matched in the wgsl compositor (mbright/
mcontrast) so GPU == CPU oracle (gpu parity 18/18). Golden tests pin both
curves to the captured Photoshop samples.
Corpus oracle: brightnesscontrast_grayscale.psd max error 0.478 -> 0.086
(bad px 91% -> 29%); 113 pass / 30 differ unchanged (no regressions).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
42266291d5 |
Fidelity, GPU fallbacks, interactive performance; print spool-file fix
PSD oracle 111 -> 113 (tent effect blurs, bevel height/shading, glow range, spread rounding, text gamma, gradient span, Lab blending, colour burn/dodge edges), channel restrictions (brst), bevel contour/texture, chisel techniques. Vector masks, pass-through clips, artboards, pattern fills and bevel textures render on the GPU (parity <=1/255). PNG export 7x, PSD save 4x, .pcraft save 3.6x faster; CPU effects refresh 11x; deadlock in the effect-map blur fixed. interactive_bench example. Print: unique spool file per call (concurrent prints of same-named documents overwrote each other). Workspace: 2,040 tests pass, clippy clean, layers + wasm OK, parity 600/625. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
ceb2667ac4 |
Preset panels, character/paragraph styles, Glyphs, Check Spelling
Gradients/Patterns/Styles/Shapes/Tool Presets/Clone Source panels with headless preset commands and a Custom Shape tool; Color card tabs match Essentials. Character and Paragraph Styles (doc model, .pcraft, PSD style sheets), Glyphs panel + type.insertText, Check Spelling (SCOWL-50 list). Checkpoint also includes compiling in-progress work of the remaining agents. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
aa87012aab |
Analysis, notes, proof views, workspaces (+ in-progress work of other agents)
Image > Analysis (ruler, count, measurement scale/log/record, scale marker), notes (PSD Anno round trip, Notes panel, import), Modifier Keys panel, Proof Setup plates / Legacy Mac RGB / colour blindness (Vienot), 32-bit preview options, custom pixel aspect, extras options, New/Delete/ Lock Workspace. Parity 600/625, FLOOR 595. Checkpoint: also contains compiling in-progress work from the parallel panels, type styles, File menu, photo, filters, UI-parity and fidelity agents, plus photocraft-cli serve. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
aad724f28e | work in progress |