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)