Compare commits

..

5 Commits

Author SHA1 Message Date
Eliot Jones
487e368e9e Merge pull request #224 from InusualZ/object-copier
Writer: New API to copy token
2020-10-06 14:57:52 +01:00
InusualZ
704c56285c Address CI Errors 2020-10-05 20:16:41 -04:00
InusualZ
a190653683 Use the new IObjectCopier API in the PdfMerger
Added a new PagesCopier, this class would be the one responsible to give a parent to the page tree that are copied from other documents
2020-10-02 21:33:36 -04:00
InusualZ
8f0326a818 Introduce a new API for intercepting token that are being copied
This API would allow us to track any type of token while is/was copied to a stream, so when a similar token come again, we can decide if want to just use the already written token or the new one.

This API would allow us to divide the code for each specific thing that we are trying to avoid having duplicate, while not penalizing performance.

Another plus would be, that since every "deduplicator" code would be behind a class, if a class is causing some performance regression that the user don't want, the user could decide not to add it and the resultant pdf would still be valid
2020-10-02 21:11:46 -04:00
InusualZ
5a98b21c2f Don't brute force search for similar token. since the process is slow
Since this class is the writer of token,  it should never be the one responsible to check if a token is duplicated.
2020-10-02 20:36:31 -04:00
56 changed files with 683 additions and 828 deletions

View File

@@ -160,9 +160,9 @@
throw new ArgumentNullException("LineTo(): currentPosition is null.");
}
}
/// <summary>
/// Add a rectangle following the pdf specification (m, l, l, l, c) path. A new subpath is created.
/// Adds 4 <see cref="Line"/>s forming a rectangle to the path.
/// </summary>
public void Rectangle(double x, double y, double width, double height)
{
@@ -174,6 +174,8 @@
CloseSubpath(); // h
IsDrawnAsRectangle = true;
}
internal void QuadraticCurveTo(double x1, double y1, double x2, double y2) { }
/// <summary>
/// Add a <see cref="BezierCurve"/> to the path.

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net45;net451;net452;net46;net461;net462;net47</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Version>0.1.4</Version>
<Version>0.1.3-alpha001</Version>
<IsTestProject>False</IsTestProject>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<SignAssembly>true</SignAssembly>

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net45;net451;net452;net46;net461;net462;net47</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Version>0.1.4</Version>
<Version>0.1.3-alpha001</Version>
<IsTestProject>False</IsTestProject>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<SignAssembly>true</SignAssembly>

View File

@@ -76,12 +76,6 @@
AddAdobeFontMetrics("Times,Italic", "Times-Italic");
AddAdobeFontMetrics("Times,Bold", "Times-Bold");
AddAdobeFontMetrics("Times,BoldItalic", "Times-BoldItalic");
// Additional names for Arial
AddAdobeFontMetrics("ArialMT", "Helvetica");
AddAdobeFontMetrics("Arial-ItalicMT", "Helvetica-Oblique");
AddAdobeFontMetrics("Arial-BoldMT", "Helvetica-Bold");
AddAdobeFontMetrics("Arial-BoldItalicMT", "Helvetica-BoldOblique");
}
private static void AddAdobeFontMetrics(string fontName, Standard14Font? type = null)
@@ -149,7 +143,7 @@
{
return Standard14Cache[BuilderTypesToNames[fontType]];
}
/// <summary>
/// Determines if a font with this name is a standard 14 font.
/// </summary>
@@ -165,7 +159,7 @@
{
return new HashSet<string>(Standard14Names);
}
/// <summary>
/// Get the official Standard 14 name of the actual font which the given font name maps to.
/// </summary>

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net45;net451;net452;net46;net461;net462;net47</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Version>0.1.4</Version>
<Version>0.1.3-alpha001</Version>
<IsTestProject>False</IsTestProject>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<SignAssembly>true</SignAssembly>

View File

@@ -1,22 +0,0 @@
namespace UglyToad.PdfPig.Tests.Fonts
{
using UglyToad.PdfPig.Tests.Dla;
using Xunit;
public class PointSizeTests
{
[Fact]
public void RotatedText()
{
using (var document = PdfDocument.Open(DlaHelper.GetDocumentPath("complex rotated")))
{
var page = document.GetPage(1);
foreach (var letter in page.Letters)
{
Assert.Equal(12, letter.PointSize);
}
}
}
}
}

View File

@@ -10,7 +10,7 @@
{
var names = Standard14.GetNames().Count;
Assert.Equal(38, names);
Assert.Equal(34, names);
}
}
}

View File

@@ -7,8 +7,6 @@
using PdfPig.Tokens;
using PdfPig.Core;
using Tokens;
using UglyToad.PdfPig.Graphics.Core;
using UglyToad.PdfPig.Graphics.Operations.TextPositioning;
internal class TestOperationContext : IOperationContext
{
@@ -90,66 +88,6 @@
}
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()
{
}
@@ -186,91 +124,6 @@
{
}
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;
}
private class TestFontFactory : IFontFactory
{
public IFont Get(DictionaryToken dictionary)

View File

@@ -74,7 +74,6 @@
"UglyToad.PdfPig.Content.IPdfImage",
"UglyToad.PdfPig.Content.Letter",
"UglyToad.PdfPig.Content.MarkedContentElement",
"UglyToad.PdfPig.Content.MediaBox",
"UglyToad.PdfPig.Content.Page",
"UglyToad.PdfPig.Content.PageRotationDegrees",
"UglyToad.PdfPig.Content.PageSize",
@@ -198,8 +197,6 @@
"UglyToad.PdfPig.ParsingOptions",
"UglyToad.PdfPig.PdfDocument",
"UglyToad.PdfPig.PdfExtensions",
"UglyToad.PdfPig.Rendering.IPageImageRenderer",
"UglyToad.PdfPig.Rendering.PdfRendererImageFormat",
"UglyToad.PdfPig.Structure",
"UglyToad.PdfPig.Util.Adler32Checksum",
"UglyToad.PdfPig.Util.IWordExtractor",

View File

@@ -588,21 +588,6 @@
}
}
[Fact]
public void CanCreateDocumentWithFilledRectangle()
{
var builder = new PdfDocumentBuilder();
var page = builder.AddPage(PageSize.A4);
page.SetTextAndFillColor(255, 0, 0);
page.SetStrokeColor(0, 0, 255);
page.DrawRectangle(new PdfPoint(20, 100), 200, 100, 1.5m, true);
var file = builder.Build();
WriteFile(nameof(CanCreateDocumentWithFilledRectangle), file);
}
private static void WriteFile(string name, byte[] bytes, string extension = "pdf")
{
try

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net45;net451;net452;net46;net461;net462;net47</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Version>0.1.4</Version>
<Version>0.1.3-alpha001</Version>
<IsTestProject>False</IsTestProject>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<SignAssembly>true</SignAssembly>

View File

@@ -338,7 +338,6 @@
public static readonly NameToken MarkInfo = new NameToken("MarkInfo");
public static readonly NameToken Mask = new NameToken("Mask");
public static readonly NameToken Matrix = new NameToken("Matrix");
public static readonly NameToken Matte = new NameToken("Matte");
public static readonly NameToken MaxLen = new NameToken("MaxLen");
public static readonly NameToken MaxWidth = new NameToken("MaxWidth");
public static readonly NameToken Mcid = new NameToken("MCID");

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net45;net451;net452;net46;net461;net462;net47</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Version>0.1.4</Version>
<Version>0.1.3-alpha001</Version>
<IsTestProject>False</IsTestProject>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<SignAssembly>true</SignAssembly>

View File

@@ -68,6 +68,7 @@
/// <summary>
/// The size of the font in points.
/// <para>This is considered experimental because the calculated value is incorrect for some documents at present.</para>
/// </summary>
public double PointSize { get; }

View File

@@ -9,7 +9,7 @@
/// <remarks>
/// See table 3.27 from the PDF specification version 1.7.
/// </remarks>
public class MediaBox
internal class MediaBox
{
///<summary>
/// User space units per inch.
@@ -66,14 +66,8 @@
/// </summary>
public static readonly MediaBox A6 = new MediaBox(new PdfRectangle(0, 0, 105 * PointsPerMm, 148 * PointsPerMm));
/// <summary>
/// A rectangle, expressed in default user space units, that defines the boundaries of the physical medium on which the page shall be displayed or printed.
/// </summary>
public PdfRectangle Bounds { get; }
/// <summary>
/// Create a new <see cref="MediaBox"/>.
/// </summary>
public MediaBox(PdfRectangle? bounds)
{
Bounds = bounds ?? throw new ArgumentNullException(nameof(bounds));

View File

@@ -35,10 +35,7 @@
/// </summary>
public CropBox CropBox { get; }
/// <summary>
/// Defines the boundaries of the physical medium on which the page shall be displayed or printed.
/// </summary>
public MediaBox MediaBox { get; }
internal MediaBox MediaBox { get; }
internal PageContent Content { get; }

View File

@@ -1,5 +1,9 @@
namespace UglyToad.PdfPig.Graphics
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Colors;
using Content;
using Core;
@@ -10,13 +14,8 @@
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 UglyToad.PdfPig.Graphics.Operations.TextPositioning;
using XObjects;
using static PdfPig.Core.PdfSubpath;
@@ -242,7 +241,20 @@
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);
// TODO: this does not seem correct, produces the correct result for now but we need to revisit.
// see: https://stackoverflow.com/questions/48010235/pdf-specification-get-font-size-in-points
var fontSizeMatrix = transformationMatrix.Multiply(TextMatrices.TextMatrix).Multiply(fontSize);
var pointSize = Math.Round(fontSizeMatrix.A, 2);
// Assume a rotated letter
if (pointSize == 0)
{
pointSize = Math.Round(fontSizeMatrix.B, 2);
}
if (pointSize < 0)
{
pointSize *= -1;
}
while (bytes.MoveNext())
{
@@ -280,7 +292,7 @@
var boundingBox = font.GetBoundingBox(code);
var transformedGlyphBounds = PerformantRectangleTransformer
.Transform(renderingMatrix, textMatrix, transformationMatrix, boundingBox.GlyphBounds);
.Transform(renderingMatrix, textMatrix, transformationMatrix, boundingBox.GlyphBounds);
var transformedPdfBounds = PerformantRectangleTransformer
.Transform(renderingMatrix, textMatrix, transformationMatrix, new PdfRectangle(0, 0, boundingBox.Width, 0));
@@ -561,67 +573,7 @@
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)
@@ -805,10 +757,7 @@
if (markedContentStack.CanPop)
{
var mc = markedContentStack.Pop(pdfScanner);
if (mc != null)
{
markedContents.Add(mc);
}
if (mc != null) markedContents.Add(mc);
}
}
@@ -816,86 +765,9 @@
{
var matrix = TransformationMatrix.GetTranslationMatrix(tx, ty);
TextMatrices.TextMatrix = matrix.Multiply(TextMatrices.TextMatrix);
}
var newMatrix = 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;
TextMatrices.TextMatrix = newMatrix;
}
}
}

View File

@@ -3,13 +3,25 @@
using PdfPig.Core;
using System.Collections.Generic;
using Tokens;
using UglyToad.PdfPig.Graphics.Core;
using Util.JetBrains.Annotations;
/// <summary>
/// The current graphics state context when running a PDF content stream.
/// </summary>
public interface IOperationContext
{
/// <summary>
/// The current subpath being drawn if applicable.
/// </summary>
[CanBeNull]
PdfSubpath CurrentSubpath { get; }
/// <summary>
/// The current path being drawn if applicable.
/// </summary>
[CanBeNull]
PdfPath CurrentPath { get; }
/// <summary>
/// The active colorspaces for this content stream.
/// </summary>
@@ -20,11 +32,22 @@
/// </summary>
PdfPoint CurrentPosition { get; set; }
/// <summary>
/// Get the currently active <see cref="CurrentGraphicsState"/>. States are stored on a stack structure.
/// </summary>
/// <returns>The currently active graphics state.</returns>
CurrentGraphicsState GetCurrentState();
/// <summary>
/// The matrices for the current text state.
/// </summary>
TextMatrices TextMatrices { get; }
/// <summary>
/// The current transformation matrix
/// </summary>
TransformationMatrix CurrentTransformationMatrix { get; }
/// <summary>
/// The number of graphics states on the stack.
/// </summary>
@@ -36,7 +59,7 @@
void PopState();
/// <summary>
/// Saves a copy of the current graphics state on the stack.
/// Saves a copy of the current graphics state on the stack.
/// </summary>
void PushState();
@@ -62,7 +85,7 @@
/// Start a new sub-path.
/// </summary>
void BeginSubpath();
/// <summary>
/// Close the current subpath.
/// </summary>
@@ -93,36 +116,6 @@
/// <param name="close">Whether to also close the path.</param>
void FillStrokePath(FillingRule fillingRule, bool close);
/// <summary>
/// Add a move command to the path.
/// <para>Should implement matrix transformations.</para>
/// </summary>
void MoveTo(double x, double y);
/// <summary>
/// Add a bezier curve to the current subpath.
/// <para>Should implement matrix transformations.</para>
/// </summary>
void BezierCurveTo(double x1, double y1, double x2, double y2, double x3, double y3);
/// <summary>
/// Add a bezier curve to the current subpath.
/// <para>Should implement matrix transformations.</para>
/// </summary>
void BezierCurveTo(double x2, double y2, double x3, double y3);
/// <summary>
/// Add a line command to the subpath.
/// <para>Should implement matrix transformations.</para>
/// </summary>
void LineTo(double x, double y);
/// <summary>
/// Add a rectangle following the pdf specification (m, l, l, l, c) path. A new subpath will be created.
/// <para>Should implement matrix transformations.</para>
/// </summary>
void Rectangle(double x, double y, double width, double height);
/// <summary>
/// End the path object without filling or stroking it. This operator shall be a path-painting no-op,
/// used primarily for the side effect of changing the current clipping path (see 8.5.4, "Clipping Path Operators").
@@ -169,90 +162,5 @@
/// Modify the clipping rule of the current path.
/// </summary>
void ModifyClippingIntersect(FillingRule clippingRule);
/// <summary>
/// Set the flatness tolerance in the graphics state.
/// Flatness is a number in the range 0 to 100; a value of 0 specifies the output devices default flatness tolerance.
/// </summary>
/// <param name="tolerance"></param>
void SetFlatnessTolerance(decimal tolerance);
/// <summary>
/// Set the line cap style in the graphics state.
/// </summary>
void SetLineCap(LineCapStyle cap);
/// <summary>
/// Set the line dash pattern in the graphics state.
/// </summary>
void SetLineDashPattern(LineDashPattern pattern);
/// <summary>
/// Set the line join style in the graphics state.
/// </summary>
void SetLineJoin(LineJoinStyle join);
/// <summary>
/// Set the line width in the graphics state.
/// </summary>
void SetLineWidth(decimal width);
/// <summary>
/// Set the miter limit in the graphics state.
/// </summary>
void SetMiterLimit(decimal limit);
/// <summary>
/// Move to the start of the next line.
/// </summary>
/// <remarks>
/// This performs this operation: 0 -Tl Td
/// The offset is negative leading text (Tl) value, this is incorrect in the specification.
/// </remarks>
void MoveToNextLineWithOffset();
/// <summary>
/// Set the font and the font size.
/// Font is the name of a font resource in the Font subdictionary of the current resource dictionary.
/// Size is a number representing a scale factor.
/// </summary>
void SetFontAndSize(NameToken font, double size);
/// <summary>
/// Set the horizontal scaling.
/// </summary>
/// <param name="scale"></param>
void SetHorizontalScaling(double scale);
/// <summary>
/// Set the text leading.
/// </summary>
void SetTextLeading(double leading);
/// <summary>
/// Set the text rendering mode.
/// </summary>
void SetTextRenderingMode(TextRenderingMode mode);
/// <summary>
/// Set text rise.
/// </summary>
void SetTextRise(double rise);
/// <summary>
/// Sets the word spacing.
/// </summary>
void SetWordSpacing(double spacing);
/// <summary>
/// Modify the current transformation matrix by concatenating the specified matrix.
/// </summary>
void ModifyCurrentTransformationMatrix(double[] value);
/// <summary>
/// Set the character spacing to a number expressed in unscaled text space units.
/// Initial value: 0.
/// </summary>
void SetCharacterSpacing(double spacing);
}
}

View File

@@ -35,7 +35,7 @@
/// <inheritdoc />
public void Run(IOperationContext operationContext)
{
operationContext.SetFlatnessTolerance(Tolerance);
operationContext.GetCurrentState().Flatness = Tolerance;
}
/// <inheritdoc />

View File

@@ -45,7 +45,7 @@
/// <inheritdoc />
public void Run(IOperationContext operationContext)
{
operationContext.SetLineCap(Cap);
operationContext.GetCurrentState().CapStyle = Cap;
}
/// <inheritdoc />

View File

@@ -33,7 +33,7 @@
/// <inheritdoc />
public void Run(IOperationContext operationContext)
{
operationContext.SetLineDashPattern(Pattern);
operationContext.GetCurrentState().LineDashPattern = Pattern;
}
/// <inheritdoc />

View File

@@ -40,7 +40,7 @@
/// <inheritdoc />
public void Run(IOperationContext operationContext)
{
operationContext.SetLineJoin(Join);
operationContext.GetCurrentState().JoinStyle = Join;
}
/// <inheritdoc />

View File

@@ -33,7 +33,9 @@
/// <inheritdoc />
public void Run(IOperationContext operationContext)
{
operationContext.SetLineWidth(Width);
var currentState = operationContext.GetCurrentState();
currentState.LineWidth = Width;
}
/// <inheritdoc />

View File

@@ -33,7 +33,9 @@
/// <inheritdoc />
public void Run(IOperationContext operationContext)
{
operationContext.SetMiterLimit(Limit);
var currentState = operationContext.GetCurrentState();
currentState.MiterLimit = Limit;
}
/// <inheritdoc />

View File

@@ -20,7 +20,6 @@
/// <summary>
/// Applies the operation to the current context with the provided resources.
/// <para>Matrix transformations should be implemented in <see cref="IOperationContext"/>.</para>
/// </summary>
/// <param name="operationContext"></param>
void Run(IOperationContext operationContext);

View File

@@ -71,7 +71,14 @@
/// <inheritdoc />
public void Run(IOperationContext operationContext)
{
operationContext.BezierCurveTo((double)X1, (double)Y1, (double)X2, (double)Y2, (double)X3, (double)Y3);
if (operationContext.CurrentSubpath == null) return;
var controlPoint1 = operationContext.CurrentTransformationMatrix.Transform(new PdfPoint(X1, Y1));
var controlPoint2 = operationContext.CurrentTransformationMatrix.Transform(new PdfPoint(X2, Y2));
var end = operationContext.CurrentTransformationMatrix.Transform(new PdfPoint(X3, Y3));
operationContext.CurrentSubpath.BezierCurveTo(controlPoint1.X, controlPoint1.Y,
controlPoint2.X, controlPoint2.Y, end.X, end.Y);
operationContext.CurrentPosition = end;
}
/// <inheritdoc />

View File

@@ -56,7 +56,12 @@
/// <inheritdoc />
public void Run(IOperationContext operationContext)
{
operationContext.BezierCurveTo((double)X1, (double)Y1, (double)X3, (double)Y3, (double)X3, (double)Y3);
if (operationContext.CurrentSubpath == null) return;
var controlPoint1 = operationContext.CurrentTransformationMatrix.Transform(new PdfPoint(X1, Y1));
var end = operationContext.CurrentTransformationMatrix.Transform(new PdfPoint(X3, Y3));
operationContext.CurrentSubpath.BezierCurveTo(controlPoint1.X, controlPoint1.Y, end.X, end.Y, end.X, end.Y);
operationContext.CurrentPosition = end;
}
/// <inheritdoc />

View File

@@ -56,7 +56,12 @@
/// <inheritdoc />
public void Run(IOperationContext operationContext)
{
operationContext.Rectangle((double)LowerLeftX, (double)LowerLeftY, (double)Width, (double)Height);
operationContext.BeginSubpath();
var lowerLeft = operationContext.CurrentTransformationMatrix.Transform(new PdfPoint(LowerLeftX, LowerLeftY));
var upperRight = operationContext.CurrentTransformationMatrix.Transform(new PdfPoint(LowerLeftX + Width, LowerLeftY + Height));
operationContext.CurrentSubpath.Rectangle(lowerLeft.X, lowerLeft.Y, upperRight.X - lowerLeft.X, upperRight.Y - lowerLeft.Y);
operationContext.AddCurrentSubpath();
}
/// <inheritdoc />

View File

@@ -56,7 +56,17 @@
/// <inheritdoc />
public void Run(IOperationContext operationContext)
{
operationContext.BezierCurveTo((double)X2, (double)Y2, (double)X3, (double)Y3);
if (operationContext.CurrentSubpath == null) return;
var controlPoint2 = operationContext.CurrentTransformationMatrix.Transform(new PdfPoint(X2, Y2));
var end = operationContext.CurrentTransformationMatrix.Transform(new PdfPoint(X3, Y3));
operationContext.CurrentSubpath.BezierCurveTo(operationContext.CurrentPosition.X,
operationContext.CurrentPosition.Y,
controlPoint2.X,
controlPoint2.Y,
end.X,
end.Y);
operationContext.CurrentPosition = end;
}
/// <inheritdoc />

View File

@@ -41,7 +41,11 @@
/// <inheritdoc />
public void Run(IOperationContext operationContext)
{
operationContext.LineTo((double)X, (double)Y);
if (operationContext.CurrentSubpath == null) return;
var endPoint = operationContext.CurrentTransformationMatrix.Transform(new PdfPoint(X, Y));
operationContext.CurrentSubpath.LineTo(endPoint.X, endPoint.Y);
operationContext.CurrentPosition = endPoint;
}
/// <inheritdoc />

View File

@@ -41,7 +41,10 @@
/// <inheritdoc />
public void Run(IOperationContext operationContext)
{
operationContext.MoveTo((double)X, (double)Y);
operationContext.BeginSubpath();
var point = operationContext.CurrentTransformationMatrix.Transform(new PdfPoint(X, Y));
operationContext.CurrentPosition = point;
operationContext.CurrentSubpath.MoveTo(point.X, point.Y);
}
/// <inheritdoc />

View File

@@ -44,7 +44,13 @@
/// <inheritdoc />
public void Run(IOperationContext operationContext)
{
operationContext.ModifyCurrentTransformationMatrix(Array.ConvertAll(Value, x => (double)x));
var newMatrix = TransformationMatrix.FromArray(Value);
var ctm = operationContext.GetCurrentState().CurrentTransformationMatrix;
var newCtm = newMatrix.Multiply(ctm);
operationContext.GetCurrentState().CurrentTransformationMatrix = newCtm;
}
/// <inheritdoc />

View File

@@ -32,7 +32,9 @@
/// <inheritdoc />
public void Run(IOperationContext operationContext)
{
operationContext.MoveToNextLineWithOffset();
var tdOperation = new MoveToNextLineWithOffset(0, -1 * (decimal)operationContext.GetCurrentState().FontState.Leading);
tdOperation.Run(operationContext);
}
/// <inheritdoc />

View File

@@ -34,7 +34,9 @@
/// <inheritdoc />
public void Run(IOperationContext operationContext)
{
operationContext.SetCharacterSpacing((double)Spacing);
var currentState = operationContext.GetCurrentState();
currentState.FontState.CharacterSpacing = (double)Spacing;
}
/// <inheritdoc />

View File

@@ -48,7 +48,10 @@
/// <inheritdoc />
public void Run(IOperationContext operationContext)
{
operationContext.SetFontAndSize(Font, (double)Size);
var currentState = operationContext.GetCurrentState();
currentState.FontState.FontSize = (double)Size;
currentState.FontState.FontName = Font;
}
/// <inheritdoc />

View File

@@ -30,7 +30,9 @@
/// <inheritdoc />
public void Run(IOperationContext operationContext)
{
operationContext.SetHorizontalScaling((double)Scale);
var currentState = operationContext.GetCurrentState();
currentState.FontState.HorizontalScaling = (double)Scale;
}
/// <inheritdoc />

View File

@@ -33,7 +33,9 @@
/// <inheritdoc />
public void Run(IOperationContext operationContext)
{
operationContext.SetTextLeading((double)Leading);
var currentState = operationContext.GetCurrentState();
currentState.FontState.Leading = (double)Leading;
}
/// <inheritdoc />

View File

@@ -33,7 +33,9 @@
/// <inheritdoc />
public void Run(IOperationContext operationContext)
{
operationContext.SetTextRenderingMode(Mode);
var currentState = operationContext.GetCurrentState();
currentState.FontState.TextRenderingMode = Mode;
}
/// <inheritdoc />

View File

@@ -33,7 +33,9 @@
/// <inheritdoc />
public void Run(IOperationContext operationContext)
{
operationContext.SetTextRise((double)Rise);
var currentState = operationContext.GetCurrentState();
currentState.FontState.Rise = (double)Rise;
}
/// <inheritdoc />

View File

@@ -34,7 +34,9 @@
/// <inheritdoc />
public void Run(IOperationContext operationContext)
{
operationContext.SetWordSpacing((double)Spacing);
var currentState = operationContext.GetCurrentState();
currentState.FontState.WordSpacing = (double)Spacing;
}
/// <inheritdoc />

View File

@@ -46,7 +46,7 @@
do
{
if (loopProtection > 10_000_000)
if (loopProtection > 1_000_000)
{
throw new PdfDocumentFormatException("Failed to brute-force search the file due to an infinite loop.");
}

View File

@@ -7,6 +7,7 @@
using PdfPig.Parser.Parts;
using Tokenization.Scanner;
using Tokens;
using Util;
internal class EncodingReader : IEncodingReader
{
@@ -94,17 +95,20 @@
return differences;
}
var currentIndex = -1;
foreach (var differenceEntry in differenceArray.Data)
var activeCode = differenceArray.GetNumeric(0).Int;
for (int i = 1; i < differenceArray.Data.Count; i++)
{
if (differenceEntry is NumericToken number)
var entry = differenceArray.Data[i];
if (entry is NumericToken numeric)
{
currentIndex = number.Int;
activeCode = numeric.Int;
}
else if (differenceEntry is NameToken name)
else if (entry is NameToken name)
{
differences.Add((currentIndex, name.Data));
currentIndex++;
differences.Add((activeCode, name.Data));
activeCode++;
}
else
{

View File

@@ -47,7 +47,7 @@
}
else
{
descendantFontDictionary = (DictionaryToken)descendantObject;
descendantFontDictionary = (DictionaryToken) descendantObject;
}
cidFont = ParseDescendant(descendantFontDictionary);
@@ -107,7 +107,7 @@
{
if (array.Data[0] is IndirectReferenceToken objArr)
{
descendant = objArr;
descendant = objArr;
}
else if (array.Data[0] is DictionaryToken dict)
{
@@ -140,28 +140,30 @@
private CMap ReadEncoding(DictionaryToken dictionary, out bool isCMapPredefined)
{
isCMapPredefined = false;
CMap result;
CMap result = default(CMap);
if (dictionary.TryGet(NameToken.Encoding, scanner, out NameToken encodingName))
if (dictionary.TryGet(NameToken.Encoding, out var value))
{
var cmap = CMapCache.Get(encodingName.Data);
if (value is NameToken encodingName)
{
var cmap = CMapCache.Get(encodingName.Data);
result = cmap ?? throw new InvalidOperationException($"Missing CMap named {encodingName.Data}.");
result = cmap ?? throw new InvalidOperationException("Missing CMap for " + encodingName.Data);
isCMapPredefined = true;
}
else if (dictionary.TryGet(NameToken.Encoding, scanner, out StreamToken stream))
{
var decoded = stream.Decode(filterProvider);
isCMapPredefined = true;
}
else if (value is StreamToken stream)
{
var decoded = stream.Decode(filterProvider);
var cmap = CMapCache.Parse(new ByteArrayInputBytes(decoded));
var cmap = CMapCache.Parse(new ByteArrayInputBytes(decoded));
result = cmap ?? throw new InvalidOperationException($"Could not read CMap from stream in the dictionary: {dictionary}");
}
else
{
throw new InvalidOperationException(
$"Could not read the encoding, expected a name or a stream but it was not found in the dictionary: {dictionary}");
result = cmap ?? throw new InvalidOperationException("Could not read CMap for " + dictionary);
}
else
{
throw new InvalidOperationException("Could not read the encoding, expected a name or a stream but got a: " + value.GetType().Name);
}
}
return result;

View File

@@ -1,19 +0,0 @@
using UglyToad.PdfPig.Content;
namespace UglyToad.PdfPig.Rendering
{
/// <summary>
/// Render page as an image.
/// </summary>
public interface IPageImageRenderer
{
/// <summary>
/// Render page as an image.
/// </summary>
/// <param name="page">The pdf page.</param>
/// <param name="scale">The scale to apply to the page (i.e. zoom level).</param>
/// <param name="imageFormat">The output image format, if supported.</param>
/// <returns>The image as a memory stream.</returns>
byte[] Render(Page page, double scale, PdfRendererImageFormat imageFormat);
}
}

View File

@@ -1,33 +0,0 @@
namespace UglyToad.PdfPig.Rendering
{
/// <summary>
/// The output image format of the <see cref="IPageImageRenderer"/>.
/// </summary>
public enum PdfRendererImageFormat : byte
{
/// <summary>
/// Bitmap image format.
/// </summary>
Bmp,
/// <summary>
/// Jpeg/Jpg image format.
/// </summary>
Jpeg,
/// <summary>
/// Png image format.
/// </summary>
Png,
/// <summary>
/// Tiff image format.
/// </summary>
Tiff,
/// <summary>
/// Gif image format.
/// </summary>
Gif
}
}

View File

@@ -800,13 +800,6 @@
var token = scanner.CurrentToken;
if (token.Equals(OperatorToken.EndObject))
{
scanner.MoveNext();
token = scanner.CurrentToken;
}
results.Add(new ObjectToken(offset, new IndirectReference(obj.Item1, 0), token));
}

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net45;net451;net452;net46;net461;net462;net47</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Version>0.1.4</Version>
<Version>0.1.3-alpha001</Version>
<IsTestProject>False</IsTestProject>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<SignAssembly>true</SignAssembly>

View File

@@ -0,0 +1,24 @@
namespace UglyToad.PdfPig.Writer.Copier
{
using System;
using Tokens;
/// <summary>
/// An interface for copying token
/// </summary>
internal interface IObjectCopier
{
/// <summary>
/// Copy the token to the destination stream
/// </summary>
/// <param name="sourceToken">Token to copy</param>
/// <param name="tokenScanner">Function to resolve indirect reference identified in the token to copy</param>
/// <returns></returns>
public IToken CopyObject(IToken sourceToken, Func<IndirectReferenceToken, IToken> tokenScanner);
/// <summary>
/// Clear the references of the previously copied object
/// </summary>
public void ClearReference();
}
}

View File

@@ -0,0 +1,75 @@
namespace UglyToad.PdfPig.Writer.Copier
{
using System;
using System.Collections.Generic;
using Tokens;
using Writer;
/// <inheritdoc/>
internal class MultiCopier : ObjectCopier
{
private readonly List<IObjectCopier> copiers;
/// <inheritdoc/>
public MultiCopier(PdfStreamWriter destinationStream) : base(destinationStream)
{
copiers = new List<IObjectCopier>();
}
/// <summary>
///
/// </summary>
/// <param name="copier"></param>
public void AddCopier(IObjectCopier copier)
{
copiers.Add(copier);
}
/// <summary>
///
/// </summary>
/// <param name="copier"></param>
/// <returns></returns>
public bool RemoveCopier(IObjectCopier copier)
{
return copiers.Remove(copier);
}
/// <summary>
///
/// </summary>
/// <returns></returns>
public IReadOnlyList<IObjectCopier> GetCopiers()
{
return copiers;
}
/// <inheritdoc/>
public override IToken CopyObject(IToken sourceToken, Func<IndirectReferenceToken, IToken> tokenScanner)
{
// We give the token to the child copiers, to see if they have a better way of copying the token
foreach (var copier in copiers)
{
var newToken = copier.CopyObject(sourceToken, tokenScanner);
if (newToken != null)
{
return newToken;
}
}
// If the token did not found a suitable copier, let just do a simple copy of the token
return base.CopyObject(sourceToken, tokenScanner);
}
/// <inheritdoc/>
public override void ClearReference()
{
foreach (var copier in copiers)
{
copier.ClearReference();
}
base.ClearReference();
}
}
}

View File

@@ -0,0 +1,167 @@
namespace UglyToad.PdfPig.Writer.Copier
{
using System;
using System.Collections.Generic;
using PdfPig;
using Tokenization.Scanner;
using Tokens;
using Writer;
/// <inheritdoc/>
internal class ObjectCopier : IObjectCopier
{
private readonly PdfStreamWriter pdfStream;
private readonly Dictionary<IndirectReferenceToken, IndirectReferenceToken> newReferenceMap;
/// <inheritdoc/>
public ObjectCopier(PdfStreamWriter destinationStream)
{
pdfStream = destinationStream ?? throw new ArgumentNullException(nameof(destinationStream));
newReferenceMap = new Dictionary<IndirectReferenceToken, IndirectReferenceToken>();
}
/// <inheritdoc/>
public IToken CopyObject(IToken sourceToken, PdfDocument sourceDocument)
{
IToken tokenScanner(IndirectReferenceToken referenceToken)
{
var objToken = sourceDocument.Structure.GetObject(referenceToken.Data);
return objToken.Data;
}
return CopyObject(sourceToken, tokenScanner);
}
/// <inheritdoc/>
public IToken CopyObject(IToken sourceToken, IPdfTokenScanner tokenScanner)
{
IToken tokenGetter(IndirectReferenceToken referenceToken)
{
var objToken = tokenScanner.Get(referenceToken.Data);
return objToken.Data;
}
return CopyObject(sourceToken, tokenGetter);
}
/// <inheritdoc/>
public virtual IToken CopyObject(IToken sourceToken, Func<IndirectReferenceToken, IToken> tokenScanner)
{
// This token need to be deep copied, because they could contain reference. So we have to update them.
switch (sourceToken)
{
case DictionaryToken dictionaryToken:
{
var newContent = new Dictionary<NameToken, IToken>();
foreach (var setPair in dictionaryToken.Data)
{
var name = setPair.Key;
var token = setPair.Value;
newContent.Add(NameToken.Create(name), CopyObject(token, tokenScanner));
}
return new DictionaryToken(newContent);
}
case ArrayToken arrayToken:
{
var newArray = new List<IToken>(arrayToken.Length);
foreach (var token in arrayToken.Data)
{
newArray.Add(CopyObject(token, tokenScanner));
}
return new ArrayToken(newArray);
}
case IndirectReferenceToken referenceToken:
{
if (TryGetNewReference(referenceToken, out var newReferenceToken))
{
return newReferenceToken;
}
var referencedToken = tokenScanner(referenceToken);
var newReferencedToken = CopyObject(referencedToken, tokenScanner);
var newToken = WriteToken(newReferencedToken);
SetNewReference(referenceToken, newToken);
return newToken;
}
case StreamToken streamToken:
{
var properties = CopyObject(streamToken.StreamDictionary, tokenScanner);
var bytes = streamToken.Data;
return new StreamToken(properties as DictionaryToken, bytes);
}
case ObjectToken _:
{
throw new NotSupportedException("Copying a Object Token is not supported");
}
}
return sourceToken;
}
/// <summary>
///
/// </summary>
/// <param name="sourceReferenceToken"></param>
/// <param name="newReferenceToken"></param>
/// <returns></returns>
public virtual bool TryGetNewReference(IndirectReferenceToken sourceReferenceToken, out IndirectReferenceToken newReferenceToken)
{
newReferenceToken = default;
foreach (var referenceSet in newReferenceMap)
{
if (!referenceSet.Key.Equals(sourceReferenceToken))
{
continue;
}
newReferenceToken = referenceSet.Value;
return true;
}
return false;
}
/// <inheritdoc/>
public virtual void ClearReference()
{
newReferenceMap.Clear();
}
/// <summary>
///
/// </summary>
/// <param name="oldToken"></param>
/// <param name="newToken"></param>
public void SetNewReference(IndirectReferenceToken oldToken, IndirectReferenceToken newToken)
{
newReferenceMap.Add(oldToken, newToken);
}
/// <summary>
///
/// </summary>
/// <returns></returns>
public int ReserveTokenNumber()
{
return pdfStream.ReserveNumber();
}
/// <summary>
///
/// </summary>
/// <param name="token"></param>
/// <param name="reservedNumber"></param>
/// <returns></returns>
public IndirectReferenceToken WriteToken(IToken token, int? reservedNumber = null)
{
return pdfStream.WriteToken(token, reservedNumber);
}
}
}

View File

@@ -0,0 +1,96 @@
namespace UglyToad.PdfPig.Writer.Copier.Page
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Core;
using Tokens;
/// <inheritdoc/>
internal class PagesCopier : IObjectCopier
{
private readonly ObjectCopier copier;
private readonly IndirectReferenceToken rootPagesReferenceToken;
/// <inheritdoc/>
public PagesCopier(ObjectCopier mainCopier, IndirectReferenceToken rootPagesToken = null)
{
copier = mainCopier;
rootPagesReferenceToken = rootPagesToken;
}
/// <inheritdoc/>
public IToken CopyObject(IToken sourceToken, Func<IndirectReferenceToken, IToken> tokenScanner)
{
if (!(sourceToken is IndirectReferenceToken sourceReferenceToken))
{
return null;
}
// Check if this token haven't been copied before
if (copier.TryGetNewReference(sourceReferenceToken, out var newReferenceToken))
{
return newReferenceToken;
}
// Make sure that we are copying a DictionaryToken
var token = tokenScanner(sourceReferenceToken);
if (!(token is DictionaryToken dictionaryToken))
{
return null;
}
// Make sure we are copying a `/Pages` Dictionary
if (!dictionaryToken.TryGet(NameToken.Type, out var nameTypeToken) || !nameTypeToken.Equals(NameToken.Pages))
{
return null;
}
// We have to reserve the reference before hand, because if we don't, we would fall in a loop.
// The child `/Page` have a reference to the parent
var tokenNumber = copier.ReserveTokenNumber();
copier.SetNewReference(sourceReferenceToken, new IndirectReferenceToken(new IndirectReference(tokenNumber, 0)));
// If `/Pages` is not the root page node, copy the token normally
// We are testing for one:
// * If @rootPagesReferenceToken is null, just do a normal copy of the tree
// * If the tree have a Parent NameToken, it means the tree is not a root tree so we don't have to assign him
// a new parent
if (rootPagesReferenceToken == null || dictionaryToken.TryGet(NameToken.Parent, out IndirectReferenceToken _))
{
return copier.WriteToken(copier.CopyObject(dictionaryToken, tokenScanner), tokenNumber);
}
// Since the tree is a root tree, it means that the tree comes from another document, we have to make sure
// that the new tree is a child of the new root tree, this we do by adding a Parent NameToken to the tree,
// that point to @rootPagesReferenceToken
return CopyPagesTree(dictionaryToken, tokenNumber, tokenScanner);
}
private IndirectReferenceToken CopyPagesTree(DictionaryToken pagesDictionary, int reservedNumber, Func<IndirectReferenceToken, IToken> tokenScanner)
{
Debug.Assert(rootPagesReferenceToken != null);
var newContent = new Dictionary<NameToken, IToken>()
{
{NameToken.Parent, rootPagesReferenceToken}
};
foreach (var dataSet in pagesDictionary.Data)
{
newContent.Add(NameToken.Create(dataSet.Key), copier.CopyObject(dataSet.Value, tokenScanner));
}
var newPagesTree = new DictionaryToken(newContent);
return copier.WriteToken(newPagesTree, reservedNumber);
}
/// <inheritdoc/>
public void ClearReference()
{
// Nothing to do
}
}
}

View File

@@ -0,0 +1,34 @@
namespace UglyToad.PdfPig.Writer.Copier
{
using System;
using Tokens;
internal static class TokenHelper
{
// This is to avoid infinite loop in production. Although, it should never happen
const int MAX_ITERATIONS = 10;
public static T GetTokenAs<T>(IToken token, Func<IndirectReferenceToken, IToken> lookupFunc) where T : IToken
{
var iterations = 0;
var original = token;
while (iterations++ < MAX_ITERATIONS)
{
switch (token)
{
case T result:
return result;
case IndirectReferenceToken tokenReference:
token = lookupFunc(tokenReference);
continue;
case ObjectToken tokenObject:
token = tokenObject.Data;
continue;
}
}
throw new InvalidOperationException($"Unable to extract a {typeof(T)} token from {original}");
}
}
}

View File

@@ -2,9 +2,10 @@
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using Content;
using Copier;
using Copier.Page;
using Core;
using CrossReference;
using Encryption;
@@ -149,31 +150,36 @@
private class DocumentMerger
{
private const decimal DefaultVersion = 1.2m;
private readonly PdfStreamWriter context = new PdfStreamWriter();
private readonly List<IndirectReferenceToken> pagesTokenReferences = new List<IndirectReferenceToken>();
private readonly PdfStreamWriter context;
private readonly List<IndirectReferenceToken> pagesTokenReferences;
private readonly IndirectReferenceToken rootPagesReference;
private readonly MultiCopier copier;
private decimal currentVersion = DefaultVersion;
private int pageCount = 0;
private readonly Dictionary<IndirectReferenceToken, IndirectReferenceToken> referencesFromDocument =
new Dictionary<IndirectReferenceToken, IndirectReferenceToken>();
public DocumentMerger()
{
context = new PdfStreamWriter();
pagesTokenReferences = new List<IndirectReferenceToken>();
rootPagesReference = context.ReserveNumberToken();
copier = new MultiCopier(context);
copier.AddCopier(new PagesCopier(copier, rootPagesReference));
}
public void AppendDocument(Catalog documentCatalog, decimal version, IPdfTokenScanner tokenScanner)
{
currentVersion = Math.Max(version, currentVersion);
var (pagesReference, count) = CopyPagesTree(documentCatalog.PageTree, rootPagesReference, tokenScanner);
pageCount += count;
pagesTokenReferences.Add(pagesReference);
var copiedPages = copier.CopyObject(new IndirectReferenceToken(documentCatalog.PageTree.Reference), tokenScanner) as IndirectReferenceToken;
pagesTokenReferences.Add(copiedPages);
referencesFromDocument.Clear();
pageCount += documentCatalog.PagesDictionary.Get<NumericToken>(NameToken.Count, tokenScanner).Int;
copier.ClearReference();
}
public byte[] Build()
@@ -190,7 +196,7 @@
{ NameToken.Count, new NumericToken(pageCount) }
});
var pagesRef = context.WriteToken( pagesDictionary, (int)rootPagesReference.Data.ObjectNumber);
var pagesRef = context.WriteToken(pagesDictionary, (int)rootPagesReference.Data.ObjectNumber);
var catalog = new DictionaryToken(new Dictionary<NameToken, IToken>
{
@@ -199,9 +205,9 @@
});
var catalogRef = context.WriteToken(catalog);
context.Flush(currentVersion, catalogRef);
var bytes = context.ToArray();
Close();
@@ -209,165 +215,10 @@
return bytes;
}
public void Close()
private void Close()
{
context.Dispose();
}
private (IndirectReferenceToken, int) CopyPagesTree(PageTreeNode treeNode, IndirectReferenceToken treeParentReference, IPdfTokenScanner tokenScanner)
{
Debug.Assert(!treeNode.IsPage);
var currentNodeReference = context.ReserveNumberToken();
var pageReferences = new List<IndirectReferenceToken>();
var nodeCount = 0;
foreach (var pageNode in treeNode.Children)
{
IndirectReferenceToken newEntry;
if (!pageNode.IsPage)
{
var count = 0;
(newEntry, count) = CopyPagesTree(pageNode, currentNodeReference, tokenScanner);
nodeCount += count;
}
else
{
newEntry = CopyPageNode(pageNode, currentNodeReference, tokenScanner);
++nodeCount;
}
pageReferences.Add(newEntry);
}
var newPagesNode = new Dictionary<NameToken, IToken>
{
{ NameToken.Type, NameToken.Pages },
{ NameToken.Kids, new ArrayToken(pageReferences) },
{ NameToken.Count, new NumericToken(nodeCount) },
{ NameToken.Parent, treeParentReference }
};
foreach (var pair in treeNode.NodeDictionary.Data)
{
if (IgnoreKeyForPagesNode(pair))
{
continue;
}
newPagesNode[NameToken.Create(pair.Key)] = CopyToken(pair.Value, tokenScanner);
}
var pagesDictionary = new DictionaryToken(newPagesNode);
return (context.WriteToken(pagesDictionary, (int)currentNodeReference.Data.ObjectNumber), nodeCount);
}
private IndirectReferenceToken CopyPageNode(PageTreeNode pageNode, IndirectReferenceToken parentPagesObject, IPdfTokenScanner tokenScanner)
{
Debug.Assert(pageNode.IsPage);
var pageDictionary = new Dictionary<NameToken, IToken>
{
{NameToken.Parent, parentPagesObject},
};
foreach (var setPair in pageNode.NodeDictionary.Data)
{
var name = setPair.Key;
var token = setPair.Value;
if (name == NameToken.Parent)
{
// Skip Parent token, since we have to reassign it
continue;
}
pageDictionary.Add(NameToken.Create(name), CopyToken(token, tokenScanner));
}
return context.WriteToken(new DictionaryToken(pageDictionary));
}
/// <summary>
/// The purpose of this method is to resolve indirect reference. That mean copy the reference's content to the new document's stream
/// and replace the indirect reference with the correct/new one
/// </summary>
/// <param name="tokenToCopy">Token to inspect for reference</param>
/// <param name="tokenScanner">scanner get the content from the original document</param>
/// <returns>A reference of the token that was copied. With all the reference updated</returns>
private IToken CopyToken(IToken tokenToCopy, IPdfTokenScanner tokenScanner)
{
// This token need to be deep copied, because they could contain reference. So we have to update them.
switch (tokenToCopy)
{
case DictionaryToken dictionaryToken:
{
var newContent = new Dictionary<NameToken, IToken>();
foreach (var setPair in dictionaryToken.Data)
{
var name = setPair.Key;
var token = setPair.Value;
newContent.Add(NameToken.Create(name), CopyToken(token, tokenScanner));
}
return new DictionaryToken(newContent);
}
case ArrayToken arrayToken:
{
var newArray = new List<IToken>(arrayToken.Length);
foreach (var token in arrayToken.Data)
{
newArray.Add(CopyToken(token, tokenScanner));
}
return new ArrayToken(newArray);
}
case IndirectReferenceToken referenceToken:
{
if (referencesFromDocument.TryGetValue(referenceToken, out var newReferenceToken))
{
return newReferenceToken;
}
var tokenObject = DirectObjectFinder.Get<IToken>(referenceToken.Data, tokenScanner);
Debug.Assert(!(tokenObject is IndirectReferenceToken));
var newToken = CopyToken(tokenObject, tokenScanner);
newReferenceToken = context.WriteToken(newToken);
referencesFromDocument.Add(referenceToken, newReferenceToken);
return newReferenceToken;
}
case StreamToken streamToken:
{
var properties = CopyToken(streamToken.StreamDictionary, tokenScanner) as DictionaryToken;
Debug.Assert(properties != null);
var bytes = streamToken.Data;
return new StreamToken(properties, bytes);
}
case ObjectToken _:
{
// Since we don't write token directly to the stream.
// We can't know the offset. Therefore the token would be invalid
throw new NotSupportedException("Copying a Object token is not supported");
}
}
return tokenToCopy;
}
private static bool IgnoreKeyForPagesNode(KeyValuePair<string, IToken> token)
{
return string.Equals(token.Key, NameToken.Type.Data, StringComparison.OrdinalIgnoreCase)
|| string.Equals(token.Key, NameToken.Kids.Data, StringComparison.OrdinalIgnoreCase)
|| string.Equals(token.Key, NameToken.Count.Data, StringComparison.OrdinalIgnoreCase)
|| string.Equals(token.Key, NameToken.Parent.Data, StringComparison.OrdinalIgnoreCase);
}
}
}
}

View File

@@ -91,8 +91,7 @@
/// <param name="width">The width of the rectangle.</param>
/// <param name="height">The height of the rectangle.</param>
/// <param name="lineWidth">The width of the line border of the rectangle.</param>
/// <param name="fill">Whether to fill with the color set by <see cref="SetTextAndFillColor"/>.</param>
public void DrawRectangle(PdfPoint position, decimal width, decimal height, decimal lineWidth = 1, bool fill = false)
public void DrawRectangle(PdfPoint position, decimal width, decimal height, decimal lineWidth = 1)
{
if (lineWidth != 1)
{
@@ -100,15 +99,7 @@
}
operations.Add(new AppendRectangle((decimal)position.X, (decimal)position.Y, width, height));
if (fill)
{
operations.Add(FillPathEvenOddRuleAndStroke.Value);
}
else
{
operations.Add(StrokePath.Value);
}
operations.Add(StrokePath.Value);
if (lineWidth != 1)
{

View File

@@ -9,7 +9,8 @@
using Tokens;
/// <summary>
/// This class would lazily flush all token. Allowing us to make changes to references without need to rewrite the whole stream
/// This class would lazily flush all token.
/// Allowing us to make changes to references without need to rewrite the whole stream
/// </summary>
internal class PdfStreamWriter : IDisposable
{
@@ -17,20 +18,34 @@
private readonly Dictionary<IndirectReferenceToken, IToken> tokenReferences = new Dictionary<IndirectReferenceToken, IToken>();
public int CurrentNumber { get; private set; } = 1;
private int currentNumber = 1;
public Stream Stream { get; private set; }
private Stream stream;
/// <summary>
/// Flag to set whether or not we want to dispose the stream
/// </summary>
public bool DisposeStream { get; set; }
/// <summary>
/// Construct a PdfStreamWriter with a memory stream
/// </summary>
public PdfStreamWriter() : this(new MemoryStream()) { }
/// <summary>
/// Construct a PdfStreamWriter
/// </summary>
public PdfStreamWriter(Stream baseStream, bool disposeStream = true)
{
Stream = baseStream ?? throw new ArgumentNullException(nameof(baseStream));
stream = baseStream ?? throw new ArgumentNullException(nameof(baseStream));
DisposeStream = disposeStream;
}
/// <summary>
/// Flush the document with all the token that we have accumulated
/// </summary>
/// <param name="version">Pdf Version that we are targeting</param>
/// <param name="catalogReference">Catalog's indirect reference token to which the token are related</param>
public void Flush(decimal version, IndirectReferenceToken catalogReference)
{
if (catalogReference == null)
@@ -38,14 +53,14 @@
throw new ArgumentNullException(nameof(catalogReference));
}
WriteString($"%PDF-{version.ToString("0.0", CultureInfo.InvariantCulture)}", Stream);
WriteString($"%PDF-{version.ToString("0.0", CultureInfo.InvariantCulture)}", stream);
Stream.WriteText("%");
Stream.WriteByte(169);
Stream.WriteByte(205);
Stream.WriteByte(196);
Stream.WriteByte(210);
Stream.WriteNewLine();
stream.WriteText("%");
stream.WriteByte(169);
stream.WriteByte(205);
stream.WriteByte(196);
stream.WriteByte(210);
stream.WriteNewLine();
var offsets = new Dictionary<IndirectReference, long>();
ObjectToken catalogToken = null;
@@ -53,10 +68,10 @@
{
var referenceToken = pair.Key;
var token = pair.Value;
var offset = Stream.Position;
var offset = stream.Position;
var obj = new ObjectToken(offset, referenceToken.Data, token);
TokenWriter.WriteToken(obj, Stream);
TokenWriter.WriteToken(obj, stream);
offsets.Add(referenceToken.Data, offset);
@@ -72,75 +87,105 @@
}
// TODO: Support document information
TokenWriter.WriteCrossReferenceTable(offsets, catalogToken, Stream, null);
TokenWriter.WriteCrossReferenceTable(offsets, catalogToken, stream, null);
}
/// <summary>
/// Push a new token to be written
/// </summary>
/// <param name="token"></param>
/// <param name="reservedNumber"></param>
/// <returns></returns>
public IndirectReferenceToken WriteToken(IToken token, int? reservedNumber = null)
{
// if you can't consider deduplicating the token.
// It must be because it's referenced by his child element, so you must have reserved a number before hand
// Example /Pages Obj
var canBeDuplicated = !reservedNumber.HasValue;
if (!canBeDuplicated)
if (reservedNumber.HasValue)
{
if (!reservedNumbers.Remove(reservedNumber.Value))
{
throw new InvalidOperationException("You can't reuse a reserved number");
}
// When we end up writing this token, all of his child would already have been added and checked for duplicate
return AddToken(token, reservedNumber.Value);
}
var reference = FindToken(token);
if (reference == null)
{
return AddToken(token, CurrentNumber++);
}
return reference;
return AddToken(token, currentNumber++);
}
/// <summary>
/// Get a token based on his indirect reference
/// </summary>
/// <param name="referenceToken"></param>
/// <returns></returns>
public IToken GetToken(IndirectReferenceToken referenceToken)
{
return tokenReferences.TryGetValue(referenceToken, out var token) ? token : null;
}
/// <summary>
/// Replace a token base on his indirect reference
/// </summary>
/// <param name="referenceToken"></param>
/// <param name="newToken"></param>
public void ReplaceToken(IndirectReferenceToken referenceToken, IToken newToken)
{
tokenReferences[referenceToken] = newToken;
}
/// <summary>
/// Reserve a number for a token
/// </summary>
/// <returns></returns>
public int ReserveNumber()
{
var reserved = CurrentNumber;
var reserved = currentNumber;
reservedNumbers.Add(reserved);
CurrentNumber++;
currentNumber++;
return reserved;
}
/// <summary>
/// Reserve a number and create a token with it
/// </summary>
/// <returns></returns>
public IndirectReferenceToken ReserveNumberToken()
{
return new IndirectReferenceToken(new IndirectReference(ReserveNumber(), 0));
}
/// <summary>
/// Return the bytes that have been flushed to the stream
/// </summary>
/// <returns></returns>
public byte[] ToArray()
{
var currentPosition = Stream.Position;
Stream.Seek(0, SeekOrigin.Begin);
var currentPosition = stream.Position;
stream.Seek(0, SeekOrigin.Begin);
var bytes = new byte[Stream.Length];
var bytes = new byte[stream.Length];
if (Stream.Read(bytes, 0, bytes.Length) != bytes.Length)
if (stream.Read(bytes, 0, bytes.Length) != bytes.Length)
{
throw new Exception("Unable to read all the bytes from stream");
throw new IOException("Unable to read all the bytes from stream");
}
Stream.Seek(currentPosition, SeekOrigin.Begin);
stream.Seek(currentPosition, SeekOrigin.Begin);
return bytes;
}
/// <summary>
/// Dispose the stream if the PdfStreamWriter#DisposeStream flag is set
/// </summary>
public void Dispose()
{
if (!DisposeStream)
{
Stream = null;
stream = null;
return;
}
Stream?.Dispose();
Stream = null;
stream?.Dispose();
stream = null;
}
private IndirectReferenceToken AddToken(IToken token, int reservedNumber)
@@ -151,21 +196,6 @@
return referenceToken;
}
private IndirectReferenceToken FindToken(IToken token)
{
foreach (var pair in tokenReferences)
{
var reference = pair.Key;
var storedToken = pair.Value;
if (storedToken.Equals(token))
{
return reference;
}
}
return null;
}
private static void WriteString(string text, Stream stream)
{
var bytes = OtherEncodings.StringAsLatin1Bytes(text);

View File

@@ -12,7 +12,7 @@
<PackageTags>PDF;Reader;Document;Adobe;PDFBox;PdfPig;pdf-extract;pdf-to-text;pdf;file;text;C#;dotnet;.NET</PackageTags>
<RepositoryUrl>https://github.com/UglyToad/PdfPig</RepositoryUrl>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<Version>0.1.4</Version>
<Version>0.1.3-alpha001</Version>
<AssemblyVersion>0.1.1.0</AssemblyVersion>
<FileVersion>0.1.1.0</FileVersion>
<PackageIconUrl>https://raw.githubusercontent.com/UglyToad/PdfPig/master/documentation/pdfpig.png</PackageIconUrl>