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 * Smack API for Consistent Color Generation (XEP-0392).
026 * <p>
027 * Since XMPP can be used on multiple platforms at the same time, it might be a
028 * good idea to render given Strings like nicknames in the same color on all
029 * platforms to provide a consistent user experience.
030 * </p>
031 * <h2>Usage</h2>
032 *
033 * <h2>Color Deficiency Corrections</h2>
034 * <p>
035 * Some users might suffer from color vision deficiencies. To compensate those
036 * deficiencies, the API allows for color correction.
037 * </p>
038 *
039 * @author Paul Schaub
040 */
041public class ConsistentColor {
042
043    private static final ConsistentColorSettings DEFAULT_SETTINGS = new ConsistentColorSettings();
044
045    public enum Deficiency {
046        /**
047         * Do not apply measurements for color vision deficiency correction.
048         */
049        none,
050
051        /**
052         * Activate color correction for users suffering from red-green-blindness.
053         */
054        redGreenBlindness,
055
056        /**
057         * Activate color correction for users suffering from blue-blindness.
058         */
059        blueBlindness
060    }
061
062    /**
063     * Generate an angle in the HSLuv color space from the input string.
064     * @see <a href="https://xmpp.org/extensions/xep-0392.html#algorithm-angle">§5.1: Angle generation</a>
065     *
066     * @param input input string
067     * @return output angle in degrees
068     */
069    private static double createAngle(CharSequence input) {
070        byte[] h = SHA1.bytes(input.toString());
071        double v = u(h[0]) + (256 * u(h[1]));
072        double d = v / 65536;
073        return d * 360;
074    }
075
076    /**
077     * Apply correction for color vision deficiencies to an angle in the CbCr plane.
078     * @see <a href="https://xmpp.org/extensions/xep-0392.html#algorithm-cvd">§5.2: Corrections for Color Vision Deficiencies</a>
079     *
080     * @param angle angle in CbCr plane
081     * @param deficiency type of vision deficiency
082     * @return corrected angle in CbCr plane
083     */
084    private static double applyColorDeficiencyCorrection(double angle, Deficiency deficiency) {
085        switch (deficiency) {
086            case none:
087                break;
088            case redGreenBlindness:
089                angle += 90;
090                angle %= 180;
091                angle += 270; // equivalent to -90 % 360, but eliminates negative results
092                angle %= 360;
093                break;
094            case blueBlindness:
095                angle %= 180;
096                break;
097        }
098        return angle;
099    }
100
101    /**
102     * Converting a HSLuv angle to RGB.
103     * Saturation is set to 100 and lightness to 50, according to the XEP.
104     *
105     * @param hue angle
106     * @return rgb values between 0 and 1
107     *
108     * @see <a href="https://xmpp.org/extensions/xep-0392.html#algorithm-rgb">XEP-0392 §5.4: RGB generation</a>
109     */
110    private static double[] hsluvToRgb(double hue) {
111        return hsluvToRgb(hue, 100, 50);
112    }
113
114    /**
115     * Converting a HSLuv angle to RGB.
116     *
117     * @param hue angle 0 <= hue < 360
118     * @param saturation saturation 0 <= saturation <= 100
119     * @param lightness lightness 0 <= lightness <= 100
120     * @return rbg array with values 0 <= (r,g,b) <= 1
121     *
122     * @see <a href="https://www.rapidtables.com/convert/color/hsl-to-rgb.html">HSL to RGB conversion</a>
123     */
124    private static double[] hsluvToRgb(double hue, double saturation, double lightness) {
125        return HUSLColorConverter.hsluvToRgb(new double[] {hue, saturation, lightness});
126    }
127
128    private static double[] mixWithBackground(double[] rgbi, float[] rgbb) {
129        return new double[] {
130                0.2 * (1 - rgbb[0]) + 0.8 * rgbi[0],
131                0.2 * (1 - rgbb[1]) + 0.8 * rgbi[1],
132                0.2 * (1 - rgbb[2]) + 0.8 * rgbi[2]
133        };
134    }
135
136    /**
137     * Treat a signed java byte as unsigned to get its numerical value.
138     *
139     * @param b signed java byte
140     * @return integer value of its unsigned representation
141     */
142    private static int u(byte b) {
143        // Get unsigned value of signed byte as an integer.
144        return b & 0xFF;
145    }
146
147    /**
148     * Return the consistent RGB color value of the input.
149     * This method uses the default {@link ConsistentColorSettings}.
150     *
151     * @param input input string (for example username)
152     * @return consistent color of that username as RGB values in range [0,1].
153     * @see #RGBFrom(CharSequence, ConsistentColorSettings)
154     */
155    public static float[] RGBFrom(CharSequence input) {
156        return RGBFrom(input, DEFAULT_SETTINGS);
157    }
158
159    /**
160     * Return the consistent RGB color value for the input.
161     * This method respects the color vision deficiency mode set by the user.
162     *
163     * @param input input string (for example username)
164     * @param settings the settings for consistent color creation.
165     * @return consistent color of that username as RGB values in range [0,1].
166     */
167    public static float[] RGBFrom(CharSequence input, ConsistentColorSettings settings) {
168        double angle = createAngle(input);
169        double correctedAngle = applyColorDeficiencyCorrection(angle, settings.getDeficiency());
170        double[] rgb = hsluvToRgb(correctedAngle);
171        if (settings.backgroundRGB != null) {
172            rgb = mixWithBackground(rgb, settings.backgroundRGB);
173        }
174
175        return new float[] {(float) rgb[0], (float) rgb[1], (float) rgb[2]};
176    }
177
178    public static int[] floatRgbToInts(float[] floats) {
179        return new int[] {
180                (int) (floats[0] * 255),
181                (int) (floats[1] * 255),
182                (int) (floats[2] * 255)
183        };
184    }
185
186    public static class ConsistentColorSettings {
187
188        private final Deficiency deficiency;
189        private final float[] backgroundRGB;
190
191        public ConsistentColorSettings() {
192            this.deficiency = Deficiency.none;
193            this.backgroundRGB = null;
194        }
195
196        public ConsistentColorSettings(Deficiency deficiency) {
197            this.deficiency = Objects.requireNonNull(deficiency, "Deficiency must be given");
198            this.backgroundRGB = null;
199        }
200
201        public ConsistentColorSettings(Deficiency deficiency,
202                                       float[] backgroundRGB) {
203            this.deficiency = Objects.requireNonNull(deficiency, "Deficiency must be given");
204            if (backgroundRGB.length != 3) {
205                throw new IllegalArgumentException("Background RGB value array must have length 3.");
206            }
207
208            for (float f : backgroundRGB) {
209                checkRange(f, 0, 1);
210            }
211            this.backgroundRGB = backgroundRGB;
212        }
213
214        private static void checkRange(float value, float lower, float upper) {
215            if (lower > value || upper < value) {
216                throw new IllegalArgumentException("Value out of range.");
217            }
218        }
219
220        /**
221         * Return the deficiency setting.
222         *
223         * @return deficiency setting.
224         */
225        public Deficiency getDeficiency() {
226            return deficiency;
227        }
228    }
229}