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 00000000..170f6339
Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Integration/Documents/pdfpig.png differ
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 b05a547d..87125b32 100644
--- a/src/UglyToad.PdfPig.Tests/Writer/PdfDocumentBuilderTests.cs
+++ b/src/UglyToad.PdfPig.Tests/Writer/PdfDocumentBuilderTests.cs
@@ -529,7 +529,48 @@
}
}
- private static void WriteFile(string name, byte[] bytes)
+ [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.True(image.TryGetPng(out var png));
+ Assert.NotNull(png);
+
+ WriteFile(nameof(CanWriteSinglePageWithPng) + "out", png, "png");
+ }
+ }
+
+ private static void WriteFile(string name, byte[] bytes, string extension = "pdf")
{
try
{
@@ -538,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..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;
///
///
@@ -111,6 +112,9 @@
return true;
}
+ ///
+ public bool TryGetPng(out byte[] bytes) => PngFromPdfImageFactory.TryGenerate(this, out bytes);
+
///
public override string ToString()
{
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..59ace1f8
--- /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.
+ ///
+ internal 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..7ccb486c
--- /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.
+ ///
+ internal 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..9e0eb5fd
--- /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]
+ internal 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..49226ec7
--- /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.
+ ///
+ internal 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..d571018a
--- /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.
+ ///
+ internal 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..af35d9a1
--- /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.
+ ///
+ internal 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..85c0de28
--- /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.
+ ///
+ internal 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..98fece51
--- /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.
+ ///
+ 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}},
+ {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..6bd8d94c
--- /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.
+ ///
+ internal 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..46c5766c
--- /dev/null
+++ b/src/UglyToad.PdfPig/Images/Png/Pixel.cs
@@ -0,0 +1,123 @@
+namespace UglyToad.PdfPig.Images.Png
+{
+ ///
+ /// A pixel in a image.
+ ///
+ internal 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..c2979fb7
--- /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.
+ ///
+ internal 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/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/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/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/Writer/PdfPageBuilder.cs b/src/UglyToad.PdfPig/Writer/PdfPageBuilder.cs
index 96c43805..669ac2b2 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.
@@ -332,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))
@@ -357,6 +364,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;
diff --git a/src/UglyToad.PdfPig/XObjects/XObjectImage.cs b/src/UglyToad.PdfPig/XObjects/XObjectImage.cs
index f7591c6a..4bd8bd8c 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,9 @@
return true;
}
+ ///
+ public bool TryGetPng(out byte[] bytes) => PngFromPdfImageFactory.TryGenerate(this, out bytes);
+
///
public override string ToString()
{