mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-30 21:07:04 -06:00
2fbd171427b2fe3c1908552d239822e1d9f4172f
52
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> 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 <noreply@anthropic.com> 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 <noreply@anthropic.com> 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 <noreply@anthropic.com> 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 <noreply@anthropic.com> 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 <noreply@anthropic.com> 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 <noreply@anthropic.com> 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * i18n: keep main's catalog row where both added the same label Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs/gaps.md: the Tilt-Shift and Spin paragraph replaces the Field/Iris one --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> 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 <noreply@anthropic.com> 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 <noreply@anthropic.com> 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 <noreply@anthropic.com> 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 <noreply@anthropic.com> 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
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> |
||
|
|
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 <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> * 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> --------- 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> |
||
|
|
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 <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> |
||
|
|
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 <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> * 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> * Empty commit to resync the pull request head --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
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) <noreply@anthropic.com> * Batch: translate Include All Subfolders for Hungarian and Turkish Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <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> |
||
|
|
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 <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> |
||
|
|
93a16218de |
Gradient Map is driven by the W3C luminosity, as in Photoshop (#5018)
Photoshop's Gradient Map picks each pixel's place on the gradient by 0.3 R + 0.59 G + 0.11 B, the luminosity of the blend modes. We used the Rec. 601 grey (0.299, 0.587, 0.114). That is a few thousandths off, but it is enough to land two levels away on steep stretches of a gradient. The photoshop corpus's rgb8 and rgb16 gradient-map.psd now pass (they were 2 levels off on 0.8-1.7% of pixels). Sampling the gradient through a 256- or 1024-entry table didn't help; the weights did. CPU (compose::adjust) and GPU (the shader's lum) both change. Lab documents keep their Gradient Map driven by L, and grey pixels map as before. Photoshop oracles 145 -> 147 of 258; floor 145 -> 147. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
d11be6de78 |
docs: drop stale missing-tool rows, only Art History Brush is missing (#5063)
Task T-0002 del lab artcraft-lab. File toccati: docs/brush-parity.md | 5 +++-- docs/gaps.md | 6 +++--- docs/target-app-parity.md | 5 +++-- docs/ui-parity.md | 7 ++++--- 4 files changed, 13 insertions(+), 10 deletions(-) Evidenze: artifacts/T-0002/ (nel lab, non in questo repo). |
||
|
|
be7a1f80fc |
Blur Gallery: Field Blur pins on the image (#3373) (#3801)
* 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> 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 <noreply@anthropic.com> 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 <noreply@anthropic.com> 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 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
01b354a340 |
Add filename templates to Batch export (#4466)
* Add filename templates to Batch export * Keep Batch naming evidence out of the shared gap-summary header * Avoid catalog append conflicts for Batch naming labels * Keep Batch naming labels clear of catalog append conflicts * Keep Batch naming details in the automation parity row * Separate Batch documentation from evolving Actions details --------- Co-authored-by: jonathnwxh <258340642+jonathanwxh-cell@users.noreply.github.com> |
||
|
|
f43d019a72 |
Brush Settings: use an amount slider for Hue Jitter (#4874)
Use the shared percentage row so the jitter amount does not look like a hue selection. Cover the rendered themed track, numeric and pointer edits, neighboring settings and engine journal replay. Addresses the Hue Jitter appearance item in #3825; other assessment items remain open. Co-authored-by: augrclk <217665348+augrclk@users.noreply.github.com> Co-authored-by: GPT-6 <noreply@openai.com> |
||
|
|
c7a34b04ec |
Scorecard: Smart Object corner skew and perspective is done (FILE-215-6, #426) (#5094)
#426 gave smart objects a projective placement (`SmartObject.perspective`, with `transform` as its affine approximation): Distort and Perspective set it, the layer re-renders from its source through the homography, Warp composes with it, and the PSD placement reads and writes `nonAffineTransform` / `Trnf`. The scorecard row still said "missing" with the pre-#426 note, as did the ui-parity row and the #579 mention in gaps.md. |
||
|
|
36112e35d4 |
Brush Settings: disable minimum inputs when Control is Off (#4775)
Keep configured minimum values across Control changes and cover disabled input, independent controls and reactivation with UI regressions. Refs #3825 (minimum input state only). Co-authored-by: augrclk <217665348+augrclk@users.noreply.github.com> Co-authored-by: GPT-6 <noreply@openai.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> |
||
|
|
3a307a757e |
Play real Photoshop action sets: TrueGrit .atn mappings and fixes (#4356, #219) (#4698)
Third-party action sets that load since
|
||
|
|
4fd08b5395 |
Free Transform: H and V skew fields in the options bar (#4699)
* Free Transform: H and V skew fields in the options bar Photoshop's Free Transform options bar has H and V skew fields after the angle; ours stopped at the angle (ui-parity: "Options bar H/V skew fields | missing", gaps G5). The bar now reads the box as Photoshop does: the frame scaled (W, H), skewed (H, V) and rotated about the reference point (`Parts`). Typing a skew leans the box about the reference point and keeps W, H and the angle. A box can be read with any V skew, so the session keeps the typed one (`TransformSession::skew_v`, serde default). Skews stop at ±89°. A sheared box used to read as taller (H = 100/cos h %); it now reads as an H skew with H at 100 %. Readouts without skew are unchanged, flips included. Docs: ui-parity row done, G5 and target-app-parity notes updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * i18n(sr): translate the Free Transform skew field tooltips 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> |
||
|
|
575f8eb2bc |
Allow toolbar tool shortcuts to be remapped (#4194)
Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0002d861ee |
fix(io): read CS2 placed layers that carry only a PlLd block (#2465) (#4268)
Photoshop CS2 predates SoLd: its smart objects have a PlLd (plcL) block alone, with the embedded file in a global lnkD block. Import only read SoLd/SoLE, so such layers got an empty source id and Edit Contents said "the smart object's contents are unavailable". Parse PlLd from the Adobe PSD spec (unique id and the 8 corner doubles). PlLd stores no source size, so the transform takes it from the embedded file's header (linked::source_size_probe: PSD, TIFF, PNG and JPEG headers, any other image decoded flat by photocraft-codecs). It never imports the file as a document: a layered TIFF can carry PSD layer data whose lnkD holds another file, so that would nest without bound. Without the file, 100 % scale is assumed. source_geometry moves to linked.rs. Non-parallelogram corners give the projective placement as for SoLd. An unreadable PlLd is reported as a warning. The PlLd corners are the same placed quad as SoLd's Trnf, so they frame the same source-space box (#3677, blocks::placed_frame): the bounding box of a single-patch custom warp's 16 control points, which may spill past the source, else the source rect. corners_affine now maps such a frame onto the corners and is shared by parse_smart and plld_transform, which is the inverse of smart_map::quad_points (what plld_bytes writes): affine corners frame placed_frame(warp), projective ones the source rect. A custom warp's box needs no source size, so a spilled mesh is placed right even without the embedded file. corners_affine refuses non-finite corners or an empty frame (identity, as before). CS2 can warp a smart object, and PlLd keeps that warp after the corners (u32 warp version, then a versioned warp descriptor). It is read with the same code as the SoLd warp (warp_of), so a re-render from the now available source (Free Transform, Image Size, mode changes) stays warped. A stored warp that can't be read leaves the contents unavailable, keeping the warped pixels, with a warning. An edited PlLd-only layer (moved, transformed, warped, filtered or given other contents) is saved like any other smart object, SoLd plus a regenerated PlLd, with its current placement; an unedited one keeps its record verbatim, as does one whose file isn't embedded (its id is never read as a disk path). plld_edited compares against plld_transform with the record's own warp, so it uses the same frame as import and as the writer. A layer with both SoLE and PlLd is read from SoLE. Tests: PlLd round trip and malformed input (truncation, bad magic, oversized Pascal length, NaN) in blocks.rs; PlLd warps round trip through plld_bytes (preset, custom mesh, identity, none) and unreadable warps are reported as such; a PlLd whose custom mesh spills past the source places like the equivalent SoLd, with and without the source size; PlLd-only layers with a missing file, a broken record, a warp or an unreadable warp; a spilled-mesh PlLd-only layer imports with the SoLd layer's placement and an unedited one saves its PlLd verbatim (no SoLd) and reopens in place; and engine tests that rewrite an export into the CS2 layout (no SoLd, lnkD with version-2 liFD items) and check transform, contents, re-render, Edit Contents, and that a warped smart object re-renders warped. source_size_probe has header tests for each format and malformed JPEG marker walks. Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
96f4d68db7 |
feat(ui): spring-loaded tool letters (#2672) (#4238)
In Photoshop, holding a tool's letter gives a temporary tool: releasing the key returns to the tool you had. A tool letter still switches tools when pressed; if the key then stays down for 300 ms or more, or the canvas is used while it's down, the switch was temporary and the release gives the previous tool back with its options. A quick tap stays a permanent switch. - Shift+letter (cycling the group) and Alt+letter never spring; nor do letters while typing in a field, editing type or under a dialog. - The current group's own letter cycles the group, as before. - A drag in progress when the key comes up finishes with the temporary tool; the previous tool returns once the button is up. - The newest held letter wins; the original tool returns when all are up. - A tool picked another way while a letter is down (the toolbar, a menu, automation) stands. - Key repeats of tool letters are dropped instead of cycling the group. - Z is one case: hold Z to zoom, add Alt to zoom out. The state is UiState::spring, readable over the control channel and never restored from settings. Tests that pressed tool letters without releasing them now release them, as a keyboard does. docs/ui-parity.md and docs/gaps.md mark #2672 done. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
af9316b97e |
Docs: refresh gaps.md against main (#4412) (#4413)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
f48a72cbe3 |
Codecs: open and save DPX frames (#1049) (#4104)
Read SMPTE 268M DPX in both byte orders (SDPX, XPDS): the first image element, descriptor 6 (luma), 50 (RGB) or 51 (RGBA), at 8, 10, 12 or 16 bits, with 10-bit packing 0, 1 (method A) or 2 (method B) and 12-bit packing 0 or 1. 10- and 12-bit code values open as U16 round(c * 65535 / (2^b - 1)) and come back exactly when written at their depth. No colour transform: the transfer characteristic and colorimetric specification are reported as import notes; extra image elements are reported; the orientation is applied on import. Write big-endian, one unencoded element, at 8, 10, 12 or 16 bits (EncodeOptions::dpx_bits; `dpxBits` on the save commands, MCP and RPC; `--dpx-bits` in the CLI), following the document depth by default. Gray with alpha is written as luma. Creator, project, copyright, creation time, transfer, colorimetric, frame position, frame rate and time code travel as DPX:* text metadata and are written back. Unsupported descriptors, signed or run-length-encoded data and other bit depths return CodecError::Unsupported; malformed and truncated files return errors. Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
61c52c08db |
Brush Settings: toggle switch rows without losing smoothing settings (#4463)
Noise, Wet Edges, Build-up, Smoothing and Protect Texture toggle from anywhere on their row while keeping the selected controls open. Keep lock clicks independent and expose each switch as one accessible checkbox. Give Smoothing a default-on enable flag so disabling it preserves the amount and modes. Old brush JSON stays compatible; toolbar amount edits and explicit stroke overrides enable it, and ABR imports retain disabled smoothing values. Cover row, checkbox, lock and keyboard gestures, old section selections, command replay, presets, legacy JSON and 8/16/32-bit rendering. Fixes #4283 Co-authored-by: augrclk <217665348+augrclk@users.noreply.github.com> Co-authored-by: GPT-6 <noreply@openai.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
90c42f5555 |
Map Place, Open, Image Size in percent, the Paint Bucket and Insert Conditional (#219) (#4470)
- image.imageSize takes percent (both sides; an explicit side wins).
- actions.conditional {if, then, else}: Photoshop's Insert Conditional as a step, 33 conditions
on the document (shape, mode, depth, selection, layers, alpha channels, unsaved) and the
active layer (kind, locks, visibility, masks, effects); a failing branch stops the caller.
- .atn events: placeEvent → file.placeEmbedded/placeLinked (path, scale), open → file.openAs,
imageSize in percent, fill from a point → paint.bucket (pixels), conditional. 105 → 108.
- Panel labels conditional steps as 'If cond → then / else'.
|
||
|
|
13f5c104f6 |
Map the remaining frequent Photoshop action events (#219) (#4465)
- edit.transform gains the relative form (scaleX/scaleY %, rotate, dx/dy about the frame centre), as a Photoshop action records a Free Transform; image.applyImage takes a source layer by name. - .atn events: transform (layer → translate or relative transform; selection → transformSelection), applyImageEvent, export (Save for Web, with the txtu path record decoded and written back; kept verbatim without a destination), newPlacedLayer / placedLayerEditContents / updatePlacedLayer, close with the Save answer → save + close, delete of the target channel → layer mask delete, Quick Mask on/off, polygon selections → select.lasso, Gradient Map (direct and as an adjustment layer). 92 → 105 events. - Export tries every event listing a command id (Quick Mask on → set, off → clearEvent). - Docs: event counts in the parity docs. |
||
|
|
9862dfbb09 |
Actions panel: set folders, dialog toggle, Insert Stop (#219) (#4456)
* Actions panel: set folders, dialog toggle column, Insert Stop (#219) - Engine actions_edit_cmds: actions.insert (any command at a position; Insert Stop… uses it with actions.stopMessage), actions.dialog (the per-step dialog flag), actions.assignSet. Flag sets follow inserted steps; nothing edits during playback. - Panel: loose actions first, then each set as a collapsible folder row with its count (ui.actions.collapsedSets); dropping an action among another set's rows moves it there; Save Set… on a folder's context menu; a dialog toggle column next to the check marks; Insert Stop… in the panel menu with a small message/Allow Continue dialog. - Shell playback pauses on a dialog-toggled filter or adjustment step: the command's dialog opens seeded with the recorded values; OK runs the dialog's values and resumes from the next step, Cancel stops. Agents play headlessly. The play result reports paused: {step, id, dialog}. - Docs: control protocol, scorecard AUTO-219-2 done, gaps G11. * Translate the Actions panel's set, dialog-toggle and Insert Stop strings * Resume a paused action only after the dialog's background job lands A filter confirmed from a dialog runs as a background job in the desktop app; the resumed steps met a locked document and failed. Wait for the job first (test covers background jobs). |
||
|
|
3204231d7d |
Load and save Photoshop Actions (.atn) (#4356, #219) (#4434)
* Load and save Photoshop Actions (.atn) (#219) - photocraft-psd: `atn` parser and writer for version-16 action sets, exact round trip, truncation/garbage safe, fuzz target and `atn_dump` example. Descriptor fix: `indx` and `Idnt` reference items carry a class before the number (real action files failed to parse). - photocraft-engine: `atn_cmds` maps 92 Photoshop events to commands and back (filters, adjustments per channel, image/canvas/crop/trim/rotate/flip/mode, make/set/select/delete/ hide/show/move/duplicate, fill/stroke, selection, clipboard, purge, profiles, play, stop). Other steps load as `actions.photoshopStep` (descriptor kept, written back by save, fails at playback by name); tool selections load switched off. Commands `actions.load`, `actions.save`, `actions.stopMessage`, `actions.photoshopStep`. `Action` gains `dialogs` and `set`. `layer` params accept a layer name, so loaded steps replay in any document. - UI: Actions panel menu › Load Actions… / Save Actions…; Photoshop steps labelled. - CLI: `batch --actions set.atn`. Automation policy covers the new path params. - Docs: file-format parity (.atn R/W), gaps G11, target-app parity, scorecard AUTO-219-2 partial, development guide. * Translate the Actions panel's Load/Save strings in every catalog The eight new panel strings (Load/Save Actions…, the loaded/saved counts, the kept-step label) were missing from all 19 catalogs; the languages that were complete are back at 100%. * Show the Actions entries in the default dock's panel menu The legacy dock renders its own group menu and only knew the Layers and Swatches entries, so Load Actions… and Save Actions… were missing there. |
||
|
|
4736a4bcbe |
Character panel: OpenType feature buttons (TYPE-218-5, #3752) (#3757)
The Character panel (and the Properties Character section) gains Photoshop's row of eight OpenType buttons: Standard Ligatures, Contextual Alternates, Discretionary Ligatures, Swash, Stylistic Alternates, Titling Alternates, Ordinals and Fractions. Each shows pressed while its feature is on at the selection (else the layer) and toggles it through the existing type.openType.* command, on the same scope as the other Character controls (the selected characters while typing, else every selected type layer). Scorecard TYPE-218-5 stays partial: the Numerals and Stylistic Sets submenus are still missing. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
7fd53f0e3f |
Puppet Warp: the mesh follows the object outline (#3105) (#3117)
The mesh was every square grid cell touching the layer's opaque pixels: a staircase up to a cell outside the shape, and shapes closer than about a cell shared cells, so pinning one dragged the other along. build_mesh now traces the outline of the opaque region grown (or shrunk) by Expansion from a signed distance field, simplifies it (Douglas-Peucker, corners kept), splits its sides to the Density spacing, fills the inside with a triangular lattice and triangulates the points (Bowyer-Watson Delaunay, outline sides recovered by midpoint splits), keeping the triangles inside the outline. Separate shapes get separate meshes, so an unpinned one stays put, as in Photoshop. Vertices are numbered in reverse Cuthill-McKee order to keep the solver's banded factor narrow, and the density's spacing counts the grown region so a large Expansion on a small layer no longer builds tens of thousands of vertices. Scorecard TOOL-214-6 done; docs/gaps.md G7 updated. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
aa1febaffb |
Keep fitted Camera Raw colour during tuning and crop Nikon D5100 borders (#3201)
* raw: fit non-DNG colour to the camera's embedded JPEG * raw: preserve fitted Camera Raw look and crop D5100 borders --------- Co-authored-by: Gert Pellin <gert@pellin.be> Co-authored-by: optimalbytee <optimalbytee@users.noreply.github.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
b26256faee |
Type: triple-click selects a line, quadruple a paragraph, five clicks all text (#3493) (#3496)
While editing type, a run of clicks at one spot now selects like Photoshop: two clicks the word, three the laid-out line (a column in vertical type), four the paragraph up to its hard return, five all text; the next quick click starts over with a caret. egui stops counting at three, so the Type tool keeps its own count per layer (next_click_count) and the ranges come from a pure helper over the text and its layout (click_selection). Scorecard TYPE-218-8 is done; docs/gaps.md G8 updated. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
72bf3e13b6 |
i18n(cs): translate the dynamic labels the Czech catalog was missing (#3454)
* i18n(cs): translate dynamic labels the literal scanner can't see The Czech catalog covered every enforced key (menus, tl! literals, blend modes, preferences) but not the labels built at run time: filter and Filter Gallery parameter names and choices humanized from their keys, Camera Raw controls, the Flame / Picture Frame / Tree render filters, measurement columns, file info fields and the macOS application menu. Those showed in English under Czech. Adds 853 entries (2,441 -> 3,294) for keys that the other catalogs already translate and that still exist in the code. Existing entries are unchanged. Translated clean-room from the plain meaning of the English labels, with the terminology already used in cs.tsv; no proprietary localisation was consulted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * i18n(cs): credit the Czech catalog in ATTRIBUTION.md and LICENSE-translations.txt cs.tsv is a bundled translation asset but had no row in ATTRIBUTION.md and was not named in LICENSE-translations.txt. Add both, worded like the other catalogs (PhotoCraft contributors, original translations, MIT OR Apache-2.0). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * i18n(cs): terminology review fixes for the new dynamic labels Changes from an independent review pass over the added block: - "Create By" (De-Interlace: how the missing lines are made) reads "Vytvořit pomocí", not "Vytvořil" (author) - luminance is "jas" in Camera Raw, as in the existing "Luminance" label ("Detail jasu", "HSL – jas", "Jas") - slider labels are nouns, like the existing "Redukce šumu" (Reduce Color Noise, Sharpen Details, Lighten Grout) - grain and flame choices agree in gender with their neighbours; "Shadow" matches "Highlight"/"Světla" as "Stíny"; "At" is "Pozice"; "Water Paper" is "Mokrý papír"; "Leaves" is "listů" everywhere Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * contributors: add my own people.toml entry Own entry only, as AGENTS.md describes: credited by the name on my public GitHub profile (sync_github_name), otherwise by @username. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * i18n(cs): drop three rows that main added in the meantime "Columns", "Margin" and "Automatic" were added to cs.tsv on main while this branch was open, with the same translations, so the appended block no longer needs them (the catalog has no duplicate keys). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * i18n (cs): translate two strings that are still missing from cs.tsv "Drag (lasso) or click points (polygonal) ..." and "The current gradient (pick one in Window > Gradients)" exist in the other catalogs but not yet in cs.tsv on main. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * i18n (cs): translate context-menu, channel and layer-style labels These labels are rendered through tl!() with a variable, so the literal-based coverage test does not see them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Localize Czech status messages and notice wrappers Translate shell messages before interpolating file names and engine errors. Use the existing plural API for counted objects and preserve English fallback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Localize Czech UI labels and hardware information Translate brush controls, accessibility names and rasterization prompts at render sites. Preserve adapter, driver and display/profile identifiers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Localize Czech composed labels and distinguish Navigator zoom Translate profile, preset and shortcut messages before interpolation, and use a Navigator context for the zoom level. Keep shortcut warning state separate from translated text and preserve document names and shortcut codes. Cover remaining visible counts and add rendering/glyph regression tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fix Czech UI localization review follow-ups Keep copied System Info and API diagnostics in English while localizing the on-screen text. Avoid double translation of accessible names, honor command-id shortcut labels, and translate stored auto backend values only for display. Refine Czech warning punctuation and catalog wording, simplify template blocks, and correct the documented catalog count to 3,522 rows. Add four regression tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * people.toml: keep axlecoffee's sync_github_name with their entry Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Losminek <301412321+Losminek@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
28874bbfad |
Select and Mask: Output To New Document, with or without a layer mask (#2998) (#3007)
* Select and Mask: Output To New Document, with or without a layer mask (#2998) select.refineEdge takes output newDocument / newDocumentWithMask: a new document like the source (size, mode, depth, resolution, profile) with the active layer cut out by the refined edge, or with the matte as a layer mask. The source document is left untouched. The dialog's Output choice lists all six targets, translated in every catalog. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * i18n: drop the duplicate pt-br New Layer row (main already has it) 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> |
||
|
|
c1bd296dcf |
Select and Mask: Refine Edge Brush (#2972) (#2989)
* Select and Mask: live preview in Photoshop's view modes (#1754) The Select and Mask dialog had no preview of the refined selection. It now previews live in Photoshop's view modes: Onion Skin (default), Marching Ants, Overlay, On Black, On White, Black & White and On Layers, with a per-view Opacity, Show Edge (J) and Show Original (P). F cycles the views, O/M/V/A/T/K/Y pick one and holding X shows the image without a view. The view state lives in the dialog's fields, so ui.dialog.set drives it. The preview runs select.refineEdge on the proxy with output=selection and builds the view from it (veil fill layers, a layer mask, or the refined selection, whose marching ants the canvas draws scaled back up), through the existing filter-preview worker. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Select and Mask: Refine Edge Brush (#2972) Select and Mask had no Refine Edge Brush. select.refineEdge now takes refineStrokes: [{"points":[[x,y],...],"size":px,"mode":"add|subtract"}], applied in painting order. Added areas become unknown and take a colour estimate: each pixel's colour projected on the line between the mean known foreground and background colours around it (normalised convolution over a box of the brush size, four times as far where none are near), so hair or fur beyond the edge-detection band joins the selection. Subtracted areas keep the original edge. Malformed or oversized strokes are rejected (stroke / point / swept-area limits), never a panic. The dialog gets the brush: Refine Edge Brush (R), Mode (Expand Detection Area / Restore Original Edge, Alt swaps), Size ([ and ]) and Clear Brush Strokes. With it on, dragging on the canvas paints strokes into the dialog's refineStrokes field, so the live preview, Show Edge and OK use them, and ui.dialog.set / agents drive them like any other param. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3360f6e442 |
Speed up Tilt-Shift with existing per-level halo trimming (#2880)
Co-authored-by: optimalbytee <optimalbytee@users.noreply.github.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
5025fd2895 |
Color Sampler tool (#1046) (#2753)
* Color Sampler tool (#1046) Add the Color Sampler to the Eyedropper (I) flyout: click to pin numbered sample points to canvas pixels, drag to move, Alt-click (or drag off-canvas) to remove, Clear All in the options bar, at most 10 per document. Each point reads one composite pixel (RGB 0-255 and naive device CMYK, same as the pointer readout) and the Info panel lists them under their number with the position in the ruler unit; the readouts refresh when the document changes. Points are per-document engine view state (DocState.color_samplers): they belong to their document, are discarded on close, and are not saved to .pcraft or PSD. They are driven by commands shared by the UI, CLI, control channel and MCP (view.colorSamplers.add/.move/.delete/.clear/.list); the mutations change no pixels and add no History step. Points follow the canvas geometry (crop, Canvas Size, Image Size, Image rotation/flip, rotated crop) through a pending Session.sampler_map that Session::edit applies before culling the points that land outside the new canvas; the count removed is reported in the status bar. - engine: new sampler_cmds.rs (commands + shared readout helpers) and the sampler_map wiring in lib.rs, image_cmds.rs, mode_cmds.rs, commands.rs, analysis_cmds.rs (compound rolls points back on error). - ui-egui: Tool::ColorSampler + original sampler.svg icon, toolbar slot, Info panel list, options bar, pointer handling, overlay markers, cursor. - docs/scorecard.md regenerated (TOOL-1649-1 no longer says the tool is missing). No menu item, so parity is unchanged (627/627). * Color Sampler follow-up: tool count, README, i18n, roadmap The full ui-egui suite (not just the filtered tests) surfaced these gaps: - state.rs: Tool::ALL.len() assertion 52 -> 53. - README.md: "52 tools" -> "53 tools" and add Color Sampler to the list, so readme_tool_list_matches_tool_all passes. - i18n: the options-bar hint, "{n} of {max} sample points" and "No document open" need entries in every catalog that claims complete menus (every_tl_literal_is_translated); added to all 16 (coverage 1584/1584). The translations were drafted alongside the tool. - docs/roadmap.md: drop Color Sampler from the missing-tools list and note it landed. * Color Sampler: fit main's new tools and Image Size resample type Reset Tool covers ColorSampler, the test ViewXform sets aspect, and Image Size checks Method::None instead of an Option. 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> |
||
|
|
d8d8d9f3d2 |
Tools: Convert Point tool in the Pen flyout (#3204) (#3211)
Convert Point is a tool of its own, built on the existing path.convertPoint edit that Pen + Alt already drives. A click makes a smooth anchor a corner, a drag from an anchor pulls out smooth handles, a drag on a handle moves it alone. The tool selects the anchor it converts so its handles show; a press on empty canvas or a segment does nothing, and Cmd/Ctrl is Direct Selection, as with the Pen. It shares the Pen's key (P). Original glyph with an ATTRIBUTION.md row; tool name in all catalogs; README tool list and the G6 / ui-parity / roadmap counts updated (tools 56 of 68, the earlier 53 was stale). Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0aaf5b60fe |
Select and Mask: live preview in Photoshop's view modes (#1754) (#2970)
The Select and Mask dialog had no preview of the refined selection. It now previews live in Photoshop's view modes: Onion Skin (default), Marching Ants, Overlay, On Black, On White, Black & White and On Layers, with a per-view Opacity, Show Edge (J) and Show Original (P). F cycles the views, O/M/V/A/T/K/Y pick one and holding X shows the image without a view. The view state lives in the dialog's fields, so ui.dialog.set drives it. The preview runs select.refineEdge on the proxy with output=selection and builds the view from it (veil fill layers, a layer mask, or the refined selection, whose marching ants the canvas draws scaled back up), through the existing filter-preview worker. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
42e4af6416 |
Tools: Frame tool (#2914) (#2915)
The Frame tool (K, its own toolbar slot after the Crop group) draws a rectangular or elliptical frame; the options bar picks the shape, Shift constrains to a square or circle and Alt draws from the centre. A frame drawn over the selected pixel layer's content takes that layer in; otherwise it is an empty frame. Frames are built as Frame from Layers builds them: a group clipped by a layer mask (rectangular or anti-aliased elliptical). The new engine command layer.new.frame does it in one Session::edit, so one undo step. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
017d1aea1c |
Tools: Perspective Crop (#1043) (#2864)
New Perspective Crop tool in the Crop flyout (C): drag a box or click four
corners, drag corners independently onto a skewed object, perspective grid
(Show Grid), Enter commits, Esc cancels. W/H/Resolution come from the Crop
tool's fields (Front Image, Clear), or the quad's mean side lengths.
Engine: image.perspectiveCrop {corners, width?, height?, resolution?,
interpolation?} maps the quad onto an upright canvas through the
quad-to-rectangle homography (Heckbert's square-to-quad map, inverted):
every raster layer, mask, alpha channel and the Quick Mask, one undo step.
The Background stays opaque over the background colour; type, shape, fill
and smart object layers are rasterized first; vector geometry follows the
nearest affine map. Non-convex, self-crossing, counter-clockwise or
degenerate quads are refused with an error.
algo: warp_surface_clipped bounds the per-tile warp by the new canvas even
when a layer reaches past the map's horizon; convex_quad, quad_size,
quad_to_rect.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
2e0c426692 |
Tools: Artboard tool (#2858) (#2862)
Photoshop's Artboard tool in the Move flyout (V, ⇧V cycles Move ↔ Artboard): dragging on an empty spot draws a new board (`layer.new.artboard`), clicking a board selects it, dragging it moves it with its contents, and the selected board's edge and corner handles resize it (`layer.artboard.set`, moveContents: false). Each gesture is one undo step. Options bar: Size preset, W/H, Make Portrait, Make Landscape, Add New Artboard. Original toolbar glyph (assets/icons/artboard.svg, ATTRIBUTION.md row). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
5866cd2cf6 |
Tools: Horizontal and Vertical Type Mask (#1055) (#2861)
The Type flyout (T, Shift+T cycle) gains Vertical Type Mask and Horizontal Type Mask, in Photoshop's order. They type like the Type tools, with the canvas under Quick Mask's colour and the glyphs clear while editing. On commit the glyph coverage replaces the selection; no layer is left behind and the whole session is one "Type Mask" history step. Escape always cancels and leaves the selection unchanged. The commit runs a new engine command, type.createSelection: a type layer's glyphs (optionally removing that layer, the tools' hidden working text) or type.create's keys directly, with new/add/subtract/intersect and an anti-aliasing override. type.create's layer building is shared through type_cmds::new_text_layer. Original dashed-T icons (ATTRIBUTION.md), tool label translations in every catalog, README tool list and the gaps/parity docs updated. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
c436aa7294 |
Tools: Freeform Pen and Curvature Pen (#1054) (#2856)
The Pen flyout (P / Shift+P) now holds Pen, Freeform Pen and Curvature Pen. Freeform Pen: a drag draws freehand; on release the trace is fitted with cubic Beziers within the options bar's Curve Fit (default 2 px) by photocraft_vector::trace::fit_trace (Schneider, "An Algorithm for Automatically Fitting Digitized Curves", Graphics Gems 1990, the fitting Make Work Path already uses), keeping turns sharper than 60 degrees as corners. Releasing on the start point closes the path. Curvature Pen: each click adds a smooth point and the path runs through every point (Catmull-Rom style handles from photocraft_vector::edit::curvature_knots); dragging a point moves it, double-clicking toggles corner/smooth, clicking the first point closes, Enter/Esc finish and Backspace removes the last point, as with the Pen. Both commit through the Pen's path: a shape layer in Shape mode, else the targeted vector mask, else the work path, as one History step. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |