001/**
002 *
003 * Copyright © 2018-2019 Paul Schaub
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smackx.colors;
018
019import org.jivesoftware.smack.util.Objects;
020import org.jivesoftware.smack.util.SHA1;
021
022import org.hsluv.HUSLColorConverter;
023
024/**
025 * Implementation of XEP-0392: Consistent Color Generation version 0.6.0.
026 *
027 * @author Paul Schaub
028 */
029public class ConsistentColor {
030
031    private static final ConsistentColorSettings DEFAULT_SETTINGS = new ConsistentColorSettings();
032
033    public enum Deficiency {
034        /**
035         * Do not apply measurements for color vision deficiency correction.
036         */
037        none,
038
039        /**
040         * Activate color correction for users suffering from red-green-blindness.
041         */
042        redGreenBlindness,
043
044        /**
045         * Activate color correction for users suffering from blue-blindness.
046         */
047        blueBlindness
048    }
049
050    /**
051     * Generate an angle in the HSLuv color space from the input string.
052     * @see <a href="https://xmpp.org/extensions/xep-0392.html#algorithm-angle">§5.1: Angle generation</a>
053     *
054     * @param input input string
055     * @return output angle in degrees
056     */
057    private static double createAngle(CharSequence input) {
058        byte[] h = SHA1.bytes(input.toString());
059        double v = u(h[0]) + (256 * u(h[1]));
060        double d = v / 65536;
061        return d * 360;
062    }
063
064    /**
065     * Apply correction for color vision deficiencies to an angle in the CbCr plane.
066     * @see <a href="https://xmpp.org/extensions/xep-0392.html#algorithm-cvd">§5.2: Corrections for Color Vision Deficiencies</a>
067     *
068     * @param angle angle in CbCr plane
069     * @param deficiency type of vision deficiency
070     * @return corrected angle in CbCr plane
071     */
072    private static double applyColorDeficiencyCorrection(double angle, Deficiency deficiency) {
073        switch (deficiency) {
074            case none:
075                break;
076            case redGreenBlindness:
077                angle += 90;
078                angle %= 180;
079                angle += 270; // equivalent to -90 % 360, but eliminates negative results
080                angle %= 360;
081                break;
082            case blueBlindness:
083                angle %= 180;
084                break;
085        }
086        return angle;
087    }
088
089    /**
090     * Converting a HSLuv angle to RGB.
091     * Saturation is set to 100 and lightness to 50, according to the XEP.
092     *
093     * @param hue angle
094     * @return rgb values between 0 and 1
095     *
096     * @see <a href="https://xmpp.org/extensions/xep-0392.html#algorithm-rgb">XEP-0392 §5.4: RGB generation</a>
097     */
098    private static double[] hsluvToRgb(double hue) {
099        return hsluvToRgb(hue, 100, 50);
100    }
101
102    /**
103     * Converting a HSLuv angle to RGB.
104     *
105     * @param hue angle 0 <= hue < 360
106     * @param saturation saturation 0 <= saturation <= 100
107     * @param lightness lightness 0 <= lightness <= 100
108     * @return rbg array with values 0 <= (r,g,b) <= 1
109     *
110     * @see <a href="https://www.rapidtables.com/convert/color/hsl-to-rgb.html">HSL to RGB conversion</a>
111     */
112    private static double[] hsluvToRgb(double hue, double saturation, double lightness) {
113        return HUSLColorConverter.hsluvToRgb(new double[] {hue, saturation, lightness});
114    }
115
116    private static double[] mixWithBackground(double[] rgbi, float[] rgbb) {
117        return new double[] {
118                0.2 * (1 - rgbb[0]) + 0.8 * rgbi[0],
119                0.2 * (1 - rgbb[1]) + 0.8 * rgbi[1],
120                0.2 * (1 - rgbb[2]) + 0.8 * rgbi[2]
121        };
122    }
123
124    /**
125     * Treat a signed java byte as unsigned to get its numerical value.
126     *
127     * @param b signed java byte
128     * @return integer value of its unsigned representation
129     */
130    private static int u(byte b) {
131        // Get unsigned value of signed byte as an integer.
132        return b & 0xFF;
133    }
134
135    /**
136     * Return the consistent RGB color value of the input.
137     * This method uses the default {@link ConsistentColorSettings}.
138     *
139     * @param input input string (for example username)
140     * @return consistent color of that username as RGB values in range [0,1].
141     * @see #RGBFrom(CharSequence, ConsistentColorSettings)
142     */
143    public static float[] RGBFrom(CharSequence input) {
144        return RGBFrom(input, DEFAULT_SETTINGS);
145    }
146
147    /**
148     * Return the consistent RGB color value for the input.
149     * This method respects the color vision deficiency mode set by the user.
150     *
151     * @param input input string (for example username)
152     * @param settings the settings for consistent color creation.
153     * @return consistent color of that username as RGB values in range [0,1].
154     */
155    public static float[] RGBFrom(CharSequence input, ConsistentColorSettings settings) {
156        double angle = createAngle(input);
157        double correctedAngle = applyColorDeficiencyCorrection(angle, settings.getDeficiency());
158        double[] rgb = hsluvToRgb(correctedAngle);
159        if (settings.backgroundRGB != null) {
160            rgb = mixWithBackground(rgb, settings.backgroundRGB);
161        }
162
163        return new float[] {(float) rgb[0], (float) rgb[1], (float) rgb[2]};
164    }
165
166    public static int[] floatRgbToInts(float[] floats) {
167        return new int[] {
168                (int) (floats[0] * 255),
169                (int) (floats[1] * 255),
170                (int) (floats[2] * 255)
171        };
172    }
173
174    public static class ConsistentColorSettings {
175
176        private final Deficiency deficiency;
177        private final float[] backgroundRGB;
178
179        public ConsistentColorSettings() {
180            this.deficiency = Deficiency.none;
181            this.backgroundRGB = null;
182        }
183
184        public ConsistentColorSettings(Deficiency deficiency) {
185            this.deficiency = Objects.requireNonNull(deficiency, "Deficiency must be given");
186            this.backgroundRGB = null;
187        }
188
189        public ConsistentColorSettings(Deficiency deficiency,
190                                       float[] backgroundRGB) {
191            this.deficiency = Objects.requireNonNull(deficiency, "Deficiency must be given");
192            if (backgroundRGB.length != 3) {
193                throw new IllegalArgumentException("Background RGB value array must have length 3.");
194            }
195
196            for (float f : backgroundRGB) {
197                checkRange(f, 0, 1);
198            }
199            this.backgroundRGB = backgroundRGB;
200        }
201
202        private static void checkRange(float value, float lower, float upper) {
203            if (lower > value || upper < value) {
204                throw new IllegalArgumentException("Value out of range.");
205            }
206        }
207
208        /**
209         * Return the deficiency setting.
210         *
211         * @return deficiency setting.
212         */
213        public Deficiency getDeficiency() {
214            return deficiency;
215        }
216    }
217}