Compare commits

..

28 Commits

Author SHA1 Message Date
EliotJones
f8af7daeeb restore copy link func logic 2025-07-24 19:00:53 -05:00
EliotJones
d7f0fd96d8 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-23 20:56:26 -05: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
43 changed files with 1647 additions and 929 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

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

@@ -1,25 +1,36 @@
namespace UglyToad.PdfPig.Tests.Integration
{
using System.Diagnostics;
//using System.Diagnostics;
/// <summary>
/// A class for testing files which are not checked in to source control.
/// </summary>
public class LocalTests
{
[Fact]
public void Tests()
{
var file = File.ReadAllBytes(@"D:\temp\200708170550023.pdf");
using (var document = PdfDocument.Open(file, new ParsingOptions { UseLenientParsing = false }))
{
for (var i = 1; i <= document.NumberOfPages; i++)
{
var page = document.GetPage(i);
var text = page.Text;
Trace.WriteLine(text);
}
}
}
//[Fact]
//public void Tests()
//{
// var files = Directory.GetFiles(@"C:\temp\pdfs", "*.pdf");
// foreach (var file in files)
// {
// try
// {
// using (var document = PdfDocument.Open(file, new ParsingOptions { UseLenientParsing = false }))
// {
// for (var i = 1; i <= document.NumberOfPages; i++)
// {
// var page = document.GetPage(i);
// var text = page.Text;
// Trace.WriteLine(text);
// }
// }
// }
// catch (Exception ex)
// {
// throw new InvalidOperationException($"Error parsing: {Path.GetFileName(file)}.", ex);
// }
// }
//}
}
}

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

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

@@ -406,49 +406,6 @@ endobj";
Assert.Equal(7, token.Number.ObjectNumber);
}
[Fact]
public void ReadsStreamWithDoubleEndstreamSimple()
{
const string s =
"""
250 0 obj
<< /Filter /FlateDecode >>
stream
012endstream
endstream
endobj
""";
var scanner = GetScanner(s);
var tokens = ReadToEnd(scanner);
}
[Fact]
public void ReadsStreamWithDoubleEndstream()
{
const string s =
"""
1974 0 obj
<<
/Filter /FlateDecode
/Length 1975 0 R
>>
stream
]ÔÏ@ñ'ð;øØ"Œg !Ué…Cÿ¨´ ö:B*Æ2äÀÛw¿™MZõ'þ°½ë]<ï>ïÆÓ­^|Ÿ/Ý>Ýêá4ösº^^ç.ÕÇôr«e[÷§îVÎü؝SµÈ7ïï×[:ïÆáRm6ÕâGþðzïõ‡Oýå˜>VosŸæÓøRøõ¼ÏçûםÎi¼ÕMµÝÖ}òƒ¾¦¯‡sª~ÛîϟŸn÷‡|Ïß+~Þ§T·~¾ŒÉt—>]§C—æÃøªM»ÜÖ­ÒØÿ÷ÙJããðïµ~†&msh ­YK4BK0yÈ¿rXVzš°Žà}$<zЁðDxò`þÐáAGÂ1:BÏða{B{$$Bа& „!ÂSÒä¿ýCC€B£ePHx´x
R<˜º@!á!>,âW@!á!¼œ@!áÑ2uBÂC=@!á¡þP(¤xðU
R< (¤°PHx(SW(4<S(4<´#@¡á¡ÌT¡Ð²><@¡á¡Œ¢PhxSW(4<õ¡Phxè Ç£PhY|Q
GëÃB¡e}à¡Phx˜¿ B¡áÑú°Phx´ÆÔ
+,ƒÂÂ#/× °²>3(¬.¡nPXx˜_ùC¡°²>x}ƒÂÂCx9ƒÂНoPXxˆš&ù!ÙگŠÿ jbky yyÛJØlØßw±îužóæ¦ï\ìY§1½ï«Ó.ÿùz°gAendstream
endstream
endobj
""";
var scanner = GetScanner(s);
var tokens = ReadToEnd(scanner);
}
[Fact]
public void ReadsStringsWithMissingEndBracket()
{

View File

@@ -1,6 +1,7 @@
namespace UglyToad.PdfPig.Tests.Util;
using PdfPig.Util;
using System;
public class CircularByteBufferTests
{
@@ -43,4 +44,4 @@ public class CircularByteBufferTests
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

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

@@ -10,15 +10,11 @@
using Core;
using Encryption;
using Filters;
using System.Text;
using Tokens;
using Util;
internal class PdfTokenScanner : IPdfTokenScanner
{
private static ReadOnlySpan<byte> EndstreamBytes => "endstream"u8;
private static ReadOnlySpan<byte> EndObjBytes => "endobj"u8;
private static ReadOnlySpan<byte> StartstreamBytes => "stream"u8;
private static readonly Regex EndsWithNumberRegex = new Regex(@"(?<=^[^\s\d]+)\d+$");
@@ -168,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;
}
@@ -182,22 +176,20 @@
coreTokenScanner.Seek(previousTokenPositions[2]);
break;
}
if (readTokens.Count == 1)
{
// An obj was encountered after reading the actual token and the object and generation number of the following token.
var actualReference = new IndirectReference(objectNumber.Int, generation.Int);
var actualToken = encryptionHandler.Decrypt(actualReference, readTokens[0]);
CurrentToken = new ObjectToken(startPosition, actualReference, actualToken);
readTokens.Clear();
coreTokenScanner.Seek(previousTokenPositions[2]);
return true;
}
// This should never happen.
Debug.Assert(false, $"Encountered a '{coreTokenScanner.CurrentToken}' operator before the end of the previous object.");
return false;
}
@@ -272,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];
@@ -315,9 +313,6 @@
{
stream = null;
// Used for shared reading of "stream", "endstream" and "endobj" candidates.
var buffer = new byte[EndstreamBytes.Length];
DictionaryToken streamDictionaryToken = GetStreamDictionary();
// Get the expected length from the stream dictionary if present.
@@ -329,7 +324,7 @@
}
// Verify again that we start with "stream"
var hasStartStreamToken = ReadStreamTokenStart(inputBytes, startStreamTokenOffset, buffer);
var hasStartStreamToken = ReadStreamTokenStart(inputBytes, startStreamTokenOffset);
if (!hasStartStreamToken)
{
return false;
@@ -356,9 +351,9 @@
{
inputBytes.Seek(inputBytes.CurrentOffset - 1);
}
break;
}
} while ((char)inputBytes.CurrentByte != '\n');
// Store where we started reading the first byte of data.
@@ -367,7 +362,16 @@
// Store how many bytes we have read for checking against Length.
long read = 0;
if (TryReadUsingLength(inputBytes, length, startDataOffset, buffer, out var streamData))
// We want to check if we ever read 'endobj' or 'endstream'.
int endObjPosition = 0;
int endStreamPosition = 0;
int commonPartPosition = 0;
const string endWordPart = "end";
const string streamPart = "stream";
const string objPart = "obj";
if (TryReadUsingLength(inputBytes, length, startDataOffset, out var streamData))
{
stream = new StreamToken(streamDictionaryToken, streamData);
return true;
@@ -377,100 +381,99 @@
PossibleStreamEndLocation? possibleEndLocation = null;
// We're looking for either 'endobj' or 'endstream', so we look at every 'e'.
const byte sentinelByte = (byte)'e';
var queue = new CircularByteBuffer(EndstreamBytes.Length + 1);
var sentinelPosQueue = new Queue<long>();
var endLocations = new Stack<long>();
while (inputBytes.MoveNext())
{
if (inputBytes.CurrentByte == sentinelByte)
if (length.HasValue && read == length)
{
sentinelPosQueue.Enqueue(inputBytes.CurrentOffset);
queue.Add(inputBytes.CurrentByte);
// TODO: read ahead and check we're at the end...
// break;
}
else if (sentinelPosQueue.Count > 0)
// We are reading 'end' (possibly).
if (commonPartPosition < endWordPart.Length && inputBytes.CurrentByte == endWordPart[commonPartPosition])
{
if (ReadHelper.IsWhitespace(inputBytes.CurrentByte))
commonPartPosition++;
}
else if (commonPartPosition == endWordPart.Length)
{
// We are reading 'stream' after 'end'
if (inputBytes.CurrentByte == streamPart[endStreamPosition])
{
// Normalize whitespace
queue.Add((byte)' ');
endObjPosition = 0;
endStreamPosition++;
// We've finished reading 'endstream', add it to the end tokens we've seen.
if (endStreamPosition == streamPart.Length && (!inputBytes.MoveNext() || ReadHelper.IsWhitespace(inputBytes.CurrentByte)))
{
var token = new PossibleStreamEndLocation(inputBytes.CurrentOffset - OperatorToken.EndStream.Data.Length, OperatorToken.EndStream);
possibleEndLocation = token;
if (length.HasValue && read > length)
{
break;
}
endStreamPosition = 0;
}
}
else if (inputBytes.CurrentByte == objPart[endObjPosition])
{
// We are reading 'obj' after 'end'
endStreamPosition = 0;
endObjPosition++;
// We have finished reading 'endobj'.
if (endObjPosition == objPart.Length)
{
// If we saw an 'endstream' or 'endobj' previously we've definitely hit the end now.
if (possibleEndLocation != null)
{
var lastEndToken = possibleEndLocation.Value;
inputBytes.Seek(lastEndToken.Offset + lastEndToken.Type.Data.Length + 1);
break;
}
var token = new PossibleStreamEndLocation(inputBytes.CurrentOffset - OperatorToken.EndObject.Data.Length, OperatorToken.EndObject);
possibleEndLocation = token;
if (read > length)
{
break;
}
}
}
else
{
queue.Add(inputBytes.CurrentByte);
// We were reading 'end' but then we had a character mismatch.
// Reset all the counters.
endStreamPosition = 0;
endObjPosition = 0;
commonPartPosition = 0;
}
}
else
{
// For safety reset every counter in case we had a partial read.
bool hasDequeuePotential;
do
{
hasDequeuePotential = false;
var currPos = sentinelPosQueue.Peek();
var distanceFromSentinel = inputBytes.CurrentOffset - currPos;
if (distanceFromSentinel > EndstreamBytes.Length)
{
sentinelPosQueue.Dequeue();
hasDequeuePotential = sentinelPosQueue.Count > 0;
}
if (distanceFromSentinel == EndstreamBytes.Length)
{
var isEndStream = queue.EndsWith("endstream ");
if (isEndStream)
{
endLocations.Push(currPos);
sentinelPosQueue.Clear();
}
else
{
sentinelPosQueue.Dequeue();
}
}
else if (distanceFromSentinel == EndObjBytes.Length)
{
var isEndObj = queue.EndsWith("endobj ");
if (isEndObj)
{
endLocations.Push(-currPos);
sentinelPosQueue.Clear();
}
else
{
sentinelPosQueue.Dequeue();
}
}
} while (hasDequeuePotential);
endStreamPosition = 0;
endObjPosition = 0;
commonPartPosition = (inputBytes.CurrentByte == endWordPart[0]) ? 1 : 0;
}
read++;
}
if (sentinelPosQueue.Count > 0)
{
var isEndObj = queue.EndsWith("endobj");
if (isEndObj)
{
var location = inputBytes.CurrentOffset - EndObjBytes.Length + 1;
endLocations.Push(-location);
}
else
{
var isEndStr = queue.EndsWith("endstream");
if (isEndStr)
{
endLocations.Push(inputBytes.CurrentOffset - EndstreamBytes.Length + 1);
}
}
}
long streamDataEnd = inputBytes.CurrentOffset + 1;
if (possibleEndLocation == null)
{
return false;
}
var lastEnd = possibleEndLocation;
@@ -501,12 +504,7 @@
return true;
}
private static bool TryReadUsingLength(
IInputBytes inputBytes,
long? length,
long startDataOffset,
byte[] buffer,
[NotNullWhen(true)] out byte[]? data)
private static bool TryReadUsingLength(IInputBytes inputBytes, long? length, long startDataOffset, [NotNullWhen(true)] out byte[]? data)
{
data = null;
@@ -515,6 +513,8 @@
return false;
}
var readBuffer = new byte[EndstreamBytes.Length];
var newlineCount = 0;
inputBytes.Seek(length.Value + startDataOffset);
@@ -535,17 +535,20 @@
}
}
var readLength = inputBytes.Read(buffer);
var readLength = inputBytes.Read(readBuffer);
if (readLength != EndstreamBytes.Length)
if (readLength != readBuffer.Length)
{
return false;
}
if (!ByteArraysEqual(buffer, EndstreamBytes))
for (var i = 0; i < EndstreamBytes.Length; i++)
{
inputBytes.Seek(startDataOffset);
return false;
if (readBuffer[i] != EndstreamBytes[i])
{
inputBytes.Seek(startDataOffset);
return false;
}
}
inputBytes.Seek(startDataOffset);
@@ -559,7 +562,7 @@
throw new InvalidOperationException($"Reading using the stream length failed to read as many bytes as the stream specified. Wanted {length.Value}, got {countRead} at {startDataOffset + 1}.");
}
inputBytes.Read(buffer);
inputBytes.Read(readBuffer);
// Skip for the line break before 'endstream'.
for (var i = 0; i < newlineCount; i++)
{
@@ -656,29 +659,22 @@
return length;
}
private static bool ReadStreamTokenStart(IInputBytes input, long tokenStart, byte[] buffer)
private static bool ReadStreamTokenStart(IInputBytes input, long tokenStart)
{
input.Seek(tokenStart);
var readCount = input.Read(buffer);
if (readCount < StartstreamBytes.Length
|| !ByteArraysEqual(buffer.AsSpan(0, StartstreamBytes.Length), StartstreamBytes))
for (var i = 0; i < OperatorToken.StartStream.Data.Length; i++)
{
input.Seek(tokenStart);
return false;
if (!input.MoveNext() || input.CurrentByte != OperatorToken.StartStream.Data[i])
{
input.Seek(tokenStart);
return false;
}
}
input.Seek(tokenStart + StartstreamBytes.Length);
return true;
}
private static bool ByteArraysEqual(ReadOnlySpan<byte> array1, ReadOnlySpan<byte> array2)
{
return array1.SequenceEqual(array2);
}
public bool TryReadToken<T>(out T token) where T : class, IToken
{
if (isDisposed)
@@ -718,7 +714,7 @@
coreTokenScanner.DeregisterCustomTokenizer(tokenizer);
}
public ObjectToken? Get(IndirectReference reference)
{
if (isDisposed)
@@ -852,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

@@ -1,89 +1,90 @@
namespace UglyToad.PdfPig.Util;
using System;
using System.Text;
internal class CircularByteBuffer(int size)
internal sealed class CircularByteBuffer(int size)
{
private readonly byte[] buffer = new byte[size];
private readonly byte[] buffer = new byte[size];
private int start;
private int count;
private int start;
private int count;
public void Add(byte b)
public void Add(byte b)
{
var insertionPosition = (start + count) % buffer.Length;
buffer[insertionPosition] = b;
if (count < buffer.Length)
{
var insertionPosition = (start + count) % buffer.Length;
count++;
}
else
{
start = (start + 1) % 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;
}
public bool EndsWith(string s)
for (var i = 0; i < s.Length; i++)
{
if (s.Length > count)
var str = s[i];
var inBuffer = count - (s.Length - i);
var buff = buffer[IndexToBufferIndex(inBuffer)];
if (buff != str)
{
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)
return true;
}
public bool IsCurrentlyEqual(string s)
{
if (s.Length > buffer.Length)
{
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;
}
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()
return true;
}
public ReadOnlySpan<byte> AsSpan()
{
Span<byte> tmp = new byte[count];
for (int i = 0; i < count; i++)
{
Span<byte> tmp = new byte[count];
for (int i = 0; i < count; i++)
{
tmp[i] = buffer[IndexToBufferIndex(i)];
}
return tmp;
tmp[i] = buffer[IndexToBufferIndex(i)];
}
public override string ToString()
{
return Encoding.ASCII.GetString(AsSpan());
}
return tmp;
}
private int IndexToBufferIndex(int i) => (start + i) % buffer.Length;
}
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);