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

View File

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

View File

@ -13,9 +13,12 @@
{
private readonly IPdfTokenScanner tokenScanner;
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.pageDictionary = pageDictionary ?? throw new ArgumentNullException(nameof(pageDictionary));
}
@ -37,11 +40,12 @@
var type = annotationDictionary.Get<NameToken>(NameToken.Subtype, tokenScanner);
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 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;
if (annotationDictionary.TryGet(NameToken.F, out var flagsToken) && DirectObjectFinder.TryGet(flagsToken, tokenScanner, out NumericToken flagsNumericToken))
@ -83,10 +87,10 @@
{
quadPointRectangles.Add(new QuadPointsQuadrilateral(new[]
{
new PdfPoint(values[0], values[1]),
new PdfPoint(values[2], values[3]),
new PdfPoint(values[4], values[5]),
new PdfPoint(values[6], values[7])
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();
@ -94,8 +98,29 @@
}
}
yield return new Annotation(annotationDictionary, annotationType, rectangle, contents, name, modifiedDate, flags, border,
quadPointRectangles);
StreamToken normalAppearanceStream = null, downAppearanceStream = null, rollOverAppearanceStream = null;
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,
UserSpaceUnit userSpaceUnit,
PdfRectangle mediaBox,
PdfRectangle cropBox,
MediaBox mediaBox,
CropBox cropBox,
PageRotationDegrees rotation,
IPdfTokenScanner pdfScanner,
IPageContentParser pageContentParser,
@ -105,7 +105,7 @@
// initiate CurrentClippingPath to cropBox
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 };
clippingPath.SetClipping(FillingRule.EvenOdd);
@ -120,13 +120,13 @@
[System.Diagnostics.Contracts.Pure]
internal static TransformationMatrix GetInitialMatrix(UserSpaceUnit userSpaceUnit,
PdfRectangle mediaBox,
PdfRectangle cropBox,
MediaBox mediaBox,
CropBox cropBox,
PageRotationDegrees rotation)
{
// Cater for scenario where the cropbox is larger than the mediabox.
// 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
&& viewBox.Left == 0

View File

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