Pdf merger support copy links

This commit is contained in:
Yufei Huang
2023-04-21 11:23:16 +08:00
committed by BobLd
parent 147b8997cc
commit 3898f09a5f
6 changed files with 464 additions and 264 deletions

View File

@@ -1,244 +1,275 @@
namespace UglyToad.PdfPig.Tests.Writer
{
using Integration;
using PdfPig.Writer;
using System;
using System.Collections.Generic;
using System.IO;
using Xunit;
public class PdfMergerTests
{
[Fact]
public void CanMerge2SimpleDocuments()
{
var one = IntegrationHelpers.GetDocumentPath("Single Page Simple - from inkscape.pdf");
var two = IntegrationHelpers.GetDocumentPath("Single Page Simple - from open office.pdf");
var result = PdfMerger.Merge(one, two);
CanMerge2SimpleDocumentsAssertions(new MemoryStream(result), "Write something inInkscape", "I am a simple pdf.");
}
[Fact]
public void CanMerge2SimpleDocumentsIntoStream()
{
var one = IntegrationHelpers.GetDocumentPath("Single Page Simple - from inkscape.pdf");
var two = IntegrationHelpers.GetDocumentPath("Single Page Simple - from open office.pdf");
using (var outputStream = GetSelfDestructingNewFileStream("merge2"))
{
if (outputStream is null)
{
return;//we can't create a file in this test session
}
PdfMerger.Merge(one, two, outputStream);
CanMerge2SimpleDocumentsAssertions(outputStream, "Write something inInkscape", "I am a simple pdf.");
}
}
[Fact]
public void CanMerge2SimpleDocumentsReversed()
{
var one = IntegrationHelpers.GetDocumentPath("Single Page Simple - from open office.pdf");
var two = IntegrationHelpers.GetDocumentPath("Single Page Simple - from inkscape.pdf");
var result = PdfMerger.Merge(one, two);
CanMerge2SimpleDocumentsAssertions(new MemoryStream(result), "I am a simple pdf.", "Write something inInkscape");
}
internal static void CanMerge2SimpleDocumentsAssertions(Stream stream, string page1Text, string page2Text, bool checkVersion=true)
{
stream.Position = 0;
using (var document = PdfDocument.Open(stream, ParsingOptions.LenientParsingOff))
{
Assert.Equal(2, document.NumberOfPages);
if (checkVersion)
{
Assert.Equal(1.5m, document.Version);
}
var page1 = document.GetPage(1);
Assert.Equal(page1Text, page1.Text);
var page2 = document.GetPage(2);
Assert.Equal(page2Text, page2.Text);
}
}
[Fact]
public void RootNodePageCount()
{
var one = IntegrationHelpers.GetDocumentPath("Single Page Simple - from open office.pdf");
var two = IntegrationHelpers.GetDocumentPath("Single Page Simple - from inkscape.pdf");
var result = PdfMerger.Merge(one, two);
using (var document = PdfDocument.Open(result, ParsingOptions.LenientParsingOff))
{
Assert.Equal(2, document.NumberOfPages);
}
var oneBytes = File.ReadAllBytes(one);
var result2 = PdfMerger.Merge(new[] { result, oneBytes });
using (var document = PdfDocument.Open(result2, ParsingOptions.LenientParsingOff))
{
Assert.Equal(3, document.NumberOfPages);
}
}
[Fact]
public void ObjectCountLower()
{
var one = IntegrationHelpers.GetDocumentPath("Single Page Simple - from inkscape.pdf");
var two = IntegrationHelpers.GetDocumentPath("Single Page Simple - from inkscape.pdf");
var result = PdfMerger.Merge(one, two);
using (var document = PdfDocument.Open(result, ParsingOptions.LenientParsingOff))
{
Assert.Equal(2, document.NumberOfPages);
Assert.True(document.Structure.CrossReferenceTable.ObjectOffsets.Count <= 24,
"Expected object count to be lower than 24");
}
}
[Fact]
public void DedupsObjectsFromSameDoc()
{
var one = IntegrationHelpers.GetDocumentPath("Multiple Page - from Mortality Statistics.pdf");
var result = PdfMerger.Merge(new List<byte[]> { File.ReadAllBytes(one) }, new List<IReadOnlyList<int>> { new List<int> { 1, 2} });
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 CanMergeWithObjectStream()
{
var first = IntegrationHelpers.GetDocumentPath("Single Page Simple - from google drive.pdf");
var second = IntegrationHelpers.GetDocumentPath("Multiple Page - from Mortality Statistics.pdf");
var result = PdfMerger.Merge(first, second);
WriteFile(nameof(CanMergeWithObjectStream), result);
using (var document = PdfDocument.Open(result, ParsingOptions.LenientParsingOff))
{
Assert.Equal(7, document.NumberOfPages);
foreach (var page in document.GetPages())
{
Assert.NotNull(page.Text);
}
}
}
[Fact]
public void CanMergeWithSelection()
{
var first = IntegrationHelpers.GetDocumentPath("Multiple Page - from Mortality Statistics.pdf");
var contents = File.ReadAllBytes(first);
var toCopy = new[] {2, 1, 4, 3, 6, 5};
var result = PdfMerger.Merge(new [] { contents }, new [] { toCopy });
WriteFile(nameof(CanMergeWithSelection), result);
using (var existing = PdfDocument.Open(contents, ParsingOptions.LenientParsingOff))
using (var merged = PdfDocument.Open(result, ParsingOptions.LenientParsingOff))
{
Assert.Equal(6, merged.NumberOfPages);
namespace UglyToad.PdfPig.Tests.Writer
{
using Integration;
using PdfPig.Writer;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Xunit;
public class PdfMergerTests
{
[Fact]
public void CanMerge2SimpleDocuments()
{
var one = IntegrationHelpers.GetDocumentPath("Single Page Simple - from inkscape.pdf");
var two = IntegrationHelpers.GetDocumentPath("Single Page Simple - from open office.pdf");
var result = PdfMerger.Merge(one, two);
CanMerge2SimpleDocumentsAssertions(new MemoryStream(result), "Write something inInkscape", "I am a simple pdf.");
}
[Fact]
public void CanMerge2SimpleDocumentsIntoStream()
{
var one = IntegrationHelpers.GetDocumentPath("Single Page Simple - from inkscape.pdf");
var two = IntegrationHelpers.GetDocumentPath("Single Page Simple - from open office.pdf");
using (var outputStream = GetSelfDestructingNewFileStream("merge2"))
{
if (outputStream is null)
{
return;//we can't create a file in this test session
}
PdfMerger.Merge(one, two, outputStream);
CanMerge2SimpleDocumentsAssertions(outputStream, "Write something inInkscape", "I am a simple pdf.");
}
}
[Fact]
public void CanMerge2SimpleDocumentsReversed()
{
var one = IntegrationHelpers.GetDocumentPath("Single Page Simple - from open office.pdf");
var two = IntegrationHelpers.GetDocumentPath("Single Page Simple - from inkscape.pdf");
var result = PdfMerger.Merge(one, two);
CanMerge2SimpleDocumentsAssertions(new MemoryStream(result), "I am a simple pdf.", "Write something inInkscape");
}
internal static void CanMerge2SimpleDocumentsAssertions(Stream stream, string page1Text, string page2Text, bool checkVersion=true)
{
stream.Position = 0;
using (var document = PdfDocument.Open(stream, ParsingOptions.LenientParsingOff))
{
Assert.Equal(2, document.NumberOfPages);
if (checkVersion)
{
Assert.Equal(1.5m, document.Version);
}
var page1 = document.GetPage(1);
Assert.Equal(page1Text, page1.Text);
var page2 = document.GetPage(2);
Assert.Equal(page2Text, page2.Text);
}
}
[Fact]
public void RootNodePageCount()
{
var one = IntegrationHelpers.GetDocumentPath("Single Page Simple - from open office.pdf");
var two = IntegrationHelpers.GetDocumentPath("Single Page Simple - from inkscape.pdf");
var result = PdfMerger.Merge(one, two);
using (var document = PdfDocument.Open(result, ParsingOptions.LenientParsingOff))
{
Assert.Equal(2, document.NumberOfPages);
}
var oneBytes = File.ReadAllBytes(one);
var result2 = PdfMerger.Merge(new[] { result, oneBytes });
using (var document = PdfDocument.Open(result2, ParsingOptions.LenientParsingOff))
{
Assert.Equal(3, document.NumberOfPages);
}
}
[Fact]
public void ObjectCountLower()
{
var one = IntegrationHelpers.GetDocumentPath("Single Page Simple - from inkscape.pdf");
var two = IntegrationHelpers.GetDocumentPath("Single Page Simple - from inkscape.pdf");
var result = PdfMerger.Merge(one, two);
using (var document = PdfDocument.Open(result, ParsingOptions.LenientParsingOff))
{
Assert.Equal(2, document.NumberOfPages);
Assert.True(document.Structure.CrossReferenceTable.ObjectOffsets.Count <= 24,
"Expected object count to be lower than 24");
}
}
[Fact]
public void DedupsObjectsFromSameDoc()
{
var one = IntegrationHelpers.GetDocumentPath("Multiple Page - from Mortality Statistics.pdf");
var result = PdfMerger.Merge(new List<byte[]> { File.ReadAllBytes(one) }, new List<IReadOnlyList<int>> { new List<int> { 1, 2} });
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 CanMergeWithObjectStream()
{
var first = IntegrationHelpers.GetDocumentPath("Single Page Simple - from google drive.pdf");
var second = IntegrationHelpers.GetDocumentPath("Multiple Page - from Mortality Statistics.pdf");
var result = PdfMerger.Merge(first, second);
WriteFile(nameof(CanMergeWithObjectStream), result);
using (var document = PdfDocument.Open(result, ParsingOptions.LenientParsingOff))
{
Assert.Equal(7, document.NumberOfPages);
foreach (var page in document.GetPages())
{
Assert.NotNull(page.Text);
}
}
}
[Fact]
public void CanMergeWithSelection()
{
var first = IntegrationHelpers.GetDocumentPath("Multiple Page - from Mortality Statistics.pdf");
var contents = File.ReadAllBytes(first);
var toCopy = new[] {2, 1, 4, 3, 6, 5};
var result = PdfMerger.Merge(new [] { contents }, new [] { toCopy });
WriteFile(nameof(CanMergeWithSelection), result);
using (var existing = PdfDocument.Open(contents, ParsingOptions.LenientParsingOff))
using (var merged = PdfDocument.Open(result, ParsingOptions.LenientParsingOff))
{
Assert.Equal(6, merged.NumberOfPages);
for (var i =1;i<merged.NumberOfPages;i++)
{
Assert.Equal(
existing.GetPage(toCopy[i-1]).Text,
existing.GetPage(toCopy[i-1]).Text,
merged.GetPage(i).Text
);
}
}
}
}
}
[Fact]
public void CanMergeMultipleWithSelection()
{
var first = IntegrationHelpers.GetDocumentPath("Multiple Page - from Mortality Statistics.pdf");
var second = IntegrationHelpers.GetDocumentPath("Old Gutnish Internet Explorer.pdf");
var result = PdfMerger.Merge(new[] { File.ReadAllBytes(first), File.ReadAllBytes(second) }, new[] { new[] { 2, 1, 4, 3, 6, 5 }, new []{ 3, 2, 1 } });
WriteFile(nameof(CanMergeMultipleWithSelection), result);
using (var document = PdfDocument.Open(result, ParsingOptions.LenientParsingOff))
{
Assert.Equal(9, document.NumberOfPages);
foreach (var page in document.GetPages())
{
Assert.NotNull(page.Text);
}
}
}
private static void WriteFile(string name, byte[] bytes)
{
try
{
if (!Directory.Exists("Merger"))
{
Directory.CreateDirectory("Merger");
}
var output = Path.Combine("Merger", $"{name}.pdf");
File.WriteAllBytes(output, bytes);
}
catch
{
// ignored.
}
}
private static FileStream GetSelfDestructingNewFileStream(string name)
{
try
{
if (!Directory.Exists("Merger"))
{
Directory.CreateDirectory("Merger");
}
var output = Path.Combine("Merger", $"{name}.pdf");
return File.Create(output, 4096, FileOptions.DeleteOnClose);
}
catch
{
return null;
}
}
[Fact]
public void NoStackoverflow()
{
try
{
var bytes = PdfMerger.Merge(IntegrationHelpers.GetDocumentPath("68-1990-01_A.pdf"));
using (var document = PdfDocument.Open(bytes, ParsingOptions.LenientParsingOff))
{
Assert.Equal(45, document.NumberOfPages);
}
}
catch (StackOverflowException)
{
Assert.True(false);
}
}
}
}
[Fact]
public void CanMergeMultipleWithSelection()
{
var first = IntegrationHelpers.GetDocumentPath("Multiple Page - from Mortality Statistics.pdf");
var second = IntegrationHelpers.GetDocumentPath("Old Gutnish Internet Explorer.pdf");
var result = PdfMerger.Merge(new[] { File.ReadAllBytes(first), File.ReadAllBytes(second) }, new[] { new[] { 2, 1, 4, 3, 6, 5 }, new []{ 3, 2, 1 } });
WriteFile(nameof(CanMergeMultipleWithSelection), result);
using (var document = PdfDocument.Open(result, ParsingOptions.LenientParsingOff))
{
Assert.Equal(9, document.NumberOfPages);
foreach (var page in document.GetPages())
{
Assert.NotNull(page.Text);
}
}
}
[Fact]
public void CanMergeWithLinks()
{
var test = IntegrationHelpers.GetDocumentPath("outline.pdf");
var result = PdfMerger.Merge(new[] { File.ReadAllBytes(test), File.ReadAllBytes(test) });
WriteFile(nameof(CanMergeWithLinks), result);
using (var document = PdfDocument.Open(result, ParsingOptions.LenientParsingOff))
{
Assert.Equal(2, document.GetPages().Sum(
page => page.ExperimentalAccess.GetAnnotations().Count(x => x.Type == Annotations.AnnotationType.Link)));
}
}
[Fact]
public void CanMergeWithLinksWithSelection()
{
var test = IntegrationHelpers.GetDocumentPath("outline.pdf");
var result = PdfMerger.Merge(new[] { File.ReadAllBytes(test), File.ReadAllBytes(test) }, new[] { new[] { 2, 1 }, new[] { 3, 1 } });
WriteFile(nameof(CanMergeWithLinksWithSelection), result);
using (var document = PdfDocument.Open(result, ParsingOptions.LenientParsingOff))
{
Assert.Equal(1, document.GetPages().Sum(
page => page.ExperimentalAccess.GetAnnotations().Count(x => x.Type == Annotations.AnnotationType.Link)));
}
}
private static void WriteFile(string name, byte[] bytes)
{
try
{
if (!Directory.Exists("Merger"))
{
Directory.CreateDirectory("Merger");
}
var output = Path.Combine("Merger", $"{name}.pdf");
File.WriteAllBytes(output, bytes);
}
catch
{
// ignored.
}
}
private static FileStream GetSelfDestructingNewFileStream(string name)
{
try
{
if (!Directory.Exists("Merger"))
{
Directory.CreateDirectory("Merger");
}
var output = Path.Combine("Merger", $"{name}.pdf");
return File.Create(output, 4096, FileOptions.DeleteOnClose);
}
catch
{
return null;
}
}
[Fact]
public void NoStackoverflow()
{
try
{
var bytes = PdfMerger.Merge(IntegrationHelpers.GetDocumentPath("68-1990-01_A.pdf"));
using (var document = PdfDocument.Open(bytes, ParsingOptions.LenientParsingOff))
{
Assert.Equal(45, document.NumberOfPages);
}
}
catch (StackOverflowException)
{
Assert.True(false);
}
}
}
}

View File

@@ -142,7 +142,7 @@
}
}
private PdfAction GetAction(DictionaryToken annotationDictionary)
internal PdfAction GetAction(DictionaryToken annotationDictionary)
{
// If this annotation returns a direct destination, turn it into a GoTo action.
if (DestinationProvider.TryGetDestination(annotationDictionary,

View File

@@ -18,7 +18,7 @@
/// </summary>
public class Page
{
private readonly AnnotationProvider annotationProvider;
internal readonly AnnotationProvider annotationProvider;
internal readonly IPdfTokenScanner pdfScanner;
private readonly Lazy<string> textLazy;

View File

@@ -5,15 +5,16 @@ namespace UglyToad.PdfPig.Writer
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using Content;
using Core;
using Fonts;
using PdfPig.Actions;
using PdfPig.Fonts.TrueType;
using PdfPig.Fonts.Standard14Fonts;
using PdfPig.Fonts.TrueType.Parser;
using PdfPig.Outline;
using PdfPig.Outline.Destinations;
using System.Runtime.CompilerServices;
using Tokenization.Scanner;
using Tokens;
@@ -306,6 +307,18 @@ namespace UglyToad.PdfPig.Writer
/// <param name="pageNumber">Page to copy.</param>
/// <returns>A builder for editing the page.</returns>
public PdfPageBuilder AddPage(PdfDocument document, int pageNumber)
{
return AddPage(document, pageNumber, null);
}
/// <summary>
/// Add a new page with the specified size, this page will be included in the output when <see cref="Build"/> is called.
/// </summary>
/// <param name="document">Source document.</param>
/// <param name="pageNumber">Page to copy.</param>
/// <param name="copyLink">If set, links are copied based on the result of the delegate.</param>
/// <returns>A builder for editing the page.</returns>
public PdfPageBuilder AddPage(PdfDocument document, int pageNumber, Func<PdfAction, PdfAction> copyLink)
{
if (!existingCopies.TryGetValue(document.Structure.TokenScanner, out var refs))
{
@@ -335,6 +348,8 @@ namespace UglyToad.PdfPig.Writer
throw new KeyNotFoundException($"Page {pageNumber} was not found in the source document.");
}
var page = document.GetPage(pageNumber);
// copy content streams
var streams = new List<PdfPageBuilder.CopiedContentStream>();
if (pageInfo.Page.TryGet(NameToken.Contents, out IToken contentsToken))
@@ -365,6 +380,7 @@ namespace UglyToad.PdfPig.Writer
// manually copy page dict / resources as we need to modify some
var copiedPageDict = new Dictionary<NameToken, IToken>();
var links = new List<(DictionaryToken token, PdfAction action)>();
Dictionary<NameToken, IToken> resources = new Dictionary<NameToken, IToken>();
// just put all parent resources into new page
@@ -424,9 +440,7 @@ namespace UglyToad.PdfPig.Writer
continue;
}
// -> ignore links to resolve issues with refencing non-existing pages
// at some point should add support for copying the links if the
// pages are copied as well but for now just fix corruption
// if copyLink is unset, ignore links to resolve issues with refencing non-existing pages
var toAdd = new List<IToken>();
foreach (var annot in arr.Data)
{
@@ -436,10 +450,38 @@ namespace UglyToad.PdfPig.Writer
// malformed
continue;
}
if (tk.TryGet(NameToken.Subtype, out var st) && st is NameToken nm && nm == NameToken.Link)
{
// link -> ignore
continue;
if (copyLink == null)
{
// ingore link if don't know how to copy
continue;
}
var link = page.annotationProvider.GetAction(tk);
if (link == null)
{
// ignore unknown link actions
continue;
}
var copiedLink = copyLink(link);
if (copiedLink == null)
{
// ignore if caller wants to skip the link
continue;
}
if (copiedLink != link)
{
// defer to write links when all pages are added
var copiedToken = (DictionaryToken)WriterUtil.CopyToken(context, tk, document.Structure.TokenScanner, refs);
links.Add((copiedToken, copiedLink));
continue;
}
// copy as is if caller returns the same link
}
toAdd.Add(WriterUtil.CopyToken(context, tk, document.Structure.TokenScanner, refs));
}
@@ -454,7 +496,7 @@ namespace UglyToad.PdfPig.Writer
copiedPageDict[NameToken.Resources] = new DictionaryToken(resources);
var builder = new PdfPageBuilder(pages.Count + 1, this, streams, copiedPageDict);
var builder = new PdfPageBuilder(pages.Count + 1, this, streams, copiedPageDict, links);
pages[builder.PageNumber] = builder;
return builder;
@@ -556,7 +598,7 @@ namespace UglyToad.PdfPig.Writer
}
int leafNum = 0;
var pageReferences = new Dictionary<int, IndirectReferenceToken>(pages.Count);
var pageReferences = pages.ToDictionary(p => p.Key, p => context.ReserveObjectNumber());
foreach (var page in pages)
{
@@ -601,8 +643,25 @@ namespace UglyToad.PdfPig.Writer
}
context.AttemptDeduplication = prev;
pageReferences[page.Key] = context.WriteToken(new DictionaryToken(pageDictionary));
leafChildren[leafNum].Add(pageReferences[page.Key]);
// write links
if (page.Value.links != null && page.Value.links.Count > 0)
{
var annots = new List<IToken>();
if (pageDictionary.TryGetValue(NameToken.Annots, out var existingAnnots))
{
annots.AddRange(((ArrayToken)existingAnnots).Data);
}
foreach (var (token, action) in page.Value.links)
{
annots.Add(CreateLinkAnnotationToken(token, action, pageReferences));
}
pageDictionary[NameToken.Annots] = new ArrayToken(annots);
}
leafChildren[leafNum].Add(context.WriteToken(new DictionaryToken(pageDictionary), pageReferences[page.Key]));
if (leafChildren[leafNum].Count >= desiredLeafSize)
{
@@ -816,12 +875,7 @@ namespace UglyToad.PdfPig.Writer
switch (node)
{
case DocumentBookmarkNode documentBookmarkNode:
if (!pageReferences.TryGetValue(documentBookmarkNode.PageNumber, out var pageReference))
{
throw new KeyNotFoundException($"Page {documentBookmarkNode.PageNumber} was not found in the source document.");
}
data[NameToken.Dest] = CreateExplicitDestinationToken(documentBookmarkNode.Destination, pageReference);
data[NameToken.Dest] = CreateExplicitDestinationToken(documentBookmarkNode.Destination, pageReferences);
break;
case UriBookmarkNode uriBookmarkNode:
@@ -842,8 +896,13 @@ namespace UglyToad.PdfPig.Writer
return childObjectNumbers;
}
private static ArrayToken CreateExplicitDestinationToken(ExplicitDestination destination, IndirectReferenceToken page)
private static ArrayToken CreateExplicitDestinationToken(ExplicitDestination destination, Dictionary<int, IndirectReferenceToken> pageReferences)
{
if (!pageReferences.TryGetValue(destination.PageNumber, out var page))
{
throw new KeyNotFoundException($"Page {destination.PageNumber} was not found in the source document.");
}
switch (destination.Type)
{
case ExplicitDestinationType.XyzCoordinates:
@@ -917,6 +976,65 @@ namespace UglyToad.PdfPig.Writer
}
}
private static DictionaryToken CreateLinkAnnotationToken(DictionaryToken token, PdfAction action, Dictionary<int, IndirectReferenceToken> pageReferences)
{
var data = new Dictionary<NameToken, IToken>();
foreach (var item in token.Data)
{
var nameToken = NameToken.Create(item.Key);
if (nameToken == NameToken.A || nameToken == NameToken.Dest)
{
// ignore /A and /Dest
continue;
}
data[nameToken] = item.Value;
}
data[NameToken.A] = CreateActionToken(action, pageReferences);
return new DictionaryToken(data);
}
private static DictionaryToken CreateActionToken(PdfAction action, Dictionary<int, IndirectReferenceToken> pageReferences)
{
switch (action)
{
case UriAction uriAction:
return new DictionaryToken(new Dictionary<NameToken, IToken>()
{
[NameToken.S] = NameToken.Uri,
[NameToken.Uri] = new StringToken(uriAction.Uri),
});
case GoToAction goToAction:
return new DictionaryToken(new Dictionary<NameToken, IToken>()
{
[NameToken.S] = NameToken.GoTo,
[NameToken.D] = CreateExplicitDestinationToken(goToAction.Destination, pageReferences),
});
case GoToEAction goToEAction:
return new DictionaryToken(new Dictionary<NameToken, IToken>()
{
[NameToken.S] = NameToken.GoToE,
[NameToken.F] = new StringToken(goToEAction.FileSpecification),
[NameToken.D] = CreateExplicitDestinationToken(goToEAction.Destination, pageReferences),
});
case GoToRAction goToRAction:
return new DictionaryToken(new Dictionary<NameToken, IToken>()
{
[NameToken.S] = NameToken.GoToR,
[NameToken.F] = new StringToken(goToRAction.Filename),
[NameToken.D] = CreateExplicitDestinationToken(goToRAction.Destination, pageReferences),
});
default:
throw new NotSupportedException($"{action.GetType().Name} is not a supported PDF action type.");
}
}
internal class FontStored
{
[NotNull]

View File

@@ -6,6 +6,8 @@
using Filters;
using Logging;
using System.Linq;
using UglyToad.PdfPig.Actions;
using UglyToad.PdfPig.Outline.Destinations;
/// <summary>
/// Merges PDF documents into each other.
@@ -151,19 +153,63 @@
pages = pagesBundle[fileIndex];
}
var basePageNumber = document.Pages.Count;
if (pages == null)
{
for (var i = 1; i <= existing.NumberOfPages; i++)
{
document.AddPage(existing, i);
}
} else
{
foreach (var i in pages)
{
document.AddPage(existing, i);
document.AddPage(existing, i, link => CopyLink(link, n => basePageNumber + n));
}
}
else
{
var pageNumbers = new Dictionary<int, int>();
for (var i = 0; i < pages.Count; i++)
{
pageNumbers[pages[i]] = basePageNumber + i + 1;
}
foreach (var i in pages)
{
document.AddPage(existing, i, link => CopyLink(
link, n => pageNumbers.TryGetValue(n, out var pageNumber) ? pageNumber : null));
}
}
}
}
PdfAction CopyLink(PdfAction action, Func<int, int?> getPageNumber)
{
var link = action as AbstractGoToAction;
if (link == null)
{
// copy the link if it is not a link to PDF documents
return action;
}
var newPageNumber = getPageNumber(link.Destination.PageNumber);
if (newPageNumber == null)
{
// ignore the link if the target page does not exist in the PDF document
return null;
}
var newDestination = new ExplicitDestination(newPageNumber.Value, link.Destination.Type, link.Destination.Coordinates);
switch (action)
{
case GoToAction goToAction:
return new GoToAction(newDestination);
case GoToEAction goToEAction:
return new GoToEAction(newDestination, goToEAction.FileSpecification);
case GoToRAction goToRAction:
return new GoToRAction(newDestination, goToRAction.Filename);
default:
return action;
}
}
}

View File

@@ -21,6 +21,7 @@
using Tokens;
using Graphics.Operations.PathPainting;
using Images.Png;
using UglyToad.PdfPig.Actions;
/// <summary>
/// A builder used to add construct a page in a PDF document.
@@ -36,6 +37,9 @@
// streams
internal readonly List<IPageContentStream> contentStreams;
private IPageContentStream currentStream;
// links to be resolved when all page references are available
internal readonly List<(DictionaryToken token, PdfAction action)> links;
// maps fonts added using PdfDocumentBuilder to page font names
private readonly Dictionary<Guid, NameToken> documentFonts = new Dictionary<Guid, NameToken>();
@@ -80,16 +84,17 @@
}
internal PdfPageBuilder(int number, PdfDocumentBuilder documentBuilder, IEnumerable<CopiedContentStream> copied,
Dictionary<NameToken, IToken> pageDict)
Dictionary<NameToken, IToken> pageDict, List<(DictionaryToken token, PdfAction action)> links)
{
this.documentBuilder = documentBuilder ?? throw new ArgumentNullException(nameof(documentBuilder));
this.links = links;
PageNumber = number;
pageDictionary = pageDict;
contentStreams = new List<IPageContentStream>();
contentStreams.AddRange(copied);
currentStream = new DefaultContentStream();
contentStreams.Add(currentStream);
}
}
/// <summary>
/// Allow to append a new content stream before the current one and select it