mirror of
https://github.com/UglyToad/PdfPig.git
synced 2025-09-19 02:37:56 +08:00
add initial png support
This commit is contained in:
BIN
src/UglyToad.PdfPig.Tests/Integration/Documents/pdfpig.png
Normal file
BIN
src/UglyToad.PdfPig.Tests/Integration/Documents/pdfpig.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
@@ -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
|
||||
|
114
src/UglyToad.PdfPig/Images/Png/Adam7.cs
Normal file
114
src/UglyToad.PdfPig/Images/Png/Adam7.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
namespace UglyToad.PdfPig.Images.Png
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
|
||||
internal static class Adam7
|
||||
{
|
||||
/// <summary>
|
||||
/// For a given pass number (1 indexed) the scanline indexes of the lines included in that pass in the 8x8 grid.
|
||||
/// </summary>
|
||||
private static readonly IReadOnlyDictionary<int, int[]> PassToScanlineGridIndex = new Dictionary<int, int[]>
|
||||
{
|
||||
{ 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<int, int[]> PassToScanlineColumnIndex = new Dictionary<int, int[]>
|
||||
{
|
||||
{ 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]);
|
||||
}
|
||||
}
|
||||
}
|
35
src/UglyToad.PdfPig/Images/Png/Adler32Checksum.cs
Normal file
35
src/UglyToad.PdfPig/Images/Png/Adler32Checksum.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
namespace UglyToad.PdfPig.Images.Png
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
|
||||
/// <summary>
|
||||
/// Used to calculate the Adler-32 checksum used for ZLIB data in accordance with
|
||||
/// RFC 1950: ZLIB Compressed Data Format Specification.
|
||||
/// </summary>
|
||||
public static class Adler32Checksum
|
||||
{
|
||||
// Both sums (s1 and s2) are done modulo 65521.
|
||||
private const int AdlerModulus = 65521;
|
||||
|
||||
/// <summary>
|
||||
/// Calculate the Adler-32 checksum for some data.
|
||||
/// </summary>
|
||||
public static int Calculate(IEnumerable<byte> 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;
|
||||
}
|
||||
}
|
||||
}
|
62
src/UglyToad.PdfPig/Images/Png/ChunkHeader.cs
Normal file
62
src/UglyToad.PdfPig/Images/Png/ChunkHeader.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
namespace UglyToad.PdfPig.Images.Png
|
||||
{
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// The header for a data chunk in a PNG file.
|
||||
/// </summary>
|
||||
public readonly struct ChunkHeader
|
||||
{
|
||||
/// <summary>
|
||||
/// The position/start of the chunk header within the stream.
|
||||
/// </summary>
|
||||
public long Position { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The length of the chunk in bytes.
|
||||
/// </summary>
|
||||
public int Length { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the chunk, uppercase first letter means the chunk is critical (vs. ancillary).
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the chunk is critical (must be read by all readers) or ancillary (may be ignored).
|
||||
/// </summary>
|
||||
public bool IsCritical => char.IsUpper(Name[0]);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public bool IsPublic => char.IsUpper(Name[1]);
|
||||
|
||||
/// <summary>
|
||||
/// Whether the (if unrecognized) chunk is safe to copy.
|
||||
/// </summary>
|
||||
public bool IsSafeToCopy => char.IsUpper(Name[3]);
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="ChunkHeader"/>.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Name} at {Position} (length: {Length}).";
|
||||
}
|
||||
}
|
||||
}
|
28
src/UglyToad.PdfPig/Images/Png/ColorType.cs
Normal file
28
src/UglyToad.PdfPig/Images/Png/ColorType.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
namespace UglyToad.PdfPig.Images.Png
|
||||
{
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// Describes the interpretation of the image data.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum ColorType : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Grayscale.
|
||||
/// </summary>
|
||||
None = 0,
|
||||
/// <summary>
|
||||
/// Colors are stored in a palette rather than directly in the data.
|
||||
/// </summary>
|
||||
PaletteUsed = 1,
|
||||
/// <summary>
|
||||
/// The image uses color.
|
||||
/// </summary>
|
||||
ColorUsed = 2,
|
||||
/// <summary>
|
||||
/// The image has an alpha channel.
|
||||
/// </summary>
|
||||
AlphaChannelUsed = 4
|
||||
}
|
||||
}
|
13
src/UglyToad.PdfPig/Images/Png/CompressionMethod.cs
Normal file
13
src/UglyToad.PdfPig/Images/Png/CompressionMethod.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace UglyToad.PdfPig.Images.Png
|
||||
{
|
||||
/// <summary>
|
||||
/// The method used to compress the image data.
|
||||
/// </summary>
|
||||
public enum CompressionMethod : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Deflate/inflate compression with a sliding window of at most 32768 bytes.
|
||||
/// </summary>
|
||||
DeflateWithSlidingWindow = 0
|
||||
}
|
||||
}
|
87
src/UglyToad.PdfPig/Images/Png/Crc32.cs
Normal file
87
src/UglyToad.PdfPig/Images/Png/Crc32.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
namespace UglyToad.PdfPig.Images.Png
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
|
||||
/// <summary>
|
||||
/// 32-bit Cyclic Redundancy Code used by the PNG for checking the data is intact.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculate the CRC32 for data.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculate the CRC32 for data.
|
||||
/// </summary>
|
||||
public static uint Calculate(List<byte> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculate the combined CRC32 for data.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
211
src/UglyToad.PdfPig/Images/Png/Decoder.cs
Normal file
211
src/UglyToad.PdfPig/Images/Png/Decoder.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
13
src/UglyToad.PdfPig/Images/Png/FilterMethod.cs
Normal file
13
src/UglyToad.PdfPig/Images/Png/FilterMethod.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace UglyToad.PdfPig.Images.Png
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates the pre-processing method applied to the image data before compression.
|
||||
/// </summary>
|
||||
public enum FilterMethod
|
||||
{
|
||||
/// <summary>
|
||||
/// Adaptive filtering with five basic filter types.
|
||||
/// </summary>
|
||||
AdaptiveFiltering = 0
|
||||
}
|
||||
}
|
26
src/UglyToad.PdfPig/Images/Png/FilterType.cs
Normal file
26
src/UglyToad.PdfPig/Images/Png/FilterType.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
namespace UglyToad.PdfPig.Images.Png
|
||||
{
|
||||
internal enum FilterType
|
||||
{
|
||||
/// <summary>
|
||||
/// The raw byte is unaltered.
|
||||
/// </summary>
|
||||
None = 0,
|
||||
/// <summary>
|
||||
/// The byte to the left.
|
||||
/// </summary>
|
||||
Sub = 1,
|
||||
/// <summary>
|
||||
/// The byte above.
|
||||
/// </summary>
|
||||
Up = 2,
|
||||
/// <summary>
|
||||
/// The mean of bytes left and above, rounded down.
|
||||
/// </summary>
|
||||
Average = 3,
|
||||
/// <summary>
|
||||
/// Byte to the left, above or top-left based on Paeth's algorithm.
|
||||
/// </summary>
|
||||
Paeth = 4
|
||||
}
|
||||
}
|
54
src/UglyToad.PdfPig/Images/Png/HeaderValidationResult.cs
Normal file
54
src/UglyToad.PdfPig/Images/Png/HeaderValidationResult.cs
Normal file
@@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
15
src/UglyToad.PdfPig/Images/Png/IChunkVisitor.cs
Normal file
15
src/UglyToad.PdfPig/Images/Png/IChunkVisitor.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace UglyToad.PdfPig.Images.Png
|
||||
{
|
||||
using System.IO;
|
||||
|
||||
/// <summary>
|
||||
/// Enables execution of custom logic whenever a chunk is read.
|
||||
/// </summary>
|
||||
public interface IChunkVisitor
|
||||
{
|
||||
/// <summary>
|
||||
/// Called by the PNG reader after a chunk is read.
|
||||
/// </summary>
|
||||
void Visit(Stream stream, ImageHeader header, ChunkHeader chunkHeader, byte[] data, byte[] crc);
|
||||
}
|
||||
}
|
96
src/UglyToad.PdfPig/Images/Png/ImageHeader.cs
Normal file
96
src/UglyToad.PdfPig/Images/Png/ImageHeader.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
namespace UglyToad.PdfPig.Images.Png
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
/// <summary>
|
||||
/// The high level information about the image.
|
||||
/// </summary>
|
||||
public readonly struct ImageHeader
|
||||
{
|
||||
internal static readonly byte[] HeaderBytes = {
|
||||
73, 72, 68, 82
|
||||
};
|
||||
|
||||
private static readonly IReadOnlyDictionary<ColorType, HashSet<byte>> PermittedBitDepths = new Dictionary<ColorType, HashSet<byte>>
|
||||
{
|
||||
{ColorType.None, new HashSet<byte> {1, 2, 4, 8, 16}},
|
||||
{ColorType.ColorUsed, new HashSet<byte> {8, 16}},
|
||||
{ColorType.PaletteUsed | ColorType.ColorUsed, new HashSet<byte> {1, 2, 4, 8}},
|
||||
{ColorType.AlphaChannelUsed, new HashSet<byte> {8, 16}},
|
||||
{ColorType.AlphaChannelUsed | ColorType.ColorUsed, new HashSet<byte> {8, 16}},
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The width of the image in pixels.
|
||||
/// </summary>
|
||||
public int Width { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The height of the image in pixels.
|
||||
/// </summary>
|
||||
public int Height { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The bit depth of the image.
|
||||
/// </summary>
|
||||
public byte BitDepth { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The color type of the image.
|
||||
/// </summary>
|
||||
public ColorType ColorType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The compression method used for the image.
|
||||
/// </summary>
|
||||
public CompressionMethod CompressionMethod { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The filter method used for the image.
|
||||
/// </summary>
|
||||
public FilterMethod FilterMethod { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The interlace method used by the image..
|
||||
/// </summary>
|
||||
public InterlaceMethod InterlaceMethod { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="ImageHeader"/>.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"w: {Width}, h: {Height}, bitDepth: {BitDepth}, colorType: {ColorType}, " +
|
||||
$"compression: {CompressionMethod}, filter: {FilterMethod}, interlace: {InterlaceMethod}.";
|
||||
}
|
||||
}
|
||||
}
|
17
src/UglyToad.PdfPig/Images/Png/InterlaceMethod.cs
Normal file
17
src/UglyToad.PdfPig/Images/Png/InterlaceMethod.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace UglyToad.PdfPig.Images.Png
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates the transmission order of the image data.
|
||||
/// </summary>
|
||||
public enum InterlaceMethod : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// No interlace.
|
||||
/// </summary>
|
||||
None = 0,
|
||||
/// <summary>
|
||||
/// Adam7 interlace.
|
||||
/// </summary>
|
||||
Adam7 = 1
|
||||
}
|
||||
}
|
19
src/UglyToad.PdfPig/Images/Png/Palette.cs
Normal file
19
src/UglyToad.PdfPig/Images/Png/Palette.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
123
src/UglyToad.PdfPig/Images/Png/Pixel.cs
Normal file
123
src/UglyToad.PdfPig/Images/Png/Pixel.cs
Normal file
@@ -0,0 +1,123 @@
|
||||
namespace UglyToad.PdfPig.Images.Png
|
||||
{
|
||||
/// <summary>
|
||||
/// A pixel in a <see cref="Png"/> image.
|
||||
/// </summary>
|
||||
public readonly struct Pixel
|
||||
{
|
||||
/// <summary>
|
||||
/// The red value for the pixel.
|
||||
/// </summary>
|
||||
public byte R { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The green value for the pixel.
|
||||
/// </summary>
|
||||
public byte G { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The blue value for the pixel.
|
||||
/// </summary>
|
||||
public byte B { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The alpha transparency value for the pixel.
|
||||
/// </summary>
|
||||
public byte A { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the pixel is grayscale (if <see langword="true"/> <see cref="R"/>, <see cref="G"/> and <see cref="B"/> will all have the same value).
|
||||
/// </summary>
|
||||
public bool IsGrayscale { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="Pixel"/>.
|
||||
/// </summary>
|
||||
/// <param name="r">The red value for the pixel.</param>
|
||||
/// <param name="g">The green value for the pixel.</param>
|
||||
/// <param name="b">The blue value for the pixel.</param>
|
||||
/// <param name="a">The alpha transparency value for the pixel.</param>
|
||||
/// <param name="isGrayscale">Whether the pixel is grayscale.</param>
|
||||
public Pixel(byte r, byte g, byte b, byte a, bool isGrayscale)
|
||||
{
|
||||
R = r;
|
||||
G = g;
|
||||
B = b;
|
||||
A = a;
|
||||
IsGrayscale = isGrayscale;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="Pixel"/> which has <see cref="IsGrayscale"/> false and is fully opaque.
|
||||
/// </summary>
|
||||
/// <param name="r">The red value for the pixel.</param>
|
||||
/// <param name="g">The green value for the pixel.</param>
|
||||
/// <param name="b">The blue value for the pixel.</param>
|
||||
public Pixel(byte r, byte g, byte b)
|
||||
{
|
||||
R = r;
|
||||
G = g;
|
||||
B = b;
|
||||
A = 255;
|
||||
IsGrayscale = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new grayscale <see cref="Pixel"/>.
|
||||
/// </summary>
|
||||
/// <param name="grayscale">The grayscale value.</param>
|
||||
public Pixel(byte grayscale)
|
||||
{
|
||||
R = grayscale;
|
||||
G = grayscale;
|
||||
B = grayscale;
|
||||
A = 255;
|
||||
IsGrayscale = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the pixel values are equal.
|
||||
/// </summary>
|
||||
/// <param name="other">The other pixel.</param>
|
||||
/// <returns><see langword="true"/> if all pixel values are equal otherwise <see langword="false"/>.</returns>
|
||||
public bool Equals(Pixel other)
|
||||
{
|
||||
return R == other.R && G == other.G && B == other.B && A == other.A && IsGrayscale == other.IsGrayscale;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"({R}, {G}, {B}, {A})";
|
||||
}
|
||||
}
|
||||
}
|
89
src/UglyToad.PdfPig/Images/Png/Png.cs
Normal file
89
src/UglyToad.PdfPig/Images/Png/Png.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
namespace UglyToad.PdfPig.Images.Png
|
||||
{
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
/// <summary>
|
||||
/// A PNG image. Call <see cref="Open(byte[],IChunkVisitor)"/> to open from file or bytes.
|
||||
/// </summary>
|
||||
public class Png
|
||||
{
|
||||
private readonly RawPngData data;
|
||||
|
||||
/// <summary>
|
||||
/// The header data from the PNG image.
|
||||
/// </summary>
|
||||
public ImageHeader Header { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The width of the image in pixels.
|
||||
/// </summary>
|
||||
public int Width => Header.Width;
|
||||
|
||||
/// <summary>
|
||||
/// The height of the image in pixels.
|
||||
/// </summary>
|
||||
public int Height => Header.Height;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the image has an alpha (transparency) layer.
|
||||
/// </summary>
|
||||
public bool HasAlphaChannel => (Header.ColorType & ColorType.AlphaChannelUsed) != 0;
|
||||
|
||||
internal Png(ImageHeader header, RawPngData data)
|
||||
{
|
||||
Header = header;
|
||||
this.data = data ?? throw new ArgumentNullException(nameof(data));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the pixel at the given column and row (x, y).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
/// <param name="x">The x coordinate (column).</param>
|
||||
/// <param name="y">The y coordinate (row).</param>
|
||||
/// <returns>The pixel at the coordinate.</returns>
|
||||
public Pixel GetPixel(int x, int y) => data.GetPixel(x, y);
|
||||
|
||||
/// <summary>
|
||||
/// Read the PNG image from the stream.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream containing PNG data to be read.</param>
|
||||
/// <param name="chunkVisitor">Optional: A visitor which is called whenever a chunk is read by the library.</param>
|
||||
/// <returns>The <see cref="Png"/> data from the stream.</returns>
|
||||
public static Png Open(Stream stream, IChunkVisitor chunkVisitor = null)
|
||||
=> PngOpener.Open(stream, chunkVisitor);
|
||||
|
||||
/// <summary>
|
||||
/// Read the PNG image from the bytes.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The bytes of the PNG data to be read.</param>
|
||||
/// <param name="chunkVisitor">Optional: A visitor which is called whenever a chunk is read by the library.</param>
|
||||
/// <returns>The <see cref="Png"/> data from the bytes.</returns>
|
||||
public static Png Open(byte[] bytes, IChunkVisitor chunkVisitor = null)
|
||||
{
|
||||
using (var memoryStream = new MemoryStream(bytes))
|
||||
{
|
||||
return PngOpener.Open(memoryStream, chunkVisitor);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read the PNG from the file path.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The path to the PNG file to open.</param>
|
||||
/// <param name="chunkVisitor">Optional: A visitor which is called whenever a chunk is read by the library.</param>
|
||||
/// <remarks>This will open the file to obtain a <see cref="FileStream"/> so will lock the file during reading.</remarks>
|
||||
/// <returns>The <see cref="Png"/> data from the file.</returns>
|
||||
public static Png Open(string filePath, IChunkVisitor chunkVisitor = null)
|
||||
{
|
||||
using (var fileStream = File.OpenRead(filePath))
|
||||
{
|
||||
return Open(fileStream, chunkVisitor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
183
src/UglyToad.PdfPig/Images/Png/PngOpener.cs
Normal file
183
src/UglyToad.PdfPig/Images/Png/PngOpener.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
63
src/UglyToad.PdfPig/Images/Png/PngStreamWriteHelper.cs
Normal file
63
src/UglyToad.PdfPig/Images/Png/PngStreamWriteHelper.cs
Normal file
@@ -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<byte> written = new List<byte>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
105
src/UglyToad.PdfPig/Images/Png/RawPngData.cs
Normal file
105
src/UglyToad.PdfPig/Images/Png/RawPngData.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
namespace UglyToad.PdfPig.Images.Png
|
||||
{
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// Provides convenience methods for indexing into a raw byte array to extract pixel values.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="RawPngData"/>.
|
||||
/// </summary>
|
||||
/// <param name="data">The decoded pixel data as bytes.</param>
|
||||
/// <param name="bytesPerPixel">The number of bytes in each pixel.</param>
|
||||
/// <param name="width">The width of the image in pixels.</param>
|
||||
/// <param name="interlaceMethod">The interlace method used.</param>
|
||||
/// <param name="palette">The palette for images using indexed colors.</param>
|
||||
/// <param name="colorType">The color type.</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
46
src/UglyToad.PdfPig/Images/Png/StreamHelper.cs
Normal file
46
src/UglyToad.PdfPig/Images/Png/StreamHelper.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -38,4 +38,7 @@
|
||||
<ProjectReference Include="..\UglyToad.PdfPig.Tokenization\UglyToad.PdfPig.Tokenization.csproj" />
|
||||
<ProjectReference Include="..\UglyToad.PdfPig.Tokens\UglyToad.PdfPig.Tokens.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Images\Png\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
@@ -19,6 +19,7 @@
|
||||
using PdfFonts;
|
||||
using Tokens;
|
||||
using Graphics.Operations.PathPainting;
|
||||
using Images.Png;
|
||||
|
||||
/// <summary>
|
||||
/// A builder used to add construct a page in a PDF document.
|
||||
@@ -357,6 +358,86 @@
|
||||
operations.Add(Pop.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the PNG image represented by the input bytes at the specified location.
|
||||
/// </summary>
|
||||
public AddedImage AddPng(byte[] pngBytes, PdfRectangle placementRectangle)
|
||||
{
|
||||
using (var memoryStream = new MemoryStream(pngBytes))
|
||||
{
|
||||
return AddPng(memoryStream, placementRectangle);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the PNG image represented by the input stream at the specified location.
|
||||
/// </summary>
|
||||
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, IToken>
|
||||
{
|
||||
{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<NameToken, IToken>());
|
||||
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<Letter> DrawLetters(string text, IWritingFont font, TransformationMatrix fontMatrix, decimal fontSize, TransformationMatrix textMatrix)
|
||||
{
|
||||
var horizontalScaling = 1;
|
||||
|
Reference in New Issue
Block a user