diff --git a/documentation/extensions/consistent_colors.md b/documentation/extensions/consistent_colors.md new file mode 100644 index 000000000..ea59716b2 --- /dev/null +++ b/documentation/extensions/consistent_colors.md @@ -0,0 +1,37 @@ +Consistent Colors +================= + +[Back](index.md) + +Since XMPP can be used on multiple platforms at the same time, +it might be a good idea to render given Strings like nicknames in the same +color on all platforms to provide a consistent user experience. + +The utility class `ConsistentColor` allows the generation of colors to a given +string following the specification of [XEP-0392](https://xmpp.org/extensions/xep-0392.html). + +##Usage +To generate a consistent color for a given string, call +``` +float[] rgb = ConsistentColor.RGBFrom(input); +``` +The resulting float array contains values for RGB in the range of 0 to 1. + +##Color Deficiency Corrections +Some users might suffer from color vision deficiencies. To compensate those deficiencies, +the API allows for color correction. The color correction mode is a static value, which can be changed at any time. + +To correct colors for users with red-green color deficiency use the following code: +``` +ConsistentColor.activateRedGreenBlindnessCorrection(); +``` + +For color correction for users with blue-blindness, call +``` +ConsistentColor.activateBlueBlindnessCorrection(); +``` + +To deactivate color vision deficiency correction, call +``` +ConsistenColor.deactivateDeficiencyCorrection(); +``` \ No newline at end of file diff --git a/documentation/extensions/index.md b/documentation/extensions/index.md index abc5e2f1f..687fabc0c 100644 --- a/documentation/extensions/index.md +++ b/documentation/extensions/index.md @@ -95,6 +95,7 @@ Experimental Smack Extensions and currently supported XEPs of smack-experimental | HTTP File Upload | [XEP-0363](http://xmpp.org/extensions/xep-0363.html) | Protocol to request permissions to upload a file to an HTTP server and get a shareable URL. | | [Multi-User Chat Light](muclight.md) | [XEP-xxxx](http://mongooseim.readthedocs.io/en/latest/open-extensions/xeps/xep-muc-light.html) | Multi-User Chats for mobile XMPP applications and specific enviroment. | | [OMEMO Multi End Message and Object Encryption](omemo.md) | [XEP-XXXX](https://conversations.im/omemo/xep-omemo.html) | Encrypt messages using OMEMO encryption (currently only with smack-omemo-signal -> GPLv3). | +| [Consistent Color Generation](consistent_colors.md) | [XEP-0392](http://xmpp.org/extensions/xep-0392.html) | Generate consistent colors for identifiers like usernames to provide a consistent user experience. | | Google GCM JSON payload | n/a | Semantically the same as XEP-0335: JSON Containers | diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/colors/ConsistentColor.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/colors/ConsistentColor.java new file mode 100644 index 000000000..a9610c719 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/colors/ConsistentColor.java @@ -0,0 +1,205 @@ +/** + * + * Copyright © 2018 Paul Schaub + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.colors; + +import org.jivesoftware.smack.util.SHA1; + +public class ConsistentColor { + + // See XEP-0392 §13.1 Constants for YCbCr (BT.601) + private static final double KR = 0.299; + private static final double KG = 0.587; + private static final double KB = 0.114; + + // See XEP-0392 §5.4 CbCr to RGB + private static final double Y = 0.732; + + public enum Deficiency { + none, + redGreenBlindness, + blueBlindness + } + + /** + * Generate an angle in the CbCr plane from the input string. + * @see §5.1: Angle generation + * + * @param input input string + * @return output angle + */ + private static double createAngle(CharSequence input) { + byte[] h = SHA1.bytes(input.toString()); + double v = u(h[0]) + (256 * u(h[1])); + double d = v / 65536; + return d * 2 * Math.PI; + } + + /** + * Apply correction for color vision deficiencies to an angle in the CbCr plane. + * @see §5.2: Corrections for Color Vision Deficiencies + * + * @param angle angle in CbCr plane + * @param deficiency type of vision deficiency + * @return corrected angle in CbCr plane + */ + private static double applyColorDeficiencyCorrection(double angle, Deficiency deficiency) { + double _angle = angle; + switch (deficiency) { + case none: + break; + case redGreenBlindness: + _angle = _angle % Math.PI; + break; + case blueBlindness: + _angle -= Math.PI / 2; + _angle = _angle % Math.PI; + _angle += Math.PI / 2; + break; + } + return _angle; + } + + /** + * Convert an angle in the CbCr plane to values cb,cr in the YCbCr color space. + * @see §5.3: CbCr generation + * + * @param angle angel in CbCr plane. + * @return value pair cb,cr + */ + private static double[] angleToCbCr(double angle) { + double cb = Math.cos(angle); + double cr = Math.sin(angle); + + double acb = Math.abs(cb); + double acr = Math.abs(cr); + double factor; + if (acr > acb) { + factor = 0.5 / acr; + } else { + factor = 0.5 / acb; + } + + cb *= factor; + cr *= factor; + + return new double[] {cb, cr}; + } + + /** + * Convert a value pair cb,cr in the YCbCr color space to RGB. + * @see §5.4: CbCr to RGB + * + * @param cbcr value pair from the YCbCr color space + * @return RGB value triple (R,G,B in [0,1]) + */ + private static float[] CbCrToRGB(double[] cbcr, double y) { + double cb = cbcr[0]; + double cr = cbcr[1]; + + double r = 2 * (1 - KR) * cr + y; + double b = 2 * (1 - KB) * cb + y; + double g = (y - KR * r - KB * b) / KG; + + // Clip values to [0,1] + r = clip(r); + g = clip(g); + b = clip(b); + + return new float[] {(float) r, (float) g, (float) b}; + } + + /** + * Clip values to stay in range(0,1). + * + * @param value input + * @return input clipped to stay in boundaries from 0 to 1. + */ + private static double clip(double value) { + double out = value; + + if (value < 0) { + out = 0; + } + + if (value > 1) { + out = 1; + } + + return out; + } + + /** + * Treat a signed java byte as unsigned to get its numerical value. + * + * @param b signed java byte + * @return integer value of its unsigned representation + */ + private static int u(byte b) { + // Get unsigned value of signed byte as an integer. + return b & 0xFF; + } + + /** + * Return the consistent RGB color value for the input. + * This method respects the color vision deficiency mode set by the user. + * + * @param input input string (for example username) + * @return consistent color of that username as RGB values in range [0,1]. + */ + public static float[] RGBFrom(CharSequence input, Context context) { + double angle = createAngle(input); + double correctedAngle = applyColorDeficiencyCorrection(angle, context.getDeficiency()); + double[] CbCr = angleToCbCr(correctedAngle); + float[] rgb = CbCrToRGB(CbCr, Y); + return rgb; + } + + public static class Context { + + private Deficiency deficiency = Deficiency.none; + + /** + * Activate color correction for users suffering from red-green-blindness. + */ + public void activateRedGreenBlindnessCorrection() { + deficiency = Deficiency.redGreenBlindness; + } + + /** + * Activate color correction for users suffering from blue-blindness. + */ + public void activateBlueBlindnessCorrection() { + deficiency = Deficiency.blueBlindness; + } + + /** + * Deactivate color vision deficiency correction. + */ + public void deactivateDeficiencyCorrection() { + deficiency = Deficiency.none; + } + + /** + * Return the deficiency setting. + * + * @return deficiency setting. + */ + public Deficiency getDeficiency() { + return deficiency; + } + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/colors/package-info.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/colors/package-info.java new file mode 100644 index 000000000..ae50393fc --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/colors/package-info.java @@ -0,0 +1,21 @@ +/** + * + * Copyright © 2018 Paul Schaub + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Smack's API for XEP-392: Consistent Color Generation. + */ +package org.jivesoftware.smackx.colors; diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/colors/ConsistentColorsTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/colors/ConsistentColorsTest.java new file mode 100644 index 000000000..847f46c7f --- /dev/null +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/colors/ConsistentColorsTest.java @@ -0,0 +1,168 @@ +/** + * + * Copyright © 2018 Paul Schaub + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.colors; + +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertTrue; + +import org.jivesoftware.smack.test.util.SmackTestSuite; + +import org.junit.Test; + +public class ConsistentColorsTest extends SmackTestSuite { + + // Margin of error we allow due to floating point arithmetic + private static final float EPS = 0.001f; + + private static final ConsistentColor.Context noDeficiency = new ConsistentColor.Context(); + private static final ConsistentColor.Context redGreenDeficiency = new ConsistentColor.Context(); + private static final ConsistentColor.Context blueBlindnessDeficiency = new ConsistentColor.Context(); + + public ConsistentColorsTest() { + noDeficiency.deactivateDeficiencyCorrection(); + redGreenDeficiency.activateRedGreenBlindnessCorrection(); + blueBlindnessDeficiency.activateBlueBlindnessCorrection(); + } + + /* + Below tests check the test vectors from XEP-0392 §13.2. + */ + + @Test + public void romeoNoDeficiencyTest() { + String value = "Romeo"; + float[] expected = new float[] {0.281f, 0.790f, 1.000f}; + float[] actual = ConsistentColor.RGBFrom(value, noDeficiency); + assertRGBEquals(expected, actual, EPS); + } + + @Test + public void romeoRedGreenBlindnessTest() { + String value = "Romeo"; + float[] expected = new float[] {1.000f, 0.674f, 0.000f}; + float[] actual = ConsistentColor.RGBFrom(value, redGreenDeficiency); + assertRGBEquals(expected, actual, EPS); + } + + @Test + public void romeoBlueBlindnessTest() { + String value = "Romeo"; + float[] expected = new float[] {1.000f, 0.674f, 0.000f}; + float[] actual = ConsistentColor.RGBFrom(value, blueBlindnessDeficiency); + assertRGBEquals(expected, actual, EPS); + } + + @Test + public void julietNoDeficiencyTest() { + String value = "juliet@capulet.lit"; + float[] expected = new float[] {0.337f, 1.000f, 0.000f}; + float[] actual = ConsistentColor.RGBFrom(value, noDeficiency); + assertRGBEquals(expected, actual, EPS); + } + + @Test + public void julietRedGreenBlindnessTest() { + String value = "juliet@capulet.lit"; + float[] expected = new float[] {1.000f, 0.359f, 1.000f}; + float[] actual = ConsistentColor.RGBFrom(value, redGreenDeficiency); + assertRGBEquals(expected, actual, EPS); + } + + @Test + public void julietBlueBlindnessTest() { + String value = "juliet@capulet.lit"; + float[] expected = new float[] {0.337f, 1.000f, 0.000f}; + float[] actual = ConsistentColor.RGBFrom(value, blueBlindnessDeficiency); + assertRGBEquals(expected, actual, EPS); + } + + @Test + public void emojiNoDeficiencyTest() { + String value = "\uD83D\uDE3A"; + float[] expected = new float[] {0.347f, 0.756f, 1.000f}; + float[] actual = ConsistentColor.RGBFrom(value, noDeficiency); + assertRGBEquals(expected, actual, EPS); + } + + @Test + public void emojiRedGreenBlindnessTest() { + String value = "\uD83D\uDE3A"; + float[] expected = new float[] {1.000f, 0.708f, 0.000f}; + float[] actual = ConsistentColor.RGBFrom(value, redGreenDeficiency); + assertRGBEquals(expected, actual, EPS); + } + + @Test + public void emojiBlueBlindnessTest() { + String value = "\uD83D\uDE3A"; + float[] expected = new float[] {1.000f, 0.708f, 0.000f}; + float[] actual = ConsistentColor.RGBFrom(value, blueBlindnessDeficiency); + assertRGBEquals(expected, actual, EPS); + } + + @Test + public void councilNoDeficiencyTest() { + String value = "council"; + float[] expected = new float[] {0.732f, 0.560f, 1.000f}; + float[] actual = ConsistentColor.RGBFrom(value, noDeficiency); + assertRGBEquals(expected, actual, EPS); + } + + @Test + public void councilRedGreenBlindnessTest() { + String value = "council"; + float[] expected = new float[] {0.732f, 0.904f, 0.000f}; + float[] actual = ConsistentColor.RGBFrom(value, redGreenDeficiency); + assertRGBEquals(expected, actual, EPS); + } + + @Test + public void councilBlueBlindnessTest() { + String value = "council"; + float[] expected = new float[] {0.732f, 0.904f, 0.000f}; + float[] actual = ConsistentColor.RGBFrom(value, blueBlindnessDeficiency); + assertRGBEquals(expected, actual, EPS); + } + + @Test + public void contextGetterTest() { + ConsistentColor.Context context = new ConsistentColor.Context(); + assertEquals(ConsistentColor.Deficiency.none, context.getDeficiency()); + context.activateBlueBlindnessCorrection(); + assertEquals(ConsistentColor.Deficiency.blueBlindness, context.getDeficiency()); + context.activateRedGreenBlindnessCorrection(); + assertEquals(ConsistentColor.Deficiency.redGreenBlindness, context.getDeficiency()); + context.deactivateDeficiencyCorrection(); + assertEquals(ConsistentColor.Deficiency.none, context.getDeficiency()); + } + + /** + * Check, whether the values of two float arrays of size 3 are pairwise equal with an allowed error of eps. + * + * @param expected expected values + * @param actual actual values + * @param eps allowed error + */ + private static void assertRGBEquals(float[] expected, float[] actual, float eps) { + assertEquals(3, expected.length); + assertEquals(3, actual.length); + + for (int i = 0; i < actual.length; i++) { + assertTrue(Math.abs(expected[i] - actual[i]) < eps); + } + } +}