Changes for annotation positions:

- Pass in the initial matrix to the annotation provider, so that it can return
  the correct rectangles / quad points.
- Made a change / extensions to the Annotation class:
  - ModifiedDate is now a DateTimeOffset instead of unparsed string.
    If the string is invalid, ModifiedDate is set to the default value.
  - Added lookup for the "appearance streams"; all the annotations should have
    a "N" (normal) appearance, and optionally have a "R" (roll-over/hover)
    and "D" (down/click) appearance. Did not expose the actual stream objects,
    but added a flag indicating the existence of "R" / "D". At some point
    we can consider doing something with the appearances.
- Changed signature of GetInitialMatrix / ContentStreamProcessor constructor
  from PdfRectangle back to what it was earlier, namely MediaBox and CropBox,
  to prevent accidentally mixing the two up in the caller.
This commit is contained in:
mvantzet 2023-03-13 18:15:24 +01:00
parent a439b43246
commit ea77156eb8
5 changed files with 80 additions and 24 deletions

View File

@ -4,6 +4,7 @@
using PdfPig.Core; using PdfPig.Core;
using PdfPig.Geometry; using PdfPig.Geometry;
using PdfPig.Graphics; using PdfPig.Graphics;
using System.Linq;
using Xunit; using Xunit;
public class ContentStreamProcessorTests public class ContentStreamProcessorTests
@ -132,8 +133,8 @@
} }
private static void GetInitialTransformationMatrices( private static void GetInitialTransformationMatrices(
PdfRectangle mediaBox, MediaBox mediaBox,
PdfRectangle cropBox, CropBox cropBox,
PageRotationDegrees rotation, PageRotationDegrees rotation,
out TransformationMatrix initialMatrix, out TransformationMatrix initialMatrix,
out TransformationMatrix inverseMatrix) out TransformationMatrix inverseMatrix)
@ -142,6 +143,16 @@
inverseMatrix = initialMatrix.Inverse(); inverseMatrix = initialMatrix.Inverse();
} }
private static void GetInitialTransformationMatrices(
PdfRectangle mediaBox,
PdfRectangle cropBox,
PageRotationDegrees rotation,
out TransformationMatrix initialMatrix,
out TransformationMatrix inverseMatrix)
{
GetInitialTransformationMatrices(new MediaBox(mediaBox), new CropBox(cropBox), rotation, out initialMatrix, out inverseMatrix);
}
private static void AssertAreEqual(PdfRectangle r1, PdfRectangle r2) private static void AssertAreEqual(PdfRectangle r1, PdfRectangle r2)
{ {
AssertAreEqual(r1.BottomLeft, r2.BottomLeft); AssertAreEqual(r1.BottomLeft, r2.BottomLeft);

View File

@ -11,6 +11,10 @@
/// </summary> /// </summary>
public class Annotation public class Annotation
{ {
private readonly StreamToken normalAppearanceStream;
private readonly StreamToken rollOverAppearanceStream;
private readonly StreamToken downAppearanceStream;
/// <summary> /// <summary>
/// The underlying PDF dictionary which this annotation was created from. /// The underlying PDF dictionary which this annotation was created from.
/// </summary> /// </summary>
@ -43,7 +47,7 @@
/// The date and time the annotation was last modified, can be in any format. Optional. /// The date and time the annotation was last modified, can be in any format. Optional.
/// </summary> /// </summary>
[CanBeNull] [CanBeNull]
public string ModifiedDate { get; } public DateTimeOffset ModifiedDate { get; }
/// <summary> /// <summary>
/// Flags defining the appearance and behaviour of this annotation. /// Flags defining the appearance and behaviour of this annotation.
@ -62,11 +66,22 @@
/// </summary> /// </summary>
public IReadOnlyList<QuadPointsQuadrilateral> QuadPoints { get; } public IReadOnlyList<QuadPointsQuadrilateral> QuadPoints { get; }
/// <summary>
/// Indicates if a roll over appearance is present for this annotation (shown when you hover over this annotation)
/// </summary>
public bool HasRollOverAppearance => rollOverAppearanceStream != null;
/// <summary>
/// Indicates if a down appearance is present for this annotation (shown when you click on this annotation)
/// </summary>
public bool HasDownAppearance => downAppearanceStream != null;
/// <summary> /// <summary>
/// Create a new <see cref="Annotation"/>. /// Create a new <see cref="Annotation"/>.
/// </summary> /// </summary>
public Annotation(DictionaryToken annotationDictionary, AnnotationType type, PdfRectangle rectangle, string content, string name, string modifiedDate, public Annotation(DictionaryToken annotationDictionary, AnnotationType type, PdfRectangle rectangle, string content, string name, DateTimeOffset modifiedDate,
AnnotationFlags flags, AnnotationBorder border, IReadOnlyList<QuadPointsQuadrilateral> quadPoints) AnnotationFlags flags, AnnotationBorder border, IReadOnlyList<QuadPointsQuadrilateral> quadPoints,
StreamToken normalAppearanceStream, StreamToken rollOverAppearanceStream, StreamToken downAppearanceStream)
{ {
AnnotationDictionary = annotationDictionary ?? throw new ArgumentNullException(nameof(annotationDictionary)); AnnotationDictionary = annotationDictionary ?? throw new ArgumentNullException(nameof(annotationDictionary));
Type = type; Type = type;
@ -77,6 +92,9 @@
Flags = flags; Flags = flags;
Border = border; Border = border;
QuadPoints = quadPoints ?? EmptyArray<QuadPointsQuadrilateral>.Instance; QuadPoints = quadPoints ?? EmptyArray<QuadPointsQuadrilateral>.Instance;
this.normalAppearanceStream = normalAppearanceStream;
this.rollOverAppearanceStream = rollOverAppearanceStream;
this.downAppearanceStream = downAppearanceStream;
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -13,9 +13,12 @@
{ {
private readonly IPdfTokenScanner tokenScanner; private readonly IPdfTokenScanner tokenScanner;
private readonly DictionaryToken pageDictionary; private readonly DictionaryToken pageDictionary;
private readonly TransformationMatrix matrix;
public AnnotationProvider(IPdfTokenScanner tokenScanner, DictionaryToken pageDictionary) public AnnotationProvider(IPdfTokenScanner tokenScanner, DictionaryToken pageDictionary,
TransformationMatrix matrix)
{ {
this.matrix = matrix;
this.tokenScanner = tokenScanner ?? throw new ArgumentNullException(nameof(tokenScanner)); this.tokenScanner = tokenScanner ?? throw new ArgumentNullException(nameof(tokenScanner));
this.pageDictionary = pageDictionary ?? throw new ArgumentNullException(nameof(pageDictionary)); this.pageDictionary = pageDictionary ?? throw new ArgumentNullException(nameof(pageDictionary));
} }
@ -37,11 +40,12 @@
var type = annotationDictionary.Get<NameToken>(NameToken.Subtype, tokenScanner); var type = annotationDictionary.Get<NameToken>(NameToken.Subtype, tokenScanner);
var annotationType = type.ToAnnotationType(); var annotationType = type.ToAnnotationType();
var rectangle = annotationDictionary.Get<ArrayToken>(NameToken.Rect, tokenScanner).ToRectangle(tokenScanner); var rectangle = matrix.Transform(annotationDictionary.Get<ArrayToken>(NameToken.Rect, tokenScanner).ToRectangle(tokenScanner));
var contents = GetNamedString(NameToken.Contents, annotationDictionary); var contents = GetNamedString(NameToken.Contents, annotationDictionary);
var name = GetNamedString(NameToken.Nm, annotationDictionary); var name = GetNamedString(NameToken.Nm, annotationDictionary);
var modifiedDate = GetNamedString(NameToken.M, annotationDictionary); var modifiedDateAsString = GetNamedString(NameToken.M, annotationDictionary);
if (!DateFormatHelper.TryParseDateTimeOffset(modifiedDateAsString, out var modifiedDate)) modifiedDate = default(DateTimeOffset);
var flags = (AnnotationFlags)0; var flags = (AnnotationFlags)0;
if (annotationDictionary.TryGet(NameToken.F, out var flagsToken) && DirectObjectFinder.TryGet(flagsToken, tokenScanner, out NumericToken flagsNumericToken)) if (annotationDictionary.TryGet(NameToken.F, out var flagsToken) && DirectObjectFinder.TryGet(flagsToken, tokenScanner, out NumericToken flagsNumericToken))
@ -83,10 +87,10 @@
{ {
quadPointRectangles.Add(new QuadPointsQuadrilateral(new[] quadPointRectangles.Add(new QuadPointsQuadrilateral(new[]
{ {
new PdfPoint(values[0], values[1]), matrix.Transform(new PdfPoint(values[0], values[1])),
new PdfPoint(values[2], values[3]), matrix.Transform(new PdfPoint(values[2], values[3])),
new PdfPoint(values[4], values[5]), matrix.Transform(new PdfPoint(values[4], values[5])),
new PdfPoint(values[6], values[7]) matrix.Transform(new PdfPoint(values[6], values[7]))
})); }));
values.Clear(); values.Clear();
@ -94,8 +98,29 @@
} }
} }
yield return new Annotation(annotationDictionary, annotationType, rectangle, contents, name, modifiedDate, flags, border, StreamToken normalAppearanceStream = null, downAppearanceStream = null, rollOverAppearanceStream = null;
quadPointRectangles); if (annotationDictionary.TryGet(NameToken.Ap, out DictionaryToken appearanceDictionary))
{
// The normal appearance of this annotation
if (appearanceDictionary.TryGet(NameToken.N, out IndirectReferenceToken normalAppearanceRef))
{
normalAppearanceStream = tokenScanner.Get(normalAppearanceRef.Data)?.Data as StreamToken;
}
// If present, the 'roll over' appearance of this annotation (when hovering the mouse pointer over this annotation)
if (appearanceDictionary.TryGet(NameToken.R, out IndirectReferenceToken rollOverAppearanceRef))
{
rollOverAppearanceStream = tokenScanner.Get(rollOverAppearanceRef.Data)?.Data as StreamToken;
}
// If present, the 'down' appearance of this annotation (when you click on it)
if (appearanceDictionary.TryGet(NameToken.D, out IndirectReferenceToken downAppearanceRef))
{
downAppearanceStream = tokenScanner.Get(downAppearanceRef.Data)?.Data as StreamToken;
}
}
yield return new Annotation(annotationDictionary, annotationType, rectangle,
contents, name, modifiedDate, flags, border, quadPointRectangles,
normalAppearanceStream, rollOverAppearanceStream, downAppearanceStream);
} }
} }

View File

@ -87,8 +87,8 @@
public ContentStreamProcessor(IResourceStore resourceStore, public ContentStreamProcessor(IResourceStore resourceStore,
UserSpaceUnit userSpaceUnit, UserSpaceUnit userSpaceUnit,
PdfRectangle mediaBox, MediaBox mediaBox,
PdfRectangle cropBox, CropBox cropBox,
PageRotationDegrees rotation, PageRotationDegrees rotation,
IPdfTokenScanner pdfScanner, IPdfTokenScanner pdfScanner,
IPageContentParser pageContentParser, IPageContentParser pageContentParser,
@ -105,7 +105,7 @@
// initiate CurrentClippingPath to cropBox // initiate CurrentClippingPath to cropBox
var clippingSubpath = new PdfSubpath(); var clippingSubpath = new PdfSubpath();
clippingSubpath.Rectangle(cropBox.BottomLeft.X, cropBox.BottomLeft.Y, cropBox.Width, cropBox.Height); clippingSubpath.Rectangle(cropBox.Bounds.BottomLeft.X, cropBox.Bounds.BottomLeft.Y, cropBox.Bounds.Width, cropBox.Bounds.Height);
var clippingPath = new PdfPath() { clippingSubpath }; var clippingPath = new PdfPath() { clippingSubpath };
clippingPath.SetClipping(FillingRule.EvenOdd); clippingPath.SetClipping(FillingRule.EvenOdd);
@ -120,13 +120,13 @@
[System.Diagnostics.Contracts.Pure] [System.Diagnostics.Contracts.Pure]
internal static TransformationMatrix GetInitialMatrix(UserSpaceUnit userSpaceUnit, internal static TransformationMatrix GetInitialMatrix(UserSpaceUnit userSpaceUnit,
PdfRectangle mediaBox, MediaBox mediaBox,
PdfRectangle cropBox, CropBox cropBox,
PageRotationDegrees rotation) PageRotationDegrees rotation)
{ {
// Cater for scenario where the cropbox is larger than the mediabox. // Cater for scenario where the cropbox is larger than the mediabox.
// If there is no intersection (method returns null), fall back to the cropbox. // If there is no intersection (method returns null), fall back to the cropbox.
var viewBox = mediaBox.Intersect(cropBox) ?? cropBox; var viewBox = mediaBox.Bounds.Intersect(cropBox.Bounds) ?? cropBox.Bounds;
if (rotation.Value == 0 if (rotation.Value == 0
&& viewBox.Left == 0 && viewBox.Left == 0

View File

@ -133,8 +133,10 @@
content = GetContent(number, bytes, cropBox, userSpaceUnit, rotation, mediaBox, parsingOptions); content = GetContent(number, bytes, cropBox, userSpaceUnit, rotation, mediaBox, parsingOptions);
} }
var page = new Page(number, dictionary, mediaBox, cropBox, rotation, content, var initialMatrix = ContentStreamProcessor.GetInitialMatrix(userSpaceUnit, mediaBox, cropBox, rotation);
new AnnotationProvider(pdfScanner, dictionary),
var page = new Page(number, dictionary, mediaBox, cropBox, rotation, content,
new AnnotationProvider(pdfScanner, dictionary, initialMatrix),
pdfScanner); pdfScanner);
for (var i = 0; i < stackDepth; i++) for (var i = 0; i < stackDepth; i++)
@ -160,8 +162,8 @@
var context = new ContentStreamProcessor( var context = new ContentStreamProcessor(
resourceStore, resourceStore,
userSpaceUnit, userSpaceUnit,
mediaBox.Bounds, mediaBox,
cropBox.Bounds, cropBox,
rotation, rotation,
pdfScanner, pdfScanner,
pageContentParser, pageContentParser,