Compare commits

...

20 Commits

Author SHA1 Message Date
EliotJones
7e370a0eab confirm tag is on master before running publish
Some checks failed
Nightly Release / Check if this commit has already been published (push) Has been cancelled
Nightly Release / tests (push) Has been cancelled
Nightly Release / build_and_publish_nightly (push) Has been cancelled
Build, test and publish draft / build (push) Has been cancelled
Build and test [MacOS] / build (push) Has been cancelled
Run Common Crawl Tests / build (0000-0001) (push) Has been cancelled
Run Common Crawl Tests / build (0002-0003) (push) Has been cancelled
Run Common Crawl Tests / build (0004-0005) (push) Has been cancelled
Run Common Crawl Tests / build (0006-0007) (push) Has been cancelled
Run Integration Tests / build (push) Has been cancelled
Tag Release / tag_if_version_changed (push) Has been cancelled
2026-02-15 18:39:45 +00:00
EliotJones
b9aa53166d replace release flow single job with pr process
since actions do not have permissions to push
directly to master and bot accounts to achieve
the same are hard to manage we will change the
release flow to work as follows.

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

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

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

this is all chat gpt code so who knows if it will work
2026-02-15 18:39:45 +00:00
BobLd
0a2b1e076f Improve HasFormXObjectCircularReference and fix #1250 2026-02-15 18:06:35 +00:00
EliotJones
f732718852 both Tj and TJ operators should increment text sequence #1241
the text sequence was added to group letters added by the same
content stream operation together for layout analysis. TJ allows for
individual letter positioning in the same operation which modifies
the text matrix while writing whereas Tj just writes the text at the current
location. both should be semantically equivalent for this sequence
number. this was originally added in #61
2026-02-15 17:50:07 +00:00
Alexander Vinnikov
d69e3fa6a9 make ext gstates stacked
Some checks failed
Build, test and publish draft / build (push) Has been cancelled
Build and test [MacOS] / build (push) Has been cancelled
Run Common Crawl Tests / build (0000-0001) (push) Has been cancelled
Run Common Crawl Tests / build (0002-0003) (push) Has been cancelled
Run Common Crawl Tests / build (0004-0005) (push) Has been cancelled
Run Common Crawl Tests / build (0006-0007) (push) Has been cancelled
Run Integration Tests / build (push) Has been cancelled
Nightly Release / Check if this commit has already been published (push) Has been cancelled
Nightly Release / tests (push) Has been cancelled
Nightly Release / build_and_publish_nightly (push) Has been cancelled
2026-02-11 18:39:33 +00:00
BobLd
d6e86b057e Handle empty encoding in Type1FontSimple and fix #1248
Some checks failed
Build, test and publish draft / build (push) Has been cancelled
Build and test [MacOS] / build (push) Has been cancelled
Run Common Crawl Tests / build (0000-0001) (push) Has been cancelled
Run Common Crawl Tests / build (0002-0003) (push) Has been cancelled
Run Common Crawl Tests / build (0004-0005) (push) Has been cancelled
Run Common Crawl Tests / build (0006-0007) (push) Has been cancelled
Run Integration Tests / build (push) Has been cancelled
Nightly Release / Check if this commit has already been published (push) Has been cancelled
Nightly Release / tests (push) Has been cancelled
Nightly Release / build_and_publish_nightly (push) Has been cancelled
2026-02-10 06:14:39 +00:00
BobLd
c27f1b6553 Only throw if ArrayToken length is less than 4 in ToRectangle() and fix #1238
Some checks failed
Build, test and publish draft / build (push) Has been cancelled
Build and test [MacOS] / build (push) Has been cancelled
Run Common Crawl Tests / build (0000-0001) (push) Has been cancelled
Run Common Crawl Tests / build (0002-0003) (push) Has been cancelled
Run Common Crawl Tests / build (0004-0005) (push) Has been cancelled
Run Common Crawl Tests / build (0006-0007) (push) Has been cancelled
Run Integration Tests / build (push) Has been cancelled
Nightly Release / Check if this commit has already been published (push) Has been cancelled
Nightly Release / tests (push) Has been cancelled
Nightly Release / build_and_publish_nightly (push) Has been cancelled
2026-02-01 16:10:24 +00:00
BobLd
629891e80e Make LinkAnnotation internal to fix unit tests 2026-02-01 15:50:21 +00:00
David Ochsner
13825b9424 Add Links to Pdf Generation 2026-02-01 15:13:55 +00:00
BobLd
7c4f5e2424 Introduce StackDepthGuard class to check for stack depth in CoreTokenScanner and fix #1217
Some checks failed
Build, test and publish draft / build (push) Has been cancelled
Build and test [MacOS] / build (push) Has been cancelled
Run Common Crawl Tests / build (0000-0001) (push) Has been cancelled
Run Common Crawl Tests / build (0002-0003) (push) Has been cancelled
Run Common Crawl Tests / build (0004-0005) (push) Has been cancelled
Run Common Crawl Tests / build (0006-0007) (push) Has been cancelled
Run Integration Tests / build (push) Has been cancelled
Nightly Release / Check if this commit has already been published (push) Has been cancelled
Nightly Release / tests (push) Has been cancelled
Nightly Release / build_and_publish_nightly (push) Has been cancelled
2025-12-23 16:24:04 +01:00
BobLd
bd573b2a1c Increment version to 0.1.14 2025-12-23 12:00:19 +01:00
BobLd
baeac0d0c3 Do not return glyph bbox and path in Type1Font if character name is '.notdef' 2025-12-23 11:09:00 +01:00
BobLd
ee0cb1dc4a Use file header offset when doing brute force find and fix #1223
Some checks failed
Build, test and publish draft / build (push) Has been cancelled
Build and test [MacOS] / build (push) Has been cancelled
Run Common Crawl Tests / build (0000-0001) (push) Has been cancelled
Run Common Crawl Tests / build (0002-0003) (push) Has been cancelled
Run Common Crawl Tests / build (0004-0005) (push) Has been cancelled
Run Common Crawl Tests / build (0006-0007) (push) Has been cancelled
Run Integration Tests / build (push) Has been cancelled
Nightly Release / Check if this commit has already been published (push) Has been cancelled
Nightly Release / tests (push) Has been cancelled
Nightly Release / build_and_publish_nightly (push) Has been cancelled
2025-12-07 13:43:22 +00:00
BobLd
c70b343caa Minor Type1FontParser optimisations
Some checks failed
Build, test and publish draft / build (push) Has been cancelled
Build and test [MacOS] / build (push) Has been cancelled
Run Common Crawl Tests / build (0000-0001) (push) Has been cancelled
Run Common Crawl Tests / build (0002-0003) (push) Has been cancelled
Run Common Crawl Tests / build (0004-0005) (push) Has been cancelled
Run Common Crawl Tests / build (0006-0007) (push) Has been cancelled
Run Integration Tests / build (push) Has been cancelled
Nightly Release / Check if this commit has already been published (push) Has been cancelled
Nightly Release / tests (push) Has been cancelled
Nightly Release / build_and_publish_nightly (push) Has been cancelled
2025-12-02 17:29:55 +00:00
Carlo Kok
ce563db133 Clarify handling of optional content group names
Updated comments to clarify handling of optional content group names in PDFs.
2025-11-23 10:52:21 +00:00
BobLd
37a5dffcaa Skip in ReadCompositeGlyph when glyphIndex is out of range and fix #1213 2025-11-21 21:31:18 +00:00
EliotJones
aef0a78ee6 update release logic to check out master before commit
Some checks failed
Build, test and publish draft / build (push) Has been cancelled
Build and test [MacOS] / build (push) Has been cancelled
Run Common Crawl Tests / build (0000-0001) (push) Has been cancelled
Run Common Crawl Tests / build (0002-0003) (push) Has been cancelled
Run Common Crawl Tests / build (0004-0005) (push) Has been cancelled
Run Common Crawl Tests / build (0006-0007) (push) Has been cancelled
Run Integration Tests / build (push) Has been cancelled
Nightly Release / Check if this commit has already been published (push) Has been cancelled
Nightly Release / tests (push) Has been cancelled
Nightly Release / build_and_publish_nightly (push) Has been cancelled
2025-11-14 09:50:07 +00:00
BobLd
005e52783e Ensure no key end up missing in ResolveInternal and fix #1209 2025-11-13 14:53:28 +00:00
BobLd
f4e7db5b5b Simply order by offset also when not doing brute force to fix #1208 2025-11-13 14:26:35 +00:00
BobLd
52ecef0e28 Increment version to 0.1.13 2025-11-13 09:34:17 +00:00
58 changed files with 2283 additions and 159 deletions

View File

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

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

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

View File

@@ -1,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
View File

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

View File

@@ -0,0 +1,63 @@
namespace UglyToad.PdfPig.Core
{
/// <summary>
/// Provides a guard for tracking and limiting the depth of nested stack operations, such as recursive calls or
/// nested parsing.
/// </summary>
/// <remarks>Use this class to prevent excessive stack usage by enforcing a maximum nesting depth. This is
/// particularly useful in scenarios where untrusted or deeply nested input could cause stack overflows or
/// performance issues.</remarks>
public sealed class StackDepthGuard
{
/// <summary>
/// Represents a stack depth guard with no effective limit on the allowed depth.
/// </summary>
/// <remarks>Use this instance when stack depth restrictions are not required.</remarks>
public static readonly StackDepthGuard Infinite = new StackDepthGuard(int.MaxValue);
private readonly int maxStackDepth;
private int depth;
/// <summary>
/// Initializes a new instance of the StackDepthGuard class with the specified maximum stack depth.
/// </summary>
/// <param name="maxStackDepth">The maximum allowed stack depth for guarded operations. Must be a positive integer.</param>
public StackDepthGuard(int maxStackDepth)
{
if (maxStackDepth <= 0)
{
throw new ArgumentOutOfRangeException(nameof(maxStackDepth));
}
this.maxStackDepth = maxStackDepth;
}
/// <summary>
/// Increments the current nesting depth and checks against the maximum allowed stack depth.
/// </summary>
/// <exception cref="PdfDocumentFormatException">Thrown if the maximum allowed nesting depth is exceeded.</exception>
public void Enter()
{
if (++depth > maxStackDepth)
{
depth--; // Decrement so Exit remains balanced if someone catches this
throw new PdfDocumentFormatException($"Exceeded maximum nesting depth of {maxStackDepth}.");
}
}
/// <summary>
/// Decreases the current depth level by one, ensuring that the depth does not become negative.
/// </summary>
/// <remarks>If the current depth is already zero, calling this method has no effect. This method
/// is typically used to track or manage nested operations or scopes where depth must remain
/// non-negative.</remarks>
public void Exit()
{
depth--;
if (depth < 0)
{
depth = 0;
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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()
{

View File

@@ -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);
}

View File

@@ -15,7 +15,7 @@
public class PageContentParserTests
{
private readonly PageContentParser parser = new PageContentParser(ReflectionGraphicsStateOperationFactory.Instance);
private readonly PageContentParser parser = new PageContentParser(ReflectionGraphicsStateOperationFactory.Instance, new StackDepthGuard(256));
private readonly ILog log = new NoOpLog();
[Fact]
@@ -210,7 +210,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);

View File

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

View File

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

View File

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

View File

@@ -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("]")]

View File

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

View File

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

View File

@@ -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)

View File

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

View File

@@ -8,12 +8,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>();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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;

View File

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

View File

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

View File

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

View File

@@ -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);
}

View File

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

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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);

View File

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

View File

@@ -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}.");
}

View 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);
}
}
}

View File

@@ -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
{

View File

@@ -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>();

View File

@@ -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>

View File

@@ -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>