From: Woochanlee Date: Wed, 10 Mar 2021 04:29:21 +0000 (+0900) Subject: [NUI] Introduce NUI Palette APIs X-Git-Tag: citest_t1~232 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;ds=sidebyside;h=cadbd44045a570de846718e2cc971c6dd4770a18;p=platform%2Fcore%2Fcsapi%2Ftizenfx.git [NUI] Introduce NUI Palette APIs --- diff --git a/src/Tizen.NUI/src/internal/Utility/ColorCutQuantizer.cs b/src/Tizen.NUI/src/internal/Utility/ColorCutQuantizer.cs new file mode 100755 index 0000000..81f5743 --- /dev/null +++ b/src/Tizen.NUI/src/internal/Utility/ColorCutQuantizer.cs @@ -0,0 +1,629 @@ +/* + * 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 colorPopulations; + private static int[] colors; + private List 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(); + + 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(); + + 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); + } + } + + /// + /// Factory-method to generate a ColorCutQuantizer from a PixelBuffer object. + /// + 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); + } + + /// + /// return the list of quantized colors + /// + public List GetQuantizedColors() + { + return quantizedColors; + } + + private List 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 customHeap = new CustomHeap(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); + } + + /// + /// 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 + /// + private void SplitBoxes(CustomHeap 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 GenerateAverageColors(CustomHeap vboxes) + { + List colors = new List(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); + } + + /// + ///return true if the color represents a color which is close to black. + /// + private static Boolean IsBlack(float[] hslColor) + { + return hslColor[2] <= blackMaxLightness; + } + + /// + /// return true if the color represents a color which is close to white. + /// + private static Boolean IsWhite(float[] hslColor) + { + return hslColor[2] >= whiteMinLightness; + } + + /// + /// return true if the color lies close to the red side of the I line. + /// + private static Boolean IsNearRedILine(float[] hslColor) + { + return hslColor[0] >= 10f && hslColor[0] <= 37f && hslColor[1] <= 0.82f; + } + + private sealed class CustomHeap : IEnumerable + { + 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(); + + public CustomHeap(Comparer comparer) + { + if (comparer == null) throw new ArgumentNullException(nameof(comparer), "comparer is null"); + Comparer = comparer; + } + + private Comparer Comparer { get; set; } + + public IEnumerator 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; + } + + } + + /// + /// Comparator which sorts Vbox instances based on their volume, in descending order + /// + private sealed class VboxComparatorVolume : Comparer + { + public override int Compare(Vbox lhs, Vbox rhs) + { + return rhs.GetVolume() - lhs.GetVolume(); + } + } + + /// + /// Represents a tightly fitting box around a color space. + /// + 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; + } + + /// + /// Recomputes the boundaries of this box to tightly fit the colors within the box. + /// + 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; + } + } + + /// + /// Split this color box at the mid-point along it's longest dimension + /// + /// return the new ColorBox + /// + 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; + } + + /// + /// return the dimension which this box is largest in + /// + 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; + } + } + + /// + /// 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 + /// + 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; + } + + /// + /// return the average color of this box. + /// + 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); + } + + /// + /// return the midpoint of this box in the given dimension + /// + 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; + } + } + + /// + /// Modify the significant octet in a packed color int. Allows sorting based on the value of a + /// single color component. + /// + /// see Vbox#findSplitPoint() + /// + 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; + } + } + } + } +} diff --git a/src/Tizen.NUI/src/internal/Utility/ColorHistogram.cs b/src/Tizen.NUI/src/internal/Utility/ColorHistogram.cs new file mode 100755 index 0000000..1f7d094 --- /dev/null +++ b/src/Tizen.NUI/src/internal/Utility/ColorHistogram.cs @@ -0,0 +1,141 @@ +/* + * 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; + + /// + /// A new ColorHistogram instance. + /// + 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); + } + + /// + /// return number of distinct colors in the image. + /// + public int GetNumberOfColors() + { + return numberColors; + } + + /// + /// return an array containing all of the distinct colors in the image. + /// + public int[] GetColors() + { + return colors; + } + + /// + /// return an array containing the frequency of a distinct colors within the image. + /// + 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; + } + } + } + } +} diff --git a/src/Tizen.NUI/src/internal/Utility/ColorUtils.cs b/src/Tizen.NUI/src/internal/Utility/ColorUtils.cs new file mode 100755 index 0000000..724be89 --- /dev/null +++ b/src/Tizen.NUI/src/internal/Utility/ColorUtils.cs @@ -0,0 +1,352 @@ +/* + * 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; + + /// + /// 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 + /// + public static void ColorToXyz(int color, double[] outXyz) + { + RgbToXyz((color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff, outXyz); + } + + /// + /// 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 + /// + 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); + } + + /// + /// 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. + /// + public static double CalculateLuminance(int color) + { + double[] result = new double[3]; + ColorToXyz(color, result); + // Luminance is the Y component + return result[1] / 100; + } + + /// + /// Composite two potentially translucent colors over each other and returns the result. + /// + 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)); + } + + /// + /// Returns the contrast ratio between foreground and background. + /// background must be opaque. + /// + /// Formula defined + /// here. + /// + 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); + } + + /// + /// Set the alpha component of color to be alpha. + /// + 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); + } + + /// + /// 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 + /// + 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; + } + } + + /// + /// return luma value according to to XYZ color space in the range 0.0 - 1.0 + /// + 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); + } + } +} diff --git a/src/Tizen.NUI/src/public/Utility/Palette.cs b/src/Tizen.NUI/src/public/Utility/Palette.cs new file mode 100755 index 0000000..417490d --- /dev/null +++ b/src/Tizen.NUI/src/public/Utility/Palette.cs @@ -0,0 +1,650 @@ +/* + * 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 +{ + /// + /// 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 + /// }}; + /// + [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 swatches; + + private Palette() { } + + private Palette(List 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); + + /// + /// Generate a Palette asynchronously using bitmap as source. + /// + /// A Target image's pixelBuffer. + /// A method will be called with the palette when generated. + /// Thrown when the argument pixelBuffer, PaletteGeneratedEventHandler is null. + [EditorBrowsable(EditorBrowsableState.Never)] + public static void GenerateAsync(PixelBuffer pixelBuffer, PaletteGeneratedEventHandler paletteGeneratedEventHandler) + { + _ = AsyncTask(pixelBuffer, null, paletteGeneratedEventHandler); + } + + /// + /// Generate a Palette asynchronously using pixelBuffer as source. + /// And set a region of the pixelBuffer to be used exclusively when calculating the palette. + /// + /// A Target image's pixelBuffer. + /// A rectangle used for region. + /// A method will be called with the palette when generated. + /// Thrown when the argument pixelBuffer, PaletteGeneratedEventHandler is null. + [EditorBrowsable(EditorBrowsableState.Never)] + public static void GenerateAsync(PixelBuffer pixelBuffer, Tizen.NUI.Rectangle region, PaletteGeneratedEventHandler paletteGeneratedEventHandler) + { + _ = AsyncTask(pixelBuffer, region, paletteGeneratedEventHandler); + } + + /// + /// Generate a Palette synchronously using pixelBuffer as source. + /// + /// A Target image's pixelBuffer. + /// Thrown when the argument pixelBuffer is null. + /// the palette instance. + [EditorBrowsable(EditorBrowsableState.Never)] + public static Palette generate(PixelBuffer pixelBuffer) + { + return Generate(pixelBuffer, null); + } + + /// + /// Generate a Palette synchronously using pixelBuffer as source. + /// And set a region of the pixelBuffer to be used exclusively when calculating the palette. + /// + /// A Target image's pixelBuffer. + /// A rectangle used for region. + /// Thrown when the argument pixelBuffer is null. + /// the palette instance. + [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()); + } + + /// + /// Returns all of the swatches which make up the palette. + /// + /// The swatch list + [EditorBrowsable(EditorBrowsableState.Never)] + public ReadOnlyCollection GetSwatches() + { + return new ReadOnlyCollection(swatches); + } + + /// + /// Returns the dominant swatch from the palette. + /// The dominant swatch is defined as the swatch with the greatest population (frequency) within the palette. + /// + /// The swatch instance + [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; + } + + /// + /// Returns the most vibrant swatch in the palette. Might be null. + /// + /// The swatch instance + [EditorBrowsable(EditorBrowsableState.Never)] + public Swatch GetVibrantSwatch() + { + return vibrantSwatch; + } + + /// + /// Returns a light and vibrant swatch from the palette. Might be null. + /// + /// The swatch instance + [EditorBrowsable(EditorBrowsableState.Never)] + public Swatch GetLightVibrantSwatch() + { + return lightVibrantSwatch; + } + + /// + /// Returns a dark and vibrant swatch from the palette. Might be null. + /// + /// The swatch instance + [EditorBrowsable(EditorBrowsableState.Never)] + public Swatch GetDarkVibrantSwatch() + { + return darkVibrantSwatch; + } + + /// + /// Returns a muted swatch from the palette. Might be null. + /// + /// The swatch instance + [EditorBrowsable(EditorBrowsableState.Never)] + public Swatch GetMutedSwatch() + { + return mutedSwatch; + } + + /// + /// Returns a muted and light swatch from the palette. Might be null. + /// + /// The swatch instance + [EditorBrowsable(EditorBrowsableState.Never)] + public Swatch GetLightMutedSwatch() + { + return lightMutedColor; + } + + /// + /// Returns a muted and dark swatch from the palette. Might be null. + /// + /// The swatch instance + [EditorBrowsable(EditorBrowsableState.Never)] + public Swatch GetDarkMutedSwatch() + { + return darkMutedSwatch; + } + + private static async Task 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; ; + } + + /// + /// Try and generate any missing swatches from the swatches we did find. + /// + 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"); + } + } + } + + /// + /// Copy a Swatch's hsl values into a new float[]. + /// + private static float[] CopyhslValues(Swatch color) + { + float[] newHsl = new float[3]; + Array.Copy(color.GetHsl(), 0, newHsl, 0, 3); + + return newHsl; + } + + /// + /// return true if we have already selected swatch + /// + 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; + } + + /// + /// Find the Swatch with the highest population value and return the population. + /// + 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; + } + + /// + /// Scale the bitmap down so that it's smallest dimension is + /// calculateBitmapMinDimensionpx. If bitmap is smaller than this, than it + /// is returned. + /// + 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); + } + + /// + /// 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 + /// + 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. + + /// + /// Represents a color swatch generated from an image's palette. The RGB color can be retrieved calling getRgb() + /// + [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; + } + + /// + /// return this swatch's RGB color value + /// + /// A Tizen.NUI.Color value. + [EditorBrowsable(EditorBrowsableState.Never)] + public Color GetRgb() + { + return new Color((float)red / 255, (float)green / 255, (float)blue / 255, 1.0f); + } + + /// + /// Return this swatch's hsl values. + /// hsv[0] is Hue [0 .. 360) + /// hsv[1] is Saturation [0...1] + /// hsv[2] is Lightness [0...1] + /// + /// A float array value. + [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; + } + + /// + /// 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. + /// + /// A Tizen.NUI.Color value. + [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)); + } + + /// + /// 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. + /// + /// A Tizen.NUI.Color value. + [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)); + + } + + /// + /// return the number of pixels represented by this swatch. + /// + /// A number of pixels value. + [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; + } + } + } + } +} diff --git a/test/Tizen.NUI.Samples/Tizen.NUI.Samples/Samples/PaletteSample.cs b/test/Tizen.NUI.Samples/Tizen.NUI.Samples/Samples/PaletteSample.cs new file mode 100755 index 0000000..a42edd9 --- /dev/null +++ b/test/Tizen.NUI.Samples/Tizen.NUI.Samples/Samples/PaletteSample.cs @@ -0,0 +1,241 @@ +/* +* 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; + } + } +} diff --git a/test/Tizen.NUI.Samples/Tizen.NUI.Samples/res/images/PaletteTest/10by10tree.png b/test/Tizen.NUI.Samples/Tizen.NUI.Samples/res/images/PaletteTest/10by10tree.png new file mode 100755 index 0000000..639b724 Binary files /dev/null and b/test/Tizen.NUI.Samples/Tizen.NUI.Samples/res/images/PaletteTest/10by10tree.png differ diff --git a/test/Tizen.NUI.Samples/Tizen.NUI.Samples/res/images/PaletteTest/3color.jpeg b/test/Tizen.NUI.Samples/Tizen.NUI.Samples/res/images/PaletteTest/3color.jpeg new file mode 100644 index 0000000..58a7e27 Binary files /dev/null and b/test/Tizen.NUI.Samples/Tizen.NUI.Samples/res/images/PaletteTest/3color.jpeg differ diff --git a/test/Tizen.NUI.Samples/Tizen.NUI.Samples/res/images/PaletteTest/red2.jpg b/test/Tizen.NUI.Samples/Tizen.NUI.Samples/res/images/PaletteTest/red2.jpg new file mode 100755 index 0000000..e52dc4c Binary files /dev/null and b/test/Tizen.NUI.Samples/Tizen.NUI.Samples/res/images/PaletteTest/red2.jpg differ diff --git a/test/Tizen.NUI.Samples/Tizen.NUI.Samples/res/images/PaletteTest/rock.jpg b/test/Tizen.NUI.Samples/Tizen.NUI.Samples/res/images/PaletteTest/rock.jpg new file mode 100755 index 0000000..6014264 Binary files /dev/null and b/test/Tizen.NUI.Samples/Tizen.NUI.Samples/res/images/PaletteTest/rock.jpg differ