mirror of
https://github.com/UglyToad/PdfPig.git
synced 2026-03-10 00:23:29 +08:00
Compare commits
2 Commits
v0.1.3
...
debug-enco
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0370c51371 | ||
|
|
c634ec3aa2 |
@@ -18,8 +18,7 @@
|
||||
public IReadOnlyList<IPathCommand> Commands => commands;
|
||||
|
||||
/// <summary>
|
||||
/// True if the <see cref="PdfSubpath"/> was originaly drawn using the rectangle ('re') operator.
|
||||
/// <para>Always false if paths are clipped.</para>
|
||||
/// True if the <see cref="PdfSubpath"/> was originaly draw as a rectangle.
|
||||
/// </summary>
|
||||
public bool IsDrawnAsRectangle { get; internal set; }
|
||||
|
||||
@@ -35,6 +34,7 @@
|
||||
/// <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>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net45;net451;net452;net46;net461;net462;net47</TargetFrameworks>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Version>0.1.3</Version>
|
||||
<Version>0.1.2</Version>
|
||||
<IsTestProject>False</IsTestProject>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
|
||||
@@ -1,116 +1,31 @@
|
||||
namespace UglyToad.PdfPig.DocumentLayoutAnalysis.ReadingOrderDetector
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
/// <summary>
|
||||
/// Algorithm that retrieve the blocks' reading order using spatial reasoning (Allen’s 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>
|
||||
/// Algorithm that retrieve the blocks' reading order using both (spatial) Allen’s 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>
|
||||
/// </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 spatial reasoning (Allen’s interval relations) and possibly the rendering order (TextSequence).</para>
|
||||
/// <para>This detector uses the (spatial) Allen’s interval relations and rendering order (TextSequence).</para>
|
||||
/// </summary>
|
||||
public static UnsupervisedReadingOrderDetector Instance { get; } = new UnsupervisedReadingOrderDetector();
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not to also use the rendering order, as indicated by the TextSequence.
|
||||
/// </summary>
|
||||
public bool UseRenderingOrder { get; }
|
||||
private readonly double T;
|
||||
|
||||
/// <summary>
|
||||
/// 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 (Allen’s interval relations) and possibly the rendering order (TextSequence).
|
||||
/// Algorithm that retrieve the blocks' reading order using both (spatial) Allen’s interval relations and rendering order.
|
||||
/// </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>
|
||||
/// <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)
|
||||
public UnsupervisedReadingOrderDetector(double T = 5)
|
||||
{
|
||||
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>
|
||||
@@ -163,7 +78,7 @@
|
||||
if (i == j) continue;
|
||||
var b = textBlocks[j];
|
||||
|
||||
if (getBeforeInMethod(a, b, T))
|
||||
if (GetBeforeInReadingRendering(a, b, T))
|
||||
{
|
||||
graph[i].Add(j);
|
||||
}
|
||||
@@ -173,20 +88,19 @@
|
||||
return graph;
|
||||
}
|
||||
|
||||
private static bool GetBeforeInRendering(TextBlock a, TextBlock b)
|
||||
private bool GetBeforeInReadingRendering(TextBlock a, TextBlock b, double T)
|
||||
{
|
||||
return GetBeforeInReadingVertical(a, b, T) || GetBeforeInRendering(a, b);
|
||||
}
|
||||
|
||||
private 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;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
private bool GetBeforeInReading(TextBlock a, TextBlock b, double T)
|
||||
{
|
||||
IntervalRelations xRelation = GetIntervalRelationX(a, b, T);
|
||||
IntervalRelations yRelation = GetIntervalRelationY(a, b, T);
|
||||
@@ -205,7 +119,8 @@
|
||||
/// <param name="a"></param>
|
||||
/// <param name="b"></param>
|
||||
/// <param name="T">The tolerance parameter T.</param>
|
||||
private static bool GetBeforeInReadingVertical(TextBlock a, TextBlock b, double T)
|
||||
/// <returns></returns>
|
||||
private bool GetBeforeInReadingVertical(TextBlock a, TextBlock b, double T)
|
||||
{
|
||||
IntervalRelations xRelation = GetIntervalRelationX(a, b, T);
|
||||
IntervalRelations yRelation = GetIntervalRelationY(a, b, T);
|
||||
@@ -235,7 +150,7 @@
|
||||
/// <param name="a"></param>
|
||||
/// <param name="b"></param>
|
||||
/// <param name="T">The tolerance parameter T.</param>
|
||||
private static bool GetBeforeInReadingHorizontal(TextBlock a, TextBlock b, double T)
|
||||
private bool GetBeforeInReadingHorizontal(TextBlock a, TextBlock b, double T)
|
||||
{
|
||||
IntervalRelations xRelation = GetIntervalRelationX(a, b, T);
|
||||
IntervalRelations yRelation = GetIntervalRelationY(a, b, T);
|
||||
@@ -268,7 +183,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 static IntervalRelations GetIntervalRelationX(TextBlock a, TextBlock b, double T)
|
||||
private IntervalRelations GetIntervalRelationX(TextBlock a, TextBlock b, double T)
|
||||
{
|
||||
if (a.BoundingBox.Right < b.BoundingBox.Left - T)
|
||||
{
|
||||
@@ -352,7 +267,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 static IntervalRelations GetIntervalRelationY(TextBlock a, TextBlock b, double T)
|
||||
private IntervalRelations GetIntervalRelationY(TextBlock a, TextBlock b, double T)
|
||||
{
|
||||
if (a.BoundingBox.Bottom < b.BoundingBox.Top - T)
|
||||
{
|
||||
@@ -517,5 +432,13 @@
|
||||
/// </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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net45;net451;net452;net46;net461;net462;net47</TargetFrameworks>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Version>0.1.3</Version>
|
||||
<Version>0.1.2</Version>
|
||||
<IsTestProject>False</IsTestProject>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
|
||||
@@ -19,10 +19,6 @@
|
||||
try
|
||||
{
|
||||
var folder = Environment.GetEnvironmentVariable("$HOME");
|
||||
if (string.IsNullOrWhiteSpace(folder))
|
||||
{
|
||||
folder = Environment.GetEnvironmentVariable("HOME");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(folder))
|
||||
{
|
||||
@@ -52,7 +48,7 @@
|
||||
|
||||
try
|
||||
{
|
||||
files = Directory.GetFiles(directory, "*.*", SearchOption.AllDirectories);
|
||||
files = Directory.GetFiles(directory);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net45;net451;net452;net46;net461;net462;net47</TargetFrameworks>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Version>0.1.3</Version>
|
||||
<Version>0.1.2</Version>
|
||||
<IsTestProject>False</IsTestProject>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
namespace UglyToad.PdfPig.Tests.Fonts.Parser
|
||||
{
|
||||
using System.Text.RegularExpressions;
|
||||
using PdfFonts.Parser;
|
||||
using PdfPig.Core;
|
||||
using PdfPig.Encryption;
|
||||
using PdfPig.Tokenization.Scanner;
|
||||
using PdfPig.Tokens;
|
||||
using Xunit;
|
||||
|
||||
public class EncodingReaderTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetWinAnsiFromNameObject()
|
||||
{
|
||||
const string input = @"
|
||||
1 0 obj
|
||||
<< /Type /Font /Encoding 12 0 R /Name /WindowsFont1>>
|
||||
endobj
|
||||
|
||||
12 0 obj
|
||||
/WinAnsiEncoding
|
||||
endobj";
|
||||
|
||||
var scanner = GetScanner(input);
|
||||
|
||||
var reader = new EncodingReader(scanner);
|
||||
|
||||
var fontDictionary = scanner.Get(new IndirectReference(1, 0));
|
||||
|
||||
var encoding = reader.Read(fontDictionary.Data as DictionaryToken);
|
||||
|
||||
Assert.NotNull(encoding);
|
||||
}
|
||||
|
||||
private static PdfTokenScanner GetScanner(string s)
|
||||
{
|
||||
var input = StringBytesTestConverter.Convert(s, false);
|
||||
|
||||
var locationProvider = new TestObjectLocationProvider();
|
||||
|
||||
var regex = new Regex(@"\n(\d+)\s(\d+)\sobj");
|
||||
|
||||
foreach (Match match in regex.Matches(s))
|
||||
{
|
||||
var objN = match.Groups[1].Value;
|
||||
var gen = match.Groups[2].Value;
|
||||
|
||||
locationProvider.Offsets[new IndirectReference(int.Parse(objN), int.Parse(gen))] = match.Index + 1;
|
||||
}
|
||||
|
||||
return new PdfTokenScanner(input.Bytes, locationProvider,
|
||||
new TestFilterProvider(), NoOpEncryptionHandler.Instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
namespace UglyToad.PdfPig.Tests.Fonts
|
||||
{
|
||||
using UglyToad.PdfPig.Tests.Dla;
|
||||
using Xunit;
|
||||
|
||||
public class PointSizeTests
|
||||
{
|
||||
[Fact]
|
||||
public void RotatedText()
|
||||
{
|
||||
using (var document = PdfDocument.Open(DlaHelper.GetDocumentPath("complex rotated")))
|
||||
{
|
||||
var page = document.GetPage(1);
|
||||
|
||||
foreach (var letter in page.Letters)
|
||||
{
|
||||
Assert.Equal(12, letter.PointSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net45;net451;net452;net46;net461;net462;net47</TargetFrameworks>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Version>0.1.3</Version>
|
||||
<Version>0.1.2</Version>
|
||||
<IsTestProject>False</IsTestProject>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net45;net451;net452;net46;net461;net462;net47</TargetFrameworks>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Version>0.1.3</Version>
|
||||
<Version>0.1.2</Version>
|
||||
<IsTestProject>False</IsTestProject>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
|
||||
/// <summary>
|
||||
/// The size of the font in points.
|
||||
/// <para>This is considered experimental because the calculated value is incorrect for some documents at present.</para>
|
||||
/// </summary>
|
||||
public double PointSize { get; }
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
using ClipperLibrary;
|
||||
using Core;
|
||||
using Graphics;
|
||||
using UglyToad.PdfPig.Logging;
|
||||
using static Core.PdfSubpath;
|
||||
|
||||
/// <summary>
|
||||
@@ -14,7 +13,7 @@
|
||||
/// </summary>
|
||||
internal static class ClippingExtensions
|
||||
{
|
||||
public const double Factor = 10_000.0;
|
||||
private const double Factor = 10_000.0;
|
||||
|
||||
/// <summary>
|
||||
/// Number of lines to use when transforming bezier curve to polyline.
|
||||
@@ -24,7 +23,7 @@
|
||||
/// <summary>
|
||||
/// Generates the result of applying a clipping path to another path.
|
||||
/// </summary>
|
||||
public static PdfPath Clip(this PdfPath clipping, PdfPath subject, ILog log = null)
|
||||
public static PdfPath Clip(this PdfPath clipping, PdfPath subject)
|
||||
{
|
||||
if (clipping == null)
|
||||
{
|
||||
@@ -62,10 +61,7 @@
|
||||
subPathClipping.CloseSubpath();
|
||||
}
|
||||
|
||||
if (!clipper.AddPath(subPathClipping.ToClipperPolygon().ToList(), ClipperPolyType.Clip, true))
|
||||
{
|
||||
log?.Error("ClippingExtensions.Clip(): failed to add clipping subpath.");
|
||||
}
|
||||
clipper.AddPath(subPathClipping.ToClipperPolygon().ToList(), ClipperPolyType.Clip, true);
|
||||
}
|
||||
|
||||
// Subject path
|
||||
@@ -78,27 +74,13 @@
|
||||
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();
|
||||
}
|
||||
|
||||
if (!clipper.AddPath(subPathSubject.ToClipperPolygon().ToList(), ClipperPolyType.Subject, subjectClose))
|
||||
{
|
||||
log?.Error("ClippingExtensions.Clip(): failed to add subject subpath for clipping.");
|
||||
}
|
||||
clipper.AddPath(subPathSubject.ToClipperPolygon().ToList(), ClipperPolyType.Subject, subjectClose);
|
||||
}
|
||||
|
||||
var clippingFillType = clipping.FillingRule == FillingRule.NonZeroWinding ? ClipperPolyFillType.NonZero : ClipperPolyFillType.EvenOdd;
|
||||
@@ -175,19 +157,18 @@
|
||||
/// 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>
|
||||
internal static IEnumerable<ClipperIntPoint> ToClipperPolygon(this PdfSubpath pdfPath)
|
||||
private static IEnumerable<ClipperIntPoint> ToClipperPolygon(this PdfSubpath pdfPath)
|
||||
{
|
||||
if (pdfPath.Commands.Count == 0)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
ClipperIntPoint movePoint;
|
||||
if (pdfPath.Commands[0] is Move move)
|
||||
if (pdfPath.Commands[0] is Move currentMove)
|
||||
{
|
||||
movePoint = move.Location.ToClipperIntPoint();
|
||||
var previous = new ClipperIntPoint(currentMove.Location.X * Factor, currentMove.Location.Y * Factor);
|
||||
|
||||
yield return movePoint;
|
||||
yield return previous;
|
||||
|
||||
if (pdfPath.Commands.Count == 1)
|
||||
{
|
||||
@@ -204,49 +185,27 @@
|
||||
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 line.From.ToClipperIntPoint();
|
||||
yield return line.To.ToClipperIntPoint();
|
||||
yield return new ClipperIntPoint(line.From.X * Factor, line.From.Y * Factor);
|
||||
yield return new ClipperIntPoint(line.To.X * Factor, line.To.Y * Factor);
|
||||
}
|
||||
else if (command is BezierCurve curve)
|
||||
{
|
||||
foreach (var lineB in curve.ToLines(LinesInCurve))
|
||||
{
|
||||
yield return lineB.From.ToClipperIntPoint();
|
||||
yield return lineB.To.ToClipperIntPoint();
|
||||
yield return new ClipperIntPoint(lineB.From.X * Factor, lineB.From.Y * Factor);
|
||||
yield return new ClipperIntPoint(lineB.To.X * Factor, lineB.To.Y * Factor);
|
||||
}
|
||||
}
|
||||
else if (command is Close)
|
||||
{
|
||||
if (movePoint == null)
|
||||
{
|
||||
throw new ArgumentException("ToClipperPolygon(): Move command was null, cannot close the subpath.");
|
||||
}
|
||||
yield return movePoint;
|
||||
yield return new ClipperIntPoint(currentMove.Location.X * Factor, currentMove.Location.Y * Factor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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() };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
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>
|
||||
@@ -59,14 +57,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)
|
||||
{
|
||||
@@ -182,7 +180,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]));
|
||||
}
|
||||
|
||||
@@ -193,7 +191,7 @@
|
||||
/// <param name="points">The points.</param>
|
||||
public static PdfRectangle MinimumAreaRectangle(IEnumerable<PdfPoint> points)
|
||||
{
|
||||
if (points?.Any() != true)
|
||||
if (points == null || points.Count() == 0)
|
||||
{
|
||||
throw new ArgumentException("MinimumAreaRectangle(): points cannot be null and must contain at least one point.", nameof(points));
|
||||
}
|
||||
@@ -210,7 +208,7 @@
|
||||
{
|
||||
if (points == null || points.Count < 2)
|
||||
{
|
||||
throw new ArgumentException("OrientedBoundingBox(): points cannot be null and must contain at least two points.", nameof(points));
|
||||
throw new ArgumentException("OrientedBoundingBox(): points cannot be null and must contain at least two points.");
|
||||
}
|
||||
|
||||
// Fitting a line through the points
|
||||
@@ -252,7 +250,8 @@
|
||||
cos, sin, 0,
|
||||
-sin, cos, 0,
|
||||
0, 0, 1);
|
||||
return rotateBack.Transform(aabb);
|
||||
var obb = rotateBack.Transform(aabb);
|
||||
return obb;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -260,9 +259,9 @@
|
||||
/// </summary>
|
||||
public static IEnumerable<PdfPoint> GrahamScan(IEnumerable<PdfPoint> points)
|
||||
{
|
||||
if (points?.Any() != true)
|
||||
if (points == null || points.Count() == 0)
|
||||
{
|
||||
throw new ArgumentException("GrahamScan(): points cannot be null and must contain at least one point.", nameof(points));
|
||||
throw new ArgumentException("GrahamScan(): points cannot be null and must contain at least one point.");
|
||||
}
|
||||
|
||||
if (points.Count() < 3) return points;
|
||||
@@ -322,7 +321,7 @@
|
||||
|
||||
#region PdfRectangle
|
||||
/// <summary>
|
||||
/// Whether the point is located inside the rectangle.
|
||||
/// Whether the rectangle contains the point.
|
||||
/// </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>
|
||||
@@ -371,7 +370,7 @@
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the other rectangle is located inside the rectangle.
|
||||
/// Whether the rectangle contains 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>
|
||||
@@ -433,12 +432,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;
|
||||
@@ -455,7 +454,6 @@
|
||||
|
||||
/// <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)
|
||||
{
|
||||
@@ -475,70 +473,11 @@
|
||||
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 point is located on the line segment.
|
||||
/// Whether the line segment contains the point.
|
||||
/// </summary>
|
||||
public static bool Contains(this PdfLine line, PdfPoint point)
|
||||
{
|
||||
@@ -592,31 +531,11 @@
|
||||
{
|
||||
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 point is located on the line segment.
|
||||
/// Whether the line segment contains the point.
|
||||
/// </summary>
|
||||
public static bool Contains(this Line line, PdfPoint point)
|
||||
{
|
||||
@@ -736,65 +655,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
/// <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;
|
||||
@@ -876,7 +736,7 @@
|
||||
{
|
||||
return Intersect(bezierCurve, line.From, line.To);
|
||||
}
|
||||
|
||||
|
||||
private static PdfPoint[] Intersect(BezierCurve bezierCurve, PdfPoint p1, PdfPoint p2)
|
||||
{
|
||||
var ts = IntersectT(bezierCurve, p1, p2);
|
||||
@@ -951,254 +811,7 @@
|
||||
|
||||
var solution = SolveCubicEquation(a, b, c, d);
|
||||
|
||||
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;
|
||||
return solution.Where(s => !double.IsNaN(s)).Where(s => s >= -epsilon && (s - 1) <= epsilon).OrderBy(s => s).ToArray();
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -1325,7 +938,8 @@
|
||||
{
|
||||
string BboxToRect(PdfRectangle box, string stroke)
|
||||
{
|
||||
return $"<rect x='{box.Left}' y='{box.Bottom}' width='{box.Width}' height='{box.Height}' stroke-width='2' fill='none' stroke='{stroke}'></rect>";
|
||||
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;
|
||||
}
|
||||
|
||||
var glyph = p.ToSvg(height);
|
||||
@@ -1344,7 +958,9 @@
|
||||
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")));
|
||||
return $"<svg width='500' height='500'><g transform=\"scale(0.2, -0.2) translate(100, -700)\">{path} {bboxRect} {others}</g></svg>";
|
||||
var result = $"<svg width='500' height='500'><g transform=\"scale(0.2, -0.2) translate(100, -700)\">{path} {bboxRect} {others}</g></svg>";
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.EvenOdd);
|
||||
clippingPath.SetClipping(FillingRule.NonZeroWinding);
|
||||
|
||||
graphicsStack.Push(new CurrentGraphicsState()
|
||||
{
|
||||
@@ -241,7 +241,20 @@
|
||||
var renderingMatrix =
|
||||
TransformationMatrix.FromValues(fontSize * horizontalScaling, 0, 0, fontSize, 0, rise);
|
||||
|
||||
var pointSize = Math.Round(transformationMatrix.Multiply(TextMatrices.TextMatrix).Transform(new PdfRectangle(0, 0, 1, fontSize)).Height, 2);
|
||||
// TODO: this does not seem correct, produces the correct result for now but we need to revisit.
|
||||
// see: https://stackoverflow.com/questions/48010235/pdf-specification-get-font-size-in-points
|
||||
var fontSizeMatrix = transformationMatrix.Multiply(TextMatrices.TextMatrix).Multiply(fontSize);
|
||||
var pointSize = Math.Round(fontSizeMatrix.A, 2);
|
||||
// Assume a rotated letter
|
||||
if (pointSize == 0)
|
||||
{
|
||||
pointSize = Math.Round(fontSizeMatrix.B, 2);
|
||||
}
|
||||
|
||||
if (pointSize < 0)
|
||||
{
|
||||
pointSize *= -1;
|
||||
}
|
||||
|
||||
while (bytes.MoveNext())
|
||||
{
|
||||
@@ -279,7 +292,7 @@
|
||||
var boundingBox = font.GetBoundingBox(code);
|
||||
|
||||
var transformedGlyphBounds = PerformantRectangleTransformer
|
||||
.Transform(renderingMatrix, textMatrix, transformationMatrix, boundingBox.GlyphBounds);
|
||||
.Transform(renderingMatrix, textMatrix, transformationMatrix, boundingBox.GlyphBounds);
|
||||
|
||||
var transformedPdfBounds = PerformantRectangleTransformer
|
||||
.Transform(renderingMatrix, textMatrix, transformationMatrix, new PdfRectangle(0, 0, boundingBox.Width, 0));
|
||||
@@ -614,7 +627,7 @@
|
||||
|
||||
if (clipPaths)
|
||||
{
|
||||
var clippedPath = currentState.CurrentClippingPath.Clip(CurrentPath, log);
|
||||
var clippedPath = currentState.CurrentClippingPath.Clip(CurrentPath);
|
||||
if (clippedPath != null)
|
||||
{
|
||||
paths.Add(clippedPath);
|
||||
@@ -645,7 +658,7 @@
|
||||
var currentClipping = GetCurrentState().CurrentClippingPath;
|
||||
currentClipping.SetClipping(clippingRule);
|
||||
|
||||
var newClippings = CurrentPath.Clip(currentClipping, log);
|
||||
var newClippings = CurrentPath.Clip(currentClipping);
|
||||
if (newClippings == null)
|
||||
{
|
||||
log.Warn("Empty clipping path found. Clipping path not updated.");
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Core;
|
||||
using Fonts;
|
||||
using Fonts.Encodings;
|
||||
using PdfPig.Parser.Parts;
|
||||
@@ -47,13 +48,22 @@
|
||||
|
||||
return WinAnsiEncoding.Instance;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Did not recognize named font: {name} in {fontDictionary}.");
|
||||
}
|
||||
|
||||
DictionaryToken encodingDictionary = DirectObjectFinder.Get<DictionaryToken>(baseEncodingObject, pdfScanner);
|
||||
try
|
||||
{
|
||||
DictionaryToken encodingDictionary = DirectObjectFinder.Get<DictionaryToken>(baseEncodingObject, pdfScanner);
|
||||
|
||||
var encoding = ReadEncodingDictionary(encodingDictionary, fontEncoding);
|
||||
var encoding = ReadEncodingDictionary(encodingDictionary, fontEncoding);
|
||||
|
||||
return encoding;
|
||||
return encoding;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new PdfDocumentFormatException($"Failed to locate encoding in {fontDictionary}.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Encoding ReadEncodingDictionary(DictionaryToken encodingDictionary, Encoding fontEncoding)
|
||||
|
||||
@@ -49,23 +49,11 @@
|
||||
toUnicodeCMap = CMapCache.Parse(new ByteArrayInputBytes(decodedUnicodeCMap));
|
||||
}
|
||||
}
|
||||
|
||||
var name = GetFontName(dictionary);
|
||||
|
||||
return new Type3Font(name, boundingBox, fontMatrix, encoding, firstCharacter,
|
||||
|
||||
return new Type3Font(NameToken.Type3, 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))
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net45;net451;net452;net46;net461;net462;net47</TargetFrameworks>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Version>0.1.3</Version>
|
||||
<Version>0.1.2</Version>
|
||||
<IsTestProject>False</IsTestProject>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
|
||||
@@ -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</Version>
|
||||
<Version>0.1.2</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>
|
||||
|
||||
Reference in New Issue
Block a user