Compare commits

...

6 Commits

Author SHA1 Message Date
Eliot Jones
4ed1600cab version 0.1.1 2020-03-18 20:10:51 +00:00
Eliot Jones
0f91017613 fix issue with newlines in object start tokens #88
where we brute force the file and it contains newlines between object tokens we fix the parsing to prevent pseudo-infinite loops.
2020-03-17 20:09:47 +00:00
Eliot Jones
5094f9d9d0 remove debugging code
this code was used to check content streams but should not be committed.
2020-03-16 19:32:57 +00:00
Eliot Jones
98bcc16e11 fix width and height order in jpeg parsing
height is before width, incorrect order caused adobe reader to draw image strangely.
2020-03-16 19:32:57 +00:00
Eliot Jones
7212b9e38c enable re-use of jpeg images between or within pages
returns a reference to the added image object when calling addjpeg so that it can be shared between or within pages meaning the image is only written to the output file once but can appear multiple times.

this image doesn't seem to be displaying correctly in adobe reader.
2020-03-16 19:32:57 +00:00
Eliot Jones
19462d79f0 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.
2020-03-16 19:32:57 +00:00
19 changed files with 609 additions and 33 deletions

View File

@@ -123,7 +123,7 @@
}
/// <summary>
/// Determines if a character is whitespace or not.
/// Determines if a character is whitespace or not, this includes newlines.
/// </summary>
/// <remarks>
/// These values are specified in table 1 (page 12) of ISO 32000-1:2008.

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net45;net451;net452;net46;net461;net462;net47</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Version>0.1.1-alpha001</Version>
<Version>0.1.1</Version>
<IsTestProject>False</IsTestProject>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net45;net451;net452;net46;net461;net462;net47</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Version>0.1.1-alpha001</Version>
<Version>0.1.1</Version>
<IsTestProject>False</IsTestProject>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net45;net451;net452;net46;net461;net462;net47</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Version>0.1.1-alpha001</Version>
<Version>0.1.1</Version>
<IsTestProject>False</IsTestProject>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@@ -36,7 +36,7 @@ startxref
216
%%EOF";
private static readonly long[] TestDataOffsets =
private static readonly long[] TestDataOffsets =
{
TestData.IndexOf("2 17 obj", StringComparison.OrdinalIgnoreCase),
TestData.IndexOf("3 0 obj", StringComparison.OrdinalIgnoreCase),
@@ -57,7 +57,7 @@ startxref
public void SearcherFindsCorrectObjects()
{
var input = new ByteArrayInputBytes(OtherEncodings.StringAsLatin1Bytes(TestData));
var locations = BruteForceSearcher.GetObjectLocations(input);
Assert.Equal(4, locations.Count);
@@ -69,11 +69,11 @@ startxref
public void ReaderOnlyCallsOnce()
{
var reader = StringBytesTestConverter.Convert(TestData, false);
var locations = BruteForceSearcher.GetObjectLocations(reader.Bytes);
Assert.Equal(4, locations.Count);
var newLocations = BruteForceSearcher.GetObjectLocations(reader.Bytes);
Assert.Equal(4, locations.Count);
@@ -132,7 +132,7 @@ endobj
5 0 obj
<< /IsEmpty false >>
endobj";
var bytes = new ByteArrayInputBytes(OtherEncodings.StringAsLatin1Bytes(s));
var locations = BruteForceSearcher.GetObjectLocations(bytes);
@@ -168,12 +168,35 @@ endobj";
Assert.Equal(581, locations[new IndirectReference(7, 0)]);
Assert.Equal(5068, locations[new IndirectReference(8, 0)]);
Assert.Equal(5091, locations[new IndirectReference(9, 0)]);
var s = GetStringAt(bytes, locations[new IndirectReference(3, 0)]);
Assert.StartsWith("3 0 obj", s);
}
}
[Fact]
public void BruteForceSearcherBytesFileOffsetsCorrect()
{
var bytes = new ByteArrayInputBytes(File.ReadAllBytes(IntegrationHelpers.GetDocumentPath("Single Page Simple - from inkscape.pdf")));
var locations = BruteForceSearcher.GetObjectLocations(bytes);
Assert.Equal(13, locations.Count);
Assert.Equal(6183, locations[new IndirectReference(1, 0)]);
Assert.Equal(244, locations[new IndirectReference(2, 0)]);
Assert.Equal(15, locations[new IndirectReference(3, 0)]);
Assert.Equal(222, locations[new IndirectReference(4, 0)]);
Assert.Equal(5766, locations[new IndirectReference(5, 0)]);
Assert.Equal(353, locations[new IndirectReference(6, 0)]);
Assert.Equal(581, locations[new IndirectReference(7, 0)]);
Assert.Equal(5068, locations[new IndirectReference(8, 0)]);
Assert.Equal(5091, locations[new IndirectReference(9, 0)]);
var s = GetStringAt(bytes, locations[new IndirectReference(3, 0)]);
Assert.StartsWith("3 0 obj", s);
}
[Fact]
public void BruteForceSearcherFileOffsetsCorrectOpenOffice()
{

View File

@@ -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,102 @@
}
}
[Fact]
public void CanWriteSinglePageWithJpeg()
{
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(CanWriteSinglePageWithJpeg), 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);
}
}
[Fact]
public void CanWrite2PagesSharingJpeg()
{
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 expectedBounds1 = new PdfRectangle(25, page.PageSize.Height - 300, 200, page.PageSize.Height - 200);
var imageBytes = File.ReadAllBytes(img);
var expectedBounds2 = new PdfRectangle(25, 600, 75, 650);
var jpeg = page.AddJpeg(imageBytes, expectedBounds1);
page.AddJpeg(jpeg, expectedBounds2);
var expectedBounds3 = new PdfRectangle(30, 500, 130, 550);
var page2 = builder.AddPage(PageSize.A4);
page2.AddJpeg(jpeg, expectedBounds3);
var bytes = builder.Build();
WriteFile(nameof(CanWrite2PagesSharingJpeg), bytes);
using (var document = PdfDocument.Open(bytes))
{
var page1 = document.GetPage(1);
Assert.Equal("Smile", page1.Text);
var page1Images = page1.GetImages().ToList();
Assert.Equal(2, page1Images.Count);
var image1 = page1Images[0];
Assert.Equal(expectedBounds1, image1.Bounds);
var image2 = page1Images[1];
Assert.Equal(expectedBounds2, image2.Bounds);
var page2Doc = document.GetPage(2);
var image3 = Assert.Single(page2Doc.GetImages());
Assert.NotNull(image3);
Assert.Equal(expectedBounds3, image3.Bounds);
Assert.Equal(imageBytes, image1.RawBytes);
Assert.Equal(imageBytes, image2.RawBytes);
Assert.Equal(imageBytes, image3.RawBytes);
}
}
private static void WriteFile(string name, byte[] bytes)
{
try

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net45;net451;net452;net46;net461;net462;net47</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Version>0.1.1-alpha001</Version>
<Version>0.1.1</Version>
<IsTestProject>False</IsTestProject>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

View File

@@ -278,6 +278,9 @@
public static readonly NameToken If = new NameToken("IF");
public static readonly NameToken Im = new NameToken("IM");
public static readonly NameToken Image = new NameToken("Image");
public static readonly NameToken ImageB = new NameToken("ImageB");
public static readonly NameToken ImageC = new NameToken("ImageC");
public static readonly NameToken ImageI = new NameToken("ImageI");
public static readonly NameToken ImageMask = new NameToken("ImageMask");
public static readonly NameToken Index = new NameToken("Index");
public static readonly NameToken Indexed = new NameToken("Indexed");

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net45;net451;net452;net46;net461;net462;net47</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Version>0.1.1-alpha001</Version>
<Version>0.1.1</Version>
<IsTestProject>False</IsTestProject>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

View File

@@ -44,7 +44,7 @@
/// <inheritdoc />
public void Write(Stream stream)
{
stream.WriteText($"/{Name}");
stream.WriteText($"/{Name.Data}");
stream.WriteWhiteSpace();
stream.WriteText(Symbol);
stream.WriteNewLine();

View 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 height = ReadShort(stream, shortBuffer);
var width = 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]);
}
}
}

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

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

View File

@@ -42,6 +42,8 @@
var currentlyInObject = false;
var objBuffer = new byte[4];
do
{
if (loopProtection > 1_000_000)
@@ -94,17 +96,24 @@
bytes.Seek(currentOffset);
if (!ReadHelper.IsString(bytes, " obj"))
bytes.Read(objBuffer);
if (!IsStartObjMarker(objBuffer))
{
currentOffset++;
continue;
}
// Current byte is ' '[obj]
var offset = currentOffset - 1;
var offset = currentOffset + 1;
bytes.Seek(offset);
while (ReadHelper.IsWhitespace(bytes.CurrentByte) && offset >= MinimumSearchOffset)
{
bytes.Seek(--offset);
}
while (ReadHelper.IsDigit(bytes.CurrentByte) && offset >= MinimumSearchOffset)
{
generationBytes.Insert(0, (char)bytes.CurrentByte);
@@ -113,13 +122,16 @@
}
// We should now be at the space between object and generation number.
if (!ReadHelper.IsSpace(bytes.CurrentByte))
if (!ReadHelper.IsWhitespace(bytes.CurrentByte))
{
currentOffset++;
continue;
}
bytes.Seek(--offset);
while (ReadHelper.IsWhitespace(bytes.CurrentByte))
{
bytes.Seek(--offset);
}
while (ReadHelper.IsDigit(bytes.CurrentByte) && offset >= MinimumSearchOffset)
{
@@ -185,5 +197,17 @@
bytes.Seek(originalOffset);
return long.MaxValue;
}
private static bool IsStartObjMarker(byte[] data)
{
if (!ReadHelper.IsWhitespace(data[0]))
{
return false;
}
return (data[1] == 'o' || data[1] == 'O')
&& (data[2] == 'b' || data[2] == 'B')
&& (data[3] == 'j' || data[3] == 'J');
}
}
}

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net45;net451;net452;net46;net461;net462;net47</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Version>0.1.1-alpha001</Version>
<Version>0.1.1</Version>
<IsTestProject>False</IsTestProject>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

View File

@@ -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,9 +240,25 @@
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 procSet = new List<NameToken>
{
NameToken.Create("PDF"),
NameToken.Text,
NameToken.ImageB,
NameToken.ImageC,
NameToken.ImageI
};
var resources = new Dictionary<NameToken, IToken>
{
{ NameToken.ProcSet, new ArrayToken(new []{ NameToken.Create("PDF"), NameToken.Create("Text") }) }
{ NameToken.ProcSet, new ArrayToken(procSet) }
};
if (fontsWritten.Count > 0)
@@ -249,17 +277,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 +310,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 +399,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"/>.

View File

@@ -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,103 @@
return letters;
}
/// <summary>
/// Adds the JPEG image represented by the input bytes at the specified location.
/// </summary>
public AddedImage AddJpeg(byte[] fileBytes, PdfRectangle placementRectangle)
{
using (var stream = new MemoryStream(fileBytes))
{
return AddJpeg(stream, placementRectangle);
}
}
/// <summary>
/// Adds the JPEG image represented by the input stream at the specified location.
/// </summary>
public AddedImage 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);
return new AddedImage(reference, info.Width, info.Height);
}
/// <summary>
/// Adds the JPEG image previously added using <see cref="AddJpeg(byte[],PdfRectangle)"/>,
/// this will share the same image data to prevent duplication.
/// </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)
{
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(image.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;
@@ -343,5 +448,43 @@
Operations = operations;
}
}
/// <summary>
/// A key representing an image available to use for the current document builder.
/// Create it by adding an image to a page using <see cref="AddJpeg(byte[],PdfRectangle)"/>.
/// </summary>
public class AddedImage
{
/// <summary>
/// The Id uniquely identifying this image on the builder.
/// </summary>
internal Guid Id { get; }
/// <summary>
/// The reference to the stored image XObject.
/// </summary>
internal IndirectReference Reference { get; }
/// <summary>
/// The width of the raw image in pixels.
/// </summary>
public int Width { get; }
/// <summary>
/// The height of the raw image in pixels.
/// </summary>
public int Height { get; }
/// <summary>
/// Create a new <see cref="AddedImage"/>.
/// </summary>
internal AddedImage(IndirectReference reference, int width, int height)
{
Id = Guid.NewGuid();
Reference = reference;
Width = width;
Height = height;
}
}
}
}

View File

@@ -12,9 +12,9 @@
<PackageTags>PDF;Reader;Document;Adobe;PDFBox;PdfPig;pdf-extract</PackageTags>
<RepositoryUrl>https://github.com/UglyToad/PdfPig</RepositoryUrl>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<Version>0.1.1-alpha001</Version>
<AssemblyVersion>0.1.0.0</AssemblyVersion>
<FileVersion>0.1.0.0</FileVersion>
<Version>0.1.1</Version>
<AssemblyVersion>0.1.1.0</AssemblyVersion>
<FileVersion>0.1.1.0</FileVersion>
<PackageIconUrl>https://raw.githubusercontent.com/UglyToad/PdfPig/master/documentation/pdfpig.png</PackageIconUrl>
<PackageIcon>pdfpig.png</PackageIcon>
<Product>PdfPig</Product>