PdfPig/src/UglyToad.PdfPig/Graphics/ContentStreamProcessor.cs

1075 lines
41 KiB
C#
Raw Normal View History

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;
2022-04-15 08:14:09 +08:00
using Util;
using XObjects;
using static PdfPig.Core.PdfSubpath;
internal class ContentStreamProcessor : IOperationContext
{
/// <summary>
/// Stores each letter as it is encountered in the content stream.
/// </summary>
private readonly List<Letter> letters = new List<Letter>();
/// <summary>
/// Stores each path as it is encountered in the content stream.
/// </summary>
2020-04-02 23:39:48 +08:00
private readonly List<PdfPath> paths = new List<PdfPath>();
/// <summary>
/// Stores a link to each image (either inline or XObject) as it is encountered in the content stream.
/// </summary>
private readonly List<Union<XObjectContentRecord, InlineImage>> images = new List<Union<XObjectContentRecord, InlineImage>>();
/// <summary>
/// Stores each marked content as it is encountered in the content stream.
/// </summary>
private readonly List<MarkedContentElement> markedContents = new List<MarkedContentElement>();
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<CurrentGraphicsState> graphicsStack = new Stack<CurrentGraphicsState>();
private IFont activeExtendedGraphicsStateFont;
private InlineImageBuilder inlineImageBuilder;
private int pageNumber;
/// <summary>
/// A counter to track individual calls to <see cref="ShowText"/> 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.
/// </summary>
private int textSequence;
2019-08-12 02:55:59 +08:00
public TextMatrices TextMatrices { get; } = new TextMatrices();
public TransformationMatrix CurrentTransformationMatrix => GetCurrentState().CurrentTransformationMatrix;
2020-04-02 23:39:48 +08:00
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<XObjectType, List<XObjectContentRecord>> xObjects = new Dictionary<XObjectType, List<XObjectContentRecord>>
{
{XObjectType.Image, new List<XObjectContentRecord>()},
{XObjectType.PostScript, new List<XObjectContentRecord>()}
};
2023-04-07 05:34:32 +08:00
public ContentStreamProcessor(IResourceStore resourceStore,
UserSpaceUnit userSpaceUnit,
MediaBox mediaBox,
CropBox cropBox,
PageRotationDegrees rotation,
IPdfTokenScanner pdfScanner,
IPageContentParser pageContentParser,
ILookupFilterProvider filterProvider,
InternalParsingOptions parsingOptions)
{
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;
// initiate CurrentClippingPath to cropBox
var clippingSubpath = new PdfSubpath();
clippingSubpath.Rectangle(cropBox.Bounds.BottomLeft.X, cropBox.Bounds.BottomLeft.Y, cropBox.Bounds.Width, cropBox.Bounds.Height);
var clippingPath = new PdfPath() { clippingSubpath };
clippingPath.SetClipping(FillingRule.EvenOdd);
graphicsStack.Push(new CurrentGraphicsState()
{
CurrentTransformationMatrix = GetInitialMatrix(userSpaceUnit, mediaBox, cropBox, rotation, parsingOptions.Logger),
2023-04-07 05:34:32 +08:00
CurrentClippingPath = clippingPath,
ColorSpaceContext = new ColorSpaceContext(GetCurrentState, resourceStore)
});
}
[System.Diagnostics.Contracts.Pure]
2023-04-07 05:34:32 +08:00
internal static TransformationMatrix GetInitialMatrix(UserSpaceUnit userSpaceUnit,
MediaBox mediaBox,
2023-04-07 05:34:32 +08:00
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<IGraphicsStateOperation> operations)
{
pageNumber = pageNumberCurrent;
CloneAllStates();
ProcessOperations(operations);
return new PageContent(operations, letters, paths, images, markedContents, pdfScanner, filterProvider, resourceStore);
}
private void ProcessOperations(IReadOnlyList<IGraphicsStateOperation> operations)
{
foreach (var stateOperation in operations)
{
2019-01-04 06:20:53 +08:00
stateOperation.Run(this);
}
}
private Stack<CurrentGraphicsState> CloneAllStates()
{
var saved = graphicsStack;
graphicsStack = new Stack<CurrentGraphicsState>();
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;
2018-04-12 06:18:24 +08:00
var rise = currentState.FontState.Rise;
var transformationMatrix = currentState.CurrentTransformationMatrix;
2018-04-12 06:18:24 +08:00
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);
2020-01-10 22:40:28 +08:00
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);
}
2018-04-12 06:18:24 +08:00
var boundingBox = font.GetBoundingBox(code);
2020-04-18 19:10:17 +08:00
var transformedGlyphBounds = PerformantRectangleTransformer
.Transform(renderingMatrix, textMatrix, transformationMatrix, boundingBox.GlyphBounds);
2020-04-18 19:10:17 +08:00
var transformedPdfBounds = PerformantRectangleTransformer
.Transform(renderingMatrix, textMatrix, transformationMatrix, new PdfRectangle(0, 0, boundingBox.Width, 0));
2022-04-15 08:14:09 +08:00
Letter letter = null;
if (Diacritics.IsInCombiningDiacriticRange(unicode) && bytes.CurrentOffset > 0 && letters.Count > 0)
2022-04-15 08:14:09 +08:00
{
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,
2022-04-15 08:14:09 +08:00
attachTo.PointSize,
attachTo.TextSequence);
}
}
// If we did not create a letter for a combined diacritic, create one here.
if (letter == null)
2022-04-15 08:14:09 +08:00
{
letter = new Letter(
unicode,
transformedGlyphBounds,
transformedPdfBounds.BottomLeft,
transformedPdfBounds.BottomRight,
transformedPdfBounds.Width,
fontSize,
font.Details,
currentState.FontState.TextRenderingMode,
currentState.CurrentStrokingColor,
currentState.CurrentNonStrokingColor,
2022-04-15 08:14:09 +08:00
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<IToken> tokens)
{
2019-08-12 02:55:59 +08:00
textSequence++;
var currentState = GetCurrentState();
var textState = currentState.FontState;
var fontSize = textState.FontSize;
var horizontalScaling = textState.HorizontalScaling / 100.0;
var font = resourceStore.GetFont(textState.FontName);
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<byte> bytes;
if (token is HexToken hex)
{
bytes = hex.Bytes;
}
else
{
bytes = OtherEncodings.StringAsLatin1Bytes(((StringToken)token).Data);
}
ShowText(new ByteArrayInputBytes(bytes));
}
}
}
2019-01-04 06:20:53 +08:00
public void ApplyXObject(NameToken xObjectName)
{
2019-01-04 06:20:53 +08:00
var xObjectStream = resourceStore.GetXObject(xObjectName);
// 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,
2023-04-07 05:34:32 +08:00
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,
2023-04-07 05:34:32 +08:00
state.ColorSpaceContext?.CurrentStrokingColorSpace ?? DeviceRgbColorSpaceDetails.Instance);
images.Add(Union<XObjectContentRecord, InlineImage>.One(contentRecord));
markedContentStack.AddXObject(contentRecord, pdfScanner, filterProvider, resourceStore);
}
else if (subType.Equals(NameToken.Form))
{
ProcessFormXObject(xObjectStream);
}
else
{
throw new InvalidOperationException($"XObject encountered with unexpected SubType {subType}. {xObjectStream.StreamDictionary}.");
}
}
private void ProcessFormXObject(StreamToken formStream)
{
/*
* 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<DictionaryToken>(NameToken.Resources, pdfScanner, out var formResources);
if (hasResources)
{
resourceStore.LoadResourceDictionary(formResources, parsingOptions);
}
// 1. Save current state.
PushState();
var startState = GetCurrentState();
2023-04-07 05:34:32 +08:00
// Transparency Group XObjects
if (formStream.StreamDictionary.TryGet(NameToken.Group, pdfScanner, out DictionaryToken formGroupToken))
{
if (!formGroupToken.TryGet<NameToken>(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);
2023-04-07 05:34:32 +08:00
}
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 groups 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 groups 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<ArrayToken>(NameToken.Matrix, pdfScanner, out var formMatrixToken))
{
2020-06-01 21:55:45 +08:00
formMatrix = TransformationMatrix.FromArray(formMatrixToken.Data.OfType<NumericToken>().Select(x => x.Double).ToArray());
}
// 2. Update current transformation matrix.
2023-04-07 05:34:32 +08:00
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.
ProcessOperations(operations);
// 5. Restore saved state.
PopState();
if (hasResources)
{
resourceStore.UnloadResourceDictionary();
}
}
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;
}
2020-04-18 19:10:17 +08:00
public void ModifyClippingIntersect(FillingRule clippingRule)
{
if (CurrentPath == null)
{
return;
}
AddCurrentSubpath();
CurrentPath.SetClipping(clippingRule);
2020-04-02 23:39:48 +08:00
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<NameToken, IToken> 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<byte> 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<XObjectContentRecord, InlineImage>.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);
}
}
}