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);
+ }
+ }
+}