namespace UglyToad.PdfPig.Annotations { using System; using System.Collections.Generic; using System.Linq; using Actions; using Core; using Logging; using Outline.Destinations; using Parser.Parts; using Tokenization.Scanner; using Tokens; using Util; /// /// Annotation provider. /// public class AnnotationProvider { private readonly IPdfTokenScanner tokenScanner; private readonly DictionaryToken pageDictionary; private readonly NamedDestinations namedDestinations; private readonly ILog log; private readonly TransformationMatrix matrix; /// /// Create a . /// public AnnotationProvider(IPdfTokenScanner tokenScanner, DictionaryToken pageDictionary, TransformationMatrix matrix, NamedDestinations namedDestinations, ILog log) { this.matrix = matrix; this.tokenScanner = tokenScanner ?? throw new ArgumentNullException(nameof(tokenScanner)); this.pageDictionary = pageDictionary ?? throw new ArgumentNullException(nameof(pageDictionary)); this.namedDestinations = namedDestinations; this.log = log; } /// /// Get the annotations. /// public IEnumerable GetAnnotations() { var lookupAnnotations = new Dictionary(); if (!pageDictionary.TryGet(NameToken.Annots, tokenScanner, out ArrayToken? annotationsArray)) { yield break; } foreach (var token in annotationsArray.Data) { if (!DirectObjectFinder.TryGet(token, tokenScanner, out DictionaryToken? annotationDictionary)) { continue; } Annotation? replyTo = null; if (annotationDictionary.TryGet(NameToken.Irt, out IndirectReferenceToken? referencedAnnotation) && lookupAnnotations.TryGetValue(referencedAnnotation!.Data, out var linkedAnnotation)) { replyTo = linkedAnnotation; } var type = annotationDictionary.Get(NameToken.Subtype, tokenScanner); var annotationType = type.ToAnnotationType(); var action = GetAction(annotationDictionary); var rectangle = matrix.Transform(annotationDictionary.Get(NameToken.Rect, tokenScanner) .ToRectangle(tokenScanner)); var contents = GetNamedString(NameToken.Contents, annotationDictionary); var name = GetNamedString(NameToken.Nm, annotationDictionary); // As indicated in PDF reference 8.4.1, the modified date can be anything, but is usually a date formatted according to sec. 3.8.3 var modifiedDate = GetNamedString(NameToken.M, annotationDictionary); var flags = (AnnotationFlags)0; if (annotationDictionary.TryGet(NameToken.F, out var flagsToken) && DirectObjectFinder.TryGet(flagsToken, tokenScanner, out NumericToken? flagsNumericToken)) { flags = (AnnotationFlags)flagsNumericToken.Int; } var border = AnnotationBorder.Default; if (annotationDictionary.TryGet(NameToken.Border, out var borderToken) && DirectObjectFinder.TryGet(borderToken, tokenScanner, out ArrayToken? borderArray) && borderArray.Length >= 3) { var horizontal = borderArray.GetNumeric(0).Data; var vertical = borderArray.GetNumeric(1).Data; var width = borderArray.GetNumeric(2).Data; var dashes = default(IReadOnlyList); if (borderArray.Length == 4 && borderArray.Data[4] is ArrayToken dashArray) { dashes = dashArray.Data.OfType().Select(x => x.Data).ToList(); } border = new AnnotationBorder(horizontal, vertical, width, dashes); } var quadPointRectangles = new List(); if (annotationDictionary.TryGet(NameToken.Quadpoints, tokenScanner, out ArrayToken? quadPointsArray)) { var values = new List(); for (var i = 0; i < quadPointsArray.Length; i++) { if (!(quadPointsArray[i] is NumericToken value)) { continue; } values.Add(value.Data); if (values.Count == 8) { quadPointRectangles.Add(new QuadPointsQuadrilateral( [ matrix.Transform(new PdfPoint(values[0], values[1])), matrix.Transform(new PdfPoint(values[2], values[3])), matrix.Transform(new PdfPoint(values[4], values[5])), matrix.Transform(new PdfPoint(values[6], values[7])) ])); values.Clear(); } } } AppearanceStream? normalAppearanceStream = null; AppearanceStream? downAppearanceStream = null; AppearanceStream? rollOverAppearanceStream = null; if (annotationDictionary.TryGet(NameToken.Ap, out DictionaryToken appearanceDictionary)) { // The normal appearance of this annotation if (AppearanceStreamFactory.TryCreate(appearanceDictionary, NameToken.N, tokenScanner, out AppearanceStream? stream)) { normalAppearanceStream = stream; } // If present, the 'roll over' appearance of this annotation (when hovering the mouse pointer over this annotation) if (AppearanceStreamFactory.TryCreate(appearanceDictionary, NameToken.R, tokenScanner, out stream)) { rollOverAppearanceStream = stream; } // If present, the 'down' appearance of this annotation (when you click on it) if (AppearanceStreamFactory.TryCreate(appearanceDictionary, NameToken.D, tokenScanner, out stream)) { downAppearanceStream = stream; } } string? appearanceState = null; if (annotationDictionary.TryGet(NameToken.As, out NameToken appearanceStateToken)) { appearanceState = appearanceStateToken.Data; } var annotation = new Annotation( annotationDictionary, annotationType, rectangle, contents, name, modifiedDate, flags, border, quadPointRectangles, action, normalAppearanceStream, rollOverAppearanceStream, downAppearanceStream, appearanceState, replyTo); if (token is IndirectReferenceToken indirectReference) { lookupAnnotations[indirectReference.Data] = annotation; } yield return annotation; } } internal PdfAction? GetAction(DictionaryToken annotationDictionary) { // If this annotation returns a direct destination, turn it into a GoTo action. if (DestinationProvider.TryGetDestination(annotationDictionary, NameToken.Dest, namedDestinations, tokenScanner, log, false, out var destination)) { return new GoToAction(destination); } // Try get action from the dictionary. if (ActionProvider.TryGetAction(annotationDictionary, namedDestinations, tokenScanner, log, out var action)) { return action; } // No action or destination found, return null return null; } private string? GetNamedString(NameToken name, DictionaryToken dictionary) { string? content = null; if (dictionary.TryGet(name, out var contentToken)) { if (contentToken is StringToken contentString) { content = contentString.Data; } else if (contentToken is HexToken contentHex) { content = contentHex.Data; } else if (DirectObjectFinder.TryGet(contentToken, tokenScanner, out StringToken? indirectContentString)) { content = indirectContentString.Data; } } return content; } } }