mirror of
https://github.com/UglyToad/PdfPig.git
synced 2026-03-10 00:23:29 +08:00
Compare commits
23 Commits
release-ac
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3e37eafae | ||
|
|
a4047247a8 | ||
|
|
34db05f735 | ||
|
|
9c0d6893e0 | ||
|
|
adb5713621 | ||
|
|
665623561a | ||
|
|
7e370a0eab | ||
|
|
b9aa53166d | ||
|
|
0a2b1e076f | ||
|
|
f732718852 | ||
|
|
d69e3fa6a9 | ||
|
|
d6e86b057e | ||
|
|
c27f1b6553 | ||
|
|
629891e80e | ||
|
|
13825b9424 | ||
|
|
7c4f5e2424 | ||
|
|
bd573b2a1c | ||
|
|
baeac0d0c3 | ||
|
|
ee0cb1dc4a | ||
|
|
c70b343caa | ||
|
|
ce563db133 | ||
|
|
37a5dffcaa | ||
|
|
aef0a78ee6 |
40
.github/workflows/prepare_release_pr.yml
vendored
Normal file
40
.github/workflows/prepare_release_pr.yml
vendored
Normal 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
53
.github/workflows/publish_nuget.yml
vendored
Normal 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 }}
|
||||
53
.github/workflows/publish_release.yml
vendored
53
.github/workflows/publish_release.yml
vendored
@@ -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
|
||||
2
.github/workflows/run_common_crawl_tests.yml
vendored
2
.github/workflows/run_common_crawl_tests.yml
vendored
@@ -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
43
.github/workflows/tag_release.yml
vendored
Normal 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 }}"
|
||||
63
src/UglyToad.PdfPig.Core/StackDepthGuard.cs
Normal file
63
src/UglyToad.PdfPig.Core/StackDepthGuard.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
20
src/UglyToad.PdfPig.Core/XrefEntryType.cs
Normal file
20
src/UglyToad.PdfPig.Core/XrefEntryType.cs
Normal 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
|
||||
}
|
||||
42
src/UglyToad.PdfPig.Core/XrefLocation.cs
Normal file
42
src/UglyToad.PdfPig.Core/XrefLocation.cs
Normal 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);
|
||||
|
||||
}
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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) + "' />";
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
{
|
||||
letter = new Letter(
|
||||
" ",
|
||||
letter.GlyphRectangle,
|
||||
letter.BoundingBox,
|
||||
letter.GlyphRectangleLoose,
|
||||
letter.StartBaseLine,
|
||||
letter.EndBaseLine,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}));
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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("]")]
|
||||
|
||||
@@ -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]")]
|
||||
|
||||
@@ -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())
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>();
|
||||
|
||||
|
||||
@@ -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>();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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));
|
||||
|
||||
15
src/UglyToad.PdfPig/Content/IBoundingBox.cs
Normal file
15
src/UglyToad.PdfPig/Content/IBoundingBox.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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})";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -122,7 +122,7 @@ namespace UglyToad.PdfPig.Graphics
|
||||
|
||||
letter = new Letter(
|
||||
newLetter,
|
||||
attachTo.GlyphRectangle,
|
||||
attachTo.BoundingBox,
|
||||
attachTo.GlyphRectangleLoose,
|
||||
attachTo.StartBaseLine,
|
||||
attachTo.EndBaseLine,
|
||||
|
||||
@@ -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 '\'.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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++;
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user