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();
}
///