From 0cf20f91cd7118d079b30ce96cfc9d46af9506cc Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 18 Aug 2016 21:28:43 +1000 Subject: [PATCH] Png encodes moar types [skip ci] Former-commit-id: ff132f190934d317f9736ee236502d63a0d4531d Former-commit-id: b5bc0988e86d58d6ecd39c9078d1f3f9b7c8b819 Former-commit-id: c4c5599022e0cb862cb78f7d81c5105f9c66a06c --- .../Formats/Png/Filters/AverageFilter.cs | 40 ++- .../Formats/Png/Filters/FilterType.cs | 1 + .../Formats/Png/Filters/NoneFilter.cs | 1 + .../Formats/Png/Filters/PaethFilter.cs | 75 ++-- .../Formats/Png/Filters/SubFilter.cs | 29 +- .../Formats/Png/Filters/UpFilter.cs | 29 +- .../Formats/Png/PngColorType.cs | 4 +- .../Formats/Png/PngEncoderCore.cs | 325 ++++++++---------- 8 files changed, 289 insertions(+), 215 deletions(-) diff --git a/src/ImageProcessorCore/Formats/Png/Filters/AverageFilter.cs b/src/ImageProcessorCore/Formats/Png/Filters/AverageFilter.cs index c7f3f537a..087c99514 100644 --- a/src/ImageProcessorCore/Formats/Png/Filters/AverageFilter.cs +++ b/src/ImageProcessorCore/Formats/Png/Filters/AverageFilter.cs @@ -1,11 +1,31 @@ -namespace ImageProcessorCore.Formats +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats { using System; + /// + /// The Average filter uses the average of the two neighboring pixels (left and above) to predict + /// the value of a pixel. + /// + /// internal static class AverageFilter { + /// + /// Decodes the scanline + /// + /// The scanline to decode + /// The previous scanline. + /// The bytes per pixel. + /// + /// The + /// public static byte[] Decode(byte[] scanline, byte[] previousScanline, int bytesPerPixel) { + // Average(x) + floor((Raw(x-bpp)+Prior(x))/2) byte[] result = new byte[scanline.Length]; for (int x = 1; x < scanline.Length; x++) @@ -19,9 +39,17 @@ return result; } + /// + /// Encodes the scanline + /// + /// The scanline to encode + /// The previous scanline. + /// The bytes per pixel. + /// The public static byte[] Encode(byte[] scanline, byte[] previousScanline, int bytesPerPixel) { - var encodedScanline = new byte[scanline.Length + 1]; + // Average(x) = Raw(x) - floor((Raw(x-bpp)+Prior(x))/2) + byte[] encodedScanline = new byte[scanline.Length + 1]; encodedScanline[0] = (byte)FilterType.Average; @@ -36,9 +64,15 @@ return encodedScanline; } + /// + /// Calculates the average value of two bytes + /// + /// The left byte + /// The above byte + /// The private static int Average(byte left, byte above) { - return Convert.ToInt32(Math.Floor((left + above) / 2.0)); + return Convert.ToInt32(Math.Floor((left + above) / 2.0D)); } } } diff --git a/src/ImageProcessorCore/Formats/Png/Filters/FilterType.cs b/src/ImageProcessorCore/Formats/Png/Filters/FilterType.cs index 0a8a519a3..6f1f29c21 100644 --- a/src/ImageProcessorCore/Formats/Png/Filters/FilterType.cs +++ b/src/ImageProcessorCore/Formats/Png/Filters/FilterType.cs @@ -7,6 +7,7 @@ namespace ImageProcessorCore.Formats { /// /// Provides enumeration of the various png filter types. + /// /// internal enum FilterType { diff --git a/src/ImageProcessorCore/Formats/Png/Filters/NoneFilter.cs b/src/ImageProcessorCore/Formats/Png/Filters/NoneFilter.cs index a92a53a90..f4551000b 100644 --- a/src/ImageProcessorCore/Formats/Png/Filters/NoneFilter.cs +++ b/src/ImageProcessorCore/Formats/Png/Filters/NoneFilter.cs @@ -8,6 +8,7 @@ namespace ImageProcessorCore.Formats /// /// The None filter, the scanline is transmitted unmodified; it is only necessary to /// insert a filter type byte before the data. + /// /// internal static class NoneFilter { diff --git a/src/ImageProcessorCore/Formats/Png/Filters/PaethFilter.cs b/src/ImageProcessorCore/Formats/Png/Filters/PaethFilter.cs index bb90ce06f..94117b9a8 100644 --- a/src/ImageProcessorCore/Formats/Png/Filters/PaethFilter.cs +++ b/src/ImageProcessorCore/Formats/Png/Filters/PaethFilter.cs @@ -1,11 +1,30 @@ -namespace ImageProcessorCore.Formats +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats { using System; + /// + /// The Paeth filter computes a simple linear function of the three neighboring pixels (left, above, upper left), + /// then chooses as predictor the neighboring pixel closest to the computed value. + /// This technique is due to Alan W. Paeth. + /// + /// internal static class PaethFilter { + /// + /// Decodes the scanline + /// + /// The scanline to decode + /// The previous scanline. + /// The bytes per pixel. + /// The public static byte[] Decode(byte[] scanline, byte[] previousScanline, int bytesPerPixel) { + // Paeth(x) + PaethPredictor(Raw(x-bpp), Prior(x), Prior(x-bpp)) byte[] result = new byte[scanline.Length]; for (int x = 1; x < scanline.Length; x++) @@ -14,16 +33,23 @@ byte above = previousScanline[x]; byte upperLeft = (x - bytesPerPixel < 1) ? (byte)0 : previousScanline[x - bytesPerPixel]; - result[x] = (byte)((scanline[x] + PaethPredictor(left, above, upperLeft)) % 256); + result[x] = (byte)((scanline[x] + PaethPredicator(left, above, upperLeft)) % 256); } return result; } + /// + /// Encodes the scanline + /// + /// The scanline to encode + /// The previous scanline. + /// The bytes per pixel. + /// The public static byte[] Encode(byte[] scanline, byte[] previousScanline, int bytesPerPixel) { - var encodedScanline = new byte[scanline.Length + 1]; - + // Paeth(x) = Raw(x) - PaethPredictor(Raw(x-bpp), Prior(x), Prior(x - bpp)) + byte[] encodedScanline = new byte[scanline.Length + 1]; encodedScanline[0] = (byte)FilterType.Paeth; for (int x = 0; x < scanline.Length; x++) @@ -32,35 +58,40 @@ byte above = previousScanline[x]; byte upperLeft = (x - bytesPerPixel < 0) ? (byte)0 : previousScanline[x - bytesPerPixel]; - encodedScanline[x + 1] = (byte)((scanline[x] - PaethPredictor(left, above, upperLeft)) % 256); + encodedScanline[x + 1] = (byte)((scanline[x] - PaethPredicator(left, above, upperLeft)) % 256); } return encodedScanline; } - private static int PaethPredictor(int a, int b, int c) + /// + /// Computes a simple linear function of the three neighboring pixels (left, above, upper left), then chooses + /// as predictor the neighboring pixel closest to the computed value. + /// + /// The left neighbor pixel. + /// The above neighbor pixel. + /// The upper left neighbor pixel. + /// + /// The . + /// + private static byte PaethPredicator(byte left, byte above, byte upperLeft) { - int p = a + b - c; - int pa = Math.Abs(p - a); - int pb = Math.Abs(p - b); - int pc = Math.Abs(p - c); + int p = left + above - upperLeft; + int pa = Math.Abs(p - left); + int pb = Math.Abs(p - above); + int pc = Math.Abs(p - upperLeft); - if ((pa <= pb) && (pa <= pc)) + if (pa <= pb && pa <= pc) { - return a; + return left; } - else + + if (pb <= pc) { - if (pb <= pc) - { - return b; - } - else - { - return c; - } + return above; } + + return upperLeft; } } - } diff --git a/src/ImageProcessorCore/Formats/Png/Filters/SubFilter.cs b/src/ImageProcessorCore/Formats/Png/Filters/SubFilter.cs index 9734af654..6fb559f5e 100644 --- a/src/ImageProcessorCore/Formats/Png/Filters/SubFilter.cs +++ b/src/ImageProcessorCore/Formats/Png/Filters/SubFilter.cs @@ -1,9 +1,26 @@ -namespace ImageProcessorCore.Formats +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats { + /// + /// The Sub filter transmits the difference between each byte and the value of the corresponding byte + /// of the prior pixel. + /// + /// internal static class SubFilter { + /// + /// Decodes the scanline + /// + /// The scanline to decode + /// The bytes per pixel. + /// The public static byte[] Decode(byte[] scanline, int bytesPerPixel) { + // Sub(x) + Raw(x-bpp) byte[] result = new byte[scanline.Length]; for (int x = 1; x < scanline.Length; x++) @@ -16,10 +33,16 @@ return result; } + /// + /// Encodes the scanline + /// + /// The scanline to encode + /// The bytes per pixel. + /// The public static byte[] Encode(byte[] scanline, int bytesPerPixel) { - var encodedScanline = new byte[scanline.Length + 1]; - + // Sub(x) = Raw(x) - Raw(x-bpp) + byte[] encodedScanline = new byte[scanline.Length + 1]; encodedScanline[0] = (byte)FilterType.Sub; for (int x = 0; x < scanline.Length; x++) diff --git a/src/ImageProcessorCore/Formats/Png/Filters/UpFilter.cs b/src/ImageProcessorCore/Formats/Png/Filters/UpFilter.cs index a2ca0ed15..8f87ec1ca 100644 --- a/src/ImageProcessorCore/Formats/Png/Filters/UpFilter.cs +++ b/src/ImageProcessorCore/Formats/Png/Filters/UpFilter.cs @@ -1,9 +1,26 @@ -namespace ImageProcessorCore.Formats +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats { + /// + /// The Up filter is just like the Sub filter except that the pixel immediately above the current pixel, + /// rather than just to its left, is used as the predictor. + /// + /// internal static class UpFilter { + /// + /// Decodes the scanline + /// + /// The scanline to decode + /// The previous scanline. + /// The public static byte[] Decode(byte[] scanline, byte[] previousScanline) { + // Up(x) + Prior(x) byte[] result = new byte[scanline.Length]; for (int x = 1; x < scanline.Length; x++) @@ -16,10 +33,16 @@ return result; } + /// + /// Encodes the scanline + /// + /// The scanline to encode + /// The previous scanline. + /// The public static byte[] Encode(byte[] scanline, byte[] previousScanline) { - var encodedScanline = new byte[scanline.Length + 1]; - + // Up(x) = Raw(x) - Prior(x) + byte[] encodedScanline = new byte[scanline.Length + 1]; encodedScanline[0] = (byte)FilterType.Up; for (int x = 0; x < scanline.Length; x++) diff --git a/src/ImageProcessorCore/Formats/Png/PngColorType.cs b/src/ImageProcessorCore/Formats/Png/PngColorType.cs index 574ed2a09..8256a30a5 100644 --- a/src/ImageProcessorCore/Formats/Png/PngColorType.cs +++ b/src/ImageProcessorCore/Formats/Png/PngColorType.cs @@ -6,9 +6,9 @@ namespace ImageProcessorCore.Formats { /// - /// Provides enumeration of available png color types. + /// Provides enumeration of available PNG color types. /// - public enum PngColorType + public enum PngColorType : byte { /// /// Each pixel is a grayscale sample. diff --git a/src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs b/src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs index 71b9b6542..2381ae44c 100644 --- a/src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs +++ b/src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs @@ -43,6 +43,11 @@ namespace ImageProcessorCore.Formats /// private byte bitDepth; + /// + /// The number of bytes per pixel. + /// + private int bytesPerPixel; + /// /// Gets or sets the quality of output for images. /// @@ -120,7 +125,7 @@ namespace ImageProcessorCore.Formats int quality = this.Quality > 0 ? this.Quality : image.Quality; this.Quality = quality > 0 ? quality.Clamp(1, int.MaxValue) : int.MaxValue; - // Set correct color type. + // Set correct color type if the color count is 256 or less. if (Quality <= 256) { this.PngColorType = PngColorType.Palette; @@ -141,12 +146,13 @@ namespace ImageProcessorCore.Formats this.bitDepth = 8; } - // TODO: Add more color options here. + this.bytesPerPixel = CalculateBytesPerPixel(); + PngHeader header = new PngHeader { Width = image.Width, Height = image.Height, - ColorType = (byte)(this.Quality <= 256 ? 3 : 6), // 3 = indexed, 6= Each pixel is an R,G,B triple, followed by an alpha sample. + ColorType = (byte)this.PngColorType, BitDepth = this.bitDepth, FilterMethod = 0, // None CompressionMethod = 0, @@ -155,61 +161,139 @@ namespace ImageProcessorCore.Formats this.WriteHeaderChunk(stream, header); - if (this.Quality <= 256) + // Collect the pixel data + if (this.PngColorType == PngColorType.Palette) { - // Quatize the image and get the pixels - QuantizedImage quantized = this.WritePaletteChunk(stream, header, image); - pixelData = quantized.Pixels; + this.CollectIndexedBytes(image, stream, header); + } + else if (this.PngColorType == PngColorType.Grayscale || this.PngColorType == PngColorType.GrayscaleWithAlpha) + { + this.CollectGrayscaleBytes(image); } else { - // Copy the pixels across from the image. - // TODO: This should vary by bytes per pixel. - this.pixelData = new byte[this.width * this.height * 4]; - int stride = this.width * 4; - using (IPixelAccessor pixels = image.Lock()) - { - for (int y = 0; y < this.height; y++) - { - for (int x = 0; x < this.width; x++) - { - int dataOffset = (y * stride) + (x * 4); - byte[] source = pixels[x, y].ToBytes(); - - // r -> g -> b -> a - this.pixelData[dataOffset] = source[0]; - this.pixelData[dataOffset + 1] = source[1]; - this.pixelData[dataOffset + 2] = source[2]; - this.pixelData[dataOffset + 3] = source[3]; - } - } - } + this.CollectColorBytes(image); } this.WritePhysicalChunk(stream, image); this.WriteGammaChunk(stream); - - //using (IPixelAccessor pixels = image.Lock()) - //{ - // this.WriteDataChunks(stream, pixels, quantized); - //} this.WriteDataChunks(stream); - this.WriteEndChunk(stream); stream.Flush(); } + /// + /// Collects the indexed pixel data. + /// + /// The pixel format. + /// The packed format. long, float. + /// The image to encode. + /// The containing image data. + /// The . + private void CollectIndexedBytes(ImageBase image, Stream stream, PngHeader header) + where T : IPackedVector + where TP : struct + { + // Quatize the image and get the pixels + QuantizedImage quantized = this.WritePaletteChunk(stream, header, image); + pixelData = quantized.Pixels; + } + + /// + /// Collects the grayscale pixel data. + /// + /// The pixel format. + /// The packed format. long, float. + /// The image to encode. + private void CollectGrayscaleBytes(ImageBase image) + where T : IPackedVector + where TP : struct + { + // Copy the pixels across from the image. + this.pixelData = new byte[this.width * this.height * this.bytesPerPixel]; + int stride = this.width * this.bytesPerPixel; + using (IPixelAccessor pixels = image.Lock()) + { + Parallel.For( + 0, + this.height, + Bootstrapper.Instance.ParallelOptions, + y => + { + for (int x = 0; x < this.width; x++) + { + // Convert the color to YCbCr and store the luminance + // Optionally store the original color alpha. + int dataOffset = (y * stride) + (x * this.bytesPerPixel); + Color source = new Color(pixels[x, y].ToVector4()); + YCbCr luminance = source; + for (int i = 0; i < this.bytesPerPixel; i++) + { + if (i == 0) + { + this.pixelData[dataOffset] = ((byte)luminance.Y).Clamp(0, 255); + } + else + { + this.pixelData[dataOffset + i] = source.A; + } + } + } + }); + } + } + + /// + /// Collects the true color pixel data. + /// + /// The pixel format. + /// The packed format. long, float. + /// The image to encode. + private void CollectColorBytes(ImageBase image) + where T : IPackedVector + where TP : struct + { + // Copy the pixels across from the image. + this.pixelData = new byte[this.width * this.height * this.bytesPerPixel]; + int stride = this.width * this.bytesPerPixel; + using (IPixelAccessor pixels = image.Lock()) + { + Parallel.For( + 0, + this.height, + Bootstrapper.Instance.ParallelOptions, + y => + { + // Color data is stored in r -> g -> b -> a order + for (int x = 0; x < this.width; x++) + { + int dataOffset = (y * stride) + (x * this.bytesPerPixel); + byte[] source = pixels[x, y].ToBytes(); + + for (int i = 0; i < this.bytesPerPixel; i++) + { + this.pixelData[dataOffset + i] = source[i]; + } + } + }); + } + } + + /// + /// Encodes the pixel data line by line. + /// Each scanline is encoded in the most optimal manner to improve compression. + /// + /// The private byte[] EncodePixelData() { List filteredScanlines = new List(); - int bytesPerPixel = CalculateBytesPerPixel(); - byte[] previousScanline = new byte[width * bytesPerPixel]; + byte[] previousScanline = new byte[width * this.bytesPerPixel]; for (int y = 0; y < height; y++) { byte[] rawScanline = GetRawScanline(y); - byte[] filteredScanline = GetOptimalFilteredScanline(rawScanline, previousScanline, bytesPerPixel); + byte[] filteredScanline = GetOptimalFilteredScanline(rawScanline, previousScanline, this.bytesPerPixel); filteredScanlines.Add(filteredScanline); @@ -230,24 +314,24 @@ namespace ImageProcessorCore.Formats /// Applies all PNG filters to the given scanline and returns the filtered scanline that is deemed /// to be most compressible, using lowest total variation as proxy for compressibility. /// - /// - /// - /// + /// The raw scanline + /// The previous scanline + /// The number of bytes per pixel /// - private byte[] GetOptimalFilteredScanline(byte[] rawScanline, byte[] previousScanline, int bytesPerPixel) + private byte[] GetOptimalFilteredScanline(byte[] rawScanline, byte[] previousScanline, int byteCount) { List> candidates = new List>(); - byte[] sub = SubFilter.Encode(rawScanline, bytesPerPixel); + byte[] sub = SubFilter.Encode(rawScanline, byteCount); candidates.Add(new Tuple(sub, CalculateTotalVariation(sub))); byte[] up = UpFilter.Encode(rawScanline, previousScanline); candidates.Add(new Tuple(up, CalculateTotalVariation(up))); - byte[] average = AverageFilter.Encode(rawScanline, previousScanline, bytesPerPixel); + byte[] average = AverageFilter.Encode(rawScanline, previousScanline, byteCount); candidates.Add(new Tuple(average, CalculateTotalVariation(average))); - byte[] paeth = PaethFilter.Encode(rawScanline, previousScanline, bytesPerPixel); + byte[] paeth = PaethFilter.Encode(rawScanline, previousScanline, byteCount); candidates.Add(new Tuple(paeth, CalculateTotalVariation(paeth))); int lowestTotalVariation = int.MaxValue; @@ -266,11 +350,11 @@ namespace ImageProcessorCore.Formats } /// - /// Calculates the total variation of given byte array. Total variation is the sum of the absolute values of + /// Calculates the total variation of given byte array. Total variation is the sum of the absolute values of /// neighbour differences. /// - /// - /// + /// The scanline bytes + /// The private int CalculateTotalVariation(byte[] input) { int totalVariation = 0; @@ -283,15 +367,23 @@ namespace ImageProcessorCore.Formats return totalVariation; } + /// + /// Get the raw scanline data from the pixel data + /// + /// The row number + /// The private byte[] GetRawScanline(int y) { - // TODO: This should vary by bytes per pixel. - int stride = (this.PngColorType == PngColorType.Palette ? 1 : 4) * this.width; + int stride = this.bytesPerPixel * this.width; byte[] rawScanline = new byte[stride]; Array.Copy(this.pixelData, y * stride, rawScanline, 0, stride); return rawScanline; } + /// + /// Calculates the correct number of bytes per pixel for the given color type. + /// + /// The private int CalculateBytesPerPixel() { switch (this.PngColorType) @@ -309,6 +401,8 @@ namespace ImageProcessorCore.Formats return 3; // PngColorType.RgbWithAlpha + // TODO: Maybe figure out a way to detect if there are any transparent + // pixels and encode RGB if none. default: return 4; } @@ -536,139 +630,6 @@ namespace ImageProcessorCore.Formats } } - ///// - ///// Writes the pixel information to the stream. - ///// - ///// The pixel format. - ///// The packed format. long, float. - ///// The containing image data. - ///// The image pixels. - ///// The quantized image. - //private void WriteDataChunks(Stream stream, IPixelAccessor pixels, QuantizedImage quantized) - // where T : IPackedVector - // where TP : struct - //{ - // byte[] data; - // int imageWidth = pixels.Width; - // int imageHeight = pixels.Height; - - // // Indexed image. - // if (this.Quality <= 256) - // { - // int rowLength = imageWidth + 1; - // data = new byte[rowLength * imageHeight]; - - // Parallel.For( - // 0, - // imageHeight, - // Bootstrapper.Instance.ParallelOptions, - // y => - // { - // int dataOffset = (y * rowLength); - // byte compression = 0; - // if (y > 0) - // { - // compression = 2; - // } - // data[dataOffset++] = compression; - // for (int x = 0; x < imageWidth; x++) - // { - // data[dataOffset++] = quantized.Pixels[(y * imageWidth) + x]; - // if (y > 0) - // { - // data[dataOffset - 1] -= quantized.Pixels[((y - 1) * imageWidth) + x]; - // } - // } - // }); - // } - // else - // { - // // TrueColor image. - // data = new byte[(imageWidth * imageHeight * 4) + pixels.Height]; - - // int rowLength = (imageWidth * 4) + 1; - - // Parallel.For( - // 0, - // imageHeight, - // Bootstrapper.Instance.ParallelOptions, - // y => - // { - // byte compression = 0; - // if (y > 0) - // { - // compression = 2; - // } - - // data[y * rowLength] = compression; - - // for (int x = 0; x < imageWidth; x++) - // { - // byte[] color = pixels[x, y].ToBytes(); - - // // Calculate the offset for the new array. - // int dataOffset = (y * rowLength) + (x * 4) + 1; - - // // Expected format - // data[dataOffset] = color[0]; - // data[dataOffset + 1] = color[1]; - // data[dataOffset + 2] = color[2]; - // data[dataOffset + 3] = color[3]; - - // if (y > 0) - // { - // color = pixels[x, y - 1].ToBytes(); - - // data[dataOffset] -= color[0]; - // data[dataOffset + 1] -= color[1]; - // data[dataOffset + 2] -= color[2]; - // data[dataOffset + 3] -= color[3]; - // } - // } - // }); - // } - - // byte[] buffer; - // int bufferLength; - - // MemoryStream memoryStream = null; - // try - // { - // memoryStream = new MemoryStream(); - - // using (ZlibDeflateStream deflateStream = new ZlibDeflateStream(memoryStream, this.CompressionLevel)) - // { - // deflateStream.Write(data, 0, data.Length); - // } - - // bufferLength = (int)memoryStream.Length; - // buffer = memoryStream.ToArray(); - // } - // finally - // { - // memoryStream?.Dispose(); - // } - - // int numChunks = bufferLength / MaxBlockSize; - - // if (bufferLength % MaxBlockSize != 0) - // { - // numChunks++; - // } - - // for (int i = 0; i < numChunks; i++) - // { - // int length = bufferLength - (i * MaxBlockSize); - - // if (length > MaxBlockSize) - // { - // length = MaxBlockSize; - // } - - // this.WriteChunk(stream, PngChunkTypes.Data, buffer, i * MaxBlockSize, length); - // } - //} - /// /// Writes the chunk end to the stream. ///