support conversion of pdf format images to png

This commit is contained in:
Eliot Jones
2020-08-21 13:12:01 +01:00
parent 8860e29191
commit 52104b6580
9 changed files with 339 additions and 2 deletions

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

@@ -562,10 +562,15 @@
Assert.Equal(expectedBounds.BottomLeft, image.Bounds.BottomLeft);
Assert.Equal(expectedBounds.TopRight, image.Bounds.TopRight);
Assert.True(image.TryGetPng(out var png));
Assert.NotNull(png);
WriteFile(nameof(CanWriteSinglePageWithPng) + "out", png, "png");
}
}
private static void WriteFile(string name, byte[] bytes)
private static void WriteFile(string name, byte[] bytes, string extension = "pdf")
{
try
{
@@ -574,7 +579,7 @@
Directory.CreateDirectory("Builder");
}
var output = Path.Combine("Builder", $"{name}.pdf");
var output = Path.Combine("Builder", $"{name}.{extension}");
File.WriteAllBytes(output, bytes);
}

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

@@ -111,6 +111,13 @@
return true;
}
/// <inheritdoc />
public bool TryGetPng(out byte[] bytes)
{
bytes = null;
return false;
}
/// <inheritdoc />
public override string ToString()
{

View File

@@ -8,6 +8,10 @@
/// </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}},

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,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

@@ -1,15 +1,42 @@
namespace UglyToad.PdfPig.Images.Png
{
using System;
using System.IO;
internal static class StreamHelper
{
public static int ReadBigEndianInt32(Stream stream)
{
return (ReadOrTerminate(stream) << 24) + (ReadOrTerminate(stream) << 16)
+ (ReadOrTerminate(stream) << 8) + ReadOrTerminate(stream);
}
public static int ReadBigEndianInt32(byte[] bytes, int offset)
{
return (bytes[0 + offset] << 24) + (bytes[1 + offset] << 16)
+ (bytes[2 + offset] << 8) + bytes[3 + offset];
}
public static void WriteBigEndianInt32(Stream stream, int value)
{
stream.WriteByte((byte)(value >> 24));
stream.WriteByte((byte)(value >> 16));
stream.WriteByte((byte)(value >> 8));
stream.WriteByte((byte)value);
}
private static byte ReadOrTerminate(Stream stream)
{
var b = stream.ReadByte();
if (b == -1)
{
throw new InvalidOperationException($"Unexpected end of stream at {stream.Position}.");
}
return (byte) b;
}
public static bool TryReadHeaderBytes(Stream stream, out byte[] bytes)
{
bytes = new byte[8];

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,47 @@
return true;
}
/// <inheritdoc />
public bool TryGetPng(out byte[] bytes)
{
bytes = null;
if (ColorSpace != Graphics.Colors.ColorSpace.DeviceRGB || !TryGetBytes(out var bytesPure))
{
return false;
}
try
{
var builder = PngBuilder.Create(WidthInSamples, HeightInSamples, false);
var isCorrectlySized = bytesPure.Count == (WidthInSamples * HeightInSamples * (BitsPerComponent / 8) * 3);
if (!isCorrectlySized)
{
return false;
}
var i = 0;
for (var y = 0; y < HeightInSamples; y++)
{
for (var x = 0; x < WidthInSamples; x++)
{
builder.SetPixel(bytesPure[i++], bytesPure[i++], bytesPure[i++], x, y);
}
}
bytes = builder.Save();
return true;
}
catch
{
// ignored.
}
return false;
}
/// <inheritdoc />
public override string ToString()
{