The contributor docs offer github.com/storytold/craftrules as the place
to read the font rules and release playbook, but the repository
returns 404 to everyone outside the organization, so the cited text is
unreachable for outside contributors. Keep the sibling-checkout paths
and say plainly that the repo is not public and a maintainer can supply
the rules relevant to a change.
Co-authored-by: Brandon Fenty <[email protected]>
guides_in numbered the document guide list horizontal-first, but
psd_export writes resource 1032 vertical-first, so an artboard with
guides of both orientations was associated with the wrong guides.
Number vertical-first to match the resource.
Co-authored-by: Brandon Fenty <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
* fix(algo): Path Blur clamps per-path speeds (#708)
filter.blurGallery.pathBlur clamps the scalar speed to 0..500, but
each entry of the documented paths array was deserialized without any
clamp. The halo takes the maximum speed and computes
Radius(v.max(0.0).ceil() as i32 + 1): a large finite speed saturates
the float-to-int cast to i32::MAX and the + 1 overflows, so the command
dies in a caught panic (and wraps to a negative halo with overflow
checks off).
Clamp each entry speed to the same 0..500 the scalar path uses, right
where the params are built.
* Path Blur test: drop an unused before-sample (a flat background can't change under blur)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Brandon Fenty <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
filter.cameraRaw deserialized its params verbatim, so an out-of-range
noiseLuminance reached the denoise stage and became a guided-filter
radius of round((1 + 3 * n/100) * pixelScale). A value of 1e9 turned
six box-mean passes into unbounded work: the command never finished.
noiseColor had the same defect through its blur sigma.
raw_params now clamps the four noise fields to the documented 0..100,
matching how the sibling lens commands clamp their ranges at the same
boundary. The in-range path is untouched.
Co-authored-by: Brandon Fenty <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
* fix(photocraft): reject an unparseable --control port instead of ignoring it (#701)
The app read --control and PHOTOCRAFT_CONTROL_PORT with parse().ok(),
so a typo (78787), a missing value, or a following flag consumed as the
value left the app running with no control server and no diagnostic.
Validate both sources, name the offending value, and exit non-zero,
matching photocraft-cli serve --port. A missing --control value no
longer swallows the next argument.
* Exit with status 2 on a bad --control port instead of 0
A launcher or agent could not tell a rejected control port from a normal
quit. The collected port errors now map to exit status 2 (command-line usage
error) through control_args_exit_code, which is unit-tested.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Brandon Fenty <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Mapping::source_rect cast input-derived floats with `as i32` and then
added the bilinear apron. A large `source` (e.g. 3e9) with a non-
translation mapping saturates the cast to i32::MAX and the `+ 2`
overflows: debug panics, release wraps to an inverted rect and samples
the wrong region silently.
Clamp to the i32 range before casting and apply the apron with
saturating arithmetic, matching the sibling Rect::inflate convention
AGENTS.md prescribes for input-derived offsets. Normal-sized sources
produce bit-identical rects.
Adds unit tests: the issue's 3e9 + 45-degree repro stays ordered
(panics on main at clone_source.rs:127), a negative huge source
clamps the same way, and a normal mapping's rect is unchanged.
Co-authored-by: Brandon Fenty <[email protected]>
columns/rows came straight from params with no cap. layout_lines loops
once per requested line and each push_unique scans the growing guide
vector, so a count like 200000000 (well inside u32) never returned -
blocking the app, CLI, MCP and the TCP control channel.
Reject counts above 1000 with an actionable BadParams naming the
offending parameter. Photoshop's own dialog caps at 32; 1000 is a
generous ceiling whose worst case finishes instantly, so legitimate
grids are untouched.
Adds regression tests: the issue repro (200M columns, and rows)
returns an actionable error immediately, and bounded layouts at the
cap still produce their guides.
Co-authored-by: Brandon Fenty <[email protected]>
The field documents "1 = neutral; lower = more contrast", but the tone
map used gamma directly as the powf exponent, so 0.5 lightened
midtones and 2.0 darkened them - the slider ran opposite to its own
contract. The crate's other two gamma controls (hdr.rs, proof_sim.rs)
already use the reciprocal exponent.
Apply 1.0 / gamma like they do. Neutral (1.0) is unchanged; every
other value now moves midtones the documented way.
Adds a regression test on a flat 50% grey: gamma 0.5 darkens the
midtone, gamma 2.0 lightens it (reversed on main: 0.71 / 0.25).
Co-authored-by: Brandon Fenty <[email protected]>
uses_colours() derived from the params notation, but Colored Pencil
(paper tint from the background) and Neon Glow (foreground mixed over
background) read the colours in their pixel code without listing them.
The single-filter commands therefore skipped colour injection and both
filters always ran with the default white/black regardless of the
session colours, while filter.filterGallery honoured them.
Add the two filters to uses_colours() at the source, so the command
injection, the gallery dialog note, and smart-filter re-renders all
agree.
Adds regression tests: filter.gallery.coloredPencil and
filter.gallery.neonGlow output must change with the session
background (both identical on main).
Co-authored-by: Brandon Fenty <[email protected]>
gauss_box_radii followed Kovesi 2010 exactly, and the rounded triple
degenerates below sigma ~0.577: wl floors to 1 and all three radii
become 0, so every box pass early-returned and gauss_blur_n handed the
buffer back untouched for any sigma in [0.3, 0.577). Callers with
documented small sigmas (Camera Raw sharpenRadius 0.5 and colour noise,
HDR Toning radius 1, Shadows/Highlights mask blur, Oil Paint
cleanliness) silently did nothing in that band.
Clamp the degenerate case to one radius-1 pass - the smallest real
blur - which only engages where the ideal box widths are already at
their floor, leaving every sigma the paper handles well unchanged.
Adds the command-level regression test from the issue: cameraRaw
sharpenRadius 0.5 must change pixels (control: radius 3.0).
Co-authored-by: Brandon Fenty <[email protected]>