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}