@ -0,0 +1,11 @@ |
|||||
|
<StyleCopSettings Version="105"> |
||||
|
<Analyzers> |
||||
|
<Analyzer AnalyzerId="StyleCop.CSharp.DocumentationRules"> |
||||
|
<AnalyzerSettings> |
||||
|
<StringProperty Name="CompanyName">James South</StringProperty> |
||||
|
<StringProperty Name="Copyright">Copyright (c) James South. |
||||
|
Licensed under the Apache License, Version 2.0.</StringProperty> |
||||
|
</AnalyzerSettings> |
||||
|
</Analyzer> |
||||
|
</Analyzers> |
||||
|
</StyleCopSettings> |
||||
@ -1,8 +1,8 @@ |
|||||
<?xml version="1.0" encoding="utf-8"?> |
<?xml version="1.0" encoding="utf-8"?> |
||||
<packages> |
<packages> |
||||
<package id="Csharp-Sqlite" version="3.7.7.1" targetFramework="net40" /> |
<package id="Csharp-Sqlite" version="3.7.7.1" targetFramework="net40" /> |
||||
<package id="Microsoft.Bcl" version="1.0.19" targetFramework="net40" /> |
<package id="Microsoft.Bcl" version="1.1.3" targetFramework="net40" /> |
||||
<package id="Microsoft.Bcl.Async" version="1.0.16" targetFramework="net40" /> |
<package id="Microsoft.Bcl.Async" version="1.0.16" targetFramework="net40" /> |
||||
<package id="Microsoft.Bcl.Build" version="1.0.6" targetFramework="net40" /> |
<package id="Microsoft.Bcl.Build" version="1.0.8" targetFramework="net40" /> |
||||
<package id="sqlite-net" version="1.0.7" targetFramework="net40" /> |
<package id="sqlite-net" version="1.0.7" targetFramework="net40" /> |
||||
</packages> |
</packages> |
||||
@ -0,0 +1,5 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<packages > |
||||
|
<package id="Csharp-Sqlite" version="3.7.7.1" targetFramework="net40" /> |
||||
|
<package id="sqlite-net" version="1.0.7" targetFramework="net40" /> |
||||
|
</packages> |
||||
@ -0,0 +1,836 @@ |
|||||
|
// --------------------------------------------------------------------------------------------------------------------
|
||||
|
// <copyright file="ColorQuantizer.cs" company="James South">
|
||||
|
// Copyright (c) James South.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
// </copyright>
|
||||
|
// <summary>
|
||||
|
// The color quantizer.
|
||||
|
// </summary>
|
||||
|
// --------------------------------------------------------------------------------------------------------------------
|
||||
|
|
||||
|
namespace ImageProcessor.Imaging |
||||
|
{ |
||||
|
#region Using
|
||||
|
using System; |
||||
|
using System.Diagnostics.CodeAnalysis; |
||||
|
using System.Drawing; |
||||
|
using System.Drawing.Imaging; |
||||
|
using System.Runtime.InteropServices; |
||||
|
#endregion
|
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The color quantizer.
|
||||
|
/// </summary>
|
||||
|
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1650:ElementDocumentationMustBeSpelledCorrectly", Justification = "Reviewed. Suppression is OK here.")] |
||||
|
public static class ColorQuantizer |
||||
|
{ |
||||
|
#region Quantize methods
|
||||
|
/// <summary>The quantize.</summary>
|
||||
|
/// <param name="image">The image.</param>
|
||||
|
/// <param name="bitmapPixelFormat">The bitmap pixel format.</param>
|
||||
|
/// <returns>The quantized image with the recalculated color palette.</returns>
|
||||
|
public static Bitmap Quantize(Image image, PixelFormat bitmapPixelFormat) |
||||
|
{ |
||||
|
// Use dither by default
|
||||
|
return Quantize(image, bitmapPixelFormat, true); |
||||
|
} |
||||
|
|
||||
|
/// <summary>The quantize.</summary>
|
||||
|
/// <param name="image">The image.</param>
|
||||
|
/// <param name="pixelFormat">The pixel format.</param>
|
||||
|
/// <param name="useDither">The use dither.</param>
|
||||
|
/// <returns>The quantized image with the recalculated color palette.</returns>
|
||||
|
public static Bitmap Quantize(Image image, PixelFormat pixelFormat, bool useDither) |
||||
|
{ |
||||
|
Bitmap tryBitmap = image as Bitmap; |
||||
|
|
||||
|
if (tryBitmap != null && tryBitmap.PixelFormat == PixelFormat.Format32bppArgb) |
||||
|
{ |
||||
|
// The image passed to us is ALREADY a bitmap in the right format. No need to create
|
||||
|
// a copy and work from there.
|
||||
|
return DoQuantize(tryBitmap, pixelFormat, useDither); |
||||
|
} |
||||
|
|
||||
|
// We use these values a lot
|
||||
|
int width = image.Width; |
||||
|
int height = image.Height; |
||||
|
Rectangle sourceRect = Rectangle.FromLTRB(0, 0, width, height); |
||||
|
|
||||
|
// create a 24-bit rgb version of the source image
|
||||
|
using (Bitmap bitmapSource = new Bitmap(width, height, PixelFormat.Format32bppArgb)) |
||||
|
{ |
||||
|
using (Graphics grfx = Graphics.FromImage(bitmapSource)) |
||||
|
{ |
||||
|
grfx.DrawImage(image, sourceRect, 0, 0, width, height, GraphicsUnit.Pixel); |
||||
|
} |
||||
|
|
||||
|
return DoQuantize(bitmapSource, pixelFormat, useDither); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Does the quantize.
|
||||
|
/// </summary>
|
||||
|
/// <param name="bitmapSource">The bitmap source.</param>
|
||||
|
/// <param name="pixelFormat">The pixel format.</param>
|
||||
|
/// <param name="useDither">if set to <c>true</c> [use dither].</param>
|
||||
|
/// <returns>The quantized image with the recalculated color palette.</returns>
|
||||
|
private static Bitmap DoQuantize(Bitmap bitmapSource, PixelFormat pixelFormat, bool useDither) |
||||
|
{ |
||||
|
// We use these values a lot
|
||||
|
int width = bitmapSource.Width; |
||||
|
int height = bitmapSource.Height; |
||||
|
Rectangle sourceRect = Rectangle.FromLTRB(0, 0, width, height); |
||||
|
|
||||
|
Bitmap bitmapOptimized = null; |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
// Create a bitmap with the same dimensions and the desired format
|
||||
|
bitmapOptimized = new Bitmap(width, height, pixelFormat); |
||||
|
|
||||
|
// Lock the bits of the source image for reading.
|
||||
|
// we will need to write if we do the dither.
|
||||
|
BitmapData bitmapDataSource = bitmapSource.LockBits( |
||||
|
sourceRect, |
||||
|
ImageLockMode.ReadWrite, |
||||
|
PixelFormat.Format32bppArgb); |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
// Perform the first pass, which generates the octree data
|
||||
|
// Create an Octree
|
||||
|
Octree octree = new Octree(pixelFormat); |
||||
|
|
||||
|
// Stride might be negative, indicating inverted row order.
|
||||
|
// Allocate a managed buffer for the pixel data, and copy it from the unmanaged pointer.
|
||||
|
int strideSource = Math.Abs(bitmapDataSource.Stride); |
||||
|
byte[] sourceDataBuffer = new byte[strideSource * height]; |
||||
|
Marshal.Copy(bitmapDataSource.Scan0, sourceDataBuffer, 0, sourceDataBuffer.Length); |
||||
|
|
||||
|
// We could skip every other row and/or every other column when sampling the colors
|
||||
|
// of the source image, rather than hitting every other pixel. It doesn't seem to
|
||||
|
// degrade the resulting image too much. But it doesn't really help the performance
|
||||
|
// too much because the majority of the time seems to be spent in other places.
|
||||
|
|
||||
|
// For every row
|
||||
|
int rowStartSource = 0; |
||||
|
for (int ndxRow = 0; ndxRow < height; ndxRow += 1) |
||||
|
{ |
||||
|
// For each column
|
||||
|
for (int ndxCol = 0; ndxCol < width; ndxCol += 1) |
||||
|
{ |
||||
|
// Add the color (4 bytes per pixel - ARGB)
|
||||
|
Pixel pixel = GetSourcePixel(sourceDataBuffer, rowStartSource, ndxCol); |
||||
|
octree.AddColor(pixel); |
||||
|
} |
||||
|
|
||||
|
rowStartSource += strideSource; |
||||
|
} |
||||
|
|
||||
|
// Get the optimized colors
|
||||
|
Color[] colors = octree.GetPaletteColors(); |
||||
|
|
||||
|
// Set the palette from the octree
|
||||
|
ColorPalette palette = bitmapOptimized.Palette; |
||||
|
for (var ndx = 0; ndx < palette.Entries.Length; ++ndx) |
||||
|
{ |
||||
|
// Use the colors we calculated
|
||||
|
// for the rest, just set to transparent
|
||||
|
palette.Entries[ndx] = (ndx < colors.Length) |
||||
|
? colors[ndx] |
||||
|
: Color.Transparent; |
||||
|
} |
||||
|
|
||||
|
bitmapOptimized.Palette = palette; |
||||
|
|
||||
|
// Lock the bits of the optimized bitmap for writing.
|
||||
|
// we will also need to read if we are doing 1bpp or 4bpp
|
||||
|
BitmapData bitmapDataOutput = bitmapOptimized.LockBits(sourceRect, ImageLockMode.ReadWrite, pixelFormat); |
||||
|
try |
||||
|
{ |
||||
|
// Create a managed array for the destination bytes given the desired color depth
|
||||
|
// and marshal the unmanaged data to the managed array
|
||||
|
int strideOutput = Math.Abs(bitmapDataOutput.Stride); |
||||
|
byte[] bitmapOutputBuffer = new byte[strideOutput * height]; |
||||
|
|
||||
|
// For each source pixel, compute the appropriate color index
|
||||
|
rowStartSource = 0; |
||||
|
int rowStartOutput = 0; |
||||
|
|
||||
|
for (int ndxRow = 0; ndxRow < height; ++ndxRow) |
||||
|
{ |
||||
|
// For each column
|
||||
|
for (int ndxCol = 0; ndxCol < width; ++ndxCol) |
||||
|
{ |
||||
|
// Get the source color
|
||||
|
Pixel pixel = GetSourcePixel(sourceDataBuffer, rowStartSource, ndxCol); |
||||
|
|
||||
|
// Get the closest palette index
|
||||
|
int paletteIndex = octree.GetPaletteIndex(pixel); |
||||
|
|
||||
|
// If we want to dither and this isn't the transparent pixel
|
||||
|
if (useDither && pixel.Alpha != 0) |
||||
|
{ |
||||
|
// Calculate the error
|
||||
|
Color paletteColor = colors[paletteIndex]; |
||||
|
int deltaRed = pixel.Red - paletteColor.R; |
||||
|
int deltaGreen = pixel.Green - paletteColor.G; |
||||
|
int deltaBlue = pixel.Blue - paletteColor.B; |
||||
|
|
||||
|
// Propagate the dither error.
|
||||
|
// we'll use a standard Floyd-Steinberg matrix (1/16):
|
||||
|
// | 0 0 0 |
|
||||
|
// | 0 x 7 |
|
||||
|
// | 3 5 1 |
|
||||
|
|
||||
|
// Make sure we're not on the right-hand edge
|
||||
|
if (ndxCol + 1 < width) |
||||
|
{ |
||||
|
DitherSourcePixel(sourceDataBuffer, rowStartSource, ndxCol + 1, deltaRed, deltaGreen, deltaBlue, 7); |
||||
|
} |
||||
|
|
||||
|
// Make sure we're not already on the bottom row
|
||||
|
if (ndxRow + 1 < height) |
||||
|
{ |
||||
|
int nextRow = rowStartSource + strideSource; |
||||
|
|
||||
|
// Make sure we're not on the left-hand column
|
||||
|
if (ndxCol > 0) |
||||
|
{ |
||||
|
// Down one row, but back one pixel
|
||||
|
DitherSourcePixel(sourceDataBuffer, nextRow, ndxCol - 1, deltaRed, deltaGreen, deltaBlue, 3); |
||||
|
} |
||||
|
|
||||
|
// pixel directly below us
|
||||
|
DitherSourcePixel(sourceDataBuffer, nextRow, ndxCol, deltaRed, deltaGreen, deltaBlue, 5); |
||||
|
|
||||
|
// Make sure we're not on the right-hand column
|
||||
|
if (ndxCol + 1 < width) |
||||
|
{ |
||||
|
// Down one row, but right one pixel
|
||||
|
DitherSourcePixel(sourceDataBuffer, nextRow, ndxCol + 1, deltaRed, deltaGreen, deltaBlue, 1); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Set the bitmap index based on the format
|
||||
|
switch (pixelFormat) |
||||
|
{ |
||||
|
case PixelFormat.Format8bppIndexed: |
||||
|
// Each byte is a palette index
|
||||
|
bitmapOutputBuffer[rowStartOutput + ndxCol] = (byte)paletteIndex; |
||||
|
break; |
||||
|
|
||||
|
case PixelFormat.Format4bppIndexed: |
||||
|
// Each byte contains two pixels
|
||||
|
bitmapOutputBuffer[rowStartOutput + (ndxCol >> 1)] |= ((ndxCol & 1) == 1) |
||||
|
? (byte)(paletteIndex & 0x0f) // lower nibble
|
||||
|
: (byte)(paletteIndex << 4); // upper nibble
|
||||
|
break; |
||||
|
|
||||
|
case PixelFormat.Format1bppIndexed: |
||||
|
// Each byte contains eight pixels
|
||||
|
if (paletteIndex != 0) |
||||
|
{ |
||||
|
bitmapOutputBuffer[rowStartOutput + (ndxCol >> 3)] |= (byte)(0x80 >> (ndxCol & 0x07)); |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
rowStartSource += strideSource; |
||||
|
rowStartOutput += strideOutput; |
||||
|
} |
||||
|
|
||||
|
// Now copy the calculated pixel bytes from the managed array to the unmanaged bitmap
|
||||
|
Marshal.Copy(bitmapOutputBuffer, 0, bitmapDataOutput.Scan0, bitmapOutputBuffer.Length); |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
bitmapOptimized.UnlockBits(bitmapDataOutput); |
||||
|
bitmapDataOutput = null; |
||||
|
} |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
bitmapSource.UnlockBits(bitmapDataSource); |
||||
|
bitmapDataSource = null; |
||||
|
} |
||||
|
} |
||||
|
catch (Exception) |
||||
|
{ |
||||
|
// If any exception is thrown, dispose of the bitmap object
|
||||
|
// we've been working on before we rethrow and bail
|
||||
|
if (bitmapOptimized != null) |
||||
|
{ |
||||
|
bitmapOptimized.Dispose(); |
||||
|
} |
||||
|
|
||||
|
throw; |
||||
|
} |
||||
|
|
||||
|
// Caller is responsible for disposing of this bitmap!
|
||||
|
return bitmapOptimized; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Dithers the source pixel.
|
||||
|
/// </summary>
|
||||
|
/// <param name="buffer">The buffer.</param>
|
||||
|
/// <param name="rowStart">The row start.</param>
|
||||
|
/// <param name="col">The column.</param>
|
||||
|
/// <param name="deltaRed">The delta red.</param>
|
||||
|
/// <param name="deltaGreen">The delta green.</param>
|
||||
|
/// <param name="deltaBlue">The delta blue.</param>
|
||||
|
/// <param name="weight">The weight.</param>
|
||||
|
private static void DitherSourcePixel(byte[] buffer, int rowStart, int col, int deltaRed, int deltaGreen, int deltaBlue, int weight) |
||||
|
{ |
||||
|
int colorIndex = rowStart + (col * 4); |
||||
|
buffer[colorIndex + 2] = ChannelAdjustment(buffer[colorIndex + 2], (deltaRed * weight) >> 4); |
||||
|
buffer[colorIndex + 1] = ChannelAdjustment(buffer[colorIndex + 1], (deltaGreen * weight) >> 4); |
||||
|
buffer[colorIndex] = ChannelAdjustment(buffer[colorIndex], (deltaBlue * weight) >> 4); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the source pixel.
|
||||
|
/// </summary>
|
||||
|
/// <param name="buffer">The buffer.</param>
|
||||
|
/// <param name="rowStart">The row start.</param>
|
||||
|
/// <param name="col">The column.</param>
|
||||
|
/// <returns>The source pixel.</returns>
|
||||
|
private static Pixel GetSourcePixel(byte[] buffer, int rowStart, int col) |
||||
|
{ |
||||
|
int colorIndex = rowStart + (col * 4); |
||||
|
return new Pixel |
||||
|
{ |
||||
|
Alpha = buffer[colorIndex + 3], |
||||
|
Red = buffer[colorIndex + 2], |
||||
|
Green = buffer[colorIndex + 1], |
||||
|
Blue = buffer[colorIndex] |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
#endregion
|
||||
|
|
||||
|
/// <summary>Gets the channel adjustment.</summary>
|
||||
|
/// <param name="current">The current.</param>
|
||||
|
/// <param name="offset">The offset.</param>
|
||||
|
/// <returns>The channel adjustment.</returns>
|
||||
|
private static byte ChannelAdjustment(byte current, int offset) |
||||
|
{ |
||||
|
return (byte)Math.Min(255, Math.Max(0, current + offset)); |
||||
|
} |
||||
|
|
||||
|
#region Octree class
|
||||
|
|
||||
|
/// <summary>data structure for storing and reducing colors used in the source image</summary>
|
||||
|
private class Octree |
||||
|
{ |
||||
|
/// <summary>The m_max colors.</summary>
|
||||
|
private readonly int octreeMaxColors; |
||||
|
|
||||
|
/// <summary>The m_reducible nodes.</summary>
|
||||
|
private readonly OctreeNode[] octreeReducibleNodes; |
||||
|
|
||||
|
/// <summary>The m_color count.</summary>
|
||||
|
private int octreeColorCount; |
||||
|
|
||||
|
/// <summary>The m_has transparent.</summary>
|
||||
|
private bool octreeHasTransparent; |
||||
|
|
||||
|
/// <summary>The m_last argb.</summary>
|
||||
|
private int octreeLastArgb; |
||||
|
|
||||
|
/// <summary>The m_last node.</summary>
|
||||
|
private OctreeNode octreeLastNode; |
||||
|
|
||||
|
/// <summary>The m_palette.</summary>
|
||||
|
private Color[] octreePalette; |
||||
|
|
||||
|
/// <summary>The m_root.</summary>
|
||||
|
private OctreeNode octreeRoot; |
||||
|
|
||||
|
/// <summary>Initializes a new instance of the <see cref="Octree"/> class. Constructor</summary>
|
||||
|
/// <param name="pixelFormat">desired pixel format</param>
|
||||
|
internal Octree(PixelFormat pixelFormat) |
||||
|
{ |
||||
|
// figure out the maximum colors from the pixel format passed in
|
||||
|
switch (pixelFormat) |
||||
|
{ |
||||
|
case PixelFormat.Format1bppIndexed: |
||||
|
this.octreeMaxColors = 2; |
||||
|
break; |
||||
|
|
||||
|
case PixelFormat.Format4bppIndexed: |
||||
|
this.octreeMaxColors = 16; |
||||
|
break; |
||||
|
|
||||
|
case PixelFormat.Format8bppIndexed: |
||||
|
this.octreeMaxColors = 256; |
||||
|
break; |
||||
|
|
||||
|
default: |
||||
|
throw new ArgumentException("Invalid Pixel Format", "pixelFormat"); |
||||
|
} |
||||
|
|
||||
|
// we need a list for each level that may have reducible nodes.
|
||||
|
// since the last level (level 7) is only made up of leaf nodes,
|
||||
|
// we don't need an array entry for it.
|
||||
|
this.octreeReducibleNodes = new OctreeNode[7]; |
||||
|
|
||||
|
// add the initial level-0 root node
|
||||
|
this.octreeRoot = new OctreeNode(0, this); |
||||
|
} |
||||
|
|
||||
|
/// <summary>Add the given pixel color to the octree</summary>
|
||||
|
/// <param name="pixel">points to the pixel color we want to add</param>
|
||||
|
internal void AddColor(Pixel pixel) |
||||
|
{ |
||||
|
// If the A value is non-zero (ignore the transparent color)
|
||||
|
if (pixel.Alpha > 0) |
||||
|
{ |
||||
|
// If we have a previous node and this color is the same as the last...
|
||||
|
if (this.octreeLastNode != null && pixel.Argb == this.octreeLastArgb) |
||||
|
{ |
||||
|
// Just add this color to the same last node
|
||||
|
this.octreeLastNode.AddColor(pixel); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// Just start at the root. If a new color is added,
|
||||
|
// add one to the count (otherwise 0).
|
||||
|
this.octreeColorCount += this.octreeRoot.AddColor(pixel) ? 1 : 0; |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// Flag that we have a transparent color.
|
||||
|
this.octreeHasTransparent = true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Given a pixel color, return the index of the palette entry
|
||||
|
/// we want to use in the reduced image. If the color is not in the
|
||||
|
/// octree, OctreeNode.GetPaletteIndex will return a negative number.
|
||||
|
/// In that case, we will have to calculate the palette index the brute-force
|
||||
|
/// method by computing the least distance to each color in the palette array.
|
||||
|
/// </summary>
|
||||
|
/// <param name="pixel">pointer to the pixel color we want to look up</param>
|
||||
|
/// <returns>index of the palette entry we want to use for this color</returns>
|
||||
|
internal int GetPaletteIndex(Pixel pixel) |
||||
|
{ |
||||
|
int paletteIndex = 0; |
||||
|
|
||||
|
// transparent is always the first entry, so if this is transparent,
|
||||
|
// don't do anything.
|
||||
|
if (pixel.Alpha > 0) |
||||
|
{ |
||||
|
paletteIndex = this.octreeRoot.GetPaletteIndex(pixel); |
||||
|
|
||||
|
// returns -1 if this value isn't in the octree.
|
||||
|
if (paletteIndex < 0) |
||||
|
{ |
||||
|
// Use the brute-force method of calculating the closest color
|
||||
|
// in the palette to the one we want
|
||||
|
int minDistance = int.MaxValue; |
||||
|
for (int ndx = 0; ndx < this.octreePalette.Length; ++ndx) |
||||
|
{ |
||||
|
Color paletteColor = this.octreePalette[ndx]; |
||||
|
|
||||
|
// Calculate the delta for each channel
|
||||
|
int deltaRed = pixel.Red - paletteColor.R; |
||||
|
int deltaGreen = pixel.Green - paletteColor.G; |
||||
|
int deltaBlue = pixel.Blue - paletteColor.B; |
||||
|
|
||||
|
// Calculate the distance-squared by summing each channel's square
|
||||
|
int distance = (deltaRed * deltaRed) + (deltaGreen * deltaGreen) + (deltaBlue * deltaBlue); |
||||
|
if (distance < minDistance) |
||||
|
{ |
||||
|
minDistance = distance; |
||||
|
paletteIndex = ndx; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return paletteIndex; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Return a color palette for the computed octree.
|
||||
|
/// </summary>
|
||||
|
/// <returns>A color palette for the computed octree</returns>
|
||||
|
internal Color[] GetPaletteColors() |
||||
|
{ |
||||
|
// If we haven't already computed it, compute it now
|
||||
|
if (this.octreePalette == null) |
||||
|
{ |
||||
|
// Start at the second-to-last reducible level
|
||||
|
int reductionLevel = this.octreeReducibleNodes.Length - 1; |
||||
|
|
||||
|
// We want to subtract one from the target if we have a transparent
|
||||
|
// bit because we want to save room for that special color
|
||||
|
int targetCount = this.octreeMaxColors - (this.octreeHasTransparent ? 1 : 0); |
||||
|
|
||||
|
// While we still have more colors than the target...
|
||||
|
while (this.octreeColorCount > targetCount) |
||||
|
{ |
||||
|
// Find the first reducible node, starting with the last level
|
||||
|
// that can have reducible nodes
|
||||
|
while (reductionLevel > 0 && this.octreeReducibleNodes[reductionLevel] == null) |
||||
|
{ |
||||
|
--reductionLevel; |
||||
|
} |
||||
|
|
||||
|
if (this.octreeReducibleNodes[reductionLevel] == null) |
||||
|
{ |
||||
|
// Shouldn't get here
|
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
// We should have a node now
|
||||
|
OctreeNode newLeaf = this.octreeReducibleNodes[reductionLevel]; |
||||
|
this.octreeReducibleNodes[reductionLevel] = newLeaf.NextReducibleNode; |
||||
|
this.octreeColorCount -= newLeaf.Reduce() - 1; |
||||
|
} |
||||
|
|
||||
|
if (reductionLevel == 0 && !this.octreeHasTransparent) |
||||
|
{ |
||||
|
// If this was the top-most level, we now only have a single color
|
||||
|
// representing the average. That's not what we want.
|
||||
|
// use just black and white
|
||||
|
this.octreePalette = new Color[2]; |
||||
|
this.octreePalette[0] = Color.Black; |
||||
|
this.octreePalette[1] = Color.White; |
||||
|
|
||||
|
// And empty the octree so it always picks the closer of the black and white entries
|
||||
|
this.octreeRoot = new OctreeNode(0, this); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// Now walk the tree, adding all the remaining colors to the list
|
||||
|
int paletteIndex = 0; |
||||
|
this.octreePalette = new Color[this.octreeColorCount + (this.octreeHasTransparent ? 1 : 0)]; |
||||
|
|
||||
|
// Add the transparent color if we need it
|
||||
|
if (this.octreeHasTransparent) |
||||
|
{ |
||||
|
this.octreePalette[paletteIndex++] = Color.Transparent; |
||||
|
} |
||||
|
|
||||
|
// Have the nodes insert their leaf colors
|
||||
|
this.octreeRoot.AddColorsToPalette(this.octreePalette, ref paletteIndex); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return this.octreePalette; |
||||
|
} |
||||
|
|
||||
|
/// <summary>set up the values we need to reuse the given pointer if the next color is argb</summary>
|
||||
|
/// <param name="node">last node to which we added a color</param>
|
||||
|
/// <param name="argb">last color we added</param>
|
||||
|
private void SetLastNode(OctreeNode node, int argb) |
||||
|
{ |
||||
|
this.octreeLastNode = node; |
||||
|
this.octreeLastArgb = argb; |
||||
|
} |
||||
|
|
||||
|
/// <summary>When a reducible node is added, this method is called to add it to the appropriate
|
||||
|
/// reducible node list (given its level)</summary>
|
||||
|
/// <param name="reducibleNode">node to add to a reducible list</param>
|
||||
|
private void AddReducibleNode(OctreeNode reducibleNode) |
||||
|
{ |
||||
|
// hook this node into the front of the list.
|
||||
|
// this means the last one added will be the first in the list.
|
||||
|
reducibleNode.NextReducibleNode = this.octreeReducibleNodes[reducibleNode.Level]; |
||||
|
this.octreeReducibleNodes[reducibleNode.Level] = reducibleNode; |
||||
|
} |
||||
|
|
||||
|
#region OctreeNode class
|
||||
|
|
||||
|
/// <summary>Node for an Octree structure</summary>
|
||||
|
private class OctreeNode |
||||
|
{ |
||||
|
/// <summary>The s_level masks.</summary>
|
||||
|
private static readonly byte[] NodeLevelMasks = { 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 }; |
||||
|
|
||||
|
/// <summary>The m_level.</summary>
|
||||
|
private readonly int nodeLevel; |
||||
|
|
||||
|
/// <summary>The m_octree.</summary>
|
||||
|
private readonly Octree nodeOctree; |
||||
|
|
||||
|
/// <summary>The m_blue sum.</summary>
|
||||
|
private int nodeBlueSum; |
||||
|
|
||||
|
/// <summary>The m_child nodes.</summary>
|
||||
|
private OctreeNode[] nodeChildNodes; |
||||
|
|
||||
|
/// <summary>The m_green sum.</summary>
|
||||
|
private int nodeGreenSum; |
||||
|
|
||||
|
/// <summary>The m_is leaf.</summary>
|
||||
|
private bool nodeIsLeaf; |
||||
|
|
||||
|
/// <summary>The m_palette index.</summary>
|
||||
|
private int nodePaletteIndex; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The pixel count.Information we need to calculate the average color for a set of pixels
|
||||
|
/// </summary>
|
||||
|
private int nodePixelCount; |
||||
|
|
||||
|
/// <summary>The m_red sum.</summary>
|
||||
|
private int nodeRedSum; |
||||
|
|
||||
|
/// <summary>Initializes a new instance of the <see cref="OctreeNode"/> class. Constructor</summary>
|
||||
|
/// <param name="level">level for this node</param>
|
||||
|
/// <param name="octree">owning octree</param>
|
||||
|
internal OctreeNode(int level, Octree octree) |
||||
|
{ |
||||
|
this.nodeOctree = octree; |
||||
|
this.nodeLevel = level; |
||||
|
|
||||
|
// Since there are only eight levels, if we get to level 7
|
||||
|
// We automatically make this a leaf node
|
||||
|
this.nodeIsLeaf = level == 7; |
||||
|
|
||||
|
if (!this.nodeIsLeaf) |
||||
|
{ |
||||
|
// Create the child array
|
||||
|
this.nodeChildNodes = new OctreeNode[8]; |
||||
|
|
||||
|
// Add it to the tree's reducible node list
|
||||
|
this.nodeOctree.AddReducibleNode(this); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>Gets Level.</summary>
|
||||
|
internal int Level |
||||
|
{ |
||||
|
get { return this.nodeLevel; } |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets NextReducibleNode.
|
||||
|
/// Once we compute a palette, this will be set
|
||||
|
/// to the palette index associated with this leaf node.
|
||||
|
/// Nodes are arranged in linked lists of reducible nodes for a given level.
|
||||
|
/// this field and property is used to traverse that list.
|
||||
|
/// </summary>
|
||||
|
internal OctreeNode NextReducibleNode { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the average color for this node.
|
||||
|
/// </summary>
|
||||
|
private Color NodeColor |
||||
|
{ |
||||
|
get |
||||
|
{ |
||||
|
// Average color is the sum of each channel divided by the pixel count
|
||||
|
return Color.FromArgb( |
||||
|
this.nodeRedSum / this.nodePixelCount, |
||||
|
this.nodeGreenSum / this.nodePixelCount, |
||||
|
this.nodeBlueSum / this.nodePixelCount); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Add the given color to this node if it is a leaf, otherwise recurse
|
||||
|
/// down the appropriate child
|
||||
|
/// </summary>
|
||||
|
/// <param name="pixel">color to add</param>
|
||||
|
/// <returns>true if a new color was added to the octree</returns>
|
||||
|
internal bool AddColor(Pixel pixel) |
||||
|
{ |
||||
|
bool colorAdded; |
||||
|
if (this.nodeIsLeaf) |
||||
|
{ |
||||
|
// Increase the pixel count for this node, and if
|
||||
|
// the result is 1, then this is a new color
|
||||
|
colorAdded = ++this.nodePixelCount == 1; |
||||
|
|
||||
|
// Add the color to the running sum for this node
|
||||
|
this.nodeRedSum += pixel.Red; |
||||
|
this.nodeGreenSum += pixel.Green; |
||||
|
this.nodeBlueSum += pixel.Blue; |
||||
|
|
||||
|
// Set the last node so we can quickly process adjacent pixels
|
||||
|
// with the same color
|
||||
|
this.nodeOctree.SetLastNode(this, pixel.Argb); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// Get the index at this level for the rgb values
|
||||
|
int childIndex = this.GetChildIndex(pixel); |
||||
|
|
||||
|
// If there is no child, add one now to the next level
|
||||
|
if (this.nodeChildNodes[childIndex] == null) |
||||
|
{ |
||||
|
this.nodeChildNodes[childIndex] = new OctreeNode(this.nodeLevel + 1, this.nodeOctree); |
||||
|
} |
||||
|
|
||||
|
// Recurse...
|
||||
|
colorAdded = this.nodeChildNodes[childIndex].AddColor(pixel); |
||||
|
} |
||||
|
|
||||
|
return colorAdded; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Given a source color, return the palette index to use for the reduced image.
|
||||
|
/// Returns -1 if the color is not represented in the octree (this happens if
|
||||
|
/// the color has been dithered into a new color that did not appear in the
|
||||
|
/// original image when the octree was formed in pass 1.
|
||||
|
/// </summary>
|
||||
|
/// <param name="pixel">source color to look up</param>
|
||||
|
/// <returns>palette index to use</returns>
|
||||
|
internal int GetPaletteIndex(Pixel pixel) |
||||
|
{ |
||||
|
int paletteIndex = -1; |
||||
|
if (this.nodeIsLeaf) |
||||
|
{ |
||||
|
// Use this leaf node's palette index
|
||||
|
paletteIndex = this.nodePaletteIndex; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// Get the index at this level for the rgb values
|
||||
|
var childIndex = this.GetChildIndex(pixel); |
||||
|
if (this.nodeChildNodes[childIndex] != null) |
||||
|
{ |
||||
|
// Recurse...
|
||||
|
paletteIndex = this.nodeChildNodes[childIndex].GetPaletteIndex(pixel); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return paletteIndex; |
||||
|
} |
||||
|
|
||||
|
/// <summary>Reduce this node by combining all child nodes</summary>
|
||||
|
/// <returns>number of nodes removed</returns>
|
||||
|
internal int Reduce() |
||||
|
{ |
||||
|
int numReduced = 0; |
||||
|
if (!this.nodeIsLeaf) |
||||
|
{ |
||||
|
// For each child
|
||||
|
foreach (OctreeNode node in this.nodeChildNodes) |
||||
|
{ |
||||
|
if (node != null) |
||||
|
{ |
||||
|
OctreeNode childNode = node; |
||||
|
|
||||
|
// add the pixel count from the child
|
||||
|
this.nodePixelCount += childNode.nodePixelCount; |
||||
|
|
||||
|
// add the running color sums from the child
|
||||
|
this.nodeRedSum += childNode.nodeRedSum; |
||||
|
this.nodeGreenSum += childNode.nodeGreenSum; |
||||
|
this.nodeBlueSum += childNode.nodeBlueSum; |
||||
|
|
||||
|
++numReduced; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
this.nodeChildNodes = null; |
||||
|
this.nodeIsLeaf = true; |
||||
|
} |
||||
|
|
||||
|
return numReduced; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// If this is a leaf node, add its color to the palette array at the given index
|
||||
|
/// and increment the index. If not a leaf, recurse the children nodes.
|
||||
|
/// </summary>
|
||||
|
/// <param name="colorArray">array of colors</param>
|
||||
|
/// <param name="paletteIndex">index of the next empty slot in the array</param>
|
||||
|
internal void AddColorsToPalette(Color[] colorArray, ref int paletteIndex) |
||||
|
{ |
||||
|
if (this.nodeIsLeaf) |
||||
|
{ |
||||
|
// Save our index and increment the running index
|
||||
|
this.nodePaletteIndex = paletteIndex++; |
||||
|
|
||||
|
// The color for this node is the average color, which is created by
|
||||
|
// dividing the running sums for each channel by the pixel count
|
||||
|
colorArray[this.nodePaletteIndex] = this.NodeColor; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// Just run through all the non-null children and recurse
|
||||
|
foreach (OctreeNode node in this.nodeChildNodes) |
||||
|
{ |
||||
|
if (node != null) |
||||
|
{ |
||||
|
node.AddColorsToPalette(colorArray, ref paletteIndex); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Return the child index for a given color.
|
||||
|
/// Depends on which level this node is in.
|
||||
|
/// </summary>
|
||||
|
/// <param name="pixel">color pixel to compute</param>
|
||||
|
/// <returns>child index (0-7)</returns>
|
||||
|
private int GetChildIndex(Pixel pixel) |
||||
|
{ |
||||
|
// lvl: 0 1 2 3 4 5 6 7
|
||||
|
// bit: 7 6 5 4 3 2 1 0
|
||||
|
var shift = 7 - this.nodeLevel; |
||||
|
int mask = NodeLevelMasks[this.nodeLevel]; |
||||
|
return ((pixel.Red & mask) >> (shift - 2)) | |
||||
|
((pixel.Green & mask) >> (shift - 1)) | |
||||
|
((pixel.Blue & mask) >> shift); |
||||
|
} |
||||
|
} |
||||
|
#endregion
|
||||
|
} |
||||
|
#endregion
|
||||
|
|
||||
|
#region Pixel class for ARGB values
|
||||
|
/// <summary>
|
||||
|
/// Structure of a Format32bppArgb pixel in memory.
|
||||
|
/// </summary>
|
||||
|
private class Pixel |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets or sets the blue component of the pixel.
|
||||
|
/// </summary>
|
||||
|
public byte Blue { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the green component of the pixel.
|
||||
|
/// </summary>
|
||||
|
public byte Green { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the red component of the pixel.
|
||||
|
/// </summary>
|
||||
|
public byte Red { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the alpha component of the pixel.
|
||||
|
/// </summary>
|
||||
|
public byte Alpha { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the argb combination of the pixel.
|
||||
|
/// </summary>
|
||||
|
public int Argb |
||||
|
{ |
||||
|
get |
||||
|
{ |
||||
|
return this.Alpha << 24 | this.Red << 16 | this.Green << 8 | this.Blue; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
#endregion
|
||||
|
} |
||||
|
} |
||||
@ -1,543 +0,0 @@ |
|||||
// -----------------------------------------------------------------------
|
|
||||
// <copyright file="OctreeQuantizer.cs" company="James South">
|
|
||||
// Copyright (c) James South.
|
|
||||
// Licensed under the Apache License, Version 2.0.
|
|
||||
// </copyright>
|
|
||||
// -----------------------------------------------------------------------
|
|
||||
|
|
||||
namespace ImageProcessor.Imaging |
|
||||
{ |
|
||||
#region Using
|
|
||||
using System; |
|
||||
using System.Collections; |
|
||||
using System.Drawing; |
|
||||
using System.Drawing.Imaging; |
|
||||
#endregion
|
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Encapsulates methods to calculate the colour palette if an image using an octree pattern.
|
|
||||
/// </summary>
|
|
||||
internal class OctreeQuantizer : Quantizer |
|
||||
{ |
|
||||
#region Fields
|
|
||||
/// <summary>
|
|
||||
/// Stores the tree.
|
|
||||
/// </summary>
|
|
||||
private readonly Octree octree; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// The maximum allowed color depth.
|
|
||||
/// </summary>
|
|
||||
private readonly int maxColors; |
|
||||
#endregion
|
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Initializes a new instance of the <see cref="T:ImageProcessor.Imaging.OctreeQuantizer">OctreeQuantizer</see> class.
|
|
||||
/// </summary>
|
|
||||
/// <remarks>
|
|
||||
/// The Octree quantizer is a two pass algorithm. The initial pass sets up the octree,
|
|
||||
/// the second pass quantizes a colour based on the nodes in the tree
|
|
||||
/// </remarks>
|
|
||||
/// <param name="maxColors">The maximum number of colours to return, maximum 255.</param>
|
|
||||
/// <param name="maxColorBits">The number of significant bits minimum 1, maximum 8.</param>
|
|
||||
public OctreeQuantizer(int maxColors, int maxColorBits) |
|
||||
: base(false) |
|
||||
{ |
|
||||
if (maxColors > 255) |
|
||||
{ |
|
||||
throw new ArgumentOutOfRangeException("maxColors", maxColors, "The number of colours should be less than 256"); |
|
||||
} |
|
||||
|
|
||||
if ((maxColorBits < 1) | (maxColorBits > 8)) |
|
||||
{ |
|
||||
throw new ArgumentOutOfRangeException("maxColorBits", maxColorBits, "This should be between 1 and 8"); |
|
||||
} |
|
||||
|
|
||||
// Construct the octree
|
|
||||
this.octree = new Octree(maxColorBits); |
|
||||
this.maxColors = maxColors; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Process the pixel in the first pass of the algorithm.
|
|
||||
/// </summary>
|
|
||||
/// <param name="pixel">The pixel to quantize</param>
|
|
||||
/// <remarks>
|
|
||||
/// This function need only be overridden if your quantize algorithm needs two passes,
|
|
||||
/// such as an Octree quantizer.
|
|
||||
/// </remarks>
|
|
||||
protected override void InitialQuantizePixel(Color32 pixel) |
|
||||
{ |
|
||||
// Add the colour to the octree
|
|
||||
this.octree.AddColor(pixel); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Override this to process the pixel in the second pass of the algorithm.
|
|
||||
/// </summary>
|
|
||||
/// <param name="pixel">The pixel to quantize</param>
|
|
||||
/// <returns>The quantized value.</returns>
|
|
||||
protected override byte QuantizePixel(Color32 pixel) |
|
||||
{ |
|
||||
// The colour at [this.maxColors] is set to transparent
|
|
||||
byte paletteIndex; |
|
||||
|
|
||||
// Get the palette index if this non-transparent
|
|
||||
if (pixel.Alpha > 0) |
|
||||
{ |
|
||||
paletteIndex = (byte)this.octree.GetPaletteIndex(pixel); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
paletteIndex = (byte)this.maxColors; |
|
||||
} |
|
||||
|
|
||||
return paletteIndex; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Retrieve the palette for the quantized image
|
|
||||
/// </summary>
|
|
||||
/// <param name="original">Any old palette, this is overwritten</param>
|
|
||||
/// <returns>The new colour palette</returns>
|
|
||||
protected override ColorPalette GetPalette(ColorPalette original) |
|
||||
{ |
|
||||
// First off convert the octree to this.maxColors colours
|
|
||||
ArrayList palette = this.octree.Palletize(this.maxColors - 1); |
|
||||
|
|
||||
// Then convert the palette based on those colours
|
|
||||
for (int index = 0; index < palette.Count; index++) |
|
||||
{ |
|
||||
original.Entries[index] = (Color)palette[index]; |
|
||||
} |
|
||||
|
|
||||
// Add the transparent colour
|
|
||||
original.Entries[this.maxColors] = Color.FromArgb(0, 0, 0, 0); |
|
||||
|
|
||||
return original; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Describes a tree data structure in which each internal node has exactly eight children.
|
|
||||
/// </summary>
|
|
||||
private class Octree |
|
||||
{ |
|
||||
#region Fields
|
|
||||
/// <summary>
|
|
||||
/// Mask used when getting the appropriate pixels for a given node
|
|
||||
/// </summary>
|
|
||||
private static readonly int[] mask = new int[8] { 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 }; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// The root of the octree
|
|
||||
/// </summary>
|
|
||||
private readonly OctreeNode root; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Number of leaves in the tree
|
|
||||
/// </summary>
|
|
||||
private int leafCount; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Array of reducible nodes
|
|
||||
/// </summary>
|
|
||||
private OctreeNode[] reducibleNodes; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Maximum number of significant bits in the image
|
|
||||
/// </summary>
|
|
||||
private int maxColorBits; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Store the last node quantized
|
|
||||
/// </summary>
|
|
||||
private OctreeNode previousNode; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Cache the previous color quantized
|
|
||||
/// </summary>
|
|
||||
private int previousColor; |
|
||||
#endregion
|
|
||||
|
|
||||
#region Constructors
|
|
||||
/// <summary>
|
|
||||
/// Initializes a new instance of the <see cref="T:ImageProcessor.Imaging.OctreeQuantizer.Octree">Octree</see> class.
|
|
||||
/// </summary>
|
|
||||
/// <param name="maxBits">The maximum number of significant bits in the image</param>
|
|
||||
public Octree(int maxBits) |
|
||||
{ |
|
||||
this.maxColorBits = maxBits; |
|
||||
this.leafCount = 0; |
|
||||
this.reducibleNodes = new OctreeNode[9]; |
|
||||
this.root = new OctreeNode(0, this.maxColorBits, this); |
|
||||
this.previousColor = 0; |
|
||||
this.previousNode = null; |
|
||||
} |
|
||||
#endregion
|
|
||||
|
|
||||
#region Properties
|
|
||||
/// <summary>
|
|
||||
/// Gets or sets the number of leaves in the tree
|
|
||||
/// </summary>
|
|
||||
public int Leaves |
|
||||
{ |
|
||||
get { return this.leafCount; } |
|
||||
set { this.leafCount = value; } |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the array of reducible nodes
|
|
||||
/// </summary>
|
|
||||
protected OctreeNode[] ReducibleNodes |
|
||||
{ |
|
||||
get { return this.reducibleNodes; } |
|
||||
} |
|
||||
#endregion
|
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Add a given colour value to the octree
|
|
||||
/// </summary>
|
|
||||
/// <param name="pixel">
|
|
||||
/// The color value to add.
|
|
||||
/// </param>
|
|
||||
public void AddColor(Color32 pixel) |
|
||||
{ |
|
||||
// Check if this request is for the same colour as the last
|
|
||||
if (this.previousColor == pixel.ARGB) |
|
||||
{ |
|
||||
// If so, check if I have a previous node setup. This will only occur if the first colour in the image
|
|
||||
// happens to be black, with an alpha component of zero.
|
|
||||
if (null == this.previousNode) |
|
||||
{ |
|
||||
this.previousColor = pixel.ARGB; |
|
||||
this.root.AddColor(pixel, this.maxColorBits, 0, this); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
// Just update the previous node
|
|
||||
this.previousNode.Increment(pixel); |
|
||||
} |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
this.previousColor = pixel.ARGB; |
|
||||
this.root.AddColor(pixel, this.maxColorBits, 0, this); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Reduce the depth of the tree
|
|
||||
/// </summary>
|
|
||||
public void Reduce() |
|
||||
{ |
|
||||
// Find the deepest level containing at least one reducible node
|
|
||||
int index = this.maxColorBits - 1; |
|
||||
while ((index > 0) && (this.reducibleNodes[index] == null)) |
|
||||
{ |
|
||||
index--; |
|
||||
} |
|
||||
|
|
||||
// Reduce the node most recently added to the list at level 'index'
|
|
||||
OctreeNode node = this.reducibleNodes[index]; |
|
||||
this.reducibleNodes[index] = node.NextReducible; |
|
||||
|
|
||||
// Decrement the leaf count after reducing the node
|
|
||||
this.leafCount -= node.Reduce(); |
|
||||
|
|
||||
// And just in case I've reduced the last color to be added, and the next color to
|
|
||||
// be added is the same, invalidate the previousNode...
|
|
||||
this.previousNode = null; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Convert the nodes in the octree to a palette with a maximum of colorCount colours
|
|
||||
/// </summary>
|
|
||||
/// <param name="colorCount">The maximum number of colours</param>
|
|
||||
/// <returns>An array list with the palletized colours</returns>
|
|
||||
public ArrayList Palletize(int colorCount) |
|
||||
{ |
|
||||
while (this.Leaves > colorCount) |
|
||||
{ |
|
||||
this.Reduce(); |
|
||||
} |
|
||||
|
|
||||
// Now palletize the nodes
|
|
||||
ArrayList palette = new ArrayList(this.Leaves); |
|
||||
int paletteIndex = 0; |
|
||||
this.root.ConstructPalette(palette, ref paletteIndex); |
|
||||
|
|
||||
// And return the palette
|
|
||||
return palette; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Get the palette index for the passed colour.
|
|
||||
/// </summary>
|
|
||||
/// <param name="pixel">
|
|
||||
/// The color to return the palette index for.
|
|
||||
/// </param>
|
|
||||
/// <returns>
|
|
||||
/// The palette index for the passed colour.
|
|
||||
/// </returns>
|
|
||||
public int GetPaletteIndex(Color32 pixel) |
|
||||
{ |
|
||||
return this.root.GetPaletteIndex(pixel, 0); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Keep track of the previous node that was quantized
|
|
||||
/// </summary>
|
|
||||
/// <param name="node">The node last quantized</param>
|
|
||||
protected void TrackPrevious(OctreeNode node) |
|
||||
{ |
|
||||
this.previousNode = node; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Class which encapsulates each node in the tree
|
|
||||
/// </summary>
|
|
||||
protected class OctreeNode |
|
||||
{ |
|
||||
#region Fields
|
|
||||
/// <summary>
|
|
||||
/// Flag indicating that this is a leaf node
|
|
||||
/// </summary>
|
|
||||
private bool leaf; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Number of pixels in this node
|
|
||||
/// </summary>
|
|
||||
private int pixelCount; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Red component
|
|
||||
/// </summary>
|
|
||||
private int red; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Green Component
|
|
||||
/// </summary>
|
|
||||
private int green; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Blue component
|
|
||||
/// </summary>
|
|
||||
private int blue; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Pointers to any child nodes
|
|
||||
/// </summary>
|
|
||||
private OctreeNode[] children; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// The index of this node in the palette
|
|
||||
/// </summary>
|
|
||||
private int paletteIndex; |
|
||||
#endregion
|
|
||||
|
|
||||
#region Constructors
|
|
||||
/// <summary>
|
|
||||
/// Initializes a new instance of the <see cref="OctreeNode"/> class.
|
|
||||
/// </summary>
|
|
||||
/// <param name="level">
|
|
||||
/// The level in the tree = 0 - 7
|
|
||||
/// </param>
|
|
||||
/// <param name="colorBits">
|
|
||||
/// The number of significant color bits in the image
|
|
||||
/// </param>
|
|
||||
/// <param name="octree">
|
|
||||
/// The tree to which this node belongs
|
|
||||
/// </param>
|
|
||||
public OctreeNode(int level, int colorBits, Octree octree) |
|
||||
{ |
|
||||
// Construct the new node
|
|
||||
this.leaf = level == colorBits; |
|
||||
|
|
||||
this.red = this.green = this.blue = 0; |
|
||||
this.pixelCount = 0; |
|
||||
|
|
||||
// If a leaf, increment the leaf count
|
|
||||
if (this.leaf) |
|
||||
{ |
|
||||
octree.Leaves++; |
|
||||
this.NextReducible = null; |
|
||||
this.children = null; |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
// Otherwise add this to the reducible nodes
|
|
||||
this.NextReducible = octree.ReducibleNodes[level]; |
|
||||
octree.ReducibleNodes[level] = this; |
|
||||
this.children = new OctreeNode[8]; |
|
||||
} |
|
||||
} |
|
||||
#endregion
|
|
||||
|
|
||||
#region Properties
|
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets or the next reducible node
|
|
||||
/// </summary>
|
|
||||
public OctreeNode NextReducible { get; private set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the child nodes
|
|
||||
/// </summary>
|
|
||||
private OctreeNode[] Children |
|
||||
{ |
|
||||
get { return this.children; } |
|
||||
} |
|
||||
#endregion
|
|
||||
|
|
||||
#region Methods
|
|
||||
/// <summary>
|
|
||||
/// Add a color into the tree
|
|
||||
/// </summary>
|
|
||||
/// <param name="pixel">The color</param>
|
|
||||
/// <param name="colorBits">The number of significant color bits</param>
|
|
||||
/// <param name="level">The level in the tree</param>
|
|
||||
/// <param name="octree">The tree to which this node belongs</param>
|
|
||||
public void AddColor(Color32 pixel, int colorBits, int level, Octree octree) |
|
||||
{ |
|
||||
// Update the color information if this is a leaf
|
|
||||
if (this.leaf) |
|
||||
{ |
|
||||
this.Increment(pixel); |
|
||||
|
|
||||
// Setup the previous node
|
|
||||
octree.TrackPrevious(this); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
// Go to the next level down in the tree
|
|
||||
int shift = 7 - level; |
|
||||
int index = ((pixel.Red & mask[level]) >> (shift - 2)) | |
|
||||
((pixel.Green & mask[level]) >> (shift - 1)) | |
|
||||
((pixel.Blue & mask[level]) >> shift); |
|
||||
|
|
||||
OctreeNode child = this.Children[index]; |
|
||||
|
|
||||
if (null == child) |
|
||||
{ |
|
||||
// Create a new child node & store in the array
|
|
||||
child = new OctreeNode(level + 1, colorBits, octree); |
|
||||
this.Children[index] = child; |
|
||||
} |
|
||||
|
|
||||
// Add the color to the child node
|
|
||||
child.AddColor(pixel, colorBits, level + 1, octree); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Reduce this node by removing all of its children
|
|
||||
/// </summary>
|
|
||||
/// <returns>The number of leaves removed</returns>
|
|
||||
public int Reduce() |
|
||||
{ |
|
||||
this.red = this.green = this.blue = 0; |
|
||||
int childPosition = 0; |
|
||||
|
|
||||
// Loop through all children and add their information to this node
|
|
||||
for (int index = 0; index < 8; index++) |
|
||||
{ |
|
||||
if (null != this.Children[index]) |
|
||||
{ |
|
||||
this.red += this.Children[index].red; |
|
||||
this.green += this.Children[index].green; |
|
||||
this.blue += this.Children[index].blue; |
|
||||
this.pixelCount += this.Children[index].pixelCount; |
|
||||
++childPosition; |
|
||||
this.Children[index] = null; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// Now change this to a leaf node
|
|
||||
this.leaf = true; |
|
||||
|
|
||||
// Return the number of nodes to decrement the leaf count by
|
|
||||
return childPosition - 1; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Traverse the tree, building up the color palette
|
|
||||
/// </summary>
|
|
||||
/// <param name="palette">The palette</param>
|
|
||||
/// <param name="currentPaletteIndex">The current palette index</param>
|
|
||||
public void ConstructPalette(ArrayList palette, ref int currentPaletteIndex) |
|
||||
{ |
|
||||
if (this.leaf) |
|
||||
{ |
|
||||
// Consume the next palette index
|
|
||||
this.paletteIndex = currentPaletteIndex++; |
|
||||
|
|
||||
// And set the color of the palette entry
|
|
||||
palette.Add(Color.FromArgb(this.red / this.pixelCount, this.green / this.pixelCount, this.blue / this.pixelCount)); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
// Loop through children looking for leaves
|
|
||||
for (int index = 0; index < 8; index++) |
|
||||
{ |
|
||||
if (null != this.children[index]) |
|
||||
{ |
|
||||
this.children[index].ConstructPalette(palette, ref currentPaletteIndex); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Return the palette index for the passed color.
|
|
||||
/// </summary>
|
|
||||
/// <param name="pixel">
|
|
||||
/// The pixel.
|
|
||||
/// </param>
|
|
||||
/// <param name="level">
|
|
||||
/// The level.
|
|
||||
/// </param>
|
|
||||
/// <returns>
|
|
||||
/// The palette index for the passed color.
|
|
||||
/// </returns>
|
|
||||
public int GetPaletteIndex(Color32 pixel, int level) |
|
||||
{ |
|
||||
int currentPaletteIndex = this.paletteIndex; |
|
||||
|
|
||||
if (!this.leaf) |
|
||||
{ |
|
||||
int shift = 7 - level; |
|
||||
int index = ((pixel.Red & mask[level]) >> (shift - 2)) | |
|
||||
((pixel.Green & mask[level]) >> (shift - 1)) | |
|
||||
((pixel.Blue & mask[level]) >> shift); |
|
||||
|
|
||||
if (null != this.children[index]) |
|
||||
{ |
|
||||
currentPaletteIndex = this.children[index].GetPaletteIndex(pixel, level + 1); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
throw new Exception("Didn't expect this!"); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return currentPaletteIndex; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Increment the pixel count and add to the color information
|
|
||||
/// </summary>
|
|
||||
/// <param name="pixel">
|
|
||||
/// The pixel.
|
|
||||
/// </param>
|
|
||||
public void Increment(Color32 pixel) |
|
||||
{ |
|
||||
this.pixelCount++; |
|
||||
this.red += pixel.Red; |
|
||||
this.green += pixel.Green; |
|
||||
this.blue += pixel.Blue; |
|
||||
} |
|
||||
#endregion
|
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,315 +0,0 @@ |
|||||
// -----------------------------------------------------------------------
|
|
||||
// <copyright file="Quantizer.cs" company="James South">
|
|
||||
// Copyright (c) James South.
|
|
||||
// Licensed under the Apache License, Version 2.0.
|
|
||||
// </copyright>
|
|
||||
// -----------------------------------------------------------------------
|
|
||||
|
|
||||
namespace ImageProcessor.Imaging |
|
||||
{ |
|
||||
#region Using
|
|
||||
using System; |
|
||||
using System.Drawing; |
|
||||
using System.Drawing.Imaging; |
|
||||
using System.Runtime.InteropServices; |
|
||||
#endregion
|
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Encapsulates methods to calculate the color palette of an image.
|
|
||||
/// </summary>
|
|
||||
internal abstract class Quantizer |
|
||||
{ |
|
||||
#region Fields
|
|
||||
/// <summary>
|
|
||||
/// The flag used to indicate whether a single pass or two passes are needed for quantization.
|
|
||||
/// </summary>
|
|
||||
private readonly bool singlePass; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// The size in bytes of the 32 bpp Colour structure.
|
|
||||
/// </summary>
|
|
||||
private readonly int pixelSize; |
|
||||
#endregion
|
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Initializes a new instance of the <see cref="T:ImageProcessor.Imaging.Quantizer">Quantizer</see> class.
|
|
||||
/// </summary>
|
|
||||
/// <param name="singlePass">
|
|
||||
/// If set to <see langword="true"/>, then the quantizer will loop through the source pixels once;
|
|
||||
/// otherwise, <see langword="false"/>.
|
|
||||
/// </param>
|
|
||||
protected Quantizer(bool singlePass) |
|
||||
{ |
|
||||
this.singlePass = singlePass; |
|
||||
this.pixelSize = Marshal.SizeOf(typeof(Color32)); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Quantizes the given <see cref="T:System.Drawing.Image">Image</see> and returns the resulting output
|
|
||||
/// <see cref="T:System.Drawing.Bitmap">Bitmap.</see>
|
|
||||
/// </summary>
|
|
||||
/// <param name="source">The image to quantize</param>
|
|
||||
/// <returns>
|
|
||||
/// A quantized <see cref="T:System.Drawing.Bitmap">Bitmap</see> version of the <see cref="T:System.Drawing.Image">Image</see>
|
|
||||
/// </returns>
|
|
||||
public Bitmap Quantize(Image source) |
|
||||
{ |
|
||||
// Get the size of the source image
|
|
||||
int height = source.Height; |
|
||||
int width = source.Width; |
|
||||
|
|
||||
// And construct a rectangle from these dimensions
|
|
||||
Rectangle bounds = new Rectangle(0, 0, width, height); |
|
||||
|
|
||||
// First off take a 32bpp copy of the image
|
|
||||
using (Bitmap copy = new Bitmap(width, height, PixelFormat.Format32bppArgb)) |
|
||||
{ |
|
||||
Bitmap output = null; |
|
||||
|
|
||||
// Define a pointer to the bitmap data
|
|
||||
BitmapData sourceData = null; |
|
||||
try |
|
||||
{ |
|
||||
// And construct an 8bpp version
|
|
||||
output = new Bitmap(width, height, PixelFormat.Format8bppIndexed); |
|
||||
|
|
||||
// Now lock the bitmap into memory
|
|
||||
using (Graphics graphics = Graphics.FromImage(copy)) |
|
||||
{ |
|
||||
graphics.PageUnit = GraphicsUnit.Pixel; |
|
||||
|
|
||||
// Draw the source image onto the copy bitmap,
|
|
||||
// which will effect a widening as appropriate.
|
|
||||
graphics.DrawImage(source, bounds); |
|
||||
} |
|
||||
|
|
||||
// Get the source image bits and lock into memory
|
|
||||
sourceData = copy.LockBits(bounds, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); |
|
||||
|
|
||||
// Call the FirstPass function if not a single pass algorithm.
|
|
||||
// For something like an octree quantizer, this will run through
|
|
||||
// all image pixels, build a data structure, and create a palette.
|
|
||||
if (!this.singlePass) |
|
||||
{ |
|
||||
this.FirstPass(sourceData, width, height); |
|
||||
} |
|
||||
|
|
||||
// Then set the colour palette on the output bitmap. I'm passing in the current palette
|
|
||||
// as there's no way to construct a new, empty palette.
|
|
||||
output.Palette = this.GetPalette(output.Palette); |
|
||||
|
|
||||
// Then call the second pass which actually does the conversion
|
|
||||
this.SecondPass(sourceData, output, width, height, bounds); |
|
||||
} |
|
||||
catch |
|
||||
{ |
|
||||
if (output != null) |
|
||||
{ |
|
||||
output.Dispose(); |
|
||||
} |
|
||||
} |
|
||||
finally |
|
||||
{ |
|
||||
// Ensure that the bits are unlocked
|
|
||||
copy.UnlockBits(sourceData); |
|
||||
} |
|
||||
|
|
||||
// Last but not least, return the output bitmap
|
|
||||
return output; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Execute the first pass through the pixels in the image
|
|
||||
/// </summary>
|
|
||||
/// <param name="sourceData">The source data</param>
|
|
||||
/// <param name="width">The width in pixels of the image</param>
|
|
||||
/// <param name="height">The height in pixels of the image</param>
|
|
||||
protected virtual void FirstPass(BitmapData sourceData, int width, int height) |
|
||||
{ |
|
||||
// Define the source data pointers. The source row is a byte to
|
|
||||
// keep addition of the stride value easier (as this is in bytes)
|
|
||||
IntPtr sourceRow = sourceData.Scan0; |
|
||||
|
|
||||
// Loop through each row
|
|
||||
for (int row = 0; row < height; row++) |
|
||||
{ |
|
||||
// Set the source pixel to the first pixel in this row
|
|
||||
IntPtr sourcePixel = sourceRow; |
|
||||
|
|
||||
// And loop through each column
|
|
||||
for (int col = 0; col < width; col++) |
|
||||
{ |
|
||||
this.InitialQuantizePixel(new Color32(sourcePixel)); |
|
||||
sourcePixel = (IntPtr)((int)sourcePixel + this.pixelSize); |
|
||||
} |
|
||||
|
|
||||
// Now I have the pixel, call the FirstPassQuantize function.
|
|
||||
// Add the stride to the source row
|
|
||||
sourceRow = (IntPtr)((long)sourceRow + sourceData.Stride); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Execute a second pass through the bitmap
|
|
||||
/// </summary>
|
|
||||
/// <param name="sourceData">The source bitmap, locked into memory</param>
|
|
||||
/// <param name="output">The output bitmap</param>
|
|
||||
/// <param name="width">The width in pixels of the image</param>
|
|
||||
/// <param name="height">The height in pixels of the image</param>
|
|
||||
/// <param name="bounds">The bounding rectangle</param>
|
|
||||
protected virtual void SecondPass(BitmapData sourceData, Bitmap output, int width, int height, Rectangle bounds) |
|
||||
{ |
|
||||
BitmapData outputData = null; |
|
||||
|
|
||||
try |
|
||||
{ |
|
||||
// Lock the output bitmap into memory
|
|
||||
outputData = output.LockBits(bounds, ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed); |
|
||||
|
|
||||
// Define the source data pointers. The source row is a byte to
|
|
||||
// keep addition of the stride value easier (as this is in bytes)
|
|
||||
IntPtr sourceRow = sourceData.Scan0; |
|
||||
IntPtr sourcePixel = sourceRow; |
|
||||
IntPtr previousPixel = sourcePixel; |
|
||||
|
|
||||
// Now define the destination data pointers
|
|
||||
IntPtr destinationRow = outputData.Scan0; |
|
||||
IntPtr destinationPixel = destinationRow; |
|
||||
|
|
||||
// And convert the first pixel, so that I have values going into the loop.
|
|
||||
byte pixelValue = this.QuantizePixel(new Color32(sourcePixel)); |
|
||||
|
|
||||
// Assign the value of the first pixel
|
|
||||
Marshal.WriteByte(destinationPixel, pixelValue); |
|
||||
|
|
||||
// Loop through each row
|
|
||||
for (int row = 0; row < height; row++) |
|
||||
{ |
|
||||
// Set the source pixel to the first pixel in this row
|
|
||||
sourcePixel = sourceRow; |
|
||||
|
|
||||
// And set the destination pixel pointer to the first pixel in the row
|
|
||||
destinationPixel = destinationRow; |
|
||||
|
|
||||
// Loop through each pixel on this scan line
|
|
||||
for (int col = 0; col < width; col++) |
|
||||
{ |
|
||||
// Check if this is the same as the last pixel. If so use that value
|
|
||||
// rather than calculating it again. This is an inexpensive optimisation.
|
|
||||
if (Marshal.ReadByte(previousPixel) != Marshal.ReadByte(sourcePixel)) |
|
||||
{ |
|
||||
// Quantize the pixel
|
|
||||
pixelValue = this.QuantizePixel(new Color32(sourcePixel)); |
|
||||
|
|
||||
// And setup the previous pointer
|
|
||||
previousPixel = sourcePixel; |
|
||||
} |
|
||||
|
|
||||
// And set the pixel in the output
|
|
||||
Marshal.WriteByte(destinationPixel, pixelValue); |
|
||||
|
|
||||
sourcePixel = (IntPtr)((long)sourcePixel + this.pixelSize); |
|
||||
destinationPixel = (IntPtr)((long)destinationPixel + 1); |
|
||||
} |
|
||||
|
|
||||
// Add the stride to the source row
|
|
||||
sourceRow = (IntPtr)((long)sourceRow + sourceData.Stride); |
|
||||
|
|
||||
// And to the destination row
|
|
||||
destinationRow = (IntPtr)((long)destinationRow + outputData.Stride); |
|
||||
} |
|
||||
} |
|
||||
finally |
|
||||
{ |
|
||||
// Ensure that I unlock the output bits
|
|
||||
output.UnlockBits(outputData); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Override this to process the pixel in the first pass of the algorithm
|
|
||||
/// </summary>
|
|
||||
/// <param name="pixel">The pixel to quantize</param>
|
|
||||
/// <remarks>
|
|
||||
/// This function need only be overridden if your quantize algorithm needs two passes,
|
|
||||
/// such as an Octree quantizer.
|
|
||||
/// </remarks>
|
|
||||
protected virtual void InitialQuantizePixel(Color32 pixel) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Override this to process the pixel in the second pass of the algorithm.
|
|
||||
/// </summary>
|
|
||||
/// <param name="pixel">The pixel to quantize</param>
|
|
||||
/// <returns>The quantized value.</returns>
|
|
||||
protected abstract byte QuantizePixel(Color32 pixel); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Retrieve the palette for the quantized image
|
|
||||
/// </summary>
|
|
||||
/// <param name="original">Any old palette, this is overwritten</param>
|
|
||||
/// <returns>The new color palette</returns>
|
|
||||
protected abstract ColorPalette GetPalette(ColorPalette original); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Structure that defines a 32 bit color
|
|
||||
/// </summary>
|
|
||||
/// <remarks>
|
|
||||
/// This structure is used to read data from a 32 bits per pixel image
|
|
||||
/// in memory, and is ordered in this manner as this is the way that
|
|
||||
/// the data is laid out in memory
|
|
||||
/// </remarks>
|
|
||||
[StructLayout(LayoutKind.Explicit)] |
|
||||
public struct Color32 |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Holds the blue component of the color
|
|
||||
/// </summary>
|
|
||||
[FieldOffset(0)] |
|
||||
public byte Blue; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Holds the green component of the color
|
|
||||
/// </summary>
|
|
||||
[FieldOffset(1)] |
|
||||
public byte Green; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Holds the red component of the color
|
|
||||
/// </summary>
|
|
||||
[FieldOffset(2)] |
|
||||
public byte Red; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Holds the alpha component of the color
|
|
||||
/// </summary>
|
|
||||
[FieldOffset(3)] |
|
||||
public byte Alpha; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Permits the color32 to be treated as a 32 bit integer.
|
|
||||
/// </summary>
|
|
||||
[FieldOffset(0)] |
|
||||
public int ARGB; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Initializes a new instance of the <see cref="T:ImageProcessor.Imaging.Quantizer.Color32">Color32</see> structure.
|
|
||||
/// </summary>
|
|
||||
/// <param name="sourcePixel">The pointer to the pixel.</param>
|
|
||||
public Color32(IntPtr sourcePixel) |
|
||||
{ |
|
||||
this = (Color32)Marshal.PtrToStructure(sourcePixel, typeof(Color32)); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the color for this Color32 object
|
|
||||
/// </summary>
|
|
||||
public Color Color |
|
||||
{ |
|
||||
get { return Color.FromArgb(this.Alpha, this.Red, this.Green, this.Blue); } |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,11 @@ |
|||||
|
<StyleCopSettings Version="105"> |
||||
|
<Analyzers> |
||||
|
<Analyzer AnalyzerId="StyleCop.CSharp.DocumentationRules"> |
||||
|
<AnalyzerSettings> |
||||
|
<StringProperty Name="CompanyName">James South</StringProperty> |
||||
|
<StringProperty Name="Copyright">Copyright (c) James South. |
||||
|
Licensed under the Apache License, Version 2.0.</StringProperty> |
||||
|
</AnalyzerSettings> |
||||
|
</Analyzer> |
||||
|
</Analyzers> |
||||
|
</StyleCopSettings> |
||||
@ -1 +0,0 @@ |
|||||
eaff612b7db9e40f185c91161fd9c977faec69bb |
|
||||
@ -1 +0,0 @@ |
|||||
2af35ccdf0476cbe432b2440be45ffd0f6c414f4 |
|
||||
@ -1 +0,0 @@ |
|||||
1ac41e14e3ae5f8ac9b06bcfbbacc6c4a9841863 |
|
||||
@ -1 +0,0 @@ |
|||||
13403db94dce99fd4e73114b3334d4ef0c2b4ae5 |
|
||||
@ -1 +0,0 @@ |
|||||
8a3fd4491298fec4626034f03e534caac7f22941 |
|
||||
@ -1,43 +0,0 @@ |
|||||
<?xml version="1.0" encoding="utf-8"?> |
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd"> |
|
||||
<metadata> |
|
||||
<id>ImageProcessor.Web</id> |
|
||||
<version>2.2.0.1</version> |
|
||||
<title>ImageProcessor.Web</title> |
|
||||
<authors>James South</authors> |
|
||||
<owners>James South</owners> |
|
||||
<projectUrl>http://jimbobsquarepants.github.com/ImageProcessor/</projectUrl> |
|
||||
<requireLicenseAcceptance>false</requireLicenseAcceptance> |
|
||||
<description>ImageProcessor.Web adds a configurable HttpModule to your website which allows on-the-fly processing of image files. The module also comes with a file and browser based cache that can handle up to 12,960,000 images increasing your processing output and saving precious server memory. |
|
||||
|
|
||||
Methods include; Resize, Rotate, Flip, Crop, Watermark, Filter, Saturation, Brightness, Contrast, Quality, Format, Vignette, and Transparency. |
|
||||
|
|
||||
This package also requires Microsoft.Bcl.Async -pre on .NET 4.0 which will be added in the background on install if applicable. |
|
||||
|
|
||||
If you use ImageProcessor please get in touch via my twitter @james_m_south |
|
||||
|
|
||||
|
|
||||
Feedback is always welcome.</description> |
|
||||
<summary>An extension to ImageProcessor that allows on-the-fly processing of image files in an ASP.NET website</summary> |
|
||||
<releaseNotes>Fixed cache bug which caused unneccessary processing of images. |
|
||||
|
|
||||
If upgrading from < 2.2.0.0 You will have to delete your cache if upgrading to this version as the database differs.</releaseNotes> |
|
||||
<copyright>James South</copyright> |
|
||||
<language>en-GB</language> |
|
||||
<tags>Image, Imaging, ASP, Performance, Processing, HttpModule, Cache, Resize, Rotate, Flip, Crop, Filter, Effects, Quality, Watermark, Alpha, Vignette, Saturation, Brightness, Contrast, Gif, Jpeg, Bitmap, Png, Fluent</tags> |
|
||||
<dependencies> |
|
||||
<dependency id="ImageProcessor" version="1.5.0.0" /> |
|
||||
<dependency id="Csharp-Sqlite" version="3.7.7.1" /> |
|
||||
</dependencies> |
|
||||
</metadata> |
|
||||
<files> |
|
||||
<file src="content\net40\web.config.transform" target="content\net40\web.config.transform" /> |
|
||||
<file src="content\net45\web.config.transform" target="content\net45\web.config.transform" /> |
|
||||
<file src="..\ImageProcessor.Web\bin\Release\ImageProcessor.Web.dll" target="lib\net40\ImageProcessor.Web.dll" /> |
|
||||
<file src="lib\net40\System.Runtime.dll" target="lib\net40\System.Runtime.dll" /> |
|
||||
<file src="lib\net40\System.Threading.Tasks.dll" target="lib\net40\System.Threading.Tasks.dll" /> |
|
||||
<file src="..\ImageProcessor.Web\bin\Release\ImageProcessor.Web.dll" target="lib\net45\ImageProcessor.Web.dll" /> |
|
||||
<file src="tools\net40\install.ps1" target="tools\net40\install.ps1" /> |
|
||||
<file src="tools\net45\install.ps1" target="tools\net45\install.ps1" /> |
|
||||
</files> |
|
||||
</package> |
|
||||
@ -1 +0,0 @@ |
|||||
46b009d93ab9f1ea75f1ea1efb0073b3d369d3e5 |
|
||||
@ -1 +0,0 @@ |
|||||
50dc5dc47c964ccc80bb8abb22650a579ae796c3 |
|
||||
@ -1 +0,0 @@ |
|||||
9142f8cdad57d5c52d8721112c18a8d26c6f9817 |
|
||||
@ -1 +0,0 @@ |
|||||
24114542de37d7b4463b56749e64e34b0d43a9cc |
|
||||
@ -1 +0,0 @@ |
|||||
3de0aa82f042cfdda2764554fb34e686b401df85 |
|
||||
@ -0,0 +1 @@ |
|||||
|
8b33cb0b4f13802b62d2511239e212680ad67158 |
||||
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 6.5 KiB |
@ -0,0 +1,37 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<DirectedGraph GraphDirection="LeftToRight" xmlns="http://schemas.microsoft.com/vs/2009/dgml"> |
||||
|
<Nodes> |
||||
|
<Node Id="ImageProcessor.Web_NET45" Label="ImageProcessor.Web_NET45" Category="Project" /> |
||||
|
<Node Id="Csharp-Sqlite 3.7.7.1" Label="Csharp-Sqlite 3.7.7.1" Category="Package" /> |
||||
|
<Node Id="sqlite-net 1.0.7" Label="sqlite-net 1.0.7" Category="Package" /> |
||||
|
<Node Id="ImageProcessor.Web" Label="ImageProcessor.Web" Category="Project" /> |
||||
|
<Node Id="Csharp-Sqlite 3.7.7.1" Label="Csharp-Sqlite 3.7.7.1" Category="Package" /> |
||||
|
<Node Id="Microsoft.Bcl 1.0.19" Label="Microsoft.Bcl 1.0.19" Category="Package" /> |
||||
|
<Node Id="Microsoft.Bcl.Async 1.0.16" Label="Microsoft.Bcl.Async 1.0.16" Category="Package" /> |
||||
|
<Node Id="Microsoft.Bcl.Build 1.0.6" Label="Microsoft.Bcl.Build 1.0.6" Category="Package" /> |
||||
|
<Node Id="sqlite-net 1.0.7" Label="sqlite-net 1.0.7" Category="Package" /> |
||||
|
</Nodes> |
||||
|
<Links> |
||||
|
<Link Source="ImageProcessor.Web_NET45" Target="Csharp-Sqlite 3.7.7.1" Category="Installed Package" /> |
||||
|
<Link Source="ImageProcessor.Web_NET45" Target="sqlite-net 1.0.7" Category="Installed Package" /> |
||||
|
<Link Source="Microsoft.Bcl 1.0.19" Target="Microsoft.Bcl.Build 1.0.6" Category="Package Dependency" /> |
||||
|
<Link Source="Microsoft.Bcl.Async 1.0.16" Target="Microsoft.Bcl 1.0.19" Category="Package Dependency" /> |
||||
|
<Link Source="ImageProcessor.Web" Target="Csharp-Sqlite 3.7.7.1" Category="Installed Package" /> |
||||
|
<Link Source="ImageProcessor.Web" Target="Microsoft.Bcl.Async 1.0.16" Category="Installed Package" /> |
||||
|
<Link Source="ImageProcessor.Web" Target="sqlite-net 1.0.7" Category="Installed Package" /> |
||||
|
</Links> |
||||
|
<Categories> |
||||
|
<Category Id="Project" /> |
||||
|
<Category Id="Package" /> |
||||
|
</Categories> |
||||
|
<Styles> |
||||
|
<Style TargetType="Node" GroupLabel="Project" ValueLabel="True"> |
||||
|
<Condition Expression="HasCategory('Project')" /> |
||||
|
<Setter Property="Background" Value="Blue" /> |
||||
|
</Style> |
||||
|
<Style TargetType="Link" GroupLabel="Package Dependency" ValueLabel="True"> |
||||
|
<Condition Expression="HasCategory('Package Dependency')" /> |
||||
|
<Setter Property="Background" Value="Yellow" /> |
||||
|
</Style> |
||||
|
</Styles> |
||||
|
</DirectedGraph> |
||||
@ -1,212 +0,0 @@ |
|||||
/* ==|== Flexo 2.0.1 ============================================================= |
|
||||
Author: James South |
|
||||
twitter : http://twitter.com/James_M_South |
|
||||
github : https://github.com/JimBobSquarePants/Flexo |
|
||||
Copyright (c) James South. |
|
||||
Licensed under the Apache License v2.0. |
|
||||
============================================================================== */ |
|
||||
|
|
||||
/* ============================================================================= |
|
||||
Base |
|
||||
========================================================================== */ |
|
||||
html { |
|
||||
/*Use the iOS devices hardware accelerator to provide native scrolling*/ |
|
||||
-webkit-overflow-scrolling: touch; |
|
||||
/* Prevents iOS text size adjust after orientation change, without disabling user zoom. */ |
|
||||
-webkit-text-size-adjust: 100%; |
|
||||
-ms-text-size-adjust: 100%; |
|
||||
} |
|
||||
|
|
||||
html, body { |
|
||||
height: 100%; |
|
||||
margin: 0; |
|
||||
position: relative; |
|
||||
} |
|
||||
|
|
||||
.page { |
|
||||
min-height: 100%; |
|
||||
position: relative; |
|
||||
margin-bottom: -150px; |
|
||||
padding-bottom: 150px; |
|
||||
-webkit-box-sizing: border-box; |
|
||||
-moz-box-sizing: border-box; |
|
||||
-ms-box-sizing: border-box; |
|
||||
box-sizing: border-box; |
|
||||
} |
|
||||
|
|
||||
.page.no-box { |
|
||||
padding-bottom: 0; |
|
||||
} |
|
||||
|
|
||||
.page-push, .page-footer { |
|
||||
height: 150px; |
|
||||
} |
|
||||
|
|
||||
.page-footer { |
|
||||
margin: 0 auto; |
|
||||
position: relative; |
|
||||
z-index: 1; |
|
||||
} |
|
||||
|
|
||||
.container { |
|
||||
margin: 0 auto; |
|
||||
/* Manages width in a single place */ |
|
||||
width: 95%; |
|
||||
max-width: 1140px; |
|
||||
} |
|
||||
|
|
||||
/* Contains floats so all columns can float left*/ |
|
||||
.container:before, |
|
||||
.container:after, |
|
||||
.row:before, |
|
||||
.row:after { |
|
||||
content: ""; |
|
||||
display: table; |
|
||||
} |
|
||||
|
|
||||
.container:after, |
|
||||
.row:after { |
|
||||
clear: both; |
|
||||
} |
|
||||
|
|
||||
/* ============================================================================= |
|
||||
Grid |
|
||||
========================================================================== */ |
|
||||
|
|
||||
[class*="clmn"] { |
|
||||
display: block; |
|
||||
-webkit-box-sizing: border-box; |
|
||||
-moz-box-sizing: border-box; |
|
||||
-ms-box-sizing: border-box; |
|
||||
box-sizing: border-box; |
|
||||
min-height: 1px; |
|
||||
float: left; |
|
||||
} |
|
||||
|
|
||||
/* ==|== media queries =================================================== |
|
||||
Portrait phone viewport to Landscape phone < 767px |
|
||||
========================================================================== */ |
|
||||
@media screen and (max-width: 767px) { |
|
||||
body:not(.flexo-fixed) [class*="clmn"], |
|
||||
body:not(.flexo-fixed) [class*="offset"] { |
|
||||
float: none; |
|
||||
width: 100%; |
|
||||
margin-left: 0!important; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/* ============================================================================= |
|
||||
Grid |
|
||||
========================================================================== */ |
|
||||
[class*="clmn"] + [class*="clmn"]:not([class*="offset"]) { |
|
||||
margin-left: 2%; |
|
||||
} |
|
||||
|
|
||||
/* Columns */ |
|
||||
|
|
||||
/* Full width calculated with margins */ |
|
||||
.clmn1 { |
|
||||
width: 100%; |
|
||||
} |
|
||||
|
|
||||
/* 2 column */ |
|
||||
.clmn2 { |
|
||||
width: 49%; |
|
||||
} |
|
||||
|
|
||||
/* 3 column */ |
|
||||
.clmn3 { |
|
||||
width: 32%; |
|
||||
} |
|
||||
|
|
||||
/* 4 column */ |
|
||||
.clmn4 { |
|
||||
width: 23.5%; |
|
||||
} |
|
||||
|
|
||||
/* 5 column */ |
|
||||
.clmn5 { |
|
||||
width: 18.4%; |
|
||||
} |
|
||||
|
|
||||
/* Fillers*/ |
|
||||
/* 2/3 column */ |
|
||||
.clmn2-3 { |
|
||||
width: 66%; |
|
||||
} |
|
||||
|
|
||||
/* 3/4 column */ |
|
||||
.clmn3-4 { |
|
||||
width: 74.5%; |
|
||||
} |
|
||||
|
|
||||
/* 2/5 column */ |
|
||||
.clmn2-5 { |
|
||||
width: 38.8%; |
|
||||
} |
|
||||
|
|
||||
/* 3/5 column */ |
|
||||
.clmn3-5 { |
|
||||
width: 59.2%; |
|
||||
} |
|
||||
|
|
||||
/* 4/5 column */ |
|
||||
.clmn4-5 { |
|
||||
width: 79.6%; |
|
||||
} |
|
||||
|
|
||||
/* Offsetting columns */ |
|
||||
|
|
||||
/*offset 1/2*/ |
|
||||
.offset2 { |
|
||||
margin-left: 51%; |
|
||||
} |
|
||||
|
|
||||
/*offset 1/3 */ |
|
||||
.offset3 { |
|
||||
margin-left: 34%; |
|
||||
} |
|
||||
|
|
||||
/*offset 1/4 */ |
|
||||
.offset4 { |
|
||||
margin-left: 25.5%; |
|
||||
} |
|
||||
|
|
||||
/*offset 1/5 */ |
|
||||
.offset5 { |
|
||||
margin-left: 20.4%; |
|
||||
} |
|
||||
|
|
||||
/* offset 2/3 */ |
|
||||
.offset2-3 { |
|
||||
margin-left: 68%; |
|
||||
} |
|
||||
|
|
||||
/* offset 3/4 */ |
|
||||
.offset3-4 { |
|
||||
margin-left: 76.5%; |
|
||||
} |
|
||||
|
|
||||
/* offset 2/5 */ |
|
||||
.offset2-5 { |
|
||||
margin-left: 40.8%; |
|
||||
} |
|
||||
|
|
||||
/* offset 3/5 */ |
|
||||
.offset3-5 { |
|
||||
margin-left: 61.2%; |
|
||||
} |
|
||||
|
|
||||
/* offset 4/5 */ |
|
||||
.offset4-5 { |
|
||||
margin-left: 81.6%; |
|
||||
} |
|
||||
|
|
||||
|
|
||||
/* ============================================================================= |
|
||||
Fixed Grid |
|
||||
========================================================================== */ |
|
||||
.flexo-fixed .container { |
|
||||
/* Manages width in a single place */ |
|
||||
width: 1140px; |
|
||||
} |
|
||||
@ -1,64 +0,0 @@ |
|||||
body { |
|
||||
font-family: "Segoe UI",Tahoma,Arial,Verdana,Sans-Serif; |
|
||||
color: #333; |
|
||||
} |
|
||||
|
|
||||
h1, h2, h3 { |
|
||||
font-family: "Segoe UI Light", "Segoe UI",Tahoma,Arial,Verdana,Sans-Serif; |
|
||||
font-weight: 400; |
|
||||
} |
|
||||
|
|
||||
h1 { |
|
||||
margin-top: 0; |
|
||||
font-size: 3em; |
|
||||
} |
|
||||
|
|
||||
section section { |
|
||||
padding-bottom: 1em; |
|
||||
margin-bottom: 2em; |
|
||||
} |
|
||||
|
|
||||
section section:nth-child(2n) { |
|
||||
background-color: #f3f3f3; |
|
||||
} |
|
||||
|
|
||||
.no-bullets { |
|
||||
padding-left: 0; |
|
||||
} |
|
||||
|
|
||||
.no-bullets > li { |
|
||||
list-style: none; |
|
||||
float: left; |
|
||||
margin-right: .5em; |
|
||||
} |
|
||||
|
|
||||
/* |
|
||||
* Clearfix: contain floats |
|
||||
* |
|
||||
* For modern browsers |
|
||||
* 1. The space content is one way to avoid an Opera bug when the |
|
||||
* `contenteditable` attribute is included anywhere else in the document. |
|
||||
* Otherwise it causes space to appear at the top and bottom of elements |
|
||||
* that receive the `clearfix` class. |
|
||||
* 2. The use of `table` rather than `block` is only necessary if using |
|
||||
* `:before` to contain the top-margins of child elements. |
|
||||
*/ |
|
||||
|
|
||||
.clearfix:before, |
|
||||
.clearfix:after { |
|
||||
content: " "; /* 1 */ |
|
||||
display: table; /* 2 */ |
|
||||
} |
|
||||
|
|
||||
.clearfix:after { |
|
||||
clear: both; |
|
||||
} |
|
||||
|
|
||||
/* |
|
||||
* For IE 6/7 only |
|
||||
* Include this rule to trigger hasLayout and contain floats. |
|
||||
*/ |
|
||||
|
|
||||
.clearfix { |
|
||||
*zoom: 1; |
|
||||
} |
|
||||
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 40 KiB |
@ -1,14 +0,0 @@ |
|||||
@model List<string> |
|
||||
@{ |
|
||||
ViewBag.Title = "About Us"; |
|
||||
} |
|
||||
|
|
||||
<h2>About</h2> |
|
||||
<p> |
|
||||
@foreach(string image in Model) |
|
||||
{ |
|
||||
string path = image + "?width=150"; |
|
||||
|
|
||||
<img src="@path" alt="@image"/> |
|
||||
} |
|
||||
</p> |
|
||||
@ -1,20 +0,0 @@ |
|||||
@model TimeSpan |
|
||||
@{ |
|
||||
Layout = null; |
|
||||
|
|
||||
double s = Model.TotalMilliseconds; |
|
||||
} |
|
||||
|
|
||||
<!DOCTYPE html> |
|
||||
|
|
||||
<html> |
|
||||
<head> |
|
||||
<title>@s</title> |
|
||||
</head> |
|
||||
<body> |
|
||||
<div> |
|
||||
Speed In Milliseconds: @s<br/> |
|
||||
Collision Rate: @ViewBag.Collision% |
|
||||
</div> |
|
||||
</body> |
|
||||
</html> |
|
||||
@ -1,18 +0,0 @@ |
|||||
@{ |
|
||||
ViewBag.Title = "Responsive"; |
|
||||
} |
|
||||
<style type="text/css"> |
|
||||
img |
|
||||
{ |
|
||||
max-width: 100%; |
|
||||
} |
|
||||
</style> |
|
||||
<h2> |
|
||||
Responsive</h2> |
|
||||
<img src="/Images/desert.jpg?width=480" srcset="/Images/desert.jpg?width=768 480w, /Images/penguins.jpg?width=979 640w 2x, /Images/jellyfish.jpg?width=480 2x" |
|
||||
alt="desert" /> |
|
||||
@*<img src="/Images/desert.jpg?width=480" srcset="/Images/desert.jpg?width=768 480w, /Images/jellyfish.jpg?width=480 2x, /Images/penguins.jpg?width=979 640w 2x" |
|
||||
alt="desert" />*@ @*<img src="/Images/desert.jpg?width=480" srcset="/Images/desert.jpg?width=768 480w, /Images/desert.jpg?width=979 768w" |
|
||||
alt="desert" />*@ |
|
||||
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script> |
|
||||
<script src="/Scripts/img.srcsect.pollyfill.js" type="text/javascript"></script> |
|
||||
@ -1,5 +0,0 @@ |
|||||
@using (Html.BeginForm("Upload", "Home", FormMethod.Post, new { enctype = "multipart/form-data" })) |
|
||||
{ |
|
||||
<input type="file" name="file" /> |
|
||||
<button type="submit">Upload</button> |
|
||||
} |
|
||||
@ -1,9 +0,0 @@ |
|||||
@model System.Web.Mvc.HandleErrorInfo |
|
||||
|
|
||||
@{ |
|
||||
ViewBag.Title = "Error"; |
|
||||
} |
|
||||
|
|
||||
<h2> |
|
||||
Sorry, an error occurred while processing your request. |
|
||||
</h2> |
|
||||
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 20 KiB |
@ -1 +0,0 @@ |
|||||
f4e00752418f01964b99ce300d96c16a9aa8d239 |
|
||||
@ -1 +0,0 @@ |
|||||
fd46e83948190f508b8906f0822eca8dd4eda2ef |
|
||||
@ -1 +0,0 @@ |
|||||
aa7c907774ef5492ae25ffb7e3ff9755257731bd |
|
||||
@ -1 +0,0 @@ |
|||||
1038ebe89d7cfb28ec96a421867a2b0efc6dfdb4 |
|
||||
@ -1 +0,0 @@ |
|||||
1456713687c96cd486c4ec8e57d3c4f6f6437f10 |
|
||||
@ -1 +0,0 @@ |
|||||
4c16fd6b5b6b8cc426bc290d15ab41523be29403 |
|
||||
@ -1 +0,0 @@ |
|||||
bbf8bc24452deb732adf66ef3488793859f4f5b7 |
|
||||
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 15 KiB |