diff --git a/src/UglyToad.PdfPig/Writer/Copier/IObjectCopier.cs b/src/UglyToad.PdfPig/Writer/Copier/IObjectCopier.cs new file mode 100644 index 00000000..ce24aaad --- /dev/null +++ b/src/UglyToad.PdfPig/Writer/Copier/IObjectCopier.cs @@ -0,0 +1,24 @@ +namespace UglyToad.PdfPig.Writer.Copier +{ + using System; + using Tokens; + + /// + /// An interface for copying token + /// + public interface IObjectCopier + { + /// + /// Copy the token to the destination stream + /// + /// Token to copy + /// Function to resolve indirect reference identified in the token to copy + /// + public IToken CopyObject(IToken sourceToken, Func tokenScanner); + + /// + /// Clear the references of the previously copied object + /// + public void ClearReference(); + } +} \ No newline at end of file diff --git a/src/UglyToad.PdfPig/Writer/Copier/MultiCopier.cs b/src/UglyToad.PdfPig/Writer/Copier/MultiCopier.cs new file mode 100644 index 00000000..3e626253 --- /dev/null +++ b/src/UglyToad.PdfPig/Writer/Copier/MultiCopier.cs @@ -0,0 +1,75 @@ +namespace UglyToad.PdfPig.Writer.Copier +{ + using System; + using System.Collections.Generic; + using Tokens; + using Writer; + + /// + internal class MultiCopier : ObjectCopier + { + private readonly List copiers; + + /// + public MultiCopier(PdfStreamWriter destinationStream) : base(destinationStream) + { + copiers = new List(); + } + + /// + /// + /// + /// + public void AddCopier(IObjectCopier copier) + { + copiers.Add(copier); + } + + /// + /// + /// + /// + /// + public bool RemoveCopier(IObjectCopier copier) + { + return copiers.Remove(copier); + } + + /// + /// + /// + /// + public IReadOnlyList GetCopiers() + { + return copiers; + } + + /// + public override IToken CopyObject(IToken sourceToken, Func tokenScanner) + { + // We give the token to the child copiers, to see if they have a better way of copying the token + foreach (var copier in copiers) + { + var newToken = copier.CopyObject(sourceToken, tokenScanner); + if (newToken != null) + { + return newToken; + } + } + + // If the token did not found a suitable copier, let just do a simple copy of the token + return base.CopyObject(sourceToken, tokenScanner); + } + + /// + public override void ClearReference() + { + foreach (var copier in copiers) + { + copier.ClearReference(); + } + + base.ClearReference(); + } + } +} \ No newline at end of file diff --git a/src/UglyToad.PdfPig/Writer/Copier/ObjectCopier.cs b/src/UglyToad.PdfPig/Writer/Copier/ObjectCopier.cs new file mode 100644 index 00000000..bb82e094 --- /dev/null +++ b/src/UglyToad.PdfPig/Writer/Copier/ObjectCopier.cs @@ -0,0 +1,170 @@ +namespace UglyToad.PdfPig.Writer.Copier +{ + using System; + using System.Collections.Generic; + using PdfPig; + using Tokenization.Scanner; + using Tokens; + using Writer; + + /// + internal class ObjectCopier : IObjectCopier + { + private readonly PdfStreamWriter pdfStream; + + private readonly Dictionary newReferenceMap; + + /// + public ObjectCopier(PdfStreamWriter destinationStream) + { + pdfStream = destinationStream ?? throw new ArgumentNullException(nameof(destinationStream)); + newReferenceMap = new Dictionary(); + } + + /// + public IToken CopyObject(IToken sourceToken, PdfDocument sourceDocument) + { + IToken tokenScanner(IndirectReferenceToken referenceToken) + { + var objToken = sourceDocument.Structure.GetObject(referenceToken.Data); + return objToken.Data; + } + + return CopyObject(sourceToken, tokenScanner); + } + + /// + public IToken CopyObject(IToken sourceToken, IPdfTokenScanner tokenScanner) + { + IToken tokenGetter(IndirectReferenceToken referenceToken) + { + var objToken = tokenScanner.Get(referenceToken.Data); + return objToken.Data; + } + + return CopyObject(sourceToken, tokenGetter); + } + + /// + public virtual IToken CopyObject(IToken sourceToken, Func tokenScanner) + { + // This token need to be deep copied, because they could contain reference. So we have to update them. + switch (sourceToken) + { + case DictionaryToken dictionaryToken: + { + var newContent = new Dictionary(); + foreach (var setPair in dictionaryToken.Data) + { + var name = setPair.Key; + var token = setPair.Value; + + newContent.Add(NameToken.Create(name), CopyObject(token, tokenScanner)); + } + + return new DictionaryToken(newContent); + } + case ArrayToken arrayToken: + { + var newArray = new List(arrayToken.Length); + foreach (var token in arrayToken.Data) + { + newArray.Add(CopyObject(token, tokenScanner)); + } + + return new ArrayToken(newArray); + } + case IndirectReferenceToken referenceToken: + { + if (TryGetNewReference(referenceToken, out var newReferenceToken)) + { + return newReferenceToken; + } + + var referencedToken = tokenScanner(referenceToken); + var newReferencedToken = CopyObject(referencedToken, tokenScanner); + + var newToken = WriteToken(newReferencedToken); + SetNewReference(referenceToken, newToken); + return newToken; + } + + case StreamToken streamToken: + { + var properties = CopyObject(streamToken.StreamDictionary, tokenScanner); + var bytes = streamToken.Data; + return new StreamToken(properties as DictionaryToken, bytes); + } + + case ObjectToken _: + { + + // This is because, since we don't write token directly to the stream. So we can't know the offset. + // The token would be invalid. Although I don't think the copy of an object token would ever happen + throw new NotSupportedException("Copying a Object token is not supported"); + } + } + + return sourceToken; + } + + /// + /// + /// + /// + /// + /// + public virtual bool TryGetNewReference(IndirectReferenceToken sourceReferenceToken, out IndirectReferenceToken newReferenceToken) + { + newReferenceToken = default; + foreach (var referenceSet in newReferenceMap) + { + if (!referenceSet.Key.Equals(sourceReferenceToken)) + { + continue; + } + + newReferenceToken = referenceSet.Value; + return true; + } + + return false; + } + + /// + public virtual void ClearReference() + { + newReferenceMap.Clear(); + } + + /// + /// + /// + /// + /// + public void SetNewReference(IndirectReferenceToken oldToken, IndirectReferenceToken newToken) + { + newReferenceMap.Add(oldToken, newToken); + } + + /// + /// + /// + /// + public int ReserveTokenNumber() + { + return pdfStream.ReserveNumber(); + } + + /// + /// + /// + /// + /// + /// + public IndirectReferenceToken WriteToken(IToken token, int? reservedNumber = null) + { + return pdfStream.WriteToken(token, reservedNumber); + } + } +} diff --git a/src/UglyToad.PdfPig/Writer/Copier/TokenHelper.cs b/src/UglyToad.PdfPig/Writer/Copier/TokenHelper.cs new file mode 100644 index 00000000..71a7820a --- /dev/null +++ b/src/UglyToad.PdfPig/Writer/Copier/TokenHelper.cs @@ -0,0 +1,34 @@ +namespace UglyToad.PdfPig.Writer.Copier +{ + using System; + using Tokens; + + internal static class TokenHelper + { + // This is to avoid infinite loop in production. Although, it should never happen + const int MAX_ITERATIONS = 10; + + public static T GetTokenAs(IToken token, Func lookupFunc) where T : IToken + { + var iterations = 0; + + var original = token; + while (iterations++ < MAX_ITERATIONS) + { + switch (token) + { + case T result: + return result; + case IndirectReferenceToken tokenReference: + token = lookupFunc(tokenReference); + continue; + case ObjectToken tokenObject: + token = tokenObject.Data; + continue; + } + } + + throw new InvalidOperationException($"Unable to extract a {typeof(T)} token from {original}"); + } + } +} \ No newline at end of file