--- /dev/null
+/*
+ * Copyright (c) 2021 Samsung Electronics Co., Ltd.
+ *
+ * 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.
+ *
+ */
+
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Modified by Woochan Lee(wc0917.lee@samsung.com)
+ */
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Tizen.NUI
+{
+ internal sealed class ColorCutQuantizer
+ {
+ private const float blackMaxLightness = 0.05f;
+ private const float whiteMinLightness = 0.95f;
+ private const int componentRed = -3;
+ private const int componentGreen = -2;
+ private const int componentBlue = -1;
+
+ private static Dictionary<int, int> colorPopulations;
+ private static int[] colors;
+ private List<Palette.Swatch> quantizedColors;
+ private float[] tempHsl = new float[3];
+
+ private ColorCutQuantizer(ColorHistogram colorHistogram, int maxColors)
+ {
+ if (colorHistogram == null)
+ {
+ throw new ArgumentNullException(nameof(colorHistogram), "colorHistogram should not be null.");
+ }
+
+ if (maxColors < 1)
+ {
+ throw new ArgumentNullException(nameof(maxColors), "maxColors should not be null.");
+ }
+
+ int rawColorCount = colorHistogram.GetNumberOfColors();
+ int[] rawColors = colorHistogram.GetColors();
+ int[] rawColorCounts = colorHistogram.GetColorCounts();
+
+ // First, lets pack the populations into a SparseIntArray so that they can be easily
+ // retrieved without knowing a color's index
+ colorPopulations = new Dictionary<int, int>();
+
+ for (int i = 0; i < rawColors.Length; i++)
+ {
+ colorPopulations.Add(rawColors[i], rawColorCounts[i]);
+ }
+
+ // Now go through all of the colors and keep those which we do not want to ignore
+ colors = new int[rawColorCount];
+ int validColorCount = 0;
+
+ foreach (int color in rawColors)
+ {
+ if (!ShouldIgnoreColor(color))
+ {
+ colors[validColorCount++] = color;
+ }
+ }
+
+ Tizen.Log.Info("Palette", "ValidColorCount = " + validColorCount + "\n");
+ if (validColorCount <= maxColors)
+ {
+ // The image has fewer colors than the maximum requested, so just return the colors
+ quantizedColors = new List<Palette.Swatch>();
+
+ for (int i = 0; i < validColorCount; i++)
+ {
+ quantizedColors.Add(new Palette.Swatch(colors[i], colorPopulations[colors[i]]));
+ }
+ }
+ else
+ {
+
+ Tizen.Log.Info("Palette", "validColorCount = " + validColorCount + " maxColors = " + maxColors + "\n");
+ // We need use quantization to reduce the number of colors
+ quantizedColors = QuantizePixels(validColorCount - 1, maxColors);
+ }
+ }
+
+ /// <summary>
+ /// Factory-method to generate a ColorCutQuantizer from a PixelBuffer object.
+ /// </summary>
+ public static ColorCutQuantizer FromBitmap(PixelBuffer pixelBuffer, Rectangle region, int maxColors)
+ {
+ int width;
+ int height;
+ int[] pixels;
+ int i, j, index = 0;
+
+ if (region == null)
+ {
+ width = (int)pixelBuffer.GetWidth(); height = (int)pixelBuffer.GetHeight(); i = 0; j = 0;
+ }
+
+ else
+ {
+ width = region.Width; height = region.Height; i = region.X; j = region.Y;
+ }
+
+ Tizen.Log.Info("Palette", "Get pixels rawdata from (" + i + " " + j + " " + width + " " + height + ")" + "\n");
+
+ pixels = new int[width * height];
+ PixelFormat format = pixelBuffer.GetPixelFormat();
+ int pixelLength = (int)ColorUtils.GetBytesPerPixel(format);
+ IntPtr bufferIntPtr = pixelBuffer.GetBuffer();
+
+ unsafe
+ {
+ byte *rawdata = (byte *)bufferIntPtr.ToPointer();
+ int totalLength = width * height * pixelLength;
+ for (i = 0; i < totalLength; i += pixelLength)
+ {
+ //RGB888
+ if (pixelLength == 3)
+ pixels[index++] = (255 & 0xff) << 24 | (rawdata[i] & 0xff) << 16 | (rawdata[i+1] & 0xff) << 8 | (rawdata[i+2] & 0xff);
+ //RGBA8888
+ else
+ pixels[index++] = (rawdata[i + 3] & 0xff) << 24 | (rawdata[i] & 0xff) << 16 | (rawdata[i+1] & 0xff) << 8 | (rawdata[i+2] & 0xff);
+ }
+ }
+
+ return new ColorCutQuantizer(new ColorHistogram(pixels), maxColors);
+ }
+
+ /// <summary>
+ /// return the list of quantized colors
+ /// </summary>
+ public List<Palette.Swatch> GetQuantizedColors()
+ {
+ return quantizedColors;
+ }
+
+ private List<Palette.Swatch> QuantizePixels(int maxColorIndex, int maxColors)
+ {
+ // Create the priority queue which is sorted by volume descending. This means we always
+ // split the largest box in the queue
+ CustomHeap<Vbox> customHeap = new CustomHeap<Vbox>(new VboxComparatorVolume());
+ // To start, offer a box which contains all of the colors
+ customHeap.Offer(new Vbox(0, maxColorIndex));
+ // Now go through the boxes, splitting them until we have reached maxColors or there are no
+ // more boxes to split
+ SplitBoxes(customHeap, maxColors);
+ // Finally, return the average colors of the color boxes
+ return GenerateAverageColors(customHeap);
+ }
+
+ /// <summary>
+ /// Iterate through the queue, popping
+ /// ColorCutQuantizer.Vbox objects from the queue
+ /// and splitting them. Once split, the new box and the remaining box are offered back to the
+ /// queue.
+ ///
+ /// param queue PriorityQueue to poll for boxes
+ /// param maxSize Maximum amount of boxes to split
+ /// </summary>
+ private void SplitBoxes(CustomHeap<Vbox> queue, int maxSize)
+ {
+ int i = 0;
+ while (queue.count < maxSize)
+ {
+ i++;
+ Vbox vbox = queue.Poll();
+ if (vbox != null && vbox.CanSplit())
+ {
+ // First split the box, and offer the result
+ queue.Offer(vbox.SplitBox());
+ // Then offer the box back
+ queue.Offer(vbox);
+ }
+ else
+ {
+ // If we get here then there are no more boxes to split, so return
+ return;
+ }
+ }
+ }
+
+ private List<Palette.Swatch> GenerateAverageColors(CustomHeap<Vbox> vboxes)
+ {
+ List<Palette.Swatch> colors = new List<Palette.Swatch>(vboxes.count);
+ foreach (Vbox vbox in vboxes)
+ {
+ Palette.Swatch color = vbox.GetAverageColor();
+ if (!ShouldIgnoreColor(color))
+ {
+ // As we're averaging a color box, we can still get colors which we do not want, so
+ // we check again here
+ colors.Add(color);
+ }
+ }
+
+ Tizen.Log.Info("Palette", "Final generated color count = " + colors.Count + "\n");
+ return colors;
+ }
+
+ private Boolean ShouldIgnoreColor(int color)
+ {
+ ColorUtils.RgbToHsl((color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff, tempHsl);
+ return ShouldIgnoreColor(tempHsl);
+ }
+
+ private static Boolean ShouldIgnoreColor(Palette.Swatch color)
+ {
+ return ShouldIgnoreColor(color.GetHsl());
+ }
+
+ private static Boolean ShouldIgnoreColor(float[] hslColor)
+ {
+ return IsWhite(hslColor) || IsBlack(hslColor) || IsNearRedILine(hslColor);
+ }
+
+ /// <summary>
+ ///return true if the color represents a color which is close to black.
+ /// </summary>
+ private static Boolean IsBlack(float[] hslColor)
+ {
+ return hslColor[2] <= blackMaxLightness;
+ }
+
+ /// <summary>
+ /// return true if the color represents a color which is close to white.
+ /// </summary>
+ private static Boolean IsWhite(float[] hslColor)
+ {
+ return hslColor[2] >= whiteMinLightness;
+ }
+
+ /// <summary>
+ /// return true if the color lies close to the red side of the I line.
+ /// </summary>
+ private static Boolean IsNearRedILine(float[] hslColor)
+ {
+ return hslColor[0] >= 10f && hslColor[0] <= 37f && hslColor[1] <= 0.82f;
+ }
+
+ private sealed class CustomHeap<T> : IEnumerable<T>
+ {
+ private const int initialcapacity = 0;
+ private const int growFactor = 2;
+ private const int minGrow = 1;
+
+ private int capacity = initialcapacity;
+ private int tail = 0;
+ private T[] heap = Array.Empty<T>();
+
+ public CustomHeap(Comparer<T> comparer)
+ {
+ if (comparer == null) throw new ArgumentNullException(nameof(comparer), "comparer is null");
+ Comparer = comparer;
+ }
+
+ private Comparer<T> Comparer { get; set; }
+
+ public IEnumerator<T> GetEnumerator()
+ {
+ return heap.Take(count).GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ public int count { get { return tail; } }
+
+ public void Offer(T item)
+ {
+ if (count == capacity)
+ Grow();
+
+ heap[tail++] = item;
+ BubbleUp(tail - 1);
+ }
+
+ public T Peek()
+ {
+ if (count == 0) throw new InvalidOperationException("CustomHeap is empty");
+ return heap[0];
+ }
+
+ public T Poll()
+ {
+ if (count == 0) throw new InvalidOperationException("CustomHeap is empty");
+ T ret = heap[0];
+
+ tail--;
+ Swap(tail, 0);
+ BubbleDown(0);
+
+ return ret;
+ }
+
+ private void BubbleDown(int i)
+ {
+ int dominatingNode = Dominating(i);
+
+ if (dominatingNode == i) return;
+ Swap(i, dominatingNode);
+ BubbleDown(dominatingNode);
+ }
+
+ private void BubbleUp(int i)
+ {
+ if (i == 0 || Dominates(heap[Parent(i)], heap[i]))
+ return;
+
+ Swap(i, Parent(i));
+ BubbleUp(Parent(i));
+ }
+
+ private int Dominating(int i)
+ {
+ int dominatingNode = i;
+
+ dominatingNode = GetDominating(YoungChild(i), dominatingNode);
+ dominatingNode = GetDominating(OldChild(i), dominatingNode);
+ return dominatingNode;
+ }
+
+ private int GetDominating(int newNode, int dominatingNode)
+ {
+ if (newNode < tail && !Dominates(heap[dominatingNode], heap[newNode]))
+ return newNode;
+ else
+ return dominatingNode;
+ }
+
+ private void Swap(int i, int j)
+ {
+ T tmp = heap[i];
+
+ heap[i] = heap[j];
+ heap[j] = tmp;
+ }
+
+ private static int Parent(int i)
+ {
+ return (i + 1) / 2 - 1;
+ }
+
+ private static int YoungChild(int i)
+ {
+ return (i + 1) * 2 - 1;
+ }
+
+ private static int OldChild(int i)
+ {
+ return YoungChild(i) + 1;
+ }
+
+ private void Grow()
+ {
+ int newcapacity = capacity * growFactor + minGrow;
+ var newHeap = new T[newcapacity];
+
+ Array.Copy(heap, newHeap, capacity);
+ heap = newHeap;
+ capacity = newcapacity;
+ }
+
+ private bool Dominates(T x, T y)
+ {
+ return Comparer.Compare(x, y) <= 0;
+ }
+
+ }
+
+ /// <summary>
+ /// Comparator which sorts Vbox instances based on their volume, in descending order
+ /// </summary>
+ private sealed class VboxComparatorVolume : Comparer<Vbox>
+ {
+ public override int Compare(Vbox lhs, Vbox rhs)
+ {
+ return rhs.GetVolume() - lhs.GetVolume();
+ }
+ }
+
+ /// <summary>
+ /// Represents a tightly fitting box around a color space.
+ /// </summary>
+ private sealed class Vbox
+ {
+ private int lowerIndex;
+ private int upperIndex;
+ private int minRed, maxRed, minGreen, maxGreen, minBlue, maxBlue;
+
+ public Vbox(int lowerIndex, int upperIndex)
+ {
+ this.lowerIndex = lowerIndex;
+ this.upperIndex = upperIndex;
+ FitBox();
+ }
+
+ public int GetVolume()
+ {
+ return (maxRed - minRed + 1) * (maxGreen - minGreen + 1) * (maxBlue - minBlue + 1);
+ }
+
+ public Boolean CanSplit()
+ {
+ return GetColorCount() > 1;
+ }
+
+ public int GetColorCount()
+ {
+ return upperIndex - lowerIndex;
+ }
+
+ /// <summary>
+ /// Recomputes the boundaries of this box to tightly fit the colors within the box.
+ /// </summary>
+ public void FitBox()
+ {
+ // Reset the min and max to opposite values
+ minRed = minGreen = minBlue = 0xff;
+ maxRed = maxGreen = maxBlue = 0x0;
+ for (int i = lowerIndex; i <= upperIndex; i++)
+ {
+ int red = (colors[i] >> 16) & 0xff;
+ int green = (colors[i] >> 8) & 0xff;
+ int blue = colors[i] & 0xff;
+
+ maxRed = red > maxRed ? red : maxRed;
+ minRed = red < minRed ? red : minRed;
+ maxGreen = green > maxGreen ? green : maxGreen;
+ minGreen = green < minGreen ? green : minGreen;
+ maxBlue = blue > maxBlue ? blue : maxBlue;
+ minBlue = blue < minBlue ? blue : minBlue;
+ }
+ }
+
+ /// <summary>
+ /// Split this color box at the mid-point along it's longest dimension
+ ///
+ /// return the new ColorBox
+ /// </summary>
+ public Vbox SplitBox()
+ {
+ if (!CanSplit())
+ {
+ throw new InvalidOperationException("Can not split a box with only 1 color");
+ }
+
+ // find median along the longest dimension
+ int splitPoint = FindSplitPoint();
+
+ Vbox newBox = new Vbox(splitPoint + 1, upperIndex);
+ // Now change this box's upperIndex and recompute the color boundaries
+ upperIndex = splitPoint;
+ FitBox();
+
+ return newBox;
+ }
+
+ /// <summary>
+ /// return the dimension which this box is largest in
+ /// </summary>
+ public int GetLongestColorDimension()
+ {
+ int redLength = maxRed - minRed;
+ int greenLength = maxGreen - minGreen;
+ int blueLength = maxBlue - minBlue;
+
+ if (redLength >= greenLength && redLength >= blueLength)
+ {
+ return componentRed;
+ }
+ else if (greenLength >= redLength && greenLength >= blueLength)
+ {
+ return componentGreen;
+ }
+ else
+ {
+ return componentBlue;
+ }
+ }
+
+ /// <summary>
+ /// Finds the point within this box's lowerIndex and upperIndex index of where to split.
+ ///
+ /// This is calculated by finding the longest color dimension, and then sorting the
+ /// sub-array based on that dimension value in each color. The colors are then iterated over
+ /// until a color is found with at least the midpoint of the whole box's dimension midpoint.
+ ///
+ /// return the index of the colors array to split from
+ /// </summary>
+ public int FindSplitPoint()
+ {
+ int longestDimension = GetLongestColorDimension();
+
+ // We need to sort the colors in this box based on the longest color dimension.
+ // As we can't use a Comparator to define the sort logic, we modify each color so that
+ // it's most significant is the desired dimension
+ ModifySignificantOctet(longestDimension, lowerIndex, upperIndex);
+
+ Array.Sort(colors, lowerIndex, upperIndex + 1 - lowerIndex);
+
+ // Now revert all of the colors so that they are packed as RGB again
+ ModifySignificantOctet(longestDimension, lowerIndex, upperIndex);
+
+ int dimensionMidPoint = MidPoint(longestDimension);
+ for (int i = lowerIndex; i < upperIndex; i++)
+ {
+ switch (longestDimension)
+ {
+ case componentRed:
+ if (((colors[i] >> 16) & 0xff) >= dimensionMidPoint)
+ {
+ return i;
+ }
+ break;
+ case componentGreen:
+ if (((colors[i] >> 8) & 0xff) >= dimensionMidPoint)
+ {
+ return i;
+ }
+ break;
+ case componentBlue:
+ if ((colors[i] &0xff) > dimensionMidPoint)
+ {
+ return i;
+ }
+ break;
+ }
+ }
+
+ return lowerIndex;
+ }
+
+ /// <summary>
+ /// return the average color of this box.
+ /// </summary>
+ public Palette.Swatch GetAverageColor()
+ {
+ int redSum = 0;
+ int greenSum = 0;
+ int blueSum = 0;
+ int totalPopulation = 0;
+
+ for (int i = lowerIndex; i <= upperIndex; i++)
+ {
+ int colorPopulation = colorPopulations[colors[i]];
+ totalPopulation += colorPopulation;
+ redSum += colorPopulation * ((colors[i] >> 16) & 0xff);
+ greenSum += colorPopulation * ((colors[i] >> 8) & 0xff);
+ blueSum += colorPopulation * (colors[i] & 0xff);
+ }
+
+ int redAverage = (int)Math.Round(redSum / (float)totalPopulation);
+ int greenAverage = (int)Math.Round(greenSum / (float)totalPopulation);
+ int blueAverage = (int)Math.Round(blueSum / (float)totalPopulation);
+
+ return new Palette.Swatch(redAverage, greenAverage, blueAverage, totalPopulation);
+ }
+
+ /// <summary>
+ /// return the midpoint of this box in the given dimension
+ /// </summary>
+ private int MidPoint(int dimension)
+ {
+ switch (dimension)
+ {
+ case componentRed:
+ default:
+ return (minRed + maxRed) / 2;
+ case componentGreen:
+ return (minGreen + maxGreen) / 2;
+ case componentBlue:
+ return (minBlue + maxBlue) / 2;
+ }
+ }
+
+ /// <summary>
+ /// Modify the significant octet in a packed color int. Allows sorting based on the value of a
+ /// single color component.
+ ///
+ /// see Vbox#findSplitPoint()
+ /// </summary>
+ private void ModifySignificantOctet(int dimension, int lowIndex, int highIndex)
+ {
+ switch (dimension)
+ {
+ case componentRed:
+ // Already in RGB, no need to do anything
+ break;
+ case componentGreen:
+ // We need to do a RGB to GRB swap, or vice-versa
+ for (int i = lowIndex; i <= highIndex; i++)
+ {
+ int color = colors[i];
+ colors[i] = 255 << 24 | (color >> 8 & 0xff) << 16 | (color >> 16 & 0xff) << 8 | (color & 0xff);
+ }
+
+ break;
+ case componentBlue:
+ // We need to do a RGB to BGR swap, or vice-versa
+ for (int i = lowIndex; i <= highIndex; i++)
+ {
+ int color = colors[i];
+ colors[i] = 255 << 24 | (color & 0xff) << 16 | (color >> 8 & 0xff) << 8 | (color >> 16 & 0xff);
+ }
+ break;
+ }
+ }
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2021 Samsung Electronics Co., Ltd.
+ *
+ * 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.
+ *
+ */
+
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Modified by Woochan Lee(wc0917.lee@samsung.com)
+ */
+
+using System;
+
+namespace Tizen.NUI
+{
+ internal sealed class ColorHistogram
+ {
+ private int numberColors;
+ private int[] colors;
+ private int[] colorCounts;
+
+ /// <summary>
+ /// A new ColorHistogram instance.
+ /// </summary>
+ public ColorHistogram(int[] pixels)
+ {
+ // Sort the pixels to enable counting below
+ Array.Sort(pixels);
+
+ // Count number of distinct colors
+ numberColors = CountDistinctColors(pixels);
+ Tizen.Log.Info("Palette", "DistinctColor Num = " + numberColors + "\n");
+
+ // Create arrays
+ colors = new int[numberColors];
+ colorCounts = new int[numberColors];
+
+ // Finally count the frequency of each color
+ CountFrequencies(pixels);
+ }
+
+ /// <summary>
+ /// return number of distinct colors in the image.
+ /// </summary>
+ public int GetNumberOfColors()
+ {
+ return numberColors;
+ }
+
+ /// <summary>
+ /// return an array containing all of the distinct colors in the image.
+ /// </summary>
+ public int[] GetColors()
+ {
+ return colors;
+ }
+
+ /// <summary>
+ /// return an array containing the frequency of a distinct colors within the image.
+ /// </summary>
+ public int[] GetColorCounts()
+ {
+ return colorCounts;
+ }
+
+ private static int CountDistinctColors(int[] pixels)
+ {
+ if (pixels.Length < 2)
+ {
+ // If we have less than 2 pixels we can stop here
+ return pixels.Length;
+ }
+ // If we have at least 2 pixels, we have a minimum of 1 color...
+ int colorCount = 1;
+ int currentColor = pixels[0];
+
+ // Now iterate from the second pixel to the end, counting distinct colors
+ for (int i = 1; i < pixels.Length; i++)
+ {
+ // If we encounter a new color, increase the population
+ if (pixels[i] != currentColor)
+ {
+ currentColor = pixels[i];
+ colorCount++;
+ }
+ }
+
+ return colorCount;
+ }
+
+ private void CountFrequencies(int[] pixels)
+ {
+ if (pixels.Length == 0)
+ {
+ return;
+ }
+
+ int currentColorIndex = 0;
+ int currentColor = pixels[0];
+
+ colors[currentColorIndex] = currentColor;
+ colorCounts[currentColorIndex] = 1;
+
+ if (pixels.Length == 1)
+ {
+ // If we only have one pixel, we can stop here
+ return;
+ }
+
+ // Now iterate from the second pixel to the end, population distinct colors
+ for (int i = 1; i < pixels.Length; i++)
+ {
+ if (pixels[i] == currentColor)
+ {
+ // We've hit the same color as before, increase population
+ colorCounts[currentColorIndex]++;
+ }
+ else
+ {
+ // We've hit a new color, increase index
+ currentColor = pixels[i];
+ currentColorIndex++;
+ colors[currentColorIndex] = currentColor;
+ colorCounts[currentColorIndex] = 1;
+ }
+ }
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2021 Samsung Electronics Co., Ltd.
+ *
+ * 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.
+ *
+ */
+
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Modified by Woochan Lee(wc0917.lee@samsung.com)
+ */
+
+using System;
+
+namespace Tizen.NUI
+{
+ internal sealed class ColorUtils
+ {
+ private const int minAlphaSearchMaxIterations = 10;
+ private const int minAlphaSearchPrecision = 1;
+
+ /// <summary>
+ /// Convert the ARGB color to its CIE XYZ representative components.
+ ///
+ /// The resulting XYZ representation will use the D65 illuminant and the CIE
+ /// 2° Standard Observer (1931).
+ ///
+ /// outXyz[0] is X [0 ...95.047)
+ /// outXyz[1] is Y [0...100)
+ /// outXyz[2] is Z [0...108.883)
+ ///
+ /// param color the ARGB color to convert. The alpha component is ignored
+ /// param outXyz 3-element array which holds the resulting LAB components
+ /// </summary>
+ public static void ColorToXyz(int color, double[] outXyz)
+ {
+ RgbToXyz((color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff, outXyz);
+ }
+
+ /// <summary>
+ /// Convert RGB components to its CIE XYZ representative components.
+ ///
+ /// The resulting XYZ representation will use the D65 illuminant and the CIE
+ /// 2° Standard Observer (1931).
+ ///
+ /// outXyz[0] is X [0 ...95.047)
+ /// outXyz[1] is Y [0...100)
+ /// outXyz[2] is Z [0...108.883)
+ ///
+ /// r red component value [0..255]
+ /// g green component value [0..255]
+ /// b blue component value [0..255]
+ /// outXyz 3-element array which holds the resulting XYZ components
+ /// </summary>
+ public static void RgbToXyz(int red, int green, int blue, double[] outXyz)
+ {
+ if (outXyz.Length != 3)
+ {
+ throw new ArgumentException("Array legnth must be 3", nameof(outXyz));
+ }
+
+ double floatRed = red / 255.0;
+ floatRed = floatRed < 0.04045 ? floatRed / 12.92 : Math.Pow((floatRed + 0.055) / 1.055, 2.4);
+ double floatGreen = green / 255.0;
+ floatGreen = floatGreen < 0.04045 ? floatGreen / 12.92 : Math.Pow((floatGreen + 0.055) / 1.055, 2.4);
+ double floatBlue = blue / 255.0;
+ floatBlue = floatBlue < 0.04045 ? floatBlue / 12.92 : Math.Pow((floatBlue + 0.055) / 1.055, 2.4);
+
+ outXyz[0] = 100 * (floatRed * 0.4124 + floatGreen * 0.3576 + floatBlue * 0.1805);
+ outXyz[1] = 100 * (floatRed * 0.2126 + floatGreen * 0.7152 + floatBlue * 0.0722);
+ outXyz[2] = 100 * (floatRed * 0.0193 + floatGreen * 0.1192 + floatBlue * 0.9505);
+ }
+
+ /// <summary>
+ /// Returns the luminance of a color as a float between 0.0 and 1.0
+ /// Defined as the Y component in the XYZ representation of color.
+ /// </summary>
+ public static double CalculateLuminance(int color)
+ {
+ double[] result = new double[3];
+ ColorToXyz(color, result);
+ // Luminance is the Y component
+ return result[1] / 100;
+ }
+
+ /// <summary>
+ /// Composite two potentially translucent colors over each other and returns the result.
+ /// </summary>
+ public static int CompositeColors(int foreground, int background)
+ {
+ int bgAlpha = (background >> 24) & 0xff;
+ int fgAlpha = (foreground >> 24) & 0xff;
+
+ int alpha = CompositeAlpha(fgAlpha, bgAlpha);
+
+ int red = CompositeComponent((foreground >> 16) & 0xff, fgAlpha,
+ (background >> 16) & 0xff, bgAlpha, alpha);
+ int green = CompositeComponent((foreground >> 8) & 0xff, fgAlpha,
+ (background >> 8) & 0xff, bgAlpha, alpha);
+ int blue = CompositeComponent(foreground & 0xff, fgAlpha,
+ background & 0xff, bgAlpha, alpha);
+
+ return ((alpha & 0xff) << 24 | (red & 0xff) << 16 | (green & 0xff) << 8 | (blue & 0xff));
+ }
+
+ /// <summary>
+ /// Returns the contrast ratio between foreground and background.
+ /// background must be opaque.
+ ///
+ /// Formula defined
+ /// <a href="http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef">here</a>.
+ /// </summary>
+ public static double CalculateContrast(int foreground, int background)
+ {
+ if (((background >> 24) & 0xff) != 255)
+ {
+ throw new ArgumentException("background can not be translucent.");
+ }
+ if (((foreground >> 24) & 0xff) < 255)
+ {
+ // If the foreground is translucent, composite the foreground over the background
+ foreground = CompositeColors(foreground, background);
+ }
+
+ double luminance1 = CalculateLuminance(foreground) + 0.05;
+ double luminance2 = CalculateLuminance(background) + 0.05;
+
+ // Now return the lighter luminance divided by the darker luminance
+ return Math.Max(luminance1, luminance2) / Math.Min(luminance1, luminance2);
+ }
+
+ /// <summary>
+ /// Set the alpha component of color to be alpha.
+ /// </summary>
+ public static int SetAlphaComponent(int color, int alpha)
+ {
+ if (alpha < 0 || alpha > 255)
+ {
+ throw new ArgumentException("alpha must be between 0 and 255.");
+ }
+
+ return (color & 0x00ffffff) | (alpha << 24);
+ }
+
+ /// <summary>
+ /// Calculates the minimum alpha value which can be applied to foreground so that would
+ /// have a contrast value of at least minContrastRatio when compared to
+ /// background.
+ ///
+ /// param foreground the foreground color
+ /// param background the opaque background color
+ /// param minContrastRatio the minimum contrast ratio
+ /// return the alpha value in the range 0-255, or -1 if no value could be calculated
+ /// </summary>
+ public static int CalculateMinimumAlpha(int foreground, int background, float minContrastRatio)
+ {
+ if (((background >> 24) & 0xff) != 255)
+ {
+ throw new ArgumentException("background can not be translucent");
+ }
+
+ // First lets check that a fully opaque foreground has sufficient contrast
+ int testForeground = SetAlphaComponent(foreground, 255);
+ double testRatio = CalculateContrast(testForeground, background);
+
+ if (testRatio < minContrastRatio)
+ {
+ // Fully opaque foreground does not have sufficient contrast, return error
+ return -1;
+ }
+
+ // Binary search to find a value with the minimum value which provides sufficient contrast
+ int numIterations = 0;
+ int minAlpha = 0;
+ int maxAlpha = 255;
+
+ while (numIterations <= minAlphaSearchMaxIterations &&
+ (maxAlpha - minAlpha) > minAlphaSearchPrecision)
+ {
+ int testAlpha = (minAlpha + maxAlpha) / 2;
+
+ testForeground = SetAlphaComponent(foreground, testAlpha);
+ testRatio = CalculateContrast(testForeground, background);
+ if (testRatio < minContrastRatio)
+ {
+ minAlpha = testAlpha;
+ }
+ else
+ {
+ maxAlpha = testAlpha;
+ }
+
+ numIterations++;
+ }
+
+ // Conservatively return the max of the range of possible alphas, which is known to pass.
+ return maxAlpha;
+ }
+
+ public static void RgbToHsl(int red, int green, int blue, float[] hsl)
+ {
+ float floatRed = red / 255f;
+ float floatGreen = green / 255f;
+ float floatBlue = blue / 255f;
+ float max = Math.Max(floatRed, Math.Max(floatGreen, floatBlue));
+ float min = Math.Min(floatRed, Math.Min(floatGreen, floatBlue));
+ float deltaMaxMin = max - min;
+ float hue, saturation;
+ float lightness = (max + min) / 2f;
+ if (max == min)
+ {
+ // Monochromatic
+ hue = saturation = 0f;
+ }
+ else
+ {
+ if (max == floatRed)
+ {
+ hue = ((floatGreen - floatBlue) / deltaMaxMin) % 6f;
+ }
+ else if (max == floatGreen)
+ {
+ hue = ((floatBlue - floatRed) / deltaMaxMin) + 2f;
+ }
+ else
+ {
+ hue = ((floatRed - floatGreen) / deltaMaxMin) + 4f;
+ }
+ saturation = deltaMaxMin / (1f - Math.Abs(2f * lightness - 1f));
+ }
+ hsl[0] = ((hue * 60f) + 360f) % 360f;
+ hsl[1] = saturation;
+ hsl[2] = lightness;
+ }
+
+ public static int HslToRgb(float[] hsl)
+ {
+ float hue = hsl[0];
+ float saturation = hsl[1];
+ float lightness = hsl[2];
+ float constC = (1f - Math.Abs(2 * lightness - 1f)) * saturation;
+ float constM = lightness - 0.5f * constC;
+ float constX = constC * (1f - Math.Abs((hue / 60f % 2f) - 1f));
+ int hueSegment = (int)hue / 60;
+ int red = 0, green = 0, blue = 0;
+ switch (hueSegment)
+ {
+ case 0:
+ red = (int)Math.Round(255 * (constC + constM));
+ green = (int)Math.Round(255 * (constX + constM));
+ blue = (int)Math.Round(255 * constM);
+ break;
+ case 1:
+ red = (int)Math.Round(255 * (constX + constM));
+ green = (int)Math.Round(255 * (constC + constM));
+ blue = (int)Math.Round(255 * constM);
+ break;
+ case 2:
+ red = (int)Math.Round(255 * constM);
+ green = (int)Math.Round(255 * (constC + constM));
+ blue = (int)Math.Round(255 * (constX + constM));
+ break;
+ case 3:
+ red = (int)Math.Round(255 * constM);
+ green = (int)Math.Round(255 * (constX + constM));
+ blue = (int)Math.Round(255 * (constC + constM));
+ break;
+ case 4:
+ red = (int)Math.Round(255 * (constX + constM));
+ green = (int)Math.Round(255 * constM);
+ blue = (int)Math.Round(255 * (constC + constM));
+ break;
+ case 5:
+ case 6:
+ red = (int)Math.Round(255 * (constC + constM));
+ green = (int)Math.Round(255 * constM);
+ blue = (int)Math.Round(255 * (constX + constM));
+ break;
+ }
+ red = Math.Max(0, Math.Min(255, red));
+ green = Math.Max(0, Math.Min(255, green));
+ blue = Math.Max(0, Math.Min(255, blue));
+
+ //ARGB Encoding
+ return (255 << 24 | (red & 0xff) << 16 | (green & 0xff) << 8 | (blue & 0xff));
+ }
+
+ public static uint GetBytesPerPixel(PixelFormat pixelFormat)
+ {
+ switch (pixelFormat)
+ {
+ case PixelFormat.L8:
+ case PixelFormat.A8:
+ {
+ return 1;
+ }
+
+ case PixelFormat.LA88:
+ case PixelFormat.RGB565:
+ case PixelFormat.RGBA4444:
+ case PixelFormat.RGBA5551:
+ case PixelFormat.BGR565:
+ case PixelFormat.BGRA4444:
+ case PixelFormat.BGRA5551:
+ {
+ return 2;
+ }
+
+ case PixelFormat.RGB888:
+ {
+ return 3;
+ }
+
+ case PixelFormat.RGB8888:
+ case PixelFormat.BGR8888:
+ case PixelFormat.RGBA8888:
+ case PixelFormat.BGRA8888:
+ {
+ return 4;
+ }
+ default:
+ Tizen.Log.Error("Palette", "Invalied PixelFormat(" + pixelFormat + ") has been givien \n");
+ return 0;
+ }
+ }
+
+ /// <summary>
+ /// return luma value according to to XYZ color space in the range 0.0 - 1.0
+ /// </summary>
+ private static int CompositeAlpha(int foregroundAlpha, int backgroundAlpha)
+ {
+ return 0xFF - (((0xFF - backgroundAlpha) * (0xFF - foregroundAlpha)) / 0xFF);
+ }
+
+ private static int CompositeComponent(int fgC, int fgA, int bgC, int bgA, int alpha)
+ {
+ if (alpha == 0) return 0;
+ return ((0xFF * fgC * fgA) + (bgC * bgA * (0xFF - fgA))) / (alpha * 0xFF);
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2021 Samsung Electronics Co., Ltd.
+ *
+ * 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.
+ *
+ */
+
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Modified by Woochan Lee(wc0917.lee@samsung.com)
+ */
+
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Threading.Tasks;
+using System.Threading;
+
+namespace Tizen.NUI
+{
+ /// <summary>
+ /// A helper class to extract prominent colors from an image.
+ ///
+ /// A number of colors with different profiles are extracted from the image:
+ /// Vibrant, Vibrant Dark, Vibrant Light, Muted, Muted Dark, Muted Light
+ ///
+ /// These can be retrieved from the appropriate getter method.
+ ///
+ /// Palette supports both synchronous and asynchronous generation:
+ ///
+ /// Synchronous
+ /// Palette P = Palette.Generate(pixelBuffer);
+ ///
+ /// Asynchronous
+ /// Palette.GenerateAsync(pixelBuffer, (Palette p) => {
+ /// // Use generated instance
+ /// }};
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public sealed class Palette
+ {
+ private const int defaultCalculateNumberColors = 16;
+ private const int calculateBitmapMinDimension = 100;
+ private const float targetDarkLuma = 0.26f;
+ private const float maxDarkLuma = 0.45f;
+ private const float minLightLuma = 0.55f;
+ private const float targetLightLuma = 0.74f;
+ private const float minNormalLuma = 0.3f;
+ private const float targetNormalLuma = 0.5f;
+ private const float maxNormalLuma = 0.7f;
+ private const float targetMutedSaturation = 0.3f;
+ private const float maxMutedSaturation = 0.4f;
+ private const float targetVibrantSaturation = 1f;
+ private const float minVibrantSaturation = 0.35f;
+
+ private int highestPopulation;
+ private Swatch dominantSwatch;
+ private Swatch vibrantSwatch;
+ private Swatch mutedSwatch;
+ private Swatch darkVibrantSwatch;
+ private Swatch darkMutedSwatch;
+ private Swatch lightVibrantSwatch;
+ private Swatch lightMutedColor;
+ private List<Swatch> swatches;
+
+ private Palette() { }
+
+ private Palette(List<Swatch> swatcheList)
+ {
+ swatches = swatcheList;
+ highestPopulation = FindMaxPopulation();
+ vibrantSwatch = FindColor(targetNormalLuma, minNormalLuma, maxNormalLuma,
+ targetVibrantSaturation, minVibrantSaturation, 1f);
+
+ lightVibrantSwatch = FindColor(targetLightLuma, minLightLuma, 1f,
+ targetVibrantSaturation, minVibrantSaturation, 1f);
+
+ darkVibrantSwatch = FindColor(targetDarkLuma, 0f, maxDarkLuma,
+ targetVibrantSaturation, minVibrantSaturation, 1f);
+
+ mutedSwatch = FindColor(targetNormalLuma, minNormalLuma, maxNormalLuma,
+ targetMutedSaturation, 0f, maxMutedSaturation);
+
+ lightMutedColor = FindColor(targetLightLuma, minLightLuma, 1f,
+ targetMutedSaturation, 0f, maxMutedSaturation);
+
+ darkMutedSwatch = FindColor(targetDarkLuma, 0f, maxDarkLuma,
+ targetMutedSaturation, 0f, maxMutedSaturation);
+ // Now try and generate any missing colors
+ GenerateEmptyswatches();
+
+ // To get swatch infomation as string.
+ String[] swatchInfo = new String[6];
+
+ if (vibrantSwatch != null) swatchInfo[0] = vibrantSwatch.ToString();
+ if (lightVibrantSwatch != null) swatchInfo[1] = lightVibrantSwatch.ToString();
+ if (darkVibrantSwatch != null) swatchInfo[2] = darkVibrantSwatch.ToString();
+ if (mutedSwatch != null) swatchInfo[3] = mutedSwatch.ToString();
+ if (lightMutedColor != null) swatchInfo[4] = lightMutedColor.ToString();
+ if (darkMutedSwatch != null) swatchInfo[5] = darkMutedSwatch.ToString();
+
+ Tizen.Log.Info("Palette", "VibrantSwatch [" + swatchInfo[0] + "] " +
+ "lightVibrantSwatch [" + swatchInfo[1] + "] " +
+ "darkVibrantSwatch [" + swatchInfo[2] + "] " +
+ "MutedSwatch [" + swatchInfo[3] + "] " +
+ "lightMutedColor [" + swatchInfo[4] + "] " +
+ "darkMutedSwatch [" + swatchInfo[5] + "] \n");
+ }
+
+ public delegate void PaletteGeneratedEventHandler(Palette palette);
+
+ /// <summary>
+ /// Generate a Palette asynchronously using bitmap as source.
+ /// </summary>
+ /// <param name="pixelBuffer">A Target image's pixelBuffer.</param>
+ /// <param name="paletteGeneratedEventHandler">A method will be called with the palette when generated.</param>
+ /// <exception cref="ArgumentNullException">Thrown when the argument pixelBuffer, PaletteGeneratedEventHandler is null.</exception>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static void GenerateAsync(PixelBuffer pixelBuffer, PaletteGeneratedEventHandler paletteGeneratedEventHandler)
+ {
+ _ = AsyncTask(pixelBuffer, null, paletteGeneratedEventHandler);
+ }
+
+ /// <summary>
+ /// Generate a Palette asynchronously using pixelBuffer as source.
+ /// And set a region of the pixelBuffer to be used exclusively when calculating the palette.
+ /// </summary>
+ /// <param name="pixelBuffer">A Target image's pixelBuffer.</param>
+ /// <param name="region">A rectangle used for region.</param>
+ /// <param name="paletteGeneratedEventHandler">A method will be called with the palette when generated.</param>
+ /// <exception cref="ArgumentNullException">Thrown when the argument pixelBuffer, PaletteGeneratedEventHandler is null.</exception>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static void GenerateAsync(PixelBuffer pixelBuffer, Tizen.NUI.Rectangle region, PaletteGeneratedEventHandler paletteGeneratedEventHandler)
+ {
+ _ = AsyncTask(pixelBuffer, region, paletteGeneratedEventHandler);
+ }
+
+ /// <summary>
+ /// Generate a Palette synchronously using pixelBuffer as source.
+ /// </summary>
+ /// <param name="pixelBuffer">A Target image's pixelBuffer.</param>
+ /// <exception cref="ArgumentNullException">Thrown when the argument pixelBuffer is null.</exception>
+ /// <returns>the palette instance.</returns>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static Palette generate(PixelBuffer pixelBuffer)
+ {
+ return Generate(pixelBuffer, null);
+ }
+
+ /// <summary>
+ /// Generate a Palette synchronously using pixelBuffer as source.
+ /// And set a region of the pixelBuffer to be used exclusively when calculating the palette.
+ /// </summary>
+ /// <param name="pixelBuffer">A Target image's pixelBuffer.</param>
+ /// <param name="region">A rectangle used for region.</param>
+ /// <exception cref="ArgumentNullException">Thrown when the argument pixelBuffer is null.</exception>
+ /// <returns>the palette instance.</returns>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static Palette Generate(PixelBuffer pixelBuffer, Rectangle region)
+ {
+ Tizen.Log.Info("Palette", "pixelBuffer generate start with region: " + region + "\n");
+ if (pixelBuffer == null)
+ {
+ throw new ArgumentNullException(nameof(pixelBuffer), "pixelBuffer should not be null.");
+ }
+
+ // First we'll scale down the bitmap so it's shortest dimension is 100px
+ // NOTE: scaledBitmap can gets bitmap origin value and new bitmap instance as well
+ // When ScaleBitmap created newly it will be dispose below.
+ // otherwise it should not disposed because of this instance from user side.
+ bool resized = ScaleBitmapDown(pixelBuffer);
+
+ // Region set
+ if (resized && region != null)
+ {
+ double scale = pixelBuffer.GetWidth() / (double)pixelBuffer.GetHeight();
+ region.X = (int)Math.Floor(region.X * scale);
+ region.Y = (int)Math.Floor(region.Y * scale);
+ region.Width = Math.Min((int)Math.Ceiling(region.Width * scale), (int)pixelBuffer.GetWidth() );
+ region.Height = Math.Min((int)Math.Ceiling(region.Height * scale), (int)pixelBuffer.GetHeight());
+ }
+
+ // Now generate a Quantizer from the Bitmap
+ // FIXME: defaultCalculateNumberColors should be changeable?
+ ColorCutQuantizer quantizer = ColorCutQuantizer.FromBitmap(pixelBuffer, region, defaultCalculateNumberColors);
+
+ // Now return a ColorExtractor instance
+ return new Palette(quantizer.GetQuantizedColors());
+ }
+
+ /// <summary>
+ /// Returns all of the swatches which make up the palette.
+ /// </summary>
+ /// <returns>The swatch list</returns>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public ReadOnlyCollection<Swatch> GetSwatches()
+ {
+ return new ReadOnlyCollection<Swatch>(swatches);
+ }
+
+ /// <summary>
+ /// Returns the dominant swatch from the palette.
+ /// The dominant swatch is defined as the swatch with the greatest population (frequency) within the palette.
+ /// </summary>
+ /// <returns>The swatch instance</returns>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public Swatch GetDominantSwatch()
+ {
+ String swatchInfo = null;
+ if (dominantSwatch == null)
+ dominantSwatch = FinddominantSwatch();
+
+ if (dominantSwatch != null) swatchInfo = dominantSwatch.ToString();
+ Tizen.Log.Info("Palette", "dominantSwatch [" + swatchInfo + "] \n");
+
+ return dominantSwatch;
+ }
+
+ /// <summary>
+ /// Returns the most vibrant swatch in the palette. Might be null.
+ /// </summary>
+ /// <returns>The swatch instance</returns>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public Swatch GetVibrantSwatch()
+ {
+ return vibrantSwatch;
+ }
+
+ /// <summary>
+ /// Returns a light and vibrant swatch from the palette. Might be null.
+ /// </summary>
+ /// <returns>The swatch instance</returns>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public Swatch GetLightVibrantSwatch()
+ {
+ return lightVibrantSwatch;
+ }
+
+ /// <summary>
+ /// Returns a dark and vibrant swatch from the palette. Might be null.
+ /// </summary>
+ /// <returns>The swatch instance</returns>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public Swatch GetDarkVibrantSwatch()
+ {
+ return darkVibrantSwatch;
+ }
+
+ /// <summary>
+ /// Returns a muted swatch from the palette. Might be null.
+ /// </summary>
+ /// <returns>The swatch instance</returns>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public Swatch GetMutedSwatch()
+ {
+ return mutedSwatch;
+ }
+
+ /// <summary>
+ /// Returns a muted and light swatch from the palette. Might be null.
+ /// </summary>
+ /// <returns>The swatch instance</returns>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public Swatch GetLightMutedSwatch()
+ {
+ return lightMutedColor;
+ }
+
+ /// <summary>
+ /// Returns a muted and dark swatch from the palette. Might be null.
+ /// </summary>
+ /// <returns>The swatch instance</returns>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public Swatch GetDarkMutedSwatch()
+ {
+ return darkMutedSwatch;
+ }
+
+ private static async Task<Palette> AsyncTask(PixelBuffer pixelBuffer, Rectangle region, PaletteGeneratedEventHandler paletteGeneratedEventHandler)
+ {
+ if (paletteGeneratedEventHandler == null)
+ {
+ throw new ArgumentNullException(nameof(paletteGeneratedEventHandler), "PaletteGeneratedEventHandlergate should not be null.");
+ }
+
+ var GenerateTask = Task.Run(() =>
+ {
+ return Generate(pixelBuffer, region);
+ }).ConfigureAwait(false);
+
+ Palette ret = await GenerateTask;
+ paletteGeneratedEventHandler(ret);
+
+ return null; ;
+ }
+
+ /// <summary>
+ /// Try and generate any missing swatches from the swatches we did find.
+ /// </summary>
+ private void GenerateEmptyswatches()
+ {
+ if (vibrantSwatch == null)
+ {
+ // If we do not have a vibrant color...
+ if (darkVibrantSwatch != null)
+ {
+ // ...but we do have a dark vibrant, generate the value by modifying the luma
+ float[] newHsl = CopyhslValues(darkVibrantSwatch);
+ newHsl[2] = targetNormalLuma;
+ vibrantSwatch = new Swatch(ColorUtils.HslToRgb(newHsl), 0);
+ Tizen.Log.Info("Palette", "Generate Vibrant Swatch \n");
+ }
+ }
+ if (darkVibrantSwatch == null)
+ {
+ // If we do not have a dark vibrant color...
+ if (vibrantSwatch != null)
+ {
+ // ...but we do have a vibrant, generate the value by modifying the luma
+ float[] newHsl = CopyhslValues(vibrantSwatch);
+ newHsl[2] = targetDarkLuma;
+ darkVibrantSwatch = new Swatch(ColorUtils.HslToRgb(newHsl), 0);
+ Tizen.Log.Info("Palette", "Generate DarkVibrant Swatch \n");
+ }
+ }
+ }
+
+ /// <summary>
+ /// Copy a Swatch's hsl values into a new float[].
+ /// </summary>
+ private static float[] CopyhslValues(Swatch color)
+ {
+ float[] newHsl = new float[3];
+ Array.Copy(color.GetHsl(), 0, newHsl, 0, 3);
+
+ return newHsl;
+ }
+
+ /// <summary>
+ /// return true if we have already selected swatch
+ /// </summary>
+ private bool IsAlreadySelected(Swatch swatch)
+ {
+ return vibrantSwatch == swatch || darkVibrantSwatch == swatch ||
+ lightVibrantSwatch == swatch || mutedSwatch == swatch ||
+ darkMutedSwatch == swatch || lightMutedColor == swatch;
+ }
+
+ private Swatch FindColor(float targetLuma, float minLuma, float maxLuma,
+ float targetSaturation, float minSaturation, float maxSaturation)
+ {
+ Swatch max = null;
+ float maxValue = 0f;
+
+ foreach (Swatch swatch in swatches)
+ {
+ float sat = swatch.GetHsl()[1];
+ float luma = swatch.GetHsl()[2];
+ if (sat >= minSaturation && sat <= maxSaturation &&
+ luma >= minLuma && luma <= maxLuma &&
+ !IsAlreadySelected(swatch))
+ {
+ float thisValue = CreateComparisonValue(sat, targetSaturation, luma, targetLuma,
+ swatch.GetPopulation(), highestPopulation);
+ if (max == null || thisValue > maxValue)
+ {
+ max = swatch;
+ maxValue = thisValue;
+ }
+ }
+ }
+
+ return max;
+ }
+
+ /// <summary>
+ /// Find the Swatch with the highest population value and return the population.
+ /// </summary>
+ private int FindMaxPopulation()
+ {
+ int population = 0;
+
+ foreach (Swatch swatch in swatches)
+ {
+ population = Math.Max(population, swatch.GetPopulation());
+ }
+
+ return population;
+ }
+
+ private Swatch FinddominantSwatch()
+ {
+ int maxPop = -1;
+ Swatch maxSwatch = null;
+
+ foreach (Swatch swatch in swatches)
+ {
+ if (swatch.GetPopulation() > maxPop)
+ {
+ maxSwatch = swatch;
+ maxPop = swatch.GetPopulation();
+ }
+ }
+
+ return maxSwatch;
+ }
+
+ /// <summary>
+ /// Scale the bitmap down so that it's smallest dimension is
+ /// calculateBitmapMinDimensionpx. If bitmap is smaller than this, than it
+ /// is returned.
+ /// </summary>
+ private static bool ScaleBitmapDown(PixelBuffer pixelBuffer)
+ {
+ int minDimension = Math.Min((int)pixelBuffer.GetWidth(), (int)pixelBuffer.GetHeight());
+
+ if (minDimension <= calculateBitmapMinDimension)
+ {
+ // If the bitmap is small enough already, just return it
+ return false;
+ }
+
+ float scaleRatio = calculateBitmapMinDimension / (float)minDimension;
+
+ int width = (int)Math.Round((int)pixelBuffer.GetWidth() * scaleRatio);
+ int height = (int)Math.Round((int)pixelBuffer.GetHeight() * scaleRatio);
+
+ Tizen.Log.Info("Palette", "pixelBuffer resize to " + width + " " + height + "\n");
+ pixelBuffer.Resize((ushort)width, (ushort)height);
+
+ return true;
+ }
+
+ private static float CreateComparisonValue(float saturation, float targetSaturation,
+ float luma, float targetLuma,
+ int population, int highestPopulation)
+ {
+ return WeightedMean(InvertDiff(saturation, targetSaturation), 3f,
+ InvertDiff(luma, targetLuma), 6.5f,
+ population / (float)highestPopulation, 0.5f);
+ }
+
+ /// <summary>
+ /// Returns a value in the range 0-1. 1 is returned when value equals the
+ /// targetValue and then decreases as the absolute difference between value and
+ /// targetValue increases.
+ ///
+ /// param value the item's value
+ /// param targetValue the value which we desire
+ /// </summary>
+ private static float InvertDiff(float value, float targetValue)
+ {
+ return 1f - Math.Abs(value - targetValue);
+ }
+
+ private static float WeightedMean(params float[] values)
+ {
+ float sum = 0f;
+ float sumWeight = 0f;
+
+ for (int i = 0; i < values.Length; i += 2)
+ {
+ float value = values[i];
+ float weight = values[i + 1];
+ sum += (value * weight);
+ sumWeight += weight;
+ }
+
+ return sum / sumWeight;
+ }
+
+ // This is nested class for use by other internal classes(Color*), but is declared public.
+ // Futher confirmantion need of this architect.
+
+ /// <summary>
+ /// Represents a color swatch generated from an image's palette. The RGB color can be retrieved calling getRgb()
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public sealed class Swatch
+ {
+ private const float minContrastTitleText = 3.0f;
+ private const float minContrastBodyText = 4.5f;
+
+ private int red, green, blue;
+ private int colorInt, bodyTextColor, titleTextColor;
+ private int population;
+ private bool generatedTextColors;
+ private float[] hsl;
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public Swatch(int rgbcolorInt, int populationOfSwatch)
+ {
+ red = (int)(((rgbcolorInt >> 16) & 0xff) / 255.0f);
+ green = (int)(((rgbcolorInt >> 8) & 0xff) / 255.0f);
+ blue = (int)((rgbcolorInt & 0xff) / 255.0f);
+ colorInt = rgbcolorInt;
+ population = populationOfSwatch;
+ }
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public Swatch(int redValueOfSwatch, int greenValueOfSwatch, int blueValueOfSwatch, int populationOfSwatch)
+ {
+ red = redValueOfSwatch;
+ green = greenValueOfSwatch;
+ blue = blueValueOfSwatch;
+ colorInt = (255 & 0xff) << 24 | (red & 0xff) << 16 | (green & 0xff) << 8 | (blue & 0xff);
+ population = populationOfSwatch;
+ }
+
+ /// <summary>
+ /// return this swatch's RGB color value
+ /// </summary>
+ /// <returns>A Tizen.NUI.Color value.</returns>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public Color GetRgb()
+ {
+ return new Color((float)red / 255, (float)green / 255, (float)blue / 255, 1.0f);
+ }
+
+ /// <summary>
+ /// Return this swatch's hsl values.
+ /// hsv[0] is Hue [0 .. 360)
+ /// hsv[1] is Saturation [0...1]
+ /// hsv[2] is Lightness [0...1]
+ /// </summary>
+ /// <returns>A float array value.</returns>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public float[] GetHsl()
+ {
+ if (hsl == null)
+ {
+ // Lazily generate hsl values from RGB
+ hsl = new float[3];
+ ColorUtils.RgbToHsl(red, green, blue, hsl);
+ }
+
+ return hsl;
+ }
+
+ /// <summary>
+ /// Returns an appropriate color to use for any 'title' text which is displayed over this
+ /// Palette.Swatchs color. This color is guaranteed to have sufficient contrast.
+ /// </summary>
+ /// <returns>A Tizen.NUI.Color value.</returns>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public Color GetTitleTextColor()
+ {
+ EnsureTextColorsGenerated();
+
+ //Tizen.Log.Info("Palette", "Swatch Title Text Color = " + titleRgbColor + "\n");
+
+ return new Color((float)(((titleTextColor >> 16) & 0xff) / 255.0f), (float)(((titleTextColor >> 8) & 0xff) / 255.0f), (float)((titleTextColor & 0xff) / 255.0f), (float)(((titleTextColor >> 24) & 0xff) / 255.0f));
+ }
+
+ /// <summary>
+ /// Returns an appropriate color to use for any 'body' text which is displayed over this
+ /// Palette.Swatchs color. This color is guaranteed to have sufficient contrast.
+ /// </summary>
+ /// <returns>A Tizen.NUI.Color value.</returns>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public Tizen.NUI.Color GetBodyTextColor()
+ {
+ EnsureTextColorsGenerated();
+
+ //Tizen.Log.Info("Palette", "Swatch Body Text Color = " + bodyRgbColor + "\n");
+
+ return new Color((float)(((bodyTextColor >> 16) & 0xff) / 255.0f), (float)(((bodyTextColor >> 8) & 0xff) / 255.0f), (float)((bodyTextColor & 0xff) / 255.0f), (float)(((bodyTextColor >> 24) & 0xff) / 255.0f));
+
+ }
+
+ /// <summary>
+ /// return the number of pixels represented by this swatch.
+ /// </summary>
+ /// <returns>A number of pixels value.</returns>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public int GetPopulation()
+ {
+ return population;
+ }
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public override String ToString()
+ {
+ return "[ R=" + red + " G=" + green + " B=" + blue + " ] " + population;
+ }
+
+ private void EnsureTextColorsGenerated()
+ {
+ if (!generatedTextColors)
+ {
+ int colorWhite = (255 & 0xff) << 24 | (255 & 0xff) << 16 | (255 & 0xff) << 8 | (255 & 0xff);
+ int colorBlack = (255 & 0xff) << 24 | (0 & 0xff) << 16 | (0 & 0xff) << 8 | (0 & 0xff);
+
+ // First check white, as most colors will be dark
+ int lightBodyAlpha = ColorUtils.CalculateMinimumAlpha(
+ colorWhite, colorInt, minContrastBodyText);
+ int lightTitleAlpha = ColorUtils.CalculateMinimumAlpha(
+ colorWhite, colorInt, minContrastTitleText);
+
+ if (lightBodyAlpha != -1 && lightTitleAlpha != -1)
+ {
+ // If we found valid light values, use them and return
+ bodyTextColor = ColorUtils.SetAlphaComponent(colorWhite, lightBodyAlpha);
+ titleTextColor = ColorUtils.SetAlphaComponent(colorWhite, lightTitleAlpha);
+ generatedTextColors = true;
+
+ return;
+ }
+
+ int darkBodyAlpha = ColorUtils.CalculateMinimumAlpha(
+ colorBlack, colorInt, minContrastBodyText);
+ int darkTitleAlpha = ColorUtils.CalculateMinimumAlpha(
+ colorBlack, colorInt, minContrastTitleText);
+
+ if (darkBodyAlpha != -1 && darkTitleAlpha != -1)
+ {
+ // If we found valid dark values, use them and return
+ bodyTextColor = ColorUtils.SetAlphaComponent(colorBlack, darkBodyAlpha);
+ titleTextColor = ColorUtils.SetAlphaComponent(colorBlack, darkTitleAlpha);
+ generatedTextColors = true;
+
+ return;
+ }
+
+ // If we reach here then we can not find title and body values which use the same
+ // lightness, we need to use mismatched values
+ bodyTextColor = lightBodyAlpha != -1
+ ? ColorUtils.SetAlphaComponent(colorWhite, lightBodyAlpha)
+ : ColorUtils.SetAlphaComponent(colorWhite, darkBodyAlpha);
+ titleTextColor = lightTitleAlpha != -1
+ ? ColorUtils.SetAlphaComponent(colorWhite, lightTitleAlpha)
+ : ColorUtils.SetAlphaComponent(colorWhite, darkTitleAlpha);
+ generatedTextColors = true;
+ }
+ }
+ }
+ }
+}
--- /dev/null
+/*
+* Copyright (c) 2021 Samsung Electronics Co., Ltd.
+*
+* 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.
+*
+*/
+
+using System;
+using Tizen.NUI;
+using System.Collections.Generic;
+using System.Diagnostics;
+using Tizen.NUI.BaseComponents;
+using Tizen.NUI.Components;
+
+namespace Tizen.NUI.Samples
+{
+ class PaletteSample : IExample
+ {
+ private static int bottomHeight = 60;
+ private static int buttonWeight = 100;
+ private static int buttonHeight = 40;
+ private static int maxView = 2;
+ private static string resourcePath = Tizen.Applications.Application.Current.DirectoryInfo.Resource;
+ private static string[] imagePath = {
+ resourcePath + "/images/PaletteTest/rock.jpg",
+ resourcePath + "/images/PaletteTest/red2.jpg",
+ resourcePath + "/images/PaletteTest/10by10tree.png",
+ resourcePath + "/images/PaletteTest/3color.jpeg"
+ };
+
+ private int viewIndex = 0;
+ private int windowWidth, windowHeight;
+ private Window currentWindow;
+ private View view;
+ private View bottomView;
+ private ImageView imageView;
+ private Palette palette;
+ private Palette.Swatch dominantSwatch;
+ private Palette.Swatch vibrantSwatch;
+ private Palette.Swatch mutedSwatch;
+ private Palette.Swatch darkVibrantSwatch;
+ private Palette.Swatch darkMutedSwatch;
+ private Palette.Swatch lightVibrantSwatch;
+ private Palette.Swatch lightMutedSwatch;
+ private Stopwatch timer = new Stopwatch();
+
+ public void Activate()
+ {
+ Initialize();
+ }
+
+ public void Initialize()
+ {
+ currentWindow = NUIApplication.GetDefaultWindow();
+ currentWindow.BackgroundColor = Color.White;
+
+ windowWidth = Window.Instance.Size.Width;
+ windowHeight = Window.Instance.Size.Height;
+
+ CreatePage(viewIndex);
+ CreateBottomLayout();
+
+ }
+
+ public void CreateBottomLayout()
+ {
+ bottomView = new View()
+ {
+ Size = new Size(windowWidth, bottomHeight),
+ Position2D = new Position2D(0, windowHeight - bottomHeight),
+ Layout = new LinearLayout()
+ {
+ LinearOrientation = LinearLayout.Orientation.Horizontal,
+ LinearAlignment = LinearLayout.Alignment.Center,
+ }
+
+ };
+ currentWindow.Add(bottomView);
+
+ Button prevBtn = new Button()
+ {
+ Text = "Prev",
+ Size = new Size(buttonWeight, buttonHeight),
+ Margin = 10,
+ };
+ Button nextBtn = new Button()
+ {
+ Text = "next",
+ Size = new Size(buttonWeight, buttonHeight),
+ Margin = 10,
+ };
+ bottomView.Add(prevBtn);
+ bottomView.Add(nextBtn);
+
+ prevBtn.Clicked += PrevClicked;
+ nextBtn.Clicked += NextClicked;
+ }
+
+ private void PrevClicked(object sender, ClickedEventArgs e)
+ {
+ if (viewIndex == 0) return;
+
+ viewIndex--;
+ view.Unparent();
+ CreatePage(viewIndex);
+
+ }
+
+ private void NextClicked(object sender, ClickedEventArgs e)
+ {
+ if (viewIndex == maxView) return;
+
+ viewIndex++;
+ view.Unparent();
+ CreatePage(viewIndex);
+ }
+
+ public void CreatePage(int idx)
+ {
+ view = new View()
+ {
+ Size = new Size(windowWidth, windowHeight - bottomHeight),
+ Layout = new LinearLayout() { LinearOrientation = LinearLayout.Orientation.Vertical },
+ };
+ currentWindow.Add(view);
+
+ imageView = CreateImageView(viewIndex);
+ view.Add(imageView);
+
+ timer.Start();
+ palette = ImageGenerate(viewIndex);
+ timer.Stop();
+
+ TextLabel label = new TextLabel("Time = " + timer.ElapsedMilliseconds.ToString() + "ms")
+ {
+ Size2D = new Size2D((int)(windowWidth), (int)((windowHeight - windowWidth) / 9)),
+ HorizontalAlignment = HorizontalAlignment.End,
+ VerticalAlignment = VerticalAlignment.Center,
+ };
+ view.Add(label);
+
+ dominantSwatch = palette.GetDominantSwatch();
+ if (dominantSwatch != null) {
+ CreateLabel(dominantSwatch);
+ }
+
+ lightVibrantSwatch = palette.GetLightVibrantSwatch();
+ if (lightVibrantSwatch != null) {
+ CreateLabel(lightVibrantSwatch);
+ }
+
+ vibrantSwatch = palette.GetVibrantSwatch();
+ if (vibrantSwatch != null) {
+ CreateLabel(vibrantSwatch);
+ }
+
+ darkVibrantSwatch = palette.GetDarkVibrantSwatch();
+ if (darkVibrantSwatch != null) {
+ CreateLabel(darkVibrantSwatch);
+ }
+
+ lightMutedSwatch = palette.GetLightMutedSwatch();
+ if (lightMutedSwatch != null) {
+ CreateLabel(lightMutedSwatch);
+ }
+
+ mutedSwatch = palette.GetMutedSwatch();
+ if (mutedSwatch != null) {
+ CreateLabel(mutedSwatch);
+ }
+
+ darkMutedSwatch = palette.GetDarkMutedSwatch();
+ if (darkMutedSwatch != null) {
+ CreateLabel(darkMutedSwatch);
+ }
+
+ timer.Reset();
+ }
+
+ public void CreateLabel(Palette.Swatch swatch)
+ {
+ Color color = swatch.GetRgb();
+
+ string txt = " RGB(" + (int)(color.R * 255) + " " + (int)(color.G * 255) + " " + (int)(color.B * 255) + ")";
+ TextLabel label = new TextLabel(txt)
+ {
+ TextColor = swatch.GetBodyTextColor(),
+ BackgroundColor = color,
+ Size2D = new Size2D((int)(windowWidth), (int)((windowHeight - windowWidth) / 9)),
+ HorizontalAlignment = HorizontalAlignment.Begin,
+ VerticalAlignment = VerticalAlignment.Center,
+ };
+
+ view.Add(label);
+ }
+
+ public Palette ImageGenerate(int idx)
+ {
+ PixelBuffer imgBitmap = ImageLoading.LoadImageFromFile(imagePath[idx]);
+ Palette palette = Palette.generate(imgBitmap);
+
+ return palette;
+ }
+
+ public ImageView CreateImageView(int idx)
+ {
+ ImageView tempImage = new ImageView()
+ {
+ ResourceUrl = imagePath[idx],
+ Size = new Tizen.NUI.Size(Window.Instance.Size.Width, Window.Instance.Size.Width),
+ HeightResizePolicy = ResizePolicyType.Fixed,
+ WidthResizePolicy = ResizePolicyType.Fixed,
+ };
+
+ return tempImage;
+ }
+
+ public void Deactivate()
+ {
+ //Will Do FullGC in DailDemo Class
+ view.Unparent();
+ bottomView.Unparent();
+
+ view.Dispose();
+ bottomView.Dispose();
+
+ view = null;
+ bottomView = null;
+ }
+ }
+}