Compare commits

..

23 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
Alexander Vinnikov
d69e3fa6a9 make ext gstates stacked
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
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-11 18:39:33 +00:00
BobLd
d6e86b057e Handle empty encoding in Type1FontSimple and fix #1248
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
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-10 06:14:39 +00:00
BobLd
c27f1b6553 Only throw if ArrayToken length is less than 4 in ToRectangle() and fix #1238
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
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-01 16:10:24 +00:00
BobLd
629891e80e Make LinkAnnotation internal to fix unit tests 2026-02-01 15:50:21 +00:00
David Ochsner
13825b9424 Add Links to Pdf Generation 2026-02-01 15:13:55 +00:00
BobLd
7c4f5e2424 Introduce StackDepthGuard class to check for stack depth in CoreTokenScanner and fix #1217
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
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
2025-12-23 16:24:04 +01:00
BobLd
bd573b2a1c Increment version to 0.1.14 2025-12-23 12:00:19 +01:00
BobLd
baeac0d0c3 Do not return glyph bbox and path in Type1Font if character name is '.notdef' 2025-12-23 11:09:00 +01:00
BobLd
ee0cb1dc4a Use file header offset when doing brute force find and fix #1223
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
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
2025-12-07 13:43:22 +00:00
BobLd
c70b343caa Minor Type1FontParser optimisations
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
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
2025-12-02 17:29:55 +00:00
Carlo Kok
ce563db133 Clarify handling of optional content group names
Updated comments to clarify handling of optional content group names in PDFs.
2025-11-23 10:52:21 +00:00
BobLd
37a5dffcaa Skip in ReadCompositeGlyph when glyphIndex is out of range and fix #1213 2025-11-21 21:31:18 +00:00
EliotJones
aef0a78ee6 update release logic to check out master before commit
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
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
2025-11-14 09:50:07 +00:00
131 changed files with 142782 additions and 1507 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,63 @@
namespace UglyToad.PdfPig.Core
{
/// <summary>
/// Provides a guard for tracking and limiting the depth of nested stack operations, such as recursive calls or
/// nested parsing.
/// </summary>
/// <remarks>Use this class to prevent excessive stack usage by enforcing a maximum nesting depth. This is
/// particularly useful in scenarios where untrusted or deeply nested input could cause stack overflows or
/// performance issues.</remarks>
public sealed class StackDepthGuard
{
/// <summary>
/// Represents a stack depth guard with no effective limit on the allowed depth.
/// </summary>
/// <remarks>Use this instance when stack depth restrictions are not required.</remarks>
public static readonly StackDepthGuard Infinite = new StackDepthGuard(int.MaxValue);
private readonly int maxStackDepth;
private int depth;
/// <summary>
/// Initializes a new instance of the StackDepthGuard class with the specified maximum stack depth.
/// </summary>
/// <param name="maxStackDepth">The maximum allowed stack depth for guarded operations. Must be a positive integer.</param>
public StackDepthGuard(int maxStackDepth)
{
if (maxStackDepth <= 0)
{
throw new ArgumentOutOfRangeException(nameof(maxStackDepth));
}
this.maxStackDepth = maxStackDepth;
}
/// <summary>
/// Increments the current nesting depth and checks against the maximum allowed stack depth.
/// </summary>
/// <exception cref="PdfDocumentFormatException">Thrown if the maximum allowed nesting depth is exceeded.</exception>
public void Enter()
{
if (++depth > maxStackDepth)
{
depth--; // Decrement so Exit remains balanced if someone catches this
throw new PdfDocumentFormatException($"Exceeded maximum nesting depth of {maxStackDepth}.");
}
}
/// <summary>
/// Decreases the current depth level by one, ensuring that the depth does not become negative.
/// </summary>
/// <remarks>If the current depth is already zero, calling this method has no effect. This method
/// is typically used to track or manage nested operations or scopes where depth must remain
/// non-negative.</remarks>
public void Exit()
{
depth--;
if (depth < 0)
{
depth = 0;
}
}
}
}

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net462;net471;net6.0;net8.0;net9.0</TargetFrameworks>
<LangVersion>12</LangVersion>
<Version>0.1.13</Version>
<Version>0.1.14</Version>
<IsTestProject>False</IsTestProject>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<SignAssembly>true</SignAssembly>

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

@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net462;net471;net6.0;net8.0;net9.0</TargetFrameworks>
<LangVersion>12</LangVersion>
<Version>0.1.13</Version>
<Version>0.1.14</Version>
<IsTestProject>False</IsTestProject>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<SignAssembly>true</SignAssembly>

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

@@ -240,9 +240,15 @@
flags = (CompositeGlyphFlags)data.ReadUnsignedShort();
var glyphIndex = data.ReadUnsignedShort();
var childGlyph = glyphs[glyphIndex];
if (glyphIndex >= glyphs.Length)
{
// Unsure why this happens but fixes #1213
continue; // TODO - Is there a better fix?
}
if (childGlyph == null)
IGlyphDescription? childGlyph = glyphs[glyphIndex];
if (childGlyph is null)
{
if (!compositeLocations.TryGetValue(glyphIndex, out var missingComposite))
{
@@ -319,7 +325,7 @@
}
}
builderGlyph = builderGlyph ?? emptyGlyph;
builderGlyph ??= emptyGlyph;
return new Glyph(false, builderGlyph.Instructions, builderGlyph.EndPointsOfContours, builderGlyph.Points, compositeLocation.Bounds);
}
@@ -426,7 +432,7 @@
}
}
private class CompositeComponent
private sealed class CompositeComponent
{
public int Index { get; }

View File

@@ -18,16 +18,19 @@
private const int PfbFileIndicator = 0x80;
private static readonly char[] Separators = [' '];
private static readonly Type1EncryptedPortionParser EncryptedPortionParser = new Type1EncryptedPortionParser();
/// <summary>
/// Parses an embedded Adobe Type 1 font file.
/// </summary>
/// <param name="inputBytes">The bytes of the font program.</param>
/// <param name="length1">The length in bytes of the clear text portion of the font program.</param>
/// <param name="length2">The length in bytes of the encrypted portion of the font program.</param>
/// <param name="stackDepthGuard"></param>
/// <returns>The parsed type 1 font.</returns>
public static Type1Font Parse(IInputBytes inputBytes, int length1, int length2)
public static Type1Font Parse(IInputBytes inputBytes, int length1, int length2, StackDepthGuard stackDepthGuard)
{
// Sometimes the entire PFB file including the header bytes can be included which prevents parsing in the normal way.
var isEntirePfbFile = inputBytes.Peek() == PfbFileIndicator;
@@ -42,15 +45,15 @@
inputBytes = new MemoryInputBytes(ascii);
}
var scanner = new CoreTokenScanner(inputBytes, false);
var scanner = new CoreTokenScanner(inputBytes, false, stackDepthGuard);
if (!scanner.TryReadToken(out CommentToken comment) || !comment.Data.StartsWith("!"))
{
throw new InvalidFontFormatException("The Type1 program did not start with '%!'.");
}
string name;
var parts = comment.Data.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries);
var parts = comment.Data.Split(Separators, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 3)
{
name = parts[1];
@@ -60,11 +63,9 @@
name = "Unknown";
}
var comments = new List<string>();
while (scanner.MoveNext() && scanner.CurrentToken is CommentToken commentToken)
while (scanner.MoveNext() && scanner.CurrentToken is CommentToken)
{
comments.Add(commentToken.Data);
// We ignore comments
}
var dictionaries = new List<DictionaryToken>();
@@ -441,7 +442,7 @@
return null;
}
private class PreviousTokenSet
private sealed class PreviousTokenSet
{
private readonly IToken[] tokens = new IToken[3];

View File

@@ -62,6 +62,11 @@
/// </summary>
public PdfRectangle? GetCharacterBoundingBox(string characterName)
{
if (string.Equals(characterName, GlyphList.NotDefined, StringComparison.OrdinalIgnoreCase))
{
return null;
}
var glyph = GetCharacterPath(characterName);
return PdfSubpath.GetBoundingRectangle(glyph);
}
@@ -94,8 +99,13 @@
/// <summary>
/// Get the pdfpath for the character with the given name.
/// </summary>
public IReadOnlyList<PdfSubpath> GetCharacterPath(string characterName)
public IReadOnlyList<PdfSubpath>? GetCharacterPath(string characterName)
{
if (string.Equals(characterName, GlyphList.NotDefined, StringComparison.OrdinalIgnoreCase))
{
return null;
}
if (!CharStrings.TryGenerate(characterName, out var glyph))
{
return null;

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net462;net471;net6.0;net8.0;net9.0</TargetFrameworks>
<LangVersion>12</LangVersion>
<Version>0.1.13</Version>
<Version>0.1.14</Version>
<IsTestProject>False</IsTestProject>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<SignAssembly>true</SignAssembly>

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

@@ -105,7 +105,7 @@
{
var parser = new CodespaceRangeParser();
var byteArrayInput = new MemoryInputBytes(OtherEncodings.StringAsLatin1Bytes("1 begincodespacerange\nendcodespacerange"));
var tokenScanner = new CoreTokenScanner(byteArrayInput, false);
var tokenScanner = new CoreTokenScanner(byteArrayInput, false, new StackDepthGuard(256));
Assert.True(tokenScanner.MoveNext());
Assert.True(tokenScanner.CurrentToken is NumericToken);

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

@@ -11,7 +11,7 @@
{
var bytes = GetFileBytes("AdobeUtopia.pfa");
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0);
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0, new StackDepthGuard(256));
}
[Fact]
@@ -20,7 +20,7 @@
// TODO: support reading in these pfb files
var bytes = GetFileBytes("Raleway-Black.pfb");
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0);
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0, new StackDepthGuard(256));
}
[Fact]
@@ -28,7 +28,7 @@
{
var bytes = GetFileBytes("CMBX10.pfa");
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0);
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0, new StackDepthGuard(256));
}
[Fact]
@@ -36,7 +36,7 @@
{
var bytes = GetFileBytes("CMCSC10");
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0);
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0, new StackDepthGuard(256));
}
[Fact]
@@ -44,7 +44,7 @@
{
var bytes = GetFileBytes("CMBX12");
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0);
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0, new StackDepthGuard(256));
}
[Fact]
@@ -52,7 +52,7 @@
{
var bytes = GetFileBytes("CMBX10");
var result = Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0);
var result = Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0, new StackDepthGuard(256));
var builder = new StringBuilder("<!DOCTYPE html><html><head></head><body>");
foreach (var charString in result.CharStrings.CharStrings)
@@ -71,7 +71,7 @@
{
var bytes = GetFileBytes("CMR10");
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0);
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0, new StackDepthGuard(256));
}
private static byte[] GetFileBytes(string name)

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,106 @@
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()
{
var path = IntegrationHelpers.GetDocumentPath("jtehm-melillo-2679746.pdf");
using (var document = PdfDocument.Open(path, new ParsingOptions() { UseLenientParsing = true }))
{
var page = document.GetPage(1);
foreach (var letter in page.Letters)
{
var font = letter.GetFont();
if (font?.Name?.Data.Contains("TimesLT") == true)
{
Assert.True(font.TryGetPath(100, out _));
}
}
}
}
[Fact]
public void Issues1238()
{
var path = IntegrationHelpers.GetDocumentPath("6.Secrets.to.Startup.Success.PDFDrive.pdf");
using (var document = PdfDocument.Open(path, new ParsingOptions() { UseLenientParsing = true }))
{
var page = document.GetPage(159);
Assert.NotNull(page);
Assert.StartsWith("uct. At the longer-cycle, broader end of the spectrum are identity-level", page.Text);
Assert.Equal(0, page.Rotation.Value);
}
}
[Fact]
public void Issue1217()
{
var path = IntegrationHelpers.GetSpecificTestDocumentPath("stackoverflow_error.pdf");
var options = new ParsingOptions()
{
UseLenientParsing = true,
MaxStackDepth = 100
};
var ex = Assert.Throws<PdfDocumentFormatException>(() => PdfDocument.Open(path, options));
Assert.Equal($"Exceeded maximum nesting depth of {options.MaxStackDepth}.", ex.Message);
}
[Fact]
public void Issue1223()
{
var path = IntegrationHelpers.GetSpecificTestDocumentPath("23056.PMC2132516.pdf");
using (var document = PdfDocument.Open(path, new ParsingOptions() { UseLenientParsing = true }))
{
Assert.NotNull(document);
var firstPage = document.GetPage(1);
Assert.NotNull(firstPage);
Assert.Contains("The Rockefeller University Press", firstPage.Text);
}
}
[Fact]
public void Issue1213()
{
var path = IntegrationHelpers.GetDocumentPath("GlyphDataTableReadCompositeGlyphError.pdf");
using (var document = PdfDocument.Open(path, new ParsingOptions() { UseLenientParsing = true }))
{
for (int p = 1; p <= document.NumberOfPages; p++)
{
var page = document.GetPage(p);
Assert.NotNull(page);
}
}
}
[Fact]
public void Issue1208()
{
@@ -227,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]
@@ -286,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]
@@ -452,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()));
}
}
@@ -658,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

@@ -42,12 +42,12 @@ public class FirstPassParserTests
var results = FirstPassParser.Parse(
new FileHeaderOffset(0),
ib.Bytes,
new CoreTokenScanner(ib.Bytes, true));
new CoreTokenScanner(ib.Bytes, true, new StackDepthGuard(256)));
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]
@@ -114,7 +114,7 @@ public class FirstPassParserTests
var ib = StringBytesTestConverter.Convert(content, false);
var results = FirstPassParser.Parse(new FileHeaderOffset(0), ib.Bytes, new CoreTokenScanner(ib.Bytes, true));
var results = FirstPassParser.Parse(new FileHeaderOffset(0), ib.Bytes, new CoreTokenScanner(ib.Bytes, true, new StackDepthGuard(256)));
var offsets = results.Parts.Select(x => x.Offset).OrderBy(x => x).ToList();
@@ -123,7 +123,7 @@ public class FirstPassParserTests
Assert.NotNull(results.Trailer);
ib.Bytes.Seek(98);
var scanner = new CoreTokenScanner(ib.Bytes, false);
var scanner = new CoreTokenScanner(ib.Bytes, false, new StackDepthGuard(256));
scanner.MoveNext();
Assert.Equal(scanner.CurrentToken, OperatorToken.Xref);
}

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

@@ -15,7 +15,7 @@
public class PageContentParserTests
{
private readonly PageContentParser parser = new PageContentParser(ReflectionGraphicsStateOperationFactory.Instance);
private readonly PageContentParser parser = new PageContentParser(ReflectionGraphicsStateOperationFactory.Instance, new StackDepthGuard(256));
private readonly ILog log = new NoOpLog();
[Fact]
@@ -210,7 +210,69 @@ l";
var content = File.ReadAllText(path);
var input = StringBytesTestConverter.Convert(content, false);
var lenientParser = new PageContentParser(ReflectionGraphicsStateOperationFactory.Instance, true);
var lenientParser = new PageContentParser(ReflectionGraphicsStateOperationFactory.Instance, new StackDepthGuard(256), true);
var result = lenientParser.Parse(1, input.Bytes, log);
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);

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

@@ -149,7 +149,7 @@ three %PDF-1.6";
var bytes = new MemoryInputBytes(input);
var scanner = new CoreTokenScanner(bytes, true, ScannerScope.None);
var scanner = new CoreTokenScanner(bytes, true, new StackDepthGuard(256), ScannerScope.None);
var result = FileHeaderParser.Parse(scanner, bytes, false, log);

View File

@@ -1,5 +1,6 @@
namespace UglyToad.PdfPig.Tests.Parser.Parts;
using PdfPig.Core;
using PdfPig.Parser.FileStructure;
using PdfPig.Tokenization.Scanner;
@@ -26,7 +27,7 @@ public class FirstPassParserStartXrefTests
var result = FirstPassParser.GetFirstCrossReferenceOffset(
input.Bytes,
new CoreTokenScanner(input.Bytes, true),
new CoreTokenScanner(input.Bytes, true, new StackDepthGuard(256)),
new TestingLog());
Assert.Equal(456, result.StartXRefDeclaredOffset);
@@ -59,7 +60,7 @@ public class FirstPassParserStartXrefTests
var result = FirstPassParser.GetFirstCrossReferenceOffset(
input.Bytes,
new CoreTokenScanner(input.Bytes, true),
new CoreTokenScanner(input.Bytes, true, new StackDepthGuard(256)),
new TestingLog());
Assert.Equal(17, result.StartXRefDeclaredOffset);
@@ -93,7 +94,7 @@ public class FirstPassParserStartXrefTests
var result = FirstPassParser.GetFirstCrossReferenceOffset(
input.Bytes,
new CoreTokenScanner(input.Bytes, true),
new CoreTokenScanner(input.Bytes, true, new StackDepthGuard(256)),
new TestingLog());
Assert.Equal(1384733, result.StartXRefDeclaredOffset);
@@ -106,7 +107,7 @@ public class FirstPassParserStartXrefTests
var result = FirstPassParser.GetFirstCrossReferenceOffset(
input.Bytes,
new CoreTokenScanner(input.Bytes, true),
new CoreTokenScanner(input.Bytes, true, new StackDepthGuard(256)),
new TestingLog());
Assert.Null(result.StartXRefDeclaredOffset);
@@ -130,7 +131,7 @@ public class FirstPassParserStartXrefTests
var result = FirstPassParser.GetFirstCrossReferenceOffset(
input.Bytes,
new CoreTokenScanner(input.Bytes, true),
new CoreTokenScanner(input.Bytes, true, new StackDepthGuard(256)),
new TestingLog());
Assert.Null(result.StartXRefDeclaredOffset);
@@ -151,7 +152,7 @@ public class FirstPassParserStartXrefTests
var result = FirstPassParser.GetFirstCrossReferenceOffset(
input.Bytes,
new CoreTokenScanner(input.Bytes, true),
new CoreTokenScanner(input.Bytes, true, new StackDepthGuard(256)),
new TestingLog());
Assert.Null(result.StartXRefDeclaredOffset);
@@ -185,7 +186,7 @@ public class FirstPassParserStartXrefTests
var result = FirstPassParser.GetFirstCrossReferenceOffset(
input.Bytes,
new CoreTokenScanner(input.Bytes, true),
new CoreTokenScanner(input.Bytes, true, new StackDepthGuard(256)),
new TestingLog());
Assert.Equal(1274665676543, result.StartXRefDeclaredOffset);
@@ -207,7 +208,7 @@ public class FirstPassParserStartXrefTests
var result = FirstPassParser.GetFirstCrossReferenceOffset(
input.Bytes,
new CoreTokenScanner(input.Bytes, true),
new CoreTokenScanner(input.Bytes, true, new StackDepthGuard(256)),
new TestingLog());
Assert.Equal(57695, result.StartXRefDeclaredOffset);

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

@@ -34,7 +34,7 @@
internal static (CoreTokenScanner scanner, IInputBytes bytes) Scanner(string s)
{
var inputBytes = new MemoryInputBytes(OtherEncodings.StringAsLatin1Bytes(s));
var result = new CoreTokenScanner(inputBytes, true);
var result = new CoreTokenScanner(inputBytes, true, new StackDepthGuard(256));
return (result, inputBytes);
}

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

@@ -1,11 +1,12 @@
namespace UglyToad.PdfPig.Tests.Tokenization
{
using PdfPig.Core;
using PdfPig.Tokenization;
using PdfPig.Tokens;
public class ArrayTokenizerTests
{
private readonly ArrayTokenizer tokenizer = new ArrayTokenizer(true);
private readonly ArrayTokenizer tokenizer = new ArrayTokenizer(true, new StackDepthGuard(256), false);
[Theory]
[InlineData("]")]

View File

@@ -7,7 +7,7 @@ namespace UglyToad.PdfPig.Tests.Tokenization
public class DictionaryTokenizerTests
{
private readonly DictionaryTokenizer tokenizer = new DictionaryTokenizer(true);
private readonly DictionaryTokenizer tokenizer = new DictionaryTokenizer(true, new StackDepthGuard(256));
[Theory]
[InlineData("[rjee]")]

View File

@@ -11,7 +11,7 @@ namespace UglyToad.PdfPig.Tests.Tokenization.Scanner
public CoreTokenScannerTests()
{
scannerFactory = x => new CoreTokenScanner(x, true);
scannerFactory = x => new CoreTokenScanner(x, true, new StackDepthGuard(256));
}
[Fact]
@@ -231,7 +231,7 @@ endobj";
var scanner = new CoreTokenScanner(
StringBytesTestConverter.Convert(content, false).Bytes,
true,
true, new StackDepthGuard(256),
isStream: true);
while (scanner.MoveNext())
@@ -247,7 +247,7 @@ endobj";
var nonStreamScanner = new CoreTokenScanner(
StringBytesTestConverter.Convert(content, false).Bytes,
true,
true, new StackDepthGuard(256),
isStream: false);
while (nonStreamScanner.MoveNext())
@@ -293,7 +293,7 @@ endobj";
var scanner = new CoreTokenScanner(
StringBytesTestConverter.Convert(content, false).Bytes,
true,
true, new StackDepthGuard(256),
isStream: true);
while (scanner.MoveNext())

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);
}
}
}
@@ -1295,6 +1295,78 @@
}
}
[Fact]
public void CanAddLinkToPage()
{
var builder = new PdfDocumentBuilder();
var page = builder.AddPage(PageSize.A4);
var font = builder.AddStandard14Font(Standard14Font.Helvetica);
var linkArea = new PdfRectangle(25, 690, 200, 720);
page.AddLink("https://github.com", linkArea);
var bytes = builder.Build();
WriteFile(nameof(CanAddLinkToPage), bytes);
using (var document = PdfDocument.Open(bytes))
{
Assert.Equal(1, document.NumberOfPages);
var page1 = document.GetPage(1);
var annotations = page1.GetAnnotations().ToList();
Assert.Single(annotations);
var linkAnnotation = annotations[0];
Assert.Equal(Annotations.AnnotationType.Link, linkAnnotation.Type);
Assert.Equal(linkArea, linkAnnotation.Rectangle);
// Verify the URI link target
Assert.NotNull(linkAnnotation.Action);
var uriAction = Assert.IsType<Actions.UriAction>(linkAnnotation.Action);
Assert.Equal("https://github.com", uriAction.Uri);
}
}
[Fact]
public void CanAddInternalLinkToPage()
{
var builder = new PdfDocumentBuilder();
var font = builder.AddStandard14Font(Standard14Font.Helvetica);
var page1 = builder.AddPage(PageSize.A4);
var page2 = builder.AddPage(PageSize.A4);
var linkArea = new PdfRectangle(25, 690, 200, 720);
var coordinates = new ExplicitDestinationCoordinates(25, 750);
var destination = new ExplicitDestination(1, ExplicitDestinationType.XyzCoordinates, coordinates);
page2.AddLink(destination, linkArea);
var bytes = builder.Build();
WriteFile(nameof(CanAddInternalLinkToPage), bytes);
using (var document = PdfDocument.Open(bytes))
{
Assert.Equal(2, document.NumberOfPages);
var page2Doc = document.GetPage(2);
var annotations = page2Doc.GetAnnotations().ToList();
Assert.Single(annotations);
var linkAnnotation = annotations[0];
Assert.Equal(Annotations.AnnotationType.Link, linkAnnotation.Type);
Assert.Equal(linkArea, linkAnnotation.Rectangle);
// Verify the link destination
Assert.NotNull(linkAnnotation.Action);
var goToAction = Assert.IsType<Actions.GoToAction>(linkAnnotation.Action);
Assert.Equal(1, goToAction.Destination.PageNumber);
Assert.Equal(ExplicitDestinationType.XyzCoordinates, goToAction.Destination.Type);
Assert.Equal(25, goToAction.Destination.Coordinates.Left);
Assert.Equal(750, goToAction.Destination.Coordinates.Top);
}
}
private static void WriteFile(string name, byte[] bytes, string extension = "pdf")
{
try

View File

@@ -8,12 +8,16 @@
internal sealed class ArrayTokenizer : ITokenizer
{
private readonly bool usePdfDocEncoding;
private readonly StackDepthGuard stackDepthGuard;
private readonly bool useLenientParsing;
public bool ReadsNextByte { get; } = false;
public ArrayTokenizer(bool usePdfDocEncoding)
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)
@@ -25,7 +29,7 @@
return false;
}
var scanner = new CoreTokenScanner(inputBytes, usePdfDocEncoding, ScannerScope.Array);
var scanner = new CoreTokenScanner(inputBytes, usePdfDocEncoding, stackDepthGuard, ScannerScope.Array, useLenientParsing: useLenientParsing);
var contents = new List<IToken>();

View File

@@ -10,6 +10,7 @@
private readonly bool usePdfDocEncoding;
private readonly IReadOnlyList<NameToken> requiredKeys;
private readonly bool useLenientParsing;
private readonly StackDepthGuard stackDepthGuard;
public bool ReadsNextByte { get; } = false;
@@ -19,14 +20,16 @@
/// <param name="usePdfDocEncoding">
/// Whether to read strings using the PdfDocEncoding.
/// </param>
/// <param name="stackDepthGuard"></param>
/// <param name="requiredKeys">
/// Can be provided to recover from errors with missing dictionary end symbols if the
/// set of keys expected in the dictionary are known.
/// </param>
/// <param name="useLenientParsing">Whether to use lenient parsing.</param>
public DictionaryTokenizer(bool usePdfDocEncoding, IReadOnlyList<NameToken> requiredKeys = null, bool useLenientParsing = false)
public DictionaryTokenizer(bool usePdfDocEncoding, StackDepthGuard stackDepthGuard, IReadOnlyList<NameToken> requiredKeys = null, bool useLenientParsing = false)
{
this.usePdfDocEncoding = usePdfDocEncoding;
this.stackDepthGuard = stackDepthGuard;
this.requiredKeys = requiredKeys;
this.useLenientParsing = useLenientParsing;
}
@@ -83,7 +86,7 @@
return false;
}
var coreScanner = new CoreTokenScanner(inputBytes, usePdfDocEncoding, ScannerScope.Dictionary, useLenientParsing: useLenientParsing);
var coreScanner = new CoreTokenScanner(inputBytes, usePdfDocEncoding, stackDepthGuard, ScannerScope.Dictionary, useLenientParsing: useLenientParsing);
var tokens = new List<IToken>();

View File

@@ -52,12 +52,15 @@
/// </remarks>
private readonly bool isStream;
private readonly StackDepthGuard stackDepthGuard;
/// <summary>
/// Create a new <see cref="CoreTokenScanner"/> from the input.
/// </summary>
public CoreTokenScanner(
IInputBytes inputBytes,
bool usePdfDocEncoding,
StackDepthGuard stackDepthGuard,
ScannerScope scope = ScannerScope.None,
IReadOnlyDictionary<NameToken, IReadOnlyList<NameToken>> namedDictionaryRequiredKeys = null,
bool useLenientParsing = false,
@@ -65,9 +68,10 @@
{
this.inputBytes = inputBytes ?? throw new ArgumentNullException(nameof(inputBytes));
this.usePdfDocEncoding = usePdfDocEncoding;
this.stackDepthGuard = stackDepthGuard;
this.stringTokenizer = new StringTokenizer(usePdfDocEncoding);
this.arrayTokenizer = new ArrayTokenizer(usePdfDocEncoding);
this.dictionaryTokenizer = new DictionaryTokenizer(usePdfDocEncoding, useLenientParsing: useLenientParsing);
this.arrayTokenizer = new ArrayTokenizer(usePdfDocEncoding, this.stackDepthGuard, useLenientParsing);
this.dictionaryTokenizer = new DictionaryTokenizer(usePdfDocEncoding, this.stackDepthGuard, useLenientParsing: useLenientParsing);
this.scope = scope;
this.namedDictionaryRequiredKeys = namedDictionaryRequiredKeys;
this.useLenientParsing = useLenientParsing;
@@ -101,6 +105,19 @@
/// <inheritdoc />
public bool MoveNext()
{
stackDepthGuard.Enter();
try
{
return MoveNextInternal();
}
finally
{
stackDepthGuard.Exit();
}
}
private bool MoveNextInternal()
{
var endAngleBracesRead = 0;
@@ -169,7 +186,7 @@
&& CurrentToken is NameToken name
&& namedDictionaryRequiredKeys.TryGetValue(name, out var requiredKeys))
{
tokenizer = new DictionaryTokenizer(usePdfDocEncoding, requiredKeys, useLenientParsing);
tokenizer = new DictionaryTokenizer(usePdfDocEncoding, stackDepthGuard, requiredKeys, useLenientParsing);
}
}
else

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net462;net471;net6.0;net8.0;net9.0</TargetFrameworks>
<LangVersion>12</LangVersion>
<Version>0.1.13</Version>
<Version>0.1.14</Version>
<IsTestProject>False</IsTestProject>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<SignAssembly>true</SignAssembly>

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

@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net462;net471;net6.0;net8.0;net9.0</TargetFrameworks>
<LangVersion>12</LangVersion>
<Version>0.1.13</Version>
<Version>0.1.14</Version>
<IsTestProject>False</IsTestProject>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<SignAssembly>true</SignAssembly>

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

@@ -59,7 +59,10 @@ namespace UglyToad.PdfPig.Content
switch (Type)
{
case "OCG": // Optional content group dictionary
// Name - Required
// Name - Per spec this is required, but in practice some PDFs store layer names
// at the document catalog level in OCProperties rather than in marked content Properties.
// To avoid crashes, we make this optional and fall back to null or the tag name.
if (markedContentElement.Properties.TryGet(NameToken.Name, pdfTokenScanner, out NameToken? name))
{
Name = name.Data;
@@ -74,7 +77,9 @@ namespace UglyToad.PdfPig.Content
}
else
{
throw new ArgumentException($"Cannot parse optional content's {nameof(Name)} from {nameof(markedContentElement.Properties)}. This is a required field.", nameof(markedContentElement.Properties));
// Name not found in Properties - use tag as fallback or leave as null
// This handles PDFs where layer names are stored at document catalog level
Name = markedContentElement.Tag;
}
// Intent - Optional

View File

@@ -24,7 +24,7 @@
private readonly StackDictionary<NameToken, IndirectReference> currentFontState = new StackDictionary<NameToken, IndirectReference>();
private readonly StackDictionary<NameToken, IndirectReference> currentXObjectState = new StackDictionary<NameToken, IndirectReference>();
private readonly Dictionary<NameToken, DictionaryToken> extendedGraphicsStates = new Dictionary<NameToken, DictionaryToken>();
private readonly StackDictionary<NameToken, DictionaryToken> extendedGraphicsStates = new StackDictionary<NameToken, DictionaryToken>();
private readonly StackDictionary<NameToken, ResourceColorSpace> namedColorSpaces = new StackDictionary<NameToken, ResourceColorSpace>();
private readonly Dictionary<NameToken, ColorSpaceDetails> loadedNamedColorSpaceDetails = new Dictionary<NameToken, ColorSpaceDetails>();
@@ -55,7 +55,8 @@
namedColorSpaces.Push();
currentFontState.Push();
currentXObjectState.Push();
currentXObjectState.Push();
extendedGraphicsStates.Push();
if (resourceDictionary.TryGet(NameToken.Font, out var fontBase))
{
@@ -190,6 +191,7 @@
currentFontState.Pop();
currentXObjectState.Pop();
namedColorSpaces.Pop();
extendedGraphicsStates.Pop();
}
private void LoadFontDictionary(DictionaryToken fontDictionary)
@@ -348,8 +350,13 @@
public DictionaryToken? GetExtendedGraphicsStateDictionary(NameToken name)
{
if (parsingOptions.UseLenientParsing && !extendedGraphicsStates.ContainsKey(name))
{
if (parsingOptions.UseLenientParsing)
{
if (extendedGraphicsStates.TryGetValue(name, out var dictToken))
{
return dictToken;
}
parsingOptions.Logger.Error($"The graphic state dictionary does not contain the key '{name}'.");
return null;
}

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

@@ -21,6 +21,7 @@
{
private readonly IGraphicsStateOperationFactory operationFactory;
private readonly bool useLenientParsing;
private readonly StackDepthGuard stackDepthGuard;
/// <summary>
/// Initialises a new instance of the <see cref="PageContentParser"/> class.
@@ -28,12 +29,14 @@
/// <param name="operationFactory">
/// The factory responsible for creating graphics state operations.
/// </param>
/// <param name="stackDepthGuard"></param>
/// <param name="useLenientParsing">
/// A value indicating whether lenient parsing should be used. Defaults to <c>false</c>.
/// </param>
public PageContentParser(IGraphicsStateOperationFactory operationFactory, bool useLenientParsing = false)
public PageContentParser(IGraphicsStateOperationFactory operationFactory, StackDepthGuard stackDepthGuard, bool useLenientParsing = false)
{
this.operationFactory = operationFactory;
this.stackDepthGuard = stackDepthGuard;
this.useLenientParsing = useLenientParsing;
}
@@ -55,7 +58,7 @@
IInputBytes inputBytes,
ILog log)
{
var scanner = new CoreTokenScanner(inputBytes, false, useLenientParsing: useLenientParsing);
var scanner = new CoreTokenScanner(inputBytes, false, stackDepthGuard, useLenientParsing: useLenientParsing);
var precedingTokens = new List<IToken>();
var graphicsStateOperations = new List<IGraphicsStateOperation>();

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

@@ -89,7 +89,9 @@
SkipMissingFonts = false
};
var tokenScanner = new CoreTokenScanner(inputBytes, true, useLenientParsing: options.UseLenientParsing);
var stackDepthGuard = new StackDepthGuard(options.MaxStackDepth);
var tokenScanner = new CoreTokenScanner(inputBytes, true, stackDepthGuard, useLenientParsing: options.UseLenientParsing);
var passwords = new List<string>();
@@ -110,7 +112,7 @@
options.Passwords = passwords;
var document = OpenDocument(inputBytes, tokenScanner, options);
var document = OpenDocument(inputBytes, tokenScanner, options, stackDepthGuard);
return document;
}
@@ -118,14 +120,17 @@
private static PdfDocument OpenDocument(
IInputBytes inputBytes,
ISeekableTokenScanner scanner,
ParsingOptions parsingOptions)
ParsingOptions parsingOptions,
StackDepthGuard stackDepthGuard)
{
var filterProvider = new FilterProviderWithLookup(parsingOptions.FilterProvider ?? DefaultFilterProvider.Instance);
var version = FileHeaderParser.Parse(scanner, inputBytes, parsingOptions.UseLenientParsing, parsingOptions.Logger);
var fileHeaderOffset = new FileHeaderOffset((int)version.OffsetInFile);
var initialParse = FirstPassParser.Parse(
new FileHeaderOffset((int)version.OffsetInFile),
fileHeaderOffset,
inputBytes,
scanner,
parsingOptions.Logger);
@@ -143,7 +148,7 @@
initialParse.BruteForceOffsets,
inputBytes);
var pdfScanner = new PdfTokenScanner(inputBytes, locationProvider, filterProvider, NoOpEncryptionHandler.Instance, parsingOptions);
var pdfScanner = new PdfTokenScanner(inputBytes, locationProvider, filterProvider, NoOpEncryptionHandler.Instance, fileHeaderOffset, parsingOptions, stackDepthGuard);
var (rootReference, rootDictionary) = ParseTrailer(
trailer,
@@ -180,6 +185,7 @@
filterProvider,
encodingReader,
cmapCache,
stackDepthGuard,
parsingOptions.UseLenientParsing);
var trueTypeHandler = new TrueTypeFontHandler(
@@ -206,7 +212,7 @@
parsingOptions.UseLenientParsing);
var pageFactory = new PageFactory(pdfScanner, resourceContainer, filterProvider,
new PageContentParser(ReflectionGraphicsStateOperationFactory.Instance, parsingOptions.UseLenientParsing), parsingOptions);
new PageContentParser(ReflectionGraphicsStateOperationFactory.Instance, stackDepthGuard, parsingOptions.UseLenientParsing), parsingOptions);
var catalog = CatalogFactory.Create(
rootReference,

View File

@@ -52,6 +52,13 @@
/// </summary>
public bool SkipMissingFonts { get; set; } = false;
/// <summary>
/// Gets or sets the maximum allowed stack depth.
/// </summary>
/// <remarks>This property can be used to limit the depth of recursive or nested operations to
/// prevent stack overflows or excessive resource usage.</remarks>
public int MaxStackDepth { get; set; } = 256;
/// <summary>
/// Filter provider to use while parsing the document. The <see cref="DefaultFilterProvider"/> will be used if set to <c>null</c>.
/// </summary>

View File

@@ -24,6 +24,7 @@
{
var scanner = new CoreTokenScanner(inputBytes,
false,
StackDepthGuard.Infinite, // We don't check for stack overflows, we might want to change that.
namedDictionaryRequiredKeys: new Dictionary<NameToken, IReadOnlyList<NameToken>>
{
{ NameToken.CidSystemInfo, new[] { NameToken.Registry, NameToken.Ordering, NameToken.Supplement } }

Some files were not shown because too many files have changed in this diff Show More