mirror of
https://github.com/UglyToad/PdfPig.git
synced 2025-09-18 09:57:56 +08:00
add support for jpeg images in pdf document builder
since jpegs can be trivially embedded in pdf documents without changes to the data stream this is the first image format we will support. currently this is a naive approach which doesn't share an image resources between pages. ideally we will either de-duplicated images when added, return a re-usable key once an image is added, or both.
This commit is contained in:
Binary file not shown.
After Width: | Height: | Size: 6.2 KiB |
@@ -3,6 +3,7 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Content;
|
||||
using Integration;
|
||||
using PdfPig.Core;
|
||||
using PdfPig.Fonts.Standard14Fonts;
|
||||
using PdfPig.Writer;
|
||||
@@ -397,6 +398,44 @@
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanWriteSinglePageWithJpg()
|
||||
{
|
||||
var builder = new PdfDocumentBuilder();
|
||||
var page = builder.AddPage(PageSize.A4);
|
||||
|
||||
var font = builder.AddStandard14Font(Standard14Font.Helvetica);
|
||||
|
||||
page.AddText("Smile", 12, new PdfPoint(25, page.PageSize.Height - 52), font);
|
||||
|
||||
var img = IntegrationHelpers.GetDocumentPath("smile-250-by-160.jpg", false);
|
||||
|
||||
var expectedBounds = new PdfRectangle(25, page.PageSize.Height - 300, 200, page.PageSize.Height - 200);
|
||||
|
||||
var imageBytes = File.ReadAllBytes(img);
|
||||
|
||||
page.AddJpeg(imageBytes, expectedBounds);
|
||||
|
||||
var bytes = builder.Build();
|
||||
WriteFile(nameof(CanWriteSinglePageWithJpg), bytes);
|
||||
|
||||
using (var document = PdfDocument.Open(bytes))
|
||||
{
|
||||
var page1 = document.GetPage(1);
|
||||
|
||||
Assert.Equal("Smile", 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
|
||||
|
@@ -44,7 +44,7 @@
|
||||
/// <inheritdoc />
|
||||
public void Write(Stream stream)
|
||||
{
|
||||
stream.WriteText($"/{Name}");
|
||||
stream.WriteText($"/{Name.Data}");
|
||||
stream.WriteWhiteSpace();
|
||||
stream.WriteText(Symbol);
|
||||
stream.WriteNewLine();
|
||||
|
110
src/UglyToad.PdfPig/Images/JpegHandler.cs
Normal file
110
src/UglyToad.PdfPig/Images/JpegHandler.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
namespace UglyToad.PdfPig.Images
|
||||
{
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
internal static class JpegHandler
|
||||
{
|
||||
private const byte MarkerStart = 255;
|
||||
private const byte StartOfImage = 216;
|
||||
|
||||
public static JpegInformation GetInformation(Stream stream)
|
||||
{
|
||||
if (stream == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(stream));
|
||||
}
|
||||
|
||||
if (!HasRecognizedHeader(stream))
|
||||
{
|
||||
throw new InvalidOperationException("The input stream did not start with the expected JPEG header [ 255 216 ]");
|
||||
}
|
||||
|
||||
var marker = JpegMarker.StartOfImage;
|
||||
|
||||
var shortBuffer = new byte[2];
|
||||
|
||||
while (marker != JpegMarker.EndOfImage)
|
||||
{
|
||||
switch (marker)
|
||||
{
|
||||
case JpegMarker.StartOfBaselineDctFrame:
|
||||
{
|
||||
// ReSharper disable once UnusedVariable
|
||||
var length = ReadShort(stream, shortBuffer);
|
||||
var bpp = stream.ReadByte();
|
||||
var width = ReadShort(stream, shortBuffer);
|
||||
var height = ReadShort(stream, shortBuffer);
|
||||
|
||||
return new JpegInformation(width, height, bpp);
|
||||
}
|
||||
case JpegMarker.StartOfProgressiveDctFrame:
|
||||
break;
|
||||
}
|
||||
|
||||
marker = (JpegMarker)ReadSegmentMarker(stream, true);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("File was a valid JPEG but the width and height could not be determined.");
|
||||
}
|
||||
|
||||
private static bool HasRecognizedHeader(Stream stream)
|
||||
{
|
||||
var bytes = new byte[2];
|
||||
|
||||
var read = stream.Read(bytes, 0, 2);
|
||||
|
||||
if (read != 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return bytes[0] == MarkerStart
|
||||
&& bytes[1] == StartOfImage;
|
||||
}
|
||||
|
||||
private static byte ReadSegmentMarker(Stream stream, bool skipData = false)
|
||||
{
|
||||
byte? previous = null;
|
||||
int currentValue;
|
||||
while ((currentValue = stream.ReadByte()) != -1)
|
||||
{
|
||||
var b = (byte)currentValue;
|
||||
|
||||
if (!skipData)
|
||||
{
|
||||
if (!previous.HasValue && b != MarkerStart)
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
if (b != MarkerStart)
|
||||
{
|
||||
return b;
|
||||
}
|
||||
}
|
||||
|
||||
if (previous.HasValue && previous.Value == MarkerStart && b != MarkerStart)
|
||||
{
|
||||
return b;
|
||||
}
|
||||
|
||||
previous = b;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
private static short ReadShort(Stream stream, byte[] buffer)
|
||||
{
|
||||
var read = stream.Read(buffer, 0, 2);
|
||||
|
||||
if (read != 2)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to read a short where expected in the JPEG stream.");
|
||||
}
|
||||
|
||||
return (short) ((buffer[0] << 8) + buffer[1]);
|
||||
}
|
||||
}
|
||||
}
|
33
src/UglyToad.PdfPig/Images/JpegInformation.cs
Normal file
33
src/UglyToad.PdfPig/Images/JpegInformation.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
namespace UglyToad.PdfPig.Images
|
||||
{
|
||||
/// <summary>
|
||||
/// Information read from a JPEG image.
|
||||
/// </summary>
|
||||
internal class JpegInformation
|
||||
{
|
||||
/// <summary>
|
||||
/// Width of the image in pixels.
|
||||
/// </summary>
|
||||
public int Width { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Height of the image in pixels.
|
||||
/// </summary>
|
||||
public int Height { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Bits per component.
|
||||
/// </summary>
|
||||
public int BitsPerComponent { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="JpegInformation"/>.
|
||||
/// </summary>
|
||||
public JpegInformation(int width, int height, int bitsPerComponent)
|
||||
{
|
||||
Width = width;
|
||||
Height = height;
|
||||
BitsPerComponent = bitsPerComponent;
|
||||
}
|
||||
}
|
||||
}
|
86
src/UglyToad.PdfPig/Images/JpegMarker.cs
Normal file
86
src/UglyToad.PdfPig/Images/JpegMarker.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
namespace UglyToad.PdfPig.Images
|
||||
{
|
||||
internal enum JpegMarker : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates that this is a baseline DCT-based JPEG, and specifies the width, height, number of components, and component subsampling.
|
||||
/// </summary>
|
||||
StartOfBaselineDctFrame = 0xC0,
|
||||
/// <summary>
|
||||
/// Indicates that this is a progressive DCT-based JPEG, and specifies the width, height, number of components, and component subsampling.
|
||||
/// </summary>
|
||||
StartOfProgressiveDctFrame = 0xC2,
|
||||
/// <summary>
|
||||
/// Specifies one or more Huffman tables.
|
||||
/// </summary>
|
||||
DefineHuffmanTable = 0xC4,
|
||||
/// <summary>
|
||||
/// Begins a top-to-bottom scan of the image. In baseline images, there is generally a single scan.
|
||||
/// Progressive images usually contain multiple scans.
|
||||
/// </summary>
|
||||
StartOfScan = 0xDA,
|
||||
/// <summary>
|
||||
/// Specifies one or more quantization tables.
|
||||
/// </summary>
|
||||
DefineQuantizationTable = 0xDB,
|
||||
/// <summary>
|
||||
/// Specifies the interval between RSTn markers, in Minimum Coded Units (MCUs).
|
||||
/// This marker is followed by two bytes indicating the fixed size so it can be treated like any other variable size segment.
|
||||
/// </summary>
|
||||
DefineRestartInterval = 0xDD,
|
||||
/// <summary>
|
||||
/// Inserted every r macroblocks.
|
||||
/// </summary>
|
||||
Restart0 = 0xD0,
|
||||
/// <summary>
|
||||
/// Inserted every r macroblocks.
|
||||
/// </summary>
|
||||
Restart1 = 0xD1,
|
||||
/// <summary>
|
||||
/// Inserted every r macroblocks.
|
||||
/// </summary>
|
||||
Restart2 = 0xD2,
|
||||
/// <summary>
|
||||
/// Inserted every r macroblocks.
|
||||
/// </summary>
|
||||
Restart3 = 0xD3,
|
||||
/// <summary>
|
||||
/// Inserted every r macroblocks.
|
||||
/// </summary>
|
||||
Restart4 = 0xD4,
|
||||
/// <summary>
|
||||
/// Inserted every r macroblocks.
|
||||
/// </summary>
|
||||
Restart5 = 0xD5,
|
||||
/// <summary>
|
||||
/// Inserted every r macroblocks.
|
||||
/// </summary>
|
||||
Restart6 = 0xD6,
|
||||
/// <summary>
|
||||
/// Inserted every r macroblocks.
|
||||
/// </summary>
|
||||
Restart7 = 0xD7,
|
||||
/// <summary>
|
||||
/// Marks the start of a JPEG image file.
|
||||
/// </summary>
|
||||
StartOfImage = 0xD8,
|
||||
/// <summary>
|
||||
/// Marks the end of a JPEG image file.
|
||||
/// </summary>
|
||||
EndOfImage = 0xD9,
|
||||
ApplicationSpecific0 = 0xE0,
|
||||
ApplicationSpecific1 = 0xE1,
|
||||
ApplicationSpecific2 = 0xE2,
|
||||
ApplicationSpecific3 = 0xE3,
|
||||
ApplicationSpecific4 = 0xE4,
|
||||
ApplicationSpecific5 = 0xE5,
|
||||
ApplicationSpecific6 = 0xE6,
|
||||
ApplicationSpecific7 = 0xE7,
|
||||
ApplicationSpecific8 = 0xE8,
|
||||
ApplicationSpecific9 = 0xE9,
|
||||
/// <summary>
|
||||
/// Marks a text comment.
|
||||
/// </summary>
|
||||
Comment = 0xFE
|
||||
}
|
||||
}
|
@@ -19,8 +19,10 @@
|
||||
/// </summary>
|
||||
public class PdfDocumentBuilder
|
||||
{
|
||||
private readonly BuilderContext context = new BuilderContext();
|
||||
private readonly Dictionary<int, PdfPageBuilder> pages = new Dictionary<int, PdfPageBuilder>();
|
||||
private readonly Dictionary<Guid, FontStored> fonts = new Dictionary<Guid, FontStored>();
|
||||
private readonly Dictionary<Guid, ImageStored> images = new Dictionary<Guid, ImageStored>();
|
||||
|
||||
/// <summary>
|
||||
/// Whether to include the document information dictionary in the produced document.
|
||||
@@ -133,7 +135,18 @@
|
||||
|
||||
return added;
|
||||
}
|
||||
|
||||
|
||||
internal IndirectReference AddImage(DictionaryToken dictionary, byte[] bytes)
|
||||
{
|
||||
var reserved = context.ReserveNumber();
|
||||
|
||||
var stored = new ImageStored(dictionary, bytes, reserved);
|
||||
|
||||
images[stored.Id] = stored;
|
||||
|
||||
return new IndirectReference(reserved, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a new page with the specified size, this page will be included in the output when <see cref="Build"/> is called.
|
||||
/// </summary>
|
||||
@@ -206,7 +219,6 @@
|
||||
/// <returns>The bytes of the resulting PDF document.</returns>
|
||||
public byte[] Build()
|
||||
{
|
||||
var context = new BuilderContext();
|
||||
var fontsWritten = new Dictionary<Guid, ObjectToken>();
|
||||
using (var memory = new MemoryStream())
|
||||
{
|
||||
@@ -228,6 +240,13 @@
|
||||
fontsWritten.Add(font.Key, fontObj);
|
||||
}
|
||||
|
||||
foreach (var image in images)
|
||||
{
|
||||
var streamToken = new StreamToken(image.Value.StreamDictionary, image.Value.StreamData);
|
||||
|
||||
context.WriteObject(memory, streamToken, image.Value.ObjectNumber);
|
||||
}
|
||||
|
||||
var resources = new Dictionary<NameToken, IToken>
|
||||
{
|
||||
{ NameToken.ProcSet, new ArrayToken(new []{ NameToken.Create("PDF"), NameToken.Create("Text") }) }
|
||||
@@ -249,17 +268,25 @@
|
||||
var pageReferences = new List<IndirectReferenceToken>();
|
||||
foreach (var page in pages)
|
||||
{
|
||||
var individualResources = new Dictionary<NameToken, IToken>(resources);
|
||||
var pageDictionary = new Dictionary<NameToken, IToken>
|
||||
{
|
||||
{NameToken.Type, NameToken.Page},
|
||||
{
|
||||
NameToken.Resources,
|
||||
new DictionaryToken(resources)
|
||||
},
|
||||
{NameToken.MediaBox, RectangleToArray(page.Value.PageSize)},
|
||||
{NameToken.Parent, parentIndirect}
|
||||
};
|
||||
|
||||
if (page.Value.Resources.Count > 0)
|
||||
{
|
||||
foreach (var kvp in page.Value.Resources)
|
||||
{
|
||||
// TODO: combine resources if value is dictionary or array, otherwise overwrite.
|
||||
individualResources[kvp.Key] = kvp.Value;
|
||||
}
|
||||
}
|
||||
|
||||
pageDictionary[NameToken.Resources] = new DictionaryToken(individualResources);
|
||||
|
||||
if (page.Value.Operations.Count > 0)
|
||||
{
|
||||
var contentStream = WriteContentStream(page.Value.Operations);
|
||||
@@ -274,12 +301,14 @@
|
||||
pageReferences.Add(new IndirectReferenceToken(pageRef.Number));
|
||||
}
|
||||
|
||||
var pagesDictionary = new DictionaryToken(new Dictionary<NameToken, IToken>
|
||||
var pagesDictionaryData = new Dictionary<NameToken, IToken>
|
||||
{
|
||||
{ NameToken.Type, NameToken.Pages },
|
||||
{ NameToken.Kids, new ArrayToken(pageReferences) },
|
||||
{ NameToken.Count, new NumericToken(pageReferences.Count) }
|
||||
});
|
||||
{NameToken.Type, NameToken.Pages},
|
||||
{NameToken.Kids, new ArrayToken(pageReferences)},
|
||||
{NameToken.Count, new NumericToken(pageReferences.Count)}
|
||||
};
|
||||
|
||||
var pagesDictionary = new DictionaryToken(pagesDictionaryData);
|
||||
|
||||
var pagesRef = context.WriteObject(memory, pagesDictionary, reserved);
|
||||
|
||||
@@ -361,6 +390,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
internal class ImageStored
|
||||
{
|
||||
public Guid Id { get; }
|
||||
|
||||
public DictionaryToken StreamDictionary { get; }
|
||||
|
||||
public byte[] StreamData { get; }
|
||||
|
||||
public int ObjectNumber { get; }
|
||||
|
||||
public ImageStored(DictionaryToken streamDictionary, byte[] streamData, int objectNumber)
|
||||
{
|
||||
Id = Guid.NewGuid();
|
||||
StreamDictionary = streamDictionary;
|
||||
StreamData = streamData;
|
||||
ObjectNumber = objectNumber;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A key representing a font available to use on the current document builder. Create by adding a font to a document using either
|
||||
/// <see cref="AddStandard14Font"/> or <see cref="AddTrueTypeFont"/>.
|
||||
|
@@ -2,6 +2,7 @@
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Content;
|
||||
using Core;
|
||||
using Fonts;
|
||||
@@ -14,6 +15,8 @@
|
||||
using Graphics.Operations.TextPositioning;
|
||||
using Graphics.Operations.TextShowing;
|
||||
using Graphics.Operations.TextState;
|
||||
using Images;
|
||||
using Tokens;
|
||||
|
||||
/// <summary>
|
||||
/// A builder used to add construct a page in a PDF document.
|
||||
@@ -22,12 +25,17 @@
|
||||
{
|
||||
private readonly PdfDocumentBuilder documentBuilder;
|
||||
private readonly List<IGraphicsStateOperation> operations = new List<IGraphicsStateOperation>();
|
||||
private readonly Dictionary<NameToken, IToken> resourcesDictionary = new Dictionary<NameToken, IToken>();
|
||||
|
||||
//a sequence number of ShowText operation to determine whether letters belong to same operation or not (letters that belong to different operations have less changes to belong to same word)
|
||||
private int textSequence;
|
||||
|
||||
private int imageKey = 1;
|
||||
|
||||
internal IReadOnlyList<IGraphicsStateOperation> Operations => operations;
|
||||
|
||||
internal IReadOnlyDictionary<NameToken, IToken> Resources => resourcesDictionary;
|
||||
|
||||
/// <summary>
|
||||
/// The number of this page, 1-indexed.
|
||||
/// </summary>
|
||||
@@ -250,6 +258,69 @@
|
||||
return letters;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the JPEG image represented by the input bytes at the specified location.
|
||||
/// </summary>
|
||||
public void AddJpeg(byte[] fileBytes, PdfRectangle placementRectangle)
|
||||
{
|
||||
using (var stream = new MemoryStream(fileBytes))
|
||||
{
|
||||
AddJpeg(stream, placementRectangle);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the JPEG image represented by the input stream at the specified location.
|
||||
/// </summary>
|
||||
public void AddJpeg(Stream fileStream, PdfRectangle placementRectangle)
|
||||
{
|
||||
var startFrom = fileStream.Position;
|
||||
var info = JpegHandler.GetInformation(fileStream);
|
||||
|
||||
byte[] data;
|
||||
using (var memory = new MemoryStream())
|
||||
{
|
||||
fileStream.Seek(startFrom, SeekOrigin.Begin);
|
||||
fileStream.CopyTo(memory);
|
||||
data = memory.ToArray();
|
||||
}
|
||||
|
||||
var imgDictionary = new Dictionary<NameToken, IToken>
|
||||
{
|
||||
{NameToken.Type, NameToken.Xobject },
|
||||
{NameToken.Subtype, NameToken.Image },
|
||||
{NameToken.Width, new NumericToken(info.Width) },
|
||||
{NameToken.Height, new NumericToken(info.Height) },
|
||||
{NameToken.BitsPerComponent, new NumericToken(info.BitsPerComponent)},
|
||||
{NameToken.ColorSpace, NameToken.Devicergb},
|
||||
{NameToken.Filter, NameToken.DctDecode},
|
||||
{NameToken.Length, new NumericToken(data.Length)}
|
||||
};
|
||||
|
||||
var reference = documentBuilder.AddImage(new DictionaryToken(imgDictionary), data);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private List<Letter> DrawLetters(string text, IWritingFont font, TransformationMatrix fontMatrix, decimal fontSize, TransformationMatrix textMatrix)
|
||||
{
|
||||
var horizontalScaling = 1;
|
||||
|
Reference in New Issue
Block a user