-
Notifications
You must be signed in to change notification settings - Fork 327
Xref table expose #1292
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Xref table expose #1292
Changes from 7 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
fc4ec96
Added prev to trailer dictionary
vafle228 378426b
Trailer dictionary expose
vafle228 c72d8d4
CrossReferenceTable final expose
vafle228 7775d69
Added AdvancedMerge example
vafle228 446deba
Added content logic here too
vafle228 70eb343
Review fix
vafle228 b881402
Extend example (also add into Program.cs)
vafle228 67e0fa5
Fixed example
vafle228 3aca79a
Integration test
vafle228 9976249
Summary
vafle228 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,205 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.IO; | ||
| using System.Linq; | ||
| using UglyToad.PdfPig; | ||
| using UglyToad.PdfPig.Core; | ||
| using UglyToad.PdfPig.Tokens; | ||
| using UglyToad.PdfPig.Writer; | ||
|
|
||
| namespace UglyToad.Examples; | ||
|
|
||
| public class AdvancedMerge | ||
| { | ||
| public static void Run(Stream input1, Stream output) | ||
| { | ||
| using var pdf = PdfDocument.Open(input1); | ||
| var pdfObjects = pdf.Structure | ||
| .CrossReferenceTable | ||
| .ObjectOffsets | ||
| .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); | ||
|
|
||
| if (!pdf.Structure.Catalog.CatalogDictionary.TryGet<IndirectReferenceToken>(NameToken.Pages, out var pages)) | ||
| throw new ArgumentException("No pages reference were found"); | ||
|
|
||
| if (ResolveIndirect(pdf, pages) is not DictionaryToken pagesObj) | ||
| throw new ArgumentException("No pages object were found"); | ||
|
|
||
| // Assume, we have only 1 page in here | ||
| if (!pagesObj.TryGet(NameToken.Kids, out ArrayToken kids) || kids.Length != 1) | ||
| throw new ArgumentException("Invalid catalog dictionary"); | ||
|
|
||
| var kidReference = kids.Data[0] as IndirectReferenceToken; | ||
| if (ResolveIndirect(pdf, kidReference) is not DictionaryToken pageObj) | ||
| throw new ArgumentException("Invalid catalog dictionary"); | ||
|
|
||
|
|
||
| // Skip all pdf meta structure objects | ||
| var skippedRefs = new HashSet<IndirectReference> | ||
| { | ||
| pages.Data, // Pages | ||
| kidReference!.Data, // Page | ||
| pdf.Structure.Trailer.Root, // Catalog | ||
| }; | ||
|
|
||
| // Skip all refs from "skippedRefs" and order it by object number | ||
| var oldRefs = pdf.Structure.CrossReferenceTable.ObjectOffsets.Keys | ||
| .Where(k => !skippedRefs.Contains(k)) | ||
| .OrderBy(k => k.ObjectNumber) | ||
| .ToList(); | ||
|
|
||
| // Building refs map, to rebind old objects to their new values | ||
| var refMap = new Dictionary<IndirectReference, IndirectReference>(); | ||
| var currentObjectNumber = pdf.Structure.Trailer.Size; | ||
| foreach (var oldRef in oldRefs) | ||
| { | ||
| var newRef = new IndirectReference(currentObjectNumber++, 0); | ||
| refMap[oldRef] = newRef; | ||
| } | ||
|
|
||
| // Here you can extract page content and bind it in output page document | ||
| IndirectReference? root = null; | ||
| if (pageObj.TryGet<IndirectReferenceToken>(NameToken.Contents, out var contentObj)) | ||
| { | ||
| using var outputPdf = PdfDocument.Open(output); | ||
| var lastPageRef = FindLastPage(outputPdf); | ||
|
|
||
| if (ResolveIndirect(outputPdf, lastPageRef) is not DictionaryToken outputPage) | ||
| throw new ArgumentException("Invalid catalog dictionary"); | ||
| root = outputPdf.Structure.Trailer.Root; | ||
|
|
||
| IReadOnlyList<IToken> newContents = [new IndirectReferenceToken(refMap[contentObj.Data])]; | ||
|
|
||
| if (outputPage.TryGet<ArrayToken>(NameToken.Contents, out var oldContents)) | ||
| newContents = newContents.Concat(oldContents.Data).ToList(); | ||
|
|
||
| output.Seek(0, SeekOrigin.End); | ||
|
|
||
| var xrefLocation = XrefLocation.File(output.Position); | ||
| var newPageObject = outputPage.With(NameToken.Contents, new ArrayToken(newContents)); | ||
| TokenWriter.Instance.WriteToken(new ObjectToken(xrefLocation, lastPageRef.Data, newPageObject), output); | ||
|
|
||
| pdfObjects[lastPageRef.Data] = xrefLocation; | ||
| } | ||
|
|
||
| foreach (var oldRef in oldRefs) | ||
| { | ||
| var newObjRef = refMap[oldRef]; | ||
| var newXref = XrefLocation.File(output.Position); | ||
| var token = ResolveIndirect(pdf, oldRef); | ||
| var updatedToken = ReplaceReferences(token, refMap); | ||
|
|
||
| pdfObjects[newObjRef] = newXref; | ||
| output.Seek(0, SeekOrigin.End); | ||
| TokenWriter.Instance.WriteToken(new ObjectToken(newXref, newObjRef, updatedToken), output); | ||
| } | ||
|
|
||
| // Writer new xref table | ||
| output.Seek(0, SeekOrigin.End); | ||
| TokenWriter.Instance.WriteCrossReferenceTable( | ||
| pdfObjects.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Value1), | ||
| root ?? pdf.Structure.Trailer.Root, | ||
| output, | ||
| null, | ||
| pdf.Structure.Trailer.PreviousCrossReferenceOffset); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Recursively replaces IndirectReferenceToken in the token tree according to the map. | ||
| /// </summary> | ||
| private static IToken ReplaceReferences(IToken token, Dictionary<IndirectReference, IndirectReference> mapping) | ||
| { | ||
| return token switch | ||
| { | ||
| IndirectReferenceToken irt => mapping.TryGetValue(irt.Data, out var newRef) ? new IndirectReferenceToken(newRef) : token, | ||
| DictionaryToken dict => ReplaceDictionary(dict, mapping), | ||
| ArrayToken arr => ReplaceArray(arr, mapping), | ||
| StreamToken stream => ReplaceStream(stream, mapping), | ||
| _ => token | ||
| }; | ||
| } | ||
|
|
||
| private static DictionaryToken ReplaceDictionary(DictionaryToken original, Dictionary<IndirectReference, IndirectReference> mapping) | ||
| { | ||
| var newDict = new Dictionary<NameToken, IToken>(original.Data.Count); | ||
| foreach (var kvp in original.Data) | ||
| { | ||
| newDict[NameToken.Create(kvp.Key)] = ReplaceReferences(kvp.Value, mapping); | ||
| } | ||
| return new DictionaryToken(newDict); | ||
| } | ||
|
|
||
| private static ArrayToken ReplaceArray(ArrayToken original, Dictionary<IndirectReference, IndirectReference> mapping) | ||
| { | ||
| var newData = new IToken[original.Length]; | ||
| for (int i = 0; i < original.Length; i++) | ||
| { | ||
| newData[i] = ReplaceReferences(original.Data[i], mapping); | ||
| } | ||
| return new ArrayToken(newData); | ||
| } | ||
|
|
||
| private static StreamToken ReplaceStream(StreamToken original, Dictionary<IndirectReference, IndirectReference> mapping) | ||
| { | ||
| var updatedDict = ReplaceDictionary(original.StreamDictionary, mapping); | ||
| // We create a new StreamToken with the replaced dictionary, preserving the original byte stream. | ||
| return new StreamToken(updatedDict, original.Data); | ||
| } | ||
|
|
||
| private const int MaxIndirectResolutionDepth = 32; | ||
|
|
||
| private static IndirectReferenceToken FindLastPage(PdfDocument pdf) | ||
| { | ||
| if (!pdf.Structure.Catalog.CatalogDictionary.TryGet<IndirectReferenceToken>(NameToken.Pages, out var pagesRef)) | ||
| throw new ArgumentException("No pages were found in the input document."); | ||
|
|
||
| if (ResolveIndirect(pdf, pagesRef) is not DictionaryToken pages) | ||
| throw new ArgumentException("No pages were found in the input file."); | ||
|
|
||
| if (!pages.TryGet<ArrayToken>(NameToken.Kids, out var kids)) | ||
| throw new ArgumentException("No pages were found in the input document."); | ||
|
|
||
| return FindLastPage(pdf, kids); | ||
| } | ||
|
|
||
| private static IndirectReferenceToken FindLastPage(PdfDocument pdf, ArrayToken pageTree) | ||
| { | ||
| while (true) | ||
| { | ||
| if (pageTree.Length == 0) | ||
| throw new ArgumentException("No leaf in page tree"); | ||
|
|
||
| var root = pageTree.Data.Last()!; | ||
| if (ResolveIndirect(pdf, root) is not DictionaryToken newRoot) | ||
| throw new ArgumentException("Indirect page tree"); | ||
|
|
||
| if (newRoot.Data[NameToken.Type] is not NameToken type) | ||
| throw new ArgumentException("Indirect page tree"); | ||
|
|
||
| if (type.Data == NameToken.Page) | ||
| return (root as IndirectReferenceToken)!; | ||
| pageTree = (newRoot.Data[NameToken.Kids] as ArrayToken)!; | ||
| } | ||
| } | ||
|
|
||
| private static IToken ResolveIndirect(PdfDocument doc, IndirectReference reference) | ||
| { | ||
| return ResolveIndirect(doc, new IndirectReferenceToken(reference)); | ||
| } | ||
|
|
||
| private static IToken ResolveIndirect(PdfDocument doc, IToken token) | ||
| { | ||
| var depth = 0; | ||
| while (token is IndirectReferenceToken ir) | ||
| { | ||
| if (++depth > MaxIndirectResolutionDepth) | ||
| throw new ArgumentException( | ||
| "Cyclic or excessively deep indirect reference in PDF signature dictionary."); | ||
|
|
||
| var obj = doc.Structure.GetObject(ir.Data); | ||
| token = obj.Data ?? throw new ArgumentException("Failed to parse PDF digital signature."); | ||
| } | ||
|
|
||
| return token; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -47,12 +47,24 @@ public static void Main() | |
| ("Advance text extraction using layout analysis algorithms", | ||
| () => AdvancedTextExtraction.Run(Path.Combine(filesDirectory, "ICML03-081.pdf"))) | ||
| }, | ||
| { | ||
| 8, | ||
| {8, | ||
| ("Extract Words with newline detection (example with algorithm). Issue 512", | ||
| () => OpenDocumentAndExtractWords.Run(Path.Combine(filesDirectory, "OPEN.RABBIT.ENGLISH.LOP.pdf"))) | ||
| } | ||
| }; | ||
| } , | ||
| {9, | ||
| ("Advanced pdf merge, using low level pdf tools, like trailer dictionary and xref table", | ||
| () => | ||
| { | ||
| using var input2 = File.Open(Path.Combine(filesDirectory, "EmptyPdf.pdf"), FileMode.Open); | ||
| using var output = new FileStream(Path.Combine(filesDirectory, "AdvancedMergeResult.pdf"), FileMode.Create); | ||
| using var input = File.Open(Path.Combine(filesDirectory, "Single Page Simple - from google drive.pdf"), FileMode.Open); | ||
|
|
||
| input2.CopyToAsync(output); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| output.Seek(0, SeekOrigin.Begin); | ||
| AdvancedMerge.Run(input, output); | ||
| }) | ||
| } | ||
| }; | ||
|
|
||
| var choices = string.Join(Environment.NewLine, examples.Select(x => $"{x.Key}: {x.Value.name}")); | ||
|
|
||
|
|
||
Binary file not shown.
Binary file not shown.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's not write the output document to the integration tests folder.
new FileStream("AdvancedMergeResult.pdf")should be enough