1717
1818package pixelitor .filters ;
1919
20+ import com .jhlabs .image .ImageMath ;
2021import com .jhlabs .image .PointFilter ;
22+ import pixelitor .filters .gui .IntChoiceParam ;
23+ import pixelitor .filters .gui .IntChoiceParam .Item ;
2124import pixelitor .filters .gui .RangeParam ;
25+ import pixelitor .utils .ColorSpaces ;
2226
2327import java .awt .Color ;
2428import java .awt .image .BufferedImage ;
2933import static pixelitor .gui .GUIText .SATURATION ;
3034
3135/**
32- * Hue-Saturation filter
36+ * The Hue-Saturation filter.
3337 */
3438public class HueSat extends ParametrizedFilter {
3539 public static final String NAME = HUE + "/" + SATURATION ;
@@ -49,6 +53,13 @@ public class HueSat extends ParametrizedFilter {
4953 private static final int MAX_BRI = 100 ;
5054 private static final int DEFAULT_BRI = 0 ;
5155
56+ private static final int COLOR_SPACE_HSV = 0 ;
57+ private static final int COLOR_SPACE_OKLCH = 1 ;
58+
59+ private final IntChoiceParam colorSpace = new IntChoiceParam ("Color Space" , new Item []{
60+ new Item ("HSV (Faster)" , COLOR_SPACE_HSV ),
61+ new Item ("Oklch (Better)" , COLOR_SPACE_OKLCH ),
62+ });
5263 private final RangeParam hue = new RangeParam (HUE , MIN_HUE , DEFAULT_HUE , MAX_HUE );
5364 private final RangeParam saturation = new RangeParam (SATURATION , MIN_SAT , DEFAULT_SAT , MAX_SAT );
5465 private final RangeParam brightness = new RangeParam (BRIGHTNESS , MIN_BRI , DEFAULT_BRI , MAX_BRI );
@@ -61,6 +72,7 @@ public HueSat() {
6172 brightness .setPresetKey ("Brightness" );
6273
6374 initParams (
75+ colorSpace ,
6476 hue ,
6577 saturation ,
6678 brightness
@@ -69,29 +81,91 @@ public HueSat() {
6981
7082 @ Override
7183 public BufferedImage transform (BufferedImage src , BufferedImage dest ) {
72- int hueP = hue .getValue ();
73- int satP = saturation .getValue ();
74- int briP = brightness .getValue ();
75-
76- if (hueP == 0 && satP == 0 && briP == 0 ) {
84+ if (hue .isZero () && saturation .isZero () && brightness .isZero ()) {
7785 return src ;
7886 }
7987
88+ if (colorSpace .valueIs (COLOR_SPACE_OKLCH )) {
89+ float hueShift = hue .getValueAsFloat ();
90+ // satFactor is a multiplier, e.g., 1.5 for a 50% increase
91+ float satFactor = 1.0f + (float ) saturation .getPercentage ();
92+ float briShift = (float ) brightness .getPercentage ();
93+
94+ dest = new OklchImpl (hueShift , satFactor , briShift ).filter (src , dest );
95+ return dest ;
96+ }
97+
98+ // HSV color space
8099 float satShift = (float ) saturation .getPercentage ();
81100 float briShift = (float ) brightness .getPercentage ();
82101 float hueRot = hue .getValueAsFloat () / 360.0f ;
83102
84- dest = new Impl (hueRot , satShift , briShift ).filter (src , dest );
103+ dest = new HsvImpl (hueRot , satShift , briShift ).filter (src , dest );
85104
86105 return dest ;
87106 }
88107
89- private static class Impl extends PointFilter {
108+ /**
109+ * An implementation of the filter that works in the Oklch color space.
110+ */
111+ private static class OklchImpl extends PointFilter {
112+ private final float hueShift ;
113+ private final float satFactor ;
114+ private final float briShift ;
115+
116+ protected OklchImpl (float hueShift , float satFactor , float briShift ) {
117+ super (NAME );
118+ this .hueShift = hueShift ;
119+ this .satFactor = satFactor ;
120+ this .briShift = briShift ;
121+ }
122+
123+ @ Override
124+ public int processPixel (int x , int y , int rgb ) {
125+ int a = rgb & 0xFF_00_00_00 ;
126+
127+ // for the multithreaded performance it's better to
128+ // create this array here instead of reusing it as a class field
129+ float [] oklch = ColorSpaces .srgbToOklch (rgb );
130+
131+ // L is in [0, 1], C is >= 0, h is in [0, 360)
132+ float l = oklch [0 ];
133+ float c = oklch [1 ];
134+ float h = oklch [2 ];
135+
136+ // apply adjustments
137+ h += hueShift ;
138+ // normalize hue to be in the range [0, 360)
139+ if (h < 0.0f ) {
140+ h += 360.0f ;
141+ }
142+ if (h >= 360.0f ) {
143+ h -= 360.0f ;
144+ }
145+
146+ c *= satFactor ;
147+ // chroma can't be negative
148+ c = Math .max (0.0f , c );
149+
150+ l += briShift ;
151+ // clamp lightness to [0, 1]
152+ l = ImageMath .clamp01 (l );
153+
154+ oklch [0 ] = l ;
155+ oklch [1 ] = c ;
156+ oklch [2 ] = h ;
157+
158+ int newRGB = ColorSpaces .oklchToSrgb (oklch );
159+ return a | (newRGB & 0x00_FF_FF_FF );
160+ }
161+ }
162+
163+ private static class HsvImpl extends PointFilter {
90164 private final float hueRot ;
91165 private final float satShift ;
92166 private final float briShift ;
93167
94- protected Impl (float hueRot , float satShift , float briShift ) {
168+ protected HsvImpl (float hueRot , float satShift , float briShift ) {
95169 super (NAME );
96170 this .hueRot = hueRot ;
97171 this .satShift = satShift ;
@@ -111,32 +185,21 @@ public int processPixel(int x, int y, int rgb) {
111185
112186 tmpHSBArray = Color .RGBtoHSB (r , g , b , tmpHSBArray );
113187
114- float shiftedHue = tmpHSBArray [0 ] + hueRot ;
115- float shiftedSat = tmpHSBArray [1 ] + satShift ;
116- float shiftedBri = tmpHSBArray [2 ] + briShift ;
188+ float newHue = tmpHSBArray [0 ] + hueRot ;
189+ float newSat = tmpHSBArray [1 ] + satShift ;
190+ float newBri = tmpHSBArray [2 ] + briShift ;
117191
118- if (shiftedSat < 0.0f ) {
119- shiftedSat = 0.0f ;
120- }
121- if (shiftedSat > 1.0f ) {
122- shiftedSat = 1.0f ;
123- }
124-
125- if (shiftedBri < 0.0f ) {
126- shiftedBri = 0.0f ;
127- }
128- if (shiftedBri > 1.0f ) {
129- shiftedBri = 1.0f ;
130- }
192+ newSat = ImageMath .clamp01 (newSat );
193+ newBri = ImageMath .clamp01 (newBri );
131194
132- if (shiftedHue < 0 && shiftedHue > -0.00000003 ) {
195+ if (newHue < 0 && newHue > -0.00000003 ) {
133196 // workaround for a bug in Color.HSBtoRGB, see issue #87
134- shiftedHue = 0 ;
197+ newHue = 0 ;
135198 }
136199
137- int newRGB = Color .HSBtoRGB (shiftedHue , shiftedSat , shiftedBri ); // alpha is 255 here
200+ int newRGB = Color .HSBtoRGB (newHue , newSat , newBri ); // alpha is 255 here
138201 newRGB &= 0x00_FF_FF_FF ; // set alpha to 0
139202 return a | newRGB ; // add the real alpha
140203 }
141204 }
142- }
205+ }
0 commit comments