mirror of
https://github.com/UglyToad/PdfPig.git
synced 2025-09-19 02:37:56 +08:00
support conversion of pdf format images to png
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -111,6 +111,13 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetPng(out byte[] bytes)
|
||||
{
|
||||
bytes = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
|
@@ -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}},
|
||||
|
159
src/UglyToad.PdfPig/Images/Png/PngBuilder.cs
Normal file
159
src/UglyToad.PdfPig/Images/Png/PngBuilder.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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];
|
||||
|
@@ -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()
|
||||
{
|
||||
|
Reference in New Issue
Block a user