Compare commits

..

10 Commits

Author SHA1 Message Date
davebrokit
f3e37eafae Introduce IBlock and ILettersBlock interfaces (Round 2) (#1263)
Some checks failed
Build, test and publish draft / build (push) Has been cancelled
Build and test [MacOS] / build (push) Has been cancelled
Run Common Crawl Tests / build (0000-0001) (push) Has been cancelled
Run Common Crawl Tests / build (0002-0003) (push) Has been cancelled
Run Common Crawl Tests / build (0004-0005) (push) Has been cancelled
Run Common Crawl Tests / build (0006-0007) (push) Has been cancelled
Run Common Crawl Tests / build (0008-0009) (push) Has been cancelled
Run Common Crawl Tests / build (0010-0011) (push) Has been cancelled
Run Common Crawl Tests / build (0012-0013) (push) Has been cancelled
Run Integration Tests / build (push) Has been cancelled
Tag Release / tag_if_version_changed (push) Has been cancelled
Nightly Release / Check if this commit has already been published (push) Has been cancelled
Nightly Release / tests (push) Has been cancelled
Nightly Release / build_and_publish_nightly (push) Has been cancelled
* Code review changes
- Keep the Bounds property on the image classes so this isn't a major breaking API change
- Don't expose letters collection

* Minor fix

* Switch to using BoundingBox in the library

---------

Co-authored-by: davmarksman <david@brokit.co.uk>
2026-02-28 16:25:51 +00:00
Eliot Jones
a4047247a8 Additional digital corpora testing (#1261)
Some checks failed
Build, test and publish draft / build (push) Has been cancelled
Build and test [MacOS] / build (push) Has been cancelled
Run Common Crawl Tests / build (0000-0001) (push) Has been cancelled
Run Common Crawl Tests / build (0002-0003) (push) Has been cancelled
Run Common Crawl Tests / build (0004-0005) (push) Has been cancelled
Run Common Crawl Tests / build (0006-0007) (push) Has been cancelled
Run Common Crawl Tests / build (0008-0009) (push) Has been cancelled
Run Common Crawl Tests / build (0010-0011) (push) Has been cancelled
Run Common Crawl Tests / build (0012-0013) (push) Has been cancelled
Run Integration Tests / build (push) Has been cancelled
Tag Release / tag_if_version_changed (push) Has been cancelled
Nightly Release / Check if this commit has already been published (push) Has been cancelled
Nightly Release / tests (push) Has been cancelled
Nightly Release / build_and_publish_nightly (push) Has been cancelled
* add additional testing pairs 0010-0011 for integration

some of these files required the skip missing fonts flag set to true

- propagate use lenient parsing for dictionaries inside arrays, handles a
corrupt file 0012710 not in this test set

* add pair 0012-0013
2026-02-22 21:01:03 +00:00
BobLd
34db05f735 Fix Benchmarks solution and add BruteForceBenchmarks 2026-02-22 20:05:57 +00:00
Eliot Jones
9c0d6893e0 revert flate decode handling to more lenient processing (#1254)
* revert flate decode handling to more lenient processing

the change to use zlib/a adler checksum verification flow meant that
invalid flate streams would not be decoded correctly. this caused
issues for files that included invalid/missing checksums. this reverts
the processing to the old approach for files like #1235

* fix object stream offset handling and track circular refs

* update tests

* normalize line endings for mac runner

* fixes for mac clownery

* add next pair to common crawl action

* add a test case for the root cause of the int overflow
2026-02-22 15:49:50 +00:00
BobLd
adb5713621 Make LinkAnnotation public
Some checks failed
Build, test and publish draft / build (push) Has been cancelled
Build and test [MacOS] / build (push) Has been cancelled
Run Common Crawl Tests / build (0000-0001) (push) Has been cancelled
Run Common Crawl Tests / build (0002-0003) (push) Has been cancelled
Run Common Crawl Tests / build (0004-0005) (push) Has been cancelled
Run Common Crawl Tests / build (0006-0007) (push) Has been cancelled
Run Integration Tests / build (push) Has been cancelled
Tag Release / tag_if_version_changed (push) Has been cancelled
Nightly Release / Check if this commit has already been published (push) Has been cancelled
Nightly Release / tests (push) Has been cancelled
Nightly Release / build_and_publish_nightly (push) Has been cancelled
2026-02-18 14:09:20 +00:00
BobLd
665623561a Add UglyToad.PdfPig.Benchmarks and misc performance improvements
Some checks failed
Build, test and publish draft / build (push) Has been cancelled
Build and test [MacOS] / build (push) Has been cancelled
Run Common Crawl Tests / build (0000-0001) (push) Has been cancelled
Run Common Crawl Tests / build (0002-0003) (push) Has been cancelled
Run Common Crawl Tests / build (0004-0005) (push) Has been cancelled
Run Common Crawl Tests / build (0006-0007) (push) Has been cancelled
Run Integration Tests / build (push) Has been cancelled
Tag Release / tag_if_version_changed (push) Has been cancelled
Nightly Release / Check if this commit has already been published (push) Has been cancelled
Nightly Release / tests (push) Has been cancelled
Nightly Release / build_and_publish_nightly (push) Has been cancelled
2026-02-17 11:53:09 +00:00
EliotJones
7e370a0eab confirm tag is on master before running publish
Some checks failed
Nightly Release / Check if this commit has already been published (push) Has been cancelled
Nightly Release / tests (push) Has been cancelled
Nightly Release / build_and_publish_nightly (push) Has been cancelled
Build, test and publish draft / build (push) Has been cancelled
Build and test [MacOS] / build (push) Has been cancelled
Run Common Crawl Tests / build (0000-0001) (push) Has been cancelled
Run Common Crawl Tests / build (0002-0003) (push) Has been cancelled
Run Common Crawl Tests / build (0004-0005) (push) Has been cancelled
Run Common Crawl Tests / build (0006-0007) (push) Has been cancelled
Run Integration Tests / build (push) Has been cancelled
Tag Release / tag_if_version_changed (push) Has been cancelled
2026-02-15 18:39:45 +00:00
EliotJones
b9aa53166d replace release flow single job with pr process
since actions do not have permissions to push
directly to master and bot accounts to achieve
the same are hard to manage we will change the
release flow to work as follows.

1. manually invoke `prepare_release_pr.yml` action,
this creates a new branch with the version of all
project files updated and creates a pull request for
that version. this pr then should be merged using
rebase merge

2. `tag_release.yml` checks if the newest commit
name starts with the text "Release " and also verifies
if it changed the version of the package csproj. if
both those conditions are met it will create and
push a new tag, e.g. `v0.1.17` to master

3. `publish_nuget.yml` listens for new `v*` tags on
master and triggers the nuget deployment

this is all chat gpt code so who knows if it will work
2026-02-15 18:39:45 +00:00
BobLd
0a2b1e076f Improve HasFormXObjectCircularReference and fix #1250 2026-02-15 18:06:35 +00:00
EliotJones
f732718852 both Tj and TJ operators should increment text sequence #1241
the text sequence was added to group letters added by the same
content stream operation together for layout analysis. TJ allows for
individual letter positioning in the same operation which modifies
the text matrix while writing whereas Tj just writes the text at the current
location. both should be semantically equivalent for this sequence
number. this was originally added in #61
2026-02-15 17:50:07 +00:00
97 changed files with 140801 additions and 1419 deletions

View File

@@ -0,0 +1,40 @@
name: Create Release PR
on:
workflow_dispatch
permissions:
contents: write
pull-requests: write
jobs:
bump_version:
runs-on: windows-2022
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Calculate next version
id: version
run: |
$newVer = .\tools\get-next-main-version.ps1
echo "NEW_VERSION=$newVer" >> $env:GITHUB_ENV
.\tools\set-version.ps1 $newVer -UpdateAssemblyAndFileVersion
git config user.name "github-actions"
git config user.email "github-actions@github.com"
git checkout -b release/$newVer
git commit -am "Release $newVer"
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
with:
branch: release/${{ env.NEW_VERSION }}
base: master
title: "Release ${{ env.NEW_VERSION }}"
body: "Automated release PR."
delete-branch: true
merge-method: rebase

53
.github/workflows/publish_nuget.yml vendored Normal file
View File

@@ -0,0 +1,53 @@
name: Publish
on:
push:
tags:
- 'v*'
permissions:
contents: write
packages: write
jobs:
build_and_publish:
runs-on: windows-2022
steps:
- uses: actions/checkout@v4
- name: Verify tag points to master
id: verify
run: |
TAG_COMMIT=$(git rev-list -n 1 ${{ github.ref_name }})
if ! git branch -r --contains $TAG_COMMIT | grep -q 'origin/master'; then
echo "Tag is not on master — skipping publish"
exit 78 # 78 = neutral in GitHub Actions
fi
- name: Set up .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
2.1.x
6.0.x
8.0.x
9.0.x
- name: Add msbuild to PATH
uses: microsoft/setup-msbuild@v2
- name: Run tests
run: dotnet test -c Release src/UglyToad.PdfPig.sln
- name: Build package
run: dotnet pack -c Release -o package tools/UglyToad.PdfPig.Package/UglyToad.PdfPig.Package.csproj -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg
- name: Publish to NuGet
run: dotnet nuget push package/*.nupkg --api-key ${{secrets.NUGET_API_KEY}} --source https://api.nuget.org/v3/index.json
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,53 +0,0 @@
name: Release Publish
on:
release:
types: [published]
permissions:
contents: write
packages: write
jobs:
build_and_publish_release:
runs-on: windows-2022
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up dotnet core
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
2.1.x
6.0.x
8.0.x
9.0.x
- name: Add msbuild to PATH
uses: microsoft/setup-msbuild@v2
- name: Run tests
run: dotnet test -c Release src/UglyToad.PdfPig.sln
- name: Build package
run: dotnet pack -c Release -o package tools/UglyToad.PdfPig.Package/UglyToad.PdfPig.Package.csproj -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg
- name: Publish to NuGet
run: dotnet nuget push **/*.nupkg --api-key ${{secrets.NUGET_API_KEY}} --source https://api.nuget.org/v3/index.json
- name: Increment version after release
run: |
$newVer = .\tools\get-next-main-version.ps1
.\tools\set-version.ps1 $newVer -UpdateAssemblyAndFileVersion
git config user.name "github-actions"
git config user.email "github-actions@github.com"
git fetch origin master
git checkout master
git pull
git commit -am "Increment version to $newVer"
git push

View File

@@ -12,7 +12,7 @@ jobs:
strategy:
fail-fast: false
matrix:
pair: ["0000-0001", "0002-0003", "0004-0005", "0006-0007"]
pair: ["0000-0001", "0002-0003", "0004-0005", "0006-0007", "0008-0009", "0010-0011", "0012-0013"]
steps:
- uses: actions/checkout@v2

43
.github/workflows/tag_release.yml vendored Normal file
View File

@@ -0,0 +1,43 @@
name: Tag Release
on:
push:
branches:
- master
permissions:
contents: write
jobs:
tag_if_version_changed:
if: startsWith(github.event.head_commit.message, 'Release ')
runs-on: windows-2022
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Detect version change
id: versioncheck
run: |
git fetch origin master --depth=2
$diff = git diff HEAD^ HEAD -- tools/UglyToad.PdfPig.Package/UglyToad.PdfPig.Package.csproj
if ($diff -match "<Version>") {
$version = (Select-String -Path tools/UglyToad.PdfPig.Package/UglyToad.PdfPig.Package.csproj -Pattern "<Version>(.*)</Version>").Matches.Groups[1].Value
echo "version=$version" >> $env:GITHUB_OUTPUT
echo "create=true" >> $env:GITHUB_OUTPUT
} else {
echo "create=false" >> $env:GITHUB_OUTPUT
}
- name: Create tag
if: steps.versioncheck.outputs.create == 'true'
run: |
git config user.name "github-actions"
git config user.email "github-actions@github.com"
git tag "v${{ steps.versioncheck.outputs.version }}"
git push origin "v${{ steps.versioncheck.outputs.version }}"

View File

@@ -0,0 +1,20 @@
namespace UglyToad.PdfPig.Core;
/// <summary>
/// Indicates where an object is located in the Xref.
/// </summary>
public enum XrefEntryType : byte
{
/// <summary>
/// Free object.
/// </summary>
Free = 0,
/// <summary>
/// Located as an object in the file.
/// </summary>
File = 1,
/// <summary>
/// Located in a compressed object stream.
/// </summary>
ObjectStream = 2
}

View File

@@ -0,0 +1,42 @@
namespace UglyToad.PdfPig.Core;
/// <summary>
/// Information about where an object is located in the file according to the Xref (or brute force parsing).
/// </summary>
public readonly struct XrefLocation
{
/// <summary>
/// Which type of location is indicated.
/// </summary>
public readonly XrefEntryType Type;
/// <summary>
/// If <see cref="Type"/> is <see cref="XrefEntryType.File"/> then byte offset, otherwise <see cref="XrefEntryType.ObjectStream"/> this is the stream number.
/// </summary>
public readonly long Value1;
/// <summary>
/// If <see cref="Type"/> is <see cref="XrefEntryType.ObjectStream"/> then the index of the object in the stream.
/// </summary>
public readonly int Value2; // only used for ObjectStream
private XrefLocation(XrefEntryType type, long value1, int value2)
{
Type = type;
Value1 = value1;
Value2 = value2;
}
/// <summary>
/// Create a location mapped to a byte offset in the file.
/// </summary>
public static XrefLocation File(long offset)
=> new XrefLocation(XrefEntryType.File, offset, 0);
/// <summary>
/// Create a location mapped to an index inside and object stream.
/// </summary>
public static XrefLocation Stream(long objStream, int index)
=> new XrefLocation(XrefEntryType.ObjectStream, objStream, index);
}

View File

@@ -50,7 +50,15 @@
* (i,j,k) will form a group and (m,n) will form another group.
*************************************************************************************/
int[] indexes = Enumerable.Repeat(-1, elements.Count).ToArray();
int[] indexes = new int[elements.Count];
#if NET6_0_OR_GREATER
Array.Fill(indexes, -1);
#else
for (int k = 0; k < indexes.Length; k++)
{
indexes[k] = -1;
}
#endif
KdTree<T> kdTree = new KdTree<T>(elements, candidatesPoint);
ParallelOptions parallelOptions = new ParallelOptions() { MaxDegreeOfParallelism = maxDegreeOfParallelism };
@@ -118,7 +126,15 @@
* (i,j,k) will form a group and (m,n) will form another group.
*************************************************************************************/
int[] indexes = Enumerable.Repeat(-1, elements.Count).ToArray();
int[] indexes = new int[elements.Count];
#if NET6_0_OR_GREATER
Array.Fill(indexes, -1);
#else
for (int l = 0; l < indexes.Length; l++)
{
indexes[l] = -1;
}
#endif
KdTree<T> kdTree = new KdTree<T>(elements, candidatesPoint);
ParallelOptions parallelOptions = new ParallelOptions() { MaxDegreeOfParallelism = maxDegreeOfParallelism };
@@ -186,7 +202,15 @@
* (i,j,k) will form a group and (m,n) will form another group.
*************************************************************************************/
int[] indexes = Enumerable.Repeat(-1, elements.Count).ToArray();
int[] indexes = new int[elements.Count];
#if NET6_0_OR_GREATER
Array.Fill(indexes, -1);
#else
for (int k = 0; k < indexes.Length; k++)
{
indexes[k] = -1;
}
#endif
ParallelOptions parallelOptions = new ParallelOptions() { MaxDegreeOfParallelism = maxDegreeOfParallelism };

View File

@@ -3,7 +3,6 @@
using Core;
using System;
using System.Collections.Generic;
using System.Linq;
/// <summary>
/// Contains helpful tools for distance measures.
@@ -169,12 +168,11 @@
distance = double.MaxValue;
int closestPointIndex = -1;
var candidatesPoints = candidates.Select(candidatePoint).ToList();
var pivot = pivotPoint(element);
for (var i = 0; i < candidates.Count; i++)
{
double currentDistance = distanceMeasure(pivot, candidatesPoints[i]);
double currentDistance = distanceMeasure(pivot, candidatePoint(candidates[i]));
if (currentDistance < distance && !candidates[i].Equals(element))
{
distance = currentDistance;
@@ -211,12 +209,11 @@
distance = double.MaxValue;
int closestLineIndex = -1;
var candidatesLines = candidates.Select(candidateLine).ToList();
var pivot = pivotLine(element);
for (var i = 0; i < candidates.Count; i++)
{
double currentDistance = distanceMeasure(pivot, candidatesLines[i]);
double currentDistance = distanceMeasure(pivot, candidateLine(candidates[i]));
if (currentDistance < distance && !candidates[i].Equals(element))
{
distance = currentDistance;

View File

@@ -3,7 +3,6 @@
using System.Collections.Generic;
using System.Linq;
using UglyToad.PdfPig.Content;
using UglyToad.PdfPig.PdfFonts;
/// <summary>
/// Checks if each letter is a duplicate and overlaps any other letter and remove the duplicate, and flag the remaining as bold.
@@ -24,41 +23,51 @@
return letters?.ToList();
}
var queue = new Queue<Letter>(letters);
var cleanLetters = new List<Letter>() { queue.Dequeue() }; // dequeue the first letter
// Use a dictionary keyed by (Value, FontName) to look up candidate duplicates in O(1)
var duplicateIndex = new Dictionary<(string, string), List<int>>();
var cleanLetters = new List<Letter>();
while (queue.Count > 0)
foreach (var letter in letters)
{
var letter = queue.Dequeue();
bool addLetter = true;
int duplicatesOverlappingIndex = -1;
var duplicates = cleanLetters.Where(l => l.Value.Equals(letter.Value) && l.FontName.Equals(letter.FontName)); // do other checks?
if (duplicates.Any())
var key = (letter.Value, letter.FontName);
if (duplicateIndex.TryGetValue(key, out var candidateIndices))
{
double tolerance = letter.GlyphRectangle.Width / (letter.Value.Length == 0 ? 1 : letter.Value.Length) / 3.0;
double minX = letter.GlyphRectangle.BottomLeft.X - tolerance;
double maxX = letter.GlyphRectangle.BottomLeft.X + tolerance;
double minY = letter.GlyphRectangle.BottomLeft.Y - tolerance;
double maxY = letter.GlyphRectangle.BottomLeft.Y + tolerance;
double tolerance = letter.BoundingBox.Width / (letter.Value.Length == 0 ? 1 : letter.Value.Length) / 3.0;
double minX = letter.BoundingBox.BottomLeft.X - tolerance;
double maxX = letter.BoundingBox.BottomLeft.X + tolerance;
double minY = letter.BoundingBox.BottomLeft.Y - tolerance;
double maxY = letter.BoundingBox.BottomLeft.Y + tolerance;
var duplicatesOverlapping = duplicates.FirstOrDefault(l => minX <= l.GlyphRectangle.BottomLeft.X &&
maxX >= l.GlyphRectangle.BottomLeft.X &&
minY <= l.GlyphRectangle.BottomLeft.Y &&
maxY >= l.GlyphRectangle.BottomLeft.Y);
if (duplicatesOverlapping != default)
for (int ci = 0; ci < candidateIndices.Count; ci++)
{
// duplicate overlapping letter was found, keeping the existing one and not adding this one.
addLetter = false;
duplicatesOverlappingIndex = cleanLetters.IndexOf(duplicatesOverlapping);
int idx = candidateIndices[ci];
var l = cleanLetters[idx];
if (minX <= l.BoundingBox.BottomLeft.X &&
maxX >= l.BoundingBox.BottomLeft.X &&
minY <= l.BoundingBox.BottomLeft.Y &&
maxY >= l.BoundingBox.BottomLeft.Y)
{
addLetter = false;
duplicatesOverlappingIndex = idx;
break;
}
}
}
if (addLetter)
{
int newIndex = cleanLetters.Count;
cleanLetters.Add(letter);
if (!duplicateIndex.TryGetValue(key, out var list))
{
list = new List<int>();
duplicateIndex[key] = list;
}
list.Add(newIndex);
}
else if (duplicatesOverlappingIndex != -1)
{

View File

@@ -229,7 +229,7 @@
private AltoDocument.AltoIllustration ToAltoIllustration(IPdfImage pdfImage, double height)
{
illustrationCount++;
var rectangle = pdfImage.Bounds;
var rectangle = pdfImage.BoundingBox;
return new AltoDocument.AltoIllustration
{
@@ -311,10 +311,10 @@
glyphCount++;
return new AltoDocument.AltoGlyph
{
VerticalPosition = (float)Math.Round((height - letter.GlyphRectangle.Top) * scale),
HorizontalPosition = (float)Math.Round(letter.GlyphRectangle.Left * scale),
Height = (float)Math.Round(letter.GlyphRectangle.Height * scale),
Width = (float)Math.Round(letter.GlyphRectangle.Width * scale),
VerticalPosition = (float)Math.Round((height - letter.BoundingBox.Top) * scale),
HorizontalPosition = (float)Math.Round(letter.BoundingBox.Left * scale),
Height = (float)Math.Round(letter.BoundingBox.Height * scale),
Width = (float)Math.Round(letter.BoundingBox.Width * scale),
Gc = 1.0f,
Content = invalidCharacterHandler(letter.Value),
Id = "P" + pageCount + "_ST" + stringCount.ToString("#00000") + "_G" + glyphCount.ToString("#00")

View File

@@ -277,7 +277,7 @@
private string GetCode(IPdfImage pdfImage, double pageHeight, int level)
{
imageCount++;
var bbox = pdfImage.Bounds;
var bbox = pdfImage.BoundingBox;
return GetIndent(level) + "<span class='ocr_image' id='image_" + pageCount + "_"
+ imageCount + "' title='" + GetCode(bbox, pageHeight) + "' />";
}

View File

@@ -280,7 +280,7 @@
private PageXmlDocument.PageXmlImageRegion ToPageXmlImageRegion(IPdfImage pdfImage, PageXmlData data, double pageWidth, double pageHeight)
{
data.RegionsCount++;
var bbox = pdfImage.Bounds;
var bbox = pdfImage.BoundingBox;
return new PageXmlDocument.PageXmlImageRegion()
{
Coords = ToCoords(bbox, pageWidth, pageHeight),
@@ -360,7 +360,7 @@
data.GlyphsCount++;
return new PageXmlDocument.PageXmlGlyph()
{
Coords = ToCoords(letter.GlyphRectangle, pageWidth, pageHeight),
Coords = ToCoords(letter.BoundingBox, pageWidth, pageHeight),
Ligature = false,
Production = PageXmlDocument.PageXmlProductionSimpleType.Printed,
TextStyle = new PageXmlDocument.PageXmlTextStyle()

View File

@@ -96,12 +96,12 @@
{
string fontFamily = GetFontFamily(l.FontName, out string style, out string weight);
string rotation = "";
if (l.GlyphRectangle.Rotation != 0)
if (l.BoundingBox.Rotation != 0)
{
rotation = $" transform='rotate({Math.Round(-l.GlyphRectangle.Rotation, Rounding)} {Math.Round(l.GlyphRectangle.BottomLeft.X, Rounding)},{Math.Round(height - l.GlyphRectangle.TopLeft.Y, Rounding)})'";
rotation = $" transform='rotate({Math.Round(-l.BoundingBox.Rotation, Rounding)} {Math.Round(l.BoundingBox.BottomLeft.X, Rounding)},{Math.Round(height - l.BoundingBox.TopLeft.Y, Rounding)})'";
}
string fontSize = l.FontSize != 1 ? $"font-size='{l.FontSize:0}'" : $"style='font-size:{Math.Round(l.GlyphRectangle.Height, 2)}px'";
string fontSize = l.FontSize != 1 ? $"font-size='{l.FontSize:0}'" : $"style='font-size:{Math.Round(l.BoundingBox.Height, 2)}px'";
var safeValue = XmlEscape(l, doc);
var x = Math.Round(l.StartBaseLine.X, Rounding);

View File

@@ -54,9 +54,6 @@
/// <typeparam name="T"></typeparam>
public class KdTree<T>
{
private readonly KdTreeComparerY kdTreeComparerY = new KdTreeComparerY();
private readonly KdTreeComparerX kdTreeComparerX = new KdTreeComparerX();
/// <summary>
/// The root of the tree.
/// </summary>
@@ -111,11 +108,11 @@
if (depth % 2 == 0)
{
P.Sort(kdTreeComparerX);
P.Sort((p0, p1) => p0.Value.X.CompareTo(p1.Value.X));
}
else
{
P.Sort(kdTreeComparerY);
P.Sort((p0, p1) => p0.Value.Y.CompareTo(p1.Value.Y));
}
if (P.Length == 2)
@@ -131,6 +128,26 @@
return new KdTreeNode<T>(vLeft, vRight, P[median], depth);
}
#else
private sealed class KdTreeComparerY : IComparer<KdTreeElement<T>>
{
public static readonly KdTreeComparerY Shared = new KdTreeComparerY();
public int Compare(KdTreeElement<T> p0, KdTreeElement<T> p1)
{
return p0.Value.Y.CompareTo(p1.Value.Y);
}
}
private sealed class KdTreeComparerX : IComparer<KdTreeElement<T>>
{
public static readonly KdTreeComparerX Shared = new KdTreeComparerX();
public int Compare(KdTreeElement<T> p0, KdTreeElement<T> p1)
{
return p0.Value.X.CompareTo(p1.Value.X);
}
}
private KdTreeNode<T> BuildTree(ArraySegment<KdTreeElement<T>> P, int depth = 0)
{
if (P.Count == 0)
@@ -145,13 +162,13 @@
if (depth % 2 == 0)
{
P.Sort(kdTreeComparerX);
P.Sort(KdTreeComparerX.Shared);
}
else
{
P.Sort(kdTreeComparerY);
P.Sort(KdTreeComparerY.Shared);
}
if (P.Count == 2)
{
return new KdTreeNode<T>(new KdTreeLeaf<T>(P.GetAt(0), depth + 1), null, P.GetAt(1), depth);
@@ -179,13 +196,14 @@
/// <returns>The nearest neighbour's element.</returns>
public T FindNearestNeighbour(T pivot, Func<T, PdfPoint> pivotPointFunc, Func<PdfPoint, PdfPoint, double> distanceMeasure, out int index, out double distance)
{
var result = FindNearestNeighbour(Root, pivot, pivotPointFunc, distanceMeasure);
var pivotPoint = pivotPointFunc(pivot);
var result = FindNearestNeighbour(Root, pivot, pivotPoint, distanceMeasure);
index = result.Item1 != null ? result.Item1.Index : -1;
distance = result.Item2 ?? double.NaN;
return result.Item1 != null ? result.Item1.Element : default;
}
private static (KdTreeNode<T>, double?) FindNearestNeighbour(KdTreeNode<T> node, T pivot, Func<T, PdfPoint> pivotPointFunc, Func<PdfPoint, PdfPoint, double> distance)
private static (KdTreeNode<T>, double?) FindNearestNeighbour(KdTreeNode<T> node, T pivot, PdfPoint pivotPoint, Func<PdfPoint, PdfPoint, double> distance)
{
if (node == null)
{
@@ -197,23 +215,22 @@
{
return (null, null);
}
return (node, distance(node.Value, pivotPointFunc(pivot)));
return (node, distance(node.Value, pivotPoint));
}
else
{
var point = pivotPointFunc(pivot);
var currentNearestNode = node;
var currentDistance = distance(node.Value, point);
var currentDistance = distance(node.Value, pivotPoint);
KdTreeNode<T> newNode = null;
double? newDist = null;
var pointValue = node.IsAxisCutX ? point.X : point.Y;
var pointValue = node.IsAxisCutX ? pivotPoint.X : pivotPoint.Y;
if (pointValue < node.L)
{
// start left
(newNode, newDist) = FindNearestNeighbour(node.LeftChild, pivot, pivotPointFunc, distance);
(newNode, newDist) = FindNearestNeighbour(node.LeftChild, pivot, pivotPoint, distance);
if (newDist.HasValue && newDist <= currentDistance && !newNode.Element.Equals(pivot))
{
@@ -223,13 +240,13 @@
if (node.RightChild != null && pointValue + currentDistance >= node.L)
{
(newNode, newDist) = FindNearestNeighbour(node.RightChild, pivot, pivotPointFunc, distance);
(newNode, newDist) = FindNearestNeighbour(node.RightChild, pivot, pivotPoint, distance);
}
}
else
{
// start right
(newNode, newDist) = FindNearestNeighbour(node.RightChild, pivot, pivotPointFunc, distance);
(newNode, newDist) = FindNearestNeighbour(node.RightChild, pivot, pivotPoint, distance);
if (newDist.HasValue && newDist <= currentDistance && !newNode.Element.Equals(pivot))
{
@@ -239,7 +256,7 @@
if (node.LeftChild != null && pointValue - currentDistance <= node.L)
{
(newNode, newDist) = FindNearestNeighbour(node.LeftChild, pivot, pivotPointFunc, distance);
(newNode, newDist) = FindNearestNeighbour(node.LeftChild, pivot, pivotPoint, distance);
}
}
@@ -258,7 +275,7 @@
/// <summary>
/// Get the k nearest neighbours to the pivot element.
/// Might return more than k neighbours if points are equidistant.
/// <para>Use <see cref="FindNearestNeighbour(KdTreeNode{T}, T, Func{T, PdfPoint}, Func{PdfPoint, PdfPoint, double})"/> if only looking for the (single) closest point.</para>
/// <para>Use <see cref="FindNearestNeighbour(T, Func{T, PdfPoint}, Func{PdfPoint, PdfPoint, double}, out int, out double)"/> if only looking for the (single) closest point.</para>
/// </summary>
/// <param name="pivot">The element for which to find the k nearest neighbours.</param>
/// <param name="k">The number of neighbours to return. Might return more than k neighbours if points are equidistant.</param>
@@ -267,13 +284,24 @@
/// <returns>Returns a list of tuples of the k nearest neighbours. Tuples are (element, index, distance).</returns>
public IReadOnlyList<(T, int, double)> FindNearestNeighbours(T pivot, int k, Func<T, PdfPoint> pivotPointFunc, Func<PdfPoint, PdfPoint, double> distanceMeasure)
{
var pivotPoint = pivotPointFunc(pivot);
var kdTreeNodes = new KNearestNeighboursQueue(k);
FindNearestNeighbours(Root, pivot, k, pivotPointFunc, distanceMeasure, kdTreeNodes);
return kdTreeNodes.SelectMany(n => n.Value.Select(e => (e.Element, e.Index, n.Key))).ToArray();
FindNearestNeighbours(Root, pivot, k, pivotPoint, distanceMeasure, kdTreeNodes);
var results = new List<(T, int, double)>();
for (int i = 0; i < kdTreeNodes.Count; i++)
{
double dist = kdTreeNodes.Keys[i];
foreach (var e in kdTreeNodes.Values[i])
{
results.Add((e.Element, e.Index, dist));
}
}
return results;
}
private static (KdTreeNode<T>, double) FindNearestNeighbours(KdTreeNode<T> node, T pivot, int k,
Func<T, PdfPoint> pivotPointFunc, Func<PdfPoint, PdfPoint, double> distance, KNearestNeighboursQueue queue)
PdfPoint pivotPoint, Func<PdfPoint, PdfPoint, double> distance, KNearestNeighboursQueue queue)
{
if (node == null)
{
@@ -286,7 +314,7 @@
return (null, double.NaN);
}
var currentDistance = distance(node.Value, pivotPointFunc(pivot));
var currentDistance = distance(node.Value, pivotPoint);
var currentNearestNode = node;
if (!queue.IsFull || currentDistance <= queue.LastDistance)
@@ -300,9 +328,8 @@
}
else
{
var point = pivotPointFunc(pivot);
var currentNearestNode = node;
var currentDistance = distance(node.Value, point);
var currentDistance = distance(node.Value, pivotPoint);
if ((!queue.IsFull || currentDistance <= queue.LastDistance) && !node.Element.Equals(pivot))
{
queue.Add(currentDistance, currentNearestNode);
@@ -313,12 +340,12 @@
KdTreeNode<T> newNode = null;
double newDist = double.NaN;
var pointValue = node.IsAxisCutX ? point.X : point.Y;
var pointValue = node.IsAxisCutX ? pivotPoint.X : pivotPoint.Y;
if (pointValue < node.L)
{
// start left
(newNode, newDist) = FindNearestNeighbours(node.LeftChild, pivot, k, pivotPointFunc, distance, queue);
(newNode, newDist) = FindNearestNeighbours(node.LeftChild, pivot, k, pivotPoint, distance, queue);
if (!double.IsNaN(newDist) && newDist <= currentDistance && !newNode.Element.Equals(pivot))
{
@@ -329,13 +356,13 @@
if (node.RightChild != null && pointValue + currentDistance >= node.L)
{
(newNode, newDist) = FindNearestNeighbours(node.RightChild, pivot, k, pivotPointFunc, distance, queue);
(newNode, newDist) = FindNearestNeighbours(node.RightChild, pivot, k, pivotPoint, distance, queue);
}
}
else
{
// start right
(newNode, newDist) = FindNearestNeighbours(node.RightChild, pivot, k, pivotPointFunc, distance, queue);
(newNode, newDist) = FindNearestNeighbours(node.RightChild, pivot, k, pivotPoint, distance, queue);
if (!double.IsNaN(newDist) && newDist <= currentDistance && !newNode.Element.Equals(pivot))
{
@@ -346,7 +373,7 @@
if (node.LeftChild != null && pointValue - currentDistance <= node.L)
{
(newNode, newDist) = FindNearestNeighbours(node.LeftChild, pivot, k, pivotPointFunc, distance, queue);
(newNode, newDist) = FindNearestNeighbours(node.LeftChild, pivot, k, pivotPoint, distance, queue);
}
}
@@ -395,9 +422,14 @@
if (this[key].Add(value))
{
var last = this.Last();
LastElement = last.Value.Last();
LastDistance = last.Key;
LastDistance = Keys[Count - 1];
var lastSet = Values[Count - 1];
KdTreeNode<T> lastElement = null;
foreach (var e in lastSet)
{
lastElement = e;
}
LastElement = lastElement;
}
}
}
@@ -418,23 +450,7 @@
public R Element { get; }
}
private sealed class KdTreeComparerY : IComparer<KdTreeElement<T>>
{
public int Compare(KdTreeElement<T> p0, KdTreeElement<T> p1)
{
return p0.Value.Y.CompareTo(p1.Value.Y);
}
}
private sealed class KdTreeComparerX : IComparer<KdTreeElement<T>>
{
public int Compare(KdTreeElement<T> p0, KdTreeElement<T> p1)
{
return p0.Value.X.CompareTo(p1.Value.X);
}
}
/// <summary>
/// K-D tree leaf.
/// </summary>

View File

@@ -3,7 +3,6 @@
using Content;
using Core;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@@ -154,57 +153,72 @@
{
ParallelOptions parallelOptions = new ParallelOptions() { MaxDegreeOfParallelism = maxDegreeOfParallelism };
var withinLineDistList = new ConcurrentBag<double>();
var betweenLineDistList = new ConcurrentBag<double>();
var withinLineDistList = new List<double>();
var betweenLineDistList = new List<double>();
// 1. Estimate within line and between line spacing
KdTree<Word> kdTreeBottomLeft = new KdTree<Word>(words, w => w.BoundingBox.BottomLeft);
Parallel.For(0, words.Count, parallelOptions, i =>
{
var word = words[i];
// Within-line distance
// 1.1.1 Find the 2 closest neighbours words to the candidate, using euclidean distance.
foreach (var n in kdTreeBottomLeft.FindNearestNeighbours(word, 2, w => w.BoundingBox.BottomRight, Distances.Euclidean))
Parallel.For(0, words.Count, parallelOptions,
() => (wl: new List<double>(), bl: new List<double>()),
(i, _, local) =>
{
// 1.1.2 Check if the neighbour word is within the angle of the candidate
if (wlBounds.Contains(AngleWL(word, n.Item1)))
var word = words[i];
// Within-line distance
// 1.1.1 Find the 2 closest neighbours words to the candidate, using euclidean distance.
foreach (var n in kdTreeBottomLeft.FindNearestNeighbours(word, 2, w => w.BoundingBox.BottomRight, Distances.Euclidean))
{
withinLineDistList.Add(Distances.Euclidean(word.BoundingBox.BottomRight, n.Item1.BoundingBox.BottomLeft));
}
}
// Between-line distance
// 1.2.1 Find the 2 closest neighbours words to the candidate, using euclidean distance.
foreach (var n in kdTreeBottomLeft.FindNearestNeighbours(word, 2, w => w.BoundingBox.TopLeft, Distances.Euclidean))
{
// 1.2.2 Check if the candidate words is within the angle
var angle = AngleBL(word, n.Item1);
if (blBounds.Contains(angle))
{
// 1.2.3 Compute the vertical (between-line) distance between the candidate
// and the neighbour and add it to the between-line distances list
double hypotenuse = Distances.Euclidean(word.BoundingBox.Centroid, n.Item1.BoundingBox.Centroid);
// Angle is kept within [-90, 90]
if (angle > 90)
// 1.1.2 Check if the neighbour word is within the angle of the candidate
if (wlBounds.Contains(AngleWL(word, n.Item1)))
{
angle -= 180;
}
var dist = Math.Abs(hypotenuse * Math.Cos((90 - angle) * Math.PI / 180))
- word.BoundingBox.Height / 2.0 - n.Item1.BoundingBox.Height / 2.0;
// The perpendicular distance can be negative because of the subtractions.
// Could occur when words are overlapping, we ignore that.
if (dist >= 0)
{
betweenLineDistList.Add(dist);
local.wl.Add(Distances.Euclidean(word.BoundingBox.BottomRight, n.Item1.BoundingBox.BottomLeft));
}
}
}
});
// Between-line distance
// 1.2.1 Find the 2 closest neighbours words to the candidate, using euclidean distance.
foreach (var n in kdTreeBottomLeft.FindNearestNeighbours(word, 2, w => w.BoundingBox.TopLeft, Distances.Euclidean))
{
// 1.2.2 Check if the candidate words is within the angle
var angle = AngleBL(word, n.Item1);
if (blBounds.Contains(angle))
{
// 1.2.3 Compute the vertical (between-line) distance between the candidate
// and the neighbour and add it to the between-line distances list
double hypotenuse = Distances.Euclidean(word.BoundingBox.Centroid, n.Item1.BoundingBox.Centroid);
// Angle is kept within [-90, 90]
if (angle > 90)
{
angle -= 180;
}
var dist = Math.Abs(hypotenuse * Math.Cos((90 - angle) * Math.PI / 180))
- word.BoundingBox.Height / 2.0 - n.Item1.BoundingBox.Height / 2.0;
// The perpendicular distance can be negative because of the subtractions.
// Could occur when words are overlapping, we ignore that.
if (dist >= 0)
{
local.bl.Add(dist);
}
}
}
return local;
},
local =>
{
lock (withinLineDistList)
{
withinLineDistList.AddRange(local.wl);
}
lock (betweenLineDistList)
{
betweenLineDistList.AddRange(local.bl);
}
});
// Compute average peak value of distribution
double? withinLinePeak = GetPeakAverageDistance(withinLineDistList, wlBinSize);
@@ -221,9 +235,9 @@
/// </summary>
/// <param name="distances">The set of distances to average.</param>
/// <param name="binLength"></param>
private static double? GetPeakAverageDistance(IEnumerable<double> distances, int binLength = 1)
private static double? GetPeakAverageDistance(List<double> distances, int binLength = 1)
{
if (!distances.Any())
if (distances.Count == 0)
{
return null;
}
@@ -233,7 +247,16 @@
throw new ArgumentException("DocstrumBoundingBoxes: the bin length must be positive when commputing peak average distance.", nameof(binLength));
}
double maxDbl = Math.Ceiling(distances.Max());
double maxDbl = distances[0];
for (int i = 1; i < distances.Count; i++)
{
if (distances[i] > maxDbl)
{
maxDbl = distances[i];
}
}
maxDbl = Math.Ceiling(maxDbl);
if (maxDbl > int.MaxValue)
{
throw new OverflowException($"Error while casting maximum distance of {maxDbl} to integer.");
@@ -249,30 +272,49 @@
binLength = binLength > max ? max : binLength;
}
var bins = Enumerable.Range(0, (int)Math.Ceiling(max / (double)binLength) + 1)
.Select(x => x * binLength)
.ToDictionary(x => x, _ => new List<double>());
foreach (var distance in distances)
int binCount = (int)Math.Ceiling(max / (double)binLength) + 1;
var bins = new List<double>[binCount];
for (int i = 0; i < binCount; i++)
{
bins[i] = new List<double>();
}
for (int i = 0; i < distances.Count; i++)
{
var distance = distances[i];
int bin = (int)Math.Floor(distance / binLength);
if (bin < 0)
{
throw new ArgumentOutOfRangeException(nameof(bin), "DocstrumBoundingBoxes: Negative distance found while commputing peak average distance.");
}
bins[bins.Keys.ElementAt(bin)].Add(distance);
if (bin >= binCount)
{
bin = binCount - 1;
}
bins[bin].Add(distance);
}
var best = default(List<double>);
foreach (var bin in bins)
List<double> best = null;
for (int i = 0; i < binCount; i++)
{
if (best == null || bin.Value.Count > best.Count)
var bin = bins[i];
if (best == null || bin.Count > best.Count)
{
best = bin.Value;
best = bin;
}
}
return best?.Average();
if (best == null || best.Count == 0)
{
return null;
}
double sum = 0;
for (int i = 0; i < best.Count; i++)
{
sum += best[i];
}
return sum / best.Count;
}
#endregion
@@ -417,28 +459,7 @@
return double.PositiveInfinity;
}
}
private sealed class PdfPointXYComparer : IComparer<PdfPoint>
{
public static readonly PdfPointXYComparer Instance = new();
public int Compare(PdfPoint p1, PdfPoint p2)
{
int comp = p1.X.CompareTo(p2.X);
return comp == 0 ? p1.Y.CompareTo(p2.Y) : comp;
}
}
private sealed class PdfPointYComparer : IComparer<PdfPoint>
{
public static readonly PdfPointYComparer Instance = new();
public int Compare(PdfPoint p1, PdfPoint p2)
{
return p1.Y.CompareTo(p2.Y);
}
}
/// <summary>
/// Get the structural blocking parameters.
/// </summary>
@@ -486,22 +507,30 @@
if (dXj != 0)
{
ps.Sort(PdfPointXYComparer.Instance);
ps.Sort((p1, p2) =>
{
int comp = p1.X.CompareTo(p2.X);
return comp == 0 ? p1.Y.CompareTo(p2.Y) : comp;
});
}
else if (dYj != 0)
{
ps.Sort(PdfPointYComparer.Instance);
ps.Sort((p1, p2) => p1.Y.CompareTo(p2.Y));
}
#else
PdfPoint[] ps = [j.Point1, j.Point2, Aj.Value, Bj.Value];
if (dXj != 0)
{
Array.Sort(ps, PdfPointXYComparer.Instance);
Array.Sort(ps, (p1, p2) =>
{
int comp = p1.X.CompareTo(p2.X);
return comp == 0 ? p1.Y.CompareTo(p2.Y) : comp;
});
}
else if (dYj != 0)
{
Array.Sort(ps, PdfPointYComparer.Instance);
Array.Sort(ps, (p1, p2) => p1.Y.CompareTo(p2.Y));
}
#endif

View File

@@ -372,7 +372,7 @@
public Func<IEnumerable<Letter>, double> DominantFontWidthFunc { get; set; } =
(letters) =>
{
var widths = letters.Select(x => Math.Max(Math.Round(x.Width, 3), Math.Round(x.GlyphRectangle.Width, 3)));
var widths = letters.Select(x => Math.Max(Math.Round(x.Width, 3), Math.Round(x.BoundingBox.Width, 3)));
var mode = widths.Mode();
if (double.IsNaN(mode) || mode == 0)
{
@@ -389,7 +389,7 @@
public Func<IEnumerable<Letter>, double> DominantFontHeightFunc { get; set; } =
(letters) =>
{
var heights = letters.Select(x => Math.Round(x.GlyphRectangle.Height, 3));
var heights = letters.Select(x => Math.Round(x.BoundingBox.Height, 3));
var mode = heights.Mode();
if (double.IsNaN(mode) || mode == 0)
{

View File

@@ -9,7 +9,7 @@
/// <summary>
/// A block of text.
/// </summary>
public class TextBlock
public class TextBlock: IBoundingBox
{
/// <summary>
/// The separator used between lines in the block.

View File

@@ -59,7 +59,7 @@
{
letter = new Letter(
" ",
letter.GlyphRectangle,
letter.BoundingBox,
letter.GlyphRectangleLoose,
letter.StartBaseLine,
letter.EndBaseLine,

View File

@@ -9,7 +9,7 @@
/// <summary>
/// A line of text.
/// </summary>
public class TextLine
public class TextLine : IBoundingBox
{
/// <summary>
/// The separator used between words in the line.

View File

@@ -25,8 +25,8 @@
{
return GetWhitespaces(words,
images,
words.SelectMany(w => w.Letters).Select(x => x.GlyphRectangle.Width).Mode() * 1.25,
words.SelectMany(w => w.Letters).Select(x => x.GlyphRectangle.Height).Mode() * 1.25,
words.SelectMany(w => w.Letters).Select(x => x.BoundingBox.Width).Mode() * 1.25,
words.SelectMany(w => w.Letters).Select(x => x.BoundingBox.Height).Mode() * 1.25,
maxRectangleCount: maxRectangleCount,
maxBoundQueueSize: maxBoundQueueSize);
}
@@ -51,7 +51,7 @@
if (images?.Any() == true)
{
bboxes.AddRange(images.Where(w => w.Bounds.Width > 0 && w.Bounds.Height > 0).Select(o => o.Bounds));
bboxes.AddRange(images.Where(w => w.BoundingBox.Width > 0 && w.BoundingBox.Height > 0).Select(o => o.BoundingBox));
}
return GetWhitespaces(bboxes,

View File

@@ -163,8 +163,8 @@
public Func<Letter, Letter, double> MaximumDistance { get; set; } = (l1, l2) =>
{
double maxDist = Math.Max(Math.Max(Math.Max(Math.Max(Math.Max(
Math.Abs(l1.GlyphRectangle.Width),
Math.Abs(l2.GlyphRectangle.Width)),
Math.Abs(l1.BoundingBox.Width),
Math.Abs(l2.BoundingBox.Width)),
Math.Abs(l1.Width)),
Math.Abs(l2.Width)),
l1.PointSize), l2.PointSize) * 0.2;

View File

@@ -1,5 +1,6 @@
namespace UglyToad.PdfPig.Tests.Filters
{
using PdfPig.Core;
using PdfPig.Filters;
using PdfPig.Tokens;
@@ -11,15 +12,32 @@
public void EncodeAndDecodePreservesInput()
{
var parameters = new DictionaryToken(new Dictionary<NameToken, IToken>());
var input = new byte[] {67, 69, 69, 10, 4, 20, 6, 19, 120, 64, 64, 64, 32};
var input = new byte[] { 67, 69, 69, 10, 4, 20, 6, 19, 120, 64, 64, 64, 32 };
using (var inputStream = new MemoryStream(input))
{
inputStream.Seek(0, SeekOrigin.Begin);
var result = filter.Encode(inputStream, parameters, 0);
var result = filter.Encode(inputStream, parameters);
var decoded = filter.Decode(result, parameters, TestFilterProvider.Instance, 0);
Assert.Equal(input, decoded.ToArray());
}
}
[Fact]
public void CanDecodeCorruptedInputIssue1235()
{
const string hexStr =
"789C958D5D0AC2400C844FB077980B74BB7FD9D982F820B43E8B7B03C542C187EAFDC1F84B7D1164200999E49BD9044C6653D10E1E443DA1AF6636ED76EF315E7572968E1ECDAB7FB7506C4C59C0AEB3912EE270366AAAF4E36D364BF7911450DC274A5112B1AC9751D77A58680B51A4D8AE433D62953C037396E0F290FBE098B267A43051725AA34E77E44EF50B1B52B42C90E4ADF83FB94FDD0000000000";
var hex = new HexToken(hexStr.AsSpan());
var parameters = new DictionaryToken(new Dictionary<NameToken, IToken>());
var result = filter.Decode(hex.Bytes.ToArray(), parameters, TestFilterProvider.Instance, 0);
var text = OtherEncodings.BytesAsLatin1String(result.ToArray());
Assert.StartsWith("q", text);
}
}
}

View File

@@ -65,14 +65,14 @@ namespace UglyToad.PdfPig.Tests.Fonts.SystemFonts
var current = page.Letters[i];
Assert.Equal(expectedData.TopLeft.X, current.GlyphRectangle.TopLeft.X, 6);
Assert.Equal(expectedData.TopLeft.Y, current.GlyphRectangle.TopLeft.Y, 6);
Assert.Equal(expectedData.Width, current.GlyphRectangle.Width, 6);
Assert.Equal(expectedData.Height, current.GlyphRectangle.Height, 6);
Assert.Equal(expectedData.Rotation, current.GlyphRectangle.Rotation, 3);
Assert.Equal(expectedData.TopLeft.X, current.BoundingBox.TopLeft.X, 6);
Assert.Equal(expectedData.TopLeft.Y, current.BoundingBox.TopLeft.Y, 6);
Assert.Equal(expectedData.Width, current.BoundingBox.Width, 6);
Assert.Equal(expectedData.Height, current.BoundingBox.Height, 6);
Assert.Equal(expectedData.Rotation, current.BoundingBox.Rotation, 3);
Assert.True(current.GlyphRectangle.IntersectsWith(current.GlyphRectangleLoose));
Assert.Equal(current.GlyphRectangle.Rotation, current.GlyphRectangleLoose.Rotation, 3);
Assert.True(current.BoundingBox.IntersectsWith(current.GlyphRectangleLoose));
Assert.Equal(current.BoundingBox.Rotation, current.GlyphRectangleLoose.Rotation, 3);
}
}
}

View File

@@ -20,14 +20,14 @@
// check 'm'
var m = letters[0];
Assert.Equal("m", m.Value);
Assert.Equal(new PdfPoint(253.4458, 658.431), m.GlyphRectangle.BottomLeft, pointComparer);
Assert.Equal(new PdfPoint(261.22659, 662.83446), m.GlyphRectangle.TopRight, pointComparer);
Assert.Equal(new PdfPoint(253.4458, 658.431), m.BoundingBox.BottomLeft, pointComparer);
Assert.Equal(new PdfPoint(261.22659, 662.83446), m.BoundingBox.TopRight, pointComparer);
// check 'p'
var p = letters[1];
Assert.Equal("p", p.Value);
Assert.Equal(new PdfPoint(261.70778, 656.49825), p.GlyphRectangle.BottomLeft, pointComparer);
Assert.Equal(new PdfPoint(266.6193, 662.83446), p.GlyphRectangle.TopRight, pointComparer);
Assert.Equal(new PdfPoint(261.70778, 656.49825), p.BoundingBox.BottomLeft, pointComparer);
Assert.Equal(new PdfPoint(266.6193, 662.83446), p.BoundingBox.TopRight, pointComparer);
}
}
}

View File

@@ -50,7 +50,7 @@
Assert.Equal(Width, letter.Width, 1);
if (includeHeight)
{
Assert.Equal(Height, letter.GlyphRectangle.Height, 1);
Assert.Equal(Height, letter.BoundingBox.Height, 1);
}
}

View File

@@ -33,7 +33,7 @@
var rect = new PdfRectangle(207, 158, 229, 168.5);
var missingChars = letters.Where(l => rect.Contains(l.GlyphRectangle)).ToArray();
var missingChars = letters.Where(l => rect.Contains(l.BoundingBox)).ToArray();
Assert.NotEmpty(missingChars);
Assert.True(missingChars.Length == 2);

View File

@@ -11,6 +11,31 @@
public class GithubIssuesTests
{
[Fact]
public void Issues1250()
{
// Issue comes from HasFormXObjectCircularReference
var path = IntegrationHelpers.GetDocumentPath("SPE8EF26T0545.pdf");
using (var document = PdfDocument.Open(path, new ParsingOptions() { UseLenientParsing = true }))
{
var page = document.GetPage(1);
Assert.NotNull(page);
Assert.NotEmpty(page.Letters);
page = document.GetPage(7);
Assert.NotNull(page);
Assert.NotEmpty(page.Letters);
}
// Ensure still no StackOverflowException
using (var document = PdfDocument.Open(IntegrationHelpers.GetDocumentPath("issue_671")))
{
var page = document.GetPage(1);
Assert.NotNull(page);
Assert.NotEmpty(page.Letters);
}
}
[Fact]
public void Issues1248()
{
@@ -29,7 +54,7 @@
}
}
}
[Fact]
public void Issues1238()
{
@@ -302,7 +327,7 @@
var path = IntegrationHelpers.GetSpecificTestDocumentPath("StackOverflow_Issue_1122.pdf");
var ex = Assert.Throws<PdfDocumentFormatException>(() => PdfDocument.Open(path, new ParsingOptions() { UseLenientParsing = true }));
Assert.Equal("The root object in the trailer did not resolve to a readable dictionary.", ex.Message);
Assert.StartsWith("Circular reference encountered when looking", ex.Message);
}
[Fact]
@@ -361,7 +386,7 @@
{
var path = IntegrationHelpers.GetSpecificTestDocumentPath("SpookyPass.pdf");
var ex = Assert.Throws<PdfDocumentFormatException>(() => PdfDocument.Open(path, new ParsingOptions() { UseLenientParsing = true }));
Assert.Equal("The root object in the trailer did not resolve to a readable dictionary.", ex.Message);
Assert.StartsWith("Object stream cannot contain itself", ex.Message);
}
[Fact]
@@ -527,7 +552,7 @@
{
var page = document.GetPage(13);
// This used to fail with an overflow exception when we failed to validate the zlib encoded data
Assert.NotNull(DocstrumBoundingBoxes.Instance.GetBlocks(page.GetWords()));
Assert.Throws<OverflowException>(() => DocstrumBoundingBoxes.Instance.GetBlocks(page.GetWords()));
}
}
@@ -733,7 +758,7 @@
{
var letter = page1.Letters[l];
Assert.Equal(TextOrientation.Other, letter.TextOrientation);
Assert.Equal(45.0, letter.GlyphRectangle.Rotation, 5);
Assert.Equal(45.0, letter.BoundingBox.Rotation, 5);
}
var page2 = document.GetPage(2);

View File

@@ -28,7 +28,7 @@
var page = document.GetPage(i + 1);
foreach (var letter in page.Letters)
{
var bbox = letter.GlyphRectangle;
var bbox = letter.BoundingBox;
if (bbox.Height > 0)
{
if (letter.GlyphRectangleLoose.Height <= 0)

View File

@@ -34,7 +34,7 @@ namespace UglyToad.PdfPig.Tests.Integration
{
var page = document.GetPage(1);
Assert.NotEqual(0, page.Letters[0].GlyphRectangle.Height);
Assert.NotEqual(0, page.Letters[0].BoundingBox.Height);
}
}

View File

@@ -12,8 +12,8 @@
var page = document.GetPage(1);
Assert.Equal(612, page.Width); // Due to cropping
Assert.Equal(792, page.Height); // Due to cropping
var minX = page.Letters.Select(l => l.GlyphRectangle.Left).Min();
var maxX = page.Letters.Select(l => l.GlyphRectangle.Right).Max();
var minX = page.Letters.Select(l => l.BoundingBox.Left).Min();
var maxX = page.Letters.Select(l => l.BoundingBox.Right).Max();
Assert.Equal(74, minX, 0); // If cropping is not applied correctly, these values will be off
Assert.Equal(540, maxX, 0); // If cropping is not applied correctly, these values will be off
// The page is cropped at

View File

@@ -47,29 +47,29 @@
{
var page = document.GetPage(1);
var images = page.GetImages().OrderBy(x => x.Bounds.Width).ToList();
var images = page.GetImages().OrderBy(x => x.BoundingBox.Width).ToList();
var pdfPigSquare = images[0];
Assert.Equal(148.3d, pdfPigSquare.Bounds.Width, doubleComparer);
Assert.Equal(148.3d, pdfPigSquare.Bounds.Height, doubleComparer);
Assert.Equal(60.1d, pdfPigSquare.Bounds.Left, doubleComparer);
Assert.Equal(765.8d, pdfPigSquare.Bounds.Top, doubleComparer);
Assert.Equal(148.3d, pdfPigSquare.BoundingBox.Width, doubleComparer);
Assert.Equal(148.3d, pdfPigSquare.BoundingBox.Height, doubleComparer);
Assert.Equal(60.1d, pdfPigSquare.BoundingBox.Left, doubleComparer);
Assert.Equal(765.8d, pdfPigSquare.BoundingBox.Top, doubleComparer);
var pdfPigSquished = images[1];
Assert.Equal(206.8d, pdfPigSquished.Bounds.Width, doubleComparer);
Assert.Equal(83.2d, pdfPigSquished.Bounds.Height, doubleComparer);
Assert.Equal(309.8d, pdfPigSquished.Bounds.Left, doubleComparer);
Assert.Equal(552.1d, pdfPigSquished.Bounds.Top, doubleComparer);
Assert.Equal(206.8d, pdfPigSquished.BoundingBox.Width, doubleComparer);
Assert.Equal(83.2d, pdfPigSquished.BoundingBox.Height, doubleComparer);
Assert.Equal(309.8d, pdfPigSquished.BoundingBox.Left, doubleComparer);
Assert.Equal(552.1d, pdfPigSquished.BoundingBox.Top, doubleComparer);
var birthdayPigs = images[2];
Assert.Equal(391d, birthdayPigs.Bounds.Width, doubleComparer);
Assert.Equal(267.1d, birthdayPigs.Bounds.Height, doubleComparer);
Assert.Equal(102.2d, birthdayPigs.Bounds.Left, doubleComparer);
Assert.Equal(426.3d, birthdayPigs.Bounds.Top, doubleComparer);
Assert.Equal(391d, birthdayPigs.BoundingBox.Width, doubleComparer);
Assert.Equal(267.1d, birthdayPigs.BoundingBox.Height, doubleComparer);
Assert.Equal(102.2d, birthdayPigs.BoundingBox.Left, doubleComparer);
Assert.Equal(426.3d, birthdayPigs.BoundingBox.Top, doubleComparer);
}
}

View File

@@ -42,27 +42,27 @@
Assert.Equal("I", page.Letters[0].Value);
Assert.Equal(90.1d, page.Letters[0].GlyphRectangle.BottomLeft.X, comparer);
Assert.Equal(709.2d, page.Letters[0].GlyphRectangle.BottomLeft.Y, comparer);
Assert.Equal(90.1d, page.Letters[0].BoundingBox.BottomLeft.X, comparer);
Assert.Equal(709.2d, page.Letters[0].BoundingBox.BottomLeft.Y, comparer);
Assert.Equal(94.0d, page.Letters[0].GlyphRectangle.TopRight.X, comparer);
Assert.Equal(719.89d, page.Letters[0].GlyphRectangle.TopRight.Y, comparer);
Assert.Equal(94.0d, page.Letters[0].BoundingBox.TopRight.X, comparer);
Assert.Equal(719.89d, page.Letters[0].BoundingBox.TopRight.Y, comparer);
Assert.Equal("a", page.Letters[5].Value);
Assert.Equal(114.5d, page.Letters[5].GlyphRectangle.BottomLeft.X, comparer);
Assert.Equal(709.2d, page.Letters[5].GlyphRectangle.BottomLeft.Y, comparer);
Assert.Equal(114.5d, page.Letters[5].BoundingBox.BottomLeft.X, comparer);
Assert.Equal(709.2d, page.Letters[5].BoundingBox.BottomLeft.Y, comparer);
Assert.Equal(119.82d, page.Letters[5].GlyphRectangle.TopRight.X, comparer);
Assert.Equal(714.89d, page.Letters[5].GlyphRectangle.TopRight.Y, comparer);
Assert.Equal(119.82d, page.Letters[5].BoundingBox.TopRight.X, comparer);
Assert.Equal(714.89d, page.Letters[5].BoundingBox.TopRight.Y, comparer);
Assert.Equal("f", page.Letters[16].Value);
Assert.Equal(169.9d, page.Letters[16].GlyphRectangle.BottomLeft.X, comparer);
Assert.Equal(709.2d, page.Letters[16].GlyphRectangle.BottomLeft.Y, comparer);
Assert.Equal(169.9d, page.Letters[16].BoundingBox.BottomLeft.X, comparer);
Assert.Equal(709.2d, page.Letters[16].BoundingBox.BottomLeft.Y, comparer);
Assert.Equal(176.89d, page.Letters[16].GlyphRectangle.TopRight.X, comparer);
Assert.Equal(719.89d, page.Letters[16].GlyphRectangle.TopRight.Y, comparer);
Assert.Equal(176.89d, page.Letters[16].BoundingBox.TopRight.X, comparer);
Assert.Equal(719.89d, page.Letters[16].BoundingBox.TopRight.Y, comparer);
}
}

View File

@@ -51,8 +51,8 @@ namespace UglyToad.PdfPig.Tests.Integration
{
var page = document.GetPage(1);
Assert.Contains(page.Letters, x => x.GlyphRectangle.Width != 0);
Assert.Contains(page.Letters, x => x.GlyphRectangle.Height != 0);
Assert.Contains(page.Letters, x => x.BoundingBox.Width != 0);
Assert.Contains(page.Letters, x => x.BoundingBox.Height != 0);
Assert.Contains(page.Letters, x => x.GlyphRectangleLoose.Width != 0);
Assert.Contains(page.Letters, x => x.GlyphRectangleLoose.Height != 0);
}

View File

@@ -14,8 +14,8 @@ namespace UglyToad.PdfPig.Tests.Integration
{
var page = document.GetPage(1);
Assert.Contains(page.Letters, x => x.GlyphRectangle.Width != 0);
Assert.Contains(page.Letters, x => x.GlyphRectangle.Height != 0);
Assert.Contains(page.Letters, x => x.BoundingBox.Width != 0);
Assert.Contains(page.Letters, x => x.BoundingBox.Height != 0);
}
}
}

View File

@@ -175,7 +175,7 @@
foreach (var letter in page.Letters)
{
DrawRectangle(letter.GlyphRectangle, graphics, violetPen, imageHeight, scale);
DrawRectangle(letter.BoundingBox, graphics, violetPen, imageHeight, scale);
}
foreach (var annotation in page.GetAnnotations())

View File

@@ -72,7 +72,7 @@
{
foreach (var letter in page.Letters)
{
DrawRectangle(letter.GlyphRectangle, canvas, redPaint, size.Height, Scale);
DrawRectangle(letter.BoundingBox, canvas, redPaint, size.Height, Scale);
}
}

View File

@@ -47,7 +47,7 @@ public class FirstPassParserTests
Assert.Equal(2, results.Parts.Count);
Assert.NotNull(results.Trailer);
Assert.Equal(results.XrefOffsets[new IndirectReference(8, 0)], 500);
Assert.Equal(results.XrefOffsets[new IndirectReference(8, 0)].Value1, 500);
}
[Fact]

View File

@@ -589,7 +589,7 @@ public class XrefTableParserTests
{
Assert.True(table.ObjectOffsets.TryGetValue(offset.Key, out var actual));
Assert.Equal(offset.Value, actual);
Assert.Equal(offset.Value, actual.Value1);
}
}

View File

@@ -216,6 +216,68 @@ l";
Assert.NotEmpty(result);
}
[Fact]
public void HandlesIssue953_IntOverflowContent()
{
// After ( + ) Tj operator the content stream becomes corrupt, our current parser therefore reads wrong
// values for operations and this results in a problem when applying the show text operations, we should safely discard or recover on BT/ET boundaries.
const string s =
"""
BT
/TT6 1 Tf
12.007 0 0 12.007 163.2j
-0.19950 Tc
0 Tw
(x)Tj
-0.1949 1.4142 TD
(H)Tj
/TT7 1 Tf
12.031 0 0 12.031 157.38 85.2 Tm
<0077>Tj
-0.1945 1.4114 TD
<0077>Tj
/TT4 1 Tf
12.007 0 0 12.007 174.42 94.5601 Tm
0.0004 Tc
-0.0005 Tw
( + )Tj
E9 478l)]T862.68E9 478E9 484.54 9 155l)]T862.6av9 478E9 15.2(
ET
154.386( i92 m
171.6 97.62 l
S
BT
/TT6 28 Tf
12.03128 T2002.0307 163.2j
-0.19950 DAc
0 Tw853Tj
0.1945 1.4142 om)873j
-0.574142 om)68.80
-0.5797 0 TD
(f)Tj
/TT( )7Tf
0.31945 1.5341 TD371.4j
2.82
8.2652 0 5.724 TD
0 Tc
-0.0001 2748.3( = 091ity )-27483
[(te27483
[(te27483
[(te27483
[(te27483
[(te27483
[(Eq.)52 \(2.1
(
""";
var input = StringBytesTestConverter.Convert(s, false);
var lenientParser = new PageContentParser(ReflectionGraphicsStateOperationFactory.Instance, new StackDepthGuard(256), true);
var result = lenientParser.Parse(1, input.Bytes, log);
Assert.NotEmpty(result);
}
private static string LineEndingsToWhiteSpace(string str)
{
return str.Replace("\r\n", " ").Replace('\n', ' ').Replace('\r', ' ');

View File

@@ -59,7 +59,7 @@ startxref
Assert.Equal(4, locations.Count);
Assert.Equal(TestDataOffsets, locations.Values);
Assert.Equal(TestDataOffsets, locations.Values.Select(x => x.Value1));
}
[Fact]
@@ -111,7 +111,7 @@ endobj
s.IndexOf("11 0 obj", StringComparison.OrdinalIgnoreCase)
};
Assert.Equal(expectedLocations, locations.Values);
Assert.Equal(expectedLocations, locations.Values.Select(x => x.Value1));
}
[Fact]
@@ -142,7 +142,7 @@ endobj";
s.IndexOf("5 0 obj", StringComparison.OrdinalIgnoreCase)
};
Assert.Equal(expectedLocations, locations.Values);
Assert.Equal(expectedLocations, locations.Values.Select(x => x.Value1));
}
[Fact]
@@ -156,17 +156,17 @@ endobj";
Assert.Equal(13, locations.Count);
Assert.Equal(6183, locations[new IndirectReference(1, 0)]);
Assert.Equal(244, locations[new IndirectReference(2, 0)]);
Assert.Equal(15, locations[new IndirectReference(3, 0)]);
Assert.Equal(222, locations[new IndirectReference(4, 0)]);
Assert.Equal(5766, locations[new IndirectReference(5, 0)]);
Assert.Equal(353, locations[new IndirectReference(6, 0)]);
Assert.Equal(581, locations[new IndirectReference(7, 0)]);
Assert.Equal(5068, locations[new IndirectReference(8, 0)]);
Assert.Equal(5091, locations[new IndirectReference(9, 0)]);
Assert.Equal(6183, locations[new IndirectReference(1, 0)].Value1);
Assert.Equal(244, locations[new IndirectReference(2, 0)].Value1);
Assert.Equal(15, locations[new IndirectReference(3, 0)].Value1);
Assert.Equal(222, locations[new IndirectReference(4, 0)].Value1);
Assert.Equal(5766, locations[new IndirectReference(5, 0)].Value1);
Assert.Equal(353, locations[new IndirectReference(6, 0)].Value1);
Assert.Equal(581, locations[new IndirectReference(7, 0)].Value1);
Assert.Equal(5068, locations[new IndirectReference(8, 0)].Value1);
Assert.Equal(5091, locations[new IndirectReference(9, 0)].Value1);
var s = GetStringAt(bytes, locations[new IndirectReference(3, 0)]);
var s = GetStringAt(bytes, locations[new IndirectReference(3, 0)].Value1);
Assert.StartsWith("3 0 obj", s);
}
}
@@ -180,17 +180,17 @@ endobj";
Assert.Equal(13, locations.Count);
Assert.Equal(6183, locations[new IndirectReference(1, 0)]);
Assert.Equal(244, locations[new IndirectReference(2, 0)]);
Assert.Equal(15, locations[new IndirectReference(3, 0)]);
Assert.Equal(222, locations[new IndirectReference(4, 0)]);
Assert.Equal(5766, locations[new IndirectReference(5, 0)]);
Assert.Equal(353, locations[new IndirectReference(6, 0)]);
Assert.Equal(581, locations[new IndirectReference(7, 0)]);
Assert.Equal(5068, locations[new IndirectReference(8, 0)]);
Assert.Equal(5091, locations[new IndirectReference(9, 0)]);
Assert.Equal(6183, locations[new IndirectReference(1, 0)].Value1);
Assert.Equal(244, locations[new IndirectReference(2, 0)].Value1);
Assert.Equal(15, locations[new IndirectReference(3, 0)].Value1);
Assert.Equal(222, locations[new IndirectReference(4, 0)].Value1);
Assert.Equal(5766, locations[new IndirectReference(5, 0)].Value1);
Assert.Equal(353, locations[new IndirectReference(6, 0)].Value1);
Assert.Equal(581, locations[new IndirectReference(7, 0)].Value1);
Assert.Equal(5068, locations[new IndirectReference(8, 0)].Value1);
Assert.Equal(5091, locations[new IndirectReference(9, 0)].Value1);
var s = GetStringAt(bytes, locations[new IndirectReference(3, 0)]);
var s = GetStringAt(bytes, locations[new IndirectReference(3, 0)].Value1);
Assert.StartsWith("3 0 obj", s);
}
@@ -203,21 +203,21 @@ endobj";
Assert.Equal(13, locations.Count);
Assert.Equal(17, locations[new IndirectReference(1, 0)]);
Assert.Equal(249, locations[new IndirectReference(2, 0)]);
Assert.Equal(14291, locations[new IndirectReference(3, 0)]);
Assert.Equal(275, locations[new IndirectReference(4, 0)]);
Assert.Equal(382, locations[new IndirectReference(5, 0)]);
Assert.Equal(13283, locations[new IndirectReference(6, 0)]);
Assert.Equal(13309, locations[new IndirectReference(7, 0)]);
Assert.Equal(13556, locations[new IndirectReference(8, 0)]);
Assert.Equal(13926, locations[new IndirectReference(9, 0)]);
Assert.Equal(14183, locations[new IndirectReference(10, 0)]);
Assert.Equal(14224, locations[new IndirectReference(11, 0)]);
Assert.Equal(14428, locations[new IndirectReference(12, 0)]);
Assert.Equal(14488, locations[new IndirectReference(13, 0)]);
Assert.Equal(17, locations[new IndirectReference(1, 0)].Value1);
Assert.Equal(249, locations[new IndirectReference(2, 0)].Value1);
Assert.Equal(14291, locations[new IndirectReference(3, 0)].Value1);
Assert.Equal(275, locations[new IndirectReference(4, 0)].Value1);
Assert.Equal(382, locations[new IndirectReference(5, 0)].Value1);
Assert.Equal(13283, locations[new IndirectReference(6, 0)].Value1);
Assert.Equal(13309, locations[new IndirectReference(7, 0)].Value1);
Assert.Equal(13556, locations[new IndirectReference(8, 0)].Value1);
Assert.Equal(13926, locations[new IndirectReference(9, 0)].Value1);
Assert.Equal(14183, locations[new IndirectReference(10, 0)].Value1);
Assert.Equal(14224, locations[new IndirectReference(11, 0)].Value1);
Assert.Equal(14428, locations[new IndirectReference(12, 0)].Value1);
Assert.Equal(14488, locations[new IndirectReference(13, 0)].Value1);
var s = GetStringAt(bytes, locations[new IndirectReference(12, 0)]);
var s = GetStringAt(bytes, locations[new IndirectReference(12, 0)].Value1);
Assert.StartsWith("12 0 obj", s);
}
@@ -230,7 +230,7 @@ endobj";
var locations = BruteForceSearcher.GetObjectLocations(input);
Assert.Equal(TestDataOffsets, locations.Values);
Assert.Equal(TestDataOffsets, locations.Values.Select(x => x.Value1));
}
[Fact]
@@ -265,7 +265,7 @@ endobj
s.IndexOf("11 0 obj", StringComparison.OrdinalIgnoreCase)
};
Assert.Equal(expectedLocations, locations.Values);
Assert.Equal(expectedLocations, locations.Values.Select(x => x.Value1));
}
private static string GetStringAt(IInputBytes bytes, long location)

View File

@@ -15,8 +15,8 @@
var reference1 = new IndirectReference(7, 0);
var reference2 = new IndirectReference(9, 0);
scanner.Objects[reference1] = new ObjectToken(10, reference1, new IndirectReferenceToken(reference2));
scanner.Objects[reference2] = new ObjectToken(12, reference2, new NumericToken(69));
scanner.Objects[reference1] = new ObjectToken(XrefLocation.File(10), reference1, new IndirectReferenceToken(reference2));
scanner.Objects[reference2] = new ObjectToken(XrefLocation.File(12), reference2, new NumericToken(69));
Assert.True(DirectObjectFinder.TryGet(new IndirectReferenceToken(reference1), scanner, out NumericToken result));
@@ -29,8 +29,8 @@
var reference1 = new IndirectReference(7, 0);
var reference2 = new IndirectReference(9, 0);
scanner.Objects[reference1] = new ObjectToken(10, reference1, new IndirectReferenceToken(reference2));
scanner.Objects[reference2] = new ObjectToken(12, reference2, new NumericToken(69));
scanner.Objects[reference1] = new ObjectToken(XrefLocation.File(10), reference1, new IndirectReferenceToken(reference2));
scanner.Objects[reference2] = new ObjectToken(XrefLocation.File(12), reference2, new NumericToken(69));
var result = DirectObjectFinder.Get<NumericToken>(reference1, scanner);
@@ -43,8 +43,8 @@
var reference1 = new IndirectReference(7, 0);
var reference2 = new IndirectReference(9, 0);
scanner.Objects[reference1] = new ObjectToken(10, reference1, new IndirectReferenceToken(reference2));
scanner.Objects[reference2] = new ObjectToken(12, reference2, new NumericToken(69));
scanner.Objects[reference1] = new ObjectToken(XrefLocation.File(10), reference1, new IndirectReferenceToken(reference2));
scanner.Objects[reference2] = new ObjectToken(XrefLocation.File(12), reference2, new NumericToken(69));
var result = DirectObjectFinder.Get<NumericToken>(new IndirectReferenceToken(reference1), scanner);
@@ -57,7 +57,7 @@
var reference = new IndirectReference(10, 0);
const string expected = "Goopy";
scanner.Objects[reference] = new ObjectToken(10, reference, new ArrayToken(new []
scanner.Objects[reference] = new ObjectToken(XrefLocation.File(10), reference, new ArrayToken(new []
{
new StringToken(expected)
}));
@@ -74,12 +74,12 @@
var reference2 = new IndirectReference(69, 0);
const string expected = "Goopy";
scanner.Objects[reference] = new ObjectToken(10, reference, new ArrayToken(new[]
scanner.Objects[reference] = new ObjectToken(XrefLocation.File(10), reference, new ArrayToken(new[]
{
new IndirectReferenceToken(reference2)
}));
scanner.Objects[reference2] = new ObjectToken(69, reference2, new StringToken(expected));
scanner.Objects[reference2] = new ObjectToken(XrefLocation.File(69), reference2, new StringToken(expected));
var result = DirectObjectFinder.Get<StringToken>(reference, scanner);
@@ -91,7 +91,7 @@
{
var reference = new IndirectReference(10, 0);
scanner.Objects[reference] = new ObjectToken(10, reference, new ArrayToken(new[]
scanner.Objects[reference] = new ObjectToken(XrefLocation.File(10), reference, new ArrayToken(new[]
{
new NumericToken(5), new NumericToken(6), new NumericToken(0)
}));

View File

@@ -77,6 +77,7 @@
"UglyToad.PdfPig.Content.DocumentInformation",
"UglyToad.PdfPig.Content.EmbeddedFile",
"UglyToad.PdfPig.Content.Hyperlink",
"UglyToad.PdfPig.Content.IBoundingBox",
"UglyToad.PdfPig.Content.InlineImage",
"UglyToad.PdfPig.Content.IPageFactory`1",
"UglyToad.PdfPig.Content.IPdfImage",
@@ -285,6 +286,7 @@
"UglyToad.PdfPig.Util.Diacritics",
"UglyToad.PdfPig.Util.WhitespaceSizeStatistics",
"UglyToad.PdfPig.Writer.ITokenWriter",
"UglyToad.PdfPig.Writer.LinkAnnotation",
"UglyToad.PdfPig.Writer.PdfAStandard",
"UglyToad.PdfPig.Writer.PdfDocumentBuilder",
"UglyToad.PdfPig.Writer.PdfMerger",

View File

@@ -6,14 +6,14 @@
internal class TestObjectLocationProvider : IObjectLocationProvider
{
public Dictionary<IndirectReference, long> Offsets { get; } = new Dictionary<IndirectReference, long>();
public Dictionary<IndirectReference, XrefLocation> Offsets { get; } = new Dictionary<IndirectReference, XrefLocation>();
public bool TryGetOffset(IndirectReference reference, out long offset)
public bool TryGetOffset(IndirectReference reference, out XrefLocation offset)
{
return Offsets.TryGetValue(reference, out offset);
}
public void UpdateOffset(IndirectReference reference, long offset)
public void UpdateOffset(IndirectReference reference, XrefLocation offset)
{
Offsets[reference] = offset;
}

View File

@@ -9,7 +9,8 @@
public class TestPdfImage : IPdfImage
{
public PdfRectangle Bounds { get; set; }
public PdfRectangle BoundingBox { get; set; }
public PdfRectangle Bounds => BoundingBox;
public int WidthInSamples { get; set; }

View File

@@ -6,7 +6,7 @@
public class ArrayTokenizerTests
{
private readonly ArrayTokenizer tokenizer = new ArrayTokenizer(true, new StackDepthGuard(256));
private readonly ArrayTokenizer tokenizer = new ArrayTokenizer(true, new StackDepthGuard(256), false);
[Theory]
[InlineData("]")]

View File

@@ -31,10 +31,10 @@
var point = new PdfPoint(leftX, topPageY);
DateTimeStampPage(pdfBuilder, page, point, cm);
var letters = page.AddText("Adobe Standard Font ZapfDingbats", 21, point, F2);
var newY = topPageY - letters.Select(v => v.GlyphRectangle.Height).Max() * 1.2;
var newY = topPageY - letters.Select(v => v.BoundingBox.Height).Max() * 1.2;
point = new PdfPoint(leftX, newY);
letters = page.AddText("Font Specific encoding in Black (octal) and Unicode in Blue (hex)", 10, point, F2);
newY = newY - letters.Select(v => v.GlyphRectangle.Height).Max() * 3;
newY = newY - letters.Select(v => v.BoundingBox.Height).Max() * 3;
point = new PdfPoint(leftX, newY);
var eachRowY = new List<double>();
eachRowY.Add(newY); // First row
@@ -216,10 +216,10 @@
var point = new PdfPoint(leftX, topPageY);
DateTimeStampPage(pdfBuilder, page, point, cm);
var letters = page.AddText("Adobe Standard Font Symbol ", 21, point, F2);
var newY = topPageY - letters.Select(v => v.GlyphRectangle.Height).Max() * 1.2;
var newY = topPageY - letters.Select(v => v.BoundingBox.Height).Max() * 1.2;
point = new PdfPoint(leftX, newY);
letters = page.AddText("Font Specific encoding in Black (octal), Unicode in Blue (hex), Red only available using Unicode", 10, point, F2);
newY = newY - letters.Select(v => v.GlyphRectangle.Height).Max() * 3;
newY = newY - letters.Select(v => v.BoundingBox.Height).Max() * 3;
@@ -492,10 +492,10 @@
var point = new PdfPoint(leftX, topPageY);
DateTimeStampPage(pdfBuilder, page, point, cm);
var letters = page.AddText("Adobe Standard Font " + fontName, 21, point, F2);
var newY = topPageY - letters.Select(v => v.GlyphRectangle.Height).Max() * 1.2;
var newY = topPageY - letters.Select(v => v.BoundingBox.Height).Max() * 1.2;
point = new PdfPoint(leftX, newY);
letters = page.AddText("Font Specific encoding in Black, Unicode in Blue, Red only available using Unicode", 10, point, F2);
newY = newY - letters.Select(v => v.GlyphRectangle.Height).Max() * 3;
newY = newY - letters.Select(v => v.BoundingBox.Height).Max() * 3;
point = new PdfPoint(leftX, newY);
@@ -799,10 +799,10 @@
var labelPointSize = 5;
var octalString = System.Convert.ToString((int)stringToAdd[0], 8).PadLeft(3, '0');
var label = octalString;
var codeMidPoint = point.X + letter[0].GlyphRectangle.Width / 2;
var codeMidPoint = point.X + letter[0].BoundingBox.Width / 2;
var ml = page.MeasureText(label, labelPointSize, point, fontLabel);
var labelY = point.Y + ml.Max(v => v.GlyphRectangle.Height) * 0.1 + maxCharacterHeight;
var xLabel = codeMidPoint - (ml.Sum(v => v.GlyphRectangle.Width) / 2);
var labelY = point.Y + ml.Max(v => v.BoundingBox.Height) * 0.1 + maxCharacterHeight;
var xLabel = codeMidPoint - (ml.Sum(v => v.BoundingBox.Width) / 2);
var labelPoint = new PdfPoint(xLabel, labelY);
page.AddText(label, labelPointSize, labelPoint, fontLabel);
}
@@ -812,10 +812,10 @@
var labelPointSize = 3;
var hexString = $"{(int)stringToAdd[0]:X}".PadLeft(4, '0');
var label = "0x" + hexString;
var codeMidPoint = point.X + letter[0].GlyphRectangle.Width / 2;
var codeMidPoint = point.X + letter[0].BoundingBox.Width / 2;
var ml = page.MeasureText(label, labelPointSize, point, fontLabel);
var labelY = point.Y - ml.Max(v => v.GlyphRectangle.Height) * 2.5;
var xLabel = codeMidPoint - (ml.Sum(v => v.GlyphRectangle.Width) / 2);
var labelY = point.Y - ml.Max(v => v.BoundingBox.Height) * 2.5;
var xLabel = codeMidPoint - (ml.Sum(v => v.BoundingBox.Width) / 2);
var labelPoint = new PdfPoint(xLabel, labelY);
page.AddText(label, labelPointSize, labelPoint, fontLabel);
}
@@ -829,8 +829,8 @@
double inch = (page.PageSize.Width / 8.5);
double cm = inch / 2.54;
var letterWidth = letter[0].GlyphRectangle.Width * 2;
var letterHeight = letter[0].GlyphRectangle.Height * 2;
var letterWidth = letter[0].BoundingBox.Width * 2;
var letterHeight = letter[0].BoundingBox.Height * 2;
var newX = point.X + maxCharacterWidth * 1.1;
var newY = point.Y;
@@ -892,7 +892,7 @@
double maxCharacterWidth;
{
var point = new PdfPoint(10, 10);
var characterRectangles = unicodesCharacters.Select(v => page.MeasureText($"{v}", 12, point, font)[0].GlyphRectangle);
var characterRectangles = unicodesCharacters.Select(v => page.MeasureText($"{v}", 12, point, font)[0].BoundingBox);
maxCharacterHeight = characterRectangles.Max(v => v.Height);
maxCharacterWidth = characterRectangles.Max(v => v.Height);
}
@@ -921,8 +921,8 @@
{
var mtUTC = page.MeasureText(stampTextUTC, fontSize, point, courierFont);
var mtlocal = page.MeasureText(stampTextLocal, fontSize, point, courierFont);
var widthUTC = mtUTC.Sum(v => v.GlyphRectangle.Width);
var widthLocal = mtlocal.Sum(v => v.GlyphRectangle.Width);
var widthUTC = mtUTC.Sum(v => v.BoundingBox.Width);
var widthLocal = mtlocal.Sum(v => v.BoundingBox.Width);
indentFromLeft -= Math.Max(widthUTC, widthLocal);
}
@@ -930,7 +930,7 @@
{
point = new PdfPoint(indentFromLeft, point.Y);
var letters = page.AddText(stampTextUTC, 7, point, courierFont);
var maxHeight = letters.Max(v => v.GlyphRectangle.Height);
var maxHeight = letters.Max(v => v.BoundingBox.Height);
point = new PdfPoint(indentFromLeft, point.Y - maxHeight * 1.2);
}

View File

@@ -328,9 +328,9 @@
Assert.Equal(readerLetter.Value, writerLetter.Value);
Assert.Equal(readerLetter.Location, writerLetter.Location, pointComparer);
Assert.Equal(readerLetter.FontSize, writerLetter.FontSize, comparer);
Assert.Equal(readerLetter.GlyphRectangle.Width, writerLetter.GlyphRectangle.Width, comparer);
Assert.Equal(readerLetter.GlyphRectangle.Height, writerLetter.GlyphRectangle.Height, comparer);
Assert.Equal(readerLetter.GlyphRectangle.BottomLeft, writerLetter.GlyphRectangle.BottomLeft, pointComparer);
Assert.Equal(readerLetter.BoundingBox.Width, writerLetter.BoundingBox.Width, comparer);
Assert.Equal(readerLetter.BoundingBox.Height, writerLetter.BoundingBox.Height, comparer);
Assert.Equal(readerLetter.BoundingBox.BottomLeft, writerLetter.BoundingBox.BottomLeft, pointComparer);
}
}
}
@@ -471,9 +471,9 @@
Assert.Equal(readerLetter.Value, writerLetter.Value);
Assert.Equal(readerLetter.Location, writerLetter.Location, pointComparer);
Assert.Equal(readerLetter.FontSize, writerLetter.FontSize, comparer);
Assert.Equal(readerLetter.GlyphRectangle.Width, writerLetter.GlyphRectangle.Width, comparer);
Assert.Equal(readerLetter.GlyphRectangle.Height, writerLetter.GlyphRectangle.Height, comparer);
Assert.Equal(readerLetter.GlyphRectangle.BottomLeft, writerLetter.GlyphRectangle.BottomLeft, pointComparer);
Assert.Equal(readerLetter.BoundingBox.Width, writerLetter.BoundingBox.Width, comparer);
Assert.Equal(readerLetter.BoundingBox.Height, writerLetter.BoundingBox.Height, comparer);
Assert.Equal(readerLetter.BoundingBox.BottomLeft, writerLetter.BoundingBox.BottomLeft, pointComparer);
}
}
}
@@ -513,13 +513,13 @@
var topLine = new PdfPoint(30, page1.PageSize.Height - 60);
var letters = page1.AddText("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor", 9, topLine, font);
page1.AddText("incididunt ut labore et dolore magna aliqua.", 9, new PdfPoint(30, topLine.Y - letters.Max(x => x.GlyphRectangle.Height) - 5), font);
page1.AddText("incididunt ut labore et dolore magna aliqua.", 9, new PdfPoint(30, topLine.Y - letters.Max(x => x.BoundingBox.Height) - 5), font);
var page2Letters = page2.AddText("The very hungry caterpillar ate all the apples in the garden.", 12, topLine, font);
var left = page2Letters[0].GlyphRectangle.Left;
var bottom = page2Letters.Min(x => x.GlyphRectangle.Bottom);
var right = page2Letters[page2Letters.Count - 1].GlyphRectangle.Right;
var top = page2Letters.Max(x => x.GlyphRectangle.Top);
var left = page2Letters[0].BoundingBox.Left;
var bottom = page2Letters.Min(x => x.BoundingBox.Bottom);
var right = page2Letters[page2Letters.Count - 1].BoundingBox.Right;
var top = page2Letters.Max(x => x.BoundingBox.Top);
page2.SetStrokeColor(10, 250, 69);
page2.DrawRectangle(new PdfPoint(left, bottom), right - left, top - bottom);
@@ -591,8 +591,8 @@
Assert.NotNull(image);
Assert.Equal(expectedBounds.BottomLeft, image.Bounds.BottomLeft);
Assert.Equal(expectedBounds.TopRight, image.Bounds.TopRight);
Assert.Equal(expectedBounds.BottomLeft, image.BoundingBox.BottomLeft);
Assert.Equal(expectedBounds.TopRight, image.BoundingBox.TopRight);
Assert.Equal(imageBytes, image.RawMemory.ToArray());
}
@@ -637,10 +637,10 @@
Assert.Equal(2, page1Images.Count);
var image1 = page1Images[0];
Assert.Equal(expectedBounds1, image1.Bounds);
Assert.Equal(expectedBounds1, image1.BoundingBox);
var image2 = page1Images[1];
Assert.Equal(expectedBounds2, image2.Bounds);
Assert.Equal(expectedBounds2, image2.BoundingBox);
var page2Doc = document.GetPage(2);
@@ -648,7 +648,7 @@
Assert.NotNull(image3);
Assert.Equal(expectedBounds3, image3.Bounds);
Assert.Equal(expectedBounds3, image3.BoundingBox);
Assert.Equal(imageBytes, image1.RawMemory.ToArray());
Assert.Equal(imageBytes, image2.RawMemory.ToArray());
@@ -724,8 +724,8 @@
Assert.NotNull(image);
Assert.Equal(expectedBounds.BottomLeft, image.Bounds.BottomLeft);
Assert.Equal(expectedBounds.TopRight, image.Bounds.TopRight);
Assert.Equal(expectedBounds.BottomLeft, image.BoundingBox.BottomLeft);
Assert.Equal(expectedBounds.TopRight, image.BoundingBox.TopRight);
Assert.True(image.TryGetPng(out var png));
Assert.NotNull(png);
@@ -927,9 +927,9 @@
Assert.Equal(readerLetter.Value, writerLetter.Value);
Assert.Equal(readerLetter.Location, writerLetter.Location, pointComparer);
Assert.Equal(readerLetter.FontSize, writerLetter.FontSize, comparer);
Assert.Equal(readerLetter.GlyphRectangle.Width, writerLetter.GlyphRectangle.Width, comparer);
Assert.Equal(readerLetter.GlyphRectangle.Height, writerLetter.GlyphRectangle.Height, comparer);
Assert.Equal(readerLetter.GlyphRectangle.BottomLeft, writerLetter.GlyphRectangle.BottomLeft, pointComparer);
Assert.Equal(readerLetter.BoundingBox.Width, writerLetter.BoundingBox.Width, comparer);
Assert.Equal(readerLetter.BoundingBox.Height, writerLetter.BoundingBox.Height, comparer);
Assert.Equal(readerLetter.BoundingBox.BottomLeft, writerLetter.BoundingBox.BottomLeft, pointComparer);
}
}
}

View File

@@ -9,13 +9,15 @@
{
private readonly bool usePdfDocEncoding;
private readonly StackDepthGuard stackDepthGuard;
private readonly bool useLenientParsing;
public bool ReadsNextByte { get; } = false;
public ArrayTokenizer(bool usePdfDocEncoding, StackDepthGuard stackDepthGuard)
public ArrayTokenizer(bool usePdfDocEncoding, StackDepthGuard stackDepthGuard, bool useLenientParsing)
{
this.usePdfDocEncoding = usePdfDocEncoding;
this.stackDepthGuard = stackDepthGuard;
this.useLenientParsing = useLenientParsing;
}
public bool TryTokenize(byte currentByte, IInputBytes inputBytes, out IToken token)
@@ -27,7 +29,7 @@
return false;
}
var scanner = new CoreTokenScanner(inputBytes, usePdfDocEncoding, stackDepthGuard, ScannerScope.Array);
var scanner = new CoreTokenScanner(inputBytes, usePdfDocEncoding, stackDepthGuard, ScannerScope.Array, useLenientParsing: useLenientParsing);
var contents = new List<IToken>();

View File

@@ -70,7 +70,7 @@
this.usePdfDocEncoding = usePdfDocEncoding;
this.stackDepthGuard = stackDepthGuard;
this.stringTokenizer = new StringTokenizer(usePdfDocEncoding);
this.arrayTokenizer = new ArrayTokenizer(usePdfDocEncoding, this.stackDepthGuard);
this.arrayTokenizer = new ArrayTokenizer(usePdfDocEncoding, this.stackDepthGuard, useLenientParsing);
this.dictionaryTokenizer = new DictionaryTokenizer(usePdfDocEncoding, this.stackDepthGuard, useLenientParsing: useLenientParsing);
this.scope = scope;
this.namedDictionaryRequiredKeys = namedDictionaryRequiredKeys;

View File

@@ -12,7 +12,7 @@
/// <summary>
/// The offset to the start of the object number from the start of the file in bytes.
/// </summary>
public long Position { get; }
public XrefLocation Position { get; }
/// <summary>
/// The object and generation number of the object.
@@ -30,7 +30,7 @@
/// <param name="position">The offset in bytes from the start of the file for this object.</param>
/// <param name="number">The identifier for this object.</param>
/// <param name="data">The data contained in this object.</param>
public ObjectToken(long position, IndirectReference number, IToken data)
public ObjectToken(XrefLocation position, IndirectReference number, IToken data)
{
Position = position;
Number = number;

View File

@@ -28,12 +28,12 @@
private readonly IPdfTokenScanner tokenScanner;
private readonly ILookupFilterProvider filterProvider;
private readonly IReadOnlyDictionary<IndirectReference, long> objectOffsets;
private readonly IReadOnlyDictionary<IndirectReference, XrefLocation> objectOffsets;
public AcroFormFactory(
IPdfTokenScanner tokenScanner,
ILookupFilterProvider filterProvider,
IReadOnlyDictionary<IndirectReference, long> objectOffsets)
IReadOnlyDictionary<IndirectReference, XrefLocation> objectOffsets)
{
this.tokenScanner = tokenScanner ?? throw new ArgumentNullException(nameof(tokenScanner));
this.filterProvider = filterProvider ?? throw new ArgumentNullException(nameof(filterProvider));

View File

@@ -0,0 +1,15 @@
namespace UglyToad.PdfPig.Content
{
using UglyToad.PdfPig.Core;
/// <summary>
/// Interface for classes with a bounding box
/// </summary>
public interface IBoundingBox
{
/// <summary>
/// Gets the Bounding Box: The rectangle completely containing this object
/// </summary>
PdfRectangle BoundingBox { get; }
}
}

View File

@@ -12,11 +12,13 @@
/// <summary>
/// An image in a PDF document, may be an <see cref="InlineImage"/> or a PostScript image XObject (<see cref="XObjectImage"/>).
/// </summary>
public interface IPdfImage
public interface IPdfImage: IBoundingBox
{
/// <summary>
/// The placement rectangle of the image in PDF coordinates.
/// Kept so not major breaking API change. Use instead <see cref="IBoundingBox.BoundingBox"/>
/// </summary>
[Obsolete("Use BoundingBox instead.")]
PdfRectangle Bounds { get; }
/// <summary>

View File

@@ -19,7 +19,11 @@
private readonly Lazy<Memory<byte>>? memoryFactory;
/// <inheritdoc />
public PdfRectangle Bounds { get; }
public PdfRectangle BoundingBox { get; }
/// <inheritdoc />
[Obsolete("Use BoundingBox instead.")]
public PdfRectangle Bounds => BoundingBox;
/// <inheritdoc />
public int WidthInSamples { get; }
@@ -79,7 +83,7 @@
IPdfImage? softMaskImage)
{
IsInlineImage = true;
Bounds = bounds;
BoundingBox = bounds;
WidthInSamples = widthInSamples;
HeightInSamples = heightInSamples;
Decode = decode;
@@ -138,7 +142,7 @@
/// <inheritdoc />
public override string ToString()
{
return $"Inline Image (w {Bounds.Width}, h {Bounds.Height})";
return $"Inline Image (w {BoundingBox.Width}, h {BoundingBox.Height})";
}
}
}

View File

@@ -7,7 +7,7 @@
/// <summary>
/// A glyph or combination of glyphs (characters) drawn by a PDF content stream.
/// </summary>
public class Letter
public class Letter:IBoundingBox
{
/// <summary>
/// The text for this letter or unicode character.
@@ -44,10 +44,16 @@
/// For example letters with descenders, p, j, etc., will have a box extending below the <see cref="Location"/> they are placed at.
/// The width of the glyph may also be more or less than the <see cref="Width"/> allocated for the character in the PDF content.
/// </summary>
public PdfRectangle GlyphRectangle { get; }
public PdfRectangle BoundingBox { get; }
/// <summary>
/// Gets the Bounding Box: The rectangle completely containing this object.
/// </summary>
[Obsolete("Use BoundingBox instead.")]
public PdfRectangle GlyphRectangle => BoundingBox;
/// <summary>
/// The loose bounding box for the glyph. Contrary to the <see cref="GlyphRectangle"/>, the loose bounding box will be the same across all glyphes of the same font.
/// The loose bounding box for the glyph. Contrary to the <see cref="BoundingBox"/>, the loose bounding box will be the same across all glyphes of the same font.
/// It takes in account the font Ascent and Descent.
/// </summary>
public PdfRectangle GlyphRectangleLoose { get; }
@@ -173,7 +179,7 @@
int textSequence)
{
Value = value;
GlyphRectangle = glyphRectangle;
BoundingBox = glyphRectangle;
GlyphRectangleLoose = glyphRectangleLoose;
StartBaseLine = startBaseLine;
EndBaseLine = endBaseLine;
@@ -207,7 +213,7 @@
public Letter AsBold()
{
return new Letter(Value,
GlyphRectangle,
BoundingBox,
GlyphRectangleLoose,
StartBaseLine,
EndBaseLine,
@@ -273,7 +279,7 @@
private TextOrientation GetTextOrientationRot()
{
double rotation = GlyphRectangle.Rotation;
double rotation = BoundingBox.Rotation;
if (Math.Abs(rotation % 90) >= 10e-5)
{
return TextOrientation.Other;

View File

@@ -9,7 +9,7 @@
/// <summary>
/// A word.
/// </summary>
public class Word
public class Word : IBoundingBox
{
/// <summary>
/// The text of the word.
@@ -107,15 +107,15 @@
blY = letter.StartBaseLine.Y;
}
var right = letter.StartBaseLine.X + Math.Max(letter.Width, letter.GlyphRectangle.Width);
var right = letter.StartBaseLine.X + Math.Max(letter.Width, letter.BoundingBox.Width);
if (right > trX)
{
trX = right;
}
if (letter.GlyphRectangle.TopLeft.Y > trY)
if (letter.BoundingBox.TopLeft.Y > trY)
{
trY = letter.GlyphRectangle.TopLeft.Y;
trY = letter.BoundingBox.TopLeft.Y;
}
}
@@ -146,15 +146,15 @@
blY = letter.StartBaseLine.Y;
}
var right = letter.StartBaseLine.X - Math.Max(letter.Width, letter.GlyphRectangle.Width);
var right = letter.StartBaseLine.X - Math.Max(letter.Width, letter.BoundingBox.Width);
if (right < trX)
{
trX = right;
}
if (letter.GlyphRectangle.TopRight.Y < trY)
if (letter.BoundingBox.TopRight.Y < trY)
{
trY = letter.GlyphRectangle.TopRight.Y;
trY = letter.BoundingBox.TopRight.Y;
}
}
@@ -185,15 +185,15 @@
r = letter.EndBaseLine.Y;
}
var right = letter.StartBaseLine.X + letter.GlyphRectangle.Height;
var right = letter.StartBaseLine.X + letter.BoundingBox.Height;
if (right > t)
{
t = right;
}
if (letter.GlyphRectangle.BottomLeft.Y > l)
if (letter.BoundingBox.BottomLeft.Y > l)
{
l = letter.GlyphRectangle.BottomLeft.Y;
l = letter.BoundingBox.BottomLeft.Y;
}
}
@@ -226,15 +226,15 @@
l = letter.StartBaseLine.Y;
}
var right = letter.StartBaseLine.X - letter.GlyphRectangle.Height;
var right = letter.StartBaseLine.X - letter.BoundingBox.Height;
if (right < t)
{
t = right;
}
if (letter.GlyphRectangle.BottomRight.Y > r)
if (letter.BoundingBox.BottomRight.Y > r)
{
r = letter.GlyphRectangle.BottomRight.Y;
r = letter.BoundingBox.BottomRight.Y;
}
}
@@ -253,7 +253,7 @@
if (letters.Count == 1)
{
return new(builder.ToString(), letters[0].GlyphRectangle);
return new(builder.ToString(), letters[0].BoundingBox);
}
else
{
@@ -299,8 +299,8 @@
{
r.StartBaseLine,
r.EndBaseLine,
r.GlyphRectangle.TopLeft,
r.GlyphRectangle.TopRight
r.BoundingBox.TopLeft,
r.BoundingBox.TopRight
}).Distinct().Select(p => inverseRotation.Transform(p));
var aabb = new PdfRectangle(transformedPoints.Min(p => p.X),

View File

@@ -2,11 +2,10 @@
{
using Fonts;
using System;
using System.Buffers.Binary;
using System.IO;
using System.IO.Compression;
using Tokens;
using UglyToad.PdfPig.Core;
using Core;
using Util;
/// <summary>
@@ -55,89 +54,41 @@
return input;
}
private static Memory<byte> Decompress(Memory<byte> input, int predictor, int colors, int bitsPerComponent, int columns)
private static Memory<byte> Decompress(Memory<byte> input,
int predictor,
int colors,
int bitsPerComponent,
int columns)
{
#if NET
using var memoryStream = MemoryHelper.AsReadOnlyMemoryStream(input);
// The first 2 bytes are the header which DeflateStream does not support.
memoryStream.ReadByte();
memoryStream.ReadByte();
try
{
using (var zlib = new ZLibStream(memoryStream, CompressionMode.Decompress))
using (var output = new MemoryStream((int)(input.Length * 1.5)))
using (var f = PngPredictor.WrapPredictor(output, predictor, colors, bitsPerComponent, columns))
{
zlib.CopyTo(f);
f.Flush();
using var deflate = new DeflateStream(memoryStream, CompressionMode.Decompress);
using var output = new MemoryStream((int)(input.Length * 1.5));
using var f = PngPredictor.WrapPredictor(output, predictor, colors, bitsPerComponent, columns);
deflate.CopyTo(f);
f.Flush();
return output.AsMemory();
}
return output.AsMemory();
}
catch (InvalidDataException ex)
{
throw new CorruptCompressedDataException("Invalid Flate compressed stream encountered", ex);
}
#else
// Ideally we would like to use the ZLibStream class but that is only available in .NET 5+.
// We look at the raw data now
// * First we have 2 bytes, specifying the type of compression
// * Then we have the deflated data
// * Then we have a 4 byte checksum (Adler32)
// Would be so nice to have zlib do the framing here... but the deflate stream already reads data from the stream that we need.
using var memoryStream = MemoryHelper.AsReadOnlyMemoryStream(input.Slice(2, input.Length - 2 /* Header */ - 4 /* Checksum */));
// The first 2 bytes are the header which DeflateStream can't handle. After the s
var adlerBytes = input.Slice(input.Length - 4, 4).Span;
uint expected = BinaryPrimitives.ReadUInt32BigEndian(adlerBytes);
uint altExpected = expected;
// Sometimes the data ends with "\r\n", "\r" or "\n" and we don't know if it is part of the zlib
// Ideally this would have been removed by the caller from the provided length...
if (adlerBytes[3] == '\n' || adlerBytes[3] == '\r')
{
if (adlerBytes[3] == '\n' && adlerBytes[2] == '\r')
{
// Now we don't know which value is the good one. The value could be ok, or padding.
// Lets allow both values for now. Allowing two out of 2^32 is much better than allowing everything
adlerBytes = input.Slice(input.Length - 6, 4).Span;
}
else
{
// Same but now for just '\n' or '\r' instead of '\r\n'
adlerBytes = input.Slice(input.Length - 5, 4).Span;
}
altExpected = BinaryPrimitives.ReadUInt32BigEndian(adlerBytes);
}
try
{
using (var deflate = new DeflateStream(memoryStream, CompressionMode.Decompress))
using (var adlerStream = new Adler32ChecksumStream(deflate))
using (var output = new MemoryStream((int)(input.Length * 1.5)))
using (var f = PngPredictor.WrapPredictor(output, predictor, colors, bitsPerComponent, columns))
{
adlerStream.CopyTo(f);
f.Flush();
uint actual = adlerStream.Checksum;
if (expected != actual && altExpected != actual)
{
throw new CorruptCompressedDataException("Flate stream has invalid checksum");
}
return output.AsMemory();
}
}
catch (InvalidDataException ex)
{
throw new CorruptCompressedDataException("Invalid Flate compressed stream encountered", ex);
}
#endif
}
/// <inheritdoc />
public byte[] Encode(Stream input, DictionaryToken streamDictionary, int index)
/// <summary>
/// Convert a decoded data stream back to the encoded version.
/// </summary>
/// <param name="input">The decoded data.</param>
/// <param name="streamDictionary">The stream dictionary with the parameters to use.</param>
/// <returns>The Flate encoded data.</returns>
public byte[] Encode(Stream input, DictionaryToken streamDictionary)
{
const int headerLength = 2;
const int checksumLength = 4;

View File

@@ -222,7 +222,31 @@
throw new ArgumentException("MinimumAreaRectangle(): points cannot be null and must contain at least one point.", nameof(points));
}
return ParametricPerpendicularProjection(GrahamScan(points.Distinct()).ToArray());
var distinctSet = new HashSet<PdfPoint>(points);
var distinctPoints = new PdfPoint[distinctSet.Count];
distinctSet.CopyTo(distinctPoints);
var hull = GrahamScan(distinctPoints);
PdfPoint[] hullArray;
if (hull is PdfPoint[] arr)
{
hullArray = arr;
}
else if (hull is List<PdfPoint> list)
{
#if NET6_0_OR_GREATER
return ParametricPerpendicularProjection(System.Runtime.InteropServices.CollectionsMarshal.AsSpan(list));
#else
hullArray = list.ToArray();
#endif
}
else
{
hullArray = hull.ToArray();
}
return ParametricPerpendicularProjection(hullArray);
}
/// <summary>
@@ -238,9 +262,16 @@
}
// Fitting a line through the points
// to find the orientation (slope)
double x0 = points.Average(p => p.X);
double y0 = points.Average(p => p.Y);
double sumX = 0;
double sumY = 0;
for (int i = 0; i < points.Count; i++)
{
sumX += points[i].X;
sumY += points[i].Y;
}
double x0 = sumX / points.Count;
double y0 = sumY / points.Count;
double sumProduct = 0;
double sumDiffSquaredX = 0;
@@ -265,11 +296,17 @@
sin, cos, 0,
0, 0, 1);
var transformedPoints = points.Select(p => inverseRotation.Transform(p)).ToArray();
var aabb = new PdfRectangle(transformedPoints.Min(p => p.X),
transformedPoints.Min(p => p.Y),
transformedPoints.Max(p => p.X),
transformedPoints.Max(p => p.Y));
var first = inverseRotation.Transform(points[0]);
double minX = first.X, minY = first.Y, maxX = first.X, maxY = first.Y;
for (int i = 1; i < points.Count; i++)
{
var tp = inverseRotation.Transform(points[i]);
if (tp.X < minX) minX = tp.X;
if (tp.Y < minY) minY = tp.Y;
if (tp.X > maxX) maxX = tp.X;
if (tp.Y > maxY) maxY = tp.Y;
}
var aabb = new PdfRectangle(minX, minY, maxX, maxY);
// Rotate back the AABB to obtain to oriented bounding box (OBB)
var rotateBack = new TransformationMatrix(
@@ -286,18 +323,7 @@
{
return GrahamScan(points.ToArray());
}
private sealed class PdfPointXYComparer : IComparer<PdfPoint>
{
public static readonly PdfPointXYComparer Instance = new();
public int Compare(PdfPoint p1, PdfPoint p2)
{
int comp = p1.X.CompareTo(p2.X);
return comp == 0 ? p1.Y.CompareTo(p2.Y) : comp;
}
}
/// <summary>
/// Algorithm to find the convex hull of the set of points with time complexity O(n log n).
/// </summary>
@@ -320,33 +346,40 @@
return Math.Atan2(point2.Y - point1.Y, point2.X - point1.X) % Math.PI;
}
Array.Sort(points, PdfPointXYComparer.Instance);
Array.Sort(points, (p1, p2) =>
{
int comp = p1.X.CompareTo(p2.X);
return comp == 0 ? p1.Y.CompareTo(p2.Y) : comp;
});
var P0 = points[0];
var groups = points.Skip(1).GroupBy(p => polarAngle(P0, p)).OrderBy(g => g.Key).ToArray();
var sortedPoints = ArrayPool<PdfPoint>.Shared.Rent(groups.Length);
PdfPoint[]? buffer = null;
var sortedPoints = groups.Length <= 64
? stackalloc PdfPoint[groups.Length]
: buffer = ArrayPool<PdfPoint>.Shared.Rent(groups.Length);
try
{
for (int i = 0; i < groups.Length; i++)
{
var group = groups[i];
if (group.Count() == 1)
PdfPoint farthest = default;
double maxDistSq = -1;
foreach (var p in group)
{
sortedPoints[i] = group.First();
}
else
{
// if more than one point has the same angle,
// remove all but the one that is farthest from P0
sortedPoints[i] = group.OrderByDescending(p =>
double dx = p.X - P0.X;
double dy = p.Y - P0.Y;
double distSq = dx * dx + dy * dy;
if (distSq > maxDistSq)
{
double dx = p.X - P0.X;
double dy = p.Y - P0.Y;
return dx * dx + dy * dy;
}).First();
maxDistSq = distSq;
farthest = p;
}
}
sortedPoints[i] = farthest;
}
if (groups.Length < 2)
@@ -354,27 +387,32 @@
return [P0, sortedPoints[0]];
}
var stack = new Stack<PdfPoint>();
stack.Push(P0);
stack.Push(sortedPoints[0]);
stack.Push(sortedPoints[1]);
var hull = new List<PdfPoint>(groups.Length + 1);
hull.Add(P0);
hull.Add(sortedPoints[0]);
hull.Add(sortedPoints[1]);
for (int i = 2; i < groups.Length; i++)
for (int i = 2; i < groups.Length; ++i)
{
var point = sortedPoints[i];
while (stack.Count > 1 && !ccw(stack.ElementAt(1), stack.Peek(), point))
while (hull.Count > 1 && !ccw(hull[hull.Count - 2], hull[hull.Count - 1], point))
{
stack.Pop();
hull.RemoveAt(hull.Count - 1);
}
stack.Push(point);
hull.Add(point);
}
return stack;
hull.Reverse();
return hull;
}
finally
{
ArrayPool<PdfPoint>.Shared.Return(sortedPoints);
if (buffer is not null)
{
ArrayPool<PdfPoint>.Shared.Return(buffer);
}
}
}

View File

@@ -215,6 +215,8 @@
/// <inheritdoc/>
public void ShowText(IInputBytes bytes)
{
TextSequence++;
var currentState = GetCurrentState();
var font = currentState.FontState.FromExtendedGraphicsState
@@ -585,7 +587,8 @@
if (hasCircularReference)
{
if (ParsingOptions.UseLenientParsing)
{
{
// TODO - We might be removing too much, good for the moment. See Issues1250() for examples
operations = operations.Where(o => o is not InvokeNamedXObject xo || xo.Name != xObjectName)
.ToArray();
ParsingOptions.Logger.Warn(
@@ -616,14 +619,56 @@
/// <param name="xObjectName">The form's name.</param>
/// <param name="operations">The form operations parsed from original form stream.</param>
protected virtual bool HasFormXObjectCircularReference(StreamToken formStream,
NameToken xObjectName,
NameToken? xObjectName,
IReadOnlyList<IGraphicsStateOperation> operations)
{
return xObjectName != null
&& operations.OfType<InvokeNamedXObject>()?.Any(o => o.Name == xObjectName) ==
true // operations contain another form with same name
&& ResourceStore.TryGetXObject(xObjectName, out var result)
&& result.Data.Span.SequenceEqual(formStream.Data.Span); // The form contained in the operations has identical data to current form
if (xObjectName is null)
{
return false;
}
if (operations.OfType<InvokeNamedXObject>()?.Any(o => o.Name == xObjectName) != true)
{
return false;
}
if (!TryGetXObjectToken(formStream, xObjectName, PdfScanner, out var t1))
{
return false;
}
if (!ResourceStore.TryGetXObject(xObjectName, out var resourceStream))
{
return false;
}
if (!TryGetXObjectToken(resourceStream, xObjectName, PdfScanner, out var t2))
{
return false;
}
if (t1 is null || t2 is null)
{
return false;
}
return t1.Equals(t2);
static bool TryGetXObjectToken(StreamToken streamToken, NameToken xObjectName, IPdfTokenScanner scanner, out IToken? token)
{
token = null;
if (!streamToken.StreamDictionary.TryGet<DictionaryToken>(NameToken.Resources, scanner, out var formResources))
{
return false;
}
if (!formResources.TryGet<DictionaryToken>(NameToken.Xobject, out var xObjectBase) || !xObjectBase.TryGet(xObjectName, out token))
{
return false;
}
return token is not null;
}
}
/// <inheritdoc/>

View File

@@ -122,7 +122,7 @@ namespace UglyToad.PdfPig.Graphics
letter = new Letter(
newLetter,
attachTo.GlyphRectangle,
attachTo.BoundingBox,
attachTo.GlyphRectangleLoose,
attachTo.StartBaseLine,
attachTo.EndBaseLine,

View File

@@ -64,7 +64,7 @@
operationContext.ShowText(input);
}
string? EscapeText(string? text)
private static string? EscapeText(string? text)
{
if (text is null) return null;
// Fix Issue 350 from PDF Spec 1.7 (page 408) on handling 'special characters' of '(', ')' and '\'.

View File

@@ -16,7 +16,7 @@ internal static partial class FirstPassParser
{
log ??= new NoOpLog();
IReadOnlyDictionary<IndirectReference, long>? bruteForceOffsets = null;
IReadOnlyDictionary<IndirectReference, XrefLocation>? bruteForceOffsets = null;
var didBruteForce = false;
DictionaryToken? bruteForceTrailer = null;
@@ -92,7 +92,7 @@ internal static partial class FirstPassParser
}
DictionaryToken? lastTrailer = null;
var flattenedOffsets = new Dictionary<IndirectReference, long>();
var flattenedOffsets = new Dictionary<IndirectReference, XrefLocation>();
foreach (var xrefPart in orderedXrefs)
{
if (xrefPart.Dictionary != null)
@@ -230,12 +230,12 @@ internal class FirstPassResults
/// <summary>
/// All offsets found if a brute-force search was applied.
/// </summary>
public IReadOnlyDictionary<IndirectReference, long>? BruteForceOffsets { get; }
public IReadOnlyDictionary<IndirectReference, XrefLocation>? BruteForceOffsets { get; }
/// <summary>
/// All offsets found from the leaf xref.
/// </summary>
public IReadOnlyDictionary<IndirectReference, long> XrefOffsets { get; }
public IReadOnlyDictionary<IndirectReference, XrefLocation> XrefOffsets { get; }
/// <summary>
/// The trailer dictionary of the leaf xref if we found any.
@@ -244,8 +244,8 @@ internal class FirstPassResults
public FirstPassResults(
IReadOnlyList<IXrefSection> parts,
IReadOnlyDictionary<IndirectReference, long>? bruteForceOffsets,
IReadOnlyDictionary<IndirectReference, long> xrefOffsets,
IReadOnlyDictionary<IndirectReference, XrefLocation>? bruteForceOffsets,
IReadOnlyDictionary<IndirectReference, XrefLocation> xrefOffsets,
DictionaryToken? trailer)
{
Parts = parts;

View File

@@ -14,7 +14,7 @@ internal interface IXrefSection
/// <summary>
/// The bytes offsets of the objects in this xref.
/// </summary>
public IReadOnlyDictionary<IndirectReference, long> ObjectOffsets { get; }
public IReadOnlyDictionary<IndirectReference, XrefLocation> ObjectOffsets { get; }
/// <summary>
/// The dictionary for this xref, for the trailer xref this is the trailer dictionary, for streams the stream dictionary.

View File

@@ -19,7 +19,7 @@ internal static class XrefBruteForcer
// Guard against circular references; only read xref at each offset once
var xrefOffsetSeen = new HashSet<long>();
var bruteForceObjPositions = new Dictionary<IndirectReference, long>();
var bruteForceObjPositions = new Dictionary<IndirectReference, XrefLocation>();
DictionaryToken? trailer = null;
@@ -123,7 +123,7 @@ internal static class XrefBruteForcer
if (buffer.EndsWith(" obj") && numericsQueue[0] > 0)
{
bruteForceObjPositions[new IndirectReference(numericsQueue[0], (int)numericsQueue[1])] = positionsQueue[0];
bruteForceObjPositions[new IndirectReference(numericsQueue[0], (int)numericsQueue[1])] = XrefLocation.File(positionsQueue[0]);
lastObjPosition = positionsQueue[0];
@@ -208,12 +208,12 @@ internal static class XrefBruteForcer
public class Result(
IReadOnlyList<IXrefSection> xRefParts,
IReadOnlyDictionary<IndirectReference, long> objectOffsets,
IReadOnlyDictionary<IndirectReference, XrefLocation> objectOffsets,
DictionaryToken? lastTrailer)
{
public IReadOnlyList<IXrefSection> XRefParts { get; } = xRefParts;
public IReadOnlyDictionary<IndirectReference, long> ObjectOffsets { get; } = objectOffsets;
public IReadOnlyDictionary<IndirectReference, XrefLocation> ObjectOffsets { get; } = objectOffsets;
public DictionaryToken? LastTrailer { get; } = lastTrailer;
}

View File

@@ -10,7 +10,7 @@ internal sealed class XrefStream : IXrefSection
/// <summary>
/// The corresponding byte offset for each keyed object in this document.
/// </summary>
public IReadOnlyDictionary<IndirectReference, long> ObjectOffsets { get; }
public IReadOnlyDictionary<IndirectReference, XrefLocation> ObjectOffsets { get; }
public DictionaryToken Dictionary { get; }
@@ -20,7 +20,7 @@ internal sealed class XrefStream : IXrefSection
public XrefStream(
long offset,
IReadOnlyDictionary<IndirectReference, long> objectOffsets,
IReadOnlyDictionary<IndirectReference, XrefLocation> objectOffsets,
DictionaryToken streamDictionary,
XrefOffsetCorrection correctionType,
long offsetCorrection)

View File

@@ -98,7 +98,7 @@ internal static class XrefStreamParser
? stackalloc byte[fieldSizes.LineLength]
: new byte[fieldSizes.LineLength];
var numbers = new List<(long obj, int gen, int off)>();
var numbers = new List<(long obj, int gen, XrefLocation location)>();
foreach (var objectNumber in objectNumbers)
{
@@ -136,7 +136,7 @@ internal static class XrefStreamParser
return new XrefStream(
xrefOffset,
numbers.ToDictionary(x => new IndirectReference(x.obj, x.gen), x => (long)x.off),
numbers.ToDictionary(x => new IndirectReference(x.obj, x.gen), x => x.location),
dictToken,
offsetCorrectionType,
offsetCorrection);
@@ -175,7 +175,7 @@ internal static class XrefStreamParser
int type,
long objectNumber,
XrefFieldSize fieldSizes,
List<(long, int, int)> results,
List<(long, int, XrefLocation)> results,
ReadOnlySpan<byte> lineBuffer)
{
switch (type)
@@ -184,19 +184,23 @@ internal static class XrefStreamParser
// Ignore free objects.
break;
case 1:
// Non object stream entries.
var offset = 0;
for (var i = 0; i < fieldSizes.Field2Size; i++)
{
offset += (lineBuffer[i + fieldSizes.Field1Size] & 0x00ff) << ((fieldSizes.Field2Size - i - 1) * 8);
}
var genNum = 0;
for (var i = 0; i < fieldSizes.Field3Size; i++)
{
genNum += (lineBuffer[i + fieldSizes.Field1Size + fieldSizes.Field2Size] & 0x00ff) << ((fieldSizes.Field3Size - i - 1) * 8);
var offset = ReadUnsigned(
lineBuffer,
fieldSizes.Field1Size,
fieldSizes.Field2Size);
var genNum = ReadUnsigned(
lineBuffer,
fieldSizes.Field1Size + fieldSizes.Field2Size,
fieldSizes.Field3Size);
if (offset < 0)
{
throw new PdfDocumentFormatException(
$"Location with negative offset {offset} found for object {objectNumber}");
}
results.Add((objectNumber, genNum, offset));
results.Add((objectNumber, (int)genNum, XrefLocation.File(offset)));
break;
case 2:
@@ -205,28 +209,49 @@ internal static class XrefStreamParser
* 2nd argument is object number of object stream
* 3rd argument is index of object within object stream
*
* For sequential PDFParser we do not need this information
* because
* These objects are handled by the dereferenceObjects() method
* since they're only pointing to object numbers
*
* However for XRef aware parsers we have to know which objects contain
* object streams. We will store this information in normal xref mapping
* table but add object stream number with minus sign in order to
* distinguish from file offsets
*/
var objstmObjNr = 0;
for (var i = 0; i < fieldSizes.Field2Size; i++)
var objectStreamNumber = ReadUnsigned(
lineBuffer,
fieldSizes.Field1Size,
fieldSizes.Field2Size);
var streamIndex = ReadUnsigned(
lineBuffer,
fieldSizes.Field1Size + fieldSizes.Field2Size,
fieldSizes.Field3Size);
if (objectStreamNumber < 0)
{
objstmObjNr += (lineBuffer[i + fieldSizes.Field1Size] & 0x00ff) << ((fieldSizes.Field2Size - i - 1) * 8);
throw new PdfDocumentFormatException(
$"Location with negative or zero object stream number {objectStreamNumber} found for object {objectNumber}");
}
results.Add((objectNumber, 0, -objstmObjNr));
if (streamIndex < 0)
{
throw new PdfDocumentFormatException(
$"Location with negative stream index {streamIndex} found for object {objectNumber} in stream {objectStreamNumber}");
}
results.Add((objectNumber, 0, XrefLocation.Stream(objectStreamNumber, (int)streamIndex)));
break;
}
}
private static long ReadUnsigned(ReadOnlySpan<byte> buffer, int start, int width)
{
long value = 0;
for (int i = 0; i < width; i++)
{
value <<= 8;
value |= buffer[start + i];
}
return value;
}
private static (long from, long? to) ReadStreamTolerant(IInputBytes bytes)
{
var buffer = new CircularByteBuffer("endstream ".Length);

View File

@@ -13,7 +13,7 @@ internal sealed class XrefTable : IXrefSection
/// <summary>
/// The corresponding byte offset for each keyed object in this document.
/// </summary>
public IReadOnlyDictionary<IndirectReference, long> ObjectOffsets { get; }
public IReadOnlyDictionary<IndirectReference, XrefLocation> ObjectOffsets { get; }
public DictionaryToken? Dictionary { get; }
@@ -23,7 +23,7 @@ internal sealed class XrefTable : IXrefSection
public XrefTable(
long offset,
IReadOnlyDictionary<IndirectReference, long> objectOffsets,
IReadOnlyDictionary<IndirectReference, XrefLocation> objectOffsets,
DictionaryToken? trailer,
XrefOffsetCorrection correctionType,
long offsetCorrection)

View File

@@ -152,7 +152,7 @@ internal static class XrefTableParser
}
}
var offsets = new Dictionary<IndirectReference, long>();
var offsets = new Dictionary<IndirectReference, XrefLocation>();
if (readNums.Count == 0)
{
if (trailer != null)
@@ -233,7 +233,7 @@ internal static class XrefTableParser
if (type == occupiedSentinel)
{
var indirectRef = new IndirectReference(objNum, (int)gen);
offsets[indirectRef] = objOffset;
offsets[indirectRef] = XrefLocation.File(objOffset);
}
objNum++;

View File

@@ -18,7 +18,7 @@
/// </summary>
/// <param name="bytes">The bytes of the document.</param>
/// <returns>The object keys and offsets for the objects in this document.</returns>
public static IReadOnlyDictionary<IndirectReference, long> GetObjectLocations(IInputBytes bytes)
public static IReadOnlyDictionary<IndirectReference, XrefLocation> GetObjectLocations(IInputBytes bytes)
{
if (bytes is null)
{
@@ -29,7 +29,7 @@
var lastEndOfFile = GetLastEndOfFileMarker(bytes);
var results = new Dictionary<IndirectReference, long>();
var results = new Dictionary<IndirectReference, XrefLocation>();
var generationBytes = new StringBuilder();
var objectNumberBytes = new StringBuilder();
@@ -174,7 +174,7 @@
var obj = long.Parse(objectNumberBytes.ToString(), CultureInfo.InvariantCulture);
var generation = int.Parse(generationBytes.ToString(), CultureInfo.InvariantCulture);
results[new IndirectReference(obj, generation)] = bytes.CurrentOffset;
results[new IndirectReference(obj, generation)] = XrefLocation.File(bytes.CurrentOffset);
generationBytes.Clear();
objectNumberBytes.Clear();

View File

@@ -6,9 +6,9 @@
internal interface IObjectLocationProvider
{
bool TryGetOffset(IndirectReference reference, out long offset);
bool TryGetOffset(IndirectReference reference, out XrefLocation offset);
void UpdateOffset(IndirectReference reference, long offset);
void UpdateOffset(IndirectReference reference, XrefLocation offset);
bool TryGetCached(IndirectReference reference, [NotNullWhen(true)] out ObjectToken? objectToken);

View File

@@ -13,16 +13,16 @@
private readonly IInputBytes bytes;
private IReadOnlyDictionary<IndirectReference, long>? bruteForcedOffsets;
private IReadOnlyDictionary<IndirectReference, XrefLocation>? bruteForcedOffsets;
private readonly Dictionary<IndirectReference, long> offsets;
private readonly Dictionary<IndirectReference, XrefLocation> offsets;
public ObjectLocationProvider(
IReadOnlyDictionary<IndirectReference, long> xrefOffsets,
IReadOnlyDictionary<IndirectReference, long>? bruteForcedOffsets,
IReadOnlyDictionary<IndirectReference, XrefLocation> xrefOffsets,
IReadOnlyDictionary<IndirectReference, XrefLocation>? bruteForcedOffsets,
IInputBytes bytes)
{
offsets = new Dictionary<IndirectReference, long>();
offsets = new Dictionary<IndirectReference, XrefLocation>();
foreach (var xrefOffset in xrefOffsets)
{
offsets[xrefOffset.Key] = xrefOffset.Value;
@@ -32,7 +32,7 @@
this.bytes = bytes;
}
public bool TryGetOffset(IndirectReference reference, out long offset)
public bool TryGetOffset(IndirectReference reference, out XrefLocation offset)
{
if (bruteForcedOffsets != null && bruteForcedOffsets.TryGetValue(reference, out var bfOffset))
{
@@ -42,16 +42,6 @@
if (offsets.TryGetValue(reference, out offset))
{
if (offset + reference.ObjectNumber == 0)
{
// We have a case where 'offset' and
// 'reference.ObjectNumber' have the same value
// and opposite signs.
// This results in an infinite recursion in
// PdfTokenScanner.GetObjectFromStream() where
// `var streamObjectNumber = offset * -1;`
throw new PdfDocumentFormatException("Avoiding infinite recursion in ObjectLocationProvider.TryGetOffset() as 'offset' and 'reference.ObjectNumber' have the same value and opposite signs.");
}
return true;
}
@@ -63,7 +53,7 @@
return bruteForcedOffsets.TryGetValue(reference, out offset);
}
public void UpdateOffset(IndirectReference reference, long offset)
public void UpdateOffset(IndirectReference reference, XrefLocation offset)
{
offsets[reference] = offset;
}
@@ -81,8 +71,9 @@
}
// Don't cache incorrect locations.
if (!force && offsets.TryGetValue(objectToken.Number, out var expected)
&& objectToken.Position != expected)
if (!force
&& offsets.TryGetValue(objectToken.Number, out var expected)
&& (objectToken.Position.Type != expected.Type || objectToken.Position.Value1 != expected.Value1))
{
return;
}

View File

@@ -1,5 +1,8 @@
namespace UglyToad.PdfPig.Tokenization.Scanner
{
using Core;
using Encryption;
using Filters;
using System;
using System.Collections.Generic;
using System.Diagnostics;
@@ -7,9 +10,6 @@
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using Core;
using Encryption;
using Filters;
using Tokens;
using UglyToad.PdfPig.Parser.FileStructure;
@@ -164,7 +164,7 @@
var actualReference = new IndirectReference(objectNumber.Int, generation.Int);
var actualToken = encryptionHandler.Decrypt(actualReference, readTokens[0]);
CurrentToken = new ObjectToken(startPosition, actualReference, actualToken);
CurrentToken = new ObjectToken(XrefLocation.File(startPosition), actualReference, actualToken);
readTokens.Clear();
coreTokenScanner.Seek(previousTokenPositions[0]);
@@ -191,7 +191,7 @@
var actualReference = new IndirectReference(objectNumber.Int, generation.Int);
var actualToken = encryptionHandler.Decrypt(actualReference, readTokens[0]);
CurrentToken = new ObjectToken(startPosition, actualReference, actualToken);
CurrentToken = new ObjectToken(XrefLocation.File(startPosition), actualReference, actualToken);
readTokens.Clear();
coreTokenScanner.Seek(previousTokenPositions[2]);
@@ -291,9 +291,9 @@
token = encryptionHandler.Decrypt(reference, token);
CurrentToken = new ObjectToken(startPosition, reference, token);
CurrentToken = new ObjectToken(XrefLocation.File(startPosition), reference, token);
objectLocationProvider.UpdateOffset(reference, startPosition);
objectLocationProvider.UpdateOffset(reference, XrefLocation.File(startPosition));
readTokens.Clear();
return true;
@@ -626,10 +626,10 @@
// We can only find it if we know where it is.
if (objectLocationProvider.TryGetOffset(lengthReference.Data, out var offset))
{
if (offset < 0)
if (offset.Type == XrefEntryType.ObjectStream)
{
ushort searchDepth = 0;
var result = GetObjectFromStream(lengthReference.Data, offset, ref searchDepth);
Span<int> stack = stackalloc int[7];
var result = GetObjectFromStream(lengthReference.Data, offset, stack, 0);
if (!(result.Data is NumericToken streamLengthToken))
{
@@ -639,8 +639,9 @@
return streamLengthToken.Long;
}
// Move to the length object and read it.
Seek(offset);
Seek(offset.Value1);
// Keep a copy of the read tokens here since this list must be empty prior to move next.
var oldData = new List<IToken>(readTokens);
@@ -721,19 +722,31 @@
public ObjectToken? Get(IndirectReference reference)
{
ushort searchDepth = 0;
return Get(reference, ref searchDepth);
Span<int> stack = stackalloc int[7];
return Get(reference, stack, 0);
}
private ObjectToken? Get(IndirectReference reference, ref ushort searchDepth)
private ObjectToken? Get(IndirectReference reference, Span<int> navSet, byte depth)
{
if (searchDepth > 100)
if (depth >= navSet.Length)
{
throw new PdfDocumentFormatException("Reached maximum search depth while getting indirect reference.");
var chain = string.Join(", ", navSet.ToArray());
throw new PdfDocumentFormatException($"Deep object chain detected when looking for {reference}: {chain}.");
}
searchDepth++;
// Cycle detection (linear scan, but depth is tiny)
for (var i = 0; i < depth; i++)
{
if (navSet[i] == reference.ObjectNumber)
{
var chain = string.Join(", ", navSet.ToArray());
throw new PdfDocumentFormatException(
$"Circular reference encountered when looking for object {reference}. Involved objects were: {chain}");
}
}
navSet[depth] = (int)reference.ObjectNumber;
depth++;
if (isDisposed)
{
@@ -756,20 +769,20 @@
}
// Negative offsets refer to a stream with that number.
if (offset < 0)
if (offset.Type == XrefEntryType.ObjectStream)
{
var result = GetObjectFromStream(reference, offset, ref searchDepth);
if (offset.Value1 == reference.ObjectNumber)
{
throw new PdfDocumentFormatException(
$"Object stream cannot contain itself, looking for object {reference} in {offset.Value1}");
}
var result = GetObjectFromStream(reference, offset, navSet, depth);
return result;
}
if (offset == 0 && reference.Generation > ushort.MaxValue)
{
// TODO - To remove as should not happen anymore
return new ObjectToken(offset, reference, NullToken.Instance);
}
Seek(offset);
Seek(offset.Value1);
if (!MoveNext())
{
@@ -793,7 +806,7 @@
{
// Using 0 position as it isn't written to stream and this value doesn't
// seem to be used by any callers. In future may need to revisit this.
overwrittenTokens[reference] = new ObjectToken(0, reference, token);
overwrittenTokens[reference] = new ObjectToken(XrefLocation.File(0), reference, token);
}
private bool TryBruteForceFileToFindReference(IndirectReference reference, [NotNullWhen(true)] out ObjectToken? result)
@@ -826,11 +839,11 @@
}
}
private ObjectToken GetObjectFromStream(IndirectReference reference, long offset, ref ushort searchDepth)
private ObjectToken GetObjectFromStream(IndirectReference reference, XrefLocation offset, Span<int> navSet, byte depth)
{
var streamObjectNumber = offset * -1;
var streamObjectNumber = offset.Value1;
var streamObject = Get(new IndirectReference(streamObjectNumber, 0), ref searchDepth);
var streamObject = Get(new IndirectReference(streamObjectNumber, 0), navSet, depth);
if (!(streamObject?.Data is StreamToken stream))
{
@@ -853,7 +866,7 @@
return result;
}
private IReadOnlyList<ObjectToken> ParseObjectStream(StreamToken stream, long offset)
private IReadOnlyList<ObjectToken> ParseObjectStream(StreamToken stream, XrefLocation offset)
{
if (!stream.StreamDictionary.TryGet(NameToken.N, out var numberToken)
|| !(numberToken is NumericToken numberOfObjects))

View File

@@ -72,7 +72,7 @@
continue;
}
var letterHeight = Math.Max(lastLetter.GlyphRectangle.Height, letter.GlyphRectangle.Height);
var letterHeight = Math.Max(lastLetter.BoundingBox.Height, letter.BoundingBox.Height);
var gap = letter.Location.X - (lastLetter.Location.X + lastLetter.Width);
var nextToLeft = letter.Location.X < lastX.Value - 1;

View File

@@ -15,7 +15,7 @@
var profileBytes = ProfileStreamReader.GetSRgb2014();
var compressedBytes = DataCompresser.CompressBytes(profileBytes);
var compressedBytes = DataCompressor.CompressBytes(profileBytes);
var profileStreamDictionary = new Dictionary<NameToken, IToken>
{

View File

@@ -6,7 +6,7 @@
using Filters;
using Tokens;
internal static class DataCompresser
internal static class DataCompressor
{
public static byte[] CompressBytes(IReadOnlyList<byte> bytes) => CompressBytes(bytes.ToArray());
public static byte[] CompressBytes(byte[] bytes)
@@ -15,7 +15,7 @@
{
var parameters = new DictionaryToken(new Dictionary<NameToken, IToken>());
var flater = new FlateFilter();
var result = flater.Encode(memoryStream, parameters, 0);
var result = flater.Encode(memoryStream, parameters);
return result;
}
}

View File

@@ -51,7 +51,7 @@
var newEncoding = new TrueTypeSubsetEncoding(characterMapping.Keys.ToList());
var subsetBytes = TrueTypeSubsetter.Subset(fontFileBytes.ToArray(), newEncoding);
var embeddedFile = DataCompresser.CompressToStream(subsetBytes);
var embeddedFile = DataCompressor.CompressToStream(subsetBytes);
var fileRef = writer.WriteToken(embeddedFile);
@@ -110,7 +110,7 @@
var descriptor = writer.WriteToken(new DictionaryToken(descriptorDictionary));
var toUnicodeCMap = ToUnicodeCMapBuilder.ConvertToCMapStream(characterMapping);
var toUnicodeStream = DataCompresser.CompressToStream(toUnicodeCMap);
var toUnicodeStream = DataCompressor.CompressToStream(toUnicodeCMap);
var toUnicode = writer.WriteToken(toUnicodeStream);
var dictionary = new Dictionary<NameToken, IToken>

View File

@@ -9,7 +9,7 @@ namespace UglyToad.PdfPig.Writer
/// Represents a link annotation that can be added to a PDF page.
/// Link annotations provide clickable areas that can trigger actions such as navigating to another page or opening a URL.
/// </summary>
internal sealed class LinkAnnotation
public sealed class LinkAnnotation
{
/// <summary>
/// Gets the border style for the link annotation.

View File

@@ -107,7 +107,7 @@ namespace UglyToad.PdfPig.Writer
}
outputStreamT.Seek(0, SeekOrigin.Begin);
var compressedBytes = DataCompresser.CompressBytes(outputStreamT.ToArray());
var compressedBytes = DataCompressor.CompressBytes(outputStreamT.ToArray());
var outputStreamDictionary = new Dictionary<NameToken, IToken>()
{
{ NameToken.Length, new NumericToken(compressedBytes.Length) },

View File

@@ -767,7 +767,7 @@
}
}
var compressedSmask = DataCompresser.CompressBytes(smaskData);
var compressedSmask = DataCompressor.CompressBytes(smaskData);
// Create a soft-mask.
var smaskDictionary = new Dictionary<NameToken, IToken>
@@ -786,7 +786,7 @@
smaskReference = documentBuilder.AddImage(new DictionaryToken(smaskDictionary), compressedSmask);
}
var compressed = DataCompresser.CompressBytes(data);
var compressed = DataCompressor.CompressBytes(data);
var imgDictionary = new Dictionary<NameToken, IToken>
{
@@ -855,7 +855,7 @@
/// </summary>
/// <param name="link">The link annotation to add</param>
/// <returns>This page builder for method chaining</returns>
private PdfPageBuilder AddLink(LinkAnnotation link)
public PdfPageBuilder AddLink(LinkAnnotation link)
{
links.Add((link.ToToken(), link.Action));
return this;
@@ -1218,7 +1218,7 @@
var bytes = memoryStream.ToArray();
var stream = DataCompresser.CompressToStream(bytes);
var stream = DataCompressor.CompressToStream(bytes);
return writer.WriteToken(stream);
}

View File

@@ -58,7 +58,7 @@
var ir = ReserveObjectNumber();
offsets.Add(ir.Data, Stream.Position);
var obj = new ObjectToken(Stream.Position, ir.Data, token);
var obj = new ObjectToken(XrefLocation.File(Stream.Position), ir.Data, token);
TokenWriter.WriteToken(obj, Stream);
return ir;
}
@@ -71,7 +71,7 @@
}
offsets.Add(indirectReference.Data, Stream.Position);
var obj = new ObjectToken(Stream.Position, indirectReference.Data, token);
var obj = new ObjectToken(XrefLocation.File(Stream.Position), indirectReference.Data, token);
TokenWriter.WriteToken(obj, Stream);
return indirectReference;
}
@@ -98,7 +98,6 @@
TokenWriter.WriteCrossReferenceTable(offsets, catalogReference.Data, Stream, documentInformationReference?.Data);
}
public void Dispose()
{
if (DisposeStream)

View File

@@ -19,7 +19,12 @@
private readonly Lazy<Memory<byte>>? memoryFactory;
/// <inheritdoc />
public PdfRectangle Bounds { get; }
public PdfRectangle BoundingBox { get; }
/// <inheritdoc />
[Obsolete("Use BoundingBox instead.")]
public PdfRectangle Bounds => BoundingBox;
/// <inheritdoc />
public int WidthInSamples { get; }
@@ -85,7 +90,7 @@
ColorSpaceDetails? colorSpaceDetails,
IPdfImage? softMaskImage)
{
Bounds = bounds;
BoundingBox = bounds;
WidthInSamples = widthInSamples;
HeightInSamples = heightInSamples;
BitsPerComponent = bitsPerComponent;
@@ -121,7 +126,7 @@
/// <inheritdoc />
public override string ToString()
{
return $"XObject Image (w {Bounds.Width}, h {Bounds.Height}): {ImageDictionary}";
return $"XObject Image (w {BoundingBox.Width}, h {BoundingBox.Height}): {ImageDictionary}";
}
}
}

View File

@@ -0,0 +1,71 @@
using BenchmarkDotNet.Attributes;
using UglyToad.PdfPig.Content;
namespace UglyToad.PdfPig.Benchmarks;
[Config(typeof(NuGetPackageConfig))]
[MemoryDiagnoser(displayGenColumns: false)]
public class BruteForceBenchmarks
{
[Benchmark]
public IReadOnlyList<Letter> OpenOffice()
{
List<Letter> letters = new List<Letter>();
using (var doc = PdfDocument.Open("Single Page Simple - from open office.pdf"))
{
foreach (var page in doc.GetPages())
{
letters.AddRange(page.Letters);
}
}
return letters;
}
[Benchmark]
public IReadOnlyList<Letter> Inkscape()
{
List<Letter> letters = new List<Letter>();
using (var doc = PdfDocument.Open("Single Page Simple - from inkscape.pdf"))
{
foreach (var page in doc.GetPages())
{
letters.AddRange(page.Letters);
}
}
return letters;
}
[Benchmark]
public IReadOnlyList<Letter> Algo()
{
List<Letter> letters = new List<Letter>();
using (var doc = PdfDocument.Open("algo.pdf"))
{
foreach (var page in doc.GetPages())
{
letters.AddRange(page.Letters);
}
}
return letters;
}
[Benchmark]
public IReadOnlyList<Letter> PDFBOX_492_4_jar_8()
{
List<Letter> letters = new List<Letter>();
using (var doc = PdfDocument.Open("PDFBOX-492-4.jar-8.pdf"))
{
foreach (var page in doc.GetPages())
{
letters.AddRange(page.Letters);
}
}
return letters;
}
}

View File

@@ -0,0 +1,42 @@
using BenchmarkDotNet.Attributes;
using UglyToad.PdfPig.Content;
using UglyToad.PdfPig.DocumentLayoutAnalysis;
using UglyToad.PdfPig.DocumentLayoutAnalysis.PageSegmenter;
using UglyToad.PdfPig.DocumentLayoutAnalysis.WordExtractor;
namespace UglyToad.PdfPig.Benchmarks;
[Config(typeof(NuGetPackageConfig))]
[MemoryDiagnoser(displayGenColumns: false)]
public class LayoutAnalysisBenchmarks
{
private readonly Letter[] _letters;
private readonly Word[] _words;
public LayoutAnalysisBenchmarks()
{
using (var doc = PdfDocument.Open("fseprd1102849.pdf"))
{
_letters = doc.GetPage(1).Letters.ToArray();
_words = NearestNeighbourWordExtractor.Instance.GetWords(_letters).ToArray();
}
}
[Benchmark]
public IReadOnlyList<Word> GetWords_NearestNeighbourWord()
{
return NearestNeighbourWordExtractor.Instance.GetWords(_letters).ToArray();
}
[Benchmark]
public IReadOnlyList<TextBlock> GetBlocks_Docstrum()
{
return DocstrumBoundingBoxes.Instance.GetBlocks(_words);
}
[Benchmark]
public IReadOnlyList<Letter> DuplicateOverlappingText()
{
return DuplicateOverlappingTextProcessor.Get(_letters).ToArray();
}
}

View File

@@ -0,0 +1,25 @@
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Environments;
using BenchmarkDotNet.Jobs;
namespace UglyToad.PdfPig.Benchmarks;
internal class NuGetPackageConfig : ManualConfig
{
public NuGetPackageConfig()
{
var baseJob = Job.Default;
var localJob = baseJob
.WithMsBuildArguments("/p:PdfPigVersion=Local")
.WithId("Local");
var latestJob = baseJob
.WithMsBuildArguments("/p:PdfPigVersion=Latest")
.WithId("Latest")
.AsBaseline();
AddJob(localJob.WithRuntime(CoreRuntime.Core80));
AddJob(latestJob.WithRuntime(CoreRuntime.Core80));
}
}

View File

@@ -0,0 +1,13 @@
using BenchmarkDotNet.Running;
namespace UglyToad.PdfPig.Benchmarks
{
internal class Program
{
static void Main(string[] args)
{
var summary = BenchmarkRunner.Run<BruteForceBenchmarks>();
Console.ReadKey();
}
}
}

View File

@@ -0,0 +1,47 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PdfPigVersion Condition="'$(PdfPigVersion)' == ''">Local</PdfPigVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
</ItemGroup>
<ItemGroup Condition="'$(PdfPigVersion)' == 'Local'">
<ProjectReference Include="..\..\src\UglyToad.PdfPig\UglyToad.PdfPig.csproj" />
<ProjectReference Include="..\..\src\UglyToad.PdfPig.Core\UglyToad.PdfPig.Core.csproj" />
<ProjectReference Include="..\..\src\UglyToad.PdfPig.Fonts\UglyToad.PdfPig.Fonts.csproj" />
<ProjectReference Include="..\..\src\UglyToad.PdfPig.Tokenization\UglyToad.PdfPig.Tokenization.csproj" />
<ProjectReference Include="..\..\src\UglyToad.PdfPig.Tokens\UglyToad.PdfPig.Tokens.csproj" />
<ProjectReference Include="..\..\src\UglyToad.PdfPig.DocumentLayoutAnalysis\UglyToad.PdfPig.DocumentLayoutAnalysis.csproj" />
</ItemGroup>
<ItemGroup Condition="'$(PdfPigVersion)' == 'Latest'">
<PackageReference Include="PdfPig" Version="0.1.14-alpha-20260219-adb57" />
</ItemGroup>
<ItemGroup>
<None Update="algo.pdf">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="fseprd1102849.pdf">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="PDFBOX-492-4.jar-8.pdf">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Single Page Simple - from inkscape.pdf">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Single Page Simple - from open office.pdf">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,12 @@
<Solution>
<Folder Name="/PdfPig/">
<Project Path="../../src/UglyToad.PdfPig.Core/UglyToad.PdfPig.Core.csproj" />
<Project Path="../../src/UglyToad.PdfPig.DocumentLayoutAnalysis/UglyToad.PdfPig.DocumentLayoutAnalysis.csproj" />
<Project Path="../../src/UglyToad.PdfPig.Fonts/UglyToad.PdfPig.Fonts.csproj" />
<Project Path="../../src/UglyToad.PdfPig.Tokenization/UglyToad.PdfPig.Tokenization.csproj" />
<Project Path="../../src/UglyToad.PdfPig.Tokens/UglyToad.PdfPig.Tokens.csproj" />
<Project Path="../../src/UglyToad.PdfPig/UglyToad.PdfPig.csproj" />
</Folder>
<Folder Name="/Solution Items/" />
<Project Path="UglyToad.PdfPig.Benchmarks.csproj" />
</Solution>

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -185,7 +185,11 @@ namespace UglyToad.PdfPig.ConsoleRunner
sw.Reset();
sw.Start();
using (var pdfDocument = PdfDocument.Open(file))
using (var pdfDocument = PdfDocument.Open(file, new ParsingOptions
{
UseLenientParsing = true,
SkipMissingFonts = true,
}))
{
sw.Stop();

View File

@@ -46,4 +46,27 @@
0009309.pdf
0009464.pdf
0009706.pdf
0009944.pdf
0009944.pdf
0010114.pdf
0010117.pdf
0010216.pdf
0010472.pdf
0010697.pdf
0010902.pdf
0010950.pdf
0011041.pdf
0011171.pdf
0011398.pdf
0011450.pdf
0011758.pdf
0011989.pdf
0012117.pdf
0012684.pdf
0012730.pdf
0013051.pdf
0013178.pdf
0013338.pdf
0013425.pdf
0013587.pdf
0013721.pdf
0013822.pdf