Compare commits

...

25 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
Eliot Jones
034e9a9ec3 Merge pull request #222 from BobLd/read-order-improv
Improve `UnsupervisedReadingOrderDetector` customisation
2020-09-29 14:22:03 +01:00
BobLd
d3b619b6ee Fix Error CS0136 and tidy up code 2020-09-29 13:50:05 +01:00
BobLd
90dacb1fcf Improve UnsupervisedReadingOrderDetector customisation: allows changing the spatial reasoning rule in use and using or not rendering order. 2020-09-26 12:29:24 +01:00
Eliot Jones
9f225cb2c3 Merge pull request #216 from BobLd/pub-clipping
IntersectsWith(rect, line) and Intersect(rect, line)
2020-09-22 21:02:22 +01:00
Eliot Jones
2ec79852cb Merge pull request #220 from BobLd/fix-clip-line-fill
fix clipping error when trying to fill a single line
2020-09-22 20:36:24 +01:00
BobLd
8f0f7769a6 fix clipping error when trying to fill a single line; add log; set EvenOdd as default in initiate CurrentClippingPath; add tests 2020-09-22 10:47:34 +01:00
BobLd
46b183f564 add IntersectsWith(PdfRectangle, PdfLine) and Intersect(PdfRectangle, PdfLine) 2020-09-15 11:41:24 +01:00
Eliot Jones
e41ae7d3a2 Merge pull request #213 from BobLd/linux-font
fix LinuxSystemFontLister not able to find any font
2020-09-12 19:12:50 +01:00
BobLd
d29cc52973 add test that fails on linux: LinuxSystemFontLister cannot find the 'TimesNewRomanPSMT' font 2020-09-12 17:11:55 +01:00
BobLd
4ca020fc34 LinuxSystemFontLister: also check the HOME env variable and explore all sub-directories 2020-09-12 16:59:44 +01:00
Eliot Jones
1502cf60fe Merge pull request #207 from BobLd/pdfpath-ext
Adding Pdfpath/PdfSubpath extensions
2020-09-10 17:28:58 +01:00
Eliot Jones
85ef2daba8 Merge pull request #210 from Poltuu/add_font3_name
ADD : name on type 3 fonts
2020-09-10 13:59:30 +01:00
romain v
a544dc4bde refacto : pr comments 2020-09-10 14:45:24 +02:00
romain v
9315ec5a4f ADD : name on type 3 fonts 2020-09-10 14:16:14 +02:00
BobLd
7b085a8498 update documentation and add note about concave poly issue 2020-09-01 11:18:22 +01:00
BobLd
e5ab4eb571 minor corrections 2020-08-31 11:36:17 +01:00
BobLd
77d448c517 better functions documentation 2020-08-30 13:57:02 +01:00
BobLd
ce3edcd0cc tidy up code and use EvenOdd as default in PointInPaths() 2020-08-30 13:44:35 +01:00
BobLd
f656922d2d add tests for PdfPath.Contains(Rectangle) - EvenOdd 2020-08-29 19:27:12 +01:00
BobLd
b9419469ac add PdfSubpath and PdfPath geometry extensions 2020-08-29 17:08:02 +01:00
19 changed files with 1191 additions and 296 deletions

View File

@@ -18,7 +18,8 @@
public IReadOnlyList<IPathCommand> Commands => commands;
/// <summary>
/// True if the <see cref="PdfSubpath"/> was originaly draw as a rectangle.
/// True if the <see cref="PdfSubpath"/> was originaly drawn using the rectangle ('re') operator.
/// <para>Always false if paths are clipped.</para>
/// </summary>
public bool IsDrawnAsRectangle { get; internal set; }
@@ -34,7 +35,6 @@
/// <summary>
/// Return true if points are organised in a counterclockwise order. Works only with closed paths.
/// </summary>
/// <returns></returns>
public bool IsCounterClockwise => IsClosed() && shoeLaceSum < 0;
/// <summary>

View File

@@ -1,31 +1,116 @@
namespace UglyToad.PdfPig.DocumentLayoutAnalysis.ReadingOrderDetector
{
using System;
using System.Collections.Generic;
using System.Linq;
/// <summary>
/// Algorithm that retrieve the blocks' reading order using both (spatial) Allens interval relations and rendering order (TextSequence).
/// <para>See section 4.1 of 'Unsupervised document structure analysis of digital scientific articles' by S. Klampfl, M. Granitzer, K. Jack, R. Kern and 'Document Understanding for a Broad Class of Documents' by L. Todoran, M. Worring, M. Aiello and C. Monz.</para>
/// Algorithm that retrieve the blocks' reading order using spatial reasoning (Allens interval relations) and possibly the rendering order (TextSequence).
/// <para>See section 4.1 of 'Unsupervised document structure analysis of digital scientific articles' by S. Klampfl, M. Granitzer, K. Jack, R. Kern
/// and 'Document Understanding for a Broad Class of Documents' by L. Todoran, M. Worring, M. Aiello and C. Monz.</para>
/// </summary>
public class UnsupervisedReadingOrderDetector : IReadingOrderDetector
{
/// <summary>
/// The rules encoding the spatial reasoning constraints.
/// <para>See 'Document Understanding for a Broad Class of Documents' by L. Todoran, M. Worring, M. Aiello and C. Monz.</para>
/// </summary>
public enum SpatialReasoningRules
{
/// <summary>
/// Basic spacial reasoning.
/// <para>In western culture the reading order is from left to right and from top to bottom.</para>
/// </summary>
Basic = 0,
/// <summary>
/// Text-blocks are read in rows from left-to-right, top-to-bottom.
/// <para>The diagonal direction 'left-bottom to top-right' cannot be present among the Basic relations allowed.</para>
/// </summary>
RowWise = 1,
/// <summary>
/// Text-blocks are read in columns, from top-to-bottom and from left-to-right.
/// <para>The diagonal direction 'right-top to bottom-left' cannot be present among the Basic relations allowed.</para>
/// </summary>
ColumnWise = 2
}
/// <summary>
/// Create an instance of unsupervised reading order detector, <see cref="UnsupervisedReadingOrderDetector"/>.
/// <para>This detector uses the (spatial) Allens interval relations and rendering order (TextSequence).</para>
/// <para>This detector uses spatial reasoning (Allens interval relations) and possibly the rendering order (TextSequence).</para>
/// </summary>
public static UnsupervisedReadingOrderDetector Instance { get; } = new UnsupervisedReadingOrderDetector();
private readonly double T;
/// <summary>
/// Whether or not to also use the rendering order, as indicated by the TextSequence.
/// </summary>
public bool UseRenderingOrder { get; }
/// <summary>
/// Algorithm that retrieve the blocks' reading order using both (spatial) Allens interval relations and rendering order.
/// The rule to be used that encodes the spatial reasoning constraints.
/// </summary>
public SpatialReasoningRules SpatialReasoningRule { get; }
/// <summary>
/// The tolerance parameter T. If two coordinates are closer than T they are considered equal.
/// <para>This flexibility is necessary because due to the inherent noise in the PDF extraction text blocks in the
/// same column might not be exactly aligned.</para>
/// </summary>
public double T { get; }
private Func<TextBlock, TextBlock, double, bool> getBeforeInMethod;
/// <summary>
/// Algorithm that retrieve the blocks' reading order using spatial reasoning (Allens interval relations) and possibly the rendering order (TextSequence).
/// </summary>
/// <param name="T">The tolerance parameter T. If two coordinates are closer than T they are considered equal.
/// This flexibility is necessary because due to the inherent noise in the PDF extraction text blocks in the
/// same column might not be exactly aligned.</param>
public UnsupervisedReadingOrderDetector(double T = 5)
/// <param name="spatialReasoningRule">The rule to be used that encodes the spatial reasoning constraints.</param>
/// <param name="useRenderingOrder">Whether or not to also use the rendering order, as indicated by the TextSequence.</param>
public UnsupervisedReadingOrderDetector(double T = 5, SpatialReasoningRules spatialReasoningRule = SpatialReasoningRules.ColumnWise, bool useRenderingOrder = true)
{
this.T = T;
this.SpatialReasoningRule = spatialReasoningRule;
this.UseRenderingOrder = useRenderingOrder;
switch (SpatialReasoningRule)
{
case SpatialReasoningRules.ColumnWise:
if (UseRenderingOrder)
{
getBeforeInMethod = (TextBlock a, TextBlock b, double t) => GetBeforeInReadingVertical(a, b, t) || GetBeforeInRendering(a, b);
}
else
{
getBeforeInMethod = GetBeforeInReadingVertical;
}
break;
case SpatialReasoningRules.RowWise:
if (UseRenderingOrder)
{
getBeforeInMethod = (TextBlock a, TextBlock b, double t) => GetBeforeInReadingHorizontal(a, b, t) || GetBeforeInRendering(a, b);
}
else
{
getBeforeInMethod = GetBeforeInReadingHorizontal;
}
break;
case SpatialReasoningRules.Basic:
default:
if (UseRenderingOrder)
{
getBeforeInMethod = (TextBlock a, TextBlock b, double t) => GetBeforeInReading(a, b, t) || GetBeforeInRendering(a, b);
}
else
{
getBeforeInMethod = GetBeforeInReading;
}
break;
}
}
/// <summary>
@@ -78,7 +163,7 @@
if (i == j) continue;
var b = textBlocks[j];
if (GetBeforeInReadingRendering(a, b, T))
if (getBeforeInMethod(a, b, T))
{
graph[i].Add(j);
}
@@ -88,19 +173,20 @@
return graph;
}
private bool GetBeforeInReadingRendering(TextBlock a, TextBlock b, double T)
{
return GetBeforeInReadingVertical(a, b, T) || GetBeforeInRendering(a, b);
}
private bool GetBeforeInRendering(TextBlock a, TextBlock b)
private static bool GetBeforeInRendering(TextBlock a, TextBlock b)
{
var avgTextSequenceA = a.TextLines.SelectMany(tl => tl.Words).SelectMany(w => w.Letters).Select(l => l.TextSequence).Average();
var avgTextSequenceB = b.TextLines.SelectMany(tl => tl.Words).SelectMany(w => w.Letters).Select(l => l.TextSequence).Average();
return avgTextSequenceA < avgTextSequenceB;
}
private bool GetBeforeInReading(TextBlock a, TextBlock b, double T)
/// <summary>
/// Rule encoding the fact that in western culture the reading order is from left to right and from top to bottom.
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <param name="T">The tolerance parameter T.</param>
private static bool GetBeforeInReading(TextBlock a, TextBlock b, double T)
{
IntervalRelations xRelation = GetIntervalRelationX(a, b, T);
IntervalRelations yRelation = GetIntervalRelationY(a, b, T);
@@ -119,8 +205,7 @@
/// <param name="a"></param>
/// <param name="b"></param>
/// <param name="T">The tolerance parameter T.</param>
/// <returns></returns>
private bool GetBeforeInReadingVertical(TextBlock a, TextBlock b, double T)
private static bool GetBeforeInReadingVertical(TextBlock a, TextBlock b, double T)
{
IntervalRelations xRelation = GetIntervalRelationX(a, b, T);
IntervalRelations yRelation = GetIntervalRelationY(a, b, T);
@@ -150,7 +235,7 @@
/// <param name="a"></param>
/// <param name="b"></param>
/// <param name="T">The tolerance parameter T.</param>
private bool GetBeforeInReadingHorizontal(TextBlock a, TextBlock b, double T)
private static bool GetBeforeInReadingHorizontal(TextBlock a, TextBlock b, double T)
{
IntervalRelations xRelation = GetIntervalRelationX(a, b, T);
IntervalRelations yRelation = GetIntervalRelationY(a, b, T);
@@ -183,7 +268,7 @@
/// <param name="a"></param>
/// <param name="b"></param>
/// <param name="T">The tolerance parameter T. If two coordinates are closer than T they are considered equal.</param>
private IntervalRelations GetIntervalRelationX(TextBlock a, TextBlock b, double T)
private static IntervalRelations GetIntervalRelationX(TextBlock a, TextBlock b, double T)
{
if (a.BoundingBox.Right < b.BoundingBox.Left - T)
{
@@ -267,7 +352,7 @@
/// <param name="a"></param>
/// <param name="b"></param>
/// <param name="T">The tolerance parameter T. If two coordinates are closer than T they are considered equal.</param>
private IntervalRelations GetIntervalRelationY(TextBlock a, TextBlock b, double T)
private static IntervalRelations GetIntervalRelationY(TextBlock a, TextBlock b, double T)
{
if (a.BoundingBox.Bottom < b.BoundingBox.Top - T)
{
@@ -432,13 +517,5 @@
/// </summary>
Equals
}
private class NodeComparer : IComparer<KeyValuePair<int, List<int>>>
{
public int Compare(KeyValuePair<int, List<int>> x, KeyValuePair<int, List<int>> y)
{
return x.Value.Count.CompareTo(y.Value.Count);
}
}
}
}

View File

@@ -19,6 +19,10 @@
try
{
var folder = Environment.GetEnvironmentVariable("$HOME");
if (string.IsNullOrWhiteSpace(folder))
{
folder = Environment.GetEnvironmentVariable("HOME");
}
if (!string.IsNullOrWhiteSpace(folder))
{
@@ -48,7 +52,7 @@
try
{
files = Directory.GetFiles(directory);
files = Directory.GetFiles(directory, "*.*", SearchOption.AllDirectories);
}
catch
{

View File

@@ -0,0 +1,43 @@
using System.Collections.Generic;
using UglyToad.PdfPig.Tests.Dla;
using Xunit;
namespace UglyToad.PdfPig.Tests.Fonts.SystemFonts
{
public class Linux
{
public static IEnumerable<object[]> DataExtract => new[]
{
new object[]
{
"90 180 270 rotated.pdf",
new object[][]
{
new object[] { "[(x:53.88, y:759.48), 2.495859375, 0]", 0.0 },
new object[] { "[(x:514.925312502883, y:744.099765720344), 6.83203125, 7.94531249999983]", -90.0 },
new object[] { "[(x:512.505390717836, y:736.603703191305), 5.1796875, 5.68945312499983]", -90.0 },
new object[] { "[(x:512.505390785898, y:730.931828191305), 3.99609375, 5.52539062499994]", -90.0 },
}
},
};
[Theory]
[MemberData(nameof(DataExtract))]
public void GetCorrectBBoxLinux(string name, object[][] expected)
{
// success on Windows but LinuxSystemFontLister cannot find the 'TimesNewRomanPSMT' font
using (var document = PdfDocument.Open(DlaHelper.GetDocumentPath(name)))
{
var page = document.GetPage(1);
for (int i = 0; i < expected.Length; i++)
{
string bbox = (string)expected[i][0];
var rotation = (double)expected[i][1];
var current = page.Letters[i];
Assert.Equal(bbox, current.GlyphRectangle.ToString());
Assert.Equal(rotation, current.GlyphRectangle.Rotation, 3);
}
}
}
}
}

View File

@@ -0,0 +1,19 @@
using UglyToad.PdfPig.Tests.Integration;
using Xunit;
namespace UglyToad.PdfPig.Tests.Geometry
{
public class ClippingTests
{
[Fact]
public void ContainsRectangleEvenOdd()
{
using (var document = PdfDocument.Open(IntegrationHelpers.GetDocumentPath("SPARC - v9 Architecture Manual"),
new ParsingOptions() { ClipPaths = true }))
{
var page = document.GetPage(45);
Assert.Equal(28, page.ExperimentalAccess.Paths.Count);
}
}
}
}

View File

@@ -0,0 +1,38 @@
namespace UglyToad.PdfPig.Tests.Geometry
{
using System.Linq;
using UglyToad.PdfPig.Core;
using UglyToad.PdfPig.Geometry;
using UglyToad.PdfPig.Tests.Integration;
using Xunit;
public class PdfPathExtensionsTests
{
[Fact]
public void ContainsRectangleEvenOdd()
{
using (var document = PdfDocument.Open(IntegrationHelpers.GetDocumentPath("path_ext_oddeven"),
new ParsingOptions() { ClipPaths = true }))
{
var page = document.GetPage(1);
var words = page.GetWords().ToList();
foreach (var path in page.ExperimentalAccess.Paths)
{
Assert.NotEqual(FillingRule.NonZeroWinding, path.FillingRule); // allow none and even-odd
foreach (var c in words.Where(w => path.Contains(w.BoundingBox)).ToList())
{
Assert.Equal("in", c.Text.Split("_").Last());
words.Remove(c);
}
}
foreach (var w in words)
{
Assert.NotEqual("in", w.Text.Split("_").Last());
}
}
}
}
}

View File

@@ -6,6 +6,7 @@
using ClipperLibrary;
using Core;
using Graphics;
using UglyToad.PdfPig.Logging;
using static Core.PdfSubpath;
/// <summary>
@@ -13,7 +14,7 @@
/// </summary>
internal static class ClippingExtensions
{
private const double Factor = 10_000.0;
public const double Factor = 10_000.0;
/// <summary>
/// Number of lines to use when transforming bezier curve to polyline.
@@ -23,7 +24,7 @@
/// <summary>
/// Generates the result of applying a clipping path to another path.
/// </summary>
public static PdfPath Clip(this PdfPath clipping, PdfPath subject)
public static PdfPath Clip(this PdfPath clipping, PdfPath subject, ILog log = null)
{
if (clipping == null)
{
@@ -61,7 +62,10 @@
subPathClipping.CloseSubpath();
}
clipper.AddPath(subPathClipping.ToClipperPolygon().ToList(), ClipperPolyType.Clip, true);
if (!clipper.AddPath(subPathClipping.ToClipperPolygon().ToList(), ClipperPolyType.Clip, true))
{
log?.Error("ClippingExtensions.Clip(): failed to add clipping subpath.");
}
}
// Subject path
@@ -74,13 +78,27 @@
continue;
}
if (subjectClose && !subPathSubject.IsClosed()
&& subPathSubject.Commands.Count(sp => sp is Line) < 2
&& subPathSubject.Commands.Count(sp => sp is BezierCurve) == 0)
{
// strange here:
// the subpath contains maximum 1 line and no curves
// it cannot be filled or a be clipping path
// cancel closing the path/subpath
subjectClose = false;
}
// Force close subject if need be
if (subjectClose && !subPathSubject.IsClosed())
{
subPathSubject.CloseSubpath();
}
clipper.AddPath(subPathSubject.ToClipperPolygon().ToList(), ClipperPolyType.Subject, subjectClose);
if (!clipper.AddPath(subPathSubject.ToClipperPolygon().ToList(), ClipperPolyType.Subject, subjectClose))
{
log?.Error("ClippingExtensions.Clip(): failed to add subject subpath for clipping.");
}
}
var clippingFillType = clipping.FillingRule == FillingRule.NonZeroWinding ? ClipperPolyFillType.NonZero : ClipperPolyFillType.EvenOdd;
@@ -157,18 +175,19 @@
/// Converts a path to a set of points for the Clipper algorithm to use.
/// Allows duplicate points as they will be removed by Clipper.
/// </summary>
private static IEnumerable<ClipperIntPoint> ToClipperPolygon(this PdfSubpath pdfPath)
internal static IEnumerable<ClipperIntPoint> ToClipperPolygon(this PdfSubpath pdfPath)
{
if (pdfPath.Commands.Count == 0)
{
yield break;
}
if (pdfPath.Commands[0] is Move currentMove)
ClipperIntPoint movePoint;
if (pdfPath.Commands[0] is Move move)
{
var previous = new ClipperIntPoint(currentMove.Location.X * Factor, currentMove.Location.Y * Factor);
movePoint = move.Location.ToClipperIntPoint();
yield return previous;
yield return movePoint;
if (pdfPath.Commands.Count == 1)
{
@@ -185,27 +204,49 @@
var command = pdfPath.Commands[i];
if (command is Move)
{
throw new ArgumentException("ToClipperPolygon():only one move allowed per subpath.", nameof(pdfPath));
throw new ArgumentException("ToClipperPolygon(): only one move allowed per subpath.", nameof(pdfPath));
}
if (command is Line line)
{
yield return new ClipperIntPoint(line.From.X * Factor, line.From.Y * Factor);
yield return new ClipperIntPoint(line.To.X * Factor, line.To.Y * Factor);
yield return line.From.ToClipperIntPoint();
yield return line.To.ToClipperIntPoint();
}
else if (command is BezierCurve curve)
{
foreach (var lineB in curve.ToLines(LinesInCurve))
{
yield return new ClipperIntPoint(lineB.From.X * Factor, lineB.From.Y * Factor);
yield return new ClipperIntPoint(lineB.To.X * Factor, lineB.To.Y * Factor);
yield return lineB.From.ToClipperIntPoint();
yield return lineB.To.ToClipperIntPoint();
}
}
else if (command is Close)
{
yield return new ClipperIntPoint(currentMove.Location.X * Factor, currentMove.Location.Y * Factor);
if (movePoint == null)
{
throw new ArgumentException("ToClipperPolygon(): Move command was null, cannot close the subpath.");
}
yield return movePoint;
}
}
}
internal static IEnumerable<ClipperIntPoint> ToClipperPolygon(this PdfRectangle rectangle)
{
yield return rectangle.BottomLeft.ToClipperIntPoint();
yield return rectangle.TopLeft.ToClipperIntPoint();
yield return rectangle.TopRight.ToClipperIntPoint();
yield return rectangle.BottomRight.ToClipperIntPoint();
}
internal static ClipperIntPoint ToClipperIntPoint(this PdfPoint point)
{
return new ClipperIntPoint(point.X * Factor, point.Y * Factor);
}
internal static List<ClipperIntPoint> ToClipperIntPoint(this PdfLine line)
{
return new List<ClipperIntPoint>() { line.Point1.ToClipperIntPoint(), line.Point2.ToClipperIntPoint() };
}
}
}

View File

@@ -5,6 +5,8 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UglyToad.PdfPig.Geometry.ClipperLibrary;
using UglyToad.PdfPig.Graphics;
using static UglyToad.PdfPig.Core.PdfSubpath;
/// <summary>
@@ -57,14 +59,14 @@
}
/// <summary>
/// Algorithm to find a minimal bounding rectangle (MBR) such that the MBR corresponds to a rectangle
/// Algorithm to find a minimal bounding rectangle (MBR) such that the MBR corresponds to a rectangle
/// with smallest possible area completely enclosing the polygon.
/// <para>From 'A Fast Algorithm for Generating a Minimal Bounding Rectangle' by Lennert D. Den Boer.</para>
/// </summary>
/// <param name="polygon">
/// Polygon P is assumed to be both simple and convex, and to contain no duplicate (coincident) vertices.
/// The vertices of P are assumed to be in strict cyclic sequential order, either clockwise or
/// counter-clockwise relative to the origin P0.
/// The vertices of P are assumed to be in strict cyclic sequential order, either clockwise or
/// counter-clockwise relative to the origin P0.
/// </param>
private static PdfRectangle ParametricPerpendicularProjection(IReadOnlyList<PdfPoint> polygon)
{
@@ -180,7 +182,7 @@
return new PdfRectangle(new PdfPoint(MBR[4], MBR[5]),
new PdfPoint(MBR[6], MBR[7]),
new PdfPoint(MBR[2], MBR[3]),
new PdfPoint(MBR[2], MBR[3]),
new PdfPoint(MBR[0], MBR[1]));
}
@@ -191,7 +193,7 @@
/// <param name="points">The points.</param>
public static PdfRectangle MinimumAreaRectangle(IEnumerable<PdfPoint> points)
{
if (points == null || points.Count() == 0)
if (points?.Any() != true)
{
throw new ArgumentException("MinimumAreaRectangle(): points cannot be null and must contain at least one point.", nameof(points));
}
@@ -208,7 +210,7 @@
{
if (points == null || points.Count < 2)
{
throw new ArgumentException("OrientedBoundingBox(): points cannot be null and must contain at least two points.");
throw new ArgumentException("OrientedBoundingBox(): points cannot be null and must contain at least two points.", nameof(points));
}
// Fitting a line through the points
@@ -250,8 +252,7 @@
cos, sin, 0,
-sin, cos, 0,
0, 0, 1);
var obb = rotateBack.Transform(aabb);
return obb;
return rotateBack.Transform(aabb);
}
/// <summary>
@@ -259,9 +260,9 @@
/// </summary>
public static IEnumerable<PdfPoint> GrahamScan(IEnumerable<PdfPoint> points)
{
if (points == null || points.Count() == 0)
if (points?.Any() != true)
{
throw new ArgumentException("GrahamScan(): points cannot be null and must contain at least one point.");
throw new ArgumentException("GrahamScan(): points cannot be null and must contain at least one point.", nameof(points));
}
if (points.Count() < 3) return points;
@@ -321,7 +322,7 @@
#region PdfRectangle
/// <summary>
/// Whether the rectangle contains the point.
/// Whether the point is located inside the rectangle.
/// </summary>
/// <param name="rectangle">The rectangle that should contain the point.</param>
/// <param name="point">The point that should be contained within the rectangle.</param>
@@ -370,7 +371,7 @@
}
/// <summary>
/// Whether the rectangle contains the rectangle.
/// Whether the other rectangle is located inside the rectangle.
/// </summary>
/// <param name="rectangle">The rectangle that should contain the other rectangle.</param>
/// <param name="other">The other rectangle that should be contained within the rectangle.</param>
@@ -432,12 +433,12 @@
if (IntersectsWith(rectangle.BottomLeft, rectangle.BottomRight, other.BottomRight, other.TopRight)) return true;
if (IntersectsWith(rectangle.BottomLeft, rectangle.BottomRight, other.TopRight, other.TopLeft)) return true;
if (IntersectsWith(rectangle.BottomLeft, rectangle.BottomRight,other.TopLeft, other.BottomLeft)) return true;
if (IntersectsWith(rectangle.BottomRight, rectangle.TopRight, other.BottomLeft, other.BottomRight)) return true;
if (IntersectsWith(rectangle.BottomRight, rectangle.TopRight,other.BottomRight, other.TopRight)) return true;
if (IntersectsWith(rectangle.BottomRight, rectangle.TopRight, other.TopRight, other.TopLeft)) return true;
if (IntersectsWith(rectangle.BottomRight, rectangle.TopRight, other.TopLeft, other.BottomLeft)) return true;
if (IntersectsWith(rectangle.TopRight, rectangle.TopLeft, other.BottomLeft, other.BottomRight)) return true;
if (IntersectsWith(rectangle.TopRight, rectangle.TopLeft, other.BottomRight, other.TopRight)) return true;
if (IntersectsWith(rectangle.TopRight, rectangle.TopLeft, other.TopRight, other.TopLeft)) return true;
@@ -454,6 +455,7 @@
/// <summary>
/// Gets the <see cref="PdfRectangle"/> that is the intersection of two rectangles.
/// <para>Only works for axis-aligned rectangles.</para>
/// </summary>
public static PdfRectangle? Intersect(this PdfRectangle rectangle, PdfRectangle other)
{
@@ -473,11 +475,70 @@
var points = new[] { rectangle.BottomLeft, rectangle.BottomRight, rectangle.TopLeft, rectangle.TopRight };
return new PdfRectangle(points.Min(p => p.X), points.Min(p => p.Y), points.Max(p => p.X), points.Max(p => p.Y));
}
/// <summary>
/// Whether the rectangle and the line intersect.
/// </summary>
/// <param name="rectangle"></param>
/// <param name="line"></param>
public static bool IntersectsWith(this PdfRectangle rectangle, PdfLine line)
{
return IntersectsWith(rectangle, line.Point1, line.Point2);
}
/// <summary>
/// Gets the <see cref="PdfLine"/> that is the intersection of the rectangle and the line.
/// </summary>
/// <param name="rectangle"></param>
/// <param name="line"></param>
public static PdfLine? Intersect(this PdfRectangle rectangle, PdfLine line)
{
var i = Intersect(rectangle, line.Point1, line.Point2);
if (i != null)
{
return new PdfLine(i[0], i[1]);
}
return null;
}
/// <summary>
/// Gets the list of <see cref="PdfLine"/>s that are the intersection of the rectangle and the lines.
/// </summary>
/// <param name="rectangle"></param>
/// <param name="lines"></param>
/// <returns></returns>
public static List<PdfLine> Intersect(this PdfRectangle rectangle, List<PdfLine> lines)
{
var clipper = new Clipper();
clipper.AddPath(rectangle.ToClipperPolygon().ToList(), ClipperPolyType.Clip, true);
foreach (var line in lines)
{
clipper.AddPath(line.ToClipperIntPoint(), ClipperPolyType.Subject, false);
}
var solutions = new ClipperPolyTree();
if (clipper.Execute(ClipperClipType.Intersection, solutions))
{
List<PdfLine> rv = new List<PdfLine>();
foreach (var solution in solutions.Children)
{
rv.Add(new PdfLine(new PdfPoint(solution.Contour[0].X / ClippingExtensions.Factor, solution.Contour[0].Y / ClippingExtensions.Factor),
new PdfPoint(solution.Contour[1].X / ClippingExtensions.Factor, solution.Contour[1].Y / ClippingExtensions.Factor)));
}
return rv;
}
else
{
return new List<PdfLine>();
}
// clipper.clear() ??
}
#endregion
#region PdfLine
/// <summary>
/// Whether the line segment contains the point.
/// Whether the point is located on the line segment.
/// </summary>
public static bool Contains(this PdfLine line, PdfPoint point)
{
@@ -531,11 +592,31 @@
{
return ParallelTo(line.Point1, line.Point2, other.From, other.To);
}
/// <summary>
/// Gets the <see cref="PdfLine"/> that is the intersection of the rectangle and the line.
/// </summary>
/// <param name="rectangle"></param>
/// <param name="line"></param>
public static PdfLine? Intersect(this PdfLine line, PdfRectangle rectangle)
{
return rectangle.Intersect(line);
}
/// <summary>
/// Whether the rectangle and the line intersect.
/// </summary>
/// <param name="rectangle"></param>
/// <param name="line"></param>
public static bool IntersectsWith(this PdfLine line, PdfRectangle rectangle)
{
return rectangle.IntersectsWith(line);
}
#endregion
#region Path Line
/// <summary>
/// Whether the line segment contains the point.
/// Whether the point is located on the line segment.
/// </summary>
public static bool Contains(this Line line, PdfPoint point)
{
@@ -655,6 +736,65 @@
}
}
/// <summary>
/// The intersection of the line formed by <paramref name="pl1"/> and <paramref name="pl2"/>
/// intersects the rectangle.
/// </summary>
private static PdfPoint[] Intersect(PdfRectangle rectangle, PdfPoint pl1, PdfPoint pl2)
{
var clipper = new Clipper();
clipper.AddPath(rectangle.ToClipperPolygon().ToList(), ClipperPolyType.Clip, true);
clipper.AddPath(new List<ClipperIntPoint>() { pl1.ToClipperIntPoint(), pl2.ToClipperIntPoint() }, ClipperPolyType.Subject, false);
var solutions = new ClipperPolyTree();
if (clipper.Execute(ClipperClipType.Intersection, solutions))
{
if (solutions.Children.Count == 0)
{
return null;
}
else if (solutions.Children.Count == 1)
{
var solution = solutions.Children[0];
return new[]
{
new PdfPoint(solution.Contour[0].X / ClippingExtensions.Factor, solution.Contour[0].Y / ClippingExtensions.Factor),
new PdfPoint(solution.Contour[1].X / ClippingExtensions.Factor, solution.Contour[1].Y / ClippingExtensions.Factor)
};
}
else
{
throw new ArgumentException("GeometryExtensions.Intersect(PdfRectangle, PdfPoint, PdfPoint): more than one solution found.");
}
}
else
{
return null;
}
// clipper.clear() ??
}
/// <summary>
/// Whether the line formed by <paramref name="pl1"/> and <paramref name="pl2"/>
/// intersects the rectangle.
/// </summary>
public static bool IntersectsWith(PdfRectangle rectangle, PdfPoint pl1, PdfPoint pl2)
{
var clipper = new Clipper();
clipper.AddPath(rectangle.ToClipperPolygon().ToList(), ClipperPolyType.Clip, true);
clipper.AddPath(new List<ClipperIntPoint>() { pl1.ToClipperIntPoint(), pl2.ToClipperIntPoint() }, ClipperPolyType.Subject, false);
var solutions = new ClipperPolyTree();
if (clipper.Execute(ClipperClipType.Intersection, solutions))
{
return solutions.Children.Count > 0;
}
return false;
}
private static bool ParallelTo(PdfPoint p11, PdfPoint p12, PdfPoint p21, PdfPoint p22)
{
return Math.Abs((p12.Y - p11.Y) * (p22.X - p21.X) - (p22.Y - p21.Y) * (p12.X - p11.X)) < epsilon;
@@ -736,7 +876,7 @@
{
return Intersect(bezierCurve, line.From, line.To);
}
private static PdfPoint[] Intersect(BezierCurve bezierCurve, PdfPoint p1, PdfPoint p2)
{
var ts = IntersectT(bezierCurve, p1, p2);
@@ -811,7 +951,254 @@
var solution = SolveCubicEquation(a, b, c, d);
return solution.Where(s => !double.IsNaN(s)).Where(s => s >= -epsilon && (s - 1) <= epsilon).OrderBy(s => s).ToArray();
return solution.Where(s => !double.IsNaN(s) && s >= -epsilon && (s - 1) <= epsilon).OrderBy(s => s).ToArray();
}
#endregion
#region PdfPath & PdfSubpath
#region Clipper extension
// https://stackoverflow.com/questions/54723622/point-in-polygon-hit-test-algorithm
// Ported from Angus Johnson's Delphi Pascal code (Clipper's author)
// Might be made available in the next Clipper release?
private static double CrossProduct(ClipperIntPoint pt1, ClipperIntPoint pt2, ClipperIntPoint pt3)
{
return (pt2.X - pt1.X) * (pt3.Y - pt2.Y) - (pt2.Y - pt1.Y) * (pt3.X - pt2.X);
}
/// <summary>
/// nb: returns MaxInt ((2^32)-1) when pt is on a line
/// </summary>
private static int PointInPathsWindingCount(ClipperIntPoint pt, List<List<ClipperIntPoint>> paths)
{
var result = 0;
for (int i = 0; i < paths.Count; i++)
{
int j = 0;
List<ClipperIntPoint> p = paths[i];
int len = p.Count;
if (len < 3) continue;
ClipperIntPoint prevPt = p[len - 1];
while ((j < len) && (p[j].Y == prevPt.Y)) j++;
if (j == len) continue;
bool isAbove = prevPt.Y < pt.Y;
while (j < len)
{
if (isAbove)
{
while ((j < len) && (p[j].Y < pt.Y)) j++;
if (j == len)
{
break;
}
else if (j > 0)
{
prevPt = p[j - 1];
}
double crossProd = CrossProduct(prevPt, p[j], pt);
if (crossProd == 0)
{
return int.MaxValue;
}
else if (crossProd < 0)
{
result--;
}
}
else
{
while ((j < len) && (p[j].Y > pt.Y)) j++;
if (j == len)
{
break;
}
else if (j > 0)
{
prevPt = p[j - 1];
}
double crossProd = CrossProduct(prevPt, p[j], pt);
if (crossProd == 0)
{
return int.MaxValue;
}
else if (crossProd > 0)
{
result++;
}
}
j++;
isAbove = !isAbove;
}
}
return result;
}
private static bool PointInPaths(ClipperIntPoint pt, List<List<ClipperIntPoint>> paths, ClipperPolyFillType fillRule, bool includeBorder)
{
int wc = PointInPathsWindingCount(pt, paths);
if (wc == int.MaxValue)
{
return includeBorder;
}
switch (fillRule)
{
default:
case ClipperPolyFillType.EvenOdd:
return wc % 2 != 0;
case ClipperPolyFillType.NonZero:
return wc != 0;
}
}
#endregion
/// <summary>
/// Whether the point is located inside the subpath.
/// <para>Ignores winding rule.</para>
/// </summary>
/// <param name="subpath">The subpath that should contain the point.</param>
/// <param name="point">The point that should be contained within the subpath.</param>
/// <param name="includeBorder">If set to false, will return false if the point belongs to the subpath's border.</param>
public static bool Contains(this PdfSubpath subpath, PdfPoint point, bool includeBorder = false)
{
return PointInPaths(point.ToClipperIntPoint(),
new List<List<ClipperIntPoint>>() { subpath.ToClipperPolygon().ToList() },
ClipperPolyFillType.EvenOdd,
includeBorder);
}
/// <summary>
/// Whether the rectangle is located inside the subpath.
/// <para>Ignores winding rule.</para>
/// </summary>
/// <param name="subpath">The subpath that should contain the rectangle.</param>
/// <param name="rectangle">The rectangle that should be contained within the subpath.</param>
/// <param name="includeBorder">If set to false, will return false if the rectangle is on the subpath's border.</param>
public static bool Contains(this PdfSubpath subpath, PdfRectangle rectangle, bool includeBorder = false)
{
// NB, For later dev: Might not work for concave outer subpath, as it can contain all the points of the rectangle, but have overlapping edges.
var clipperPaths = new List<List<ClipperIntPoint>>() { subpath.ToClipperPolygon().ToList() };
foreach (var point in rectangle.ToClipperPolygon())
{
if (!PointInPaths(point, clipperPaths, ClipperPolyFillType.EvenOdd, includeBorder)) return false;
}
return true;
}
/// <summary>
/// Whether the other subpath is located inside the subpath.
/// <para>Ignores winding rule.</para>
/// </summary>
/// <param name="subpath">The subpath that should contain the rectangle.</param>
/// <param name="other">The other subpath that should be contained within the subpath.</param>
/// <param name="includeBorder">If set to false, will return false if the other subpath is on the subpath's border.</param>
public static bool Contains(this PdfSubpath subpath, PdfSubpath other, bool includeBorder = false)
{
// NB, For later dev: Might not work for concave outer subpath, as it can contain all the points of the inner subpath, but have overlapping edges.
var clipperPaths = new List<List<ClipperIntPoint>>() { subpath.ToClipperPolygon().ToList() };
foreach (var pt in other.ToClipperPolygon())
{
if (!PointInPaths(pt, clipperPaths, ClipperPolyFillType.EvenOdd, includeBorder)) return false;
}
return true;
}
/// <summary>
/// Get the area of the path.
/// </summary>
/// <param name="path"></param>
public static double GetArea(this PdfPath path)
{
var clipperPaths = path.Select(sp => sp.ToClipperPolygon().ToList()).ToList();
var simplifieds = Clipper.SimplifyPolygons(clipperPaths, path.FillingRule == FillingRule.NonZeroWinding ? ClipperPolyFillType.NonZero : ClipperPolyFillType.EvenOdd);
double sum = 0;
foreach (var simplified in simplifieds)
{
sum += Clipper.Area(simplified);
}
return sum;
}
/// <summary>
/// Whether the point is located inside the path.
/// </summary>
/// <param name="path">The path that should contain the point.</param>
/// <param name="point">The point that should be contained within the path.</param>
/// <param name="includeBorder">If set to false, will return false if the point belongs to the path's border.</param>
public static bool Contains(this PdfPath path, PdfPoint point, bool includeBorder = false)
{
var clipperPaths = path.Select(sp => sp.ToClipperPolygon().ToList()).ToList();
return PointInPaths(point.ToClipperIntPoint(),
clipperPaths,
path.FillingRule == FillingRule.NonZeroWinding ? ClipperPolyFillType.NonZero : ClipperPolyFillType.EvenOdd,
includeBorder);
}
/// <summary>
/// Whether the rectangle is located inside the path.
/// </summary>
/// <param name="path">The path that should contain the rectangle.</param>
/// <param name="rectangle">The rectangle that should be contained within the path.</param>
/// <param name="includeBorder">If set to false, will return false if the rectangle is on the path's border.</param>
public static bool Contains(this PdfPath path, PdfRectangle rectangle, bool includeBorder = false)
{
// NB, For later dev: Might not work for concave outer path, as it can contain all the points of the inner rectangle, but have overlapping edges.
var clipperPaths = path.Select(sp => sp.ToClipperPolygon().ToList()).ToList();
var fillType = path.FillingRule == FillingRule.NonZeroWinding ? ClipperPolyFillType.NonZero : ClipperPolyFillType.EvenOdd;
foreach (var point in rectangle.ToClipperPolygon())
{
if (!PointInPaths(point, clipperPaths, fillType, includeBorder)) return false;
}
return true;
}
/// <summary>
/// Whether the subpath is located inside the path.
/// </summary>
/// <param name="path">The path that should contain the subpath.</param>
/// <param name="subpath">The subpath that should be contained within the path.</param>
/// <param name="includeBorder">If set to false, will return false if the subpath is on the path's border.</param>
public static bool Contains(this PdfPath path, PdfSubpath subpath, bool includeBorder = false)
{
// NB, For later dev: Might not work for concave outer path, as it can contain all the points of the inner subpath, but have overlapping edges.
var clipperPaths = path.Select(sp => sp.ToClipperPolygon().ToList()).ToList();
var fillType = path.FillingRule == FillingRule.NonZeroWinding ? ClipperPolyFillType.NonZero : ClipperPolyFillType.EvenOdd;
foreach (var p in subpath.ToClipperPolygon())
{
if (!PointInPaths(p, clipperPaths, fillType, includeBorder)) return false;
}
return true;
}
/// <summary>
/// Whether the other path is located inside the path.
/// </summary>
/// <param name="path">The path that should contain the path.</param>
/// <param name="other">The other path that should be contained within the path.</param>
/// <param name="includeBorder">If set to false, will return false if the other subpath is on the path's border.</param>
public static bool Contains(this PdfPath path, PdfPath other, bool includeBorder = false)
{
// NB, For later dev: Might not work for concave outer path, as it can contain all the points of the inner path, but have overlapping edges.
var clipperPaths = path.Select(sp => sp.ToClipperPolygon().ToList()).ToList();
var fillType = path.FillingRule == FillingRule.NonZeroWinding ? ClipperPolyFillType.NonZero : ClipperPolyFillType.EvenOdd;
foreach (var subpath in other)
{
foreach (var p in subpath.ToClipperPolygon())
{
if (!PointInPaths(p, clipperPaths, fillType, includeBorder)) return false;
}
}
return true;
}
#endregion
@@ -938,8 +1325,7 @@
{
string BboxToRect(PdfRectangle box, string stroke)
{
var overallBbox = $"<rect x='{box.Left}' y='{box.Bottom}' width='{box.Width}' height='{box.Height}' stroke-width='2' fill='none' stroke='{stroke}'></rect>";
return overallBbox;
return $"<rect x='{box.Left}' y='{box.Bottom}' width='{box.Width}' height='{box.Height}' stroke-width='2' fill='none' stroke='{stroke}'></rect>";
}
var glyph = p.ToSvg(height);
@@ -958,9 +1344,7 @@
var path = $"<path d='{glyph}' stroke='cyan' stroke-width='3'></path>";
var bboxRect = bbox.HasValue ? BboxToRect(bbox.Value, "yellow") : string.Empty;
var others = string.Join(" ", bboxes.Select(x => BboxToRect(x, "gray")));
var result = $"<svg width='500' height='500'><g transform=\"scale(0.2, -0.2) translate(100, -700)\">{path} {bboxRect} {others}</g></svg>";
return result;
return $"<svg width='500' height='500'><g transform=\"scale(0.2, -0.2) translate(100, -700)\">{path} {bboxRect} {others}</g></svg>";
}
}
}

View File

@@ -106,7 +106,7 @@
var clippingSubpath = new PdfSubpath();
clippingSubpath.Rectangle(cropBox.BottomLeft.X, cropBox.BottomLeft.Y, cropBox.Width, cropBox.Height);
var clippingPath = new PdfPath() { clippingSubpath };
clippingPath.SetClipping(FillingRule.NonZeroWinding);
clippingPath.SetClipping(FillingRule.EvenOdd);
graphicsStack.Push(new CurrentGraphicsState()
{
@@ -627,7 +627,7 @@
if (clipPaths)
{
var clippedPath = currentState.CurrentClippingPath.Clip(CurrentPath);
var clippedPath = currentState.CurrentClippingPath.Clip(CurrentPath, log);
if (clippedPath != null)
{
paths.Add(clippedPath);
@@ -658,7 +658,7 @@
var currentClipping = GetCurrentState().CurrentClippingPath;
currentClipping.SetClipping(clippingRule);
var newClippings = CurrentPath.Clip(currentClipping);
var newClippings = CurrentPath.Clip(currentClipping, log);
if (newClippings == null)
{
log.Warn("Empty clipping path found. Clipping path not updated.");

View File

@@ -49,11 +49,23 @@
toUnicodeCMap = CMapCache.Parse(new ByteArrayInputBytes(decodedUnicodeCMap));
}
}
return new Type3Font(NameToken.Type3, boundingBox, fontMatrix, encoding, firstCharacter,
var name = GetFontName(dictionary);
return new Type3Font(name, boundingBox, fontMatrix, encoding, firstCharacter,
lastCharacter, widths, toUnicodeCMap);
}
private NameToken GetFontName(DictionaryToken dictionary)
{
if (dictionary.TryGet(NameToken.Name, scanner, out NameToken fontName))
{
return fontName;
}
return NameToken.Type3;
}
private TransformationMatrix GetFontMatrix(DictionaryToken dictionary)
{
if (!dictionary.TryGet(NameToken.FontMatrix, out var matrixObject))

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

@@ -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);