diff --git a/src/main/java/org/hsluv/HsluvColorConverter.java b/src/main/java/org/hsluv/HsluvColorConverter.java new file mode 100644 index 0000000..3e00de4 --- /dev/null +++ b/src/main/java/org/hsluv/HsluvColorConverter.java @@ -0,0 +1,374 @@ +package org.hsluv; + +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 <= HsluvColorConverter.epsilon) { + return Y / HsluvColorConverter.refY * HsluvColorConverter.kappa; + } else { + return 116 * Math.pow(Y / HsluvColorConverter.refY, 1 / 3) - 16; + } + } + + private static double lToY(double L) { + if (L <= 8) { + return HsluvColorConverter.refY * L / HsluvColorConverter.kappa; + } else { + return HsluvColorConverter.refY * Math.pow((L + 16) / 116, 3); + } + } + + private static String rgbChannelToHex(double chan) { + return String.format("%02x", chan); + } + + private static double hexToRgbChannel(String hex, int offset) { + return (double) Integer.parseInt(hex.substring(offset, offset + 2)); + } + + 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() { + this.hex = "#"; + this.hex += HsluvColorConverter.rgbChannelToHex(this.rgb_r); + this.hex += HsluvColorConverter.rgbChannelToHex(this.rgb_g); + this.hex += HsluvColorConverter.rgbChannelToHex(this.rgb_b); + } + + public void hexToRgb() { + this.hex = this.hex.toLowerCase(); + this.rgb_r = HsluvColorConverter.hexToRgbChannel(this.hex, 1); + this.rgb_g = HsluvColorConverter.hexToRgbChannel(this.hex, 3); + this.rgb_b = HsluvColorConverter.hexToRgbChannel(this.hex, 5); + } + + public void xyzToRgb() { + this.rgb_r = HsluvColorConverter.fromLinear(HsluvColorConverter.m_r0 * this.xyz_x + + HsluvColorConverter.m_r1 * this.xyz_y + HsluvColorConverter.m_r2 * this.xyz_z); + this.rgb_g = HsluvColorConverter.fromLinear(HsluvColorConverter.m_g0 * this.xyz_x + + HsluvColorConverter.m_g1 * this.xyz_y + HsluvColorConverter.m_g2 * this.xyz_z); + this.rgb_b = HsluvColorConverter.fromLinear(HsluvColorConverter.m_b0 * this.xyz_x + + HsluvColorConverter.m_b1 * this.xyz_y + HsluvColorConverter.m_b2 * this.xyz_z); + } + + public void rgbToXyz() { + double lr = HsluvColorConverter.toLinear(this.rgb_r); + double lg = HsluvColorConverter.toLinear(this.rgb_g); + double lb = HsluvColorConverter.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 = HsluvColorConverter.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 - HsluvColorConverter.refU); + this.luv_v = 13 * this.luv_l * (varV - HsluvColorConverter.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) + HsluvColorConverter.refU; + double varV = this.luv_v / (13 * this.luv_l) + HsluvColorConverter.refV; + this.xyz_y = HsluvColorConverter.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 > HsluvColorConverter.epsilon ? sub1 : l / HsluvColorConverter.kappa; + double s1r = sub2 * (284517 * HsluvColorConverter.m_r0 - 94839 * HsluvColorConverter.m_r2); + double s2r = sub2 * (838422 * HsluvColorConverter.m_r2 + 769860 * HsluvColorConverter.m_r1 + + 731718 * HsluvColorConverter.m_r0); + double s3r = sub2 * (632260 * HsluvColorConverter.m_r2 - 126452 * HsluvColorConverter.m_r1); + double s1g = sub2 * (284517 * HsluvColorConverter.m_g0 - 94839 * HsluvColorConverter.m_g2); + double s2g = sub2 * (838422 * HsluvColorConverter.m_g2 + 769860 * HsluvColorConverter.m_g1 + + 731718 * HsluvColorConverter.m_g0); + double s3g = sub2 * (632260 * HsluvColorConverter.m_g2 - 126452 * HsluvColorConverter.m_g1); + double s1b = sub2 * (284517 * HsluvColorConverter.m_b0 - 94839 * HsluvColorConverter.m_b2); + double s2b = sub2 * (838422 * HsluvColorConverter.m_b2 + 769860 * HsluvColorConverter.m_b1 + + 731718 * HsluvColorConverter.m_b0); + double s3b = sub2 * (632260 * HsluvColorConverter.m_b2 - 126452 * HsluvColorConverter.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 = HsluvColorConverter.distanceFromOrigin(this.r0s, this.r0i); + double r1 = HsluvColorConverter.distanceFromOrigin(this.r1s, this.r1i); + double g0 = HsluvColorConverter.distanceFromOrigin(this.g0s, this.g0i); + double g1 = HsluvColorConverter.distanceFromOrigin(this.g1s, this.g1i); + double b0 = HsluvColorConverter.distanceFromOrigin(this.b0s, this.b0i); + double b1 = HsluvColorConverter.distanceFromOrigin(this.b1s, this.b1i); + return HsluvColorConverter.min6(r0, r1, g0, g1, b0, b1); + } + + public double calcMaxChromaHsluv(double h) { + double hueRad = h / 360 * Math.PI * 2; + double r0 = HsluvColorConverter.distanceFromOriginAngle(this.r0s, this.r0i, hueRad); + double r1 = HsluvColorConverter.distanceFromOriginAngle(this.r1s, this.r1i, hueRad); + double g0 = HsluvColorConverter.distanceFromOriginAngle(this.g0s, this.g0i, hueRad); + double g1 = HsluvColorConverter.distanceFromOriginAngle(this.g1s, this.g1i, hueRad); + double b0 = HsluvColorConverter.distanceFromOriginAngle(this.b0s, this.b0i, hueRad); + double b1 = HsluvColorConverter.distanceFromOriginAngle(this.b1s, this.b1i, hueRad); + return HsluvColorConverter.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/TTest.java b/src/test/java/org/hsluv/TTest.java new file mode 100644 index 0000000..c5520a3 --- /dev/null +++ b/src/test/java/org/hsluv/TTest.java @@ -0,0 +1,92 @@ +package org.hsluv; + +import jakarta.json.*; +import static org.junit.Assert.assertEquals; + +import java.io.IOException; +import java.io.InputStream; + +import org.junit.Test; + +public class TTest { + 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"); + } + } + + 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); + } + + 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 = TTest.class.getResourceAsStream("/snapshot-rev4.json"); + + JsonReader reader = Json.createReader(snapshotStream); + JsonObject tests = reader.readObject(); + HsluvColorConverter conv = new HsluvColorConverter(); + + for (String hex : tests.keySet()) { + 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.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); + } + } +}