Merge pull request #201 from UglyToad/png-builder-support

Png builder support
This commit is contained in:
Eliot Jones
2020-08-22 15:37:20 +01:00
committed by GitHub
29 changed files with 1788 additions and 3 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -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);
}
}
}
}
}
}

View File

@@ -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);
}

View File

@@ -89,5 +89,10 @@
/// <see cref="RawBytes"/> should be used directly.
/// </summary>
bool TryGetBytes(out IReadOnlyList<byte> bytes);
/// <summary>
/// Try to convert the image to PNG. Doesn't support conversion of JPG to PNG.
/// </summary>
bool TryGetPng(out byte[] bytes);
}
}

View File

@@ -8,6 +8,7 @@
using Graphics.Colors;
using Graphics.Core;
using Tokens;
using Images.Png;
/// <inheritdoc />
/// <summary>
@@ -111,6 +112,9 @@
return true;
}
/// <inheritdoc />
public bool TryGetPng(out byte[] bytes) => PngFromPdfImageFactory.TryGenerate(this, out bytes);
/// <inheritdoc />
public override string ToString()
{

View 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]);
}
}
}

View 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>
internal 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;
}
}
}

View File

@@ -0,0 +1,62 @@
namespace UglyToad.PdfPig.Images.Png
{
using System;
/// <summary>
/// The header for a data chunk in a PNG file.
/// </summary>
internal 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}).";
}
}
}

View File

@@ -0,0 +1,28 @@
namespace UglyToad.PdfPig.Images.Png
{
using System;
/// <summary>
/// Describes the interpretation of the image data.
/// </summary>
[Flags]
internal 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
}
}

View File

@@ -0,0 +1,13 @@
namespace UglyToad.PdfPig.Images.Png
{
/// <summary>
/// The method used to compress the image data.
/// </summary>
internal enum CompressionMethod : byte
{
/// <summary>
/// Deflate/inflate compression with a sliding window of at most 32768 bytes.
/// </summary>
DeflateWithSlidingWindow = 0
}
}

View 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>
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;
}
}
/// <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;
}
}
}

View 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;
}
}
}

View File

@@ -0,0 +1,13 @@
namespace UglyToad.PdfPig.Images.Png
{
/// <summary>
/// Indicates the pre-processing method applied to the image data before compression.
/// </summary>
internal enum FilterMethod
{
/// <summary>
/// Adaptive filtering with five basic filter types.
/// </summary>
AdaptiveFiltering = 0
}
}

View 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
}
}

View 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}";
}
}
}

View 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>
internal 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);
}
}

View 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>
internal 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}.";
}
}
}

View File

@@ -0,0 +1,17 @@
namespace UglyToad.PdfPig.Images.Png
{
/// <summary>
/// Indicates the transmission order of the image data.
/// </summary>
internal enum InterlaceMethod : byte
{
/// <summary>
/// No interlace.
/// </summary>
None = 0,
/// <summary>
/// Adam7 interlace.
/// </summary>
Adam7 = 1
}
}

View 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);
}
}
}

View File

@@ -0,0 +1,123 @@
namespace UglyToad.PdfPig.Images.Png
{
/// <summary>
/// A pixel in a <see cref="Png"/> image.
/// </summary>
internal 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})";
}
}
}

View 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>
internal 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);
}
}
}
}

View File

@@ -0,0 +1,159 @@
namespace UglyToad.PdfPig.Images.Png
{
using System.IO;
using System.IO.Compression;
using System.Text;
/// <summary>
/// Used to construct PNG images. Call <see cref="Create"/> to make a new builder.
/// </summary>
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;
/// <summary>
/// Create a builder for a PNG with the given width and size.
/// </summary>
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;
}
/// <summary>
/// Sets the RGB pixel value for the given column (x) and row (y).
/// </summary>
public PngBuilder SetPixel(byte r, byte g, byte b, int x, int y) => SetPixel(new Pixel(r, g, b), x, y);
/// <summary>
/// Set the pixel value for the given column (x) and row (y).
/// </summary>
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;
}
/// <summary>
/// Get the bytes of the PNG file for this builder.
/// </summary>
public byte[] Save()
{
using (var memoryStream = new MemoryStream())
{
Save(memoryStream);
return memoryStream.ToArray();
}
}
/// <summary>
/// Write the PNG file bytes to the provided stream.
/// </summary>
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;
}
}
}
}

View File

@@ -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;
}
}
}

View 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);
}
}
}

View 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);
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View File

@@ -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.
@@ -332,7 +333,13 @@
/// </summary>
/// <param name="image">An image previously added to this page or another page.</param>
/// <param name="placementRectangle">The size and location to draw the image on this page.</param>
public void AddJpeg(AddedImage image, PdfRectangle placementRectangle)
public void AddJpeg(AddedImage image, PdfRectangle placementRectangle) => AddImage(image, placementRectangle);
/// <summary>
/// Adds the image previously added using <see cref="AddJpeg(byte[], PdfRectangle)"/>
/// or <see cref="AddPng(byte[], PdfRectangle)"/> sharing the same image to prevent duplication.
/// </summary>
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);
}
/// <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;

View File

@@ -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;
}
/// <inheritdoc />
public bool TryGetPng(out byte[] bytes) => PngFromPdfImageFactory.TryGenerate(this, out bytes);
/// <inheritdoc />
public override string ToString()
{