mirror of
https://github.com/UglyToad/PdfPig.git
synced 2026-03-10 00:23:29 +08:00
Compare commits
2 Commits
master
...
nullabilit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dcb2c2bbe2 | ||
|
|
ef5366f117 |
15
.github/workflows/build_and_test.yml
vendored
15
.github/workflows/build_and_test.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Build, test and publish draft
|
||||
name: Build and test
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
build:
|
||||
runs-on: windows-2022
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- name: Set up dotnet core
|
||||
uses: actions/setup-dotnet@v4
|
||||
@@ -29,14 +29,3 @@ jobs:
|
||||
|
||||
- name: Run the tests
|
||||
run: dotnet test -c Release src/UglyToad.PdfPig.sln
|
||||
|
||||
- name: Update draft release
|
||||
if: github.ref == 'refs/heads/master'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
draft: true
|
||||
name: "Draft release"
|
||||
tag_name: "unreleased" # fixed tag so this one draft is updated
|
||||
generate_release_notes: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
5
.github/workflows/build_and_test_macos.yml
vendored
5
.github/workflows/build_and_test_macos.yml
vendored
@@ -19,11 +19,10 @@ jobs:
|
||||
2.1.x
|
||||
6.0.x
|
||||
8.0.x
|
||||
9.0.x
|
||||
|
||||
# Build the release build
|
||||
- name: Build the solution
|
||||
run: dotnet build -c Release src/UglyToad.PdfPig.sln -f net8.0
|
||||
run: dotnet build -c Release src/UglyToad.PdfPig.sln
|
||||
|
||||
- name: Run the tests
|
||||
run: dotnet test -c Release src/UglyToad.PdfPig.sln -f net8.0
|
||||
run: dotnet test -c Release src/UglyToad.PdfPig.sln
|
||||
|
||||
61
.github/workflows/nightly_release.yml
vendored
61
.github/workflows/nightly_release.yml
vendored
@@ -5,55 +5,39 @@ on:
|
||||
- cron: "0 0 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write # Grant write permission for tagging
|
||||
|
||||
jobs:
|
||||
check_publish_needed:
|
||||
runs-on: ubuntu-latest
|
||||
name: Check if this commit has already been published
|
||||
outputs:
|
||||
should_run: ${{ steps.check.outputs.should_run }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Fetch tags
|
||||
run: git fetch --tags
|
||||
|
||||
- id: check
|
||||
run: |
|
||||
latest_commit=$(git rev-parse nightly-latest || echo "")
|
||||
echo "Latest published commit: $latest_commit"
|
||||
if [ "$latest_commit" = "${{ github.sha }}" ]; then
|
||||
echo "No new commit since last publish."
|
||||
echo "should_run=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "New commit detected."
|
||||
echo "should_run=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
tests:
|
||||
needs: check_publish_needed
|
||||
if: ${{ needs.check_publish_needed.outputs.should_run == 'true' }}
|
||||
uses: ./.github/workflows/run_integration_tests.yml
|
||||
check_date:
|
||||
runs-on: ubuntu-latest
|
||||
name: Check latest commit
|
||||
outputs:
|
||||
should_run: ${{ steps.should_run.outputs.should_run }}
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: print latest_commit
|
||||
run: echo ${{ github.sha }}
|
||||
|
||||
- id: should_run
|
||||
continue-on-error: true
|
||||
name: check latest commit is less than a day ago
|
||||
if: ${{ github.event_name == 'schedule' }}
|
||||
run: test -z $(git rev-list --after="24 hours" ${{ github.sha }}) && echo "::set-output name=should_run::false"
|
||||
build_and_publish_nightly:
|
||||
needs: [check_publish_needed, tests]
|
||||
if: ${{ needs.check_publish_needed.outputs.should_run == 'true' }}
|
||||
needs: [check_date, tests]
|
||||
if: ${{ needs.check_date.outputs.should_run != 'false' }}
|
||||
runs-on: windows-2022
|
||||
name: build_and_publish_nightly
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- name: Set up dotnet core
|
||||
uses: actions/setup-dotnet@v4
|
||||
uses: actions/setup-dotnet@v2
|
||||
with:
|
||||
dotnet-version: |
|
||||
2.1.x
|
||||
6.0.x
|
||||
8.0.x
|
||||
9.0.x
|
||||
|
||||
- name: Add msbuild to PATH
|
||||
uses: microsoft/setup-msbuild@v1.0.2
|
||||
@@ -70,10 +54,5 @@ jobs:
|
||||
|
||||
- name: Publish Nuget to GitHub registry
|
||||
run: dotnet nuget push **/*.nupkg --api-key ${{secrets.NUGET_API_KEY}} --source https://api.nuget.org/v3/index.json
|
||||
|
||||
- name: Tag latest nightly commit
|
||||
run: |
|
||||
git config user.name "github-actions"
|
||||
git config user.email "github-actions@github.com"
|
||||
git tag -f nightly-latest ${{ github.sha }}
|
||||
git push origin nightly-latest --force
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
40
.github/workflows/prepare_release_pr.yml
vendored
40
.github/workflows/prepare_release_pr.yml
vendored
@@ -1,40 +0,0 @@
|
||||
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
53
.github/workflows/publish_nuget.yml
vendored
@@ -1,53 +0,0 @@
|
||||
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 }}
|
||||
44
.github/workflows/run_common_crawl_tests.yml
vendored
44
.github/workflows/run_common_crawl_tests.yml
vendored
@@ -9,50 +9,50 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
pair: ["0000-0001", "0002-0003", "0004-0005", "0006-0007", "0008-0009", "0010-0011", "0012-0013"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up dotnet core
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: |
|
||||
8.0.x
|
||||
9.0.x
|
||||
dotnet-version: "8.0.x"
|
||||
|
||||
- name: Restore corpus cache
|
||||
- name: Restore corpus cache 0000, 0001
|
||||
id: restore-corpus
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: corpus/
|
||||
key: ${{ runner.os }}-pdf-corpus-${{ matrix.pair }}
|
||||
key: ${{ runner.os }}-pdf-corpus-0000-0001
|
||||
|
||||
- name: Download corpus if cache missed
|
||||
- name: Download corpus if cache missed 0000, 0001
|
||||
if: steps.restore-corpus.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
mkdir -p corpus/zipfiles
|
||||
cd corpus/zipfiles
|
||||
for file in $(echo "${{ matrix.pair }}" | tr '-' ' '); do
|
||||
echo "Downloading $file.zip"
|
||||
wget -nv "https://digitalcorpora.s3.amazonaws.com/corpora/files/CC-MAIN-2021-31-PDF-UNTRUNCATED/zipfiles/0000-0999/$file.zip" -O "$file.zip"
|
||||
done
|
||||
echo "Downloading 0000.zip"
|
||||
wget -nv https://digitalcorpora.s3.amazonaws.com/corpora/files/CC-MAIN-2021-31-PDF-UNTRUNCATED/zipfiles/0000-0999/0000.zip -O 0000.zip
|
||||
wget -nv https://digitalcorpora.s3.amazonaws.com/corpora/files/CC-MAIN-2021-31-PDF-UNTRUNCATED/zipfiles/0000-0999/0001.zip -O 0001.zip
|
||||
cd ..
|
||||
unzip 'zipfiles/*.zip' -d extracted
|
||||
# run: rm -f zipfiles/*.zip
|
||||
|
||||
- name: Remove unwanted test files
|
||||
run: |
|
||||
while read f || [ -n "$f" ]; do
|
||||
full="corpus/extracted/$f"
|
||||
if [ -f "$full" ]; then
|
||||
echo "Removing $full"
|
||||
rm "$full"
|
||||
skip_files=(
|
||||
"corpus/extracted/0000399.pdf"
|
||||
"corpus/extracted/0000819.pdf"
|
||||
"corpus/extracted/0000920.pdf"
|
||||
"corpus/extracted/0000300.pdf"
|
||||
"corpus/extracted/0001589.pdf"
|
||||
"corpus/extracted/0001957.pdf"
|
||||
)
|
||||
|
||||
for file in "${skip_files[@]}"; do
|
||||
if [ -f "$file" ]; then
|
||||
echo "Removing $file"
|
||||
rm "$file"
|
||||
fi
|
||||
done < tools/common-crawl-ignore.txt
|
||||
done
|
||||
|
||||
- name: Run tests against corpus
|
||||
run: dotnet run --project tools/UglyToad.PdfPig.ConsoleRunner/UglyToad.PdfPig.ConsoleRunner.csproj "corpus/extracted" --configuration Release
|
||||
run: dotnet run --project tools/UglyToad.PdfPig.ConsoleRunner/UglyToad.PdfPig.ConsoleRunner.csproj "corpus/extracted"
|
||||
|
||||
4
.github/workflows/run_integration_tests.yml
vendored
4
.github/workflows/run_integration_tests.yml
vendored
@@ -15,9 +15,7 @@ jobs:
|
||||
- name: Set up dotnet core
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: |
|
||||
8.0.x
|
||||
9.0.x
|
||||
dotnet-version: "8.0.x"
|
||||
|
||||
- name: Restore cached part 1
|
||||
id: restore-cache-p1
|
||||
|
||||
43
.github/workflows/tag_release.yml
vendored
43
.github/workflows/tag_release.yml
vendored
@@ -1,43 +0,0 @@
|
||||
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 }}"
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -248,5 +248,3 @@ _Pvt_Extensions
|
||||
/tools/ConsoleRunner/Properties/launchSettings.json
|
||||
|
||||
/docs/doxygen
|
||||
/tools/UglyToad.PdfPig.ConsoleRunner/Properties/launchSettings.json
|
||||
/src/UglyToad.PdfPig.Tests/Images/Files/Pdf/indexed-png-with-mask.png
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,12 +11,11 @@
|
||||
{
|
||||
private readonly Stream stream;
|
||||
private readonly bool shouldDispose;
|
||||
private byte? peekByte;
|
||||
|
||||
private bool isAtEnd;
|
||||
|
||||
/// <inheritdoc />
|
||||
public long CurrentOffset => peekByte.HasValue ? stream.Position - 1 : stream.Position;
|
||||
public long CurrentOffset => stream.Position;
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte CurrentByte { get; private set; }
|
||||
@@ -53,8 +52,7 @@
|
||||
/// <inheritdoc />
|
||||
public bool MoveNext()
|
||||
{
|
||||
var b = peekByte ?? stream.ReadByte();
|
||||
peekByte = null;
|
||||
var b = stream.ReadByte();
|
||||
|
||||
if (b == -1)
|
||||
{
|
||||
@@ -70,21 +68,18 @@
|
||||
/// <inheritdoc />
|
||||
public byte? Peek()
|
||||
{
|
||||
if (!peekByte.HasValue)
|
||||
{
|
||||
var v = stream.ReadByte();
|
||||
var current = CurrentOffset;
|
||||
|
||||
if (v >= 0)
|
||||
{
|
||||
peekByte = (byte)v;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var b = stream.ReadByte();
|
||||
|
||||
stream.Seek(current, SeekOrigin.Begin);
|
||||
|
||||
if (b == -1)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return peekByte;
|
||||
return (byte)b;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -97,7 +92,6 @@
|
||||
public void Seek(long position)
|
||||
{
|
||||
isAtEnd = false;
|
||||
peekByte = null;
|
||||
|
||||
if (position == 0)
|
||||
{
|
||||
@@ -118,15 +112,9 @@
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
else if (peekByte.HasValue)
|
||||
{
|
||||
buffer[0] = peekByte.Value;
|
||||
peekByte = null;
|
||||
|
||||
return Read(buffer.Slice(1)) + 1;
|
||||
}
|
||||
|
||||
int read = stream.Read(buffer);
|
||||
|
||||
if (read > 0)
|
||||
{
|
||||
CurrentByte = buffer[read - 1];
|
||||
|
||||
@@ -224,18 +224,9 @@
|
||||
[Pure]
|
||||
public double TransformX(double x)
|
||||
{
|
||||
return A * x + E; // + C * 0
|
||||
}
|
||||
var xt = A * x + C * 0 + E;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Transform an Y coordinate using this transformation matrix.
|
||||
/// </summary>
|
||||
/// <param name="y">The Y coordinate.</param>
|
||||
/// <returns>The transformed Y coordinate.</returns>
|
||||
public double TransformY(double y)
|
||||
{
|
||||
return D * y + F;
|
||||
return xt;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net462;net471;net6.0;net8.0;net9.0</TargetFrameworks>
|
||||
<TargetFrameworks>netstandard2.0;net462;net471;net6.0;net8.0</TargetFrameworks>
|
||||
<LangVersion>12</LangVersion>
|
||||
<Version>0.1.14</Version>
|
||||
<Version>0.1.12-alpha001</Version>
|
||||
<IsTestProject>False</IsTestProject>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
<AssemblyOriginatorKeyFile>..\pdfpig.snk</AssemblyOriginatorKeyFile>
|
||||
<Nullable>enable</Nullable>
|
||||
<Nullable>enable</Nullable>
|
||||
<WarningsAsErrors>nullable</WarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)'=='net462'">
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
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,15 +50,7 @@
|
||||
* (i,j,k) will form a group and (m,n) will form another group.
|
||||
*************************************************************************************/
|
||||
|
||||
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
|
||||
int[] indexes = Enumerable.Repeat(-1, elements.Count).ToArray();
|
||||
KdTree<T> kdTree = new KdTree<T>(elements, candidatesPoint);
|
||||
|
||||
ParallelOptions parallelOptions = new ParallelOptions() { MaxDegreeOfParallelism = maxDegreeOfParallelism };
|
||||
@@ -126,15 +118,7 @@
|
||||
* (i,j,k) will form a group and (m,n) will form another group.
|
||||
*************************************************************************************/
|
||||
|
||||
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
|
||||
int[] indexes = Enumerable.Repeat(-1, elements.Count).ToArray();
|
||||
KdTree<T> kdTree = new KdTree<T>(elements, candidatesPoint);
|
||||
|
||||
ParallelOptions parallelOptions = new ParallelOptions() { MaxDegreeOfParallelism = maxDegreeOfParallelism };
|
||||
@@ -202,15 +186,7 @@
|
||||
* (i,j,k) will form a group and (m,n) will form another group.
|
||||
*************************************************************************************/
|
||||
|
||||
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
|
||||
int[] indexes = Enumerable.Repeat(-1, elements.Count).ToArray();
|
||||
|
||||
ParallelOptions parallelOptions = new ParallelOptions() { MaxDegreeOfParallelism = maxDegreeOfParallelism };
|
||||
|
||||
@@ -240,37 +216,71 @@
|
||||
yield return group.Select(i => elements[i]).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
internal static List<List<int>> GroupIndexes(int[] edges)
|
||||
|
||||
/// <summary>
|
||||
/// Group elements using Depth-first search.
|
||||
/// <para>https://en.wikipedia.org/wiki/Depth-first_search</para>
|
||||
/// </summary>
|
||||
/// <param name="edges">The graph. edges[i] = j indicates that there is an edge between i and j.</param>
|
||||
/// <returns>A List of HashSets containing the grouped indexes.</returns>
|
||||
internal static List<HashSet<int>> GroupIndexes(int[] edges)
|
||||
{
|
||||
// Improved thanks to https://github.com/UglyToad/PdfPig/issues/1178
|
||||
var adjacency = new List<int>[edges.Length];
|
||||
int[][] adjacency = new int[edges.Length][];
|
||||
for (int i = 0; i < edges.Length; i++)
|
||||
{
|
||||
adjacency[i] = new List<int>();
|
||||
}
|
||||
|
||||
// one pass O(n)
|
||||
for (int i = 0; i < edges.Length; i++)
|
||||
{
|
||||
int j = edges[i];
|
||||
if (j != -1)
|
||||
HashSet<int> matches = new HashSet<int>();
|
||||
if (edges[i] != -1) matches.Add(edges[i]);
|
||||
for (int j = 0; j < edges.Length; j++)
|
||||
{
|
||||
// i <-> j
|
||||
adjacency[i].Add(j);
|
||||
adjacency[j].Add(i);
|
||||
if (edges[j] == i) matches.Add(j);
|
||||
}
|
||||
adjacency[i] = matches.ToArray();
|
||||
}
|
||||
|
||||
List<List<int>> groupedIndexes = new List<List<int>>();
|
||||
List<HashSet<int>> groupedIndexes = new List<HashSet<int>>();
|
||||
bool[] isDone = new bool[edges.Length];
|
||||
|
||||
for (int p = 0; p < edges.Length; p++)
|
||||
{
|
||||
if (isDone[p])
|
||||
if (isDone[p]) continue;
|
||||
groupedIndexes.Add(DfsIterative(p, adjacency, ref isDone));
|
||||
}
|
||||
return groupedIndexes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Group elements using Depth-first search.
|
||||
/// <para>https://en.wikipedia.org/wiki/Depth-first_search</para>
|
||||
/// </summary>
|
||||
/// <param name="edges">The graph. edges[i] = [j, k, l, ...] indicates that there is an edge between i and each element j, k, l, ...</param>
|
||||
/// <returns>A List of HashSets containing the grouped indexes.</returns>
|
||||
internal static List<HashSet<int>> GroupIndexes(int[][] edges)
|
||||
{
|
||||
int[][] adjacency = new int[edges.Length][];
|
||||
for (int i = 0; i < edges.Length; i++)
|
||||
{
|
||||
HashSet<int> matches = new HashSet<int>();
|
||||
for (int j = 0; j < edges[i].Length; j++)
|
||||
{
|
||||
continue;
|
||||
if (edges[i][j] != -1) matches.Add(edges[i][j]);
|
||||
}
|
||||
|
||||
for (int j = 0; j < edges.Length; j++)
|
||||
{
|
||||
for (int k = 0; k < edges[j].Length; k++)
|
||||
{
|
||||
if (edges[j][k] == i) matches.Add(j);
|
||||
}
|
||||
}
|
||||
adjacency[i] = matches.ToArray();
|
||||
}
|
||||
|
||||
List<HashSet<int>> groupedIndexes = new List<HashSet<int>>();
|
||||
bool[] isDone = new bool[edges.Length];
|
||||
|
||||
for (int p = 0; p < edges.Length; p++)
|
||||
{
|
||||
if (isDone[p]) continue;
|
||||
groupedIndexes.Add(DfsIterative(p, adjacency, ref isDone));
|
||||
}
|
||||
return groupedIndexes;
|
||||
@@ -280,33 +290,22 @@
|
||||
/// Depth-first search
|
||||
/// <para>https://en.wikipedia.org/wiki/Depth-first_search</para>
|
||||
/// </summary>
|
||||
private static List<int> DfsIterative(int s, List<int>[] adj, ref bool[] isDone)
|
||||
private static HashSet<int> DfsIterative(int s, int[][] adj, ref bool[] isDone)
|
||||
{
|
||||
List<int> group = new List<int>();
|
||||
Stack<int> S = new Stack<int>(4);
|
||||
HashSet<int> group = new HashSet<int>();
|
||||
Stack<int> S = new Stack<int>();
|
||||
S.Push(s);
|
||||
|
||||
isDone[s] = true;
|
||||
while (S.Count > 0)
|
||||
{
|
||||
var u = S.Pop();
|
||||
group.Add(u);
|
||||
|
||||
#if NET
|
||||
var currentAdj = System.Runtime.InteropServices.CollectionsMarshal.AsSpan(adj[u]);
|
||||
int count = currentAdj.Length;
|
||||
#else
|
||||
var currentAdj = adj[u];
|
||||
int count = currentAdj.Count;
|
||||
#endif
|
||||
for (int i = 0; i < count; ++i)
|
||||
if (!isDone[u])
|
||||
{
|
||||
var v = currentAdj[i];
|
||||
ref bool done = ref isDone[v];
|
||||
if (!done)
|
||||
group.Add(u);
|
||||
isDone[u] = true;
|
||||
foreach (var v in adj[u])
|
||||
{
|
||||
S.Push(v);
|
||||
done = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,13 +128,13 @@
|
||||
throw new ArgumentException("The algorithm cannot be used with a document of less than 2 pages.", nameof(pagesTextBlocks));
|
||||
}
|
||||
|
||||
ConcurrentDictionary<int, OrderedSet<TextBlock>> pageDecorations = new ConcurrentDictionary<int, OrderedSet<TextBlock>>();
|
||||
ConcurrentDictionary<int, List<TextBlock>> pageDecorations = new ConcurrentDictionary<int, List<TextBlock>>();
|
||||
|
||||
ParallelOptions parallelOptions = new ParallelOptions() { MaxDegreeOfParallelism = maxDegreeOfParallelism };
|
||||
|
||||
Parallel.For(0, pagesTextBlocks.Count, parallelOptions, p =>
|
||||
{
|
||||
if (!pageDecorations.TryAdd(p, new OrderedSet<TextBlock>()))
|
||||
if (!pageDecorations.TryAdd(p, new List<TextBlock>()))
|
||||
{
|
||||
throw new ArgumentException("Cannot add element with index " + p + " in ConcurrentDictionary.");
|
||||
}
|
||||
@@ -165,7 +165,7 @@
|
||||
var score = Score(current, previousPage, nextPage, minimumEditDistanceNormalised, similarityThreshold, n);
|
||||
if (score >= similarityThreshold)
|
||||
{
|
||||
pageDecorations[p].TryAdd(current);
|
||||
if (!pageDecorations[p].Contains(current)) pageDecorations[p].Add(current);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@
|
||||
var score = Score(current, previousPage, nextPage, minimumEditDistanceNormalised, similarityThreshold, n);
|
||||
if (score >= similarityThreshold)
|
||||
{
|
||||
pageDecorations[p].TryAdd(current);
|
||||
if (!pageDecorations[p].Contains(current)) pageDecorations[p].Add(current);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@
|
||||
var score = Score(current, previousPage, nextPage, minimumEditDistanceNormalised, similarityThreshold, n);
|
||||
if (score >= similarityThreshold)
|
||||
{
|
||||
pageDecorations[p].TryAdd(current);
|
||||
if (!pageDecorations[p].Contains(current)) pageDecorations[p].Add(current);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,12 +210,12 @@
|
||||
var score = Score(current, previousPage, nextPage, minimumEditDistanceNormalised, similarityThreshold, n);
|
||||
if (score >= similarityThreshold)
|
||||
{
|
||||
pageDecorations[p].TryAdd(current);
|
||||
if (!pageDecorations[p].Contains(current)) pageDecorations[p].Add(current);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return pageDecorations.OrderBy(x => x.Key).Select(x => x.Value.GetList()).ToList();
|
||||
return pageDecorations.OrderBy(x => x.Key).Select(x => x.Value).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
using Core;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
/// <summary>
|
||||
/// Contains helpful tools for distance measures.
|
||||
@@ -168,11 +169,12 @@
|
||||
|
||||
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, candidatePoint(candidates[i]));
|
||||
double currentDistance = distanceMeasure(pivot, candidatesPoints[i]);
|
||||
if (currentDistance < distance && !candidates[i].Equals(element))
|
||||
{
|
||||
distance = currentDistance;
|
||||
@@ -209,11 +211,12 @@
|
||||
|
||||
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, candidateLine(candidates[i]));
|
||||
double currentDistance = distanceMeasure(pivot, candidatesLines[i]);
|
||||
if (currentDistance < distance && !candidates[i].Equals(element))
|
||||
{
|
||||
distance = currentDistance;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
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.
|
||||
@@ -23,51 +24,41 @@
|
||||
return letters?.ToList();
|
||||
}
|
||||
|
||||
// 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>();
|
||||
var queue = new Queue<Letter>(letters);
|
||||
var cleanLetters = new List<Letter>() { queue.Dequeue() }; // dequeue the first letter
|
||||
|
||||
foreach (var letter in letters)
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var letter = queue.Dequeue();
|
||||
bool addLetter = true;
|
||||
int duplicatesOverlappingIndex = -1;
|
||||
|
||||
var key = (letter.Value, letter.FontName);
|
||||
if (duplicateIndex.TryGetValue(key, out var candidateIndices))
|
||||
{
|
||||
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 duplicates = cleanLetters.Where(l => l.Value.Equals(letter.Value) && l.FontName.Equals(letter.FontName)); // do other checks?
|
||||
|
||||
for (int ci = 0; ci < candidateIndices.Count; ci++)
|
||||
if (duplicates.Any())
|
||||
{
|
||||
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;
|
||||
|
||||
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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
// duplicate overlapping letter was found, keeping the existing one and not adding this one.
|
||||
addLetter = false;
|
||||
duplicatesOverlappingIndex = cleanLetters.IndexOf(duplicatesOverlapping);
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -77,11 +68,25 @@
|
||||
// update textSequence?
|
||||
|
||||
// update font details to bold
|
||||
var fontDetails = new FontDetails(letter.Font.Name, true, letter.Font.Weight, letter.Font.IsItalic);
|
||||
|
||||
var newLetter = new Letter(letter.Value,
|
||||
letter.GlyphRectangle,
|
||||
letter.StartBaseLine,
|
||||
letter.EndBaseLine,
|
||||
letter.Width,
|
||||
letter.FontSize,
|
||||
fontDetails,
|
||||
letter.RenderingMode,
|
||||
letter.StrokeColor,
|
||||
letter.FillColor,
|
||||
letter.PointSize,
|
||||
letter.TextSequence);
|
||||
|
||||
// update markedContentStack?
|
||||
|
||||
// update letters
|
||||
cleanLetters[duplicatesOverlappingIndex] = letter.AsBold();
|
||||
cleanLetters[duplicatesOverlappingIndex] = newLetter;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -229,7 +229,7 @@
|
||||
private AltoDocument.AltoIllustration ToAltoIllustration(IPdfImage pdfImage, double height)
|
||||
{
|
||||
illustrationCount++;
|
||||
var rectangle = pdfImage.BoundingBox;
|
||||
var rectangle = pdfImage.Bounds;
|
||||
|
||||
return new AltoDocument.AltoIllustration
|
||||
{
|
||||
@@ -311,10 +311,10 @@
|
||||
glyphCount++;
|
||||
return new AltoDocument.AltoGlyph
|
||||
{
|
||||
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),
|
||||
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),
|
||||
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.BoundingBox;
|
||||
var bbox = pdfImage.Bounds;
|
||||
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.BoundingBox;
|
||||
var bbox = pdfImage.Bounds;
|
||||
return new PageXmlDocument.PageXmlImageRegion()
|
||||
{
|
||||
Coords = ToCoords(bbox, pageWidth, pageHeight),
|
||||
@@ -360,7 +360,7 @@
|
||||
data.GlyphsCount++;
|
||||
return new PageXmlDocument.PageXmlGlyph()
|
||||
{
|
||||
Coords = ToCoords(letter.BoundingBox, pageWidth, pageHeight),
|
||||
Coords = ToCoords(letter.GlyphRectangle, 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.BoundingBox.Rotation != 0)
|
||||
if (l.GlyphRectangle.Rotation != 0)
|
||||
{
|
||||
rotation = $" transform='rotate({Math.Round(-l.BoundingBox.Rotation, Rounding)} {Math.Round(l.BoundingBox.BottomLeft.X, Rounding)},{Math.Round(height - l.BoundingBox.TopLeft.Y, Rounding)})'";
|
||||
rotation = $" transform='rotate({Math.Round(-l.GlyphRectangle.Rotation, Rounding)} {Math.Round(l.GlyphRectangle.BottomLeft.X, Rounding)},{Math.Round(height - l.GlyphRectangle.TopLeft.Y, Rounding)})'";
|
||||
}
|
||||
|
||||
string fontSize = l.FontSize != 1 ? $"font-size='{l.FontSize:0}'" : $"style='font-size:{Math.Round(l.BoundingBox.Height, 2)}px'";
|
||||
string fontSize = l.FontSize != 1 ? $"font-size='{l.FontSize:0}'" : $"style='font-size:{Math.Round(l.GlyphRectangle.Height, 2)}px'";
|
||||
|
||||
var safeValue = XmlEscape(l, doc);
|
||||
var x = Math.Round(l.StartBaseLine.X, Rounding);
|
||||
|
||||
@@ -54,6 +54,9 @@
|
||||
/// <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>
|
||||
@@ -108,11 +111,11 @@
|
||||
|
||||
if (depth % 2 == 0)
|
||||
{
|
||||
P.Sort((p0, p1) => p0.Value.X.CompareTo(p1.Value.X));
|
||||
P.Sort(kdTreeComparerX);
|
||||
}
|
||||
else
|
||||
{
|
||||
P.Sort((p0, p1) => p0.Value.Y.CompareTo(p1.Value.Y));
|
||||
P.Sort(kdTreeComparerY);
|
||||
}
|
||||
|
||||
if (P.Length == 2)
|
||||
@@ -128,26 +131,6 @@
|
||||
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)
|
||||
@@ -162,13 +145,13 @@
|
||||
|
||||
if (depth % 2 == 0)
|
||||
{
|
||||
P.Sort(KdTreeComparerX.Shared);
|
||||
P.Sort(kdTreeComparerX);
|
||||
}
|
||||
else
|
||||
{
|
||||
P.Sort(KdTreeComparerY.Shared);
|
||||
P.Sort(kdTreeComparerY);
|
||||
}
|
||||
|
||||
|
||||
if (P.Count == 2)
|
||||
{
|
||||
return new KdTreeNode<T>(new KdTreeLeaf<T>(P.GetAt(0), depth + 1), null, P.GetAt(1), depth);
|
||||
@@ -196,14 +179,13 @@
|
||||
/// <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 pivotPoint = pivotPointFunc(pivot);
|
||||
var result = FindNearestNeighbour(Root, pivot, pivotPoint, distanceMeasure);
|
||||
var result = FindNearestNeighbour(Root, pivot, pivotPointFunc, 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, PdfPoint pivotPoint, Func<PdfPoint, PdfPoint, double> distance)
|
||||
private static (KdTreeNode<T>, double?) FindNearestNeighbour(KdTreeNode<T> node, T pivot, Func<T, PdfPoint> pivotPointFunc, Func<PdfPoint, PdfPoint, double> distance)
|
||||
{
|
||||
if (node == null)
|
||||
{
|
||||
@@ -215,22 +197,23 @@
|
||||
{
|
||||
return (null, null);
|
||||
}
|
||||
return (node, distance(node.Value, pivotPoint));
|
||||
return (node, distance(node.Value, pivotPointFunc(pivot)));
|
||||
}
|
||||
else
|
||||
{
|
||||
var point = pivotPointFunc(pivot);
|
||||
var currentNearestNode = node;
|
||||
var currentDistance = distance(node.Value, pivotPoint);
|
||||
var currentDistance = distance(node.Value, point);
|
||||
|
||||
KdTreeNode<T> newNode = null;
|
||||
double? newDist = null;
|
||||
|
||||
var pointValue = node.IsAxisCutX ? pivotPoint.X : pivotPoint.Y;
|
||||
var pointValue = node.IsAxisCutX ? point.X : point.Y;
|
||||
|
||||
if (pointValue < node.L)
|
||||
{
|
||||
// start left
|
||||
(newNode, newDist) = FindNearestNeighbour(node.LeftChild, pivot, pivotPoint, distance);
|
||||
(newNode, newDist) = FindNearestNeighbour(node.LeftChild, pivot, pivotPointFunc, distance);
|
||||
|
||||
if (newDist.HasValue && newDist <= currentDistance && !newNode.Element.Equals(pivot))
|
||||
{
|
||||
@@ -240,13 +223,13 @@
|
||||
|
||||
if (node.RightChild != null && pointValue + currentDistance >= node.L)
|
||||
{
|
||||
(newNode, newDist) = FindNearestNeighbour(node.RightChild, pivot, pivotPoint, distance);
|
||||
(newNode, newDist) = FindNearestNeighbour(node.RightChild, pivot, pivotPointFunc, distance);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// start right
|
||||
(newNode, newDist) = FindNearestNeighbour(node.RightChild, pivot, pivotPoint, distance);
|
||||
(newNode, newDist) = FindNearestNeighbour(node.RightChild, pivot, pivotPointFunc, distance);
|
||||
|
||||
if (newDist.HasValue && newDist <= currentDistance && !newNode.Element.Equals(pivot))
|
||||
{
|
||||
@@ -256,7 +239,7 @@
|
||||
|
||||
if (node.LeftChild != null && pointValue - currentDistance <= node.L)
|
||||
{
|
||||
(newNode, newDist) = FindNearestNeighbour(node.LeftChild, pivot, pivotPoint, distance);
|
||||
(newNode, newDist) = FindNearestNeighbour(node.LeftChild, pivot, pivotPointFunc, distance);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,7 +258,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(T, Func{T, PdfPoint}, Func{PdfPoint, PdfPoint, double}, out int, out double)"/> if only looking for the (single) closest point.</para>
|
||||
/// <para>Use <see cref="FindNearestNeighbour(KdTreeNode{T}, T, Func{T, PdfPoint}, Func{PdfPoint, PdfPoint, 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>
|
||||
@@ -284,24 +267,13 @@
|
||||
/// <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, 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;
|
||||
FindNearestNeighbours(Root, pivot, k, pivotPointFunc, distanceMeasure, kdTreeNodes);
|
||||
return kdTreeNodes.SelectMany(n => n.Value.Select(e => (e.Element, e.Index, n.Key))).ToArray();
|
||||
}
|
||||
|
||||
private static (KdTreeNode<T>, double) FindNearestNeighbours(KdTreeNode<T> node, T pivot, int k,
|
||||
PdfPoint pivotPoint, Func<PdfPoint, PdfPoint, double> distance, KNearestNeighboursQueue queue)
|
||||
Func<T, PdfPoint> pivotPointFunc, Func<PdfPoint, PdfPoint, double> distance, KNearestNeighboursQueue queue)
|
||||
{
|
||||
if (node == null)
|
||||
{
|
||||
@@ -314,7 +286,7 @@
|
||||
return (null, double.NaN);
|
||||
}
|
||||
|
||||
var currentDistance = distance(node.Value, pivotPoint);
|
||||
var currentDistance = distance(node.Value, pivotPointFunc(pivot));
|
||||
var currentNearestNode = node;
|
||||
|
||||
if (!queue.IsFull || currentDistance <= queue.LastDistance)
|
||||
@@ -328,8 +300,9 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
var point = pivotPointFunc(pivot);
|
||||
var currentNearestNode = node;
|
||||
var currentDistance = distance(node.Value, pivotPoint);
|
||||
var currentDistance = distance(node.Value, point);
|
||||
if ((!queue.IsFull || currentDistance <= queue.LastDistance) && !node.Element.Equals(pivot))
|
||||
{
|
||||
queue.Add(currentDistance, currentNearestNode);
|
||||
@@ -340,12 +313,12 @@
|
||||
KdTreeNode<T> newNode = null;
|
||||
double newDist = double.NaN;
|
||||
|
||||
var pointValue = node.IsAxisCutX ? pivotPoint.X : pivotPoint.Y;
|
||||
var pointValue = node.IsAxisCutX ? point.X : point.Y;
|
||||
|
||||
if (pointValue < node.L)
|
||||
{
|
||||
// start left
|
||||
(newNode, newDist) = FindNearestNeighbours(node.LeftChild, pivot, k, pivotPoint, distance, queue);
|
||||
(newNode, newDist) = FindNearestNeighbours(node.LeftChild, pivot, k, pivotPointFunc, distance, queue);
|
||||
|
||||
if (!double.IsNaN(newDist) && newDist <= currentDistance && !newNode.Element.Equals(pivot))
|
||||
{
|
||||
@@ -356,13 +329,13 @@
|
||||
|
||||
if (node.RightChild != null && pointValue + currentDistance >= node.L)
|
||||
{
|
||||
(newNode, newDist) = FindNearestNeighbours(node.RightChild, pivot, k, pivotPoint, distance, queue);
|
||||
(newNode, newDist) = FindNearestNeighbours(node.RightChild, pivot, k, pivotPointFunc, distance, queue);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// start right
|
||||
(newNode, newDist) = FindNearestNeighbours(node.RightChild, pivot, k, pivotPoint, distance, queue);
|
||||
(newNode, newDist) = FindNearestNeighbours(node.RightChild, pivot, k, pivotPointFunc, distance, queue);
|
||||
|
||||
if (!double.IsNaN(newDist) && newDist <= currentDistance && !newNode.Element.Equals(pivot))
|
||||
{
|
||||
@@ -373,7 +346,7 @@
|
||||
|
||||
if (node.LeftChild != null && pointValue - currentDistance <= node.L)
|
||||
{
|
||||
(newNode, newDist) = FindNearestNeighbours(node.LeftChild, pivot, k, pivotPoint, distance, queue);
|
||||
(newNode, newDist) = FindNearestNeighbours(node.LeftChild, pivot, k, pivotPointFunc, distance, queue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -422,14 +395,9 @@
|
||||
|
||||
if (this[key].Add(value))
|
||||
{
|
||||
LastDistance = Keys[Count - 1];
|
||||
var lastSet = Values[Count - 1];
|
||||
KdTreeNode<T> lastElement = null;
|
||||
foreach (var e in lastSet)
|
||||
{
|
||||
lastElement = e;
|
||||
}
|
||||
LastElement = lastElement;
|
||||
var last = this.Last();
|
||||
LastElement = last.Value.Last();
|
||||
LastDistance = last.Key;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -450,7 +418,23 @@
|
||||
|
||||
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>
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
namespace UglyToad.PdfPig.DocumentLayoutAnalysis
|
||||
{
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
internal class OrderedSet<T>
|
||||
{
|
||||
private readonly HashSet<T> _set;
|
||||
private readonly List<T> _list;
|
||||
|
||||
public OrderedSet() : this(EqualityComparer<T>.Default)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public OrderedSet(IEqualityComparer<T> comparer)
|
||||
{
|
||||
_set = new HashSet<T>(comparer);
|
||||
_list = new List<T>();
|
||||
}
|
||||
|
||||
public int Count => _set.Count;
|
||||
|
||||
public bool TryAdd(T item)
|
||||
{
|
||||
if (_set.Contains(item)) return false;
|
||||
|
||||
_list.Add(item);
|
||||
_set.Add(item);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_list.Clear();
|
||||
_set.Clear();
|
||||
}
|
||||
|
||||
public bool Contains(T item)
|
||||
{
|
||||
return item is not null && _set.Contains(item);
|
||||
}
|
||||
|
||||
public void CopyTo(T[] array, int arrayIndex)
|
||||
{
|
||||
_list.CopyTo(array, arrayIndex);
|
||||
}
|
||||
public List<T> GetList()
|
||||
{
|
||||
return _list;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
using Content;
|
||||
using Core;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
@@ -47,19 +48,12 @@
|
||||
/// <returns>The <see cref="TextBlock"/>s generated by the document spectrum method.</returns>
|
||||
public IReadOnlyList<TextBlock> GetBlocks(IEnumerable<Word> words)
|
||||
{
|
||||
if (words is null)
|
||||
if (words?.Any() != true)
|
||||
{
|
||||
return Array.Empty<TextBlock>();
|
||||
}
|
||||
|
||||
// Avoid multiple enumeration and unnecessary ToArray() if already a list
|
||||
var wordList = words as IReadOnlyList<Word> ?? words.ToArray();
|
||||
if (wordList.Count == 0)
|
||||
{
|
||||
return Array.Empty<TextBlock>();
|
||||
}
|
||||
|
||||
return GetBlocks(wordList,
|
||||
return GetBlocks(words.ToList(),
|
||||
options.WithinLineBounds, options.WithinLineMultiplier, options.WithinLineBinSize,
|
||||
options.BetweenLineBounds, options.BetweenLineMultiplier, options.BetweenLineBinSize,
|
||||
options.AngularDifferenceBounds,
|
||||
@@ -153,72 +147,57 @@
|
||||
{
|
||||
ParallelOptions parallelOptions = new ParallelOptions() { MaxDegreeOfParallelism = maxDegreeOfParallelism };
|
||||
|
||||
var withinLineDistList = new List<double>();
|
||||
var betweenLineDistList = new List<double>();
|
||||
var withinLineDistList = new ConcurrentBag<double>();
|
||||
var betweenLineDistList = new ConcurrentBag<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,
|
||||
() => (wl: new List<double>(), bl: new List<double>()),
|
||||
(i, _, local) =>
|
||||
{
|
||||
var word = words[i];
|
||||
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))
|
||||
// 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))
|
||||
{
|
||||
// 1.1.2 Check if the neighbour word is within the angle of the candidate
|
||||
if (wlBounds.Contains(AngleWL(word, n.Item1)))
|
||||
{
|
||||
// 1.1.2 Check if the neighbour word is within the angle of the candidate
|
||||
if (wlBounds.Contains(AngleWL(word, n.Item1)))
|
||||
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)
|
||||
{
|
||||
local.wl.Add(Distances.Euclidean(word.BoundingBox.BottomRight, n.Item1.BoundingBox.BottomLeft));
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
@@ -235,9 +214,9 @@
|
||||
/// </summary>
|
||||
/// <param name="distances">The set of distances to average.</param>
|
||||
/// <param name="binLength"></param>
|
||||
private static double? GetPeakAverageDistance(List<double> distances, int binLength = 1)
|
||||
private static double? GetPeakAverageDistance(IEnumerable<double> distances, int binLength = 1)
|
||||
{
|
||||
if (distances.Count == 0)
|
||||
if (!distances.Any())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -247,16 +226,7 @@
|
||||
throw new ArgumentException("DocstrumBoundingBoxes: the bin length must be positive when commputing peak average distance.", nameof(binLength));
|
||||
}
|
||||
|
||||
double maxDbl = distances[0];
|
||||
for (int i = 1; i < distances.Count; i++)
|
||||
{
|
||||
if (distances[i] > maxDbl)
|
||||
{
|
||||
maxDbl = distances[i];
|
||||
}
|
||||
}
|
||||
|
||||
maxDbl = Math.Ceiling(maxDbl);
|
||||
double maxDbl = Math.Ceiling(distances.Max());
|
||||
if (maxDbl > int.MaxValue)
|
||||
{
|
||||
throw new OverflowException($"Error while casting maximum distance of {maxDbl} to integer.");
|
||||
@@ -272,49 +242,30 @@
|
||||
binLength = binLength > max ? max : binLength;
|
||||
}
|
||||
|
||||
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>();
|
||||
}
|
||||
var bins = Enumerable.Range(0, (int)Math.Ceiling(max / (double)binLength) + 1)
|
||||
.Select(x => x * binLength)
|
||||
.ToDictionary(x => x, _ => new List<double>());
|
||||
|
||||
for (int i = 0; i < distances.Count; i++)
|
||||
foreach (var distance in distances)
|
||||
{
|
||||
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.");
|
||||
}
|
||||
if (bin >= binCount)
|
||||
{
|
||||
bin = binCount - 1;
|
||||
}
|
||||
bins[bin].Add(distance);
|
||||
bins[bins.Keys.ElementAt(bin)].Add(distance);
|
||||
}
|
||||
|
||||
List<double> best = null;
|
||||
for (int i = 0; i < binCount; i++)
|
||||
var best = default(List<double>);
|
||||
foreach (var bin in bins)
|
||||
{
|
||||
var bin = bins[i];
|
||||
if (best == null || bin.Count > best.Count)
|
||||
if (best == null || bin.Value.Count > best.Count)
|
||||
{
|
||||
best = bin;
|
||||
best = bin.Value;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
return best?.Average();
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -459,7 +410,28 @@
|
||||
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>
|
||||
@@ -507,30 +479,22 @@
|
||||
|
||||
if (dXj != 0)
|
||||
{
|
||||
ps.Sort((p1, p2) =>
|
||||
{
|
||||
int comp = p1.X.CompareTo(p2.X);
|
||||
return comp == 0 ? p1.Y.CompareTo(p2.Y) : comp;
|
||||
});
|
||||
ps.Sort(PdfPointXYComparer.Instance);
|
||||
}
|
||||
else if (dYj != 0)
|
||||
{
|
||||
ps.Sort((p1, p2) => p1.Y.CompareTo(p2.Y));
|
||||
ps.Sort(PdfPointYComparer.Instance);
|
||||
}
|
||||
#else
|
||||
PdfPoint[] ps = [j.Point1, j.Point2, Aj.Value, Bj.Value];
|
||||
|
||||
if (dXj != 0)
|
||||
{
|
||||
Array.Sort(ps, (p1, p2) =>
|
||||
{
|
||||
int comp = p1.X.CompareTo(p2.X);
|
||||
return comp == 0 ? p1.Y.CompareTo(p2.Y) : comp;
|
||||
});
|
||||
Array.Sort(ps, PdfPointXYComparer.Instance);
|
||||
}
|
||||
else if (dYj != 0)
|
||||
{
|
||||
Array.Sort(ps, (p1, p2) => p1.Y.CompareTo(p2.Y));
|
||||
Array.Sort(ps, PdfPointYComparer.Instance);
|
||||
}
|
||||
#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.BoundingBox.Width, 3)));
|
||||
var widths = letters.Select(x => Math.Max(Math.Round(x.Width, 3), Math.Round(x.GlyphRectangle.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.BoundingBox.Height, 3));
|
||||
var heights = letters.Select(x => Math.Round(x.GlyphRectangle.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: IBoundingBox
|
||||
public class TextBlock
|
||||
{
|
||||
/// <summary>
|
||||
/// The separator used between lines in the block.
|
||||
|
||||
@@ -59,13 +59,12 @@
|
||||
{
|
||||
letter = new Letter(
|
||||
" ",
|
||||
letter.BoundingBox,
|
||||
letter.GlyphRectangleLoose,
|
||||
letter.GlyphRectangle,
|
||||
letter.StartBaseLine,
|
||||
letter.EndBaseLine,
|
||||
letter.Width,
|
||||
letter.FontSize,
|
||||
letter.GetFont()!,
|
||||
letter.Font,
|
||||
letter.RenderingMode,
|
||||
letter.StrokeColor,
|
||||
letter.FillColor,
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
/// <summary>
|
||||
/// A line of text.
|
||||
/// </summary>
|
||||
public class TextLine : IBoundingBox
|
||||
public class TextLine
|
||||
{
|
||||
/// <summary>
|
||||
/// The separator used between words in the line.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net462;net471;net6.0;net8.0;net9.0</TargetFrameworks>
|
||||
<TargetFrameworks>netstandard2.0;net462;net471;net6.0;net8.0</TargetFrameworks>
|
||||
<LangVersion>12</LangVersion>
|
||||
<Version>0.1.14</Version>
|
||||
<Version>0.1.12-alpha001</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.BoundingBox.Width).Mode() * 1.25,
|
||||
words.SelectMany(w => w.Letters).Select(x => x.BoundingBox.Height).Mode() * 1.25,
|
||||
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,
|
||||
maxRectangleCount: maxRectangleCount,
|
||||
maxBoundQueueSize: maxBoundQueueSize);
|
||||
}
|
||||
@@ -51,7 +51,7 @@
|
||||
|
||||
if (images?.Any() == true)
|
||||
{
|
||||
bboxes.AddRange(images.Where(w => w.BoundingBox.Width > 0 && w.BoundingBox.Height > 0).Select(o => o.BoundingBox));
|
||||
bboxes.AddRange(images.Where(w => w.Bounds.Width > 0 && w.Bounds.Height > 0).Select(o => o.Bounds));
|
||||
}
|
||||
|
||||
return GetWhitespaces(bboxes,
|
||||
|
||||
@@ -51,49 +51,34 @@
|
||||
|
||||
if (options.GroupByOrientation)
|
||||
{
|
||||
var buckets = new List<Letter>[5];
|
||||
for (int i = 0; i < buckets.Length; i++) buckets[i] = new List<Letter>();
|
||||
// axis aligned
|
||||
List<Word> words = GetWords(
|
||||
letters.Where(l => l.TextOrientation == TextOrientation.Horizontal).ToList(),
|
||||
options.MaximumDistance, options.DistanceMeasureAA, options.FilterPivot,
|
||||
options.Filter, options.MaxDegreeOfParallelism);
|
||||
|
||||
foreach (var l in letters)
|
||||
{
|
||||
switch (l.TextOrientation)
|
||||
{
|
||||
case TextOrientation.Horizontal: buckets[0].Add(l); break;
|
||||
case TextOrientation.Rotate270: buckets[1].Add(l); break;
|
||||
case TextOrientation.Rotate180: buckets[2].Add(l); break;
|
||||
case TextOrientation.Rotate90: buckets[3].Add(l); break;
|
||||
default: buckets[4].Add(l); break;
|
||||
}
|
||||
}
|
||||
words.AddRange(GetWords(
|
||||
letters.Where(l => l.TextOrientation == TextOrientation.Rotate270).ToList(),
|
||||
options.MaximumDistance, options.DistanceMeasureAA, options.FilterPivot,
|
||||
options.Filter, options.MaxDegreeOfParallelism));
|
||||
|
||||
// Use a thread-safe collection to avoid lock contention.
|
||||
var results = new List<Word>(letters.Count); // Pre-allocate for performance
|
||||
words.AddRange(GetWords(
|
||||
letters.Where(l => l.TextOrientation == TextOrientation.Rotate180).ToList(),
|
||||
options.MaximumDistance, options.DistanceMeasureAA, options.FilterPivot,
|
||||
options.Filter, options.MaxDegreeOfParallelism));
|
||||
|
||||
// Limit parallelism to avoid oversubscription.
|
||||
var parallelOptions = new System.Threading.Tasks.ParallelOptions
|
||||
{
|
||||
MaxDegreeOfParallelism = options.MaxDegreeOfParallelism > 0 ? options.MaxDegreeOfParallelism : Environment.ProcessorCount
|
||||
};
|
||||
words.AddRange(GetWords(
|
||||
letters.Where(l => l.TextOrientation == TextOrientation.Rotate90).ToList(),
|
||||
options.MaximumDistance, options.DistanceMeasureAA, options.FilterPivot,
|
||||
options.Filter, options.MaxDegreeOfParallelism));
|
||||
|
||||
// Use partitioner for better load balancing and avoid ConcurrentBag overhead
|
||||
System.Threading.Tasks.Parallel.ForEach(
|
||||
System.Collections.Concurrent.Partitioner.Create(0, buckets.Length),
|
||||
parallelOptions,
|
||||
range =>
|
||||
{
|
||||
for (int i = range.Item1; i < range.Item2; i++)
|
||||
{
|
||||
if (buckets[i].Count == 0) continue;
|
||||
var measure = (i == 4) ? options.DistanceMeasure : options.DistanceMeasureAA;
|
||||
var words = GetWords(buckets[i], options.MaximumDistance, measure, options.FilterPivot, options.Filter, options.MaxDegreeOfParallelism);
|
||||
lock (results)
|
||||
{
|
||||
results.AddRange(words);
|
||||
}
|
||||
}
|
||||
});
|
||||
results.TrimExcess();
|
||||
return results;
|
||||
// not axis aligned
|
||||
words.AddRange(GetWords(
|
||||
letters.Where(l => l.TextOrientation == TextOrientation.Other).ToList(),
|
||||
options.MaximumDistance, options.DistanceMeasure, options.FilterPivot,
|
||||
options.Filter, options.MaxDegreeOfParallelism));
|
||||
|
||||
return words;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -163,8 +148,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.BoundingBox.Width),
|
||||
Math.Abs(l2.BoundingBox.Width)),
|
||||
Math.Abs(l1.GlyphRectangle.Width),
|
||||
Math.Abs(l2.GlyphRectangle.Width)),
|
||||
Math.Abs(l1.Width)),
|
||||
Math.Abs(l2.Width)),
|
||||
l1.PointSize), l2.PointSize) * 0.2;
|
||||
|
||||
@@ -770,8 +770,6 @@ namespace UglyToad.PdfPig.Fonts.CompactFontFormat.CharStrings
|
||||
}
|
||||
}
|
||||
|
||||
values.TrimExcess();
|
||||
|
||||
return new Type2CharStrings.CommandSequence(values, commandIdentifiers);
|
||||
}
|
||||
|
||||
|
||||
@@ -257,10 +257,6 @@
|
||||
gidToStringIdAndNameMap[gid++] = pair;
|
||||
}
|
||||
|
||||
#if NET
|
||||
gidToStringIdAndNameMap.TrimExcess();
|
||||
#endif
|
||||
|
||||
glyphIdToStringIdAndName = gidToStringIdAndNameMap;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
/// <summary>
|
||||
/// The font matrix for this font.
|
||||
/// </summary>
|
||||
public TransformationMatrix FontMatrix => TopDictionary.FontMatrix ?? TransformationMatrix.FromValues(0.001, 0, 0, 0.001, 0, 0);
|
||||
public TransformationMatrix FontMatrix => TopDictionary.FontMatrix.HasValue ? TopDictionary.FontMatrix.Value : TransformationMatrix.FromValues(0.001, 0, 0, 0.001, 0, 0);
|
||||
|
||||
/// <summary>
|
||||
/// The value of Weight from the top dictionary or <see langword="null"/>.
|
||||
@@ -219,26 +219,22 @@
|
||||
|
||||
public override TransformationMatrix? GetFontMatrix(string characterName)
|
||||
{
|
||||
bool hasDictionary = TryGetFontDictionaryForCharacter(characterName, out var dictionary);
|
||||
// BobLd: It seems PdfBox just returns TopDictionary.FontMatrix
|
||||
// But see https://bugs.ghostscript.com/show_bug.cgi?id=690724
|
||||
// and https://github.com/veraPDF/veraPDF-library/issues/1010
|
||||
// TODO - We might need to multiply both matrices together
|
||||
|
||||
if (TopDictionary.FontMatrix.HasValue &&
|
||||
hasDictionary &&
|
||||
dictionary.FontMatrix.HasValue)
|
||||
{
|
||||
return TopDictionary.FontMatrix.Value.Multiply(dictionary.FontMatrix.Value);
|
||||
}
|
||||
|
||||
if (TopDictionary.FontMatrix.HasValue)
|
||||
if (TopDictionary.FontMatrix is not null)
|
||||
{
|
||||
return TopDictionary.FontMatrix;
|
||||
}
|
||||
|
||||
if (hasDictionary && dictionary.FontMatrix.HasValue)
|
||||
if (!TryGetFontDictionaryForCharacter(characterName, out var dictionary))
|
||||
{
|
||||
return dictionary.FontMatrix;
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
return dictionary.FontMatrix;
|
||||
}
|
||||
|
||||
private bool TryGetPrivateDictionaryForCharacter(string characterName, out CompactFontFormatPrivateDictionary dictionary)
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
namespace UglyToad.PdfPig.Fonts.SystemFonts
|
||||
{
|
||||
#if NET
|
||||
using System.Collections.Generic;
|
||||
|
||||
internal sealed class IOSSystemFontLister : ISystemFontLister
|
||||
{
|
||||
// Very early version, intended to help developing support for iOS
|
||||
|
||||
public IEnumerable<SystemFontRecord> GetAllFonts()
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -102,10 +102,6 @@ namespace UglyToad.PdfPig.Fonts.SystemFonts
|
||||
{
|
||||
lister = new MacSystemFontLister();
|
||||
}
|
||||
else if (OperatingSystem.IsIOS())
|
||||
{
|
||||
lister = new IOSSystemFontLister();
|
||||
}
|
||||
#endif
|
||||
else
|
||||
{
|
||||
@@ -117,7 +113,7 @@ namespace UglyToad.PdfPig.Fonts.SystemFonts
|
||||
#error Missing ISystemFontLister for target framework
|
||||
#endif
|
||||
|
||||
AvailableFonts = new Lazy<IReadOnlyList<SystemFontRecord>>(() => lister.GetAllFonts().ToArray());
|
||||
AvailableFonts = new Lazy<IReadOnlyList<SystemFontRecord>>(() => lister.GetAllFonts().ToList());
|
||||
}
|
||||
|
||||
private readonly ConcurrentDictionary<string, string> nameToFileNameMap = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Tables;
|
||||
using UglyToad.PdfPig.Fonts.CompactFontFormat;
|
||||
|
||||
/// <summary>
|
||||
/// Parses TrueType fonts.
|
||||
@@ -60,36 +59,7 @@
|
||||
|
||||
private static TrueTypeFont ParseTables(float version, IReadOnlyDictionary<string, TrueTypeHeaderTable> tables, TrueTypeDataBytes data)
|
||||
{
|
||||
bool isPostScript = false;
|
||||
CompactFontFormatFontCollection? cffFontCollection = null;
|
||||
|
||||
if (tables.TryGetValue(TrueTypeHeaderTable.Cff, out var cffTable))
|
||||
{
|
||||
isPostScript = true;
|
||||
try
|
||||
{
|
||||
/*
|
||||
* The presence of a CFF (Compact Font Format) table in a TrueType font creates a hybrid situation where the font
|
||||
* container uses TrueType structure but contains PostScript-based glyph descriptions. According to the OpenType
|
||||
* specification, when a TrueType font contains a CFF table instead of a traditional glyf table, it indicates
|
||||
* "an OpenType font with PostScript outlines". This creates what's known as an OpenType CFF font, which uses
|
||||
* PostScript Type 2 charstrings for glyph descriptions rather than TrueType quadratic curves.
|
||||
*
|
||||
* This is to fix P2P-33713919.pdf
|
||||
* See https://github.com/BobLd/PdfPig.Rendering.Skia/issues/46
|
||||
* TODO - Add test coverage and need to review if the logic belongs here
|
||||
*/
|
||||
|
||||
data.Seek(cffTable.Offset);
|
||||
var buffer = data.ReadByteArray((int)cffTable.Length);
|
||||
cffFontCollection = CompactFontFormatParser.Parse(new CompactFontFormatData(buffer));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine(e);
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
var isPostScript = tables.ContainsKey(TrueTypeHeaderTable.Cff);
|
||||
|
||||
var builder = new TableRegister.Builder();
|
||||
|
||||
@@ -132,7 +102,7 @@
|
||||
{
|
||||
builder.Os2Table = TableParser.Parse<Os2Table>(os2Table, data, builder);
|
||||
}
|
||||
|
||||
|
||||
if (!isPostScript)
|
||||
{
|
||||
if (!tables.TryGetValue(TrueTypeHeaderTable.Loca, out var indexToLocationHeaderTable))
|
||||
@@ -151,11 +121,11 @@
|
||||
|
||||
// glyf
|
||||
builder.GlyphDataTable = GlyphDataTable.Load(data, glyphHeaderTable, builder);
|
||||
|
||||
OptionallyParseTables(tables, data, builder);
|
||||
}
|
||||
|
||||
OptionallyParseTables(tables, data, builder);
|
||||
|
||||
return new TrueTypeFont(version, tables, builder.Build(), cffFontCollection);
|
||||
return new TrueTypeFont(version, tables, builder.Build());
|
||||
}
|
||||
|
||||
internal static NameTable GetNameTable(TrueTypeDataBytes data)
|
||||
@@ -164,7 +134,7 @@
|
||||
{
|
||||
throw new ArgumentNullException(nameof(data));
|
||||
}
|
||||
|
||||
|
||||
// Read these data points to move to the correct data location.
|
||||
data.Read32Fixed();
|
||||
int numberOfTables = data.ReadUnsignedShort();
|
||||
|
||||
@@ -121,23 +121,14 @@
|
||||
|
||||
for (var i = 0; i < glyphCount; i++)
|
||||
{
|
||||
var offset = offsets[i];
|
||||
|
||||
if (offsets[i + 1] <= offset)
|
||||
if (offsets[i + 1] <= offsets[i])
|
||||
{
|
||||
// empty glyph
|
||||
result[i] = emptyGlyph;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Invalid table, just sub in the empty glyph
|
||||
if (offset >= data.Length)
|
||||
{
|
||||
result[i] = emptyGlyph;
|
||||
continue;
|
||||
}
|
||||
|
||||
data.Seek(offset);
|
||||
data.Seek(offsets[i]);
|
||||
|
||||
var contourCount = data.ReadSignedShort();
|
||||
|
||||
@@ -240,15 +231,9 @@
|
||||
flags = (CompositeGlyphFlags)data.ReadUnsignedShort();
|
||||
var glyphIndex = data.ReadUnsignedShort();
|
||||
|
||||
if (glyphIndex >= glyphs.Length)
|
||||
{
|
||||
// Unsure why this happens but fixes #1213
|
||||
continue; // TODO - Is there a better fix?
|
||||
}
|
||||
var childGlyph = glyphs[glyphIndex];
|
||||
|
||||
IGlyphDescription? childGlyph = glyphs[glyphIndex];
|
||||
|
||||
if (childGlyph is null)
|
||||
if (childGlyph == null)
|
||||
{
|
||||
if (!compositeLocations.TryGetValue(glyphIndex, out var missingComposite))
|
||||
{
|
||||
@@ -325,7 +310,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
builderGlyph ??= emptyGlyph;
|
||||
builderGlyph = builderGlyph ?? emptyGlyph;
|
||||
|
||||
return new Glyph(false, builderGlyph.Instructions, builderGlyph.EndPointsOfContours, builderGlyph.Points, compositeLocation.Bounds);
|
||||
}
|
||||
@@ -344,15 +329,7 @@
|
||||
|
||||
for (int j = 0; j < numberOfRepeats; j++)
|
||||
{
|
||||
int p = i + j + 1;
|
||||
if (p >= result.Length)
|
||||
{
|
||||
// Unsure why this happens but fixes #1199
|
||||
// TODO - Is there a better fix?
|
||||
break;
|
||||
}
|
||||
|
||||
result[p] = result[i];
|
||||
result[i + j + 1] = result[i];
|
||||
}
|
||||
|
||||
i += numberOfRepeats;
|
||||
@@ -432,7 +409,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class CompositeComponent
|
||||
private class CompositeComponent
|
||||
{
|
||||
public int Index { get; }
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
using Core;
|
||||
using Parser;
|
||||
using Tables.CMapSubTables;
|
||||
using UglyToad.PdfPig.Fonts.CompactFontFormat;
|
||||
|
||||
/// <summary>
|
||||
/// A TrueType font.
|
||||
@@ -55,33 +54,17 @@
|
||||
/// </summary>
|
||||
public int NumberOfTables { get; }
|
||||
|
||||
// TODO - It would be better to use 'PdfCidCompactFontFormatFont' but the class is not accessible from here.
|
||||
private readonly CompactFontFormatFontCollection? cffFontCollection;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="TrueTypeFont"/>.
|
||||
/// </summary>
|
||||
internal TrueTypeFont(float version, IReadOnlyDictionary<string, TrueTypeHeaderTable> tableHeaders, TableRegister tableRegister, CompactFontFormatFontCollection? cffFontCollection)
|
||||
internal TrueTypeFont(float version, IReadOnlyDictionary<string, TrueTypeHeaderTable> tableHeaders, TableRegister tableRegister)
|
||||
{
|
||||
Version = version;
|
||||
TableHeaders = tableHeaders ?? throw new ArgumentNullException(nameof(tableHeaders));
|
||||
TableRegister = tableRegister ?? throw new ArgumentNullException(nameof(tableRegister));
|
||||
NumberOfTables = tableHeaders.Count;
|
||||
|
||||
/*
|
||||
* The presence of a CFF (Compact Font Format) table in a TrueType font creates a hybrid situation where the font
|
||||
* container uses TrueType structure but contains PostScript-based glyph descriptions. According to the OpenType
|
||||
* specification, when a TrueType font contains a CFF table instead of a traditional glyf table, it indicates
|
||||
* "an OpenType font with PostScript outlines". This creates what's known as an OpenType CFF font, which uses
|
||||
* PostScript Type 2 charstrings for glyph descriptions rather than TrueType quadratic curves.
|
||||
*
|
||||
* This is to fix P2P-33713919.pdf
|
||||
* See https://github.com/BobLd/PdfPig.Rendering.Skia/issues/46
|
||||
* TODO - Add test coverage and need to review if the logic belongs here
|
||||
*/
|
||||
this.cffFontCollection = cffFontCollection;
|
||||
|
||||
if (TableRegister.CMapTable is not null)
|
||||
if (TableRegister.CMapTable != null)
|
||||
{
|
||||
const int encodingSymbol = 0;
|
||||
const int encodingUnicode = 1;
|
||||
@@ -89,19 +72,19 @@
|
||||
|
||||
foreach (var subTable in TableRegister.CMapTable.SubTables)
|
||||
{
|
||||
if (WindowsSymbolCMap is null
|
||||
if (WindowsSymbolCMap == null
|
||||
&& subTable.PlatformId == TrueTypeCMapPlatform.Windows
|
||||
&& subTable.EncodingId == encodingSymbol)
|
||||
{
|
||||
WindowsSymbolCMap = subTable;
|
||||
}
|
||||
else if (WindowsUnicodeCMap is null
|
||||
else if (WindowsUnicodeCMap == null
|
||||
&& subTable.PlatformId == TrueTypeCMapPlatform.Windows
|
||||
&& subTable.EncodingId == encodingUnicode)
|
||||
{
|
||||
WindowsUnicodeCMap = subTable;
|
||||
}
|
||||
else if (MacRomanCMap is null
|
||||
else if (MacRomanCMap == null
|
||||
&& subTable.PlatformId == TrueTypeCMapPlatform.Macintosh
|
||||
&& subTable.EncodingId == encodingMacRoman)
|
||||
{
|
||||
@@ -124,36 +107,8 @@
|
||||
{
|
||||
boundingBox = default(PdfRectangle);
|
||||
|
||||
if (TableRegister.GlyphTable is null)
|
||||
if (TableRegister.GlyphTable == null)
|
||||
{
|
||||
if (cffFontCollection is not null)
|
||||
{
|
||||
/*
|
||||
* The presence of a CFF (Compact Font Format) table in a TrueType font creates a hybrid situation where the font
|
||||
* container uses TrueType structure but contains PostScript-based glyph descriptions. According to the OpenType
|
||||
* specification, when a TrueType font contains a CFF table instead of a traditional glyf table, it indicates
|
||||
* "an OpenType font with PostScript outlines". This creates what's known as an OpenType CFF font, which uses
|
||||
* PostScript Type 2 charstrings for glyph descriptions rather than TrueType quadratic curves.
|
||||
*
|
||||
* This is to fix P2P-33713919.pdf
|
||||
* See https://github.com/BobLd/PdfPig.Rendering.Skia/issues/46
|
||||
* TODO - Add test coverage and need to review if the logic belongs here
|
||||
*/
|
||||
|
||||
var name = cffFontCollection.FirstFont.GetCharacterName(characterCode, true); // TODO cid?
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var bbox = cffFontCollection.FirstFont.GetCharacterBoundingBox(name);
|
||||
if (bbox.HasValue)
|
||||
{
|
||||
boundingBox = bbox.Value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -188,30 +143,8 @@
|
||||
{
|
||||
path = null;
|
||||
|
||||
if (TableRegister.GlyphTable is null)
|
||||
if (TableRegister.GlyphTable == null)
|
||||
{
|
||||
if (cffFontCollection is not null)
|
||||
{
|
||||
/*
|
||||
* The presence of a CFF (Compact Font Format) table in a TrueType font creates a hybrid situation where the font
|
||||
* container uses TrueType structure but contains PostScript-based glyph descriptions. According to the OpenType
|
||||
* specification, when a TrueType font contains a CFF table instead of a traditional glyf table, it indicates
|
||||
* "an OpenType font with PostScript outlines". This creates what's known as an OpenType CFF font, which uses
|
||||
* PostScript Type 2 charstrings for glyph descriptions rather than TrueType quadratic curves.
|
||||
*
|
||||
* This is to fix P2P-33713919.pdf
|
||||
* See https://github.com/BobLd/PdfPig.Rendering.Skia/issues/46
|
||||
* TODO - Add test coverage and need to review if the logic belongs here
|
||||
*/
|
||||
|
||||
var name = cffFontCollection.FirstFont.GetCharacterName(characterCode, true);
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return cffFontCollection.FirstFont.TryGetPath(name, out path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -255,7 +188,7 @@
|
||||
{
|
||||
width = 0;
|
||||
|
||||
if (TableRegister.HorizontalMetricsTable is null)
|
||||
if (TableRegister.HorizontalMetricsTable == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -277,7 +210,7 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
if (TableRegister.CMapTable is null)
|
||||
if (TableRegister.CMapTable == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -18,19 +18,16 @@
|
||||
|
||||
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, StackDepthGuard stackDepthGuard)
|
||||
public static Type1Font Parse(IInputBytes inputBytes, int length1, int length2)
|
||||
{
|
||||
// Sometimes the entire PFB file including the header bytes can be included which prevents parsing in the normal way.
|
||||
var isEntirePfbFile = inputBytes.Peek() == PfbFileIndicator;
|
||||
@@ -45,15 +42,15 @@
|
||||
inputBytes = new MemoryInputBytes(ascii);
|
||||
}
|
||||
|
||||
var scanner = new CoreTokenScanner(inputBytes, false, stackDepthGuard);
|
||||
var scanner = new CoreTokenScanner(inputBytes, false);
|
||||
|
||||
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(Separators, StringSplitOptions.RemoveEmptyEntries);
|
||||
var parts = comment.Data.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length == 3)
|
||||
{
|
||||
name = parts[1];
|
||||
@@ -63,9 +60,11 @@
|
||||
name = "Unknown";
|
||||
}
|
||||
|
||||
while (scanner.MoveNext() && scanner.CurrentToken is CommentToken)
|
||||
var comments = new List<string>();
|
||||
|
||||
while (scanner.MoveNext() && scanner.CurrentToken is CommentToken commentToken)
|
||||
{
|
||||
// We ignore comments
|
||||
comments.Add(commentToken.Data);
|
||||
}
|
||||
|
||||
var dictionaries = new List<DictionaryToken>();
|
||||
@@ -442,7 +441,7 @@
|
||||
return null;
|
||||
}
|
||||
|
||||
private sealed class PreviousTokenSet
|
||||
private class PreviousTokenSet
|
||||
{
|
||||
private readonly IToken[] tokens = new IToken[3];
|
||||
|
||||
|
||||
@@ -62,11 +62,6 @@
|
||||
/// </summary>
|
||||
public PdfRectangle? GetCharacterBoundingBox(string characterName)
|
||||
{
|
||||
if (string.Equals(characterName, GlyphList.NotDefined, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var glyph = GetCharacterPath(characterName);
|
||||
return PdfSubpath.GetBoundingRectangle(glyph);
|
||||
}
|
||||
@@ -99,13 +94,8 @@
|
||||
/// <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;
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net462;net471;net6.0;net8.0;net9.0</TargetFrameworks>
|
||||
<TargetFrameworks>netstandard2.0;net462;net471;net6.0;net8.0</TargetFrameworks>
|
||||
<LangVersion>12</LangVersion>
|
||||
<Version>0.1.14</Version>
|
||||
<Version>0.1.12-alpha001</Version>
|
||||
<IsTestProject>False</IsTestProject>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
<AssemblyOriginatorKeyFile>..\pdfpig.snk</AssemblyOriginatorKeyFile>
|
||||
<Nullable>annotations</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="Resources\AdobeFontMetrics\*" />
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
namespace UglyToad.PdfPig.Tests.ContentStream
|
||||
{
|
||||
using PdfPig.Core;
|
||||
using System.Globalization;
|
||||
|
||||
public class IndirectReferenceTests
|
||||
{
|
||||
@@ -34,59 +33,50 @@
|
||||
[Fact]
|
||||
public void IndirectReferenceHashTest()
|
||||
{
|
||||
CultureInfo lastCulture = CultureInfo.CurrentCulture;
|
||||
CultureInfo.CurrentCulture = new CultureInfo("en-US");
|
||||
try
|
||||
{
|
||||
var reference0 = new IndirectReference(1574, 690);
|
||||
Assert.Equal(1574, reference0.ObjectNumber);
|
||||
Assert.Equal(690, reference0.Generation);
|
||||
var reference0 = new IndirectReference(1574, 690);
|
||||
Assert.Equal(1574, reference0.ObjectNumber);
|
||||
Assert.Equal(690, reference0.Generation);
|
||||
|
||||
var reference1 = new IndirectReference(-1574, 690);
|
||||
Assert.Equal(-1574, reference1.ObjectNumber);
|
||||
Assert.Equal(690, reference1.Generation);
|
||||
var reference1 = new IndirectReference(-1574, 690);
|
||||
Assert.Equal(-1574, reference1.ObjectNumber);
|
||||
Assert.Equal(690, reference1.Generation);
|
||||
|
||||
var reference2 = new IndirectReference(58949797283757, 16);
|
||||
Assert.Equal(58949797283757, reference2.ObjectNumber);
|
||||
Assert.Equal(16, reference2.Generation);
|
||||
var reference2 = new IndirectReference(58949797283757, 16);
|
||||
Assert.Equal(58949797283757, reference2.ObjectNumber);
|
||||
Assert.Equal(16, reference2.Generation);
|
||||
|
||||
var reference3 = new IndirectReference(-58949797283757, ushort.MaxValue);
|
||||
Assert.Equal(-58949797283757, reference3.ObjectNumber);
|
||||
Assert.Equal(ushort.MaxValue, reference3.Generation);
|
||||
var reference3 = new IndirectReference(-58949797283757, ushort.MaxValue);
|
||||
Assert.Equal(-58949797283757, reference3.ObjectNumber);
|
||||
Assert.Equal(ushort.MaxValue, reference3.Generation);
|
||||
|
||||
var reference4 = new IndirectReference(140737488355327, ushort.MaxValue);
|
||||
Assert.Equal(140737488355327, reference4.ObjectNumber);
|
||||
Assert.Equal(ushort.MaxValue, reference4.Generation);
|
||||
var reference4 = new IndirectReference(140737488355327, ushort.MaxValue);
|
||||
Assert.Equal(140737488355327, reference4.ObjectNumber);
|
||||
Assert.Equal(ushort.MaxValue, reference4.Generation);
|
||||
|
||||
var reference5 = new IndirectReference(-140737488355327, ushort.MaxValue);
|
||||
Assert.Equal(-140737488355327, reference5.ObjectNumber);
|
||||
Assert.Equal(ushort.MaxValue, reference5.Generation);
|
||||
var reference5 = new IndirectReference(-140737488355327, ushort.MaxValue);
|
||||
Assert.Equal(-140737488355327, reference5.ObjectNumber);
|
||||
Assert.Equal(ushort.MaxValue, reference5.Generation);
|
||||
|
||||
var ex0 = Assert.Throws<ArgumentOutOfRangeException>(() => new IndirectReference(140737488355328, 0));
|
||||
Assert.StartsWith("Object number must be between -140,737,488,355,327 and 140,737,488,355,327.", ex0.Message);
|
||||
var ex1 = Assert.Throws<ArgumentOutOfRangeException>(() => new IndirectReference(-140737488355328, 0));
|
||||
Assert.StartsWith("Object number must be between -140,737,488,355,327 and 140,737,488,355,327.", ex1.Message);
|
||||
var ex0 = Assert.Throws<ArgumentOutOfRangeException>(() => new IndirectReference(140737488355328, 0));
|
||||
Assert.StartsWith("Object number must be between -140,737,488,355,327 and 140,737,488,355,327.", ex0.Message);
|
||||
var ex1 = Assert.Throws<ArgumentOutOfRangeException>(() => new IndirectReference(-140737488355328, 0));
|
||||
Assert.StartsWith("Object number must be between -140,737,488,355,327 and 140,737,488,355,327.", ex1.Message);
|
||||
|
||||
var ex2 = Assert.Throws<ArgumentOutOfRangeException>(() => new IndirectReference(1574, -1));
|
||||
Assert.StartsWith("Generation number must not be a negative value.", ex2.Message);
|
||||
|
||||
// We make sure object number is still correct even if generation is not
|
||||
var reference6 = new IndirectReference(1574, int.MaxValue);
|
||||
Assert.Equal(1574, reference6.ObjectNumber);
|
||||
|
||||
var reference7 = new IndirectReference(-1574, ushort.MaxValue + 10);
|
||||
Assert.Equal(-1574, reference7.ObjectNumber);
|
||||
|
||||
var ex2 = Assert.Throws<ArgumentOutOfRangeException>(() => new IndirectReference(1574, -1));
|
||||
Assert.StartsWith("Generation number must not be a negative value.", ex2.Message);
|
||||
var reference9 = new IndirectReference(-140737488355327, ushort.MaxValue + 10);
|
||||
Assert.Equal(-140737488355327, reference9.ObjectNumber);
|
||||
|
||||
// We make sure object number is still correct even if generation is not
|
||||
var reference6 = new IndirectReference(1574, int.MaxValue);
|
||||
Assert.Equal(1574, reference6.ObjectNumber);
|
||||
|
||||
var reference7 = new IndirectReference(-1574, ushort.MaxValue + 10);
|
||||
Assert.Equal(-1574, reference7.ObjectNumber);
|
||||
|
||||
var reference9 = new IndirectReference(-140737488355327, ushort.MaxValue + 10);
|
||||
Assert.Equal(-140737488355327, reference9.ObjectNumber);
|
||||
|
||||
var reference10 = new IndirectReference(140737488355327, ushort.MaxValue * 10);
|
||||
Assert.Equal(140737488355327, reference10.ObjectNumber);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CultureInfo.CurrentCulture = lastCulture;
|
||||
}
|
||||
var reference10 = new IndirectReference(140737488355327, ushort.MaxValue * 10);
|
||||
Assert.Equal(140737488355327, reference10.ObjectNumber);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -2,23 +2,16 @@
|
||||
{
|
||||
internal static class DlaHelper
|
||||
{
|
||||
private static readonly string DlaFolder = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "Dla", "Documents"));
|
||||
private static readonly string IntegrationFolder = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "Integration", "Documents"));
|
||||
|
||||
public static string GetDocumentPath(string name, bool isPdf = true)
|
||||
{
|
||||
var documentFolder = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "Dla", "Documents"));
|
||||
|
||||
if (!name.EndsWith(".pdf") && isPdf)
|
||||
{
|
||||
name += ".pdf";
|
||||
}
|
||||
|
||||
string doc = Path.Combine(DlaFolder, name);
|
||||
if (File.Exists(doc))
|
||||
{
|
||||
return doc;
|
||||
}
|
||||
|
||||
return Path.Combine(IntegrationFolder, name);
|
||||
return Path.Combine(documentFolder, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
Random Big Title Lorem Ipsum text with lists Lorem ipsum dolor sit amet, consectetur adipiscing elit. In sodales gravida felis, in rhoncus velit rutrum at. Curabitur hendrerit dapibus nulla, ut hendrerit diam imperdiet quis. Pellentesque id neque ali-quam, pulvinar neque in, vulputate elit. Pel-lentesque ut erat sit amet massa suscipit ullamcor-per. Sed porttitor viverra convallis. Duis vitae sem-per metus. Pellentesque eros purus, egestas eget velit eget, elementum aliquet velit. Suspendisse potenti. Nulla vitae massa rutrum, blandit erat vi-tae, aliquet arcu. Aenean feugiat leo sed enim sodales vehicula. Sus-pendisse tempus hendrerit magna sagittis dictum. Duis ultrices dapibus egestas. Cras eu felis eu lectus suscipit pharetra at at lacus. Nulla facilisi. Proin in-terdum faucibus elit nec rhoncus. Proin sodaless metus sed tincidunt hendrerit. • Duis leo enim, convallis sit amet orci eget, condimentum mattis mi ; • Etiam dolor erat, maximus nec mi sed, con-vallis convallis orci ; • Morbi viverra diam in diam cursus, vitae aliquet velit tempus ; • Donec at nisi fermentum, ultricies odio eget, egestas massa at nisi fermentum, ul-tricies odio eget, egestas massa. Donec ultricies cursus odio sed rutrum. Nam ven-enatis metus vitae elementum scelerisque. Ali-quam tempor sapien at turpis posuere eleifend. Sed placerat posuere nunc vel efficitur. Quisque auctor felis vel lectus dictum fringilla. Quisque vo-lutpat pulvinar© elit. Aliquam ultrices feugiat ali-quam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Sus-pendisse imperdiet ex lorem, porta bibendum pu-rus ultricies id. Integer vel lacus sapien. Nam sodales ante eu risus facilisis placerat. Aliquam suscipit pulvinar ultricies. Aenean pulvinar, ex ac fermentum egestas, erat nisi feugiat velit, vitae suscipit tellus odio vitae quam. Morbi elementum sem in elit posuere, non rhoncus magna fringilla. Phasellus cursus in dolor laoreet rutrum. Curabitur tincidunt risus ullamcor-per, vehicula velit at, pulvinar metus. Donec quis ante leo. Vivamus pharetra, nisl ac vehi-cula tempor, tellus lacus aliquam sapien, eu congue nibh quam sit amet odio. Quisque metus arcu, sem-per nec consequat eu, pellentesque vel sem. Sed purus risus, tincidunt¹ sit amet dictum vitae, euis-mod id nibh. Praesent ultrices libero quis enim porta, sit amet pellentesque augue pretium. Viva-mus nec molestie nunc. Donec finibus enim nec tel-lus laoreet elementum. Curabitur efficitur placerat dolor et semper. Morbi laoreet dui eu tortor luctus, nec ultrices do-lor ullamcorper. Ut gravida sed nisl a efficitur. In tincidunt orci a condimentum semper. Suspendisse scelerisque fermentum lacinia. Vestibulum sit amet ornare tellus, aliquet euismod mauris. Cras suscipit venenatis ultrices. Sed diam erat, aliquet a tellus ut, viverra 12º ongue magna. Cras id justo tortor. Mauris in tortor vulputate, pellentesque nisl ac, facilisis ligula. Class aptent taciti² sociosqu ad li-tora torquent per conubia nostra³, per inceptos himenaeos. Aliquam eget dolor turpis. Mauris id molestie tellus. Sed elementum molestie nisi, at ali-quet sem vehicula nec. Morbi tempus nulla enim, a vulputate magna €51 luctus £66 eu. Fusce sodales, libero quis suscipit ultrices, metus erat auctor urna, sit amet dictum arcu tortor eu metus. 1. Ut volutpat, velit at interdum consectetur, nisl lorem consequat mauris, feugiat dignissim tellus massa ut nisl. 2. Praesent at est nisi. Pellentesque rutrum lorem sed dui accumsan gravida. 3. Pellentesque dictum nisl vitae urna luctus, congue pulvinar mi congue. Morbi vestibulum varius ipsum nec molestie. Proin auctor efficitur diam ut luctus. Phasellus cursus maximus ultricies. Mauris eu neque ut sem semper tempus. Curabitur non lorem eu nunc lobortis vi-verra at in diam. Pellentesque euismod purus a leo lobortis tempor. Maecenas mollis ligula at sem sus-cipit fringilla. Mauris sollicitudin tincidunt lectus id tempor. Etiam ut nisi est.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -4,84 +4,19 @@
|
||||
|
||||
public class NearestNeighbourWordExtractorTests
|
||||
{
|
||||
public static IEnumerable<object[]> DataWords => new[]
|
||||
[Fact]
|
||||
public void Words2559Doc()
|
||||
{
|
||||
new object[]
|
||||
{
|
||||
"2559 words.pdf",
|
||||
5118,
|
||||
2559
|
||||
},
|
||||
new object[]
|
||||
{
|
||||
"fseprd1102849.pdf",
|
||||
12903,
|
||||
11177
|
||||
},
|
||||
new object[]
|
||||
{
|
||||
"90 180 270 rotated.pdf",
|
||||
589,
|
||||
292
|
||||
},
|
||||
new object[]
|
||||
{
|
||||
"complex rotated.pdf",
|
||||
805,
|
||||
403
|
||||
},
|
||||
new object[]
|
||||
{
|
||||
"no horizontal distance.pdf",
|
||||
4,
|
||||
2
|
||||
},
|
||||
new object[]
|
||||
{
|
||||
"no vertical distance.pdf",
|
||||
22,
|
||||
10
|
||||
},
|
||||
new object[]
|
||||
{
|
||||
"no vertical horizontal distance.pdf",
|
||||
4,
|
||||
2
|
||||
},
|
||||
new object[]
|
||||
{
|
||||
"Random 2 Columns Lists Hyph - Justified.pdf",
|
||||
1191,
|
||||
607
|
||||
},
|
||||
new object[]
|
||||
{
|
||||
"caly-issues-56-1.pdf",
|
||||
184,
|
||||
156
|
||||
},
|
||||
new object[]
|
||||
{
|
||||
"caly-issues-58-2.pdf",
|
||||
49,
|
||||
49
|
||||
},
|
||||
};
|
||||
// Microsoft Word count of words = 2559
|
||||
|
||||
[SkippableTheory]
|
||||
[MemberData(nameof(DataWords))]
|
||||
public void WordCount(string path, int wordCount, int noSpacesWordCount)
|
||||
{
|
||||
using (var document = PdfDocument.Open(DlaHelper.GetDocumentPath(path)))
|
||||
using (var document = PdfDocument.Open(DlaHelper.GetDocumentPath("2559 words.pdf")))
|
||||
{
|
||||
var page = document.GetPage(1);
|
||||
var words = NearestNeighbourWordExtractor.Instance.GetWords(page.Letters).ToArray();
|
||||
|
||||
Assert.Equal(wordCount, words.Length);
|
||||
|
||||
var noSpacesWords = words.Where(x => !string.IsNullOrEmpty(x.Text.Trim())).ToArray();
|
||||
|
||||
Assert.Equal(noSpacesWordCount, noSpacesWords.Length);
|
||||
Assert.Equal(2559, noSpacesWords.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
namespace UglyToad.PdfPig.Tests.Dla
|
||||
{
|
||||
using PdfFonts;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UglyToad.PdfPig.Content;
|
||||
using UglyToad.PdfPig.Core;
|
||||
using UglyToad.PdfPig.DocumentLayoutAnalysis;
|
||||
using UglyToad.PdfPig.DocumentLayoutAnalysis.ReadingOrderDetector;
|
||||
using UglyToad.PdfPig.Core;
|
||||
|
||||
public class UnsupervisedReadingOrderTests
|
||||
{
|
||||
@@ -60,11 +62,10 @@
|
||||
private static TextBlock CreateFakeTextBlock(PdfRectangle boundingBox)
|
||||
{
|
||||
var letter = new Letter("a",
|
||||
boundingBox,
|
||||
boundingBox,
|
||||
boundingBox.BottomLeft,
|
||||
boundingBox.BottomRight,
|
||||
10, 1, (FontDetails)null, TextRenderingMode.NeitherClip, null, null, 0, 0);// These don't matter
|
||||
10, 1, null, TextRenderingMode.NeitherClip, null, null, 0, 0);// These don't matter
|
||||
var leftTextBlock = new TextBlock(new[] { new TextLine(new[] { new Word(new[] { letter }) }) });
|
||||
return leftTextBlock;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
namespace UglyToad.PdfPig.Tests.Filters
|
||||
{
|
||||
using PdfPig.Core;
|
||||
using PdfPig.Filters;
|
||||
using PdfPig.Tokens;
|
||||
|
||||
@@ -12,32 +11,15 @@
|
||||
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);
|
||||
var result = filter.Encode(inputStream, parameters, 0);
|
||||
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, new StackDepthGuard(256));
|
||||
var tokenScanner = new CoreTokenScanner(byteArrayInput, false);
|
||||
|
||||
Assert.True(tokenScanner.MoveNext());
|
||||
Assert.True(tokenScanner.CurrentToken is NumericToken);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
namespace UglyToad.PdfPig.Tests.Fonts.Encodings
|
||||
{
|
||||
using System.Text;
|
||||
using PdfPig.Fonts;
|
||||
|
||||
public class GlyphListTests
|
||||
@@ -93,7 +92,7 @@
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
[Fact(Skip = "TODO - String don't match")]
|
||||
public void NameToUnicodeConvertAglSpecification()
|
||||
{
|
||||
// https://github.com/adobe-type-tools/agl-specification?tab=readme-ov-file#3-examples
|
||||
@@ -104,22 +103,7 @@
|
||||
|
||||
var result = list.NameToUnicode("Lcommaaccent_uni20AC0308_u1040C.alternate");
|
||||
|
||||
// This value is not encodable in single characters in UTF-16, so we get a surrogate pair for the final unicode character
|
||||
Assert.Equal("\u013B\u20AC\u0308\uD801\uDC0C", result);
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
// But in .Net we can get the unicode rune values to verify that this is really the expected value
|
||||
var runes = result.EnumerateRunes().ToList();
|
||||
|
||||
Assert.Equal(4, runes.Count);
|
||||
|
||||
Assert.Equal(0x013B, runes[0].Value);
|
||||
Assert.Equal(0x20AC, runes[1].Value);
|
||||
Assert.Equal(0x0308, runes[2].Value);
|
||||
Assert.Equal(0x1040C, runes[3].Value);
|
||||
#endif
|
||||
// Ok, so we know this is what the actual string is. Now lets encode that last value the C# way
|
||||
Assert.Equal("\u013B\u20AC\u0308\U0001040C", result);
|
||||
Assert.Equal("\u013B\u20AC\u0308\u1040C", result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ using UglyToad.PdfPig.Tests.Dla;
|
||||
namespace UglyToad.PdfPig.Tests.Fonts.SystemFonts
|
||||
{
|
||||
using PdfPig.Core;
|
||||
using PdfPig.Geometry;
|
||||
|
||||
public class Linux
|
||||
{
|
||||
@@ -65,14 +64,11 @@ namespace UglyToad.PdfPig.Tests.Fonts.SystemFonts
|
||||
|
||||
var current = page.Letters[i];
|
||||
|
||||
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.BoundingBox.IntersectsWith(current.GlyphRectangleLoose));
|
||||
Assert.Equal(current.BoundingBox.Rotation, current.GlyphRectangleLoose.Rotation, 3);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,19 +229,5 @@ namespace UglyToad.PdfPig.Tests.Fonts.TrueType.Parser
|
||||
Assert.NotNull(font.TableRegister.NameTable);
|
||||
Assert.NotEmpty(font.TableRegister.NameTable.NameRecords);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse12623CorruptFileAndGetGlyphs()
|
||||
{
|
||||
var bytes = TrueTypeTestHelper.GetFileBytes("corrupt-12623");
|
||||
|
||||
var input = new TrueTypeDataBytes(new MemoryInputBytes(bytes));
|
||||
|
||||
var font = TrueTypeFontParser.Parse(input);
|
||||
|
||||
Assert.NotNull(font);
|
||||
|
||||
font.TryGetPath(1, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -20,14 +20,14 @@
|
||||
// check 'm'
|
||||
var m = letters[0];
|
||||
Assert.Equal("m", m.Value);
|
||||
Assert.Equal(new PdfPoint(253.4458, 658.431), m.BoundingBox.BottomLeft, pointComparer);
|
||||
Assert.Equal(new PdfPoint(261.22659, 662.83446), m.BoundingBox.TopRight, pointComparer);
|
||||
Assert.Equal(new PdfPoint(253.4458, 658.431), m.GlyphRectangle.BottomLeft, pointComparer);
|
||||
Assert.Equal(new PdfPoint(261.22659, 662.83446), m.GlyphRectangle.TopRight, pointComparer);
|
||||
|
||||
// check 'p'
|
||||
var p = letters[1];
|
||||
Assert.Equal("p", p.Value);
|
||||
Assert.Equal(new PdfPoint(261.70778, 656.49825), p.BoundingBox.BottomLeft, pointComparer);
|
||||
Assert.Equal(new PdfPoint(266.6193, 662.83446), p.BoundingBox.TopRight, pointComparer);
|
||||
Assert.Equal(new PdfPoint(261.70778, 656.49825), p.GlyphRectangle.BottomLeft, pointComparer);
|
||||
Assert.Equal(new PdfPoint(266.6193, 662.83446), p.GlyphRectangle.TopRight, pointComparer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
{
|
||||
var bytes = GetFileBytes("AdobeUtopia.pfa");
|
||||
|
||||
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0, new StackDepthGuard(256));
|
||||
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0);
|
||||
}
|
||||
|
||||
[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, new StackDepthGuard(256));
|
||||
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -28,7 +28,7 @@
|
||||
{
|
||||
var bytes = GetFileBytes("CMBX10.pfa");
|
||||
|
||||
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0, new StackDepthGuard(256));
|
||||
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -36,7 +36,7 @@
|
||||
{
|
||||
var bytes = GetFileBytes("CMCSC10");
|
||||
|
||||
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0, new StackDepthGuard(256));
|
||||
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -44,7 +44,7 @@
|
||||
{
|
||||
var bytes = GetFileBytes("CMBX12");
|
||||
|
||||
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0, new StackDepthGuard(256));
|
||||
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -52,7 +52,7 @@
|
||||
{
|
||||
var bytes = GetFileBytes("CMBX10");
|
||||
|
||||
var result = Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0, new StackDepthGuard(256));
|
||||
var result = Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0);
|
||||
|
||||
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, new StackDepthGuard(256));
|
||||
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0);
|
||||
}
|
||||
|
||||
private static byte[] GetFileBytes(string name)
|
||||
|
||||
@@ -270,10 +270,10 @@
|
||||
GetCurrentState().FontState.WordSpacing = spacing;
|
||||
}
|
||||
|
||||
public void ModifyCurrentTransformationMatrix(TransformationMatrix value)
|
||||
public void ModifyCurrentTransformationMatrix(double[] value)
|
||||
{
|
||||
var state = GetCurrentState();
|
||||
state.CurrentTransformationMatrix = value.Multiply(state.CurrentTransformationMatrix);
|
||||
var ctm = GetCurrentState().CurrentTransformationMatrix;
|
||||
GetCurrentState().CurrentTransformationMatrix = TransformationMatrix.FromArray(value).Multiply(ctm);
|
||||
}
|
||||
|
||||
public void SetCharacterSpacing(double spacing)
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
@@ -32,18 +32,4 @@ public class AccentedCharactersInBookmarksTests
|
||||
},
|
||||
nodes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanReadContainerBookmarksCorrectly()
|
||||
{
|
||||
var path = IntegrationHelpers.GetDocumentPath("dotnet-ai.pdf");
|
||||
|
||||
using var document = PdfDocument.Open(path);
|
||||
var isFound = document.TryGetBookmarks(out var bookmarks, false);
|
||||
Assert.True(isFound);
|
||||
Assert.True(bookmarks.Roots.Count == 3);
|
||||
isFound = document.TryGetBookmarks(out bookmarks, true);
|
||||
Assert.True(isFound);
|
||||
Assert.True(bookmarks.Roots.Count > 3);
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@
|
||||
Assert.Equal(Width, letter.Width, 1);
|
||||
if (includeHeight)
|
||||
{
|
||||
Assert.Equal(Height, letter.BoundingBox.Height, 1);
|
||||
Assert.Equal(Height, letter.GlyphRectangle.Height, 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
namespace UglyToad.PdfPig.Tests.Integration
|
||||
{
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
public class CMapLocalCachingTests
|
||||
{
|
||||
private static readonly Lazy<string> DocumentFolder = new Lazy<string>(() => Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "Integration", "Documents")));
|
||||
private static readonly Lazy<string> DlaFolder = new Lazy<string>(() => Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "Dla", "Documents")));
|
||||
|
||||
public static object[][] DocumentsData = new object[][]
|
||||
{
|
||||
["68-1990-01_A.pdf"],
|
||||
["Type0 Font.pdf"],
|
||||
["11194059_2017-11_de_s.pdf"],
|
||||
["2108.11480.pdf"],
|
||||
["reference-2-numeric-error.pdf"],
|
||||
["MOZILLA-3136-0.pdf"],
|
||||
["FICTIF_TABLE_INDEX.pdf"],
|
||||
["Approved_Document_B__fire_safety__volume_2_-_Buildings_other_than_dwellings__2019_edition_incorporating_2020_and_2022_amendments.pdf"],
|
||||
["dotnet-ai.pdf"],
|
||||
["Old Gutnish Internet Explorer.pdf"],
|
||||
["Random 2 Columns Lists Hyph - Justified.pdf"]
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(DocumentsData))]
|
||||
public void CheckText(string documentName)
|
||||
{
|
||||
string fullPath = Path.Combine(DocumentFolder.Value, documentName);
|
||||
if (!File.Exists(fullPath))
|
||||
{
|
||||
fullPath = Path.Combine(DlaFolder.Value, documentName);
|
||||
}
|
||||
|
||||
Assert.True(File.Exists(fullPath));
|
||||
|
||||
var sb = new StringBuilder();
|
||||
|
||||
using (var document = PdfDocument.Open(fullPath, new ParsingOptions { UseLenientParsing = true }))
|
||||
{
|
||||
for (var i = 0; i < document.NumberOfPages; i++)
|
||||
{
|
||||
var page = document.GetPage(i + 1);
|
||||
sb.Append(page.Text);
|
||||
}
|
||||
}
|
||||
|
||||
//File.WriteAllText(Path.ChangeExtension(fullPath, "txt"), sb.ToString());
|
||||
|
||||
string expected = File.ReadAllText(Path.ChangeExtension(fullPath, "txt"));
|
||||
Assert.Equal(expected, sb.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,17 +9,5 @@
|
||||
using var document = PdfDocument.Open(path);
|
||||
Assert.Equal(3, document.NumberOfPages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanReadDocumentWithCircularXRef()
|
||||
{
|
||||
string path = IntegrationHelpers.GetSpecificTestDocumentPath("B17-2000-transportation-fuels.pdf");
|
||||
|
||||
// If parser can't deal with xrefs that have circular references then
|
||||
// opening the document will loop forever
|
||||
using var document = PdfDocument.Open(path);
|
||||
|
||||
Assert.Equal(1, document.NumberOfPages);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
||||
TypeWeekly newspaperEditorJoe StuverFoundedOctober, 1918 (asthe BroadusIndependent), 1935(as the PowderRiver CountyExaminer), and1965 (as thePowder RiverExaminer)Headquarters119 1/2 N. ParkAve.Broadus, MT 59317United StatesPowder River ExaminerPowder River ExaminerThe Powder River Examiner, originally established in October, 1918 asthe Broadus Independent, is the only newspaper printed in Powder RiverCounty, Montana, and is located in the county seat of Broadus.The Broadus Independent was first published in Broadus, Montana inOctober, 1918, and continued until February, 1919.From March 6, 1919 until April 17, 1919, the paper was published inOlive, Montana as the Olive Branch.The Broadus Independent was published weekly from April 24, 1919until 1935.The Powder River County Examiner replaced the BroadusIndependent in 1935, beginning publication and continuing weekly until1965.In 1965 the newspaper's name was shortened to Powder RiverExaminer, and remains that today.Broadus Independent, Broadus, Montana, October, 1918-February, 1919.Olive Branch, Olive, Montana, March 6, 1919 – April 17, 1919.Broadus Independent, Broadus, Montana, April 24, 1919 – 1935.Powder River County Examiner, Broadus, Montana, 1935-1965.Powder River Examiner, Broadus, Montana, 1965-current.Joe Stuver, (current editor)Retrieved from "https://en.wikipedia.org/w/index.php?title=Powder_River_Examiner&oldid=747264669"This page was last edited on 1 November 2016, at 11:53.Text is available under the Creative Commons Attribution-ShareAlike License; additional terms may apply. By usingthis site, you agree to the Terms of Use and Privacy Policy. Wikipedia® is a registered trademark of the WikimediaFoundation, Inc., a non-profit organization.HistoryPreceding TitlesNotable contributorsPowder River Examiner - Wikipediahttps://en.wikipedia.org/wiki/Powder_River_Examiner1 of 130/03/2018, 03:50
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1 +0,0 @@
|
||||
(Jl σ3 法海阔』咱国司被NA咿清峭阉且啊辛茹海耀表3-19车辆齿轮用钢系列牌号化学成分(质量分数)(%)国内牌号国外牌号c Si Mn p s Ni Cr Mo Al Cu Ti B 16CrMnTiH 0.13~0.18 0. \Kl -1. 20 20CrMnTiHI 20CrMnTiH2 。.80 -1. 10 1.00-1.30 20CrMnTiH3 0. 17 -0. 37军三0.0350. 04 -0. 10 0.18句0.2320CrMnTiH4 20CrMnTiH5 0. \Kl -1. 25 I.I。”1.4520CrMnTiH6 16MnCrH 16MnCr5 0. 14 -0. 20 I. 00~1.40 O.\KJ-1.20 20MnCrH 20MnCr5 0.17~0.23 1.10-1.50 1.00-1.30 0.02~ 罢王0.20运0.120.02~0.仍525MnCrH 25MnCr5 0.23~0.28 0.055 o. ro -o. so0. 80-1.10 28MnCrH 28MnCr5 0. 25 -0. 30 运0.15运0.1016CrMnBH ZF6 0. 13 -0. 18。.80-1.100.001 -18CrMnBH ZF7 0.15 -0. 40 1.00-1.30 0. 15 -0. 20军军aα丑。0. 015 -0. 035 1.00-1.30 0.α)317CrMnBH ZF7B 17Cr2Ni2H ZFI 0.15 -0.19 0.15 -0. 40 0.40~o.ro1.40~I. 70 1.40-1.70 16CrNiH 16CrNi4 0.13“0.18 0.02-0.04 0.15~0.35 0.70句1.100. 80-1. 200. 80~1.20 :;;;0.10 0.02-0. 05 19CrNiH 19CrNi5 0.16~0. 21 0. 02 -0. 035 17Cr2Ni2MoH ZFlA 0.15”0.19 0.15~0.40 o. 40 -o. ro0.015~0.035 I. 4。”I.70 1.50-1.80 0. 25 -0. 35 20CrNiMoHI 8620Hl 0.02-0.17”0.23 0.15町0.35o. ro -o. 95 0.017~0.032 0.35~0. 75 0.35”0.65 0. 15 -0. 25 20CrNiMoH2 8620田0.045 15CrMoH 0.13”0.18 0.25~0.45 0.17~0. 37 0.4。”0.70 髦。但50.8。”1.1020CrMo 0. 18 -0. 230.15 -0. 25 20CrMoH SCM420 0.17 -0. 23 0.17町0.350.55~0. \Kl 0.85句I.25 0.15”0.35 0.02-0.0击:;;;0.15 35CrMo 0. 32 -0. 40 0.40~0. 70 0.80斗100. 15 -0. 25 运0.03520CrH 0.17~0.37 0.70-1.00 运0.200.50~0.80 40Cr 0. 18 -0. 23 0.37~0.440. 80 -1.10
|
||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 196 KiB |
@@ -33,7 +33,7 @@
|
||||
|
||||
var rect = new PdfRectangle(207, 158, 229, 168.5);
|
||||
|
||||
var missingChars = letters.Where(l => rect.Contains(l.BoundingBox)).ToArray();
|
||||
var missingChars = letters.Where(l => rect.Contains(l.GlyphRectangle)).ToArray();
|
||||
|
||||
Assert.NotEmpty(missingChars);
|
||||
Assert.True(missingChars.Length == 2);
|
||||
|
||||
@@ -86,8 +86,8 @@
|
||||
var letter = page.Letters[l];
|
||||
var expected = DataBoldItalic[l];
|
||||
Assert.Equal((string)expected[0], letter.Value);
|
||||
Assert.Equal((bool)expected[1], letter.FontDetails.IsBold);
|
||||
Assert.Equal((bool)expected[2], letter.FontDetails.IsItalic);
|
||||
Assert.Equal((bool)expected[1], letter.Font.IsBold);
|
||||
Assert.Equal((bool)expected[2], letter.Font.IsItalic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,332 +4,9 @@
|
||||
using DocumentLayoutAnalysis.PageSegmenter;
|
||||
using DocumentLayoutAnalysis.WordExtractor;
|
||||
using PdfPig.Core;
|
||||
using PdfPig.Tokens;
|
||||
using SkiaSharp;
|
||||
using UglyToad.PdfPig.AcroForms;
|
||||
using UglyToad.PdfPig.AcroForms.Fields;
|
||||
|
||||
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()
|
||||
{
|
||||
string[] files = ["Input.visible.pdf", "Input.invisible.pdf"];
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
var path = IntegrationHelpers.GetSpecificTestDocumentPath(file);
|
||||
|
||||
using (var document = PdfDocument.Open(path, new ParsingOptions() { UseLenientParsing = true }))
|
||||
{
|
||||
Assert.True(document.TryGetForm(out AcroForm form));
|
||||
Assert.Single(form.Fields);
|
||||
Assert.Equal(AcroFieldType.Signature, form.Fields[0].FieldType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Issue1209()
|
||||
{
|
||||
var path = IntegrationHelpers.GetDocumentPath("MOZILLA-9176-2.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);
|
||||
|
||||
foreach (var image in page.GetImages())
|
||||
{
|
||||
Assert.True(image.ImageDictionary.ContainsKey(NameToken.Height)); // Was missing
|
||||
Assert.True(image.ImageDictionary.ContainsKey(NameToken.Width));
|
||||
|
||||
if (image.ImageDictionary.TryGet<DictionaryToken>(NameToken.DecodeParms, out var decodeParms))
|
||||
{
|
||||
Assert.True(decodeParms.ContainsKey(NameToken.Columns)); // Was missing
|
||||
Assert.True(decodeParms.ContainsKey(NameToken.Rows));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Revert_e11dc6b()
|
||||
{
|
||||
var path = IntegrationHelpers.GetDocumentPath("GHOSTSCRIPT-699488-0.pdf");
|
||||
|
||||
using (var document = PdfDocument.Open(path, new ParsingOptions() { UseLenientParsing = true }))
|
||||
{
|
||||
var page = document.GetPage(1);
|
||||
var images = page.GetImages().ToArray();
|
||||
|
||||
Assert.Equal(9, images.Length);
|
||||
|
||||
foreach (var image in images)
|
||||
{
|
||||
if (image.ImageDictionary.TryGet(NameToken.Filter, out var token) && token is NameToken nt)
|
||||
{
|
||||
if (nt.Data.Contains("DCT"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(image.TryGetPng(out _));
|
||||
}
|
||||
|
||||
var paths = page.Paths;
|
||||
Assert.Equal(66, paths.Count);
|
||||
var letters = page.Letters;
|
||||
Assert.Equal(2685, letters.Count);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Issue1199()
|
||||
{
|
||||
var path = IntegrationHelpers.GetDocumentPath("TrueTypeTablesGlyphDataTableReadGlyphsError.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 Issue1183()
|
||||
{
|
||||
var path = IntegrationHelpers.GetDocumentPath("test_a.pdf");
|
||||
|
||||
byte[] expected =
|
||||
[
|
||||
82, 85, 134, 255, 87, 90, 139, 255, 81, 84, 133, 255, 87, 89, 139, 255, 89, 91, 141, 255, 81, 83, 133,
|
||||
255, 84, 86, 136, 255, 84, 86, 136, 255, 70, 59, 113, 255, 69, 62, 116, 255, 75, 73, 126, 255, 45, 48,
|
||||
100, 255, 42, 48, 99, 255, 50, 55, 107, 255, 56, 59, 111, 255, 64, 66, 118, 255, 68, 63, 118, 255, 61,
|
||||
56, 111, 255, 70, 64, 120, 255, 67, 62, 117, 255, 61, 56, 111, 255, 68, 63, 118, 255, 68, 62, 118, 255,
|
||||
59, 54, 109, 255, 61, 60, 117, 255, 69, 65, 122, 255, 67, 59, 116, 255, 71, 62, 118, 255, 66, 60, 115,
|
||||
255, 47, 49, 102, 255, 40, 51, 102, 255, 35, 51, 100, 255, 70, 58, 114, 255, 68, 56, 112, 255, 76, 65,
|
||||
121, 255, 68, 58, 114, 255, 66, 58, 114, 255, 71, 64, 119, 255, 62, 56, 111, 255, 67, 62, 117, 255, 77,
|
||||
61, 118, 255, 71, 56, 113, 255, 76, 63, 119, 255, 74, 63, 118, 255, 63, 55, 108, 255, 71, 64, 116, 255,
|
||||
73, 68, 119, 255, 52, 49, 99, 255, 38, 51, 99, 255, 49, 62, 110, 255, 39, 51, 100, 255, 46, 55, 106,
|
||||
255, 50, 55, 107, 255, 63, 62, 116, 255, 67, 60, 116, 255, 71, 60, 116, 255, 67, 58, 112, 255, 68, 61,
|
||||
114, 255, 70, 67, 119, 255, 50, 50, 101, 255, 42, 47, 96, 255, 49, 59, 106, 255, 40, 54, 100, 255, 42,
|
||||
57, 103, 255, 51, 51, 102, 255, 67, 60, 112, 255, 73, 62, 114, 255, 71, 65, 117, 255, 48, 53, 103, 255,
|
||||
45, 55, 104, 255, 49, 55, 105, 255, 63, 63, 114, 255, 68, 59, 115, 255, 71, 59, 115, 255, 73, 59, 115,
|
||||
255, 74, 61, 118, 255, 66, 58, 114, 255, 50, 51, 105, 255, 39, 51, 104, 255, 34, 52, 103, 255, 64, 60,
|
||||
116, 255, 67, 64, 119, 255, 66, 66, 120, 255, 46, 49, 102, 255, 45, 51, 102, 255, 52, 61, 111, 255, 39,
|
||||
51, 99, 255, 41, 54, 102, 255, 42, 54, 100, 255, 43, 53, 99, 255, 47, 55, 103, 255, 51, 56, 104, 255,
|
||||
56, 57, 108, 255, 67, 65, 117, 255, 67, 63, 116, 255, 52, 47, 100, 255, 44, 55, 106, 255, 44, 56, 106,
|
||||
255, 42, 54, 103, 255, 42, 54, 102, 255, 40, 52, 100, 255, 41, 52, 99, 255, 45, 57, 103, 255, 42, 53,
|
||||
99, 255, 38, 54, 95, 255, 39, 55, 97, 255, 47, 64, 105, 255, 37, 53, 95, 255, 37, 53, 95, 255, 46, 63,
|
||||
104, 255, 39, 55, 96, 255, 42, 58, 99, 255, 41, 55, 105, 255, 45, 55, 106, 255, 46, 51, 103, 255, 51,
|
||||
51, 103, 255, 63, 61, 114, 255, 70, 68, 121, 255, 60, 60, 113, 255, 46, 48, 100, 255, 49, 51, 101, 255,
|
||||
51, 52, 103, 255, 58, 58, 109, 255, 69, 66, 119, 255, 64, 60, 113, 255, 61, 55, 109, 255, 70, 62, 118,
|
||||
255, 67, 58, 114, 255, 72, 59, 115, 255, 70, 58, 115, 255, 72, 62, 118, 255, 61, 55, 110, 255, 64, 62,
|
||||
116, 255, 65, 65, 119, 255, 47, 50, 104, 255, 52, 56, 109, 255, 39, 53, 106, 255, 41, 54, 107, 255, 40,
|
||||
50, 102, 255, 45, 51, 103, 255, 64, 66, 117, 255, 62, 61, 112, 255, 67, 63, 114, 255, 53, 47, 98, 255,
|
||||
49, 54, 101, 255, 51, 56, 104, 255, 43, 48, 95, 255, 50, 55, 102, 255, 49, 54, 102, 255, 42, 47, 94,
|
||||
255, 51, 56, 103, 255, 47, 52, 100, 255, 72, 62, 114, 255, 71, 62, 114, 255, 72, 67, 119, 255, 52, 52,
|
||||
103, 255, 44, 48, 99, 255, 48, 57, 106, 255, 39, 52, 100, 255, 43, 58, 106, 255, 43, 51, 98, 255, 44,
|
||||
52, 99, 255, 48, 57, 104, 255, 46, 55, 102, 255, 41, 50, 97, 255, 45, 55, 101, 255, 49, 59, 105, 255,
|
||||
43, 53, 100, 255, 51, 57, 106, 255, 41, 49, 98, 255, 40, 52, 100, 255, 45, 60, 107, 255, 38, 53, 101,
|
||||
255, 36, 48, 96, 255, 46, 54, 102, 255, 49, 55, 104, 255, 44, 55, 104, 255, 46, 56, 105, 255, 48, 58,
|
||||
107, 255, 41, 49, 99, 255, 43, 50, 100, 255, 52, 59, 108, 255, 50, 55, 105, 255, 50, 55, 105, 255, 43,
|
||||
54, 105, 255, 42, 51, 102, 255, 45, 53, 104, 255, 45, 49, 101, 255, 63, 63, 116, 255, 66, 63, 116, 255,
|
||||
68, 63, 117, 255, 62, 55, 109, 255, 74, 60, 120, 255, 73, 59, 119, 255, 72, 58, 119, 255, 76, 62, 122,
|
||||
255, 74, 60, 120, 255, 71, 57, 118, 255, 75, 61, 121, 255, 76, 62, 123, 255
|
||||
];
|
||||
|
||||
using (var document = PdfDocument.Open(path, new ParsingOptions() { UseLenientParsing = true }))
|
||||
{
|
||||
var page = document.GetPage(16);
|
||||
var images = page.GetImages().ToArray();
|
||||
|
||||
Assert.Single(images);
|
||||
|
||||
var image = images[0];
|
||||
|
||||
Assert.True(image.TryGetPng(out var bytes));
|
||||
|
||||
File.WriteAllBytes("test_a_16.png", bytes);
|
||||
|
||||
using (SKBitmap actual = SKBitmap.Decode(bytes, new SKImageInfo(431, 690, SKColorType.Bgra8888)))
|
||||
{
|
||||
var pixels = actual.GetPixelSpan();
|
||||
Assert.Equal(1189560, pixels.Length);
|
||||
Assert.Equal(expected, pixels.Slice(0, 4 * 200).ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Issue1156()
|
||||
{
|
||||
var path = IntegrationHelpers.GetDocumentPath("felltypes-test.pdf");
|
||||
|
||||
using (var document = PdfDocument.Open(path, new ParsingOptions() { UseLenientParsing = true }))
|
||||
{
|
||||
var page = document.GetPage(1);
|
||||
|
||||
var letters = page.Letters;
|
||||
|
||||
var words = NearestNeighbourWordExtractor.Instance.GetWords(letters).ToArray();
|
||||
|
||||
var wordThe = words[0];
|
||||
Assert.Equal("THE", wordThe.Text);
|
||||
Assert.Equal(wordThe.BoundingBox.BottomLeft, new PdfPoint(x: 242.9877, y: 684.7435));
|
||||
Assert.Equal(wordThe.BoundingBox.BottomRight, new PdfPoint(x: 323.93999999999994, y: 684.7435));
|
||||
|
||||
var wordBook = words[2];
|
||||
Assert.Equal("BOOK:", wordBook.Text);
|
||||
Assert.Equal(wordBook.BoundingBox.BottomLeft, new PdfPoint(x: 280.4371, y: 652.0399));
|
||||
Assert.Equal(wordBook.BoundingBox.BottomRight, new PdfPoint(x: 405.65439999999995, y: 652.0399));
|
||||
|
||||
var wordPremeffa = words[35];
|
||||
Assert.Equal("preme\ue009a.", wordPremeffa.Text); // The 'ff' glyph is not properly parsed
|
||||
Assert.Equal(wordPremeffa.BoundingBox.BottomLeft, new PdfPoint(x: 331.16020000000003, y: 515.2256999999998));
|
||||
Assert.Equal(wordPremeffa.BoundingBox.BottomRight, new PdfPoint(x: 374.2954000000001, y: 515.2256999999998));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Issue1148()
|
||||
{
|
||||
var path = IntegrationHelpers.GetSpecificTestDocumentPath("P2P-33713919.pdf");
|
||||
|
||||
using (var document = PdfDocument.Open(path, new ParsingOptions() { UseLenientParsing = true }))
|
||||
{
|
||||
var page = document.GetPage(2);
|
||||
|
||||
var letters = page.Letters;
|
||||
|
||||
var words = NearestNeighbourWordExtractor.Instance.GetWords(letters).ToArray();
|
||||
|
||||
var firstTableLine = words[42];
|
||||
|
||||
Assert.EndsWith("C<--,:", firstTableLine.Text); // Just to make sure we are looking at the correct line. Text might change as this is not actually correct
|
||||
|
||||
Assert.Equal(firstTableLine.BoundingBox.BottomLeft, new PdfPoint(x: 31.890118, y: 693.035685));
|
||||
Assert.Equal(firstTableLine.BoundingBox.BottomRight, new PdfPoint(x: 563.3851179999991, y: 693.035685));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Issue1122()
|
||||
{
|
||||
var path = IntegrationHelpers.GetSpecificTestDocumentPath("StackOverflow_Issue_1122.pdf");
|
||||
|
||||
var ex = Assert.Throws<PdfDocumentFormatException>(() => PdfDocument.Open(path, new ParsingOptions() { UseLenientParsing = true }));
|
||||
Assert.StartsWith("Circular reference encountered when looking", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Issue1096()
|
||||
{
|
||||
@@ -386,7 +63,7 @@
|
||||
{
|
||||
var path = IntegrationHelpers.GetSpecificTestDocumentPath("SpookyPass.pdf");
|
||||
var ex = Assert.Throws<PdfDocumentFormatException>(() => PdfDocument.Open(path, new ParsingOptions() { UseLenientParsing = true }));
|
||||
Assert.StartsWith("Object stream cannot contain itself", ex.Message);
|
||||
Assert.Equal("Avoiding infinite recursion in ObjectLocationProvider.TryGetOffset() as 'offset' and 'reference.ObjectNumber' have the same value and opposite signs.", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -394,10 +71,8 @@
|
||||
{
|
||||
var path = IntegrationHelpers.GetSpecificTestDocumentPath("Hang.pdf");
|
||||
|
||||
using var doc = PdfDocument.Open(path, new ParsingOptions { UseLenientParsing = true });
|
||||
|
||||
var ex = Assert.Throws<PdfDocumentFormatException>(() => doc.GetPage(1));
|
||||
Assert.StartsWith("Could not find", ex.Message);
|
||||
var ex = Assert.Throws<PdfDocumentFormatException>(() => PdfDocument.Open(path, new ParsingOptions() { UseLenientParsing = true }));
|
||||
Assert.Equal("The cross reference was not found.", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -551,7 +226,6 @@
|
||||
using (var document = PdfDocument.Open(path, new ParsingOptions() { UseLenientParsing = true, SkipMissingFonts = true }))
|
||||
{
|
||||
var page = document.GetPage(13);
|
||||
// This used to fail with an overflow exception when we failed to validate the zlib encoded data
|
||||
Assert.Throws<OverflowException>(() => DocstrumBoundingBoxes.Instance.GetBlocks(page.GetWords()));
|
||||
}
|
||||
}
|
||||
@@ -629,6 +303,12 @@
|
||||
Assert.Equal(i, page.Number);
|
||||
}
|
||||
}
|
||||
|
||||
// Lenient parsing OFF
|
||||
var exception = Assert.Throws<PdfDocumentFormatException>(() =>
|
||||
PdfDocument.Open(path, new ParsingOptions() { UseLenientParsing = false }));
|
||||
|
||||
Assert.Equal("The cross references formed an infinite loop.", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -735,13 +415,19 @@
|
||||
{
|
||||
var doc = IntegrationHelpers.GetDocumentPath("ErcotFacts.pdf");
|
||||
|
||||
using (var document = PdfDocument.Open(doc, new ParsingOptions() { UseLenientParsing = true, SkipMissingFonts = false }))
|
||||
using (var document = PdfDocument.Open(doc, new ParsingOptions() { UseLenientParsing = true, SkipMissingFonts = true }))
|
||||
{
|
||||
var page1 = document.GetPage(1);
|
||||
Assert.Equal(1939, page1.Letters.Count);
|
||||
Assert.Equal(1788, page1.Letters.Count);
|
||||
|
||||
var page2 = document.GetPage(2);
|
||||
Assert.Equal(2434, page2.Letters.Count);
|
||||
Assert.Equal(2430, page2.Letters.Count);
|
||||
}
|
||||
|
||||
using (var document = PdfDocument.Open(doc, new ParsingOptions() { UseLenientParsing = true, SkipMissingFonts = false }))
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentNullException>(() => document.GetPage(1));
|
||||
Assert.StartsWith("Value cannot be null.", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -758,7 +444,7 @@
|
||||
{
|
||||
var letter = page1.Letters[l];
|
||||
Assert.Equal(TextOrientation.Other, letter.TextOrientation);
|
||||
Assert.Equal(45.0, letter.BoundingBox.Rotation, 5);
|
||||
Assert.Equal(45.0, letter.GlyphRectangle.Rotation, 5);
|
||||
}
|
||||
|
||||
var page2 = document.GetPage(2);
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
namespace UglyToad.PdfPig.Tests.Integration
|
||||
{
|
||||
using PdfPig.Geometry;
|
||||
|
||||
public class IntegrationDocumentTests
|
||||
{
|
||||
private static readonly Lazy<string> DocumentFolder = new Lazy<string>(() => Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "Integration", "Documents")));
|
||||
@@ -13,36 +11,6 @@
|
||||
"cmap-parsing-exception.pdf"
|
||||
];
|
||||
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(GetAllDocuments))]
|
||||
public void CheckGlyphLooseBoundingBoxes(string documentName)
|
||||
{
|
||||
// Add the full path back on, we removed it so we could see it in the test explorer.
|
||||
documentName = Path.Combine(DocumentFolder.Value, documentName);
|
||||
|
||||
using (var document = PdfDocument.Open(documentName, new ParsingOptions { UseLenientParsing = true }))
|
||||
{
|
||||
for (var i = 0; i < document.NumberOfPages; i++)
|
||||
{
|
||||
var page = document.GetPage(i + 1);
|
||||
foreach (var letter in page.Letters)
|
||||
{
|
||||
var bbox = letter.BoundingBox;
|
||||
if (bbox.Height > 0)
|
||||
{
|
||||
if (letter.GlyphRectangleLoose.Height <= 0)
|
||||
{
|
||||
_ = letter.GetFont().GetAscent();
|
||||
}
|
||||
|
||||
Assert.True(letter.GlyphRectangleLoose.Height > 0, $"Page {i + 1}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(GetAllDocuments))]
|
||||
public void CanReadAllPages(string documentName)
|
||||
@@ -92,13 +60,13 @@
|
||||
{
|
||||
Assert.NotNull(document.Structure.Catalog);
|
||||
|
||||
//Assert.True(document.Structure.CrossReferenceTable.ObjectOffsets.Count > 0, "Cross reference table was empty.");
|
||||
//foreach (var objectOffset in document.Structure.CrossReferenceTable.ObjectOffsets)
|
||||
//{
|
||||
// var token = document.Structure.GetObject(objectOffset.Key);
|
||||
Assert.True(document.Structure.CrossReferenceTable.ObjectOffsets.Count > 0, "Cross reference table was empty.");
|
||||
foreach (var objectOffset in document.Structure.CrossReferenceTable.ObjectOffsets)
|
||||
{
|
||||
var token = document.Structure.GetObject(objectOffset.Key);
|
||||
|
||||
// Assert.NotNull(token);
|
||||
//}
|
||||
Assert.NotNull(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace UglyToad.PdfPig.Tests.Integration
|
||||
{
|
||||
var page = document.GetPage(1);
|
||||
|
||||
Assert.NotEqual(0, page.Letters[0].BoundingBox.Height);
|
||||
Assert.NotEqual(0, page.Letters[0].GlyphRectangle.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.BoundingBox.Left).Min();
|
||||
var maxX = page.Letters.Select(l => l.BoundingBox.Right).Max();
|
||||
var minX = page.Letters.Select(l => l.GlyphRectangle.Left).Min();
|
||||
var maxX = page.Letters.Select(l => l.GlyphRectangle.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
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
namespace UglyToad.PdfPig.Tests.Integration
|
||||
{
|
||||
#if NET9_0_OR_GREATER
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
#endif
|
||||
|
||||
public class SinglePageLibreOfficeImages
|
||||
{
|
||||
private static string GetFilePath() => IntegrationHelpers.GetDocumentPath(@"Single Page Images - from libre office.pdf");
|
||||
@@ -21,23 +17,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
#if NET9_0_OR_GREATER
|
||||
[Fact]
|
||||
public void CanUseFileBufferingReadStream()
|
||||
{
|
||||
var bytes = File.ReadAllBytes(GetFilePath());
|
||||
|
||||
using var mem = new MemoryStream(bytes);
|
||||
using var fbrs = new FileBufferingReadStream(mem, 256);
|
||||
|
||||
using var doc = PdfDocument.Open(fbrs);
|
||||
|
||||
var page = doc.GetPage(1);
|
||||
|
||||
Assert.NotEmpty(page.Text);
|
||||
}
|
||||
#endif
|
||||
|
||||
[Fact]
|
||||
public void ImagesHaveCorrectDimensionsAndLocations()
|
||||
{
|
||||
@@ -47,29 +26,29 @@
|
||||
{
|
||||
var page = document.GetPage(1);
|
||||
|
||||
var images = page.GetImages().OrderBy(x => x.BoundingBox.Width).ToList();
|
||||
var images = page.GetImages().OrderBy(x => x.Bounds.Width).ToList();
|
||||
|
||||
var pdfPigSquare = images[0];
|
||||
|
||||
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);
|
||||
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);
|
||||
|
||||
|
||||
var pdfPigSquished = images[1];
|
||||
|
||||
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);
|
||||
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);
|
||||
|
||||
var birthdayPigs = images[2];
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -185,20 +185,6 @@ namespace UglyToad.PdfPig.Tests.Integration
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandleCorruptedFileOffsets()
|
||||
{
|
||||
var path = IntegrationHelpers.GetDocumentPath("Single Page Broken Offsets - from google drive.pdf");
|
||||
|
||||
using var doc = PdfDocument.Open(path);
|
||||
|
||||
var page = doc.GetPage(1);
|
||||
|
||||
var text = page.Text;
|
||||
|
||||
Assert.NotEmpty(text);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<AssertablePositionData> GetPdfBoxPositionData()
|
||||
{
|
||||
// X Y Width Letter FontSize Font
|
||||
|
||||
@@ -42,27 +42,27 @@
|
||||
|
||||
Assert.Equal("I", page.Letters[0].Value);
|
||||
|
||||
Assert.Equal(90.1d, page.Letters[0].BoundingBox.BottomLeft.X, comparer);
|
||||
Assert.Equal(709.2d, page.Letters[0].BoundingBox.BottomLeft.Y, comparer);
|
||||
Assert.Equal(90.1d, page.Letters[0].GlyphRectangle.BottomLeft.X, comparer);
|
||||
Assert.Equal(709.2d, page.Letters[0].GlyphRectangle.BottomLeft.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(94.0d, page.Letters[0].GlyphRectangle.TopRight.X, comparer);
|
||||
Assert.Equal(719.89d, page.Letters[0].GlyphRectangle.TopRight.Y, comparer);
|
||||
|
||||
Assert.Equal("a", page.Letters[5].Value);
|
||||
|
||||
Assert.Equal(114.5d, page.Letters[5].BoundingBox.BottomLeft.X, comparer);
|
||||
Assert.Equal(709.2d, page.Letters[5].BoundingBox.BottomLeft.Y, comparer);
|
||||
Assert.Equal(114.5d, page.Letters[5].GlyphRectangle.BottomLeft.X, comparer);
|
||||
Assert.Equal(709.2d, page.Letters[5].GlyphRectangle.BottomLeft.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(119.82d, page.Letters[5].GlyphRectangle.TopRight.X, comparer);
|
||||
Assert.Equal(714.89d, page.Letters[5].GlyphRectangle.TopRight.Y, comparer);
|
||||
|
||||
Assert.Equal("f", page.Letters[16].Value);
|
||||
|
||||
Assert.Equal(169.9d, page.Letters[16].BoundingBox.BottomLeft.X, comparer);
|
||||
Assert.Equal(709.2d, page.Letters[16].BoundingBox.BottomLeft.Y, comparer);
|
||||
Assert.Equal(169.9d, page.Letters[16].GlyphRectangle.BottomLeft.X, comparer);
|
||||
Assert.Equal(709.2d, page.Letters[16].GlyphRectangle.BottomLeft.Y, comparer);
|
||||
|
||||
Assert.Equal(176.89d, page.Letters[16].BoundingBox.TopRight.X, comparer);
|
||||
Assert.Equal(719.89d, page.Letters[16].BoundingBox.TopRight.Y, comparer);
|
||||
Assert.Equal(176.89d, page.Letters[16].GlyphRectangle.TopRight.X, comparer);
|
||||
Assert.Equal(719.89d, page.Letters[16].GlyphRectangle.TopRight.Y, comparer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user