Skip to content

Normalize the hue after a negative rotation - #118

Open
youdie006 wants to merge 1 commit into
anthonynsimon:masterfrom
youdie006:hue-negative-rotation
Open

Normalize the hue after a negative rotation#118
youdie006 wants to merge 1 commit into
anthonynsimon:masterfrom
youdie006:hue-negative-rotation

Conversation

@youdie006

Copy link
Copy Markdown
Contributor

Hue computed (int(h) + change) % 360. Go's % keeps the sign of the dividend, so a negative change yields a negative hue. HSLToRGB documents h as "range is from 0 to 360 degrees" (util/colormodel.go:52) and its internal hueFn wraps only once (if v < 0 { v++ }), so it absorbs a hue down to -240 and returns wrong channels below that.

-240 is easy to cross inside the documented parameter range — Hue's own doc says change is -360 to 360, so any source hue under 120 degrees reaches it.

The asymmetry is visible at a full rotation, which should be identity in both directions. adjust.Hue on RGB(192,128,64):

change   before          after
  +360   {192 128 64}    {192 128 64}
  -360   {192 128  0}    {192 128 64}
  -330   {192 192  0}    {192 192 64}
  -271   { 66 192 62}    { 66 192 64}
  -270   { 64 192 64}    { 64 192 64}

The boundary is exact: wrong at a raw hue of -241, correct at -240.

Reachable from the CLI too — cmd/adjust.go:90 binds --change with a plain IntVarP and no range check, so bild adjust hue --change -360 in.png out.png returns a changed image.

The precedent in this repo

RGBToHSL (util/colormodel.go:42-44) already normalizes with if h < 0 { h += 360 } immediately after the arithmetic that can go negative, and RGBToHSV (:135-137) does the same. adjust.Hue was the only place producing a hue that skipped it.

Testing

TestHueFullRotation added next to the existing TestHue, with a positive and a negative subtest. TestHue already varies change and already asserts 360 is identity — it stops one short of the negative extreme. Its existing -67 row passes on master because those pixels are saturated; a saturated colour clamps the out-of-range channel back onto the right value and hides this, so the new test uses desaturated samples.

  • On master the negative subtest fails (0X40 expected, 0X0 actual) and positive passes.
  • Mutation-checked both directions, and they fail different subtests: reverting to % 360 fails only negative, while dropping the outer reduction (%360 + 360) fails only positive, since an unreduced hue above 600 overruns hueFn's single wrap on the other side.
  • make build, make test, make race, go vet ./... and gofmt are all clean.

Every asserted channel sits exactly 0.5 from its rounding boundary, so the arm64 CI leg has the maximum possible margin against float drift.

I left CHANGELOG.md alone since this is a fix rather than a feature.


This patch was found and written with Claude Code. The numbers above are verbatim from running it against master and against this branch.

Go's % keeps the sign of the dividend, so Hue produced a negative hue for a
negative change. HSLToRGB documents h as "range is from 0 to 360" and its
hueFn wraps only once, so it absorbs a hue down to -240 and returns wrong
channels below that.

    adjust.Hue(src, -360) on RGB(192,128,64)
    before: {192 128 0 255}
    after:  {192 128 64 255}

RGBToHSL already normalizes with "if h < 0 { h += 360 }" right after the
arithmetic that can go negative, and so does RGBToHSV. Hue was the only
place producing a hue without it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant