Compare commits

..

1 Commits

Author SHA1 Message Date
EliotJones
282146f389 change pdf stream end token to use queue 2025-07-06 16:24:30 -05:00
44 changed files with 1020 additions and 1652 deletions

View File

@@ -1,58 +0,0 @@
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,39 +34,10 @@ O<DJ+*.@<*K0@<6L(Df-\0Ec5e;DffZ(EZee.Bl.9pF""AGXBPCsi + DGm >@3BB / F * &OCAfu2
text);
}
[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)
[Fact]
public void ReplacesZWithEmptyBytes()
{
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 bytes = Encoding.ASCII.GetBytes("9jqo^zBlbD-");
var result = filter.Decode(bytes, dictionary, TestFilterProvider.Instance, 1);
@@ -76,7 +47,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(decoded, text);
Assert.Equal("Man \0\0\0\0is d", text);
}
[Fact]
@@ -89,17 +60,14 @@ O<DJ+*.@<*K0@<6L(Df-\0Ec5e;DffZ(EZee.Bl.9pF""AGXBPCsi + DGm >@3BB / F * &OCAfu2
Assert.Throws<InvalidOperationException>(action);
}
[Theory]
[InlineData("@rH:%B", "cool")]
[InlineData("A~>", "")]
[InlineData("@rH:%A~>", "cool")]
public void SingleCharacterLastIgnores(string encoded, string decoded)
[Fact]
public void SingleCharacterLastThrows()
{
var bytes = Encoding.ASCII.GetBytes(encoded);
var bytes = Encoding.ASCII.GetBytes("9jqo^B");
var result = filter.Decode(bytes, dictionary, TestFilterProvider.Instance, 1);
Action action = () => filter.Decode(bytes, dictionary, TestFilterProvider.Instance, 1);
Assert.Equal(decoded, Encoding.ASCII.GetString(result.ToArray()));
Assert.Throws<ArgumentOutOfRangeException>(action);
}
private const string PdfContent = @"1 0 obj

View File

@@ -146,72 +146,6 @@ 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,20 +68,12 @@
[Fact]
public void ReflectionGraphicsStateOperationFactoryKnowsAllOperations()
{
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 operationsField = typeof(ReflectionGraphicsStateOperationFactory).GetField("operations", BindingFlags.NonPublic | BindingFlags.Static);
var operationDictionary = operationsField.GetValue(null) as IReadOnlyDictionary<string, Type>;
Assert.NotNull(operationDictionary);
var allOperations = GetOperationTypes();
Assert.Equal(allOperations.Count, operationDictionary.Count);
Assert.Equal(allOperations.Count(), operationDictionary.Count);
var mapped = allOperations.Select(o =>
{
@@ -92,14 +84,14 @@
Assert.Equivalent(operationDictionary, mapped, strict: true);
}
private static IReadOnlyList<Type> GetOperationTypes()
private static IEnumerable<Type> GetOperationTypes()
{
var assembly = Assembly.GetAssembly(typeof(IGraphicsStateOperation));
var operationTypes = assembly.GetTypes().Where(x => typeof(IGraphicsStateOperation).IsAssignableFrom(x)
&& !x.IsInterface);
return operationTypes.ToList();
return operationTypes;
}
private static object[] GetConstructorParameters(ParameterInfo[] parameters)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

View File

@@ -103,30 +103,6 @@
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()
{
@@ -172,12 +148,11 @@
var decodedBytes = ImageHelpers.LoadFileBytes("ccittfax-decoded.bin");
var image = new TestPdfImage
{
ColorSpaceDetails = IndexedColorSpaceDetails.Stencil(DeviceGrayColorSpaceDetails.Instance),
ColorSpaceDetails = IndexedColorSpaceDetails.Stencil(DeviceGrayColorSpaceDetails.Instance, new[] { 1.0, 0 }),
DecodedBytes = decodedBytes,
WidthInSamples = 1800,
HeightInSamples = 3113,
BitsPerComponent = 1,
Decode = [1.0, 0]
BitsPerComponent = 1
};
Assert.True(PngFromPdfImageFactory.TryGenerate(image, out var bytes));

View File

@@ -7,27 +7,6 @@
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,36 +1,25 @@
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 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);
// }
// }
//}
[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);
}
}
}
}
}

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(74, minX, 0); // If cropping is not applied correctly, these values will be off
Assert.Equal(72, 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

@@ -1,128 +0,0 @@
namespace UglyToad.PdfPig.Tests.Parser.FileStructure;
using PdfPig.Parser.FileStructure;
using PdfPig.Tokenization.Scanner;
using PdfPig.Tokens;
public class XrefOffsetValidatorTests
{
[Fact]
public void FindsTwoXrefs()
{
var content =
"""
%PDF-1.7
%âãÏÓ
5 0 obj
<</Filter/FlateDecode/Length 66>>stream
abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz
endstream
endobj
xref0 1
0000000000 65535 f
0000000500 00000 n
4 0 obj
<</Contents 5 0 R/MediaBox[0 0 595 842]/Parent 2 0 R/Resources<</Font<</F1 6 0 R>>>>/TrimBox[0 0 595 842]/Type/Page>>
endobj
xref
0 3
0000000000 65535 f
0000000443 00000 n
0000000576 00000 n
trailer
<< /Size 100 /Root 100 >>
startxref
9000
%%EOF
""";
if (Environment.NewLine == "\n")
{
content = content.Replace("\n", "\r\n");
}
var ib = StringBytesTestConverter.Convert(content, false);
var results = XrefOffsetValidator.BruteForceSearchForTables(ib.Bytes);
Assert.Contains(144, results);
Assert.Contains(331, results);
}
[Fact]
public void FindsXrefsFromRealFileTruncated()
{
var content =
"""
%PDF-1.5
%âãÏÓ
5 0 obj <</Linearized 1/L 4631/O 8/E 1125/N 1/T 4485/H [ 436 129]>>
endobj
xref
5 7
0000000016 00000 n
0000000565 00000 n
0000000436 00000 n
0000000639 00000 n
0000000796 00000 n
0000001001 00000 n
0000001098 00000 n
trailer
<</Size 12/Prev 4475/Root 6 0 R/Info 4 0 R/ID[<2c9c3edf9641f1459e947e7f933f6da0><2c9c3edf9641f1459e947e7f933f6da0>]>>
startxref
0
%%EOF
7 0 obj<</Length 52/Filter/FlateDecode/O 67/S 38>>stream
3842973893927327893237832738923732923782987348
endstream
endobj
6 0 obj<</Pages 2 0 R/Outlines 1 0 R/Type/Catalog/Metadata 3 0 R>>
endobj
8 0 obj<</Contents 9 0 R/Type/Page/Parent 2 0 R/Rotate 0/MediaBox[0 0 612 792]/CropBox[0 0 612 792]/Resources<</Font<</F1 10 0 R>>/ProcSet 11 0 R>>>>
endobj
9 0 obj<</Length 137/Filter/FlateDecode>>stream
abajsgiwgbkeeuuehxh9x3oihx2h802chc280h2082x
endstream
endobj
10 0 obj<</Type/Font/Name/F1/Encoding/MacRomanEncoding/BaseFont/Helvetica/Subtype/Type1>>
endobj
11 0 obj[/PDF/Text]
endobj
1 0 obj<</Count 0/Type/Outlines>>
endobj
2 0 obj<</Count 1/Kids[8 0 R]/Type/Pages>>
endobj
4 0 obj<</ModDate(D:20070213222810-05'00')/CreationDate(D:20070213222810-05'00')>>
endobj
xref
0 5
0000000000 65535 f
0000001125 00000 n
0000001166 00000 n
0000001216 00000 n
0000004385 00000 n
trailer
<</Size 5>>
startxref
116
%%EOF
""";
if (Environment.NewLine == "\n")
{
content = content.Replace("\n", "\r\n");
}
var ib = StringBytesTestConverter.Convert(content, false);
var results = XrefOffsetValidator.BruteForceSearchForTables(ib.Bytes);
Assert.Contains(98, results);
Assert.Contains(1186, results);
ib.Bytes.Seek(98);
var scanner = new CoreTokenScanner(ib.Bytes, false);
scanner.MoveNext();
Assert.Equal(scanner.CurrentToken, OperatorToken.Xref);
}
}

View File

@@ -29,7 +29,6 @@
public static IEnumerable<object[]> ValidNumberTestData => new []
{
new object[] {"0", 0},
new object[] {"0003", 3},
new object[] {"1", 1},
new object[] {"2", 2},
new object[] {"3", 3},
@@ -56,29 +55,19 @@
new object[] { "4.", 4},
new object[] { "-.002", -0.002},
new object[] { "0.0", 0},
new object[] {"1.57e3", 1570},
new object[] {"1.57e-3", 0.00157, 0.0000001},
new object[] {"1.24e1", 12.4},
new object[] { "1.457E2", 145.7 }
new object[] {"1.57e3", 1570}
};
[Theory]
[MemberData(nameof(ValidNumberTestData))]
public void ParsesValidNumbers(string s, double expected, double? tolerance = null)
public void ParsesValidNumbers(string s, double expected)
{
var input = StringBytesTestConverter.Convert(s);
var result = tokenizer.TryTokenize(input.First, input.Bytes, out var token);
Assert.True(result);
if (tolerance.HasValue)
{
Assert.Equal(expected, AssertNumericToken(token).Data, tolerance: tolerance.Value);
}
else
{
Assert.Equal(expected, AssertNumericToken(token).Data);
}
Assert.Equal(expected, AssertNumericToken(token).Data);
}
[Fact]

View File

@@ -191,76 +191,8 @@ 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,6 +406,49 @@ 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,7 +1,6 @@
namespace UglyToad.PdfPig.Tests.Util;
using PdfPig.Util;
using System;
public class CircularByteBufferTests
{
@@ -44,4 +43,4 @@ public class CircularByteBufferTests
Assert.True("123456"u8.SequenceEqual(buffer.AsSpan()));
}
}
}

View File

@@ -129,34 +129,6 @@
}
}
[Fact]
public void CanFastAddPageAndStripAllAnnots()
{
var first = IntegrationHelpers.GetDocumentPath("outline.pdf");
var contents = File.ReadAllBytes(first);
byte[] results = null;
using (var existing = PdfDocument.Open(contents, ParsingOptions.LenientParsingOff))
using (var output = new PdfDocumentBuilder())
{
output.AddPage(existing, 1, new PdfDocumentBuilder.AddPageOptions
{
KeepAnnotations = false
});
results = output.Build();
var pg = existing.GetPage(1);
var annots = pg.ExperimentalAccess.GetAnnotations().ToList();
Assert.NotEmpty(annots);
}
using (var rewritten = PdfDocument.Open(results, ParsingOptions.LenientParsingOff))
{
var pg = rewritten.GetPage(1);
var annots = pg.ExperimentalAccess.GetAnnotations().ToList();
Assert.Empty(annots);
}
}
[Fact]
public void CanReadSingleBlankPage()
{

View File

@@ -1,197 +1,195 @@
#nullable enable
namespace UglyToad.PdfPig.Tokenization;
using System;
using Core;
using Tokens;
internal sealed class NumericTokenizer : ITokenizer
namespace UglyToad.PdfPig.Tokenization
{
private const byte Zero = 48;
private const byte Nine = 57;
private const byte Negative = (byte)'-';
private const byte Positive = (byte)'+';
private const byte Period = (byte)'.';
private const byte ExponentLower = (byte)'e';
private const byte ExponentUpper = (byte)'E';
using System;
using System.Globalization;
using System.Text;
using Core;
using Tokens;
public bool ReadsNextByte => true;
public bool TryTokenize(byte currentByte, IInputBytes inputBytes, out IToken? token)
internal sealed class NumericTokenizer : ITokenizer
{
token = null;
private const byte Zero = 48;
private const byte Nine = 57;
var readBytes = 0;
public bool ReadsNextByte { get; } = true;
// Everything before the decimal part.
var isNegative = false;
double integerPart = 0;
// Everything after the decimal point.
var hasFraction = false;
long fractionalPart = 0;
var fractionalCount = 0;
// Support scientific notation in some font files.
var hasExponent = false;
var isExponentNegative = false;
var exponentPart = 0;
do
public bool TryTokenize(byte currentByte, IInputBytes inputBytes, out IToken token)
{
var b = inputBytes.CurrentByte;
if (b >= Zero && b <= Nine)
{
if (hasExponent)
{
exponentPart = (exponentPart * 10) + (b - Zero);
}
else if (hasFraction)
{
fractionalPart = (fractionalPart * 10) + (b - Zero);
fractionalCount++;
}
else
{
integerPart = (integerPart * 10) + (b - Zero);
}
}
else if (b == Positive)
{
// Has no impact
}
else if (b == Negative)
{
if (hasExponent)
{
isExponentNegative = true;
}
else
{
isNegative = true;
}
}
else if (b == Period)
{
if (hasExponent || hasFraction)
{
return false;
}
token = null;
hasFraction = true;
}
else if (b == ExponentLower || b == ExponentUpper)
using var characters = new ValueStringBuilder(stackalloc char[32]);
var initialSymbol = currentByte is (byte)'-' or (byte)'+';
if ((currentByte >= Zero && currentByte <= Nine) || currentByte == '.')
{
// Don't allow leading exponent.
if (readBytes == 0)
{
return false;
}
if (hasExponent)
{
return false;
}
hasExponent = true;
characters.Append((char)currentByte);
}
else if (initialSymbol)
{
characters.Append((char) currentByte);
}
else
{
// No valid first character.
if (readBytes == 0)
return false;
}
var previousSymbol = initialSymbol;
while (inputBytes.MoveNext())
{
var b = inputBytes.CurrentByte;
if (b == '+' || b == '-')
{
return false;
if (previousSymbol)
{
continue;
}
characters.Append((char) b);
previousSymbol = true;
}
else if ((b >= Zero && b <= Nine) ||
b == '.' ||
b == 'E' ||
b == 'e')
{
previousSymbol = false;
characters.Append((char)b);
}
else
{
break;
}
break;
}
readBytes++;
} while (inputBytes.MoveNext());
var str = characters.ToString();
if (hasExponent && !isExponentNegative)
{
// Apply the multiplication before any fraction logic to avoid loss of precision.
// E.g. 1.53E3 should be exactly 1,530.
// Move the whole part to the left of the decimal point.
var combined = integerPart * Pow10(fractionalCount) + fractionalPart;
// For 1.53E3 we changed this to 153 above, 2 fractional parts, so now we are missing (3-2) 1 additional power of 10.
var shift = exponentPart - fractionalCount;
if (shift >= 0)
switch (str)
{
integerPart = combined * Pow10(shift);
}
else
{
// Still a positive exponent, but not enough to fully shift
// For example 1.457E2 becomes 1,457 but shift is (2-3) -1, the outcome should be 145.7
integerPart = combined / Pow10(-shift);
}
hasFraction = false;
hasExponent = false;
}
if (hasFraction && fractionalCount > 0)
{
switch (fractionalCount)
{
case 1:
integerPart += fractionalPart / 10.0;
break;
case 2:
integerPart += fractionalPart / 100.0;
break;
case 3:
integerPart += fractionalPart / 1000.0;
break;
case "-1":
token = NumericToken.MinusOne;
return true;
case "-":
case ".":
case "0":
case "0000":
token = NumericToken.Zero;
return true;
case "1":
token = NumericToken.One;
return true;
case "2":
token = NumericToken.Two;
return true;
case "3":
token = NumericToken.Three;
return true;
case "4":
token = NumericToken.Four;
return true;
case "5":
token = NumericToken.Five;
return true;
case "6":
token = NumericToken.Six;
return true;
case "7":
token = NumericToken.Seven;
return true;
case "8":
token = NumericToken.Eight;
return true;
case "9":
token = NumericToken.Nine;
return true;
case "10":
token = NumericToken.Ten;
return true;
case "11":
token = NumericToken.Eleven;
return true;
case "12":
token = NumericToken.Twelve;
return true;
case "13":
token = NumericToken.Thirteen;
return true;
case "14":
token = NumericToken.Fourteen;
return true;
case "15":
token = NumericToken.Fifteen;
return true;
case "16":
token = NumericToken.Sixteen;
return true;
case "17":
token = NumericToken.Seventeen;
return true;
case "18":
token = NumericToken.Eighteen;
return true;
case "19":
token = NumericToken.Nineteen;
return true;
case "20":
token = NumericToken.Twenty;
return true;
case "100":
token = NumericToken.OneHundred;
return true;
case "500":
token = NumericToken.FiveHundred;
return true;
case "1000":
token = NumericToken.OneThousand;
return true;
default:
integerPart += fractionalPart / Math.Pow(10, fractionalCount);
break;
if (!double.TryParse(str, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
{
if (TryParseInvalidNumber(str, out value))
{
token = new NumericToken(value);
return true;
}
return false;
}
token = new NumericToken(value);
return true;
}
}
private static bool TryParseInvalidNumber(string numeric, out double result)
{
result = 0;
if (!numeric.Contains("-") && !numeric.Contains("+"))
{
return false;
}
}
if (hasExponent)
{
var signedExponent = isExponentNegative ? -exponentPart : exponentPart;
integerPart *= Math.Pow(10, signedExponent);
}
var parts = numeric.Split(new string[] { "+", "-" }, StringSplitOptions.RemoveEmptyEntries);
if (isNegative)
{
integerPart = -integerPart;
}
if (parts.Length == 0)
{
return false;
}
if (integerPart == 0)
{
token = NumericToken.Zero;
}
else
{
token = new NumericToken(integerPart);
}
foreach (var part in parts)
{
if (!double.TryParse(part, NumberStyles.Any, CultureInfo.InvariantCulture, out var partNumber))
{
return false;
}
return true;
result += partNumber;
}
return true;
}
}
private static double Pow10(int exp)
{
return exp switch
{
0 => 1,
1 => 10,
2 => 100,
3 => 1000,
4 => 10000,
5 => 100000,
6 => 1000000,
7 => 10000000,
8 => 100000000,
9 => 1000000000,
_ => Math.Pow(10, exp)
};
}
}
}

View File

@@ -43,14 +43,6 @@
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.
@@ -60,8 +52,7 @@
bool usePdfDocEncoding,
ScannerScope scope = ScannerScope.None,
IReadOnlyDictionary<NameToken, IReadOnlyList<NameToken>> namedDictionaryRequiredKeys = null,
bool useLenientParsing = false,
bool isStream = false)
bool useLenientParsing = false)
{
this.inputBytes = inputBytes ?? throw new ArgumentNullException(nameof(inputBytes));
this.usePdfDocEncoding = usePdfDocEncoding;
@@ -71,7 +62,6 @@
this.scope = scope;
this.namedDictionaryRequiredKeys = namedDictionaryRequiredKeys;
this.useLenientParsing = useLenientParsing;
this.isStream = isStream;
}
/// <inheritdoc />
@@ -104,7 +94,6 @@
{
var endAngleBracesRead = 0;
bool isSkippingLine = false;
bool isSkippingSymbol = false;
while ((hasBytePreRead && !inputBytes.IsAtEnd()) || inputBytes.MoveNext())
{
@@ -112,17 +101,6 @@
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)
{
@@ -141,12 +119,6 @@
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

@@ -10,7 +10,13 @@
/// </summary>
public static bool NeedsReverseDecode(this IPdfImage pdfImage)
{
return pdfImage.Decode?.Count >= 2 && pdfImage.Decode[0] == 1 && pdfImage.Decode[1] == 0;
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;
}
}
}

View File

@@ -2,7 +2,6 @@
{
using System;
using Core;
using System.Text;
using Tokens;
/// <summary>
@@ -14,7 +13,7 @@
private const byte Offset = (byte)'!';
private const byte EmptyCharacterPadding = (byte)'u';
private static ReadOnlySpan<byte> EndOfDataBytes => "~>"u8;
private static ReadOnlySpan<byte> EndOfDataBytes => [(byte)'~', (byte)'>'];
private static readonly int[] PowerByIndex =
[
@@ -53,7 +52,7 @@
{
if (index > 0)
{
WriteData(asciiBuffer, index, writer, true);
WriteData(asciiBuffer, index, writer);
}
index = 0;
@@ -89,36 +88,24 @@
if (index == 5)
{
WriteData(asciiBuffer, index, writer, false);
WriteData(asciiBuffer, index, writer);
index = 0;
}
}
if (index > 0)
{
WriteData(asciiBuffer, index, writer, true);
WriteData(asciiBuffer, index, writer);
}
return writer.WrittenMemory.ToArray();
}
private static void WriteData(
Span<byte> ascii,
int index,
ArrayPoolBufferWriter<byte> writer,
bool isAtEnd)
private static void WriteData(Span<byte> ascii, int index, ArrayPoolBufferWriter<byte> writer)
{
if (index < 2)
{
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}'");
throw new ArgumentOutOfRangeException(nameof(index), "Cannot convert a block padded by 4 'u' characters.");
}
// Write any empty padding if the block ended early.

View File

@@ -511,22 +511,21 @@
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,6 +15,12 @@
/// </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>
@@ -39,10 +45,11 @@
/// <summary>
/// Create a new <see cref="ColorSpaceDetails"/>.
/// </summary>
protected internal ColorSpaceDetails(ColorSpace type)
protected internal ColorSpaceDetails(ColorSpace type, bool isStencil = false)
{
Type = type;
BaseType = type;
IsStencil = isStencil;
}
/// <summary>
@@ -271,12 +278,15 @@
private readonly ConcurrentDictionary<double, IColor> cache = new ConcurrentDictionary<double, IColor>();
/// <summary>
/// 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).
/// 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.
/// </summary>
internal static ColorSpaceDetails Stencil(ColorSpaceDetails colorSpaceDetails)
internal static ColorSpaceDetails Stencil(ColorSpaceDetails colorSpaceDetails, double[] decode)
{
return new IndexedColorSpaceDetails(colorSpaceDetails, 1, [0, 255]);
var blackIsOne = decode.Length >= 2 && decode[0] == 1 && decode[1] == 0;
return new IndexedColorSpaceDetails(colorSpaceDetails, 1, blackIsOne ? [255, 0] : [0, 255], true);
}
/// <inheritdoc/>
@@ -311,7 +321,14 @@
/// Create a new <see cref="IndexedColorSpaceDetails"/>.
/// </summary>
public IndexedColorSpaceDetails(ColorSpaceDetails baseColorSpaceDetails, byte hiVal, byte[] colorTable)
: base(ColorSpace.Indexed)
: 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)
{
BaseColorSpace = baseColorSpaceDetails ?? throw new ArgumentNullException(nameof(baseColorSpaceDetails));
HiVal = hiVal;

View File

@@ -1,64 +1,65 @@
namespace UglyToad.PdfPig.Graphics.Operations.InlineImages;
namespace UglyToad.PdfPig.Graphics.Operations.InlineImages
{
using System;
using System.Collections.Generic;
using System.IO;
using Tokens;
using UglyToad.PdfPig.Writer;
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)
/// <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 name = item.Key;
var value = item.Value;
var tokenWriter = TokenWriter.Instance;
foreach (var item in Dictionary)
{
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,6 +20,7 @@ namespace UglyToad.PdfPig.Graphics
#endif
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Tokens;
/// <summary>
@@ -37,7 +38,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) },
@@ -414,14 +415,10 @@ 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:
var setStrokeColorCmykArgs = GetExpectedDoubles(SetNonStrokeColorDeviceCmyk.Symbol,
operands,
4);
return new SetStrokeColorDeviceCmyk(
setStrokeColorCmykArgs[0],
setStrokeColorCmykArgs[1],
setStrokeColorCmykArgs[2],
setStrokeColorCmykArgs[3]);
return new SetStrokeColorDeviceCmyk(OperandToDouble(operands[0]),
OperandToDouble(operands[1]),
OperandToDouble(operands[2]),
OperandToDouble(operands[3]));
case SetStrokeColorDeviceGray.Symbol:
return new SetStrokeColorDeviceGray(OperandToDouble(operands[0]));
case SetStrokeColorDeviceRgb.Symbol:
@@ -465,64 +462,128 @@ 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 _))
if (!operations.TryGetValue(op.Data, out Type? operationType))
{
return null;
}
throw new NotImplementedException(
$"No support implemented for content operator {op.Data}");
}
var constructors = operationType.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
private static double[] GetExpectedDoubles(string operatorSymbol, IReadOnlyList<IToken> operands, int resultCount)
{
var results = new double[resultCount];
if (operands.Count < resultCount)
if (constructors.Length == 0)
{
throw new InvalidOperationException(
$"Invalid operands for {operatorSymbol}, needed {resultCount} numbers, got: {PrintOperands(operands)}");
throw new InvalidOperationException("No constructors to invoke were found for operation type: " + operationType.FullName);
}
for (var i = 0; i < resultCount; i++)
{
var op = operands[i];
// This only works by luck...
var constructor = constructors[0];
if (op is not NumericToken nt)
if (constructor.IsPrivate)
{
return (IGraphicsStateOperation)operationType.GetField("Value")?.GetValue(null)!;
}
var parameters = constructor.GetParameters();
var offset = 0;
var arguments = new List<object>();
foreach (var parameter in parameters)
{
if (offset >= operands.Count)
{
throw new InvalidOperationException(
$"Invalid operands for {operatorSymbol}, needed {resultCount} numbers, got: {PrintOperands(operands)}");
throw new InvalidOperationException($"Fewer operands {operands.Count} found than required ({offset + 1}) for operator: {op.Data}.");
}
results[i] = nt.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}.");
}
}
return results;
}
var result = constructor.Invoke(arguments.ToArray());
private static string PrintOperands(IEnumerable<IToken> operands)
{
return "[" + string.Join(", ", operands.Select(x => x.ToString())) + "]";
return (IGraphicsStateOperation)result;
}
}
}

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.MaxValue : byte.MinValue;
unpacked[u++] = (byte)((b >> i) & right) == 1 ? byte.MinValue : byte.MaxValue;
}
}

View File

@@ -7,40 +7,36 @@
internal static class PngFromPdfImageFactory
{
private static bool TryGenerateSoftMask(IPdfImage image, [NotNullWhen(true)] out ReadOnlySpan<byte> maskBytes)
private static bool TryGenerateSoftMask(IPdfImage image, [NotNullWhen(true)] out ReadOnlySpan<byte> bytes)
{
maskBytes = ReadOnlySpan<byte>.Empty;
bytes = ReadOnlySpan<byte>.Empty;
var mask = image.MaskImage;
if (mask is null)
if (image.MaskImage 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 != mask.HeightInSamples ||
image.WidthInSamples != mask.WidthInSamples)
if (image.HeightInSamples != image.MaskImage.HeightInSamples ||
image.WidthInSamples != image.MaskImage.WidthInSamples)
{
return false;
}
if (!mask.TryGetBytesAsMemory(out var maskMemory))
if (!image.TryGetBytesAsMemory(out var imageMemory))
{
return false;
}
try
{
maskBytes = ColorSpaceDetailsByteConverter.Convert(
mask.ColorSpaceDetails!,
maskMemory.Span,
mask.BitsPerComponent,
mask.WidthInSamples,
mask.HeightInSamples);
return IsCorrectlySized(mask, maskBytes);
bytes = ColorSpaceDetailsByteConverter.Convert(image.ColorSpaceDetails!,
imageMemory.Span,
image.BitsPerComponent,
image.WidthInSamples,
image.HeightInSamples);
return IsCorrectlySized(image, bytes);
}
catch (Exception)
{
@@ -159,28 +155,13 @@
else
{
int i = 0;
if (!image.NeedsReverseDecode()) // TODO - Need to properly implement decode for other numberOfComponents
for (int col = 0; col < image.HeightInSamples; col++)
{
for (int col = 0; col < image.HeightInSamples; col++)
for (int row = 0; row < image.WidthInSamples; row++)
{
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);
}
byte a = getAlphaChannel(i);
byte pixel = bytesPure[i++];
builder.SetPixel(new Pixel(pixel, pixel, pixel, a, false), row, col);
}
}
}

View File

@@ -6,7 +6,6 @@
using Logging;
using Tokenization.Scanner;
using Tokens;
using Util;
internal sealed class XrefOffsetValidator
{
@@ -91,10 +90,7 @@
long newOffsetTable = -1;
long newOffsetStream = -1;
if (bfSearchXRefTablesOffsets == null)
{
bfSearchXRefTablesOffsets = BruteForceSearchForTables(reader);
}
BruteForceSearchForTables(reader);
BfSearchForXRefStreams(reader);
@@ -221,39 +217,40 @@
bytes.Seek(startOffset);
}
public static List<long> BruteForceSearchForTables(IInputBytes bytes)
private void BruteForceSearchForTables(IInputBytes bytes)
{
if (bfSearchXRefTablesOffsets != null)
{
return;
}
// a pdf may contain more than one xref entry
var resultOffsets = new List<long>();
bfSearchXRefTablesOffsets = 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.IsWhitespace(bytes.CurrentByte))
if (ReadHelper.IsString(bytes, XRefBytes))
{
// Normalize whitespace
buffer.Add((byte)' ');
}
else
{
buffer.Add(bytes.CurrentByte);
}
var newOffset = bytes.CurrentOffset;
if (buffer.IsCurrentlyEqual(" xref"))
{
resultOffsets.Add(bytes.CurrentOffset - 4);
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);
}
}
bytes.Seek(startOffset);
return resultOffsets;
}
private void BfSearchForXRefStreams(IInputBytes bytes)

View File

@@ -168,11 +168,7 @@
pdfScanner,
parsingOptions);
var type1Handler = new Type1FontHandler(
pdfScanner,
filterProvider,
encodingReader,
parsingOptions.UseLenientParsing);
var type1Handler = new Type1FontHandler(pdfScanner, filterProvider, encodingReader);
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, List<IndirectReference>? visited = null) where T : IToken
internal static T? Resolve<T>(this T? token, IPdfTokenScanner scanner) where T : IToken
{
return (T?)ResolveInternal(token, scanner, visited ?? []);
return (T?)ResolveInternal(token, scanner);
}
private static IToken? ResolveInternal(this IToken? token, IPdfTokenScanner scanner, List<IndirectReference> visited)
private static IToken? ResolveInternal(this IToken? token, IPdfTokenScanner scanner)
{
if (token is StreamToken stream)
{
return new StreamToken(Resolve(stream.StreamDictionary, scanner, visited), stream.Data);
return new StreamToken(Resolve(stream.StreamDictionary, scanner), stream.Data);
}
if (token is DictionaryToken dict)
@@ -139,17 +139,8 @@
var resolvedItems = new Dictionary<NameToken, IToken>();
foreach (var kvp in dict.Data)
{
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);
var value = kvp.Value is IndirectReferenceToken reference ? scanner.Get(reference.Data)?.Data : kvp.Value;
resolvedItems[NameToken.Create(kvp.Key)] = ResolveInternal(value, scanner);
}
return new DictionaryToken(resolvedItems);
@@ -161,7 +152,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, visited));
resolvedItems.Add(ResolveInternal(value, scanner));
}
return new ArrayToken(resolvedItems);
}

View File

@@ -147,8 +147,6 @@
public int ReadCode(IInputBytes bytes, bool useLenientParsing)
{
var myPosition = bytes.CurrentOffset;
if (hasEmptyCodespace)
{
var data = new byte[minCodeLength];
@@ -186,20 +184,7 @@
}
}
// 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)}.");
throw new PdfDocumentFormatException($"CMap is invalid, min code length was {minCodeLength}, max was {maxCodeLength}.");
}
private static byte ReadByte(IInputBytes bytes, bool useLenientParsing)

View File

@@ -49,18 +49,13 @@
public IFont Generate(DictionaryToken dictionary)
{
int? firstChar = null;
if (!dictionary.TryGetOptionalTokenDirect(NameToken.FirstChar,
pdfScanner,
out NumericToken? firstCharacterToken)
if (!dictionary.TryGetOptionalTokenDirect(NameToken.FirstChar, pdfScanner, out NumericToken? firstCharacterToken)
|| !dictionary.TryGet<IToken>(NameToken.FontDescriptor, pdfScanner, out _)
|| !dictionary.TryGet(NameToken.Widths, out _))
|| !dictionary.TryGet(NameToken.Widths, out IToken _))
{
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!
@@ -68,53 +63,37 @@
if (standard14Font is null)
{
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}.");
}
throw new InvalidFontFormatException($"The provided TrueType font dictionary did not have a /FirstChar and did not match a Standard 14 font: {dictionary}.");
}
if (isStandard14)
var fileSystemFont = systemFontFinder.GetTrueTypeFont(baseFont.Data);
var thisEncoding = encodingReader.Read(dictionary);
if (thisEncoding is null)
{
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));
thisEncoding = new AdobeFontMetricsEncoding(standard14Font);
}
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 = firstChar ?? firstCharacterToken!.Int;
var firstCharacter = firstCharacterToken.Int;
var widths = FontDictionaryAccessHelper.GetWidths(pdfScanner, dictionary);

View File

@@ -20,18 +20,13 @@
private readonly IPdfTokenScanner pdfScanner;
private readonly ILookupFilterProvider filterProvider;
private readonly IEncodingReader encodingReader;
private readonly bool isLenientParsing;
public Type1FontHandler(
IPdfTokenScanner pdfScanner,
ILookupFilterProvider filterProvider,
IEncodingReader encodingReader,
bool isLenientParsing)
public Type1FontHandler(IPdfTokenScanner pdfScanner, ILookupFilterProvider filterProvider,
IEncodingReader encodingReader)
{
this.pdfScanner = pdfScanner;
this.filterProvider = filterProvider;
this.encodingReader = encodingReader;
this.isLenientParsing = isLenientParsing;
}
public IFont Generate(DictionaryToken dictionary)
@@ -74,26 +69,12 @@
widths = [];
}
if (!dictionary.TryGet(NameToken.FontDescriptor, out _))
if (!dictionary.TryGet(NameToken.FontDescriptor, out var _))
{
if (dictionary.TryGet(NameToken.BaseFont, pdfScanner, out NameToken? baseFontToken))
if (dictionary.TryGet(NameToken.BaseFont, out var baseFontToken) &&
DirectObjectFinder.TryGet(baseFontToken, pdfScanner, out NameToken? baseFontName))
{
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 metrics = Standard14.GetAdobeFontMetrics(baseFontName.Data);
var overrideEncoding = encodingReader.Read(dictionary);

View File

@@ -1,6 +1,8 @@
namespace UglyToad.PdfPig.PdfFonts.Parser.Parts
{
using System;
using System.Collections.Generic;
using System.Linq;
using Cmap;
using Fonts;
using Tokenization.Scanner;
@@ -18,13 +20,6 @@
// 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,6 +3,7 @@ namespace UglyToad.PdfPig.PdfFonts.Simple
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Core;
using Fonts;
@@ -83,31 +84,36 @@ namespace UglyToad.PdfPig.PdfFonts.Simple
public CharacterBoundingBox GetBoundingBox(int characterCode)
{
var (boundingBox, advanceWidth) = GetBoundingBoxInGlyphSpace(characterCode);
var boundingBox = GetBoundingBoxInGlyphSpace(characterCode);
boundingBox = fontMatrix.Transform(boundingBox);
advanceWidth = fontMatrix.TransformX(advanceWidth);
return new CharacterBoundingBox(boundingBox, advanceWidth);
return new CharacterBoundingBox(boundingBox, boundingBox.Width);
}
private (PdfRectangle bounds, double advanceWidth) GetBoundingBoxInGlyphSpace(int characterCode)
private PdfRectangle GetBoundingBoxInGlyphSpace(int characterCode)
{
var name = encoding.GetName(characterCode);
if (!standardFontMetrics.CharacterMetrics.TryGetValue(name, out var metrics))
{
return (new PdfRectangle(0, 0, 250, 0), 250);
return new PdfRectangle(0, 0, 250, 0);
}
var x = metrics.Width.X;
var y = metrics.Width.Y;
if (metrics.Width.X == 0 && metrics.BoundingBox.Width > 0)
{
x = metrics.BoundingBox.Width;
}
return (metrics.BoundingBox, x);
if (metrics.Width.Y == 0 && metrics.BoundingBox.Height > 0)
{
y = metrics.BoundingBox.Height;
}
return new PdfRectangle(0, 0, x, y);
}
public TransformationMatrix GetFontMatrix()

View File

@@ -10,11 +10,15 @@
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+$");
@@ -164,6 +168,8 @@
return true;
}
// This should never happen.
Debug.Assert(false, "Encountered a start object 'obj' operator before the end of the previous object.");
return false;
}
@@ -176,20 +182,22 @@
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;
}
@@ -264,12 +272,6 @@
{
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];
@@ -313,6 +315,9 @@
{
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.
@@ -324,7 +329,7 @@
}
// Verify again that we start with "stream"
var hasStartStreamToken = ReadStreamTokenStart(inputBytes, startStreamTokenOffset);
var hasStartStreamToken = ReadStreamTokenStart(inputBytes, startStreamTokenOffset, buffer);
if (!hasStartStreamToken)
{
return false;
@@ -351,9 +356,9 @@
{
inputBytes.Seek(inputBytes.CurrentOffset - 1);
}
break;
}
} while ((char)inputBytes.CurrentByte != '\n');
// Store where we started reading the first byte of data.
@@ -362,16 +367,7 @@
// Store how many bytes we have read for checking against Length.
long read = 0;
// 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))
if (TryReadUsingLength(inputBytes, length, startDataOffset, buffer, out var streamData))
{
stream = new StreamToken(streamDictionaryToken, streamData);
return true;
@@ -381,99 +377,100 @@
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 (length.HasValue && read == length)
if (inputBytes.CurrentByte == sentinelByte)
{
// TODO: read ahead and check we're at the end...
// break;
sentinelPosQueue.Enqueue(inputBytes.CurrentOffset);
queue.Add(inputBytes.CurrentByte);
}
// We are reading 'end' (possibly).
if (commonPartPosition < endWordPart.Length && inputBytes.CurrentByte == endWordPart[commonPartPosition])
else if (sentinelPosQueue.Count > 0)
{
commonPartPosition++;
}
else if (commonPartPosition == endWordPart.Length)
{
// We are reading 'stream' after 'end'
if (inputBytes.CurrentByte == streamPart[endStreamPosition])
if (ReadHelper.IsWhitespace(inputBytes.CurrentByte))
{
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;
}
}
// Normalize whitespace
queue.Add((byte)' ');
}
else
{
// We were reading 'end' but then we had a character mismatch.
// Reset all the counters.
endStreamPosition = 0;
endObjPosition = 0;
commonPartPosition = 0;
queue.Add(inputBytes.CurrentByte);
}
}
else
{
// For safety reset every counter in case we had a partial read.
endStreamPosition = 0;
endObjPosition = 0;
commonPartPosition = (inputBytes.CurrentByte == endWordPart[0]) ? 1 : 0;
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);
}
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;
@@ -504,7 +501,12 @@
return true;
}
private static bool TryReadUsingLength(IInputBytes inputBytes, long? length, long startDataOffset, [NotNullWhen(true)] out byte[]? data)
private static bool TryReadUsingLength(
IInputBytes inputBytes,
long? length,
long startDataOffset,
byte[] buffer,
[NotNullWhen(true)] out byte[]? data)
{
data = null;
@@ -513,8 +515,6 @@
return false;
}
var readBuffer = new byte[EndstreamBytes.Length];
var newlineCount = 0;
inputBytes.Seek(length.Value + startDataOffset);
@@ -535,20 +535,17 @@
}
}
var readLength = inputBytes.Read(readBuffer);
var readLength = inputBytes.Read(buffer);
if (readLength != readBuffer.Length)
if (readLength != EndstreamBytes.Length)
{
return false;
}
for (var i = 0; i < EndstreamBytes.Length; i++)
if (!ByteArraysEqual(buffer, EndstreamBytes))
{
if (readBuffer[i] != EndstreamBytes[i])
{
inputBytes.Seek(startDataOffset);
return false;
}
inputBytes.Seek(startDataOffset);
return false;
}
inputBytes.Seek(startDataOffset);
@@ -562,7 +559,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(readBuffer);
inputBytes.Read(buffer);
// Skip for the line break before 'endstream'.
for (var i = 0; i < newlineCount; i++)
{
@@ -659,22 +656,29 @@
return length;
}
private static bool ReadStreamTokenStart(IInputBytes input, long tokenStart)
private static bool ReadStreamTokenStart(IInputBytes input, long tokenStart, byte[] buffer)
{
input.Seek(tokenStart);
for (var i = 0; i < OperatorToken.StartStream.Data.Length; i++)
var readCount = input.Read(buffer);
if (readCount < StartstreamBytes.Length
|| !ByteArraysEqual(buffer.AsSpan(0, StartstreamBytes.Length), StartstreamBytes))
{
if (!input.MoveNext() || input.CurrentByte != OperatorToken.StartStream.Data[i])
{
input.Seek(tokenStart);
return false;
}
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)
@@ -714,7 +718,7 @@
coreTokenScanner.DeregisterCustomTokenizer(tokenizer);
}
public ObjectToken? Get(IndirectReference reference)
{
if (isDisposed)
@@ -848,11 +852,7 @@
// Read the N integers
var bytes = new MemoryInputBytes(stream.Decode(filterProvider, this));
var scanner = new CoreTokenScanner(
bytes,
true,
useLenientParsing: parsingOptions.UseLenientParsing,
isStream: true);
var scanner = new CoreTokenScanner(bytes, true, useLenientParsing: parsingOptions.UseLenientParsing);
var objects = new List<(long, long)>();

View File

@@ -1,90 +1,89 @@
namespace UglyToad.PdfPig.Util;
using System;
using System.Text;
internal sealed class CircularByteBuffer(int size)
internal 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)
{
var insertionPosition = (start + count) % buffer.Length;
buffer[insertionPosition] = b;
if (count < buffer.Length)
public void Add(byte b)
{
count++;
}
else
{
start = (start + 1) % buffer.Length;
}
}
var insertionPosition = (start + count) % buffer.Length;
public bool EndsWith(string s)
{
if (s.Length > count)
{
return false;
}
for (var i = 0; i < s.Length; i++)
{
var str = s[i];
var inBuffer = count - (s.Length - i);
var buff = buffer[IndexToBufferIndex(inBuffer)];
if (buff != str)
buffer[insertionPosition] = b;
if (count < buffer.Length)
{
return false;
count++;
}
else
{
start = (start + 1) % buffer.Length;
}
}
return true;
}
public bool IsCurrentlyEqual(string s)
{
if (s.Length > buffer.Length)
public bool EndsWith(string s)
{
return false;
}
for (var i = 0; i < s.Length; i++)
{
var b = (byte)s[i];
var buff = buffer[IndexToBufferIndex(i)];
if (b != buff)
if (s.Length > count)
{
return false;
}
for (var i = 0; i < s.Length; i++)
{
var str = s[i];
var inBuffer = count - (s.Length - i);
var buff = buffer[IndexToBufferIndex(inBuffer)];
if (buff != str)
{
return false;
}
}
return true;
}
return true;
}
public ReadOnlySpan<byte> AsSpan()
{
Span<byte> tmp = new byte[count];
for (int i = 0; i < count; i++)
public bool IsCurrentlyEqual(string s)
{
tmp[i] = buffer[IndexToBufferIndex(i)];
if (s.Length > buffer.Length)
{
return false;
}
for (var i = 0; i < s.Length; i++)
{
var b = (byte)s[i];
var buff = buffer[IndexToBufferIndex(i)];
if (b != buff)
{
return false;
}
}
return true;
}
return tmp;
}
public ReadOnlySpan<byte> AsSpan()
{
Span<byte> tmp = new byte[count];
for (int i = 0; i < count; i++)
{
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,7 +54,11 @@
}
var colorSpaceDetails = GetColorSpaceDetails(colorSpace, imageDictionary.Without(NameToken.Filter).Without(NameToken.F), scanner, resourceStore, filterProvider, true);
return IndexedColorSpaceDetails.Stencil(colorSpaceDetails);
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);
}
if (!colorSpace.HasValue)

View File

@@ -1,36 +0,0 @@
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

@@ -11,16 +11,11 @@ namespace UglyToad.PdfPig.Writer
using Core;
using Fonts;
using Actions;
using Filters;
using Graphics;
using Logging;
using PdfPig.Fonts.TrueType;
using PdfPig.Fonts.Standard14Fonts;
using PdfPig.Fonts.TrueType.Parser;
using Outline;
using Outline.Destinations;
using Parser;
using Parser.Parts;
using Tokenization.Scanner;
using Tokens;
@@ -307,11 +302,10 @@ namespace UglyToad.PdfPig.Writer
/// </summary>
/// <param name="document">Source document.</param>
/// <param name="pageNumber">Page to copy.</param>
/// <param name="options">Control how copying for the page occurs.</param>
/// <returns>A builder for editing the page.</returns>
public PdfPageBuilder AddPage(PdfDocument document, int pageNumber)
{
return AddPage(document, pageNumber, new AddPageOptions());
return AddPage(document, pageNumber, null);
}
/// <summary>
@@ -319,9 +313,9 @@ namespace UglyToad.PdfPig.Writer
/// </summary>
/// <param name="document">Source document.</param>
/// <param name="pageNumber">Page to copy.</param>
/// <param name="options">Control how copying for the page occurs.</param>
/// <param name="copyLink">If set, links are copied based on the result of the delegate.</param>
/// <returns>A builder for editing the page.</returns>
public PdfPageBuilder AddPage(PdfDocument document, int pageNumber, AddPageOptions options)
public PdfPageBuilder AddPage(PdfDocument document, int pageNumber, Func<PdfAction, PdfAction?>? copyLink)
{
if (!existingCopies.TryGetValue(document.Structure.TokenScanner, out var refs))
{
@@ -348,7 +342,6 @@ 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>();
@@ -359,54 +352,23 @@ 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)
{
contentReferences.Add(ir);
streams.Add(new PdfPageBuilder.CopiedContentStream(
(IndirectReferenceToken)WriterUtil.CopyToken(context, ir, document.Structure.TokenScanner, refs)));
}
}
}
else if (contentsToken is IndirectReferenceToken ir)
{
contentReferences.Add(ir);
streams.Add(new PdfPageBuilder.CopiedContentStream(
(IndirectReferenceToken)WriterUtil.CopyToken(context, ir, document.Structure.TokenScanner, refs)));
}
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;
}
@@ -454,11 +416,6 @@ namespace UglyToad.PdfPig.Writer
if (kvp.Key == NameToken.Annots)
{
if (!options.KeepAnnotations)
{
continue;
}
var val = kvp.Value;
if (kvp.Value is IndirectReferenceToken ir)
{
@@ -490,7 +447,7 @@ namespace UglyToad.PdfPig.Writer
if (tk.TryGet(NameToken.Subtype, out var st) && st is NameToken nm && nm == NameToken.Link)
{
if (options.CopyLinkFunc is null)
if (copyLink is null)
{
// ignore link if don't know how to copy
continue;
@@ -503,7 +460,7 @@ namespace UglyToad.PdfPig.Writer
continue;
}
var copiedLink = options.CopyLinkFunc(link);
var copiedLink = copyLink(link);
if (copiedLink is null)
{
// ignore if caller wants to skip the link
@@ -544,12 +501,9 @@ namespace UglyToad.PdfPig.Writer
{
return;
}
foreach (var item in dict.Data)
{
var key = NameToken.Create(item.Key);
if (!destinationDict.ContainsKey(key))
if (!destinationDict.ContainsKey(NameToken.Create(item.Key)))
{
if (item.Value is IndirectReferenceToken ir)
{
@@ -558,55 +512,43 @@ namespace UglyToad.PdfPig.Writer
if (obj.Data is StreamToken)
{
// rare case, have seen /SubType as stream token, can't make direct
destinationDict[key] = WriterUtil.CopyToken(context, item.Value, document.Structure.TokenScanner, refs);
destinationDict[NameToken.Create(item.Key)] = WriterUtil.CopyToken(context, item.Value, document.Structure.TokenScanner, refs);
}
else
{
destinationDict[key] = WriterUtil.CopyToken(context, obj.Data, document.Structure.TokenScanner, refs);
destinationDict[NameToken.Create(item.Key)] = WriterUtil.CopyToken(context, obj.Data, document.Structure.TokenScanner, refs);
}
}
else
{
destinationDict[key] = WriterUtil.CopyToken(context, item.Value, document.Structure.TokenScanner, refs);
destinationDict[NameToken.Create(item.Key)] = WriterUtil.CopyToken(context, item.Value, document.Structure.TokenScanner, refs);
}
continue;
}
var subDict = GetRemoteDict(item.Value);
var destSubDict = destinationDict[key] as DictionaryToken;
var destSubDict = destinationDict[NameToken.Create(item.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[key] = WriterUtil.CopyToken(context, document.Structure.TokenScanner.Get(ir.Data).Data, document.Structure.TokenScanner, refs);
destinationDict[NameToken.Create(item.Key)] = WriterUtil.CopyToken(context, document.Structure.TokenScanner.Get(ir.Data).Data, document.Structure.TokenScanner, refs);
}
else
{
destinationDict[key] = WriterUtil.CopyToken(context, item.Value, document.Structure.TokenScanner, refs);
destinationDict[NameToken.Create(item.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
mutableSubDict[NameToken.Create(subItem.Key)] = WriterUtil.CopyToken(
context,
subItem.Value,
document.Structure.TokenScanner,
refs);
// last copied most important important
destinationDict[NameToken.Create(subItem.Key)] = WriterUtil.CopyToken(context, subItem.Value,
document.Structure.TokenScanner, refs);
}
destinationDict[key] = new DictionaryToken(mutableSubDict);
}
}
@@ -1248,21 +1190,5 @@ 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,240 +1,236 @@
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
namespace UglyToad.PdfPig.Writer
{
/// <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();
}
}
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"/> into the output stream.
/// Merges PDF documents into each other.
/// </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)
public static class PdfMerger
{
_ = file1 ?? throw new ArgumentNullException(nameof(file1));
_ = file2 ?? throw new ArgumentNullException(nameof(file2));
private static readonly ILog Log = new NoOpLog();
using (var stream1 = File.OpenRead(file1))
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 stream2 = File.OpenRead(file2))
using (var output = new MemoryStream())
{
Merge(new[] { stream1, stream2 }, output, new[] { file1Selection, file2Selection }, archiveStandard, docInfoBuilder);
Merge(file1, file2, output, file1Selection, file2Selection, archiveStandard, docInfoBuilder);
return output.ToArray();
}
}
}
/// <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())
/// <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)
{
Merge(output, archiveStandard, docInfoBuilder, filePaths);
return output.ToArray();
}
}
_ = file1 ?? throw new ArgumentNullException(nameof(file1));
_ = file2 ?? throw new ArgumentNullException(nameof(file2));
/// <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++)
using (var stream1 = File.OpenRead(file1))
{
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)
using (var stream2 = File.OpenRead(file2))
{
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)
});
}
Merge(new[] { stream1, stream2 }, output, new[] { file1Selection, file2Selection }, archiveStandard, docInfoBuilder);
}
}
}
PdfAction? CopyLink(PdfAction action, Func<int, int?> getPageNumber)
/// <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)
{
var link = action as AbstractGoToAction;
if (link is null)
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())
{
// copy the link if it is not a link to PDF documents
return action;
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));
}
}
}
}
var newPageNumber = getPageNumber(link.Destination.PageNumber);
if (newPageNumber is null)
PdfAction? CopyLink(PdfAction action, Func<int, int?> getPageNumber)
{
// ignore the link if the target page does not exist in the PDF document
return null;
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
};
}
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,42 +140,16 @@
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>(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 = new List<IPageContentStream>();
contentStreams.AddRange(copied);
currentStream = new DefaultContentStream();
contentStreams.Add(currentStream);
}
@@ -857,7 +831,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, srcPage.pdfScanner);
var resources = pageDictionary.GetOrCreateDict(NameToken.Resources);
foreach (var set in srcResourceDictionary.Data)
{
@@ -884,7 +858,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, srcPage.pdfScanner);
var pageFontsDictionary = resources.GetOrCreateDict(NameToken.Font);
foreach (var fontSet in fontsDictionary.Data)
{
@@ -929,7 +903,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, srcPage.pdfScanner);
var pageXobjectsDictionary = resources.GetOrCreateDict(NameToken.Xobject);
foreach (var xobjectSet in xobjectsDictionary.Data)
{
@@ -971,7 +945,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, srcPage.pdfScanner);
var pageGstateDictionary = resources.GetOrCreateDict(NameToken.ExtGState);
foreach (var gstate in gsDictionary.Data)
{
@@ -1013,22 +987,6 @@
}
}
// 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;
@@ -1132,18 +1090,9 @@
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
@@ -1160,11 +1109,8 @@
}
public bool ReadOnly => false;
public bool HasContent => operations.Any();
public TransformationMatrix? GlobalTransform => null;
public void Add(IGraphicsStateOperation operation)
{
operations.Add(operation);
@@ -1193,18 +1139,11 @@
internal class CopiedContentStream : IPageContentStream
{
private readonly IndirectReferenceToken token;
public bool ReadOnly => true;
public bool HasContent => true;
public TransformationMatrix? GlobalTransform { get; }
public CopiedContentStream(
IndirectReferenceToken indirectReferenceToken,
TransformationMatrix? globalTransform)
public CopiedContentStream(IndirectReferenceToken indirectReferenceToken)
{
GlobalTransform = globalTransform;
token = indirectReferenceToken;
}

View File

@@ -12,36 +12,13 @@
internal static class WriterUtil
{
public static Dictionary<string, IToken> GetOrCreateDict<T>(
this Dictionary<T, IToken> dict,
T key,
IPdfTokenScanner? sourceScanner = null) where T : notnull
public static Dictionary<string, IToken> GetOrCreateDict(this Dictionary<NameToken, IToken> dict, NameToken key)
{
if (dict.TryGetValue(key, out var item))
{
var chainCount = 0;
var itemChain = item;
while (itemChain is IndirectReferenceToken ir && chainCount < 100)
if (!(item is DictionaryToken dt))
{
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()}");
throw new ApplicationException("Expected dictionary token, got " + item.GetType());
}
if (dt.Data is Dictionary<string, IToken> mutable)
@@ -60,6 +37,30 @@
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,6 +92,7 @@
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);