namespace UglyToad.PdfPig.Graphics { using Colors; using Content; using Core; using Filters; using Geometry; using Logging; using Operations; using Parser; using PdfFonts; using PdfPig.Core; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using Tokenization.Scanner; using Tokens; using Operations.TextPositioning; using Util; using XObjects; using static PdfPig.Core.PdfSubpath; internal class ContentStreamProcessor : IOperationContext { /// /// Stores each letter as it is encountered in the content stream. /// private readonly List letters = new List(); /// /// Stores each path as it is encountered in the content stream. /// private readonly List paths = new List(); /// /// Stores a link to each image (either inline or XObject) as it is encountered in the content stream. /// private readonly List> images = new List>(); /// /// Stores each marked content as it is encountered in the content stream. /// private readonly List markedContents = new List(); private readonly IResourceStore resourceStore; private readonly UserSpaceUnit userSpaceUnit; private readonly PageRotationDegrees rotation; private readonly IPdfTokenScanner pdfScanner; private readonly IPageContentParser pageContentParser; private readonly ILookupFilterProvider filterProvider; private readonly InternalParsingOptions parsingOptions; private readonly MarkedContentStack markedContentStack = new MarkedContentStack(); private Stack graphicsStack = new Stack(); private IFont activeExtendedGraphicsStateFont; private InlineImageBuilder inlineImageBuilder; private int pageNumber; /// /// A counter to track individual calls to operations used to determine if letters are likely to be /// in the same word/group. This exposes internal grouping of letters used by the PDF creator which may correspond to the /// intended grouping of letters into words. /// private int textSequence; public TextMatrices TextMatrices { get; } = new TextMatrices(); public TransformationMatrix CurrentTransformationMatrix => GetCurrentState().CurrentTransformationMatrix; public PdfSubpath CurrentSubpath { get; private set; } public PdfPath CurrentPath { get; private set; } public PdfPoint CurrentPosition { get; set; } public int StackSize => graphicsStack.Count; private readonly Dictionary> xObjects = new Dictionary> { {XObjectType.Image, new List()}, {XObjectType.PostScript, new List()} }; public ContentStreamProcessor( int pageNumber, IResourceStore resourceStore, UserSpaceUnit userSpaceUnit, MediaBox mediaBox, CropBox cropBox, PageRotationDegrees rotation, IPdfTokenScanner pdfScanner, IPageContentParser pageContentParser, ILookupFilterProvider filterProvider, InternalParsingOptions parsingOptions) { this.pageNumber = pageNumber; this.resourceStore = resourceStore; this.userSpaceUnit = userSpaceUnit; this.rotation = rotation; this.pdfScanner = pdfScanner ?? throw new ArgumentNullException(nameof(pdfScanner)); this.pageContentParser = pageContentParser ?? throw new ArgumentNullException(nameof(pageContentParser)); this.filterProvider = filterProvider ?? throw new ArgumentNullException(nameof(filterProvider)); this.parsingOptions = parsingOptions; TransformationMatrix initialMatrix = GetInitialMatrix(userSpaceUnit, mediaBox, cropBox, rotation, parsingOptions.Logger); graphicsStack.Push(new CurrentGraphicsState() { CurrentTransformationMatrix = initialMatrix, CurrentClippingPath = GetInitialClipping(cropBox, initialMatrix), ColorSpaceContext = new ColorSpaceContext(GetCurrentState, resourceStore) }); } /// /// Get the initial clipping path using the crop box and the initial transformation matrix. /// private static PdfPath GetInitialClipping(CropBox cropBox, TransformationMatrix initialMatrix) { var transformedCropBox = initialMatrix.Transform(cropBox.Bounds); // We re-compute width and height to get possible negative values. double width = transformedCropBox.TopRight.X - transformedCropBox.BottomLeft.X; double height = transformedCropBox.TopRight.Y - transformedCropBox.BottomLeft.Y; // initiate CurrentClippingPath to cropBox var clippingSubpath = new PdfSubpath(); clippingSubpath.Rectangle(transformedCropBox.BottomLeft.X, transformedCropBox.BottomLeft.Y, width, height); var clippingPath = new PdfPath() { clippingSubpath }; clippingPath.SetClipping(FillingRule.EvenOdd); return clippingPath; } [System.Diagnostics.Contracts.Pure] internal static TransformationMatrix GetInitialMatrix(UserSpaceUnit userSpaceUnit, MediaBox mediaBox, CropBox cropBox, PageRotationDegrees rotation, ILog log) { // Cater for scenario where the cropbox is larger than the mediabox. // If there is no intersection (method returns null), fall back to the cropbox. var viewBox = mediaBox.Bounds.Intersect(cropBox.Bounds) ?? cropBox.Bounds; if (rotation.Value == 0 && viewBox.Left == 0 && viewBox.Bottom == 0 && userSpaceUnit.PointMultiples == 1) { return TransformationMatrix.Identity; } // Move points so that (0,0) is equal to the viewbox bottom left corner. var t1 = TransformationMatrix.GetTranslationMatrix(-viewBox.Left, -viewBox.Bottom); if (userSpaceUnit.PointMultiples != 1) { log.Warn("User space unit other than 1 is not implemented"); } // After rotating around the origin, our points will have negative x/y coordinates. // Fix this by translating them by a certain dx/dy after rotation based on the viewbox. double dx, dy; switch (rotation.Value) { case 0: // No need to rotate / translate after rotation, just return the initial // translation matrix. return t1; case 90: // Move rotated points up by our (unrotated) viewbox width dx = 0; dy = viewBox.Width; break; case 180: // Move rotated points up/right using the (unrotated) viewbox width/height dx = viewBox.Width; dy = viewBox.Height; break; case 270: // Move rotated points right using the (unrotated) viewbox height dx = viewBox.Height; dy = 0; break; default: throw new InvalidOperationException($"Invalid value for page rotation: {rotation.Value}."); } // GetRotationMatrix uses counter clockwise angles, whereas our page rotation // is a clockwise angle, so flip the sign. var r = TransformationMatrix.GetRotationMatrix(-rotation.Value); // Fix up negative coordinates after rotation var t2 = TransformationMatrix.GetTranslationMatrix(dx, dy); // Now get the final combined matrix T1 > R > T2 return t1.Multiply(r.Multiply(t2)); } public PageContent Process(int pageNumberCurrent, IReadOnlyList operations) { pageNumber = pageNumberCurrent; CloneAllStates(); ProcessOperations(operations); return new PageContent(operations, letters, paths, images, markedContents, pdfScanner, filterProvider, resourceStore); } private void ProcessOperations(IReadOnlyList operations) { foreach (var stateOperation in operations) { stateOperation.Run(this); } } private Stack CloneAllStates() { var saved = graphicsStack; graphicsStack = new Stack(); graphicsStack.Push(saved.Peek().DeepClone()); return saved; } [DebuggerStepThrough] public CurrentGraphicsState GetCurrentState() { return graphicsStack.Peek(); } public void PopState() { graphicsStack.Pop(); activeExtendedGraphicsStateFont = null; } public void PushState() { graphicsStack.Push(graphicsStack.Peek().DeepClone()); } public void ShowText(IInputBytes bytes) { var currentState = GetCurrentState(); var font = currentState.FontState.FromExtendedGraphicsState ? activeExtendedGraphicsStateFont : resourceStore.GetFont(currentState.FontState.FontName); if (font == null) { if (parsingOptions.SkipMissingFonts) { parsingOptions.Logger.Warn($"Skipping a missing font with name {currentState.FontState.FontName} " + $"since it is not present in the document and {nameof(InternalParsingOptions.SkipMissingFonts)} " + "is set to true. This may result in some text being skipped and not included in the output."); return; } throw new InvalidOperationException($"Could not find the font with name {currentState.FontState.FontName} in the resource store. It has not been loaded yet."); } var fontSize = currentState.FontState.FontSize; var horizontalScaling = currentState.FontState.HorizontalScaling / 100.0; var characterSpacing = currentState.FontState.CharacterSpacing; var rise = currentState.FontState.Rise; var transformationMatrix = currentState.CurrentTransformationMatrix; var renderingMatrix = TransformationMatrix.FromValues(fontSize * horizontalScaling, 0, 0, fontSize, 0, rise); var pointSize = Math.Round(transformationMatrix.Multiply(TextMatrices.TextMatrix).Transform(new PdfRectangle(0, 0, 1, fontSize)).Height, 2); while (bytes.MoveNext()) { var code = font.ReadCharacterCode(bytes, out int codeLength); var foundUnicode = font.TryGetUnicode(code, out var unicode); if (!foundUnicode || unicode == null) { parsingOptions.Logger.Warn($"We could not find the corresponding character with code {code} in font {font.Name}."); // Try casting directly to string as in PDFBox 1.8. unicode = new string((char)code, 1); } var wordSpacing = 0.0; if (code == ' ' && codeLength == 1) { wordSpacing += GetCurrentState().FontState.WordSpacing; } var textMatrix = TextMatrices.TextMatrix; if (font.IsVertical) { if (!(font is IVerticalWritingSupported verticalFont)) { throw new InvalidOperationException($"Font {font.Name} was in vertical writing mode but did not implement {nameof(IVerticalWritingSupported)}."); } var positionVector = verticalFont.GetPositionVector(code); textMatrix = textMatrix.Translate(positionVector.X, positionVector.Y); } var boundingBox = font.GetBoundingBox(code); var transformedGlyphBounds = PerformantRectangleTransformer .Transform(renderingMatrix, textMatrix, transformationMatrix, boundingBox.GlyphBounds); var transformedPdfBounds = PerformantRectangleTransformer .Transform(renderingMatrix, textMatrix, transformationMatrix, new PdfRectangle(0, 0, boundingBox.Width, 0)); Letter letter = null; if (Diacritics.IsInCombiningDiacriticRange(unicode) && bytes.CurrentOffset > 0 && letters.Count > 0) { var attachTo = letters[letters.Count - 1]; if (attachTo.TextSequence == textSequence && Diacritics.TryCombineDiacriticWithPreviousLetter(unicode, attachTo.Value, out var newLetter)) { // TODO: union of bounding boxes. letters.Remove(attachTo); letter = new Letter( newLetter, attachTo.GlyphRectangle, attachTo.StartBaseLine, attachTo.EndBaseLine, attachTo.Width, attachTo.FontSize, attachTo.Font, attachTo.RenderingMode, attachTo.StrokeColor, attachTo.FillColor, attachTo.PointSize, attachTo.TextSequence); } } // If we did not create a letter for a combined diacritic, create one here. if (letter == null) { letter = new Letter( unicode, transformedGlyphBounds, transformedPdfBounds.BottomLeft, transformedPdfBounds.BottomRight, transformedPdfBounds.Width, fontSize, font.Details, currentState.FontState.TextRenderingMode, currentState.CurrentStrokingColor, currentState.CurrentNonStrokingColor, pointSize, textSequence); } letters.Add(letter); markedContentStack.AddLetter(letter); double tx, ty; if (font.IsVertical) { var verticalFont = (IVerticalWritingSupported)font; var displacement = verticalFont.GetDisplacementVector(code); tx = 0; ty = (displacement.Y * fontSize) + characterSpacing + wordSpacing; } else { tx = (boundingBox.Width * fontSize + characterSpacing + wordSpacing) * horizontalScaling; ty = 0; } TextMatrices.TextMatrix = TextMatrices.TextMatrix.Translate(tx, ty); } } public void ShowPositionedText(IReadOnlyList tokens) { textSequence++; var currentState = GetCurrentState(); var textState = currentState.FontState; var fontSize = textState.FontSize; var horizontalScaling = textState.HorizontalScaling / 100.0; var font = resourceStore.GetFont(textState.FontName); if (font == null) { if (parsingOptions.SkipMissingFonts) { parsingOptions.Logger.Warn($"Skipping a missing font with name {currentState.FontState.FontName} " + $"since it is not present in the document and {nameof(InternalParsingOptions.SkipMissingFonts)} " + "is set to true. This may result in some text being skipped and not included in the output."); return; } throw new InvalidOperationException($"Could not find the font with name {currentState.FontState.FontName} in the resource store. It has not been loaded yet."); } var isVertical = font.IsVertical; foreach (var token in tokens) { if (token is NumericToken number) { var positionAdjustment = (double)number.Data; double tx, ty; if (isVertical) { tx = 0; ty = -positionAdjustment / 1000 * fontSize; } else { tx = -positionAdjustment / 1000 * fontSize * horizontalScaling; ty = 0; } AdjustTextMatrix(tx, ty); } else { IReadOnlyList bytes; if (token is HexToken hex) { bytes = hex.Bytes; } else { bytes = OtherEncodings.StringAsLatin1Bytes(((StringToken)token).Data); } ShowText(new ByteArrayInputBytes(bytes)); } } } public 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); images.Add(Union.One(contentRecord)); markedContentStack.AddXObject(contentRecord, pdfScanner, filterProvider, resourceStore); } else if (subType.Equals(NameToken.Form)) { ProcessFormXObject(xObjectStream, xObjectName); } else { throw new InvalidOperationException($"XObject encountered with unexpected SubType {subType}. {xObjectStream.StreamDictionary}."); } } private void ProcessFormXObject(StreamToken formStream, NameToken xObjectName) { /* * When a form XObject is invoked the following should happen: * * 1. Save the current graphics state, as if by invoking the q operator. * 2. Concatenate the matrix from the form dictionary's Matrix entry with the current transformation matrix. * 3. Clip according to the form dictionary's BBox entry. * 4. Paint the graphics objects specified in the form's content stream. * 5. Restore the saved graphics state, as if by invoking the Q operator. */ var hasResources = formStream.StreamDictionary.TryGet(NameToken.Resources, pdfScanner, out var formResources); if (hasResources) { resourceStore.LoadResourceDictionary(formResources, parsingOptions); } // 1. Save current state. PushState(); var startState = GetCurrentState(); // Transparency Group XObjects if (formStream.StreamDictionary.TryGet(NameToken.Group, pdfScanner, out DictionaryToken formGroupToken)) { if (!formGroupToken.TryGet(NameToken.S, pdfScanner, out var sToken) || sToken != NameToken.Transparency) { throw new InvalidOperationException($"Invalid Transparency Group XObject, '{NameToken.S}' token is not set or not equal to '{NameToken.Transparency}'."); } /* blend mode * A conforming reader shall implicitly reset this parameter to its initial value at the beginning of execution of a * transparency group XObject (see 11.6.6, "Transparency Group XObjects"). Initial value: Normal. */ //startState.BlendMode = BlendMode.Normal; /* soft mask * A conforming reader shall implicitly reset this parameter implicitly reset to its initial value at the beginning * of execution of a transparency group XObject (see 11.6.6, "Transparency Group XObjects"). Initial value: None. */ // TODO /* alpha constant * A conforming reader shall implicitly reset this parameter to its initial value at the beginning of execution of a * transparency group XObject (see 11.6.6, "Transparency Group XObjects"). Initial value: 1.0. */ startState.AlphaConstantNonStroking = 1.0m; startState.AlphaConstantStroking = 1.0m; if (formGroupToken.TryGet(NameToken.Cs, pdfScanner, out NameToken csNameToken)) { startState.ColorSpaceContext.SetNonStrokingColorspace(csNameToken); } else if (formGroupToken.TryGet(NameToken.Cs, pdfScanner, out ArrayToken csArrayToken) && csArrayToken.Length > 0) { if (csArrayToken.Data[0] is NameToken firstColorSpaceName) { startState.ColorSpaceContext.SetNonStrokingColorspace(firstColorSpaceName, formGroupToken); } else { throw new InvalidOperationException("Invalid color space in Transparency Group XObjects."); } } bool isolated = false; if (formGroupToken.TryGet(NameToken.I, pdfScanner, out BooleanToken isolatedToken)) { /* * (Optional) A flag specifying whether the transparency group is isolated (see “Isolated Groups”). * If this flag is true, objects within the group shall be composited against a fully transparent * initial backdrop; if false, they shall be composited against the group’s backdrop. * Default value: false. */ isolated = isolatedToken.Data; } bool knockout = false; if (formGroupToken.TryGet(NameToken.K, pdfScanner, out BooleanToken knockoutToken)) { /* * (Optional) A flag specifying whether the transparency group is a knockout group (see “Knockout Groups”). * If this flag is false, later objects within the group shall be composited with earlier ones with which * they overlap; if true, they shall be composited with the group’s initial backdrop and shall overwrite * (“knock out”) any earlier overlapping objects. * Default value: false. */ knockout = knockoutToken.Data; } } var formMatrix = TransformationMatrix.Identity; if (formStream.StreamDictionary.TryGet(NameToken.Matrix, pdfScanner, out var formMatrixToken)) { formMatrix = TransformationMatrix.FromArray(formMatrixToken.Data.OfType().Select(x => x.Double).ToArray()); } // 2. Update current transformation matrix. startState.CurrentTransformationMatrix = formMatrix.Multiply(startState.CurrentTransformationMatrix); var contentStream = formStream.Decode(filterProvider, pdfScanner); var operations = pageContentParser.Parse(pageNumber, new ByteArrayInputBytes(contentStream), parsingOptions.Logger); // 3. We don't respect clipping currently. // 4. Paint the objects. bool hasCircularReference = HasFormXObjectCircularReference(formStream, xObjectName, operations); if (hasCircularReference) { if (parsingOptions.UseLenientParsing) { operations = operations.Where(o => o is not InvokeNamedXObject xo || xo.Name != xObjectName).ToArray(); parsingOptions.Logger.Warn($"An XObject form named '{xObjectName}' is referencing itself which can cause unexpected behaviour. The self reference was removed from the operations before further processing."); } else { throw new PdfDocumentFormatException($"An XObject form named '{xObjectName}' is referencing itself which can cause unexpected behaviour."); } } ProcessOperations(operations); // 5. Restore saved state. PopState(); if (hasResources) { resourceStore.UnloadResourceDictionary(); } } /// /// Check for circular reference in the XObject form. /// /// The original form stream. /// The form's name. /// The form operations parsed from original form stream. private bool HasFormXObjectCircularReference(StreamToken formStream, NameToken xObjectName, IReadOnlyList operations) { return xObjectName != null && operations.OfType()?.Any(o => o.Name == xObjectName) == true // operations contain another form with same name && resourceStore.TryGetXObject(xObjectName, out var result) && result.Data.SequenceEqual(formStream.Data); // The form contained in the operations has identical data to current form } public void BeginSubpath() { if (CurrentPath == null) { CurrentPath = new PdfPath(); } AddCurrentSubpath(); CurrentSubpath = new PdfSubpath(); } public PdfPoint? CloseSubpath() { if (CurrentSubpath == null) { return null; } PdfPoint point; if (CurrentSubpath.Commands[0] is Move move) { point = move.Location; } else { throw new ArgumentException("CloseSubpath(): first command not Move."); } CurrentSubpath.CloseSubpath(); AddCurrentSubpath(); return point; } public void AddCurrentSubpath() { if (CurrentSubpath == null) { return; } CurrentPath.Add(CurrentSubpath); CurrentSubpath = null; } public void StrokePath(bool close) { if (CurrentPath == null) { return; } CurrentPath.SetStroked(); if (close) { CurrentSubpath?.CloseSubpath(); } ClosePath(); } public void FillPath(FillingRule fillingRule, bool close) { if (CurrentPath == null) { return; } CurrentPath.SetFilled(fillingRule); if (close) { CurrentSubpath?.CloseSubpath(); } ClosePath(); } public void FillStrokePath(FillingRule fillingRule, bool close) { if (CurrentPath == null) { return; } CurrentPath.SetFilled(fillingRule); CurrentPath.SetStroked(); if (close) { CurrentSubpath?.CloseSubpath(); } ClosePath(); } public void MoveTo(double x, double y) { BeginSubpath(); var point = CurrentTransformationMatrix.Transform(new PdfPoint(x, y)); CurrentPosition = point; CurrentSubpath.MoveTo(point.X, point.Y); } public void BezierCurveTo(double x2, double y2, double x3, double y3) { if (CurrentSubpath == null) { return; } var controlPoint2 = CurrentTransformationMatrix.Transform(new PdfPoint(x2, y2)); var end = CurrentTransformationMatrix.Transform(new PdfPoint(x3, y3)); CurrentSubpath.BezierCurveTo(CurrentPosition.X, CurrentPosition.Y, controlPoint2.X, controlPoint2.Y, end.X, end.Y); CurrentPosition = end; } public void BezierCurveTo(double x1, double y1, double x2, double y2, double x3, double y3) { if (CurrentSubpath == null) { return; } var controlPoint1 = CurrentTransformationMatrix.Transform(new PdfPoint(x1, y1)); var controlPoint2 = CurrentTransformationMatrix.Transform(new PdfPoint(x2, y2)); var end = CurrentTransformationMatrix.Transform(new PdfPoint(x3, y3)); CurrentSubpath.BezierCurveTo(controlPoint1.X, controlPoint1.Y, controlPoint2.X, controlPoint2.Y, end.X, end.Y); CurrentPosition = end; } public void LineTo(double x, double y) { if (CurrentSubpath == null) { return; } var endPoint = CurrentTransformationMatrix.Transform(new PdfPoint(x, y)); CurrentSubpath.LineTo(endPoint.X, endPoint.Y); CurrentPosition = endPoint; } public void Rectangle(double x, double y, double width, double height) { BeginSubpath(); var lowerLeft = CurrentTransformationMatrix.Transform(new PdfPoint(x, y)); var upperRight = CurrentTransformationMatrix.Transform(new PdfPoint(x + width, y + height)); CurrentSubpath.Rectangle(lowerLeft.X, lowerLeft.Y, upperRight.X - lowerLeft.X, upperRight.Y - lowerLeft.Y); AddCurrentSubpath(); } public void EndPath() { if (CurrentPath == null) { return; } AddCurrentSubpath(); if (CurrentPath.IsClipping) { if (!parsingOptions.ClipPaths) { // if we don't clip paths, add clipping path to paths paths.Add(CurrentPath); markedContentStack.AddPath(CurrentPath); } CurrentPath = null; return; } paths.Add(CurrentPath); markedContentStack.AddPath(CurrentPath); CurrentPath = null; } public void ClosePath() { AddCurrentSubpath(); if (CurrentPath.IsClipping) { EndPath(); return; } var currentState = GetCurrentState(); if (CurrentPath.IsStroked) { CurrentPath.LineDashPattern = currentState.LineDashPattern; CurrentPath.StrokeColor = currentState.CurrentStrokingColor; CurrentPath.LineWidth = currentState.LineWidth; CurrentPath.LineCapStyle = currentState.CapStyle; CurrentPath.LineJoinStyle = currentState.JoinStyle; } if (CurrentPath.IsFilled) { CurrentPath.FillColor = currentState.CurrentNonStrokingColor; } if (parsingOptions.ClipPaths) { var clippedPath = currentState.CurrentClippingPath.Clip(CurrentPath, parsingOptions.Logger); if (clippedPath != null) { paths.Add(clippedPath); markedContentStack.AddPath(clippedPath); } } else { paths.Add(CurrentPath); markedContentStack.AddPath(CurrentPath); } CurrentPath = null; } public void ModifyClippingIntersect(FillingRule clippingRule) { if (CurrentPath == null) { return; } AddCurrentSubpath(); CurrentPath.SetClipping(clippingRule); if (parsingOptions.ClipPaths) { var currentClipping = GetCurrentState().CurrentClippingPath; currentClipping.SetClipping(clippingRule); var newClippings = CurrentPath.Clip(currentClipping, parsingOptions.Logger); if (newClippings == null) { parsingOptions.Logger.Warn("Empty clipping path found. Clipping path not updated."); } else { GetCurrentState().CurrentClippingPath = newClippings; } } } public void SetNamedGraphicsState(NameToken stateName) { var currentGraphicsState = GetCurrentState(); var state = resourceStore.GetExtendedGraphicsStateDictionary(stateName); if (state.TryGet(NameToken.Lw, pdfScanner, out NumericToken lwToken)) { currentGraphicsState.LineWidth = lwToken.Data; } if (state.TryGet(NameToken.Lc, pdfScanner, out NumericToken lcToken)) { currentGraphicsState.CapStyle = (LineCapStyle)lcToken.Int; } if (state.TryGet(NameToken.Lj, pdfScanner, out NumericToken ljToken)) { currentGraphicsState.JoinStyle = (LineJoinStyle)ljToken.Int; } if (state.TryGet(NameToken.Font, pdfScanner, out ArrayToken fontArray) && fontArray.Length == 2 && fontArray.Data[0] is IndirectReferenceToken fontReference && fontArray.Data[1] is NumericToken sizeToken) { currentGraphicsState.FontState.FromExtendedGraphicsState = true; currentGraphicsState.FontState.FontSize = (double)sizeToken.Data; activeExtendedGraphicsStateFont = resourceStore.GetFontDirectly(fontReference); } if (state.TryGet(NameToken.Ais, pdfScanner, out BooleanToken aisToken)) { // The alpha source flag (“alpha is shape”), specifying // whether the current soft mask and alpha constant are to be interpreted as // shape values (true) or opacity values (false). currentGraphicsState.AlphaSource = aisToken.Data; } if (state.TryGet(NameToken.Ca, pdfScanner, out NumericToken caToken)) { // (Optional; PDF 1.4) The current stroking alpha constant, specifying the constant // shape or constant opacity value to be used for stroking operations in the // transparent imaging model (see “Source Shape and Opacity” on page 526 and // “Constant Shape and Opacity” on page 551). currentGraphicsState.AlphaConstantStroking = caToken.Data; } if (state.TryGet(NameToken.CaNs, pdfScanner, out NumericToken cansToken)) { // (Optional; PDF 1.4) The current stroking alpha constant, specifying the constant // shape or constant opacity value to be used for NON-stroking operations in the // transparent imaging model (see “Source Shape and Opacity” on page 526 and // “Constant Shape and Opacity” on page 551). currentGraphicsState.AlphaConstantNonStroking = cansToken.Data; } if (state.TryGet(NameToken.Op, pdfScanner, out BooleanToken OPToken)) { // (Optional) A flag specifying whether to apply overprint (see Section 4.5.6, // “Overprint Control”). In PDF 1.2 and earlier, there is a single overprint // parameter that applies to all painting operations. Beginning with PDF 1.3, // there are two separate overprint parameters: one for stroking and one for all // other painting operations. Specifying an OP entry sets both parameters unless there // is also an op entry in the same graphics state parameter dictionary, // in which case the OP entry sets only the overprint parameter for stroking. currentGraphicsState.Overprint = OPToken.Data; } if (state.TryGet(NameToken.OpNs, pdfScanner, out BooleanToken opToken)) { // (Optional; PDF 1.3) A flag specifying whether to apply overprint (see Section // 4.5.6, “Overprint Control”) for painting operations other than stroking. If // this entry is absent, the OP entry, if any, sets this parameter. currentGraphicsState.NonStrokingOverprint = opToken.Data; } if (state.TryGet(NameToken.Opm, pdfScanner, out NumericToken opmToken)) { // (Optional; PDF 1.3) The overprint mode (see Section 4.5.6, “Overprint Control”). currentGraphicsState.OverprintMode = opmToken.Data; } if (state.TryGet(NameToken.Sa, pdfScanner, out BooleanToken saToken)) { // (Optional) A flag specifying whether to apply automatic stroke adjustment // (see Section 6.5.4, “Automatic Stroke Adjustment”). currentGraphicsState.StrokeAdjustment = saToken.Data; } } public void BeginInlineImage() { if (inlineImageBuilder != null) { parsingOptions.Logger.Error("Begin inline image (BI) command encountered while another inline image was active."); } inlineImageBuilder = new InlineImageBuilder(); } public void SetInlineImageProperties(IReadOnlyDictionary properties) { if (inlineImageBuilder == null) { parsingOptions.Logger.Error("Begin inline image data (ID) command encountered without a corresponding begin inline image (BI) command."); return; } inlineImageBuilder.Properties = properties; } public void EndInlineImage(IReadOnlyList bytes) { if (inlineImageBuilder == null) { parsingOptions.Logger.Error("End inline image (EI) command encountered without a corresponding begin inline image (BI) command."); return; } inlineImageBuilder.Bytes = bytes; var image = inlineImageBuilder.CreateInlineImage(CurrentTransformationMatrix, filterProvider, pdfScanner, GetCurrentState().RenderingIntent, resourceStore); images.Add(Union.Two(image)); markedContentStack.AddImage(image); inlineImageBuilder = null; } public void BeginMarkedContent(NameToken name, NameToken propertyDictionaryName, DictionaryToken properties) { if (propertyDictionaryName != null) { var actual = resourceStore.GetMarkedContentPropertiesDictionary(propertyDictionaryName); properties = actual ?? properties; } markedContentStack.Push(name, properties); } public void EndMarkedContent() { if (markedContentStack.CanPop) { var mc = markedContentStack.Pop(pdfScanner); if (mc != null) { markedContents.Add(mc); } } } private void AdjustTextMatrix(double tx, double ty) { var matrix = TransformationMatrix.GetTranslationMatrix(tx, ty); TextMatrices.TextMatrix = matrix.Multiply(TextMatrices.TextMatrix); } public void SetFlatnessTolerance(decimal tolerance) { GetCurrentState().Flatness = tolerance; } public void SetLineCap(LineCapStyle cap) { GetCurrentState().CapStyle = cap; } public void SetLineDashPattern(LineDashPattern pattern) { GetCurrentState().LineDashPattern = pattern; } public void SetLineJoin(LineJoinStyle join) { GetCurrentState().JoinStyle = join; } public void SetLineWidth(decimal width) { GetCurrentState().LineWidth = width; } public void SetMiterLimit(decimal limit) { GetCurrentState().MiterLimit = limit; } public void MoveToNextLineWithOffset() { var tdOperation = new MoveToNextLineWithOffset(0, -1 * (decimal)GetCurrentState().FontState.Leading); tdOperation.Run(this); } public void SetFontAndSize(NameToken font, double size) { var currentState = GetCurrentState(); currentState.FontState.FontSize = size; currentState.FontState.FontName = font; } public void SetHorizontalScaling(double scale) { GetCurrentState().FontState.HorizontalScaling = scale; } public void SetTextLeading(double leading) { GetCurrentState().FontState.Leading = leading; } public void SetTextRenderingMode(TextRenderingMode mode) { GetCurrentState().FontState.TextRenderingMode = mode; } public void SetTextRise(double rise) { GetCurrentState().FontState.Rise = rise; } public void SetWordSpacing(double spacing) { GetCurrentState().FontState.WordSpacing = spacing; } public void ModifyCurrentTransformationMatrix(double[] value) { var ctm = GetCurrentState().CurrentTransformationMatrix; GetCurrentState().CurrentTransformationMatrix = TransformationMatrix.FromArray(value).Multiply(ctm); } public void SetCharacterSpacing(double spacing) { GetCurrentState().FontState.CharacterSpacing = spacing; } public void PaintShading(NameToken shadingName) { // We do nothing for the moment // Do the following if you need to access the shading: // var shading = resourceStore.GetShading(shadingName); } } }