mirror of
https://github.com/UglyToad/PdfPig.git
synced 2026-03-10 00:23:29 +08:00
Compare commits
9 Commits
explore-is
...
nullabilit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dcb2c2bbe2 | ||
|
|
b6bd0a3169 | ||
|
|
3d2e12cb16 | ||
|
|
ef5366f117 | ||
|
|
9cb3b71e62 | ||
|
|
27df4af5f9 | ||
|
|
50f878b2ba | ||
|
|
2a10b6c285 | ||
|
|
85fc63d585 |
66
README.md
66
README.md
@@ -2,19 +2,11 @@
|
||||
|
||||
# PdfPig
|
||||
|
||||
[](https://gitter.im/pdfpig/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
[](https://www.nuget.org/packages/PdfPig/)
|
||||
|
||||
[](https://github.com/UglyToad/PdfPig/actions/workflows/build_and_test.yml)
|
||||
[![Build and test [MacOS]](https://github.com/UglyToad/PdfPig/actions/workflows/build_and_test_macos.yml/badge.svg)](https://github.com/UglyToad/PdfPig/actions/workflows/build_and_test_macos.yml)
|
||||
|
||||
This project allows users to read and extract text and other content from PDF files. In addition the library can be used to create simple PDF documents
|
||||
containing text and geometrical shapes.
|
||||
|
||||
This project aims to port [PDFBox](https://github.com/apache/pdfbox) to C#.
|
||||
|
||||
## Wiki
|
||||
Check out our [wiki](https://github.com/UglyToad/PdfPig/wiki) for more examples and detailed guides on the API.
|
||||
PdfPig supports reading text and content from PDF files. It also supports basic PDF file creation.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -32,29 +24,26 @@ While the version is below 1.0.0 minor versions will change the public API witho
|
||||
|
||||
See the [wiki](https://github.com/UglyToad/PdfPig/wiki) for more examples
|
||||
|
||||
### Read words in a page
|
||||
### Reading text from a PDF
|
||||
|
||||
The simplest usage at this stage is to open a document, reading the words from every page:
|
||||
|
||||
```cs
|
||||
// using UglyToad.PdfPig.DocumentLayoutAnalysis.TextExtractor;
|
||||
// using UglyToad.PdfPig.DocumentLayoutAnalysis.WordExtractor;
|
||||
using (PdfDocument document = PdfDocument.Open(@"C:\Documents\document.pdf"))
|
||||
{
|
||||
foreach (Page page in document.GetPages())
|
||||
{
|
||||
string pageText = page.Text;
|
||||
|
||||
foreach (Word word in page.GetWords())
|
||||
{
|
||||
Console.WriteLine(word.Text);
|
||||
}
|
||||
}
|
||||
foreach (Page page in document.GetPages())
|
||||
{
|
||||
string text = ContentOrderTextExtractor.GetText(page);
|
||||
IEnumerable<Word> words = page.GetWords(NearestNeighbourWordExtractor.Instance);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
An example of the output of this is shown below:
|
||||
You **should not** use `page.Text` directly, unless you know what you're doing. The `Text` property preserves the internal content order which is rarely ever the text in the order you want.
|
||||
|
||||

|
||||
|
||||
Where for the PDF text ("Write something in") shown at the top the 3 words (in pink) are detected and each word contains the individual letters with glyph bounding boxes.
|
||||
These layout analysis tools should get you the text you want in most cases.
|
||||
|
||||
### Create PDF Document
|
||||
To create documents use the class `PdfDocumentBuilder`. The Standard 14 fonts provide a quick way to get started:
|
||||
@@ -80,6 +69,12 @@ The output is a 1 page PDF document with the text "Hello World!" in Helvetica ne
|
||||
|
||||
Each font must be registered with the `PdfDocumentBuilder` prior to use enable pages to share the font resources. Only Standard 14 fonts and TrueType fonts (.ttf) are supported.
|
||||
|
||||
Document creation supports very limited changes to existing PDF documents. However it does not support any of the following:
|
||||
|
||||
- Editing forms
|
||||
- Copying or changing annotations, metadata or document structure data
|
||||
- Adding or removing text with existing fonts
|
||||
|
||||
### Advanced Document Extraction
|
||||
In this example a more advanced document extraction is performed. `PdfDocumentBuilder` is used to create a copy of the pdf with debug information (bounding boxes and reading order) added.
|
||||
|
||||
@@ -259,7 +254,7 @@ string title = document.Information.Title;
|
||||
|
||||
### Document Structure
|
||||
|
||||
The document now has a Structure member:
|
||||
The `PdfDocument` has a Structure member:
|
||||
|
||||
UglyToad.PdfPig.Structure structure = document.Structure;
|
||||
|
||||
@@ -283,7 +278,7 @@ PageSize size = Page.Size;
|
||||
bool isA4 = size == PageSize.A4;
|
||||
```
|
||||
|
||||
`Page` provides access to the text of the page:
|
||||
`Page` provides access to the text of the page but you should use `ContentOrderTextExtractor` or alternatives if indexing the text, e.g. for RAG/LLMs:
|
||||
|
||||
string text = page.Text;
|
||||
|
||||
@@ -329,7 +324,7 @@ Retrieving annotations on each page is provided using the method:
|
||||
|
||||
page.GetAnnotations()
|
||||
|
||||
This call is not cached and the document must not have been disposed prior to use.
|
||||
This call is not cached and the document must not have been disposed prior to use. Annotations cannot be edited.
|
||||
|
||||
### Bookmarks
|
||||
|
||||
@@ -357,6 +352,8 @@ A page has a method to extract hyperlinks (annotations of link type):
|
||||
|
||||
IReadOnlyList<UglyToad.PdfPig.Content.Hyperlink> hyperlinks = page.GetHyperlinks();
|
||||
|
||||
Hyperlinks cannot be added or edited when building documents.
|
||||
|
||||
### TrueType
|
||||
|
||||
The classes used to work with TrueType fonts in the PDF file are available for public consumption. Given an input file:
|
||||
@@ -396,18 +393,17 @@ var resultFileBytes = PdfMerger.Merge(filePath1, filePath2);
|
||||
File.WriteAllBytes(@"C:\pdfs\outputfilename.pdf", resultFileBytes);
|
||||
```
|
||||
|
||||
## Wiki
|
||||
Check out our [wiki](https://github.com/UglyToad/PdfPig/wiki) for more examples and detailed guides on the API.
|
||||
|
||||
## Issues
|
||||
|
||||
Please do file an issue if you encounter a bug. See our [issue policy](https://github.com/UglyToad/PdfPig/issues/1095) and [contributing guide](https://github.com/UglyToad/PdfPig/blob/master/CONTRIBUTING.md) for details.
|
||||
|
||||
## API Reference
|
||||
|
||||
If you wish to generate doxygen documentation, run `doxygen doxygen-docs` and open `docs/doxygen/html/index.html`.
|
||||
|
||||
See also the [wiki](https://github.com/UglyToad/PdfPig/wiki) for a detailed documentation on parts of the API
|
||||
|
||||
## Issues
|
||||
|
||||
Please do file an issue if you encounter a bug.
|
||||
|
||||
However in order for us to assist you, you **must** provide the file which causes your issue. Please host this in a publically available place.
|
||||
|
||||
## Credit
|
||||
|
||||
This project wouldn't be possible without the work done by the [PDFBox](https://pdfbox.apache.org/) team and the Apache Foundation.
|
||||
This project started as an effort to port [PDFBox](https://github.com/apache/pdfbox) to C#. This project wouldn't be possible without the work done by the [PDFBox](https://pdfbox.apache.org/) team and the Apache Foundation.
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Buffers;
|
||||
|
||||
namespace UglyToad.PdfPig.Core;
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(object obj)
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is IndirectReference other && Equals(other);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
/// <summary>
|
||||
/// Convert the string to bytes using the ISO 8859-1 encoding.
|
||||
/// </summary>
|
||||
public static byte[] StringAsLatin1Bytes(string s)
|
||||
public static byte[]? StringAsLatin1Bytes(string? s)
|
||||
{
|
||||
if (s == null)
|
||||
{
|
||||
|
||||
@@ -264,7 +264,7 @@
|
||||
/// Try to convert raw bytes to a PdfDocEncoding encoded string. If unsupported characters are encountered
|
||||
/// meaning we cannot safely round-trip the value to bytes this will instead return false.
|
||||
/// </summary>
|
||||
public static bool TryConvertBytesToString(ReadOnlySpan<byte> bytes, out string result)
|
||||
public static bool TryConvertBytesToString(ReadOnlySpan<byte> bytes, out string? result)
|
||||
{
|
||||
result = null;
|
||||
if (bytes.Length == 0)
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
/// <summary>
|
||||
/// Returns a value indicating whether this <see cref="PdfLine"/> is equal to a specified <see cref="PdfLine"/> .
|
||||
/// </summary>
|
||||
public override bool Equals(object obj)
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is PdfLine other && Equals(other);
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
/// <summary>
|
||||
/// Returns a value indicating whether this <see cref="PdfPoint"/> is equal to a specified <see cref="PdfPoint"/> .
|
||||
/// </summary>
|
||||
public override bool Equals(object obj)
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is PdfPoint other && Equals(other);
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(object obj)
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is PdfRectangle other && Equals(other);
|
||||
}
|
||||
|
||||
@@ -241,8 +241,8 @@
|
||||
public bool IsClosed()
|
||||
{
|
||||
var filteredCount = 0;
|
||||
IPathCommand last = null;
|
||||
IPathCommand first = null;
|
||||
IPathCommand? last = null;
|
||||
IPathCommand? first = null;
|
||||
for (int i = Commands.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var cmd = Commands[i];
|
||||
@@ -376,14 +376,14 @@
|
||||
/// Gets a <see cref="PdfRectangle"/> which entirely contains the geometry of the defined path.
|
||||
/// </summary>
|
||||
/// <returns>For paths which don't define any geometry this returns <see langword="null"/>.</returns>
|
||||
public static PdfRectangle? GetBoundingRectangle(IReadOnlyList<PdfSubpath> path)
|
||||
public static PdfRectangle? GetBoundingRectangle(IReadOnlyList<PdfSubpath>? path)
|
||||
{
|
||||
if (path == null || path.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var bboxes = path.Select(x => x.GetBoundingRectangle()).Where(x => x.HasValue).Select(x => x.Value).ToList();
|
||||
var bboxes = path.Select(x => x.GetBoundingRectangle()).Where(x => x.HasValue).Select(x => x!.Value).ToList();
|
||||
if (bboxes.Count == 0)
|
||||
{
|
||||
return null;
|
||||
@@ -433,7 +433,7 @@
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(object obj)
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return (obj is Close);
|
||||
}
|
||||
@@ -479,7 +479,7 @@
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(object obj)
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is Move move)
|
||||
{
|
||||
@@ -545,7 +545,7 @@
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(object obj)
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is Line line)
|
||||
{
|
||||
@@ -651,7 +651,7 @@
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(object obj)
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is QuadraticBezierCurve curve)
|
||||
{
|
||||
@@ -809,7 +809,7 @@
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(object obj)
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is CubicBezierCurve curve)
|
||||
{
|
||||
@@ -944,7 +944,7 @@
|
||||
/// <summary>
|
||||
/// Compares two <see cref="PdfSubpath"/>s for equality. Paths will only be considered equal if the commands which construct the paths are in the same order.
|
||||
/// </summary>
|
||||
public override bool Equals(object obj)
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (!(obj is PdfSubpath path) || Commands.Count != path.Commands.Count)
|
||||
{
|
||||
|
||||
@@ -463,7 +463,7 @@
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(object obj)
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is TransformationMatrix other && Equals(other);
|
||||
}
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net462;net471;net6.0;net8.0</TargetFrameworks>
|
||||
<LangVersion>12</LangVersion>
|
||||
<Version>0.1.11-alpha001</Version>
|
||||
<IsTestProject>False</IsTestProject>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
<AssemblyOriginatorKeyFile>..\pdfpig.snk</AssemblyOriginatorKeyFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)'=='net462'">
|
||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)'=='netstandard2.0' or '$(TargetFramework)'=='net462' OR '$(TargetFramework)'=='net471'">
|
||||
<PackageReference Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)'=='netstandard2.0' or '$(TargetFramework)'=='net462' or '$(TargetFramework)'=='net471'">
|
||||
<PackageReference Include="System.Memory" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\pdfpig.snk" Link="pdfpig.snk" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net462;net471;net6.0;net8.0</TargetFrameworks>
|
||||
<LangVersion>12</LangVersion>
|
||||
<Version>0.1.12-alpha001</Version>
|
||||
<IsTestProject>False</IsTestProject>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
<AssemblyOriginatorKeyFile>..\pdfpig.snk</AssemblyOriginatorKeyFile>
|
||||
<Nullable>enable</Nullable>
|
||||
<WarningsAsErrors>nullable</WarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)'=='net462'">
|
||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)'=='netstandard2.0' or '$(TargetFramework)'=='net462' OR '$(TargetFramework)'=='net471'">
|
||||
<PackageReference Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)'=='netstandard2.0' or '$(TargetFramework)'=='net462' or '$(TargetFramework)'=='net471'">
|
||||
<PackageReference Include="System.Memory" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\pdfpig.snk" Link="pdfpig.snk" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -89,7 +89,7 @@ namespace UglyToad.PdfPig.Core
|
||||
/// <inheritdoc />
|
||||
public override bool TryGetSecond(out B b)
|
||||
{
|
||||
b = default(B);
|
||||
b = default!;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ namespace UglyToad.PdfPig.Core
|
||||
/// <inheritdoc />
|
||||
public override bool TryGetFirst(out A a)
|
||||
{
|
||||
a = default(A);
|
||||
a = default!;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net462;net471;net6.0;net8.0</TargetFrameworks>
|
||||
<LangVersion>12</LangVersion>
|
||||
<Version>0.1.11-alpha001</Version>
|
||||
<IsTestProject>False</IsTestProject>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
<AssemblyOriginatorKeyFile>..\pdfpig.snk</AssemblyOriginatorKeyFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)'=='net462'">
|
||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\pdfpig.snk" Link="pdfpig.snk" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\UglyToad.PdfPig.Core\UglyToad.PdfPig.Core.csproj" />
|
||||
<ProjectReference Include="..\UglyToad.PdfPig.Fonts\UglyToad.PdfPig.Fonts.csproj" />
|
||||
<ProjectReference Include="..\UglyToad.PdfPig\UglyToad.PdfPig.csproj" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net462;net471;net6.0;net8.0</TargetFrameworks>
|
||||
<LangVersion>12</LangVersion>
|
||||
<Version>0.1.12-alpha001</Version>
|
||||
<IsTestProject>False</IsTestProject>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
<AssemblyOriginatorKeyFile>..\pdfpig.snk</AssemblyOriginatorKeyFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)'=='net462'">
|
||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\pdfpig.snk" Link="pdfpig.snk" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\UglyToad.PdfPig.Core\UglyToad.PdfPig.Core.csproj" />
|
||||
<ProjectReference Include="..\UglyToad.PdfPig.Fonts\UglyToad.PdfPig.Fonts.csproj" />
|
||||
<ProjectReference Include="..\UglyToad.PdfPig\UglyToad.PdfPig.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
public virtual string GetNameByStringId(int stringId)
|
||||
{
|
||||
return GlyphIdToStringIdAndName.SingleOrDefault(x => x.Value.stringId == stringId).Value.name;
|
||||
return GlyphIdToStringIdAndName.FirstOrDefault(x => x.Value.stringId == stringId).Value.name;
|
||||
}
|
||||
|
||||
public virtual int GetStringIdByGlyphId(int glyphId)
|
||||
|
||||
@@ -1,40 +1,40 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net462;net471;net6.0;net8.0</TargetFrameworks>
|
||||
<LangVersion>12</LangVersion>
|
||||
<Version>0.1.11-alpha001</Version>
|
||||
<IsTestProject>False</IsTestProject>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
<AssemblyOriginatorKeyFile>..\pdfpig.snk</AssemblyOriginatorKeyFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="Resources\AdobeFontMetrics\*" />
|
||||
<None Remove="Resources\GlyphList\*" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="Resources\AdobeFontMetrics\MustRead.html">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\AdobeFontMetrics\*" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\GlyphList\*" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\pdfpig.snk" Link="pdfpig.snk" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\UglyToad.PdfPig.Core\UglyToad.PdfPig.Core.csproj" />
|
||||
<ProjectReference Include="..\UglyToad.PdfPig.Tokenization\UglyToad.PdfPig.Tokenization.csproj" />
|
||||
<ProjectReference Include="..\UglyToad.PdfPig.Tokens\UglyToad.PdfPig.Tokens.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)'=='net462'">
|
||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)'=='netstandard2.0' or '$(TargetFramework)'=='net462' or '$(TargetFramework)'=='net471'">
|
||||
<PackageReference Include="System.Memory" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net462;net471;net6.0;net8.0</TargetFrameworks>
|
||||
<LangVersion>12</LangVersion>
|
||||
<Version>0.1.12-alpha001</Version>
|
||||
<IsTestProject>False</IsTestProject>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
<AssemblyOriginatorKeyFile>..\pdfpig.snk</AssemblyOriginatorKeyFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="Resources\AdobeFontMetrics\*" />
|
||||
<None Remove="Resources\GlyphList\*" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="Resources\AdobeFontMetrics\MustRead.html">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\AdobeFontMetrics\*" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\GlyphList\*" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\pdfpig.snk" Link="pdfpig.snk" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\UglyToad.PdfPig.Core\UglyToad.PdfPig.Core.csproj" />
|
||||
<ProjectReference Include="..\UglyToad.PdfPig.Tokenization\UglyToad.PdfPig.Tokenization.csproj" />
|
||||
<ProjectReference Include="..\UglyToad.PdfPig.Tokens\UglyToad.PdfPig.Tokens.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)'=='net462'">
|
||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)'=='netstandard2.0' or '$(TargetFramework)'=='net462' or '$(TargetFramework)'=='net471'">
|
||||
<PackageReference Include="System.Memory" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -29,6 +29,7 @@
|
||||
public static IEnumerable<object[]> ValidNumberTestData => new []
|
||||
{
|
||||
new object[] {"0", 0},
|
||||
new object[] {"0003", 3},
|
||||
new object[] {"1", 1},
|
||||
new object[] {"2", 2},
|
||||
new object[] {"3", 3},
|
||||
@@ -55,19 +56,29 @@
|
||||
new object[] { "4.", 4},
|
||||
new object[] { "-.002", -0.002},
|
||||
new object[] { "0.0", 0},
|
||||
new object[] {"1.57e3", 1570}
|
||||
new object[] {"1.57e3", 1570},
|
||||
new object[] {"1.57e-3", 0.00157, 0.0000001},
|
||||
new object[] {"1.24e1", 12.4},
|
||||
new object[] { "1.457E2", 145.7 }
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ValidNumberTestData))]
|
||||
public void ParsesValidNumbers(string s, double expected)
|
||||
public void ParsesValidNumbers(string s, double expected, double? tolerance = null)
|
||||
{
|
||||
var input = StringBytesTestConverter.Convert(s);
|
||||
|
||||
var result = tokenizer.TryTokenize(input.First, input.Bytes, out var token);
|
||||
|
||||
Assert.True(result);
|
||||
Assert.Equal(expected, AssertNumericToken(token).Data);
|
||||
if (tolerance.HasValue)
|
||||
{
|
||||
Assert.Equal(expected, AssertNumericToken(token).Data, tolerance: tolerance.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Equal(expected, AssertNumericToken(token).Data);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -1,195 +1,197 @@
|
||||
namespace UglyToad.PdfPig.Tokenization
|
||||
#nullable enable
|
||||
namespace UglyToad.PdfPig.Tokenization;
|
||||
|
||||
using System;
|
||||
using Core;
|
||||
using Tokens;
|
||||
|
||||
internal sealed class NumericTokenizer : ITokenizer
|
||||
{
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using Core;
|
||||
using Tokens;
|
||||
private const byte Zero = 48;
|
||||
private const byte Nine = 57;
|
||||
private const byte Negative = (byte)'-';
|
||||
private const byte Positive = (byte)'+';
|
||||
private const byte Period = (byte)'.';
|
||||
private const byte ExponentLower = (byte)'e';
|
||||
private const byte ExponentUpper = (byte)'E';
|
||||
|
||||
internal sealed class NumericTokenizer : ITokenizer
|
||||
public bool ReadsNextByte => true;
|
||||
|
||||
public bool TryTokenize(byte currentByte, IInputBytes inputBytes, out IToken? token)
|
||||
{
|
||||
private const byte Zero = 48;
|
||||
private const byte Nine = 57;
|
||||
token = null;
|
||||
|
||||
public bool ReadsNextByte { get; } = true;
|
||||
var readBytes = 0;
|
||||
|
||||
public bool TryTokenize(byte currentByte, IInputBytes inputBytes, out IToken token)
|
||||
// Everything before the decimal part.
|
||||
var isNegative = false;
|
||||
double integerPart = 0;
|
||||
|
||||
// Everything after the decimal point.
|
||||
var hasFraction = false;
|
||||
long fractionalPart = 0;
|
||||
var fractionalCount = 0;
|
||||
|
||||
// Support scientific notation in some font files.
|
||||
var hasExponent = false;
|
||||
var isExponentNegative = false;
|
||||
var exponentPart = 0;
|
||||
|
||||
do
|
||||
{
|
||||
token = null;
|
||||
|
||||
using var characters = new ValueStringBuilder(stackalloc char[32]);
|
||||
|
||||
var initialSymbol = currentByte is (byte)'-' or (byte)'+';
|
||||
|
||||
if ((currentByte >= Zero && currentByte <= Nine) || currentByte == '.')
|
||||
var b = inputBytes.CurrentByte;
|
||||
if (b >= Zero && b <= Nine)
|
||||
{
|
||||
characters.Append((char)currentByte);
|
||||
}
|
||||
else if (initialSymbol)
|
||||
{
|
||||
characters.Append((char) currentByte);
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var previousSymbol = initialSymbol;
|
||||
|
||||
while (inputBytes.MoveNext())
|
||||
{
|
||||
var b = inputBytes.CurrentByte;
|
||||
|
||||
if (b == '+' || b == '-')
|
||||
if (hasExponent)
|
||||
{
|
||||
if (previousSymbol)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
characters.Append((char) b);
|
||||
previousSymbol = true;
|
||||
exponentPart = (exponentPart * 10) + (b - Zero);
|
||||
}
|
||||
else if ((b >= Zero && b <= Nine) ||
|
||||
b == '.' ||
|
||||
b == 'E' ||
|
||||
b == 'e')
|
||||
else if (hasFraction)
|
||||
{
|
||||
previousSymbol = false;
|
||||
characters.Append((char)b);
|
||||
fractionalPart = (fractionalPart * 10) + (b - Zero);
|
||||
fractionalCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
integerPart = (integerPart * 10) + (b - Zero);
|
||||
}
|
||||
}
|
||||
|
||||
var str = characters.ToString();
|
||||
|
||||
switch (str)
|
||||
else if (b == Positive)
|
||||
{
|
||||
case "-1":
|
||||
token = NumericToken.MinusOne;
|
||||
return true;
|
||||
case "-":
|
||||
case ".":
|
||||
case "0":
|
||||
case "0000":
|
||||
token = NumericToken.Zero;
|
||||
return true;
|
||||
case "1":
|
||||
token = NumericToken.One;
|
||||
return true;
|
||||
case "2":
|
||||
token = NumericToken.Two;
|
||||
return true;
|
||||
case "3":
|
||||
token = NumericToken.Three;
|
||||
return true;
|
||||
case "4":
|
||||
token = NumericToken.Four;
|
||||
return true;
|
||||
case "5":
|
||||
token = NumericToken.Five;
|
||||
return true;
|
||||
case "6":
|
||||
token = NumericToken.Six;
|
||||
return true;
|
||||
case "7":
|
||||
token = NumericToken.Seven;
|
||||
return true;
|
||||
case "8":
|
||||
token = NumericToken.Eight;
|
||||
return true;
|
||||
case "9":
|
||||
token = NumericToken.Nine;
|
||||
return true;
|
||||
case "10":
|
||||
token = NumericToken.Ten;
|
||||
return true;
|
||||
case "11":
|
||||
token = NumericToken.Eleven;
|
||||
return true;
|
||||
case "12":
|
||||
token = NumericToken.Twelve;
|
||||
return true;
|
||||
case "13":
|
||||
token = NumericToken.Thirteen;
|
||||
return true;
|
||||
case "14":
|
||||
token = NumericToken.Fourteen;
|
||||
return true;
|
||||
case "15":
|
||||
token = NumericToken.Fifteen;
|
||||
return true;
|
||||
case "16":
|
||||
token = NumericToken.Sixteen;
|
||||
return true;
|
||||
case "17":
|
||||
token = NumericToken.Seventeen;
|
||||
return true;
|
||||
case "18":
|
||||
token = NumericToken.Eighteen;
|
||||
return true;
|
||||
case "19":
|
||||
token = NumericToken.Nineteen;
|
||||
return true;
|
||||
case "20":
|
||||
token = NumericToken.Twenty;
|
||||
return true;
|
||||
case "100":
|
||||
token = NumericToken.OneHundred;
|
||||
return true;
|
||||
case "500":
|
||||
token = NumericToken.FiveHundred;
|
||||
return true;
|
||||
case "1000":
|
||||
token = NumericToken.OneThousand;
|
||||
return true;
|
||||
default:
|
||||
if (!double.TryParse(str, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
|
||||
{
|
||||
if (TryParseInvalidNumber(str, out value))
|
||||
{
|
||||
token = new NumericToken(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
token = new NumericToken(value);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseInvalidNumber(string numeric, out double result)
|
||||
{
|
||||
result = 0;
|
||||
|
||||
if (!numeric.Contains("-") && !numeric.Contains("+"))
|
||||
{
|
||||
return false;
|
||||
// Has no impact
|
||||
}
|
||||
|
||||
var parts = numeric.Split(new string[] { "+", "-" }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if (parts.Length == 0)
|
||||
else if (b == Negative)
|
||||
{
|
||||
return false;
|
||||
if (hasExponent)
|
||||
{
|
||||
isExponentNegative = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
isNegative = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var part in parts)
|
||||
else if (b == Period)
|
||||
{
|
||||
if (!double.TryParse(part, NumberStyles.Any, CultureInfo.InvariantCulture, out var partNumber))
|
||||
if (hasExponent || hasFraction)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result += partNumber;
|
||||
hasFraction = true;
|
||||
}
|
||||
else if (b == ExponentLower || b == ExponentUpper)
|
||||
{
|
||||
// Don't allow leading exponent.
|
||||
if (readBytes == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hasExponent)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
hasExponent = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// No valid first character.
|
||||
if (readBytes == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
readBytes++;
|
||||
} while (inputBytes.MoveNext());
|
||||
|
||||
if (hasExponent && !isExponentNegative)
|
||||
{
|
||||
// Apply the multiplication before any fraction logic to avoid loss of precision.
|
||||
// E.g. 1.53E3 should be exactly 1,530.
|
||||
|
||||
// Move the whole part to the left of the decimal point.
|
||||
var combined = integerPart * Pow10(fractionalCount) + fractionalPart;
|
||||
|
||||
// For 1.53E3 we changed this to 153 above, 2 fractional parts, so now we are missing (3-2) 1 additional power of 10.
|
||||
var shift = exponentPart - fractionalCount;
|
||||
|
||||
if (shift >= 0)
|
||||
{
|
||||
integerPart = combined * Pow10(shift);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Still a positive exponent, but not enough to fully shift
|
||||
// For example 1.457E2 becomes 1,457 but shift is (2-3) -1, the outcome should be 145.7
|
||||
integerPart = combined / Pow10(-shift);
|
||||
}
|
||||
|
||||
hasFraction = false;
|
||||
hasExponent = false;
|
||||
}
|
||||
|
||||
if (hasFraction && fractionalCount > 0)
|
||||
{
|
||||
switch (fractionalCount)
|
||||
{
|
||||
case 1:
|
||||
integerPart += fractionalPart / 10.0;
|
||||
break;
|
||||
case 2:
|
||||
integerPart += fractionalPart / 100.0;
|
||||
break;
|
||||
case 3:
|
||||
integerPart += fractionalPart / 1000.0;
|
||||
break;
|
||||
default:
|
||||
integerPart += fractionalPart / Math.Pow(10, fractionalCount);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasExponent)
|
||||
{
|
||||
var signedExponent = isExponentNegative ? -exponentPart : exponentPart;
|
||||
integerPart *= Math.Pow(10, signedExponent);
|
||||
}
|
||||
|
||||
if (isNegative)
|
||||
{
|
||||
integerPart = -integerPart;
|
||||
}
|
||||
|
||||
if (integerPart == 0)
|
||||
{
|
||||
token = NumericToken.Zero;
|
||||
}
|
||||
else
|
||||
{
|
||||
token = new NumericToken(integerPart);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static double Pow10(int exp)
|
||||
{
|
||||
return exp switch
|
||||
{
|
||||
0 => 1,
|
||||
1 => 10,
|
||||
2 => 100,
|
||||
3 => 1000,
|
||||
4 => 10000,
|
||||
5 => 100000,
|
||||
6 => 1000000,
|
||||
7 => 10000000,
|
||||
8 => 100000000,
|
||||
9 => 1000000000,
|
||||
_ => Math.Pow(10, exp)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net462;net471;net6.0;net8.0</TargetFrameworks>
|
||||
<LangVersion>12</LangVersion>
|
||||
<Version>0.1.11-alpha001</Version>
|
||||
<IsTestProject>False</IsTestProject>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
<AssemblyOriginatorKeyFile>..\pdfpig.snk</AssemblyOriginatorKeyFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)'=='net462'">
|
||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\pdfpig.snk" Link="pdfpig.snk" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\UglyToad.PdfPig.Core\UglyToad.PdfPig.Core.csproj" />
|
||||
<ProjectReference Include="..\UglyToad.PdfPig.Tokens\UglyToad.PdfPig.Tokens.csproj" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net462;net471;net6.0;net8.0</TargetFrameworks>
|
||||
<LangVersion>12</LangVersion>
|
||||
<Version>0.1.12-alpha001</Version>
|
||||
<IsTestProject>False</IsTestProject>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
<AssemblyOriginatorKeyFile>..\pdfpig.snk</AssemblyOriginatorKeyFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)'=='net462'">
|
||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\pdfpig.snk" Link="pdfpig.snk" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\UglyToad.PdfPig.Core\UglyToad.PdfPig.Core.csproj" />
|
||||
<ProjectReference Include="..\UglyToad.PdfPig.Tokens\UglyToad.PdfPig.Tokens.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -310,6 +310,7 @@
|
||||
public static readonly NameToken Last = new NameToken("Last");
|
||||
public static readonly NameToken LastChar = new NameToken("LastChar");
|
||||
public static readonly NameToken LastModified = new NameToken("LastModified");
|
||||
public static readonly NameToken Launch = new NameToken("Launch");
|
||||
public static readonly NameToken Lc = new NameToken("LC");
|
||||
public static readonly NameToken Le = new NameToken("LE");
|
||||
public static readonly NameToken Leading = new NameToken("Leading");
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net462;net471;net6.0;net8.0</TargetFrameworks>
|
||||
<LangVersion>12</LangVersion>
|
||||
<Version>0.1.11-alpha001</Version>
|
||||
<IsTestProject>False</IsTestProject>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
<AssemblyOriginatorKeyFile>..\pdfpig.snk</AssemblyOriginatorKeyFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)'=='net462'">
|
||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)'=='netstandard2.0' or '$(TargetFramework)'=='net462' or '$(TargetFramework)'=='net471'">
|
||||
<PackageReference Include="System.Memory" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\pdfpig.snk" Link="pdfpig.snk" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\UglyToad.PdfPig.Core\UglyToad.PdfPig.Core.csproj" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net462;net471;net6.0;net8.0</TargetFrameworks>
|
||||
<LangVersion>12</LangVersion>
|
||||
<Version>0.1.12-alpha001</Version>
|
||||
<IsTestProject>False</IsTestProject>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
<AssemblyOriginatorKeyFile>..\pdfpig.snk</AssemblyOriginatorKeyFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)'=='net462'">
|
||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)'=='netstandard2.0' or '$(TargetFramework)'=='net462' or '$(TargetFramework)'=='net471'">
|
||||
<PackageReference Include="System.Memory" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\pdfpig.snk" Link="pdfpig.snk" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\UglyToad.PdfPig.Core\UglyToad.PdfPig.Core.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -44,6 +44,25 @@
|
||||
return handler.Generate(dictionary);
|
||||
}
|
||||
|
||||
// Try simple font recovery:
|
||||
NameToken[] orderedFallbacks = [NameToken.Type1, NameToken.TrueType];
|
||||
foreach (var fallback in orderedFallbacks)
|
||||
{
|
||||
if (!handlers.TryGetValue(fallback, out handler))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return handler.Generate(dictionary);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log?.Error($"Tried to parse font as fallback type: {fallback}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
throw new NotImplementedException($"Parsing not implemented for fonts of type: {subtype}, please submit a pull request or an issue.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net462;net471;net6.0;net8.0</TargetFrameworks>
|
||||
<LangVersion>12</LangVersion>
|
||||
<Version>0.1.11-alpha001</Version>
|
||||
<IsTestProject>False</IsTestProject>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
<AssemblyOriginatorKeyFile>..\pdfpig.snk</AssemblyOriginatorKeyFile>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="Resources\CMap\*" />
|
||||
<None Remove="Resources\ICC\*" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\CMap\*" />
|
||||
<EmbeddedResource Include="Resources\ICC\*" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\pdfpig.snk" Link="pdfpig.snk" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)'=='net462'">
|
||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)'=='netstandard2.0' or '$(TargetFramework)'=='net462' or '$(TargetFramework)'=='net471'">
|
||||
<PackageReference Include="System.Memory" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\UglyToad.PdfPig.Core\UglyToad.PdfPig.Core.csproj" />
|
||||
<ProjectReference Include="..\UglyToad.PdfPig.Fonts\UglyToad.PdfPig.Fonts.csproj" />
|
||||
<ProjectReference Include="..\UglyToad.PdfPig.Tokenization\UglyToad.PdfPig.Tokenization.csproj" />
|
||||
<ProjectReference Include="..\UglyToad.PdfPig.Tokens\UglyToad.PdfPig.Tokens.csproj" />
|
||||
</ItemGroup>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net462;net471;net6.0;net8.0</TargetFrameworks>
|
||||
<LangVersion>12</LangVersion>
|
||||
<Version>0.1.12-alpha001</Version>
|
||||
<IsTestProject>False</IsTestProject>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
<AssemblyOriginatorKeyFile>..\pdfpig.snk</AssemblyOriginatorKeyFile>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="Resources\CMap\*" />
|
||||
<None Remove="Resources\ICC\*" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\CMap\*" />
|
||||
<EmbeddedResource Include="Resources\ICC\*" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\pdfpig.snk" Link="pdfpig.snk" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)'=='net462'">
|
||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)'=='netstandard2.0' or '$(TargetFramework)'=='net462' or '$(TargetFramework)'=='net471'">
|
||||
<PackageReference Include="System.Memory" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\UglyToad.PdfPig.Core\UglyToad.PdfPig.Core.csproj" />
|
||||
<ProjectReference Include="..\UglyToad.PdfPig.Fonts\UglyToad.PdfPig.Fonts.csproj" />
|
||||
<ProjectReference Include="..\UglyToad.PdfPig.Tokenization\UglyToad.PdfPig.Tokenization.csproj" />
|
||||
<ProjectReference Include="..\UglyToad.PdfPig.Tokens\UglyToad.PdfPig.Tokens.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,26 +1,26 @@
|
||||
|
||||
namespace UglyToad.PdfPig.Writer
|
||||
{
|
||||
using Actions;
|
||||
using Content;
|
||||
using Core;
|
||||
using Filters;
|
||||
using Fonts;
|
||||
using Graphics;
|
||||
using Logging;
|
||||
using Outline;
|
||||
using Outline.Destinations;
|
||||
using Parser;
|
||||
using Parser.Parts;
|
||||
using PdfPig.Fonts.Standard14Fonts;
|
||||
using PdfPig.Fonts.TrueType;
|
||||
using PdfPig.Fonts.TrueType.Parser;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Xml.Linq;
|
||||
using Content;
|
||||
using Core;
|
||||
using Fonts;
|
||||
using Actions;
|
||||
using Filters;
|
||||
using Graphics;
|
||||
using Logging;
|
||||
using PdfPig.Fonts.TrueType;
|
||||
using PdfPig.Fonts.Standard14Fonts;
|
||||
using PdfPig.Fonts.TrueType.Parser;
|
||||
using Outline;
|
||||
using Outline.Destinations;
|
||||
using Parser;
|
||||
using Parser.Parts;
|
||||
using Tokenization.Scanner;
|
||||
using Tokens;
|
||||
|
||||
@@ -307,7 +307,6 @@ namespace UglyToad.PdfPig.Writer
|
||||
/// </summary>
|
||||
/// <param name="document">Source document.</param>
|
||||
/// <param name="pageNumber">Page to copy.</param>
|
||||
/// <param name="options">Control how copying for the page occurs.</param>
|
||||
/// <returns>A builder for editing the page.</returns>
|
||||
public PdfPageBuilder AddPage(PdfDocument document, int pageNumber)
|
||||
{
|
||||
@@ -458,72 +457,16 @@ namespace UglyToad.PdfPig.Writer
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var val = kvp.Value;
|
||||
if (kvp.Value is IndirectReferenceToken ir)
|
||||
{
|
||||
ObjectToken tk = document.Structure.TokenScanner.Get(ir.Data);
|
||||
if (tk is null)
|
||||
{
|
||||
// malformed
|
||||
continue;
|
||||
}
|
||||
val = tk.Data;
|
||||
}
|
||||
|
||||
if (!(val is ArrayToken arr))
|
||||
{
|
||||
// should be array... ignore and remove bad dict
|
||||
continue;
|
||||
}
|
||||
var copiedTokens = CopyAnnotationsFromPageSource(
|
||||
kvp.Value,
|
||||
document.Structure.TokenScanner,
|
||||
refs,
|
||||
page,
|
||||
options.CopyLinkFunc,
|
||||
x => links.Add(x));
|
||||
|
||||
// if copyLink is unset, ignore links to resolve issues with refencing non-existing pages
|
||||
var toAdd = new List<IToken>();
|
||||
foreach (var annot in arr.Data)
|
||||
{
|
||||
DictionaryToken? tk = GetRemoteDict(annot);
|
||||
if (tk is null)
|
||||
{
|
||||
// malformed
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tk.TryGet(NameToken.Subtype, out var st) && st is NameToken nm && nm == NameToken.Link)
|
||||
{
|
||||
if (options.CopyLinkFunc is null)
|
||||
{
|
||||
// ignore link if don't know how to copy
|
||||
continue;
|
||||
}
|
||||
|
||||
var link = page.annotationProvider.GetAction(tk);
|
||||
if (link is null)
|
||||
{
|
||||
// ignore unknown link actions
|
||||
continue;
|
||||
}
|
||||
|
||||
var copiedLink = options.CopyLinkFunc(link);
|
||||
if (copiedLink is null)
|
||||
{
|
||||
// ignore if caller wants to skip the link
|
||||
continue;
|
||||
}
|
||||
|
||||
if (copiedLink != link)
|
||||
{
|
||||
// defer to write links when all pages are added
|
||||
var copiedToken = (DictionaryToken)WriterUtil.CopyToken(context, tk, document.Structure.TokenScanner, refs);
|
||||
links.Add((copiedToken, copiedLink));
|
||||
continue;
|
||||
}
|
||||
|
||||
// copy as is if caller returns the same link
|
||||
}
|
||||
toAdd.Add(WriterUtil.CopyToken(context, tk, document.Structure.TokenScanner, refs));
|
||||
}
|
||||
// copy rest
|
||||
copiedPageDict[NameToken.Annots] = new ArrayToken(toAdd);
|
||||
copiedPageDict[NameToken.Annots] = new ArrayToken(copiedTokens);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -625,6 +568,161 @@ namespace UglyToad.PdfPig.Writer
|
||||
}
|
||||
}
|
||||
|
||||
private IReadOnlyList<IToken> CopyAnnotationsFromPageSource(
|
||||
IToken val,
|
||||
IPdfTokenScanner sourceScanner,
|
||||
IDictionary<IndirectReference, IndirectReferenceToken> refs,
|
||||
Page page,
|
||||
Func<PdfAction, PdfAction?>? linkCopyFunc = null,
|
||||
Action<(DictionaryToken, PdfAction)>? deferredActionUpdate = null)
|
||||
{
|
||||
var permittedLinkActionTypes = new HashSet<NameToken>
|
||||
{
|
||||
// A web URI.
|
||||
NameToken.Uri,
|
||||
// A page in a different non-embedded document.
|
||||
NameToken.GoToR,
|
||||
// Launch an external application.
|
||||
NameToken.Launch,
|
||||
};
|
||||
|
||||
if (!DirectObjectFinder.TryGet(val, sourceScanner, out ArrayToken? annotationsArray))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var copiedAnnotations = new List<IToken>();
|
||||
foreach (var annotEntry in annotationsArray.Data)
|
||||
{
|
||||
if (!DirectObjectFinder.TryGet(annotEntry, sourceScanner, out DictionaryToken? annotDict))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var removedKeys = new List<NameToken>();
|
||||
|
||||
/*
|
||||
* An indirect reference to the page object with which this annotation is associated.
|
||||
* Note: This entry is required for screen annotations associated with rendition actions.
|
||||
*/
|
||||
if (annotDict.TryGet(NameToken.P, out _))
|
||||
{
|
||||
// If we have a page reference we should update it when this page is written.
|
||||
// For now, we'll remove it. This will corrupt screen annotations as noted above.
|
||||
removedKeys.Add(NameToken.P);
|
||||
}
|
||||
|
||||
// We don't copy the struct tree so skip this for now.
|
||||
if (annotDict.TryGet(NameToken.StructParent, out _))
|
||||
{
|
||||
removedKeys.Add(NameToken.StructParent);
|
||||
}
|
||||
|
||||
// We treat non-link annotations as ok for now, we should revisit this.
|
||||
if (!annotDict.TryGet(NameToken.Subtype, sourceScanner, out NameToken? subtype)
|
||||
|| subtype != NameToken.Link)
|
||||
{
|
||||
var copiedRef = WriterUtil.CopyToken(
|
||||
context,
|
||||
CopyWithSkippedKeys(annotDict, removedKeys),
|
||||
sourceScanner,
|
||||
refs);
|
||||
|
||||
copiedAnnotations.Add(copiedRef);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (linkCopyFunc != null && deferredActionUpdate != null)
|
||||
{
|
||||
var action = page.annotationProvider.GetAction(annotDict);
|
||||
|
||||
if (action != null)
|
||||
{
|
||||
var copiedLink = linkCopyFunc(action);
|
||||
if (copiedLink != action && copiedLink != null)
|
||||
{
|
||||
// defer to write links when all pages are added
|
||||
var copiedToken = (DictionaryToken)WriterUtil.CopyToken(context, annotDict, sourceScanner, refs);
|
||||
deferredActionUpdate((copiedToken, copiedLink));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the link has an action then this link can point elsewhere in this document, maybe not to a page we copied?
|
||||
if (annotDict.TryGet(NameToken.A, sourceScanner, out DictionaryToken? actionDict))
|
||||
{
|
||||
// If the link annotation points somewhere inside our document we can't currently maintain validity on-copy.
|
||||
if (!actionDict.TryGet(NameToken.S, sourceScanner, out NameToken? actionType)
|
||||
|| !permittedLinkActionTypes.Contains(actionType))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var copiedRef = WriterUtil.CopyToken(
|
||||
context,
|
||||
CopyWithSkippedKeys(annotDict, removedKeys),
|
||||
sourceScanner,
|
||||
refs);
|
||||
|
||||
copiedAnnotations.Add(copiedRef);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// A dest can point elsewhere in this document, maybe not to a page we copied?
|
||||
if (annotDict.TryGet(NameToken.Dest, out _))
|
||||
{
|
||||
// Skip for now.
|
||||
continue;
|
||||
}
|
||||
|
||||
// If neither /A nor /Dest are present then I don't really know what this link does, so it should be safe to copy:
|
||||
var finalCopiedRef = WriterUtil.CopyToken(
|
||||
context,
|
||||
CopyWithSkippedKeys(annotDict, removedKeys),
|
||||
sourceScanner,
|
||||
refs);
|
||||
|
||||
copiedAnnotations.Add(finalCopiedRef);
|
||||
}
|
||||
|
||||
return copiedAnnotations;
|
||||
}
|
||||
|
||||
private static DictionaryToken CopyWithSkippedKeys(
|
||||
DictionaryToken source,
|
||||
IReadOnlyList<NameToken> skipped)
|
||||
{
|
||||
var dict = new Dictionary<NameToken, IToken>();
|
||||
|
||||
foreach (var kvp in source.Data)
|
||||
{
|
||||
var name = NameToken.Create(kvp.Key);
|
||||
|
||||
var ignore = false;
|
||||
|
||||
foreach (var skippedName in skipped)
|
||||
{
|
||||
if (skippedName == name)
|
||||
{
|
||||
ignore = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ignore)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
dict[name] = kvp.Value;
|
||||
}
|
||||
|
||||
return new DictionaryToken(dict);
|
||||
}
|
||||
|
||||
private void CompleteDocument()
|
||||
{
|
||||
// write fonts to reserved object numbers
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net462;net471;net6.0;net8.0</TargetFrameworks>
|
||||
<PackageId>PdfPig</PackageId>
|
||||
@@ -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.11-alpha001</Version>
|
||||
<AssemblyVersion>0.1.10.0</AssemblyVersion>
|
||||
<FileVersion>0.1.10.0</FileVersion>
|
||||
<Version>0.1.12-alpha001</Version>
|
||||
<AssemblyVersion>0.1.12.0</AssemblyVersion>
|
||||
<FileVersion>0.1.12.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