From aea6e9033822c0a568169cf48f8ca384e3ffb0f2 Mon Sep 17 00:00:00 2001 From: BobLd <38405645+BobLd@users.noreply.github.com> Date: Sat, 16 May 2026 18:08:53 +0100 Subject: [PATCH] Allow overriding BeginText and EndText methods in IOperationContext and BaseStreamProcessor --- .../Graphics/TestOperationContext.cs | 12 + .../PublicApiScannerTests.cs | 1 + .../Graphics/BaseStreamProcessor.cs | 2036 +++++++++-------- .../Graphics/Core/RenderingModeExtensions.cs | 15 +- .../Graphics/IOperationContext.cs | 14 + .../Operations/TextObjects/BeginText.cs | 4 +- .../Operations/TextObjects/EndText.cs | 4 +- 7 files changed, 1068 insertions(+), 1018 deletions(-) diff --git a/src/UglyToad.PdfPig.Tests/Graphics/TestOperationContext.cs b/src/UglyToad.PdfPig.Tests/Graphics/TestOperationContext.cs index 426c40df0..3f2985428 100644 --- a/src/UglyToad.PdfPig.Tests/Graphics/TestOperationContext.cs +++ b/src/UglyToad.PdfPig.Tests/Graphics/TestOperationContext.cs @@ -62,6 +62,18 @@ public void PushState() StateStack.Push(StateStack.Peek().DeepClone()); } + public void BeginText() + { + TextMatrices.TextMatrix = TransformationMatrix.Identity; + TextMatrices.TextLineMatrix = TransformationMatrix.Identity; + } + + public void EndText() + { + TextMatrices.TextMatrix = TransformationMatrix.Identity; + TextMatrices.TextLineMatrix = TransformationMatrix.Identity; + } + public void ShowText(IInputBytes bytes) { } diff --git a/src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs b/src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs index 292c1b25e..af2449ee2 100644 --- a/src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs +++ b/src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs @@ -170,6 +170,7 @@ public void OnlyExposedApiIsPublic() "UglyToad.PdfPig.Graphics.Core.LineDashPattern", "UglyToad.PdfPig.Graphics.Core.LineJoinStyle", "UglyToad.PdfPig.Graphics.Core.RenderingIntent", + "UglyToad.PdfPig.Graphics.Core.RenderingModeExtensions", "UglyToad.PdfPig.Graphics.CurrentFontState", "UglyToad.PdfPig.Graphics.CurrentGraphicsState", "UglyToad.PdfPig.Graphics.IColorSpaceContext", diff --git a/src/UglyToad.PdfPig/Graphics/BaseStreamProcessor.cs b/src/UglyToad.PdfPig/Graphics/BaseStreamProcessor.cs index 3b7360f07..9498fded8 100644 --- a/src/UglyToad.PdfPig/Graphics/BaseStreamProcessor.cs +++ b/src/UglyToad.PdfPig/Graphics/BaseStreamProcessor.cs @@ -1,1020 +1,1034 @@ -namespace UglyToad.PdfPig.Graphics -{ - using System; - using System.Collections.Generic; - using System.Diagnostics; - using System.Linq; - - using Colors; - using Content; - using Core; - using Filters; - using Geometry; - using Operations; - using Operations.TextPositioning; - using Parser; - using PdfFonts; - using PdfPig.Core; - using Tokenization.Scanner; - using Tokens; - using 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 ParsingOptions 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; - - /// - /// 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; - - /// - public TextMatrices TextMatrices { get; } = new TextMatrices(); - - /// - /// The current transformation matrix. - /// - public TransformationMatrix CurrentTransformationMatrix => GetCurrentState().CurrentTransformationMatrix; - - /// - public PdfPoint CurrentPosition { get; set; } - - /// - public int StackSize => GraphicsStack.Count; - - private readonly Dictionary> xObjects = - new Dictionary> - { - { XObjectType.Image, new List() }, - { XObjectType.PostScript, new List() } - }; - - /// - /// Abstract stream processor constructor. - /// - protected BaseStreamProcessor( - int pageNumber, - IResourceStore resourceStore, - IPdfTokenScanner pdfScanner, - IPageContentParser pageContentParser, - ILookupFilterProvider filterProvider, - CropBox cropBox, - UserSpaceUnit userSpaceUnit, - PageRotationDegrees rotation, - in TransformationMatrix initialMatrix, - ParsingOptions 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; - - GraphicsStack.Push(new CurrentGraphicsState() - { - CurrentTransformationMatrix = initialMatrix, - CurrentClippingPath = GetInitialClipping(cropBox), - ColorSpaceContext = new ColorSpaceContext(GetCurrentState, resourceStore) - }); - } - - /// - /// Get the initial clipping path using the crop box and the initial transformation matrix. - /// - protected static PdfPath GetInitialClipping(CropBox cropBox) - { - // Initiate CurrentClippingPath to cropBox - var clippingPath = cropBox.Bounds.ToPdfPath(); - clippingPath.SetClipping(FillingRule.EvenOdd); - return clippingPath; - } - - /// - /// 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); - } - } - - /// - /// Clone the current state and push it at the top of the stack. - /// - 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() - { - if (StackSize > 1) - { - GraphicsStack.Pop(); - } - else - { - const string error = "Cannot execute a pop of the graphics state stack, it would leave the stack empty."; - ParsingOptions.Logger.Error(error); - - if (!ParsingOptions.UseLenientParsing) - { - throw new InvalidOperationException(error); - } - } - - ActiveExtendedGraphicsStateFont = null; - } - - /// - public virtual void PushState() - { - GraphicsStack.Push(GraphicsStack.Peek().DeepClone()); - } - - /// - public void ShowText(IInputBytes bytes) - { +namespace UglyToad.PdfPig.Graphics +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Linq; + + using Colors; + using Content; + using Core; + using Filters; + using Geometry; + using Operations; + using Operations.TextPositioning; + using Parser; + using PdfFonts; + using PdfPig.Core; + using Tokenization.Scanner; + using Tokens; + using 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 ParsingOptions 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; + + /// + /// 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; + + /// + public TextMatrices TextMatrices { get; } = new TextMatrices(); + + /// + /// The current transformation matrix. + /// + public TransformationMatrix CurrentTransformationMatrix => GetCurrentState().CurrentTransformationMatrix; + + /// + public PdfPoint CurrentPosition { get; set; } + + /// + public int StackSize => GraphicsStack.Count; + + private readonly Dictionary> xObjects = + new Dictionary> + { + { XObjectType.Image, new List() }, + { XObjectType.PostScript, new List() } + }; + + /// + /// Abstract stream processor constructor. + /// + protected BaseStreamProcessor( + int pageNumber, + IResourceStore resourceStore, + IPdfTokenScanner pdfScanner, + IPageContentParser pageContentParser, + ILookupFilterProvider filterProvider, + CropBox cropBox, + UserSpaceUnit userSpaceUnit, + PageRotationDegrees rotation, + in TransformationMatrix initialMatrix, + ParsingOptions 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; + + GraphicsStack.Push(new CurrentGraphicsState() + { + CurrentTransformationMatrix = initialMatrix, + CurrentClippingPath = GetInitialClipping(cropBox), + ColorSpaceContext = new ColorSpaceContext(GetCurrentState, resourceStore) + }); + } + + /// + /// Get the initial clipping path using the crop box and the initial transformation matrix. + /// + protected static PdfPath GetInitialClipping(CropBox cropBox) + { + // Initiate CurrentClippingPath to cropBox + var clippingPath = cropBox.Bounds.ToPdfPath(); + clippingPath.SetClipping(FillingRule.EvenOdd); + return clippingPath; + } + + /// + /// 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); + } + } + + /// + /// Clone the current state and push it at the top of the stack. + /// + 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() + { + if (StackSize > 1) + { + GraphicsStack.Pop(); + } + else + { + const string error = "Cannot execute a pop of the graphics state stack, it would leave the stack empty."; + ParsingOptions.Logger.Error(error); + + if (!ParsingOptions.UseLenientParsing) + { + throw new InvalidOperationException(error); + } + } + + ActiveExtendedGraphicsStateFont = null; + } + + /// + public virtual void PushState() + { + GraphicsStack.Push(GraphicsStack.Peek().DeepClone()); + } + + /// + public virtual void BeginText() + { + TextMatrices.TextMatrix = TransformationMatrix.Identity; + TextMatrices.TextLineMatrix = TransformationMatrix.Identity; + } + + /// + public virtual void EndText() + { + TextMatrices.TextMatrix = TransformationMatrix.Identity; + TextMatrices.TextLineMatrix = TransformationMatrix.Identity; + } + + /// + public void ShowText(IInputBytes bytes) + { TextSequence++; - - var currentState = GetCurrentState(); - - var font = currentState.FontState.FromExtendedGraphicsState - ? ActiveExtendedGraphicsStateFont - : ResourceStore.GetFont(currentState.FontState.FontName); - - if (font is 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(PdfPig.ParsingOptions.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 is 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, - 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, - CurrentGraphicsState currentState, - double fontSize, - double pointSize, - int code, - string unicode, - long currentOffset, - in TransformationMatrix renderingMatrix, - in TransformationMatrix textMatrix, - in 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 is 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(PdfPig.ParsingOptions.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 = 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 - { - byte[] bytes; - if (token is HexToken hex) - { - bytes = [.. hex.Bytes]; - } - else - { - bytes = ((StringToken)token).GetBytes(); - } - - ShowText(new MemoryInputBytes(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}."); - } - } - - /// - /// Render XObject image implementation. - /// - /// - protected abstract void RenderXObjectImage(XObjectContentRecord xObjectContentRecord); - - /// - /// Process a XObject form. - /// - protected virtual 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. - */ - - if (formStream.StreamDictionary.TryGet(NameToken.Resources, - PdfScanner, - out var formResources)) - { - ResourceStore.LoadResourceDictionary(formResources); - } - - // 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. - startState.SoftMask = null; - - // 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.0; - startState.AlphaConstantStroking = 1.0; - - // TODO: the /CS colorspace of the transparency group should not affect the main colorspace, only the transparent imaging model. - 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()); - } - + + var currentState = GetCurrentState(); + + var font = currentState.FontState.FromExtendedGraphicsState + ? ActiveExtendedGraphicsStateFont + : ResourceStore.GetFont(currentState.FontState.FontName); + + if (font is 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(PdfPig.ParsingOptions.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 is 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, + 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, + CurrentGraphicsState currentState, + double fontSize, + double pointSize, + int code, + string unicode, + long currentOffset, + in TransformationMatrix renderingMatrix, + in TransformationMatrix textMatrix, + in 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 is 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(PdfPig.ParsingOptions.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 = 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 + { + byte[] bytes; + if (token is HexToken hex) + { + bytes = [.. hex.Bytes]; + } + else + { + bytes = ((StringToken)token).GetBytes(); + } + + ShowText(new MemoryInputBytes(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}."); + } + } + + /// + /// Render XObject image implementation. + /// + /// + protected abstract void RenderXObjectImage(XObjectContentRecord xObjectContentRecord); + + /// + /// Process a XObject form. + /// + protected virtual 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. + */ + + if (formStream.StreamDictionary.TryGet(NameToken.Resources, + PdfScanner, + out var formResources)) + { + ResourceStore.LoadResourceDictionary(formResources); + } + + // 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. + startState.SoftMask = null; + + // 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.0; + startState.AlphaConstantStroking = 1.0; + + // TODO: the /CS colorspace of the transparency group should not affect the main colorspace, only the transparent imaging model. + 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. - ModifyCurrentTransformationMatrix(formMatrix); - - var contentStream = formStream.Decode(FilterProvider, PdfScanner); - - var operations = PageContentParser.Parse(PageNumber, - new MemoryInputBytes(contentStream), - ParsingOptions.Logger); - - // 3. Clip according to the form dictionary's BBox entry. - if (formStream.StreamDictionary.TryGet(NameToken.Bbox, PdfScanner, out var bboxToken)) - { - var points = bboxToken.Data.OfType().Select(x => x.Double).ToArray(); - PdfRectangle bbox = new PdfRectangle(points[0], points[1], points[2], points[3]).Normalise(); - ClipToRectangle(bbox, FillingRule.EvenOdd); // TODO - Check that Even Odd is valid - } - - // 4. Paint the objects. - bool hasCircularReference = HasFormXObjectCircularReference(formStream, xObjectName, operations); - if (hasCircularReference) - { - if (ParsingOptions.UseLenientParsing) + ModifyCurrentTransformationMatrix(formMatrix); + + var contentStream = formStream.Decode(FilterProvider, PdfScanner); + + var operations = PageContentParser.Parse(PageNumber, + new MemoryInputBytes(contentStream), + ParsingOptions.Logger); + + // 3. Clip according to the form dictionary's BBox entry. + if (formStream.StreamDictionary.TryGet(NameToken.Bbox, PdfScanner, out var bboxToken)) + { + var points = bboxToken.Data.OfType().Select(x => x.Double).ToArray(); + PdfRectangle bbox = new PdfRectangle(points[0], points[1], points[2], points[3]).Normalise(); + ClipToRectangle(bbox, FillingRule.EvenOdd); // TODO - Check that Even Odd is valid + } + + // 4. Paint the objects. + bool hasCircularReference = HasFormXObjectCircularReference(formStream, xObjectName, operations); + if (hasCircularReference) + { + if (ParsingOptions.UseLenientParsing) + { + // TODO - We might be removing too much, good for the moment. See Issues1250() for examples + 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 { - // TODO - We might be removing too much, good for the moment. See Issues1250() for examples - 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 (formResources != null) // has resources - { - 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. - protected virtual bool HasFormXObjectCircularReference(StreamToken formStream, - NameToken? xObjectName, - IReadOnlyList operations) - { - if (xObjectName is null) - { - return false; - } - - if (operations.OfType()?.Any(o => o.Name == xObjectName) != true) - { - return false; - } - - if (!TryGetXObjectToken(formStream, xObjectName, PdfScanner, out var t1)) - { - return false; - } - - if (!ResourceStore.TryGetXObject(xObjectName, out var resourceStream)) - { - return false; - } - - if (!TryGetXObjectToken(resourceStream, xObjectName, PdfScanner, out var t2)) - { - return false; - } - + throw new PdfDocumentFormatException( + $"An XObject form named '{xObjectName}' is referencing itself which can cause unexpected behaviour."); + } + } + + ProcessOperations(operations); + + // 5. Restore saved state. + PopState(); + + if (formResources != null) // has resources + { + 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. + protected virtual bool HasFormXObjectCircularReference(StreamToken formStream, + NameToken? xObjectName, + IReadOnlyList operations) + { + if (xObjectName is null) + { + return false; + } + + if (operations.OfType()?.Any(o => o.Name == xObjectName) != true) + { + return false; + } + + if (!TryGetXObjectToken(formStream, xObjectName, PdfScanner, out var t1)) + { + return false; + } + + if (!ResourceStore.TryGetXObject(xObjectName, out var resourceStream)) + { + return false; + } + + if (!TryGetXObjectToken(resourceStream, xObjectName, PdfScanner, out var t2)) + { + return false; + } + if (t1 is null || t2 is null) { return false; - } - - return t1.Equals(t2); - - static bool TryGetXObjectToken(StreamToken streamToken, NameToken xObjectName, IPdfTokenScanner scanner, out IToken? token) - { - token = null; - if (!streamToken.StreamDictionary.TryGet(NameToken.Resources, scanner, out var formResources)) - { - return false; + } + + return t1.Equals(t2); + + static bool TryGetXObjectToken(StreamToken streamToken, NameToken xObjectName, IPdfTokenScanner scanner, out IToken? token) + { + token = null; + if (!streamToken.StreamDictionary.TryGet(NameToken.Resources, scanner, out var formResources)) + { + return false; } - if (!formResources.TryGet(NameToken.Xobject, out var xObjectBase) || !xObjectBase.TryGet(xObjectName, out token)) - { - return false; - } - - return token is not null; - } - } - - /// - 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); - - /// - protected abstract void ClipToRectangle(PdfRectangle rectangle, FillingRule clippingRule); - - /// - public virtual void SetNamedGraphicsState(NameToken stateName) - { - var currentGraphicsState = GetCurrentState(); - - var state = ResourceStore.GetExtendedGraphicsStateDictionary(stateName); - - if (state is null) - { - return; - } - - 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 = 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; - } - - // (PDF 1.4, array is deprecated in PDF 2.0) The current blend mode that shall be - // used in the transparent imaging model (see 11.3.5, "Blend mode"). A PDF reader - // shall implicitly reset this parameter to its initial value at the (array is - // deprecated beginning of execution of a transparency group XObject - // (see 11.6.6, in PDF 2.0) "Transparency group XObjects"). The value shall be - // either a name object, designating one of the standard blend modes listed in - // "Table 134 — Standard separable blend modes" and "Table 135 — Standard - // non-separable blend modes" in 11.3.5, "Blend mode", or an array of such names. - // In the latter case, the PDF reader shall use the first blend mode in the array - // that it recognises (or Normal if it recognises none of them). - // - // Initial value: Normal. - if (state.TryGet(NameToken.Bm, PdfScanner, out NameToken? bmToken)) - { - currentGraphicsState.BlendMode = bmToken.Data.ToBlendMode() ?? BlendMode.Normal; - } - else if (state.TryGet(NameToken.Bm, PdfScanner, out ArrayToken? bmArrayToken)) - { - // The PDF reader shall use the first blend mode in the array that it - // recognises (or Normal if it recognises none of them). - - currentGraphicsState.BlendMode = BlendMode.Normal; - - foreach (var token in bmArrayToken.Data.OfType()) - { - var bm = token.Data.ToBlendMode(); - if (bm.HasValue) - { - currentGraphicsState.BlendMode = bm.Value; - break; - } - } - } - - if (state.TryGet(NameToken.Smask, PdfScanner, out NameToken? smToken) && smToken.Equals(NameToken.None)) - { - currentGraphicsState.SoftMask = null; - } - else if (state.TryGet(NameToken.Smask, PdfScanner, out DictionaryToken? smDictToken)) - { - currentGraphicsState.SoftMask = SoftMask.Parse(smDictToken, PdfScanner, FilterProvider); - } - } - - /// - 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 is 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(Memory bytes) - { - if (InlineImageBuilder is 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. - /// - protected 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(double 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(double width) - { - GetCurrentState().LineWidth = width; - } - - /// - public virtual void SetMiterLimit(double limit) - { - GetCurrentState().MiterLimit = limit; - } - - /// - public virtual void MoveToNextLineWithOffset() - { - var tdOperation = new MoveToNextLineWithOffset(0, -1 * 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(TransformationMatrix value) - { + if (!formResources.TryGet(NameToken.Xobject, out var xObjectBase) || !xObjectBase.TryGet(xObjectName, out token)) + { + return false; + } + + return token is not null; + } + } + + /// + 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); + + /// + protected abstract void ClipToRectangle(PdfRectangle rectangle, FillingRule clippingRule); + + /// + public virtual void SetNamedGraphicsState(NameToken stateName) + { + var currentGraphicsState = GetCurrentState(); + + var state = ResourceStore.GetExtendedGraphicsStateDictionary(stateName); + + if (state is null) + { + return; + } + + 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 = 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; + } + + // (PDF 1.4, array is deprecated in PDF 2.0) The current blend mode that shall be + // used in the transparent imaging model (see 11.3.5, "Blend mode"). A PDF reader + // shall implicitly reset this parameter to its initial value at the (array is + // deprecated beginning of execution of a transparency group XObject + // (see 11.6.6, in PDF 2.0) "Transparency group XObjects"). The value shall be + // either a name object, designating one of the standard blend modes listed in + // "Table 134 — Standard separable blend modes" and "Table 135 — Standard + // non-separable blend modes" in 11.3.5, "Blend mode", or an array of such names. + // In the latter case, the PDF reader shall use the first blend mode in the array + // that it recognises (or Normal if it recognises none of them). + // + // Initial value: Normal. + if (state.TryGet(NameToken.Bm, PdfScanner, out NameToken? bmToken)) + { + currentGraphicsState.BlendMode = bmToken.Data.ToBlendMode() ?? BlendMode.Normal; + } + else if (state.TryGet(NameToken.Bm, PdfScanner, out ArrayToken? bmArrayToken)) + { + // The PDF reader shall use the first blend mode in the array that it + // recognises (or Normal if it recognises none of them). + + currentGraphicsState.BlendMode = BlendMode.Normal; + + foreach (var token in bmArrayToken.Data.OfType()) + { + var bm = token.Data.ToBlendMode(); + if (bm.HasValue) + { + currentGraphicsState.BlendMode = bm.Value; + break; + } + } + } + + if (state.TryGet(NameToken.Smask, PdfScanner, out NameToken? smToken) && smToken.Equals(NameToken.None)) + { + currentGraphicsState.SoftMask = null; + } + else if (state.TryGet(NameToken.Smask, PdfScanner, out DictionaryToken? smDictToken)) + { + currentGraphicsState.SoftMask = SoftMask.Parse(smDictToken, PdfScanner, FilterProvider); + } + } + + /// + 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 is 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(Memory bytes) + { + if (InlineImageBuilder is 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. + /// + protected 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(double 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(double width) + { + GetCurrentState().LineWidth = width; + } + + /// + public virtual void SetMiterLimit(double limit) + { + GetCurrentState().MiterLimit = limit; + } + + /// + public virtual void MoveToNextLineWithOffset() + { + var tdOperation = new MoveToNextLineWithOffset(0, -1 * 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(TransformationMatrix value) + { var state = GetCurrentState(); - state.CurrentTransformationMatrix = value.Multiply(state.CurrentTransformationMatrix); - } - - /// - public virtual void SetCharacterSpacing(double spacing) - { - GetCurrentState().FontState.CharacterSpacing = spacing; - } - - /// - public abstract void PaintShading(NameToken shadingName); - } -} + state.CurrentTransformationMatrix = value.Multiply(state.CurrentTransformationMatrix); + } + + /// + public virtual void SetCharacterSpacing(double spacing) + { + GetCurrentState().FontState.CharacterSpacing = spacing; + } + + /// + public abstract void PaintShading(NameToken shadingName); + } +} diff --git a/src/UglyToad.PdfPig/Graphics/Core/RenderingModeExtensions.cs b/src/UglyToad.PdfPig/Graphics/Core/RenderingModeExtensions.cs index 88fafb64e..9c1233117 100644 --- a/src/UglyToad.PdfPig/Graphics/Core/RenderingModeExtensions.cs +++ b/src/UglyToad.PdfPig/Graphics/Core/RenderingModeExtensions.cs @@ -2,8 +2,14 @@ { using UglyToad.PdfPig.Core; - internal static class RenderingModeExtensions + /// + /// Convenience predicates for . + /// + public static class RenderingModeExtensions { + /// + /// True for the rendering modes that paint the glyph fill (0, 2, 4, 6). + /// public static bool IsFill(this TextRenderingMode mode) { return mode == TextRenderingMode.Fill @@ -12,6 +18,9 @@ public static bool IsFill(this TextRenderingMode mode) || mode == TextRenderingMode.FillThenStrokeClip; } + /// + /// True for the rendering modes that stroke the glyph outline (1, 2, 5, 6). + /// public static bool IsStroke(this TextRenderingMode mode) { return mode == TextRenderingMode.Stroke @@ -20,6 +29,10 @@ public static bool IsStroke(this TextRenderingMode mode) || mode == TextRenderingMode.FillThenStrokeClip; } + /// + /// True for the rendering modes that add the glyph outline to the text clipping path at + /// ET (4, 5, 6, 7). + /// public static bool IsClip(this TextRenderingMode mode) { return mode == TextRenderingMode.FillClip diff --git a/src/UglyToad.PdfPig/Graphics/IOperationContext.cs b/src/UglyToad.PdfPig/Graphics/IOperationContext.cs index 6a631dde1..59d6d6aba 100644 --- a/src/UglyToad.PdfPig/Graphics/IOperationContext.cs +++ b/src/UglyToad.PdfPig/Graphics/IOperationContext.cs @@ -41,6 +41,20 @@ public interface IOperationContext /// void PushState(); + /// + /// Begin a text object (BT). Initialises the text matrix and text line matrix to the + /// identity, and gives the renderer an opportunity to start accumulating a text clipping + /// path (PDF 1.7 §9.3.1, §9.3.6 — text rendering modes 4–7 affect the clipping path on ET). + /// + void BeginText(); + + /// + /// End the current text object (ET). Resets the text matrices and gives the renderer an + /// opportunity to apply any text clipping path that has been accumulated for rendering + /// modes 4–7 (PDF 1.7 §9.3.6). + /// + void EndText(); + /// /// Shows the text represented by the provided bytes using the current graphics state. /// diff --git a/src/UglyToad.PdfPig/Graphics/Operations/TextObjects/BeginText.cs b/src/UglyToad.PdfPig/Graphics/Operations/TextObjects/BeginText.cs index f872f8dca..034de5f93 100644 --- a/src/UglyToad.PdfPig/Graphics/Operations/TextObjects/BeginText.cs +++ b/src/UglyToad.PdfPig/Graphics/Operations/TextObjects/BeginText.cs @@ -1,7 +1,6 @@ namespace UglyToad.PdfPig.Graphics.Operations.TextObjects { using System.IO; - using PdfPig.Core; /// /// @@ -29,8 +28,7 @@ private BeginText() /// public void Run(IOperationContext operationContext) { - operationContext.TextMatrices.TextMatrix = TransformationMatrix.Identity; - operationContext.TextMatrices.TextLineMatrix = TransformationMatrix.Identity; + operationContext.BeginText(); } /// diff --git a/src/UglyToad.PdfPig/Graphics/Operations/TextObjects/EndText.cs b/src/UglyToad.PdfPig/Graphics/Operations/TextObjects/EndText.cs index eaa298bde..4f78f6c84 100644 --- a/src/UglyToad.PdfPig/Graphics/Operations/TextObjects/EndText.cs +++ b/src/UglyToad.PdfPig/Graphics/Operations/TextObjects/EndText.cs @@ -1,7 +1,6 @@ namespace UglyToad.PdfPig.Graphics.Operations.TextObjects { using System.IO; - using PdfPig.Core; /// /// @@ -29,8 +28,7 @@ private EndText() /// public void Run(IOperationContext operationContext) { - operationContext.TextMatrices.TextMatrix = TransformationMatrix.Identity; - operationContext.TextMatrices.TextLineMatrix = TransformationMatrix.Identity; + operationContext.EndText(); } ///