001/**
002 *
003 * Copyright © 2018 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
022public class ConsistentColor {
023
024    private static final ConsistentColorSettings DEFAULT_SETTINGS = new ConsistentColorSettings();
025
026    // See XEP-0392 §13.1 Constants for YCbCr (BT.601)
027    private static final double KR = 0.299;
028    private static final double KG = 0.587;
029    private static final double KB = 0.114;
030
031    // See XEP-0392 §5.4 CbCr to RGB
032    private static final double Y = 0.732;
033
034    public enum Deficiency {
035        /**
036         * Do not apply measurements for color vision deficiency correction.
037         */
038        none,
039
040        /**
041         * Activate color correction for users suffering from red-green-blindness.
042         */
043        redGreenBlindness,
044
045        /**
046         * Activate color correction for users suffering from blue-blindness.
047         */
048        blueBlindness
049    }
050
051    /**
052     * Generate an angle in the CbCr plane from the input string.
053     * @see <a href="https://xmpp.org/extensions/xep-0392.html#algorithm-angle">§5.1: Angle generation</a>
054     *
055     * @param input input string
056     * @return output angle
057     */
058    private static double createAngle(CharSequence input) {
059        byte[] h = SHA1.bytes(input.toString());
060        double v = u(h[0]) + (256 * u(h[1]));
061        double d = v / 65536;
062        return d * 2 * Math.PI;
063    }
064
065    /**
066     * Apply correction for color vision deficiencies to an angle in the CbCr plane.
067     * @see <a href="https://xmpp.org/extensions/xep-0392.html#algorithm-cvd">§5.2: Corrections for Color Vision Deficiencies</a>
068     *
069     * @param angle angle in CbCr plane
070     * @param deficiency type of vision deficiency
071     * @return corrected angle in CbCr plane
072     */
073    private static double applyColorDeficiencyCorrection(double angle, Deficiency deficiency) {
074        switch (deficiency) {
075            case none:
076                break;
077            case redGreenBlindness:
078                angle %= Math.PI;
079                break;
080            case blueBlindness:
081                angle -= Math.PI / 2;
082                angle %= Math.PI;
083                angle += Math.PI / 2;
084                break;
085        }
086        return angle;
087    }
088
089    /**
090     * Convert an angle in the CbCr plane to values cb,cr in the YCbCr color space.
091     * @see <a href="https://xmpp.org/extensions/xep-0392.html#algorithm-cbcr">§5.3: CbCr generation</a>
092     *
093     * @param angle angel in CbCr plane.
094     * @return value pair cb,cr
095     */
096    private static double[] angleToCbCr(double angle) {
097        double cb = Math.cos(angle);
098        double cr = Math.sin(angle);
099
100        double acb = Math.abs(cb);
101        double acr = Math.abs(cr);
102        double factor;
103        if (acr > acb) {
104            factor = 0.5 / acr;
105        } else {
106            factor = 0.5 / acb;
107        }
108
109        cb *= factor;
110        cr *= factor;
111
112        return new double[] {cb, cr};
113    }
114
115    /**
116     * Convert a value pair cb,cr in the YCbCr color space to RGB.
117     * @see <a href="https://xmpp.org/extensions/xep-0392.html#algorithm-rgb">§5.4: CbCr to RGB</a>
118     *
119     * @param cbcr value pair from the YCbCr color space
120     * @return RGB value triple (R,G,B in [0,1])
121     */
122    private static float[] CbCrToRGB(double[] cbcr, double y) {
123        double cb = cbcr[0];
124        double cr = cbcr[1];
125
126        double r = 2 * (1 - KR) * cr + y;
127        double b = 2 * (1 - KB) * cb + y;
128        double g = (y - KR * r - KB * b) / KG;
129
130        // Clip values to [0,1]
131        r = clip(r);
132        g = clip(g);
133        b = clip(b);
134
135        return new float[] {(float) r, (float) g, (float) b};
136    }
137
138    /**
139     * Clip values to stay in range(0,1).
140     *
141     * @param value input
142     * @return input clipped to stay in boundaries from 0 to 1.
143     */
144    private static double clip(double value) {
145        double out = value;
146
147        if (value < 0) {
148            out = 0;
149        }
150
151        if (value > 1) {
152            out = 1;
153        }
154
155        return out;
156    }
157
158    /**
159     * Treat a signed java byte as unsigned to get its numerical value.
160     *
161     * @param b signed java byte
162     * @return integer value of its unsigned representation
163     */
164    private static int u(byte b) {
165        // Get unsigned value of signed byte as an integer.
166        return b & 0xFF;
167    }
168
169    /**
170     * Return the consistent RGB color value of the input.
171     * This method uses the default {@link ConsistentColorSettings}.
172     *
173     * @param input input string (for example username)
174     * @return consistent color of that username as RGB values in range [0,1].
175     * @see #RGBFrom(CharSequence, ConsistentColorSettings)
176     */
177    public static float[] RGBFrom(CharSequence input) {
178        return RGBFrom(input, DEFAULT_SETTINGS);
179    }
180
181    /**
182     * Return the consistent RGB color value for the input.
183     * This method respects the color vision deficiency mode set by the user.
184     *
185     * @param input input string (for example username)
186     * @param settings the settings for consistent color creation.
187     * @return consistent color of that username as RGB values in range [0,1].
188     */
189    public static float[] RGBFrom(CharSequence input, ConsistentColorSettings settings) {
190        double angle = createAngle(input);
191        double correctedAngle = applyColorDeficiencyCorrection(angle, settings.getDeficiency());
192        double[] CbCr = angleToCbCr(correctedAngle);
193        float[] rgb = CbCrToRGB(CbCr, Y);
194        return rgb;
195    }
196
197    public static class ConsistentColorSettings {
198
199        private final Deficiency deficiency;
200
201        public ConsistentColorSettings() {
202            this(Deficiency.none);
203        }
204
205        public ConsistentColorSettings(Deficiency deficiency) {
206            this.deficiency = Objects.requireNonNull(deficiency, "Deficiency must be given");
207        }
208
209        /**
210         * Return the deficiency setting.
211         *
212         * @return deficiency setting.
213         */
214        public Deficiency getDeficiency() {
215            return deficiency;
216        }
217    }
218}