Compare commits

...

32 Commits

Author SHA1 Message Date
EliotJones
6b764e7c20 rogue tab 2025-07-26 12:51:40 -05:00
EliotJones
fed46965ff tabs to spaces 2025-07-26 12:51:02 -05:00
EliotJones
03fa53ece0 update readme to avoid people using page.Text or asking about editing docs
we need to be more clear because beloved chat gpt falls into the trap of
recommending `page.Text` when asked about the library even though this
text is usually the wrong field to use
2025-07-26 12:49:42 -05:00
EliotJones
50f878b2ba restore copy link func logic 2025-07-25 18:18:22 +01:00
EliotJones
2a10b6c285 make link copying more tolerant when adding page
in #1082 and other issues relating to annotations we're running into
constraints of the current model of building a pdf document. currently
we skip all link type annotations, i think we can support copying of links
where the link destination is outside the current document. however the
more i look at this code the more i think we need a radical redesign of
how document building is done because it has been pushed far beyond
its current capabilities, i'll detail my thinking in the related pr in more
detail
2025-07-25 18:18:22 +01:00
EliotJones
85fc63d585 rework numeric tokenizer hot path
the existing numeric tokenizer involved allocations and string parsing. since
the number formats in pdf files are fairly predictable we can improve this
substantially
2025-07-25 18:12:43 +01:00
EliotJones
5abdfcb96c fix test case due to field renaming 2025-07-20 20:33:46 +01:00
EliotJones
00ca268092 move last uncovered operators to switch statement
in order to remove reflection from the core content stream operators
construction we ensure all types covered by the operations dictionary
have corresponding switch statement support. this moves the remaining
2 operators to the switch statement. fix for #1062
2025-07-20 20:33:46 +01:00
BobLd
813d3baa18 Track IndirectReference instead of only ObjectNumber when checking for cycles during indirect reference resolutionv and add test 2025-07-20 19:24:31 +01:00
EliotJones
2b11961c8c remove debug asserts causing test failures
we encountered a fence constructed in the middle of a field for an unknown
reason so we demolished it. i think this was intended to catch flaws in the
parser logic but the reality is in a pdf anything can happen so we no longer
want to catch these issues and this restores a green test run in debug mode.

fix for #915
2025-07-20 17:42:34 +01:00
EliotJones
efb8c2a803 i merged a pr which broke the build, this updates the build to work
move all arguments to add page to a setting object so it can be extended
in future in a non-breaking api change
2025-07-20 17:36:19 +01:00
jan-sutter
e636212ec8 check for cycles during indirect reference resolution (#1097)
Co-authored-by: Jan Sutter <jan@suttermail.de>
2025-07-20 11:12:55 -05:00
EnraH
3b318e1944 add option to strip annotation (#492)
* add option to strip annotation

* fix implementation and tests

---------

Co-authored-by: arne.hansen <arne.hansen@digitecgalaxus.ch>
Co-authored-by: Eliot Jones <elioty@hotmail.co.uk>
2025-07-20 11:10:15 -05:00
EliotJones
377eb507e8 when writing content to an existing page inverse any global transform #614
when adding a page to a builder from an existing document using either
addpage or copyfrom methods the added page's content stream can contain
a global transform matrix change that will subsequently change all the locations
of any modifications made by the user. here whenever using an existing stream
we apply the inverse of any active transformation matrix

there could be a bug here where if you use 'copy from' with a global transform
active, we then apply the inverse, and you use 'copy from' again to the same
destination page our inverse transform is now active and could potentially
affect the second stream, but I don't think it will
2025-07-20 00:53:03 +01:00
BobLd
ff4e763192 Update hack for 1bpc + DeviceGray 2025-07-19 21:45:41 +01:00
BobLd
6a06452103 Remove decode parameter application from Stencil color space for consistency 2025-07-19 13:45:06 +01:00
BobLd
a5e92cd11c Update run_common_crawl_tests.yml 2025-07-19 12:21:10 +01:00
EliotJones
4bf746c747 add new action to run integration against common crawl corpus 2025-07-19 11:49:34 +01:00
EliotJones
bffd51425d support bfrange having incorrect length in a cmap
the corpus file 0001413.pdf has an off-by-one error in its count
for cmap bfranges. here we exit early if an unexpected
endbfrange operator is encountered early. this matches the pdfbox
behavior:

067d56e4db/fontbox/src/main/java/org/apache/fontbox/cmap/CMapParser.java (L373)
2025-07-19 11:48:04 +01:00
Eliot Jones
e3388ec6b6 fix colorspace error when form xobject contains a transparency group (#1088)
* fix colorspace error when form xobject contains a transparency group

when a form xobject contains a reference to a group xobject this can only
be used to change attributes of the transparency imaging model. the old
code was setting the main colorspaces incorrectly causing errors when the
transparency component had a different number of channels. this was
causing #1071 in addition to the failure in file 0000355.pdf of the test corpus

* add master integration tests for corpus group 0000

* tidy up actions

* remove invalid reference in echo

* move new action to different branch
2025-07-19 11:46:56 +01:00
EliotJones
31658ca020 allow reading to continue if encountering an invalid surrogate pair
investigating the corpus at
https://digitalcorpora.s3.amazonaws.com/s3_browser.html#corpora/files/CC-MAIN-2021-31-PDF-UNTRUNCATED/zipfiles/0000-0999/

the input file 0000000.pdf contained a utf-16 surrogate pair in an input
defined as usc2. the approach of various parsers varies here, adobe
acrobat seems to hard crash, pdf js returns the same text we now
parse, chrome parses the intended text (2 invalid characters and
"ib exam"). we don't care too much about matching chrome exactly
so doing the same as firefox is fine here
2025-07-16 07:45:40 +01:00
EliotJones
1021729727 fall back to times-roman as standard 14 font when lenient
if parsing in lenient mode and encountering a malformed base name
(in this case 'helveticai') we fallback to times-roman as the adobe font
metrics file for a standard 14 font. this aligns with the behavior of pdfbox.
we also log a more informative error in non-lenient modes

this fixes document 0000086.pdf from the corpus
2025-07-16 07:43:49 +01:00
Eliot Jones
9503f9c137 fix off-by-one and optimize brute force xref search #1078 (#1079)
* fix off-by-one and optimize brute force xref search #1078

when performing a brute force xref search we were ending up
off-by-one, update the search to use a ring buffer to reduce
seeking and fix xref detection

* make method testable and add test coverage

* normalize test input on other platforms

* seal circular buffer class
2025-07-16 07:35:24 +01:00
Eliot Jones
016b754c5b back-calculate first char if last char and widths present (#1081)
* back-calculate first char if last char and widths present

when a truetype font has a last char and widths array in its font
dictionary the first char can be calculated #644

* fix off by 1 in last char calculation
2025-07-14 21:57:01 +01:00
Eliot Jones
de3b6ac6f4 use correct bounding boxes for standard 14 glyphs #850 (#1080)
* use correct bounding boxes for standard 14 glyphs #850

previously every bounding box for type 1 standard 14 fonts was assumed
to start at 0,0 and ignored the bounding box data in the font metrics file.
now we correctly read the glyph bounding box while preserving the
existing advance width values for advancing the renderer position

* update test case for new logic
2025-07-14 21:54:42 +01:00
EliotJones
b11f936f22 fix copying of sub-dictionary when keys collide
when copying from a ancestor node of a page's resource dictionary
we were incorrectly writing nested nodes of e.g. /fonts to the root
of the target dictionary, here we write to the intended target node
correctly
2025-07-10 18:32:20 +01:00
EliotJones
7fe60ff8c3 skip single letter final blocks
align with the behavior of pdfbox and c implementations where
single character final blocks are ignored rather than being written.
also makes the error more informative in case it is ever encountered
again.

add more test cases.

it is possible this is hiding the problem and will move the error elsewhere
but this matches the implementation behavior of the 2 reference
implementations. one other potential source for the error is if pdf supports
'<~' as a start of data marker which i can't find in the spec but wikipedia
says might be possible? without documents to trigger the error i think
this is the best fix for now
2025-07-09 07:33:12 +01:00
EliotJones
781991b6bf fix #670 by ignoring duplicate endstream definitions
when parsing a stream object with multiple endstream tokens
the last parsed token was selected instead of the actual stream
token so instead we just skip all following tokens if the first
is a stream and the following tokens are `endstream` operators
only
2025-07-07 20:34:26 +01:00
EliotJones
daaac9350d writer util did not follow reference links #1032
when copying various dictionaries from a source document
to the builder any indirect references in the source document
would throw because the code expected the dictionary token
directly. now we follow the list of indirect references until we
find a non-indirect leaf token. also changes the exception type.
2025-07-06 07:15:35 +01:00
EliotJones
f099dd5827 add test coverage to stream scanning 2025-07-06 07:13:55 +01:00
EliotJones
0586713da3 skip comments in pdf objects streams #926
the file provided in issue #926 contains the following syntax
in pdf object streams:

```
% 750 0 obj
<< >>
```

currently we read the comment token and skip the rest
however this producer is writing nonsense to the stream.
comment tokens are only valid outside streams in pdf files
so we align to the behavior of pdfbox here by skipping the
entire line containing a comment inside a stream which fixes
parsing this file.
2025-07-06 07:13:55 +01:00
xufeng
62612588c8 Fix bug in PngFromPdfImageFactory where softmask is wrongly referenced. 2025-07-06 07:10:11 +01:00
44 changed files with 1801 additions and 901 deletions

View 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"

View File

@@ -2,19 +2,11 @@
# PdfPig
[![Gitter](https://badges.gitter.im/pdfpig/community.svg)](https://gitter.im/pdfpig/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
[![nuget](https://img.shields.io/nuget/dt/PdfPig)](https://www.nuget.org/packages/PdfPig/)
[![Build and test](https://github.com/UglyToad/PdfPig/actions/workflows/build_and_test.yml/badge.svg)](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.
![Image shows three words 'Write something in' in 2 sections, the top section is the normal PDF output, the bottom section is the same text with 3 word bounding boxes in pink and letter bounding boxes in blue-green](https://raw.githubusercontent.com/UglyToad/Pdf/master/documentation/Letters/example-text-extraction.png)
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.

View File

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

View File

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

View File

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

After

Width:  |  Height:  |  Size: 79 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -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 != '>')
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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