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}