2018-12-21 02:18:32 +08:00
|
|
|
|
namespace UglyToad.PdfPig.Annotations
|
|
|
|
|
{
|
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
2018-12-22 23:54:32 +08:00
|
|
|
|
using System.Linq;
|
2024-03-18 02:51:40 +08:00
|
|
|
|
using Actions;
|
2020-01-05 06:39:13 +08:00
|
|
|
|
using Core;
|
2023-04-11 00:14:14 +08:00
|
|
|
|
using Logging;
|
|
|
|
|
using Outline.Destinations;
|
2018-12-21 02:18:32 +08:00
|
|
|
|
using Parser.Parts;
|
|
|
|
|
using Tokenization.Scanner;
|
|
|
|
|
using Tokens;
|
|
|
|
|
using Util;
|
|
|
|
|
|
2024-03-16 20:41:53 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Annotation provider.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public class AnnotationProvider
|
2018-12-21 02:18:32 +08:00
|
|
|
|
{
|
|
|
|
|
private readonly IPdfTokenScanner tokenScanner;
|
|
|
|
|
private readonly DictionaryToken pageDictionary;
|
2023-04-11 00:14:14 +08:00
|
|
|
|
private readonly NamedDestinations namedDestinations;
|
|
|
|
|
private readonly ILog log;
|
2023-03-14 01:15:24 +08:00
|
|
|
|
private readonly TransformationMatrix matrix;
|
2018-12-21 02:18:32 +08:00
|
|
|
|
|
2024-03-16 20:41:53 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Create a <see cref="AnnotationProvider"/>.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public AnnotationProvider(IPdfTokenScanner tokenScanner,
|
|
|
|
|
DictionaryToken pageDictionary,
|
|
|
|
|
TransformationMatrix matrix,
|
|
|
|
|
NamedDestinations namedDestinations,
|
|
|
|
|
ILog log)
|
2018-12-21 02:18:32 +08:00
|
|
|
|
{
|
2023-03-14 01:15:24 +08:00
|
|
|
|
this.matrix = matrix;
|
2018-12-21 02:18:32 +08:00
|
|
|
|
this.tokenScanner = tokenScanner ?? throw new ArgumentNullException(nameof(tokenScanner));
|
|
|
|
|
this.pageDictionary = pageDictionary ?? throw new ArgumentNullException(nameof(pageDictionary));
|
2023-04-11 00:14:14 +08:00
|
|
|
|
this.namedDestinations = namedDestinations;
|
|
|
|
|
this.log = log;
|
2018-12-21 02:18:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
2024-03-16 20:41:53 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Get the annotations.
|
|
|
|
|
/// </summary>
|
2018-12-21 02:18:32 +08:00
|
|
|
|
public IEnumerable<Annotation> GetAnnotations()
|
|
|
|
|
{
|
2023-05-27 20:40:01 +08:00
|
|
|
|
var lookupAnnotations = new Dictionary<IndirectReference, Annotation>();
|
|
|
|
|
|
2024-03-18 02:51:40 +08:00
|
|
|
|
if (!pageDictionary.TryGet(NameToken.Annots, tokenScanner, out ArrayToken? annotationsArray))
|
2018-12-21 02:18:32 +08:00
|
|
|
|
{
|
|
|
|
|
yield break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach (var token in annotationsArray.Data)
|
|
|
|
|
{
|
2024-03-18 02:51:40 +08:00
|
|
|
|
if (!DirectObjectFinder.TryGet(token, tokenScanner, out DictionaryToken? annotationDictionary))
|
2018-12-21 02:18:32 +08:00
|
|
|
|
{
|
2024-03-16 20:41:53 +08:00
|
|
|
|
continue;
|
2018-12-21 02:18:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
2024-03-18 02:51:40 +08:00
|
|
|
|
Annotation? replyTo = null;
|
|
|
|
|
if (annotationDictionary.TryGet(NameToken.Irt, out IndirectReferenceToken? referencedAnnotation)
|
|
|
|
|
&& lookupAnnotations.TryGetValue(referencedAnnotation!.Data, out var linkedAnnotation))
|
2023-05-27 20:40:01 +08:00
|
|
|
|
{
|
|
|
|
|
replyTo = linkedAnnotation;
|
|
|
|
|
}
|
|
|
|
|
|
2018-12-21 02:18:32 +08:00
|
|
|
|
var type = annotationDictionary.Get<NameToken>(NameToken.Subtype, tokenScanner);
|
|
|
|
|
var annotationType = type.ToAnnotationType();
|
2023-04-11 00:14:14 +08:00
|
|
|
|
var action = GetAction(annotationDictionary);
|
2024-03-16 20:41:53 +08:00
|
|
|
|
var rectangle = matrix.Transform(annotationDictionary.Get<ArrayToken>(NameToken.Rect, tokenScanner)
|
|
|
|
|
.ToRectangle(tokenScanner));
|
2018-12-22 23:54:32 +08:00
|
|
|
|
var contents = GetNamedString(NameToken.Contents, annotationDictionary);
|
|
|
|
|
var name = GetNamedString(NameToken.Nm, annotationDictionary);
|
2023-03-14 19:01:24 +08:00
|
|
|
|
// 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);
|
2018-12-22 23:54:32 +08:00
|
|
|
|
|
2020-01-06 00:23:07 +08:00
|
|
|
|
var flags = (AnnotationFlags)0;
|
2024-03-16 20:41:53 +08:00
|
|
|
|
if (annotationDictionary.TryGet(NameToken.F, out var flagsToken) &&
|
2024-03-18 02:51:40 +08:00
|
|
|
|
DirectObjectFinder.TryGet(flagsToken, tokenScanner, out NumericToken? flagsNumericToken))
|
2018-12-21 02:18:32 +08:00
|
|
|
|
{
|
2020-01-06 00:23:07 +08:00
|
|
|
|
flags = (AnnotationFlags)flagsNumericToken.Int;
|
2018-12-21 02:18:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
2018-12-22 23:54:32 +08:00
|
|
|
|
var border = AnnotationBorder.Default;
|
2024-03-16 20:41:53 +08:00
|
|
|
|
if (annotationDictionary.TryGet(NameToken.Border, out var borderToken) &&
|
2024-03-18 02:51:40 +08:00
|
|
|
|
DirectObjectFinder.TryGet(borderToken, tokenScanner, out ArrayToken? borderArray)
|
2018-12-22 23:54:32 +08:00
|
|
|
|
&& borderArray.Length >= 3)
|
|
|
|
|
{
|
|
|
|
|
var horizontal = borderArray.GetNumeric(0).Data;
|
|
|
|
|
var vertical = borderArray.GetNumeric(1).Data;
|
|
|
|
|
var width = borderArray.GetNumeric(2).Data;
|
2024-01-30 23:11:12 +08:00
|
|
|
|
var dashes = default(IReadOnlyList<double>);
|
2018-12-21 02:18:32 +08:00
|
|
|
|
|
2018-12-22 23:54:32 +08:00
|
|
|
|
if (borderArray.Length == 4 && borderArray.Data[4] is ArrayToken dashArray)
|
|
|
|
|
{
|
|
|
|
|
dashes = dashArray.Data.OfType<NumericToken>().Select(x => x.Data).ToList();
|
|
|
|
|
}
|
2018-12-21 02:18:32 +08:00
|
|
|
|
|
2018-12-22 23:54:32 +08:00
|
|
|
|
border = new AnnotationBorder(horizontal, vertical, width, dashes);
|
|
|
|
|
}
|
2018-12-21 02:18:32 +08:00
|
|
|
|
|
2020-01-06 00:23:07 +08:00
|
|
|
|
var quadPointRectangles = new List<QuadPointsQuadrilateral>();
|
2024-03-18 02:51:40 +08:00
|
|
|
|
if (annotationDictionary.TryGet(NameToken.Quadpoints, tokenScanner, out ArrayToken? quadPointsArray))
|
2020-01-06 00:23:07 +08:00
|
|
|
|
{
|
2024-01-30 23:11:12 +08:00
|
|
|
|
var values = new List<double>();
|
2020-01-06 00:23:07 +08:00
|
|
|
|
for (var i = 0; i < quadPointsArray.Length; i++)
|
|
|
|
|
{
|
|
|
|
|
if (!(quadPointsArray[i] is NumericToken value))
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
values.Add(value.Data);
|
|
|
|
|
|
|
|
|
|
if (values.Count == 8)
|
|
|
|
|
{
|
2024-04-02 10:13:44 +08:00
|
|
|
|
quadPointRectangles.Add(new QuadPointsQuadrilateral(
|
|
|
|
|
[
|
2024-01-30 23:11:12 +08:00
|
|
|
|
matrix.Transform(new PdfPoint(values[0], values[1])),
|
|
|
|
|
matrix.Transform(new PdfPoint(values[2], values[3])),
|
|
|
|
|
matrix.Transform(new PdfPoint(values[4], values[5])),
|
2023-03-14 01:15:24 +08:00
|
|
|
|
matrix.Transform(new PdfPoint(values[6], values[7]))
|
2024-04-02 10:13:44 +08:00
|
|
|
|
]));
|
2020-01-06 00:23:07 +08:00
|
|
|
|
|
|
|
|
|
values.Clear();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-18 02:51:40 +08:00
|
|
|
|
AppearanceStream? normalAppearanceStream = null;
|
|
|
|
|
AppearanceStream? downAppearanceStream = null;
|
|
|
|
|
AppearanceStream? rollOverAppearanceStream = null;
|
2023-04-11 00:14:14 +08:00
|
|
|
|
|
2023-03-14 01:15:24 +08:00
|
|
|
|
if (annotationDictionary.TryGet(NameToken.Ap, out DictionaryToken appearanceDictionary))
|
|
|
|
|
{
|
|
|
|
|
// The normal appearance of this annotation
|
2024-03-18 02:51:40 +08:00
|
|
|
|
if (AppearanceStreamFactory.TryCreate(appearanceDictionary, NameToken.N, tokenScanner, out AppearanceStream? stream))
|
2023-03-14 01:15:24 +08:00
|
|
|
|
{
|
2023-04-11 00:14:14 +08:00
|
|
|
|
normalAppearanceStream = stream;
|
2023-03-14 01:15:24 +08:00
|
|
|
|
}
|
2023-04-11 00:14:14 +08:00
|
|
|
|
|
2023-03-14 01:15:24 +08:00
|
|
|
|
// If present, the 'roll over' appearance of this annotation (when hovering the mouse pointer over this annotation)
|
2023-04-11 00:14:14 +08:00
|
|
|
|
if (AppearanceStreamFactory.TryCreate(appearanceDictionary, NameToken.R, tokenScanner, out stream))
|
2023-03-14 01:15:24 +08:00
|
|
|
|
{
|
2023-04-11 00:14:14 +08:00
|
|
|
|
rollOverAppearanceStream = stream;
|
2023-03-14 01:15:24 +08:00
|
|
|
|
}
|
2023-04-11 00:14:14 +08:00
|
|
|
|
|
2023-03-14 01:15:24 +08:00
|
|
|
|
// If present, the 'down' appearance of this annotation (when you click on it)
|
2023-04-11 00:14:14 +08:00
|
|
|
|
if (AppearanceStreamFactory.TryCreate(appearanceDictionary, NameToken.D, tokenScanner, out stream))
|
2023-03-14 01:15:24 +08:00
|
|
|
|
{
|
2023-04-11 00:14:14 +08:00
|
|
|
|
downAppearanceStream = stream;
|
2023-03-14 01:15:24 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-18 02:51:40 +08:00
|
|
|
|
string? appearanceState = null;
|
2023-04-11 00:14:14 +08:00
|
|
|
|
if (annotationDictionary.TryGet(NameToken.As, out NameToken appearanceStateToken))
|
|
|
|
|
{
|
|
|
|
|
appearanceState = appearanceStateToken.Data;
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-27 20:40:01 +08:00
|
|
|
|
var annotation = new Annotation(
|
|
|
|
|
annotationDictionary,
|
|
|
|
|
annotationType,
|
2024-03-16 20:41:53 +08:00
|
|
|
|
rectangle,
|
2023-05-27 20:40:01 +08:00
|
|
|
|
contents,
|
|
|
|
|
name,
|
|
|
|
|
modifiedDate,
|
|
|
|
|
flags,
|
|
|
|
|
border,
|
|
|
|
|
quadPointRectangles,
|
|
|
|
|
action,
|
|
|
|
|
normalAppearanceStream,
|
|
|
|
|
rollOverAppearanceStream,
|
|
|
|
|
downAppearanceStream,
|
|
|
|
|
appearanceState,
|
|
|
|
|
replyTo);
|
|
|
|
|
|
|
|
|
|
if (token is IndirectReferenceToken indirectReference)
|
|
|
|
|
{
|
|
|
|
|
lookupAnnotations[indirectReference.Data] = annotation;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
yield return annotation;
|
2018-12-22 23:54:32 +08:00
|
|
|
|
}
|
2018-12-21 02:18:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
2024-03-18 02:51:40 +08:00
|
|
|
|
internal PdfAction? GetAction(DictionaryToken annotationDictionary)
|
2023-04-11 00:14:14 +08:00
|
|
|
|
{
|
|
|
|
|
// 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);
|
|
|
|
|
}
|
2024-01-30 23:11:12 +08:00
|
|
|
|
|
2023-04-11 00:14:14 +08:00
|
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-18 02:51:40 +08:00
|
|
|
|
private string? GetNamedString(NameToken name, DictionaryToken dictionary)
|
2018-12-21 02:18:32 +08:00
|
|
|
|
{
|
2024-03-18 02:51:40 +08:00
|
|
|
|
string? content = null;
|
2018-12-22 23:54:32 +08:00
|
|
|
|
if (dictionary.TryGet(name, out var contentToken))
|
2018-12-21 02:18:32 +08:00
|
|
|
|
{
|
2018-12-22 23:54:32 +08:00
|
|
|
|
if (contentToken is StringToken contentString)
|
|
|
|
|
{
|
|
|
|
|
content = contentString.Data;
|
|
|
|
|
}
|
|
|
|
|
else if (contentToken is HexToken contentHex)
|
|
|
|
|
{
|
|
|
|
|
content = contentHex.Data;
|
|
|
|
|
}
|
2024-03-18 02:51:40 +08:00
|
|
|
|
else if (DirectObjectFinder.TryGet(contentToken, tokenScanner, out StringToken? indirectContentString))
|
2018-12-22 23:54:32 +08:00
|
|
|
|
{
|
|
|
|
|
content = indirectContentString.Data;
|
|
|
|
|
}
|
2018-12-21 02:18:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
2018-12-22 23:54:32 +08:00
|
|
|
|
return content;
|
2018-12-21 02:18:32 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2018-12-22 23:54:32 +08:00
|
|
|
|
}
|