diff --git a/src/ImageProcessorCore/Formats/Png/Filters/AverageFilter.cs b/src/ImageProcessorCore/Formats/Png/Filters/AverageFilter.cs new file mode 100644 index 0000000000..c7f3f537aa --- /dev/null +++ b/src/ImageProcessorCore/Formats/Png/Filters/AverageFilter.cs @@ -0,0 +1,44 @@ +namespace ImageProcessorCore.Formats +{ + using System; + + internal static class AverageFilter + { + public static byte[] Decode(byte[] scanline, byte[] previousScanline, int bytesPerPixel) + { + byte[] result = new byte[scanline.Length]; + + for (int x = 1; x < scanline.Length; x++) + { + byte left = (x - bytesPerPixel < 1) ? (byte)0 : result[x - bytesPerPixel]; + byte above = previousScanline[x]; + + result[x] = (byte)((scanline[x] + Average(left, above)) % 256); + } + + return result; + } + + public static byte[] Encode(byte[] scanline, byte[] previousScanline, int bytesPerPixel) + { + var encodedScanline = new byte[scanline.Length + 1]; + + encodedScanline[0] = (byte)FilterType.Average; + + for (int x = 0; x < scanline.Length; x++) + { + byte left = (x - bytesPerPixel < 0) ? (byte)0 : scanline[x - bytesPerPixel]; + byte above = previousScanline[x]; + + encodedScanline[x + 1] = (byte)((scanline[x] - Average(left, above)) % 256); + } + + return encodedScanline; + } + + private static int Average(byte left, byte above) + { + return Convert.ToInt32(Math.Floor((left + above) / 2.0)); + } + } +} diff --git a/src/ImageProcessorCore/Formats/Png/Filters/FilterType.cs b/src/ImageProcessorCore/Formats/Png/Filters/FilterType.cs new file mode 100644 index 0000000000..0a8a519a3b --- /dev/null +++ b/src/ImageProcessorCore/Formats/Png/Filters/FilterType.cs @@ -0,0 +1,44 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats +{ + /// + /// Provides enumeration of the various png filter types. + /// + internal enum FilterType + { + /// + /// With the None filter, the scanline is transmitted unmodified; it is only necessary to + /// insert a filter type byte before the data. + /// + None = 0, + + /// + /// The Sub filter transmits the difference between each byte and the value of the corresponding + /// byte of the prior pixel. + /// + Sub = 1, + + /// + /// 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. + /// + Up = 2, + + /// + /// The Average filter uses the average of the two neighboring pixels (left and above) to + /// predict the value of a pixel. + /// + Average = 3, + + /// + /// 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 + /// + Paeth = 4 + } +} diff --git a/src/ImageProcessorCore/Formats/Png/Filters/NoneFilter.cs b/src/ImageProcessorCore/Formats/Png/Filters/NoneFilter.cs new file mode 100644 index 0000000000..a92a53a90e --- /dev/null +++ b/src/ImageProcessorCore/Formats/Png/Filters/NoneFilter.cs @@ -0,0 +1,40 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +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 + { + /// + /// Decodes the scanline + /// + /// The scanline to decode + /// The + public static byte[] Decode(byte[] scanline) + { + // No change required. + return scanline; + } + + /// + /// Encodes the scanline + /// + /// The scanline to encode + /// The + public static byte[] Encode(byte[] scanline) + { + // Insert a byte before the data. + byte[] encodedScanline = new byte[scanline.Length + 1]; + encodedScanline[0] = (byte)FilterType.None; + scanline.CopyTo(encodedScanline, 1); + + return encodedScanline; + } + } +} diff --git a/src/ImageProcessorCore/Formats/Png/Filters/PaethFilter.cs b/src/ImageProcessorCore/Formats/Png/Filters/PaethFilter.cs new file mode 100644 index 0000000000..bb90ce06fc --- /dev/null +++ b/src/ImageProcessorCore/Formats/Png/Filters/PaethFilter.cs @@ -0,0 +1,66 @@ +namespace ImageProcessorCore.Formats +{ + using System; + + internal static class PaethFilter + { + public static byte[] Decode(byte[] scanline, byte[] previousScanline, int bytesPerPixel) + { + byte[] result = new byte[scanline.Length]; + + for (int x = 1; x < scanline.Length; x++) + { + byte left = (x - bytesPerPixel < 1) ? (byte)0 : result[x - bytesPerPixel]; + byte above = previousScanline[x]; + byte upperLeft = (x - bytesPerPixel < 1) ? (byte)0 : previousScanline[x - bytesPerPixel]; + + result[x] = (byte)((scanline[x] + PaethPredictor(left, above, upperLeft)) % 256); + } + + return result; + } + + public static byte[] Encode(byte[] scanline, byte[] previousScanline, int bytesPerPixel) + { + var encodedScanline = new byte[scanline.Length + 1]; + + encodedScanline[0] = (byte)FilterType.Paeth; + + for (int x = 0; x < scanline.Length; x++) + { + byte left = (x - bytesPerPixel < 0) ? (byte)0 : scanline[x - bytesPerPixel]; + 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); + } + + return encodedScanline; + } + + private static int PaethPredictor(int a, int b, int c) + { + int p = a + b - c; + int pa = Math.Abs(p - a); + int pb = Math.Abs(p - b); + int pc = Math.Abs(p - c); + + if ((pa <= pb) && (pa <= pc)) + { + return a; + } + else + { + if (pb <= pc) + { + return b; + } + else + { + return c; + } + } + } + } + +} diff --git a/src/ImageProcessorCore/Formats/Png/Filters/SubFilter.cs b/src/ImageProcessorCore/Formats/Png/Filters/SubFilter.cs new file mode 100644 index 0000000000..9734af6547 --- /dev/null +++ b/src/ImageProcessorCore/Formats/Png/Filters/SubFilter.cs @@ -0,0 +1,35 @@ +namespace ImageProcessorCore.Formats +{ + internal static class SubFilter + { + public static byte[] Decode(byte[] scanline, int bytesPerPixel) + { + byte[] result = new byte[scanline.Length]; + + for (int x = 1; x < scanline.Length; x++) + { + byte priorRawByte = (x - bytesPerPixel < 1) ? (byte)0 : result[x - bytesPerPixel]; + + result[x] = (byte)((scanline[x] + priorRawByte) % 256); + } + + return result; + } + + public static byte[] Encode(byte[] scanline, int bytesPerPixel) + { + var encodedScanline = new byte[scanline.Length + 1]; + + encodedScanline[0] = (byte)FilterType.Sub; + + for (int x = 0; x < scanline.Length; x++) + { + byte priorRawByte = (x - bytesPerPixel < 0) ? (byte)0 : scanline[x - bytesPerPixel]; + + encodedScanline[x + 1] = (byte)((scanline[x] - priorRawByte) % 256); + } + + return encodedScanline; + } + } +} diff --git a/src/ImageProcessorCore/Formats/Png/Filters/UpFilter.cs b/src/ImageProcessorCore/Formats/Png/Filters/UpFilter.cs new file mode 100644 index 0000000000..a2ca0ed15f --- /dev/null +++ b/src/ImageProcessorCore/Formats/Png/Filters/UpFilter.cs @@ -0,0 +1,35 @@ +namespace ImageProcessorCore.Formats +{ + internal static class UpFilter + { + public static byte[] Decode(byte[] scanline, byte[] previousScanline) + { + byte[] result = new byte[scanline.Length]; + + for (int x = 1; x < scanline.Length; x++) + { + byte above = previousScanline[x]; + + result[x] = (byte)((scanline[x] + above) % 256); + } + + return result; + } + + public static byte[] Encode(byte[] scanline, byte[] previousScanline) + { + var encodedScanline = new byte[scanline.Length + 1]; + + encodedScanline[0] = (byte)FilterType.Up; + + for (int x = 0; x < scanline.Length; x++) + { + byte above = previousScanline[x]; + + encodedScanline[x + 1] = (byte)((scanline[x] - above) % 256); + } + + return encodedScanline; + } + } +} diff --git a/src/ImageProcessorCore/Formats/Png/PngColorType.cs b/src/ImageProcessorCore/Formats/Png/PngColorType.cs new file mode 100644 index 0000000000..574ed2a09e --- /dev/null +++ b/src/ImageProcessorCore/Formats/Png/PngColorType.cs @@ -0,0 +1,38 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats +{ + /// + /// Provides enumeration of available png color types. + /// + public enum PngColorType + { + /// + /// Each pixel is a grayscale sample. + /// + Grayscale = 0, + + /// + /// Each pixel is an R,G,B triple. + /// + Rgb = 2, + + /// + /// Each pixel is a palette index; a PLTE chunk must appear. + /// + Palette = 3, + + /// + /// Each pixel is a grayscale sample, followed by an alpha sample. + /// + GrayscaleWithAlpha = 4, + + /// + /// Each pixel is an R,G,B triple, followed by an alpha sample. + /// + RgbWithAlpha = 6 + } +} \ No newline at end of file diff --git a/src/ImageProcessorCore/Formats/Png/PngDecoderCore.cs b/src/ImageProcessorCore/Formats/Png/PngDecoderCore.cs index 777a8e7664..36728a7388 100644 --- a/src/ImageProcessorCore/Formats/Png/PngDecoderCore.cs +++ b/src/ImageProcessorCore/Formats/Png/PngDecoderCore.cs @@ -22,11 +22,6 @@ namespace ImageProcessorCore.Formats private static readonly Dictionary ColorTypes = new Dictionary(); - /// - /// The image to decode. - /// - //private IImage currentImage; - /// /// The stream to decode from. /// diff --git a/src/ImageProcessorCore/Formats/Png/PngEncoder.cs b/src/ImageProcessorCore/Formats/Png/PngEncoder.cs index 3aaaf19a75..abe487d40c 100644 --- a/src/ImageProcessorCore/Formats/Png/PngEncoder.cs +++ b/src/ImageProcessorCore/Formats/Png/PngEncoder.cs @@ -20,6 +20,11 @@ namespace ImageProcessorCore.Formats /// public int Quality { get; set; } + /// + /// Gets or sets the png color type + /// + public PngColorType PngColorType { get; set; } = PngColorType.RgbWithAlpha; + /// public string MimeType => "image/png"; @@ -76,6 +81,7 @@ namespace ImageProcessorCore.Formats CompressionLevel = this.CompressionLevel, Gamma = this.Gamma, Quality = this.Quality, + PngColorType = PngColorType, Quantizer = this.Quantizer, WriteGamma = this.WriteGamma, Threshold = this.Threshold diff --git a/src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs b/src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs index eeb7c651f4..71b9b6542a 100644 --- a/src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs +++ b/src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs @@ -6,6 +6,7 @@ namespace ImageProcessorCore.Formats { using System; + using System.Collections.Generic; using System.IO; using System.Threading.Tasks; @@ -22,6 +23,21 @@ namespace ImageProcessorCore.Formats /// private const int MaxBlockSize = 65535; + /// + /// Contains the raw pixel data from the image. + /// + byte[] pixelData; + + /// + /// The image width. + /// + private int width; + + /// + /// The image height. + /// + private int height; + /// /// The number of bits required to encode the colors in the png. /// @@ -32,6 +48,11 @@ namespace ImageProcessorCore.Formats /// public int Quality { get; set; } + /// + /// Gets or sets the png color type + /// + public PngColorType PngColorType { get; set; } + /// /// The compression level 1-9. /// Defaults to 6. @@ -76,6 +97,9 @@ namespace ImageProcessorCore.Formats Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); + this.width = image.Width; + this.height = image.Height; + // Write the png header. stream.Write( new byte[] @@ -96,6 +120,13 @@ 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. + if (Quality <= 256) + { + this.PngColorType = PngColorType.Palette; + } + + // Set correct bit depth. this.bitDepth = this.Quality <= 256 ? (byte)(ImageMaths.GetBitsNeededForColorDepth(this.Quality).Clamp(1, 8)) : (byte)8; @@ -123,19 +154,166 @@ namespace ImageProcessorCore.Formats }; this.WriteHeaderChunk(stream, header); - QuantizedImage quantized = this.WritePaletteChunk(stream, header, image); - this.WritePhysicalChunk(stream, image); - this.WriteGammaChunk(stream); - using (IPixelAccessor pixels = image.Lock()) + if (this.Quality <= 256) { - this.WriteDataChunks(stream, pixels, quantized); + // Quatize the image and get the pixels + QuantizedImage quantized = this.WritePaletteChunk(stream, header, image); + pixelData = quantized.Pixels; } + 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.WritePhysicalChunk(stream, image); + this.WriteGammaChunk(stream); + + //using (IPixelAccessor pixels = image.Lock()) + //{ + // this.WriteDataChunks(stream, pixels, quantized); + //} + this.WriteDataChunks(stream); this.WriteEndChunk(stream); stream.Flush(); } + private byte[] EncodePixelData() + { + List filteredScanlines = new List(); + + int bytesPerPixel = CalculateBytesPerPixel(); + byte[] previousScanline = new byte[width * bytesPerPixel]; + + for (int y = 0; y < height; y++) + { + byte[] rawScanline = GetRawScanline(y); + byte[] filteredScanline = GetOptimalFilteredScanline(rawScanline, previousScanline, bytesPerPixel); + + filteredScanlines.Add(filteredScanline); + + previousScanline = rawScanline; + } + + List result = new List(); + + foreach (var encodedScanline in filteredScanlines) + { + result.AddRange(encodedScanline); + } + + return result.ToArray(); + } + + /// + /// 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. + /// + /// + /// + /// + /// + private byte[] GetOptimalFilteredScanline(byte[] rawScanline, byte[] previousScanline, int bytesPerPixel) + { + List> candidates = new List>(); + + byte[] sub = SubFilter.Encode(rawScanline, bytesPerPixel); + 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); + candidates.Add(new Tuple(average, CalculateTotalVariation(average))); + + byte[] paeth = PaethFilter.Encode(rawScanline, previousScanline, bytesPerPixel); + candidates.Add(new Tuple(paeth, CalculateTotalVariation(paeth))); + + int lowestTotalVariation = int.MaxValue; + int lowestTotalVariationIndex = 0; + + for (int i = 0; i < candidates.Count; i++) + { + if (candidates[i].Item2 < lowestTotalVariation) + { + lowestTotalVariationIndex = i; + lowestTotalVariation = candidates[i].Item2; + } + } + + return candidates[lowestTotalVariationIndex].Item1; + } + + /// + /// Calculates the total variation of given byte array. Total variation is the sum of the absolute values of + /// neighbour differences. + /// + /// + /// + private int CalculateTotalVariation(byte[] input) + { + int totalVariation = 0; + + for (int i = 1; i < input.Length; i++) + { + totalVariation += Math.Abs(input[i] - input[i - 1]); + } + + return totalVariation; + } + + private byte[] GetRawScanline(int y) + { + // TODO: This should vary by bytes per pixel. + int stride = (this.PngColorType == PngColorType.Palette ? 1 : 4) * this.width; + byte[] rawScanline = new byte[stride]; + Array.Copy(this.pixelData, y * stride, rawScanline, 0, stride); + return rawScanline; + } + + private int CalculateBytesPerPixel() + { + switch (this.PngColorType) + { + case PngColorType.Grayscale: + return 1; + + case PngColorType.GrayscaleWithAlpha: + return 2; + + case PngColorType.Palette: + return 1; + + case PngColorType.Rgb: + return 3; + + // PngColorType.RgbWithAlpha + default: + return 4; + } + } + /// /// Writes an integer to the byte array. /// @@ -313,94 +491,9 @@ 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 + private void WriteDataChunks(Stream stream) { - 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[] data = this.EncodePixelData(); byte[] buffer; int bufferLength; @@ -443,6 +536,139 @@ 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. ///