mirror of
https://github.com/UglyToad/PdfPig.git
synced 2026-03-10 00:23:29 +08:00
Compare commits
20 Commits
v0.1.12
...
7e370a0eab
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e370a0eab | ||
|
|
b9aa53166d | ||
|
|
0a2b1e076f | ||
|
|
f732718852 | ||
|
|
d69e3fa6a9 | ||
|
|
d6e86b057e | ||
|
|
c27f1b6553 | ||
|
|
629891e80e | ||
|
|
13825b9424 | ||
|
|
7c4f5e2424 | ||
|
|
bd573b2a1c | ||
|
|
baeac0d0c3 | ||
|
|
ee0cb1dc4a | ||
|
|
c70b343caa | ||
|
|
ce563db133 | ||
|
|
37a5dffcaa | ||
|
|
aef0a78ee6 | ||
|
|
005e52783e | ||
|
|
f4e7db5b5b | ||
|
|
52ecef0e28 |
40
.github/workflows/prepare_release_pr.yml
vendored
Normal file
40
.github/workflows/prepare_release_pr.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: Create Release PR
|
||||
|
||||
on:
|
||||
workflow_dispatch
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
bump_version:
|
||||
runs-on: windows-2022
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Calculate next version
|
||||
id: version
|
||||
run: |
|
||||
$newVer = .\tools\get-next-main-version.ps1
|
||||
echo "NEW_VERSION=$newVer" >> $env:GITHUB_ENV
|
||||
.\tools\set-version.ps1 $newVer -UpdateAssemblyAndFileVersion
|
||||
|
||||
git config user.name "github-actions"
|
||||
git config user.email "github-actions@github.com"
|
||||
|
||||
git checkout -b release/$newVer
|
||||
git commit -am "Release $newVer"
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
branch: release/${{ env.NEW_VERSION }}
|
||||
base: master
|
||||
title: "Release ${{ env.NEW_VERSION }}"
|
||||
body: "Automated release PR."
|
||||
delete-branch: true
|
||||
merge-method: rebase
|
||||
53
.github/workflows/publish_nuget.yml
vendored
Normal file
53
.github/workflows/publish_nuget.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
name: Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
build_and_publish:
|
||||
runs-on: windows-2022
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Verify tag points to master
|
||||
id: verify
|
||||
run: |
|
||||
TAG_COMMIT=$(git rev-list -n 1 ${{ github.ref_name }})
|
||||
if ! git branch -r --contains $TAG_COMMIT | grep -q 'origin/master'; then
|
||||
echo "Tag is not on master — skipping publish"
|
||||
exit 78 # 78 = neutral in GitHub Actions
|
||||
fi
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: |
|
||||
2.1.x
|
||||
6.0.x
|
||||
8.0.x
|
||||
9.0.x
|
||||
|
||||
- name: Add msbuild to PATH
|
||||
uses: microsoft/setup-msbuild@v2
|
||||
|
||||
- name: Run tests
|
||||
run: dotnet test -c Release src/UglyToad.PdfPig.sln
|
||||
|
||||
- name: Build package
|
||||
run: dotnet pack -c Release -o package tools/UglyToad.PdfPig.Package/UglyToad.PdfPig.Package.csproj -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg
|
||||
|
||||
- name: Publish to NuGet
|
||||
run: dotnet nuget push package/*.nupkg --api-key ${{secrets.NUGET_API_KEY}} --source https://api.nuget.org/v3/index.json
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ github.ref_name }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
48
.github/workflows/publish_release.yml
vendored
48
.github/workflows/publish_release.yml
vendored
@@ -1,48 +0,0 @@
|
||||
name: Release Publish
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
build_and_publish_release:
|
||||
runs-on: windows-2022
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up dotnet core
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: |
|
||||
2.1.x
|
||||
6.0.x
|
||||
8.0.x
|
||||
9.0.x
|
||||
|
||||
- name: Add msbuild to PATH
|
||||
uses: microsoft/setup-msbuild@v2
|
||||
|
||||
- name: Run tests
|
||||
run: dotnet test -c Release src/UglyToad.PdfPig.sln
|
||||
|
||||
- name: Build package
|
||||
run: dotnet pack -c Release -o package tools/UglyToad.PdfPig.Package/UglyToad.PdfPig.Package.csproj -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg
|
||||
|
||||
- name: Publish to NuGet
|
||||
run: dotnet nuget push **/*.nupkg --api-key ${{secrets.NUGET_API_KEY}} --source https://api.nuget.org/v3/index.json
|
||||
|
||||
- name: Increment version after release
|
||||
run: |
|
||||
$newVer = .\tools\get-next-main-version.ps1
|
||||
.\tools\set-version.ps1 $newVer -UpdateAssemblyAndFileVersion
|
||||
git config user.name "github-actions"
|
||||
git config user.email "github-actions@github.com"
|
||||
git commit -am "Increment version to $newVer"
|
||||
git push
|
||||
43
.github/workflows/tag_release.yml
vendored
Normal file
43
.github/workflows/tag_release.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: Tag Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
tag_if_version_changed:
|
||||
if: startsWith(github.event.head_commit.message, 'Release ')
|
||||
runs-on: windows-2022
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Detect version change
|
||||
id: versioncheck
|
||||
run: |
|
||||
git fetch origin master --depth=2
|
||||
|
||||
$diff = git diff HEAD^ HEAD -- tools/UglyToad.PdfPig.Package/UglyToad.PdfPig.Package.csproj
|
||||
|
||||
if ($diff -match "<Version>") {
|
||||
$version = (Select-String -Path tools/UglyToad.PdfPig.Package/UglyToad.PdfPig.Package.csproj -Pattern "<Version>(.*)</Version>").Matches.Groups[1].Value
|
||||
echo "version=$version" >> $env:GITHUB_OUTPUT
|
||||
echo "create=true" >> $env:GITHUB_OUTPUT
|
||||
} else {
|
||||
echo "create=false" >> $env:GITHUB_OUTPUT
|
||||
}
|
||||
|
||||
- name: Create tag
|
||||
if: steps.versioncheck.outputs.create == 'true'
|
||||
run: |
|
||||
git config user.name "github-actions"
|
||||
git config user.email "github-actions@github.com"
|
||||
|
||||
git tag "v${{ steps.versioncheck.outputs.version }}"
|
||||
git push origin "v${{ steps.versioncheck.outputs.version }}"
|
||||
63
src/UglyToad.PdfPig.Core/StackDepthGuard.cs
Normal file
63
src/UglyToad.PdfPig.Core/StackDepthGuard.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
namespace UglyToad.PdfPig.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a guard for tracking and limiting the depth of nested stack operations, such as recursive calls or
|
||||
/// nested parsing.
|
||||
/// </summary>
|
||||
/// <remarks>Use this class to prevent excessive stack usage by enforcing a maximum nesting depth. This is
|
||||
/// particularly useful in scenarios where untrusted or deeply nested input could cause stack overflows or
|
||||
/// performance issues.</remarks>
|
||||
public sealed class StackDepthGuard
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a stack depth guard with no effective limit on the allowed depth.
|
||||
/// </summary>
|
||||
/// <remarks>Use this instance when stack depth restrictions are not required.</remarks>
|
||||
public static readonly StackDepthGuard Infinite = new StackDepthGuard(int.MaxValue);
|
||||
|
||||
private readonly int maxStackDepth;
|
||||
|
||||
private int depth;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the StackDepthGuard class with the specified maximum stack depth.
|
||||
/// </summary>
|
||||
/// <param name="maxStackDepth">The maximum allowed stack depth for guarded operations. Must be a positive integer.</param>
|
||||
public StackDepthGuard(int maxStackDepth)
|
||||
{
|
||||
if (maxStackDepth <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(maxStackDepth));
|
||||
}
|
||||
this.maxStackDepth = maxStackDepth;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Increments the current nesting depth and checks against the maximum allowed stack depth.
|
||||
/// </summary>
|
||||
/// <exception cref="PdfDocumentFormatException">Thrown if the maximum allowed nesting depth is exceeded.</exception>
|
||||
public void Enter()
|
||||
{
|
||||
if (++depth > maxStackDepth)
|
||||
{
|
||||
depth--; // Decrement so Exit remains balanced if someone catches this
|
||||
throw new PdfDocumentFormatException($"Exceeded maximum nesting depth of {maxStackDepth}.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decreases the current depth level by one, ensuring that the depth does not become negative.
|
||||
/// </summary>
|
||||
/// <remarks>If the current depth is already zero, calling this method has no effect. This method
|
||||
/// is typically used to track or manage nested operations or scopes where depth must remain
|
||||
/// non-negative.</remarks>
|
||||
public void Exit()
|
||||
{
|
||||
depth--;
|
||||
if (depth < 0)
|
||||
{
|
||||
depth = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net462;net471;net6.0;net8.0;net9.0</TargetFrameworks>
|
||||
<LangVersion>12</LangVersion>
|
||||
<Version>0.1.12</Version>
|
||||
<Version>0.1.14</Version>
|
||||
<IsTestProject>False</IsTestProject>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net462;net471;net6.0;net8.0;net9.0</TargetFrameworks>
|
||||
<LangVersion>12</LangVersion>
|
||||
<Version>0.1.12</Version>
|
||||
<Version>0.1.14</Version>
|
||||
<IsTestProject>False</IsTestProject>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
|
||||
@@ -240,9 +240,15 @@
|
||||
flags = (CompositeGlyphFlags)data.ReadUnsignedShort();
|
||||
var glyphIndex = data.ReadUnsignedShort();
|
||||
|
||||
var childGlyph = glyphs[glyphIndex];
|
||||
if (glyphIndex >= glyphs.Length)
|
||||
{
|
||||
// Unsure why this happens but fixes #1213
|
||||
continue; // TODO - Is there a better fix?
|
||||
}
|
||||
|
||||
if (childGlyph == null)
|
||||
IGlyphDescription? childGlyph = glyphs[glyphIndex];
|
||||
|
||||
if (childGlyph is null)
|
||||
{
|
||||
if (!compositeLocations.TryGetValue(glyphIndex, out var missingComposite))
|
||||
{
|
||||
@@ -319,7 +325,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
builderGlyph = builderGlyph ?? emptyGlyph;
|
||||
builderGlyph ??= emptyGlyph;
|
||||
|
||||
return new Glyph(false, builderGlyph.Instructions, builderGlyph.EndPointsOfContours, builderGlyph.Points, compositeLocation.Bounds);
|
||||
}
|
||||
@@ -426,7 +432,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
private class CompositeComponent
|
||||
private sealed class CompositeComponent
|
||||
{
|
||||
public int Index { get; }
|
||||
|
||||
|
||||
@@ -18,16 +18,19 @@
|
||||
|
||||
private const int PfbFileIndicator = 0x80;
|
||||
|
||||
private static readonly char[] Separators = [' '];
|
||||
|
||||
private static readonly Type1EncryptedPortionParser EncryptedPortionParser = new Type1EncryptedPortionParser();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Parses an embedded Adobe Type 1 font file.
|
||||
/// </summary>
|
||||
/// <param name="inputBytes">The bytes of the font program.</param>
|
||||
/// <param name="length1">The length in bytes of the clear text portion of the font program.</param>
|
||||
/// <param name="length2">The length in bytes of the encrypted portion of the font program.</param>
|
||||
/// <param name="stackDepthGuard"></param>
|
||||
/// <returns>The parsed type 1 font.</returns>
|
||||
public static Type1Font Parse(IInputBytes inputBytes, int length1, int length2)
|
||||
public static Type1Font Parse(IInputBytes inputBytes, int length1, int length2, StackDepthGuard stackDepthGuard)
|
||||
{
|
||||
// Sometimes the entire PFB file including the header bytes can be included which prevents parsing in the normal way.
|
||||
var isEntirePfbFile = inputBytes.Peek() == PfbFileIndicator;
|
||||
@@ -42,15 +45,15 @@
|
||||
inputBytes = new MemoryInputBytes(ascii);
|
||||
}
|
||||
|
||||
var scanner = new CoreTokenScanner(inputBytes, false);
|
||||
var scanner = new CoreTokenScanner(inputBytes, false, stackDepthGuard);
|
||||
|
||||
if (!scanner.TryReadToken(out CommentToken comment) || !comment.Data.StartsWith("!"))
|
||||
{
|
||||
throw new InvalidFontFormatException("The Type1 program did not start with '%!'.");
|
||||
}
|
||||
|
||||
|
||||
string name;
|
||||
var parts = comment.Data.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var parts = comment.Data.Split(Separators, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length == 3)
|
||||
{
|
||||
name = parts[1];
|
||||
@@ -60,11 +63,9 @@
|
||||
name = "Unknown";
|
||||
}
|
||||
|
||||
var comments = new List<string>();
|
||||
|
||||
while (scanner.MoveNext() && scanner.CurrentToken is CommentToken commentToken)
|
||||
while (scanner.MoveNext() && scanner.CurrentToken is CommentToken)
|
||||
{
|
||||
comments.Add(commentToken.Data);
|
||||
// We ignore comments
|
||||
}
|
||||
|
||||
var dictionaries = new List<DictionaryToken>();
|
||||
@@ -441,7 +442,7 @@
|
||||
return null;
|
||||
}
|
||||
|
||||
private class PreviousTokenSet
|
||||
private sealed class PreviousTokenSet
|
||||
{
|
||||
private readonly IToken[] tokens = new IToken[3];
|
||||
|
||||
|
||||
@@ -62,6 +62,11 @@
|
||||
/// </summary>
|
||||
public PdfRectangle? GetCharacterBoundingBox(string characterName)
|
||||
{
|
||||
if (string.Equals(characterName, GlyphList.NotDefined, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var glyph = GetCharacterPath(characterName);
|
||||
return PdfSubpath.GetBoundingRectangle(glyph);
|
||||
}
|
||||
@@ -94,8 +99,13 @@
|
||||
/// <summary>
|
||||
/// Get the pdfpath for the character with the given name.
|
||||
/// </summary>
|
||||
public IReadOnlyList<PdfSubpath> GetCharacterPath(string characterName)
|
||||
public IReadOnlyList<PdfSubpath>? GetCharacterPath(string characterName)
|
||||
{
|
||||
if (string.Equals(characterName, GlyphList.NotDefined, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!CharStrings.TryGenerate(characterName, out var glyph))
|
||||
{
|
||||
return null;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net462;net471;net6.0;net8.0;net9.0</TargetFrameworks>
|
||||
<LangVersion>12</LangVersion>
|
||||
<Version>0.1.12</Version>
|
||||
<Version>0.1.14</Version>
|
||||
<IsTestProject>False</IsTestProject>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
{
|
||||
var parser = new CodespaceRangeParser();
|
||||
var byteArrayInput = new MemoryInputBytes(OtherEncodings.StringAsLatin1Bytes("1 begincodespacerange\nendcodespacerange"));
|
||||
var tokenScanner = new CoreTokenScanner(byteArrayInput, false);
|
||||
var tokenScanner = new CoreTokenScanner(byteArrayInput, false, new StackDepthGuard(256));
|
||||
|
||||
Assert.True(tokenScanner.MoveNext());
|
||||
Assert.True(tokenScanner.CurrentToken is NumericToken);
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
{
|
||||
var bytes = GetFileBytes("AdobeUtopia.pfa");
|
||||
|
||||
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0);
|
||||
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0, new StackDepthGuard(256));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -20,7 +20,7 @@
|
||||
// TODO: support reading in these pfb files
|
||||
var bytes = GetFileBytes("Raleway-Black.pfb");
|
||||
|
||||
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0);
|
||||
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0, new StackDepthGuard(256));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -28,7 +28,7 @@
|
||||
{
|
||||
var bytes = GetFileBytes("CMBX10.pfa");
|
||||
|
||||
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0);
|
||||
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0, new StackDepthGuard(256));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -36,7 +36,7 @@
|
||||
{
|
||||
var bytes = GetFileBytes("CMCSC10");
|
||||
|
||||
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0);
|
||||
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0, new StackDepthGuard(256));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -44,7 +44,7 @@
|
||||
{
|
||||
var bytes = GetFileBytes("CMBX12");
|
||||
|
||||
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0);
|
||||
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0, new StackDepthGuard(256));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -52,7 +52,7 @@
|
||||
{
|
||||
var bytes = GetFileBytes("CMBX10");
|
||||
|
||||
var result = Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0);
|
||||
var result = Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0, new StackDepthGuard(256));
|
||||
|
||||
var builder = new StringBuilder("<!DOCTYPE html><html><head></head><body>");
|
||||
foreach (var charString in result.CharStrings.CharStrings)
|
||||
@@ -71,7 +71,7 @@
|
||||
{
|
||||
var bytes = GetFileBytes("CMR10");
|
||||
|
||||
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0);
|
||||
Type1FontParser.Parse(new MemoryInputBytes(bytes), 0, 0, new StackDepthGuard(256));
|
||||
}
|
||||
|
||||
private static byte[] GetFileBytes(string name)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -6,9 +6,156 @@
|
||||
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()
|
||||
{
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -42,7 +42,7 @@ public class FirstPassParserTests
|
||||
var results = FirstPassParser.Parse(
|
||||
new FileHeaderOffset(0),
|
||||
ib.Bytes,
|
||||
new CoreTokenScanner(ib.Bytes, true));
|
||||
new CoreTokenScanner(ib.Bytes, true, new StackDepthGuard(256)));
|
||||
|
||||
Assert.Equal(2, results.Parts.Count);
|
||||
Assert.NotNull(results.Trailer);
|
||||
@@ -114,7 +114,7 @@ public class FirstPassParserTests
|
||||
|
||||
var ib = StringBytesTestConverter.Convert(content, false);
|
||||
|
||||
var results = FirstPassParser.Parse(new FileHeaderOffset(0), ib.Bytes, new CoreTokenScanner(ib.Bytes, true));
|
||||
var results = FirstPassParser.Parse(new FileHeaderOffset(0), ib.Bytes, new CoreTokenScanner(ib.Bytes, true, new StackDepthGuard(256)));
|
||||
|
||||
var offsets = results.Parts.Select(x => x.Offset).OrderBy(x => x).ToList();
|
||||
|
||||
@@ -123,7 +123,7 @@ public class FirstPassParserTests
|
||||
Assert.NotNull(results.Trailer);
|
||||
|
||||
ib.Bytes.Seek(98);
|
||||
var scanner = new CoreTokenScanner(ib.Bytes, false);
|
||||
var scanner = new CoreTokenScanner(ib.Bytes, false, new StackDepthGuard(256));
|
||||
scanner.MoveNext();
|
||||
Assert.Equal(scanner.CurrentToken, OperatorToken.Xref);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
public class PageContentParserTests
|
||||
{
|
||||
private readonly PageContentParser parser = new PageContentParser(ReflectionGraphicsStateOperationFactory.Instance);
|
||||
private readonly PageContentParser parser = new PageContentParser(ReflectionGraphicsStateOperationFactory.Instance, new StackDepthGuard(256));
|
||||
private readonly ILog log = new NoOpLog();
|
||||
|
||||
[Fact]
|
||||
@@ -210,7 +210,7 @@ l";
|
||||
var content = File.ReadAllText(path);
|
||||
var input = StringBytesTestConverter.Convert(content, false);
|
||||
|
||||
var lenientParser = new PageContentParser(ReflectionGraphicsStateOperationFactory.Instance, true);
|
||||
var lenientParser = new PageContentParser(ReflectionGraphicsStateOperationFactory.Instance, new StackDepthGuard(256), true);
|
||||
var result = lenientParser.Parse(1, input.Bytes, log);
|
||||
|
||||
Assert.NotEmpty(result);
|
||||
|
||||
@@ -149,7 +149,7 @@ three %PDF-1.6";
|
||||
|
||||
var bytes = new MemoryInputBytes(input);
|
||||
|
||||
var scanner = new CoreTokenScanner(bytes, true, ScannerScope.None);
|
||||
var scanner = new CoreTokenScanner(bytes, true, new StackDepthGuard(256), ScannerScope.None);
|
||||
|
||||
var result = FileHeaderParser.Parse(scanner, bytes, false, log);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
namespace UglyToad.PdfPig.Tests.Parser.Parts;
|
||||
|
||||
using PdfPig.Core;
|
||||
using PdfPig.Parser.FileStructure;
|
||||
using PdfPig.Tokenization.Scanner;
|
||||
|
||||
@@ -26,7 +27,7 @@ public class FirstPassParserStartXrefTests
|
||||
|
||||
var result = FirstPassParser.GetFirstCrossReferenceOffset(
|
||||
input.Bytes,
|
||||
new CoreTokenScanner(input.Bytes, true),
|
||||
new CoreTokenScanner(input.Bytes, true, new StackDepthGuard(256)),
|
||||
new TestingLog());
|
||||
|
||||
Assert.Equal(456, result.StartXRefDeclaredOffset);
|
||||
@@ -59,7 +60,7 @@ public class FirstPassParserStartXrefTests
|
||||
|
||||
var result = FirstPassParser.GetFirstCrossReferenceOffset(
|
||||
input.Bytes,
|
||||
new CoreTokenScanner(input.Bytes, true),
|
||||
new CoreTokenScanner(input.Bytes, true, new StackDepthGuard(256)),
|
||||
new TestingLog());
|
||||
|
||||
Assert.Equal(17, result.StartXRefDeclaredOffset);
|
||||
@@ -93,7 +94,7 @@ public class FirstPassParserStartXrefTests
|
||||
|
||||
var result = FirstPassParser.GetFirstCrossReferenceOffset(
|
||||
input.Bytes,
|
||||
new CoreTokenScanner(input.Bytes, true),
|
||||
new CoreTokenScanner(input.Bytes, true, new StackDepthGuard(256)),
|
||||
new TestingLog());
|
||||
|
||||
Assert.Equal(1384733, result.StartXRefDeclaredOffset);
|
||||
@@ -106,7 +107,7 @@ public class FirstPassParserStartXrefTests
|
||||
|
||||
var result = FirstPassParser.GetFirstCrossReferenceOffset(
|
||||
input.Bytes,
|
||||
new CoreTokenScanner(input.Bytes, true),
|
||||
new CoreTokenScanner(input.Bytes, true, new StackDepthGuard(256)),
|
||||
new TestingLog());
|
||||
|
||||
Assert.Null(result.StartXRefDeclaredOffset);
|
||||
@@ -130,7 +131,7 @@ public class FirstPassParserStartXrefTests
|
||||
|
||||
var result = FirstPassParser.GetFirstCrossReferenceOffset(
|
||||
input.Bytes,
|
||||
new CoreTokenScanner(input.Bytes, true),
|
||||
new CoreTokenScanner(input.Bytes, true, new StackDepthGuard(256)),
|
||||
new TestingLog());
|
||||
|
||||
Assert.Null(result.StartXRefDeclaredOffset);
|
||||
@@ -151,7 +152,7 @@ public class FirstPassParserStartXrefTests
|
||||
|
||||
var result = FirstPassParser.GetFirstCrossReferenceOffset(
|
||||
input.Bytes,
|
||||
new CoreTokenScanner(input.Bytes, true),
|
||||
new CoreTokenScanner(input.Bytes, true, new StackDepthGuard(256)),
|
||||
new TestingLog());
|
||||
|
||||
Assert.Null(result.StartXRefDeclaredOffset);
|
||||
@@ -185,7 +186,7 @@ public class FirstPassParserStartXrefTests
|
||||
|
||||
var result = FirstPassParser.GetFirstCrossReferenceOffset(
|
||||
input.Bytes,
|
||||
new CoreTokenScanner(input.Bytes, true),
|
||||
new CoreTokenScanner(input.Bytes, true, new StackDepthGuard(256)),
|
||||
new TestingLog());
|
||||
|
||||
Assert.Equal(1274665676543, result.StartXRefDeclaredOffset);
|
||||
@@ -207,7 +208,7 @@ public class FirstPassParserStartXrefTests
|
||||
|
||||
var result = FirstPassParser.GetFirstCrossReferenceOffset(
|
||||
input.Bytes,
|
||||
new CoreTokenScanner(input.Bytes, true),
|
||||
new CoreTokenScanner(input.Bytes, true, new StackDepthGuard(256)),
|
||||
new TestingLog());
|
||||
|
||||
Assert.Equal(57695, result.StartXRefDeclaredOffset);
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
internal static (CoreTokenScanner scanner, IInputBytes bytes) Scanner(string s)
|
||||
{
|
||||
var inputBytes = new MemoryInputBytes(OtherEncodings.StringAsLatin1Bytes(s));
|
||||
var result = new CoreTokenScanner(inputBytes, true);
|
||||
var result = new CoreTokenScanner(inputBytes, true, new StackDepthGuard(256));
|
||||
|
||||
return (result, inputBytes);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
namespace UglyToad.PdfPig.Tests.Tokenization
|
||||
{
|
||||
using PdfPig.Core;
|
||||
using PdfPig.Tokenization;
|
||||
using PdfPig.Tokens;
|
||||
|
||||
public class ArrayTokenizerTests
|
||||
{
|
||||
private readonly ArrayTokenizer tokenizer = new ArrayTokenizer(true);
|
||||
private readonly ArrayTokenizer tokenizer = new ArrayTokenizer(true, new StackDepthGuard(256));
|
||||
|
||||
[Theory]
|
||||
[InlineData("]")]
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace UglyToad.PdfPig.Tests.Tokenization
|
||||
|
||||
public class DictionaryTokenizerTests
|
||||
{
|
||||
private readonly DictionaryTokenizer tokenizer = new DictionaryTokenizer(true);
|
||||
private readonly DictionaryTokenizer tokenizer = new DictionaryTokenizer(true, new StackDepthGuard(256));
|
||||
|
||||
[Theory]
|
||||
[InlineData("[rjee]")]
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace UglyToad.PdfPig.Tests.Tokenization.Scanner
|
||||
|
||||
public CoreTokenScannerTests()
|
||||
{
|
||||
scannerFactory = x => new CoreTokenScanner(x, true);
|
||||
scannerFactory = x => new CoreTokenScanner(x, true, new StackDepthGuard(256));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -231,7 +231,7 @@ endobj";
|
||||
|
||||
var scanner = new CoreTokenScanner(
|
||||
StringBytesTestConverter.Convert(content, false).Bytes,
|
||||
true,
|
||||
true, new StackDepthGuard(256),
|
||||
isStream: true);
|
||||
|
||||
while (scanner.MoveNext())
|
||||
@@ -247,7 +247,7 @@ endobj";
|
||||
|
||||
var nonStreamScanner = new CoreTokenScanner(
|
||||
StringBytesTestConverter.Convert(content, false).Bytes,
|
||||
true,
|
||||
true, new StackDepthGuard(256),
|
||||
isStream: false);
|
||||
|
||||
while (nonStreamScanner.MoveNext())
|
||||
@@ -293,7 +293,7 @@ endobj";
|
||||
|
||||
var scanner = new CoreTokenScanner(
|
||||
StringBytesTestConverter.Convert(content, false).Bytes,
|
||||
true,
|
||||
true, new StackDepthGuard(256),
|
||||
isStream: true);
|
||||
|
||||
while (scanner.MoveNext())
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
using System.Text;
|
||||
using PdfPig.Core;
|
||||
using PdfPig.Encryption;
|
||||
using PdfPig.Parser.FileStructure;
|
||||
using PdfPig.Tokenization.Scanner;
|
||||
using PdfPig.Tokens;
|
||||
|
||||
@@ -720,8 +721,13 @@ endobj";
|
||||
{
|
||||
var input = StringBytesTestConverter.Convert(s, false);
|
||||
|
||||
return new PdfTokenScanner(input.Bytes, locationProvider ?? new TestObjectLocationProvider(),
|
||||
new TestFilterProvider(), NoOpEncryptionHandler.Instance, useLenientParsing ? new ParsingOptions() : ParsingOptions.LenientParsingOff);
|
||||
return new PdfTokenScanner(input.Bytes,
|
||||
locationProvider ?? new TestObjectLocationProvider(),
|
||||
new TestFilterProvider(),
|
||||
NoOpEncryptionHandler.Instance,
|
||||
new FileHeaderOffset(0),
|
||||
useLenientParsing ? new ParsingOptions() : ParsingOptions.LenientParsingOff,
|
||||
new StackDepthGuard(256));
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ObjectToken> ReadToEnd(PdfTokenScanner scanner)
|
||||
|
||||
@@ -1295,6 +1295,78 @@
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanAddLinkToPage()
|
||||
{
|
||||
var builder = new PdfDocumentBuilder();
|
||||
var page = builder.AddPage(PageSize.A4);
|
||||
var font = builder.AddStandard14Font(Standard14Font.Helvetica);
|
||||
|
||||
var linkArea = new PdfRectangle(25, 690, 200, 720);
|
||||
page.AddLink("https://github.com", linkArea);
|
||||
|
||||
var bytes = builder.Build();
|
||||
WriteFile(nameof(CanAddLinkToPage), bytes);
|
||||
|
||||
using (var document = PdfDocument.Open(bytes))
|
||||
{
|
||||
Assert.Equal(1, document.NumberOfPages);
|
||||
var page1 = document.GetPage(1);
|
||||
|
||||
var annotations = page1.GetAnnotations().ToList();
|
||||
Assert.Single(annotations);
|
||||
|
||||
var linkAnnotation = annotations[0];
|
||||
Assert.Equal(Annotations.AnnotationType.Link, linkAnnotation.Type);
|
||||
Assert.Equal(linkArea, linkAnnotation.Rectangle);
|
||||
|
||||
// Verify the URI link target
|
||||
Assert.NotNull(linkAnnotation.Action);
|
||||
var uriAction = Assert.IsType<Actions.UriAction>(linkAnnotation.Action);
|
||||
Assert.Equal("https://github.com", uriAction.Uri);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanAddInternalLinkToPage()
|
||||
{
|
||||
var builder = new PdfDocumentBuilder();
|
||||
var font = builder.AddStandard14Font(Standard14Font.Helvetica);
|
||||
|
||||
var page1 = builder.AddPage(PageSize.A4);
|
||||
var page2 = builder.AddPage(PageSize.A4);
|
||||
|
||||
var linkArea = new PdfRectangle(25, 690, 200, 720);
|
||||
var coordinates = new ExplicitDestinationCoordinates(25, 750);
|
||||
var destination = new ExplicitDestination(1, ExplicitDestinationType.XyzCoordinates, coordinates);
|
||||
page2.AddLink(destination, linkArea);
|
||||
|
||||
var bytes = builder.Build();
|
||||
WriteFile(nameof(CanAddInternalLinkToPage), bytes);
|
||||
|
||||
using (var document = PdfDocument.Open(bytes))
|
||||
{
|
||||
Assert.Equal(2, document.NumberOfPages);
|
||||
|
||||
var page2Doc = document.GetPage(2);
|
||||
|
||||
var annotations = page2Doc.GetAnnotations().ToList();
|
||||
Assert.Single(annotations);
|
||||
|
||||
var linkAnnotation = annotations[0];
|
||||
Assert.Equal(Annotations.AnnotationType.Link, linkAnnotation.Type);
|
||||
Assert.Equal(linkArea, linkAnnotation.Rectangle);
|
||||
|
||||
// Verify the link destination
|
||||
Assert.NotNull(linkAnnotation.Action);
|
||||
var goToAction = Assert.IsType<Actions.GoToAction>(linkAnnotation.Action);
|
||||
Assert.Equal(1, goToAction.Destination.PageNumber);
|
||||
Assert.Equal(ExplicitDestinationType.XyzCoordinates, goToAction.Destination.Type);
|
||||
Assert.Equal(25, goToAction.Destination.Coordinates.Left);
|
||||
Assert.Equal(750, goToAction.Destination.Coordinates.Top);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteFile(string name, byte[] bytes, string extension = "pdf")
|
||||
{
|
||||
try
|
||||
|
||||
@@ -8,12 +8,14 @@
|
||||
internal sealed class ArrayTokenizer : ITokenizer
|
||||
{
|
||||
private readonly bool usePdfDocEncoding;
|
||||
private readonly StackDepthGuard stackDepthGuard;
|
||||
|
||||
public bool ReadsNextByte { get; } = false;
|
||||
|
||||
public ArrayTokenizer(bool usePdfDocEncoding)
|
||||
public ArrayTokenizer(bool usePdfDocEncoding, StackDepthGuard stackDepthGuard)
|
||||
{
|
||||
this.usePdfDocEncoding = usePdfDocEncoding;
|
||||
this.stackDepthGuard = stackDepthGuard;
|
||||
}
|
||||
|
||||
public bool TryTokenize(byte currentByte, IInputBytes inputBytes, out IToken token)
|
||||
@@ -25,7 +27,7 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
var scanner = new CoreTokenScanner(inputBytes, usePdfDocEncoding, ScannerScope.Array);
|
||||
var scanner = new CoreTokenScanner(inputBytes, usePdfDocEncoding, stackDepthGuard, ScannerScope.Array);
|
||||
|
||||
var contents = new List<IToken>();
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
private readonly bool usePdfDocEncoding;
|
||||
private readonly IReadOnlyList<NameToken> requiredKeys;
|
||||
private readonly bool useLenientParsing;
|
||||
private readonly StackDepthGuard stackDepthGuard;
|
||||
|
||||
public bool ReadsNextByte { get; } = false;
|
||||
|
||||
@@ -19,14 +20,16 @@
|
||||
/// <param name="usePdfDocEncoding">
|
||||
/// Whether to read strings using the PdfDocEncoding.
|
||||
/// </param>
|
||||
/// <param name="stackDepthGuard"></param>
|
||||
/// <param name="requiredKeys">
|
||||
/// Can be provided to recover from errors with missing dictionary end symbols if the
|
||||
/// set of keys expected in the dictionary are known.
|
||||
/// </param>
|
||||
/// <param name="useLenientParsing">Whether to use lenient parsing.</param>
|
||||
public DictionaryTokenizer(bool usePdfDocEncoding, IReadOnlyList<NameToken> requiredKeys = null, bool useLenientParsing = false)
|
||||
public DictionaryTokenizer(bool usePdfDocEncoding, StackDepthGuard stackDepthGuard, IReadOnlyList<NameToken> requiredKeys = null, bool useLenientParsing = false)
|
||||
{
|
||||
this.usePdfDocEncoding = usePdfDocEncoding;
|
||||
this.stackDepthGuard = stackDepthGuard;
|
||||
this.requiredKeys = requiredKeys;
|
||||
this.useLenientParsing = useLenientParsing;
|
||||
}
|
||||
@@ -83,7 +86,7 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
var coreScanner = new CoreTokenScanner(inputBytes, usePdfDocEncoding, ScannerScope.Dictionary, useLenientParsing: useLenientParsing);
|
||||
var coreScanner = new CoreTokenScanner(inputBytes, usePdfDocEncoding, stackDepthGuard, ScannerScope.Dictionary, useLenientParsing: useLenientParsing);
|
||||
|
||||
var tokens = new List<IToken>();
|
||||
|
||||
|
||||
@@ -52,12 +52,15 @@
|
||||
/// </remarks>
|
||||
private readonly bool isStream;
|
||||
|
||||
private readonly StackDepthGuard stackDepthGuard;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="CoreTokenScanner"/> from the input.
|
||||
/// </summary>
|
||||
public CoreTokenScanner(
|
||||
IInputBytes inputBytes,
|
||||
bool usePdfDocEncoding,
|
||||
StackDepthGuard stackDepthGuard,
|
||||
ScannerScope scope = ScannerScope.None,
|
||||
IReadOnlyDictionary<NameToken, IReadOnlyList<NameToken>> namedDictionaryRequiredKeys = null,
|
||||
bool useLenientParsing = false,
|
||||
@@ -65,9 +68,10 @@
|
||||
{
|
||||
this.inputBytes = inputBytes ?? throw new ArgumentNullException(nameof(inputBytes));
|
||||
this.usePdfDocEncoding = usePdfDocEncoding;
|
||||
this.stackDepthGuard = stackDepthGuard;
|
||||
this.stringTokenizer = new StringTokenizer(usePdfDocEncoding);
|
||||
this.arrayTokenizer = new ArrayTokenizer(usePdfDocEncoding);
|
||||
this.dictionaryTokenizer = new DictionaryTokenizer(usePdfDocEncoding, useLenientParsing: useLenientParsing);
|
||||
this.arrayTokenizer = new ArrayTokenizer(usePdfDocEncoding, this.stackDepthGuard);
|
||||
this.dictionaryTokenizer = new DictionaryTokenizer(usePdfDocEncoding, this.stackDepthGuard, useLenientParsing: useLenientParsing);
|
||||
this.scope = scope;
|
||||
this.namedDictionaryRequiredKeys = namedDictionaryRequiredKeys;
|
||||
this.useLenientParsing = useLenientParsing;
|
||||
@@ -101,6 +105,19 @@
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool MoveNext()
|
||||
{
|
||||
stackDepthGuard.Enter();
|
||||
try
|
||||
{
|
||||
return MoveNextInternal();
|
||||
}
|
||||
finally
|
||||
{
|
||||
stackDepthGuard.Exit();
|
||||
}
|
||||
}
|
||||
|
||||
private bool MoveNextInternal()
|
||||
{
|
||||
var endAngleBracesRead = 0;
|
||||
|
||||
@@ -169,7 +186,7 @@
|
||||
&& CurrentToken is NameToken name
|
||||
&& namedDictionaryRequiredKeys.TryGetValue(name, out var requiredKeys))
|
||||
{
|
||||
tokenizer = new DictionaryTokenizer(usePdfDocEncoding, requiredKeys, useLenientParsing);
|
||||
tokenizer = new DictionaryTokenizer(usePdfDocEncoding, stackDepthGuard, requiredKeys, useLenientParsing);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net462;net471;net6.0;net8.0;net9.0</TargetFrameworks>
|
||||
<LangVersion>12</LangVersion>
|
||||
<Version>0.1.12</Version>
|
||||
<Version>0.1.14</Version>
|
||||
<IsTestProject>False</IsTestProject>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net462;net471;net6.0;net8.0;net9.0</TargetFrameworks>
|
||||
<LangVersion>12</LangVersion>
|
||||
<Version>0.1.12</Version>
|
||||
<Version>0.1.14</Version>
|
||||
<IsTestProject>False</IsTestProject>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
|
||||
@@ -59,7 +59,10 @@ namespace UglyToad.PdfPig.Content
|
||||
switch (Type)
|
||||
{
|
||||
case "OCG": // Optional content group dictionary
|
||||
// Name - Required
|
||||
// Name - Per spec this is required, but in practice some PDFs store layer names
|
||||
// at the document catalog level in OCProperties rather than in marked content Properties.
|
||||
// To avoid crashes, we make this optional and fall back to null or the tag name.
|
||||
|
||||
if (markedContentElement.Properties.TryGet(NameToken.Name, pdfTokenScanner, out NameToken? name))
|
||||
{
|
||||
Name = name.Data;
|
||||
@@ -74,7 +77,9 @@ namespace UglyToad.PdfPig.Content
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException($"Cannot parse optional content's {nameof(Name)} from {nameof(markedContentElement.Properties)}. This is a required field.", nameof(markedContentElement.Properties));
|
||||
// Name not found in Properties - use tag as fallback or leave as null
|
||||
// This handles PDFs where layer names are stored at document catalog level
|
||||
Name = markedContentElement.Tag;
|
||||
}
|
||||
|
||||
// Intent - Optional
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
private readonly StackDictionary<NameToken, IndirectReference> currentFontState = new StackDictionary<NameToken, IndirectReference>();
|
||||
private readonly StackDictionary<NameToken, IndirectReference> currentXObjectState = new StackDictionary<NameToken, IndirectReference>();
|
||||
|
||||
private readonly Dictionary<NameToken, DictionaryToken> extendedGraphicsStates = new Dictionary<NameToken, DictionaryToken>();
|
||||
private readonly StackDictionary<NameToken, DictionaryToken> extendedGraphicsStates = new StackDictionary<NameToken, DictionaryToken>();
|
||||
|
||||
private readonly StackDictionary<NameToken, ResourceColorSpace> namedColorSpaces = new StackDictionary<NameToken, ResourceColorSpace>();
|
||||
private readonly Dictionary<NameToken, ColorSpaceDetails> loadedNamedColorSpaceDetails = new Dictionary<NameToken, ColorSpaceDetails>();
|
||||
@@ -55,7 +55,8 @@
|
||||
|
||||
namedColorSpaces.Push();
|
||||
currentFontState.Push();
|
||||
currentXObjectState.Push();
|
||||
currentXObjectState.Push();
|
||||
extendedGraphicsStates.Push();
|
||||
|
||||
if (resourceDictionary.TryGet(NameToken.Font, out var fontBase))
|
||||
{
|
||||
@@ -190,6 +191,7 @@
|
||||
currentFontState.Pop();
|
||||
currentXObjectState.Pop();
|
||||
namedColorSpaces.Pop();
|
||||
extendedGraphicsStates.Pop();
|
||||
}
|
||||
|
||||
private void LoadFontDictionary(DictionaryToken fontDictionary)
|
||||
@@ -348,8 +350,13 @@
|
||||
|
||||
public DictionaryToken? GetExtendedGraphicsStateDictionary(NameToken name)
|
||||
{
|
||||
if (parsingOptions.UseLenientParsing && !extendedGraphicsStates.ContainsKey(name))
|
||||
{
|
||||
if (parsingOptions.UseLenientParsing)
|
||||
{
|
||||
if (extendedGraphicsStates.TryGetValue(name, out var dictToken))
|
||||
{
|
||||
return dictToken;
|
||||
}
|
||||
|
||||
parsingOptions.Logger.Error($"The graphic state dictionary does not contain the key '{name}'.");
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -215,6 +215,8 @@
|
||||
/// <inheritdoc/>
|
||||
public void ShowText(IInputBytes bytes)
|
||||
{
|
||||
TextSequence++;
|
||||
|
||||
var currentState = GetCurrentState();
|
||||
|
||||
var font = currentState.FontState.FromExtendedGraphicsState
|
||||
@@ -585,7 +587,8 @@
|
||||
if (hasCircularReference)
|
||||
{
|
||||
if (ParsingOptions.UseLenientParsing)
|
||||
{
|
||||
{
|
||||
// TODO - We might be removing too much, good for the moment. See Issues1250() for examples
|
||||
operations = operations.Where(o => o is not InvokeNamedXObject xo || xo.Name != xObjectName)
|
||||
.ToArray();
|
||||
ParsingOptions.Logger.Warn(
|
||||
@@ -616,14 +619,56 @@
|
||||
/// <param name="xObjectName">The form's name.</param>
|
||||
/// <param name="operations">The form operations parsed from original form stream.</param>
|
||||
protected virtual bool HasFormXObjectCircularReference(StreamToken formStream,
|
||||
NameToken xObjectName,
|
||||
NameToken? xObjectName,
|
||||
IReadOnlyList<IGraphicsStateOperation> operations)
|
||||
{
|
||||
return xObjectName != null
|
||||
&& operations.OfType<InvokeNamedXObject>()?.Any(o => o.Name == xObjectName) ==
|
||||
true // operations contain another form with same name
|
||||
&& ResourceStore.TryGetXObject(xObjectName, out var result)
|
||||
&& result.Data.Span.SequenceEqual(formStream.Data.Span); // The form contained in the operations has identical data to current form
|
||||
if (xObjectName is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (operations.OfType<InvokeNamedXObject>()?.Any(o => o.Name == xObjectName) != true)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryGetXObjectToken(formStream, xObjectName, PdfScanner, out var t1))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ResourceStore.TryGetXObject(xObjectName, out var resourceStream))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryGetXObjectToken(resourceStream, xObjectName, PdfScanner, out var t2))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (t1 is null || t2 is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return t1.Equals(t2);
|
||||
|
||||
static bool TryGetXObjectToken(StreamToken streamToken, NameToken xObjectName, IPdfTokenScanner scanner, out IToken? token)
|
||||
{
|
||||
token = null;
|
||||
if (!streamToken.StreamDictionary.TryGet<DictionaryToken>(NameToken.Resources, scanner, out var formResources))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!formResources.TryGet<DictionaryToken>(NameToken.Xobject, out var xObjectBase) || !xObjectBase.TryGet(xObjectName, out token))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return token is not null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
operationContext.ShowText(input);
|
||||
}
|
||||
|
||||
string? EscapeText(string? text)
|
||||
private static string? EscapeText(string? text)
|
||||
{
|
||||
if (text is null) return null;
|
||||
// Fix Issue 350 from PDF Spec 1.7 (page 408) on handling 'special characters' of '(', ')' and '\'.
|
||||
|
||||
@@ -61,6 +61,9 @@ internal static partial class FirstPassParser
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Code below commented to fix issue #1208, while not failing test Issue874
|
||||
* The logic would need to be reviewed before re-enabling.
|
||||
|
||||
// If we didn't brute force then use the previous position for ordering.
|
||||
foreach (var obj in streamsAndTables)
|
||||
{
|
||||
@@ -81,6 +84,11 @@ internal static partial class FirstPassParser
|
||||
orderedXrefs.Add(obj);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
orderedXrefs.AddRange(
|
||||
streamsAndTables
|
||||
.OrderBy(x => x.Offset));
|
||||
}
|
||||
|
||||
DictionaryToken? lastTrailer = null;
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
{
|
||||
private readonly IGraphicsStateOperationFactory operationFactory;
|
||||
private readonly bool useLenientParsing;
|
||||
private readonly StackDepthGuard stackDepthGuard;
|
||||
|
||||
/// <summary>
|
||||
/// Initialises a new instance of the <see cref="PageContentParser"/> class.
|
||||
@@ -28,12 +29,14 @@
|
||||
/// <param name="operationFactory">
|
||||
/// The factory responsible for creating graphics state operations.
|
||||
/// </param>
|
||||
/// <param name="stackDepthGuard"></param>
|
||||
/// <param name="useLenientParsing">
|
||||
/// A value indicating whether lenient parsing should be used. Defaults to <c>false</c>.
|
||||
/// </param>
|
||||
public PageContentParser(IGraphicsStateOperationFactory operationFactory, bool useLenientParsing = false)
|
||||
public PageContentParser(IGraphicsStateOperationFactory operationFactory, StackDepthGuard stackDepthGuard, bool useLenientParsing = false)
|
||||
{
|
||||
this.operationFactory = operationFactory;
|
||||
this.stackDepthGuard = stackDepthGuard;
|
||||
this.useLenientParsing = useLenientParsing;
|
||||
}
|
||||
|
||||
@@ -55,7 +58,7 @@
|
||||
IInputBytes inputBytes,
|
||||
ILog log)
|
||||
{
|
||||
var scanner = new CoreTokenScanner(inputBytes, false, useLenientParsing: useLenientParsing);
|
||||
var scanner = new CoreTokenScanner(inputBytes, false, stackDepthGuard, useLenientParsing: useLenientParsing);
|
||||
|
||||
var precedingTokens = new List<IToken>();
|
||||
var graphicsStateOperations = new List<IGraphicsStateOperation>();
|
||||
|
||||
@@ -89,7 +89,9 @@
|
||||
SkipMissingFonts = false
|
||||
};
|
||||
|
||||
var tokenScanner = new CoreTokenScanner(inputBytes, true, useLenientParsing: options.UseLenientParsing);
|
||||
var stackDepthGuard = new StackDepthGuard(options.MaxStackDepth);
|
||||
|
||||
var tokenScanner = new CoreTokenScanner(inputBytes, true, stackDepthGuard, useLenientParsing: options.UseLenientParsing);
|
||||
|
||||
var passwords = new List<string>();
|
||||
|
||||
@@ -110,7 +112,7 @@
|
||||
|
||||
options.Passwords = passwords;
|
||||
|
||||
var document = OpenDocument(inputBytes, tokenScanner, options);
|
||||
var document = OpenDocument(inputBytes, tokenScanner, options, stackDepthGuard);
|
||||
|
||||
return document;
|
||||
}
|
||||
@@ -118,14 +120,17 @@
|
||||
private static PdfDocument OpenDocument(
|
||||
IInputBytes inputBytes,
|
||||
ISeekableTokenScanner scanner,
|
||||
ParsingOptions parsingOptions)
|
||||
ParsingOptions parsingOptions,
|
||||
StackDepthGuard stackDepthGuard)
|
||||
{
|
||||
var filterProvider = new FilterProviderWithLookup(parsingOptions.FilterProvider ?? DefaultFilterProvider.Instance);
|
||||
|
||||
var version = FileHeaderParser.Parse(scanner, inputBytes, parsingOptions.UseLenientParsing, parsingOptions.Logger);
|
||||
|
||||
var fileHeaderOffset = new FileHeaderOffset((int)version.OffsetInFile);
|
||||
|
||||
var initialParse = FirstPassParser.Parse(
|
||||
new FileHeaderOffset((int)version.OffsetInFile),
|
||||
fileHeaderOffset,
|
||||
inputBytes,
|
||||
scanner,
|
||||
parsingOptions.Logger);
|
||||
@@ -143,7 +148,7 @@
|
||||
initialParse.BruteForceOffsets,
|
||||
inputBytes);
|
||||
|
||||
var pdfScanner = new PdfTokenScanner(inputBytes, locationProvider, filterProvider, NoOpEncryptionHandler.Instance, parsingOptions);
|
||||
var pdfScanner = new PdfTokenScanner(inputBytes, locationProvider, filterProvider, NoOpEncryptionHandler.Instance, fileHeaderOffset, parsingOptions, stackDepthGuard);
|
||||
|
||||
var (rootReference, rootDictionary) = ParseTrailer(
|
||||
trailer,
|
||||
@@ -180,6 +185,7 @@
|
||||
filterProvider,
|
||||
encodingReader,
|
||||
cmapCache,
|
||||
stackDepthGuard,
|
||||
parsingOptions.UseLenientParsing);
|
||||
|
||||
var trueTypeHandler = new TrueTypeFontHandler(
|
||||
@@ -206,7 +212,7 @@
|
||||
parsingOptions.UseLenientParsing);
|
||||
|
||||
var pageFactory = new PageFactory(pdfScanner, resourceContainer, filterProvider,
|
||||
new PageContentParser(ReflectionGraphicsStateOperationFactory.Instance, parsingOptions.UseLenientParsing), parsingOptions);
|
||||
new PageContentParser(ReflectionGraphicsStateOperationFactory.Instance, stackDepthGuard, parsingOptions.UseLenientParsing), parsingOptions);
|
||||
|
||||
var catalog = CatalogFactory.Create(
|
||||
rootReference,
|
||||
|
||||
@@ -52,6 +52,13 @@
|
||||
/// </summary>
|
||||
public bool SkipMissingFonts { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum allowed stack depth.
|
||||
/// </summary>
|
||||
/// <remarks>This property can be used to limit the depth of recursive or nested operations to
|
||||
/// prevent stack overflows or excessive resource usage.</remarks>
|
||||
public int MaxStackDepth { get; set; } = 256;
|
||||
|
||||
/// <summary>
|
||||
/// Filter provider to use while parsing the document. The <see cref="DefaultFilterProvider"/> will be used if set to <c>null</c>.
|
||||
/// </summary>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Core;
|
||||
using Filters;
|
||||
using Parser.Parts;
|
||||
@@ -156,6 +157,23 @@
|
||||
resolvedItems[NameToken.Create(kvp.Key)] = ResolveInternal(value, scanner, visited);
|
||||
}
|
||||
|
||||
if (resolvedItems.Count != dict.Data.Count)
|
||||
{
|
||||
if (resolvedItems.Count > dict.Data.Count)
|
||||
{
|
||||
throw new InvalidOperationException("Resolved more items than were present in the original dictionary. This should not be possible.");
|
||||
}
|
||||
|
||||
// We missed some due to cycles, try and resolve them now.
|
||||
foreach (var missing in dict.Data.Keys.Except(resolvedItems.Keys.Select(k => k.Data), StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
if (dict.Data[missing] is IndirectReferenceToken reference)
|
||||
{
|
||||
resolvedItems[NameToken.Create(missing)] = ResolveInternal(reference, scanner, visited);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new DictionaryToken(resolvedItems);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
{
|
||||
var scanner = new CoreTokenScanner(inputBytes,
|
||||
false,
|
||||
StackDepthGuard.Infinite, // We don't check for stack overflows, we might want to change that.
|
||||
namedDictionaryRequiredKeys: new Dictionary<NameToken, IReadOnlyList<NameToken>>
|
||||
{
|
||||
{ NameToken.CidSystemInfo, new[] { NameToken.Registry, NameToken.Ordering, NameToken.Supplement } }
|
||||
|
||||
@@ -21,12 +21,14 @@
|
||||
private readonly IEncodingReader encodingReader;
|
||||
private readonly CMapLocalCache cmapLocalCache;
|
||||
private readonly bool isLenientParsing;
|
||||
private readonly StackDepthGuard stackDepthGuard;
|
||||
|
||||
public Type1FontHandler(
|
||||
IPdfTokenScanner pdfScanner,
|
||||
ILookupFilterProvider filterProvider,
|
||||
IEncodingReader encodingReader,
|
||||
CMapLocalCache cmapLocalCache,
|
||||
StackDepthGuard stackDepthGuard,
|
||||
bool isLenientParsing)
|
||||
{
|
||||
this.pdfScanner = pdfScanner;
|
||||
@@ -34,6 +36,7 @@
|
||||
this.encodingReader = encodingReader;
|
||||
this.cmapLocalCache = cmapLocalCache;
|
||||
this.isLenientParsing = isLenientParsing;
|
||||
this.stackDepthGuard = stackDepthGuard;
|
||||
}
|
||||
|
||||
public IFont Generate(DictionaryToken dictionary)
|
||||
@@ -122,7 +125,7 @@
|
||||
{
|
||||
if (font.TryGetFirst(out var t1Font))
|
||||
{
|
||||
fromFont = t1Font.Encoding is not null ? new BuiltInEncoding(t1Font.Encoding) : default(Encoding);
|
||||
fromFont = (t1Font.Encoding is not null && t1Font.Encoding.Count > 0) ? new BuiltInEncoding(t1Font.Encoding) : default(Encoding);
|
||||
}
|
||||
else if (font.TryGetSecond(out var cffFont))
|
||||
{
|
||||
@@ -132,7 +135,7 @@
|
||||
|
||||
var encoding = encodingReader.Read(dictionary, descriptor, fromFont);
|
||||
|
||||
if (encoding is null && font != null && font.TryGetFirst(out var t1FontReplacement))
|
||||
if (encoding is null && font != null && font.TryGetFirst(out var t1FontReplacement) && t1FontReplacement.Encoding?.Count > 0)
|
||||
{
|
||||
encoding = new BuiltInEncoding(t1FontReplacement.Encoding);
|
||||
}
|
||||
@@ -172,7 +175,7 @@
|
||||
var length1 = stream.StreamDictionary.Get<NumericToken>(NameToken.Length1, pdfScanner);
|
||||
var length2 = stream.StreamDictionary.Get<NumericToken>(NameToken.Length2, pdfScanner);
|
||||
|
||||
var font = Type1FontParser.Parse(new MemoryInputBytes(bytes), length1.Int, length2.Int);
|
||||
var font = Type1FontParser.Parse(new MemoryInputBytes(bytes), length1.Int, length2.Int, stackDepthGuard);
|
||||
|
||||
return Union<Type1Font, CompactFontFormatFontCollection>.One(font);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
private readonly FontDescriptor fontDescriptor;
|
||||
|
||||
private readonly Encoding encoding;
|
||||
private readonly Encoding? encoding;
|
||||
|
||||
private readonly Union<Type1Font, CompactFontFormatFontCollection>? fontProgram;
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
|
||||
var matrix = DefaultTransformationMatrix;
|
||||
|
||||
if (fontProgram != null)
|
||||
if (fontProgram is not null)
|
||||
{
|
||||
if (fontProgram.TryGetFirst(out var t1Font))
|
||||
{
|
||||
@@ -183,15 +183,7 @@
|
||||
|
||||
try
|
||||
{
|
||||
if (isZapfDingbats)
|
||||
{
|
||||
value = GlyphList.ZapfDingbats.NameToUnicode(name);
|
||||
if (value is not null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
value = GlyphList.AdobeGlyphList.NameToUnicode(name);
|
||||
value = NameToUnicode(name);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -201,6 +193,34 @@
|
||||
return value is not null;
|
||||
}
|
||||
|
||||
private string? NameToUnicode(string name)
|
||||
{
|
||||
if (isZapfDingbats)
|
||||
{
|
||||
string value = GlyphList.ZapfDingbats.NameToUnicode(name);
|
||||
if (value is not null)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return GlyphList.AdobeGlyphList.NameToUnicode(name);
|
||||
}
|
||||
|
||||
public string UnicodeCodePointToName(int unicodeValue)
|
||||
{
|
||||
if (isZapfDingbats)
|
||||
{
|
||||
string value = GlyphList.ZapfDingbats.UnicodeCodePointToName(unicodeValue);
|
||||
if (value is not null)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return GlyphList.AdobeGlyphList.UnicodeCodePointToName(unicodeValue);
|
||||
}
|
||||
|
||||
public CharacterBoundingBox GetBoundingBox(int characterCode)
|
||||
{
|
||||
if (cachedBoundingBoxes.TryGetValue(characterCode, out var box))
|
||||
@@ -255,7 +275,16 @@
|
||||
PdfRectangle? rect = null;
|
||||
if (fontProgram.TryGetFirst(out var t1Font))
|
||||
{
|
||||
var name = encoding.GetName(characterCode);
|
||||
string name;
|
||||
if (encoding is not null)
|
||||
{
|
||||
name = encoding.GetName(characterCode);
|
||||
}
|
||||
else
|
||||
{
|
||||
name = UnicodeCodePointToName(characterCode);
|
||||
}
|
||||
|
||||
rect = t1Font.GetCharacterBoundingBox(name);
|
||||
}
|
||||
else if (fontProgram.TryGetSecond(out var cffFont))
|
||||
@@ -308,21 +337,30 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fontProgram == null)
|
||||
if (fontProgram is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fontProgram.TryGetFirst(out var t1Font))
|
||||
{
|
||||
var name = encoding.GetName(characterCode);
|
||||
string name;
|
||||
if (encoding is not null)
|
||||
{
|
||||
name = encoding.GetName(characterCode);
|
||||
}
|
||||
else
|
||||
{
|
||||
name = UnicodeCodePointToName(characterCode);
|
||||
}
|
||||
|
||||
tempPath = t1Font.GetCharacterPath(name);
|
||||
}
|
||||
else if (fontProgram.TryGetSecond(out var cffFont))
|
||||
{
|
||||
var first = cffFont.FirstFont;
|
||||
string characterName;
|
||||
if (encoding != null)
|
||||
if (encoding is not null)
|
||||
{
|
||||
characterName = encoding.GetName(characterCode);
|
||||
}
|
||||
@@ -334,7 +372,7 @@
|
||||
tempPath = first.GetCharacterPath(characterName);
|
||||
}
|
||||
|
||||
if (tempPath != null)
|
||||
if (tempPath is not null)
|
||||
{
|
||||
path = tempPath;
|
||||
return true;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
using Encryption;
|
||||
using Filters;
|
||||
using Tokens;
|
||||
using UglyToad.PdfPig.Parser.FileStructure;
|
||||
|
||||
internal class PdfTokenScanner : IPdfTokenScanner
|
||||
{
|
||||
@@ -23,6 +24,7 @@
|
||||
private readonly ILookupFilterProvider filterProvider;
|
||||
private readonly CoreTokenScanner coreTokenScanner;
|
||||
private readonly ParsingOptions parsingOptions;
|
||||
private readonly FileHeaderOffset fileHeaderOffset;
|
||||
|
||||
private IEncryptionHandler encryptionHandler;
|
||||
private bool isDisposed;
|
||||
@@ -49,19 +51,25 @@
|
||||
|
||||
public long Length => coreTokenScanner.Length;
|
||||
|
||||
private readonly StackDepthGuard stackDepthGuard;
|
||||
|
||||
public PdfTokenScanner(
|
||||
IInputBytes inputBytes,
|
||||
IObjectLocationProvider objectLocationProvider,
|
||||
ILookupFilterProvider filterProvider,
|
||||
IEncryptionHandler encryptionHandler,
|
||||
ParsingOptions parsingOptions)
|
||||
FileHeaderOffset fileHeaderOffset,
|
||||
ParsingOptions parsingOptions,
|
||||
StackDepthGuard stackDepthGuard)
|
||||
{
|
||||
this.inputBytes = inputBytes;
|
||||
this.objectLocationProvider = objectLocationProvider;
|
||||
this.filterProvider = filterProvider;
|
||||
this.encryptionHandler = encryptionHandler;
|
||||
this.fileHeaderOffset = fileHeaderOffset;
|
||||
this.parsingOptions = parsingOptions;
|
||||
coreTokenScanner = new CoreTokenScanner(inputBytes, true, useLenientParsing: parsingOptions.UseLenientParsing);
|
||||
this.stackDepthGuard = stackDepthGuard;
|
||||
coreTokenScanner = new CoreTokenScanner(inputBytes, true, stackDepthGuard, useLenientParsing: parsingOptions.UseLenientParsing);
|
||||
}
|
||||
|
||||
public void UpdateEncryptionHandler(IEncryptionHandler newHandler)
|
||||
@@ -795,8 +803,8 @@
|
||||
{
|
||||
// Brute force read the entire file
|
||||
isBruteForcing = true;
|
||||
|
||||
Seek(0);
|
||||
|
||||
Seek(fileHeaderOffset.Value);
|
||||
|
||||
while (MoveNext())
|
||||
{
|
||||
@@ -867,6 +875,7 @@
|
||||
var scanner = new CoreTokenScanner(
|
||||
bytes,
|
||||
true,
|
||||
stackDepthGuard,
|
||||
useLenientParsing: parsingOptions.UseLenientParsing,
|
||||
isStream: true);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net462;net471;net6.0;net8.0;net9.0</TargetFrameworks>
|
||||
<LangVersion>12</LangVersion>
|
||||
<Version>0.1.12</Version>
|
||||
<Version>0.1.14</Version>
|
||||
<IsTestProject>False</IsTestProject>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
|
||||
@@ -250,8 +250,9 @@
|
||||
throw new ArgumentNullException(nameof(array));
|
||||
}
|
||||
|
||||
if (array.Data.Count != 4)
|
||||
if (array.Data.Count < 4)
|
||||
{
|
||||
// Should be exactly 4, but can be more (see issues 1238). We ignore the rest.
|
||||
throw new PdfDocumentFormatException($"Cannot convert array to rectangle, expected 4 values instead got: {array.Data.Count}.");
|
||||
}
|
||||
|
||||
|
||||
178
src/UglyToad.PdfPig/Writer/LinkAnnotation.cs
Normal file
178
src/UglyToad.PdfPig/Writer/LinkAnnotation.cs
Normal file
@@ -0,0 +1,178 @@
|
||||
namespace UglyToad.PdfPig.Writer
|
||||
{
|
||||
using UglyToad.PdfPig.Actions;
|
||||
using UglyToad.PdfPig.Annotations;
|
||||
using UglyToad.PdfPig.Core;
|
||||
using UglyToad.PdfPig.Tokens;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a link annotation that can be added to a PDF page.
|
||||
/// Link annotations provide clickable areas that can trigger actions such as navigating to another page or opening a URL.
|
||||
/// </summary>
|
||||
internal sealed class LinkAnnotation
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the border style for the link annotation.
|
||||
/// This is overwritten by the <see cref="AnnotationBorder"/> if both are provided.
|
||||
/// </summary>
|
||||
public AnnotationBorder? AnnotationBorder { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the border style for the link annotation.
|
||||
/// </summary>
|
||||
public BorderStyle? Border { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the width of the border for the link annotation.
|
||||
/// </summary>
|
||||
public int? BorderWidth { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the rectangle defining the location and size of the link annotation on the page.
|
||||
/// </summary>
|
||||
public PdfRectangle Rect { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the quadrilaterals defining the clickable regions of the link.
|
||||
/// These are typically used to define precise clickable areas that may not be rectangular.
|
||||
/// </summary>
|
||||
public IReadOnlyList<QuadPointsQuadrilateral> QuadPoints { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the action to be performed when the link is activated.
|
||||
/// </summary>
|
||||
public PdfAction Action { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the border style for a link annotation.
|
||||
/// </summary>
|
||||
public enum BorderStyle
|
||||
{
|
||||
/// <summary>
|
||||
/// A solid border.
|
||||
/// </summary>
|
||||
Solid,
|
||||
|
||||
/// <summary>
|
||||
/// A dashed border.
|
||||
/// </summary>
|
||||
Dashed,
|
||||
|
||||
/// <summary>
|
||||
/// A simulated embossed border that appears to be raised above the surface of the page.
|
||||
/// </summary>
|
||||
Beveled,
|
||||
|
||||
/// <summary>
|
||||
/// A simulated engraved border that appears to be recessed below the surface of the page.
|
||||
/// </summary>
|
||||
Inset,
|
||||
|
||||
/// <summary>
|
||||
/// An underline border drawn along the bottom of the annotation rectangle.
|
||||
/// </summary>
|
||||
Underline,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="LinkAnnotation"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to be performed when the link is activated.</param>
|
||||
/// <param name="rect">The rectangle defining the location and size of the link on the page.</param>
|
||||
/// <param name="annotationBorder">The border style for the link annotation. Optional, overwritten by <see cref="Border"/>.</param>
|
||||
/// <param name="borderStyle">The border style for the link annotation. Optional.</param>
|
||||
/// <param name="borderWidth">The width of the border for the link annotation. Optional.</param>
|
||||
/// <param name="quadPoints">The quadrilaterals defining the clickable regions. Optional.</param>
|
||||
public LinkAnnotation(
|
||||
PdfAction action,
|
||||
PdfRectangle rect,
|
||||
AnnotationBorder? annotationBorder = null,
|
||||
BorderStyle? borderStyle = null,
|
||||
int? borderWidth = null,
|
||||
IReadOnlyList<QuadPointsQuadrilateral>? quadPoints = null)
|
||||
{
|
||||
Action = action;
|
||||
Rect = rect;
|
||||
AnnotationBorder = annotationBorder;
|
||||
Border = borderStyle;
|
||||
BorderWidth = borderWidth;
|
||||
QuadPoints = quadPoints ?? new List<QuadPointsQuadrilateral>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts this link annotation to a PDF dictionary token representation.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="DictionaryToken"/> representing this link annotation in PDF format.</returns>
|
||||
public DictionaryToken ToToken()
|
||||
{
|
||||
var dict = new Dictionary<NameToken, IToken>
|
||||
{
|
||||
[NameToken.Type] = NameToken.Annot,
|
||||
[NameToken.Subtype] = NameToken.Link,
|
||||
[NameToken.Rect] = new ArrayToken([
|
||||
new NumericToken(Rect.BottomLeft.X),
|
||||
new NumericToken(Rect.BottomLeft.Y),
|
||||
new NumericToken(Rect.TopRight.X),
|
||||
new NumericToken(Rect.TopRight.Y)
|
||||
]),
|
||||
};
|
||||
|
||||
if (QuadPoints.Count > 0)
|
||||
{
|
||||
var quadPointsArray = new List<NumericToken>();
|
||||
foreach (var quad in QuadPoints)
|
||||
{
|
||||
foreach (var point in quad.Points)
|
||||
{
|
||||
quadPointsArray.Add(new NumericToken(point.X));
|
||||
quadPointsArray.Add(new NumericToken(point.Y));
|
||||
}
|
||||
}
|
||||
|
||||
dict.Add(NameToken.Quadpoints, new ArrayToken(quadPointsArray));
|
||||
}
|
||||
|
||||
if (AnnotationBorder != null)
|
||||
{
|
||||
var borderArray = new List<IToken>
|
||||
{
|
||||
new NumericToken(AnnotationBorder.HorizontalCornerRadius),
|
||||
new NumericToken(AnnotationBorder.VerticalCornerRadius),
|
||||
new NumericToken(AnnotationBorder.BorderWidth),
|
||||
};
|
||||
|
||||
if (AnnotationBorder.LineDashPattern != null && AnnotationBorder.LineDashPattern.Count > 0)
|
||||
{
|
||||
var dashArray = new List<NumericToken>();
|
||||
foreach (var dash in AnnotationBorder.LineDashPattern)
|
||||
{
|
||||
dashArray.Add(new NumericToken(dash));
|
||||
}
|
||||
borderArray.Add(new ArrayToken(dashArray));
|
||||
}
|
||||
dict.Add(NameToken.Border, new ArrayToken(borderArray));
|
||||
}
|
||||
|
||||
if (Border != null)
|
||||
{
|
||||
dict.Add(NameToken.Bs, new DictionaryToken(new Dictionary<NameToken, IToken>
|
||||
{
|
||||
[NameToken.S] = Border switch
|
||||
{
|
||||
BorderStyle.Solid => NameToken.S,
|
||||
BorderStyle.Dashed => NameToken.D,
|
||||
BorderStyle.Beveled => NameToken.B,
|
||||
BorderStyle.Inset => NameToken.I,
|
||||
BorderStyle.Underline => NameToken.U,
|
||||
_ => NameToken.S,
|
||||
},
|
||||
[NameToken.W] = new NumericToken(BorderWidth ?? 1)
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
|
||||
return new DictionaryToken(dict);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,7 +74,7 @@ namespace UglyToad.PdfPig.Writer
|
||||
return false;
|
||||
}
|
||||
|
||||
var pageContentParser = new PageContentParser(ReflectionGraphicsStateOperationFactory.Instance);
|
||||
var pageContentParser = new PageContentParser(ReflectionGraphicsStateOperationFactory.Instance, StackDepthGuard.Infinite);
|
||||
IReadOnlyList<IGraphicsStateOperation> operations;
|
||||
try
|
||||
{
|
||||
|
||||
@@ -347,7 +347,7 @@ namespace UglyToad.PdfPig.Writer
|
||||
}
|
||||
|
||||
var page = document.GetPage(pageNumber);
|
||||
var pcp = new PageContentParser(ReflectionGraphicsStateOperationFactory.Instance, true);
|
||||
var pcp = new PageContentParser(ReflectionGraphicsStateOperationFactory.Instance, StackDepthGuard.Infinite, true);
|
||||
|
||||
// copy content streams
|
||||
var streams = new List<PdfPageBuilder.CopiedContentStream>();
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
using Graphics.Operations.TextPositioning;
|
||||
using Graphics.Operations.TextShowing;
|
||||
using Graphics.Operations.TextState;
|
||||
using Outline.Destinations;
|
||||
using Images;
|
||||
using PdfFonts;
|
||||
using Tokens;
|
||||
@@ -94,7 +95,7 @@
|
||||
private IPageContentStream currentStream;
|
||||
|
||||
// links to be resolved when all page references are available
|
||||
internal readonly List<(DictionaryToken token, PdfAction action)>? links;
|
||||
internal readonly List<(DictionaryToken token, PdfAction action)> links = [];
|
||||
|
||||
// maps fonts added using PdfDocumentBuilder to page font names
|
||||
private readonly Dictionary<Guid, NameToken> documentFonts = new Dictionary<Guid, NameToken>();
|
||||
@@ -827,6 +828,39 @@
|
||||
return new AddedImage(reference.Data, png.Width, png.Height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a URL link annotation to the page at the specified rectangle area.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL to link to</param>
|
||||
/// <param name="linkArea">The rectangular area on the page that will be clickable</param>
|
||||
/// <returns>This page builder for method chaining</returns>
|
||||
public PdfPageBuilder AddLink(string url, PdfRectangle linkArea)
|
||||
{
|
||||
return AddLink(new LinkAnnotation(new UriAction(url), linkArea));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an internal document link annotation to the page at the specified rectangle area.
|
||||
/// </summary>
|
||||
/// <param name="destination">The destination within the current document to link to</param>
|
||||
/// <param name="linkArea">The rectangular area on the page that will be clickable</param>
|
||||
/// <returns>This page builder for method chaining</returns>
|
||||
public PdfPageBuilder AddLink(ExplicitDestination destination, PdfRectangle linkArea)
|
||||
{
|
||||
return AddLink(new LinkAnnotation(new GoToAction(destination), linkArea));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a link annotation to the page.
|
||||
/// </summary>
|
||||
/// <param name="link">The link annotation to add</param>
|
||||
/// <returns>This page builder for method chaining</returns>
|
||||
private PdfPageBuilder AddLink(LinkAnnotation link)
|
||||
{
|
||||
links.Add((link.ToToken(), link.Action));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy a page from unknown source to this page
|
||||
/// </summary>
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
<PackageTags>PDF;Reader;Document;Adobe;PDFBox;PdfPig;pdf-extract;pdf-to-text;pdf;file;text;C#;dotnet;.NET</PackageTags>
|
||||
<RepositoryUrl>https://github.com/UglyToad/PdfPig</RepositoryUrl>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<Version>0.1.12</Version>
|
||||
<AssemblyVersion>0.1.12.0</AssemblyVersion>
|
||||
<FileVersion>0.1.12.0</FileVersion>
|
||||
<Version>0.1.14</Version>
|
||||
<AssemblyVersion>0.1.14.0</AssemblyVersion>
|
||||
<FileVersion>0.1.14.0</FileVersion>
|
||||
<PackageIconUrl>https://raw.githubusercontent.com/UglyToad/PdfPig/master/documentation/pdfpig.png</PackageIconUrl>
|
||||
<PackageIcon>pdfpig.png</PackageIcon>
|
||||
<Product>PdfPig</Product>
|
||||
|
||||
Reference in New Issue
Block a user