mirror of
https://github.com/UglyToad/PdfPig.git
synced 2026-03-10 00:23:29 +08:00
Compare commits
32 Commits
queue-appr
...
rewrite-re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b764e7c20 | ||
|
|
fed46965ff | ||
|
|
03fa53ece0 | ||
|
|
50f878b2ba | ||
|
|
2a10b6c285 | ||
|
|
85fc63d585 | ||
|
|
5abdfcb96c | ||
|
|
00ca268092 | ||
|
|
813d3baa18 | ||
|
|
2b11961c8c | ||
|
|
efb8c2a803 | ||
|
|
e636212ec8 | ||
|
|
3b318e1944 | ||
|
|
377eb507e8 | ||
|
|
ff4e763192 | ||
|
|
6a06452103 | ||
|
|
a5e92cd11c | ||
|
|
4bf746c747 | ||
|
|
bffd51425d | ||
|
|
e3388ec6b6 | ||
|
|
31658ca020 | ||
|
|
1021729727 | ||
|
|
9503f9c137 | ||
|
|
016b754c5b | ||
|
|
de3b6ac6f4 | ||
|
|
b11f936f22 | ||
|
|
7fe60ff8c3 | ||
|
|
781991b6bf | ||
|
|
daaac9350d | ||
|
|
f099dd5827 | ||
|
|
0586713da3 | ||
|
|
62612588c8 |
58
.github/workflows/run_common_crawl_tests.yml
vendored
Normal file
58
.github/workflows/run_common_crawl_tests.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
name: Run Common Crawl Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up dotnet core
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: "8.0.x"
|
||||
|
||||
- name: Restore corpus cache 0000, 0001
|
||||
id: restore-corpus
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: corpus/
|
||||
key: ${{ runner.os }}-pdf-corpus-0000-0001
|
||||
|
||||
- name: Download corpus if cache missed 0000, 0001
|
||||
if: steps.restore-corpus.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
mkdir -p corpus/zipfiles
|
||||
cd corpus/zipfiles
|
||||
echo "Downloading 0000.zip"
|
||||
wget -nv https://digitalcorpora.s3.amazonaws.com/corpora/files/CC-MAIN-2021-31-PDF-UNTRUNCATED/zipfiles/0000-0999/0000.zip -O 0000.zip
|
||||
wget -nv https://digitalcorpora.s3.amazonaws.com/corpora/files/CC-MAIN-2021-31-PDF-UNTRUNCATED/zipfiles/0000-0999/0001.zip -O 0001.zip
|
||||
cd ..
|
||||
unzip 'zipfiles/*.zip' -d extracted
|
||||
# run: rm -f zipfiles/*.zip
|
||||
|
||||
- name: Remove unwanted test files
|
||||
run: |
|
||||
skip_files=(
|
||||
"corpus/extracted/0000399.pdf"
|
||||
"corpus/extracted/0000819.pdf"
|
||||
"corpus/extracted/0000920.pdf"
|
||||
"corpus/extracted/0000300.pdf"
|
||||
"corpus/extracted/0001589.pdf"
|
||||
"corpus/extracted/0001957.pdf"
|
||||
)
|
||||
|
||||
for file in "${skip_files[@]}"; do
|
||||
if [ -f "$file" ]; then
|
||||
echo "Removing $file"
|
||||
rm "$file"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Run tests against corpus
|
||||
run: dotnet run --project tools/UglyToad.PdfPig.ConsoleRunner/UglyToad.PdfPig.ConsoleRunner.csproj "corpus/extracted"
|
||||
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.
|
||||
|
||||
@@ -34,10 +34,39 @@ O<DJ+*.@<*K0@<6L(Df-\0Ec5e;DffZ(EZee.Bl.9pF""AGXBPCsi + DGm >@3BB / F * &OCAfu2
|
||||
text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReplacesZWithEmptyBytes()
|
||||
[Theory]
|
||||
[InlineData("BE", "h")]
|
||||
[InlineData("BOq", "he")]
|
||||
[InlineData("BOtu", "hel")]
|
||||
[InlineData("BOu!r", "hell")]
|
||||
[InlineData("BOu!rDZ", "hello")]
|
||||
[InlineData("BOu!rD]f", "hello ")]
|
||||
[InlineData("BOu!rD]j6", "hello w")]
|
||||
[InlineData("BOu!rD]j7B", "hello wo")]
|
||||
[InlineData("BOu!rD]j7BEW", "hello wor")]
|
||||
[InlineData("BOu!rD]j7BEbk", "hello worl")]
|
||||
[InlineData("BOu!rD]j7BEbo7", "hello world")]
|
||||
[InlineData("BOu!rD]j7BEbo80", "hello world!")]
|
||||
public void DecodesHelloWorld(string encoded, string decoded)
|
||||
{
|
||||
var bytes = Encoding.ASCII.GetBytes("9jqo^zBlbD-");
|
||||
var result = filter.Decode(
|
||||
Encoding.ASCII.GetBytes(encoded),
|
||||
dictionary,
|
||||
TestFilterProvider.Instance,
|
||||
0);
|
||||
|
||||
Assert.Equal(decoded, Encoding.ASCII.GetString(result.ToArray()));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("9jqo^zBlbD-", "Man \0\0\0\0is d")]
|
||||
[InlineData("", "")]
|
||||
[InlineData("z", "\0\0\0\0")]
|
||||
[InlineData("zz", "\0\0\0\0\0\0\0\0")]
|
||||
[InlineData("zzz", "\0\0\0\0\0\0\0\0\0\0\0\0")]
|
||||
public void ReplacesZWithEmptyBytes(string encoded, string decoded)
|
||||
{
|
||||
var bytes = Encoding.ASCII.GetBytes(encoded);
|
||||
|
||||
var result = filter.Decode(bytes, dictionary, TestFilterProvider.Instance, 1);
|
||||
|
||||
@@ -47,7 +76,7 @@ O<DJ+*.@<*K0@<6L(Df-\0Ec5e;DffZ(EZee.Bl.9pF""AGXBPCsi + DGm >@3BB / F * &OCAfu2
|
||||
string text = Encoding.ASCII.GetString(result.Span);
|
||||
#endif
|
||||
|
||||
Assert.Equal("Man \0\0\0\0is d", text);
|
||||
Assert.Equal(decoded, text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -60,14 +89,17 @@ O<DJ+*.@<*K0@<6L(Df-\0Ec5e;DffZ(EZee.Bl.9pF""AGXBPCsi + DGm >@3BB / F * &OCAfu2
|
||||
Assert.Throws<InvalidOperationException>(action);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SingleCharacterLastThrows()
|
||||
[Theory]
|
||||
[InlineData("@rH:%B", "cool")]
|
||||
[InlineData("A~>", "")]
|
||||
[InlineData("@rH:%A~>", "cool")]
|
||||
public void SingleCharacterLastIgnores(string encoded, string decoded)
|
||||
{
|
||||
var bytes = Encoding.ASCII.GetBytes("9jqo^B");
|
||||
var bytes = Encoding.ASCII.GetBytes(encoded);
|
||||
|
||||
Action action = () => filter.Decode(bytes, dictionary, TestFilterProvider.Instance, 1);
|
||||
var result = filter.Decode(bytes, dictionary, TestFilterProvider.Instance, 1);
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(action);
|
||||
Assert.Equal(decoded, Encoding.ASCII.GetString(result.ToArray()));
|
||||
}
|
||||
|
||||
private const string PdfContent = @"1 0 obj
|
||||
|
||||
@@ -146,6 +146,72 @@ endbfchar";
|
||||
Assert.Equal("10.003", cmap.Version);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanParseFile0001413CMap()
|
||||
{
|
||||
const string cmap =
|
||||
"""
|
||||
/CIDInit /ProcSet findresource
|
||||
begin
|
||||
12 dict
|
||||
begin
|
||||
begincmap
|
||||
/CIDSystemInfo <</Ordering (GB1) /Registry (Adobe) /Supplement 2 >> def
|
||||
/CMapName /Adobe-GB1-2 def
|
||||
/CMapType 2 def
|
||||
1 begincodespacerange
|
||||
<0000> <ffff> endcodespacerange
|
||||
100 beginbfrange
|
||||
<0001> <0002> <0022> <0003> <0019> <0025> <001b> <0023> <003f> <0024> <0025> <0049> <0026> <0033> <004c> <0034> <0036> <005d> <0037> <003d> <0061> <003e> <004b> <0069> <004c> <004d> <0079> <0053> <0054> <3001> <0055> <005a> <300a> <005c> <005d> <4e00> <0060> <0062> <4e09> <0063> <0064> <4e0d> <0066> <0067> <4e13> <006a> <006d> <4e1a> <006f> <0070> <4e24> <0076> <0079> <4e38> <007a> <007b> <4e3d> <007e> <007f> <4e48> <0080> <0085> <4e4b> <0089> <008b> <4e5f> <008d> <008e> <4e70> <0090> <0091> <4e88> <0092> <0093> <4e8b> <0094> <0095> <4e8e> <0097> <0098> <4e94> <0099> <009a> <4e9a> <009c> <00a0> <4ea4> <00a3> <00a4> <4ec0> <00a5> <00a6> <4ec5> <00a7> <00a8> <4eca> <00a9> <00aa> <4ecd> <00ac> <00af> <4ed3> <00b0> <00b1> <4ed8> <00b3> <00b5> <4ee3> <00b9> <00ba> <4ef6> <00bf> <00c1> <4f0f> <00c2> <00c5> <4f17> <00c6> <00c7> <4f1f> <00cb> <00cc> <4f2f> <00d2> <00d4> <4f4d> <00d8> <00d9> <4f5b> <00e2> <00e3> <4fa6> <00e7> <00e8> <4fc3> <00ef> <00f0> <5018> <0108> <010a> <516b> <010b> <010c> <5170> <010d> <010e> <5173> <010f> <0112> <5176> <0113> <0114> <517b> <0117> <0118> <518c> <011a> <011b> <519b> <011e> <011f> <51b2> <0124> <0125> <51e0> <0128> <012a> <51f9> <012b> <012c> <5200> <012d> <012e> <5206> <0132> <0134> <5219> <0138> <0139> <5228> <0144> <0147> <529e> <0149> <014b> <52a8> <0156> <0157> <5316> <0158> <0159> <533a> <015e> <015f> <534e> <0161> <0163> <5355> <0167> <0169> <5360> <016b> <016c> <5370> <016d> <016e> <5373> <0175> <0176> <538b> <017c> <0181> <53c8> <0183> <0186> <53d6> <0187> <018a> <53e3> <018b> <0193> <53e8> <0194> <0195> <53f2> <0196> <0197> <53f7> <019a> <019b> <5403> <019c> <019d> <5408> <019e> <01a0> <540c> <01a4> <01a5> <542b> <01ae> <01b0> <547b> <01be> <01bf> <552e> <01cb> <01cc> <56da> <01d5> <01d6> <56fd> <01e3> <01e4> <574e> <01e7> <01e8> <575a> <01e9> <01ea> <5760> <01f8> <01f9> <57f9> <0211> <0212> <5915> <0216> <0218> <5929> <0223> <0224> <5956> <0233> <0234> <5a03> <023e> <023f> <5b57> <0242> <0243> <5b63> <0248> <0249> <5b88> <024e> <0250> <5b9c> <0251> <0253> <5ba1> <0256> <0257> <5bb3> <025a> <025c> <5bbd> endbfrange
|
||||
100 beginbfrange
|
||||
<0260> <0264> <5bf8> <0268> <0269> <5c09> <0272> <0274> <5c3c> <0279> <027a> <5c4a> <0286> <0289> <5de5> <028c> <028f> <5df1> <0291> <0293> <5e01> <0298> <0299> <5e15> <029a> <029b> <5e1c> <029d> <029e> <5e2d> <02a2> <02a4> <5e72> <02ab> <02ad> <5e93> <02b0> <02b1> <5ea6> <02b3> <02b4> <5eb6> <02b6> <02b7> <5ef6> <02b9> <02ba> <5eff> <02bb> <02bc> <5f02> <02c1> <02c2> <5f17> <02c7> <02c8> <5f39> <02c9> <02ca> <5f52> <02cf> <02d0> <5f7b> <02d1> <02d2> <5f80> <02d3> <02d4> <5f84> <02e6> <02e7> <6000> <0300> <0301> <6167> <0308> <0309> <6210> <030e> <030f> <623f> <0311> <0312> <624d> <0315> <0316> <6266> <031c> <031e> <627e> <0321> <0324> <6295> <0327> <0328> <62a4> <0330> <0331> <62cc> <0332> <0333> <62d8> <0337> <0338> <62e8> <033b> <033c> <6301> <0340> <0341> <631a> <0355> <0356> <63a7> <0358> <0359> <63cf> <036b> <036c> <653e> <037d> <037e> <65af> <0387> <038b> <65e5> <038d> <038e> <65f6> <0393> <0394> <661f> <0397> <0398> <666e> <03a0> <03a2> <66fe> <03a3> <03a4> <6708> <03aa> <03ad> <672a> <03b2> <03b3> <6742> <03b4> <03b5> <6750> <03c2> <03c3> <67d0> <03ca> <03cc> <6837> <03db> <03de> <6b20> <03e2> <03e7> <6b62> <03e8> <03e9> <6b8a> <03f1> <03f2> <6bd4> <03fb> <03fc> <6c2e> <040d> <040e> <6cbb> <040f> <0410> <6cbe> <0421> <0422> <6d3d> <0425> <0426> <6d4a> <042d> <042e> <6d88> <043b> <043c> <6e20> <0457> <0458> <707e> <045a> <045b> <70b8> <045d> <045e> <70c2> <046d> <046e> <7247> <0476> <0477> <7279> <047b> <047c> <72ec> <0485> <0486> <73af> <0494> <0495> <7530> <0498> <0499> <7537> <04a9> <04aa> <767d> <04b0> <04b1> <76d0> <04ca> <04cb> <786b> <04d5> <04d6> <795d> <04e1> <04e2> <79ef> <04e9> <04ea> <7a76> <04f2> <04f4> <7ade> <0515> <0516> <7ea6> <0519> <051a> <7eb2> <051e> <051f> <7ec3> <0520> <0522> <7ec6> <0526> <0528> <7ed8> <0529> <052a> <7edc> <0531> <0532> <7f13> <0542> <0543> <800c> <0546> <0547> <804b> <0564> <0565> <81f3> <056f> <0570> <826f> <0579> <057a> <82e5> <0586> <0587> <83b1> <05b6> <05b7> <89c1> <05ba> <05bb> <89c8> <05c1> <05c2> <8ba1> <05c4> <05c5> <8ba8> <05c6> <05c9> <8bad> <05cd> <05ce> <8bbe> <05da> <05db> <8be5> <05dc> <05dd> <8bec> endbfrange
|
||||
33 beginbfrange
|
||||
<05df> <05e0> <8bf1> <05e2> <05e3> <8bf7> <05f4> <05f6> <8d21> <05f8> <05f9> <8d27> <05fa> <05fb> <8d2a> <05fc> <05fd> <8d2e> <05ff> <0601> <8d37> <060e> <060f> <8d8a> <061e> <061f> <8f90> <0623> <0624> <8fa8> <0629> <062a> <8fc4> <062d> <062e> <8fd0> <0630> <0631> <8fd8> <0632> <0636> <8fdb> <0637> <0638> <8fea> <063d> <0640> <9000> <0641> <0642> <9009> <0646> <0647> <901a> <0648> <0649> <901f> <0651> <0652> <906d> <0654> <0655> <907f> <0658> <0659> <90ae> <065d> <065e> <90e7> <0660> <0662> <914b> <0667> <066a> <91cc> <0671> <0672> <94c0> <067f> <0680> <95ed> <068d> <068f> <9644> <0694> <0695> <9676> <069c> <069d> <96c5> <069f> <06a0> <96f6> <06b0> <06b2> <9879> <06b6> <06b7> <9886> <06bc> <06bd> <98de> endbfrange
|
||||
100 beginbfchar
|
||||
<0000> <0020> <001a> <003d> <004e> <007d> <004f> <00a8> <0050> <2014> <0051> <2026> <0052> <25a1> <005b> <ff0c> <005e> <4e03> <005f> <4e07> <0065> <4e11> <0068> <4e16> <0069> <4e18> <006e> <4e22> <0071> <4e27> <0072> <4e2a> <0073> <4e2d> <0074> <4e30> <0075> <4e34> <007c> <4e43> <007d> <4e45> <0086> <4e53> <0087> <4e59> <0088> <4e5d> <008c> <4e66> <008f> <4e86> <0096> <4e92> <009b> <4ea2> <00a1> <4eab> <00a2> <4eba> <00ab> <4ed1> <00b2> <4edf> <00b6> <4eea> <00b7> <4eec> <00b8> <4ef2> <00bb> <4efb> <00bc> <4efd> <00bd> <4f01> <00be> <4f0a> <00c8> <4f24> <00c9> <4f26> <00ca> <4f2a> <00cd> <4f34> <00ce> <4f36> <00cf> <4f38> <00d0> <4f3c> <00d1> <4f46> <00d5> <4f53> <00d6> <4f55> <00d7> <4f59> <00da> <4f69> <00db> <4f6f> <00dc> <4f73> <00dd> <4f7f> <00de> <4f8b> <00df> <4f9b> <00e0> <4f9d> <00e1> <4fa3> <00e4> <4fae> <00e5> <4fb5> <00e6> <4fbf> <00e9> <4fdd> <00ea> <4fe1> <00eb> <4fee> <00ec> <4ff1> <00ed> <500d> <00ee> <5012> <00f1> <501f> <00f2> <5021> <00f3> <503c> <00f4> <503e> <00f5> <5047> <00f6> <505a> <00f7> <505c> <00f8> <5065> <00f9> <507f> <00fa> <5085> <00fb> <50a8> <00fc> <50b2> <00fd> <50f3> <00fe> <50fb> <00ff> <513f> <0100> <5141> <0101> <5143> <0102> <5145> <0103> <5148> <0104> <514b> <0105> <514d> <0106> <5165> <0107> <5168> <0115> <5180> <0116> <5185> <0119> <5199> <011c> <51ac> <011d> <51b0> <0120> <51b5> <0121> <51c0> <0122> <51c6> <0123> <51cf> <0126> <51ed> endbfchar
|
||||
100 beginbfchar
|
||||
<0127> <51ef> <012f> <520a> <0130> <5212> <0131> <5217> <0135> <521d> <0136> <5220> <0137> <5224> <013a> <522b> <013b> <5230> <013c> <5236> <013d> <523b> <013e> <5242> <013f> <524a> <0140> <524d> <0141> <5269> <0142> <526f> <0143> <529b> <0148> <52a3> <014c> <52b1> <014d> <52b3> <014e> <52bf> <014f> <52c3> <0150> <52d2> <0151> <52d8> <0152> <52e4> <0153> <52fa> <0154> <5300> <0155> <5305> <015a> <5341> <015b> <5343> <015c> <5347> <015d> <534a> <0160> <5353> <0164> <535a> <0165> <535c> <0166> <535e> <016a> <536b> <016f> <5378> <0170> <537f> <0171> <5382> <0172> <5384> <0173> <5386> <0174> <5389> <0177> <5398> <0178> <539a> <0179> <539f> <017a> <53bb> <017b> <53c2> <0182> <53d1> <0198> <53fc> <0199> <5401> <01a1> <5411> <01a2> <5417> <01a3> <5426> <01a6> <542f> <01a7> <5431> <01a8> <5446> <01a9> <544a> <01aa> <5458> <01ab> <5468> <01ac> <5473> <01ad> <5475> <01b1> <548c> <01b2> <5490> <01b3> <5496> <01b4> <54a8> <01b5> <54b1> <01b6> <54c1> <01b7> <54c8> <01b8> <54cd> <01b9> <54df> <01ba> <54e5> <01bb> <54ea> <01bc> <5510> <01bd> <5524> <01c0> <5531> <01c1> <5546> <01c2> <554a> <01c3> <5564> <01c4> <5566> <01c5> <5580> <01c6> <5584> <01c7> <558a> <01c8> <559c> <01c9> <5668> <01ca> <566a> <01cd> <56de> <01ce> <56e0> <01cf> <56e2> <01d0> <56e4> <01d1> <56ed> <01d2> <56f0> <01d3> <56f4> <01d4> <56fa> <01d7> <5703> <01d8> <5706> <01d9> <5708> <01da> <571f> <01db> <5723> <01dc> <5728> endbfchar
|
||||
100 beginbfchar
|
||||
<01dd> <572d> <01de> <5730> <01df> <573a> <01e0> <5740> <01e1> <5747> <01e2> <574a> <01e5> <5751> <01e6> <5757> <01eb> <5764> <01ec> <5766> <01ed> <5782> <01ee> <5784> <01ef> <578b> <01f0> <5792> <01f1> <579b> <01f2> <57a6> <01f3> <57ab> <01f4> <57c3> <01f5> <57ce> <01f6> <57d4> <01f7> <57df> <01fa> <5802> <01fb> <5806> <01fc> <5815> <01fd> <5821> <01fe> <5824> <01ff> <582a> <0200> <5851> <0201> <5854> <0202> <585e> <0203> <586b> <0204> <5883> <0205> <5893> <0206> <5899> <0207> <589e> <0208> <58a8> <0209> <58c1> <020a> <58e4> <020b> <58eb> <020c> <58f0> <020d> <5904> <020e> <5907> <020f> <590d> <0210> <590f> <0213> <591a> <0214> <591f> <0215> <5927> <0219> <592e> <021a> <5931> <021b> <5934> <021c> <5938> <021d> <593a> <021e> <5947> <021f> <5949> <0220> <594b> <0221> <594e> <0222> <5951> <0225> <5962> <0226> <5965> <0227> <5973> <0228> <5979> <0229> <597d> <022a> <5982> <022b> <5999> <022c> <59a5> <022d> <59a8> <022e> <59bb> <022f> <59c6> <0230> <59cb> <0231> <59d4> <0232> <5a01> <0235> <5a1c> <0236> <5a31> <0237> <5a74> <0238> <5a92> <0239> <5ac1> <023a> <5acc> <023b> <5ae9> <023c> <5b50> <023d> <5b54> <0240> <5b5c> <0241> <5b5f> <0244> <5b66> <0245> <5b6a> <0246> <5b83> <0247> <5b85> <024a> <5b8c> <024b> <5b8f> <024c> <5b97> <024d> <5b9a> <0254> <5baa> <0255> <5bb0> <0258> <5bb6> <0259> <5bb9> <025d> <5bc6> <025e> <5bcc> <025f> <5bdf> <0265> <5bff> <0266> <5c04> <0267> <5c06> endbfchar
|
||||
100 beginbfchar
|
||||
<026a> <5c0f> <026b> <5c11> <026c> <5c14> <026d> <5c1a> <026e> <5c1d> <026f> <5c24> <0270> <5c31> <0271> <5c38> <0275> <5c40> <0276> <5c42> <0277> <5c45> <0278> <5c48> <027b> <5c55> <027c> <5c5e> <027d> <5c65> <027e> <5c71> <027f> <5c7f> <0280> <5c97> <0281> <5c9b> <0282> <5cad> <0283> <5cb8> <0284> <5ddd> <0285> <5de1> <028a> <5deb> <028b> <5dee> <0290> <5dfe> <0294> <5e05> <0295> <5e08> <0296> <5e0c> <0297> <5e10> <029c> <5e26> <029f> <5e38> <02a0> <5e45> <02a1> <5e55> <02a5> <5e76> <02a6> <5e78> <02a7> <5e7f> <02a8> <5e86> <02a9> <5e8a> <02aa> <5e8f> <02ae> <5e9c> <02af> <5e9f> <02b2> <5ead> <02b5> <5ec9> <02b8> <5efa> <02bd> <5f0a> <02be> <5f0f> <02bf> <5f13> <02c0> <5f15> <02c3> <5f20> <02c4> <5f25> <02c5> <5f27> <02c6> <5f31> <02cb> <5f55> <02cc> <5f62> <02cd> <5f64> <02ce> <5f71> <02d5> <5f88> <02d6> <5f8b> <02d7> <5f92> <02d8> <5f97> <02d9> <5fa1> <02da> <5faa> <02db> <5fae> <02dc> <5fb5> <02dd> <5fb7> <02de> <5fc3> <02df> <5fc5> <02e0> <5fcd> <02e1> <5fd7> <02e2> <5fd9> <02e3> <5feb> <02e4> <5ff1> <02e5> <5ff5> <02e8> <600e> <02e9> <6015> <02ea> <601d> <02eb> <6025> <02ec> <6027> <02ed> <603b> <02ee> <6050> <02ef> <6062> <02f0> <6069> <02f1> <606f> <02f2> <6076> <02f3> <6089> <02f4> <60a0> <02f5> <60ac> <02f6> <60c5> <02f7> <60ca> <02f8> <60e7> <02f9> <60ef> <02fa> <60f3> <02fb> <6108> <02fc> <610f> <02fd> <611f> <02fe> <613f> <02ff> <614e> <0302> <6177> <0303> <61be> endbfchar
|
||||
100 beginbfchar
|
||||
<0304> <61c2> <0305> <61c8> <0306> <61d2> <0307> <6208> <030a> <6216> <030b> <6218> <030c> <621a> <030d> <6234> <0310> <624b> <0313> <6258> <0314> <6263> <0317> <6269> <0318> <6270> <0319> <6273> <031a> <6276> <031b> <6279> <031f> <628a> <0320> <6291> <0325> <629b> <0326> <62a2> <0329> <62b1> <032a> <62b5> <032b> <62b9> <032c> <62bd> <032d> <62c2> <032e> <62c5> <032f> <62c9> <0334> <62db> <0335> <62df> <0336> <62e5> <0339> <62ec> <033a> <62ff> <033d> <6307> <033e> <6309> <033f> <6311> <0342> <631d> <0343> <6325> <0344> <6328> <0345> <632a> <0346> <633d> <0347> <634e> <0348> <6350> <0349> <6355> <034a> <635f> <034b> <6362> <034c> <636e> <034d> <6377> <034e> <6388> <034f> <638c> <0350> <6392> <0351> <6398> <0352> <63a0> <0353> <63a2> <0354> <63a5> <0357> <63aa> <035a> <63d2> <035b> <63e1> <035c> <63ed> <035d> <63f4> <035e> <641c> <035f> <641e> <0360> <642d> <0361> <6458> <0362> <6469> <0363> <64a4> <0364> <64ad> <0365> <64bc> <0366> <64cd> <0367> <652f> <0368> <6536> <0369> <6539> <036a> <653b> <036d> <6545> <036e> <6548> <036f> <6551> <0370> <6559> <0371> <6563> <0372> <6566> <0373> <656c> <0374> <6570> <0375> <6574> <0376> <6587> <0377> <6597> <0378> <6599> <0379> <659f> <037b> <65a5> <037c> <65ad> <037f> <65b9> <0380> <65bd> <0381> <65c5> <0382> <65cb> <0383> <65cf> <0384> <65d7> <0385> <65e0> <0386> <65e2> <038c> <65f1> <038f> <65fa> <0390> <6606> <0391> <660e> <0392> <6613> endbfchar
|
||||
100 beginbfchar
|
||||
<0395> <662f> <0396> <663e> <0399> <667a> <039a> <6682> <039b> <66b4> <039c> <66dd> <039d> <66f2> <039e> <66f4> <039f> <66fc> <03a5> <670d> <03a6> <6717> <03a7> <671b> <03a8> <671f> <03a9> <6728> <03ae> <672f> <03af> <6734> <03b0> <673a> <03b1> <6740> <03b6> <675f> <03b7> <6761> <03b8> <6765> <03b9> <6770> <03ba> <677e> <03bb> <6781> <03bc> <6784> <03bd> <6790> <03be> <6797> <03bf> <679c> <03c0> <67af> <03c1> <67b6> <03c4> <67d3> <03c5> <67e5> <03c6> <6807> <03c7> <6811> <03c8> <6816> <03c9> <6821> <03cd> <683c> <03ce> <6848> <03cf> <6851> <03d0> <6863> <03d1> <6886> <03d2> <68a2> <03d3> <68c0> <03d4> <68da> <03d5> <68ee> <03d6> <690d> <03d7> <695a> <03d8> <6982> <03d9> <6a0a> <03da> <6a21> <03df> <6b27> <03e0> <6b32> <03e1> <6b3e> <03ea> <6b96> <03eb> <6bb5> <03ec> <6bc1> <03ed> <6bcb> <03ee> <6bcd> <03ef> <6bcf> <03f0> <6bd2> <03f3> <6bd7> <03f4> <6bdb> <03f5> <6beb> <03f6> <6c0f> <03f7> <6c11> <03f8> <6c14> <03f9> <6c1b> <03fa> <6c27> <03fd> <6c34> <03fe> <6c38> <03ff> <6c42> <0400> <6c47> <0401> <6c50> <0402> <6c57> <0403> <6c5f> <0404> <6c61> <0405> <6c7e> <0406> <6c89> <0407> <6c99> <0408> <6c9f> <0409> <6ca1> <040a> <6caa> <040b> <6cb3> <040c> <6cb9> <0411> <6cc4> <0412> <6cca> <0413> <6cd5> <0414> <6cdb> <0415> <6ce2> <0416> <6ce5> <0417> <6ce8> <0418> <6cf0> <0419> <6cfb> <041a> <6cfd> <041b> <6d01> <041c> <6d0b> <041d> <6d1b> <041e> <6d2a> <041f> <6d32> <0420> <6d3b> endbfchar
|
||||
100 beginbfchar
|
||||
<0423> <6d41> <0424> <6d46> <0427> <6d4e> <0428> <6d53> <0429> <6d66> <042a> <6d6a> <042b> <6d6e> <042c> <6d77> <042f> <6d95> <0430> <6da8> <0431> <6db2> <0432> <6dc0> <0433> <6dc6> <0434> <6de1> <0435> <6df1> <0436> <6df7> <0437> <6dfb> <0438> <6e05> <0439> <6e10> <043a> <6e14> <043d> <6e23> <043e> <6e29> <043f> <6e2f> <0440> <6e38> <0441> <6e56> <0442> <6e58> <0443> <6e7f> <0444> <6e89> <0445> <6e90> <0446> <6ecb> <0447> <6ee1> <0448> <6ee8> <0449> <6f0f> <044a> <6f14> <044b> <6f20> <044c> <6f2b> <044d> <6f5c> <044e> <6f6e> <044f> <6f84> <0450> <6fb3> <0451> <6fc0> <0452> <704c> <0453> <706b> <0454> <706d> <0455> <7070> <0456> <7075> <0459> <70ad> <045c> <70bc> <045f> <70c8> <0460> <70e7> <0461> <70ed> <0462> <711a> <0463> <7136> <0464> <7164> <0465> <7167> <0466> <719f> <0467> <71c3> <0468> <71e5> <0469> <7206> <046a> <722a> <046b> <722c> <046c> <7231> <046f> <7259> <0470> <725b> <0471> <7261> <0472> <7267> <0473> <7269> <0474> <7272> <0475> <7275> <0478> <72af> <0479> <72b6> <047a> <72de> <047d> <730e> <047e> <731b> <047f> <732e> <0480> <736d> <0481> <7387> <0482> <738b> <0483> <7396> <0484> <73ab> <0487> <73cd> <0488> <73d0> <0489> <73ed> <048a> <7403> <048b> <7406> <048c> <745e> <048d> <74a7> <048e> <74dc> <048f> <74e3> <0490> <74e6> <0491> <751a> <0492> <751f> <0493> <7528> <0496> <7533> <0497> <7535> <049a> <754c> <049b> <7559> <049c> <755c> <049d> <7565> <049e> <7574> endbfchar
|
||||
100 beginbfchar
|
||||
<049f> <7578> <04a0> <7591> <04a1> <75ab> <04a2> <75be> <04a3> <75c5> <04a4> <75c7> <04a5> <75d8> <04a6> <75db> <04a7> <764c> <04a8> <767b> <04ab> <7684> <04ac> <7687> <04ad> <76bf> <04ae> <76c6> <04af> <76ca> <04b2> <76d6> <04b3> <76d8> <04b4> <76df> <04b5> <76ee> <04b6> <76f2> <04b7> <76f4> <04b8> <76f8> <04b9> <76fe> <04ba> <770b> <04bb> <771f> <04bc> <773c> <04bd> <7740> <04be> <7763> <04bf> <77db> <04c0> <77e5> <04c1> <77ed> <04c2> <77ff> <04c3> <7801> <04c4> <780d> <04c5> <7814> <04c6> <7834> <04c7> <7838> <04c8> <7840> <04c9> <7845> <04cc> <786e> <04cd> <7877> <04ce> <788d> <04cf> <7891> <04d0> <78b0> <04d1> <78b3> <04d2> <78cb> <04d3> <793a> <04d4> <793e> <04d7> <7965> <04d8> <7968> <04d9> <7978> <04da> <7981> <04db> <798f> <04dc> <79bb> <04dd> <79c1> <04de> <79cd> <04df> <79d1> <04e0> <79d8> <04e3> <79fb> <04e4> <7a00> <04e5> <7a0b> <04e6> <7a0e> <04e7> <7a33> <04e8> <7a46> <04eb> <7a7a> <04ec> <7a81> <04ed> <7a84> <04ee> <7a92> <04ef> <7a96> <04f0> <7acb> <04f1> <7ad9> <04f5> <7ae5> <04f6> <7aed> <04f7> <7aef> <04f8> <7af9> <04f9> <7b14> <04fa> <7b26> <04fb> <7b2c> <04fc> <7b49> <04fd> <7b4f> <04fe> <7b51> <04ff> <7b54> <0500> <7b56> <0501> <7b79> <0502> <7b7e> <0503> <7b80> <0504> <7b97> <0505> <7ba1> <0506> <7c73> <0507> <7c7b> <0508> <7c92> <0509> <7c95> <050a> <7cae> <050b> <7cbe> <050c> <7cca> <050d> <7cfb> <050e> <7d20> <050f> <7d22> <0510> <7d27> <0511> <7d2f> endbfchar
|
||||
100 beginbfchar
|
||||
<0512> <7e41> <0513> <7e82> <0514> <7ea0> <0517> <7eaa> <0518> <7eaf> <051b> <7eb5> <051c> <7ebd> <051d> <7ebf> <0523> <7ecd> <0524> <7ecf> <0525> <7ed3> <052b> <7edf> <052c> <7ee7> <052d> <7ee9> <052e> <7eed> <052f> <7ef4> <0530> <7efc> <0533> <7f16> <0534> <7f29> <0535> <7f3a> <0536> <7f51> <0537> <7f57> <0538> <7f6e> <0539> <7f72> <053a> <7f8e> <053b> <7fa4> <053c> <7fc5> <053d> <7ff0> <053e> <7ffb> <053f> <8001> <0540> <8003> <0541> <8005> <0544> <8017> <0545> <8033> <0548> <8054> <0549> <8058> <054a> <805a> <054b> <806a> <054c> <8083> <054d> <8089> <054e> <8098> <054f> <809b> <0550> <80a5> <0551> <80a9> <0552> <80af> <0553> <80b2> <0554> <80c1> <0555> <80cc> <0556> <80ce> <0557> <80da> <0558> <80dc> <0559> <80fd> <055a> <8106> <055b> <811a> <055c> <8131> <055d> <814a> <055e> <8155> <055f> <8179> <0560> <818a> <0561> <818f> <0562> <81ea> <0563> <81ed> <0566> <8206> <0567> <820c> <0568> <8212> <0569> <821f> <056a> <822a> <056b> <822c> <056c> <8236> <056d> <8239> <056e> <8247> <0571> <8272> <0572> <827a> <0573> <8282> <0574> <82a6> <0575> <82ac> <0576> <82af> <0577> <82c7> <0578> <82cf> <057b> <82ef> <057c> <82f1> <057d> <8303> <057e> <8328> <057f> <8349> <0580> <835a> <0581> <8363> <0582> <836f> <0583> <8377> <0584> <8392> <0585> <83ab> <0588> <83b7> <0589> <83c7> <058a> <83cc> <058b> <83f2> <058c> <8404> <058d> <841d> <058e> <8425> <058f> <8428> <0590> <843d> <0591> <8461> endbfchar
|
||||
100 beginbfchar
|
||||
<0592> <848b> <0593> <8499> <0594> <84c4> <0595> <84dd> <0596> <8584> <0597> <85c9> <0598> <85cf> <0599> <8651> <059a> <866b> <059b> <867d> <059c> <8680> <059d> <8682> <059e> <86cb> <059f> <8718> <05a0> <8757> <05a1> <878d> <05a2> <87f9> <05a3> <8840> <05a4> <884c> <05a5> <8854> <05a6> <8857> <05a7> <8861> <05a8> <8863> <05a9> <8865> <05aa> <8868> <05ab> <8877> <05ac> <8896> <05ad> <88ab> <05ae> <88c1> <05af> <88f4> <05b0> <8902> <05b1> <8925> <05b2> <8944> <05b3> <897f> <05b4> <8981> <05b5> <8986> <05b8> <89c4> <05b9> <89c6> <05bc> <89e3> <05bd> <89e6> <05be> <8a00> <05bf> <8b66> <05c0> <8b6c> <05c3> <8ba4> <05ca> <8bb2> <05cb> <8bb8> <05cc> <8bba> <05cf> <8bc1> <05d0> <8bc4> <05d1> <8bc6> <05d2> <8bca> <05d3> <8bcd> <05d4> <8bd1> <05d5> <8bd5> <05d6> <8bd7> <05d7> <8bda> <05d8> <8bdd> <05d9> <8be2> <05de> <8bef> <05e1> <8bf4> <05e4> <8bfa> <05e5> <8bfe> <05e6> <8c03> <05e7> <8c08> <05e8> <8c0b> <05e9> <8c10> <05ea> <8c13> <05eb> <8c22> <05ec> <8c28> <05ed> <8c34> <05ee> <8c37> <05ef> <8c41> <05f0> <8c61> <05f1> <8c6a> <05f2> <8d1d> <05f3> <8d1f> <05f7> <8d25> <05fe> <8d35> <0602> <8d44> <0603> <8d4b> <0604> <8d4f> <0605> <8d54> <0606> <8d56> <0607> <8d5b> <0608> <8d5e> <0609> <8d66> <060a> <8d70> <060b> <8d75> <060c> <8d77> <060d> <8d85> <0610> <8da3> <0611> <8db3> <0612> <8dc3> <0613> <8dd1> <0614> <8ddd> <0615> <8def> <0616> <8e0a> <0617> <8eab> <0618> <8f67> <0619> <8f6c> endbfchar
|
||||
100 beginbfchar
|
||||
<061a> <8f7b> <061b> <8f7d> <061c> <8f83> <061d> <8f85> <0620> <8f93> <0621> <8f96> <0622> <8f9b> <0625> <8fb1> <0626> <8fb9> <0627> <8fbe> <0628> <8fc1> <062b> <8fc7> <062c> <8fce> <062f> <8fd4> <0639> <8ff0> <063a> <8ff7> <063b> <8ff9> <063c> <8ffd> <0643> <9010> <0644> <9012> <0645> <9014> <064a> <903b> <064b> <9047> <064c> <904d> <064d> <9053> <064e> <9057> <064f> <9063> <0650> <9065> <0653> <9075> <0656> <90a3> <0657> <90a6> <065a> <90b1> <065b> <90bb> <065c> <90d1> <065f> <90fd> <0663> <9192> <0664> <919a> <0665> <91c7> <0666> <91ca> <066b> <91d1> <066c> <9274> <066d> <9488> <066e> <94ae> <066f> <94b1> <0670> <94b3> <0673> <94f2> <0674> <94f6> <0675> <94fe> <0676> <9500> <0677> <9504> <0678> <9519> <0679> <9521> <067a> <9528> <067b> <952e> <067c> <953a> <067d> <957f> <067e> <95e8> <0681> <95f4> <0682> <95f7> <0683> <95fb> <0684> <95fd> <0685> <9602> <0686> <9610> <0687> <9614> <0688> <9632> <0689> <9634> <068a> <9636> <068b> <963b> <068c> <963f> <0690> <9650> <0691> <9662> <0692> <9664> <0693> <9669> <0696> <9686> <0697> <968f> <0698> <9694> <0699> <969c> <069a> <96be> <069b> <96c0> <069e> <96e8> <06a1> <9700> <06a2> <9707> <06a3> <970d> <06a4> <9713> <06a5> <9732> <06a6> <9752> <06a7> <9759> <06a8> <975e> <06a9> <9760> <06aa> <9762> <06ab> <978b> <06ac> <97e6> <06ad> <97e9> <06ae> <97f3> <06af> <9877> <06b3> <987e> <06b4> <9881> <06b5> <9884> <06b8> <9898> <06b9> <989d> endbfchar
|
||||
24 beginbfchar
|
||||
<06ba> <98a0> <06bb> <98ce> <06be> <9996> <06bf> <9a6c> <06c0> <9a71> <06c1> <9a7b> <06c2> <9a8c> <06c3> <9aa4> <06c4> <9aa8> <06c5> <9ad8> <06c6> <9c7c> <06c7> <9c81> <06c8> <9c9c> <06c9> <9ca4> <06ca> <9cb8> <06cb> <9cc3> <06cc> <9cd6> <06cd> <9e64> <06ce> <9ea6> <06cf> <9ece> <06d0> <9f13> <06d1> <9f3b> <06d2> <9f84> <06d3> <9f9a> endbfchar
|
||||
endcmap
|
||||
CMapName
|
||||
currentdict
|
||||
/CMap defineresource
|
||||
pop
|
||||
end
|
||||
end
|
||||
""";
|
||||
|
||||
var input = StringBytesTestConverter.Convert(cmap, false);
|
||||
|
||||
var result = cMapParser.Parse(input.Bytes);
|
||||
|
||||
Assert.Equal("Adobe", result.Info.Registry);
|
||||
Assert.Equal("GB1", result.Info.Ordering);
|
||||
Assert.Equal(2, result.Info.Supplement);
|
||||
|
||||
Assert.Equal("Adobe-GB1-2", result.Name);
|
||||
Assert.Equal(2, result.Type);
|
||||
}
|
||||
|
||||
private static byte[] ReadResourceBytes(string name)
|
||||
{
|
||||
using (var resource = typeof(CMapParser).Assembly.GetManifestResourceStream(name))
|
||||
|
||||
@@ -68,12 +68,20 @@
|
||||
[Fact]
|
||||
public void ReflectionGraphicsStateOperationFactoryKnowsAllOperations()
|
||||
{
|
||||
var operationsField = typeof(ReflectionGraphicsStateOperationFactory).GetField("operations", BindingFlags.NonPublic | BindingFlags.Static);
|
||||
var operationDictionary = operationsField.GetValue(null) as IReadOnlyDictionary<string, Type>;
|
||||
Assert.NotNull(operationDictionary);
|
||||
var operationsField = typeof(ReflectionGraphicsStateOperationFactory)
|
||||
.GetField("Operations", BindingFlags.NonPublic | BindingFlags.Static);
|
||||
|
||||
Assert.NotNull(operationsField);
|
||||
|
||||
var operationDictionaryRaw = operationsField.GetValue(null) as IReadOnlyDictionary<string, Type>;
|
||||
|
||||
Assert.NotNull(operationDictionaryRaw);
|
||||
|
||||
var operationDictionary = new Dictionary<string, Type>(operationDictionaryRaw!.ToDictionary(x => x.Key, x => x.Value));
|
||||
|
||||
var allOperations = GetOperationTypes();
|
||||
Assert.Equal(allOperations.Count(), operationDictionary.Count);
|
||||
|
||||
Assert.Equal(allOperations.Count, operationDictionary.Count);
|
||||
|
||||
var mapped = allOperations.Select(o =>
|
||||
{
|
||||
@@ -84,14 +92,14 @@
|
||||
Assert.Equivalent(operationDictionary, mapped, strict: true);
|
||||
}
|
||||
|
||||
private static IEnumerable<Type> GetOperationTypes()
|
||||
private static IReadOnlyList<Type> GetOperationTypes()
|
||||
{
|
||||
var assembly = Assembly.GetAssembly(typeof(IGraphicsStateOperation));
|
||||
|
||||
var operationTypes = assembly.GetTypes().Where(x => typeof(IGraphicsStateOperation).IsAssignableFrom(x)
|
||||
&& !x.IsInterface);
|
||||
|
||||
return operationTypes;
|
||||
return operationTypes.ToList();
|
||||
}
|
||||
|
||||
private static object[] GetConstructorParameters(ParameterInfo[] parameters)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
@@ -103,6 +103,30 @@
|
||||
Assert.True(ImageHelpers.ImagesAreEqual(LoadImage("3x3.png"), bytes));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanExtractPngFromPdfWithIndexedImageData8bpc()
|
||||
{
|
||||
var documetFolder = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "Images", "Files"));
|
||||
|
||||
var pdfPath = Path.Combine(documetFolder, "Pdf", "indexed-png-with-mask.pdf");
|
||||
|
||||
using (PdfDocument document = PdfDocument.Open(pdfPath))
|
||||
{
|
||||
var page = document.GetPage(1);
|
||||
|
||||
var img = page.GetImages().First();
|
||||
|
||||
var result = img.TryGetPng(out var bytes);
|
||||
|
||||
if (result)
|
||||
{
|
||||
var outputPath = Path.Combine(documetFolder, "Pdf", "indexed-png-with-mask.png");
|
||||
|
||||
File.WriteAllBytes(outputPath, bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanGeneratePngFromIndexedImageData1bpc()
|
||||
{
|
||||
@@ -148,11 +172,12 @@
|
||||
var decodedBytes = ImageHelpers.LoadFileBytes("ccittfax-decoded.bin");
|
||||
var image = new TestPdfImage
|
||||
{
|
||||
ColorSpaceDetails = IndexedColorSpaceDetails.Stencil(DeviceGrayColorSpaceDetails.Instance, new[] { 1.0, 0 }),
|
||||
ColorSpaceDetails = IndexedColorSpaceDetails.Stencil(DeviceGrayColorSpaceDetails.Instance),
|
||||
DecodedBytes = decodedBytes,
|
||||
WidthInSamples = 1800,
|
||||
HeightInSamples = 3113,
|
||||
BitsPerComponent = 1
|
||||
BitsPerComponent = 1,
|
||||
Decode = [1.0, 0]
|
||||
};
|
||||
|
||||
Assert.True(PngFromPdfImageFactory.TryGenerate(image, out var bytes));
|
||||
|
||||
@@ -7,6 +7,27 @@
|
||||
|
||||
public class GithubIssuesTests
|
||||
{
|
||||
[Fact]
|
||||
public void Issue1096()
|
||||
{
|
||||
// Ensure no StackOverflowException
|
||||
// (already fixed by https://github.com/UglyToad/PdfPig/pull/1097)
|
||||
|
||||
var path = IntegrationHelpers.GetSpecificTestDocumentPath("issue_1096.pdf");
|
||||
|
||||
using (var document = PdfDocument.Open(path, new ParsingOptions() { UseLenientParsing = true }))
|
||||
{
|
||||
for (int p = 1; p <= document.NumberOfPages; p++)
|
||||
{
|
||||
var page = document.GetPage(p);
|
||||
foreach (var image in page.GetImages())
|
||||
{
|
||||
Assert.NotNull(image);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Issue1067()
|
||||
{
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
Assert.Equal(792, page.Height); // Due to cropping
|
||||
var minX = page.Letters.Select(l => l.GlyphRectangle.Left).Min();
|
||||
var maxX = page.Letters.Select(l => l.GlyphRectangle.Right).Max();
|
||||
Assert.Equal(72, minX, 0); // If cropping is not applied correctly, these values will be off
|
||||
Assert.Equal(74, minX, 0); // If cropping is not applied correctly, these values will be off
|
||||
Assert.Equal(540, maxX, 0); // If cropping is not applied correctly, these values will be off
|
||||
// The page is cropped at
|
||||
Assert.NotNull(page.Content);
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,128 @@
|
||||
namespace UglyToad.PdfPig.Tests.Parser.FileStructure;
|
||||
|
||||
using PdfPig.Parser.FileStructure;
|
||||
using PdfPig.Tokenization.Scanner;
|
||||
using PdfPig.Tokens;
|
||||
|
||||
public class XrefOffsetValidatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void FindsTwoXrefs()
|
||||
{
|
||||
var content =
|
||||
"""
|
||||
%PDF-1.7
|
||||
%âãÏÓ
|
||||
5 0 obj
|
||||
<</Filter/FlateDecode/Length 66>>stream
|
||||
abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz
|
||||
endstream
|
||||
endobj
|
||||
xref0 1
|
||||
0000000000 65535 f
|
||||
0000000500 00000 n
|
||||
4 0 obj
|
||||
<</Contents 5 0 R/MediaBox[0 0 595 842]/Parent 2 0 R/Resources<</Font<</F1 6 0 R>>>>/TrimBox[0 0 595 842]/Type/Page>>
|
||||
endobj
|
||||
xref
|
||||
0 3
|
||||
0000000000 65535 f
|
||||
0000000443 00000 n
|
||||
0000000576 00000 n
|
||||
trailer
|
||||
<< /Size 100 /Root 100 >>
|
||||
startxref
|
||||
9000
|
||||
%%EOF
|
||||
""";
|
||||
|
||||
if (Environment.NewLine == "\n")
|
||||
{
|
||||
content = content.Replace("\n", "\r\n");
|
||||
}
|
||||
|
||||
var ib = StringBytesTestConverter.Convert(content, false);
|
||||
|
||||
var results = XrefOffsetValidator.BruteForceSearchForTables(ib.Bytes);
|
||||
|
||||
Assert.Contains(144, results);
|
||||
Assert.Contains(331, results);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindsXrefsFromRealFileTruncated()
|
||||
{
|
||||
var content =
|
||||
"""
|
||||
%PDF-1.5
|
||||
%âãÏÓ
|
||||
5 0 obj <</Linearized 1/L 4631/O 8/E 1125/N 1/T 4485/H [ 436 129]>>
|
||||
endobj
|
||||
xref
|
||||
5 7
|
||||
0000000016 00000 n
|
||||
0000000565 00000 n
|
||||
0000000436 00000 n
|
||||
0000000639 00000 n
|
||||
0000000796 00000 n
|
||||
0000001001 00000 n
|
||||
0000001098 00000 n
|
||||
trailer
|
||||
<</Size 12/Prev 4475/Root 6 0 R/Info 4 0 R/ID[<2c9c3edf9641f1459e947e7f933f6da0><2c9c3edf9641f1459e947e7f933f6da0>]>>
|
||||
startxref
|
||||
0
|
||||
%%EOF
|
||||
7 0 obj<</Length 52/Filter/FlateDecode/O 67/S 38>>stream
|
||||
3842973893927327893237832738923732923782987348
|
||||
endstream
|
||||
endobj
|
||||
6 0 obj<</Pages 2 0 R/Outlines 1 0 R/Type/Catalog/Metadata 3 0 R>>
|
||||
endobj
|
||||
8 0 obj<</Contents 9 0 R/Type/Page/Parent 2 0 R/Rotate 0/MediaBox[0 0 612 792]/CropBox[0 0 612 792]/Resources<</Font<</F1 10 0 R>>/ProcSet 11 0 R>>>>
|
||||
endobj
|
||||
9 0 obj<</Length 137/Filter/FlateDecode>>stream
|
||||
abajsgiwgbkeeuuehxh9x3oihx2h802chc280h2082x
|
||||
endstream
|
||||
endobj
|
||||
10 0 obj<</Type/Font/Name/F1/Encoding/MacRomanEncoding/BaseFont/Helvetica/Subtype/Type1>>
|
||||
endobj
|
||||
11 0 obj[/PDF/Text]
|
||||
endobj
|
||||
1 0 obj<</Count 0/Type/Outlines>>
|
||||
endobj
|
||||
2 0 obj<</Count 1/Kids[8 0 R]/Type/Pages>>
|
||||
endobj
|
||||
4 0 obj<</ModDate(D:20070213222810-05'00')/CreationDate(D:20070213222810-05'00')>>
|
||||
endobj
|
||||
xref
|
||||
0 5
|
||||
0000000000 65535 f
|
||||
0000001125 00000 n
|
||||
0000001166 00000 n
|
||||
0000001216 00000 n
|
||||
0000004385 00000 n
|
||||
trailer
|
||||
<</Size 5>>
|
||||
startxref
|
||||
116
|
||||
%%EOF
|
||||
""";
|
||||
|
||||
if (Environment.NewLine == "\n")
|
||||
{
|
||||
content = content.Replace("\n", "\r\n");
|
||||
}
|
||||
|
||||
var ib = StringBytesTestConverter.Convert(content, false);
|
||||
|
||||
var results = XrefOffsetValidator.BruteForceSearchForTables(ib.Bytes);
|
||||
|
||||
Assert.Contains(98, results);
|
||||
Assert.Contains(1186, results);
|
||||
|
||||
ib.Bytes.Seek(98);
|
||||
var scanner = new CoreTokenScanner(ib.Bytes, false);
|
||||
scanner.MoveNext();
|
||||
Assert.Equal(scanner.CurrentToken, OperatorToken.Xref);
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -191,8 +191,76 @@ endobj";
|
||||
AssertCorrectToken<NameToken, string>(tokens[3], "F1");
|
||||
AssertCorrectToken<NumericToken, double>(tokens[4], 8);
|
||||
AssertCorrectToken<OperatorToken, string>(tokens[5], "Tf");
|
||||
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SkipsCommentsInStreams()
|
||||
{
|
||||
const string content =
|
||||
"""
|
||||
% 641 0 obj
|
||||
<<
|
||||
/Type /Encoding
|
||||
/Differences [16/quotedblleft/quotedblright 21/endash 27/ff/fi/fl/ffi 39/quoteright/parenleft/parenright 43/plus/comma/hyphen/period/slash/zero/one/two/three/four/five/six/seven/eight/nine/colon 64/at/A/B/C/D/E/F/G/H/I/J/K/L/M/N/O/P/Q/R/S/T/U/V/W/X/Y/Z/bracketleft 93/bracketright 97/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/braceleft 125/braceright 225/aacute 232/egrave/eacute 252/udieresis]
|
||||
>>
|
||||
% 315 0 obj
|
||||
<<
|
||||
/Type /Font
|
||||
/Subtype /Type1
|
||||
/BaseFont /IXNPPI+CMEX10
|
||||
/FontDescriptor 661 0 R
|
||||
/FirstChar 80
|
||||
/LastChar 88
|
||||
/Widths 644 0 R
|
||||
/ToUnicode 699 0 R
|
||||
>>
|
||||
% 306 0 obj
|
||||
<<
|
||||
/Type /Font
|
||||
/Subtype /Type1
|
||||
/BaseFont /MSNKTF+CMMI10
|
||||
/FontDescriptor 663 0 R
|
||||
/FirstChar 58
|
||||
/LastChar 119
|
||||
/Widths 651 0 R
|
||||
/ToUnicode 700 0 R
|
||||
>>
|
||||
""";
|
||||
|
||||
var tokens = new List<IToken>();
|
||||
|
||||
var scanner = new CoreTokenScanner(
|
||||
StringBytesTestConverter.Convert(content, false).Bytes,
|
||||
true,
|
||||
isStream: true);
|
||||
|
||||
while (scanner.MoveNext())
|
||||
{
|
||||
tokens.Add(scanner.CurrentToken);
|
||||
}
|
||||
|
||||
Assert.Equal(3, tokens.Count);
|
||||
|
||||
Assert.All(tokens, x => Assert.IsType<DictionaryToken>(x));
|
||||
|
||||
tokens.Clear();
|
||||
|
||||
var nonStreamScanner = new CoreTokenScanner(
|
||||
StringBytesTestConverter.Convert(content, false).Bytes,
|
||||
true,
|
||||
isStream: false);
|
||||
|
||||
while (nonStreamScanner.MoveNext())
|
||||
{
|
||||
tokens.Add(nonStreamScanner.CurrentToken);
|
||||
}
|
||||
|
||||
Assert.Equal(6, tokens.Count);
|
||||
|
||||
Assert.Equal(3, tokens.OfType<CommentToken>().Count());
|
||||
Assert.Equal(3, tokens.OfType<DictionaryToken>().Count());
|
||||
}
|
||||
|
||||
private static void AssertCorrectToken<T, TData>(IToken token, TData expected) where T : IDataToken<TData>
|
||||
{
|
||||
var cast = Assert.IsType<T>(token);
|
||||
|
||||
47
src/UglyToad.PdfPig.Tests/Util/CircularByteBufferTests.cs
Normal file
47
src/UglyToad.PdfPig.Tests/Util/CircularByteBufferTests.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
namespace UglyToad.PdfPig.Tests.Util;
|
||||
|
||||
using PdfPig.Util;
|
||||
using System;
|
||||
|
||||
public class CircularByteBufferTests
|
||||
{
|
||||
[Fact]
|
||||
public void CanExceedCapacity()
|
||||
{
|
||||
var buffer = new CircularByteBuffer(3);
|
||||
|
||||
var input = "123456"u8;
|
||||
for (var i = 0; i < input.Length; i++)
|
||||
{
|
||||
buffer.Add(input[i]);
|
||||
}
|
||||
|
||||
Assert.True(buffer.IsCurrentlyEqual("456"));
|
||||
|
||||
Assert.True("456"u8.SequenceEqual(buffer.AsSpan()));
|
||||
|
||||
Assert.True(buffer.EndsWith("6"));
|
||||
Assert.True(buffer.EndsWith("56"));
|
||||
Assert.True(buffer.EndsWith("456"));
|
||||
Assert.False(buffer.EndsWith("3456"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanUndershootCapacity()
|
||||
{
|
||||
var buffer = new CircularByteBuffer(9);
|
||||
|
||||
var input = "123456"u8;
|
||||
for (var i = 0; i < input.Length; i++)
|
||||
{
|
||||
buffer.Add(input[i]);
|
||||
}
|
||||
|
||||
Assert.True(buffer.IsCurrentlyEqual("123456"));
|
||||
|
||||
Assert.True(buffer.EndsWith("3456"));
|
||||
Assert.False(buffer.EndsWith("123"));
|
||||
|
||||
Assert.True("123456"u8.SequenceEqual(buffer.AsSpan()));
|
||||
}
|
||||
}
|
||||
@@ -129,6 +129,34 @@
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanFastAddPageAndStripAllAnnots()
|
||||
{
|
||||
var first = IntegrationHelpers.GetDocumentPath("outline.pdf");
|
||||
var contents = File.ReadAllBytes(first);
|
||||
|
||||
byte[] results = null;
|
||||
using (var existing = PdfDocument.Open(contents, ParsingOptions.LenientParsingOff))
|
||||
using (var output = new PdfDocumentBuilder())
|
||||
{
|
||||
output.AddPage(existing, 1, new PdfDocumentBuilder.AddPageOptions
|
||||
{
|
||||
KeepAnnotations = false
|
||||
});
|
||||
results = output.Build();
|
||||
var pg = existing.GetPage(1);
|
||||
var annots = pg.ExperimentalAccess.GetAnnotations().ToList();
|
||||
Assert.NotEmpty(annots);
|
||||
}
|
||||
|
||||
using (var rewritten = PdfDocument.Open(results, ParsingOptions.LenientParsingOff))
|
||||
{
|
||||
var pg = rewritten.GetPage(1);
|
||||
var annots = pg.ExperimentalAccess.GetAnnotations().ToList();
|
||||
Assert.Empty(annots);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanReadSingleBlankPage()
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,14 @@
|
||||
|
||||
private bool hasBytePreRead;
|
||||
private bool isInInlineImage;
|
||||
/// <summary>
|
||||
/// '%' only identifies comments outside of PDF streams and strings, inside these we should ignore it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// PDFBox skips all of a line following a comment character inside streams, see:
|
||||
/// https://github.com/apache/pdfbox/blob/0e1c42dace1c3a2631d5309f662de5628b80fda6/pdfbox/src/main/java/org/apache/pdfbox/pdfparser/BaseParser.java#L1319
|
||||
/// </remarks>
|
||||
private readonly bool isStream;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="CoreTokenScanner"/> from the input.
|
||||
@@ -52,7 +60,8 @@
|
||||
bool usePdfDocEncoding,
|
||||
ScannerScope scope = ScannerScope.None,
|
||||
IReadOnlyDictionary<NameToken, IReadOnlyList<NameToken>> namedDictionaryRequiredKeys = null,
|
||||
bool useLenientParsing = false)
|
||||
bool useLenientParsing = false,
|
||||
bool isStream = false)
|
||||
{
|
||||
this.inputBytes = inputBytes ?? throw new ArgumentNullException(nameof(inputBytes));
|
||||
this.usePdfDocEncoding = usePdfDocEncoding;
|
||||
@@ -62,6 +71,7 @@
|
||||
this.scope = scope;
|
||||
this.namedDictionaryRequiredKeys = namedDictionaryRequiredKeys;
|
||||
this.useLenientParsing = useLenientParsing;
|
||||
this.isStream = isStream;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -94,6 +104,7 @@
|
||||
{
|
||||
var endAngleBracesRead = 0;
|
||||
|
||||
bool isSkippingLine = false;
|
||||
bool isSkippingSymbol = false;
|
||||
while ((hasBytePreRead && !inputBytes.IsAtEnd()) || inputBytes.MoveNext())
|
||||
{
|
||||
@@ -101,6 +112,17 @@
|
||||
var currentByte = inputBytes.CurrentByte;
|
||||
var c = (char) currentByte;
|
||||
|
||||
if (isSkippingLine)
|
||||
{
|
||||
if (ReadHelper.IsEndOfLine(c))
|
||||
{
|
||||
isSkippingLine = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
ITokenizer tokenizer = null;
|
||||
foreach (var customTokenizer in customTokenizers)
|
||||
{
|
||||
@@ -119,6 +141,12 @@
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentByte == (byte)'%' && isStream)
|
||||
{
|
||||
isSkippingLine = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we failed to read the symbol for whatever reason we pass over it.
|
||||
if (isSkippingSymbol && c != '>')
|
||||
{
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -10,13 +10,7 @@
|
||||
/// </summary>
|
||||
public static bool NeedsReverseDecode(this IPdfImage pdfImage)
|
||||
{
|
||||
if (pdfImage.ColorSpaceDetails?.IsStencil == true)
|
||||
{
|
||||
// Stencil color space already takes care of reversing.
|
||||
return false;
|
||||
}
|
||||
|
||||
return pdfImage.Decode.Count >= 2 && pdfImage.Decode[0] == 1 && pdfImage.Decode[1] == 0;
|
||||
return pdfImage.Decode?.Count >= 2 && pdfImage.Decode[0] == 1 && pdfImage.Decode[1] == 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{
|
||||
using System;
|
||||
using Core;
|
||||
using System.Text;
|
||||
using Tokens;
|
||||
|
||||
/// <summary>
|
||||
@@ -13,7 +14,7 @@
|
||||
private const byte Offset = (byte)'!';
|
||||
private const byte EmptyCharacterPadding = (byte)'u';
|
||||
|
||||
private static ReadOnlySpan<byte> EndOfDataBytes => [(byte)'~', (byte)'>'];
|
||||
private static ReadOnlySpan<byte> EndOfDataBytes => "~>"u8;
|
||||
|
||||
private static readonly int[] PowerByIndex =
|
||||
[
|
||||
@@ -52,7 +53,7 @@
|
||||
{
|
||||
if (index > 0)
|
||||
{
|
||||
WriteData(asciiBuffer, index, writer);
|
||||
WriteData(asciiBuffer, index, writer, true);
|
||||
}
|
||||
|
||||
index = 0;
|
||||
@@ -88,24 +89,36 @@
|
||||
|
||||
if (index == 5)
|
||||
{
|
||||
WriteData(asciiBuffer, index, writer);
|
||||
WriteData(asciiBuffer, index, writer, false);
|
||||
index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (index > 0)
|
||||
{
|
||||
WriteData(asciiBuffer, index, writer);
|
||||
WriteData(asciiBuffer, index, writer, true);
|
||||
}
|
||||
|
||||
return writer.WrittenMemory.ToArray();
|
||||
}
|
||||
|
||||
private static void WriteData(Span<byte> ascii, int index, ArrayPoolBufferWriter<byte> writer)
|
||||
private static void WriteData(
|
||||
Span<byte> ascii,
|
||||
int index,
|
||||
ArrayPoolBufferWriter<byte> writer,
|
||||
bool isAtEnd)
|
||||
{
|
||||
if (index < 2)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(index), "Cannot convert a block padded by 4 'u' characters.");
|
||||
if (isAtEnd)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var bufferTxt = Encoding.ASCII.GetString(ascii);
|
||||
var soFar = Encoding.ASCII.GetString(writer.GetSpan());
|
||||
throw new ArgumentOutOfRangeException(nameof(index),
|
||||
$"Cannot convert a this block because we're not at the end of the stream. Chunk: '{bufferTxt}'. Content: '{soFar}'");
|
||||
}
|
||||
|
||||
// Write any empty padding if the block ended early.
|
||||
|
||||
@@ -511,21 +511,22 @@
|
||||
startState.AlphaConstantNonStroking = 1.0;
|
||||
startState.AlphaConstantStroking = 1.0;
|
||||
|
||||
// TODO: the /CS colorspace of the transparency group should not affect the main colorspace, only the transparent imaging model.
|
||||
if (formGroupToken.TryGet(NameToken.Cs, PdfScanner, out NameToken? csNameToken))
|
||||
{
|
||||
startState.ColorSpaceContext!.SetNonStrokingColorspace(csNameToken);
|
||||
// startState.ColorSpaceContext!.SetNonStrokingColorspace(csNameToken);
|
||||
}
|
||||
else if (formGroupToken.TryGet(NameToken.Cs, PdfScanner, out ArrayToken? csArrayToken)
|
||||
&& csArrayToken.Length > 0)
|
||||
{
|
||||
if (csArrayToken.Data[0] is NameToken firstColorSpaceName)
|
||||
{
|
||||
startState.ColorSpaceContext!.SetNonStrokingColorspace(firstColorSpaceName, formGroupToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Invalid color space in Transparency Group XObjects.");
|
||||
}
|
||||
//if (csArrayToken.Data[0] is NameToken firstColorSpaceName)
|
||||
//{
|
||||
// startState.ColorSpaceContext!.SetNonStrokingColorspace(firstColorSpaceName, formGroupToken);
|
||||
//}
|
||||
//else
|
||||
//{
|
||||
// throw new InvalidOperationException("Invalid color space in Transparency Group XObjects.");
|
||||
//}
|
||||
}
|
||||
|
||||
bool isolated = false;
|
||||
|
||||
@@ -15,12 +15,6 @@
|
||||
/// </summary>
|
||||
public abstract class ColorSpaceDetails
|
||||
{
|
||||
/// <summary>
|
||||
/// Is the color space a stencil indexed color space.
|
||||
/// <para>Stencil color spaces take care of inverting colors based on the Decode array.</para>
|
||||
/// </summary>
|
||||
public bool IsStencil { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of the ColorSpace.
|
||||
/// </summary>
|
||||
@@ -45,11 +39,10 @@
|
||||
/// <summary>
|
||||
/// Create a new <see cref="ColorSpaceDetails"/>.
|
||||
/// </summary>
|
||||
protected internal ColorSpaceDetails(ColorSpace type, bool isStencil = false)
|
||||
protected internal ColorSpaceDetails(ColorSpace type)
|
||||
{
|
||||
Type = type;
|
||||
BaseType = type;
|
||||
IsStencil = isStencil;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -278,15 +271,12 @@
|
||||
private readonly ConcurrentDictionary<double, IColor> cache = new ConcurrentDictionary<double, IColor>();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a indexed color space useful for extracting stencil masks as black-and-white images,
|
||||
/// i.e. with a color palette of two colors (black and white). If the decode parameter array is
|
||||
/// [0, 1] it indicates that black is at index 0 in the color palette, whereas [1, 0] indicates
|
||||
/// that the black color is at index 1.
|
||||
/// Creates an indexed color space useful for extracting stencil masks as black-and-white images,
|
||||
/// i.e. with a color palette of two colors (black and white).
|
||||
/// </summary>
|
||||
internal static ColorSpaceDetails Stencil(ColorSpaceDetails colorSpaceDetails, double[] decode)
|
||||
internal static ColorSpaceDetails Stencil(ColorSpaceDetails colorSpaceDetails)
|
||||
{
|
||||
var blackIsOne = decode.Length >= 2 && decode[0] == 1 && decode[1] == 0;
|
||||
return new IndexedColorSpaceDetails(colorSpaceDetails, 1, blackIsOne ? [255, 0] : [0, 255], true);
|
||||
return new IndexedColorSpaceDetails(colorSpaceDetails, 1, [0, 255]);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -321,14 +311,7 @@
|
||||
/// Create a new <see cref="IndexedColorSpaceDetails"/>.
|
||||
/// </summary>
|
||||
public IndexedColorSpaceDetails(ColorSpaceDetails baseColorSpaceDetails, byte hiVal, byte[] colorTable)
|
||||
: this(baseColorSpaceDetails, hiVal, colorTable, false)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="IndexedColorSpaceDetails"/>.
|
||||
/// </summary>
|
||||
private IndexedColorSpaceDetails(ColorSpaceDetails baseColorSpaceDetails, byte hiVal, byte[] colorTable, bool isStencil)
|
||||
: base(ColorSpace.Indexed, isStencil)
|
||||
: base(ColorSpace.Indexed)
|
||||
{
|
||||
BaseColorSpace = baseColorSpaceDetails ?? throw new ArgumentNullException(nameof(baseColorSpaceDetails));
|
||||
HiVal = hiVal;
|
||||
|
||||
@@ -1,65 +1,64 @@
|
||||
namespace UglyToad.PdfPig.Graphics.Operations.InlineImages
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Tokens;
|
||||
using UglyToad.PdfPig.Writer;
|
||||
namespace UglyToad.PdfPig.Graphics.Operations.InlineImages;
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Begin the image data for an inline image object.
|
||||
/// </summary>
|
||||
public class BeginInlineImageData : IGraphicsStateOperation
|
||||
{
|
||||
/// <summary>
|
||||
/// The symbol for this operation in a stream.
|
||||
/// </summary>
|
||||
public const string Symbol = "ID";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Operator => Symbol;
|
||||
|
||||
/// <summary>
|
||||
/// The key-value pairs which specify attributes of the following image.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<NameToken, IToken> Dictionary { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="BeginInlineImageData"/>.
|
||||
/// </summary>
|
||||
public BeginInlineImageData(IReadOnlyDictionary<NameToken, IToken> dictionary)
|
||||
{
|
||||
Dictionary = dictionary ?? throw new ArgumentNullException(nameof(dictionary));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Run(IOperationContext operationContext)
|
||||
{
|
||||
operationContext.SetInlineImageProperties(Dictionary);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Write(Stream stream)
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Tokens;
|
||||
using Writer;
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Begin the image data for an inline image object.
|
||||
/// </summary>
|
||||
public class BeginInlineImageData : IGraphicsStateOperation
|
||||
{
|
||||
/// <summary>
|
||||
/// The symbol for this operation in a stream.
|
||||
/// </summary>
|
||||
public const string Symbol = "ID";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Operator => Symbol;
|
||||
|
||||
/// <summary>
|
||||
/// The key-value pairs which specify attributes of the following image.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<NameToken, IToken> Dictionary { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="BeginInlineImageData"/>.
|
||||
/// </summary>
|
||||
public BeginInlineImageData(IReadOnlyDictionary<NameToken, IToken> dictionary)
|
||||
{
|
||||
Dictionary = dictionary ?? throw new ArgumentNullException(nameof(dictionary));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Run(IOperationContext operationContext)
|
||||
{
|
||||
operationContext.SetInlineImageProperties(Dictionary);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Write(Stream stream)
|
||||
{
|
||||
var tokenWriter = TokenWriter.Instance;
|
||||
foreach (var item in Dictionary)
|
||||
{
|
||||
var tokenWriter = TokenWriter.Instance;
|
||||
foreach (var item in Dictionary)
|
||||
{
|
||||
var name = item.Key;
|
||||
var value = item.Value;
|
||||
var name = item.Key;
|
||||
var value = item.Value;
|
||||
|
||||
stream.WriteText($"{name} ");
|
||||
tokenWriter.WriteToken(value, stream);
|
||||
stream.WriteNewLine();
|
||||
}
|
||||
stream.WriteText(Symbol);
|
||||
stream.WriteNewLine();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return Symbol;
|
||||
}
|
||||
}
|
||||
stream.WriteText($"{name} ");
|
||||
tokenWriter.WriteToken(value, stream);
|
||||
stream.WriteNewLine();
|
||||
}
|
||||
stream.WriteText(Symbol);
|
||||
stream.WriteNewLine();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return Symbol;
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,6 @@ namespace UglyToad.PdfPig.Graphics
|
||||
#endif
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Tokens;
|
||||
|
||||
/// <summary>
|
||||
@@ -38,7 +37,7 @@ namespace UglyToad.PdfPig.Graphics
|
||||
// private
|
||||
}
|
||||
|
||||
private static readonly IReadOnlyDictionary<string, Type> operations =
|
||||
private static readonly IReadOnlyDictionary<string, Type> Operations =
|
||||
new Dictionary<string, Type>
|
||||
{
|
||||
{ SetStrokeColorAdvanced.Symbol, typeof(SetStrokeColorAdvanced) },
|
||||
@@ -415,10 +414,14 @@ namespace UglyToad.PdfPig.Graphics
|
||||
var errorMessageScn = string.Join(", ", operands.Select(x => x.ToString()));
|
||||
throw new PdfDocumentFormatException($"Attempted to set a stroke color space (SCN) with invalid arguments: [{errorMessageScn}]");
|
||||
case SetStrokeColorDeviceCmyk.Symbol:
|
||||
return new SetStrokeColorDeviceCmyk(OperandToDouble(operands[0]),
|
||||
OperandToDouble(operands[1]),
|
||||
OperandToDouble(operands[2]),
|
||||
OperandToDouble(operands[3]));
|
||||
var setStrokeColorCmykArgs = GetExpectedDoubles(SetNonStrokeColorDeviceCmyk.Symbol,
|
||||
operands,
|
||||
4);
|
||||
return new SetStrokeColorDeviceCmyk(
|
||||
setStrokeColorCmykArgs[0],
|
||||
setStrokeColorCmykArgs[1],
|
||||
setStrokeColorCmykArgs[2],
|
||||
setStrokeColorCmykArgs[3]);
|
||||
case SetStrokeColorDeviceGray.Symbol:
|
||||
return new SetStrokeColorDeviceGray(OperandToDouble(operands[0]));
|
||||
case SetStrokeColorDeviceRgb.Symbol:
|
||||
@@ -462,128 +465,64 @@ namespace UglyToad.PdfPig.Graphics
|
||||
var array = operands.ToArray();
|
||||
|
||||
return new ShowTextsWithPositioning(array);
|
||||
case BeginInlineImageData.Symbol:
|
||||
// Should never be encountered because it is handled by the page content parser.
|
||||
return null;
|
||||
case EndInlineImage.Symbol:
|
||||
// Should never be encountered because it is handled by the page content parser.
|
||||
return null;
|
||||
case Type3SetGlyphWidth.Symbol:
|
||||
var t3SetWidthArgs = GetExpectedDoubles(Type3SetGlyphWidth.Symbol, operands, 2);
|
||||
return new Type3SetGlyphWidth(t3SetWidthArgs[0], t3SetWidthArgs[1]);
|
||||
case Type3SetGlyphWidthAndBoundingBox.Symbol:
|
||||
var t3SetWidthAndBbArgs = GetExpectedDoubles(Type3SetGlyphWidthAndBoundingBox.Symbol, operands, 6);
|
||||
return new Type3SetGlyphWidthAndBoundingBox(
|
||||
t3SetWidthAndBbArgs[0],
|
||||
t3SetWidthAndBbArgs[1],
|
||||
t3SetWidthAndBbArgs[2],
|
||||
t3SetWidthAndBbArgs[3],
|
||||
t3SetWidthAndBbArgs[4],
|
||||
t3SetWidthAndBbArgs[5]);
|
||||
}
|
||||
|
||||
if (!operations.TryGetValue(op.Data, out Type? operationType))
|
||||
if (!Operations.TryGetValue(op.Data, out _))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var constructors = operationType.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
throw new NotImplementedException(
|
||||
$"No support implemented for content operator {op.Data}");
|
||||
}
|
||||
|
||||
if (constructors.Length == 0)
|
||||
private static double[] GetExpectedDoubles(string operatorSymbol, IReadOnlyList<IToken> operands, int resultCount)
|
||||
{
|
||||
var results = new double[resultCount];
|
||||
|
||||
if (operands.Count < resultCount)
|
||||
{
|
||||
throw new InvalidOperationException("No constructors to invoke were found for operation type: " + operationType.FullName);
|
||||
throw new InvalidOperationException(
|
||||
$"Invalid operands for {operatorSymbol}, needed {resultCount} numbers, got: {PrintOperands(operands)}");
|
||||
}
|
||||
|
||||
// This only works by luck...
|
||||
var constructor = constructors[0];
|
||||
|
||||
if (constructor.IsPrivate)
|
||||
for (var i = 0; i < resultCount; i++)
|
||||
{
|
||||
return (IGraphicsStateOperation)operationType.GetField("Value")?.GetValue(null)!;
|
||||
var op = operands[i];
|
||||
|
||||
if (op is not NumericToken nt)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Invalid operands for {operatorSymbol}, needed {resultCount} numbers, got: {PrintOperands(operands)}");
|
||||
}
|
||||
|
||||
results[i] = nt.Data;
|
||||
}
|
||||
|
||||
var parameters = constructor.GetParameters();
|
||||
return results;
|
||||
}
|
||||
|
||||
var offset = 0;
|
||||
|
||||
var arguments = new List<object>();
|
||||
|
||||
foreach (var parameter in parameters)
|
||||
{
|
||||
if (offset >= operands.Count)
|
||||
{
|
||||
throw new InvalidOperationException($"Fewer operands {operands.Count} found than required ({offset + 1}) for operator: {op.Data}.");
|
||||
}
|
||||
|
||||
if (parameter.ParameterType == typeof(double))
|
||||
{
|
||||
if (operands[offset] is NumericToken numeric)
|
||||
{
|
||||
arguments.Add(numeric.Data);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Expected a double parameter for operation type {operationType.FullName}. Instead got: {operands[offset]}");
|
||||
}
|
||||
|
||||
offset++;
|
||||
}
|
||||
else if (parameter.ParameterType == typeof(int))
|
||||
{
|
||||
if (operands[offset] is NumericToken numeric)
|
||||
{
|
||||
arguments.Add(numeric.Int);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Expected an integer parameter for operation type {operationType.FullName}. Instead got: {operands[offset]}");
|
||||
}
|
||||
|
||||
offset++;
|
||||
}
|
||||
else if (parameter.ParameterType == typeof(double[]))
|
||||
{
|
||||
if (operands[offset] is ArrayToken arr)
|
||||
{
|
||||
arguments.Add(arr.Data.OfType<NumericToken>().Select(x => x.Data).ToArray());
|
||||
offset++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var array = new List<double>();
|
||||
while (offset < operands.Count && operands[offset] is NumericToken numeric)
|
||||
{
|
||||
array.Add(numeric.Data);
|
||||
offset++;
|
||||
}
|
||||
|
||||
arguments.Add(array.ToArray());
|
||||
}
|
||||
else if (parameter.ParameterType == typeof(NameToken))
|
||||
{
|
||||
if (operands[offset] is NameToken name)
|
||||
{
|
||||
arguments.Add(name);
|
||||
}
|
||||
else if (operands[offset] is StringToken s)
|
||||
{
|
||||
arguments.Add(NameToken.Create(s.Data));
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Expected a NameToken parameter for operation type {operationType.FullName}. Instead got: {operands[offset]}");
|
||||
}
|
||||
|
||||
offset++;
|
||||
}
|
||||
else if (parameter.ParameterType == typeof(string))
|
||||
{
|
||||
if (operands[offset] is StringToken stringToken)
|
||||
{
|
||||
arguments.Add(stringToken.Data);
|
||||
}
|
||||
else if (operands[offset] is HexToken hexToken)
|
||||
{
|
||||
arguments.Add(hexToken.Data);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Expected a string parameter for operation type {operationType.FullName}. Instead got: {operands[offset]}");
|
||||
}
|
||||
|
||||
offset++;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotImplementedException($"Unsupported parameter type {parameter.ParameterType.FullName} for operation type {operationType.FullName}.");
|
||||
}
|
||||
}
|
||||
|
||||
var result = constructor.Invoke(arguments.ToArray());
|
||||
|
||||
return (IGraphicsStateOperation)result;
|
||||
private static string PrintOperands(IEnumerable<IToken> operands)
|
||||
{
|
||||
return "[" + string.Join(", ", operands.Select(x => x.ToString())) + "]";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,7 +79,7 @@
|
||||
// Enumerate bits in bitsPerComponent-sized chunks from MSB to LSB, masking on the appropriate bits
|
||||
for (int i = end; i >= 0; --i)
|
||||
{
|
||||
unpacked[u++] = (byte)((b >> i) & right) == 1 ? byte.MinValue : byte.MaxValue;
|
||||
unpacked[u++] = (byte)((b >> i) & right) == 1 ? byte.MaxValue : byte.MinValue;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,36 +7,40 @@
|
||||
|
||||
internal static class PngFromPdfImageFactory
|
||||
{
|
||||
private static bool TryGenerateSoftMask(IPdfImage image, [NotNullWhen(true)] out ReadOnlySpan<byte> bytes)
|
||||
private static bool TryGenerateSoftMask(IPdfImage image, [NotNullWhen(true)] out ReadOnlySpan<byte> maskBytes)
|
||||
{
|
||||
bytes = ReadOnlySpan<byte>.Empty;
|
||||
maskBytes = ReadOnlySpan<byte>.Empty;
|
||||
|
||||
if (image.MaskImage is null)
|
||||
var mask = image.MaskImage;
|
||||
|
||||
if (mask is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Because we cannot resize images directly in PdfPig, we only
|
||||
// apply the mask if it has the same size as the image
|
||||
if (image.HeightInSamples != image.MaskImage.HeightInSamples ||
|
||||
image.WidthInSamples != image.MaskImage.WidthInSamples)
|
||||
if (image.HeightInSamples != mask.HeightInSamples ||
|
||||
image.WidthInSamples != mask.WidthInSamples)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!image.TryGetBytesAsMemory(out var imageMemory))
|
||||
if (!mask.TryGetBytesAsMemory(out var maskMemory))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
bytes = ColorSpaceDetailsByteConverter.Convert(image.ColorSpaceDetails!,
|
||||
imageMemory.Span,
|
||||
image.BitsPerComponent,
|
||||
image.WidthInSamples,
|
||||
image.HeightInSamples);
|
||||
return IsCorrectlySized(image, bytes);
|
||||
maskBytes = ColorSpaceDetailsByteConverter.Convert(
|
||||
mask.ColorSpaceDetails!,
|
||||
maskMemory.Span,
|
||||
mask.BitsPerComponent,
|
||||
mask.WidthInSamples,
|
||||
mask.HeightInSamples);
|
||||
|
||||
return IsCorrectlySized(mask, maskBytes);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
@@ -155,13 +159,28 @@
|
||||
else
|
||||
{
|
||||
int i = 0;
|
||||
for (int col = 0; col < image.HeightInSamples; col++)
|
||||
if (!image.NeedsReverseDecode()) // TODO - Need to properly implement decode for other numberOfComponents
|
||||
{
|
||||
for (int row = 0; row < image.WidthInSamples; row++)
|
||||
for (int col = 0; col < image.HeightInSamples; col++)
|
||||
{
|
||||
byte a = getAlphaChannel(i);
|
||||
byte pixel = bytesPure[i++];
|
||||
builder.SetPixel(new Pixel(pixel, pixel, pixel, a, false), row, col);
|
||||
for (int row = 0; row < image.WidthInSamples; row++)
|
||||
{
|
||||
byte a = getAlphaChannel(i);
|
||||
byte pixel = bytesPure[i++];
|
||||
builder.SetPixel(new Pixel(pixel, pixel, pixel, a, false), row, col);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int col = 0; col < image.HeightInSamples; col++)
|
||||
{
|
||||
for (int row = 0; row < image.WidthInSamples; row++)
|
||||
{
|
||||
byte a = getAlphaChannel(i);
|
||||
byte pixel = (byte)(255 - bytesPure[i++]); // Inverse decode
|
||||
builder.SetPixel(new Pixel(pixel, pixel, pixel, a, false), row, col);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
using Logging;
|
||||
using Tokenization.Scanner;
|
||||
using Tokens;
|
||||
using Util;
|
||||
|
||||
internal sealed class XrefOffsetValidator
|
||||
{
|
||||
@@ -90,7 +91,10 @@
|
||||
long newOffsetTable = -1;
|
||||
long newOffsetStream = -1;
|
||||
|
||||
BruteForceSearchForTables(reader);
|
||||
if (bfSearchXRefTablesOffsets == null)
|
||||
{
|
||||
bfSearchXRefTablesOffsets = BruteForceSearchForTables(reader);
|
||||
}
|
||||
|
||||
BfSearchForXRefStreams(reader);
|
||||
|
||||
@@ -217,40 +221,39 @@
|
||||
bytes.Seek(startOffset);
|
||||
}
|
||||
|
||||
private void BruteForceSearchForTables(IInputBytes bytes)
|
||||
public static List<long> BruteForceSearchForTables(IInputBytes bytes)
|
||||
{
|
||||
if (bfSearchXRefTablesOffsets != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// a pdf may contain more than one xref entry
|
||||
bfSearchXRefTablesOffsets = new List<long>();
|
||||
var resultOffsets = new List<long>();
|
||||
|
||||
var startOffset = bytes.CurrentOffset;
|
||||
|
||||
bytes.Seek(MinimumSearchOffset);
|
||||
|
||||
var buffer = new CircularByteBuffer(XRefBytes.Length + 1);
|
||||
|
||||
// search for xref tables
|
||||
while (bytes.MoveNext() && !bytes.IsAtEnd())
|
||||
{
|
||||
if (ReadHelper.IsString(bytes, XRefBytes))
|
||||
if (ReadHelper.IsWhitespace(bytes.CurrentByte))
|
||||
{
|
||||
var newOffset = bytes.CurrentOffset;
|
||||
// Normalize whitespace
|
||||
buffer.Add((byte)' ');
|
||||
}
|
||||
else
|
||||
{
|
||||
buffer.Add(bytes.CurrentByte);
|
||||
}
|
||||
|
||||
bytes.Seek(newOffset - 1);
|
||||
|
||||
// ensure that we don't read "startxref" instead of "xref"
|
||||
if (ReadHelper.IsWhitespace(bytes.CurrentByte))
|
||||
{
|
||||
bfSearchXRefTablesOffsets.Add(newOffset);
|
||||
}
|
||||
|
||||
bytes.Seek(newOffset + 4);
|
||||
if (buffer.IsCurrentlyEqual(" xref"))
|
||||
{
|
||||
resultOffsets.Add(bytes.CurrentOffset - 4);
|
||||
}
|
||||
}
|
||||
|
||||
bytes.Seek(startOffset);
|
||||
|
||||
return resultOffsets;
|
||||
}
|
||||
|
||||
private void BfSearchForXRefStreams(IInputBytes bytes)
|
||||
|
||||
@@ -168,7 +168,11 @@
|
||||
pdfScanner,
|
||||
parsingOptions);
|
||||
|
||||
var type1Handler = new Type1FontHandler(pdfScanner, filterProvider, encodingReader);
|
||||
var type1Handler = new Type1FontHandler(
|
||||
pdfScanner,
|
||||
filterProvider,
|
||||
encodingReader,
|
||||
parsingOptions.UseLenientParsing);
|
||||
|
||||
var trueTypeHandler = new TrueTypeFontHandler(parsingOptions.Logger,
|
||||
pdfScanner,
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
{
|
||||
var filter = filters[i];
|
||||
totalMaxEstSize *= GetEstimatedSizeMultiplier(filter);
|
||||
|
||||
|
||||
transform = filter.Decode(transform, stream.StreamDictionary, filterProvider, i);
|
||||
|
||||
if (i < filters.Count - 1 && transform.Length > totalMaxEstSize)
|
||||
@@ -93,9 +93,9 @@
|
||||
{
|
||||
var filter = filters[i];
|
||||
totalMaxEstSize *= GetEstimatedSizeMultiplier(filter);
|
||||
|
||||
|
||||
transform = filter.Decode(transform, stream.StreamDictionary, filterProvider, i);
|
||||
|
||||
|
||||
if (i < filters.Count - 1 && transform.Length > totalMaxEstSize)
|
||||
{
|
||||
// Try to prevent malicious decompression, leading to OOM issues
|
||||
@@ -122,16 +122,16 @@
|
||||
/// Returns an equivalent token where any indirect references of child objects are
|
||||
/// recursively traversed and resolved.
|
||||
/// </summary>
|
||||
internal static T? Resolve<T>(this T? token, IPdfTokenScanner scanner) where T : IToken
|
||||
internal static T? Resolve<T>(this T? token, IPdfTokenScanner scanner, List<IndirectReference>? visited = null) where T : IToken
|
||||
{
|
||||
return (T?)ResolveInternal(token, scanner);
|
||||
return (T?)ResolveInternal(token, scanner, visited ?? []);
|
||||
}
|
||||
|
||||
private static IToken? ResolveInternal(this IToken? token, IPdfTokenScanner scanner)
|
||||
private static IToken? ResolveInternal(this IToken? token, IPdfTokenScanner scanner, List<IndirectReference> visited)
|
||||
{
|
||||
if (token is StreamToken stream)
|
||||
{
|
||||
return new StreamToken(Resolve(stream.StreamDictionary, scanner), stream.Data);
|
||||
return new StreamToken(Resolve(stream.StreamDictionary, scanner, visited), stream.Data);
|
||||
}
|
||||
|
||||
if (token is DictionaryToken dict)
|
||||
@@ -139,8 +139,17 @@
|
||||
var resolvedItems = new Dictionary<NameToken, IToken>();
|
||||
foreach (var kvp in dict.Data)
|
||||
{
|
||||
var value = kvp.Value is IndirectReferenceToken reference ? scanner.Get(reference.Data)?.Data : kvp.Value;
|
||||
resolvedItems[NameToken.Create(kvp.Key)] = ResolveInternal(value, scanner);
|
||||
var value = kvp.Value;
|
||||
if (kvp.Value is IndirectReferenceToken reference)
|
||||
{
|
||||
if (visited.Contains(reference.Data))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
value = scanner.Get(reference.Data)?.Data;
|
||||
visited.Add(reference.Data);
|
||||
}
|
||||
resolvedItems[NameToken.Create(kvp.Key)] = ResolveInternal(value, scanner, visited);
|
||||
}
|
||||
|
||||
return new DictionaryToken(resolvedItems);
|
||||
@@ -152,7 +161,7 @@
|
||||
for (int i = 0; i < arr.Length; i++)
|
||||
{
|
||||
var value = arr.Data[i] is IndirectReferenceToken reference ? scanner.Get(reference.Data)?.Data : arr.Data[i];
|
||||
resolvedItems.Add(ResolveInternal(value, scanner));
|
||||
resolvedItems.Add(ResolveInternal(value, scanner, visited));
|
||||
}
|
||||
return new ArrayToken(resolvedItems);
|
||||
}
|
||||
|
||||
@@ -147,6 +147,8 @@
|
||||
|
||||
public int ReadCode(IInputBytes bytes, bool useLenientParsing)
|
||||
{
|
||||
var myPosition = bytes.CurrentOffset;
|
||||
|
||||
if (hasEmptyCodespace)
|
||||
{
|
||||
var data = new byte[minCodeLength];
|
||||
@@ -184,7 +186,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
throw new PdfDocumentFormatException($"CMap is invalid, min code length was {minCodeLength}, max was {maxCodeLength}.");
|
||||
// If we encounter invalid inputs we read min bytes and convert directly to an integer.
|
||||
if (useLenientParsing)
|
||||
{
|
||||
bytes.Seek(myPosition);
|
||||
for (var i = 0; i < minCodeLength; i++)
|
||||
{
|
||||
result[i] = ReadByte(bytes, useLenientParsing);
|
||||
}
|
||||
|
||||
// https://github.com/apache/pdfbox/blob/f81c7c5a06126db68aa985a0e755cdbffed7d270/fontbox/src/main/java/org/apache/fontbox/cmap/CMap.java#L207
|
||||
return ByteArrayToInt(result.AsSpan(0, minCodeLength));
|
||||
}
|
||||
|
||||
throw new PdfDocumentFormatException($"CMap is invalid, min code length was {minCodeLength}, max was {maxCodeLength}. Bytes: {BitConverter.ToString(result)}.");
|
||||
}
|
||||
|
||||
private static byte ReadByte(IInputBytes bytes, bool useLenientParsing)
|
||||
|
||||
@@ -49,13 +49,18 @@
|
||||
|
||||
public IFont Generate(DictionaryToken dictionary)
|
||||
{
|
||||
if (!dictionary.TryGetOptionalTokenDirect(NameToken.FirstChar, pdfScanner, out NumericToken? firstCharacterToken)
|
||||
int? firstChar = null;
|
||||
if (!dictionary.TryGetOptionalTokenDirect(NameToken.FirstChar,
|
||||
pdfScanner,
|
||||
out NumericToken? firstCharacterToken)
|
||||
|| !dictionary.TryGet<IToken>(NameToken.FontDescriptor, pdfScanner, out _)
|
||||
|| !dictionary.TryGet(NameToken.Widths, out IToken _))
|
||||
|| !dictionary.TryGet(NameToken.Widths, out _))
|
||||
{
|
||||
var isStandard14 = true;
|
||||
if (!dictionary.TryGetOptionalTokenDirect(NameToken.BaseFont, pdfScanner, out NameToken? baseFont))
|
||||
{
|
||||
throw new InvalidFontFormatException($"The provided TrueType font dictionary did not contain a /FirstChar or a /BaseFont entry: {dictionary}.");
|
||||
throw new InvalidFontFormatException(
|
||||
$"The provided TrueType font dictionary did not contain a /FirstChar or a /BaseFont entry: {dictionary}.");
|
||||
}
|
||||
|
||||
// Can use the AFM descriptor despite not being Type 1!
|
||||
@@ -63,37 +68,53 @@
|
||||
|
||||
if (standard14Font is null)
|
||||
{
|
||||
throw new InvalidFontFormatException($"The provided TrueType font dictionary did not have a /FirstChar and did not match a Standard 14 font: {dictionary}.");
|
||||
if (dictionary.TryGet(NameToken.LastChar, pdfScanner, out NumericToken? lastCharToken)
|
||||
&& dictionary.TryGet(NameToken.Widths, pdfScanner, out ArrayToken? widthsArrayLoc))
|
||||
{
|
||||
// If the widths array contains widths for characters 0 - 3 it will contain 4 entries but last char is 3.
|
||||
firstChar = (lastCharToken.Int - widthsArrayLoc.Length) + 1;
|
||||
isStandard14 = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidFontFormatException(
|
||||
$"The provided TrueType font dictionary did not have a /FirstChar and did not match a Standard 14 font: {dictionary}.");
|
||||
}
|
||||
}
|
||||
|
||||
var fileSystemFont = systemFontFinder.GetTrueTypeFont(baseFont.Data);
|
||||
|
||||
var thisEncoding = encodingReader.Read(dictionary);
|
||||
|
||||
if (thisEncoding is null)
|
||||
if (isStandard14)
|
||||
{
|
||||
thisEncoding = new AdobeFontMetricsEncoding(standard14Font);
|
||||
var fileSystemFont = systemFontFinder.GetTrueTypeFont(baseFont.Data);
|
||||
|
||||
var thisEncoding = encodingReader.Read(dictionary);
|
||||
|
||||
if (thisEncoding is null)
|
||||
{
|
||||
thisEncoding = new AdobeFontMetricsEncoding(standard14Font);
|
||||
}
|
||||
|
||||
double[]? widthsOverride = null;
|
||||
|
||||
if (dictionary.TryGet(NameToken.FirstChar, pdfScanner, out firstCharacterToken))
|
||||
{
|
||||
firstChar = firstCharacterToken.Int;
|
||||
}
|
||||
|
||||
if (dictionary.TryGet(NameToken.Widths, pdfScanner, out ArrayToken? widthsArray))
|
||||
{
|
||||
widthsOverride = widthsArray.Data.OfType<NumericToken>()
|
||||
.Select(x => x.Double).ToArray();
|
||||
}
|
||||
|
||||
return new TrueTypeStandard14FallbackSimpleFont(baseFont,
|
||||
standard14Font!,
|
||||
thisEncoding,
|
||||
fileSystemFont,
|
||||
new TrueTypeStandard14FallbackSimpleFont.MetricOverrides(firstChar, widthsOverride));
|
||||
}
|
||||
|
||||
int? firstChar = null;
|
||||
double[]? widthsOverride = null;
|
||||
|
||||
if (dictionary.TryGet(NameToken.FirstChar, pdfScanner, out firstCharacterToken))
|
||||
{
|
||||
firstChar = firstCharacterToken.Int;
|
||||
}
|
||||
|
||||
if (dictionary.TryGet(NameToken.Widths, pdfScanner, out ArrayToken? widthsArray))
|
||||
{
|
||||
widthsOverride = widthsArray.Data.OfType<NumericToken>()
|
||||
.Select(x => x.Double).ToArray();
|
||||
}
|
||||
|
||||
return new TrueTypeStandard14FallbackSimpleFont(baseFont, standard14Font, thisEncoding, fileSystemFont,
|
||||
new TrueTypeStandard14FallbackSimpleFont.MetricOverrides(firstChar, widthsOverride));
|
||||
}
|
||||
|
||||
var firstCharacter = firstCharacterToken.Int;
|
||||
var firstCharacter = firstChar ?? firstCharacterToken!.Int;
|
||||
|
||||
var widths = FontDictionaryAccessHelper.GetWidths(pdfScanner, dictionary);
|
||||
|
||||
|
||||
@@ -20,13 +20,18 @@
|
||||
private readonly IPdfTokenScanner pdfScanner;
|
||||
private readonly ILookupFilterProvider filterProvider;
|
||||
private readonly IEncodingReader encodingReader;
|
||||
private readonly bool isLenientParsing;
|
||||
|
||||
public Type1FontHandler(IPdfTokenScanner pdfScanner, ILookupFilterProvider filterProvider,
|
||||
IEncodingReader encodingReader)
|
||||
public Type1FontHandler(
|
||||
IPdfTokenScanner pdfScanner,
|
||||
ILookupFilterProvider filterProvider,
|
||||
IEncodingReader encodingReader,
|
||||
bool isLenientParsing)
|
||||
{
|
||||
this.pdfScanner = pdfScanner;
|
||||
this.filterProvider = filterProvider;
|
||||
this.encodingReader = encodingReader;
|
||||
this.isLenientParsing = isLenientParsing;
|
||||
}
|
||||
|
||||
public IFont Generate(DictionaryToken dictionary)
|
||||
@@ -69,12 +74,26 @@
|
||||
widths = [];
|
||||
}
|
||||
|
||||
if (!dictionary.TryGet(NameToken.FontDescriptor, out var _))
|
||||
if (!dictionary.TryGet(NameToken.FontDescriptor, out _))
|
||||
{
|
||||
if (dictionary.TryGet(NameToken.BaseFont, out var baseFontToken) &&
|
||||
DirectObjectFinder.TryGet(baseFontToken, pdfScanner, out NameToken? baseFontName))
|
||||
if (dictionary.TryGet(NameToken.BaseFont, pdfScanner, out NameToken? baseFontToken))
|
||||
{
|
||||
var metrics = Standard14.GetAdobeFontMetrics(baseFontName.Data);
|
||||
var metrics = Standard14.GetAdobeFontMetrics(baseFontToken.Data);
|
||||
|
||||
if (metrics == null)
|
||||
{
|
||||
if (isLenientParsing)
|
||||
{
|
||||
// We can support a fallback here to return content.
|
||||
// https://github.com/apache/pdfbox/blob/f81c7c5a06126db68aa985a0e755cdbffed7d270/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/FontMapperImpl.java#L304
|
||||
metrics = Standard14.GetAdobeFontMetrics(Standard14Font.TimesRoman);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new PdfDocumentFormatException(
|
||||
$"Type 1 Standard 14 font with name {baseFontToken} requested, this is an invalid name.");
|
||||
}
|
||||
}
|
||||
|
||||
var overrideEncoding = encodingReader.Read(dictionary);
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
namespace UglyToad.PdfPig.PdfFonts.Parser.Parts
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cmap;
|
||||
using Fonts;
|
||||
using Tokenization.Scanner;
|
||||
@@ -20,6 +18,13 @@
|
||||
// The start of the input code range.
|
||||
if (!scanner.TryReadToken(out HexToken lowSourceCode))
|
||||
{
|
||||
// Allow a miscount.
|
||||
if (scanner.CurrentToken is OperatorToken ot &&
|
||||
ot.Data.Equals("endbfrange", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
throw new InvalidFontFormatException($"bfrange was missing the low source code: {scanner.CurrentToken}");
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ namespace UglyToad.PdfPig.PdfFonts.Simple
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Core;
|
||||
using Fonts;
|
||||
@@ -84,36 +83,31 @@ namespace UglyToad.PdfPig.PdfFonts.Simple
|
||||
|
||||
public CharacterBoundingBox GetBoundingBox(int characterCode)
|
||||
{
|
||||
var boundingBox = GetBoundingBoxInGlyphSpace(characterCode);
|
||||
var (boundingBox, advanceWidth) = GetBoundingBoxInGlyphSpace(characterCode);
|
||||
|
||||
boundingBox = fontMatrix.Transform(boundingBox);
|
||||
advanceWidth = fontMatrix.TransformX(advanceWidth);
|
||||
|
||||
return new CharacterBoundingBox(boundingBox, boundingBox.Width);
|
||||
return new CharacterBoundingBox(boundingBox, advanceWidth);
|
||||
}
|
||||
|
||||
private PdfRectangle GetBoundingBoxInGlyphSpace(int characterCode)
|
||||
private (PdfRectangle bounds, double advanceWidth) GetBoundingBoxInGlyphSpace(int characterCode)
|
||||
{
|
||||
var name = encoding.GetName(characterCode);
|
||||
|
||||
if (!standardFontMetrics.CharacterMetrics.TryGetValue(name, out var metrics))
|
||||
{
|
||||
return new PdfRectangle(0, 0, 250, 0);
|
||||
return (new PdfRectangle(0, 0, 250, 0), 250);
|
||||
}
|
||||
|
||||
var x = metrics.Width.X;
|
||||
var y = metrics.Width.Y;
|
||||
|
||||
if (metrics.Width.X == 0 && metrics.BoundingBox.Width > 0)
|
||||
{
|
||||
x = metrics.BoundingBox.Width;
|
||||
}
|
||||
|
||||
if (metrics.Width.Y == 0 && metrics.BoundingBox.Height > 0)
|
||||
{
|
||||
y = metrics.BoundingBox.Height;
|
||||
}
|
||||
|
||||
return new PdfRectangle(0, 0, x, y);
|
||||
return (metrics.BoundingBox, x);
|
||||
}
|
||||
|
||||
public TransformationMatrix GetFontMatrix()
|
||||
|
||||
@@ -164,8 +164,6 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
// This should never happen.
|
||||
Debug.Assert(false, "Encountered a start object 'obj' operator before the end of the previous object.");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -191,9 +189,7 @@
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// This should never happen.
|
||||
Debug.Assert(false, $"Encountered a '{coreTokenScanner.CurrentToken}' operator before the end of the previous object.");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -268,6 +264,12 @@
|
||||
{
|
||||
token = trimmedDuplicatedEndTokens[0];
|
||||
}
|
||||
else if (readTokens[0] is StreamToken str
|
||||
&& readTokens.Skip(1).All(x => x is OperatorToken op && op.Equals(OperatorToken.EndStream)))
|
||||
{
|
||||
// If a stream token is followed by "endstream" operator tokens just skip the following duplicated tokens.
|
||||
token = str;
|
||||
}
|
||||
else
|
||||
{
|
||||
token = readTokens[readTokens.Count - 1];
|
||||
@@ -846,7 +848,11 @@
|
||||
// Read the N integers
|
||||
var bytes = new MemoryInputBytes(stream.Decode(filterProvider, this));
|
||||
|
||||
var scanner = new CoreTokenScanner(bytes, true, useLenientParsing: parsingOptions.UseLenientParsing);
|
||||
var scanner = new CoreTokenScanner(
|
||||
bytes,
|
||||
true,
|
||||
useLenientParsing: parsingOptions.UseLenientParsing,
|
||||
isStream: true);
|
||||
|
||||
var objects = new List<(long, long)>();
|
||||
|
||||
|
||||
90
src/UglyToad.PdfPig/Util/CircularByteBuffer.cs
Normal file
90
src/UglyToad.PdfPig/Util/CircularByteBuffer.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
namespace UglyToad.PdfPig.Util;
|
||||
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
internal sealed class CircularByteBuffer(int size)
|
||||
{
|
||||
private readonly byte[] buffer = new byte[size];
|
||||
|
||||
private int start;
|
||||
private int count;
|
||||
|
||||
public void Add(byte b)
|
||||
{
|
||||
var insertionPosition = (start + count) % buffer.Length;
|
||||
|
||||
buffer[insertionPosition] = b;
|
||||
if (count < buffer.Length)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
else
|
||||
{
|
||||
start = (start + 1) % buffer.Length;
|
||||
}
|
||||
}
|
||||
|
||||
public bool EndsWith(string s)
|
||||
{
|
||||
if (s.Length > count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < s.Length; i++)
|
||||
{
|
||||
var str = s[i];
|
||||
|
||||
var inBuffer = count - (s.Length - i);
|
||||
|
||||
var buff = buffer[IndexToBufferIndex(inBuffer)];
|
||||
|
||||
if (buff != str)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool IsCurrentlyEqual(string s)
|
||||
{
|
||||
if (s.Length > buffer.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < s.Length; i++)
|
||||
{
|
||||
var b = (byte)s[i];
|
||||
var buff = buffer[IndexToBufferIndex(i)];
|
||||
|
||||
if (b != buff)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public ReadOnlySpan<byte> AsSpan()
|
||||
{
|
||||
Span<byte> tmp = new byte[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
tmp[i] = buffer[IndexToBufferIndex(i)];
|
||||
}
|
||||
|
||||
return tmp;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Encoding.ASCII.GetString(AsSpan());
|
||||
}
|
||||
|
||||
private int IndexToBufferIndex(int i) => (start + i) % buffer.Length;
|
||||
}
|
||||
@@ -54,11 +54,7 @@
|
||||
}
|
||||
|
||||
var colorSpaceDetails = GetColorSpaceDetails(colorSpace, imageDictionary.Without(NameToken.Filter).Without(NameToken.F), scanner, resourceStore, filterProvider, true);
|
||||
|
||||
var decodeRaw = imageDictionary.GetObjectOrDefault(NameToken.Decode, NameToken.D) as ArrayToken ?? new ArrayToken([]);
|
||||
var decode = decodeRaw.Data.OfType<NumericToken>().Select(static x => x.Double).ToArray();
|
||||
|
||||
return IndexedColorSpaceDetails.Stencil(colorSpaceDetails, decode);
|
||||
return IndexedColorSpaceDetails.Stencil(colorSpaceDetails);
|
||||
}
|
||||
|
||||
if (!colorSpace.HasValue)
|
||||
|
||||
36
src/UglyToad.PdfPig/Writer/PdfContentTransformationReader.cs
Normal file
36
src/UglyToad.PdfPig/Writer/PdfContentTransformationReader.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
namespace UglyToad.PdfPig.Writer;
|
||||
|
||||
using Core;
|
||||
using Graphics.Operations;
|
||||
using Graphics.Operations.SpecialGraphicsState;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
internal static class PdfContentTransformationReader
|
||||
{
|
||||
public static TransformationMatrix? GetGlobalTransform(IEnumerable<IGraphicsStateOperation> operations)
|
||||
{
|
||||
TransformationMatrix? activeMatrix = null;
|
||||
var stackDepth = 0;
|
||||
foreach (var operation in operations)
|
||||
{
|
||||
if (operation is ModifyCurrentTransformationMatrix cm)
|
||||
{
|
||||
if (stackDepth == 0 && cm.Value.Length == 6)
|
||||
{
|
||||
activeMatrix = TransformationMatrix.FromArray(cm.Value);
|
||||
}
|
||||
}
|
||||
else if (operation is Push push)
|
||||
{
|
||||
stackDepth++;
|
||||
}
|
||||
else if (operation is Pop pop)
|
||||
{
|
||||
stackDepth--;
|
||||
}
|
||||
}
|
||||
|
||||
return activeMatrix;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +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 PdfPig.Fonts.TrueType;
|
||||
using PdfPig.Fonts.Standard14Fonts;
|
||||
using PdfPig.Fonts.TrueType.Parser;
|
||||
using Outline;
|
||||
using Outline.Destinations;
|
||||
using Tokenization.Scanner;
|
||||
using Tokens;
|
||||
|
||||
@@ -305,7 +310,7 @@ namespace UglyToad.PdfPig.Writer
|
||||
/// <returns>A builder for editing the page.</returns>
|
||||
public PdfPageBuilder AddPage(PdfDocument document, int pageNumber)
|
||||
{
|
||||
return AddPage(document, pageNumber, null);
|
||||
return AddPage(document, pageNumber, new AddPageOptions());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -313,9 +318,9 @@ namespace UglyToad.PdfPig.Writer
|
||||
/// </summary>
|
||||
/// <param name="document">Source document.</param>
|
||||
/// <param name="pageNumber">Page to copy.</param>
|
||||
/// <param name="copyLink">If set, links are copied based on the result of the delegate.</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, Func<PdfAction, PdfAction?>? copyLink)
|
||||
public PdfPageBuilder AddPage(PdfDocument document, int pageNumber, AddPageOptions options)
|
||||
{
|
||||
if (!existingCopies.TryGetValue(document.Structure.TokenScanner, out var refs))
|
||||
{
|
||||
@@ -342,6 +347,7 @@ namespace UglyToad.PdfPig.Writer
|
||||
}
|
||||
|
||||
var page = document.GetPage(pageNumber);
|
||||
var pcp = new PageContentParser(ReflectionGraphicsStateOperationFactory.Instance, true);
|
||||
|
||||
// copy content streams
|
||||
var streams = new List<PdfPageBuilder.CopiedContentStream>();
|
||||
@@ -352,23 +358,54 @@ namespace UglyToad.PdfPig.Writer
|
||||
var prev = context.AttemptDeduplication;
|
||||
context.AttemptDeduplication = false;
|
||||
context.WritingPageContents = true;
|
||||
|
||||
var contentReferences = new List<IndirectReferenceToken>();
|
||||
|
||||
if (contentsToken is ArrayToken array)
|
||||
{
|
||||
foreach (var item in array.Data)
|
||||
{
|
||||
if (item is IndirectReferenceToken ir)
|
||||
{
|
||||
streams.Add(new PdfPageBuilder.CopiedContentStream(
|
||||
(IndirectReferenceToken)WriterUtil.CopyToken(context, ir, document.Structure.TokenScanner, refs)));
|
||||
contentReferences.Add(ir);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
else if (contentsToken is IndirectReferenceToken ir)
|
||||
{
|
||||
streams.Add(new PdfPageBuilder.CopiedContentStream(
|
||||
(IndirectReferenceToken)WriterUtil.CopyToken(context, ir, document.Structure.TokenScanner, refs)));
|
||||
contentReferences.Add(ir);
|
||||
}
|
||||
|
||||
foreach (var indirectReferenceToken in contentReferences)
|
||||
{
|
||||
// Detect any globally applied transforms to the graphics state from the content stream.
|
||||
TransformationMatrix? globalTransform = null;
|
||||
|
||||
try
|
||||
{
|
||||
// If we don't manage to do this it's not the end of the world.
|
||||
if (DirectObjectFinder.TryGet<StreamToken>(indirectReferenceToken, document.Structure.TokenScanner, out var contentStream))
|
||||
{
|
||||
var contentBytes = contentStream.Decode(DefaultFilterProvider.Instance);
|
||||
var parsedOperations = pcp.Parse(0, new MemoryInputBytes(contentBytes), new NoOpLog());
|
||||
globalTransform = PdfContentTransformationReader.GetGlobalTransform(parsedOperations);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore and continue writing.
|
||||
}
|
||||
|
||||
var updatedIndirect = (IndirectReferenceToken)WriterUtil.CopyToken(
|
||||
context,
|
||||
indirectReferenceToken,
|
||||
document.Structure.TokenScanner,
|
||||
refs);
|
||||
|
||||
streams.Add(new PdfPageBuilder.CopiedContentStream(updatedIndirect, globalTransform));
|
||||
}
|
||||
|
||||
context.AttemptDeduplication = prev;
|
||||
context.WritingPageContents = false;
|
||||
}
|
||||
@@ -416,71 +453,20 @@ namespace UglyToad.PdfPig.Writer
|
||||
|
||||
if (kvp.Key == NameToken.Annots)
|
||||
{
|
||||
var val = kvp.Value;
|
||||
if (kvp.Value is IndirectReferenceToken ir)
|
||||
if (!options.KeepAnnotations)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
var copiedTokens = CopyAnnotationsFromPageSource(
|
||||
kvp.Value,
|
||||
document.Structure.TokenScanner,
|
||||
refs,
|
||||
page,
|
||||
options.CopyLinkFunc,
|
||||
x => links.Add(x));
|
||||
|
||||
if (tk.TryGet(NameToken.Subtype, out var st) && st is NameToken nm && nm == NameToken.Link)
|
||||
{
|
||||
if (copyLink 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 = copyLink(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;
|
||||
}
|
||||
|
||||
@@ -501,9 +487,12 @@ namespace UglyToad.PdfPig.Writer
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var item in dict.Data)
|
||||
{
|
||||
if (!destinationDict.ContainsKey(NameToken.Create(item.Key)))
|
||||
var key = NameToken.Create(item.Key);
|
||||
|
||||
if (!destinationDict.ContainsKey(key))
|
||||
{
|
||||
if (item.Value is IndirectReferenceToken ir)
|
||||
{
|
||||
@@ -512,43 +501,55 @@ namespace UglyToad.PdfPig.Writer
|
||||
if (obj.Data is StreamToken)
|
||||
{
|
||||
// rare case, have seen /SubType as stream token, can't make direct
|
||||
destinationDict[NameToken.Create(item.Key)] = WriterUtil.CopyToken(context, item.Value, document.Structure.TokenScanner, refs);
|
||||
destinationDict[key] = WriterUtil.CopyToken(context, item.Value, document.Structure.TokenScanner, refs);
|
||||
}
|
||||
else
|
||||
{
|
||||
destinationDict[NameToken.Create(item.Key)] = WriterUtil.CopyToken(context, obj.Data, document.Structure.TokenScanner, refs);
|
||||
destinationDict[key] = WriterUtil.CopyToken(context, obj.Data, document.Structure.TokenScanner, refs);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
destinationDict[NameToken.Create(item.Key)] = WriterUtil.CopyToken(context, item.Value, document.Structure.TokenScanner, refs);
|
||||
destinationDict[key] = WriterUtil.CopyToken(context, item.Value, document.Structure.TokenScanner, refs);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var subDict = GetRemoteDict(item.Value);
|
||||
var destSubDict = destinationDict[NameToken.Create(item.Key)] as DictionaryToken;
|
||||
var destSubDict = destinationDict[key] as DictionaryToken;
|
||||
if (destSubDict is null || subDict is null)
|
||||
{
|
||||
// not a dict.. just overwrite with more important one? should maybe check arrays?
|
||||
if (item.Value is IndirectReferenceToken ir)
|
||||
{
|
||||
// convert indirect to direct as PdfPageBuilder needs to modify resource entries
|
||||
destinationDict[NameToken.Create(item.Key)] = WriterUtil.CopyToken(context, document.Structure.TokenScanner.Get(ir.Data).Data, document.Structure.TokenScanner, refs);
|
||||
destinationDict[key] = WriterUtil.CopyToken(context, document.Structure.TokenScanner.Get(ir.Data).Data, document.Structure.TokenScanner, refs);
|
||||
}
|
||||
else
|
||||
{
|
||||
destinationDict[NameToken.Create(item.Key)] = WriterUtil.CopyToken(context, item.Value, document.Structure.TokenScanner, refs);
|
||||
destinationDict[key] = WriterUtil.CopyToken(context, item.Value, document.Structure.TokenScanner, refs);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
var mutableSubDict = new Dictionary<NameToken, IToken>();
|
||||
foreach (var kvp in destSubDict.Data)
|
||||
{
|
||||
mutableSubDict[NameToken.Create(kvp.Key)] = kvp.Value;
|
||||
}
|
||||
|
||||
foreach (var subItem in subDict.Data)
|
||||
{
|
||||
// last copied most important important
|
||||
destinationDict[NameToken.Create(subItem.Key)] = WriterUtil.CopyToken(context, subItem.Value,
|
||||
document.Structure.TokenScanner, refs);
|
||||
// last copied most important
|
||||
mutableSubDict[NameToken.Create(subItem.Key)] = WriterUtil.CopyToken(
|
||||
context,
|
||||
subItem.Value,
|
||||
document.Structure.TokenScanner,
|
||||
refs);
|
||||
}
|
||||
|
||||
destinationDict[key] = new DictionaryToken(mutableSubDict);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -567,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
|
||||
@@ -1190,5 +1346,21 @@ namespace UglyToad.PdfPig.Writer
|
||||
|
||||
context.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for how a page should be copied to the current builder when using AddPage.
|
||||
/// </summary>
|
||||
public class AddPageOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether to preserve annotation objects in the copied page.
|
||||
/// </summary>
|
||||
public bool KeepAnnotations { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Intercept how actions are copied from the source to destination page.
|
||||
/// </summary>
|
||||
public Func<PdfAction, PdfAction?>? CopyLinkFunc { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,236 +1,240 @@
|
||||
namespace UglyToad.PdfPig.Writer
|
||||
namespace UglyToad.PdfPig.Writer;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Actions;
|
||||
using Outline.Destinations;
|
||||
|
||||
/// <summary>
|
||||
/// Merges PDF documents into each other.
|
||||
/// </summary>
|
||||
public static class PdfMerger
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Filters;
|
||||
using Logging;
|
||||
using System.Linq;
|
||||
using UglyToad.PdfPig.Actions;
|
||||
using UglyToad.PdfPig.Outline.Destinations;
|
||||
/// <summary>
|
||||
/// Merge two PDF documents together with the pages from <paramref name="file1"/> followed by <paramref name="file2"/>.
|
||||
/// </summary>
|
||||
public static byte[] Merge(
|
||||
string file1,
|
||||
string file2,
|
||||
IReadOnlyList<int>? file1Selection = null,
|
||||
IReadOnlyList<int>? file2Selection = null,
|
||||
PdfAStandard archiveStandard = PdfAStandard.None,
|
||||
PdfDocumentBuilder.DocumentInformationBuilder? docInfoBuilder = null)
|
||||
{
|
||||
using (var output = new MemoryStream())
|
||||
{
|
||||
Merge(file1, file2, output, file1Selection, file2Selection, archiveStandard, docInfoBuilder);
|
||||
return output.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merges PDF documents into each other.
|
||||
/// Merge two PDF documents together with the pages from <paramref name="file1"/> followed by <paramref name="file2"/> into the output stream.
|
||||
/// </summary>
|
||||
public static class PdfMerger
|
||||
public static void Merge(
|
||||
string file1,
|
||||
string file2,
|
||||
Stream output,
|
||||
IReadOnlyList<int>? file1Selection = null,
|
||||
IReadOnlyList<int>? file2Selection = null,
|
||||
PdfAStandard archiveStandard = PdfAStandard.None,
|
||||
PdfDocumentBuilder.DocumentInformationBuilder? docInfoBuilder = null)
|
||||
{
|
||||
private static readonly ILog Log = new NoOpLog();
|
||||
_ = file1 ?? throw new ArgumentNullException(nameof(file1));
|
||||
_ = file2 ?? throw new ArgumentNullException(nameof(file2));
|
||||
|
||||
private static readonly IFilterProvider FilterProvider = DefaultFilterProvider.Instance;
|
||||
|
||||
/// <summary>
|
||||
/// Merge two PDF documents together with the pages from <paramref name="file1"/> followed by <paramref name="file2"/>.
|
||||
/// </summary>
|
||||
public static byte[] Merge(
|
||||
string file1,
|
||||
string file2,
|
||||
IReadOnlyList<int>? file1Selection = null,
|
||||
IReadOnlyList<int>? file2Selection = null,
|
||||
PdfAStandard archiveStandard = PdfAStandard.None,
|
||||
PdfDocumentBuilder.DocumentInformationBuilder? docInfoBuilder = null)
|
||||
using (var stream1 = File.OpenRead(file1))
|
||||
{
|
||||
using (var output = new MemoryStream())
|
||||
using (var stream2 = File.OpenRead(file2))
|
||||
{
|
||||
Merge(file1, file2, output, file1Selection, file2Selection, archiveStandard, docInfoBuilder);
|
||||
return output.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merge two PDF documents together with the pages from <paramref name="file1"/> followed by <paramref name="file2"/> into the output stream.
|
||||
/// </summary>
|
||||
public static void Merge(
|
||||
string file1,
|
||||
string file2,
|
||||
Stream output,
|
||||
IReadOnlyList<int>? file1Selection = null,
|
||||
IReadOnlyList<int>? file2Selection = null,
|
||||
PdfAStandard archiveStandard = PdfAStandard.None,
|
||||
PdfDocumentBuilder.DocumentInformationBuilder? docInfoBuilder = null)
|
||||
{
|
||||
_ = file1 ?? throw new ArgumentNullException(nameof(file1));
|
||||
_ = file2 ?? throw new ArgumentNullException(nameof(file2));
|
||||
|
||||
using (var stream1 = File.OpenRead(file1))
|
||||
{
|
||||
using (var stream2 = File.OpenRead(file2))
|
||||
{
|
||||
Merge(new[] { stream1, stream2 }, output, new[] { file1Selection, file2Selection }, archiveStandard, docInfoBuilder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merge multiple PDF documents together with the pages in the order the file paths are provided.
|
||||
/// </summary>
|
||||
public static byte[] Merge(params string[] filePaths)
|
||||
{
|
||||
return Merge(PdfAStandard.None, null, filePaths);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merge multiple PDF documents together with the pages in the order the file paths are provided.
|
||||
/// </summary>
|
||||
public static byte[] Merge(PdfAStandard archiveStandard, PdfDocumentBuilder.DocumentInformationBuilder? docInfoBuilder, params string[] filePaths)
|
||||
{
|
||||
using (var output = new MemoryStream())
|
||||
{
|
||||
Merge(output, archiveStandard, docInfoBuilder, filePaths);
|
||||
return output.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merge multiple PDF documents together with the pages in the order the file paths are provided into the output stream
|
||||
/// </summary>
|
||||
public static void Merge(Stream output, params string[] filePaths)
|
||||
{
|
||||
Merge(output, PdfAStandard.None, null, filePaths);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merge multiple PDF documents together with the pages in the order the file paths are provided into the output stream
|
||||
/// </summary>
|
||||
public static void Merge(Stream output, PdfAStandard archiveStandard, PdfDocumentBuilder.DocumentInformationBuilder? docInfoBuilder, params string[] filePaths)
|
||||
{
|
||||
var streams = new List<Stream>(filePaths.Length);
|
||||
try
|
||||
{
|
||||
for (var i = 0; i < filePaths.Length; i++)
|
||||
{
|
||||
var filePath = filePaths[i] ?? throw new ArgumentNullException(nameof(filePaths), $"Null filepath at index {i}.");
|
||||
streams.Add(File.OpenRead(filePath));
|
||||
}
|
||||
|
||||
Merge(streams, output, null, archiveStandard, docInfoBuilder);
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var stream in streams)
|
||||
{
|
||||
stream.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merge the set of PDF documents.
|
||||
/// </summary>
|
||||
public static byte[] Merge(
|
||||
IReadOnlyList<byte[]> files,
|
||||
IReadOnlyList<IReadOnlyList<int>>? pagesBundle = null,
|
||||
PdfAStandard archiveStandard = PdfAStandard.None,
|
||||
PdfDocumentBuilder.DocumentInformationBuilder? docInfoBuilder = null)
|
||||
{
|
||||
_ = files ?? throw new ArgumentNullException(nameof(files));
|
||||
|
||||
using (var output = new MemoryStream())
|
||||
{
|
||||
Merge(files.Select(f => PdfDocument.Open(f)).ToArray(), output, pagesBundle, archiveStandard, docInfoBuilder);
|
||||
return output.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merge the set of PDF documents into the output stream
|
||||
/// The caller must manage disposing the stream. The created PdfDocument will not dispose the stream.
|
||||
/// <param name="streams">
|
||||
/// A list of streams for the files contents, this must support reading and seeking.
|
||||
/// </param>
|
||||
/// <param name="output">Must be writable</param>
|
||||
/// <param name="pagesBundle"></param>
|
||||
/// <param name="archiveStandard"></param>
|
||||
/// <param name="docInfoBuilder"></param>
|
||||
/// </summary>
|
||||
public static void Merge(
|
||||
IReadOnlyList<Stream> streams,
|
||||
Stream output,
|
||||
IReadOnlyList<IReadOnlyList<int>?>? pagesBundle = null,
|
||||
PdfAStandard archiveStandard = PdfAStandard.None,
|
||||
PdfDocumentBuilder.DocumentInformationBuilder? docInfoBuilder = null)
|
||||
{
|
||||
_ = streams ?? throw new ArgumentNullException(nameof(streams));
|
||||
_ = output ?? throw new ArgumentNullException(nameof(output));
|
||||
|
||||
Merge(streams.Select(f => PdfDocument.Open(f)).ToArray(), output, pagesBundle, archiveStandard, docInfoBuilder);
|
||||
}
|
||||
|
||||
private static void Merge(
|
||||
IReadOnlyList<PdfDocument> files,
|
||||
Stream output,
|
||||
IReadOnlyList<IReadOnlyList<int>?>? pagesBundle,
|
||||
PdfAStandard archiveStandard = PdfAStandard.None,
|
||||
PdfDocumentBuilder.DocumentInformationBuilder? docInfoBuilder = null)
|
||||
{
|
||||
var maxVersion = files.Select(x => x.Version).Max();
|
||||
using (var document = new PdfDocumentBuilder(output, false, PdfWriterType.Default, maxVersion))
|
||||
{
|
||||
document.ArchiveStandard = archiveStandard;
|
||||
if (docInfoBuilder != null)
|
||||
{
|
||||
document.IncludeDocumentInformation = true;
|
||||
document.DocumentInformation = docInfoBuilder;
|
||||
}
|
||||
foreach (var fileIndex in Enumerable.Range(0, files.Count))
|
||||
{
|
||||
var existing = files[fileIndex];
|
||||
IReadOnlyList<int>? pages = null;
|
||||
if (pagesBundle != null && fileIndex < pagesBundle.Count)
|
||||
{
|
||||
pages = pagesBundle[fileIndex];
|
||||
}
|
||||
|
||||
var basePageNumber = document.Pages.Count;
|
||||
|
||||
if (pages is null)
|
||||
{
|
||||
for (var i = 1; i <= existing.NumberOfPages; i++)
|
||||
{
|
||||
document.AddPage(existing, i, link => CopyLink(link, n => basePageNumber + n));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var pageNumbers = new Dictionary<int, int>();
|
||||
for (var i = 0; i < pages.Count; i++)
|
||||
{
|
||||
pageNumbers[pages[i]] = basePageNumber + i + 1;
|
||||
}
|
||||
|
||||
foreach (var i in pages)
|
||||
{
|
||||
document.AddPage(existing, i, link => CopyLink(
|
||||
link, n => pageNumbers.TryGetValue(n, out var pageNumber) ? pageNumber : null));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PdfAction? CopyLink(PdfAction action, Func<int, int?> getPageNumber)
|
||||
{
|
||||
var link = action as AbstractGoToAction;
|
||||
if (link is null)
|
||||
{
|
||||
// copy the link if it is not a link to PDF documents
|
||||
return action;
|
||||
}
|
||||
|
||||
var newPageNumber = getPageNumber(link.Destination.PageNumber);
|
||||
if (newPageNumber is null)
|
||||
{
|
||||
// ignore the link if the target page does not exist in the PDF document
|
||||
return null;
|
||||
}
|
||||
|
||||
var newDestination = new ExplicitDestination(newPageNumber.Value, link.Destination.Type, link.Destination.Coordinates);
|
||||
|
||||
return action switch {
|
||||
GoToAction goToAction => new GoToAction(newDestination),
|
||||
GoToEAction goToEAction => new GoToEAction(newDestination, goToEAction.FileSpecification),
|
||||
GoToRAction goToRAction => new GoToRAction(newDestination, goToRAction.Filename),
|
||||
_ => action
|
||||
};
|
||||
Merge(new[] { stream1, stream2 }, output, new[] { file1Selection, file2Selection }, archiveStandard, docInfoBuilder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merge multiple PDF documents together with the pages in the order the file paths are provided.
|
||||
/// </summary>
|
||||
public static byte[] Merge(params string[] filePaths)
|
||||
{
|
||||
return Merge(PdfAStandard.None, null, filePaths);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merge multiple PDF documents together with the pages in the order the file paths are provided.
|
||||
/// </summary>
|
||||
public static byte[] Merge(PdfAStandard archiveStandard, PdfDocumentBuilder.DocumentInformationBuilder? docInfoBuilder, params string[] filePaths)
|
||||
{
|
||||
using (var output = new MemoryStream())
|
||||
{
|
||||
Merge(output, archiveStandard, docInfoBuilder, filePaths);
|
||||
return output.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merge multiple PDF documents together with the pages in the order the file paths are provided into the output stream
|
||||
/// </summary>
|
||||
public static void Merge(Stream output, params string[] filePaths)
|
||||
{
|
||||
Merge(output, PdfAStandard.None, null, filePaths);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merge multiple PDF documents together with the pages in the order the file paths are provided into the output stream
|
||||
/// </summary>
|
||||
public static void Merge(Stream output, PdfAStandard archiveStandard, PdfDocumentBuilder.DocumentInformationBuilder? docInfoBuilder, params string[] filePaths)
|
||||
{
|
||||
var streams = new List<Stream>(filePaths.Length);
|
||||
try
|
||||
{
|
||||
for (var i = 0; i < filePaths.Length; i++)
|
||||
{
|
||||
var filePath = filePaths[i] ?? throw new ArgumentNullException(nameof(filePaths), $"Null filepath at index {i}.");
|
||||
streams.Add(File.OpenRead(filePath));
|
||||
}
|
||||
|
||||
Merge(streams, output, null, archiveStandard, docInfoBuilder);
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var stream in streams)
|
||||
{
|
||||
stream.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merge the set of PDF documents.
|
||||
/// </summary>
|
||||
public static byte[] Merge(
|
||||
IReadOnlyList<byte[]> files,
|
||||
IReadOnlyList<IReadOnlyList<int>>? pagesBundle = null,
|
||||
PdfAStandard archiveStandard = PdfAStandard.None,
|
||||
PdfDocumentBuilder.DocumentInformationBuilder? docInfoBuilder = null)
|
||||
{
|
||||
_ = files ?? throw new ArgumentNullException(nameof(files));
|
||||
|
||||
using (var output = new MemoryStream())
|
||||
{
|
||||
Merge(files.Select(f => PdfDocument.Open(f)).ToArray(), output, pagesBundle, archiveStandard, docInfoBuilder);
|
||||
return output.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merge the set of PDF documents into the output stream
|
||||
/// The caller must manage disposing the stream. The created PdfDocument will not dispose the stream.
|
||||
/// <param name="streams">
|
||||
/// A list of streams for the files contents, this must support reading and seeking.
|
||||
/// </param>
|
||||
/// <param name="output">Must be writable</param>
|
||||
/// <param name="pagesBundle"></param>
|
||||
/// <param name="archiveStandard"></param>
|
||||
/// <param name="docInfoBuilder"></param>
|
||||
/// </summary>
|
||||
public static void Merge(
|
||||
IReadOnlyList<Stream> streams,
|
||||
Stream output,
|
||||
IReadOnlyList<IReadOnlyList<int>?>? pagesBundle = null,
|
||||
PdfAStandard archiveStandard = PdfAStandard.None,
|
||||
PdfDocumentBuilder.DocumentInformationBuilder? docInfoBuilder = null)
|
||||
{
|
||||
_ = streams ?? throw new ArgumentNullException(nameof(streams));
|
||||
_ = output ?? throw new ArgumentNullException(nameof(output));
|
||||
|
||||
Merge(streams.Select(f => PdfDocument.Open(f)).ToArray(), output, pagesBundle, archiveStandard, docInfoBuilder);
|
||||
}
|
||||
|
||||
private static void Merge(
|
||||
IReadOnlyList<PdfDocument> files,
|
||||
Stream output,
|
||||
IReadOnlyList<IReadOnlyList<int>?>? pagesBundle,
|
||||
PdfAStandard archiveStandard = PdfAStandard.None,
|
||||
PdfDocumentBuilder.DocumentInformationBuilder? docInfoBuilder = null)
|
||||
{
|
||||
var maxVersion = files.Select(x => x.Version).Max();
|
||||
using (var document = new PdfDocumentBuilder(output, false, PdfWriterType.Default, maxVersion))
|
||||
{
|
||||
document.ArchiveStandard = archiveStandard;
|
||||
if (docInfoBuilder != null)
|
||||
{
|
||||
document.IncludeDocumentInformation = true;
|
||||
document.DocumentInformation = docInfoBuilder;
|
||||
}
|
||||
foreach (var fileIndex in Enumerable.Range(0, files.Count))
|
||||
{
|
||||
var existing = files[fileIndex];
|
||||
IReadOnlyList<int>? pages = null;
|
||||
if (pagesBundle != null && fileIndex < pagesBundle.Count)
|
||||
{
|
||||
pages = pagesBundle[fileIndex];
|
||||
}
|
||||
|
||||
var basePageNumber = document.Pages.Count;
|
||||
|
||||
if (pages is null)
|
||||
{
|
||||
for (var i = 1; i <= existing.NumberOfPages; i++)
|
||||
{
|
||||
document.AddPage(existing,
|
||||
i,
|
||||
new PdfDocumentBuilder.AddPageOptions
|
||||
{
|
||||
CopyLinkFunc = link => CopyLink(link, n => basePageNumber + n)
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var pageNumbers = new Dictionary<int, int>();
|
||||
for (var i = 0; i < pages.Count; i++)
|
||||
{
|
||||
pageNumbers[pages[i]] = basePageNumber + i + 1;
|
||||
}
|
||||
|
||||
foreach (var i in pages)
|
||||
{
|
||||
document.AddPage(existing,
|
||||
i,
|
||||
new PdfDocumentBuilder.AddPageOptions
|
||||
{
|
||||
CopyLinkFunc = link => CopyLink(
|
||||
link,
|
||||
n => pageNumbers.TryGetValue(n, out var pageNumber) ? pageNumber : null)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PdfAction? CopyLink(PdfAction action, Func<int, int?> getPageNumber)
|
||||
{
|
||||
var link = action as AbstractGoToAction;
|
||||
if (link is null)
|
||||
{
|
||||
// copy the link if it is not a link to PDF documents
|
||||
return action;
|
||||
}
|
||||
|
||||
var newPageNumber = getPageNumber(link.Destination.PageNumber);
|
||||
if (newPageNumber is null)
|
||||
{
|
||||
// ignore the link if the target page does not exist in the PDF document
|
||||
return null;
|
||||
}
|
||||
|
||||
var newDestination = new ExplicitDestination(newPageNumber.Value, link.Destination.Type, link.Destination.Coordinates);
|
||||
|
||||
return action switch {
|
||||
GoToAction => new GoToAction(newDestination),
|
||||
GoToEAction goToEAction => new GoToEAction(newDestination, goToEAction.FileSpecification),
|
||||
GoToRAction goToRAction => new GoToRAction(newDestination, goToRAction.Filename),
|
||||
_ => action
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,16 +140,42 @@
|
||||
contentStreams = new List<IPageContentStream>() { currentStream };
|
||||
}
|
||||
|
||||
internal PdfPageBuilder(int number, PdfDocumentBuilder documentBuilder, IEnumerable<CopiedContentStream> copied,
|
||||
Dictionary<NameToken, IToken> pageDict, List<(DictionaryToken token, PdfAction action)> links)
|
||||
internal PdfPageBuilder(
|
||||
int number,
|
||||
PdfDocumentBuilder documentBuilder,
|
||||
IEnumerable<CopiedContentStream> copied,
|
||||
Dictionary<NameToken, IToken> pageDict,
|
||||
List<(DictionaryToken token, PdfAction action)> links)
|
||||
{
|
||||
this.documentBuilder = documentBuilder ?? throw new ArgumentNullException(nameof(documentBuilder));
|
||||
this.links = links;
|
||||
PageNumber = number;
|
||||
pageDictionary = pageDict;
|
||||
contentStreams = new List<IPageContentStream>();
|
||||
contentStreams.AddRange(copied);
|
||||
currentStream = new DefaultContentStream();
|
||||
contentStreams = new List<IPageContentStream>(copied);
|
||||
|
||||
var writeableContentStream = new DefaultContentStream();
|
||||
if (contentStreams.Count > 0)
|
||||
{
|
||||
var lastGlobalTransform = contentStreams.LastOrDefault(x => x.GlobalTransform.HasValue);
|
||||
|
||||
if (lastGlobalTransform?.GlobalTransform != null)
|
||||
{
|
||||
var inverse = lastGlobalTransform.GlobalTransform.Value.Inverse();
|
||||
writeableContentStream.Add(
|
||||
new ModifyCurrentTransformationMatrix(
|
||||
[
|
||||
inverse.A,
|
||||
inverse.B,
|
||||
inverse.C,
|
||||
inverse.D,
|
||||
inverse.E,
|
||||
inverse.F
|
||||
]
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
currentStream = writeableContentStream;
|
||||
contentStreams.Add(currentStream);
|
||||
}
|
||||
|
||||
@@ -831,7 +857,7 @@
|
||||
// We need to relocate the resources, and we have to make sure that none of the resources collide with
|
||||
// the already written operation's resources
|
||||
|
||||
var resources = pageDictionary.GetOrCreateDict(NameToken.Resources);
|
||||
var resources = pageDictionary.GetOrCreateDict(NameToken.Resources, srcPage.pdfScanner);
|
||||
|
||||
foreach (var set in srcResourceDictionary.Data)
|
||||
{
|
||||
@@ -858,7 +884,7 @@
|
||||
// Since we don't directly add font's to the pages resources, we have to go look at the document's font
|
||||
if (srcResourceDictionary.TryGet(NameToken.Font, srcPage.pdfScanner, out DictionaryToken? fontsDictionary))
|
||||
{
|
||||
var pageFontsDictionary = resources.GetOrCreateDict(NameToken.Font);
|
||||
var pageFontsDictionary = resources.GetOrCreateDict(NameToken.Font, srcPage.pdfScanner);
|
||||
|
||||
foreach (var fontSet in fontsDictionary.Data)
|
||||
{
|
||||
@@ -903,7 +929,7 @@
|
||||
// Since we don't directly add xobjects's to the pages resources, we have to go look at the document's xobjects
|
||||
if (srcResourceDictionary.TryGet(NameToken.Xobject, srcPage.pdfScanner, out DictionaryToken? xobjectsDictionary))
|
||||
{
|
||||
var pageXobjectsDictionary = resources.GetOrCreateDict(NameToken.Xobject);
|
||||
var pageXobjectsDictionary = resources.GetOrCreateDict(NameToken.Xobject, srcPage.pdfScanner);
|
||||
|
||||
foreach (var xobjectSet in xobjectsDictionary.Data)
|
||||
{
|
||||
@@ -945,7 +971,7 @@
|
||||
// Since we don't directly add xobjects's to the pages resources, we have to go look at the document's xobjects
|
||||
if (srcResourceDictionary.TryGet(NameToken.ExtGState, srcPage.pdfScanner, out DictionaryToken? gsDictionary))
|
||||
{
|
||||
var pageGstateDictionary = resources.GetOrCreateDict(NameToken.ExtGState);
|
||||
var pageGstateDictionary = resources.GetOrCreateDict(NameToken.ExtGState, srcPage.pdfScanner);
|
||||
|
||||
foreach (var gstate in gsDictionary.Data)
|
||||
{
|
||||
@@ -987,6 +1013,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Reset the graphics state to what we'd expect to be able to write our content in the correct locations.
|
||||
var globalTransform = PdfContentTransformationReader.GetGlobalTransform(operations);
|
||||
if (globalTransform.HasValue)
|
||||
{
|
||||
var inverse = globalTransform.Value.Inverse();
|
||||
operations.Add(new ModifyCurrentTransformationMatrix(
|
||||
[
|
||||
inverse.A,
|
||||
inverse.B,
|
||||
inverse.C,
|
||||
inverse.D,
|
||||
inverse.E,
|
||||
inverse.F
|
||||
]));
|
||||
}
|
||||
|
||||
destinationStream.Operations.AddRange(operations);
|
||||
|
||||
return this;
|
||||
@@ -1090,9 +1132,18 @@
|
||||
internal interface IPageContentStream : IContentStream
|
||||
{
|
||||
bool ReadOnly { get; }
|
||||
|
||||
bool HasContent { get; }
|
||||
|
||||
void Add(IGraphicsStateOperation operation);
|
||||
|
||||
IndirectReferenceToken Write(IPdfStreamWriter writer);
|
||||
|
||||
/// <summary>
|
||||
/// If this content stream applied any global transform to the graphics state this will
|
||||
/// tell you which one is currently active at the end of this stream being applied.
|
||||
/// </summary>
|
||||
TransformationMatrix? GlobalTransform { get; }
|
||||
}
|
||||
|
||||
internal class DefaultContentStream : IPageContentStream
|
||||
@@ -1109,8 +1160,11 @@
|
||||
}
|
||||
|
||||
public bool ReadOnly => false;
|
||||
|
||||
public bool HasContent => operations.Any();
|
||||
|
||||
public TransformationMatrix? GlobalTransform => null;
|
||||
|
||||
public void Add(IGraphicsStateOperation operation)
|
||||
{
|
||||
operations.Add(operation);
|
||||
@@ -1139,11 +1193,18 @@
|
||||
internal class CopiedContentStream : IPageContentStream
|
||||
{
|
||||
private readonly IndirectReferenceToken token;
|
||||
|
||||
public bool ReadOnly => true;
|
||||
|
||||
public bool HasContent => true;
|
||||
|
||||
public CopiedContentStream(IndirectReferenceToken indirectReferenceToken)
|
||||
public TransformationMatrix? GlobalTransform { get; }
|
||||
|
||||
public CopiedContentStream(
|
||||
IndirectReferenceToken indirectReferenceToken,
|
||||
TransformationMatrix? globalTransform)
|
||||
{
|
||||
GlobalTransform = globalTransform;
|
||||
token = indirectReferenceToken;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,13 +12,36 @@
|
||||
|
||||
internal static class WriterUtil
|
||||
{
|
||||
public static Dictionary<string, IToken> GetOrCreateDict(this Dictionary<NameToken, IToken> dict, NameToken key)
|
||||
public static Dictionary<string, IToken> GetOrCreateDict<T>(
|
||||
this Dictionary<T, IToken> dict,
|
||||
T key,
|
||||
IPdfTokenScanner? sourceScanner = null) where T : notnull
|
||||
{
|
||||
if (dict.TryGetValue(key, out var item))
|
||||
{
|
||||
if (!(item is DictionaryToken dt))
|
||||
var chainCount = 0;
|
||||
var itemChain = item;
|
||||
while (itemChain is IndirectReferenceToken ir && chainCount < 100)
|
||||
{
|
||||
throw new ApplicationException("Expected dictionary token, got " + item.GetType());
|
||||
if (sourceScanner == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
itemChain = sourceScanner.Get(ir.Data);
|
||||
|
||||
chainCount++;
|
||||
|
||||
if (itemChain is ObjectToken ot)
|
||||
{
|
||||
itemChain = ot.Data;
|
||||
}
|
||||
}
|
||||
|
||||
if (itemChain is not DictionaryToken dt)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"While trying to copy token called {key} which should have been a dictionary token we found a token of type {item.GetType()}");
|
||||
}
|
||||
|
||||
if (dt.Data is Dictionary<string, IToken> mutable)
|
||||
@@ -37,30 +60,6 @@
|
||||
return created;
|
||||
}
|
||||
|
||||
public static Dictionary<string, IToken> GetOrCreateDict(this Dictionary<string, IToken> dict, string key)
|
||||
{
|
||||
if (dict.TryGetValue(key, out var item))
|
||||
{
|
||||
if (!(item is DictionaryToken dt))
|
||||
{
|
||||
throw new ApplicationException("Expected dictionary token, got " + item.GetType());
|
||||
}
|
||||
|
||||
if (dt.Data is Dictionary<string, IToken> mutable)
|
||||
{
|
||||
return mutable;
|
||||
}
|
||||
|
||||
mutable = dt.Data.
|
||||
ToDictionary(x => x.Key, x => x.Value);
|
||||
dict[key] = DictionaryToken.With(mutable);
|
||||
return mutable;
|
||||
}
|
||||
|
||||
var created = new Dictionary<string, IToken>();
|
||||
dict[key] = DictionaryToken.With(created);
|
||||
return created;
|
||||
}
|
||||
/// <summary>
|
||||
/// The purpose of this method is to resolve indirect reference. That mean copy the reference's content to the new document's stream
|
||||
/// and replace the indirect reference with the correct/new one
|
||||
|
||||
@@ -92,7 +92,6 @@
|
||||
null);
|
||||
|
||||
softMaskImage = ReadImage(maskImageRecord, pdfScanner, filterProvider, resourceStore);
|
||||
System.Diagnostics.Debug.Assert(softMaskImage.ColorSpaceDetails?.IsStencil == true);
|
||||
}
|
||||
|
||||
var isJpxDecode = dictionary.TryGet(NameToken.Filter, out NameToken filterName) && filterName.Equals(NameToken.JpxDecode);
|
||||
|
||||
Reference in New Issue
Block a user