diff --git a/.gitignore b/.gitignore index 322456c..0635683 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.iml +/.vscode target/ settings.xml pom.xml.tag diff --git a/README.md b/README.md index b57f512..7123d5c 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,53 @@ If you are using Maven, add the following to your `pom.xml` file: org.hsluv hsluv - 0.2 + 1.0 - -# Documentation -Javadocs: http://www.javadoc.io/doc/org.hsluv/hsluv +# Usage + +The API is designed to avoid heap allocation. The `HSLuv` class defines the following public fields: + +- RGB: `hex:String`, `rgb_r` [0;1], `rgb_g` [0;1], `rgb_r` [0;1] +- CIE XYZ: `xyz_x`, `xyz_y`, `xyz_z` +- CIE LUV: `luv_l`, `luv_u`, `luv_v` +- CIE LUV LCh: `lch_l`, `lch_c`, `lch_h` +- HSLuv: `hsluv_h` [0;360], `hsluv_s` [0;100], `hsluv_l` [0;100] +- HPLuv: `hpluv_h` [0;360], `hpluv_p` [0;100], `hpluv_l` [0;100] + +To convert between color spaces, simply set the properties of the source color space, run the +conversion methods, then read the properties of the target color space. + +Use the following methods to convert to and from RGB: + +- HSLuv: `hsluvToRgb()`, `hsluvToHex()`, `rgbToHsluv()`, `hexToHsluv()` +- HPLuv: `hpluvToRgb()`, `hpluvToHex()`, `rgbToHpluv()`, `hexToHpluv()` + +Use the following methods to do step-by-step conversion: + +- Forward: `hsluvToLch()` (or `hpluvToLch()`), `lchToLuv()`, `luvToXyz()`, `xyzToRgb()`, `rgbToHex()` +- Backward: `hexToRgb()`, `rgbToXyz()`, `xyzToLuv()`, `luvToLch()`, `lchToHsluv()` (or `lchToHpluv()`) + +For advanced usage, we also export the [bounding lines](https://www.hsluv.org/math/) in slope-intercept +format, two for each RGB channel representing the limit of the gamut. + +- R < 0: `r0s`, `r0i` +- R > 1: `r1s`, `r1i` +- G < 0: `g0s`, `g0i` +- G > 1: `g1s`, `g1i` +- B < 0: `b0s`, `b0i` +- B > 1: `b1s`, `b1i` + +Example: + +```java +HsluvColorConverter conv = new HsluvColorConverter(); +conv.hsluv_h = 10; +conv.hsluv_s = 75; +conv.hsluv_l = 65; +conv.hsluvToHex(); +System.out.println(conv.hex); // Will print "#ec7d82" +``` # Testing @@ -24,7 +65,8 @@ Javadocs: http://www.javadoc.io/doc/org.hsluv/hsluv # Deployment -Docs: +Docs: + - https://central.sonatype.org/publish/publish-maven/ - https://central.sonatype.org/publish/requirements/gpg/ @@ -59,4 +101,3 @@ Then run: mvn versions:set -DnewVersion=0.3 # bump version mvn clean deploy -P release ``` - diff --git a/pom.xml b/pom.xml index 2fd412f..f1c00da 100644 --- a/pom.xml +++ b/pom.xml @@ -1,11 +1,12 @@ - + 4.0.0 org.hsluv hsluv jar - 0.3 + 1.0 hsluv Human-friendly HSL @@ -48,12 +49,6 @@ 4.13.2 test - - javax - javaee-api - 8.0.1 - test - org.glassfish jakarta.json @@ -121,8 +116,8 @@ sign - 0xD54740FB - 0xD54740FB + EBFD22439E5C59D664D0CFC750617B51F61187C2 + EBFD22439E5C59D664D0CFC750617B51F61187C2 --pinentry-mode loopback @@ -133,4 +128,4 @@ - + \ No newline at end of file diff --git a/src/main/java/org/hsluv/HUSLColorConverter.java b/src/main/java/org/hsluv/HUSLColorConverter.java deleted file mode 100644 index faca3a6..0000000 --- a/src/main/java/org/hsluv/HUSLColorConverter.java +++ /dev/null @@ -1,415 +0,0 @@ -package org.hsluv; - -import java.util.ArrayList; -import java.util.List; - -public class HUSLColorConverter { - private static double[][] m = new double[][] - { - new double[]{3.240969941904521, -1.537383177570093, -0.498610760293}, - new double[]{-0.96924363628087, 1.87596750150772, 0.041555057407175}, - new double[]{0.055630079696993, -0.20397695888897, 1.056971514242878}, - }; - - private static double[][] minv = new double[][] - { - new double[]{0.41239079926595, 0.35758433938387, 0.18048078840183}, - new double[]{0.21263900587151, 0.71516867876775, 0.072192315360733}, - new double[]{0.019330818715591, 0.11919477979462, 0.95053215224966}, - }; - - private static double refY = 1.0; - - private static double refU = 0.19783000664283; - private static double refV = 0.46831999493879; - - private static double kappa = 903.2962962; - private static double epsilon = 0.0088564516; - - private static List getBounds(double L) { - ArrayList result = new ArrayList(); - - double sub1 = Math.pow(L + 16, 3) / 1560896; - double sub2 = sub1 > epsilon ? sub1 : L / kappa; - - for (int c = 0; c < 3; ++c) { - double m1 = m[c][0]; - double m2 = m[c][1]; - double m3 = m[c][2]; - - for (int t = 0; t < 2; ++t) { - double top1 = (284517 * m1 - 94839 * m3) * sub2; - double top2 = (838422 * m3 + 769860 * m2 + 731718 * m1) * L * sub2 - 769860 * t * L; - double bottom = (632260 * m3 - 126452 * m2) * sub2 + 126452 * t; - - result.add(new double[]{top1 / bottom, top2 / bottom}); - } - } - - return result; - } - - private static double intersectLineLine(double[] lineA, double[] lineB) { - return (lineA[1] - lineB[1]) / (lineB[0] - lineA[0]); - } - - private static double distanceFromPole(double[] point) { - return Math.sqrt(Math.pow(point[0], 2) + Math.pow(point[1], 2)); - } - - private static Length lengthOfRayUntilIntersect(double theta, double[] line) { - double length = line[1] / (Math.sin(theta) - line[0] * Math.cos(theta)); - - return new Length(length); - } - - private static class Length { - final boolean greaterEqualZero; - final double length; - - - private Length(double length) { - this.greaterEqualZero = length >= 0; - this.length = length; - } - } - - private static double maxSafeChromaForL(double L) { - List bounds = getBounds(L); - double min = Double.MAX_VALUE; - - for (int i = 0; i < 2; ++i) { - double m1 = bounds.get(i)[0]; - double b1 = bounds.get(i)[1]; - double[] line = new double[]{m1, b1}; - - double x = intersectLineLine(line, new double[]{-1 / m1, 0}); - double length = distanceFromPole(new double[]{x, b1 + x * m1}); - - min = Math.min(min, length); - } - - return min; - } - - private static double maxChromaForLH(double L, double H) { - double hrad = H / 360 * Math.PI * 2; - - List bounds = getBounds(L); - double min = Double.MAX_VALUE; - - for (double[] bound : bounds) { - Length length = lengthOfRayUntilIntersect(hrad, bound); - if (length.greaterEqualZero) { - min = Math.min(min, length.length); - } - } - - return min; - } - - private static double dotProduct(double[] a, double[] b) { - double sum = 0; - - for (int i = 0; i < a.length; ++i) { - sum += a[i] * b[i]; - } - - return sum; - } - - private static double round(double value, int places) { - double n = Math.pow(10, places); - - return Math.round(value * n) / n; - } - - private static double fromLinear(double c) { - if (c <= 0.0031308) { - return 12.92 * c; - } else { - return 1.055 * Math.pow(c, 1 / 2.4) - 0.055; - } - } - - private static double toLinear(double c) { - if (c > 0.04045) { - return Math.pow((c + 0.055) / (1 + 0.055), 2.4); - } else { - return c / 12.92; - } - } - - private static int[] rgbPrepare(double[] tuple) { - - int[] results = new int[tuple.length]; - - for (int i = 0; i < tuple.length; ++i) { - double chan = tuple[i]; - double rounded = round(chan, 3); - - if (rounded < -0.0001 || rounded > 1.0001) { - throw new IllegalArgumentException("Illegal rgb value: " + rounded); - } - - results[i] = (int) Math.round(rounded * 255); - } - - return results; - } - - public static double[] xyzToRgb(double[] tuple) { - return new double[] - { - fromLinear(dotProduct(m[0], tuple)), - fromLinear(dotProduct(m[1], tuple)), - fromLinear(dotProduct(m[2], tuple)), - }; - } - - public static double[] rgbToXyz(double[] tuple) { - double[] rgbl = new double[] - { - toLinear(tuple[0]), - toLinear(tuple[1]), - toLinear(tuple[2]), - }; - - return new double[] - { - dotProduct(minv[0], rgbl), - dotProduct(minv[1], rgbl), - dotProduct(minv[2], rgbl), - }; - } - - private static double yToL(double Y) { - if (Y <= epsilon) { - return (Y / refY) * kappa; - } else { - return 116 * Math.pow(Y / refY, 1.0 / 3.0) - 16; - } - } - - private static double lToY(double L) { - if (L <= 8) { - return refY * L / kappa; - } else { - return refY * Math.pow((L + 16) / 116, 3); - } - } - - public static double[] xyzToLuv(double[] tuple) { - double X = tuple[0]; - double Y = tuple[1]; - double Z = tuple[2]; - - double varU = (4 * X) / (X + (15 * Y) + (3 * Z)); - double varV = (9 * Y) / (X + (15 * Y) + (3 * Z)); - - double L = yToL(Y); - - if (L == 0) { - return new double[]{0, 0, 0}; - } - - double U = 13 * L * (varU - refU); - double V = 13 * L * (varV - refV); - - return new double[]{L, U, V}; - } - - public static double[] luvToXyz(double[] tuple) { - double L = tuple[0]; - double U = tuple[1]; - double V = tuple[2]; - - if (L == 0) { - return new double[]{0, 0, 0}; - } - - double varU = U / (13 * L) + refU; - double varV = V / (13 * L) + refV; - - double Y = lToY(L); - double X = 0 - (9 * Y * varU) / ((varU - 4) * varV - varU * varV); - double Z = (9 * Y - (15 * varV * Y) - (varV * X)) / (3 * varV); - - return new double[]{X, Y, Z}; - } - - public static double[] luvToLch(double[] tuple) { - double L = tuple[0]; - double U = tuple[1]; - double V = tuple[2]; - - double C = Math.sqrt(U * U + V * V); - double H; - - if (C < 0.00000001) { - H = 0; - } else { - double Hrad = Math.atan2(V, U); - - // pi to more digits than they provide it in the stdlib - H = (Hrad * 180.0) / 3.1415926535897932; - - if (H < 0) { - H = 360 + H; - } - } - - return new double[]{L, C, H}; - } - - public static double[] lchToLuv(double[] tuple) { - double L = tuple[0]; - double C = tuple[1]; - double H = tuple[2]; - - double Hrad = H / 360.0 * 2 * Math.PI; - double U = Math.cos(Hrad) * C; - double V = Math.sin(Hrad) * C; - - return new double[]{L, U, V}; - } - - public static double[] hsluvToLch(double[] tuple) { - double H = tuple[0]; - double S = tuple[1]; - double L = tuple[2]; - - if (L > 99.9999999) { - return new double[]{100d, 0, H}; - } - - if (L < 0.00000001) { - return new double[]{0, 0, H}; - } - - double max = maxChromaForLH(L, H); - double C = max / 100 * S; - - return new double[]{L, C, H}; - } - - public static double[] lchToHsluv(double[] tuple) { - double L = tuple[0]; - double C = tuple[1]; - double H = tuple[2]; - - if (L > 99.9999999) { - return new double[]{H, 0, 100}; - } - - if (L < 0.00000001) { - return new double[]{H, 0, 0}; - } - - double max = maxChromaForLH(L, H); - double S = C / max * 100; - - return new double[]{H, S, L}; - } - - public static double[] hpluvToLch(double[] tuple) { - double H = tuple[0]; - double S = tuple[1]; - double L = tuple[2]; - - if (L > 99.9999999) { - return new double[]{100, 0, H}; - } - - if (L < 0.00000001) { - return new double[]{0, 0, H}; - } - - double max = maxSafeChromaForL(L); - double C = max / 100 * S; - - return new double[]{L, C, H}; - } - - public static double[] lchToHpluv(double[] tuple) { - double L = tuple[0]; - double C = tuple[1]; - double H = tuple[2]; - - if (L > 99.9999999) { - return new double[]{H, 0, 100}; - } - - if (L < 0.00000001) { - return new double[]{H, 0, 0}; - } - - double max = maxSafeChromaForL(L); - double S = C / max * 100; - - return new double[]{H, S, L}; - } - - public static String rgbToHex(double[] tuple) { - int[] prepared = rgbPrepare(tuple); - - return String.format("#%02x%02x%02x", - prepared[0], - prepared[1], - prepared[2]); - } - - public static double[] hexToRgb(String hex) { - return new double[] - { - Integer.parseInt(hex.substring(1, 3), 16) / 255.0, - Integer.parseInt(hex.substring(3, 5), 16) / 255.0, - Integer.parseInt(hex.substring(5, 7), 16) / 255.0, - }; - } - - public static double[] lchToRgb(double[] tuple) { - return xyzToRgb(luvToXyz(lchToLuv(tuple))); - } - - public static double[] rgbToLch(double[] tuple) { - return luvToLch(xyzToLuv(rgbToXyz(tuple))); - } - - // RGB <--> HUSL(p) - - public static double[] hsluvToRgb(double[] tuple) { - return lchToRgb(hsluvToLch(tuple)); - } - - public static double[] rgbToHsluv(double[] tuple) { - return lchToHsluv(rgbToLch(tuple)); - } - - public static double[] hpluvToRgb(double[] tuple) { - return lchToRgb(hpluvToLch(tuple)); - } - - public static double[] rgbToHpluv(double[] tuple) { - return lchToHpluv(rgbToLch(tuple)); - } - - // Hex - - public static String hsluvToHex(double[] tuple) { - return rgbToHex(hsluvToRgb(tuple)); - } - - public static String hpluvToHex(double[] tuple) { - return rgbToHex(hpluvToRgb(tuple)); - } - - public static double[] hexToHsluv(String s) { - return rgbToHsluv(hexToRgb(s)); - } - - public static double[] hexToHpluv(String s) { - return rgbToHpluv(hexToRgb(s)); - } - -} diff --git a/src/main/java/org/hsluv/HsluvColorConverter.java b/src/main/java/org/hsluv/HsluvColorConverter.java new file mode 100644 index 0000000..76b0146 --- /dev/null +++ b/src/main/java/org/hsluv/HsluvColorConverter.java @@ -0,0 +1,368 @@ +package org.hsluv; + +/** + * README: https://github.com/hsluv/hsluv-java + * + */ +public class HsluvColorConverter { + private static double refY = 1.0; + private static double refU = 0.19783000664283; + private static double refV = 0.46831999493879; + private static double kappa = 903.2962962; + private static double epsilon = 0.0088564516; + private static double m_r0 = 3.240969941904521; + private static double m_r1 = -1.537383177570093; + private static double m_r2 = -0.498610760293; + private static double m_g0 = -0.96924363628087; + private static double m_g1 = 1.87596750150772; + private static double m_g2 = 0.041555057407175; + private static double m_b0 = 0.055630079696993; + private static double m_b1 = -0.20397695888897; + private static double m_b2 = 1.056971514242878; + + // RGB + public String hex = "#000000"; + public double rgb_r = 0; + public double rgb_g = 0; + public double rgb_b = 0; + + // CIE XYZ + public double xyz_x = 0; + public double xyz_y = 0; + public double xyz_z = 0; + + // CIE LUV + public double luv_l = 0; + public double luv_u = 0; + public double luv_v = 0; + + // CIE LUV LCh + public double lch_l = 0; + public double lch_c = 0; + public double lch_h = 0; + + // HSLuv + public double hsluv_h = 0; + public double hsluv_s = 0; + public double hsluv_l = 0; + + // HPLuv + public double hpluv_h = 0; + public double hpluv_p = 0; + public double hpluv_l = 0; + + // 6 lines in slope-intercept format: R < 0, R > 1, G < 0, G > 1, B < 0, B > 1 + public double r0s = 0; + public double r0i = 0; + public double r1s = 0; + public double r1i = 0; + + public double g0s = 0; + public double g0i = 0; + public double g1s = 0; + public double g1i = 0; + + public double b0s = 0; + public double b0i = 0; + public double b1s = 0; + public double b1i = 0; + + private static double fromLinear(double c) { + if (c <= 0.0031308) { + return 12.92 * c; + } else { + return 1.055 * Math.pow(c, 1 / 2.4) - 0.055; + } + } + + private static double toLinear(double c) { + if (c > 0.04045) { + return Math.pow((c + 0.055) / 1.055, 2.4); + } else { + return c / 12.92; + } + } + + private static double yToL(double Y) { + if (Y <= epsilon) { + return Y / refY * kappa; + } else { + return 116 * Math.pow(Y / refY, 1.0 / 3) - 16; + } + } + + private static double lToY(double L) { + if (L <= 8) { + return refY * L / kappa; + } else { + return refY * Math.pow((L + 16) / 116, 3); + } + } + + private static double hexToRgbChannel(String hex, int offset) { + return Integer.parseInt(hex.substring(offset, offset + 2), 16) / 255.0; + } + + private static double distanceFromOriginAngle(double slope, double intercept, double angle) { + double d = intercept / (Math.sin(angle) - slope * Math.cos(angle)); + if (d < 0) { + return Double.POSITIVE_INFINITY; + } else { + return d; + } + } + + private static double distanceFromOrigin(double slope, double intercept) { + return Math.abs(intercept) / Math.sqrt(Math.pow(slope, 2) + 1); + } + + private static double min6(double f1, double f2, double f3, double f4, double f5, double f6) { + return Math.min(f1, Math.min(f2, Math.min(f3, Math.min(f4, Math.min(f5, f6))))); + } + + public void rgbToHex() { + long r = Math.round(this.rgb_r * 255); + long g = Math.round(this.rgb_g * 255); + long b = Math.round(this.rgb_b * 255); + this.hex = "#" + String.format("%06x", r * 256 * 256 + g * 256 + b); + } + + public void hexToRgb() { + this.hex = this.hex.toLowerCase(); + this.rgb_r = hexToRgbChannel(this.hex, 1); + this.rgb_g = hexToRgbChannel(this.hex, 3); + this.rgb_b = hexToRgbChannel(this.hex, 5); + } + + public void xyzToRgb() { + this.rgb_r = fromLinear(m_r0 * this.xyz_x + m_r1 * this.xyz_y + m_r2 * this.xyz_z); + this.rgb_g = fromLinear(m_g0 * this.xyz_x + m_g1 * this.xyz_y + m_g2 * this.xyz_z); + this.rgb_b = fromLinear(m_b0 * this.xyz_x + m_b1 * this.xyz_y + m_b2 * this.xyz_z); + } + + public void rgbToXyz() { + double lr = toLinear(this.rgb_r); + double lg = toLinear(this.rgb_g); + double lb = toLinear(this.rgb_b); + this.xyz_x = 0.41239079926595 * lr + 0.35758433938387 * lg + 0.18048078840183 * lb; + this.xyz_y = 0.21263900587151 * lr + 0.71516867876775 * lg + 0.072192315360733 * lb; + this.xyz_z = 0.019330818715591 * lr + 0.11919477979462 * lg + 0.95053215224966 * lb; + } + + public void xyzToLuv() { + double divider = this.xyz_x + 15 * this.xyz_y + 3 * this.xyz_z; + double varU = 4 * this.xyz_x; + double varV = 9 * this.xyz_y; + if (divider != 0) { + varU /= divider; + varV /= divider; + } else { + varU = Double.NaN; + varV = Double.NaN; + } + this.luv_l = yToL(this.xyz_y); + if (this.luv_l == 0) { + this.luv_u = 0; + this.luv_v = 0; + } else { + this.luv_u = 13 * this.luv_l * (varU - refU); + this.luv_v = 13 * this.luv_l * (varV - refV); + } + } + + public void luvToXyz() { + if (this.luv_l == 0) { + this.xyz_x = 0; + this.xyz_y = 0; + this.xyz_z = 0; + return; + } + double varU = this.luv_u / (13 * this.luv_l) + refU; + double varV = this.luv_v / (13 * this.luv_l) + refV; + this.xyz_y = lToY(this.luv_l); + this.xyz_x = 0 - 9 * this.xyz_y * varU / ((varU - 4) * varV - varU * varV); + this.xyz_z = (9 * this.xyz_y - 15 * varV * this.xyz_y - varV * this.xyz_x) / (3 * varV); + } + + public void luvToLch() { + this.lch_l = this.luv_l; + this.lch_c = Math.sqrt(this.luv_u * this.luv_u + this.luv_v * this.luv_v); + if (this.lch_c < 0.00000001) { + this.lch_h = 0; + } else { + double hrad = Math.atan2(this.luv_v, this.luv_u); + this.lch_h = hrad * 180.0 / Math.PI; + if (this.lch_h < 0) { + this.lch_h = 360 + this.lch_h; + } + } + } + + public void lchToLuv() { + double hrad = this.lch_h / 180.0 * Math.PI; + this.luv_l = this.lch_l; + this.luv_u = Math.cos(hrad) * this.lch_c; + this.luv_v = Math.sin(hrad) * this.lch_c; + } + + public void calculateBoundingLines(double l) { + double sub1 = Math.pow(l + 16, 3) / 1560896; + double sub2 = sub1 > epsilon ? sub1 : l / kappa; + double s1r = sub2 * (284517 * m_r0 - 94839 * m_r2); + double s2r = sub2 * (838422 * m_r2 + 769860 * m_r1 + 731718 * m_r0); + double s3r = sub2 * (632260 * m_r2 - 126452 * m_r1); + double s1g = sub2 * (284517 * m_g0 - 94839 * m_g2); + double s2g = sub2 * (838422 * m_g2 + 769860 * m_g1 + 731718 * m_g0); + double s3g = sub2 * (632260 * m_g2 - 126452 * m_g1); + double s1b = sub2 * (284517 * m_b0 - 94839 * m_b2); + double s2b = sub2 * (838422 * m_b2 + 769860 * m_b1 + 731718 * m_b0); + double s3b = sub2 * (632260 * m_b2 - 126452 * m_b1); + this.r0s = s1r / s3r; + this.r0i = s2r * l / s3r; + this.r1s = s1r / (s3r + 126452); + this.r1i = (s2r - 769860) * l / (s3r + 126452); + this.g0s = s1g / s3g; + this.g0i = s2g * l / s3g; + this.g1s = s1g / (s3g + 126452); + this.g1i = (s2g - 769860) * l / (s3g + 126452); + this.b0s = s1b / s3b; + this.b0i = s2b * l / s3b; + this.b1s = s1b / (s3b + 126452); + this.b1i = (s2b - 769860) * l / (s3b + 126452); + } + + public double calcMaxChromaHpluv() { + double r0 = distanceFromOrigin(this.r0s, this.r0i); + double r1 = distanceFromOrigin(this.r1s, this.r1i); + double g0 = distanceFromOrigin(this.g0s, this.g0i); + double g1 = distanceFromOrigin(this.g1s, this.g1i); + double b0 = distanceFromOrigin(this.b0s, this.b0i); + double b1 = distanceFromOrigin(this.b1s, this.b1i); + return min6(r0, r1, g0, g1, b0, b1); + } + + public double calcMaxChromaHsluv(double h) { + double hueRad = h / 360 * Math.PI * 2; + double r0 = distanceFromOriginAngle(this.r0s, this.r0i, hueRad); + double r1 = distanceFromOriginAngle(this.r1s, this.r1i, hueRad); + double g0 = distanceFromOriginAngle(this.g0s, this.g0i, hueRad); + double g1 = distanceFromOriginAngle(this.g1s, this.g1i, hueRad); + double b0 = distanceFromOriginAngle(this.b0s, this.b0i, hueRad); + double b1 = distanceFromOriginAngle(this.b1s, this.b1i, hueRad); + return min6(r0, r1, g0, g1, b0, b1); + } + + public void hsluvToLch() { + if (this.hsluv_l > 99.9999999) { + this.lch_l = 100; + this.lch_c = 0; + } else if (this.hsluv_l < 0.00000001) { + this.lch_l = 0; + this.lch_c = 0; + } else { + this.lch_l = this.hsluv_l; + this.calculateBoundingLines(this.hsluv_l); + double max = this.calcMaxChromaHsluv(this.hsluv_h); + this.lch_c = max / 100 * this.hsluv_s; + } + this.lch_h = this.hsluv_h; + } + + public void lchToHsluv() { + if (this.lch_l > 99.9999999) { + this.hsluv_s = 0; + this.hsluv_l = 100; + } else if (this.lch_l < 0.00000001) { + this.hsluv_s = 0; + this.hsluv_l = 0; + } else { + this.calculateBoundingLines(this.lch_l); + double max = this.calcMaxChromaHsluv(this.lch_h); + this.hsluv_s = this.lch_c / max * 100; + this.hsluv_l = this.lch_l; + } + this.hsluv_h = this.lch_h; + } + + public void hpluvToLch() { + if (this.hpluv_l > 99.9999999) { + this.lch_l = 100; + this.lch_c = 0; + } else if (this.hpluv_l < 0.00000001) { + this.lch_l = 0; + this.lch_c = 0; + } else { + this.lch_l = this.hpluv_l; + this.calculateBoundingLines(this.hpluv_l); + double max = this.calcMaxChromaHpluv(); + this.lch_c = max / 100 * this.hpluv_p; + } + this.lch_h = this.hpluv_h; + } + + public void lchToHpluv() { + if (this.lch_l > 99.9999999) { + this.hpluv_p = 0; + this.hpluv_l = 100; + } else if (this.lch_l < 0.00000001) { + this.hpluv_p = 0; + this.hpluv_l = 0; + } else { + this.calculateBoundingLines(this.lch_l); + double max = this.calcMaxChromaHpluv(); + this.hpluv_p = this.lch_c / max * 100; + this.hpluv_l = this.lch_l; + } + this.hpluv_h = this.lch_h; + } + + public void hsluvToRgb() { + this.hsluvToLch(); + this.lchToLuv(); + this.luvToXyz(); + this.xyzToRgb(); + } + + public void hpluvToRgb() { + this.hpluvToLch(); + this.lchToLuv(); + this.luvToXyz(); + this.xyzToRgb(); + } + + public void hsluvToHex() { + this.hsluvToRgb(); + this.rgbToHex(); + } + + public void hpluvToHex() { + this.hpluvToRgb(); + this.rgbToHex(); + } + + public void rgbToHsluv() { + this.rgbToXyz(); + this.xyzToLuv(); + this.luvToLch(); + this.lchToHpluv(); + this.lchToHsluv(); + } + + public void rgbToHpluv() { + this.rgbToXyz(); + this.xyzToLuv(); + this.luvToLch(); + this.lchToHpluv(); + this.lchToHpluv(); + } + + public void hexToHsluv() { + this.hexToRgb(); + this.rgbToHsluv(); + } + + public void hexToHpluv() { + this.hexToRgb(); + this.rgbToHpluv(); + } +} \ No newline at end of file diff --git a/src/test/java/org/hsluv/ColorConverterTest.java b/src/test/java/org/hsluv/ColorConverterTest.java index 0565a9c..973a879 100644 --- a/src/test/java/org/hsluv/ColorConverterTest.java +++ b/src/test/java/org/hsluv/ColorConverterTest.java @@ -1,130 +1,94 @@ package org.hsluv; import jakarta.json.*; -import junit.framework.Test; -import junit.framework.TestCase; -import junit.framework.TestSuite; +import static org.junit.Assert.assertEquals; import java.io.IOException; import java.io.InputStream; +import org.junit.Test; -public class ColorConverterTest extends TestCase { - - private static final double MAXDIFF = 0.0000000001; - private static final double MAXRELDIFF = 0.000000001; - - /** - * modified from - * https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/ - */ - private boolean assertAlmostEqualRelativeAndAbs(double a, double b) { - // Check if the numbers are really close -- needed - // when comparing numbers near zero. - double diff = Math.abs(a - b); - if (diff <= MAXDIFF) { - return true; - } - - a = Math.abs(a); - b = Math.abs(b); - double largest = (b > a) ? b : a; - - return diff <= largest * MAXRELDIFF; - } - - private void assertTuplesClose(String label, double[] expected, double[] actual) { - boolean mismatch = false; - double[] deltas = new double[expected.length]; - - for (int i = 0; i < expected.length; ++i) { - deltas[i] = Math.abs(expected[i] - actual[i]); - if (!assertAlmostEqualRelativeAndAbs(expected[i], actual[i])) { - mismatch = true; - } - } - - if (mismatch) { - System.out.printf("MISMATCH %s\n", label); - System.out.printf(" expected: %.10f,%.10f,%.10f\n", expected[0], expected[1], expected[2]); - System.out.printf(" actual: %.10f,%.10f,%.10f\n", actual[0], actual[1], actual[2]); - System.out.printf(" deltas: %.10f,%.10f,%.10f\n", deltas[0], deltas[1], deltas[2]); +public class ColorConverterTest { + static void assertFloatClose(double expected, double actual) { + if (Math.abs(expected - actual) > 1e-10) { + System.out.println(expected); + System.out.println(actual); + throw new RuntimeException("Not equals"); } - - assertFalse(mismatch); } - - public static Test suite() { - return new TestSuite(ColorConverterTest.class); + static void assertClose(HsluvColorConverter expected, HsluvColorConverter actual) { + assertEquals(expected.hex, actual.hex); + assertFloatClose(expected.rgb_r, actual.rgb_r); + assertFloatClose(expected.rgb_g, actual.rgb_g); + assertFloatClose(expected.rgb_b, actual.rgb_b); + assertFloatClose(expected.xyz_x, actual.xyz_x); + assertFloatClose(expected.xyz_y, actual.xyz_y); + assertFloatClose(expected.xyz_z, actual.xyz_z); + assertFloatClose(expected.luv_l, actual.luv_l); + assertFloatClose(expected.luv_u, actual.luv_u); + assertFloatClose(expected.luv_v, actual.luv_v); + assertFloatClose(expected.lch_l, actual.lch_l); + assertFloatClose(expected.lch_c, actual.lch_c); + assertFloatClose(expected.lch_h, actual.lch_h); + assertFloatClose(expected.hsluv_h, actual.hsluv_h); + assertFloatClose(expected.hsluv_s, actual.hsluv_s); + assertFloatClose(expected.hsluv_l, actual.hsluv_l); + assertFloatClose(expected.hpluv_h, actual.hpluv_h); + assertFloatClose(expected.hpluv_p, actual.hpluv_p); + assertFloatClose(expected.hpluv_l, actual.hpluv_l); } - private double[] tupleFromJsonArray(JsonArray arr) { - return new double[]{ - arr.getJsonNumber(0).doubleValue(), - arr.getJsonNumber(1).doubleValue(), - arr.getJsonNumber(2).doubleValue() - }; + static double getSample(JsonObject s, String cs, int index) { + return s.getJsonArray(cs).getJsonNumber(index).doubleValue(); } + @Test public void testHsluv() throws IOException { System.out.println("Running test"); InputStream snapshotStream = ColorConverterTest.class.getResourceAsStream("/snapshot-rev4.json"); JsonReader reader = Json.createReader(snapshotStream); JsonObject tests = reader.readObject(); + HsluvColorConverter conv = new HsluvColorConverter(); for (String hex : tests.keySet()) { - JsonObject expected = tests.getJsonObject(hex); - double[] rgb = tupleFromJsonArray(expected.getJsonArray("rgb")); - double[] xyz = tupleFromJsonArray(expected.getJsonArray("xyz")); - double[] luv = tupleFromJsonArray(expected.getJsonArray("luv")); - double[] lch = tupleFromJsonArray(expected.getJsonArray("lch")); - double[] hsluv = tupleFromJsonArray(expected.getJsonArray("hsluv")); - double[] hpluv = tupleFromJsonArray(expected.getJsonArray("hpluv")); - - System.out.println("testing " + hex); - - // forward functions - - double[] rgbFromHex = HUSLColorConverter.hexToRgb(hex); - double[] xyzFromRgb = HUSLColorConverter.rgbToXyz(rgbFromHex); - double[] luvFromXyz = HUSLColorConverter.xyzToLuv(xyzFromRgb); - double[] lchFromLuv = HUSLColorConverter.luvToLch(luvFromXyz); - double[] hsluvFromLch = HUSLColorConverter.lchToHsluv(lchFromLuv); - double[] hpluvFromLch = HUSLColorConverter.lchToHpluv(lchFromLuv); - double[] hsluvFromHex = HUSLColorConverter.hexToHsluv(hex); - double[] hpluvFromHex = HUSLColorConverter.hexToHpluv(hex); - - assertTuplesClose("hexToRgb", rgb, rgbFromHex); - assertTuplesClose("rgbToXyz", xyz, xyzFromRgb); - assertTuplesClose("xyzToLuv", luv, luvFromXyz); - assertTuplesClose("luvToLch", lch, lchFromLuv); - assertTuplesClose("lchToHsluv", hsluv, hsluvFromLch); - assertTuplesClose("lchToHpluv", hpluv, hpluvFromLch); - assertTuplesClose("hexToHsluv", hsluv, hsluvFromHex); - assertTuplesClose("hexToHpluv", hpluv, hpluvFromHex); - - // backward functions - - double[] lchFromHsluv = HUSLColorConverter.hsluvToLch(hsluv); - double[] lchFromHpluv = HUSLColorConverter.hpluvToLch(hpluv); - double[] luvFromLch = HUSLColorConverter.lchToLuv(lch); - double[] xyzFromLuv = HUSLColorConverter.luvToXyz(luv); - double[] rgbFromXyz = HUSLColorConverter.xyzToRgb(xyz); - String hexFromRgb = HUSLColorConverter.rgbToHex(rgb); - String hexFromHsluv = HUSLColorConverter.hsluvToHex(hsluv); - String hexFromHpluv = HUSLColorConverter.hpluvToHex(hpluv); - - assertTuplesClose("hsluvToLch", lch, lchFromHsluv); - assertTuplesClose("hpluvToLch", lch, lchFromHpluv); - assertTuplesClose("lchToLuv", luv, luvFromLch); - assertTuplesClose("luvToXyz", xyz, xyzFromLuv); - assertTuplesClose("xyzToRgb", rgb, rgbFromXyz); - assertEquals(hex, hexFromRgb); - assertEquals(hex, hexFromHsluv); - assertEquals(hex, hexFromHpluv); - + JsonObject s = tests.getJsonObject(hex); + HsluvColorConverter sample = new HsluvColorConverter(); + sample.hex = hex; + sample.rgb_r = getSample(s, "rgb", 0); + sample.rgb_g = getSample(s, "rgb", 1); + sample.rgb_b = getSample(s, "rgb", 2); + sample.xyz_x = getSample(s, "xyz", 0); + sample.xyz_y = getSample(s, "xyz", 1); + sample.xyz_z = getSample(s, "xyz", 2); + sample.luv_l = getSample(s, "luv", 0); + sample.luv_u = getSample(s, "luv", 1); + sample.luv_v = getSample(s, "luv", 2); + sample.lch_l = getSample(s, "lch", 0); + sample.lch_c = getSample(s, "lch", 1); + sample.lch_h = getSample(s, "lch", 2); + sample.hsluv_h = getSample(s, "hsluv", 0); + sample.hsluv_s = getSample(s, "hsluv", 1); + sample.hsluv_l = getSample(s, "hsluv", 2); + sample.hpluv_h = getSample(s, "hpluv", 0); + sample.hpluv_p = getSample(s, "hpluv", 1); + sample.hpluv_l = getSample(s, "hpluv", 2); + conv.hex = hex; + conv.hexToHsluv(); + assertClose(conv, sample); + conv.hexToHpluv(); + assertClose(conv, sample); + conv.hsluv_h = sample.hsluv_h; + conv.hsluv_s = sample.hsluv_s; + conv.hsluv_l = sample.hsluv_l; + conv.hsluvToHex(); + assertClose(conv, sample); + conv.hpluv_h = sample.hpluv_h; + conv.hpluv_p = sample.hpluv_p; + conv.hpluv_l = sample.hpluv_l; + conv.hpluvToHex(); + assertClose(conv, sample); } } }