mirror of
https://github.com/UglyToad/PdfPig.git
synced 2026-03-10 00:23:29 +08:00
refactored previous work to fit pr #250
This commit is contained in:
@@ -209,6 +209,7 @@
|
|||||||
"UglyToad.PdfPig.Writer.PdfAStandard",
|
"UglyToad.PdfPig.Writer.PdfAStandard",
|
||||||
"UglyToad.PdfPig.Writer.PdfDocumentBuilder",
|
"UglyToad.PdfPig.Writer.PdfDocumentBuilder",
|
||||||
"UglyToad.PdfPig.Writer.PdfMerger",
|
"UglyToad.PdfPig.Writer.PdfMerger",
|
||||||
|
"UglyToad.PdfPig.Writer.PdfWriterType",
|
||||||
"UglyToad.PdfPig.Writer.PdfPageBuilder",
|
"UglyToad.PdfPig.Writer.PdfPageBuilder",
|
||||||
"UglyToad.PdfPig.Writer.TokenWriter",
|
"UglyToad.PdfPig.Writer.TokenWriter",
|
||||||
"UglyToad.PdfPig.XObjects.XObjectImage"
|
"UglyToad.PdfPig.XObjects.XObjectImage"
|
||||||
|
|||||||
@@ -45,6 +45,11 @@ namespace UglyToad.PdfPig.Tests.Tokens
|
|||||||
return Objects[reference];
|
return Objects[reference];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ReplaceToken(IndirectReference reference, IToken token)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -691,6 +691,186 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanAddHelloWorldToSimplePage()
|
||||||
|
{
|
||||||
|
var path = IntegrationHelpers.GetDocumentPath("Single Page Simple - from open office.pdf");
|
||||||
|
var doc = PdfDocument.Open(path);
|
||||||
|
var builder = new PdfDocumentBuilder();
|
||||||
|
|
||||||
|
var page = builder.AddPage(doc, 1);
|
||||||
|
|
||||||
|
page.DrawLine(new PdfPoint(30, 520), new PdfPoint(360, 520));
|
||||||
|
page.DrawLine(new PdfPoint(360, 520), new PdfPoint(360, 250));
|
||||||
|
|
||||||
|
page.SetStrokeColor(250, 132, 131);
|
||||||
|
page.DrawLine(new PdfPoint(25, 70), new PdfPoint(100, 70), 3);
|
||||||
|
page.ResetColor();
|
||||||
|
page.DrawRectangle(new PdfPoint(30, 200), 250, 100, 0.5m);
|
||||||
|
page.DrawRectangle(new PdfPoint(30, 100), 250, 100, 0.5m);
|
||||||
|
|
||||||
|
var file = TrueTypeTestHelper.GetFileBytes("Andada-Regular.ttf");
|
||||||
|
|
||||||
|
var font = builder.AddTrueTypeFont(file);
|
||||||
|
|
||||||
|
var letters = page.AddText("Hello World!", 12, new PdfPoint(30, 50), font);
|
||||||
|
|
||||||
|
Assert.NotEmpty(page.CurrentStream.Operations);
|
||||||
|
|
||||||
|
var b = builder.Build();
|
||||||
|
|
||||||
|
WriteFile(nameof(CanWriteSinglePageHelloWorld), b);
|
||||||
|
|
||||||
|
Assert.NotEmpty(b);
|
||||||
|
|
||||||
|
using (var document = PdfDocument.Open(b))
|
||||||
|
{
|
||||||
|
var page1 = document.GetPage(1);
|
||||||
|
|
||||||
|
Assert.Equal("I am a simple pdf.Hello World!", page1.Text);
|
||||||
|
|
||||||
|
var h = page1.Letters[18];
|
||||||
|
|
||||||
|
Assert.Equal("H", h.Value);
|
||||||
|
Assert.Equal("Andada-Regular", h.FontName);
|
||||||
|
|
||||||
|
var comparer = new DoubleComparer(0.01);
|
||||||
|
var pointComparer = new PointComparer(comparer);
|
||||||
|
|
||||||
|
for (int i = 0; i < letters.Count; i++)
|
||||||
|
{
|
||||||
|
var readerLetter = page1.Letters[i+18];
|
||||||
|
var writerLetter = letters[i];
|
||||||
|
|
||||||
|
Assert.Equal(readerLetter.Value, writerLetter.Value);
|
||||||
|
Assert.Equal(readerLetter.Location, writerLetter.Location, pointComparer);
|
||||||
|
Assert.Equal(readerLetter.FontSize, writerLetter.FontSize, comparer);
|
||||||
|
Assert.Equal(readerLetter.GlyphRectangle.Width, writerLetter.GlyphRectangle.Width, comparer);
|
||||||
|
Assert.Equal(readerLetter.GlyphRectangle.Height, writerLetter.GlyphRectangle.Height, comparer);
|
||||||
|
Assert.Equal(readerLetter.GlyphRectangle.BottomLeft, writerLetter.GlyphRectangle.BottomLeft, pointComparer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanMerge2SimpleDocumentsReversed_Builder()
|
||||||
|
{
|
||||||
|
var one = IntegrationHelpers.GetDocumentPath("Single Page Simple - from open office.pdf");
|
||||||
|
var two = IntegrationHelpers.GetDocumentPath("Single Page Simple - from inkscape.pdf");
|
||||||
|
|
||||||
|
using var docOne = PdfDocument.Open(one);
|
||||||
|
using var docTwo = PdfDocument.Open(two);
|
||||||
|
var builder = new PdfDocumentBuilder();
|
||||||
|
builder.AddPage(docOne, 1);
|
||||||
|
builder.AddPage(docTwo, 1);
|
||||||
|
var result = builder.Build();
|
||||||
|
PdfMergerTests.CanMerge2SimpleDocumentsAssertions(new MemoryStream(result), "I am a simple pdf.", "Write something inInkscape", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanMerge2SimpleDocuments_Builder()
|
||||||
|
{
|
||||||
|
var one = IntegrationHelpers.GetDocumentPath("Single Page Simple - from inkscape.pdf");
|
||||||
|
var two = IntegrationHelpers.GetDocumentPath("Single Page Simple - from open office.pdf");
|
||||||
|
|
||||||
|
using var docOne = PdfDocument.Open(one);
|
||||||
|
using var docTwo = PdfDocument.Open(two);
|
||||||
|
var builder = new PdfDocumentBuilder();
|
||||||
|
builder.AddPage(docOne, 1);
|
||||||
|
builder.AddPage(docTwo, 1);
|
||||||
|
var result = builder.Build();
|
||||||
|
PdfMergerTests.CanMerge2SimpleDocumentsAssertions(new MemoryStream(result), "Write something inInkscape", "I am a simple pdf.", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanDedupObjectsFromSameDoc_Builder()
|
||||||
|
{
|
||||||
|
var one = IntegrationHelpers.GetDocumentPath("Multiple Page - from Mortality Statistics.pdf");
|
||||||
|
|
||||||
|
using var doc = PdfDocument.Open(one);
|
||||||
|
|
||||||
|
using var builder = new PdfDocumentBuilder();
|
||||||
|
builder.AddPage(doc, 1);
|
||||||
|
builder.AddPage(doc, 1);
|
||||||
|
|
||||||
|
var result = builder.Build();
|
||||||
|
|
||||||
|
using (var document = PdfDocument.Open(result, ParsingOptions.LenientParsingOff))
|
||||||
|
{
|
||||||
|
Assert.Equal(2, document.NumberOfPages);
|
||||||
|
Assert.True(document.Structure.CrossReferenceTable.ObjectOffsets.Count <= 29,
|
||||||
|
"Expected object count to be lower than 30"); // 45 objects with duplicates, 29 with correct re-use
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanDedupObjectsFromDifferentDoc_HashBuilder()
|
||||||
|
{
|
||||||
|
var one = IntegrationHelpers.GetDocumentPath("Multiple Page - from Mortality Statistics.pdf");
|
||||||
|
|
||||||
|
using var doc = PdfDocument.Open(one);
|
||||||
|
using var doc2 = PdfDocument.Open(one);
|
||||||
|
|
||||||
|
using var builder = new PdfDocumentBuilder(new MemoryStream(), true, PdfWriterType.ObjectInMemoryDedup);
|
||||||
|
builder.AddPage(doc, 1);
|
||||||
|
builder.AddPage(doc2, 1);
|
||||||
|
|
||||||
|
var result = builder.Build();
|
||||||
|
|
||||||
|
using (var document = PdfDocument.Open(result, ParsingOptions.LenientParsingOff))
|
||||||
|
{
|
||||||
|
Assert.Equal(2, document.NumberOfPages);
|
||||||
|
Assert.True(document.Structure.CrossReferenceTable.ObjectOffsets.Count <= 29,
|
||||||
|
"Expected object count to be lower than 30"); // 45 objects with duplicates, 29 with correct re-use
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[InlineData("Single Page Simple - from google drive.pdf")]
|
||||||
|
[InlineData("Old Gutnish Internet Explorer.pdf")]
|
||||||
|
[InlineData("68-1990-01_A.pdf")]
|
||||||
|
[InlineData("Multiple Page - from Mortality Statistics.pdf")]
|
||||||
|
[Theory]
|
||||||
|
public void CopiedPagesResultInSameData(string name)
|
||||||
|
{
|
||||||
|
var docPath = IntegrationHelpers.GetDocumentPath(name);
|
||||||
|
|
||||||
|
using var doc = PdfDocument.Open(docPath, ParsingOptions.LenientParsingOff);
|
||||||
|
var count1 = GetCounts(doc);
|
||||||
|
|
||||||
|
using var builder = new PdfDocumentBuilder();
|
||||||
|
for (var i = 1; i <= doc.NumberOfPages; i++)
|
||||||
|
{
|
||||||
|
builder.AddPage(doc, i);
|
||||||
|
}
|
||||||
|
var result = builder.Build();
|
||||||
|
|
||||||
|
using (var doc2 = PdfDocument.Open(result, ParsingOptions.LenientParsingOff))
|
||||||
|
{
|
||||||
|
var count2 = GetCounts(doc2);
|
||||||
|
Assert.Equal(count1.Item1, count2.Item1);
|
||||||
|
Assert.Equal(count1.Item2, count2.Item2);
|
||||||
|
}
|
||||||
|
|
||||||
|
(int, double) GetCounts(PdfDocument toCount)
|
||||||
|
{
|
||||||
|
int letters = 0;
|
||||||
|
double location = 0;
|
||||||
|
foreach (var page in toCount.GetPages())
|
||||||
|
{
|
||||||
|
foreach (var letter in page.Letters)
|
||||||
|
{
|
||||||
|
unchecked { letters += 1; }
|
||||||
|
unchecked {
|
||||||
|
location += letter.Location.X;
|
||||||
|
location += letter.Location.Y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (letters, location);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void WriteFile(string name, byte[] bytes, string extension = "pdf")
|
private static void WriteFile(string name, byte[] bytes, string extension = "pdf")
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -47,13 +47,16 @@
|
|||||||
CanMerge2SimpleDocumentsAssertions(new MemoryStream(result), "I am a simple pdf.", "Write something inInkscape");
|
CanMerge2SimpleDocumentsAssertions(new MemoryStream(result), "I am a simple pdf.", "Write something inInkscape");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CanMerge2SimpleDocumentsAssertions(Stream stream, string page1Text, string page2Text)
|
internal static void CanMerge2SimpleDocumentsAssertions(Stream stream, string page1Text, string page2Text, bool checkVersion=true)
|
||||||
{
|
{
|
||||||
stream.Position = 0;
|
stream.Position = 0;
|
||||||
using (var document = PdfDocument.Open(stream, ParsingOptions.LenientParsingOff))
|
using (var document = PdfDocument.Open(stream, ParsingOptions.LenientParsingOff))
|
||||||
{
|
{
|
||||||
Assert.Equal(2, document.NumberOfPages);
|
Assert.Equal(2, document.NumberOfPages);
|
||||||
|
if (checkVersion)
|
||||||
|
{
|
||||||
Assert.Equal(1.5m, document.Version);
|
Assert.Equal(1.5m, document.Version);
|
||||||
|
}
|
||||||
|
|
||||||
var page1 = document.GetPage(1);
|
var page1 = document.GetPage(1);
|
||||||
Assert.Equal(page1Text, page1.Text);
|
Assert.Equal(page1Text, page1.Text);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Content;
|
using Content;
|
||||||
|
using Core;
|
||||||
using Filters;
|
using Filters;
|
||||||
using Parser.Parts;
|
using Parser.Parts;
|
||||||
using Tokenization.Scanner;
|
using Tokenization.Scanner;
|
||||||
@@ -82,6 +83,30 @@
|
|||||||
return embeddedFiles.Count > 0;
|
return embeddedFiles.Count > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Replaces the token in an internal cache that will be returned instead of
|
||||||
|
/// scanning the source PDF data for future requests.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reference">The object number for the object to replace.</param>
|
||||||
|
/// <param name="replacer">Func that takes existing token as input and return new token.</param>
|
||||||
|
public void ReplaceIndirectObject(IndirectReference reference, Func<IToken, IToken> replacer)
|
||||||
|
{
|
||||||
|
var obj = pdfScanner.Get(reference);
|
||||||
|
var replacement = replacer(obj.Data);
|
||||||
|
pdfScanner.ReplaceToken(reference, replacement);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Replaces the token in an internal cache that will be returned instead of
|
||||||
|
/// scanning the source PDF data for future requests.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reference">The object number for the object to replace.</param>
|
||||||
|
/// <param name="replacement">Replacement token to use.</param>
|
||||||
|
public void ReplaceIndirectObject(IndirectReference reference, IToken replacement)
|
||||||
|
{
|
||||||
|
pdfScanner.ReplaceToken(reference, replacement);
|
||||||
|
}
|
||||||
|
|
||||||
private void GuardDisposed()
|
private void GuardDisposed()
|
||||||
{
|
{
|
||||||
if (isDisposed)
|
if (isDisposed)
|
||||||
|
|||||||
@@ -16,5 +16,13 @@
|
|||||||
/// <param name="reference">The object number for the object to tokenize.</param>
|
/// <param name="reference">The object number for the object to tokenize.</param>
|
||||||
/// <returns>The tokenized object.</returns>
|
/// <returns>The tokenized object.</returns>
|
||||||
ObjectToken Get(IndirectReference reference);
|
ObjectToken Get(IndirectReference reference);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds the token to an internal cache that will be returned instead of
|
||||||
|
/// scanning the source PDF data.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reference">The object number for the object to replace.</param>
|
||||||
|
/// <param name="token">The token to replace the existing data.</param>
|
||||||
|
void ReplaceToken(IndirectReference reference, IToken token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,6 +29,9 @@
|
|||||||
private bool isDisposed;
|
private bool isDisposed;
|
||||||
private bool isBruteForcing;
|
private bool isBruteForcing;
|
||||||
|
|
||||||
|
private readonly Dictionary<IndirectReference, ObjectToken> overwrittenTokens =
|
||||||
|
new Dictionary<IndirectReference, ObjectToken>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stores tokens encountered between obj - endobj markers for each <see cref="MoveNext"/> call.
|
/// Stores tokens encountered between obj - endobj markers for each <see cref="MoveNext"/> call.
|
||||||
/// Cleared after each operation.
|
/// Cleared after each operation.
|
||||||
@@ -670,6 +673,11 @@
|
|||||||
throw new ObjectDisposedException(nameof(PdfTokenScanner));
|
throw new ObjectDisposedException(nameof(PdfTokenScanner));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (overwrittenTokens.TryGetValue(reference, out var value))
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
if (objectLocationProvider.TryGetCached(reference, out var objectToken))
|
if (objectLocationProvider.TryGetCached(reference, out var objectToken))
|
||||||
{
|
{
|
||||||
return objectToken;
|
return objectToken;
|
||||||
@@ -705,6 +713,13 @@
|
|||||||
return BruteForceFileToFindReference(reference);
|
return BruteForceFileToFindReference(reference);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ReplaceToken(IndirectReference reference, IToken token)
|
||||||
|
{
|
||||||
|
// Using 0 position as it isn't written to stream and this value doesn't
|
||||||
|
// seem to be used by any callers. In future may need to revisit this.
|
||||||
|
overwrittenTokens[reference] = new ObjectToken(0, reference, token);
|
||||||
|
}
|
||||||
|
|
||||||
private ObjectToken BruteForceFileToFindReference(IndirectReference reference)
|
private ObjectToken BruteForceFileToFindReference(IndirectReference reference)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
private const string SrgbIec61966OutputCondition = "sRGB IEC61966-2.1";
|
private const string SrgbIec61966OutputCondition = "sRGB IEC61966-2.1";
|
||||||
private const string RegistryName = "http://www.color.org";
|
private const string RegistryName = "http://www.color.org";
|
||||||
|
|
||||||
public static ArrayToken GetOutputIntentsArray(Func<IToken, ObjectToken> objectWriter)
|
public static ArrayToken GetOutputIntentsArray(Func<IToken, IndirectReferenceToken> objectWriter)
|
||||||
{
|
{
|
||||||
var rgbColorCondition = new StringToken(SrgbIec61966OutputCondition);
|
var rgbColorCondition = new StringToken(SrgbIec61966OutputCondition);
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
{NameToken.OutputConditionIdentifier, rgbColorCondition},
|
{NameToken.OutputConditionIdentifier, rgbColorCondition},
|
||||||
{NameToken.RegistryName, new StringToken(RegistryName)},
|
{NameToken.RegistryName, new StringToken(RegistryName)},
|
||||||
{NameToken.Info, rgbColorCondition},
|
{NameToken.Info, rgbColorCondition},
|
||||||
{NameToken.DestOutputProfile, new IndirectReferenceToken(written.Number)}
|
{NameToken.DestOutputProfile, written}
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
TransformationMatrix GetFontMatrix();
|
TransformationMatrix GetFontMatrix();
|
||||||
|
|
||||||
ObjectToken WriteFont(NameToken fontKeyName, Stream outputStream, BuilderContext context);
|
IndirectReferenceToken WriteFont(IPdfStreamWriter writer, NameToken fontKeyName);
|
||||||
|
|
||||||
byte GetValueForCharacter(char character);
|
byte GetValueForCharacter(char character);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
return TransformationMatrix.FromValues(1/1000.0, 0, 0, 1/1000.0, 0, 0);
|
return TransformationMatrix.FromValues(1/1000.0, 0, 0, 1/1000.0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObjectToken WriteFont(NameToken fontKeyName, Stream outputStream, BuilderContext context)
|
public IndirectReferenceToken WriteFont(IPdfStreamWriter writer, NameToken fontKeyName)
|
||||||
{
|
{
|
||||||
var dictionary = new Dictionary<NameToken, IToken>
|
var dictionary = new Dictionary<NameToken, IToken>
|
||||||
{
|
{
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
|
|
||||||
var token = new DictionaryToken(dictionary);
|
var token = new DictionaryToken(dictionary);
|
||||||
|
|
||||||
var result = context.WriteObject(outputStream, token);
|
var result = writer.WriteToken(token);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -93,45 +93,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class BuilderContext
|
|
||||||
{
|
|
||||||
private readonly List<int> reservedNumbers = new List<int>();
|
|
||||||
|
|
||||||
public int CurrentNumber { get; private set; } = 1;
|
|
||||||
|
|
||||||
private readonly Dictionary<IndirectReference, long> objectOffsets = new Dictionary<IndirectReference, long>();
|
|
||||||
public IReadOnlyDictionary<IndirectReference, long> ObjectOffsets => objectOffsets;
|
|
||||||
|
|
||||||
public ObjectToken WriteObject(Stream stream, IToken token, int? reservedNumber = null)
|
|
||||||
{
|
|
||||||
int number;
|
|
||||||
if (reservedNumber.HasValue)
|
|
||||||
{
|
|
||||||
if (!reservedNumbers.Remove(reservedNumber.Value))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
number = reservedNumber.Value;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
number = CurrentNumber++;
|
|
||||||
}
|
|
||||||
|
|
||||||
var reference = new IndirectReference(number, 0);
|
|
||||||
var obj = new ObjectToken(stream.Position, reference, token);
|
|
||||||
objectOffsets.Add(reference, obj.Position);
|
|
||||||
TokenWriter.WriteToken(obj, stream);
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int ReserveNumber()
|
|
||||||
{
|
|
||||||
var reserved = CurrentNumber;
|
|
||||||
reservedNumbers.Add(reserved);
|
|
||||||
CurrentNumber++;
|
|
||||||
return reserved;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -47,14 +47,14 @@
|
|||||||
return TransformationMatrix.FromValues(1.0 / unitsPerEm, 0, 0, 1.0 / unitsPerEm, 0, 0);
|
return TransformationMatrix.FromValues(1.0 / unitsPerEm, 0, 0, 1.0 / unitsPerEm, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObjectToken WriteFont(NameToken fontKeyName, Stream outputStream, BuilderContext context)
|
public IndirectReferenceToken WriteFont(IPdfStreamWriter writer, NameToken fontKeyName)
|
||||||
{
|
{
|
||||||
var newEncoding = new TrueTypeSubsetEncoding(characterMapping.Keys.ToList());
|
var newEncoding = new TrueTypeSubsetEncoding(characterMapping.Keys.ToList());
|
||||||
var subsetBytes = TrueTypeSubsetter.Subset(fontFileBytes.ToArray(), newEncoding);
|
var subsetBytes = TrueTypeSubsetter.Subset(fontFileBytes.ToArray(), newEncoding);
|
||||||
|
|
||||||
var embeddedFile = DataCompresser.CompressToStream(subsetBytes);
|
var embeddedFile = DataCompresser.CompressToStream(subsetBytes);
|
||||||
|
|
||||||
var fileRef = context.WriteObject(outputStream, embeddedFile);
|
var fileRef = writer.WriteToken(embeddedFile);
|
||||||
|
|
||||||
var baseFont = NameToken.Create(font.TableRegister.NameTable.GetPostscriptName());
|
var baseFont = NameToken.Create(font.TableRegister.NameTable.GetPostscriptName());
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
{ NameToken.Descent, new NumericToken(Math.Round(hhead.Descent * scaling, 2)) },
|
{ NameToken.Descent, new NumericToken(Math.Round(hhead.Descent * scaling, 2)) },
|
||||||
{ NameToken.CapHeight, new NumericToken(90) },
|
{ NameToken.CapHeight, new NumericToken(90) },
|
||||||
{ NameToken.StemV, new NumericToken(90) },
|
{ NameToken.StemV, new NumericToken(90) },
|
||||||
{ NameToken.FontFile2, new IndirectReferenceToken(fileRef.Number) }
|
{ NameToken.FontFile2, fileRef }
|
||||||
};
|
};
|
||||||
|
|
||||||
var os2 = font.TableRegister.Os2Table;
|
var os2 = font.TableRegister.Os2Table;
|
||||||
@@ -108,27 +108,27 @@
|
|||||||
widths.Add(new NumericToken(width));
|
widths.Add(new NumericToken(width));
|
||||||
}
|
}
|
||||||
|
|
||||||
var descriptor = context.WriteObject(outputStream, new DictionaryToken(descriptorDictionary));
|
var descriptor = writer.WriteToken(new DictionaryToken(descriptorDictionary));
|
||||||
|
|
||||||
var toUnicodeCMap = ToUnicodeCMapBuilder.ConvertToCMapStream(characterMapping);
|
var toUnicodeCMap = ToUnicodeCMapBuilder.ConvertToCMapStream(characterMapping);
|
||||||
var toUnicodeStream = DataCompresser.CompressToStream(toUnicodeCMap);
|
var toUnicodeStream = DataCompresser.CompressToStream(toUnicodeCMap);
|
||||||
var toUnicode = context.WriteObject(outputStream, toUnicodeStream);
|
var toUnicode = writer.WriteToken(toUnicodeStream);
|
||||||
|
|
||||||
var dictionary = new Dictionary<NameToken, IToken>
|
var dictionary = new Dictionary<NameToken, IToken>
|
||||||
{
|
{
|
||||||
{ NameToken.Type, NameToken.Font },
|
{ NameToken.Type, NameToken.Font },
|
||||||
{ NameToken.Subtype, NameToken.TrueType },
|
{ NameToken.Subtype, NameToken.TrueType },
|
||||||
{ NameToken.BaseFont, baseFont },
|
{ NameToken.BaseFont, baseFont },
|
||||||
{ NameToken.FontDescriptor, new IndirectReferenceToken(descriptor.Number) },
|
{ NameToken.FontDescriptor, descriptor },
|
||||||
{ NameToken.FirstChar, new NumericToken(0) },
|
{ NameToken.FirstChar, new NumericToken(0) },
|
||||||
{ NameToken.LastChar, new NumericToken(lastCharacter) },
|
{ NameToken.LastChar, new NumericToken(lastCharacter) },
|
||||||
{ NameToken.Widths, new ArrayToken(widths) },
|
{ NameToken.Widths, new ArrayToken(widths) },
|
||||||
{NameToken.ToUnicode, new IndirectReferenceToken(toUnicode.Number) }
|
{NameToken.ToUnicode, toUnicode }
|
||||||
};
|
};
|
||||||
|
|
||||||
var token = new DictionaryToken(dictionary);
|
var token = new DictionaryToken(dictionary);
|
||||||
|
|
||||||
var result = context.WriteObject(outputStream, token);
|
var result = writer.WriteToken(token);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
51
src/UglyToad.PdfPig/Writer/IPdfStreamWriter.cs
Normal file
51
src/UglyToad.PdfPig/Writer/IPdfStreamWriter.cs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
namespace UglyToad.PdfPig.Writer
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using Tokens;
|
||||||
|
|
||||||
|
internal interface IPdfStreamWriter : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
Stream Stream { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
IndirectReferenceToken WriteToken(IToken token);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
/// <param name="indirectReference"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
IndirectReferenceToken WriteToken(IToken token, IndirectReferenceToken indirectReference);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
IndirectReferenceToken ReserveObjectNumber();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="version"></param>
|
||||||
|
void InitializePdf(decimal version);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="catalogReference"></param>
|
||||||
|
/// <param name="documentInformationReference"></param>
|
||||||
|
void CompletePdf(IndirectReferenceToken catalogReference, IndirectReferenceToken documentInformationReference=null);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,14 +8,14 @@ namespace UglyToad.PdfPig.Writer
|
|||||||
{
|
{
|
||||||
internal static class PdfABaselineRuleBuilder
|
internal static class PdfABaselineRuleBuilder
|
||||||
{
|
{
|
||||||
public static void Obey(Dictionary<NameToken, IToken> catalog, Func<IToken, ObjectToken> writerFunc,
|
public static void Obey(Dictionary<NameToken, IToken> catalog, Func<IToken, IndirectReferenceToken> writerFunc,
|
||||||
PdfDocumentBuilder.DocumentInformationBuilder documentInformationBuilder,
|
PdfDocumentBuilder.DocumentInformationBuilder documentInformationBuilder,
|
||||||
PdfAStandard archiveStandard)
|
PdfAStandard archiveStandard)
|
||||||
{
|
{
|
||||||
catalog[NameToken.OutputIntents] = OutputIntentsFactory.GetOutputIntentsArray(writerFunc);
|
catalog[NameToken.OutputIntents] = OutputIntentsFactory.GetOutputIntentsArray(writerFunc);
|
||||||
var xmpStream = XmpWriter.GenerateXmpStream(documentInformationBuilder, 1.7m, archiveStandard);
|
var xmpStream = XmpWriter.GenerateXmpStream(documentInformationBuilder, 1.7m, archiveStandard);
|
||||||
var xmpObj = writerFunc(xmpStream);
|
var xmpObj = writerFunc(xmpStream);
|
||||||
catalog[NameToken.Metadata] = new IndirectReferenceToken(xmpObj.Number);
|
catalog[NameToken.Metadata] = xmpObj;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
200
src/UglyToad.PdfPig/Writer/PdfDedupStreamWriter.cs
Normal file
200
src/UglyToad.PdfPig/Writer/PdfDedupStreamWriter.cs
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
namespace UglyToad.PdfPig.Writer
|
||||||
|
{
|
||||||
|
using Core;
|
||||||
|
using Graphics.Operations;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using Tokens;
|
||||||
|
|
||||||
|
internal class PdfDedupStreamWriter : IPdfStreamWriter
|
||||||
|
{
|
||||||
|
|
||||||
|
public Stream Stream { get; }
|
||||||
|
private int CurrentNumber { get; set; } = 1;
|
||||||
|
private bool DisposeStream { get; set; }
|
||||||
|
private const decimal DefaultVersion = 1.2m;
|
||||||
|
private bool Initialized { get; set; }
|
||||||
|
private readonly Dictionary<IndirectReference, long> offsets = new Dictionary<IndirectReference, long>();
|
||||||
|
private readonly Dictionary<byte[], IndirectReferenceToken> hashes = new (new FNVByteComparison());
|
||||||
|
|
||||||
|
public PdfDedupStreamWriter(Stream stream, bool dispose)
|
||||||
|
{
|
||||||
|
Stream = stream;
|
||||||
|
DisposeStream = dispose;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MemoryStream ms = new MemoryStream();
|
||||||
|
public IndirectReferenceToken WriteToken(IToken token)
|
||||||
|
{
|
||||||
|
if (!Initialized)
|
||||||
|
{
|
||||||
|
InitializePdf(DefaultVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
ms.SetLength(0);
|
||||||
|
TokenWriter.WriteToken(token, ms);
|
||||||
|
var contents = ms.ToArray();
|
||||||
|
if (hashes.TryGetValue(contents, out var value))
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ir = ReserveObjectNumber();
|
||||||
|
hashes.Add(contents, ir);
|
||||||
|
|
||||||
|
offsets.Add(ir.Data, Stream.Position);
|
||||||
|
TokenWriter.WriteObject(ir.Data.ObjectNumber, ir.Data.Generation, contents, Stream);
|
||||||
|
|
||||||
|
return ir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IndirectReferenceToken WriteToken(IToken token, IndirectReferenceToken indirectReference)
|
||||||
|
{
|
||||||
|
if (!Initialized)
|
||||||
|
{
|
||||||
|
InitializePdf(DefaultVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
ms.SetLength(0);
|
||||||
|
TokenWriter.WriteToken(token, ms);
|
||||||
|
var contents = ms.ToArray();
|
||||||
|
|
||||||
|
hashes.Add(contents, indirectReference);
|
||||||
|
offsets.Add(indirectReference.Data, Stream.Position);
|
||||||
|
TokenWriter.WriteObject(indirectReference.Data.ObjectNumber, indirectReference.Data.Generation, contents, Stream);
|
||||||
|
return indirectReference;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IndirectReferenceToken ReserveObjectNumber()
|
||||||
|
{
|
||||||
|
return new IndirectReferenceToken(new IndirectReference(CurrentNumber++, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InitializePdf(decimal version)
|
||||||
|
{
|
||||||
|
WriteString($"%PDF-{version.ToString("0.0", CultureInfo.InvariantCulture)}", Stream);
|
||||||
|
|
||||||
|
Stream.WriteText("%");
|
||||||
|
Stream.WriteByte(169);
|
||||||
|
Stream.WriteByte(205);
|
||||||
|
Stream.WriteByte(196);
|
||||||
|
Stream.WriteByte(210);
|
||||||
|
Stream.WriteNewLine();
|
||||||
|
|
||||||
|
Initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CompletePdf(IndirectReferenceToken catalogReference, IndirectReferenceToken documentInformationReference=null)
|
||||||
|
{
|
||||||
|
TokenWriter.WriteCrossReferenceTable(offsets, catalogReference.Data, Stream, documentInformationReference?.Data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteString(string text, Stream stream)
|
||||||
|
{
|
||||||
|
var bytes = OtherEncodings.StringAsLatin1Bytes(text);
|
||||||
|
stream.Write(bytes, 0, bytes.Length);
|
||||||
|
stream.WriteNewLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (DisposeStream)
|
||||||
|
{
|
||||||
|
Stream.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
hashes.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
class FNVByteComparison : IEqualityComparer<byte[]>
|
||||||
|
{
|
||||||
|
public bool Equals(byte[] x, byte[] y)
|
||||||
|
{
|
||||||
|
if (x.Length != y.Length)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < x.Length; i++)
|
||||||
|
{
|
||||||
|
if (x[i] != y[i])
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetHashCode(byte[] obj)
|
||||||
|
{
|
||||||
|
var hash = FnvHash.Create();
|
||||||
|
foreach (var t in obj)
|
||||||
|
{
|
||||||
|
hash.Combine(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash.HashCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A hash combiner that is implemented with the Fowler/Noll/Vo algorithm (FNV-1a). This is a mutable struct for performance reasons.
|
||||||
|
/// </summary>
|
||||||
|
struct FnvHash
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The starting point of the FNV hash.
|
||||||
|
/// </summary>
|
||||||
|
public const int Offset = unchecked((int)2166136261);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The prime number used to compute the FNV hash.
|
||||||
|
/// </summary>
|
||||||
|
private const int Prime = 16777619;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current result of the hash function.
|
||||||
|
/// </summary>
|
||||||
|
public int HashCode { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new FNV hash initialized to <see cref="Offset"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static FnvHash Create()
|
||||||
|
{
|
||||||
|
var result = new FnvHash();
|
||||||
|
result.HashCode = Offset;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds the specified byte to the hash.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">The byte to hash.</param>
|
||||||
|
public void Combine(byte data)
|
||||||
|
{
|
||||||
|
unchecked
|
||||||
|
{
|
||||||
|
HashCode ^= data;
|
||||||
|
HashCode *= Prime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds the specified integer to this hash, in little-endian order.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">The integer to hash.</param>
|
||||||
|
public void Combine(int data)
|
||||||
|
{
|
||||||
|
Combine(unchecked((byte)data));
|
||||||
|
Combine(unchecked((byte)(data >> 8)));
|
||||||
|
Combine(unchecked((byte)(data >> 16)));
|
||||||
|
Combine(unchecked((byte)(data >> 24)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ namespace UglyToad.PdfPig.Writer
|
|||||||
using Parser.Parts;
|
using Parser.Parts;
|
||||||
using PdfPig.Fonts.Standard14Fonts;
|
using PdfPig.Fonts.Standard14Fonts;
|
||||||
using PdfPig.Fonts.TrueType.Parser;
|
using PdfPig.Fonts.TrueType.Parser;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using Tokenization.Scanner;
|
using Tokenization.Scanner;
|
||||||
using Tokens;
|
using Tokens;
|
||||||
|
|
||||||
@@ -22,9 +23,9 @@ namespace UglyToad.PdfPig.Writer
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides methods to construct new PDF documents.
|
/// Provides methods to construct new PDF documents.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class PdfDocumentBuilder
|
public class PdfDocumentBuilder : IDisposable
|
||||||
{
|
{
|
||||||
private readonly BuilderContext context = new BuilderContext();
|
private readonly IPdfStreamWriter context;
|
||||||
private readonly Dictionary<int, PdfPageBuilder> pages = new Dictionary<int, PdfPageBuilder>();
|
private readonly Dictionary<int, PdfPageBuilder> pages = new Dictionary<int, PdfPageBuilder>();
|
||||||
private readonly Dictionary<Guid, FontStored> fonts = new Dictionary<Guid, FontStored>();
|
private readonly Dictionary<Guid, FontStored> fonts = new Dictionary<Guid, FontStored>();
|
||||||
private readonly Dictionary<Guid, ImageStored> images = new Dictionary<Guid, ImageStored>();
|
private readonly Dictionary<Guid, ImageStored> images = new Dictionary<Guid, ImageStored>();
|
||||||
@@ -63,6 +64,36 @@ namespace UglyToad.PdfPig.Writer
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal IReadOnlyDictionary<Guid, ImageStored> Images => images;
|
internal IReadOnlyDictionary<Guid, ImageStored> Images => images;
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a document builder keeping resources in memory.
|
||||||
|
/// </summary>
|
||||||
|
public PdfDocumentBuilder()
|
||||||
|
{
|
||||||
|
context = new PdfStreamWriter(new MemoryStream(), true);
|
||||||
|
context.InitializePdf(1.7m);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a document builder using the supplied stream.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="stream">Steam to write pdf to.</param>
|
||||||
|
/// <param name="disposeStream">If stream should be disposed when builder is.</param>
|
||||||
|
/// <param name="type">Type of pdf stream writer to use</param>
|
||||||
|
public PdfDocumentBuilder(Stream stream, bool disposeStream=false, PdfWriterType type=PdfWriterType.Default)
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case PdfWriterType.ObjectInMemoryDedup:
|
||||||
|
context = new PdfDedupStreamWriter(stream, disposeStream);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
context = new PdfStreamWriter(stream, disposeStream);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
context.InitializePdf(1.7m);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determines whether the bytes of the TrueType font file provided can be used in a PDF document.
|
/// Determines whether the bytes of the TrueType font file provided can be used in a PDF document.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -158,15 +189,10 @@ namespace UglyToad.PdfPig.Writer
|
|||||||
return added;
|
return added;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal IndirectReference AddImage(DictionaryToken dictionary, byte[] bytes)
|
internal IndirectReferenceToken AddImage(DictionaryToken dictionary, byte[] bytes)
|
||||||
{
|
{
|
||||||
var reserved = context.ReserveNumber();
|
var streamToken = new StreamToken(dictionary, bytes);
|
||||||
|
return context.WriteToken(streamToken);
|
||||||
var stored = new ImageStored(dictionary, bytes, reserved);
|
|
||||||
|
|
||||||
images[stored.Id] = stored;
|
|
||||||
|
|
||||||
return new IndirectReference(reserved, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -235,45 +261,171 @@ namespace UglyToad.PdfPig.Writer
|
|||||||
return AddPage(rectangle.Width, rectangle.Height);
|
return AddPage(rectangle.Width, rectangle.Height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
internal IToken CopyToken(IPdfTokenScanner source, IToken token)
|
||||||
|
{
|
||||||
|
if (!existingCopies.TryGetValue(source, out var refs))
|
||||||
|
{
|
||||||
|
refs = new Dictionary<IndirectReference, IndirectReferenceToken>();
|
||||||
|
existingCopies.Add(source, refs);
|
||||||
|
}
|
||||||
|
|
||||||
|
return WriterUtil.CopyToken(context, token, source, refs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly ConditionalWeakTable<IPdfTokenScanner, Dictionary<IndirectReference, IndirectReferenceToken>> existingCopies =
|
||||||
|
new ConditionalWeakTable<IPdfTokenScanner, Dictionary<IndirectReference, IndirectReferenceToken>>();
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Builds a PDF document from the current content of this builder and its pages.
|
/// Add a new page with the specified size, this page will be included in the output when <see cref="Build"/> is called.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The bytes of the resulting PDF document.</returns>
|
/// <param name="document">Source document.</param>
|
||||||
public byte[] Build()
|
/// <param name="pageNumber">Page to copy.</param>
|
||||||
|
/// <returns>A builder for editing the page.</returns>
|
||||||
|
public PdfPageBuilder AddPage(PdfDocument document, int pageNumber)
|
||||||
{
|
{
|
||||||
var fontsWritten = new Dictionary<Guid, ObjectToken>();
|
if (!existingCopies.TryGetValue(document.Structure.TokenScanner, out var refs))
|
||||||
using (var memory = new MemoryStream())
|
|
||||||
{
|
{
|
||||||
// Header
|
refs = new Dictionary<IndirectReference, IndirectReferenceToken>();
|
||||||
WriteString("%PDF-1.7", memory);
|
existingCopies.Add(document.Structure.TokenScanner, refs);
|
||||||
|
}
|
||||||
|
|
||||||
// Files with binary data should contain a 2nd comment line followed by 4 bytes with values > 127
|
int i = 1;
|
||||||
memory.WriteText("%");
|
foreach (var (pageDict, parents) in WriterUtil.WalkTree(document.Structure.Catalog.PageTree))
|
||||||
memory.WriteByte(169);
|
{
|
||||||
memory.WriteByte(205);
|
if (i == pageNumber)
|
||||||
memory.WriteByte(196);
|
{
|
||||||
memory.WriteByte(210);
|
// copy content streams
|
||||||
memory.WriteNewLine();
|
var streams = new List<PdfPageBuilder.CopiedContentStream>();
|
||||||
|
if (pageDict.ContainsKey(NameToken.Contents))
|
||||||
|
{
|
||||||
|
var token = pageDict.Data[NameToken.Contents];
|
||||||
|
if (token is ArrayToken array)
|
||||||
|
{
|
||||||
|
foreach (var item in array.Data)
|
||||||
|
{
|
||||||
|
if (item is IndirectReferenceToken ir)
|
||||||
|
{
|
||||||
|
streams.Add(new PdfPageBuilder.CopiedContentStream(
|
||||||
|
WriterUtil.CopyToken(context, ir, document.Structure.TokenScanner, refs) as IndirectReferenceToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (token is IndirectReferenceToken ir)
|
||||||
|
{
|
||||||
|
streams.Add(new PdfPageBuilder.CopiedContentStream(
|
||||||
|
WriterUtil.CopyToken(context, ir, document.Structure.TokenScanner, refs) as IndirectReferenceToken));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// manually copy page dict / resources as we need to modify some
|
||||||
|
var copiedPageDict = new Dictionary<NameToken, IToken>();
|
||||||
|
Dictionary<NameToken, IToken> resources = new Dictionary<NameToken, IToken>();
|
||||||
|
|
||||||
|
// just put all parent resources into new page
|
||||||
|
foreach (var dict in parents)
|
||||||
|
{
|
||||||
|
if (dict.TryGet(NameToken.Resources, out var token))
|
||||||
|
{
|
||||||
|
CopyResourceDict(token, resources);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
foreach (var kvp in pageDict.Data)
|
||||||
|
{
|
||||||
|
if (kvp.Key == NameToken.Contents || kvp.Key == NameToken.Parent || kvp.Key == NameToken.Type)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kvp.Key == NameToken.Resources)
|
||||||
|
{
|
||||||
|
CopyResourceDict(kvp.Value, resources);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
copiedPageDict[NameToken.Create(kvp.Key)] =
|
||||||
|
WriterUtil.CopyToken(context, kvp.Value, document.Structure.TokenScanner, refs);
|
||||||
|
}
|
||||||
|
|
||||||
|
var builder = new PdfPageBuilder(pages.Count + 1, this, streams, resources, copiedPageDict);
|
||||||
|
pages[builder.PageNumber] = builder;
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new KeyNotFoundException($"Page {pageNumber} was not found in the source document.");
|
||||||
|
|
||||||
|
void CopyResourceDict(IToken token, Dictionary<NameToken, IToken> destinationDict)
|
||||||
|
{
|
||||||
|
DictionaryToken dict = GetRemoteDict(token);
|
||||||
|
if (dict == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
foreach (var item in dict.Data)
|
||||||
|
{
|
||||||
|
|
||||||
|
if (!destinationDict.ContainsKey(NameToken.Create(item.Key)))
|
||||||
|
{
|
||||||
|
if (item.Value is IndirectReferenceToken ir)
|
||||||
|
{
|
||||||
|
destinationDict[NameToken.Create(item.Key)] = WriterUtil.CopyToken(context, document.Structure.TokenScanner.Get(ir.Data).Data, document.Structure.TokenScanner, refs);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
destinationDict[NameToken.Create(item.Key)] = WriterUtil.CopyToken(context, item.Value, document.Structure.TokenScanner, refs);
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var subDict = GetRemoteDict(item.Value);
|
||||||
|
var destSubDict = destinationDict[NameToken.Create(item.Key)] as DictionaryToken;
|
||||||
|
if (destSubDict == null || subDict == null)
|
||||||
|
{
|
||||||
|
// not a dict.. just overwrite with more important one? should maybe check arrays?
|
||||||
|
destinationDict[NameToken.Create(item.Key)] = WriterUtil.CopyToken(context, item.Value, document.Structure.TokenScanner, refs);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
foreach (var subItem in subDict.Data)
|
||||||
|
{
|
||||||
|
// last copied most important important
|
||||||
|
destinationDict[NameToken.Create(subItem.Key)] = WriterUtil.CopyToken(context, subItem.Value,
|
||||||
|
document.Structure.TokenScanner, refs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DictionaryToken GetRemoteDict(IToken token)
|
||||||
|
{
|
||||||
|
DictionaryToken dict = null;
|
||||||
|
if (token is IndirectReferenceToken ir)
|
||||||
|
{
|
||||||
|
dict = document.Structure.TokenScanner.Get(ir.Data).Data as DictionaryToken;
|
||||||
|
}
|
||||||
|
else if (token is DictionaryToken dt)
|
||||||
|
{
|
||||||
|
dict = dt;
|
||||||
|
}
|
||||||
|
return dict;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CompleteDocument()
|
||||||
|
{
|
||||||
|
var fontsWritten = new Dictionary<Guid, IndirectReferenceToken>();
|
||||||
|
|
||||||
// Body
|
|
||||||
foreach (var font in fonts)
|
foreach (var font in fonts)
|
||||||
{
|
{
|
||||||
var fontObj = font.Value.FontProgram.WriteFont(font.Value.FontKey.Name, memory, context);
|
var fontObj = font.Value.FontProgram.WriteFont(context, font.Value.FontKey.Name);
|
||||||
fontsWritten.Add(font.Key, fontObj);
|
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var tokenSet in unwrittenTokens)
|
|
||||||
{
|
|
||||||
context.WriteObject(memory, tokenSet.Value, (int)tokenSet.Key.Data.ObjectNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
var procSet = new List<NameToken>
|
var procSet = new List<NameToken>
|
||||||
{
|
{
|
||||||
NameToken.Create("PDF"),
|
NameToken.Create("PDF"),
|
||||||
@@ -290,100 +442,72 @@ namespace UglyToad.PdfPig.Writer
|
|||||||
|
|
||||||
if (fontsWritten.Count > 0)
|
if (fontsWritten.Count > 0)
|
||||||
{
|
{
|
||||||
var fontsDictionary = new DictionaryToken(fontsWritten.Select(x => (fonts[x.Key].FontKey.Name, (IToken)new IndirectReferenceToken(x.Value.Number)))
|
var fontsDictionary = new DictionaryToken(fontsWritten.Select(x =>
|
||||||
|
(fonts[x.Key].FontKey.Name, (IToken)x.Value))
|
||||||
.ToDictionary(x => x.Item1, x => x.Item2));
|
.ToDictionary(x => x.Item1, x => x.Item2));
|
||||||
|
|
||||||
resources.Add(NameToken.Font, fontsDictionary);
|
var fontsDictionaryRef = context.WriteToken(fontsDictionary);
|
||||||
|
|
||||||
|
resources.Add(NameToken.Font, fontsDictionaryRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
var reserved = context.ReserveNumber();
|
var parentIndirect = context.ReserveObjectNumber();
|
||||||
var parentIndirect = new IndirectReferenceToken(new IndirectReference(reserved, 0));
|
|
||||||
|
|
||||||
var pageReferences = new List<IndirectReferenceToken>();
|
var pageReferences = new List<IndirectReferenceToken>();
|
||||||
foreach (var page in pages)
|
foreach (var page in pages)
|
||||||
{
|
{
|
||||||
var individualResources = new Dictionary<NameToken, IToken>(resources);
|
var pageDictionary = page.Value.additionalPageProperties;
|
||||||
var pageDictionary = new Dictionary<NameToken, IToken>
|
pageDictionary[NameToken.Type] = NameToken.Page;
|
||||||
|
pageDictionary[NameToken.Parent] = parentIndirect;
|
||||||
|
if (!pageDictionary.ContainsKey(NameToken.MediaBox))
|
||||||
{
|
{
|
||||||
{NameToken.Type, NameToken.Page},
|
pageDictionary[NameToken.MediaBox] = RectangleToArray(page.Value.PageSize);
|
||||||
{NameToken.MediaBox, RectangleToArray(page.Value.PageSize)},
|
|
||||||
{NameToken.Parent, parentIndirect}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (page.Value.Resources.Count > 0)
|
|
||||||
{
|
|
||||||
foreach (var kvp in page.Value.Resources)
|
|
||||||
{
|
|
||||||
var value = kvp.Value;
|
|
||||||
if (individualResources.TryGetValue(kvp.Key, out var pageToken))
|
|
||||||
{
|
|
||||||
if (pageToken is DictionaryToken leftDictionary && value is DictionaryToken rightDictionary)
|
|
||||||
{
|
|
||||||
var merged = leftDictionary.Data.ToDictionary(k => NameToken.Create(k.Key), v => v.Value);
|
|
||||||
foreach (var set in rightDictionary.Data)
|
|
||||||
{
|
|
||||||
merged[NameToken.Create(set.Key)] = set.Value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
value = new DictionaryToken(merged);
|
pageDictionary[NameToken.Resources] = new DictionaryToken(page.Value.Resources);
|
||||||
|
|
||||||
}
|
if (page.Value.contentStreams.Count == 1)
|
||||||
// Else override
|
|
||||||
}
|
|
||||||
|
|
||||||
individualResources[kvp.Key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pageDictionary[NameToken.Resources] = new DictionaryToken(individualResources);
|
|
||||||
|
|
||||||
if (page.Value.ContentStreams.Count == 1)
|
|
||||||
{
|
{
|
||||||
var contentStream = WriteContentStream(page.Value.CurrentStream.Operations);
|
pageDictionary[NameToken.Contents] = page.Value.contentStreams[0].Write(context);
|
||||||
|
|
||||||
var contentStreamObj = context.WriteObject(memory, contentStream);
|
|
||||||
|
|
||||||
pageDictionary[NameToken.Contents] = new IndirectReferenceToken(contentStreamObj.Number);
|
|
||||||
}
|
}
|
||||||
else if (page.Value.ContentStreams.Count > 1)
|
else
|
||||||
{
|
{
|
||||||
var streamTokens = page.Value.ContentStreams.Select(contentStream =>
|
var streams = new List<IToken>();
|
||||||
|
foreach (var stream in page.Value.contentStreams)
|
||||||
{
|
{
|
||||||
var streamToken = WriteContentStream(contentStream.Operations);
|
streams.Add(stream.Write(context));
|
||||||
|
|
||||||
var contentStreamObj = context.WriteObject(memory, streamToken);
|
|
||||||
|
|
||||||
return new IndirectReferenceToken(contentStreamObj.Number);
|
|
||||||
}).ToList();
|
|
||||||
|
|
||||||
pageDictionary[NameToken.Contents] = new ArrayToken(streamTokens);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var pageRef = context.WriteObject(memory, new DictionaryToken(pageDictionary));
|
pageDictionary[NameToken.Contents] = new ArrayToken(streams);
|
||||||
|
}
|
||||||
|
|
||||||
pageReferences.Add(new IndirectReferenceToken(pageRef.Number));
|
|
||||||
|
var pageRef = context.WriteToken( new DictionaryToken(pageDictionary));
|
||||||
|
|
||||||
|
pageReferences.Add(pageRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
var pagesDictionaryData = new Dictionary<NameToken, IToken>
|
var pagesDictionaryData = new Dictionary<NameToken, IToken>
|
||||||
{
|
{
|
||||||
{NameToken.Type, NameToken.Pages},
|
{NameToken.Type, NameToken.Pages},
|
||||||
{NameToken.Kids, new ArrayToken(pageReferences)},
|
{NameToken.Kids, new ArrayToken(pageReferences)},
|
||||||
|
{NameToken.Resources, new DictionaryToken(resources)},
|
||||||
{NameToken.Count, new NumericToken(pageReferences.Count)}
|
{NameToken.Count, new NumericToken(pageReferences.Count)}
|
||||||
};
|
};
|
||||||
|
|
||||||
var pagesDictionary = new DictionaryToken(pagesDictionaryData);
|
var pagesDictionary = new DictionaryToken(pagesDictionaryData);
|
||||||
|
|
||||||
var pagesRef = context.WriteObject(memory, pagesDictionary, reserved);
|
var pagesRef = context.WriteToken(pagesDictionary, parentIndirect);
|
||||||
|
|
||||||
var catalogDictionary = new Dictionary<NameToken, IToken>
|
var catalogDictionary = new Dictionary<NameToken, IToken>
|
||||||
{
|
{
|
||||||
{NameToken.Type, NameToken.Catalog},
|
{NameToken.Type, NameToken.Catalog},
|
||||||
{NameToken.Pages, new IndirectReferenceToken(pagesRef.Number)}
|
{NameToken.Pages, pagesRef}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (ArchiveStandard != PdfAStandard.None)
|
if (ArchiveStandard != PdfAStandard.None)
|
||||||
{
|
{
|
||||||
Func<IToken, ObjectToken> writerFunc = x => context.WriteObject(memory, x);
|
Func<IToken, IndirectReferenceToken> writerFunc = x => context.WriteToken(x);
|
||||||
|
|
||||||
PdfABaselineRuleBuilder.Obey(catalogDictionary, writerFunc, DocumentInformation, ArchiveStandard);
|
PdfABaselineRuleBuilder.Obey(catalogDictionary, writerFunc, DocumentInformation, ArchiveStandard);
|
||||||
|
|
||||||
@@ -402,108 +526,45 @@ namespace UglyToad.PdfPig.Writer
|
|||||||
|
|
||||||
var catalog = new DictionaryToken(catalogDictionary);
|
var catalog = new DictionaryToken(catalogDictionary);
|
||||||
|
|
||||||
var catalogRef = context.WriteObject(memory, catalog);
|
var catalogRef = context.WriteToken(catalog);
|
||||||
|
|
||||||
var informationReference = default(IndirectReference?);
|
var informationReference = default(IndirectReferenceToken);
|
||||||
if (IncludeDocumentInformation)
|
if (IncludeDocumentInformation)
|
||||||
{
|
{
|
||||||
var informationDictionary = DocumentInformation.ToDictionary();
|
var informationDictionary = DocumentInformation.ToDictionary();
|
||||||
if (informationDictionary.Count > 0)
|
if (informationDictionary.Count > 0)
|
||||||
{
|
{
|
||||||
var dictionary = new DictionaryToken(informationDictionary);
|
var dictionary = new DictionaryToken(informationDictionary);
|
||||||
informationReference = context.WriteObject(memory, dictionary).Number;
|
informationReference = context.WriteToken(dictionary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TokenWriter.WriteCrossReferenceTable(context.ObjectOffsets, catalogRef, memory, informationReference);
|
context.CompletePdf(catalogRef, informationReference);
|
||||||
|
|
||||||
return memory.ToArray();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The purpose of this method is to resolve indirect reference. That mean copy the reference's content to the new document's stream
|
/// Builds a PDF document from the current content of this builder and its pages.
|
||||||
/// and replace the indirect reference with the correct/new one
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="tokenToCopy">Token to inspect for reference</param>
|
/// <returns>The bytes of the resulting PDF document.</returns>
|
||||||
/// <param name="tokenScanner">scanner get the content from the original document</param>
|
public byte[] Build()
|
||||||
/// <returns>A reference of the token that was copied. With all the reference updated</returns>
|
|
||||||
internal IToken CopyToken(IToken tokenToCopy, IPdfTokenScanner tokenScanner)
|
|
||||||
{
|
{
|
||||||
// This token need to be deep copied, because they could contain reference. So we have to update them.
|
CompleteDocument();
|
||||||
switch (tokenToCopy)
|
|
||||||
|
if (context.Stream is MemoryStream ms)
|
||||||
{
|
{
|
||||||
case DictionaryToken dictionaryToken:
|
return ms.ToArray();
|
||||||
{
|
|
||||||
var newContent = new Dictionary<NameToken, IToken>();
|
|
||||||
foreach (var setPair in dictionaryToken.Data)
|
|
||||||
{
|
|
||||||
var name = setPair.Key;
|
|
||||||
var token = setPair.Value;
|
|
||||||
newContent.Add(NameToken.Create(name), CopyToken(token, tokenScanner));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new DictionaryToken(newContent);
|
if (!context.Stream.CanSeek)
|
||||||
}
|
|
||||||
case ArrayToken arrayToken:
|
|
||||||
{
|
{
|
||||||
var newArray = new List<IToken>(arrayToken.Length);
|
throw new InvalidOperationException("PdfDocument.Build() called with non-seekable stream.");
|
||||||
foreach (var token in arrayToken.Data)
|
}
|
||||||
|
|
||||||
|
using (var temp = new MemoryStream())
|
||||||
{
|
{
|
||||||
newArray.Add(CopyToken(token, tokenScanner));
|
context.Stream.Seek(0, SeekOrigin.Begin);
|
||||||
}
|
context.Stream.CopyTo(temp);
|
||||||
|
return temp.ToArray();
|
||||||
return new ArrayToken(newArray);
|
|
||||||
}
|
|
||||||
case IndirectReferenceToken referenceToken:
|
|
||||||
{
|
|
||||||
var tokenObject = DirectObjectFinder.Get<IToken>(referenceToken.Data, tokenScanner);
|
|
||||||
|
|
||||||
Debug.Assert(!(tokenObject is IndirectReferenceToken));
|
|
||||||
|
|
||||||
var newToken = CopyToken(tokenObject, tokenScanner);
|
|
||||||
|
|
||||||
var reserved = context.ReserveNumber();
|
|
||||||
var newReference = new IndirectReferenceToken(new IndirectReference(reserved, 0));
|
|
||||||
|
|
||||||
unwrittenTokens.Add(newReference, newToken);
|
|
||||||
|
|
||||||
return newReference;
|
|
||||||
}
|
|
||||||
case StreamToken streamToken:
|
|
||||||
{
|
|
||||||
var properties = CopyToken(streamToken.StreamDictionary, tokenScanner) as DictionaryToken;
|
|
||||||
Debug.Assert(properties != null);
|
|
||||||
|
|
||||||
var bytes = streamToken.Data;
|
|
||||||
return new StreamToken(properties, bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
case ObjectToken _:
|
|
||||||
{
|
|
||||||
// Since we don't write token directly to the stream.
|
|
||||||
// We can't know the offset. Therefore the token would be invalid
|
|
||||||
throw new NotSupportedException("Copying a Object token is not supported");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tokenToCopy;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static StreamToken WriteContentStream(IReadOnlyList<IGraphicsStateOperation> content)
|
|
||||||
{
|
|
||||||
using (var memoryStream = new MemoryStream())
|
|
||||||
{
|
|
||||||
foreach (var operation in content)
|
|
||||||
{
|
|
||||||
operation.Write(memoryStream);
|
|
||||||
}
|
|
||||||
|
|
||||||
var bytes = memoryStream.ToArray();
|
|
||||||
|
|
||||||
var stream = DataCompresser.CompressToStream(bytes);
|
|
||||||
|
|
||||||
return stream;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -661,5 +722,13 @@ namespace UglyToad.PdfPig.Writer
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disposes underlying stream if set to do so.
|
||||||
|
/// </summary>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
context.Dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,7 +129,21 @@
|
|||||||
{
|
{
|
||||||
const bool isLenientParsing = false;
|
const bool isLenientParsing = false;
|
||||||
|
|
||||||
var documentBuilder = new DocumentMerger(output);
|
var writer = new PdfStreamWriter(output, false);
|
||||||
|
var documentBuilder = new DocumentMerger(writer);
|
||||||
|
|
||||||
|
var maxVersion = 1.2m;
|
||||||
|
var infos = new List<(CoreTokenScanner CoreScanner, HeaderVersion Version)>();
|
||||||
|
foreach (var fileIndex in Enumerable.Range(0, files.Count))
|
||||||
|
{
|
||||||
|
var inputBytes = files[fileIndex];
|
||||||
|
var coreScanner = new CoreTokenScanner(inputBytes);
|
||||||
|
|
||||||
|
var version = FileHeaderParser.Parse(coreScanner, isLenientParsing, Log);
|
||||||
|
maxVersion = Math.Max(maxVersion, version.Version);
|
||||||
|
infos.Add((coreScanner, version));
|
||||||
|
}
|
||||||
|
writer.InitializePdf(maxVersion);
|
||||||
|
|
||||||
foreach (var fileIndex in Enumerable.Range(0, files.Count))
|
foreach (var fileIndex in Enumerable.Range(0, files.Count))
|
||||||
{
|
{
|
||||||
@@ -140,9 +154,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
var inputBytes = files[fileIndex];
|
var inputBytes = files[fileIndex];
|
||||||
var coreScanner = new CoreTokenScanner(inputBytes);
|
var (coreScanner, version) = infos[fileIndex];
|
||||||
|
|
||||||
var version = FileHeaderParser.Parse(coreScanner, isLenientParsing, Log);
|
|
||||||
|
|
||||||
var crossReferenceParser = new CrossReferenceParser(Log, new XrefOffsetValidator(Log),
|
var crossReferenceParser = new CrossReferenceParser(Log, new XrefOffsetValidator(Log),
|
||||||
new Parser.Parts.CrossReference.CrossReferenceStreamParser(FilterProvider));
|
new Parser.Parts.CrossReference.CrossReferenceStreamParser(FilterProvider));
|
||||||
@@ -165,7 +177,7 @@
|
|||||||
|
|
||||||
var documentCatalog = CatalogFactory.Create(crossReference.Trailer.Root, catalogDictionaryToken, pdfScanner, isLenientParsing);
|
var documentCatalog = CatalogFactory.Create(crossReference.Trailer.Root, catalogDictionaryToken, pdfScanner, isLenientParsing);
|
||||||
|
|
||||||
documentBuilder.AppendDocument(documentCatalog, version.Version, pdfScanner, pages);
|
documentBuilder.AppendDocument(documentCatalog, pdfScanner, pages);
|
||||||
}
|
}
|
||||||
|
|
||||||
documentBuilder.Build();
|
documentBuilder.Build();
|
||||||
@@ -201,24 +213,21 @@
|
|||||||
|
|
||||||
private class DocumentMerger
|
private class DocumentMerger
|
||||||
{
|
{
|
||||||
private const decimal DefaultVersion = 1.2m;
|
|
||||||
|
|
||||||
private const int ARTIFICIAL_NODE_LIMIT = 100;
|
private const int ARTIFICIAL_NODE_LIMIT = 100;
|
||||||
|
|
||||||
private readonly PdfStreamWriter context;
|
private readonly IPdfStreamWriter context;
|
||||||
private readonly List<IndirectReferenceToken> pagesTokenReferences = new List<IndirectReferenceToken>();
|
private readonly List<IndirectReferenceToken> pagesTokenReferences = new List<IndirectReferenceToken>();
|
||||||
private readonly IndirectReferenceToken rootPagesReference;
|
private readonly IndirectReferenceToken rootPagesReference;
|
||||||
|
|
||||||
private decimal currentVersion = DefaultVersion;
|
|
||||||
private int pageCount = 0;
|
private int pageCount = 0;
|
||||||
|
|
||||||
public DocumentMerger(Stream baseStream)
|
public DocumentMerger(IPdfStreamWriter writer)
|
||||||
{
|
{
|
||||||
context = new PdfStreamWriter(baseStream, false);
|
context = writer;
|
||||||
rootPagesReference = context.ReserveNumberToken();
|
rootPagesReference = context.ReserveObjectNumber();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AppendDocument(Catalog catalog, decimal version, IPdfTokenScanner tokenScanner, IReadOnlyList<int> pages)
|
public void AppendDocument(Catalog catalog, IPdfTokenScanner tokenScanner, IReadOnlyList<int> pages)
|
||||||
{
|
{
|
||||||
IEnumerable<int> pageIndices;
|
IEnumerable<int> pageIndices;
|
||||||
if (pages == null)
|
if (pages == null)
|
||||||
@@ -240,11 +249,9 @@
|
|||||||
pageIndices = pages;
|
pageIndices = pages;
|
||||||
}
|
}
|
||||||
|
|
||||||
currentVersion = Math.Max(version, currentVersion);
|
|
||||||
|
|
||||||
var referencesFromDocument = new Dictionary<IndirectReference, IndirectReferenceToken>();
|
var referencesFromDocument = new Dictionary<IndirectReference, IndirectReferenceToken>();
|
||||||
|
|
||||||
var currentNodeReference = context.ReserveNumberToken();
|
var currentNodeReference = context.ReserveObjectNumber();
|
||||||
var pagesReferences = new List<IndirectReferenceToken>();
|
var pagesReferences = new List<IndirectReferenceToken>();
|
||||||
var resources = new Dictionary<string, IToken>();
|
var resources = new Dictionary<string, IToken>();
|
||||||
|
|
||||||
@@ -323,7 +330,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
var pagesDictionary = new DictionaryToken(newPagesNode);
|
var pagesDictionary = new DictionaryToken(newPagesNode);
|
||||||
pagesTokenReferences.Add(context.WriteToken(pagesDictionary, (int)currentNodeReference.Data.ObjectNumber));
|
context.WriteToken(pagesDictionary, currentNodeReference);
|
||||||
|
pagesTokenReferences.Add(currentNodeReference);
|
||||||
|
|
||||||
pageCount += pagesReferences.Count;
|
pageCount += pagesReferences.Count;
|
||||||
};
|
};
|
||||||
@@ -335,7 +343,7 @@
|
|||||||
{
|
{
|
||||||
CreateTree();
|
CreateTree();
|
||||||
|
|
||||||
currentNodeReference = context.ReserveNumberToken();
|
currentNodeReference = context.ReserveObjectNumber();
|
||||||
pagesReferences = new List<IndirectReferenceToken>();
|
pagesReferences = new List<IndirectReferenceToken>();
|
||||||
resources = new Dictionary<string, IToken>();
|
resources = new Dictionary<string, IToken>();
|
||||||
}
|
}
|
||||||
@@ -366,7 +374,7 @@
|
|||||||
{ NameToken.Count, new NumericToken(pageCount) }
|
{ NameToken.Count, new NumericToken(pageCount) }
|
||||||
});
|
});
|
||||||
|
|
||||||
var pagesRef = context.WriteToken(pagesDictionary, (int)rootPagesReference.Data.ObjectNumber);
|
var pagesRef = context.WriteToken(pagesDictionary, rootPagesReference);
|
||||||
|
|
||||||
var catalog = new DictionaryToken(new Dictionary<NameToken, IToken>
|
var catalog = new DictionaryToken(new Dictionary<NameToken, IToken>
|
||||||
{
|
{
|
||||||
@@ -376,7 +384,7 @@
|
|||||||
|
|
||||||
var catalogRef = context.WriteToken(catalog);
|
var catalogRef = context.WriteToken(catalog);
|
||||||
|
|
||||||
context.Flush(currentVersion, catalogRef);
|
context.CompletePdf(catalogRef);
|
||||||
|
|
||||||
Close();
|
Close();
|
||||||
}
|
}
|
||||||
@@ -423,67 +431,7 @@
|
|||||||
/// <returns>A reference of the token that was copied. With all the reference updated</returns>
|
/// <returns>A reference of the token that was copied. With all the reference updated</returns>
|
||||||
private IToken CopyToken(IToken tokenToCopy, IPdfTokenScanner tokenScanner, IDictionary<IndirectReference, IndirectReferenceToken> referencesFromDocument)
|
private IToken CopyToken(IToken tokenToCopy, IPdfTokenScanner tokenScanner, IDictionary<IndirectReference, IndirectReferenceToken> referencesFromDocument)
|
||||||
{
|
{
|
||||||
// This token need to be deep copied, because they could contain reference. So we have to update them.
|
return WriterUtil.CopyToken(context, tokenToCopy, tokenScanner, referencesFromDocument);
|
||||||
switch (tokenToCopy)
|
|
||||||
{
|
|
||||||
case DictionaryToken dictionaryToken:
|
|
||||||
{
|
|
||||||
var newContent = new Dictionary<NameToken, IToken>();
|
|
||||||
foreach (var setPair in dictionaryToken.Data)
|
|
||||||
{
|
|
||||||
var name = setPair.Key;
|
|
||||||
var token = setPair.Value;
|
|
||||||
newContent.Add(NameToken.Create(name), CopyToken(token, tokenScanner, referencesFromDocument));
|
|
||||||
}
|
|
||||||
|
|
||||||
return new DictionaryToken(newContent);
|
|
||||||
}
|
|
||||||
case ArrayToken arrayToken:
|
|
||||||
{
|
|
||||||
var newArray = new List<IToken>(arrayToken.Length);
|
|
||||||
foreach (var token in arrayToken.Data)
|
|
||||||
{
|
|
||||||
newArray.Add(CopyToken(token, tokenScanner, referencesFromDocument));
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ArrayToken(newArray);
|
|
||||||
}
|
|
||||||
case IndirectReferenceToken referenceToken:
|
|
||||||
{
|
|
||||||
if (referencesFromDocument.TryGetValue(referenceToken.Data, out var newReferenceToken))
|
|
||||||
{
|
|
||||||
return newReferenceToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
//we add the token to referencesFromDocument to prevent stackoverflow on references cycles
|
|
||||||
newReferenceToken = context.ReserveNumberToken();
|
|
||||||
referencesFromDocument.Add(referenceToken.Data, newReferenceToken);
|
|
||||||
|
|
||||||
var tokenObject = DirectObjectFinder.Get<IToken>(referenceToken.Data, tokenScanner);
|
|
||||||
Debug.Assert(!(tokenObject is IndirectReferenceToken));
|
|
||||||
|
|
||||||
var newToken = CopyToken(tokenObject, tokenScanner, referencesFromDocument);
|
|
||||||
context.WriteToken(newReferenceToken, newToken);
|
|
||||||
return newReferenceToken;
|
|
||||||
}
|
|
||||||
case StreamToken streamToken:
|
|
||||||
{
|
|
||||||
var properties = CopyToken(streamToken.StreamDictionary, tokenScanner, referencesFromDocument) as DictionaryToken;
|
|
||||||
Debug.Assert(properties != null);
|
|
||||||
|
|
||||||
var bytes = streamToken.Data;
|
|
||||||
return new StreamToken(properties, bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
case ObjectToken _:
|
|
||||||
{
|
|
||||||
// Since we don't write token directly to the stream.
|
|
||||||
// We can't know the offset. Therefore the token would be invalid
|
|
||||||
throw new NotSupportedException("Copying a Object token is not supported");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tokenToCopy;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,9 @@
|
|||||||
public class PdfPageBuilder
|
public class PdfPageBuilder
|
||||||
{
|
{
|
||||||
private readonly PdfDocumentBuilder documentBuilder;
|
private readonly PdfDocumentBuilder documentBuilder;
|
||||||
private readonly List<ContentStream> contentStreams;
|
private IPageContentStream currentStream;
|
||||||
|
internal readonly List<IPageContentStream> contentStreams;
|
||||||
|
internal readonly Dictionary<NameToken, IToken> additionalPageProperties = new Dictionary<NameToken, IToken>();
|
||||||
private readonly Dictionary<NameToken, IToken> resourcesDictionary = new Dictionary<NameToken, IToken>();
|
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)
|
//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)
|
||||||
@@ -52,23 +54,33 @@
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Access to the underlying data structures for advanced use cases.
|
/// Access to the underlying data structures for advanced use cases.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ContentStream CurrentStream { get; private set; }
|
public IContentStream CurrentStream => currentStream;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Access to
|
/// Access to
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyList<ContentStream> ContentStreams { get; }
|
public IReadOnlyList<IContentStream> ContentStreams => contentStreams;
|
||||||
|
|
||||||
internal PdfPageBuilder(int number, PdfDocumentBuilder documentBuilder)
|
internal PdfPageBuilder(int number, PdfDocumentBuilder documentBuilder)
|
||||||
{
|
{
|
||||||
this.documentBuilder = documentBuilder ?? throw new ArgumentNullException(nameof(documentBuilder));
|
this.documentBuilder = documentBuilder ?? throw new ArgumentNullException(nameof(documentBuilder));
|
||||||
PageNumber = number;
|
PageNumber = number;
|
||||||
|
|
||||||
CurrentStream = new ContentStream();
|
currentStream = new DefaultContentStream();
|
||||||
ContentStreams = contentStreams = new List<ContentStream>()
|
contentStreams = new List<IPageContentStream>() {currentStream};
|
||||||
|
}
|
||||||
|
|
||||||
|
internal PdfPageBuilder(int number, PdfDocumentBuilder documentBuilder, IEnumerable<CopiedContentStream> copied,
|
||||||
|
Dictionary<NameToken, IToken> existingResources, Dictionary<NameToken, IToken> pageDict)
|
||||||
{
|
{
|
||||||
CurrentStream
|
this.documentBuilder = documentBuilder ?? throw new ArgumentNullException(nameof(documentBuilder));
|
||||||
};
|
PageNumber = number;
|
||||||
|
contentStreams = new List<IPageContentStream>();
|
||||||
|
contentStreams.AddRange(copied);
|
||||||
|
currentStream = new DefaultContentStream();
|
||||||
|
contentStreams.Add(currentStream);
|
||||||
|
additionalPageProperties =pageDict ?? new Dictionary<NameToken, IToken>();
|
||||||
|
resourcesDictionary = existingResources;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -76,10 +88,10 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void NewContentStreamBefore()
|
public void NewContentStreamBefore()
|
||||||
{
|
{
|
||||||
var index = Math.Max(contentStreams.IndexOf(CurrentStream) - 1, 0);
|
var index = Math.Max(contentStreams.IndexOf(currentStream) - 1, 0);
|
||||||
|
|
||||||
CurrentStream = new ContentStream();
|
currentStream = new DefaultContentStream();
|
||||||
contentStreams.Insert(index, CurrentStream);
|
contentStreams.Insert(index, currentStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -87,10 +99,10 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void NewContentStreamAfter()
|
public void NewContentStreamAfter()
|
||||||
{
|
{
|
||||||
var index = Math.Min(contentStreams.IndexOf(CurrentStream) + 1, contentStreams.Count);
|
var index = Math.Min(contentStreams.IndexOf(currentStream) + 1, contentStreams.Count);
|
||||||
|
|
||||||
CurrentStream = new ContentStream();
|
currentStream = new DefaultContentStream();
|
||||||
contentStreams.Insert(index, CurrentStream);
|
contentStreams.Insert(index, currentStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -99,12 +111,12 @@
|
|||||||
/// <param name="index">index of the content stream to be selected</param>
|
/// <param name="index">index of the content stream to be selected</param>
|
||||||
public void SelectContentStream(int index)
|
public void SelectContentStream(int index)
|
||||||
{
|
{
|
||||||
if (index < 0 || index >= ContentStreams.Count)
|
if (index < 0 || index >= contentStreams.Count)
|
||||||
{
|
{
|
||||||
throw new IndexOutOfRangeException(nameof(index));
|
throw new IndexOutOfRangeException(nameof(index));
|
||||||
}
|
}
|
||||||
|
|
||||||
CurrentStream = ContentStreams[index];
|
currentStream = contentStreams[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -117,16 +129,16 @@
|
|||||||
{
|
{
|
||||||
if (lineWidth != 1)
|
if (lineWidth != 1)
|
||||||
{
|
{
|
||||||
CurrentStream.Add(new SetLineWidth(lineWidth));
|
currentStream.Add(new SetLineWidth(lineWidth));
|
||||||
}
|
}
|
||||||
|
|
||||||
CurrentStream.Add(new BeginNewSubpath((decimal)from.X, (decimal)from.Y));
|
currentStream.Add(new BeginNewSubpath((decimal)from.X, (decimal)from.Y));
|
||||||
CurrentStream.Add(new AppendStraightLineSegment((decimal)to.X, (decimal)to.Y));
|
currentStream.Add(new AppendStraightLineSegment((decimal)to.X, (decimal)to.Y));
|
||||||
CurrentStream.Add(StrokePath.Value);
|
currentStream.Add(StrokePath.Value);
|
||||||
|
|
||||||
if (lineWidth != 1)
|
if (lineWidth != 1)
|
||||||
{
|
{
|
||||||
CurrentStream.Add(new SetLineWidth(1));
|
currentStream.Add(new SetLineWidth(1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,23 +154,23 @@
|
|||||||
{
|
{
|
||||||
if (lineWidth != 1)
|
if (lineWidth != 1)
|
||||||
{
|
{
|
||||||
CurrentStream.Add(new SetLineWidth(lineWidth));
|
currentStream.Add(new SetLineWidth(lineWidth));
|
||||||
}
|
}
|
||||||
|
|
||||||
CurrentStream.Add(new AppendRectangle((decimal)position.X, (decimal)position.Y, width, height));
|
currentStream.Add(new AppendRectangle((decimal)position.X, (decimal)position.Y, width, height));
|
||||||
|
|
||||||
if (fill)
|
if (fill)
|
||||||
{
|
{
|
||||||
CurrentStream.Add(FillPathEvenOddRuleAndStroke.Value);
|
currentStream.Add(FillPathEvenOddRuleAndStroke.Value);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
CurrentStream.Add(StrokePath.Value);
|
currentStream.Add(StrokePath.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lineWidth != 1)
|
if (lineWidth != 1)
|
||||||
{
|
{
|
||||||
CurrentStream.Add(new SetLineWidth(lineWidth));
|
currentStream.Add(new SetLineWidth(lineWidth));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,8 +182,8 @@
|
|||||||
/// <param name="b">Blue - 0 to 255</param>
|
/// <param name="b">Blue - 0 to 255</param>
|
||||||
public void SetStrokeColor(byte r, byte g, byte b)
|
public void SetStrokeColor(byte r, byte g, byte b)
|
||||||
{
|
{
|
||||||
CurrentStream.Add(Push.Value);
|
currentStream.Add(Push.Value);
|
||||||
CurrentStream.Add(new SetStrokeColorDeviceRgb(RgbToDecimal(r), RgbToDecimal(g), RgbToDecimal(b)));
|
currentStream.Add(new SetStrokeColorDeviceRgb(RgbToDecimal(r), RgbToDecimal(g), RgbToDecimal(b)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -182,8 +194,8 @@
|
|||||||
/// <param name="b">Blue - 0 to 1</param>
|
/// <param name="b">Blue - 0 to 1</param>
|
||||||
internal void SetStrokeColorExact(decimal r, decimal g, decimal b)
|
internal void SetStrokeColorExact(decimal r, decimal g, decimal b)
|
||||||
{
|
{
|
||||||
CurrentStream.Add(Push.Value);
|
currentStream.Add(Push.Value);
|
||||||
CurrentStream.Add(new SetStrokeColorDeviceRgb(CheckRgbDecimal(r, nameof(r)),
|
currentStream.Add(new SetStrokeColorDeviceRgb(CheckRgbDecimal(r, nameof(r)),
|
||||||
CheckRgbDecimal(g, nameof(g)), CheckRgbDecimal(b, nameof(b))));
|
CheckRgbDecimal(g, nameof(g)), CheckRgbDecimal(b, nameof(b))));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,8 +207,8 @@
|
|||||||
/// <param name="b">Blue - 0 to 255</param>
|
/// <param name="b">Blue - 0 to 255</param>
|
||||||
public void SetTextAndFillColor(byte r, byte g, byte b)
|
public void SetTextAndFillColor(byte r, byte g, byte b)
|
||||||
{
|
{
|
||||||
CurrentStream.Add(Push.Value);
|
currentStream.Add(Push.Value);
|
||||||
CurrentStream.Add(new SetNonStrokeColorDeviceRgb(RgbToDecimal(r), RgbToDecimal(g), RgbToDecimal(b)));
|
currentStream.Add(new SetNonStrokeColorDeviceRgb(RgbToDecimal(r), RgbToDecimal(g), RgbToDecimal(b)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -204,7 +216,7 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void ResetColor()
|
public void ResetColor()
|
||||||
{
|
{
|
||||||
CurrentStream.Add(Pop.Value);
|
currentStream.Add(Pop.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -294,15 +306,15 @@
|
|||||||
|
|
||||||
var letters = DrawLetters(text, fontProgram, fm, fontSize, textMatrix);
|
var letters = DrawLetters(text, fontProgram, fm, fontSize, textMatrix);
|
||||||
|
|
||||||
CurrentStream.Add(BeginText.Value);
|
currentStream.Add(BeginText.Value);
|
||||||
CurrentStream.Add(new SetFontAndSize(font.Name, fontSize));
|
currentStream.Add(new SetFontAndSize(font.Name, fontSize));
|
||||||
CurrentStream.Add(new MoveToNextLineWithOffset((decimal)position.X, (decimal)position.Y));
|
currentStream.Add(new MoveToNextLineWithOffset((decimal)position.X, (decimal)position.Y));
|
||||||
var bytesPerShow = new List<byte>();
|
var bytesPerShow = new List<byte>();
|
||||||
foreach (var letter in text)
|
foreach (var letter in text)
|
||||||
{
|
{
|
||||||
if (char.IsWhiteSpace(letter))
|
if (char.IsWhiteSpace(letter))
|
||||||
{
|
{
|
||||||
CurrentStream.Add(new ShowText(bytesPerShow.ToArray()));
|
currentStream.Add(new ShowText(bytesPerShow.ToArray()));
|
||||||
bytesPerShow.Clear();
|
bytesPerShow.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,10 +324,10 @@
|
|||||||
|
|
||||||
if (bytesPerShow.Count > 0)
|
if (bytesPerShow.Count > 0)
|
||||||
{
|
{
|
||||||
CurrentStream.Add(new ShowText(bytesPerShow.ToArray()));
|
currentStream.Add(new ShowText(bytesPerShow.ToArray()));
|
||||||
}
|
}
|
||||||
|
|
||||||
CurrentStream.Add(EndText.Value);
|
currentStream.Add(EndText.Value);
|
||||||
|
|
||||||
return letters;
|
return letters;
|
||||||
}
|
}
|
||||||
@@ -370,20 +382,20 @@
|
|||||||
|
|
||||||
var key = NameToken.Create($"I{imageKey++}");
|
var key = NameToken.Create($"I{imageKey++}");
|
||||||
|
|
||||||
resourcesDictionary[NameToken.Xobject] = xobjects.With(key, new IndirectReferenceToken(reference));
|
resourcesDictionary[NameToken.Xobject] = xobjects.With(key, reference);
|
||||||
|
|
||||||
CurrentStream.Add(Push.Value);
|
currentStream.Add(Push.Value);
|
||||||
// This needs to be the placement rectangle.
|
// This needs to be the placement rectangle.
|
||||||
CurrentStream.Add(new ModifyCurrentTransformationMatrix(new []
|
currentStream.Add(new ModifyCurrentTransformationMatrix(new []
|
||||||
{
|
{
|
||||||
(decimal)placementRectangle.Width, 0,
|
(decimal)placementRectangle.Width, 0,
|
||||||
0, (decimal)placementRectangle.Height,
|
0, (decimal)placementRectangle.Height,
|
||||||
(decimal)placementRectangle.BottomLeft.X, (decimal)placementRectangle.BottomLeft.Y
|
(decimal)placementRectangle.BottomLeft.X, (decimal)placementRectangle.BottomLeft.Y
|
||||||
}));
|
}));
|
||||||
CurrentStream.Add(new InvokeNamedXObject(key));
|
currentStream.Add(new InvokeNamedXObject(key));
|
||||||
CurrentStream.Add(Pop.Value);
|
currentStream.Add(Pop.Value);
|
||||||
|
|
||||||
return new AddedImage(reference, info.Width, info.Height);
|
return new AddedImage(reference.Data, info.Width, info.Height);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -411,16 +423,16 @@
|
|||||||
|
|
||||||
resourcesDictionary[NameToken.Xobject] = xobjects.With(key, new IndirectReferenceToken(image.Reference));
|
resourcesDictionary[NameToken.Xobject] = xobjects.With(key, new IndirectReferenceToken(image.Reference));
|
||||||
|
|
||||||
CurrentStream.Add(Push.Value);
|
currentStream.Add(Push.Value);
|
||||||
// This needs to be the placement rectangle.
|
// This needs to be the placement rectangle.
|
||||||
CurrentStream.Add(new ModifyCurrentTransformationMatrix(new[]
|
currentStream.Add(new ModifyCurrentTransformationMatrix(new[]
|
||||||
{
|
{
|
||||||
(decimal)placementRectangle.Width, 0,
|
(decimal)placementRectangle.Width, 0,
|
||||||
0, (decimal)placementRectangle.Height,
|
0, (decimal)placementRectangle.Height,
|
||||||
(decimal)placementRectangle.BottomLeft.X, (decimal)placementRectangle.BottomLeft.Y
|
(decimal)placementRectangle.BottomLeft.X, (decimal)placementRectangle.BottomLeft.Y
|
||||||
}));
|
}));
|
||||||
CurrentStream.Add(new InvokeNamedXObject(key));
|
currentStream.Add(new InvokeNamedXObject(key));
|
||||||
CurrentStream.Add(Pop.Value);
|
currentStream.Add(Pop.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -487,20 +499,20 @@
|
|||||||
|
|
||||||
var key = NameToken.Create($"I{imageKey++}");
|
var key = NameToken.Create($"I{imageKey++}");
|
||||||
|
|
||||||
resourcesDictionary[NameToken.Xobject] = xobjects.With(key, new IndirectReferenceToken(reference));
|
resourcesDictionary[NameToken.Xobject] = xobjects.With(key, reference);
|
||||||
|
|
||||||
CurrentStream.Add(Push.Value);
|
currentStream.Add(Push.Value);
|
||||||
// This needs to be the placement rectangle.
|
// This needs to be the placement rectangle.
|
||||||
CurrentStream.Add(new ModifyCurrentTransformationMatrix(new[]
|
currentStream.Add(new ModifyCurrentTransformationMatrix(new[]
|
||||||
{
|
{
|
||||||
(decimal)placementRectangle.Width, 0,
|
(decimal)placementRectangle.Width, 0,
|
||||||
0, (decimal)placementRectangle.Height,
|
0, (decimal)placementRectangle.Height,
|
||||||
(decimal)placementRectangle.BottomLeft.X, (decimal)placementRectangle.BottomLeft.Y
|
(decimal)placementRectangle.BottomLeft.X, (decimal)placementRectangle.BottomLeft.Y
|
||||||
}));
|
}));
|
||||||
CurrentStream.Add(new InvokeNamedXObject(key));
|
currentStream.Add(new InvokeNamedXObject(key));
|
||||||
CurrentStream.Add(Pop.Value);
|
currentStream.Add(Pop.Value);
|
||||||
|
|
||||||
return new AddedImage(reference, png.Width, png.Height);
|
return new AddedImage(reference.Data, png.Width, png.Height);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -509,13 +521,12 @@
|
|||||||
/// <param name="srcPage">Page to be copied</param>
|
/// <param name="srcPage">Page to be copied</param>
|
||||||
public void CopyFrom(Page srcPage)
|
public void CopyFrom(Page srcPage)
|
||||||
{
|
{
|
||||||
ContentStream destinationStream = null;
|
if (currentStream.Operations.Count > 0)
|
||||||
if (CurrentStream.Operations.Count > 0)
|
|
||||||
{
|
{
|
||||||
NewContentStreamAfter();
|
NewContentStreamAfter();
|
||||||
}
|
}
|
||||||
|
|
||||||
destinationStream = CurrentStream;
|
var destinationStream = currentStream;
|
||||||
|
|
||||||
if (!srcPage.Dictionary.TryGet(NameToken.Resources, srcPage.pdfScanner, out DictionaryToken srcResourceDictionary))
|
if (!srcPage.Dictionary.TryGet(NameToken.Resources, srcPage.pdfScanner, out DictionaryToken srcResourceDictionary))
|
||||||
{
|
{
|
||||||
@@ -547,7 +558,7 @@
|
|||||||
{
|
{
|
||||||
// It means that this type of resources doesn't currently exist in the page, so we can copy it
|
// It means that this type of resources doesn't currently exist in the page, so we can copy it
|
||||||
// with no problem
|
// with no problem
|
||||||
resourcesDictionary[nameToken] = documentBuilder.CopyToken(set.Value, srcPage.pdfScanner);
|
resourcesDictionary[nameToken] = documentBuilder.CopyToken(srcPage.pdfScanner, set.Value);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -604,7 +615,7 @@
|
|||||||
throw new PdfDocumentFormatException($"Expected a IndirectReferenceToken for the font, got a {fontSet.Value.GetType().Name}");
|
throw new PdfDocumentFormatException($"Expected a IndirectReferenceToken for the font, got a {fontSet.Value.GetType().Name}");
|
||||||
}
|
}
|
||||||
|
|
||||||
pageFontsDictionary.Add(NameToken.Create(fontName), documentBuilder.CopyToken(fontReferenceToken, srcPage.pdfScanner));
|
pageFontsDictionary.Add(NameToken.Create(fontName), documentBuilder.CopyToken(srcPage.pdfScanner, fontReferenceToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
resourcesDictionary[NameToken.Font] = new DictionaryToken(pageFontsDictionary);
|
resourcesDictionary[NameToken.Font] = new DictionaryToken(pageFontsDictionary);
|
||||||
@@ -657,7 +668,7 @@
|
|||||||
throw new PdfDocumentFormatException($"Expected a IndirectReferenceToken for the XObject, got a {xobjectSet.Value.GetType().Name}");
|
throw new PdfDocumentFormatException($"Expected a IndirectReferenceToken for the XObject, got a {xobjectSet.Value.GetType().Name}");
|
||||||
}
|
}
|
||||||
|
|
||||||
pageXobjectsDictionary.Add(NameToken.Create(xobjectName), documentBuilder.CopyToken(fontReferenceToken, srcPage.pdfScanner));
|
pageXobjectsDictionary.Add(NameToken.Create(xobjectName), documentBuilder.CopyToken(srcPage.pdfScanner, fontReferenceToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
resourcesDictionary[NameToken.Xobject] = new DictionaryToken(pageXobjectsDictionary);
|
resourcesDictionary[NameToken.Xobject] = new DictionaryToken(pageXobjectsDictionary);
|
||||||
@@ -741,30 +752,90 @@
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal interface IPageContentStream : IContentStream
|
||||||
|
{
|
||||||
|
bool ReadOnly { get; }
|
||||||
|
void Add(IGraphicsStateOperation operation);
|
||||||
|
IndirectReferenceToken Write(IPdfStreamWriter writer);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides access to the raw page data structures for advanced editing use cases.
|
/// Provides access to the raw page data structures for advanced editing use cases.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ContentStream
|
public interface IContentStream
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The operations making up the page content stream.
|
/// The operations making up the page content stream.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<IGraphicsStateOperation> Operations { get; }
|
List<IGraphicsStateOperation> Operations { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a new <see cref="ContentStream"/>.
|
|
||||||
/// </summary>
|
|
||||||
internal ContentStream()
|
|
||||||
{
|
|
||||||
Operations = new List<IGraphicsStateOperation>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void Add(IGraphicsStateOperation newOperation)
|
internal class DefaultContentStream : IPageContentStream
|
||||||
{
|
{
|
||||||
Operations.Add(newOperation);
|
private readonly List<IGraphicsStateOperation> operations;
|
||||||
|
|
||||||
|
public DefaultContentStream() : this(new List<IGraphicsStateOperation>())
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
public DefaultContentStream(List<IGraphicsStateOperation> operations)
|
||||||
|
{
|
||||||
|
this.operations = operations;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ReadOnly => false;
|
||||||
|
|
||||||
|
public void Add(IGraphicsStateOperation operation)
|
||||||
|
{
|
||||||
|
operations.Add(operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<IGraphicsStateOperation> Operations => operations;
|
||||||
|
|
||||||
|
public IndirectReferenceToken Write(IPdfStreamWriter writer)
|
||||||
|
{
|
||||||
|
using (var memoryStream = new MemoryStream())
|
||||||
|
{
|
||||||
|
foreach (var operation in operations)
|
||||||
|
{
|
||||||
|
operation.Write(memoryStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes = memoryStream.ToArray();
|
||||||
|
|
||||||
|
var stream = DataCompresser.CompressToStream(bytes);
|
||||||
|
|
||||||
|
return writer.WriteToken(stream);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal class CopiedContentStream : IPageContentStream
|
||||||
|
{
|
||||||
|
private readonly IndirectReferenceToken token;
|
||||||
|
public CopiedContentStream(IndirectReferenceToken indirectReferenceToken)
|
||||||
|
{
|
||||||
|
token = indirectReferenceToken;
|
||||||
|
}
|
||||||
|
public bool ReadOnly => true;
|
||||||
|
|
||||||
|
|
||||||
|
public IndirectReferenceToken Write(IPdfStreamWriter writer)
|
||||||
|
{
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Add(IGraphicsStateOperation operation)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException("Writing to a copied content stream is not supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<IGraphicsStateOperation> Operations =>
|
||||||
|
throw new NotSupportedException("Reading raw operations is not supported from a copied content stream.");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A key representing an image available to use for the current document builder.
|
/// 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)"/>.
|
/// Create it by adding an image to a page using <see cref="AddJpeg(byte[],PdfRectangle)"/>.
|
||||||
|
|||||||
@@ -11,17 +11,16 @@
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// This class would lazily flush all token. Allowing us to make changes to references without need to rewrite the whole stream
|
/// This class would lazily flush all token. Allowing us to make changes to references without need to rewrite the whole stream
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal class PdfStreamWriter : IDisposable
|
internal class PdfStreamWriter : IPdfStreamWriter
|
||||||
{
|
{
|
||||||
private readonly List<int> reservedNumbers = new List<int>();
|
private Dictionary<IndirectReference, long> offsets = new Dictionary<IndirectReference, long>();
|
||||||
|
private const decimal DefaultVersion = 1.2m;
|
||||||
|
private bool Initialized { get; set; }
|
||||||
|
private int CurrentNumber { get; set; } = 1;
|
||||||
|
|
||||||
private readonly Dictionary<IndirectReferenceToken, IToken> tokenReferences = new Dictionary<IndirectReferenceToken, IToken>();
|
public Stream Stream { get; set; }
|
||||||
|
|
||||||
public int CurrentNumber { get; private set; } = 1;
|
private bool DisposeStream { get; set; }
|
||||||
|
|
||||||
public Stream Stream { get; private set; }
|
|
||||||
|
|
||||||
public bool DisposeStream { get; set; }
|
|
||||||
|
|
||||||
public PdfStreamWriter(Stream baseStream, bool disposeStream = true)
|
public PdfStreamWriter(Stream baseStream, bool disposeStream = true)
|
||||||
{
|
{
|
||||||
@@ -34,13 +33,8 @@
|
|||||||
DisposeStream = disposeStream;
|
DisposeStream = disposeStream;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Flush(decimal version, IndirectReferenceToken catalogReference)
|
public void InitializePdf(decimal version)
|
||||||
{
|
{
|
||||||
if (catalogReference == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(catalogReference));
|
|
||||||
}
|
|
||||||
|
|
||||||
WriteString($"%PDF-{version.ToString("0.0", CultureInfo.InvariantCulture)}", Stream);
|
WriteString($"%PDF-{version.ToString("0.0", CultureInfo.InvariantCulture)}", Stream);
|
||||||
|
|
||||||
Stream.WriteText("%");
|
Stream.WriteText("%");
|
||||||
@@ -49,67 +43,6 @@
|
|||||||
Stream.WriteByte(196);
|
Stream.WriteByte(196);
|
||||||
Stream.WriteByte(210);
|
Stream.WriteByte(210);
|
||||||
Stream.WriteNewLine();
|
Stream.WriteNewLine();
|
||||||
|
|
||||||
var offsets = new Dictionary<IndirectReference, long>();
|
|
||||||
ObjectToken catalogToken = null;
|
|
||||||
foreach (var pair in tokenReferences)
|
|
||||||
{
|
|
||||||
var referenceToken = pair.Key;
|
|
||||||
var token = pair.Value;
|
|
||||||
var offset = Stream.Position;
|
|
||||||
var obj = new ObjectToken(offset, referenceToken.Data, token);
|
|
||||||
|
|
||||||
TokenWriter.WriteToken(obj, Stream);
|
|
||||||
|
|
||||||
offsets.Add(referenceToken.Data, offset);
|
|
||||||
|
|
||||||
if (catalogToken == null && referenceToken == catalogReference)
|
|
||||||
{
|
|
||||||
catalogToken = obj;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (catalogToken == null)
|
|
||||||
{
|
|
||||||
throw new Exception("Catalog object wasn't found");
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Support document information
|
|
||||||
TokenWriter.WriteCrossReferenceTable(offsets, catalogToken, Stream, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IndirectReferenceToken WriteToken(IToken token, int? reservedNumber = null)
|
|
||||||
{
|
|
||||||
if (!reservedNumber.HasValue)
|
|
||||||
{
|
|
||||||
return AddToken(token, CurrentNumber++);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!reservedNumbers.Remove(reservedNumber.Value))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("You can't reuse a reserved number");
|
|
||||||
}
|
|
||||||
|
|
||||||
// When we end up writing this token, all of his child would already have been added and checked for duplicate
|
|
||||||
return AddToken(token, reservedNumber.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void WriteToken(IndirectReferenceToken referenceToken, IToken token)
|
|
||||||
{
|
|
||||||
tokenReferences.Add(referenceToken, token);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int ReserveNumber()
|
|
||||||
{
|
|
||||||
var reserved = CurrentNumber;
|
|
||||||
reservedNumbers.Add(reserved);
|
|
||||||
CurrentNumber++;
|
|
||||||
return reserved;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IndirectReferenceToken ReserveNumberToken()
|
|
||||||
{
|
|
||||||
return new IndirectReferenceToken(new IndirectReference(ReserveNumber(), 0));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
@@ -124,13 +57,6 @@
|
|||||||
Stream = null;
|
Stream = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IndirectReferenceToken AddToken(IToken token, int reservedNumber)
|
|
||||||
{
|
|
||||||
var reference = new IndirectReference(reservedNumber, 0);
|
|
||||||
var referenceToken = new IndirectReferenceToken(reference);
|
|
||||||
tokenReferences.Add(referenceToken, token);
|
|
||||||
return referenceToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void WriteString(string text, Stream stream)
|
private static void WriteString(string text, Stream stream)
|
||||||
{
|
{
|
||||||
@@ -138,5 +64,43 @@
|
|||||||
stream.Write(bytes, 0, bytes.Length);
|
stream.Write(bytes, 0, bytes.Length);
|
||||||
stream.WriteNewLine();
|
stream.WriteNewLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IndirectReferenceToken WriteToken(IToken token)
|
||||||
|
{
|
||||||
|
if (!Initialized)
|
||||||
|
{
|
||||||
|
InitializePdf(DefaultVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
var ir = ReserveObjectNumber();
|
||||||
|
offsets.Add(ir.Data, Stream.Position);
|
||||||
|
var obj = new ObjectToken(Stream.Position, ir.Data, token);
|
||||||
|
TokenWriter.WriteToken(obj, Stream);
|
||||||
|
return ir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IndirectReferenceToken WriteToken(IToken token, IndirectReferenceToken indirectReference)
|
||||||
|
{
|
||||||
|
if (!Initialized)
|
||||||
|
{
|
||||||
|
InitializePdf(DefaultVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
offsets.Add(indirectReference.Data, Stream.Position);
|
||||||
|
var obj = new ObjectToken(Stream.Position, indirectReference.Data, token);
|
||||||
|
TokenWriter.WriteToken(obj, Stream);
|
||||||
|
return indirectReference;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IndirectReferenceToken ReserveObjectNumber()
|
||||||
|
{
|
||||||
|
return new IndirectReferenceToken(new IndirectReference(CurrentNumber++, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void CompletePdf(IndirectReferenceToken catalogReference, IndirectReferenceToken documentInformationReference=null)
|
||||||
|
{
|
||||||
|
TokenWriter.WriteCrossReferenceTable(offsets, catalogReference.Data, Stream, documentInformationReference?.Data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
src/UglyToad.PdfPig/Writer/PdfWriterType.cs
Normal file
21
src/UglyToad.PdfPig/Writer/PdfWriterType.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
namespace UglyToad.PdfPig.Writer
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Type of pdf writer to use.
|
||||||
|
/// </summary>
|
||||||
|
public enum PdfWriterType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Default output writer
|
||||||
|
/// </summary>
|
||||||
|
Default,
|
||||||
|
/// <summary>
|
||||||
|
/// De-duplicates objects while writing but requires keeping in memory reference.
|
||||||
|
/// </summary>
|
||||||
|
ObjectInMemoryDedup
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -131,7 +131,7 @@
|
|||||||
/// <param name="outputStream">The output stream to write to.</param>
|
/// <param name="outputStream">The output stream to write to.</param>
|
||||||
/// <param name="documentInformationReference">The object reference for the document information dictionary if present.</param>
|
/// <param name="documentInformationReference">The object reference for the document information dictionary if present.</param>
|
||||||
internal static void WriteCrossReferenceTable(IReadOnlyDictionary<IndirectReference, long> objectOffsets,
|
internal static void WriteCrossReferenceTable(IReadOnlyDictionary<IndirectReference, long> objectOffsets,
|
||||||
ObjectToken catalogToken,
|
IndirectReference catalogToken,
|
||||||
Stream outputStream,
|
Stream outputStream,
|
||||||
IndirectReference? documentInformationReference)
|
IndirectReference? documentInformationReference)
|
||||||
{
|
{
|
||||||
@@ -201,7 +201,7 @@
|
|||||||
{
|
{
|
||||||
// 1 for the free entry.
|
// 1 for the free entry.
|
||||||
{NameToken.Size, new NumericToken(objectOffsets.Count + 1)},
|
{NameToken.Size, new NumericToken(objectOffsets.Count + 1)},
|
||||||
{NameToken.Root, new IndirectReferenceToken(catalogToken.Number)},
|
{NameToken.Root, new IndirectReferenceToken(catalogToken)},
|
||||||
{NameToken.Id, identifier}
|
{NameToken.Id, identifier}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -225,6 +225,32 @@
|
|||||||
outputStream.Write(Eof, 0, Eof.Length);
|
outputStream.Write(Eof, 0, Eof.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes pre-serialized token as an object token to the output stream.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="objectNumber">Object number of the indirect object.</param>
|
||||||
|
/// <param name="generation">Generation of the indirect object.</param>
|
||||||
|
/// <param name="data">Pre-serialized object contents.</param>
|
||||||
|
/// <param name="outputStream">The stream to write the token to.</param>
|
||||||
|
internal static void WriteObject(long objectNumber, int generation, byte[] data, Stream outputStream)
|
||||||
|
{
|
||||||
|
WriteLong(objectNumber, outputStream);
|
||||||
|
WriteWhitespace(outputStream);
|
||||||
|
|
||||||
|
WriteInt(generation, outputStream);
|
||||||
|
WriteWhitespace(outputStream);
|
||||||
|
|
||||||
|
outputStream.Write(ObjStart, 0, ObjStart.Length);
|
||||||
|
WriteLineBreak(outputStream);
|
||||||
|
|
||||||
|
outputStream.Write(data, 0, data.Length);
|
||||||
|
|
||||||
|
WriteLineBreak(outputStream);
|
||||||
|
outputStream.Write(ObjEnd, 0, ObjEnd.Length);
|
||||||
|
|
||||||
|
WriteLineBreak(outputStream);
|
||||||
|
}
|
||||||
|
|
||||||
private static void WriteHex(HexToken hex, Stream stream)
|
private static void WriteHex(HexToken hex, Stream stream)
|
||||||
{
|
{
|
||||||
stream.WriteByte(HexStart);
|
stream.WriteByte(HexStart);
|
||||||
|
|||||||
135
src/UglyToad.PdfPig/Writer/WriterUtil.cs
Normal file
135
src/UglyToad.PdfPig/Writer/WriterUtil.cs
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
namespace UglyToad.PdfPig.Writer
|
||||||
|
{
|
||||||
|
using Content;
|
||||||
|
using Core;
|
||||||
|
using Parser.Parts;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using Tokenization.Scanner;
|
||||||
|
using Tokens;
|
||||||
|
internal class WriterUtil
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The purpose of this method is to resolve indirect reference. That mean copy the reference's content to the new document's stream
|
||||||
|
/// and replace the indirect reference with the correct/new one
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="writer">PDF stream writer</param>
|
||||||
|
/// <param name="tokenToCopy">Token to inspect for reference</param>
|
||||||
|
/// <param name="tokenScanner">scanner get the content from the original document</param>
|
||||||
|
/// <param name="referencesFromDocument">Map of previously copied tokens for original document.</param>
|
||||||
|
/// <param name="callstack">Call stack of indirect references</param>
|
||||||
|
/// <returns>A reference of the token that was copied. With all the reference updated</returns>
|
||||||
|
public static IToken CopyToken(IPdfStreamWriter writer, IToken tokenToCopy, IPdfTokenScanner tokenScanner,
|
||||||
|
IDictionary<IndirectReference, IndirectReferenceToken> referencesFromDocument, Dictionary<IndirectReference, IndirectReferenceToken> callstack=null)
|
||||||
|
{
|
||||||
|
callstack ??= new Dictionary<IndirectReference, IndirectReferenceToken>();
|
||||||
|
// This token need to be deep copied, because they could contain reference. So we have to update them.
|
||||||
|
switch (tokenToCopy)
|
||||||
|
{
|
||||||
|
case DictionaryToken dictionaryToken:
|
||||||
|
{
|
||||||
|
var newContent = new Dictionary<NameToken, IToken>();
|
||||||
|
foreach (var setPair in dictionaryToken.Data)
|
||||||
|
{
|
||||||
|
var name = setPair.Key;
|
||||||
|
var token = setPair.Value;
|
||||||
|
newContent.Add(NameToken.Create(name), CopyToken(writer, token, tokenScanner, referencesFromDocument, callstack));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DictionaryToken(newContent);
|
||||||
|
}
|
||||||
|
case ArrayToken arrayToken:
|
||||||
|
{
|
||||||
|
var newArray = new List<IToken>(arrayToken.Length);
|
||||||
|
foreach (var token in arrayToken.Data)
|
||||||
|
{
|
||||||
|
newArray.Add(CopyToken(writer, token, tokenScanner, referencesFromDocument, callstack));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ArrayToken(newArray);
|
||||||
|
}
|
||||||
|
case IndirectReferenceToken referenceToken:
|
||||||
|
{
|
||||||
|
if (referencesFromDocument.TryGetValue(referenceToken.Data, out var newReferenceToken))
|
||||||
|
{
|
||||||
|
return newReferenceToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callstack.ContainsKey(referenceToken.Data) && callstack[referenceToken.Data] == null)
|
||||||
|
{
|
||||||
|
newReferenceToken = writer.ReserveObjectNumber();
|
||||||
|
callstack[referenceToken.Data] = newReferenceToken;
|
||||||
|
referencesFromDocument.Add(referenceToken.Data, newReferenceToken);
|
||||||
|
return newReferenceToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
callstack.Add(referenceToken.Data, null);
|
||||||
|
|
||||||
|
// we add the token to referencesFromDocument to prevent stackoverflow on references cycles
|
||||||
|
// newReferenceToken = context.ReserveNumberToken();
|
||||||
|
// callstack.Add(newReferenceToken.Data.ObjectNumber);
|
||||||
|
// referencesFromDocument.Add(referenceToken.Data, newReferenceToken);
|
||||||
|
//
|
||||||
|
var tokenObject = DirectObjectFinder.Get<IToken>(referenceToken.Data, tokenScanner);
|
||||||
|
Debug.Assert(!(tokenObject is IndirectReferenceToken));
|
||||||
|
var result = CopyToken(writer, tokenObject, tokenScanner, referencesFromDocument, callstack);
|
||||||
|
|
||||||
|
if (callstack[referenceToken.Data] != null)
|
||||||
|
{
|
||||||
|
return writer.WriteToken(result, callstack[referenceToken.Data]);
|
||||||
|
}
|
||||||
|
|
||||||
|
newReferenceToken = writer.WriteToken(result);
|
||||||
|
referencesFromDocument.Add(referenceToken.Data, newReferenceToken);
|
||||||
|
return newReferenceToken;
|
||||||
|
}
|
||||||
|
case StreamToken streamToken:
|
||||||
|
{
|
||||||
|
var properties = CopyToken(writer, streamToken.StreamDictionary, tokenScanner, referencesFromDocument, callstack) as DictionaryToken;
|
||||||
|
Debug.Assert(properties != null);
|
||||||
|
|
||||||
|
var bytes = streamToken.Data;
|
||||||
|
return new StreamToken(properties, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
case ObjectToken _:
|
||||||
|
{
|
||||||
|
// Since we don't write token directly to the stream.
|
||||||
|
// We can't know the offset. Therefore the token would be invalid
|
||||||
|
throw new NotSupportedException("Copying a Object token is not supported");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenToCopy;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static IEnumerable<(DictionaryToken, List<DictionaryToken>)> WalkTree(PageTreeNode node, List<DictionaryToken> parents=null)
|
||||||
|
{
|
||||||
|
if (parents == null)
|
||||||
|
{
|
||||||
|
parents = new List<DictionaryToken>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.IsPage)
|
||||||
|
{
|
||||||
|
yield return (node.NodeDictionary, parents);
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
parents = parents.ToList();
|
||||||
|
parents.Add(node.NodeDictionary);
|
||||||
|
foreach (var child in node.Children)
|
||||||
|
{
|
||||||
|
foreach (var item in WalkTree(child, parents))
|
||||||
|
{
|
||||||
|
yield return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user