diff --git a/src/UglyToad.PdfPig.Core/TransformationMatrix.cs b/src/UglyToad.PdfPig.Core/TransformationMatrix.cs
index 75b51ed46..64d9309e3 100644
--- a/src/UglyToad.PdfPig.Core/TransformationMatrix.cs
+++ b/src/UglyToad.PdfPig.Core/TransformationMatrix.cs
@@ -1,7 +1,6 @@
namespace UglyToad.PdfPig.Core
{
using System;
- using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
@@ -10,22 +9,22 @@
///
/// Specifies the conversion from the transformed coordinate space to the original untransformed coordinate space.
///
- public struct TransformationMatrix
+ public readonly struct TransformationMatrix
{
///
/// The default .
///
- public static TransformationMatrix Identity = new TransformationMatrix(1,0,0,
- 0,1,0,
- 0,0,1);
-
+ public static TransformationMatrix Identity = new TransformationMatrix(1, 0, 0,
+ 0, 1, 0,
+ 0, 0, 1);
+
///
/// Create a new with the X and Y translation values set.
///
public static TransformationMatrix GetTranslationMatrix(double x, double y) => new TransformationMatrix(1, 0, 0,
0, 1, 0,
x, y, 1);
-
+
///
/// Create a new with the X and Y scaling values set.
///
@@ -105,7 +104,7 @@ public static TransformationMatrix GetRotationMatrix(double degreesCounterclockw
/// The value at (2, 1) - translation in Y.
///
public readonly double F;
-
+
///
/// Get the value at the specific row and column.
///
@@ -224,10 +223,20 @@ public TransformationMatrix(double a, double b, double r1, double c, double d, d
[Pure]
public PdfPoint Transform(PdfPoint original)
{
- var x = A * original.X + C * original.Y + E;
- var y = B * original.X + D * original.Y + F;
+ (double x, double y) xy = Transform(original.X, original.Y);
+ return new PdfPoint(xy.x, xy.y);
+ }
- return new PdfPoint(x, y);
+ ///
+ /// Transform a point using this transformation matrix.
+ ///
+ /// The original point X coordinate.
+ /// The original point Y coordinate.
+ /// A new point which is the result of applying this transformation matrix.
+ [Pure]
+ public (double x, double y) Transform(double x, double y)
+ {
+ return new(A * x + C * y + E, B * x + D * y + F);
}
///
@@ -353,7 +362,7 @@ public static TransformationMatrix FromValues(double a, double b, double c, doub
/// Either all 9 values of the matrix, 6 values in the default PDF order or the 4 values of the top left square.
///
public static TransformationMatrix FromArray(decimal[] values)
- => FromArray(values.Select(x => (double) x).ToArray());
+ => FromArray(values.Select(x => (double)x).ToArray());
///
/// Create a new from the values.
@@ -404,8 +413,8 @@ public TransformationMatrix Multiply(TransformationMatrix matrix)
var f = (E * matrix.B) + (F * matrix.D) + (row3 * matrix.F);
var r3 = (E * matrix.row1) + (F * matrix.row2) + (row3 * matrix.row3);
- return new TransformationMatrix(a, b, r1,
- c, d, r2,
+ return new TransformationMatrix(a, b, r1,
+ c, d, r2,
e, f, r3);
}
diff --git a/src/UglyToad.PdfPig.Core/UglyToad.PdfPig.Core.csproj b/src/UglyToad.PdfPig.Core/UglyToad.PdfPig.Core.csproj
index 16cc760fe..724892a1a 100644
--- a/src/UglyToad.PdfPig.Core/UglyToad.PdfPig.Core.csproj
+++ b/src/UglyToad.PdfPig.Core/UglyToad.PdfPig.Core.csproj
@@ -7,6 +7,7 @@
true
true
..\pdfpig.snk
+ annotations
true
diff --git a/src/UglyToad.PdfPig.DocumentLayoutAnalysis/UglyToad.PdfPig.DocumentLayoutAnalysis.csproj b/src/UglyToad.PdfPig.DocumentLayoutAnalysis/UglyToad.PdfPig.DocumentLayoutAnalysis.csproj
index 6d4328b4b..682cab8e8 100644
--- a/src/UglyToad.PdfPig.DocumentLayoutAnalysis/UglyToad.PdfPig.DocumentLayoutAnalysis.csproj
+++ b/src/UglyToad.PdfPig.DocumentLayoutAnalysis/UglyToad.PdfPig.DocumentLayoutAnalysis.csproj
@@ -7,6 +7,7 @@
true
true
..\pdfpig.snk
+ annotations
true
diff --git a/src/UglyToad.PdfPig.Fonts/UglyToad.PdfPig.Fonts.csproj b/src/UglyToad.PdfPig.Fonts/UglyToad.PdfPig.Fonts.csproj
index b892f265b..e7284302a 100644
--- a/src/UglyToad.PdfPig.Fonts/UglyToad.PdfPig.Fonts.csproj
+++ b/src/UglyToad.PdfPig.Fonts/UglyToad.PdfPig.Fonts.csproj
@@ -7,6 +7,7 @@
true
true
..\pdfpig.snk
+ annotations
true
diff --git a/src/UglyToad.PdfPig.Rendering.Skia.Tests/Helpers.cs b/src/UglyToad.PdfPig.Rendering.Skia.Tests/Helpers.cs
new file mode 100644
index 000000000..0b5e522a4
--- /dev/null
+++ b/src/UglyToad.PdfPig.Rendering.Skia.Tests/Helpers.cs
@@ -0,0 +1,22 @@
+namespace UglyToad.PdfPig.Rendering.Skia.Tests
+{
+ internal static class Helpers
+ {
+ private static readonly string basePath = Path.GetFullPath("..\\..\\..\\..\\UglyToad.PdfPig.Tests\\Integration\\Documents");
+
+ public static string GetDocumentPath(string fileName)
+ {
+ if (!fileName.EndsWith(".pdf"))
+ {
+ fileName += ".pdf";
+ }
+
+ return Path.Combine(basePath, fileName);
+ }
+
+ public static string[] GetAllDocuments()
+ {
+ return Directory.GetFiles(basePath, "*.pdf");
+ }
+ }
+}
diff --git a/src/UglyToad.PdfPig.Rendering.Skia.Tests/RenderTests.cs b/src/UglyToad.PdfPig.Rendering.Skia.Tests/RenderTests.cs
new file mode 100644
index 000000000..9421239ab
--- /dev/null
+++ b/src/UglyToad.PdfPig.Rendering.Skia.Tests/RenderTests.cs
@@ -0,0 +1,97 @@
+using SkiaSharp;
+using UglyToad.PdfPig.Rendering.Skia.Parser;
+
+namespace UglyToad.PdfPig.Rendering.Skia.Tests
+{
+ public class RenderTests
+ {
+ // Image rotated bug
+
+ const float scale = 1.5f;
+
+ public RenderTests()
+ {
+ Directory.CreateDirectory("renders");
+ }
+
+ [Fact]
+ public void PigProductionHandbook()
+ {
+ RenderDocument("Pig Production Handbook");
+ }
+
+ [Fact]
+ public void d_68_1990_01_A()
+ {
+ RenderDocument("68-1990-01_A");
+ }
+
+ [Fact]
+ public void d_22060_A1_01_Plans_1()
+ {
+ RenderDocument("22060_A1_01_Plans-1");
+ }
+
+ [Fact]
+ public void cat_genetics_bobld()
+ {
+ RenderDocument("cat-genetics_bobld");
+ }
+
+ [Fact(Skip = "For debugging purpose")]
+ public void RenderAllDocuments()
+ {
+ foreach (string doc in Helpers.GetAllDocuments())
+ {
+ string fileName = Path.GetFileNameWithoutExtension(doc);
+
+ using (var document = PdfDocument.Open(doc))
+ {
+ document.AddPageFactory();
+
+ for (int p = 1; p <= document.NumberOfPages; p++)
+ {
+ var page = document.GetPage(p);
+
+ using (var picture = document.GetPage(p))
+ {
+ Assert.NotNull(picture);
+
+ using (var fs = new FileStream($"renders\\{fileName}_{p}.png", FileMode.Create))
+ using (var image = SKImage.FromPicture(picture, new SKSizeI((int)(page.Width * scale), (int)(page.Height * scale)), SKMatrix.CreateScale(scale, scale)))
+ using (SKData d = image.Encode(SKEncodedImageFormat.Png, 100))
+ {
+ d.SaveTo(fs);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private static void RenderDocument(string path)
+ {
+ using (var document = PdfDocument.Open(Helpers.GetDocumentPath(path)))
+ {
+ document.AddPageFactory();
+
+ for (int p = 1; p <= document.NumberOfPages; p++)
+ {
+ var page = document.GetPage(p);
+
+ using (var picture = document.GetPage(p))
+ {
+ Assert.NotNull(picture);
+
+ using (var fs = new FileStream($"renders\\{path}_{p}.png", FileMode.Create))
+ using (var image = SKImage.FromPicture(picture, new SKSizeI((int)(page.Width * scale), (int)(page.Height * scale)), SKMatrix.CreateScale(scale, scale)))
+ using (SKData d = image.Encode(SKEncodedImageFormat.Png, 100))
+ {
+ d.SaveTo(fs);
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/UglyToad.PdfPig.Rendering.Skia.Tests/UglyToad.PdfPig.Rendering.Skia.Tests.csproj b/src/UglyToad.PdfPig.Rendering.Skia.Tests/UglyToad.PdfPig.Rendering.Skia.Tests.csproj
new file mode 100644
index 000000000..3e6f06bb7
--- /dev/null
+++ b/src/UglyToad.PdfPig.Rendering.Skia.Tests/UglyToad.PdfPig.Rendering.Skia.Tests.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
diff --git a/src/UglyToad.PdfPig.Rendering.Skia.Tests/Usings.cs b/src/UglyToad.PdfPig.Rendering.Skia.Tests/Usings.cs
new file mode 100644
index 000000000..8c927eb74
--- /dev/null
+++ b/src/UglyToad.PdfPig.Rendering.Skia.Tests/Usings.cs
@@ -0,0 +1 @@
+global using Xunit;
\ No newline at end of file
diff --git a/src/UglyToad.PdfPig.Rendering.Skia/Graphics/BaseRenderStreamProcessor.cs b/src/UglyToad.PdfPig.Rendering.Skia/Graphics/BaseRenderStreamProcessor.cs
new file mode 100644
index 000000000..4bdd28c0c
--- /dev/null
+++ b/src/UglyToad.PdfPig.Rendering.Skia/Graphics/BaseRenderStreamProcessor.cs
@@ -0,0 +1,1043 @@
+using UglyToad.PdfPig.Annotations;
+using UglyToad.PdfPig.Content;
+using UglyToad.PdfPig.Core;
+using UglyToad.PdfPig.Filters;
+using UglyToad.PdfPig.Geometry;
+using UglyToad.PdfPig.Graphics;
+using UglyToad.PdfPig.Graphics.Colors;
+using UglyToad.PdfPig.Graphics.Operations;
+using UglyToad.PdfPig.Graphics.Operations.PathConstruction;
+using UglyToad.PdfPig.Parser;
+using UglyToad.PdfPig.Tokenization.Scanner;
+using UglyToad.PdfPig.Tokens;
+using UglyToad.PdfPig.XObjects;
+
+namespace UglyToad.PdfPig.Rendering.Skia.Graphics
+{
+ ///
+ /// TODO
+ ///
+ public abstract class BaseRenderStreamProcessor : BaseStreamProcessor
+ {
+ ///
+ /// Default FieldsHighlightColor from Adobe Acrobat Reader.
+ /// TODO - make an option of that
+ ///
+ public static readonly RGBColor DefaultFieldsHighlightColor = new RGBColor(204 / 255.0, 215 / 255.0, 1);
+
+ ///
+ /// Default Required FieldsHighlightColor from Adobe Acrobat Reader.
+ /// TODO - make an option of that
+ ///
+ public static readonly RGBColor DefaultRequiredFieldsHighlightColor = new RGBColor(1, 0, 0);
+
+ private readonly DictionaryToken dictionary;
+
+ protected BaseRenderStreamProcessor(
+ int pageNumber,
+ IResourceStore resourceStore,
+ UserSpaceUnit userSpaceUnit,
+ MediaBox mediaBox,
+ CropBox cropBox,
+ PageRotationDegrees rotation,
+ IPdfTokenScanner pdfScanner,
+ IPageContentParser pageContentParser,
+ ILookupFilterProvider filterProvider,
+ IParsingOptions parsingOptions)
+ : base(pageNumber, resourceStore, userSpaceUnit, mediaBox, cropBox, rotation, pdfScanner, pageContentParser, filterProvider, parsingOptions)
+ { }
+
+ ///
+ /// TODO
+ ///
+ protected DictionaryToken? GetAppearance(Annotation annotation)
+ {
+ if (annotation.AnnotationDictionary.TryGet(NameToken.Ap, PdfScanner, out var appearance))
+ {
+ return appearance;
+ }
+ return null;
+ }
+
+ ///
+ /// todo
+ ///
+ ///
+ protected StreamToken? GetNormalAppearanceAsStream(Annotation annotation)
+ {
+ var dict = GetAppearance(annotation);
+
+ // https://github.com/apache/pdfbox/blob/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java
+ // for highlight default colors from Adobe
+
+ if (dict == null)
+ {
+ return GenerateNormalAppearanceAsStream(annotation);
+ }
+
+ // get Normal Appearance
+ if (!dict.Data.TryGetValue(NameToken.N, out var data))
+ {
+ return null;
+ }
+
+ if (data is IndirectReferenceToken irt)
+ {
+ data = Get(irt);
+ if (data is null)
+ {
+ return null;
+ }
+ }
+
+ StreamToken normalAppearance = null;
+
+ if (data is StreamToken streamToken)
+ {
+ normalAppearance = streamToken;
+ }
+ else if (data is DictionaryToken dictionaryToken)
+ {
+ if (annotation.AnnotationDictionary.TryGet(NameToken.As, PdfScanner, out var appearanceState))
+ {
+ if (!dictionaryToken.TryGet(appearanceState, PdfScanner, out normalAppearance))
+ {
+ System.Diagnostics.Debug.WriteLine($"GetNormalAppearanceAsStream: Error could not find token '{appearanceState.Data}' in annotation dictionary or in D dictionary.");
+ }
+ }
+ }
+ else if (data is ObjectToken objectToken)
+ {
+ if (objectToken.Data is StreamToken streamToken2)
+ {
+ normalAppearance = streamToken2;
+ }
+ else if (objectToken.Data is DictionaryToken dictionaryToken2)
+ {
+ if (annotation.AnnotationDictionary.TryGet(NameToken.As, PdfScanner, out var appearanceState))
+ {
+ if (!dictionaryToken2.TryGet(appearanceState, PdfScanner, out normalAppearance))
+ {
+ System.Diagnostics.Debug.WriteLine($"GetNormalAppearanceAsStream: Error could not find token '{appearanceState.Data}' in annotation dictionary or in D dictionary.");
+ }
+ }
+ }
+ }
+ else
+ {
+ throw new ArgumentException("TODO GetNormalAppearanceAsStream");
+ }
+
+ if (annotation.Type == AnnotationType.Widget)
+ {
+ /*
+ var contentStream = normalAppearance.Decode(filterProvider, pdfScanner);
+ var operations = pageContentParser.Parse(pageNumber, new ByteArrayInputBytes(contentStream), parsingOptions.Logger).ToList();
+
+ // DO STUFF
+
+ using (MemoryStream newMs = new MemoryStream())
+ {
+ foreach (var operation in operations)
+ {
+ operation.Write(newMs);
+ }
+
+ normalAppearance = new StreamToken(normalAppearance.StreamDictionary, newMs.ToArray());
+ }
+ */
+ }
+ return normalAppearance;
+ }
+
+ private StreamToken? GenerateNormalAppearanceAsStream(Annotation annotation)
+ {
+ // https://github.com/apache/pdfbox/blob/c4b212ecf42a1c0a55529873b132ea338a8ba901/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/PDAbstractAppearanceHandler.java#L479
+
+ switch (annotation.Type)
+ {
+ case AnnotationType.StrikeOut:
+ return GenerateStrikeOutNormalAppearanceAsStream(annotation);
+
+ case AnnotationType.Highlight:
+ return GenerateHighlightNormalAppearanceAsStream(annotation);
+
+ case AnnotationType.Underline:
+ return GenerateUnderlineNormalAppearanceAsStream(annotation);
+
+ case AnnotationType.Link:
+ return GenerateLinkNormalAppearanceAsStream(annotation);
+
+ case AnnotationType.Widget:
+ return GenerateWidgetNormalAppearanceAsStream(annotation);
+ }
+
+ return null;
+ }
+
+ private StreamToken? GenerateWidgetNormalAppearanceAsStream(Annotation annotation)
+ {
+ // This will create an appearance with the default background color from Acrobat reader
+ PdfRectangle rect = annotation.Rectangle;
+ var ab = annotation.Border;
+
+ using (var ms = new MemoryStream())
+ {
+ decimal lineWidth = ab.BorderWidth;
+
+ var (r, g, b) = DefaultFieldsHighlightColor.ToRGBValues();
+ // GetAnnotationNonStrokeColorOperation(new decimal[] { r, g, b })?.Write(ms); // let's not fill anything for now
+
+ float[] pathsArray = null;
+ if (annotation.AnnotationDictionary.TryGet(NameToken.Quadpoints, PdfScanner, out var quadpoints))
+ {
+ pathsArray = quadpoints.Data?.OfType().Select(x => (float)x.Double)?.ToArray();
+ }
+
+ if (pathsArray != null)
+ {
+ // QuadPoints shall be ignored if any coordinate in the array lies outside
+ // the region specified by Rect.
+ for (int i = 0; i < pathsArray.Length / 2; ++i)
+ {
+ if (!rect.Contains(new PdfPoint(pathsArray[i * 2], pathsArray[i * 2 + 1])))
+ {
+ //LOG.warn("At least one /QuadPoints entry (" +
+ // pathsArray[i * 2] + ";" + pathsArray[i * 2 + 1] +
+ // ") is outside of rectangle, " + rect +
+ // ", /QuadPoints are ignored and /Rect is used instead");
+ pathsArray = null;
+ break;
+ }
+ }
+ }
+
+ if (pathsArray == null)
+ {
+ // Convert rectangle coordinates as if it was a /QuadPoints entry
+ pathsArray = new float[8];
+ pathsArray[0] = (float)rect.BottomLeft.X;
+ pathsArray[1] = (float)rect.BottomLeft.Y;
+ pathsArray[2] = (float)rect.TopRight.X;
+ pathsArray[3] = (float)rect.BottomLeft.Y;
+ pathsArray[4] = (float)rect.TopRight.X;
+ pathsArray[5] = (float)rect.TopRight.Y;
+ pathsArray[6] = (float)rect.BottomLeft.X;
+ pathsArray[7] = (float)rect.TopRight.Y;
+ }
+
+ int of = 0;
+ while (of + 7 < pathsArray.Length)
+ {
+ new BeginNewSubpath((decimal)pathsArray[of], (decimal)pathsArray[of + 1]).Write(ms);
+ new AppendStraightLineSegment((decimal)pathsArray[of + 2], (decimal)pathsArray[of + 3]).Write(ms);
+
+ new AppendStraightLineSegment((decimal)pathsArray[of + 4], (decimal)pathsArray[of + 5]).Write(ms);
+ new AppendStraightLineSegment((decimal)pathsArray[of + 6], (decimal)pathsArray[of + 7]).Write(ms);
+ PdfPig.Graphics.Operations.PathConstruction.CloseSubpath.Value.Write(ms);
+ of += 8;
+ }
+
+ //PdfPig.Graphics.Operations.PathPainting.FillPathEvenOddRule.Value.Write(ms); // let's not fill anything for now
+
+ var dict = dictionary; //.Data.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
+
+ if (annotation.AnnotationDictionary.TryGet(NameToken.Rect, out var rectToken))
+ {
+ dict = dict.With(NameToken.Bbox.Data, rectToken);
+ //dict.Add(NameToken.Bbox.Data, rectToken); // should use new rect
+ }
+
+ //return new StreamToken(new DictionaryToken(dict), ms.ToArray());
+ return new StreamToken(dict, ms.ToArray());
+ }
+ }
+
+ private static IGraphicsStateOperation GetAnnotationNonStrokeColorOperation(decimal[] color)
+ {
+ // An array of numbers in the range 0.0 to 1.0, representing a colour used for the following purposes:
+ // The background of the annotation’s icon when closed
+ // The title bar of the annotation’s pop - up window
+ // The border of a link annotation
+ // The number of array elements determines the colour space in which the colour shall be defined:
+ // 0 No colour; transparent
+ // 1 DeviceGray
+ // 3 DeviceRGB
+ // 4 DeviceCMYK
+ switch (color.Length)
+ {
+ case 0:
+ return null;
+ case 1:
+ return new SetNonStrokeColorDeviceGray(color[0]);
+ case 3:
+ return new SetNonStrokeColorDeviceRgb(color[0], color[1], color[2]);
+ case 4:
+ return new SetNonStrokeColorDeviceCmyk(color[0], color[1], color[2], color[3]);
+ default:
+ throw new ArgumentException("TODO", nameof(color));
+ }
+ }
+
+ private static IGraphicsStateOperation GetAnnotationStrokeColorOperation(decimal[] color)
+ {
+ // An array of numbers in the range 0.0 to 1.0, representing a colour used for the following purposes:
+ // The background of the annotation’s icon when closed
+ // The title bar of the annotation’s pop - up window
+ // The border of a link annotation
+ // The number of array elements determines the colour space in which the colour shall be defined:
+ // 0 No colour; transparent
+ // 1 DeviceGray
+ // 3 DeviceRGB
+ // 4 DeviceCMYK
+ switch (color.Length)
+ {
+ case 0:
+ return null;
+ case 1:
+ return new SetStrokeColorDeviceGray(color[0]);
+ case 3:
+ return new SetStrokeColorDeviceRgb(color[0], color[1], color[2]);
+ case 4:
+ return new SetStrokeColorDeviceCmyk(color[0], color[1], color[2], color[3]);
+ default:
+ throw new ArgumentException("TODO", nameof(color));
+ }
+ }
+
+ private StreamToken? GenerateHighlightNormalAppearanceAsStream(Annotation annotation)
+ {
+ // TODO - draws on top of text, should be below
+ // https://github.com/apache/pdfbox/blob/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/PDHighlightAppearanceHandler.java
+ PdfRectangle rect = annotation.Rectangle;
+
+ if (!annotation.AnnotationDictionary.TryGet(NameToken.Quadpoints, PdfScanner, out var quadpoints))
+ {
+ return null;
+ }
+
+ var pathsArray = quadpoints.Data.OfType().Select(x => (float)x.Double).ToArray();
+
+ var ab = annotation.Border;
+
+ if (!annotation.AnnotationDictionary.TryGet(NameToken.C, PdfScanner, out var colorToken) || colorToken.Data.Count == 0)
+ {
+ return null;
+ }
+ var color = colorToken.Data.OfType().Select(x => x.Data).ToArray();
+
+ decimal width = ab.BorderWidth;
+
+ // Adjust rectangle even if not empty, see PLPDF.com-MarkupAnnotations.pdf
+ //TODO in a class structure this should be overridable
+ // this is similar to polyline but different data type
+ //TODO padding should consider the curves too; needs to know in advance where the curve is
+ float minX = float.MaxValue;
+ float minY = float.MaxValue;
+ float maxX = float.MinValue;
+ float maxY = float.MinValue;
+ for (int i = 0; i < pathsArray.Length / 2; ++i)
+ {
+ float x = pathsArray[i * 2];
+ float y = pathsArray[i * 2 + 1];
+ minX = Math.Min(minX, x);
+ minY = Math.Min(minY, y);
+ maxX = Math.Max(maxX, x);
+ maxY = Math.Max(maxY, y);
+ }
+
+ // get the delta used for curves and use it for padding
+ float maxDelta = 0;
+ for (int i = 0; i < pathsArray.Length / 8; ++i)
+ {
+ // one of the two is 0, depending whether the rectangle is
+ // horizontal or vertical
+ // if it is diagonal then... uh...
+ float delta = Math.Max((pathsArray[i + 0] - pathsArray[i + 4]) / 4,
+ (pathsArray[i + 1] - pathsArray[i + 5]) / 4);
+ maxDelta = Math.Max(delta, maxDelta);
+ }
+
+ var setLowerLeftX = Math.Min(minX - (float)width / 2.0, rect.BottomLeft.X);
+ var setLowerLeftY = Math.Min(minY - (float)width / 2.0, rect.BottomLeft.Y);
+ var setUpperRightX = Math.Max(maxX + (float)width / 2.0, rect.TopRight.X);
+ var setUpperRightY = Math.Max(maxY + (float)width / 2.0, rect.TopRight.Y);
+ PdfRectangle pdfRectangle = new PdfRectangle(setLowerLeftX, setLowerLeftY, setUpperRightX, setUpperRightY);
+
+ try
+ {
+ using (var ms = new MemoryStream())
+ {
+ /*
+ PDExtendedGraphicsState r0 = new PDExtendedGraphicsState();
+ PDExtendedGraphicsState r1 = new PDExtendedGraphicsState();
+ r0.setAlphaSourceFlag(false);
+ r0.setStrokingAlphaConstant(annotation.getConstantOpacity());
+ r0.setNonStrokingAlphaConstant(annotation.getConstantOpacity());
+ r1.setAlphaSourceFlag(false);
+ r1.setBlendMode(BlendMode.MULTIPLY);
+ cs.setGraphicsStateParameters(r0);
+ cs.setGraphicsStateParameters(r1);
+ PDFormXObject frm1 = new PDFormXObject(createCOSStream());
+ PDFormXObject frm2 = new PDFormXObject(createCOSStream());
+ frm1.setResources(new PDResources());
+ try (PDFormContentStream mwfofrmCS = new PDFormContentStream(frm1))
+ {
+ mwfofrmCS.drawForm(frm2);
+ }
+ frm1.setBBox(annotation.getRectangle());
+ COSDictionary groupDict = new COSDictionary();
+ groupDict.setItem(COSName.S, COSName.TRANSPARENCY);
+ //TODO PDFormXObject.setGroup() is missing
+ frm1.getCOSObject().setItem(COSName.GROUP, groupDict);
+ cs.drawForm(frm1);
+ frm2.setBBox(annotation.getRectangle());
+ */
+
+ GetAnnotationNonStrokeColorOperation(color)?.Write(ms);
+
+ int of = 0;
+ while (of + 7 < pathsArray.Length)
+ {
+ // quadpoints spec sequence is incorrect, correct one is (4,5 0,1 2,3 6,7)
+ // https://stackoverflow.com/questions/9855814/pdf-spec-vs-acrobat-creation-quadpoints
+
+ // for "curvy" highlighting, two Bézier control points are used that seem to have a
+ // distance of about 1/4 of the height.
+ // note that curves won't appear if outside of the rectangle
+ float delta = 0;
+ if (pathsArray[of + 0] == pathsArray[of + 4] &&
+ pathsArray[of + 1] == pathsArray[of + 3] &&
+ pathsArray[of + 2] == pathsArray[of + 6] &&
+ pathsArray[of + 5] == pathsArray[of + 7])
+ {
+ // horizontal highlight
+ delta = (pathsArray[of + 1] - pathsArray[of + 5]) / 4;
+ }
+ else if (pathsArray[of + 1] == pathsArray[of + 5] &&
+ pathsArray[of + 0] == pathsArray[of + 2] &&
+ pathsArray[of + 3] == pathsArray[of + 7] &&
+ pathsArray[of + 4] == pathsArray[of + 6])
+ {
+ // vertical highlight
+ delta = (pathsArray[of + 0] - pathsArray[of + 4]) / 4;
+ }
+
+ new BeginNewSubpath((decimal)pathsArray[of + 4], (decimal)pathsArray[of + 5]).Write(ms);
+
+ if (pathsArray[of + 0] == pathsArray[of + 4])
+ {
+ // horizontal highlight
+ new AppendDualControlPointBezierCurve(
+ (decimal)(pathsArray[of + 4] - delta), (decimal)(pathsArray[of + 5] + delta),
+ (decimal)(pathsArray[of + 0] - delta), (decimal)(pathsArray[of + 1] - delta),
+ (decimal)pathsArray[of + 0], (decimal)pathsArray[of + 1])
+ .Write(ms);
+ }
+ else if (pathsArray[of + 5] == pathsArray[of + 1])
+ {
+ // vertical highlight
+ new AppendDualControlPointBezierCurve(
+ (decimal)(pathsArray[of + 4] + delta), (decimal)(pathsArray[of + 5] + delta),
+ (decimal)(pathsArray[of + 0] - delta), (decimal)(pathsArray[of + 1] + delta),
+ (decimal)pathsArray[of + 0], (decimal)pathsArray[of + 1])
+ .Write(ms);
+ }
+ else
+ {
+ new AppendStraightLineSegment((decimal)pathsArray[of + 0], (decimal)pathsArray[of + 1])
+ .Write(ms);
+ }
+ new AppendStraightLineSegment((decimal)pathsArray[of + 2], (decimal)pathsArray[of + 3]).
+ Write(ms);
+
+ if (pathsArray[of + 2] == pathsArray[of + 6])
+ {
+ // horizontal highlight
+ new AppendDualControlPointBezierCurve(
+ (decimal)(pathsArray[of + 2] + delta), (decimal)(pathsArray[of + 3] - delta),
+ (decimal)(pathsArray[of + 6] + delta), (decimal)(pathsArray[of + 7] + delta),
+ (decimal)pathsArray[of + 6], (decimal)pathsArray[of + 7])
+ .Write(ms);
+ }
+ else if (pathsArray[of + 3] == pathsArray[of + 7])
+ {
+ // vertical highlight
+ new AppendDualControlPointBezierCurve(
+ (decimal)(pathsArray[of + 2] - delta), (decimal)(pathsArray[of + 3] - delta),
+ (decimal)(pathsArray[of + 6] + delta), (decimal)(pathsArray[of + 7] - delta),
+ (decimal)pathsArray[of + 6], (decimal)pathsArray[of + 7])
+ .Write(ms);
+ }
+ else
+ {
+ new AppendStraightLineSegment((decimal)pathsArray[of + 6], (decimal)pathsArray[of + 7])
+ .Write(ms);
+ }
+
+ PdfPig.Graphics.Operations.PathPainting.FillPathEvenOddRule.Value.Write(ms);
+ of += 8;
+ }
+
+ // https://github.com/apache/pdfbox/blob/c4b212ecf42a1c0a55529873b132ea338a8ba901/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/PDAbstractAppearanceHandler.java#L511
+ var dict = dictionary; //.Data.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
+
+ /*
+ private void setTransformationMatrix(PDAppearanceStream appearanceStream)
+ {
+ PDRectangle bbox = getRectangle();
+ appearanceStream.setBBox(bbox);
+ AffineTransform transform = AffineTransform.getTranslateInstance(-bbox.getLowerLeftX(),
+ -bbox.getLowerLeftY());
+ appearanceStream.setMatrix(transform);
+ }
+ */
+ if (annotation.AnnotationDictionary.TryGet(NameToken.Rect, out var rectToken))
+ {
+ dict = dict.With(NameToken.Bbox.Data, rectToken);
+ //dict.Add(NameToken.Bbox.Data, rectToken); // should use new rect
+ }
+
+ return new StreamToken(dict, ms.ToArray());
+ }
+ }
+ catch (Exception)
+ {
+ Console.WriteLine("");
+ // log
+ }
+ return null;
+ }
+
+ private static PdfRectangle GetPaddedRectangle(PdfRectangle rectangle, float padding)
+ {
+ return new PdfRectangle(
+ rectangle.BottomLeft.X + padding,
+ rectangle.BottomLeft.Y + padding,
+ rectangle.BottomLeft.X + (rectangle.Width - 2 * padding),
+ rectangle.BottomLeft.Y + (rectangle.Height - 2 * padding));
+ }
+
+ private StreamToken? GenerateLinkNormalAppearanceAsStream(Annotation annotation)
+ {
+ // https://github.com/apache/pdfbox/blob/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/PDLinkAppearanceHandler.java
+
+ PdfRectangle rect = annotation.Rectangle;
+
+ var ab = annotation.Border;
+ try
+ {
+ using (var ms = new MemoryStream())
+ {
+ decimal[] color = null;
+ if (annotation.AnnotationDictionary.TryGet(NameToken.C, PdfScanner, out var colorToken) && colorToken.Data.Count > 0)
+ {
+ color = colorToken.Data.OfType().Select(x => x.Data).ToArray();
+ }
+ else
+ {
+ // spec is unclear, but black is what Adobe does
+ //color = new decimal[] { 0 }; // DeviceGray black (from Pdfbox)
+ color = Array.Empty(); // Empty array, transparant
+ }
+
+ BaseRenderStreamProcessor.GetAnnotationStrokeColorOperation(color)?.Write(ms);
+
+ decimal lineWidth = ab.BorderWidth;
+
+ new PdfPig.Graphics.Operations.General.SetLineWidth(lineWidth).Write(ms);
+
+ // Acrobat applies a padding to each side of the bbox so the line is completely within
+ // the bbox.
+ PdfRectangle borderEdge = BaseRenderStreamProcessor.GetPaddedRectangle(rect, (float)(lineWidth / 2.0m));
+
+ float[] pathsArray = null;
+ if (annotation.AnnotationDictionary.TryGet(NameToken.Quadpoints, PdfScanner, out var quadpoints))
+ {
+ pathsArray = quadpoints.Data?.OfType().Select(x => (float)x.Double)?.ToArray();
+ }
+
+ if (pathsArray != null)
+ {
+ // QuadPoints shall be ignored if any coordinate in the array lies outside
+ // the region specified by Rect.
+ for (int i = 0; i < pathsArray.Length / 2; ++i)
+ {
+ if (!rect.Contains(new PdfPoint(pathsArray[i * 2], pathsArray[i * 2 + 1])))
+ {
+ //LOG.warn("At least one /QuadPoints entry (" +
+ // pathsArray[i * 2] + ";" + pathsArray[i * 2 + 1] +
+ // ") is outside of rectangle, " + rect +
+ // ", /QuadPoints are ignored and /Rect is used instead");
+ pathsArray = null;
+ break;
+ }
+ }
+ }
+
+ if (pathsArray == null)
+ {
+ // Convert rectangle coordinates as if it was a /QuadPoints entry
+ pathsArray = new float[8];
+ pathsArray[0] = (float)borderEdge.BottomLeft.X;
+ pathsArray[1] = (float)borderEdge.BottomLeft.Y;
+ pathsArray[2] = (float)borderEdge.TopRight.X;
+ pathsArray[3] = (float)borderEdge.BottomLeft.Y;
+ pathsArray[4] = (float)borderEdge.TopRight.X;
+ pathsArray[5] = (float)borderEdge.TopRight.Y;
+ pathsArray[6] = (float)borderEdge.BottomLeft.X;
+ pathsArray[7] = (float)borderEdge.TopRight.Y;
+ }
+
+ bool underlined = false;
+ if (pathsArray.Length >= 8)
+ {
+ // Get border style
+ if (annotation.AnnotationDictionary.TryGet(NameToken.Bs, PdfScanner, out var borderStyleToken))
+ {
+ if (borderStyleToken.TryGet(NameToken.S, PdfScanner, out var styleToken))
+ {
+ underlined = styleToken.Data.Equals("U");
+ // Optional) The border style:
+ // S (Solid) A solid rectangle surrounding the annotation.
+ // D (Dashed) A dashed rectangle surrounding the annotation. The dash pattern may be specified by the D entry.
+ // B (Beveled) A simulated embossed rectangle that appears to be raised above the surface of the page.
+ // I (Inset) A simulated engraved rectangle that appears to be recessed below the surface of the page.
+ // U (Underline) A single line along the bottom of the annotation rectangle.
+ // A conforming reader shall tolerate other border styles that it does not recognize and shall use the default value.
+ }
+ }
+ }
+
+ int of = 0;
+ while (of + 7 < pathsArray.Length)
+ {
+ new BeginNewSubpath((decimal)pathsArray[of], (decimal)pathsArray[of + 1]).Write(ms);
+ new AppendStraightLineSegment((decimal)pathsArray[of + 2], (decimal)pathsArray[of + 3]).Write(ms);
+ if (!underlined)
+ {
+ new AppendStraightLineSegment((decimal)pathsArray[of + 4], (decimal)pathsArray[of + 5]).Write(ms);
+ new AppendStraightLineSegment((decimal)pathsArray[of + 6], (decimal)pathsArray[of + 7]).Write(ms);
+ PdfPig.Graphics.Operations.PathConstruction.CloseSubpath.Value.Write(ms);
+ }
+ of += 8;
+ }
+
+ if (lineWidth > 0 && color.Length > 0) // TO CHECK
+ {
+ PdfPig.Graphics.Operations.PathPainting.StrokePath.Value.Write(ms);
+ }
+ //contentStream.drawShape(lineWidth, hasStroke, false);
+
+ // https://github.com/apache/pdfbox/blob/c4b212ecf42a1c0a55529873b132ea338a8ba901/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/PDAbstractAppearanceHandler.java#L511
+ var dict = dictionary; //.Data.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
+
+ /*
+ private void setTransformationMatrix(PDAppearanceStream appearanceStream)
+ {
+ PDRectangle bbox = getRectangle();
+ appearanceStream.setBBox(bbox);
+ AffineTransform transform = AffineTransform.getTranslateInstance(-bbox.getLowerLeftX(),
+ -bbox.getLowerLeftY());
+ appearanceStream.setMatrix(transform);
+ }
+ */
+ if (annotation.AnnotationDictionary.TryGet(NameToken.Rect, out var rectToken))
+ {
+ dict = dict.With(NameToken.Bbox.Data, rectToken); // should use new rect
+ }
+
+ return new StreamToken(dict, ms.ToArray());
+ }
+ }
+ catch (Exception)
+ {
+ Console.WriteLine("");
+ // log
+ }
+
+ return null;
+ }
+
+ private StreamToken? GenerateStrikeOutNormalAppearanceAsStream(Annotation annotation)
+ {
+ // https://github.com/apache/pdfbox/blob/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/PDStrikeoutAppearanceHandler.java
+
+ PdfRectangle rect = annotation.Rectangle;
+
+ if (!annotation.AnnotationDictionary.TryGet(NameToken.Quadpoints, PdfScanner, out var quadpoints))
+ {
+ return null;
+ }
+
+ var pathsArray = quadpoints.Data.OfType().Select(x => (float)x.Double).ToArray();
+
+ var ab = annotation.Border;
+
+ if (!annotation.AnnotationDictionary.TryGet(NameToken.C, PdfScanner, out var colorToken) || colorToken.Data.Count == 0)
+ {
+ return null;
+ }
+ var color = colorToken.Data.OfType().Select(x => x.Data).ToArray();
+
+ decimal width = ab.BorderWidth;
+ if (width == 0)
+ {
+ width = 1.5m;
+ }
+
+ // Adjust rectangle even if not empty, see PLPDF.com-MarkupAnnotations.pdf
+ //TODO in a class structure this should be overridable
+ // this is similar to polyline but different data type
+ float minX = float.MaxValue;
+ float minY = float.MaxValue;
+ float maxX = float.MinValue;
+ float maxY = float.MinValue;
+ for (int i = 0; i < pathsArray.Length / 2; ++i)
+ {
+ float x = pathsArray[i * 2];
+ float y = pathsArray[i * 2 + 1];
+ minX = Math.Min(minX, x);
+ minY = Math.Min(minY, y);
+ maxX = Math.Max(maxX, x);
+ maxY = Math.Max(maxY, y);
+ }
+ var setLowerLeftX = Math.Min(minX - (float)width / 2.0, rect.BottomLeft.X); //.getLowerLeftX()));
+ var setLowerLeftY = Math.Min(minY - (float)width / 2.0, rect.BottomLeft.Y); // .getLowerLeftY()));
+ var setUpperRightX = Math.Max(maxX + (float)width / 2.0, rect.TopRight.X); //.getUpperRightX()));
+ var setUpperRightY = Math.Max(maxY + (float)width / 2.0, rect.TopRight.Y); //rect.getUpperRightY()));
+ PdfRectangle pdfRectangle = new PdfRectangle(setLowerLeftX, setLowerLeftY, setUpperRightX, setUpperRightY); //annotation.setRectangle(rect);
+
+ try
+ {
+ using (var ms = new MemoryStream())
+ {
+ //setOpacity(cs, annotation.getConstantOpacity()); // TODO
+
+ BaseRenderStreamProcessor.GetAnnotationStrokeColorOperation(color)?.Write(ms);
+
+ //if (ab.dashArray != null)
+ //{
+ // cs.setLineDashPattern(ab.dashArray, 0);
+ //}
+
+ new PdfPig.Graphics.Operations.General.SetLineWidth(width).Write(ms);
+
+ // spec is incorrect
+ // https://stackoverflow.com/questions/9855814/pdf-spec-vs-acrobat-creation-quadpoints
+ for (int i = 0; i < pathsArray.Length / 8; ++i)
+ {
+ // get mid point between bounds, subtract the line width to approximate what Adobe is doing
+ // See e.g. CTAN-example-Annotations.pdf and PLPDF.com-MarkupAnnotations.pdf
+ // and https://bugs.ghostscript.com/show_bug.cgi?id=693664
+ // do the math for diagonal annotations with this weird old trick:
+ // https://stackoverflow.com/questions/7740507/extend-a-line-segment-a-specific-distance
+ float len0 = (float)Math.Sqrt(Math.Pow(pathsArray[i * 8] - pathsArray[i * 8 + 4], 2) +
+ Math.Pow(pathsArray[i * 8 + 1] - pathsArray[i * 8 + 5], 2));
+ float x0 = pathsArray[i * 8 + 4];
+ float y0 = pathsArray[i * 8 + 5];
+ if (len0 != 0)
+ {
+ // only if both coordinates are not identical to avoid divide by zero
+ x0 += (pathsArray[i * 8] - pathsArray[i * 8 + 4]) / len0 * (len0 / 2 - (float)ab.BorderWidth);
+ y0 += (pathsArray[i * 8 + 1] - pathsArray[i * 8 + 5]) / len0 * (len0 / 2 - (float)ab.BorderWidth);
+ }
+ float len1 = (float)Math.Sqrt(Math.Pow(pathsArray[i * 8 + 2] - pathsArray[i * 8 + 6], 2) +
+ Math.Pow(pathsArray[i * 8 + 3] - pathsArray[i * 8 + 7], 2));
+ float x1 = pathsArray[i * 8 + 6];
+ float y1 = pathsArray[i * 8 + 7];
+ if (len1 != 0)
+ {
+ // only if both coordinates are not identical to avoid divide by zero
+ x1 += (pathsArray[i * 8 + 2] - pathsArray[i * 8 + 6]) / len1 * (len1 / 2 - (float)ab.BorderWidth);
+ y1 += (pathsArray[i * 8 + 3] - pathsArray[i * 8 + 7]) / len1 * (len1 / 2 - (float)ab.BorderWidth);
+ }
+ new BeginNewSubpath((decimal)x0, (decimal)y0).Write(ms);
+ new AppendStraightLineSegment((decimal)x1, (decimal)y1).Write(ms);
+ }
+ PdfPig.Graphics.Operations.PathPainting.StrokePath.Value.Write(ms);
+
+ // https://github.com/apache/pdfbox/blob/c4b212ecf42a1c0a55529873b132ea338a8ba901/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/PDAbstractAppearanceHandler.java#L511
+ var dict = dictionary; //.Data.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
+
+ /*
+ private void setTransformationMatrix(PDAppearanceStream appearanceStream)
+ {
+ PDRectangle bbox = getRectangle();
+ appearanceStream.setBBox(bbox);
+ AffineTransform transform = AffineTransform.getTranslateInstance(-bbox.getLowerLeftX(),
+ -bbox.getLowerLeftY());
+ appearanceStream.setMatrix(transform);
+ }
+ */
+ if (annotation.AnnotationDictionary.TryGet(NameToken.Rect, out var rectToken))
+ {
+ dict = dict.With(NameToken.Bbox.Data, rectToken); // should use new rect
+ }
+
+ return new StreamToken(dict, ms.ToArray());
+ }
+ }
+ catch (Exception)
+ {
+ Console.WriteLine("");
+ // log
+ }
+
+ return null;
+ }
+
+ private StreamToken? GenerateUnderlineNormalAppearanceAsStream(Annotation annotation)
+ {
+ // https://github.com/apache/pdfbox/blob/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/PDStrikeoutAppearanceHandler.java
+
+ PdfRectangle rect = annotation.Rectangle;
+
+ if (!annotation.AnnotationDictionary.TryGet(NameToken.Quadpoints, PdfScanner, out var quadpoints))
+ {
+ return null;
+ }
+
+ var pathsArray = quadpoints.Data.OfType().Select(x => (float)x.Double).ToArray();
+
+ var ab = annotation.Border;
+
+ if (!annotation.AnnotationDictionary.TryGet(NameToken.C, PdfScanner, out var colorToken) || colorToken.Data.Count == 0)
+ {
+ return null;
+ }
+ var color = colorToken.Data.OfType().Select(x => x.Data).ToArray();
+
+ decimal width = ab.BorderWidth;
+ if (width == 0)
+ {
+ // value found in adobe reader
+ width = 1.5m;
+ }
+
+ // Adjust rectangle even if not empty, see PLPDF.com-MarkupAnnotations.pdf
+ //TODO in a class structure this should be overridable
+ // this is similar to polyline but different data type
+ // all coordinates (unlike painting) are used because I'm lazy
+ float minX = float.MaxValue;
+ float minY = float.MaxValue;
+ float maxX = float.MinValue;
+ float maxY = float.MinValue;
+ for (int i = 0; i < pathsArray.Length / 2; ++i)
+ {
+ float x = pathsArray[i * 2];
+ float y = pathsArray[i * 2 + 1];
+ minX = Math.Min(minX, x);
+ minY = Math.Min(minY, y);
+ maxX = Math.Max(maxX, x);
+ maxY = Math.Max(maxY, y);
+ }
+ var setLowerLeftX = Math.Min(minX - (float)width / 2.0, rect.BottomLeft.X);
+ var setLowerLeftY = Math.Min(minY - (float)width / 2.0, rect.BottomLeft.Y);
+ var setUpperRightX = Math.Max(maxX + (float)width / 2.0, rect.TopRight.X);
+ var setUpperRightY = Math.Max(maxY + (float)width / 2.0, rect.TopRight.Y);
+ PdfRectangle pdfRectangle = new PdfRectangle(setLowerLeftX, setLowerLeftY, setUpperRightX, setUpperRightY);
+
+ try
+ {
+ using (var ms = new MemoryStream())
+ {
+ //setOpacity(cs, annotation.getConstantOpacity()); // TODO
+
+ BaseRenderStreamProcessor.GetAnnotationStrokeColorOperation(color)?.Write(ms);
+
+ //if (ab.dashArray != null)
+ //{
+ // cs.setLineDashPattern(ab.dashArray, 0);
+ //}
+
+ new PdfPig.Graphics.Operations.General.SetLineWidth(width).Write(ms);
+
+ // spec is incorrect
+ // https://stackoverflow.com/questions/9855814/pdf-spec-vs-acrobat-creation-quadpoints
+ for (int i = 0; i < pathsArray.Length / 8; ++i)
+ {
+ // Adobe doesn't use the lower coordinate for the line, it uses lower + delta / 7.
+ // do the math for diagonal annotations with this weird old trick:
+ // https://stackoverflow.com/questions/7740507/extend-a-line-segment-a-specific-distance
+ float len0 = (float)Math.Sqrt(Math.Pow(pathsArray[i * 8] - pathsArray[i * 8 + 4], 2) +
+ Math.Pow(pathsArray[i * 8 + 1] - pathsArray[i * 8 + 5], 2));
+ float x0 = pathsArray[i * 8 + 4];
+ float y0 = pathsArray[i * 8 + 5];
+ if (len0 != 0)
+ {
+ // only if both coordinates are not identical to avoid divide by zero
+ x0 += (pathsArray[i * 8] - pathsArray[i * 8 + 4]) / len0 * len0 / 7;
+ y0 += (pathsArray[i * 8 + 1] - pathsArray[i * 8 + 5]) / len0 * (len0 / 7);
+ }
+ float len1 = (float)Math.Sqrt(Math.Pow(pathsArray[i * 8 + 2] - pathsArray[i * 8 + 6], 2) +
+ Math.Pow(pathsArray[i * 8 + 3] - pathsArray[i * 8 + 7], 2));
+ float x1 = pathsArray[i * 8 + 6];
+ float y1 = pathsArray[i * 8 + 7];
+ if (len1 != 0)
+ {
+ // only if both coordinates are not identical to avoid divide by zero
+ x1 += (pathsArray[i * 8 + 2] - pathsArray[i * 8 + 6]) / len1 * len1 / 7;
+ y1 += (pathsArray[i * 8 + 3] - pathsArray[i * 8 + 7]) / len1 * len1 / 7;
+ }
+
+ new BeginNewSubpath((decimal)x0, (decimal)y0).Write(ms);
+ new AppendStraightLineSegment((decimal)x1, (decimal)y1).Write(ms);
+ }
+ PdfPig.Graphics.Operations.PathPainting.StrokePath.Value.Write(ms);
+
+ // https://github.com/apache/pdfbox/blob/c4b212ecf42a1c0a55529873b132ea338a8ba901/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/PDAbstractAppearanceHandler.java#L511
+ var dict = dictionary; //.Data.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
+
+ /*
+ private void setTransformationMatrix(PDAppearanceStream appearanceStream)
+ {
+ PDRectangle bbox = getRectangle();
+ appearanceStream.setBBox(bbox);
+ AffineTransform transform = AffineTransform.getTranslateInstance(-bbox.getLowerLeftX(),
+ -bbox.getLowerLeftY());
+ appearanceStream.setMatrix(transform);
+ }
+ */
+ if (annotation.AnnotationDictionary.TryGet(NameToken.Rect, out var rectToken))
+ {
+ dict = dict.With(NameToken.Bbox.Data, rectToken); // should use new rect
+ }
+
+ return new StreamToken(dict, ms.ToArray());
+ }
+ }
+ catch (Exception)
+ {
+ Console.WriteLine("");
+ // log
+ }
+
+ return null;
+ }
+
+ ///
+ /// TODO
+ ///
+ protected IToken? GetNormalAppearance(Annotation annotation)
+ {
+ var dict = GetAppearance(annotation);
+
+ if (dict == null)
+ {
+ return null;
+ }
+
+ // get Normal Appearance
+ if (!dict.Data.TryGetValue(NameToken.N, out var data))
+ {
+ return null;
+ }
+
+ if (data is IndirectReferenceToken irt)
+ {
+ data = Get(irt);
+ if (data is null)
+ {
+ return null;
+ }
+ }
+
+ if (data is StreamToken streamToken)
+ {
+ return streamToken;
+ }
+ else if (data is DictionaryToken dictionaryToken)
+ {
+ return dictionaryToken;
+ }
+ else if (data is ObjectToken objectToken)
+ {
+ if (objectToken.Data is StreamToken streamToken2)
+ {
+ return streamToken2;
+ }
+ else if (objectToken.Data is DictionaryToken dictionaryToken2)
+ {
+ return dictionaryToken2;
+ }
+ }
+
+ throw new ArgumentException();
+ }
+
+ ///
+ /// TODO
+ ///
+ protected IPdfImage GetImageFromXObject(XObjectContentRecord xObjectContentRecord)
+ {
+ return XObjectFactory.ReadImage(xObjectContentRecord, PdfScanner, FilterProvider, ResourceStore);
+ }
+
+ ///
+ /// TODO
+ ///
+ ///
+ protected IToken Get(IndirectReferenceToken nameToken)
+ {
+ return base.PdfScanner.Get(nameToken.Data);
+ }
+
+ ///
+ /// TODO
+ ///
+ ///
+ ///
+ ///
+ ///
+ protected static (double x, double y) TransformPoint(TransformationMatrix first, TransformationMatrix second, TransformationMatrix third, PdfPoint tl)
+ {
+ var topLeftX = tl.X;
+ var topLeftY = tl.Y;
+
+ // First
+ var x = first.A * topLeftX + first.C * topLeftY + first.E;
+ var y = first.B * topLeftX + first.D * topLeftY + first.F;
+ topLeftX = x;
+ topLeftY = y;
+
+ // Second
+ x = second.A * topLeftX + second.C * topLeftY + second.E;
+ y = second.B * topLeftX + second.D * topLeftY + second.F;
+ topLeftX = x;
+ topLeftY = y;
+
+ // Third
+ x = third.A * topLeftX + third.C * topLeftY + third.E;
+ y = third.B * topLeftX + third.D * topLeftY + third.F;
+ topLeftX = x;
+ topLeftY = y;
+
+ return (topLeftX, topLeftY);
+ }
+
+ ///
+ public override void BeginMarkedContent(NameToken name, NameToken propertyDictionaryName, DictionaryToken properties)
+ {
+ // Do nothing
+ }
+
+ ///
+ public override void EndMarkedContent()
+ {
+ // Do nothing
+ }
+
+ ///
+ public override void PaintShading(NameToken shading)
+ {
+ RenderShading(ResourceStore.GetShading(shading));
+ }
+
+ ///
+ /// TODO
+ ///
+ ///
+ protected abstract void RenderShading(Shading shading);
+ }
+}
diff --git a/src/UglyToad.PdfPig.Rendering.Skia/Graphics/SkiaExtensions.cs b/src/UglyToad.PdfPig.Rendering.Skia/Graphics/SkiaExtensions.cs
new file mode 100644
index 000000000..fa466ba5d
--- /dev/null
+++ b/src/UglyToad.PdfPig.Rendering.Skia/Graphics/SkiaExtensions.cs
@@ -0,0 +1,343 @@
+using SkiaSharp;
+using UglyToad.PdfPig.Content;
+using UglyToad.PdfPig.Core;
+using UglyToad.PdfPig.Graphics;
+using UglyToad.PdfPig.Graphics.Colors;
+using UglyToad.PdfPig.Graphics.Core;
+using UglyToad.PdfPig.PdfFonts;
+using static UglyToad.PdfPig.Core.PdfSubpath;
+
+namespace UglyToad.PdfPig.Rendering.Skia.Graphics
+{
+ internal static class SkiaExtensions
+ {
+ public static SKFontStyle GetFontStyle(this FontDetails fontDetails)
+ {
+ if (fontDetails.IsBold && fontDetails.IsItalic)
+ {
+ return SKFontStyle.BoldItalic;
+ }
+ else if (fontDetails.IsBold)
+ {
+ return SKFontStyle.Bold;
+ }
+ else if (fontDetails.IsItalic)
+ {
+ return SKFontStyle.Italic;
+ }
+ return SKFontStyle.Normal;
+ }
+
+ public static string GetCleanFontName(this IFont font)
+ {
+ string fontName = font.Name;
+ if (fontName.Length > 7 && fontName[6].Equals('+'))
+ {
+ string subset = fontName.Substring(0, 6);
+ if (subset.Equals(subset.ToUpper()))
+ {
+ return fontName.Split('+')[1];
+ }
+ }
+
+ return fontName;
+ }
+
+ public static SKPath PdfPathToGraphicsPath(this PdfPath path, int height)
+ {
+ var gp = PdfSubpathsToGraphicsPath(path, height);
+ gp.FillType = path.FillingRule == FillingRule.NonZeroWinding ? SKPathFillType.Winding : SKPathFillType.EvenOdd;
+ return gp;
+ }
+
+ public static SKPath PdfSubpathsToGraphicsPath(this IReadOnlyList pdfSubpaths, int height)
+ {
+ var gp = new SKPath();
+
+ foreach (var subpath in pdfSubpaths)
+ {
+ foreach (var c in subpath.Commands)
+ {
+ if (c is Move move)
+ {
+ gp.MoveTo(move.Location.ToSKPoint(height));
+ }
+ else if (c is Line line)
+ {
+ gp.LineTo(line.To.ToSKPoint(height));
+ }
+ else if (c is BezierCurve curve)
+ {
+ if (curve.StartPoint.Equals(curve.FirstControlPoint))
+ {
+ // Quad curve
+ gp.QuadTo(curve.SecondControlPoint.ToSKPoint(height),
+ curve.EndPoint.ToSKPoint(height));
+ }
+ else
+ {
+ // Cubic curve
+ gp.CubicTo(curve.FirstControlPoint.ToSKPoint(height),
+ curve.SecondControlPoint.ToSKPoint(height),
+ curve.EndPoint.ToSKPoint(height));
+ }
+ }
+ else if (c is Close)
+ {
+ gp.Close();
+ }
+ }
+ }
+ return gp;
+ }
+
+ public static SKPoint ToSKPoint(this PdfPoint pdfPoint, int height)
+ {
+ return new SKPoint((float)(pdfPoint.X), (float)(height - pdfPoint.Y));
+ }
+
+ public static SKStrokeJoin ToSKStrokeJoin(this LineJoinStyle lineJoinStyle)
+ {
+ switch (lineJoinStyle)
+ {
+ case LineJoinStyle.Bevel:
+ return SKStrokeJoin.Bevel;
+
+ case LineJoinStyle.Miter:
+ return SKStrokeJoin.Miter;
+
+ case LineJoinStyle.Round:
+ return SKStrokeJoin.Round;
+
+ default:
+ throw new NotImplementedException($"Unknown LineJoinStyle '{lineJoinStyle}'.");
+ }
+ }
+
+ public static SKStrokeCap ToSKStrokeCap(this LineCapStyle lineCapStyle)
+ {
+ switch (lineCapStyle) // to put in helper
+ {
+ case LineCapStyle.Butt:
+ return SKStrokeCap.Butt;
+
+ case LineCapStyle.ProjectingSquare:
+ return SKStrokeCap.Square;
+
+ case LineCapStyle.Round:
+ return SKStrokeCap.Round;
+
+ default:
+ throw new NotImplementedException($"Unknown LineCapStyle '{lineCapStyle}'.");
+ }
+ }
+
+ public static SKPathEffect? ToSKPathEffect(this LineDashPattern lineDashPattern)
+ {
+ const float oneOver72 = (float)(1.0 / 72.0);
+
+ if (lineDashPattern.Phase != 0 || lineDashPattern.Array?.Count > 0) // to put in helper
+ {
+ //* https://docs.microsoft.com/en-us/dotnet/api/system.drawing.pen.dashpattern?view=dotnet-plat-ext-3.1
+ //* The elements in the dashArray array set the length of each dash and space in the dash pattern.
+ //* The first element sets the length of a dash, the second element sets the length of a space, the
+ //* third element sets the length of a dash, and so on. Consequently, each element should be a
+ //* non-zero positive number.
+
+ if (lineDashPattern.Array.Count == 1)
+ {
+ List pattern = new List();
+ var v = lineDashPattern.Array[0];
+ pattern.Add((float)v);
+ pattern.Add((float)v);
+ return SKPathEffect.CreateDash(pattern.ToArray(), (float)v); // TODO
+ }
+ else if (lineDashPattern.Array.Count > 0)
+ {
+ List pattern = new List();
+ for (int i = 0; i < lineDashPattern.Array.Count; i++)
+ {
+ var v = lineDashPattern.Array[i];
+ if (v == 0)
+ {
+ pattern.Add(oneOver72);
+ }
+ else
+ {
+ pattern.Add((float)v);
+ }
+ }
+ //pen.DashPattern = pattern.ToArray(); // TODO
+ return SKPathEffect.CreateDash(pattern.ToArray(), pattern[0]); // TODO
+ }
+ //pen.DashOffset = path.LineDashPattern.Value.Phase; // mult?? // // TODO
+ }
+ return null;
+ }
+
+ public static SKPathFillType ToSKPathFillType(this FillingRule fillingRule)
+ {
+ return fillingRule == FillingRule.NonZeroWinding ? SKPathFillType.Winding : SKPathFillType.EvenOdd;
+ }
+
+ public static SKColor ToSKColor(this IColor pdfColor, decimal alpha)
+ {
+ SKColor color = SKColors.Black;
+ if (pdfColor != null)
+ {
+ var colorRgb = pdfColor.ToRGBValues();
+ double r = colorRgb.r;
+ double g = colorRgb.g;
+ double b = colorRgb.b;
+
+ color = new SKColor(Convert.ToByte(r * 255), Convert.ToByte(g * 255), Convert.ToByte(b * 255));
+ }
+ return color.WithAlpha(Convert.ToByte(alpha * 255));
+ }
+
+ public static SKColor GetCurrentNonStrokingColorSKColor(this CurrentGraphicsState currentGraphicsState)
+ {
+ return currentGraphicsState.CurrentNonStrokingColor.ToSKColor(currentGraphicsState.AlphaConstantNonStroking);
+ }
+
+ public static SKColor GetCurrentStrokingColorSKColor(this CurrentGraphicsState currentGraphicsState)
+ {
+ return currentGraphicsState.CurrentStrokingColor.ToSKColor(currentGraphicsState.AlphaConstantStroking);
+ }
+
+ /*
+ private static bool doBlending = false;
+
+ public static SKBlendMode ToSKBlendMode(this BlendMode blendMode)
+ {
+ if (!doBlending)
+ {
+ return SKBlendMode.SrcOver;
+ }
+
+ switch (blendMode)
+ {
+ // Standard separable blend modes
+ case BlendMode.Normal:
+ case BlendMode.Compatible:
+ return SKBlendMode.SrcOver; // TODO - Check if correct
+
+ case BlendMode.Multiply:
+ return SKBlendMode.Multiply;
+
+ case BlendMode.Screen:
+ return SKBlendMode.Screen;
+
+ case BlendMode.Overlay:
+ return SKBlendMode.Overlay;
+
+ case BlendMode.Darken:
+ return SKBlendMode.Darken;
+
+ case BlendMode.Lighten:
+ return SKBlendMode.Lighten;
+
+ case BlendMode.ColorDodge:
+ return SKBlendMode.ColorDodge;
+
+ case BlendMode.ColorBurn:
+ return SKBlendMode.ColorBurn;
+
+ case BlendMode.HardLight:
+ return SKBlendMode.HardLight;
+
+ case BlendMode.SoftLight:
+ return SKBlendMode.SoftLight;
+
+ case BlendMode.Difference:
+ return SKBlendMode.Difference;
+
+ case BlendMode.Exclusion:
+ return SKBlendMode.Exclusion;
+
+ // Standard nonseparable blend modes
+ case BlendMode.Hue:
+ return SKBlendMode.Hue;
+
+ case BlendMode.Saturation:
+ return SKBlendMode.Saturation;
+
+ case BlendMode.Color:
+ return SKBlendMode.Color;
+
+ case BlendMode.Luminosity:
+ return SKBlendMode.Luminosity;
+
+ default:
+ throw new NotImplementedException($"Cannot convert blend mode '{blendMode}' to SKBlendMode.");
+ }
+ }
+ */
+
+ public static SKBitmap GetSKBitmap(this IPdfImage image)
+ {
+ var bitmap = SKBitmap.Decode(image.GetImageBytes());
+
+ /*
+ if (image.SMask != null)
+ {
+ byte[] bytesSMask = image.SMask.GetImageBytes();
+ using (var bitmapSMask = SKBitmap.Decode(bytesSMask))
+ {
+ bitmap.ApplySMask(bitmapSMask);
+ //SKMask mask = SKMask.Create(bitmapSMask.Bytes, bitmapSMask.Info.Rect, (uint)bitmapSMask.RowBytes, SKMaskFormat.A8);
+ //if (!bitmap.InstallMaskPixels(mask))
+ //{
+ // System.Diagnostics.Debug.WriteLine("Could not install mask pixels.");
+ //}
+ }
+ }
+ */
+ return bitmap;
+ }
+
+ public static void ApplySMask(this SKBitmap image, SKBitmap smask)
+ {
+ // What about 'Alpha source' flag?
+ SKBitmap scaled;
+ if (!image.Info.Rect.Equals(smask.Info.Rect))
+ {
+ scaled = new SKBitmap(image.Info);
+ if (!smask.ScalePixels(scaled, SKFilterQuality.High))
+ {
+ // log
+ }
+ }
+ else
+ {
+ scaled = smask;
+ }
+
+ for (int x = 0; x < image.Width; x++)
+ {
+ for (int y = 0; y < image.Height; y++)
+ {
+ var pix = image.GetPixel(x, y);
+ byte alpha = scaled.GetPixel(x, y).Red; // Gray CS (r = g = b)
+ image.SetPixel(x, y, pix.WithAlpha(alpha));
+ }
+ }
+ scaled.Dispose();
+ }
+
+ public static byte[] GetImageBytes(this IPdfImage pdfImage)
+ {
+ if (pdfImage.TryGetPng(out byte[] bytes) && bytes?.Length > 0)
+ {
+ return bytes;
+ }
+
+ if (pdfImage.TryGetBytes(out var bytesL) && bytesL?.Count > 0)
+ {
+ return bytesL.ToArray();
+ }
+
+ return pdfImage.RawBytes.ToArray();
+ }
+ }
+}
diff --git a/src/UglyToad.PdfPig.Rendering.Skia/Graphics/SkiaStreamProcessor.Annotations.cs b/src/UglyToad.PdfPig.Rendering.Skia/Graphics/SkiaStreamProcessor.Annotations.cs
new file mode 100644
index 000000000..8991a4e5a
--- /dev/null
+++ b/src/UglyToad.PdfPig.Rendering.Skia/Graphics/SkiaStreamProcessor.Annotations.cs
@@ -0,0 +1,109 @@
+using UglyToad.PdfPig.Annotations;
+using UglyToad.PdfPig.Core;
+using UglyToad.PdfPig.Geometry;
+using UglyToad.PdfPig.Tokens;
+
+namespace UglyToad.PdfPig.Rendering.Skia.Graphics
+{
+ internal partial class SkiaStreamProcessor
+ {
+ ///
+ /// Very hackish
+ ///
+ private static bool IsAnnotationBelowText(Annotation annotation)
+ {
+ switch (annotation.Type)
+ {
+ case AnnotationType.Highlight:
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ private void DrawAnnotations(bool isBelowText)
+ {
+ // https://github.com/apache/pdfbox/blob/trunk/pdfbox/src/main/java/org/apache/pdfbox/rendering/PageDrawer.java
+ // https://github.com/apache/pdfbox/blob/c4b212ecf42a1c0a55529873b132ea338a8ba901/pdfbox/src/main/java/org/apache/pdfbox/contentstream/PDFStreamEngine.java#L312
+ foreach (var annotation in page.ExperimentalAccess.GetAnnotations().Where(a => IsAnnotationBelowText(a) == isBelowText))
+ {
+ // Check if visible
+
+ // Get appearance
+ var appearance = base.GetNormalAppearanceAsStream(annotation);
+
+ PdfRectangle? bbox = null;
+ PdfRectangle? rect = annotation.Rectangle;
+
+ if (appearance is not null)
+ {
+ if (appearance.StreamDictionary.TryGet(NameToken.Bbox, out var bboxToken))
+ {
+ var points = bboxToken.Data.OfType().Select(x => x.Double).ToArray();
+ bbox = new PdfRectangle(points[0], points[1], points[2], points[3]);
+ }
+
+ // zero-sized rectangles are not valid
+ if (rect.HasValue && rect.Value.Width > 0 && rect.Value.Height > 0 &&
+ bbox.HasValue && bbox.Value.Width > 0 && bbox.Value.Height > 0)
+ {
+ var matrix = TransformationMatrix.Identity;
+ if (appearance.StreamDictionary.TryGet(NameToken.Matrix, out var matrixToken))
+ {
+ matrix = TransformationMatrix.FromArray(matrixToken.Data.OfType().Select(x => x.Double).ToArray());
+ }
+
+ PushState();
+
+ // transformed appearance box fixme: may be an arbitrary shape
+ PdfRectangle transformedBox = matrix.Transform(bbox.Value).Normalise();
+
+ // Matrix a = Matrix.getTranslateInstance(rect.getLowerLeftX(), rect.getLowerLeftY());
+ TransformationMatrix a = TransformationMatrix.GetTranslationMatrix(rect.Value.TopLeft.X, rect.Value.TopLeft.Y);
+ a = Scale(a, (float)(rect.Value.Width / transformedBox.Width), (float)(rect.Value.Height / transformedBox.Height));
+ a = a.Translate(-transformedBox.TopLeft.X, -transformedBox.TopLeft.Y);
+
+ // Matrix shall be concatenated with A to form a matrix AA that maps from the appearance's
+ // coordinate system to the annotation's rectangle in default user space
+ //
+ // HOWEVER only the opposite order works for rotated pages with
+ // filled fields / annotations that have a matrix in the appearance stream, see PDFBOX-3083
+ //Matrix aa = Matrix.concatenate(a, matrix);
+ //TransformationMatrix aa = a.Multiply(matrix);
+
+ GetCurrentState().CurrentTransformationMatrix = a;
+
+ try
+ {
+ base.ProcessFormXObject(appearance, null);
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"DrawAnnotations: {ex}");
+ }
+ finally
+ {
+ PopState();
+ }
+ }
+ }
+ else
+ {
+ DebugDrawRect(annotation.Rectangle);
+ }
+ }
+ }
+
+ private static TransformationMatrix Scale(TransformationMatrix matrix, float sx, float sy)
+ {
+ var x0 = matrix[0, 0] * sx;
+ var x1 = matrix[0, 1] * sx;
+ var x2 = matrix[0, 2] * sx;
+ var y0 = matrix[1, 0] * sy;
+ var y1 = matrix[1, 1] * sy;
+ var y2 = matrix[1, 2] * sy;
+ return new TransformationMatrix(x0, x1, x2, y0, y1, y2, matrix[2, 0], matrix[2, 1], matrix[2, 2]);
+ }
+ }
+}
diff --git a/src/UglyToad.PdfPig.Rendering.Skia/Graphics/SkiaStreamProcessor.Glyph.cs b/src/UglyToad.PdfPig.Rendering.Skia/Graphics/SkiaStreamProcessor.Glyph.cs
new file mode 100644
index 000000000..d8c2ae486
--- /dev/null
+++ b/src/UglyToad.PdfPig.Rendering.Skia/Graphics/SkiaStreamProcessor.Glyph.cs
@@ -0,0 +1,222 @@
+using SkiaSharp;
+using UglyToad.PdfPig.Core;
+using UglyToad.PdfPig.Graphics;
+using UglyToad.PdfPig.Graphics.Colors;
+using UglyToad.PdfPig.PdfFonts;
+using static UglyToad.PdfPig.Core.PdfSubpath;
+
+namespace UglyToad.PdfPig.Rendering.Skia.Graphics
+{
+ internal partial class SkiaStreamProcessor
+ {
+ public override void RenderGlyph(IFont font, IColor strokingColor, IColor nonStrokingColor, TextRenderingMode textRenderingMode, double fontSize, double pointSize, int code, string unicode, long currentOffset,
+ TransformationMatrix renderingMatrix, TransformationMatrix textMatrix, TransformationMatrix transformationMatrix, CharacterBoundingBox characterBoundingBox)
+ {
+ // TODO - update with strokingColor, nonStrokingColor, and textRenderingMode
+
+ IColor color = textRenderingMode == TextRenderingMode.Fill ? nonStrokingColor : strokingColor;
+
+ try
+ {
+ if (font.TryGetNormalisedPath(code, out var path))
+ {
+ ShowVectorFontGlyph(path, color, renderingMatrix, textMatrix, transformationMatrix);
+ }
+ else
+ {
+ // VCarefull here sone controle char have a representation - need testing, see issue 655
+ if (!CanRender(unicode))
+ {
+ return;
+ }
+
+ ShowNonVectorFontGlyph(font, color, pointSize, unicode, renderingMatrix, textMatrix, transformationMatrix, characterBoundingBox);
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"ShowGlyph: {ex}");
+ }
+ }
+
+ private void ShowVectorFontGlyph(IReadOnlyList path, IColor color,
+ TransformationMatrix renderingMatrix, TransformationMatrix textMatrix, TransformationMatrix transformationMatrix)
+ {
+ // Vector based font
+ using (var gp = new SKPath() { FillType = SKPathFillType.EvenOdd })
+ {
+ foreach (var subpath in path)
+ {
+ foreach (var c in subpath.Commands)
+ {
+ if (c is Move move)
+ {
+ var (x, y) = TransformPoint(renderingMatrix, textMatrix, transformationMatrix, move.Location);
+ gp.MoveTo((float)x, (float)(height - y));
+ }
+ else if (c is Line line)
+ {
+ var (x, y) = TransformPoint(renderingMatrix, textMatrix, transformationMatrix, line.To);
+ gp.LineTo((float)x, (float)(height - y));
+ }
+ else if (c is BezierCurve curve)
+ {
+ if (curve.StartPoint.Equals(curve.FirstControlPoint))
+ {
+ // Quad curve
+ var second = TransformPoint(renderingMatrix, textMatrix, transformationMatrix, curve.SecondControlPoint);
+ var end = TransformPoint(renderingMatrix, textMatrix, transformationMatrix, curve.EndPoint);
+ gp.QuadTo((float)(second.x), (float)(height - second.y),
+ (float)(end.x), (float)(height - end.y));
+ }
+ else
+ {
+ // Cubic curve
+ var first = TransformPoint(renderingMatrix, textMatrix, transformationMatrix, curve.FirstControlPoint);
+ var second = TransformPoint(renderingMatrix, textMatrix, transformationMatrix, curve.SecondControlPoint);
+ var end = TransformPoint(renderingMatrix, textMatrix, transformationMatrix, curve.EndPoint);
+ gp.CubicTo((float)first.x, (float)(height - first.y),
+ (float)second.x, (float)(height - second.y),
+ (float)end.x, (float)(height - end.y));
+ }
+ }
+ else if (c is Close)
+ {
+ gp.Close();
+ }
+ }
+ }
+
+ using (var fillBrush = new SKPaint()
+ {
+ Style = SKPaintStyle.Fill,
+ //BlendMode = GetCurrentState().BlendMode.ToSKBlendMode(), // TODO - check if correct
+ Color = SKColors.Black,
+ IsAntialias = antiAliasing
+ })
+ {
+ if (color != null)
+ {
+ fillBrush.Color = color.ToSKColor(GetCurrentState().AlphaConstantNonStroking); // todo - check intent, could be stroking
+ }
+ canvas.DrawPath(gp, fillBrush);
+ }
+ }
+ }
+
+ private static bool FontStyleEquals(SKFontStyle fontStyle1, SKFontStyle fontStyle2)
+ {
+ return fontStyle1.Width == fontStyle2.Width &&
+ fontStyle1.Weight == fontStyle2.Weight &&
+ fontStyle1.Slant == fontStyle2.Slant;
+ }
+
+ private SKTypeface GetTypefaceOrFallback(IFont font, string unicode)
+ {
+ using (var style = font.Details.GetFontStyle())
+ {
+ if (typefaces.TryGetValue(font.Name, out SKTypeface? drawTypeface) && drawTypeface is not null &&
+ (string.IsNullOrWhiteSpace(unicode) || drawTypeface.ContainsGlyph(unicode[0]))) // Check if can render
+ {
+ if (FontStyleEquals(drawTypeface.FontStyle, style))
+ {
+ return drawTypeface;
+ }
+
+ drawTypeface = SKFontManager.Default.MatchTypeface(drawTypeface, style);
+ if (drawTypeface is not null)
+ {
+ return drawTypeface;
+ }
+ }
+
+ string cleanFontName = font.GetCleanFontName();
+
+ drawTypeface = SKTypeface.FromFamilyName(cleanFontName, style);
+
+ //if (!drawTypeface.FamilyName.Equals(cleanFontName, StringComparison.OrdinalIgnoreCase) &&
+ // SystemFontFinder.NameSubstitutes.TryGetValue(cleanFontName, out string[]? subs) && subs != null)
+ //{
+ // foreach (var sub in subs)
+ // {
+ // drawTypeface = SKTypeface.FromFamilyName(sub, style);
+ // if (drawTypeface.FamilyName.Equals(sub))
+ // {
+ // break;
+ // }
+ // }
+ //}
+
+ // Fallback font
+ // https://github.com/mono/SkiaSharp/issues/232
+ if (!string.IsNullOrWhiteSpace(unicode) && !drawTypeface.ContainsGlyph(unicode[0]))
+ {
+ var fallback = SKFontManager.Default.MatchCharacter(unicode[0]);
+ if (fallback is not null)
+ {
+ drawTypeface = SKFontManager.Default.MatchTypeface(fallback, style);
+ }
+ }
+
+ typefaces[font.Name] = drawTypeface;
+
+ return drawTypeface;
+ }
+ }
+
+ private void ShowNonVectorFontGlyph(IFont font, IColor color, double pointSize, string unicode,
+ TransformationMatrix renderingMatrix, TransformationMatrix textMatrix, TransformationMatrix transformationMatrix,
+ CharacterBoundingBox characterBoundingBox)
+ {
+ // Not vector based font
+ var transformedGlyphBounds = PerformantRectangleTransformer
+ .Transform(renderingMatrix, textMatrix, transformationMatrix, characterBoundingBox.GlyphBounds);
+
+ var transformedPdfBounds = PerformantRectangleTransformer
+ .Transform(renderingMatrix, textMatrix, transformationMatrix, new PdfRectangle(0, 0, characterBoundingBox.Width, 0));
+
+ var startBaseLine = transformedPdfBounds.BottomLeft.ToSKPoint(height);
+ if (transformedGlyphBounds.Rotation != 0)
+ {
+ canvas.RotateDegrees((float)-transformedGlyphBounds.Rotation, startBaseLine.X, startBaseLine.Y);
+ }
+
+ SKTypeface drawTypeface = GetTypefaceOrFallback(font, unicode);
+
+ var fontPaint = new SKPaint(drawTypeface.ToFont((float)pointSize))
+ {
+ Style = SKPaintStyle.Fill,
+ //BlendMode = GetCurrentState().BlendMode.ToSKBlendMode(), // TODO - check if correct
+ Color = (color?.ToSKColor(GetCurrentState().AlphaConstantNonStroking)) ?? SKColors.Black, // todo - check intent, could be stroking
+ IsAntialias = antiAliasing
+ };
+
+ canvas.DrawText(unicode, startBaseLine, fontPaint);
+ canvas.ResetMatrix();
+
+ fontPaint.Dispose();
+ drawTypeface.Dispose();
+ }
+
+ private static bool CanRender(string unicode)
+ {
+ if (string.IsNullOrEmpty(unicode?.Trim()))
+ {
+ return false; // Nothing to render
+ }
+
+ // https://character.construction/blanks
+ switch (char.GetUnicodeCategory(unicode[0]))
+ {
+ case System.Globalization.UnicodeCategory.Control:
+ case System.Globalization.UnicodeCategory.Format:
+ case System.Globalization.UnicodeCategory.ParagraphSeparator:
+ case System.Globalization.UnicodeCategory.LineSeparator:
+ case System.Globalization.UnicodeCategory.SpaceSeparator:
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/src/UglyToad.PdfPig.Rendering.Skia/Graphics/SkiaStreamProcessor.Image.cs b/src/UglyToad.PdfPig.Rendering.Skia/Graphics/SkiaStreamProcessor.Image.cs
new file mode 100644
index 000000000..d56313679
--- /dev/null
+++ b/src/UglyToad.PdfPig.Rendering.Skia/Graphics/SkiaStreamProcessor.Image.cs
@@ -0,0 +1,78 @@
+using SkiaSharp;
+using UglyToad.PdfPig.Content;
+using UglyToad.PdfPig.Graphics;
+
+namespace UglyToad.PdfPig.Rendering.Skia.Graphics
+{
+ internal partial class SkiaStreamProcessor
+ {
+ public override void RenderXObjectImage(XObjectContentRecord xObjectContentRecord)
+ {
+ var image = GetImageFromXObject(xObjectContentRecord);
+ RenderImage(image);
+ }
+
+ public override void RenderInlineImage(InlineImage inlineImage)
+ {
+ RenderImage(inlineImage);
+ }
+
+ private void RenderImage(IPdfImage image)
+ {
+ var currentState = GetCurrentState();
+
+ // see issue_484Test, Pig production p15
+ // need better handling for images where rotation is not 180
+ float left = (float)image.Bounds.Left;
+ float top = (float)(height - image.Bounds.Top);
+ float right = left + (float)image.Bounds.Width;
+ float bottom = top + (float)image.Bounds.Height;
+ var destRect = new SKRect(left, top, right, bottom);
+
+ try
+ {
+ try
+ {
+ using (var paint = new SKPaint() { IsAntialias = antiAliasing }) // { BlendMode = currentState.BlendMode.ToSKBlendMode() })
+ using (var bitmap = image.GetSKBitmap())
+ {
+ canvas.DrawBitmap(bitmap, destRect, paint);
+ }
+ }
+ catch (Exception)
+ {
+ // Try with raw bytes
+ using (var paint = new SKPaint() { IsAntialias = antiAliasing }) // { BlendMode = currentState.BlendMode.ToSKBlendMode() })
+ using (var bitmap = SKBitmap.Decode(image.RawBytes.ToArray()))
+ {
+ canvas.DrawBitmap(bitmap, destRect, paint);
+ }
+ }
+ }
+ catch (Exception)
+ {
+ using (var bitmap = new SKBitmap((int)destRect.Width, (int)destRect.Height))
+ using (var canvas = new SKCanvas(bitmap))
+ using (var paint = new SKPaint
+ {
+ Style = SKPaintStyle.Fill,
+ Color = new SKColor(SKColors.Aquamarine.Red, SKColors.Aquamarine.Green, SKColors.Aquamarine.Blue, 80),
+ IsAntialias = antiAliasing
+ })
+ {
+ canvas.DrawRect(0, 0, destRect.Width, destRect.Height, paint);
+#if DEBUG
+ Directory.CreateDirectory("images_not_rendered");
+ string imagePath = Path.Combine("images_not_rendered", $"{Guid.NewGuid().ToString().ToLower()}.jp2");
+ File.WriteAllBytes(imagePath, image.RawBytes.ToArray());
+#endif
+ this.canvas.DrawBitmap(bitmap, destRect);
+ }
+ }
+ finally
+ {
+ //_canvas.ResetMatrix();
+ }
+ }
+ }
+}
diff --git a/src/UglyToad.PdfPig.Rendering.Skia/Graphics/SkiaStreamProcessor.Path.cs b/src/UglyToad.PdfPig.Rendering.Skia/Graphics/SkiaStreamProcessor.Path.cs
new file mode 100644
index 000000000..46fdc4902
--- /dev/null
+++ b/src/UglyToad.PdfPig.Rendering.Skia/Graphics/SkiaStreamProcessor.Path.cs
@@ -0,0 +1,288 @@
+using SkiaSharp;
+using UglyToad.PdfPig.Core;
+using UglyToad.PdfPig.Graphics;
+using UglyToad.PdfPig.Graphics.Colors;
+
+namespace UglyToad.PdfPig.Rendering.Skia.Graphics
+{
+ internal partial class SkiaStreamProcessor
+ {
+ private SKPath? CurrentPath { get; set; }
+
+ public override void BeginSubpath()
+ {
+ CurrentPath ??= new SKPath();
+ }
+
+ public override PdfPoint? CloseSubpath()
+ {
+ if (CurrentPath == null)
+ {
+ return null;
+ }
+
+ CurrentPath.Close();
+ return null;
+ }
+
+ public override void MoveTo(double x, double y)
+ {
+ BeginSubpath();
+
+ if (CurrentPath == null)
+ {
+ return;
+ }
+
+ var point = CurrentTransformationMatrix.Transform(new PdfPoint(x, y));
+ float xs = (float)point.X;
+ float ys = (float)(height - point.Y);
+
+ CurrentPath.MoveTo(xs, ys);
+ }
+
+ public override void LineTo(double x, double y)
+ {
+ if (CurrentPath == null)
+ {
+ return;
+ }
+
+ var point = CurrentTransformationMatrix.Transform(new PdfPoint(x, y));
+ float xs = (float)point.X;
+ float ys = (float)(height - point.Y);
+
+ CurrentPath.LineTo(xs, ys);
+ }
+
+ public override void BezierCurveTo(double x2, double y2, double x3, double y3)
+ {
+ if (CurrentPath == null)
+ {
+ return;
+ }
+
+ var controlPoint2 = CurrentTransformationMatrix.Transform(new PdfPoint(x2, y2));
+ var end = CurrentTransformationMatrix.Transform(new PdfPoint(x3, y3));
+ float x2s = (float)controlPoint2.X;
+ float y2s = (float)(height - controlPoint2.Y);
+ float x3s = (float)end.X;
+ float y3s = (float)(height - end.Y);
+
+ CurrentPath.QuadTo(x2s, y2s, x3s, y3s);
+ }
+
+ public override void BezierCurveTo(double x1, double y1, double x2, double y2, double x3, double y3)
+ {
+ if (CurrentPath == null)
+ {
+ return;
+ }
+
+ var controlPoint1 = CurrentTransformationMatrix.Transform(new PdfPoint(x1, y1));
+ var controlPoint2 = CurrentTransformationMatrix.Transform(new PdfPoint(x2, y2));
+ var end = CurrentTransformationMatrix.Transform(new PdfPoint(x3, y3));
+ float x1s = (float)controlPoint1.X;
+ float y1s = (float)(height - controlPoint1.Y);
+ float x2s = (float)controlPoint2.X;
+ float y2s = (float)(height - controlPoint2.Y);
+ float x3s = (float)end.X;
+ float y3s = (float)(height - end.Y);
+
+ CurrentPath.CubicTo(x1s, y1s, x2s, y2s, x3s, y3s);
+ }
+
+ public override void ClosePath()
+ {
+ // TODO - to check, does nothing
+ }
+
+ public override void EndPath()
+ {
+ if (CurrentPath == null)
+ {
+ return;
+ }
+
+ // TODO
+ CurrentPath.Dispose();
+ CurrentPath = null;
+ }
+
+ public override void Rectangle(double x, double y, double width, double height)
+ {
+ BeginSubpath();
+
+ if (CurrentPath == null)
+ {
+ return;
+ }
+
+ var lowerLeft = CurrentTransformationMatrix.Transform(new PdfPoint(x, y));
+ var upperRight = CurrentTransformationMatrix.Transform(new PdfPoint(x + width, y + height));
+ float left = (float)lowerLeft.X;
+ float top = (float)(this.height - upperRight.Y);
+ float right = (float)upperRight.X;
+ float bottom = (float)(this.height - lowerLeft.Y);
+ SKRect rect = new SKRect(left, top, right, bottom);
+ CurrentPath.AddRect(rect);
+ }
+
+ private float GetScaledLineWidth()
+ {
+ var currentState = GetCurrentState();
+ // https://stackoverflow.com/questions/25690496/how-does-pdf-line-width-interact-with-the-ctm-in-both-horizontal-and-vertical-di
+ // TODO - a hack but works, to put in ContentStreamProcessor
+ return (float)(currentState.LineWidth * (decimal)currentState.CurrentTransformationMatrix.A);
+ }
+
+ public override void StrokePath(bool close)
+ {
+ if (CurrentPath == null)
+ {
+ return;
+ }
+
+ if (close)
+ {
+ CurrentPath.Close();
+ }
+
+ var currentState = GetCurrentState();
+
+ PaintStrokePath(currentState);
+
+ CurrentPath.Dispose();
+ CurrentPath = null;
+ }
+
+ private void PaintStrokePath(CurrentGraphicsState currentGraphicsState)
+ {
+ if (currentGraphicsState.CurrentStrokingColor?.ColorSpace == ColorSpace.Pattern)
+ {
+ if (!(currentGraphicsState.CurrentStrokingColor is PatternColor pattern))
+ {
+ throw new ArgumentNullException("TODO");
+ }
+
+ switch (pattern.PatternType)
+ {
+ case PatternType.Tiling:
+ RenderTilingPattern(pattern as TilingPatternColor, true);
+ break;
+
+ case PatternType.Shading:
+ this.RenderShadingPattern(pattern as ShadingPatternColor, true);
+ break;
+ }
+ }
+ else
+ {
+ using (var paint = new SKPaint()
+ {
+ IsAntialias = antiAliasing,
+ Color = currentGraphicsState.GetCurrentStrokingColorSKColor(),
+ Style = SKPaintStyle.Stroke,
+ StrokeWidth = Math.Max((float)0.5, GetScaledLineWidth()), // A guess
+ StrokeJoin = currentGraphicsState.JoinStyle.ToSKStrokeJoin(),
+ StrokeCap = currentGraphicsState.CapStyle.ToSKStrokeCap(),
+ PathEffect = currentGraphicsState.LineDashPattern.ToSKPathEffect()
+ })
+ {
+ //paint.BlendMode = currentGraphicsState.BlendMode.ToSKBlendMode();
+ canvas.DrawPath(CurrentPath, paint);
+ }
+ }
+ }
+
+ public override void FillPath(FillingRule fillingRule, bool close)
+ {
+ if (CurrentPath == null)
+ {
+ return;
+ }
+
+ if (close)
+ {
+ CurrentPath.Close();
+ }
+
+ var currentState = GetCurrentState();
+
+ PaintFillPath(currentState, fillingRule);
+
+ CurrentPath.Dispose();
+ CurrentPath = null;
+ }
+
+ private void PaintFillPath(CurrentGraphicsState currentGraphicsState, FillingRule fillingRule)
+ {
+ if (CurrentPath == null)
+ {
+ return;
+ }
+
+ CurrentPath.FillType = fillingRule.ToSKPathFillType();
+
+ if (currentGraphicsState.CurrentNonStrokingColor?.ColorSpace == ColorSpace.Pattern)
+ {
+ if (!(currentGraphicsState.CurrentNonStrokingColor is PatternColor pattern))
+ {
+ throw new ArgumentNullException("TODO");
+ }
+
+ switch (pattern.PatternType)
+ {
+ case PatternType.Tiling:
+ RenderTilingPattern(pattern as TilingPatternColor, false);
+ break;
+
+ case PatternType.Shading:
+ this.RenderShadingPattern(pattern as ShadingPatternColor, false);
+ break;
+ }
+ }
+ else
+ {
+ using (SKPaint paint = new SKPaint()
+ {
+ IsAntialias = antiAliasing,
+ Color = currentGraphicsState.GetCurrentNonStrokingColorSKColor(),
+ Style = SKPaintStyle.Fill
+ })
+ {
+ //paint.BlendMode = currentGraphicsState.BlendMode.ToSKBlendMode();
+ canvas.DrawPath(CurrentPath, paint);
+ }
+ }
+ }
+
+ public override void FillStrokePath(FillingRule fillingRule, bool close)
+ {
+ if (CurrentPath == null)
+ {
+ return;
+ }
+
+ if (close)
+ {
+ CurrentPath.Close();
+ }
+
+ var currentState = GetCurrentState();
+
+ PaintFillPath(currentState, fillingRule);
+ PaintStrokePath(currentState);
+
+ CurrentPath.Dispose();
+ CurrentPath = null;
+ }
+
+ private void RenderTilingPattern(TilingPatternColor pattern, bool isStroke)
+ {
+ System.Diagnostics.Debug.WriteLine($"WARNING: Tiling Shader not implemented");
+ DebugDrawRect(CurrentPath.Bounds);
+ //throw new NotImplementedException("PaintStrokePath Tiling Shader");
+ }
+ }
+}
diff --git a/src/UglyToad.PdfPig.Rendering.Skia/Graphics/SkiaStreamProcessor.Shading.cs b/src/UglyToad.PdfPig.Rendering.Skia/Graphics/SkiaStreamProcessor.Shading.cs
new file mode 100644
index 000000000..dd8dc9ffb
--- /dev/null
+++ b/src/UglyToad.PdfPig.Rendering.Skia/Graphics/SkiaStreamProcessor.Shading.cs
@@ -0,0 +1,298 @@
+using SkiaSharp;
+using UglyToad.PdfPig.Core;
+using UglyToad.PdfPig.Graphics.Colors;
+
+namespace UglyToad.PdfPig.Rendering.Skia.Graphics
+{
+ internal partial class SkiaStreamProcessor
+ {
+ protected override void RenderShading(Shading shading)
+ {
+ float maxX = canvas.DeviceClipBounds.Right;
+ float maxY = canvas.DeviceClipBounds.Top;
+ float minX = canvas.DeviceClipBounds.Left;
+ float minY = canvas.DeviceClipBounds.Bottom;
+
+ switch (shading.ShadingType)
+ {
+ case ShadingType.Axial:
+ RenderAxialShading(shading as AxialShading, CurrentTransformationMatrix, minX, minY, maxX, maxY);
+ break;
+
+ case ShadingType.Radial:
+ RenderRadialShading(shading as RadialShading, CurrentTransformationMatrix, minX, minY, maxX, maxY);
+ break;
+
+ case ShadingType.FunctionBased:
+ case ShadingType.FreeFormGouraud:
+ case ShadingType.LatticeFormGouraud:
+ case ShadingType.CoonsPatch:
+ case ShadingType.TensorProductPatch:
+ default:
+ RenderUnsupportedShading(shading, CurrentTransformationMatrix);
+ break;
+ }
+ }
+
+ private void RenderUnsupportedShading(Shading shading, TransformationMatrix transformationMatrix)
+ {
+ var (x0, y0) = transformationMatrix.Transform(0, 0);
+ var (x1, y1) = transformationMatrix.Transform(0, 1);
+
+ float xs0 = (float)x0;
+ float ys0 = (float)(height - y0);
+ float xs1 = (float)x1;
+ float ys1 = (float)(height - y1);
+ using (var paint = new SKPaint() { IsAntialias = shading.AntiAlias })
+ {
+ //paint.BlendMode = GetCurrentState().BlendMode.ToSKBlendMode(); // TODO - check if correct
+
+ paint.Shader = SKShader.CreateLinearGradient(
+ new SKPoint(xs0, ys0),
+ new SKPoint(xs1, ys1),
+ new[]
+ {
+ SKColors.Red,
+ SKColors.Green
+ },
+ SKShaderTileMode.Clamp);
+
+ // check if bbox not null
+
+ canvas.DrawPaint(paint);
+ }
+ }
+
+ private void RenderRadialShading(RadialShading shading, TransformationMatrix transformationMatrix, float minX, float minY, float maxX, float maxY,
+ bool isStroke = false, SKPath? path = null)
+ {
+ // Not correct
+ var coords = shading.Coords.Select(c => (float)c).ToArray();
+ var domain = shading.Domain.Select(c => (float)c).ToArray();
+
+ float r0 = coords[2];
+ float r1 = coords[5];
+
+ // If one radius is 0, the corresponding circle shall be treated as a point;
+ // if both are 0, nothing shall be painted.
+ if (r0 == 0 && r1 == 0)
+ {
+ return;
+ }
+
+ (double x0, double y0) = transformationMatrix.Transform(coords[0], coords[1]);
+ (double x1, double y1) = transformationMatrix.Transform(coords[3], coords[4]);
+
+ float xs0 = (float)x0;
+ float ys0 = (float)(height - y0);
+ float xs1 = (float)x1;
+ float ys1 = (float)(height - y1);
+ float r0s = (float)r0;
+ float r1s = (float)r1;
+
+ var colors = new List();
+ float t0 = domain[0];
+ float t1 = domain[1];
+
+ // worst case for the number of steps is opposite diagonal corners, so use that
+ double dist = Math.Sqrt(Math.Pow(maxX - minX, 2) + Math.Pow(maxY - minY, 2));
+ int factor = (int)Math.Ceiling(dist); // too much?
+ for (int t = 0; t <= factor; t++)
+ {
+ double tx = t0 + (t / (double)factor) * t1;
+ double[] v = shading.Eval(tx);
+ IColor c = shading.ColorSpace.GetColor(v);
+ colors.Add(c.ToSKColor(GetCurrentState().AlphaConstantNonStroking)); // TODO - is it non stroking??
+ }
+
+ if (shading.BBox.HasValue)
+ {
+
+ }
+
+ if (shading.Background != null)
+ {
+
+ }
+
+ if (r0s == 0)
+ {
+ using (var paint = new SKPaint() { IsAntialias = shading.AntiAlias })
+ {
+ //paint.BlendMode = GetCurrentState().BlendMode.ToSKBlendMode(); // TODO - check if correct
+
+ paint.Shader = SKShader.CreateRadialGradient(
+ new SKPoint((float)xs1, (float)ys1),
+ r1s * (float)dist,
+ colors.ToArray(),
+ SKShaderTileMode.Clamp);
+
+ // check if bbox not null
+
+ canvas.DrawPaint(paint);
+ }
+ }
+ else if (r1s == 0)
+ {
+ using (var paint = new SKPaint() { IsAntialias = shading.AntiAlias })
+ {
+ //paint.BlendMode = GetCurrentState().BlendMode.ToSKBlendMode(); // TODO - check if correct
+
+ paint.Shader = SKShader.CreateRadialGradient(
+ new SKPoint((float)xs0, (float)ys0),
+ r0s * (float)dist,
+ colors.ToArray(),
+ SKShaderTileMode.Clamp);
+
+ // check if bbox not null
+
+ canvas.DrawPaint(paint);
+ }
+ }
+ else
+ {
+ using (var paint = new SKPaint() { IsAntialias = shading.AntiAlias })
+ {
+ //paint.BlendMode = GetCurrentState().BlendMode.ToSKBlendMode(); // TODO - check if correct
+
+ paint.Shader = SKShader.CreateTwoPointConicalGradient(
+ new SKPoint((float)xs0, (float)ys0),
+ r0s * (float)dist,
+ new SKPoint((float)xs1, (float)ys1),
+ r1s * (float)dist,
+ colors.ToArray(),
+ SKShaderTileMode.Clamp);
+
+ // check if bbox not null
+
+ if (isStroke)
+ {
+ // TODO - To Check
+ paint.Style = SKPaintStyle.Stroke;
+ paint.StrokeWidth = Math.Max(0.5f, GetScaledLineWidth()); // A guess
+ paint.StrokeJoin = GetCurrentState().JoinStyle.ToSKStrokeJoin();
+ paint.StrokeCap = GetCurrentState().CapStyle.ToSKStrokeCap();
+ paint.PathEffect = GetCurrentState().LineDashPattern.ToSKPathEffect();
+ }
+
+ if (path is null)
+ {
+ canvas.DrawPaint(paint);
+ }
+ else
+ {
+ canvas.DrawPath(CurrentPath, paint);
+ }
+ }
+ }
+ }
+
+ private void RenderAxialShading(AxialShading shading, TransformationMatrix transformationMatrix, float minX, float minY, float maxX, float maxY,
+ bool isStroke = false, SKPath? path = null)
+ {
+ var coords = shading.Coords.Select(c => (float)c).ToArray();
+ var domain = shading.Domain.Select(c => (float)c).ToArray();
+
+ (double x0, double y0) = transformationMatrix.Transform(coords[0], coords[1]);
+ (double x1, double y1) = transformationMatrix.Transform(coords[2], coords[3]);
+
+ float xs0 = (float)x0;
+ float ys0 = (float)(height - y0);
+ float xs1 = (float)x1;
+ float ys1 = (float)(height - y1);
+
+ var colors = new List();
+ float t0 = domain[0];
+ float t1 = domain[1];
+
+ if (shading.BBox.HasValue)
+ {
+
+ }
+
+ if (shading.Background != null)
+ {
+
+ }
+
+ // worst case for the number of steps is opposite diagonal corners, so use that
+ double dist = Math.Sqrt(Math.Pow(maxX - minX, 2) + Math.Pow(maxY - minY, 2));
+ int factor = Math.Max(10, (int)Math.Ceiling(dist)); // too much? - Min of 10
+
+ for (int t = 0; t <= factor; t++)
+ {
+ double tx = t0 + (t / (double)factor) * t1;
+ double[] v = shading.Eval(tx);
+ IColor c = shading.ColorSpace.GetColor(v);
+ colors.Add(c.ToSKColor(GetCurrentState().AlphaConstantNonStroking)); // TODO - is it non stroking??
+ }
+
+ using (var paint = new SKPaint() { IsAntialias = shading.AntiAlias })
+ {
+ //paint.BlendMode = GetCurrentState().BlendMode.ToSKBlendMode(); // TODO - check if correct
+
+ paint.Shader = SKShader.CreateLinearGradient(
+ new SKPoint(xs0, ys0),
+ new SKPoint(xs1, ys1),
+ colors.ToArray(),
+ SKShaderTileMode.Clamp);
+
+ // check if bbox not null
+
+ if (isStroke)
+ {
+ // TODO - To Check
+ paint.Style = SKPaintStyle.Stroke;
+ paint.StrokeWidth = Math.Max(0.5f, GetScaledLineWidth()); // A guess
+ paint.StrokeJoin = GetCurrentState().JoinStyle.ToSKStrokeJoin();
+ paint.StrokeCap = GetCurrentState().CapStyle.ToSKStrokeCap();
+ paint.PathEffect = GetCurrentState().LineDashPattern.ToSKPathEffect();
+ }
+
+ if (path is null)
+ {
+ canvas.DrawPaint(paint);
+ }
+ else
+ {
+ canvas.DrawPath(CurrentPath, paint);
+ }
+ }
+ }
+
+ private void RenderShadingPattern(ShadingPatternColor pattern, bool isStroke)
+ {
+ if (pattern.ExtGState != null)
+ {
+ // TODO
+ }
+
+ TransformationMatrix transformationMatrix = pattern.Matrix.Multiply(CurrentTransformationMatrix);
+
+ float maxX = CurrentPath!.Bounds.Right;
+ float maxY = CurrentPath.Bounds.Top;
+ float minX = CurrentPath.Bounds.Left;
+ float minY = CurrentPath.Bounds.Bottom;
+
+ switch (pattern.Shading.ShadingType)
+ {
+ case ShadingType.Axial:
+ RenderAxialShading(pattern.Shading as AxialShading, transformationMatrix, minX, minY, maxX, maxY, isStroke, CurrentPath);
+ break;
+
+ case ShadingType.Radial:
+ RenderRadialShading(pattern.Shading as RadialShading, transformationMatrix, minX, minY, maxX, maxY, isStroke, CurrentPath);
+ break;
+
+ case ShadingType.FunctionBased:
+ case ShadingType.FreeFormGouraud:
+ case ShadingType.LatticeFormGouraud:
+ case ShadingType.CoonsPatch:
+ case ShadingType.TensorProductPatch:
+ default:
+ RenderUnsupportedShading(pattern.Shading, CurrentTransformationMatrix);
+ break;
+ }
+ }
+ }
+}
diff --git a/src/UglyToad.PdfPig.Rendering.Skia/Graphics/SkiaStreamProcessor.cs b/src/UglyToad.PdfPig.Rendering.Skia/Graphics/SkiaStreamProcessor.cs
new file mode 100644
index 000000000..88ff68744
--- /dev/null
+++ b/src/UglyToad.PdfPig.Rendering.Skia/Graphics/SkiaStreamProcessor.cs
@@ -0,0 +1,132 @@
+using SkiaSharp;
+using UglyToad.PdfPig.Content;
+using UglyToad.PdfPig.Core;
+using UglyToad.PdfPig.Filters;
+using UglyToad.PdfPig.Geometry;
+using UglyToad.PdfPig.Graphics.Operations;
+using UglyToad.PdfPig.Parser;
+using UglyToad.PdfPig.Tokenization.Scanner;
+
+namespace UglyToad.PdfPig.Rendering.Skia.Graphics
+{
+ internal sealed partial class SkiaStreamProcessor : BaseRenderStreamProcessor
+ {
+ private readonly int height;
+ private readonly int width;
+
+ private SKCanvas canvas;
+ private readonly Page page;
+
+ private readonly bool antiAliasing = true;
+
+ private readonly Dictionary typefaces = new Dictionary();
+
+ public SkiaStreamProcessor(
+ int pageNumber,
+ IResourceStore resourceStore,
+ UserSpaceUnit userSpaceUnit,
+ MediaBox mediaBox,
+ CropBox cropBox,
+ PageRotationDegrees rotation,
+ IPdfTokenScanner pdfScanner,
+ IPageContentParser pageContentParser,
+ ILookupFilterProvider filterProvider,
+ IParsingOptions parsingOptions)
+ : base(pageNumber, resourceStore, userSpaceUnit, mediaBox, cropBox, rotation, pdfScanner, pageContentParser, filterProvider, parsingOptions)
+ {
+ // Special case where cropbox is outside mediabox: use cropbox instead of intersection
+ var viewBox = mediaBox.Bounds.Intersect(cropBox.Bounds) ?? cropBox.Bounds;
+
+ width = (int)(rotation.SwapsAxis ? viewBox.Height : viewBox.Width);
+ height = (int)(rotation.SwapsAxis ? viewBox.Width : viewBox.Height);
+ }
+
+ public override SKPicture Process(int pageNumberCurrent, IReadOnlyList operations)
+ {
+ // https://github.com/apache/pdfbox/blob/94b3d15fd24b9840abccece261173593625ff85c/pdfbox/src/main/java/org/apache/pdfbox/rendering/PDFRenderer.java#L274
+
+ CloneAllStates();
+
+ using (var recorder = new SKPictureRecorder())
+ using (canvas = recorder.BeginRecording(SKRect.Create(width, height)))
+ {
+ canvas.Clear(SKColors.White);
+
+ // TODO Annotation to render (maybe in a different layer)
+
+ //DrawAnnotations(true);
+
+ ProcessOperations(operations);
+
+ //DrawAnnotations(false);
+
+ canvas.Flush();
+
+ return recorder.EndRecording();
+ }
+ }
+
+ private void DebugDrawRect(SKRect destRect)
+ {
+ using (new SKAutoCanvasRestore(canvas))
+ {
+ // https://learn.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/curves/effects
+ SKPathEffect diagLinesPath = SKPathEffect.Create2DLine(4,
+ SKMatrix.Concat(SKMatrix.CreateScale(6, 6), SKMatrix.CreateRotationDegrees(45)));
+
+ var fillBrush = new SKPaint()
+ {
+ Style = SKPaintStyle.Fill,
+ Color = new SKColor(SKColors.Aqua.Red, SKColors.Aqua.Green, SKColors.Aqua.Blue),
+ PathEffect = diagLinesPath,
+ IsAntialias = antiAliasing
+ };
+ canvas.DrawRect(destRect, fillBrush);
+
+ fillBrush = new SKPaint()
+ {
+ Style = SKPaintStyle.Stroke,
+ Color = new SKColor(SKColors.Red.Red, SKColors.Red.Green, SKColors.Red.Blue),
+ StrokeWidth = 5,
+ IsAntialias = antiAliasing
+ };
+ canvas.DrawRect(destRect, fillBrush);
+
+ diagLinesPath.Dispose();
+ fillBrush.Dispose();
+ }
+ }
+
+ private void DebugDrawRect(PdfRectangle rect)
+ {
+ var upperLeft = rect.TopLeft.ToSKPoint(height);
+ var destRect = new SKRect(upperLeft.X, upperLeft.Y,
+ upperLeft.X + (float)(rect.Width),
+ upperLeft.Y + (float)(rect.Height)).Standardized;
+ DebugDrawRect(destRect);
+ }
+
+ public override void PopState()
+ {
+ base.PopState();
+ canvas.Restore();
+ }
+
+ public override void PushState()
+ {
+ base.PushState();
+ canvas.Save();
+ }
+
+ public override void ModifyClippingIntersect(FillingRule clippingRule)
+ {
+ if (CurrentPath == null)
+ {
+ return;
+ }
+
+ CurrentPath.FillType = clippingRule.ToSKPathFillType();
+ canvas.ClipPath(CurrentPath, SKClipOperation.Intersect);
+ }
+ }
+}
diff --git a/src/UglyToad.PdfPig.Rendering.Skia/Parser/SkiaPageFactory.cs b/src/UglyToad.PdfPig.Rendering.Skia/Parser/SkiaPageFactory.cs
new file mode 100644
index 000000000..6cc7ca1d5
--- /dev/null
+++ b/src/UglyToad.PdfPig.Rendering.Skia/Parser/SkiaPageFactory.cs
@@ -0,0 +1,38 @@
+using SkiaSharp;
+using UglyToad.PdfPig.Content;
+using UglyToad.PdfPig.Core;
+using UglyToad.PdfPig.Filters;
+using UglyToad.PdfPig.Geometry;
+using UglyToad.PdfPig.Graphics.Operations;
+using UglyToad.PdfPig.Logging;
+using UglyToad.PdfPig.Outline;
+using UglyToad.PdfPig.Parser;
+using UglyToad.PdfPig.Rendering.Skia.Graphics;
+using UglyToad.PdfPig.Tokenization.Scanner;
+using UglyToad.PdfPig.Tokens;
+
+namespace UglyToad.PdfPig.Rendering.Skia.Parser
+{
+ public sealed class SkiaPageFactory : PageFactoryBase
+ {
+ public SkiaPageFactory(IPdfTokenScanner pdfScanner, IResourceStore resourceStore, ILookupFilterProvider filterProvider, IPageContentParser pageContentParser, ILog log)
+ : base(pdfScanner, resourceStore, filterProvider, pageContentParser, log)
+ { }
+
+ protected override SKPicture ProcessPage(int pageNumber, DictionaryToken dictionary, NamedDestinations namedDestinations, IReadOnlyList contentBytes, CropBox cropBox,
+ UserSpaceUnit userSpaceUnit, PageRotationDegrees rotation, MediaBox mediaBox, IParsingOptions parsingOptions)
+ {
+ var context = new SkiaStreamProcessor(pageNumber, ResourceStore, userSpaceUnit, mediaBox, cropBox, rotation, PdfScanner, PageContentParser, FilterProvider, parsingOptions);
+
+ var operations = PageContentParser.Parse(pageNumber, new ByteArrayInputBytes(contentBytes), parsingOptions.Logger);
+ return context.Process(pageNumber, operations);
+ }
+
+ protected override SKPicture ProcessPage(int pageNumber, DictionaryToken dictionary, NamedDestinations namedDestinations, CropBox cropBox,
+ UserSpaceUnit userSpaceUnit, PageRotationDegrees rotation, MediaBox mediaBox, IParsingOptions parsingOptions)
+ {
+ var context = new SkiaStreamProcessor(pageNumber, ResourceStore, userSpaceUnit, mediaBox, cropBox, rotation, PdfScanner, PageContentParser, FilterProvider, parsingOptions);
+ return context.Process(pageNumber, EmptyArray.Instance);
+ }
+ }
+}
diff --git a/src/UglyToad.PdfPig.Rendering.Skia/UglyToad.PdfPig.Rendering.Skia.csproj b/src/UglyToad.PdfPig.Rendering.Skia/UglyToad.PdfPig.Rendering.Skia.csproj
new file mode 100644
index 000000000..db34878cd
--- /dev/null
+++ b/src/UglyToad.PdfPig.Rendering.Skia/UglyToad.PdfPig.Rendering.Skia.csproj
@@ -0,0 +1,17 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/UglyToad.PdfPig.Tests/Graphics/ContentStreamProcessorTests.cs b/src/UglyToad.PdfPig.Tests/Graphics/ContentStreamProcessorTests.cs
index 01d8cea86..750b1bf0b 100644
--- a/src/UglyToad.PdfPig.Tests/Graphics/ContentStreamProcessorTests.cs
+++ b/src/UglyToad.PdfPig.Tests/Graphics/ContentStreamProcessorTests.cs
@@ -140,7 +140,7 @@ private static void GetInitialTransformationMatrices(
out TransformationMatrix initialMatrix,
out TransformationMatrix inverseMatrix)
{
- initialMatrix = ContentStreamProcessor.GetInitialMatrix(UserSpaceUnit.Default, mediaBox, cropBox, rotation, new TestingLog());
+ initialMatrix = StreamProcessorHelper.GetInitialMatrix(UserSpaceUnit.Default, mediaBox, cropBox, rotation, new TestingLog());
inverseMatrix = initialMatrix.Inverse();
}
diff --git a/src/UglyToad.PdfPig.Tests/Integration/PageFactoryTests.cs b/src/UglyToad.PdfPig.Tests/Integration/PageFactoryTests.cs
new file mode 100644
index 000000000..8338c4ce5
--- /dev/null
+++ b/src/UglyToad.PdfPig.Tests/Integration/PageFactoryTests.cs
@@ -0,0 +1,176 @@
+namespace UglyToad.PdfPig.Tests.Integration
+{
+ using System.Collections.Generic;
+ using UglyToad.PdfPig.Content;
+ using UglyToad.PdfPig.Filters;
+ using UglyToad.PdfPig.Geometry;
+ using UglyToad.PdfPig.Logging;
+ using UglyToad.PdfPig.Outline;
+ using UglyToad.PdfPig.Parser;
+ using UglyToad.PdfPig.Tokenization.Scanner;
+ using UglyToad.PdfPig.Tokens;
+ using Xunit;
+
+ public class PageFactoryTests
+ {
+ [Fact]
+ public void SimpleFactory1()
+ {
+ var file = IntegrationHelpers.GetDocumentPath("Various Content Types");
+
+ using (var document = PdfDocument.Open(file))
+ {
+ document.AddPageFactory();
+
+ var page = document.GetPage(1);
+ Assert.Equal(1, page.Number);
+
+ page = document.GetPage(1);
+ Assert.Equal(1, page.Number);
+ }
+ }
+
+ [Fact]
+ public void SimpleFactory2()
+ {
+ var file = IntegrationHelpers.GetDocumentPath("Various Content Types");
+
+ using (var document = PdfDocument.Open(file))
+ {
+ document.AddPageFactory(new SimplePageFactory());
+
+ var page = document.GetPage(1);
+ Assert.Equal(1, page.Number);
+
+ page = document.GetPage(1);
+ Assert.Equal(1, page.Number);
+ }
+ }
+
+ [Fact]
+ public void InformationFactory()
+ {
+ var file = IntegrationHelpers.GetDocumentPath("Various Content Types");
+
+ using (var document = PdfDocument.Open(file))
+ {
+ document.AddPageFactory();
+
+ Page page = document.GetPage(1);
+
+ PageInformation pageInfo = document.GetPage(1);
+ Assert.Equal(page.Number, pageInfo.Number);
+ Assert.Equal(page.Rotation, pageInfo.Rotation);
+ Assert.Equal(page.MediaBox.Bounds, pageInfo.MediaBox.Bounds);
+ Assert.Equal(page.CropBox.Bounds, pageInfo.CropBox.Bounds);
+ //Assert.Equal(page.Unit, pageInfo.UserSpaceUnit);
+
+ pageInfo = document.GetPage(1);
+ Assert.Equal(page.Number, pageInfo.Number);
+ Assert.Equal(page.Rotation, pageInfo.Rotation);
+ Assert.Equal(page.MediaBox.Bounds, pageInfo.MediaBox.Bounds);
+ Assert.Equal(page.CropBox.Bounds, pageInfo.CropBox.Bounds);
+ }
+ }
+
+ #region SimplePage
+ public class SimplePage
+ {
+ public int Number { get; }
+
+ public int Rotation { get; }
+
+ public MediaBox MediaBox { get; }
+
+ public SimplePage(int number, int rotation, MediaBox mediaBox)
+ {
+ Number = number;
+ Rotation = rotation;
+ MediaBox = mediaBox;
+ }
+ }
+
+ public class SimplePageFactory : IPageFactory
+ {
+ public SimplePageFactory()
+ {
+ // do nothing
+ }
+
+ public SimplePageFactory(
+ IPdfTokenScanner pdfScanner,
+ IResourceStore resourceStore,
+ ILookupFilterProvider filterProvider,
+ IPageContentParser pageContentParser,
+ ILog log)
+ { }
+
+ public SimplePage Create(int number, DictionaryToken dictionary, PageTreeMembers pageTreeMembers, NamedDestinations annotationProvider, IParsingOptions parsingOptions)
+ {
+ return new SimplePage(number, pageTreeMembers.Rotation, pageTreeMembers.MediaBox);
+ }
+ }
+ #endregion
+
+ #region PageInformation
+ public class PageInformation
+ {
+ public int Number { get; set; }
+
+ public PageRotationDegrees Rotation { get; set; }
+
+ public MediaBox MediaBox { get; set; }
+
+ public CropBox CropBox { get; set; }
+
+ public UserSpaceUnit UserSpaceUnit { get; set; }
+ }
+
+ public class PageInformationFactory : PageFactoryBase
+ {
+ public PageInformationFactory(
+ IPdfTokenScanner pdfScanner,
+ IResourceStore resourceStore,
+ ILookupFilterProvider filterProvider,
+ IPageContentParser pageContentParser,
+ ILog log)
+ : base(pdfScanner, resourceStore, filterProvider, pageContentParser, log)
+ {
+ }
+
+ protected override PageInformation ProcessPage(
+ int pageNumber,
+ DictionaryToken dictionary,
+ NamedDestinations namedDestinations,
+ IReadOnlyList contentBytes,
+ CropBox cropBox,
+ UserSpaceUnit userSpaceUnit,
+ PageRotationDegrees rotation,
+ MediaBox mediaBox,
+ IParsingOptions parsingOptions)
+ {
+ return ProcessPage(pageNumber, dictionary, namedDestinations, cropBox, userSpaceUnit, rotation, mediaBox, parsingOptions);
+ }
+
+ protected override PageInformation ProcessPage(int pageNumber,
+ DictionaryToken dictionary,
+ NamedDestinations namedDestinations,
+ CropBox cropBox,
+ UserSpaceUnit userSpaceUnit,
+ PageRotationDegrees rotation,
+ MediaBox mediaBox,
+ IParsingOptions parsingOptions)
+ {
+ return new PageInformation()
+ {
+ Number = pageNumber,
+ Rotation = rotation,
+ MediaBox = mediaBox,
+ CropBox = cropBox,
+ UserSpaceUnit = userSpaceUnit
+ };
+ }
+ }
+ #endregion
+ }
+}
diff --git a/src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs b/src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs
index b8716073f..cf8911fff 100644
--- a/src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs
+++ b/src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs
@@ -81,17 +81,21 @@ public void OnlyExposedApiIsPublic()
"UglyToad.PdfPig.Content.Hyperlink",
"UglyToad.PdfPig.Content.InlineImage",
"UglyToad.PdfPig.Content.IPdfImage",
+ "UglyToad.PdfPig.Content.IResourceStore",
"UglyToad.PdfPig.Content.Letter",
"UglyToad.PdfPig.Content.MarkedContentElement",
"UglyToad.PdfPig.Content.MediaBox",
"UglyToad.PdfPig.Content.OptionalContentGroupElement",
"UglyToad.PdfPig.Content.Page",
+ "UglyToad.PdfPig.Content.PageFactoryBase`1",
"UglyToad.PdfPig.Content.PageRotationDegrees",
"UglyToad.PdfPig.Content.PageSize",
"UglyToad.PdfPig.Content.PageTreeNode",
+ "UglyToad.PdfPig.Content.PageTreeMembers",
"UglyToad.PdfPig.Content.Word",
"UglyToad.PdfPig.Content.TextOrientation",
"UglyToad.PdfPig.Content.XmpMetadata",
+ "UglyToad.PdfPig.Content.IPageFactory`1",
"UglyToad.PdfPig.CrossReference.CrossReferenceTable",
"UglyToad.PdfPig.CrossReference.CrossReferenceType",
"UglyToad.PdfPig.CrossReference.TrailerDictionary",
@@ -99,6 +103,7 @@ public void OnlyExposedApiIsPublic()
"UglyToad.PdfPig.Filters.DefaultFilterProvider",
"UglyToad.PdfPig.Filters.IFilter",
"UglyToad.PdfPig.Filters.IFilterProvider",
+ "UglyToad.PdfPig.Filters.ILookupFilterProvider",
"UglyToad.PdfPig.Functions.FunctionTypes",
"UglyToad.PdfPig.Functions.PdfFunction",
"UglyToad.PdfPig.PdfFonts.CharacterBoundingBox",
@@ -109,9 +114,12 @@ public void OnlyExposedApiIsPublic()
"UglyToad.PdfPig.PdfFonts.FontStretch",
"UglyToad.PdfPig.PdfFonts.IFont",
"UglyToad.PdfPig.Geometry.GeometryExtensions",
+ "UglyToad.PdfPig.Geometry.UserSpaceUnit",
+ "UglyToad.PdfPig.Graphics.BaseStreamProcessor`1",
"UglyToad.PdfPig.Graphics.Colors.CMYKColor",
"UglyToad.PdfPig.Graphics.Colors.ColorSpace",
"UglyToad.PdfPig.Graphics.PdfPath",
+ "UglyToad.PdfPig.Graphics.Colors.ResourceColorSpace",
"UglyToad.PdfPig.Graphics.Colors.ColorSpaceExtensions",
"UglyToad.PdfPig.Graphics.Colors.ColorSpaceFamily",
"UglyToad.PdfPig.Graphics.Colors.GrayColor",
@@ -153,6 +161,7 @@ public void OnlyExposedApiIsPublic()
"UglyToad.PdfPig.Graphics.CurrentGraphicsState",
"UglyToad.PdfPig.Graphics.IColorSpaceContext",
"UglyToad.PdfPig.Graphics.IOperationContext",
+ "UglyToad.PdfPig.Graphics.InlineImageBuilder",
"UglyToad.PdfPig.Graphics.Operations.ClippingPaths.ModifyClippingByEvenOddIntersect",
"UglyToad.PdfPig.Graphics.Operations.ClippingPaths.ModifyClippingByNonZeroWindingIntersect",
"UglyToad.PdfPig.Graphics.Operations.Compatibility.BeginCompatibilitySection",
@@ -227,9 +236,12 @@ public void OnlyExposedApiIsPublic()
"UglyToad.PdfPig.Graphics.Operations.TextState.SetWordSpacing",
"UglyToad.PdfPig.Graphics.Operations.TextState.Type3SetGlyphWidth",
"UglyToad.PdfPig.Graphics.Operations.TextState.Type3SetGlyphWidthAndBoundingBox",
+ "UglyToad.PdfPig.Graphics.PerformantRectangleTransformer",
+ "UglyToad.PdfPig.Graphics.StreamProcessorHelper",
"UglyToad.PdfPig.Graphics.TextMatrices",
"UglyToad.PdfPig.Graphics.XObjectContentRecord",
"UglyToad.PdfPig.Images.ColorSpaceDetailsByteConverter",
+ "UglyToad.PdfPig.IParsingOptions",
"UglyToad.PdfPig.Logging.ILog",
"UglyToad.PdfPig.Outline.Bookmarks",
"UglyToad.PdfPig.Outline.BookmarkNode",
@@ -237,15 +249,18 @@ public void OnlyExposedApiIsPublic()
"UglyToad.PdfPig.Outline.EmbeddedBookmarkNode",
"UglyToad.PdfPig.Outline.ExternalBookmarkNode",
"UglyToad.PdfPig.Outline.UriBookmarkNode",
+ "UglyToad.PdfPig.Outline.NamedDestinations",
"UglyToad.PdfPig.Outline.Destinations.ExplicitDestination",
"UglyToad.PdfPig.Outline.Destinations.ExplicitDestinationCoordinates",
"UglyToad.PdfPig.Outline.Destinations.ExplicitDestinationType",
"UglyToad.PdfPig.ParsingOptions",
+ "UglyToad.PdfPig.Parser.IPageContentParser",
"UglyToad.PdfPig.PdfDocument",
"UglyToad.PdfPig.PdfExtensions",
"UglyToad.PdfPig.Rendering.IPageImageRenderer",
"UglyToad.PdfPig.Rendering.PdfRendererImageFormat",
"UglyToad.PdfPig.Structure",
+ "UglyToad.PdfPig.Tokenization.Scanner.IPdfTokenScanner",
"UglyToad.PdfPig.Util.Adler32Checksum",
"UglyToad.PdfPig.Util.IWordExtractor",
"UglyToad.PdfPig.Util.DefaultWordExtractor",
@@ -259,6 +274,7 @@ public void OnlyExposedApiIsPublic()
"UglyToad.PdfPig.Writer.PdfWriterType",
"UglyToad.PdfPig.Writer.PdfPageBuilder",
"UglyToad.PdfPig.Writer.TokenWriter",
+ "UglyToad.PdfPig.XObjects.XObjectFactory",
"UglyToad.PdfPig.XObjects.XObjectImage",
"UglyToad.PdfPig.XObjects.XObjectImage",
"UglyToad.PdfPig.XObjects.XObjectType"
diff --git a/src/UglyToad.PdfPig.Tokenization/UglyToad.PdfPig.Tokenization.csproj b/src/UglyToad.PdfPig.Tokenization/UglyToad.PdfPig.Tokenization.csproj
index 47504044b..79c74f20b 100644
--- a/src/UglyToad.PdfPig.Tokenization/UglyToad.PdfPig.Tokenization.csproj
+++ b/src/UglyToad.PdfPig.Tokenization/UglyToad.PdfPig.Tokenization.csproj
@@ -7,6 +7,7 @@
true
true
..\pdfpig.snk
+ annotations
true
diff --git a/src/UglyToad.PdfPig.Tokens/UglyToad.PdfPig.Tokens.csproj b/src/UglyToad.PdfPig.Tokens/UglyToad.PdfPig.Tokens.csproj
index 8f665d09a..3f6d2d6ed 100644
--- a/src/UglyToad.PdfPig.Tokens/UglyToad.PdfPig.Tokens.csproj
+++ b/src/UglyToad.PdfPig.Tokens/UglyToad.PdfPig.Tokens.csproj
@@ -7,6 +7,7 @@
true
true
..\pdfpig.snk
+ annotations
true
diff --git a/src/UglyToad.PdfPig.sln b/src/UglyToad.PdfPig.sln
index 444459e42..a5d90e22b 100644
--- a/src/UglyToad.PdfPig.sln
+++ b/src/UglyToad.PdfPig.sln
@@ -22,6 +22,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UglyToad.PdfPig.Tokens", "U
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UglyToad.PdfPig.Tokenization", "UglyToad.PdfPig.Tokenization\UglyToad.PdfPig.Tokenization.csproj", "{FD005C50-CD2C-497E-8F7E-6D791091E9B0}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UglyToad.PdfPig.Rendering.Skia", "UglyToad.PdfPig.Rendering.Skia\UglyToad.PdfPig.Rendering.Skia.csproj", "{14D9E07B-1839-456C-8CB6-40F705D8C5E5}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UglyToad.PdfPig.Rendering.Skia.Tests", "UglyToad.PdfPig.Rendering.Skia.Tests\UglyToad.PdfPig.Rendering.Skia.Tests.csproj", "{E2D3929F-F205-4233-ACFA-2019A4F68EE9}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -56,6 +60,14 @@ Global
{FD005C50-CD2C-497E-8F7E-6D791091E9B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FD005C50-CD2C-497E-8F7E-6D791091E9B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FD005C50-CD2C-497E-8F7E-6D791091E9B0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {14D9E07B-1839-456C-8CB6-40F705D8C5E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {14D9E07B-1839-456C-8CB6-40F705D8C5E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {14D9E07B-1839-456C-8CB6-40F705D8C5E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {14D9E07B-1839-456C-8CB6-40F705D8C5E5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E2D3929F-F205-4233-ACFA-2019A4F68EE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E2D3929F-F205-4233-ACFA-2019A4F68EE9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E2D3929F-F205-4233-ACFA-2019A4F68EE9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E2D3929F-F205-4233-ACFA-2019A4F68EE9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/src/UglyToad.PdfPig/Content/IPageFactory.cs b/src/UglyToad.PdfPig/Content/IPageFactory.cs
index c0380cad5..7c005a54a 100644
--- a/src/UglyToad.PdfPig/Content/IPageFactory.cs
+++ b/src/UglyToad.PdfPig/Content/IPageFactory.cs
@@ -3,12 +3,19 @@
using Outline;
using Tokens;
- internal interface IPageFactory
+ ///
+ /// Page factory interface.
+ ///
+ /// The type of page the page factory creates.
+ public interface IPageFactory
{
- Page Create(int number,
+ ///
+ /// Create the page.
+ ///
+ TPage Create(int number,
DictionaryToken dictionary,
PageTreeMembers pageTreeMembers,
NamedDestinations annotationProvider,
- InternalParsingOptions parsingOptions);
+ IParsingOptions parsingOptions);
}
}
\ No newline at end of file
diff --git a/src/UglyToad.PdfPig/Content/IResourceStore.cs b/src/UglyToad.PdfPig/Content/IResourceStore.cs
index 50346a4c9..bb83988f1 100644
--- a/src/UglyToad.PdfPig/Content/IResourceStore.cs
+++ b/src/UglyToad.PdfPig/Content/IResourceStore.cs
@@ -5,9 +5,15 @@
using System.Collections.Generic;
using Tokens;
- internal interface IResourceStore
+ ///
+ /// Resource store.
+ ///
+ public interface IResourceStore
{
- void LoadResourceDictionary(DictionaryToken resourceDictionary, InternalParsingOptions parsingOptions);
+ ///
+ /// Load the resource dictionary.
+ ///
+ void LoadResourceDictionary(DictionaryToken resourceDictionary, IParsingOptions parsingOptions);
///
/// Remove any named resources and associated state for the last resource dictionary loaded.
@@ -15,22 +21,49 @@ internal interface IResourceStore
///
void UnloadResourceDictionary();
+ ///
+ /// Get the font corresponding to the name.
+ ///
IFont GetFont(NameToken name);
+ ///
+ /// Try get the XObject corresponding to the name.
+ ///
bool TryGetXObject(NameToken name, out StreamToken stream);
+ ///
+ /// Get the extended graphics state dictionary corresponding to the name.
+ ///
DictionaryToken GetExtendedGraphicsStateDictionary(NameToken name);
+ ///
+ /// Get the font from the .
+ ///
IFont GetFontDirectly(IndirectReferenceToken fontReferenceToken);
+ ///
+ /// Get the named color space by its name.
+ ///
bool TryGetNamedColorSpace(NameToken name, out ResourceColorSpace namedColorSpace);
+ ///
+ /// Get the color space details corresponding to the name.
+ ///
ColorSpaceDetails GetColorSpaceDetails(NameToken name, DictionaryToken dictionary);
+ ///
+ /// Get the marked content properties dictionary corresponding to the name.
+ ///
DictionaryToken GetMarkedContentPropertiesDictionary(NameToken name);
+ ///
+ /// Get all as a dictionnary. Keys are the names.
+ ///
IReadOnlyDictionary GetPatterns();
+ ///
+ /// Get the shading corresponding to the name.
+ ///
Shading GetShading(NameToken name);
}
}
\ No newline at end of file
diff --git a/src/UglyToad.PdfPig/Content/PageFactoryBase.cs b/src/UglyToad.PdfPig/Content/PageFactoryBase.cs
new file mode 100644
index 000000000..4b4903f4b
--- /dev/null
+++ b/src/UglyToad.PdfPig/Content/PageFactoryBase.cs
@@ -0,0 +1,264 @@
+namespace UglyToad.PdfPig.Content
+{
+ using Core;
+ using System;
+ using System.Collections.Generic;
+ using UglyToad.PdfPig.Filters;
+ using UglyToad.PdfPig.Geometry;
+ using UglyToad.PdfPig.Logging;
+ using UglyToad.PdfPig.Outline;
+ using UglyToad.PdfPig.Parser;
+ using UglyToad.PdfPig.Parser.Parts;
+ using UglyToad.PdfPig.Tokenization.Scanner;
+ using UglyToad.PdfPig.Tokens;
+ using UglyToad.PdfPig.Util;
+
+ ///
+ /// Page factory abstract class.
+ ///
+ /// The type of page the page factory creates.
+ public abstract class PageFactoryBase : IPageFactory
+ {
+ ///
+ /// The Pdf token scanner.
+ ///
+ public readonly IPdfTokenScanner PdfScanner;
+
+ ///
+ /// The resource store.
+ ///
+ public readonly IResourceStore ResourceStore;
+
+ ///
+ /// The filter provider.
+ ///
+ public readonly ILookupFilterProvider FilterProvider;
+
+ ///
+ /// The page content parser.
+ ///
+ public readonly IPageContentParser PageContentParser;
+
+ ///
+ /// The used to record messages raised by the parsing process.
+ ///
+ public readonly ILog Log;
+
+ ///
+ /// Create a .
+ ///
+ protected PageFactoryBase(
+ IPdfTokenScanner pdfScanner,
+ IResourceStore resourceStore,
+ ILookupFilterProvider filterProvider,
+ IPageContentParser pageContentParser,
+ ILog log)
+ {
+ ResourceStore = resourceStore;
+ FilterProvider = filterProvider;
+ PageContentParser = pageContentParser;
+ PdfScanner = pdfScanner;
+ Log = log;
+ }
+
+ ///
+ public TPage Create(int number, DictionaryToken dictionary, PageTreeMembers pageTreeMembers,
+ NamedDestinations namedDestinations, IParsingOptions parsingOptions)
+ {
+ if (dictionary == null)
+ {
+ throw new ArgumentNullException(nameof(dictionary));
+ }
+
+ var type = dictionary.GetNameOrDefault(NameToken.Type);
+
+ if (type != null && !type.Equals(NameToken.Page))
+ {
+ parsingOptions.Logger.Error($"Page {number} had its type specified as {type} rather than 'Page'.");
+ }
+
+ MediaBox mediaBox = GetMediaBox(number, dictionary, pageTreeMembers);
+ CropBox cropBox = GetCropBox(dictionary, pageTreeMembers, mediaBox);
+
+ var rotation = new PageRotationDegrees(pageTreeMembers.Rotation);
+ // TODO - check if NameToken.Rotate is already looked for in Pages.cs, we don't need to look again
+ if (dictionary.TryGet(NameToken.Rotate, PdfScanner, out NumericToken rotateToken))
+ {
+ rotation = new PageRotationDegrees(rotateToken.Int);
+ }
+
+ var stackDepth = 0;
+
+ while (pageTreeMembers.ParentResources.Count > 0)
+ {
+ var resource = pageTreeMembers.ParentResources.Dequeue();
+
+ ResourceStore.LoadResourceDictionary(resource, parsingOptions);
+ stackDepth++;
+ }
+
+ if (dictionary.TryGet(NameToken.Resources, PdfScanner, out DictionaryToken resources))
+ {
+ ResourceStore.LoadResourceDictionary(resources, parsingOptions);
+ stackDepth++;
+ }
+
+ UserSpaceUnit userSpaceUnit = GetUserSpaceUnits(dictionary);
+
+ TPage page;
+
+ if (!dictionary.TryGet(NameToken.Contents, out var contents))
+ {
+ page = ProcessPage(number, dictionary, namedDestinations, cropBox, userSpaceUnit, rotation, mediaBox, parsingOptions);
+ }
+ else if (DirectObjectFinder.TryGet(contents, PdfScanner, out var array))
+ {
+ var bytes = new List();
+
+ for (var i = 0; i < array.Data.Count; i++)
+ {
+ var item = array.Data[i];
+
+ if (!(item is IndirectReferenceToken obj))
+ {
+ throw new PdfDocumentFormatException($"The contents contained something which was not an indirect reference: {item}.");
+ }
+
+ var contentStream = DirectObjectFinder.Get(obj, PdfScanner);
+
+ if (contentStream == null)
+ {
+ throw new InvalidOperationException($"Could not find the contents for object {obj}.");
+ }
+
+ bytes.AddRange(contentStream.Decode(FilterProvider, PdfScanner));
+
+ if (i < array.Data.Count - 1)
+ {
+ bytes.Add((byte)'\n');
+ }
+ }
+
+ page = ProcessPage(number, dictionary, namedDestinations, bytes, cropBox, userSpaceUnit, rotation, mediaBox, parsingOptions);
+ }
+ else
+ {
+ var contentStream = DirectObjectFinder.Get(contents, PdfScanner);
+
+ if (contentStream == null)
+ {
+ throw new InvalidOperationException("Failed to parse the content for the page: " + number);
+ }
+
+ var bytes = contentStream.Decode(FilterProvider, PdfScanner);
+
+ page = ProcessPage(number, dictionary, namedDestinations, bytes, cropBox, userSpaceUnit, rotation, mediaBox, parsingOptions);
+ }
+
+ for (var i = 0; i < stackDepth; i++)
+ {
+ ResourceStore.UnloadResourceDictionary();
+ }
+
+ return page;
+ }
+
+ ///
+ /// Process a page with no content.
+ ///
+ protected abstract TPage ProcessPage(
+ int pageNumber,
+ DictionaryToken dictionary,
+ NamedDestinations namedDestinations,
+ IReadOnlyList contentBytes,
+ CropBox cropBox,
+ UserSpaceUnit userSpaceUnit,
+ PageRotationDegrees rotation,
+ MediaBox mediaBox,
+ IParsingOptions parsingOptions);
+
+ ///
+ /// Process a page with no content.
+ ///
+ protected abstract TPage ProcessPage(
+ int pageNumber,
+ DictionaryToken dictionary,
+ NamedDestinations namedDestinations,
+ CropBox cropBox,
+ UserSpaceUnit userSpaceUnit,
+ PageRotationDegrees rotation,
+ MediaBox mediaBox,
+ IParsingOptions parsingOptions);
+
+ ///
+ /// Get the user space units.
+ ///
+ protected static UserSpaceUnit GetUserSpaceUnits(DictionaryToken dictionary)
+ {
+ var spaceUnits = UserSpaceUnit.Default;
+ if (dictionary.TryGet(NameToken.UserUnit, out var userUnitBase) && userUnitBase is NumericToken userUnitNumber)
+ {
+ spaceUnits = new UserSpaceUnit(userUnitNumber.Int);
+ }
+
+ return spaceUnits;
+ }
+
+ ///
+ /// Get the crop box.
+ ///
+ protected CropBox GetCropBox(DictionaryToken dictionary, PageTreeMembers pageTreeMembers, MediaBox mediaBox)
+ {
+ if (dictionary.TryGet(NameToken.CropBox, out var cropBoxObject) &&
+ DirectObjectFinder.TryGet(cropBoxObject, PdfScanner, out ArrayToken cropBoxArray))
+ {
+ if (cropBoxArray.Length != 4)
+ {
+ Log.Error($"The CropBox was the wrong length in the dictionary: {dictionary}. Array was: {cropBoxArray}. Using MediaBox.");
+
+ return new CropBox(mediaBox.Bounds);
+ }
+
+ return new CropBox(cropBoxArray.ToRectangle(PdfScanner));
+ }
+ else
+ {
+ return pageTreeMembers.GetCropBox() ?? new CropBox(mediaBox.Bounds);
+ }
+ }
+
+ ///
+ /// Get the media box.
+ ///
+ protected MediaBox GetMediaBox(int number, DictionaryToken dictionary, PageTreeMembers pageTreeMembers)
+ {
+ MediaBox mediaBox;
+ if (dictionary.TryGet(NameToken.MediaBox, out var mediaBoxObject)
+ && DirectObjectFinder.TryGet(mediaBoxObject, PdfScanner, out ArrayToken mediaBoxArray))
+ {
+ if (mediaBoxArray.Length != 4)
+ {
+ Log.Error($"The MediaBox was the wrong length in the dictionary: {dictionary}. Array was: {mediaBoxArray}. Defaulting to US Letter.");
+
+ return MediaBox.Letter;
+ }
+
+ mediaBox = new MediaBox(mediaBoxArray.ToRectangle(PdfScanner));
+ }
+ else
+ {
+ mediaBox = pageTreeMembers.MediaBox;
+
+ if (mediaBox == null)
+ {
+ Log.Error($"The MediaBox was the wrong missing for page {number}. Using US Letter.");
+
+ // PDFBox defaults to US Letter.
+ mediaBox = MediaBox.Letter;
+ }
+ }
+
+ return mediaBox;
+ }
+ }
+}
diff --git a/src/UglyToad.PdfPig/Content/PageTreeMembers.cs b/src/UglyToad.PdfPig/Content/PageTreeMembers.cs
index 20ae07440..3a949a0d6 100644
--- a/src/UglyToad.PdfPig/Content/PageTreeMembers.cs
+++ b/src/UglyToad.PdfPig/Content/PageTreeMembers.cs
@@ -6,17 +6,26 @@
///
/// Contains the values inherited from the Page Tree for this page.
///
- internal class PageTreeMembers
+ public class PageTreeMembers
{
- public CropBox GetCropBox()
+ internal CropBox GetCropBox()
{
return null;
}
+ ///
+ /// The page media box.
+ ///
public MediaBox MediaBox { get; set; }
+ ///
+ /// The page rotation.
+ ///
public int Rotation { get; set; }
+ ///
+ /// The page parent resources.
+ ///
public Queue ParentResources { get; } = new Queue();
}
}
\ No newline at end of file
diff --git a/src/UglyToad.PdfPig/Content/Pages.cs b/src/UglyToad.PdfPig/Content/Pages.cs
index e25d0137d..6d27d4c86 100644
--- a/src/UglyToad.PdfPig/Content/Pages.cs
+++ b/src/UglyToad.PdfPig/Content/Pages.cs
@@ -3,14 +3,22 @@
using Core;
using Outline;
using System;
+ using System.Collections.Concurrent;
using System.Collections.Generic;
+ using System.Data;
+ using System.Linq;
+ using System.Runtime.Serialization;
+ using System.Runtime.Versioning;
using Tokenization.Scanner;
using Tokens;
+ using UglyToad.PdfPig.Parser;
using Util;
internal class Pages
{
- private readonly IPageFactory pageFactory;
+ private readonly ConcurrentDictionary pageFactoryCache = new ConcurrentDictionary();
+
+ private readonly IPageFactory defaultPageFactory;
private readonly IPdfTokenScanner pdfScanner;
private readonly Dictionary pagesByNumber;
public int Count => pagesByNumber.Count;
@@ -20,21 +28,35 @@ internal class Pages
///
public PageTreeNode PageTree { get; }
- internal Pages(IPageFactory pageFactory, IPdfTokenScanner pdfScanner, PageTreeNode pageTree, Dictionary pagesByNumber)
+ internal Pages(IPageFactory pageFactory, IPdfTokenScanner pdfScanner, PageTreeNode pageTree, Dictionary pagesByNumber)
{
- this.pageFactory = pageFactory ?? throw new ArgumentNullException(nameof(pageFactory));
+ this.defaultPageFactory = pageFactory ?? throw new ArgumentNullException(nameof(pageFactory));
this.pdfScanner = pdfScanner ?? throw new ArgumentNullException(nameof(pdfScanner));
this.pagesByNumber = pagesByNumber;
PageTree = pageTree;
+
+ AddPageFactory(this.defaultPageFactory);
+ }
+
+ internal Page GetPage(int pageNumber, NamedDestinations namedDestinations, InternalParsingOptions parsingOptions) => GetPage(defaultPageFactory, pageNumber, namedDestinations, parsingOptions);
+
+ internal TPage GetPage(int pageNumber, NamedDestinations namedDestinations, InternalParsingOptions parsingOptions)
+ {
+ if (pageFactoryCache.TryGetValue(typeof(TPage), out var o) && o is IPageFactory pageFactory)
+ {
+ return GetPage(pageFactory, pageNumber, namedDestinations, parsingOptions);
+ }
+
+ throw new InvalidOperationException($"Could not find {typeof(IPageFactory)} for page type {typeof(TPage)}.");
}
- internal Page GetPage(int pageNumber, NamedDestinations namedDestinations, InternalParsingOptions parsingOptions)
+ private TPage GetPage(IPageFactory pageFactory, int pageNumber, NamedDestinations namedDestinations, InternalParsingOptions parsingOptions)
{
if (pageNumber <= 0 || pageNumber > Count)
{
parsingOptions.Logger.Error($"Page {pageNumber} requested but is out of range.");
- throw new ArgumentOutOfRangeException(nameof(pageNumber),
+ throw new ArgumentOutOfRangeException(nameof(pageNumber),
$"Page number {pageNumber} invalid, must be between 1 and {Count}.");
}
@@ -49,7 +71,7 @@ internal Page GetPage(int pageNumber, NamedDestinations namedDestinations, Inter
}
var pageTreeMembers = new PageTreeMembers();
-
+
while (pageStack.Count > 0)
{
currentNode = pageStack.Pop();
@@ -58,7 +80,7 @@ internal Page GetPage(int pageNumber, NamedDestinations namedDestinations, Inter
{
pageTreeMembers.ParentResources.Enqueue(resourcesDictionary);
}
-
+
if (currentNode.NodeDictionary.TryGet(NameToken.MediaBox, pdfScanner, out ArrayToken mediaBox))
{
pageTreeMembers.MediaBox = new MediaBox(mediaBox.ToRectangle(pdfScanner));
@@ -70,14 +92,30 @@ internal Page GetPage(int pageNumber, NamedDestinations namedDestinations, Inter
}
}
- var page = pageFactory.Create(
+ return pageFactory.Create(
pageNumber,
pageNode.NodeDictionary,
pageTreeMembers,
namedDestinations,
parsingOptions);
-
- return page;
+ }
+
+ internal void AddPageFactory(IPageFactory pageFactory)
+ {
+ // TODO - throw if already exists
+ pageFactoryCache.TryAdd(typeof(TPage), pageFactory);
+ }
+
+ internal void AddPageFactory() where TPageFactory : IPageFactory
+ {
+ var defaultPageFactory = (PageFactory)pageFactoryCache[typeof(Page)];
+
+ // TODO - careful here - resourceStore is not thread safe
+ var pageFactory = (IPageFactory)Activator.CreateInstance(typeof(TPageFactory),
+ defaultPageFactory.PdfScanner, defaultPageFactory.ResourceStore,
+ defaultPageFactory.FilterProvider, defaultPageFactory.PageContentParser,
+ defaultPageFactory.Log);
+ AddPageFactory(pageFactory);
}
internal PageTreeNode GetPageNode(int pageNumber)
diff --git a/src/UglyToad.PdfPig/Content/PagesFactory.cs b/src/UglyToad.PdfPig/Content/PagesFactory.cs
index d6de06aa8..1837b5084 100644
--- a/src/UglyToad.PdfPig/Content/PagesFactory.cs
+++ b/src/UglyToad.PdfPig/Content/PagesFactory.cs
@@ -10,7 +10,7 @@
using Tokens;
using Util;
- internal class PagesFactory
+ internal static class PagesFactory
{
private class PageCounter
{
@@ -21,7 +21,7 @@ public void Increment()
}
}
- public static Pages Create(IndirectReference pagesReference, DictionaryToken pagesDictionary, IPdfTokenScanner scanner, IPageFactory pageFactory, ILog log, bool isLenientParsing)
+ public static Pages Create(IndirectReference pagesReference, DictionaryToken pagesDictionary, IPdfTokenScanner scanner, IPageFactory pageFactory, ILog log, bool isLenientParsing)
{
var pageNumber = new PageCounter();
diff --git a/src/UglyToad.PdfPig/Content/ResourceStore.cs b/src/UglyToad.PdfPig/Content/ResourceStore.cs
index 08ea50d2e..e45f33e9d 100644
--- a/src/UglyToad.PdfPig/Content/ResourceStore.cs
+++ b/src/UglyToad.PdfPig/Content/ResourceStore.cs
@@ -41,7 +41,7 @@ public ResourceStore(IPdfTokenScanner scanner, IFontFactory fontFactory, ILookup
this.filterProvider = filterProvider;
}
- public void LoadResourceDictionary(DictionaryToken resourceDictionary, InternalParsingOptions parsingOptions)
+ public void LoadResourceDictionary(DictionaryToken resourceDictionary, IParsingOptions parsingOptions)
{
lastLoadedFont = (null, null);
loadedNamedColorSpaceDetails.Clear();
@@ -176,7 +176,7 @@ public void UnloadResourceDictionary()
namedColorSpaces.Pop();
}
- private void LoadFontDictionary(DictionaryToken fontDictionary, InternalParsingOptions parsingOptions)
+ private void LoadFontDictionary(DictionaryToken fontDictionary, IParsingOptions parsingOptions)
{
lastLoadedFont = (null, null);
diff --git a/src/UglyToad.PdfPig/Filters/IFilterProvider.cs b/src/UglyToad.PdfPig/Filters/IFilterProvider.cs
index 51bcfecc5..9120e30a1 100644
--- a/src/UglyToad.PdfPig/Filters/IFilterProvider.cs
+++ b/src/UglyToad.PdfPig/Filters/IFilterProvider.cs
@@ -25,8 +25,14 @@ public interface IFilterProvider
IReadOnlyList GetAllFilters();
}
- internal interface ILookupFilterProvider : IFilterProvider
+ ///
+ /// Gets filter implementations () for decoding PDF data with lookup.
+ ///
+ public interface ILookupFilterProvider : IFilterProvider
{
+ ///
+ /// Get the filters specified in this dictionary.
+ ///
IReadOnlyList GetFilters(DictionaryToken dictionary, IPdfTokenScanner scanner);
}
}
\ No newline at end of file
diff --git a/src/UglyToad.PdfPig/Geometry/UserSpaceUnit.cs b/src/UglyToad.PdfPig/Geometry/UserSpaceUnit.cs
index 65f15a223..e644ed742 100644
--- a/src/UglyToad.PdfPig/Geometry/UserSpaceUnit.cs
+++ b/src/UglyToad.PdfPig/Geometry/UserSpaceUnit.cs
@@ -7,8 +7,11 @@
/// By default user space units correspond to 1/72nd of an inch (a typographic point).
/// The UserUnit entry in a page dictionary can define the space units as a different multiple of 1/72 (1 point).
///
- internal readonly struct UserSpaceUnit
+ public readonly struct UserSpaceUnit
{
+ ///
+ /// Default with set to 1.
+ ///
public static readonly UserSpaceUnit Default = new UserSpaceUnit(1);
///
@@ -29,6 +32,7 @@ public UserSpaceUnit(int pointMultiples)
PointMultiples = pointMultiples;
}
+ ///
public override string ToString()
{
return PointMultiples.ToString(CultureInfo.InvariantCulture);
diff --git a/src/UglyToad.PdfPig/Graphics/BaseStreamProcessor.cs b/src/UglyToad.PdfPig/Graphics/BaseStreamProcessor.cs
new file mode 100644
index 000000000..fa96aa612
--- /dev/null
+++ b/src/UglyToad.PdfPig/Graphics/BaseStreamProcessor.cs
@@ -0,0 +1,844 @@
+namespace UglyToad.PdfPig.Graphics
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using System.Linq;
+ using UglyToad.PdfPig.Content;
+ using UglyToad.PdfPig.Core;
+ using UglyToad.PdfPig.Filters;
+ using UglyToad.PdfPig.Geometry;
+ using UglyToad.PdfPig.Graphics.Colors;
+ using UglyToad.PdfPig.Graphics.Core;
+ using UglyToad.PdfPig.Graphics.Operations;
+ using UglyToad.PdfPig.Graphics.Operations.TextPositioning;
+ using UglyToad.PdfPig.Parser;
+ using UglyToad.PdfPig.PdfFonts;
+ using UglyToad.PdfPig.Tokenization.Scanner;
+ using UglyToad.PdfPig.Tokens;
+ using UglyToad.PdfPig.XObjects;
+
+ ///
+ /// Stream processor Abstract class.
+ ///
+ ///
+ public abstract class BaseStreamProcessor : IOperationContext
+ {
+ ///
+ /// The resource store.
+ ///
+ protected readonly IResourceStore ResourceStore;
+
+ ///
+ /// The user space unit.
+ ///
+ protected readonly UserSpaceUnit UserSpaceUnit;
+
+ ///
+ /// The page rotation.
+ ///
+ protected readonly PageRotationDegrees Rotation;
+
+ ///
+ /// The scanner.
+ ///
+ protected readonly IPdfTokenScanner PdfScanner;
+
+ ///
+ /// The page content parser.
+ ///
+ protected readonly IPageContentParser PageContentParser;
+
+ ///
+ /// The filter provider.
+ ///
+ protected readonly ILookupFilterProvider FilterProvider;
+
+ ///
+ /// The parsing options.
+ ///
+ protected readonly IParsingOptions ParsingOptions;
+
+ ///
+ /// The graphics stack.
+ ///
+ protected Stack GraphicsStack = new Stack();
+
+ ///
+ /// The active ExtendedGraphicsState font.
+ ///
+ protected IFont ActiveExtendedGraphicsStateFont;
+
+ ///
+ /// Inline image builder.
+ ///
+ protected InlineImageBuilder InlineImageBuilder;
+
+ ///
+ /// The page number.
+ ///
+ protected int PageNumber;
+
+ ///
+ public TextMatrices TextMatrices { get; } = new TextMatrices();
+
+ ///
+ public TransformationMatrix CurrentTransformationMatrix => GetCurrentState().CurrentTransformationMatrix;
+
+ ///
+ public PdfPoint CurrentPosition { get; set; }
+
+ ///
+ public int StackSize => GraphicsStack.Count;
+
+ ///
+ /// A counter to track individual calls to operations used to determine if letters are likely to be
+ /// in the same word/group. This exposes internal grouping of letters used by the PDF creator which may correspond to the
+ /// intended grouping of letters into words.
+ ///
+ protected int TextSequence;
+
+ private readonly Dictionary> xObjects = new Dictionary>
+ {
+ { XObjectType.Image, new List() },
+ { XObjectType.PostScript, new List() }
+ };
+
+ ///
+ /// Abstract constructor.
+ ///
+ protected BaseStreamProcessor(
+ int pageNumber,
+ IResourceStore resourceStore,
+ UserSpaceUnit userSpaceUnit,
+ MediaBox mediaBox,
+ CropBox cropBox,
+ PageRotationDegrees rotation,
+ IPdfTokenScanner pdfScanner,
+ IPageContentParser pageContentParser,
+ ILookupFilterProvider filterProvider,
+ IParsingOptions parsingOptions)
+ {
+ this.PageNumber = pageNumber;
+ this.ResourceStore = resourceStore;
+ this.UserSpaceUnit = userSpaceUnit;
+ this.Rotation = rotation;
+ this.PdfScanner = pdfScanner ?? throw new ArgumentNullException(nameof(pdfScanner));
+ this.PageContentParser = pageContentParser ?? throw new ArgumentNullException(nameof(pageContentParser));
+ this.FilterProvider = filterProvider ?? throw new ArgumentNullException(nameof(filterProvider));
+ this.ParsingOptions = parsingOptions;
+
+ TransformationMatrix initialMatrix = StreamProcessorHelper.GetInitialMatrix(userSpaceUnit, mediaBox, cropBox, rotation, parsingOptions.Logger);
+
+ GraphicsStack.Push(new CurrentGraphicsState()
+ {
+ CurrentTransformationMatrix = initialMatrix,
+ CurrentClippingPath = StreamProcessorHelper.GetInitialClipping(cropBox, initialMatrix),
+ ColorSpaceContext = new ColorSpaceContext(GetCurrentState, resourceStore)
+ });
+ }
+
+ ///
+ /// Process the s and return content.
+ ///
+ public abstract TPageContent Process(int pageNumberCurrent, IReadOnlyList operations);
+
+ ///
+ /// Process the s.
+ ///
+ protected void ProcessOperations(IReadOnlyList operations)
+ {
+ foreach (var stateOperation in operations)
+ {
+ stateOperation.Run(this);
+ }
+ }
+
+ ///
+ protected Stack CloneAllStates()
+ {
+ var saved = GraphicsStack;
+ GraphicsStack = new Stack();
+ GraphicsStack.Push(saved.Peek().DeepClone());
+ return saved;
+ }
+
+ ///
+ [DebuggerStepThrough]
+ public CurrentGraphicsState GetCurrentState()
+ {
+ return GraphicsStack.Peek();
+ }
+
+ ///
+ public virtual void PopState()
+ {
+ GraphicsStack.Pop();
+ ActiveExtendedGraphicsStateFont = null;
+ }
+
+ ///
+ public virtual void PushState()
+ {
+ GraphicsStack.Push(GraphicsStack.Peek().DeepClone());
+ }
+
+ ///
+ public virtual void ShowText(IInputBytes bytes)
+ {
+ var currentState = GetCurrentState();
+
+ var font = currentState.FontState.FromExtendedGraphicsState ? ActiveExtendedGraphicsStateFont : ResourceStore.GetFont(currentState.FontState.FontName);
+
+ if (font == null)
+ {
+ if (ParsingOptions.SkipMissingFonts)
+ {
+ ParsingOptions.Logger.Warn($"Skipping a missing font with name {currentState.FontState.FontName} " +
+ $"since it is not present in the document and {nameof(InternalParsingOptions.SkipMissingFonts)} " +
+ "is set to true. This may result in some text being skipped and not included in the output.");
+
+ return;
+ }
+
+ throw new InvalidOperationException($"Could not find the font with name {currentState.FontState.FontName} in the resource store. It has not been loaded yet.");
+ }
+
+ var fontSize = currentState.FontState.FontSize;
+ var horizontalScaling = currentState.FontState.HorizontalScaling / 100.0;
+ var characterSpacing = currentState.FontState.CharacterSpacing;
+ var rise = currentState.FontState.Rise;
+
+ var transformationMatrix = currentState.CurrentTransformationMatrix;
+
+ var renderingMatrix =
+ TransformationMatrix.FromValues(fontSize * horizontalScaling, 0, 0, fontSize, 0, rise);
+
+ var pointSize = Math.Round(transformationMatrix.Multiply(TextMatrices.TextMatrix).Transform(new PdfRectangle(0, 0, 1, fontSize)).Height, 2);
+
+ while (bytes.MoveNext())
+ {
+ var code = font.ReadCharacterCode(bytes, out int codeLength);
+
+ var foundUnicode = font.TryGetUnicode(code, out var unicode);
+
+ if (!foundUnicode || unicode == null)
+ {
+ ParsingOptions.Logger.Warn($"We could not find the corresponding character with code {code} in font {font.Name}.");
+
+ // Try casting directly to string as in PDFBox 1.8.
+ unicode = new string((char)code, 1);
+ }
+
+ var wordSpacing = 0.0;
+ if (code == ' ' && codeLength == 1)
+ {
+ wordSpacing += GetCurrentState().FontState.WordSpacing;
+ }
+
+ var textMatrix = TextMatrices.TextMatrix;
+
+ if (font.IsVertical)
+ {
+ if (!(font is IVerticalWritingSupported verticalFont))
+ {
+ throw new InvalidOperationException($"Font {font.Name} was in vertical writing mode but did not implement {nameof(IVerticalWritingSupported)}.");
+ }
+
+ var positionVector = verticalFont.GetPositionVector(code);
+
+ textMatrix = textMatrix.Translate(positionVector.X, positionVector.Y);
+ }
+
+ var boundingBox = font.GetBoundingBox(code);
+
+ RenderGlyph(font,
+ currentState.CurrentStrokingColor,
+ currentState.CurrentNonStrokingColor,
+ currentState.FontState.TextRenderingMode,
+ fontSize,
+ pointSize,
+ code,
+ unicode,
+ bytes.CurrentOffset,
+ renderingMatrix,
+ textMatrix,
+ transformationMatrix,
+ boundingBox);
+
+ double tx, ty;
+ if (font.IsVertical)
+ {
+ var verticalFont = (IVerticalWritingSupported)font;
+ var displacement = verticalFont.GetDisplacementVector(code);
+ tx = 0;
+ ty = (displacement.Y * fontSize) + characterSpacing + wordSpacing;
+ }
+ else
+ {
+ tx = (boundingBox.Width * fontSize + characterSpacing + wordSpacing) * horizontalScaling;
+ ty = 0;
+ }
+
+ TextMatrices.TextMatrix = TextMatrices.TextMatrix.Translate(tx, ty);
+ }
+ }
+
+ ///
+ /// Render glyph implement.
+ ///
+ public abstract void RenderGlyph(IFont font, IColor strokingColor, IColor nonStrokingColor, TextRenderingMode textRenderingMode, double fontSize, double pointSize, int code, string unicode, long currentOffset,
+ TransformationMatrix renderingMatrix, TransformationMatrix textMatrix, TransformationMatrix transformationMatrix, CharacterBoundingBox characterBoundingBox);
+
+ ///
+ public virtual void ShowPositionedText(IReadOnlyList tokens)
+ {
+ TextSequence++;
+
+ var currentState = GetCurrentState();
+
+ var textState = currentState.FontState;
+
+ var fontSize = textState.FontSize;
+ var horizontalScaling = textState.HorizontalScaling / 100.0;
+ var font = ResourceStore.GetFont(textState.FontName);
+
+ if (font == null)
+ {
+ if (ParsingOptions.SkipMissingFonts)
+ {
+ ParsingOptions.Logger.Warn($"Skipping a missing font with name {currentState.FontState.FontName} " +
+ $"since it is not present in the document and {nameof(InternalParsingOptions.SkipMissingFonts)} " +
+ "is set to true. This may result in some text being skipped and not included in the output.");
+
+ return;
+ }
+
+ throw new InvalidOperationException($"Could not find the font with name {currentState.FontState.FontName} in the resource store. It has not been loaded yet.");
+ }
+
+ var isVertical = font.IsVertical;
+
+ foreach (var token in tokens)
+ {
+ if (token is NumericToken number)
+ {
+ var positionAdjustment = (double)number.Data;
+
+ double tx, ty;
+ if (isVertical)
+ {
+ tx = 0;
+ ty = -positionAdjustment / 1000 * fontSize;
+ }
+ else
+ {
+ tx = -positionAdjustment / 1000 * fontSize * horizontalScaling;
+ ty = 0;
+ }
+
+ AdjustTextMatrix(tx, ty);
+ }
+ else
+ {
+ IReadOnlyList bytes;
+ if (token is HexToken hex)
+ {
+ bytes = hex.Bytes;
+ }
+ else
+ {
+ bytes = OtherEncodings.StringAsLatin1Bytes(((StringToken)token).Data);
+ }
+
+ ShowText(new ByteArrayInputBytes(bytes));
+ }
+ }
+ }
+
+ ///
+ public virtual void ApplyXObject(NameToken xObjectName)
+ {
+ if (!ResourceStore.TryGetXObject(xObjectName, out var xObjectStream))
+ {
+ if (ParsingOptions.SkipMissingFonts)
+ {
+ return;
+ }
+
+ throw new PdfDocumentFormatException($"No XObject with name {xObjectName} found on page {PageNumber}.");
+ }
+
+ // For now we will determine the type and store the object with the graphics state information preceding it.
+ // Then consumers of the page can request the object(s) to be retrieved by type.
+ var subType = (NameToken)xObjectStream.StreamDictionary.Data[NameToken.Subtype.Data];
+
+ var state = GetCurrentState();
+
+ var matrix = state.CurrentTransformationMatrix;
+
+ if (subType.Equals(NameToken.Ps))
+ {
+ var contentRecord = new XObjectContentRecord(XObjectType.PostScript, xObjectStream, matrix, state.RenderingIntent,
+ state.ColorSpaceContext?.CurrentStrokingColorSpace ?? DeviceRgbColorSpaceDetails.Instance);
+ xObjects[XObjectType.PostScript].Add(contentRecord);
+ }
+ else if (subType.Equals(NameToken.Image))
+ {
+ var contentRecord = new XObjectContentRecord(XObjectType.Image, xObjectStream, matrix, state.RenderingIntent,
+ state.ColorSpaceContext?.CurrentStrokingColorSpace ?? DeviceRgbColorSpaceDetails.Instance);
+
+ RenderXObjectImage(contentRecord);
+ }
+ else if (subType.Equals(NameToken.Form))
+ {
+ ProcessFormXObject(xObjectStream, xObjectName);
+ }
+ else
+ {
+ throw new InvalidOperationException($"XObject encountered with unexpected SubType {subType}. {xObjectStream.StreamDictionary}.");
+ }
+ }
+
+ ///
+ /// Process a XObject form.
+ ///
+ protected void ProcessFormXObject(StreamToken formStream, NameToken xObjectName)
+ {
+ /*
+ * When a form XObject is invoked the following should happen:
+ *
+ * 1. Save the current graphics state, as if by invoking the q operator.
+ * 2. Concatenate the matrix from the form dictionary's Matrix entry with the current transformation matrix.
+ * 3. Clip according to the form dictionary's BBox entry.
+ * 4. Paint the graphics objects specified in the form's content stream.
+ * 5. Restore the saved graphics state, as if by invoking the Q operator.
+ */
+
+ var hasResources = formStream.StreamDictionary.TryGet(NameToken.Resources, PdfScanner, out var formResources);
+ if (hasResources)
+ {
+ ResourceStore.LoadResourceDictionary(formResources, ParsingOptions);
+ }
+
+ // 1. Save current state.
+ PushState();
+
+ var startState = GetCurrentState();
+
+ // Transparency Group XObjects
+ if (formStream.StreamDictionary.TryGet(NameToken.Group, PdfScanner, out DictionaryToken formGroupToken))
+ {
+ if (!formGroupToken.TryGet(NameToken.S, PdfScanner, out var sToken) || sToken != NameToken.Transparency)
+ {
+ throw new InvalidOperationException($"Invalid Transparency Group XObject, '{NameToken.S}' token is not set or not equal to '{NameToken.Transparency}'.");
+ }
+
+ /* blend mode
+ * A conforming reader shall implicitly reset this parameter to its initial value at the beginning of execution of a
+ * transparency group XObject (see 11.6.6, "Transparency Group XObjects"). Initial value: Normal.
+ */
+ //startState.BlendMode = BlendMode.Normal;
+
+ /* soft mask
+ * A conforming reader shall implicitly reset this parameter implicitly reset to its initial value at the beginning
+ * of execution of a transparency group XObject (see 11.6.6, "Transparency Group XObjects"). Initial value: None.
+ */
+ // TODO
+
+ /* alpha constant
+ * A conforming reader shall implicitly reset this parameter to its initial value at the beginning of execution of a
+ * transparency group XObject (see 11.6.6, "Transparency Group XObjects"). Initial value: 1.0.
+ */
+ startState.AlphaConstantNonStroking = 1.0m;
+ startState.AlphaConstantStroking = 1.0m;
+
+ if (formGroupToken.TryGet(NameToken.Cs, PdfScanner, out NameToken csNameToken))
+ {
+ startState.ColorSpaceContext.SetNonStrokingColorspace(csNameToken);
+ }
+ else if (formGroupToken.TryGet(NameToken.Cs, PdfScanner, out ArrayToken csArrayToken)
+ && csArrayToken.Length > 0)
+ {
+ if (csArrayToken.Data[0] is NameToken firstColorSpaceName)
+ {
+ startState.ColorSpaceContext.SetNonStrokingColorspace(firstColorSpaceName, formGroupToken);
+ }
+ else
+ {
+ throw new InvalidOperationException("Invalid color space in Transparency Group XObjects.");
+ }
+ }
+
+ bool isolated = false;
+ if (formGroupToken.TryGet(NameToken.I, PdfScanner, out BooleanToken isolatedToken))
+ {
+ /*
+ * (Optional) A flag specifying whether the transparency group is isolated (see “Isolated Groups”).
+ * If this flag is true, objects within the group shall be composited against a fully transparent
+ * initial backdrop; if false, they shall be composited against the group’s backdrop.
+ * Default value: false.
+ */
+ isolated = isolatedToken.Data;
+ }
+
+ bool knockout = false;
+ if (formGroupToken.TryGet(NameToken.K, PdfScanner, out BooleanToken knockoutToken))
+ {
+ /*
+ * (Optional) A flag specifying whether the transparency group is a knockout group (see “Knockout Groups”).
+ * If this flag is false, later objects within the group shall be composited with earlier ones with which
+ * they overlap; if true, they shall be composited with the group’s initial backdrop and shall overwrite
+ * (“knock out”) any earlier overlapping objects.
+ * Default value: false.
+ */
+ knockout = knockoutToken.Data;
+ }
+ }
+
+ var formMatrix = TransformationMatrix.Identity;
+ if (formStream.StreamDictionary.TryGet(NameToken.Matrix, PdfScanner, out var formMatrixToken))
+ {
+ formMatrix = TransformationMatrix.FromArray(formMatrixToken.Data.OfType().Select(x => x.Double).ToArray());
+ }
+
+ // 2. Update current transformation matrix.
+ startState.CurrentTransformationMatrix = formMatrix.Multiply(startState.CurrentTransformationMatrix);
+
+ var contentStream = formStream.Decode(FilterProvider, PdfScanner);
+
+ var operations = PageContentParser.Parse(PageNumber, new ByteArrayInputBytes(contentStream), ParsingOptions.Logger);
+
+ // 3. We don't respect clipping currently.
+
+ // 4. Paint the objects.
+ bool hasCircularReference = HasFormXObjectCircularReference(formStream, xObjectName, operations);
+ if (hasCircularReference)
+ {
+ if (ParsingOptions.UseLenientParsing)
+ {
+ operations = operations.Where(o => o is not InvokeNamedXObject xo || xo.Name != xObjectName).ToArray();
+ ParsingOptions.Logger.Warn($"An XObject form named '{xObjectName}' is referencing itself which can cause unexpected behaviour. The self reference was removed from the operations before further processing.");
+ }
+ else
+ {
+ throw new PdfDocumentFormatException($"An XObject form named '{xObjectName}' is referencing itself which can cause unexpected behaviour.");
+ }
+ }
+
+ ProcessOperations(operations);
+
+ // 5. Restore saved state.
+ PopState();
+
+ if (hasResources)
+ {
+ ResourceStore.UnloadResourceDictionary();
+ }
+ }
+
+ ///
+ /// Check for circular reference in the XObject form.
+ ///
+ /// The original form stream.
+ /// The form's name.
+ /// The form operations parsed from original form stream.
+ private bool HasFormXObjectCircularReference(StreamToken formStream, NameToken xObjectName, IReadOnlyList operations)
+ {
+ return xObjectName != null
+ && operations.OfType()?.Any(o => o.Name == xObjectName) == true // operations contain another form with same name
+ && ResourceStore.TryGetXObject(xObjectName, out var result)
+ && result.Data.SequenceEqual(formStream.Data); // The form contained in the operations has identical data to current form
+ }
+
+ ///
+ /// Render XObject image implementation.
+ ///
+ ///
+ public abstract void RenderXObjectImage(XObjectContentRecord xObjectContentRecord);
+
+ ///
+ public abstract void BeginSubpath();
+
+ ///
+ public abstract PdfPoint? CloseSubpath();
+
+ ///
+ public abstract void StrokePath(bool close);
+
+ ///
+ public abstract void FillPath(FillingRule fillingRule, bool close);
+
+ ///
+ public abstract void FillStrokePath(FillingRule fillingRule, bool close);
+
+ ///
+ public abstract void MoveTo(double x, double y);
+
+ ///
+ public abstract void BezierCurveTo(double x2, double y2, double x3, double y3);
+
+ ///
+ public abstract void BezierCurveTo(double x1, double y1, double x2, double y2, double x3, double y3);
+
+ ///
+ public abstract void LineTo(double x, double y);
+
+ ///
+ public abstract void Rectangle(double x, double y, double width, double height);
+
+ ///
+ public abstract void EndPath();
+
+ ///
+ public abstract void ClosePath();
+
+ ///
+ public abstract void ModifyClippingIntersect(FillingRule clippingRule);
+
+ ///
+ public virtual void SetNamedGraphicsState(NameToken stateName)
+ {
+ var currentGraphicsState = GetCurrentState();
+
+ var state = ResourceStore.GetExtendedGraphicsStateDictionary(stateName);
+
+ if (state.TryGet(NameToken.Lw, PdfScanner, out NumericToken lwToken))
+ {
+ currentGraphicsState.LineWidth = lwToken.Data;
+ }
+
+ if (state.TryGet(NameToken.Lc, PdfScanner, out NumericToken lcToken))
+ {
+ currentGraphicsState.CapStyle = (LineCapStyle)lcToken.Int;
+ }
+
+ if (state.TryGet(NameToken.Lj, PdfScanner, out NumericToken ljToken))
+ {
+ currentGraphicsState.JoinStyle = (LineJoinStyle)ljToken.Int;
+ }
+
+ if (state.TryGet(NameToken.Font, PdfScanner, out ArrayToken fontArray) && fontArray.Length == 2
+ && fontArray.Data[0] is IndirectReferenceToken fontReference && fontArray.Data[1] is NumericToken sizeToken)
+ {
+ currentGraphicsState.FontState.FromExtendedGraphicsState = true;
+ currentGraphicsState.FontState.FontSize = (double)sizeToken.Data;
+ ActiveExtendedGraphicsStateFont = ResourceStore.GetFontDirectly(fontReference);
+ }
+
+ if (state.TryGet(NameToken.Ais, PdfScanner, out BooleanToken aisToken))
+ {
+ // The alpha source flag (“alpha is shape”), specifying
+ // whether the current soft mask and alpha constant are to be interpreted as
+ // shape values (true) or opacity values (false).
+ currentGraphicsState.AlphaSource = aisToken.Data;
+ }
+
+ if (state.TryGet(NameToken.Ca, PdfScanner, out NumericToken caToken))
+ {
+ // (Optional; PDF 1.4) The current stroking alpha constant, specifying the constant
+ // shape or constant opacity value to be used for stroking operations in the
+ // transparent imaging model (see “Source Shape and Opacity” on page 526 and
+ // “Constant Shape and Opacity” on page 551).
+ currentGraphicsState.AlphaConstantStroking = caToken.Data;
+ }
+
+ if (state.TryGet(NameToken.CaNs, PdfScanner, out NumericToken cansToken))
+ {
+ // (Optional; PDF 1.4) The current stroking alpha constant, specifying the constant
+ // shape or constant opacity value to be used for NON-stroking operations in the
+ // transparent imaging model (see “Source Shape and Opacity” on page 526 and
+ // “Constant Shape and Opacity” on page 551).
+ currentGraphicsState.AlphaConstantNonStroking = cansToken.Data;
+ }
+
+ if (state.TryGet(NameToken.Op, PdfScanner, out BooleanToken OPToken))
+ {
+ // (Optional) A flag specifying whether to apply overprint (see Section 4.5.6,
+ // “Overprint Control”). In PDF 1.2 and earlier, there is a single overprint
+ // parameter that applies to all painting operations. Beginning with PDF 1.3,
+ // there are two separate overprint parameters: one for stroking and one for all
+ // other painting operations. Specifying an OP entry sets both parameters unless there
+ // is also an op entry in the same graphics state parameter dictionary,
+ // in which case the OP entry sets only the overprint parameter for stroking.
+ currentGraphicsState.Overprint = OPToken.Data;
+ }
+
+ if (state.TryGet(NameToken.OpNs, PdfScanner, out BooleanToken opToken))
+ {
+ // (Optional; PDF 1.3) A flag specifying whether to apply overprint (see Section
+ // 4.5.6, “Overprint Control”) for painting operations other than stroking. If
+ // this entry is absent, the OP entry, if any, sets this parameter.
+ currentGraphicsState.NonStrokingOverprint = opToken.Data;
+ }
+
+ if (state.TryGet(NameToken.Opm, PdfScanner, out NumericToken opmToken))
+ {
+ // (Optional; PDF 1.3) The overprint mode (see Section 4.5.6, “Overprint Control”).
+ currentGraphicsState.OverprintMode = opmToken.Data;
+ }
+
+ if (state.TryGet(NameToken.Sa, PdfScanner, out BooleanToken saToken))
+ {
+ // (Optional) A flag specifying whether to apply automatic stroke adjustment
+ // (see Section 6.5.4, “Automatic Stroke Adjustment”).
+ currentGraphicsState.StrokeAdjustment = saToken.Data;
+ }
+ }
+
+ ///
+ public virtual void BeginInlineImage()
+ {
+ if (InlineImageBuilder != null)
+ {
+ ParsingOptions.Logger.Error("Begin inline image (BI) command encountered while another inline image was active.");
+ }
+
+ InlineImageBuilder = new InlineImageBuilder();
+ }
+
+ ///
+ public virtual void SetInlineImageProperties(IReadOnlyDictionary properties)
+ {
+ if (InlineImageBuilder == null)
+ {
+ ParsingOptions.Logger.Error("Begin inline image data (ID) command encountered without a corresponding begin inline image (BI) command.");
+ return;
+ }
+
+ InlineImageBuilder.Properties = properties;
+ }
+
+ ///
+ public virtual void EndInlineImage(IReadOnlyList bytes)
+ {
+ if (InlineImageBuilder == null)
+ {
+ ParsingOptions.Logger.Error("End inline image (EI) command encountered without a corresponding begin inline image (BI) command.");
+ return;
+ }
+
+ InlineImageBuilder.Bytes = bytes;
+
+ var image = InlineImageBuilder.CreateInlineImage(CurrentTransformationMatrix, FilterProvider, PdfScanner, GetCurrentState().RenderingIntent, ResourceStore);
+
+ RenderInlineImage(image);
+
+ InlineImageBuilder = null;
+ }
+
+ ///
+ /// Render Inline image implementation.
+ ///
+ public abstract void RenderInlineImage(InlineImage inlineImage);
+
+ ///
+ public abstract void BeginMarkedContent(NameToken name, NameToken propertyDictionaryName, DictionaryToken properties);
+
+ ///
+ public abstract void EndMarkedContent();
+
+ private void AdjustTextMatrix(double tx, double ty)
+ {
+ var matrix = TransformationMatrix.GetTranslationMatrix(tx, ty);
+ TextMatrices.TextMatrix = matrix.Multiply(TextMatrices.TextMatrix);
+ }
+
+ ///
+ public virtual void SetFlatnessTolerance(decimal tolerance)
+ {
+ GetCurrentState().Flatness = tolerance;
+ }
+
+ ///
+ public virtual void SetLineCap(LineCapStyle cap)
+ {
+ GetCurrentState().CapStyle = cap;
+ }
+
+ ///
+ public virtual void SetLineDashPattern(LineDashPattern pattern)
+ {
+ GetCurrentState().LineDashPattern = pattern;
+ }
+
+ ///
+ public virtual void SetLineJoin(LineJoinStyle join)
+ {
+ GetCurrentState().JoinStyle = join;
+ }
+
+ ///
+ public virtual void SetLineWidth(decimal width)
+ {
+ GetCurrentState().LineWidth = width;
+ }
+
+ ///
+ public virtual void SetMiterLimit(decimal limit)
+ {
+ GetCurrentState().MiterLimit = limit;
+ }
+
+ ///
+ public virtual void MoveToNextLineWithOffset()
+ {
+ var tdOperation = new MoveToNextLineWithOffset(0, -1 * (decimal)GetCurrentState().FontState.Leading);
+ tdOperation.Run(this);
+ }
+
+ ///
+ public virtual void SetFontAndSize(NameToken font, double size)
+ {
+ var currentState = GetCurrentState();
+ currentState.FontState.FontSize = size;
+ currentState.FontState.FontName = font;
+ }
+
+ ///
+ public virtual void SetHorizontalScaling(double scale)
+ {
+ GetCurrentState().FontState.HorizontalScaling = scale;
+ }
+
+ ///
+ public virtual void SetTextLeading(double leading)
+ {
+ GetCurrentState().FontState.Leading = leading;
+ }
+
+ ///
+ public virtual void SetTextRenderingMode(TextRenderingMode mode)
+ {
+ GetCurrentState().FontState.TextRenderingMode = mode;
+ }
+
+ ///
+ public virtual void SetTextRise(double rise)
+ {
+ GetCurrentState().FontState.Rise = rise;
+ }
+
+ ///
+ public virtual void SetWordSpacing(double spacing)
+ {
+ GetCurrentState().FontState.WordSpacing = spacing;
+ }
+
+ ///
+ public virtual void ModifyCurrentTransformationMatrix(double[] value)
+ {
+ var ctm = GetCurrentState().CurrentTransformationMatrix;
+ GetCurrentState().CurrentTransformationMatrix = TransformationMatrix.FromArray(value).Multiply(ctm);
+ }
+
+ ///
+ public virtual void SetCharacterSpacing(double spacing)
+ {
+ GetCurrentState().FontState.CharacterSpacing = spacing;
+ }
+
+ ///
+ public abstract void PaintShading(NameToken shadingName);
+ }
+}
diff --git a/src/UglyToad.PdfPig/Graphics/Colors/ResourceColorSpace.cs b/src/UglyToad.PdfPig/Graphics/Colors/ResourceColorSpace.cs
index 57580f70d..8d0cafe8f 100644
--- a/src/UglyToad.PdfPig/Graphics/Colors/ResourceColorSpace.cs
+++ b/src/UglyToad.PdfPig/Graphics/Colors/ResourceColorSpace.cs
@@ -5,18 +5,24 @@
///
/// A color space definition from a resource dictionary.
///
- internal struct ResourceColorSpace
+ public readonly struct ResourceColorSpace
{
+ ///
+ /// The color space name.
+ ///
public NameToken Name { get; }
+ ///
+ /// The color space data.
+ ///
public IToken Data { get; }
- public ResourceColorSpace(NameToken name, IToken data)
+ internal ResourceColorSpace(NameToken name, IToken data)
{
Name = name;
Data = data;
}
- public ResourceColorSpace(NameToken name) : this(name, null) { }
+ internal ResourceColorSpace(NameToken name) : this(name, null) { }
}
}
diff --git a/src/UglyToad.PdfPig/Graphics/ContentStreamProcessor.cs b/src/UglyToad.PdfPig/Graphics/ContentStreamProcessor.cs
index a33b02186..b3d9485f2 100644
--- a/src/UglyToad.PdfPig/Graphics/ContentStreamProcessor.cs
+++ b/src/UglyToad.PdfPig/Graphics/ContentStreamProcessor.cs
@@ -2,26 +2,20 @@
{
using Colors;
using Content;
- using Core;
using Filters;
using Geometry;
- using Logging;
using Operations;
using Parser;
using PdfFonts;
using PdfPig.Core;
using System;
using System.Collections.Generic;
- using System.Diagnostics;
- using System.Linq;
using Tokenization.Scanner;
using Tokens;
- using Operations.TextPositioning;
using Util;
- using XObjects;
using static PdfPig.Core.PdfSubpath;
- internal class ContentStreamProcessor : IOperationContext
+ internal class ContentStreamProcessor : BaseStreamProcessor
{
///
/// Stores each letter as it is encountered in the content stream.
@@ -43,45 +37,12 @@ internal class ContentStreamProcessor : IOperationContext
///
private readonly List markedContents = new List();
- private readonly IResourceStore resourceStore;
- private readonly UserSpaceUnit userSpaceUnit;
- private readonly PageRotationDegrees rotation;
- private readonly IPdfTokenScanner pdfScanner;
- private readonly IPageContentParser pageContentParser;
- private readonly ILookupFilterProvider filterProvider;
- private readonly InternalParsingOptions parsingOptions;
private readonly MarkedContentStack markedContentStack = new MarkedContentStack();
- private Stack graphicsStack = new Stack();
- private IFont activeExtendedGraphicsStateFont;
- private InlineImageBuilder inlineImageBuilder;
- private int pageNumber;
-
- ///
- /// A counter to track individual calls to operations used to determine if letters are likely to be
- /// in the same word/group. This exposes internal grouping of letters used by the PDF creator which may correspond to the
- /// intended grouping of letters into words.
- ///
- private int textSequence;
-
- public TextMatrices TextMatrices { get; } = new TextMatrices();
-
- public TransformationMatrix CurrentTransformationMatrix => GetCurrentState().CurrentTransformationMatrix;
-
public PdfSubpath CurrentSubpath { get; private set; }
public PdfPath CurrentPath { get; private set; }
- public PdfPoint CurrentPosition { get; set; }
-
- public int StackSize => graphicsStack.Count;
-
- private readonly Dictionary> xObjects = new Dictionary>
- {
- {XObjectType.Image, new List()},
- {XObjectType.PostScript, new List()}
- };
-
public ContentStreamProcessor(
int pageNumber,
IResourceStore resourceStore,
@@ -92,557 +53,86 @@ public ContentStreamProcessor(
IPdfTokenScanner pdfScanner,
IPageContentParser pageContentParser,
ILookupFilterProvider filterProvider,
- InternalParsingOptions parsingOptions)
- {
- this.pageNumber = pageNumber;
- this.resourceStore = resourceStore;
- this.userSpaceUnit = userSpaceUnit;
- this.rotation = rotation;
- this.pdfScanner = pdfScanner ?? throw new ArgumentNullException(nameof(pdfScanner));
- this.pageContentParser = pageContentParser ?? throw new ArgumentNullException(nameof(pageContentParser));
- this.filterProvider = filterProvider ?? throw new ArgumentNullException(nameof(filterProvider));
- this.parsingOptions = parsingOptions;
-
- TransformationMatrix initialMatrix = GetInitialMatrix(userSpaceUnit, mediaBox, cropBox, rotation, parsingOptions.Logger);
-
- graphicsStack.Push(new CurrentGraphicsState()
- {
- CurrentTransformationMatrix = initialMatrix,
- CurrentClippingPath = GetInitialClipping(cropBox, initialMatrix),
- ColorSpaceContext = new ColorSpaceContext(GetCurrentState, resourceStore)
- });
- }
+ IParsingOptions parsingOptions)
+ : base(pageNumber, resourceStore, userSpaceUnit, mediaBox, cropBox, rotation, pdfScanner, pageContentParser, filterProvider, parsingOptions)
+ { }
- ///
- /// Get the initial clipping path using the crop box and the initial transformation matrix.
- ///
- private static PdfPath GetInitialClipping(CropBox cropBox, TransformationMatrix initialMatrix)
+ public override PageContent Process(int pageNumberCurrent, IReadOnlyList operations)
{
- var transformedCropBox = initialMatrix.Transform(cropBox.Bounds);
-
- // We re-compute width and height to get possible negative values.
- double width = transformedCropBox.TopRight.X - transformedCropBox.BottomLeft.X;
- double height = transformedCropBox.TopRight.Y - transformedCropBox.BottomLeft.Y;
-
- // initiate CurrentClippingPath to cropBox
- var clippingSubpath = new PdfSubpath();
- clippingSubpath.Rectangle(transformedCropBox.BottomLeft.X, transformedCropBox.BottomLeft.Y, width, height);
- var clippingPath = new PdfPath() { clippingSubpath };
- clippingPath.SetClipping(FillingRule.EvenOdd);
- return clippingPath;
- }
-
- [System.Diagnostics.Contracts.Pure]
- internal static TransformationMatrix GetInitialMatrix(UserSpaceUnit userSpaceUnit,
- MediaBox mediaBox,
- CropBox cropBox,
- PageRotationDegrees rotation,
- ILog log)
- {
- // 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.Bounds.Intersect(cropBox.Bounds) ?? cropBox.Bounds;
-
- if (rotation.Value == 0
- && viewBox.Left == 0
- && viewBox.Bottom == 0
- && userSpaceUnit.PointMultiples == 1)
- {
- return TransformationMatrix.Identity;
- }
-
- // Move points so that (0,0) is equal to the viewbox bottom left corner.
- var t1 = TransformationMatrix.GetTranslationMatrix(-viewBox.Left, -viewBox.Bottom);
-
- if (userSpaceUnit.PointMultiples != 1)
- {
- log.Warn("User space unit other than 1 is not implemented");
- }
-
- // After rotating around the origin, our points will have negative x/y coordinates.
- // Fix this by translating them by a certain dx/dy after rotation based on the viewbox.
- double dx, dy;
- switch (rotation.Value)
- {
- case 0:
- // No need to rotate / translate after rotation, just return the initial
- // translation matrix.
- return t1;
- case 90:
- // Move rotated points up by our (unrotated) viewbox width
- dx = 0;
- dy = viewBox.Width;
- break;
- case 180:
- // Move rotated points up/right using the (unrotated) viewbox width/height
- dx = viewBox.Width;
- dy = viewBox.Height;
- break;
- case 270:
- // Move rotated points right using the (unrotated) viewbox height
- dx = viewBox.Height;
- dy = 0;
- break;
- default:
- throw new InvalidOperationException($"Invalid value for page rotation: {rotation.Value}.");
- }
-
- // GetRotationMatrix uses counter clockwise angles, whereas our page rotation
- // is a clockwise angle, so flip the sign.
- var r = TransformationMatrix.GetRotationMatrix(-rotation.Value);
-
- // Fix up negative coordinates after rotation
- var t2 = TransformationMatrix.GetTranslationMatrix(dx, dy);
-
- // Now get the final combined matrix T1 > R > T2
- return t1.Multiply(r.Multiply(t2));
- }
-
- public PageContent Process(int pageNumberCurrent, IReadOnlyList operations)
- {
- pageNumber = pageNumberCurrent;
+ PageNumber = pageNumberCurrent;
CloneAllStates();
ProcessOperations(operations);
- return new PageContent(operations, letters, paths, images, markedContents, pdfScanner, filterProvider, resourceStore);
+ return new PageContent(operations, letters, paths, images, markedContents, PdfScanner, FilterProvider, ResourceStore);
}
- private void ProcessOperations(IReadOnlyList operations)
+ public override void RenderGlyph(IFont font, IColor strokingColor, IColor nonStrokingColor, TextRenderingMode textRenderingMode, double fontSize, double pointSize, int code, string unicode,
+ long currentOffset, TransformationMatrix renderingMatrix, TransformationMatrix textMatrix, TransformationMatrix transformationMatrix, CharacterBoundingBox characterBoundingBox)
{
- foreach (var stateOperation in operations)
- {
- stateOperation.Run(this);
- }
- }
-
- private Stack CloneAllStates()
- {
- var saved = graphicsStack;
- graphicsStack = new Stack();
- graphicsStack.Push(saved.Peek().DeepClone());
- return saved;
- }
-
- [DebuggerStepThrough]
- public CurrentGraphicsState GetCurrentState()
- {
- return graphicsStack.Peek();
- }
-
- public void PopState()
- {
- graphicsStack.Pop();
- activeExtendedGraphicsStateFont = null;
- }
-
- public void PushState()
- {
- graphicsStack.Push(graphicsStack.Peek().DeepClone());
- }
-
- public void ShowText(IInputBytes bytes)
- {
- var currentState = GetCurrentState();
-
- var font = currentState.FontState.FromExtendedGraphicsState ? activeExtendedGraphicsStateFont : resourceStore.GetFont(currentState.FontState.FontName);
+ var transformedGlyphBounds = PerformantRectangleTransformer
+ .Transform(renderingMatrix, textMatrix, transformationMatrix, characterBoundingBox.GlyphBounds);
- if (font == null)
- {
- if (parsingOptions.SkipMissingFonts)
- {
- parsingOptions.Logger.Warn($"Skipping a missing font with name {currentState.FontState.FontName} " +
- $"since it is not present in the document and {nameof(InternalParsingOptions.SkipMissingFonts)} " +
- "is set to true. This may result in some text being skipped and not included in the output.");
-
- return;
- }
-
- throw new InvalidOperationException($"Could not find the font with name {currentState.FontState.FontName} in the resource store. It has not been loaded yet.");
- }
-
- var fontSize = currentState.FontState.FontSize;
- var horizontalScaling = currentState.FontState.HorizontalScaling / 100.0;
- var characterSpacing = currentState.FontState.CharacterSpacing;
- var rise = currentState.FontState.Rise;
-
- var transformationMatrix = currentState.CurrentTransformationMatrix;
-
- var renderingMatrix =
- TransformationMatrix.FromValues(fontSize * horizontalScaling, 0, 0, fontSize, 0, rise);
+ var transformedPdfBounds = PerformantRectangleTransformer
+ .Transform(renderingMatrix, textMatrix, transformationMatrix, new PdfRectangle(0, 0, characterBoundingBox.Width, 0));
- var pointSize = Math.Round(transformationMatrix.Multiply(TextMatrices.TextMatrix).Transform(new PdfRectangle(0, 0, 1, fontSize)).Height, 2);
-
- while (bytes.MoveNext())
+ Letter letter = null;
+ if (Diacritics.IsInCombiningDiacriticRange(unicode) && currentOffset > 0 && letters.Count > 0)
{
- var code = font.ReadCharacterCode(bytes, out int codeLength);
-
- var foundUnicode = font.TryGetUnicode(code, out var unicode);
+ var attachTo = letters[letters.Count - 1];
- if (!foundUnicode || unicode == null)
+ if (attachTo.TextSequence == TextSequence
+ && Diacritics.TryCombineDiacriticWithPreviousLetter(unicode, attachTo.Value, out var newLetter))
{
- parsingOptions.Logger.Warn($"We could not find the corresponding character with code {code} in font {font.Name}.");
+ // TODO: union of bounding boxes.
+ letters.Remove(attachTo);
- // Try casting directly to string as in PDFBox 1.8.
- unicode = new string((char)code, 1);
- }
-
- var wordSpacing = 0.0;
- if (code == ' ' && codeLength == 1)
- {
- wordSpacing += GetCurrentState().FontState.WordSpacing;
- }
-
- var textMatrix = TextMatrices.TextMatrix;
-
- if (font.IsVertical)
- {
- if (!(font is IVerticalWritingSupported verticalFont))
- {
- throw new InvalidOperationException($"Font {font.Name} was in vertical writing mode but did not implement {nameof(IVerticalWritingSupported)}.");
- }
-
- var positionVector = verticalFont.GetPositionVector(code);
-
- textMatrix = textMatrix.Translate(positionVector.X, positionVector.Y);
- }
-
- var boundingBox = font.GetBoundingBox(code);
-
- var transformedGlyphBounds = PerformantRectangleTransformer
- .Transform(renderingMatrix, textMatrix, transformationMatrix, boundingBox.GlyphBounds);
-
- var transformedPdfBounds = PerformantRectangleTransformer
- .Transform(renderingMatrix, textMatrix, transformationMatrix, new PdfRectangle(0, 0, boundingBox.Width, 0));
-
-
- Letter letter = null;
- if (Diacritics.IsInCombiningDiacriticRange(unicode) && bytes.CurrentOffset > 0 && letters.Count > 0)
- {
- var attachTo = letters[letters.Count - 1];
-
- if (attachTo.TextSequence == textSequence
- && Diacritics.TryCombineDiacriticWithPreviousLetter(unicode, attachTo.Value, out var newLetter))
- {
- // TODO: union of bounding boxes.
- letters.Remove(attachTo);
-
- letter = new Letter(
- newLetter,
- attachTo.GlyphRectangle,
- attachTo.StartBaseLine,
- attachTo.EndBaseLine,
- attachTo.Width,
- attachTo.FontSize,
- attachTo.Font,
- attachTo.RenderingMode,
- attachTo.StrokeColor,
- attachTo.FillColor,
- attachTo.PointSize,
- attachTo.TextSequence);
- }
- }
-
- // If we did not create a letter for a combined diacritic, create one here.
- if (letter == null)
- {
letter = new Letter(
- unicode,
- transformedGlyphBounds,
- transformedPdfBounds.BottomLeft,
- transformedPdfBounds.BottomRight,
- transformedPdfBounds.Width,
- fontSize,
- font.Details,
- currentState.FontState.TextRenderingMode,
- currentState.CurrentStrokingColor,
- currentState.CurrentNonStrokingColor,
- pointSize,
- textSequence);
- }
-
- letters.Add(letter);
-
- markedContentStack.AddLetter(letter);
-
- double tx, ty;
- if (font.IsVertical)
- {
- var verticalFont = (IVerticalWritingSupported)font;
- var displacement = verticalFont.GetDisplacementVector(code);
- tx = 0;
- ty = (displacement.Y * fontSize) + characterSpacing + wordSpacing;
- }
- else
- {
- tx = (boundingBox.Width * fontSize + characterSpacing + wordSpacing) * horizontalScaling;
- ty = 0;
+ newLetter,
+ attachTo.GlyphRectangle,
+ attachTo.StartBaseLine,
+ attachTo.EndBaseLine,
+ attachTo.Width,
+ attachTo.FontSize,
+ attachTo.Font,
+ attachTo.RenderingMode,
+ attachTo.StrokeColor,
+ attachTo.FillColor,
+ attachTo.PointSize,
+ attachTo.TextSequence);
}
-
- TextMatrices.TextMatrix = TextMatrices.TextMatrix.Translate(tx, ty);
}
- }
-
- public void ShowPositionedText(IReadOnlyList tokens)
- {
- textSequence++;
-
- var currentState = GetCurrentState();
-
- var textState = currentState.FontState;
-
- var fontSize = textState.FontSize;
- var horizontalScaling = textState.HorizontalScaling / 100.0;
- var font = resourceStore.GetFont(textState.FontName);
- if (font == null)
+ // If we did not create a letter for a combined diacritic, create one here.
+ if (letter == null)
{
- if (parsingOptions.SkipMissingFonts)
- {
- parsingOptions.Logger.Warn($"Skipping a missing font with name {currentState.FontState.FontName} " +
- $"since it is not present in the document and {nameof(InternalParsingOptions.SkipMissingFonts)} " +
- "is set to true. This may result in some text being skipped and not included in the output.");
-
- return;
- }
-
- throw new InvalidOperationException($"Could not find the font with name {currentState.FontState.FontName} in the resource store. It has not been loaded yet.");
+ letter = new Letter(
+ unicode,
+ transformedGlyphBounds,
+ transformedPdfBounds.BottomLeft,
+ transformedPdfBounds.BottomRight,
+ transformedPdfBounds.Width,
+ fontSize,
+ font.Details,
+ textRenderingMode,
+ strokingColor,
+ nonStrokingColor,
+ pointSize,
+ TextSequence);
}
- var isVertical = font.IsVertical;
+ letters.Add(letter);
- foreach (var token in tokens)
- {
- if (token is NumericToken number)
- {
- var positionAdjustment = (double)number.Data;
-
- double tx, ty;
- if (isVertical)
- {
- tx = 0;
- ty = -positionAdjustment / 1000 * fontSize;
- }
- else
- {
- tx = -positionAdjustment / 1000 * fontSize * horizontalScaling;
- ty = 0;
- }
-
- AdjustTextMatrix(tx, ty);
- }
- else
- {
- IReadOnlyList bytes;
- if (token is HexToken hex)
- {
- bytes = hex.Bytes;
- }
- else
- {
- bytes = OtherEncodings.StringAsLatin1Bytes(((StringToken)token).Data);
- }
-
- ShowText(new ByteArrayInputBytes(bytes));
- }
- }
+ markedContentStack.AddLetter(letter);
}
- public void ApplyXObject(NameToken xObjectName)
+ public override void RenderXObjectImage(XObjectContentRecord xObjectContentRecord)
{
- if (!resourceStore.TryGetXObject(xObjectName, out var xObjectStream))
- {
- if (parsingOptions.SkipMissingFonts)
- {
- return;
- }
-
- throw new PdfDocumentFormatException($"No XObject with name {xObjectName} found on page {pageNumber}.");
- }
-
- // For now we will determine the type and store the object with the graphics state information preceding it.
- // Then consumers of the page can request the object(s) to be retrieved by type.
- var subType = (NameToken)xObjectStream.StreamDictionary.Data[NameToken.Subtype.Data];
-
- var state = GetCurrentState();
-
- var matrix = state.CurrentTransformationMatrix;
-
- if (subType.Equals(NameToken.Ps))
- {
- var contentRecord = new XObjectContentRecord(XObjectType.PostScript, xObjectStream, matrix, state.RenderingIntent,
- state.ColorSpaceContext?.CurrentStrokingColorSpace ?? DeviceRgbColorSpaceDetails.Instance);
-
- xObjects[XObjectType.PostScript].Add(contentRecord);
- }
- else if (subType.Equals(NameToken.Image))
- {
- var contentRecord = new XObjectContentRecord(XObjectType.Image, xObjectStream, matrix, state.RenderingIntent,
- state.ColorSpaceContext?.CurrentStrokingColorSpace ?? DeviceRgbColorSpaceDetails.Instance);
-
- images.Add(Union.One(contentRecord));
-
- markedContentStack.AddXObject(contentRecord, pdfScanner, filterProvider, resourceStore);
- }
- else if (subType.Equals(NameToken.Form))
- {
- ProcessFormXObject(xObjectStream, xObjectName);
- }
- else
- {
- throw new InvalidOperationException($"XObject encountered with unexpected SubType {subType}. {xObjectStream.StreamDictionary}.");
- }
+ images.Add(Union.One(xObjectContentRecord));
+ markedContentStack.AddXObject(xObjectContentRecord, PdfScanner, FilterProvider, ResourceStore);
}
- private void ProcessFormXObject(StreamToken formStream, NameToken xObjectName)
- {
- /*
- * When a form XObject is invoked the following should happen:
- *
- * 1. Save the current graphics state, as if by invoking the q operator.
- * 2. Concatenate the matrix from the form dictionary's Matrix entry with the current transformation matrix.
- * 3. Clip according to the form dictionary's BBox entry.
- * 4. Paint the graphics objects specified in the form's content stream.
- * 5. Restore the saved graphics state, as if by invoking the Q operator.
- */
-
- var hasResources = formStream.StreamDictionary.TryGet(NameToken.Resources, pdfScanner, out var formResources);
- if (hasResources)
- {
- resourceStore.LoadResourceDictionary(formResources, parsingOptions);
- }
-
- // 1. Save current state.
- PushState();
-
- var startState = GetCurrentState();
-
- // Transparency Group XObjects
- if (formStream.StreamDictionary.TryGet(NameToken.Group, pdfScanner, out DictionaryToken formGroupToken))
- {
- if (!formGroupToken.TryGet(NameToken.S, pdfScanner, out var sToken) || sToken != NameToken.Transparency)
- {
- throw new InvalidOperationException($"Invalid Transparency Group XObject, '{NameToken.S}' token is not set or not equal to '{NameToken.Transparency}'.");
- }
-
- /* blend mode
- * A conforming reader shall implicitly reset this parameter to its initial value at the beginning of execution of a
- * transparency group XObject (see 11.6.6, "Transparency Group XObjects"). Initial value: Normal.
- */
- //startState.BlendMode = BlendMode.Normal;
-
- /* soft mask
- * A conforming reader shall implicitly reset this parameter implicitly reset to its initial value at the beginning
- * of execution of a transparency group XObject (see 11.6.6, "Transparency Group XObjects"). Initial value: None.
- */
- // TODO
-
- /* alpha constant
- * A conforming reader shall implicitly reset this parameter to its initial value at the beginning of execution of a
- * transparency group XObject (see 11.6.6, "Transparency Group XObjects"). Initial value: 1.0.
- */
- startState.AlphaConstantNonStroking = 1.0m;
- startState.AlphaConstantStroking = 1.0m;
-
- if (formGroupToken.TryGet(NameToken.Cs, pdfScanner, out NameToken csNameToken))
- {
- startState.ColorSpaceContext.SetNonStrokingColorspace(csNameToken);
- }
- else if (formGroupToken.TryGet(NameToken.Cs, pdfScanner, out ArrayToken csArrayToken)
- && csArrayToken.Length > 0)
- {
- if (csArrayToken.Data[0] is NameToken firstColorSpaceName)
- {
- startState.ColorSpaceContext.SetNonStrokingColorspace(firstColorSpaceName, formGroupToken);
- }
- else
- {
- throw new InvalidOperationException("Invalid color space in Transparency Group XObjects.");
- }
- }
-
- bool isolated = false;
- if (formGroupToken.TryGet(NameToken.I, pdfScanner, out BooleanToken isolatedToken))
- {
- /*
- * (Optional) A flag specifying whether the transparency group is isolated (see “Isolated Groups”).
- * If this flag is true, objects within the group shall be composited against a fully transparent
- * initial backdrop; if false, they shall be composited against the group’s backdrop.
- * Default value: false.
- */
- isolated = isolatedToken.Data;
- }
-
- bool knockout = false;
- if (formGroupToken.TryGet(NameToken.K, pdfScanner, out BooleanToken knockoutToken))
- {
- /*
- * (Optional) A flag specifying whether the transparency group is a knockout group (see “Knockout Groups”).
- * If this flag is false, later objects within the group shall be composited with earlier ones with which
- * they overlap; if true, they shall be composited with the group’s initial backdrop and shall overwrite
- * (“knock out”) any earlier overlapping objects.
- * Default value: false.
- */
- knockout = knockoutToken.Data;
- }
- }
-
- var formMatrix = TransformationMatrix.Identity;
- if (formStream.StreamDictionary.TryGet(NameToken.Matrix, pdfScanner, out var formMatrixToken))
- {
- formMatrix = TransformationMatrix.FromArray(formMatrixToken.Data.OfType().Select(x => x.Double).ToArray());
- }
-
- // 2. Update current transformation matrix.
- startState.CurrentTransformationMatrix = formMatrix.Multiply(startState.CurrentTransformationMatrix);
-
- var contentStream = formStream.Decode(filterProvider, pdfScanner);
-
- var operations = pageContentParser.Parse(pageNumber, new ByteArrayInputBytes(contentStream), parsingOptions.Logger);
-
- // 3. We don't respect clipping currently.
-
- // 4. Paint the objects.
- bool hasCircularReference = HasFormXObjectCircularReference(formStream, xObjectName, operations);
- if (hasCircularReference)
- {
- if (parsingOptions.UseLenientParsing)
- {
- operations = operations.Where(o => o is not InvokeNamedXObject xo || xo.Name != xObjectName).ToArray();
- parsingOptions.Logger.Warn($"An XObject form named '{xObjectName}' is referencing itself which can cause unexpected behaviour. The self reference was removed from the operations before further processing.");
- }
- else
- {
- throw new PdfDocumentFormatException($"An XObject form named '{xObjectName}' is referencing itself which can cause unexpected behaviour.");
- }
- }
-
- ProcessOperations(operations);
-
- // 5. Restore saved state.
- PopState();
-
- if (hasResources)
- {
- resourceStore.UnloadResourceDictionary();
- }
- }
-
- ///
- /// Check for circular reference in the XObject form.
- ///
- /// The original form stream.
- /// The form's name.
- /// The form operations parsed from original form stream.
- private bool HasFormXObjectCircularReference(StreamToken formStream, NameToken xObjectName, IReadOnlyList operations)
- {
- return xObjectName != null
- && operations.OfType()?.Any(o => o.Name == xObjectName) == true // operations contain another form with same name
- && resourceStore.TryGetXObject(xObjectName, out var result)
- && result.Data.SequenceEqual(formStream.Data); // The form contained in the operations has identical data to current form
- }
-
- public void BeginSubpath()
+ public override void BeginSubpath()
{
if (CurrentPath == null)
{
@@ -653,7 +143,7 @@ public void BeginSubpath()
CurrentSubpath = new PdfSubpath();
}
- public PdfPoint? CloseSubpath()
+ public override PdfPoint? CloseSubpath()
{
if (CurrentSubpath == null)
{
@@ -686,7 +176,7 @@ public void AddCurrentSubpath()
CurrentSubpath = null;
}
- public void StrokePath(bool close)
+ public override void StrokePath(bool close)
{
if (CurrentPath == null)
{
@@ -703,7 +193,7 @@ public void StrokePath(bool close)
ClosePath();
}
- public void FillPath(FillingRule fillingRule, bool close)
+ public override void FillPath(FillingRule fillingRule, bool close)
{
if (CurrentPath == null)
{
@@ -720,7 +210,7 @@ public void FillPath(FillingRule fillingRule, bool close)
ClosePath();
}
- public void FillStrokePath(FillingRule fillingRule, bool close)
+ public override void FillStrokePath(FillingRule fillingRule, bool close)
{
if (CurrentPath == null)
{
@@ -738,7 +228,7 @@ public void FillStrokePath(FillingRule fillingRule, bool close)
ClosePath();
}
- public void MoveTo(double x, double y)
+ public override void MoveTo(double x, double y)
{
BeginSubpath();
var point = CurrentTransformationMatrix.Transform(new PdfPoint(x, y));
@@ -746,7 +236,7 @@ public void MoveTo(double x, double y)
CurrentSubpath.MoveTo(point.X, point.Y);
}
- public void BezierCurveTo(double x2, double y2, double x3, double y3)
+ public override void BezierCurveTo(double x2, double y2, double x3, double y3)
{
if (CurrentSubpath == null)
{
@@ -760,7 +250,7 @@ public void BezierCurveTo(double x2, double y2, double x3, double y3)
CurrentPosition = end;
}
- public void BezierCurveTo(double x1, double y1, double x2, double y2, double x3, double y3)
+ public override void BezierCurveTo(double x1, double y1, double x2, double y2, double x3, double y3)
{
if (CurrentSubpath == null)
{
@@ -775,7 +265,7 @@ public void BezierCurveTo(double x1, double y1, double x2, double y2, double x3,
CurrentPosition = end;
}
- public void LineTo(double x, double y)
+ public override void LineTo(double x, double y)
{
if (CurrentSubpath == null)
{
@@ -788,7 +278,7 @@ public void LineTo(double x, double y)
CurrentPosition = endPoint;
}
- public void Rectangle(double x, double y, double width, double height)
+ public override void Rectangle(double x, double y, double width, double height)
{
BeginSubpath();
var lowerLeft = CurrentTransformationMatrix.Transform(new PdfPoint(x, y));
@@ -798,7 +288,7 @@ public void Rectangle(double x, double y, double width, double height)
AddCurrentSubpath();
}
- public void EndPath()
+ public override void EndPath()
{
if (CurrentPath == null)
{
@@ -809,7 +299,7 @@ public void EndPath()
if (CurrentPath.IsClipping)
{
- if (!parsingOptions.ClipPaths)
+ if (!ParsingOptions.ClipPaths)
{
// if we don't clip paths, add clipping path to paths
paths.Add(CurrentPath);
@@ -824,7 +314,7 @@ public void EndPath()
CurrentPath = null;
}
- public void ClosePath()
+ public override void ClosePath()
{
AddCurrentSubpath();
@@ -849,9 +339,9 @@ public void ClosePath()
CurrentPath.FillColor = currentState.CurrentNonStrokingColor;
}
- if (parsingOptions.ClipPaths)
+ if (ParsingOptions.ClipPaths)
{
- var clippedPath = currentState.CurrentClippingPath.Clip(CurrentPath, parsingOptions.Logger);
+ var clippedPath = currentState.CurrentClippingPath.Clip(CurrentPath, ParsingOptions.Logger);
if (clippedPath != null)
{
paths.Add(clippedPath);
@@ -867,7 +357,7 @@ public void ClosePath()
CurrentPath = null;
}
- public void ModifyClippingIntersect(FillingRule clippingRule)
+ public override void ModifyClippingIntersect(FillingRule clippingRule)
{
if (CurrentPath == null)
{
@@ -877,15 +367,15 @@ public void ModifyClippingIntersect(FillingRule clippingRule)
AddCurrentSubpath();
CurrentPath.SetClipping(clippingRule);
- if (parsingOptions.ClipPaths)
+ if (ParsingOptions.ClipPaths)
{
var currentClipping = GetCurrentState().CurrentClippingPath;
currentClipping.SetClipping(clippingRule);
- var newClippings = CurrentPath.Clip(currentClipping, parsingOptions.Logger);
+ var newClippings = CurrentPath.Clip(currentClipping, ParsingOptions.Logger);
if (newClippings == null)
{
- parsingOptions.Logger.Warn("Empty clipping path found. Clipping path not updated.");
+ ParsingOptions.Logger.Warn("Empty clipping path found. Clipping path not updated.");
}
else
{
@@ -894,140 +384,17 @@ public void ModifyClippingIntersect(FillingRule clippingRule)
}
}
- public void SetNamedGraphicsState(NameToken stateName)
- {
- var currentGraphicsState = GetCurrentState();
-
- var state = resourceStore.GetExtendedGraphicsStateDictionary(stateName);
-
- if (state.TryGet(NameToken.Lw, pdfScanner, out NumericToken lwToken))
- {
- currentGraphicsState.LineWidth = lwToken.Data;
- }
-
- if (state.TryGet(NameToken.Lc, pdfScanner, out NumericToken lcToken))
- {
- currentGraphicsState.CapStyle = (LineCapStyle)lcToken.Int;
- }
-
- if (state.TryGet(NameToken.Lj, pdfScanner, out NumericToken ljToken))
- {
- currentGraphicsState.JoinStyle = (LineJoinStyle)ljToken.Int;
- }
-
- if (state.TryGet(NameToken.Font, pdfScanner, out ArrayToken fontArray) && fontArray.Length == 2
- && fontArray.Data[0] is IndirectReferenceToken fontReference && fontArray.Data[1] is NumericToken sizeToken)
- {
- currentGraphicsState.FontState.FromExtendedGraphicsState = true;
- currentGraphicsState.FontState.FontSize = (double)sizeToken.Data;
- activeExtendedGraphicsStateFont = resourceStore.GetFontDirectly(fontReference);
- }
-
- if (state.TryGet(NameToken.Ais, pdfScanner, out BooleanToken aisToken))
- {
- // The alpha source flag (“alpha is shape”), specifying
- // whether the current soft mask and alpha constant are to be interpreted as
- // shape values (true) or opacity values (false).
- currentGraphicsState.AlphaSource = aisToken.Data;
- }
-
- if (state.TryGet(NameToken.Ca, pdfScanner, out NumericToken caToken))
- {
- // (Optional; PDF 1.4) The current stroking alpha constant, specifying the constant
- // shape or constant opacity value to be used for stroking operations in the
- // transparent imaging model (see “Source Shape and Opacity” on page 526 and
- // “Constant Shape and Opacity” on page 551).
- currentGraphicsState.AlphaConstantStroking = caToken.Data;
- }
-
- if (state.TryGet(NameToken.CaNs, pdfScanner, out NumericToken cansToken))
- {
- // (Optional; PDF 1.4) The current stroking alpha constant, specifying the constant
- // shape or constant opacity value to be used for NON-stroking operations in the
- // transparent imaging model (see “Source Shape and Opacity” on page 526 and
- // “Constant Shape and Opacity” on page 551).
- currentGraphicsState.AlphaConstantNonStroking = cansToken.Data;
- }
-
- if (state.TryGet(NameToken.Op, pdfScanner, out BooleanToken OPToken))
- {
- // (Optional) A flag specifying whether to apply overprint (see Section 4.5.6,
- // “Overprint Control”). In PDF 1.2 and earlier, there is a single overprint
- // parameter that applies to all painting operations. Beginning with PDF 1.3,
- // there are two separate overprint parameters: one for stroking and one for all
- // other painting operations. Specifying an OP entry sets both parameters unless there
- // is also an op entry in the same graphics state parameter dictionary,
- // in which case the OP entry sets only the overprint parameter for stroking.
- currentGraphicsState.Overprint = OPToken.Data;
- }
-
- if (state.TryGet(NameToken.OpNs, pdfScanner, out BooleanToken opToken))
- {
- // (Optional; PDF 1.3) A flag specifying whether to apply overprint (see Section
- // 4.5.6, “Overprint Control”) for painting operations other than stroking. If
- // this entry is absent, the OP entry, if any, sets this parameter.
- currentGraphicsState.NonStrokingOverprint = opToken.Data;
- }
-
- if (state.TryGet(NameToken.Opm, pdfScanner, out NumericToken opmToken))
- {
- // (Optional; PDF 1.3) The overprint mode (see Section 4.5.6, “Overprint Control”).
- currentGraphicsState.OverprintMode = opmToken.Data;
- }
-
- if (state.TryGet(NameToken.Sa, pdfScanner, out BooleanToken saToken))
- {
- // (Optional) A flag specifying whether to apply automatic stroke adjustment
- // (see Section 6.5.4, “Automatic Stroke Adjustment”).
- currentGraphicsState.StrokeAdjustment = saToken.Data;
- }
- }
-
- public void BeginInlineImage()
- {
- if (inlineImageBuilder != null)
- {
- parsingOptions.Logger.Error("Begin inline image (BI) command encountered while another inline image was active.");
- }
-
- inlineImageBuilder = new InlineImageBuilder();
- }
-
- public void SetInlineImageProperties(IReadOnlyDictionary properties)
- {
- if (inlineImageBuilder == null)
- {
- parsingOptions.Logger.Error("Begin inline image data (ID) command encountered without a corresponding begin inline image (BI) command.");
- return;
- }
-
- inlineImageBuilder.Properties = properties;
- }
-
- public void EndInlineImage(IReadOnlyList bytes)
+ public override void RenderInlineImage(InlineImage inlineImage)
{
- if (inlineImageBuilder == null)
- {
- parsingOptions.Logger.Error("End inline image (EI) command encountered without a corresponding begin inline image (BI) command.");
- return;
- }
-
- inlineImageBuilder.Bytes = bytes;
-
- var image = inlineImageBuilder.CreateInlineImage(CurrentTransformationMatrix, filterProvider, pdfScanner, GetCurrentState().RenderingIntent, resourceStore);
-
- images.Add(Union.Two(image));
-
- markedContentStack.AddImage(image);
-
- inlineImageBuilder = null;
+ images.Add(Union.Two(inlineImage));
+ markedContentStack.AddImage(inlineImage);
}
- public void BeginMarkedContent(NameToken name, NameToken propertyDictionaryName, DictionaryToken properties)
+ public override void BeginMarkedContent(NameToken name, NameToken propertyDictionaryName, DictionaryToken properties)
{
if (propertyDictionaryName != null)
{
- var actual = resourceStore.GetMarkedContentPropertiesDictionary(propertyDictionaryName);
+ var actual = ResourceStore.GetMarkedContentPropertiesDictionary(propertyDictionaryName);
properties = actual ?? properties;
}
@@ -1035,11 +402,11 @@ public void BeginMarkedContent(NameToken name, NameToken propertyDictionaryName,
markedContentStack.Push(name, properties);
}
- public void EndMarkedContent()
+ public override void EndMarkedContent()
{
if (markedContentStack.CanPop)
{
- var mc = markedContentStack.Pop(pdfScanner);
+ var mc = markedContentStack.Pop(PdfScanner);
if (mc != null)
{
markedContents.Add(mc);
@@ -1047,93 +414,7 @@ public void EndMarkedContent()
}
}
- private void AdjustTextMatrix(double tx, double ty)
- {
- var matrix = TransformationMatrix.GetTranslationMatrix(tx, ty);
-
- TextMatrices.TextMatrix = matrix.Multiply(TextMatrices.TextMatrix);
- }
-
- public void SetFlatnessTolerance(decimal tolerance)
- {
- GetCurrentState().Flatness = tolerance;
- }
-
- public void SetLineCap(LineCapStyle cap)
- {
- GetCurrentState().CapStyle = cap;
- }
-
- public void SetLineDashPattern(LineDashPattern pattern)
- {
- GetCurrentState().LineDashPattern = pattern;
- }
-
- public void SetLineJoin(LineJoinStyle join)
- {
- GetCurrentState().JoinStyle = join;
- }
-
- public void SetLineWidth(decimal width)
- {
- GetCurrentState().LineWidth = width;
- }
-
- public void SetMiterLimit(decimal limit)
- {
- GetCurrentState().MiterLimit = limit;
- }
-
- public void MoveToNextLineWithOffset()
- {
- var tdOperation = new MoveToNextLineWithOffset(0, -1 * (decimal)GetCurrentState().FontState.Leading);
- tdOperation.Run(this);
- }
-
- public void SetFontAndSize(NameToken font, double size)
- {
- var currentState = GetCurrentState();
- currentState.FontState.FontSize = size;
- currentState.FontState.FontName = font;
- }
-
- public void SetHorizontalScaling(double scale)
- {
- GetCurrentState().FontState.HorizontalScaling = scale;
- }
-
- public void SetTextLeading(double leading)
- {
- GetCurrentState().FontState.Leading = leading;
- }
-
- public void SetTextRenderingMode(TextRenderingMode mode)
- {
- GetCurrentState().FontState.TextRenderingMode = mode;
- }
-
- public void SetTextRise(double rise)
- {
- GetCurrentState().FontState.Rise = rise;
- }
-
- public void SetWordSpacing(double spacing)
- {
- GetCurrentState().FontState.WordSpacing = spacing;
- }
-
- public void ModifyCurrentTransformationMatrix(double[] value)
- {
- var ctm = GetCurrentState().CurrentTransformationMatrix;
- GetCurrentState().CurrentTransformationMatrix = TransformationMatrix.FromArray(value).Multiply(ctm);
- }
-
- public void SetCharacterSpacing(double spacing)
- {
- GetCurrentState().FontState.CharacterSpacing = spacing;
- }
-
- public void PaintShading(NameToken shadingName)
+ public override void PaintShading(NameToken shadingName)
{
// We do nothing for the moment
// Do the following if you need to access the shading:
diff --git a/src/UglyToad.PdfPig/Graphics/InlineImageBuilder.cs b/src/UglyToad.PdfPig/Graphics/InlineImageBuilder.cs
index 213fa4e4d..0735ec44e 100644
--- a/src/UglyToad.PdfPig/Graphics/InlineImageBuilder.cs
+++ b/src/UglyToad.PdfPig/Graphics/InlineImageBuilder.cs
@@ -1,24 +1,31 @@
namespace UglyToad.PdfPig.Graphics
{
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using Colors;
using Content;
using Core;
using Filters;
using PdfPig.Core;
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
using Tokenization.Scanner;
using Tokens;
- using Util;
- internal class InlineImageBuilder
+ ///
+ /// Inline Image Builder.
+ ///
+ public class InlineImageBuilder
{
+ ///
+ /// Inline image properties.
+ ///
public IReadOnlyDictionary Properties { get; set; }
+ ///
+ /// Inline image bytes.
+ ///
public IReadOnlyList Bytes { get; set; }
- public InlineImage CreateInlineImage(TransformationMatrix transformationMatrix, ILookupFilterProvider filterProvider,
+ internal InlineImage CreateInlineImage(TransformationMatrix transformationMatrix, ILookupFilterProvider filterProvider,
IPdfTokenScanner tokenScanner,
RenderingIntent defaultRenderingIntent,
IResourceStore resourceStore)
diff --git a/src/UglyToad.PdfPig/Graphics/PerformantRectangleTransformer.cs b/src/UglyToad.PdfPig/Graphics/PerformantRectangleTransformer.cs
index 9dd104333..1e2ac4b02 100644
--- a/src/UglyToad.PdfPig/Graphics/PerformantRectangleTransformer.cs
+++ b/src/UglyToad.PdfPig/Graphics/PerformantRectangleTransformer.cs
@@ -2,8 +2,14 @@
{
using PdfPig.Core;
- internal static class PerformantRectangleTransformer
+ ///
+ /// Performant rectangle transformer.
+ ///
+ public static class PerformantRectangleTransformer
{
+ ///
+ /// Transform the rectangle using the matrices.
+ ///
public static PdfRectangle Transform(TransformationMatrix first, TransformationMatrix second, TransformationMatrix third, PdfRectangle rectangle)
{
var tl = rectangle.TopLeft;
diff --git a/src/UglyToad.PdfPig/Graphics/StreamProcessorHelper.cs b/src/UglyToad.PdfPig/Graphics/StreamProcessorHelper.cs
new file mode 100644
index 000000000..48cca73de
--- /dev/null
+++ b/src/UglyToad.PdfPig/Graphics/StreamProcessorHelper.cs
@@ -0,0 +1,102 @@
+namespace UglyToad.PdfPig.Graphics
+{
+ using System;
+ using UglyToad.PdfPig.Content;
+ using UglyToad.PdfPig.Core;
+ using UglyToad.PdfPig.Geometry;
+ using UglyToad.PdfPig.Logging;
+
+ ///
+ /// Helper class for stream processor.
+ ///
+ public static class StreamProcessorHelper
+ {
+ ///
+ /// Get the initial clipping path using the crop box and the initial transformation matrix.
+ ///
+ public static PdfPath GetInitialClipping(CropBox cropBox, TransformationMatrix initialMatrix)
+ {
+ var transformedCropBox = initialMatrix.Transform(cropBox.Bounds);
+
+ // We re-compute width and height to get possible negative values.
+ double width = transformedCropBox.TopRight.X - transformedCropBox.BottomLeft.X;
+ double height = transformedCropBox.TopRight.Y - transformedCropBox.BottomLeft.Y;
+
+ // initiate CurrentClippingPath to cropBox
+ var clippingSubpath = new PdfSubpath();
+ clippingSubpath.Rectangle(transformedCropBox.BottomLeft.X, transformedCropBox.BottomLeft.Y, width, height);
+ var clippingPath = new PdfPath() { clippingSubpath };
+ clippingPath.SetClipping(FillingRule.EvenOdd);
+ return clippingPath;
+ }
+
+ ///
+ /// Gets the initial transformation matrix for the stream processor.
+ ///
+ [System.Diagnostics.Contracts.Pure]
+ public static TransformationMatrix GetInitialMatrix(UserSpaceUnit userSpaceUnit,
+ MediaBox mediaBox,
+ CropBox cropBox,
+ PageRotationDegrees rotation,
+ ILog log)
+ {
+ // 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.Bounds.Intersect(cropBox.Bounds) ?? cropBox.Bounds;
+
+ if (rotation.Value == 0
+ && viewBox.Left == 0
+ && viewBox.Bottom == 0
+ && userSpaceUnit.PointMultiples == 1)
+ {
+ return TransformationMatrix.Identity;
+ }
+
+ // Move points so that (0,0) is equal to the viewbox bottom left corner.
+ var t1 = TransformationMatrix.GetTranslationMatrix(-viewBox.Left, -viewBox.Bottom);
+
+ if (userSpaceUnit.PointMultiples != 1)
+ {
+ log.Warn("User space unit other than 1 is not implemented");
+ }
+
+ // After rotating around the origin, our points will have negative x/y coordinates.
+ // Fix this by translating them by a certain dx/dy after rotation based on the viewbox.
+ double dx, dy;
+ switch (rotation.Value)
+ {
+ case 0:
+ // No need to rotate / translate after rotation, just return the initial
+ // translation matrix.
+ return t1;
+ case 90:
+ // Move rotated points up by our (unrotated) viewbox width
+ dx = 0;
+ dy = viewBox.Width;
+ break;
+ case 180:
+ // Move rotated points up/right using the (unrotated) viewbox width/height
+ dx = viewBox.Width;
+ dy = viewBox.Height;
+ break;
+ case 270:
+ // Move rotated points right using the (unrotated) viewbox height
+ dx = viewBox.Height;
+ dy = 0;
+ break;
+ default:
+ throw new InvalidOperationException($"Invalid value for page rotation: {rotation.Value}.");
+ }
+
+ // GetRotationMatrix uses counter clockwise angles, whereas our page rotation
+ // is a clockwise angle, so flip the sign.
+ var r = TransformationMatrix.GetRotationMatrix(-rotation.Value);
+
+ // Fix up negative coordinates after rotation
+ var t2 = TransformationMatrix.GetTranslationMatrix(dx, dy);
+
+ // Now get the final combined matrix T1 > R > T2
+ return t1.Multiply(r.Multiply(t2));
+ }
+ }
+}
diff --git a/src/UglyToad.PdfPig/IParsingOptions.cs b/src/UglyToad.PdfPig/IParsingOptions.cs
new file mode 100644
index 000000000..3f0bce0b4
--- /dev/null
+++ b/src/UglyToad.PdfPig/IParsingOptions.cs
@@ -0,0 +1,40 @@
+namespace UglyToad.PdfPig
+{
+ using System.Collections.Generic;
+ using UglyToad.PdfPig.Logging;
+
+ ///
+ /// Parsing options interface.
+ ///
+ public interface IParsingOptions
+ {
+ ///
+ /// Should the parser apply clipping to paths?
+ /// Defaults to .
+ /// Bezier curves will be transformed into polylines if clipping is set to .
+ ///
+ bool ClipPaths { get; }
+
+ ///
+ /// Should the parser ignore issues where the document does not conform to the PDF specification?
+ ///
+ bool UseLenientParsing { get; }
+
+ ///
+ /// All passwords to try when opening this document, will include any values set for .
+ ///
+ List Passwords { get; }
+
+ ///
+ /// Skip extracting content where the font could not be found, will result in some letters being skipped/missed
+ /// but will prevent the library throwing where the source PDF has some corrupted text. Also skips XObjects like
+ /// forms and images when missing.
+ ///
+ bool SkipMissingFonts { get; }
+
+ ///
+ /// The used to record messages raised by the parsing process.
+ ///
+ ILog Logger { get; }
+ }
+}
diff --git a/src/UglyToad.PdfPig/InternalParsingOptions.cs b/src/UglyToad.PdfPig/InternalParsingOptions.cs
index 3b6c4f26f..2b704cd8f 100644
--- a/src/UglyToad.PdfPig/InternalParsingOptions.cs
+++ b/src/UglyToad.PdfPig/InternalParsingOptions.cs
@@ -4,11 +4,11 @@
using System.Collections.Generic;
///
- /// but without being a public API/
+ /// but without being a public API.
///
- internal class InternalParsingOptions
+ internal class InternalParsingOptions : IParsingOptions
{
- public IReadOnlyList Passwords { get; }
+ public List Passwords { get; }
public bool UseLenientParsing { get; }
@@ -21,7 +21,7 @@ internal class InternalParsingOptions
public ILog Logger { get; }
public InternalParsingOptions(
- IReadOnlyList passwords,
+ List passwords,
bool useLenientParsing,
bool clipPaths,
bool skipMissingFonts,
diff --git a/src/UglyToad.PdfPig/Outline/Destinations/NamedDestinations.cs b/src/UglyToad.PdfPig/Outline/Destinations/NamedDestinations.cs
index 016a3f2b3..bebcad20c 100644
--- a/src/UglyToad.PdfPig/Outline/Destinations/NamedDestinations.cs
+++ b/src/UglyToad.PdfPig/Outline/Destinations/NamedDestinations.cs
@@ -9,7 +9,7 @@
///
/// Named destinations in a PDF document
///
- internal class NamedDestinations
+ public class NamedDestinations
{
///
/// Dictionary containing explicit destinations, keyed by name
diff --git a/src/UglyToad.PdfPig/Parser/IPageContentParser.cs b/src/UglyToad.PdfPig/Parser/IPageContentParser.cs
index c7817b2fc..2968d642f 100644
--- a/src/UglyToad.PdfPig/Parser/IPageContentParser.cs
+++ b/src/UglyToad.PdfPig/Parser/IPageContentParser.cs
@@ -1,13 +1,18 @@
namespace UglyToad.PdfPig.Parser
{
- using System.Collections.Generic;
using Core;
using Graphics.Operations;
using Logging;
+ using System.Collections.Generic;
- internal interface IPageContentParser
+ ///
+ /// Page content parser interface.
+ ///
+ public interface IPageContentParser
{
- IReadOnlyList Parse(int pageNumber, IInputBytes inputBytes,
- ILog log);
+ ///
+ /// Parse the into s.
+ ///
+ IReadOnlyList Parse(int pageNumber, IInputBytes inputBytes, ILog log);
}
}
\ No newline at end of file
diff --git a/src/UglyToad.PdfPig/Parser/PageFactory.cs b/src/UglyToad.PdfPig/Parser/PageFactory.cs
index d1cbddaf7..e12300624 100644
--- a/src/UglyToad.PdfPig/Parser/PageFactory.cs
+++ b/src/UglyToad.PdfPig/Parser/PageFactory.cs
@@ -1,255 +1,83 @@
namespace UglyToad.PdfPig.Parser
{
- using System;
- using System.Collections.Generic;
using Annotations;
using Content;
- using Core;
using Filters;
using Geometry;
using Graphics;
using Graphics.Operations;
using Logging;
using Outline;
- using Parts;
+ using System.Collections.Generic;
using Tokenization.Scanner;
using Tokens;
- using Util;
+ using UglyToad.PdfPig.Core;
- internal class PageFactory : IPageFactory
+ internal sealed class PageFactory : PageFactoryBase
{
- private readonly IPdfTokenScanner pdfScanner;
- private readonly IResourceStore resourceStore;
- private readonly ILookupFilterProvider filterProvider;
- private readonly IPageContentParser pageContentParser;
- private readonly ILog log;
-
public PageFactory(
IPdfTokenScanner pdfScanner,
IResourceStore resourceStore,
ILookupFilterProvider filterProvider,
IPageContentParser pageContentParser,
ILog log)
- {
- this.resourceStore = resourceStore;
- this.filterProvider = filterProvider;
- this.pageContentParser = pageContentParser;
- this.pdfScanner = pdfScanner;
- this.log = log;
- }
-
- public Page Create(int number, DictionaryToken dictionary, PageTreeMembers pageTreeMembers,
- NamedDestinations namedDestinations, InternalParsingOptions parsingOptions)
- {
- if (dictionary == null)
- {
- throw new ArgumentNullException(nameof(dictionary));
- }
-
- var type = dictionary.GetNameOrDefault(NameToken.Type);
-
- if (type != null && !type.Equals(NameToken.Page))
- {
- parsingOptions.Logger.Error($"Page {number} had its type specified as {type} rather than 'Page'.");
- }
-
- MediaBox mediaBox = GetMediaBox(number, dictionary, pageTreeMembers);
- CropBox cropBox = GetCropBox(dictionary, pageTreeMembers, mediaBox);
-
- var rotation = new PageRotationDegrees(pageTreeMembers.Rotation);
- if (dictionary.TryGet(NameToken.Rotate, pdfScanner, out NumericToken rotateToken))
- {
- rotation = new PageRotationDegrees(rotateToken.Int);
- }
-
- var stackDepth = 0;
-
- while (pageTreeMembers.ParentResources.Count > 0)
- {
- var resource = pageTreeMembers.ParentResources.Dequeue();
-
- resourceStore.LoadResourceDictionary(resource, parsingOptions);
- stackDepth++;
- }
-
- if (dictionary.TryGet(NameToken.Resources, pdfScanner, out DictionaryToken resources))
- {
- resourceStore.LoadResourceDictionary(resources, parsingOptions);
- stackDepth++;
- }
-
- UserSpaceUnit userSpaceUnit = GetUserSpaceUnits(dictionary);
-
- PageContent content;
-
- if (!dictionary.TryGet(NameToken.Contents, out var contents))
- {
- content = new PageContent(EmptyArray.Instance,
- EmptyArray.Instance,
- EmptyArray.Instance,
- EmptyArray>.Instance,
- EmptyArray.Instance,
- pdfScanner,
- filterProvider,
- resourceStore);
- // ignored for now, is it possible? check the spec...
- }
- else if (DirectObjectFinder.TryGet(contents, pdfScanner, out var array))
- {
- var bytes = new List();
-
- for (var i = 0; i < array.Data.Count; i++)
- {
- var item = array.Data[i];
-
- if (!(item is IndirectReferenceToken obj))
- {
- throw new PdfDocumentFormatException($"The contents contained something which was not an indirect reference: {item}.");
- }
-
- var contentStream = DirectObjectFinder.Get(obj, pdfScanner);
-
- if (contentStream == null)
- {
- throw new InvalidOperationException($"Could not find the contents for object {obj}.");
- }
-
- bytes.AddRange(contentStream.Decode(filterProvider, pdfScanner));
-
- if (i < array.Data.Count - 1)
- {
- bytes.Add((byte)'\n');
- }
- }
-
- content = GetContent(number, bytes, cropBox, userSpaceUnit, rotation, mediaBox, parsingOptions);
- }
- else
- {
- var contentStream = DirectObjectFinder.Get(contents, pdfScanner);
-
- if (contentStream == null)
- {
- throw new InvalidOperationException("Failed to parse the content for the page: " + number);
- }
-
- var bytes = contentStream.Decode(filterProvider, pdfScanner);
-
- content = GetContent(number, bytes, cropBox, userSpaceUnit, rotation, mediaBox, parsingOptions);
- }
+ : base(pdfScanner, resourceStore, filterProvider, pageContentParser, log)
+ { }
- var initialMatrix = ContentStreamProcessor.GetInitialMatrix(userSpaceUnit, mediaBox, cropBox, rotation, log);
- var annotationProvider = new AnnotationProvider(pdfScanner, dictionary, initialMatrix, namedDestinations, log);
- var page = new Page(number, dictionary, mediaBox, cropBox, rotation, content, annotationProvider, pdfScanner);
-
- for (var i = 0; i < stackDepth; i++)
- {
- resourceStore.UnloadResourceDictionary();
- }
-
- return page;
- }
-
- private PageContent GetContent(
- int pageNumber,
+ protected override Page ProcessPage(int pageNumber,
+ DictionaryToken dictionary,
+ NamedDestinations namedDestinations,
IReadOnlyList contentBytes,
CropBox cropBox,
UserSpaceUnit userSpaceUnit,
PageRotationDegrees rotation,
MediaBox mediaBox,
- InternalParsingOptions parsingOptions)
+ IParsingOptions parsingOptions)
{
- var operations = pageContentParser.Parse(pageNumber, new ByteArrayInputBytes(contentBytes),
- parsingOptions.Logger);
-
var context = new ContentStreamProcessor(
pageNumber,
- resourceStore,
+ ResourceStore,
userSpaceUnit,
mediaBox,
cropBox,
rotation,
- pdfScanner,
- pageContentParser,
- filterProvider,
+ PdfScanner,
+ PageContentParser,
+ FilterProvider,
parsingOptions);
- return context.Process(pageNumber, operations);
- }
-
- private static UserSpaceUnit GetUserSpaceUnits(DictionaryToken dictionary)
- {
- var spaceUnits = UserSpaceUnit.Default;
- if (dictionary.TryGet(NameToken.UserUnit, out var userUnitBase) && userUnitBase is NumericToken userUnitNumber)
- {
- spaceUnits = new UserSpaceUnit(userUnitNumber.Int);
- }
-
- return spaceUnits;
- }
-
- private CropBox GetCropBox(
- DictionaryToken dictionary,
- PageTreeMembers pageTreeMembers,
- MediaBox mediaBox)
- {
- CropBox cropBox;
- if (dictionary.TryGet(NameToken.CropBox, out var cropBoxObject) &&
- DirectObjectFinder.TryGet(cropBoxObject, pdfScanner, out ArrayToken cropBoxArray))
- {
- if (cropBoxArray.Length != 4)
- {
- log.Error($"The CropBox was the wrong length in the dictionary: {dictionary}. Array was: {cropBoxArray}. Using MediaBox.");
-
- cropBox = new CropBox(mediaBox.Bounds);
-
- return cropBox;
- }
+ var operations = PageContentParser.Parse(pageNumber, new ByteArrayInputBytes(contentBytes), parsingOptions.Logger);
+ var content = context.Process(pageNumber, operations);
- cropBox = new CropBox(cropBoxArray.ToRectangle(pdfScanner));
- }
- else
- {
- cropBox = pageTreeMembers.GetCropBox() ?? new CropBox(mediaBox.Bounds);
- }
-
- return cropBox;
+ var initialMatrix = StreamProcessorHelper.GetInitialMatrix(userSpaceUnit, mediaBox, cropBox, rotation, Log);
+ var annotationProvider = new AnnotationProvider(PdfScanner, dictionary, initialMatrix, namedDestinations, Log);
+ return new Page(pageNumber, dictionary, mediaBox, cropBox, rotation, content, annotationProvider, PdfScanner);
}
- private MediaBox GetMediaBox(
- int number,
+ protected override Page ProcessPage(
+ int pageNumber,
DictionaryToken dictionary,
- PageTreeMembers pageTreeMembers)
+ NamedDestinations namedDestinations,
+ CropBox cropBox,
+ UserSpaceUnit userSpaceUnit,
+ PageRotationDegrees rotation,
+ MediaBox mediaBox,
+ IParsingOptions parsingOptions)
{
- MediaBox mediaBox;
- if (dictionary.TryGet(NameToken.MediaBox, out var mediaBoxObject)
- && DirectObjectFinder.TryGet(mediaBoxObject, pdfScanner, out ArrayToken mediaBoxArray))
- {
- if (mediaBoxArray.Length != 4)
- {
- log.Error($"The MediaBox was the wrong length in the dictionary: {dictionary}. Array was: {mediaBoxArray}. Defaulting to US Letter.");
-
- mediaBox = MediaBox.Letter;
-
- return mediaBox;
- }
-
- mediaBox = new MediaBox(mediaBoxArray.ToRectangle(pdfScanner));
- }
- else
- {
- mediaBox = pageTreeMembers.MediaBox;
-
- if (mediaBox == null)
- {
- log.Error($"The MediaBox was the wrong missing for page {number}. Using US Letter.");
-
- // PDFBox defaults to US Letter.
- mediaBox = MediaBox.Letter;
- }
- }
-
- return mediaBox;
+ var initialMatrix = StreamProcessorHelper.GetInitialMatrix(userSpaceUnit, mediaBox, cropBox, rotation, Log);
+ var annotationProvider = new AnnotationProvider(PdfScanner, dictionary, initialMatrix, namedDestinations, Log);
+
+ var content = new PageContent(EmptyArray.Instance,
+ EmptyArray.Instance,
+ EmptyArray.Instance,
+ EmptyArray>.Instance,
+ EmptyArray.Instance,
+ PdfScanner,
+ FilterProvider,
+ ResourceStore);
+ // ignored for now, is it possible? check the spec...
+
+ return new Page(pageNumber, dictionary, mediaBox, cropBox, rotation, content, annotationProvider, PdfScanner);
}
}
}
diff --git a/src/UglyToad.PdfPig/ParsingOptions.cs b/src/UglyToad.PdfPig/ParsingOptions.cs
index 3767a6948..531e77aa6 100644
--- a/src/UglyToad.PdfPig/ParsingOptions.cs
+++ b/src/UglyToad.PdfPig/ParsingOptions.cs
@@ -6,7 +6,7 @@
///
/// Configures options used by the parser when reading PDF documents.
///
- public class ParsingOptions
+ public class ParsingOptions : IParsingOptions
{
///
/// A default with set to false.
diff --git a/src/UglyToad.PdfPig/PdfDocument.cs b/src/UglyToad.PdfPig/PdfDocument.cs
index fb27420c7..252e2d065 100644
--- a/src/UglyToad.PdfPig/PdfDocument.cs
+++ b/src/UglyToad.PdfPig/PdfDocument.cs
@@ -161,6 +161,57 @@ public Page GetPage(int pageNumber)
}
}
+ ///
+ /// Get the page with the specified page number (1 indexed).
+ ///
+ ///
+ /// The number of the page to return, this starts from 1.
+ /// The page.
+ public TPage GetPage(int pageNumber)
+ {
+ // TODO - update log with log type
+ if (isDisposed)
+ {
+ throw new ObjectDisposedException("Cannot access page after the document is disposed.");
+ }
+
+ parsingOptions.Logger.Debug($"Accessing page {pageNumber}.");
+
+ try
+ {
+ return pages.GetPage(pageNumber, namedDestinations, parsingOptions);
+ }
+ catch (Exception ex)
+ {
+ if (IsEncrypted)
+ {
+ throw new PdfDocumentEncryptedException("Document was encrypted which may have caused error when retrieving page.", encryptionDictionary, ex);
+ }
+
+ throw;
+ }
+ }
+
+ ///
+ /// TODO
+ ///
+ ///
+ ///
+ public void AddPageFactory(IPageFactory pageFactory)
+ {
+ pages.AddPageFactory(pageFactory);
+ }
+
+ ///
+ /// TODO
+ ///
+ ///
+ ///
+ public void AddPageFactory() where TPageFactory : IPageFactory
+ {
+ pages.AddPageFactory();
+ }
+
///
/// Gets all pages in this document in order.
///
diff --git a/src/UglyToad.PdfPig/PdfExtensions.cs b/src/UglyToad.PdfPig/PdfExtensions.cs
index 9d4154553..5d0fee9f4 100644
--- a/src/UglyToad.PdfPig/PdfExtensions.cs
+++ b/src/UglyToad.PdfPig/PdfExtensions.cs
@@ -15,7 +15,7 @@ public static class PdfExtensions
///
/// Try and get the entry with a given name and type or look-up the object if it's an indirect reference.
///
- internal static bool TryGet(this DictionaryToken dictionary, NameToken name, IPdfTokenScanner tokenScanner, out T token) where T : IToken
+ public static bool TryGet(this DictionaryToken dictionary, NameToken name, IPdfTokenScanner tokenScanner, out T token) where T : IToken
{
token = default(T);
if (!dictionary.TryGet(name, out var t) || !(t is T typedToken))
@@ -31,8 +31,11 @@ internal static bool TryGet(this DictionaryToken dictionary, NameToken name,
token = typedToken;
return true;
}
-
- internal static T Get(this DictionaryToken dictionary, NameToken name, IPdfTokenScanner scanner) where T : class, IToken
+
+ ///
+ /// Get the entry with a given name and type or look-up the object if it's an indirect reference.
+ ///
+ public static T Get(this DictionaryToken dictionary, NameToken name, IPdfTokenScanner scanner) where T : class, IToken
{
if (!dictionary.TryGet(name, out var token) || !(token is T typedToken))
{
diff --git a/src/UglyToad.PdfPig/Tokenization/Scanner/IPdfTokenScanner.cs b/src/UglyToad.PdfPig/Tokenization/Scanner/IPdfTokenScanner.cs
index 31171cc41..5ba7269c5 100644
--- a/src/UglyToad.PdfPig/Tokenization/Scanner/IPdfTokenScanner.cs
+++ b/src/UglyToad.PdfPig/Tokenization/Scanner/IPdfTokenScanner.cs
@@ -7,7 +7,7 @@
///
/// Tokenizes objects from bytes in a PDF file.
///
- internal interface IPdfTokenScanner : ISeekableTokenScanner, IDisposable
+ public interface IPdfTokenScanner : ISeekableTokenScanner, IDisposable
{
///
/// Tokenize the object with a given object number.
diff --git a/src/UglyToad.PdfPig/UglyToad.PdfPig.csproj b/src/UglyToad.PdfPig/UglyToad.PdfPig.csproj
index 8158276ae..74f15f44d 100644
--- a/src/UglyToad.PdfPig/UglyToad.PdfPig.csproj
+++ b/src/UglyToad.PdfPig/UglyToad.PdfPig.csproj
@@ -7,6 +7,7 @@
true
true
..\pdfpig.snk
+ annotations
true
diff --git a/src/UglyToad.PdfPig/XObjects/XObjectFactory.cs b/src/UglyToad.PdfPig/XObjects/XObjectFactory.cs
index 0dc9ba995..ce9b4e330 100644
--- a/src/UglyToad.PdfPig/XObjects/XObjectFactory.cs
+++ b/src/UglyToad.PdfPig/XObjects/XObjectFactory.cs
@@ -12,9 +12,15 @@
using Tokenization.Scanner;
using Tokens;
using Util;
-
- internal static class XObjectFactory
- {
+
+ ///
+ /// XObject factory.
+ ///
+ public static class XObjectFactory
+ {
+ ///
+ /// Read the image.
+ ///
public static XObjectImage ReadImage(XObjectContentRecord xObject, IPdfTokenScanner pdfScanner,
ILookupFilterProvider filterProvider,
IResourceStore resourceStore)