Compare commits

..

24 Commits

Author SHA1 Message Date
Eliot Jones
237fd96f9e version 0.1.4 2020-11-29 14:02:42 -04:00
BobLd
6e69160b4e Merge pull request #237 from BobLd/operationcontext-refactor
Using `byte[]` for `IPageImageRenderer` and keeping the point on `ModifyCurrentTransformationMatrix.cs` in mind.
2020-11-27 11:29:45 +00:00
BobLd
2b60474e11 IPageImageRenderer returns byte[] instead of MemoryStream. 2020-11-27 11:18:29 +00:00
Eliot Jones
cbd6eebecc up brute force range to 10mib #233
previously this was 1mib but the search range was exceeded in a file with over 1 million bytes of xref table.
2020-11-26 21:23:40 -04:00
Eliot Jones
7fdc8e2e86 allow indirect references for type0 font encoding #238 2020-11-26 21:11:53 -04:00
BobLd
52eb935182 Clean IOperationContext documentation. 2020-11-23 13:40:43 +00:00
BobLd
54f7f7a84a Update PublicApiScannerTests with new public methods 2020-11-23 12:53:35 +00:00
BobLd
69e23be450 Improve documentation for PdfSubpath.Rectangle(...). 2020-11-23 12:47:01 +00:00
BobLd
3c510c62f2 Add PdfRendererImageFormat 2020-11-23 12:44:23 +00:00
BobLd
68e487d5ff Add IPageImageRenderer 2020-11-23 12:38:44 +00:00
BobLd
95fa002b4a Add Matte constant token. 2020-11-23 11:26:58 +00:00
BobLd
4d86dc399e Make MediaBox public 2020-11-23 11:21:10 +00:00
BobLd
910e22997c Remove CurrentTransformationMatrix from IOperationContext. 2020-11-23 11:13:46 +00:00
BobLd
eb85f67b18 Remove CurrentGraphicsState GetCurrentState() from IOperationContext. 2020-11-23 11:11:07 +00:00
BobLd
14465f0198 remove ref to Util.JetBrains.Annotations in IOperationContext. 2020-11-23 10:53:09 +00:00
BobLd
d07baa97d5 Remove reference from CurrentSubpath and CurrentPath in IOperationContext and add MoveTo(), BezierCurveTo(), LineTo() and Rectangle(). 2020-11-23 10:49:50 +00:00
Eliot Jones
ccd7d81ae3 #234 handle missing first index for differences array 2020-11-22 13:07:05 -04:00
Eliot Jones
fa5e37dc8c handle presence of endobj markers in object stream #235 2020-11-22 12:51:38 -04:00
Eliot Jones
09a85d0497 treat arial mt as standard 14 font #236 2020-11-22 12:49:47 -04:00
Eliot Jones
d8e0263ec7 #215 support filling rectangles on pdf builder 2020-11-17 17:00:13 -04:00
Eliot Jones
ad0fb4ec5b version 0.1.3 2020-11-15 12:08:37 -04:00
Eliot Jones
d959a30bf0 Merge pull request #229 from BobLd/fix-point-size
Fix Letter's PointSize computation
2020-10-13 09:27:40 +01:00
BobLd
61d3e94a95 update PointSize description 2020-10-12 16:52:33 +01:00
BobLd
cd9ac6ac6c - fix letter's PointSize computation by applying the transform to a rectangle of height fontSize
- add test with rotated letters
2020-10-12 12:59:02 +01:00
56 changed files with 828 additions and 683 deletions

View File

@@ -160,9 +160,9 @@
throw new ArgumentNullException("LineTo(): currentPosition is null.");
}
}
/// <summary>
/// Adds 4 <see cref="Line"/>s forming a rectangle to the path.
/// Add a rectangle following the pdf specification (m, l, l, l, c) path. A new subpath is created.
/// </summary>
public void Rectangle(double x, double y, double width, double height)
{
@@ -174,8 +174,6 @@
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.3-alpha001</Version>
<Version>0.1.4</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.3-alpha001</Version>
<Version>0.1.4</Version>
<IsTestProject>False</IsTestProject>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<SignAssembly>true</SignAssembly>

View File

@@ -76,6 +76,12 @@
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)
@@ -143,7 +149,7 @@
{
return Standard14Cache[BuilderTypesToNames[fontType]];
}
/// <summary>
/// Determines if a font with this name is a standard 14 font.
/// </summary>
@@ -159,7 +165,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.3-alpha001</Version>
<Version>0.1.4</Version>
<IsTestProject>False</IsTestProject>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<SignAssembly>true</SignAssembly>

View File

@@ -0,0 +1,22 @@
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(34, names);
Assert.Equal(38, names);
}
}
}

View File

@@ -7,6 +7,8 @@
using PdfPig.Tokens;
using PdfPig.Core;
using Tokens;
using UglyToad.PdfPig.Graphics.Core;
using UglyToad.PdfPig.Graphics.Operations.TextPositioning;
internal class TestOperationContext : IOperationContext
{
@@ -88,6 +90,66 @@
}
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()
{
}
@@ -124,6 +186,91 @@
{
}
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,6 +74,7 @@
"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",
@@ -197,6 +198,8 @@
"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,6 +588,21 @@
}
}
[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.3-alpha001</Version>
<Version>0.1.4</Version>
<IsTestProject>False</IsTestProject>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<SignAssembly>true</SignAssembly>

View File

@@ -338,6 +338,7 @@
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.3-alpha001</Version>
<Version>0.1.4</Version>
<IsTestProject>False</IsTestProject>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<SignAssembly>true</SignAssembly>

View File

@@ -68,7 +68,6 @@
/// <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>
internal class MediaBox
public class MediaBox
{
///<summary>
/// User space units per inch.
@@ -66,8 +66,14 @@
/// </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,7 +35,10 @@
/// </summary>
public CropBox CropBox { get; }
internal MediaBox MediaBox { get; }
/// <summary>
/// Defines the boundaries of the physical medium on which the page shall be displayed or printed.
/// </summary>
public MediaBox MediaBox { get; }
internal PageContent Content { get; }

View File

@@ -1,9 +1,5 @@
namespace UglyToad.PdfPig.Graphics
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Colors;
using Content;
using Core;
@@ -14,8 +10,13 @@
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;
@@ -241,20 +242,7 @@
var renderingMatrix =
TransformationMatrix.FromValues(fontSize * horizontalScaling, 0, 0, fontSize, 0, rise);
// 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;
}
var pointSize = Math.Round(transformationMatrix.Multiply(TextMatrices.TextMatrix).Transform(new PdfRectangle(0, 0, 1, fontSize)).Height, 2);
while (bytes.MoveNext())
{
@@ -292,7 +280,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));
@@ -573,7 +561,67 @@
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)
@@ -757,7 +805,10 @@
if (markedContentStack.CanPop)
{
var mc = markedContentStack.Pop(pdfScanner);
if (mc != null) markedContents.Add(mc);
if (mc != null)
{
markedContents.Add(mc);
}
}
}
@@ -765,9 +816,86 @@
{
var matrix = TransformationMatrix.GetTranslationMatrix(tx, ty);
var newMatrix = matrix.Multiply(TextMatrices.TextMatrix);
TextMatrices.TextMatrix = matrix.Multiply(TextMatrices.TextMatrix);
}
TextMatrices.TextMatrix = newMatrix;
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;
}
}
}

View File

@@ -3,25 +3,13 @@
using PdfPig.Core;
using System.Collections.Generic;
using Tokens;
using Util.JetBrains.Annotations;
using UglyToad.PdfPig.Graphics.Core;
/// <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>
@@ -32,22 +20,11 @@
/// </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>
@@ -59,7 +36,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();
@@ -85,7 +62,7 @@
/// Start a new sub-path.
/// </summary>
void BeginSubpath();
/// <summary>
/// Close the current subpath.
/// </summary>
@@ -116,6 +93,36 @@
/// <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").
@@ -162,5 +169,90 @@
/// 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.GetCurrentState().Flatness = Tolerance;
operationContext.SetFlatnessTolerance(Tolerance);
}
/// <inheritdoc />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,6 +20,7 @@
/// <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,14 +71,7 @@
/// <inheritdoc />
public void Run(IOperationContext operationContext)
{
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;
operationContext.BezierCurveTo((double)X1, (double)Y1, (double)X2, (double)Y2, (double)X3, (double)Y3);
}
/// <inheritdoc />

View File

@@ -56,12 +56,7 @@
/// <inheritdoc />
public void Run(IOperationContext operationContext)
{
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;
operationContext.BezierCurveTo((double)X1, (double)Y1, (double)X3, (double)Y3, (double)X3, (double)Y3);
}
/// <inheritdoc />

View File

@@ -56,12 +56,7 @@
/// <inheritdoc />
public void Run(IOperationContext operationContext)
{
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();
operationContext.Rectangle((double)LowerLeftX, (double)LowerLeftY, (double)Width, (double)Height);
}
/// <inheritdoc />

View File

@@ -56,17 +56,7 @@
/// <inheritdoc />
public void Run(IOperationContext operationContext)
{
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;
operationContext.BezierCurveTo((double)X2, (double)Y2, (double)X3, (double)Y3);
}
/// <inheritdoc />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,7 +7,6 @@
using PdfPig.Parser.Parts;
using Tokenization.Scanner;
using Tokens;
using Util;
internal class EncodingReader : IEncodingReader
{
@@ -95,20 +94,17 @@
return differences;
}
var activeCode = differenceArray.GetNumeric(0).Int;
for (int i = 1; i < differenceArray.Data.Count; i++)
var currentIndex = -1;
foreach (var differenceEntry in differenceArray.Data)
{
var entry = differenceArray.Data[i];
if (entry is NumericToken numeric)
if (differenceEntry is NumericToken number)
{
activeCode = numeric.Int;
currentIndex = number.Int;
}
else if (entry is NameToken name)
else if (differenceEntry is NameToken name)
{
differences.Add((activeCode, name.Data));
activeCode++;
differences.Add((currentIndex, name.Data));
currentIndex++;
}
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,30 +140,28 @@
private CMap ReadEncoding(DictionaryToken dictionary, out bool isCMapPredefined)
{
isCMapPredefined = false;
CMap result = default(CMap);
CMap result;
if (dictionary.TryGet(NameToken.Encoding, out var value))
if (dictionary.TryGet(NameToken.Encoding, scanner, out NameToken encodingName))
{
if (value is NameToken encodingName)
{
var cmap = CMapCache.Get(encodingName.Data);
var cmap = CMapCache.Get(encodingName.Data);
result = cmap ?? throw new InvalidOperationException("Missing CMap for " + encodingName.Data);
result = cmap ?? throw new InvalidOperationException($"Missing CMap named {encodingName.Data}.");
isCMapPredefined = true;
}
else if (value is StreamToken stream)
{
var decoded = stream.Decode(filterProvider);
isCMapPredefined = true;
}
else if (dictionary.TryGet(NameToken.Encoding, scanner, out 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 for " + dictionary);
}
else
{
throw new InvalidOperationException("Could not read the encoding, expected a name or a stream but got a: " + value.GetType().Name);
}
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}");
}
return result;

View File

@@ -0,0 +1,19 @@
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

@@ -0,0 +1,33 @@
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,6 +800,13 @@
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.3-alpha001</Version>
<Version>0.1.4</Version>
<IsTestProject>False</IsTestProject>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<SignAssembly>true</SignAssembly>

View File

@@ -1,24 +0,0 @@
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

@@ -1,75 +0,0 @@
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

@@ -1,167 +0,0 @@
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

@@ -1,96 +0,0 @@
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

@@ -1,34 +0,0 @@
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,10 +2,9 @@
{
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;
@@ -150,36 +149,31 @@
private class DocumentMerger
{
private const decimal DefaultVersion = 1.2m;
private readonly PdfStreamWriter context;
private readonly List<IndirectReferenceToken> pagesTokenReferences;
private readonly PdfStreamWriter context = new PdfStreamWriter();
private readonly List<IndirectReferenceToken> pagesTokenReferences = new List<IndirectReferenceToken>();
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 copiedPages = copier.CopyObject(new IndirectReferenceToken(documentCatalog.PageTree.Reference), tokenScanner) as IndirectReferenceToken;
pagesTokenReferences.Add(copiedPages);
var (pagesReference, count) = CopyPagesTree(documentCatalog.PageTree, rootPagesReference, tokenScanner);
pageCount += count;
pagesTokenReferences.Add(pagesReference);
pageCount += documentCatalog.PagesDictionary.Get<NumericToken>(NameToken.Count, tokenScanner).Int;
copier.ClearReference();
referencesFromDocument.Clear();
}
public byte[] Build()
@@ -196,7 +190,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>
{
@@ -205,9 +199,9 @@
});
var catalogRef = context.WriteToken(catalog);
context.Flush(currentVersion, catalogRef);
var bytes = context.ToArray();
Close();
@@ -215,10 +209,165 @@
return bytes;
}
private void Close()
public 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,7 +91,8 @@
/// <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>
public void DrawRectangle(PdfPoint position, decimal width, decimal height, decimal lineWidth = 1)
/// <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)
{
if (lineWidth != 1)
{
@@ -99,7 +100,15 @@
}
operations.Add(new AppendRectangle((decimal)position.X, (decimal)position.Y, width, height));
operations.Add(StrokePath.Value);
if (fill)
{
operations.Add(FillPathEvenOddRuleAndStroke.Value);
}
else
{
operations.Add(StrokePath.Value);
}
if (lineWidth != 1)
{

View File

@@ -9,8 +9,7 @@
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
{
@@ -18,34 +17,20 @@
private readonly Dictionary<IndirectReferenceToken, IToken> tokenReferences = new Dictionary<IndirectReferenceToken, IToken>();
private int currentNumber = 1;
public int CurrentNumber { get; private set; } = 1;
private Stream stream;
public Stream Stream { get; private set; }
/// <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)
@@ -53,14 +38,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;
@@ -68,10 +53,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);
@@ -87,105 +72,75 @@
}
// 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 (reservedNumber.HasValue)
// 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 (!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);
}
return AddToken(token, currentNumber++);
var reference = FindToken(token);
if (reference == null)
{
return AddToken(token, CurrentNumber++);
}
return reference;
}
/// <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 IOException("Unable to read all the bytes from stream");
throw new Exception("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)
@@ -196,6 +151,21 @@
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.3-alpha001</Version>
<Version>0.1.4</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>