From 5ac7a957d0377ac6d4458efd2f90b677092e0907 Mon Sep 17 00:00:00 2001 From: Eliot Jones Date: Fri, 21 Aug 2020 10:50:17 +0100 Subject: [PATCH 1/4] add initial png support --- .../Integration/Documents/pdfpig.png | Bin 0 -> 21497 bytes .../Writer/PdfDocumentBuilderTests.cs | 38 ++++ src/UglyToad.PdfPig/Images/Png/Adam7.cs | 114 ++++++++++ .../Images/Png/Adler32Checksum.cs | 35 +++ src/UglyToad.PdfPig/Images/Png/ChunkHeader.cs | 62 +++++ src/UglyToad.PdfPig/Images/Png/ColorType.cs | 28 +++ .../Images/Png/CompressionMethod.cs | 13 ++ src/UglyToad.PdfPig/Images/Png/Crc32.cs | 87 ++++++++ src/UglyToad.PdfPig/Images/Png/Decoder.cs | 211 ++++++++++++++++++ .../Images/Png/FilterMethod.cs | 13 ++ src/UglyToad.PdfPig/Images/Png/FilterType.cs | 26 +++ .../Images/Png/HeaderValidationResult.cs | 54 +++++ .../Images/Png/IChunkVisitor.cs | 15 ++ src/UglyToad.PdfPig/Images/Png/ImageHeader.cs | 96 ++++++++ .../Images/Png/InterlaceMethod.cs | 17 ++ src/UglyToad.PdfPig/Images/Png/Palette.cs | 19 ++ src/UglyToad.PdfPig/Images/Png/Pixel.cs | 123 ++++++++++ src/UglyToad.PdfPig/Images/Png/Png.cs | 89 ++++++++ src/UglyToad.PdfPig/Images/Png/PngOpener.cs | 183 +++++++++++++++ .../Images/Png/PngStreamWriteHelper.cs | 63 ++++++ src/UglyToad.PdfPig/Images/Png/RawPngData.cs | 105 +++++++++ .../Images/Png/StreamHelper.cs | 46 ++++ src/UglyToad.PdfPig/UglyToad.PdfPig.csproj | 3 + src/UglyToad.PdfPig/Writer/PdfPageBuilder.cs | 81 +++++++ 24 files changed, 1521 insertions(+) create mode 100644 src/UglyToad.PdfPig.Tests/Integration/Documents/pdfpig.png create mode 100644 src/UglyToad.PdfPig/Images/Png/Adam7.cs create mode 100644 src/UglyToad.PdfPig/Images/Png/Adler32Checksum.cs create mode 100644 src/UglyToad.PdfPig/Images/Png/ChunkHeader.cs create mode 100644 src/UglyToad.PdfPig/Images/Png/ColorType.cs create mode 100644 src/UglyToad.PdfPig/Images/Png/CompressionMethod.cs create mode 100644 src/UglyToad.PdfPig/Images/Png/Crc32.cs create mode 100644 src/UglyToad.PdfPig/Images/Png/Decoder.cs create mode 100644 src/UglyToad.PdfPig/Images/Png/FilterMethod.cs create mode 100644 src/UglyToad.PdfPig/Images/Png/FilterType.cs create mode 100644 src/UglyToad.PdfPig/Images/Png/HeaderValidationResult.cs create mode 100644 src/UglyToad.PdfPig/Images/Png/IChunkVisitor.cs create mode 100644 src/UglyToad.PdfPig/Images/Png/ImageHeader.cs create mode 100644 src/UglyToad.PdfPig/Images/Png/InterlaceMethod.cs create mode 100644 src/UglyToad.PdfPig/Images/Png/Palette.cs create mode 100644 src/UglyToad.PdfPig/Images/Png/Pixel.cs create mode 100644 src/UglyToad.PdfPig/Images/Png/Png.cs create mode 100644 src/UglyToad.PdfPig/Images/Png/PngOpener.cs create mode 100644 src/UglyToad.PdfPig/Images/Png/PngStreamWriteHelper.cs create mode 100644 src/UglyToad.PdfPig/Images/Png/RawPngData.cs create mode 100644 src/UglyToad.PdfPig/Images/Png/StreamHelper.cs diff --git a/src/UglyToad.PdfPig.Tests/Integration/Documents/pdfpig.png b/src/UglyToad.PdfPig.Tests/Integration/Documents/pdfpig.png new file mode 100644 index 0000000000000000000000000000000000000000..170f63390060137ddf0fb36bfd43aac3d5c33efe GIT binary patch literal 21497 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 LNkvXXu0mjfKlk7R literal 0 HcmV?d00001 diff --git a/src/UglyToad.PdfPig.Tests/Writer/PdfDocumentBuilderTests.cs b/src/UglyToad.PdfPig.Tests/Writer/PdfDocumentBuilderTests.cs index b05a547d..05f2d661 100644 --- a/src/UglyToad.PdfPig.Tests/Writer/PdfDocumentBuilderTests.cs +++ b/src/UglyToad.PdfPig.Tests/Writer/PdfDocumentBuilderTests.cs @@ -529,6 +529,44 @@ } } + [Fact] + public void CanWriteSinglePageWithPng() + { + var builder = new PdfDocumentBuilder(); + var page = builder.AddPage(PageSize.A4); + + var font = builder.AddStandard14Font(Standard14Font.Helvetica); + + page.AddText("Piggy", 12, new PdfPoint(25, page.PageSize.Height - 52), font); + + var img = IntegrationHelpers.GetDocumentPath("pdfpig.png", false); + + var expectedBounds = new PdfRectangle(25, page.PageSize.Height - 300, 200, page.PageSize.Height - 200); + + var imageBytes = File.ReadAllBytes(img); + + page.AddPng(imageBytes, expectedBounds); + + var bytes = builder.Build(); + WriteFile(nameof(CanWriteSinglePageWithPng), bytes); + + using (var document = PdfDocument.Open(bytes)) + { + var page1 = document.GetPage(1); + + Assert.Equal("Piggy", page1.Text); + + var image = Assert.Single(page1.GetImages()); + + Assert.NotNull(image); + + Assert.Equal(expectedBounds.BottomLeft, image.Bounds.BottomLeft); + Assert.Equal(expectedBounds.TopRight, image.Bounds.TopRight); + + Assert.Equal(imageBytes, image.RawBytes); + } + } + private static void WriteFile(string name, byte[] bytes) { try diff --git a/src/UglyToad.PdfPig/Images/Png/Adam7.cs b/src/UglyToad.PdfPig/Images/Png/Adam7.cs new file mode 100644 index 00000000..49e46b07 --- /dev/null +++ b/src/UglyToad.PdfPig/Images/Png/Adam7.cs @@ -0,0 +1,114 @@ +namespace UglyToad.PdfPig.Images.Png +{ + using System.Collections.Generic; + + internal static class Adam7 + { + /// + /// For a given pass number (1 indexed) the scanline indexes of the lines included in that pass in the 8x8 grid. + /// + private static readonly IReadOnlyDictionary PassToScanlineGridIndex = new Dictionary + { + { 1, new []{ 0 } }, + { 2, new []{ 0 } }, + { 3, new []{ 4 } }, + { 4, new []{ 0, 4 } }, + { 5, new []{ 2, 6 } }, + { 6, new[] { 0, 2, 4, 6 } }, + { 7, new[] { 1, 3, 5, 7 } } + }; + + private static readonly IReadOnlyDictionary PassToScanlineColumnIndex = new Dictionary + { + { 1, new []{ 0 } }, + { 2, new []{ 4 } }, + { 3, new []{ 0, 4 } }, + { 4, new []{ 2, 6 } }, + { 5, new []{ 0, 2, 4, 6 } }, + { 6, new []{ 1, 3, 5, 7 } }, + { 7, new []{ 0, 1, 2, 3, 4, 5, 6, 7 } } + }; + + /* + * To go from raw image data to interlaced: + * + * An 8x8 grid is repeated over the image. There are 7 passes and the indexes in this grid correspond to the + * pass number including that pixel. Each row in the grid corresponds to a scanline. + * + * 1 6 4 6 2 6 4 6 - Scanline 0: pass 1 has pixel 0, 8, 16, etc. pass 2 has pixel 4, 12, 20, etc. + * 7 7 7 7 7 7 7 7 + * 5 6 5 6 5 6 5 6 + * 7 7 7 7 7 7 7 7 + * 3 6 4 6 3 6 4 6 + * 7 7 7 7 7 7 7 7 + * 5 6 5 6 5 6 5 6 + * 7 7 7 7 7 7 7 7 + * + * + * + */ + + public static int GetNumberOfScanlinesInPass(ImageHeader header, int pass) + { + var indices = PassToScanlineGridIndex[pass + 1]; + + var mod = header.Height % 8; + + var fitsExactly = mod == 0; + + if (fitsExactly) + { + return indices.Length * (header.Height / 8); + } + + var additionalLines = 0; + for (var i = 0; i < indices.Length; i++) + { + if (indices[i] < mod) + { + additionalLines++; + } + } + + return (indices.Length * (header.Height / 8)) + additionalLines; + } + + public static int GetPixelsPerScanlineInPass(ImageHeader header, int pass) + { + var indices = PassToScanlineColumnIndex[pass + 1]; + + var mod = header.Width % 8; + + var fitsExactly = mod == 0; + + if (fitsExactly) + { + return indices.Length * (header.Width / 8); + } + + var additionalColumns = 0; + for (int i = 0; i < indices.Length; i++) + { + if (indices[i] < mod) + { + additionalColumns++; + } + } + + return (indices.Length * (header.Width / 8)) + additionalColumns; + } + + public static (int x, int y) GetPixelIndexForScanlineInPass(ImageHeader header, int pass, int scanlineIndex, int indexInScanline) + { + var columnIndices = PassToScanlineColumnIndex[pass + 1]; + var rows = PassToScanlineGridIndex[pass + 1]; + + var actualRow = scanlineIndex % rows.Length; + var actualCol = indexInScanline % columnIndices.Length; + var precedingRows = 8 * (scanlineIndex / rows.Length); + var precedingCols = 8 * (indexInScanline / columnIndices.Length); + + return (precedingCols + columnIndices[actualCol], precedingRows + rows[actualRow]); + } + } +} \ No newline at end of file diff --git a/src/UglyToad.PdfPig/Images/Png/Adler32Checksum.cs b/src/UglyToad.PdfPig/Images/Png/Adler32Checksum.cs new file mode 100644 index 00000000..027b6148 --- /dev/null +++ b/src/UglyToad.PdfPig/Images/Png/Adler32Checksum.cs @@ -0,0 +1,35 @@ +namespace UglyToad.PdfPig.Images.Png +{ + using System.Collections.Generic; + + /// + /// Used to calculate the Adler-32 checksum used for ZLIB data in accordance with + /// RFC 1950: ZLIB Compressed Data Format Specification. + /// + public static class Adler32Checksum + { + // Both sums (s1 and s2) are done modulo 65521. + private const int AdlerModulus = 65521; + + /// + /// Calculate the Adler-32 checksum for some data. + /// + public static int Calculate(IEnumerable data) + { + // s1 is the sum of all bytes. + var s1 = 1; + + // s2 is the sum of all s1 values. + var s2 = 0; + + foreach (var b in data) + { + s1 = (s1 + b) % AdlerModulus; + s2 = (s1 + s2) % AdlerModulus; + } + + // The Adler-32 checksum is stored as s2*65536 + s1. + return s2 * 65536 + s1; + } + } +} \ No newline at end of file diff --git a/src/UglyToad.PdfPig/Images/Png/ChunkHeader.cs b/src/UglyToad.PdfPig/Images/Png/ChunkHeader.cs new file mode 100644 index 00000000..91d1bf68 --- /dev/null +++ b/src/UglyToad.PdfPig/Images/Png/ChunkHeader.cs @@ -0,0 +1,62 @@ +namespace UglyToad.PdfPig.Images.Png +{ + using System; + + /// + /// The header for a data chunk in a PNG file. + /// + public readonly struct ChunkHeader + { + /// + /// The position/start of the chunk header within the stream. + /// + public long Position { get; } + + /// + /// The length of the chunk in bytes. + /// + public int Length { get; } + + /// + /// The name of the chunk, uppercase first letter means the chunk is critical (vs. ancillary). + /// + public string Name { get; } + + /// + /// Whether the chunk is critical (must be read by all readers) or ancillary (may be ignored). + /// + public bool IsCritical => char.IsUpper(Name[0]); + + /// + /// A public chunk is one that is defined in the International Standard or is registered in the list of public chunk types maintained by the Registration Authority. + /// Applications can also define private (unregistered) chunk types for their own purposes. + /// + public bool IsPublic => char.IsUpper(Name[1]); + + /// + /// Whether the (if unrecognized) chunk is safe to copy. + /// + public bool IsSafeToCopy => char.IsUpper(Name[3]); + + /// + /// Create a new . + /// + public ChunkHeader(long position, int length, string name) + { + if (length < 0) + { + throw new ArgumentException($"Length less than zero ({length}) encountered when reading chunk at position {position}."); + } + + Position = position; + Length = length; + Name = name; + } + + /// + public override string ToString() + { + return $"{Name} at {Position} (length: {Length})."; + } + } +} \ No newline at end of file diff --git a/src/UglyToad.PdfPig/Images/Png/ColorType.cs b/src/UglyToad.PdfPig/Images/Png/ColorType.cs new file mode 100644 index 00000000..79657f4e --- /dev/null +++ b/src/UglyToad.PdfPig/Images/Png/ColorType.cs @@ -0,0 +1,28 @@ +namespace UglyToad.PdfPig.Images.Png +{ + using System; + + /// + /// Describes the interpretation of the image data. + /// + [Flags] + public enum ColorType : byte + { + /// + /// Grayscale. + /// + None = 0, + /// + /// Colors are stored in a palette rather than directly in the data. + /// + PaletteUsed = 1, + /// + /// The image uses color. + /// + ColorUsed = 2, + /// + /// The image has an alpha channel. + /// + AlphaChannelUsed = 4 + } +} \ No newline at end of file diff --git a/src/UglyToad.PdfPig/Images/Png/CompressionMethod.cs b/src/UglyToad.PdfPig/Images/Png/CompressionMethod.cs new file mode 100644 index 00000000..5aa7d5f6 --- /dev/null +++ b/src/UglyToad.PdfPig/Images/Png/CompressionMethod.cs @@ -0,0 +1,13 @@ +namespace UglyToad.PdfPig.Images.Png +{ + /// + /// The method used to compress the image data. + /// + public enum CompressionMethod : byte + { + /// + /// Deflate/inflate compression with a sliding window of at most 32768 bytes. + /// + DeflateWithSlidingWindow = 0 + } +} \ No newline at end of file diff --git a/src/UglyToad.PdfPig/Images/Png/Crc32.cs b/src/UglyToad.PdfPig/Images/Png/Crc32.cs new file mode 100644 index 00000000..9225b4d9 --- /dev/null +++ b/src/UglyToad.PdfPig/Images/Png/Crc32.cs @@ -0,0 +1,87 @@ +namespace UglyToad.PdfPig.Images.Png +{ + using System.Collections.Generic; + + /// + /// 32-bit Cyclic Redundancy Code used by the PNG for checking the data is intact. + /// + public static class Crc32 + { + private const uint Polynomial = 0xEDB88320; + + private static readonly uint[] Lookup; + + static Crc32() + { + Lookup = new uint[256]; + for (uint i = 0; i < 256; i++) + { + var value = i; + for (var j = 0; j < 8; ++j) + { + if ((value & 1) != 0) + { + value = (value >> 1) ^ Polynomial; + } + else + { + value >>= 1; + } + } + + Lookup[i] = value; + } + } + + /// + /// Calculate the CRC32 for data. + /// + public static uint Calculate(byte[] data) + { + var crc32 = uint.MaxValue; + for (var i = 0; i < data.Length; i++) + { + var index = (crc32 ^ data[i]) & 0xFF; + crc32 = (crc32 >> 8) ^ Lookup[index]; + } + + return crc32 ^ uint.MaxValue; + } + + /// + /// Calculate the CRC32 for data. + /// + public static uint Calculate(List data) + { + var crc32 = uint.MaxValue; + for (var i = 0; i < data.Count; i++) + { + var index = (crc32 ^ data[i]) & 0xFF; + crc32 = (crc32 >> 8) ^ Lookup[index]; + } + + return crc32 ^ uint.MaxValue; + } + + /// + /// Calculate the combined CRC32 for data. + /// + public static uint Calculate(byte[] data, byte[] data2) + { + var crc32 = uint.MaxValue; + for (var i = 0; i < data.Length; i++) + { + var index = (crc32 ^ data[i]) & 0xFF; + crc32 = (crc32 >> 8) ^ Lookup[index]; + } + + for (var i = 0; i < data2.Length; i++) + { + var index = (crc32 ^ data2[i]) & 0xFF; + crc32 = (crc32 >> 8) ^ Lookup[index]; + } + + return crc32 ^ uint.MaxValue; + } + } +} diff --git a/src/UglyToad.PdfPig/Images/Png/Decoder.cs b/src/UglyToad.PdfPig/Images/Png/Decoder.cs new file mode 100644 index 00000000..20236101 --- /dev/null +++ b/src/UglyToad.PdfPig/Images/Png/Decoder.cs @@ -0,0 +1,211 @@ +namespace UglyToad.PdfPig.Images.Png +{ + using System; + + internal static class Decoder + { + public static (byte bytesPerPixel, byte samplesPerPixel) GetBytesAndSamplesPerPixel(ImageHeader header) + { + var bitDepthCorrected = (header.BitDepth + 7) / 8; + + var samplesPerPixel = SamplesPerPixel(header); + + return ((byte)(samplesPerPixel * bitDepthCorrected), samplesPerPixel); + } + + public static byte[] Decode(byte[] decompressedData, ImageHeader header, byte bytesPerPixel, byte samplesPerPixel) + { + switch (header.InterlaceMethod) + { + case InterlaceMethod.None: + { + var bytesPerScanline = BytesPerScanline(header, samplesPerPixel); + + var currentRowStartByteAbsolute = 1; + for (var rowIndex = 0; rowIndex < header.Height; rowIndex++) + { + var filterType = (FilterType)decompressedData[currentRowStartByteAbsolute - 1]; + + var previousRowStartByteAbsolute = (rowIndex) + (bytesPerScanline * (rowIndex - 1)); + + var end = currentRowStartByteAbsolute + bytesPerScanline; + for (var currentByteAbsolute = currentRowStartByteAbsolute; currentByteAbsolute < end; currentByteAbsolute++) + { + ReverseFilter(decompressedData, filterType, previousRowStartByteAbsolute, currentRowStartByteAbsolute, currentByteAbsolute, currentByteAbsolute - currentRowStartByteAbsolute, bytesPerPixel); + } + + currentRowStartByteAbsolute += bytesPerScanline + 1; + } + + return decompressedData; + } + case InterlaceMethod.Adam7: + { + var pixelsPerRow = header.Width * bytesPerPixel; + var newBytes = new byte[header.Height * pixelsPerRow]; + var i = 0; + var previousStartRowByteAbsolute = -1; + // 7 passes + for (var pass = 0; pass < 7; pass++) + { + var numberOfScanlines = Adam7.GetNumberOfScanlinesInPass(header, pass); + var numberOfPixelsPerScanline = Adam7.GetPixelsPerScanlineInPass(header, pass); + + if (numberOfScanlines <= 0 || numberOfPixelsPerScanline <= 0) + { + continue; + } + + for (var scanlineIndex = 0; scanlineIndex < numberOfScanlines; scanlineIndex++) + { + var filterType = (FilterType)decompressedData[i++]; + var rowStartByte = i; + + for (var j = 0; j < numberOfPixelsPerScanline; j++) + { + var pixelIndex = Adam7.GetPixelIndexForScanlineInPass(header, pass, scanlineIndex, j); + for (var k = 0; k < bytesPerPixel; k++) + { + var byteLineNumber = (j * bytesPerPixel) + k; + ReverseFilter(decompressedData, filterType, previousStartRowByteAbsolute, rowStartByte, i, byteLineNumber, bytesPerPixel); + i++; + } + + var start = pixelsPerRow * pixelIndex.y + pixelIndex.x * bytesPerPixel; + Array.ConstrainedCopy(decompressedData, rowStartByte + j * bytesPerPixel, newBytes, start, bytesPerPixel); + } + + previousStartRowByteAbsolute = rowStartByte; + } + } + + return newBytes; + } + default: + throw new ArgumentOutOfRangeException($"Invalid interlace method: {header.InterlaceMethod}."); + } + } + + private static byte SamplesPerPixel(ImageHeader header) + { + switch (header.ColorType) + { + case ColorType.None: + return 1; + case ColorType.PaletteUsed: + return 1; + case ColorType.ColorUsed: + return 3; + case ColorType.AlphaChannelUsed: + return 2; + case ColorType.ColorUsed | ColorType.AlphaChannelUsed: + return 4; + default: + return 0; + } + } + + private static int BytesPerScanline(ImageHeader header, byte samplesPerPixel) + { + var width = header.Width; + + switch (header.BitDepth) + { + case 1: + return (width + 7) / 8; + case 2: + return (width + 3) / 4; + case 4: + return (width + 1) / 2; + case 8: + case 16: + return width * samplesPerPixel * (header.BitDepth / 8); + default: + return 0; + } + } + + private static void ReverseFilter(byte[] data, FilterType type, int previousRowStartByteAbsolute, int rowStartByteAbsolute, int byteAbsolute, int rowByteIndex, int bytesPerPixel) + { + byte GetLeftByteValue() + { + var leftIndex = rowByteIndex - bytesPerPixel; + var leftValue = leftIndex >= 0 ? data[rowStartByteAbsolute + leftIndex] : (byte)0; + return leftValue; + } + + byte GetAboveByteValue() + { + var upIndex = previousRowStartByteAbsolute + rowByteIndex; + return upIndex >= 0 ? data[upIndex] : (byte)0; + } + + byte GetAboveLeftByteValue() + { + var index = previousRowStartByteAbsolute + rowByteIndex - bytesPerPixel; + return index < previousRowStartByteAbsolute || previousRowStartByteAbsolute < 0 ? (byte)0 : data[index]; + } + + // Moved out of the switch for performance. + if (type == FilterType.Up) + { + var above = previousRowStartByteAbsolute + rowByteIndex; + if (above < 0) + { + return; + } + + data[byteAbsolute] += data[above]; + return; + } + + if (type == FilterType.Sub) + { + var leftIndex = rowByteIndex - bytesPerPixel; + if (leftIndex < 0) + { + return; + } + + data[byteAbsolute] += data[rowStartByteAbsolute + leftIndex]; + return; + } + + switch (type) + { + case FilterType.None: + return; + case FilterType.Average: + data[byteAbsolute] += (byte)((GetLeftByteValue() + GetAboveByteValue()) / 2); + break; + case FilterType.Paeth: + var a = GetLeftByteValue(); + var b = GetAboveByteValue(); + var c = GetAboveLeftByteValue(); + data[byteAbsolute] += GetPaethValue(a, b, c); + break; + default: + throw new ArgumentOutOfRangeException(nameof(type), type, null); + } + } + + /// + /// 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. + /// + private static byte GetPaethValue(byte a, byte b, byte c) + { + var p = a + b - c; + var pa = Math.Abs(p - a); + var pb = Math.Abs(p - b); + var pc = Math.Abs(p - c); + + if (pa <= pb && pa <= pc) + { + return a; + } + + return pb <= pc ? b : c; + } + } +} diff --git a/src/UglyToad.PdfPig/Images/Png/FilterMethod.cs b/src/UglyToad.PdfPig/Images/Png/FilterMethod.cs new file mode 100644 index 00000000..d2ebd363 --- /dev/null +++ b/src/UglyToad.PdfPig/Images/Png/FilterMethod.cs @@ -0,0 +1,13 @@ +namespace UglyToad.PdfPig.Images.Png +{ + /// + /// Indicates the pre-processing method applied to the image data before compression. + /// + public enum FilterMethod + { + /// + /// Adaptive filtering with five basic filter types. + /// + AdaptiveFiltering = 0 + } +} \ No newline at end of file diff --git a/src/UglyToad.PdfPig/Images/Png/FilterType.cs b/src/UglyToad.PdfPig/Images/Png/FilterType.cs new file mode 100644 index 00000000..3688d6d8 --- /dev/null +++ b/src/UglyToad.PdfPig/Images/Png/FilterType.cs @@ -0,0 +1,26 @@ +namespace UglyToad.PdfPig.Images.Png +{ + internal enum FilterType + { + /// + /// The raw byte is unaltered. + /// + None = 0, + /// + /// The byte to the left. + /// + Sub = 1, + /// + /// The byte above. + /// + Up = 2, + /// + /// The mean of bytes left and above, rounded down. + /// + Average = 3, + /// + /// Byte to the left, above or top-left based on Paeth's algorithm. + /// + Paeth = 4 + } +} \ No newline at end of file diff --git a/src/UglyToad.PdfPig/Images/Png/HeaderValidationResult.cs b/src/UglyToad.PdfPig/Images/Png/HeaderValidationResult.cs new file mode 100644 index 00000000..d763bcba --- /dev/null +++ b/src/UglyToad.PdfPig/Images/Png/HeaderValidationResult.cs @@ -0,0 +1,54 @@ +namespace UglyToad.PdfPig.Images.Png +{ + internal readonly struct HeaderValidationResult + { + public static readonly byte[] ExpectedHeader = { + 137, + 80, + 78, + 71, + 13, + 10, + 26, + 10 + }; + + public int Byte1 { get; } + + public int Byte2 { get; } + + public int Byte3 { get; } + + public int Byte4 { get; } + + public int Byte5 { get; } + + public int Byte6 { get; } + + public int Byte7 { get; } + + public int Byte8 { get; } + + public bool IsValid { get; } + + public HeaderValidationResult(int byte1, int byte2, int byte3, int byte4, int byte5, int byte6, int byte7, int byte8) + { + Byte1 = byte1; + Byte2 = byte2; + Byte3 = byte3; + Byte4 = byte4; + Byte5 = byte5; + Byte6 = byte6; + Byte7 = byte7; + Byte8 = byte8; + IsValid = byte1 == ExpectedHeader[0] && byte2 == ExpectedHeader[1] && byte3 == ExpectedHeader[2] + && byte4 == ExpectedHeader[3] && byte5 == ExpectedHeader[4] && byte6 == ExpectedHeader[5] + && byte7 == ExpectedHeader[6] && byte8 == ExpectedHeader[7]; + } + + public override string ToString() + { + return $"{Byte1} {Byte2} {Byte3} {Byte4} {Byte5} {Byte6} {Byte7} {Byte8}"; + } + } +} \ No newline at end of file diff --git a/src/UglyToad.PdfPig/Images/Png/IChunkVisitor.cs b/src/UglyToad.PdfPig/Images/Png/IChunkVisitor.cs new file mode 100644 index 00000000..a5c72cfe --- /dev/null +++ b/src/UglyToad.PdfPig/Images/Png/IChunkVisitor.cs @@ -0,0 +1,15 @@ +namespace UglyToad.PdfPig.Images.Png +{ + using System.IO; + + /// + /// Enables execution of custom logic whenever a chunk is read. + /// + public interface IChunkVisitor + { + /// + /// Called by the PNG reader after a chunk is read. + /// + void Visit(Stream stream, ImageHeader header, ChunkHeader chunkHeader, byte[] data, byte[] crc); + } +} \ No newline at end of file diff --git a/src/UglyToad.PdfPig/Images/Png/ImageHeader.cs b/src/UglyToad.PdfPig/Images/Png/ImageHeader.cs new file mode 100644 index 00000000..c2eaad39 --- /dev/null +++ b/src/UglyToad.PdfPig/Images/Png/ImageHeader.cs @@ -0,0 +1,96 @@ +namespace UglyToad.PdfPig.Images.Png +{ + using System; + using System.Collections.Generic; + + /// + /// The high level information about the image. + /// + public readonly struct ImageHeader + { + internal static readonly byte[] HeaderBytes = { + 73, 72, 68, 82 + }; + + private static readonly IReadOnlyDictionary> PermittedBitDepths = new Dictionary> + { + {ColorType.None, new HashSet {1, 2, 4, 8, 16}}, + {ColorType.ColorUsed, new HashSet {8, 16}}, + {ColorType.PaletteUsed | ColorType.ColorUsed, new HashSet {1, 2, 4, 8}}, + {ColorType.AlphaChannelUsed, new HashSet {8, 16}}, + {ColorType.AlphaChannelUsed | ColorType.ColorUsed, new HashSet {8, 16}}, + }; + + /// + /// The width of the image in pixels. + /// + public int Width { get; } + + /// + /// The height of the image in pixels. + /// + public int Height { get; } + + /// + /// The bit depth of the image. + /// + public byte BitDepth { get; } + + /// + /// The color type of the image. + /// + public ColorType ColorType { get; } + + /// + /// The compression method used for the image. + /// + public CompressionMethod CompressionMethod { get; } + + /// + /// The filter method used for the image. + /// + public FilterMethod FilterMethod { get; } + + /// + /// The interlace method used by the image.. + /// + public InterlaceMethod InterlaceMethod { get; } + + /// + /// Create a new . + /// + public ImageHeader(int width, int height, byte bitDepth, ColorType colorType, CompressionMethod compressionMethod, FilterMethod filterMethod, InterlaceMethod interlaceMethod) + { + if (width == 0) + { + throw new ArgumentOutOfRangeException(nameof(width), "Invalid width (0) for image."); + } + + if (height == 0) + { + throw new ArgumentOutOfRangeException(nameof(height), "Invalid height (0) for image."); + } + + if (!PermittedBitDepths.TryGetValue(colorType, out var permitted) + || !permitted.Contains(bitDepth)) + { + throw new ArgumentException($"The bit depth {bitDepth} is not permitted for color type {colorType}."); + } + + Width = width; + Height = height; + BitDepth = bitDepth; + ColorType = colorType; + CompressionMethod = compressionMethod; + FilterMethod = filterMethod; + InterlaceMethod = interlaceMethod; + } + + /// + public override string ToString() + { + return $"w: {Width}, h: {Height}, bitDepth: {BitDepth}, colorType: {ColorType}, " + + $"compression: {CompressionMethod}, filter: {FilterMethod}, interlace: {InterlaceMethod}."; + } + } +} \ No newline at end of file diff --git a/src/UglyToad.PdfPig/Images/Png/InterlaceMethod.cs b/src/UglyToad.PdfPig/Images/Png/InterlaceMethod.cs new file mode 100644 index 00000000..f3a30ff8 --- /dev/null +++ b/src/UglyToad.PdfPig/Images/Png/InterlaceMethod.cs @@ -0,0 +1,17 @@ +namespace UglyToad.PdfPig.Images.Png +{ + /// + /// Indicates the transmission order of the image data. + /// + public enum InterlaceMethod : byte + { + /// + /// No interlace. + /// + None = 0, + /// + /// Adam7 interlace. + /// + Adam7 = 1 + } +} \ No newline at end of file diff --git a/src/UglyToad.PdfPig/Images/Png/Palette.cs b/src/UglyToad.PdfPig/Images/Png/Palette.cs new file mode 100644 index 00000000..e1292149 --- /dev/null +++ b/src/UglyToad.PdfPig/Images/Png/Palette.cs @@ -0,0 +1,19 @@ +namespace UglyToad.PdfPig.Images.Png +{ + internal class Palette + { + public byte[] Data { get; } + + public Palette(byte[] data) + { + Data = data; + } + + public Pixel GetPixel(int index) + { + var start = index * 3; + + return new Pixel(Data[start], Data[start + 1], Data[start + 2], 255, false); + } + } +} \ No newline at end of file diff --git a/src/UglyToad.PdfPig/Images/Png/Pixel.cs b/src/UglyToad.PdfPig/Images/Png/Pixel.cs new file mode 100644 index 00000000..2fa38cf2 --- /dev/null +++ b/src/UglyToad.PdfPig/Images/Png/Pixel.cs @@ -0,0 +1,123 @@ +namespace UglyToad.PdfPig.Images.Png +{ + /// + /// A pixel in a image. + /// + public readonly struct Pixel + { + /// + /// The red value for the pixel. + /// + public byte R { get; } + + /// + /// The green value for the pixel. + /// + public byte G { get; } + + /// + /// The blue value for the pixel. + /// + public byte B { get; } + + /// + /// The alpha transparency value for the pixel. + /// + public byte A { get; } + + /// + /// Whether the pixel is grayscale (if , and will all have the same value). + /// + public bool IsGrayscale { get; } + + /// + /// Create a new . + /// + /// The red value for the pixel. + /// The green value for the pixel. + /// The blue value for the pixel. + /// The alpha transparency value for the pixel. + /// Whether the pixel is grayscale. + public Pixel(byte r, byte g, byte b, byte a, bool isGrayscale) + { + R = r; + G = g; + B = b; + A = a; + IsGrayscale = isGrayscale; + } + + /// + /// Create a new which has false and is fully opaque. + /// + /// The red value for the pixel. + /// The green value for the pixel. + /// The blue value for the pixel. + public Pixel(byte r, byte g, byte b) + { + R = r; + G = g; + B = b; + A = 255; + IsGrayscale = false; + } + + /// + /// Create a new grayscale . + /// + /// The grayscale value. + public Pixel(byte grayscale) + { + R = grayscale; + G = grayscale; + B = grayscale; + A = 255; + IsGrayscale = true; + } + + /// + public override bool Equals(object obj) + { + if (obj is Pixel pixel) + { + return IsGrayscale == pixel.IsGrayscale + && A == pixel.A + && R == pixel.R + && G == pixel.G + && B == pixel.B; + } + + return false; + } + + /// + /// Whether the pixel values are equal. + /// + /// The other pixel. + /// if all pixel values are equal otherwise . + public bool Equals(Pixel other) + { + return R == other.R && G == other.G && B == other.B && A == other.A && IsGrayscale == other.IsGrayscale; + } + + /// + public override int GetHashCode() + { + unchecked + { + var hashCode = R.GetHashCode(); + hashCode = (hashCode * 397) ^ G.GetHashCode(); + hashCode = (hashCode * 397) ^ B.GetHashCode(); + hashCode = (hashCode * 397) ^ A.GetHashCode(); + hashCode = (hashCode * 397) ^ IsGrayscale.GetHashCode(); + return hashCode; + } + } + + /// + public override string ToString() + { + return $"({R}, {G}, {B}, {A})"; + } + } +} \ No newline at end of file diff --git a/src/UglyToad.PdfPig/Images/Png/Png.cs b/src/UglyToad.PdfPig/Images/Png/Png.cs new file mode 100644 index 00000000..1433dc99 --- /dev/null +++ b/src/UglyToad.PdfPig/Images/Png/Png.cs @@ -0,0 +1,89 @@ +namespace UglyToad.PdfPig.Images.Png +{ + using System; + using System.IO; + + /// + /// A PNG image. Call to open from file or bytes. + /// + public class Png + { + private readonly RawPngData data; + + /// + /// The header data from the PNG image. + /// + public ImageHeader Header { get; } + + /// + /// The width of the image in pixels. + /// + public int Width => Header.Width; + + /// + /// The height of the image in pixels. + /// + public int Height => Header.Height; + + /// + /// Whether the image has an alpha (transparency) layer. + /// + public bool HasAlphaChannel => (Header.ColorType & ColorType.AlphaChannelUsed) != 0; + + internal Png(ImageHeader header, RawPngData data) + { + Header = header; + this.data = data ?? throw new ArgumentNullException(nameof(data)); + } + + /// + /// Get the pixel at the given column and row (x, y). + /// + /// + /// Pixel values are generated on demand from the underlying data to prevent holding many items in memory at once, so consumers + /// should cache values if they're going to be looped over many time. + /// + /// The x coordinate (column). + /// The y coordinate (row). + /// The pixel at the coordinate. + public Pixel GetPixel(int x, int y) => data.GetPixel(x, y); + + /// + /// Read the PNG image from the stream. + /// + /// The stream containing PNG data to be read. + /// Optional: A visitor which is called whenever a chunk is read by the library. + /// The data from the stream. + public static Png Open(Stream stream, IChunkVisitor chunkVisitor = null) + => PngOpener.Open(stream, chunkVisitor); + + /// + /// Read the PNG image from the bytes. + /// + /// The bytes of the PNG data to be read. + /// Optional: A visitor which is called whenever a chunk is read by the library. + /// The data from the bytes. + public static Png Open(byte[] bytes, IChunkVisitor chunkVisitor = null) + { + using (var memoryStream = new MemoryStream(bytes)) + { + return PngOpener.Open(memoryStream, chunkVisitor); + } + } + + /// + /// Read the PNG from the file path. + /// + /// The path to the PNG file to open. + /// Optional: A visitor which is called whenever a chunk is read by the library. + /// This will open the file to obtain a so will lock the file during reading. + /// The data from the file. + public static Png Open(string filePath, IChunkVisitor chunkVisitor = null) + { + using (var fileStream = File.OpenRead(filePath)) + { + return Open(fileStream, chunkVisitor); + } + } + } +} diff --git a/src/UglyToad.PdfPig/Images/Png/PngOpener.cs b/src/UglyToad.PdfPig/Images/Png/PngOpener.cs new file mode 100644 index 00000000..0705d1f7 --- /dev/null +++ b/src/UglyToad.PdfPig/Images/Png/PngOpener.cs @@ -0,0 +1,183 @@ +namespace UglyToad.PdfPig.Images.Png +{ + using System; + using System.IO; + using System.IO.Compression; + using System.Text; + + internal static class PngOpener + { + public static Png Open(Stream stream, IChunkVisitor chunkVisitor = null) + { + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } + + if (!stream.CanRead) + { + throw new ArgumentException($"The provided stream of type {stream.GetType().FullName} was not readable."); + } + + var validHeader = HasValidHeader(stream); + + if (!validHeader.IsValid) + { + throw new ArgumentException($"The provided stream did not start with the PNG header. Got {validHeader}."); + } + + var crc = new byte[4]; + var imageHeader = ReadImageHeader(stream, crc); + + var hasEncounteredImageEnd = false; + + Palette palette = null; + + using (var output = new MemoryStream()) + { + using (var memoryStream = new MemoryStream()) + { + while (TryReadChunkHeader(stream, out var header)) + { + if (hasEncounteredImageEnd) + { + throw new InvalidOperationException($"Found another chunk {header} after already reading the IEND chunk."); + } + + var bytes = new byte[header.Length]; + var read = stream.Read(bytes, 0, bytes.Length); + if (read != bytes.Length) + { + throw new InvalidOperationException($"Did not read {header.Length} bytes for the {header} header, only found: {read}."); + } + + if (header.IsCritical) + { + switch (header.Name) + { + case "PLTE": + if (header.Length % 3 != 0) + { + throw new InvalidOperationException($"Palette data must be multiple of 3, got {header.Length}."); + } + + palette = new Palette(bytes); + + break; + case "IDAT": + memoryStream.Write(bytes, 0, bytes.Length); + break; + case "IEND": + hasEncounteredImageEnd = true; + break; + default: + throw new NotSupportedException($"Encountered critical header {header} which was not recognised."); + } + } + + read = stream.Read(crc, 0, crc.Length); + if (read != 4) + { + throw new InvalidOperationException($"Did not read 4 bytes for the CRC, only found: {read}."); + } + + var result = (int)Crc32.Calculate(Encoding.ASCII.GetBytes(header.Name), bytes); + var crcActual = (crc[0] << 24) + (crc[1] << 16) + (crc[2] << 8) + crc[3]; + + if (result != crcActual) + { + throw new InvalidOperationException($"CRC calculated {result} did not match file {crcActual} for chunk: {header.Name}."); + } + + chunkVisitor?.Visit(stream, imageHeader, header, bytes, crc); + } + + memoryStream.Flush(); + memoryStream.Seek(2, SeekOrigin.Begin); + + using (var deflateStream = new DeflateStream(memoryStream, CompressionMode.Decompress)) + { + deflateStream.CopyTo(output); + deflateStream.Close(); + } + } + + var bytesOut = output.ToArray(); + + var (bytesPerPixel, samplesPerPixel) = Decoder.GetBytesAndSamplesPerPixel(imageHeader); + + bytesOut = Decoder.Decode(bytesOut, imageHeader, bytesPerPixel, samplesPerPixel); + + return new Png(imageHeader, new RawPngData(bytesOut, bytesPerPixel, imageHeader.Width, imageHeader.InterlaceMethod, palette, imageHeader.ColorType)); + } + } + + private static HeaderValidationResult HasValidHeader(Stream stream) + { + return new HeaderValidationResult(stream.ReadByte(), stream.ReadByte(), stream.ReadByte(), stream.ReadByte(), + stream.ReadByte(), stream.ReadByte(), stream.ReadByte(), stream.ReadByte()); + } + + private static bool TryReadChunkHeader(Stream stream, out ChunkHeader chunkHeader) + { + chunkHeader = default; + + var position = stream.Position; + if (!StreamHelper.TryReadHeaderBytes(stream, out var headerBytes)) + { + return false; + } + + var length = StreamHelper.ReadBigEndianInt32(headerBytes, 0); + + var name = Encoding.ASCII.GetString(headerBytes, 4, 4); + + chunkHeader = new ChunkHeader(position, length, name); + + return true; + } + + private static ImageHeader ReadImageHeader(Stream stream, byte[] crc) + { + if (!TryReadChunkHeader(stream, out var header)) + { + throw new ArgumentException("The provided stream did not contain a single chunk."); + } + + if (header.Name != "IHDR") + { + throw new ArgumentException($"The first chunk was not the IHDR chunk: {header}."); + } + + if (header.Length != 13) + { + throw new ArgumentException($"The first chunk did not have a length of 13 bytes: {header}."); + } + + var ihdrBytes = new byte[13]; + var read = stream.Read(ihdrBytes, 0, ihdrBytes.Length); + + if (read != 13) + { + throw new InvalidOperationException($"Did not read 13 bytes for the IHDR, only found: {read}."); + } + + read = stream.Read(crc, 0, crc.Length); + if (read != 4) + { + throw new InvalidOperationException($"Did not read 4 bytes for the CRC, only found: {read}."); + } + + var width = StreamHelper.ReadBigEndianInt32(ihdrBytes, 0); + var height = StreamHelper.ReadBigEndianInt32(ihdrBytes, 4); + var bitDepth = ihdrBytes[8]; + var colorType = ihdrBytes[9]; + var compressionMethod = ihdrBytes[10]; + var filterMethod = ihdrBytes[11]; + var interlaceMethod = ihdrBytes[12]; + + return new ImageHeader(width, height, bitDepth, (ColorType)colorType, (CompressionMethod)compressionMethod, (FilterMethod)filterMethod, + (InterlaceMethod)interlaceMethod); + } + } +} \ No newline at end of file diff --git a/src/UglyToad.PdfPig/Images/Png/PngStreamWriteHelper.cs b/src/UglyToad.PdfPig/Images/Png/PngStreamWriteHelper.cs new file mode 100644 index 00000000..98d2a297 --- /dev/null +++ b/src/UglyToad.PdfPig/Images/Png/PngStreamWriteHelper.cs @@ -0,0 +1,63 @@ +namespace UglyToad.PdfPig.Images.Png +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + + internal class PngStreamWriteHelper : Stream + { + private readonly Stream inner; + private readonly List written = new List(); + + public override bool CanRead => inner.CanRead; + + public override bool CanSeek => inner.CanSeek; + + public override bool CanWrite => inner.CanWrite; + + public override long Length => inner.Length; + + public override long Position + { + get => inner.Position; + set => inner.Position = value; + } + + public PngStreamWriteHelper(Stream inner) + { + this.inner = inner ?? throw new ArgumentNullException(nameof(inner)); + } + + public override void Flush() => inner.Flush(); + + public void WriteChunkHeader(byte[] header) + { + written.Clear(); + Write(header, 0, header.Length); + } + + public void WriteChunkLength(int length) + { + StreamHelper.WriteBigEndianInt32(inner, length); + } + + public override int Read(byte[] buffer, int offset, int count) => inner.Read(buffer, offset, count); + + public override long Seek(long offset, SeekOrigin origin) => inner.Seek(offset, origin); + + public override void SetLength(long value) => inner.SetLength(value); + + public override void Write(byte[] buffer, int offset, int count) + { + written.AddRange(buffer.Skip(offset).Take(count)); + inner.Write(buffer, offset, count); + } + + public void WriteCrc() + { + var result = (int)Crc32.Calculate(written); + StreamHelper.WriteBigEndianInt32(inner, result); + } + } +} \ No newline at end of file diff --git a/src/UglyToad.PdfPig/Images/Png/RawPngData.cs b/src/UglyToad.PdfPig/Images/Png/RawPngData.cs new file mode 100644 index 00000000..9fc3d4d0 --- /dev/null +++ b/src/UglyToad.PdfPig/Images/Png/RawPngData.cs @@ -0,0 +1,105 @@ +namespace UglyToad.PdfPig.Images.Png +{ + using System; + + /// + /// Provides convenience methods for indexing into a raw byte array to extract pixel values. + /// + internal class RawPngData + { + private readonly byte[] data; + private readonly int bytesPerPixel; + private readonly int width; + private readonly Palette palette; + private readonly ColorType colorType; + private readonly int rowOffset; + + /// + /// Create a new . + /// + /// The decoded pixel data as bytes. + /// The number of bytes in each pixel. + /// The width of the image in pixels. + /// The interlace method used. + /// The palette for images using indexed colors. + /// The color type. + public RawPngData(byte[] data, int bytesPerPixel, int width, InterlaceMethod interlaceMethod, Palette palette, ColorType colorType) + { + if (width < 0) + { + throw new ArgumentOutOfRangeException($"Width must be greater than or equal to 0, got {width}."); + } + + this.data = data ?? throw new ArgumentNullException(nameof(data)); + this.bytesPerPixel = bytesPerPixel; + this.width = width; + this.palette = palette; + this.colorType = colorType; + rowOffset = interlaceMethod == InterlaceMethod.Adam7 ? 0 : 1; + } + + public Pixel GetPixel(int x, int y) + { + var rowStartPixel = (rowOffset + (rowOffset * y)) + (bytesPerPixel * width * y); + + var pixelStartIndex = rowStartPixel + (bytesPerPixel * x); + + var first = data[pixelStartIndex]; + + if (palette != null) + { + return palette.GetPixel(first); + } + + switch (bytesPerPixel) + { + case 1: + return new Pixel(first, first, first, 255, true); + case 2: + switch (colorType) + { + case ColorType.None: + { + byte second = data[pixelStartIndex + 1]; + var value = ToSingleByte(first, second); + return new Pixel(value, value, value, 255, true); + + } + default: + return new Pixel(first, first, first, data[pixelStartIndex + 1], true); + } + + case 3: + return new Pixel(first, data[pixelStartIndex + 1], data[pixelStartIndex + 2], 255, false); + case 4: + switch (colorType) + { + case ColorType.None | ColorType.AlphaChannelUsed: + { + var second = data[pixelStartIndex + 1]; + var firstAlpha = data[pixelStartIndex + 2]; + var secondAlpha = data[pixelStartIndex + 3]; + var gray = ToSingleByte(first, second); + var alpha = ToSingleByte(firstAlpha, secondAlpha); + return new Pixel(gray, gray, gray, alpha, true); + } + default: + return new Pixel(first, data[pixelStartIndex + 1], data[pixelStartIndex + 2], data[pixelStartIndex + 3], false); + } + case 6: + return new Pixel(first, data[pixelStartIndex + 2], data[pixelStartIndex + 4], 255, false); + case 8: + return new Pixel(first, data[pixelStartIndex + 2], data[pixelStartIndex + 4], data[pixelStartIndex + 6], false); + default: + throw new InvalidOperationException($"Unreconized number of bytes per pixel: {bytesPerPixel}."); + } + } + + private static byte ToSingleByte(byte first, byte second) + { + var us = (first << 8) + second; + var result = (byte)Math.Round((255 * us) / (double)ushort.MaxValue); + return result; + } + } +} \ No newline at end of file diff --git a/src/UglyToad.PdfPig/Images/Png/StreamHelper.cs b/src/UglyToad.PdfPig/Images/Png/StreamHelper.cs new file mode 100644 index 00000000..346c4400 --- /dev/null +++ b/src/UglyToad.PdfPig/Images/Png/StreamHelper.cs @@ -0,0 +1,46 @@ +namespace UglyToad.PdfPig.Images.Png +{ + using System; + using System.IO; + + internal static class StreamHelper + { + public static int ReadBigEndianInt32(Stream stream) + { + return (ReadOrTerminate(stream) << 24) + (ReadOrTerminate(stream) << 16) + + (ReadOrTerminate(stream) << 8) + ReadOrTerminate(stream); + } + + public static int ReadBigEndianInt32(byte[] bytes, int offset) + { + return (bytes[0 + offset] << 24) + (bytes[1 + offset] << 16) + + (bytes[2 + offset] << 8) + bytes[3 + offset]; + } + + public static void WriteBigEndianInt32(Stream stream, int value) + { + stream.WriteByte((byte)(value >> 24)); + stream.WriteByte((byte)(value >> 16)); + stream.WriteByte((byte)(value >> 8)); + stream.WriteByte((byte)value); + } + + private static byte ReadOrTerminate(Stream stream) + { + var b = stream.ReadByte(); + + if (b == -1) + { + throw new InvalidOperationException($"Unexpected end of stream at {stream.Position}."); + } + + return (byte) b; + } + + public static bool TryReadHeaderBytes(Stream stream, out byte[] bytes) + { + bytes = new byte[8]; + return stream.Read(bytes, 0, 8) == 8; + } + } +} \ No newline at end of file diff --git a/src/UglyToad.PdfPig/UglyToad.PdfPig.csproj b/src/UglyToad.PdfPig/UglyToad.PdfPig.csproj index 345d5a68..0675a71f 100644 --- a/src/UglyToad.PdfPig/UglyToad.PdfPig.csproj +++ b/src/UglyToad.PdfPig/UglyToad.PdfPig.csproj @@ -38,4 +38,7 @@ + + + \ No newline at end of file diff --git a/src/UglyToad.PdfPig/Writer/PdfPageBuilder.cs b/src/UglyToad.PdfPig/Writer/PdfPageBuilder.cs index 96c43805..7c5253a5 100644 --- a/src/UglyToad.PdfPig/Writer/PdfPageBuilder.cs +++ b/src/UglyToad.PdfPig/Writer/PdfPageBuilder.cs @@ -19,6 +19,7 @@ using PdfFonts; using Tokens; using Graphics.Operations.PathPainting; + using Images.Png; /// /// A builder used to add construct a page in a PDF document. @@ -357,6 +358,86 @@ operations.Add(Pop.Value); } + /// + /// Adds the PNG image represented by the input bytes at the specified location. + /// + public AddedImage AddPng(byte[] pngBytes, PdfRectangle placementRectangle) + { + using (var memoryStream = new MemoryStream(pngBytes)) + { + return AddPng(memoryStream, placementRectangle); + } + } + + /// + /// Adds the PNG image represented by the input stream at the specified location. + /// + public AddedImage AddPng(Stream pngStream, PdfRectangle placementRectangle) + { + var png = Png.Open(pngStream); + + byte[] data; + var pixelBuffer = new byte[3]; + using (var memoryStream = new MemoryStream()) + { + for (var rowIndex = 0; rowIndex < png.Height; rowIndex++) + { + for (var colIndex = 0; colIndex < png.Width; colIndex++) + { + var pixel = png.GetPixel(colIndex, rowIndex); + + pixelBuffer[0] = pixel.R; + pixelBuffer[1] = pixel.G; + pixelBuffer[2] = pixel.B; + + memoryStream.Write(pixelBuffer, 0, pixelBuffer.Length); + } + } + + data = memoryStream.ToArray(); + } + + var compressed = DataCompresser.CompressBytes(data); + + var imgDictionary = new Dictionary + { + {NameToken.Type, NameToken.Xobject }, + {NameToken.Subtype, NameToken.Image }, + {NameToken.Width, new NumericToken(png.Width) }, + {NameToken.Height, new NumericToken(png.Height) }, + {NameToken.BitsPerComponent, new NumericToken(png.Header.BitDepth)}, + {NameToken.ColorSpace, NameToken.Devicergb}, + {NameToken.Filter, NameToken.FlateDecode}, + {NameToken.Length, new NumericToken(compressed.Length)} + }; + + var reference = documentBuilder.AddImage(new DictionaryToken(imgDictionary), compressed); + + if (!resourcesDictionary.TryGetValue(NameToken.Xobject, out var xobjectsDict) + || !(xobjectsDict is DictionaryToken xobjects)) + { + xobjects = new DictionaryToken(new Dictionary()); + resourcesDictionary[NameToken.Xobject] = xobjects; + } + + var key = NameToken.Create($"I{imageKey++}"); + + resourcesDictionary[NameToken.Xobject] = xobjects.With(key, new IndirectReferenceToken(reference)); + + operations.Add(Push.Value); + // This needs to be the placement rectangle. + operations.Add(new ModifyCurrentTransformationMatrix(new[] + { + (decimal)placementRectangle.Width, 0, + 0, (decimal)placementRectangle.Height, + (decimal)placementRectangle.BottomLeft.X, (decimal)placementRectangle.BottomLeft.Y + })); + operations.Add(new InvokeNamedXObject(key)); + operations.Add(Pop.Value); + + return new AddedImage(reference, png.Width, png.Height); + } + private List DrawLetters(string text, IWritingFont font, TransformationMatrix fontMatrix, decimal fontSize, TransformationMatrix textMatrix) { var horizontalScaling = 1; From 8860e29191a1892adb0196c6a94808b917af1023 Mon Sep 17 00:00:00 2001 From: Eliot Jones Date: Fri, 21 Aug 2020 12:11:27 +0100 Subject: [PATCH 2/4] tidy up png support --- .../Writer/PdfDocumentBuilderTests.cs | 2 - .../Images/Png/Adler32Checksum.cs | 2 +- src/UglyToad.PdfPig/Images/Png/ChunkHeader.cs | 2 +- src/UglyToad.PdfPig/Images/Png/ColorType.cs | 2 +- .../Images/Png/CompressionMethod.cs | 2 +- src/UglyToad.PdfPig/Images/Png/Crc32.cs | 2 +- .../Images/Png/FilterMethod.cs | 2 +- .../Images/Png/IChunkVisitor.cs | 2 +- src/UglyToad.PdfPig/Images/Png/ImageHeader.cs | 6 +- .../Images/Png/InterlaceMethod.cs | 2 +- src/UglyToad.PdfPig/Images/Png/Pixel.cs | 2 +- src/UglyToad.PdfPig/Images/Png/Png.cs | 2 +- .../Images/Png/PngStreamWriteHelper.cs | 63 ------------------- .../Images/Png/StreamHelper.cs | 27 -------- 14 files changed, 11 insertions(+), 107 deletions(-) delete mode 100644 src/UglyToad.PdfPig/Images/Png/PngStreamWriteHelper.cs diff --git a/src/UglyToad.PdfPig.Tests/Writer/PdfDocumentBuilderTests.cs b/src/UglyToad.PdfPig.Tests/Writer/PdfDocumentBuilderTests.cs index 05f2d661..e152516f 100644 --- a/src/UglyToad.PdfPig.Tests/Writer/PdfDocumentBuilderTests.cs +++ b/src/UglyToad.PdfPig.Tests/Writer/PdfDocumentBuilderTests.cs @@ -562,8 +562,6 @@ Assert.Equal(expectedBounds.BottomLeft, image.Bounds.BottomLeft); Assert.Equal(expectedBounds.TopRight, image.Bounds.TopRight); - - Assert.Equal(imageBytes, image.RawBytes); } } diff --git a/src/UglyToad.PdfPig/Images/Png/Adler32Checksum.cs b/src/UglyToad.PdfPig/Images/Png/Adler32Checksum.cs index 027b6148..59ace1f8 100644 --- a/src/UglyToad.PdfPig/Images/Png/Adler32Checksum.cs +++ b/src/UglyToad.PdfPig/Images/Png/Adler32Checksum.cs @@ -6,7 +6,7 @@ /// Used to calculate the Adler-32 checksum used for ZLIB data in accordance with /// RFC 1950: ZLIB Compressed Data Format Specification. /// - public static class Adler32Checksum + internal static class Adler32Checksum { // Both sums (s1 and s2) are done modulo 65521. private const int AdlerModulus = 65521; diff --git a/src/UglyToad.PdfPig/Images/Png/ChunkHeader.cs b/src/UglyToad.PdfPig/Images/Png/ChunkHeader.cs index 91d1bf68..7ccb486c 100644 --- a/src/UglyToad.PdfPig/Images/Png/ChunkHeader.cs +++ b/src/UglyToad.PdfPig/Images/Png/ChunkHeader.cs @@ -5,7 +5,7 @@ /// /// The header for a data chunk in a PNG file. /// - public readonly struct ChunkHeader + internal readonly struct ChunkHeader { /// /// The position/start of the chunk header within the stream. diff --git a/src/UglyToad.PdfPig/Images/Png/ColorType.cs b/src/UglyToad.PdfPig/Images/Png/ColorType.cs index 79657f4e..9e0eb5fd 100644 --- a/src/UglyToad.PdfPig/Images/Png/ColorType.cs +++ b/src/UglyToad.PdfPig/Images/Png/ColorType.cs @@ -6,7 +6,7 @@ /// Describes the interpretation of the image data. /// [Flags] - public enum ColorType : byte + internal enum ColorType : byte { /// /// Grayscale. diff --git a/src/UglyToad.PdfPig/Images/Png/CompressionMethod.cs b/src/UglyToad.PdfPig/Images/Png/CompressionMethod.cs index 5aa7d5f6..49226ec7 100644 --- a/src/UglyToad.PdfPig/Images/Png/CompressionMethod.cs +++ b/src/UglyToad.PdfPig/Images/Png/CompressionMethod.cs @@ -3,7 +3,7 @@ /// /// The method used to compress the image data. /// - public enum CompressionMethod : byte + internal enum CompressionMethod : byte { /// /// Deflate/inflate compression with a sliding window of at most 32768 bytes. diff --git a/src/UglyToad.PdfPig/Images/Png/Crc32.cs b/src/UglyToad.PdfPig/Images/Png/Crc32.cs index 9225b4d9..d571018a 100644 --- a/src/UglyToad.PdfPig/Images/Png/Crc32.cs +++ b/src/UglyToad.PdfPig/Images/Png/Crc32.cs @@ -5,7 +5,7 @@ /// /// 32-bit Cyclic Redundancy Code used by the PNG for checking the data is intact. /// - public static class Crc32 + internal static class Crc32 { private const uint Polynomial = 0xEDB88320; diff --git a/src/UglyToad.PdfPig/Images/Png/FilterMethod.cs b/src/UglyToad.PdfPig/Images/Png/FilterMethod.cs index d2ebd363..af35d9a1 100644 --- a/src/UglyToad.PdfPig/Images/Png/FilterMethod.cs +++ b/src/UglyToad.PdfPig/Images/Png/FilterMethod.cs @@ -3,7 +3,7 @@ /// /// Indicates the pre-processing method applied to the image data before compression. /// - public enum FilterMethod + internal enum FilterMethod { /// /// Adaptive filtering with five basic filter types. diff --git a/src/UglyToad.PdfPig/Images/Png/IChunkVisitor.cs b/src/UglyToad.PdfPig/Images/Png/IChunkVisitor.cs index a5c72cfe..85c0de28 100644 --- a/src/UglyToad.PdfPig/Images/Png/IChunkVisitor.cs +++ b/src/UglyToad.PdfPig/Images/Png/IChunkVisitor.cs @@ -5,7 +5,7 @@ /// /// Enables execution of custom logic whenever a chunk is read. /// - public interface IChunkVisitor + internal interface IChunkVisitor { /// /// Called by the PNG reader after a chunk is read. diff --git a/src/UglyToad.PdfPig/Images/Png/ImageHeader.cs b/src/UglyToad.PdfPig/Images/Png/ImageHeader.cs index c2eaad39..88d332db 100644 --- a/src/UglyToad.PdfPig/Images/Png/ImageHeader.cs +++ b/src/UglyToad.PdfPig/Images/Png/ImageHeader.cs @@ -6,12 +6,8 @@ /// /// The high level information about the image. /// - public readonly struct ImageHeader + internal readonly struct ImageHeader { - internal static readonly byte[] HeaderBytes = { - 73, 72, 68, 82 - }; - private static readonly IReadOnlyDictionary> PermittedBitDepths = new Dictionary> { {ColorType.None, new HashSet {1, 2, 4, 8, 16}}, diff --git a/src/UglyToad.PdfPig/Images/Png/InterlaceMethod.cs b/src/UglyToad.PdfPig/Images/Png/InterlaceMethod.cs index f3a30ff8..6bd8d94c 100644 --- a/src/UglyToad.PdfPig/Images/Png/InterlaceMethod.cs +++ b/src/UglyToad.PdfPig/Images/Png/InterlaceMethod.cs @@ -3,7 +3,7 @@ /// /// Indicates the transmission order of the image data. /// - public enum InterlaceMethod : byte + internal enum InterlaceMethod : byte { /// /// No interlace. diff --git a/src/UglyToad.PdfPig/Images/Png/Pixel.cs b/src/UglyToad.PdfPig/Images/Png/Pixel.cs index 2fa38cf2..46c5766c 100644 --- a/src/UglyToad.PdfPig/Images/Png/Pixel.cs +++ b/src/UglyToad.PdfPig/Images/Png/Pixel.cs @@ -3,7 +3,7 @@ /// /// A pixel in a image. /// - public readonly struct Pixel + internal readonly struct Pixel { /// /// The red value for the pixel. diff --git a/src/UglyToad.PdfPig/Images/Png/Png.cs b/src/UglyToad.PdfPig/Images/Png/Png.cs index 1433dc99..c2979fb7 100644 --- a/src/UglyToad.PdfPig/Images/Png/Png.cs +++ b/src/UglyToad.PdfPig/Images/Png/Png.cs @@ -6,7 +6,7 @@ /// /// A PNG image. Call to open from file or bytes. /// - public class Png + internal class Png { private readonly RawPngData data; diff --git a/src/UglyToad.PdfPig/Images/Png/PngStreamWriteHelper.cs b/src/UglyToad.PdfPig/Images/Png/PngStreamWriteHelper.cs deleted file mode 100644 index 98d2a297..00000000 --- a/src/UglyToad.PdfPig/Images/Png/PngStreamWriteHelper.cs +++ /dev/null @@ -1,63 +0,0 @@ -namespace UglyToad.PdfPig.Images.Png -{ - using System; - using System.Collections.Generic; - using System.IO; - using System.Linq; - - internal class PngStreamWriteHelper : Stream - { - private readonly Stream inner; - private readonly List written = new List(); - - public override bool CanRead => inner.CanRead; - - public override bool CanSeek => inner.CanSeek; - - public override bool CanWrite => inner.CanWrite; - - public override long Length => inner.Length; - - public override long Position - { - get => inner.Position; - set => inner.Position = value; - } - - public PngStreamWriteHelper(Stream inner) - { - this.inner = inner ?? throw new ArgumentNullException(nameof(inner)); - } - - public override void Flush() => inner.Flush(); - - public void WriteChunkHeader(byte[] header) - { - written.Clear(); - Write(header, 0, header.Length); - } - - public void WriteChunkLength(int length) - { - StreamHelper.WriteBigEndianInt32(inner, length); - } - - public override int Read(byte[] buffer, int offset, int count) => inner.Read(buffer, offset, count); - - public override long Seek(long offset, SeekOrigin origin) => inner.Seek(offset, origin); - - public override void SetLength(long value) => inner.SetLength(value); - - public override void Write(byte[] buffer, int offset, int count) - { - written.AddRange(buffer.Skip(offset).Take(count)); - inner.Write(buffer, offset, count); - } - - public void WriteCrc() - { - var result = (int)Crc32.Calculate(written); - StreamHelper.WriteBigEndianInt32(inner, result); - } - } -} \ No newline at end of file diff --git a/src/UglyToad.PdfPig/Images/Png/StreamHelper.cs b/src/UglyToad.PdfPig/Images/Png/StreamHelper.cs index 346c4400..9ab36156 100644 --- a/src/UglyToad.PdfPig/Images/Png/StreamHelper.cs +++ b/src/UglyToad.PdfPig/Images/Png/StreamHelper.cs @@ -1,42 +1,15 @@ namespace UglyToad.PdfPig.Images.Png { - using System; using System.IO; internal static class StreamHelper { - public static int ReadBigEndianInt32(Stream stream) - { - return (ReadOrTerminate(stream) << 24) + (ReadOrTerminate(stream) << 16) - + (ReadOrTerminate(stream) << 8) + ReadOrTerminate(stream); - } - public static int ReadBigEndianInt32(byte[] bytes, int offset) { return (bytes[0 + offset] << 24) + (bytes[1 + offset] << 16) + (bytes[2 + offset] << 8) + bytes[3 + offset]; } - public static void WriteBigEndianInt32(Stream stream, int value) - { - stream.WriteByte((byte)(value >> 24)); - stream.WriteByte((byte)(value >> 16)); - stream.WriteByte((byte)(value >> 8)); - stream.WriteByte((byte)value); - } - - private static byte ReadOrTerminate(Stream stream) - { - var b = stream.ReadByte(); - - if (b == -1) - { - throw new InvalidOperationException($"Unexpected end of stream at {stream.Position}."); - } - - return (byte) b; - } - public static bool TryReadHeaderBytes(Stream stream, out byte[] bytes) { bytes = new byte[8]; From 52104b658029438716e9a2f13ca0505699a491c3 Mon Sep 17 00:00:00 2001 From: Eliot Jones Date: Fri, 21 Aug 2020 13:12:01 +0100 Subject: [PATCH 3/4] support conversion of pdf format images to png --- .../SwedishTouringCarChampionshipTests.cs | 25 +++ .../Writer/PdfDocumentBuilderTests.cs | 9 +- src/UglyToad.PdfPig/Content/IPdfImage.cs | 5 + src/UglyToad.PdfPig/Content/InlineImage.cs | 7 + src/UglyToad.PdfPig/Images/Png/ImageHeader.cs | 4 + src/UglyToad.PdfPig/Images/Png/PngBuilder.cs | 159 ++++++++++++++++++ .../Images/Png/PngStreamWriteHelper.cs | 63 +++++++ .../Images/Png/StreamHelper.cs | 27 +++ src/UglyToad.PdfPig/XObjects/XObjectImage.cs | 42 +++++ 9 files changed, 339 insertions(+), 2 deletions(-) create mode 100644 src/UglyToad.PdfPig/Images/Png/PngBuilder.cs create mode 100644 src/UglyToad.PdfPig/Images/Png/PngStreamWriteHelper.cs diff --git a/src/UglyToad.PdfPig.Tests/Integration/SwedishTouringCarChampionshipTests.cs b/src/UglyToad.PdfPig.Tests/Integration/SwedishTouringCarChampionshipTests.cs index 9af4cb79..64124edf 100644 --- a/src/UglyToad.PdfPig.Tests/Integration/SwedishTouringCarChampionshipTests.cs +++ b/src/UglyToad.PdfPig.Tests/Integration/SwedishTouringCarChampionshipTests.cs @@ -1,6 +1,7 @@ namespace UglyToad.PdfPig.Tests.Integration { using Content; + using Images.Png; using Xunit; public class SwedishTouringCarChampionshipTests @@ -89,5 +90,29 @@ Assert.Equal("https://en.wikipedia.org/wiki/Swedish_Touring_Car_Championship", fullLink.Uri); } } + + [Fact] + public void GetsImagesAsPng() + { + using (var document = PdfDocument.Open(GetFilename())) + { + foreach (var page in document.GetPages()) + { + foreach (var image in page.GetImages()) + { + if (!image.TryGetBytes(out _)) + { + continue; + } + + Assert.True(image.TryGetPng(out var png)); + + var pngActual = Png.Open(png); + + Assert.NotNull(pngActual); + } + } + } + } } } diff --git a/src/UglyToad.PdfPig.Tests/Writer/PdfDocumentBuilderTests.cs b/src/UglyToad.PdfPig.Tests/Writer/PdfDocumentBuilderTests.cs index e152516f..87125b32 100644 --- a/src/UglyToad.PdfPig.Tests/Writer/PdfDocumentBuilderTests.cs +++ b/src/UglyToad.PdfPig.Tests/Writer/PdfDocumentBuilderTests.cs @@ -562,10 +562,15 @@ Assert.Equal(expectedBounds.BottomLeft, image.Bounds.BottomLeft); Assert.Equal(expectedBounds.TopRight, image.Bounds.TopRight); + + Assert.True(image.TryGetPng(out var png)); + Assert.NotNull(png); + + WriteFile(nameof(CanWriteSinglePageWithPng) + "out", png, "png"); } } - private static void WriteFile(string name, byte[] bytes) + private static void WriteFile(string name, byte[] bytes, string extension = "pdf") { try { @@ -574,7 +579,7 @@ Directory.CreateDirectory("Builder"); } - var output = Path.Combine("Builder", $"{name}.pdf"); + var output = Path.Combine("Builder", $"{name}.{extension}"); File.WriteAllBytes(output, bytes); } diff --git a/src/UglyToad.PdfPig/Content/IPdfImage.cs b/src/UglyToad.PdfPig/Content/IPdfImage.cs index bb6dc9e7..d82df72f 100644 --- a/src/UglyToad.PdfPig/Content/IPdfImage.cs +++ b/src/UglyToad.PdfPig/Content/IPdfImage.cs @@ -89,5 +89,10 @@ /// should be used directly. /// bool TryGetBytes(out IReadOnlyList bytes); + + /// + /// Try to convert the image to PNG. Doesn't support conversion of JPG to PNG. + /// + bool TryGetPng(out byte[] bytes); } } diff --git a/src/UglyToad.PdfPig/Content/InlineImage.cs b/src/UglyToad.PdfPig/Content/InlineImage.cs index b92d6041..c1863d3d 100644 --- a/src/UglyToad.PdfPig/Content/InlineImage.cs +++ b/src/UglyToad.PdfPig/Content/InlineImage.cs @@ -111,6 +111,13 @@ return true; } + /// + public bool TryGetPng(out byte[] bytes) + { + bytes = null; + return false; + } + /// public override string ToString() { diff --git a/src/UglyToad.PdfPig/Images/Png/ImageHeader.cs b/src/UglyToad.PdfPig/Images/Png/ImageHeader.cs index 88d332db..98fece51 100644 --- a/src/UglyToad.PdfPig/Images/Png/ImageHeader.cs +++ b/src/UglyToad.PdfPig/Images/Png/ImageHeader.cs @@ -8,6 +8,10 @@ /// internal readonly struct ImageHeader { + internal static readonly byte[] HeaderBytes = { + 73, 72, 68, 82 + }; + private static readonly IReadOnlyDictionary> PermittedBitDepths = new Dictionary> { {ColorType.None, new HashSet {1, 2, 4, 8, 16}}, diff --git a/src/UglyToad.PdfPig/Images/Png/PngBuilder.cs b/src/UglyToad.PdfPig/Images/Png/PngBuilder.cs new file mode 100644 index 00000000..ece5f2c7 --- /dev/null +++ b/src/UglyToad.PdfPig/Images/Png/PngBuilder.cs @@ -0,0 +1,159 @@ +namespace UglyToad.PdfPig.Images.Png +{ + using System.IO; + using System.IO.Compression; + using System.Text; + + /// + /// Used to construct PNG images. Call to make a new builder. + /// + internal class PngBuilder + { + private const byte Deflate32KbWindow = 120; + private const byte ChecksumBits = 1; + + private readonly byte[] rawData; + private readonly bool hasAlphaChannel; + private readonly int width; + private readonly int height; + private readonly int bytesPerPixel; + + /// + /// Create a builder for a PNG with the given width and size. + /// + public static PngBuilder Create(int width, int height, bool hasAlphaChannel) + { + var bpp = hasAlphaChannel ? 4 : 3; + + var length = (height * width * bpp) + height; + + return new PngBuilder(new byte[length], hasAlphaChannel, width, height, bpp); + } + + private PngBuilder(byte[] rawData, bool hasAlphaChannel, int width, int height, int bytesPerPixel) + { + this.rawData = rawData; + this.hasAlphaChannel = hasAlphaChannel; + this.width = width; + this.height = height; + this.bytesPerPixel = bytesPerPixel; + } + + /// + /// Sets the RGB pixel value for the given column (x) and row (y). + /// + public PngBuilder SetPixel(byte r, byte g, byte b, int x, int y) => SetPixel(new Pixel(r, g, b), x, y); + + /// + /// Set the pixel value for the given column (x) and row (y). + /// + public PngBuilder SetPixel(Pixel pixel, int x, int y) + { + var start = (y * ((width * bytesPerPixel) + 1)) + 1 + (x * bytesPerPixel); + + rawData[start++] = pixel.R; + rawData[start++] = pixel.G; + rawData[start++] = pixel.B; + + if (hasAlphaChannel) + { + rawData[start] = pixel.A; + } + + return this; + } + + /// + /// Get the bytes of the PNG file for this builder. + /// + public byte[] Save() + { + using (var memoryStream = new MemoryStream()) + { + Save(memoryStream); + return memoryStream.ToArray(); + } + } + + /// + /// Write the PNG file bytes to the provided stream. + /// + public void Save(Stream outputStream) + { + outputStream.Write(HeaderValidationResult.ExpectedHeader, 0, HeaderValidationResult.ExpectedHeader.Length); + + var stream = new PngStreamWriteHelper(outputStream); + + stream.WriteChunkLength(13); + stream.WriteChunkHeader(ImageHeader.HeaderBytes); + + StreamHelper.WriteBigEndianInt32(stream, width); + StreamHelper.WriteBigEndianInt32(stream, height); + stream.WriteByte(8); + + var colorType = ColorType.ColorUsed; + if (hasAlphaChannel) + { + colorType |= ColorType.AlphaChannelUsed; + } + + stream.WriteByte((byte)colorType); + stream.WriteByte((byte)CompressionMethod.DeflateWithSlidingWindow); + stream.WriteByte((byte)FilterMethod.AdaptiveFiltering); + stream.WriteByte((byte)InterlaceMethod.None); + + stream.WriteCrc(); + + var imageData = Compress(rawData); + stream.WriteChunkLength(imageData.Length); + stream.WriteChunkHeader(Encoding.ASCII.GetBytes("IDAT")); + stream.Write(imageData, 0, imageData.Length); + stream.WriteCrc(); + + stream.WriteChunkLength(0); + stream.WriteChunkHeader(Encoding.ASCII.GetBytes("IEND")); + stream.WriteCrc(); + } + + private static byte[] Compress(byte[] data) + { + const int headerLength = 2; + const int checksumLength = 4; + using (var compressStream = new MemoryStream()) + using (var compressor = new DeflateStream(compressStream, CompressionLevel.Fastest, true)) + { + compressor.Write(data, 0, data.Length); + compressor.Close(); + + compressStream.Seek(0, SeekOrigin.Begin); + + var result = new byte[headerLength + compressStream.Length + checksumLength]; + + // Write the ZLib header. + result[0] = Deflate32KbWindow; + result[1] = ChecksumBits; + + // Write the compressed data. + int streamValue; + var i = 0; + while ((streamValue = compressStream.ReadByte()) != -1) + { + result[headerLength + i] = (byte) streamValue; + i++; + } + + // Write Checksum of raw data. + var checksum = Adler32Checksum.Calculate(data); + + var offset = headerLength + compressStream.Length; + + result[offset++] = (byte)(checksum >> 24); + result[offset++] = (byte)(checksum >> 16); + result[offset++] = (byte)(checksum >> 8); + result[offset] = (byte)(checksum >> 0); + + return result; + } + } + } +} diff --git a/src/UglyToad.PdfPig/Images/Png/PngStreamWriteHelper.cs b/src/UglyToad.PdfPig/Images/Png/PngStreamWriteHelper.cs new file mode 100644 index 00000000..98d2a297 --- /dev/null +++ b/src/UglyToad.PdfPig/Images/Png/PngStreamWriteHelper.cs @@ -0,0 +1,63 @@ +namespace UglyToad.PdfPig.Images.Png +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + + internal class PngStreamWriteHelper : Stream + { + private readonly Stream inner; + private readonly List written = new List(); + + public override bool CanRead => inner.CanRead; + + public override bool CanSeek => inner.CanSeek; + + public override bool CanWrite => inner.CanWrite; + + public override long Length => inner.Length; + + public override long Position + { + get => inner.Position; + set => inner.Position = value; + } + + public PngStreamWriteHelper(Stream inner) + { + this.inner = inner ?? throw new ArgumentNullException(nameof(inner)); + } + + public override void Flush() => inner.Flush(); + + public void WriteChunkHeader(byte[] header) + { + written.Clear(); + Write(header, 0, header.Length); + } + + public void WriteChunkLength(int length) + { + StreamHelper.WriteBigEndianInt32(inner, length); + } + + public override int Read(byte[] buffer, int offset, int count) => inner.Read(buffer, offset, count); + + public override long Seek(long offset, SeekOrigin origin) => inner.Seek(offset, origin); + + public override void SetLength(long value) => inner.SetLength(value); + + public override void Write(byte[] buffer, int offset, int count) + { + written.AddRange(buffer.Skip(offset).Take(count)); + inner.Write(buffer, offset, count); + } + + public void WriteCrc() + { + var result = (int)Crc32.Calculate(written); + StreamHelper.WriteBigEndianInt32(inner, result); + } + } +} \ No newline at end of file diff --git a/src/UglyToad.PdfPig/Images/Png/StreamHelper.cs b/src/UglyToad.PdfPig/Images/Png/StreamHelper.cs index 9ab36156..346c4400 100644 --- a/src/UglyToad.PdfPig/Images/Png/StreamHelper.cs +++ b/src/UglyToad.PdfPig/Images/Png/StreamHelper.cs @@ -1,15 +1,42 @@ namespace UglyToad.PdfPig.Images.Png { + using System; using System.IO; internal static class StreamHelper { + public static int ReadBigEndianInt32(Stream stream) + { + return (ReadOrTerminate(stream) << 24) + (ReadOrTerminate(stream) << 16) + + (ReadOrTerminate(stream) << 8) + ReadOrTerminate(stream); + } + public static int ReadBigEndianInt32(byte[] bytes, int offset) { return (bytes[0 + offset] << 24) + (bytes[1 + offset] << 16) + (bytes[2 + offset] << 8) + bytes[3 + offset]; } + public static void WriteBigEndianInt32(Stream stream, int value) + { + stream.WriteByte((byte)(value >> 24)); + stream.WriteByte((byte)(value >> 16)); + stream.WriteByte((byte)(value >> 8)); + stream.WriteByte((byte)value); + } + + private static byte ReadOrTerminate(Stream stream) + { + var b = stream.ReadByte(); + + if (b == -1) + { + throw new InvalidOperationException($"Unexpected end of stream at {stream.Position}."); + } + + return (byte) b; + } + public static bool TryReadHeaderBytes(Stream stream, out byte[] bytes) { bytes = new byte[8]; diff --git a/src/UglyToad.PdfPig/XObjects/XObjectImage.cs b/src/UglyToad.PdfPig/XObjects/XObjectImage.cs index f7591c6a..3f8817f1 100644 --- a/src/UglyToad.PdfPig/XObjects/XObjectImage.cs +++ b/src/UglyToad.PdfPig/XObjects/XObjectImage.cs @@ -6,6 +6,7 @@ using Core; using Graphics.Colors; using Graphics.Core; + using Images.Png; using Tokens; using Util.JetBrains.Annotations; @@ -107,6 +108,47 @@ return true; } + /// + public bool TryGetPng(out byte[] bytes) + { + bytes = null; + if (ColorSpace != Graphics.Colors.ColorSpace.DeviceRGB || !TryGetBytes(out var bytesPure)) + { + return false; + } + + try + { + var builder = PngBuilder.Create(WidthInSamples, HeightInSamples, false); + + var isCorrectlySized = bytesPure.Count == (WidthInSamples * HeightInSamples * (BitsPerComponent / 8) * 3); + + if (!isCorrectlySized) + { + return false; + } + + var i = 0; + for (var y = 0; y < HeightInSamples; y++) + { + for (var x = 0; x < WidthInSamples; x++) + { + builder.SetPixel(bytesPure[i++], bytesPure[i++], bytesPure[i++], x, y); + } + } + + bytes = builder.Save(); + + return true; + } + catch + { + // ignored. + } + + return false; + } + /// public override string ToString() { From 54f227ea9504379a1c166ff46ba8f5c6816c87b4 Mon Sep 17 00:00:00 2001 From: Eliot Jones Date: Sat, 22 Aug 2020 15:08:59 +0100 Subject: [PATCH 4/4] add support for extracting grayscale images and inline images --- src/UglyToad.PdfPig/Content/InlineImage.cs | 7 +-- .../Images/Png/PngFromPdfImageFactory.cs | 61 +++++++++++++++++++ src/UglyToad.PdfPig/UglyToad.PdfPig.csproj | 3 - src/UglyToad.PdfPig/Writer/PdfPageBuilder.cs | 8 ++- src/UglyToad.PdfPig/XObjects/XObjectImage.cs | 40 +----------- 5 files changed, 71 insertions(+), 48 deletions(-) create mode 100644 src/UglyToad.PdfPig/Images/Png/PngFromPdfImageFactory.cs diff --git a/src/UglyToad.PdfPig/Content/InlineImage.cs b/src/UglyToad.PdfPig/Content/InlineImage.cs index c1863d3d..3c104cfa 100644 --- a/src/UglyToad.PdfPig/Content/InlineImage.cs +++ b/src/UglyToad.PdfPig/Content/InlineImage.cs @@ -8,6 +8,7 @@ using Graphics.Colors; using Graphics.Core; using Tokens; + using Images.Png; /// /// @@ -112,11 +113,7 @@ } /// - public bool TryGetPng(out byte[] bytes) - { - bytes = null; - return false; - } + public bool TryGetPng(out byte[] bytes) => PngFromPdfImageFactory.TryGenerate(this, out bytes); /// public override string ToString() diff --git a/src/UglyToad.PdfPig/Images/Png/PngFromPdfImageFactory.cs b/src/UglyToad.PdfPig/Images/Png/PngFromPdfImageFactory.cs new file mode 100644 index 00000000..54dcadd0 --- /dev/null +++ b/src/UglyToad.PdfPig/Images/Png/PngFromPdfImageFactory.cs @@ -0,0 +1,61 @@ +namespace UglyToad.PdfPig.Images.Png +{ + using Content; + using Graphics.Colors; + + internal static class PngFromPdfImageFactory + { + public static bool TryGenerate(IPdfImage image, out byte[] bytes) + { + bytes = null; + + var isColorSpaceSupported = image.ColorSpace == ColorSpace.DeviceGray || image.ColorSpace == ColorSpace.DeviceRGB; + if (!isColorSpaceSupported || !image.TryGetBytes(out var bytesPure)) + { + return false; + } + + try + { + var is3Byte = image.ColorSpace == ColorSpace.DeviceRGB; + var multiplier = is3Byte ? 3 : 1; + + var builder = PngBuilder.Create(image.WidthInSamples, image.HeightInSamples, false); + + var isCorrectlySized = bytesPure.Count == (image.WidthInSamples * image.HeightInSamples * (image.BitsPerComponent / 8) * multiplier); + + if (!isCorrectlySized) + { + return false; + } + + var i = 0; + for (var y = 0; y < image.HeightInSamples; y++) + { + for (var x = 0; x < image.WidthInSamples; x++) + { + if (is3Byte) + { + builder.SetPixel(bytesPure[i++], bytesPure[i++], bytesPure[i++], x, y); + } + else + { + var pixel = bytesPure[i++]; + builder.SetPixel(pixel, pixel, pixel, x, y); + } + } + } + + bytes = builder.Save(); + + return true; + } + catch + { + // ignored. + } + + return false; + } + } +} diff --git a/src/UglyToad.PdfPig/UglyToad.PdfPig.csproj b/src/UglyToad.PdfPig/UglyToad.PdfPig.csproj index 0675a71f..345d5a68 100644 --- a/src/UglyToad.PdfPig/UglyToad.PdfPig.csproj +++ b/src/UglyToad.PdfPig/UglyToad.PdfPig.csproj @@ -38,7 +38,4 @@ - - - \ No newline at end of file diff --git a/src/UglyToad.PdfPig/Writer/PdfPageBuilder.cs b/src/UglyToad.PdfPig/Writer/PdfPageBuilder.cs index 7c5253a5..669ac2b2 100644 --- a/src/UglyToad.PdfPig/Writer/PdfPageBuilder.cs +++ b/src/UglyToad.PdfPig/Writer/PdfPageBuilder.cs @@ -333,7 +333,13 @@ /// /// An image previously added to this page or another page. /// The size and location to draw the image on this page. - public void AddJpeg(AddedImage image, PdfRectangle placementRectangle) + public void AddJpeg(AddedImage image, PdfRectangle placementRectangle) => AddImage(image, placementRectangle); + + /// + /// Adds the image previously added using + /// or sharing the same image to prevent duplication. + /// + public void AddImage(AddedImage image, PdfRectangle placementRectangle) { if (!resourcesDictionary.TryGetValue(NameToken.Xobject, out var xobjectsDict) || !(xobjectsDict is DictionaryToken xobjects)) diff --git a/src/UglyToad.PdfPig/XObjects/XObjectImage.cs b/src/UglyToad.PdfPig/XObjects/XObjectImage.cs index 3f8817f1..4bd8bd8c 100644 --- a/src/UglyToad.PdfPig/XObjects/XObjectImage.cs +++ b/src/UglyToad.PdfPig/XObjects/XObjectImage.cs @@ -109,45 +109,7 @@ } /// - public bool TryGetPng(out byte[] bytes) - { - bytes = null; - if (ColorSpace != Graphics.Colors.ColorSpace.DeviceRGB || !TryGetBytes(out var bytesPure)) - { - return false; - } - - try - { - var builder = PngBuilder.Create(WidthInSamples, HeightInSamples, false); - - var isCorrectlySized = bytesPure.Count == (WidthInSamples * HeightInSamples * (BitsPerComponent / 8) * 3); - - if (!isCorrectlySized) - { - return false; - } - - var i = 0; - for (var y = 0; y < HeightInSamples; y++) - { - for (var x = 0; x < WidthInSamples; x++) - { - builder.SetPixel(bytesPure[i++], bytesPure[i++], bytesPure[i++], x, y); - } - } - - bytes = builder.Save(); - - return true; - } - catch - { - // ignored. - } - - return false; - } + public bool TryGetPng(out byte[] bytes) => PngFromPdfImageFactory.TryGenerate(this, out bytes); /// public override string ToString()