forked from mozilla/pdf.js
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathparser.js
More file actions
1419 lines (1322 loc) · 42 KB
/
parser.js
File metadata and controls
1419 lines (1322 loc) · 42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
assert,
bytesToString,
FormatError,
info,
StreamType,
warn,
} from "../shared/util.js";
import { Cmd, Dict, EOF, isCmd, Name, Ref } from "./primitives.js";
import {
isWhiteSpace,
MissingDataException,
ParserEOFException,
} from "./core_utils.js";
import { Ascii85Stream } from "./ascii_85_stream.js";
import { AsciiHexStream } from "./ascii_hex_stream.js";
import { CCITTFaxStream } from "./ccitt_stream.js";
import { FlateStream } from "./flate_stream.js";
import { Jbig2Stream } from "./jbig2_stream.js";
import { JpegStream } from "./jpeg_stream.js";
import { JpxStream } from "./jpx_stream.js";
import { LZWStream } from "./lzw_stream.js";
import { NullStream } from "./stream.js";
import { PredictorStream } from "./predictor_stream.js";
import { RunLengthStream } from "./run_length_stream.js";
const MAX_LENGTH_TO_CACHE = 1000;
const MAX_ADLER32_LENGTH = 5552;
function computeAdler32(bytes) {
const bytesLength = bytes.length;
if (
typeof PDFJSDev === "undefined" ||
PDFJSDev.test("!PRODUCTION || TESTING")
) {
assert(
bytesLength < MAX_ADLER32_LENGTH,
'computeAdler32: Unsupported "bytes" length.'
);
}
let a = 1,
b = 0;
for (let i = 0; i < bytesLength; ++i) {
// No modulo required in the loop if `bytesLength < 5552`.
a += bytes[i] & 0xff;
b += a;
}
return (b % 65521 << 16) | a % 65521;
}
class Parser {
constructor({ lexer, xref, allowStreams = false, recoveryMode = false }) {
this.lexer = lexer;
this.xref = xref;
this.allowStreams = allowStreams;
this.recoveryMode = recoveryMode;
this.imageCache = Object.create(null);
this.refill();
}
refill() {
this.buf1 = this.lexer.getObj();
this.buf2 = this.lexer.getObj();
}
shift() {
if (this.buf2 instanceof Cmd && this.buf2.cmd === "ID") {
this.buf1 = this.buf2;
this.buf2 = null;
} else {
this.buf1 = this.buf2;
this.buf2 = this.lexer.getObj();
}
}
tryShift() {
try {
this.shift();
return true;
} catch (e) {
if (e instanceof MissingDataException) {
throw e;
}
// Upon failure, the caller should reset this.lexer.pos to a known good
// state and call this.shift() twice to reset the buffers.
return false;
}
}
getObj(cipherTransform = null) {
const buf1 = this.buf1;
this.shift();
if (buf1 instanceof Cmd) {
switch (buf1.cmd) {
case "BI": // inline image
return this.makeInlineImage(cipherTransform);
case "[": // array
const array = [];
while (!isCmd(this.buf1, "]") && this.buf1 !== EOF) {
array.push(this.getObj(cipherTransform));
}
if (this.buf1 === EOF) {
if (this.recoveryMode) {
return array;
}
throw new ParserEOFException("End of file inside array.");
}
this.shift();
return array;
case "<<": // dictionary or stream
const dict = new Dict(this.xref);
while (!isCmd(this.buf1, ">>") && this.buf1 !== EOF) {
if (!(this.buf1 instanceof Name)) {
info("Malformed dictionary: key must be a name object");
this.shift();
continue;
}
const key = this.buf1.name;
this.shift();
if (this.buf1 === EOF) {
break;
}
dict.set(key, this.getObj(cipherTransform));
}
if (this.buf1 === EOF) {
if (this.recoveryMode) {
return dict;
}
throw new ParserEOFException("End of file inside dictionary.");
}
// Stream objects are not allowed inside content streams or
// object streams.
if (isCmd(this.buf2, "stream")) {
return this.allowStreams
? this.makeStream(dict, cipherTransform)
: dict;
}
this.shift();
return dict;
default: // simple object
return buf1;
}
}
if (Number.isInteger(buf1)) {
// indirect reference or integer
if (Number.isInteger(this.buf1) && isCmd(this.buf2, "R")) {
const ref = Ref.get(buf1, this.buf1);
this.shift();
this.shift();
return ref;
}
return buf1;
}
if (typeof buf1 === "string") {
if (cipherTransform) {
return cipherTransform.decryptString(buf1);
}
return buf1;
}
// simple object
return buf1;
}
/**
* Find the end of the stream by searching for the /EI\s/.
* @returns {number} The inline stream length.
*/
findDefaultInlineStreamEnd(stream) {
const E = 0x45,
I = 0x49,
SPACE = 0x20,
LF = 0xa,
CR = 0xd,
NUL = 0x0;
const lexer = this.lexer,
startPos = stream.pos,
n = 10;
let state = 0,
ch,
maybeEIPos;
while ((ch = stream.getByte()) !== -1) {
if (state === 0) {
state = ch === E ? 1 : 0;
} else if (state === 1) {
state = ch === I ? 2 : 0;
} else {
assert(state === 2, "findDefaultInlineStreamEnd - invalid state.");
if (ch === SPACE || ch === LF || ch === CR) {
maybeEIPos = stream.pos;
// Let's check that the next `n` bytes are ASCII... just to be sure.
const followingBytes = stream.peekBytes(n);
for (let i = 0, ii = followingBytes.length; i < ii; i++) {
ch = followingBytes[i];
if (ch === NUL && followingBytes[i + 1] !== NUL) {
// NUL bytes are not supposed to occur *outside* of inline
// images, but some PDF generators violate that assumption,
// thus breaking the EI detection heuristics used below.
//
// However, we can't unconditionally treat NUL bytes as "ASCII",
// since that *could* result in inline images being truncated.
//
// To attempt to address this, we'll still treat any *sequence*
// of NUL bytes as non-ASCII, but for a *single* NUL byte we'll
// continue checking the `followingBytes` (fixes issue8823.pdf).
continue;
}
if (ch !== LF && ch !== CR && (ch < SPACE || ch > 0x7f)) {
// Not a LF, CR, SPACE or any visible ASCII character, i.e.
// it's binary stuff. Resetting the state.
state = 0;
break;
}
}
if (state !== 2) {
continue;
}
// Check that the "EI" sequence isn't part of the image data, since
// that would cause the image to be truncated (fixes issue11124.pdf).
if (lexer.knownCommands) {
const nextObj = lexer.peekObj();
if (nextObj instanceof Cmd && !lexer.knownCommands[nextObj.cmd]) {
// Not a valid command, i.e. the inline image data *itself*
// contains an "EI" sequence. Resetting the state.
state = 0;
}
} else {
warn(
"findDefaultInlineStreamEnd - `lexer.knownCommands` is undefined."
);
}
if (state === 2) {
break; // Finished!
}
} else {
state = 0;
}
}
}
if (ch === -1) {
warn(
"findDefaultInlineStreamEnd: " +
"Reached the end of the stream without finding a valid EI marker"
);
if (maybeEIPos) {
warn('... trying to recover by using the last "EI" occurrence.');
stream.skip(-(stream.pos - maybeEIPos)); // Reset the stream position.
}
}
let endOffset = 4;
stream.skip(-endOffset); // Set the stream position to just before "EI".
ch = stream.peekByte();
stream.skip(endOffset); // ... and remember to reset the stream position.
// Ensure that we don't accidentally truncate the inline image, when the
// data is immediately followed by the "EI" marker (fixes issue10388.pdf).
if (!isWhiteSpace(ch)) {
endOffset--;
}
return stream.pos - endOffset - startPos;
}
/**
* Find the EOI (end-of-image) marker 0xFFD9 of the stream.
* @returns {number} The inline stream length.
*/
findDCTDecodeInlineStreamEnd(stream) {
const startPos = stream.pos;
let foundEOI = false,
b,
markerLength;
while ((b = stream.getByte()) !== -1) {
if (b !== 0xff) {
// Not a valid marker.
continue;
}
switch (stream.getByte()) {
case 0x00: // Byte stuffing.
// 0xFF00 appears to be a very common byte sequence in JPEG images.
break;
case 0xff: // Fill byte.
// Avoid skipping a valid marker, resetting the stream position.
stream.skip(-1);
break;
case 0xd9: // EOI
foundEOI = true;
break;
case 0xc0: // SOF0
case 0xc1: // SOF1
case 0xc2: // SOF2
case 0xc3: // SOF3
/* falls through */
case 0xc5: // SOF5
case 0xc6: // SOF6
case 0xc7: // SOF7
/* falls through */
case 0xc9: // SOF9
case 0xca: // SOF10
case 0xcb: // SOF11
/* falls through */
case 0xcd: // SOF13
case 0xce: // SOF14
case 0xcf: // SOF15
/* falls through */
case 0xc4: // DHT
case 0xcc: // DAC
/* falls through */
case 0xda: // SOS
case 0xdb: // DQT
case 0xdc: // DNL
case 0xdd: // DRI
case 0xde: // DHP
case 0xdf: // EXP
/* falls through */
case 0xe0: // APP0
case 0xe1: // APP1
case 0xe2: // APP2
case 0xe3: // APP3
case 0xe4: // APP4
case 0xe5: // APP5
case 0xe6: // APP6
case 0xe7: // APP7
case 0xe8: // APP8
case 0xe9: // APP9
case 0xea: // APP10
case 0xeb: // APP11
case 0xec: // APP12
case 0xed: // APP13
case 0xee: // APP14
case 0xef: // APP15
/* falls through */
case 0xfe: // COM
// The marker should be followed by the length of the segment.
markerLength = stream.getUint16();
if (markerLength > 2) {
// |markerLength| contains the byte length of the marker segment,
// including its own length (2 bytes) and excluding the marker.
stream.skip(markerLength - 2); // Jump to the next marker.
} else {
// The marker length is invalid, resetting the stream position.
stream.skip(-2);
}
break;
}
if (foundEOI) {
break;
}
}
const length = stream.pos - startPos;
if (b === -1) {
warn(
"Inline DCTDecode image stream: " +
"EOI marker not found, searching for /EI/ instead."
);
stream.skip(-length); // Reset the stream position.
return this.findDefaultInlineStreamEnd(stream);
}
this.inlineStreamSkipEI(stream);
return length;
}
/**
* Find the EOD (end-of-data) marker '~>' (i.e. TILDE + GT) of the stream.
* @returns {number} The inline stream length.
*/
findASCII85DecodeInlineStreamEnd(stream) {
const TILDE = 0x7e,
GT = 0x3e;
const startPos = stream.pos;
let ch;
while ((ch = stream.getByte()) !== -1) {
if (ch === TILDE) {
const tildePos = stream.pos;
ch = stream.peekByte();
// Handle corrupt PDF documents which contains whitespace "inside" of
// the EOD marker (fixes issue10614.pdf).
while (isWhiteSpace(ch)) {
stream.skip();
ch = stream.peekByte();
}
if (ch === GT) {
stream.skip();
break;
}
// Handle corrupt PDF documents which contains truncated EOD markers,
// where the '>' character is missing (fixes issue11385.pdf).
if (stream.pos > tildePos) {
const maybeEI = stream.peekBytes(2);
if (maybeEI[0] === /* E = */ 0x45 && maybeEI[1] === /* I = */ 0x49) {
break;
}
}
}
}
const length = stream.pos - startPos;
if (ch === -1) {
warn(
"Inline ASCII85Decode image stream: " +
"EOD marker not found, searching for /EI/ instead."
);
stream.skip(-length); // Reset the stream position.
return this.findDefaultInlineStreamEnd(stream);
}
this.inlineStreamSkipEI(stream);
return length;
}
/**
* Find the EOD (end-of-data) marker '>' (i.e. GT) of the stream.
* @returns {number} The inline stream length.
*/
findASCIIHexDecodeInlineStreamEnd(stream) {
const GT = 0x3e;
const startPos = stream.pos;
let ch;
while ((ch = stream.getByte()) !== -1) {
if (ch === GT) {
break;
}
}
const length = stream.pos - startPos;
if (ch === -1) {
warn(
"Inline ASCIIHexDecode image stream: " +
"EOD marker not found, searching for /EI/ instead."
);
stream.skip(-length); // Reset the stream position.
return this.findDefaultInlineStreamEnd(stream);
}
this.inlineStreamSkipEI(stream);
return length;
}
/**
* Skip over the /EI/ for streams where we search for an EOD marker.
*/
inlineStreamSkipEI(stream) {
const E = 0x45,
I = 0x49;
let state = 0,
ch;
while ((ch = stream.getByte()) !== -1) {
if (state === 0) {
state = ch === E ? 1 : 0;
} else if (state === 1) {
state = ch === I ? 2 : 0;
} else if (state === 2) {
break;
}
}
}
makeInlineImage(cipherTransform) {
const lexer = this.lexer;
const stream = lexer.stream;
// Parse dictionary.
const dict = new Dict(this.xref);
let dictLength;
while (!isCmd(this.buf1, "ID") && this.buf1 !== EOF) {
if (!(this.buf1 instanceof Name)) {
throw new FormatError("Dictionary key must be a name object");
}
const key = this.buf1.name;
this.shift();
if (this.buf1 === EOF) {
break;
}
dict.set(key, this.getObj(cipherTransform));
}
if (lexer.beginInlineImagePos !== -1) {
dictLength = stream.pos - lexer.beginInlineImagePos;
}
// Extract the name of the first (i.e. the current) image filter.
const filter = dict.get("F", "Filter");
let filterName;
if (filter instanceof Name) {
filterName = filter.name;
} else if (Array.isArray(filter)) {
const filterZero = this.xref.fetchIfRef(filter[0]);
if (filterZero instanceof Name) {
filterName = filterZero.name;
}
}
// Parse image stream.
const startPos = stream.pos;
let length;
switch (filterName) {
case "DCT":
case "DCTDecode":
length = this.findDCTDecodeInlineStreamEnd(stream);
break;
case "A85":
case "ASCII85Decode":
length = this.findASCII85DecodeInlineStreamEnd(stream);
break;
case "AHx":
case "ASCIIHexDecode":
length = this.findASCIIHexDecodeInlineStreamEnd(stream);
break;
default:
length = this.findDefaultInlineStreamEnd(stream);
}
let imageStream = stream.makeSubStream(startPos, length, dict);
// Cache all images below the MAX_LENGTH_TO_CACHE threshold by their
// adler32 checksum.
let cacheKey;
if (length < MAX_LENGTH_TO_CACHE && dictLength < MAX_ADLER32_LENGTH) {
const imageBytes = imageStream.getBytes();
imageStream.reset();
const initialStreamPos = stream.pos;
// Set the stream position to the beginning of the dictionary data...
stream.pos = lexer.beginInlineImagePos;
// ... and fetch the bytes of the *entire* dictionary.
const dictBytes = stream.getBytes(dictLength);
// Finally, don't forget to reset the stream position.
stream.pos = initialStreamPos;
cacheKey = computeAdler32(imageBytes) + "_" + computeAdler32(dictBytes);
const cacheEntry = this.imageCache[cacheKey];
if (cacheEntry !== undefined) {
this.buf2 = Cmd.get("EI");
this.shift();
cacheEntry.reset();
return cacheEntry;
}
}
if (cipherTransform) {
imageStream = cipherTransform.createStream(imageStream, length);
}
imageStream = this.filter(imageStream, dict, length);
imageStream.dict = dict;
if (cacheKey !== undefined) {
imageStream.cacheKey = `inline_${length}_${cacheKey}`;
this.imageCache[cacheKey] = imageStream;
}
this.buf2 = Cmd.get("EI");
this.shift();
return imageStream;
}
_findStreamLength(startPos, signature) {
const { stream } = this.lexer;
stream.pos = startPos;
const SCAN_BLOCK_LENGTH = 2048;
const signatureLength = signature.length;
while (stream.pos < stream.end) {
const scanBytes = stream.peekBytes(SCAN_BLOCK_LENGTH);
const scanLength = scanBytes.length - signatureLength;
if (scanLength <= 0) {
break;
}
let pos = 0;
while (pos < scanLength) {
let j = 0;
while (j < signatureLength && scanBytes[pos + j] === signature[j]) {
j++;
}
if (j >= signatureLength) {
// `signature` found.
stream.pos += pos;
return stream.pos - startPos;
}
pos++;
}
stream.pos += scanLength;
}
return -1;
}
makeStream(dict, cipherTransform) {
const lexer = this.lexer;
let stream = lexer.stream;
// Get the stream's start position.
lexer.skipToNextLine();
const startPos = stream.pos - 1;
// Get the length.
let length = dict.get("Length");
if (!Number.isInteger(length)) {
info(`Bad length "${length && length.toString()}" in stream.`);
length = 0;
}
// Skip over the stream data.
stream.pos = startPos + length;
lexer.nextChar();
// Shift '>>' and check whether the new object marks the end of the stream.
if (this.tryShift() && isCmd(this.buf2, "endstream")) {
this.shift(); // 'stream'
} else {
// Bad stream length, scanning for endstream command.
const ENDSTREAM_SIGNATURE = new Uint8Array([
0x65, 0x6e, 0x64, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d,
]);
let actualLength = this._findStreamLength(startPos, ENDSTREAM_SIGNATURE);
if (actualLength < 0) {
// Only allow limited truncation of the endstream signature,
// to prevent false positives.
const MAX_TRUNCATION = 1;
// Check if the PDF generator included truncated endstream commands,
// such as e.g. "endstrea" (fixes issue10004.pdf).
for (let i = 1; i <= MAX_TRUNCATION; i++) {
const end = ENDSTREAM_SIGNATURE.length - i;
const TRUNCATED_SIGNATURE = ENDSTREAM_SIGNATURE.slice(0, end);
const maybeLength = this._findStreamLength(
startPos,
TRUNCATED_SIGNATURE
);
if (maybeLength >= 0) {
// Ensure that the byte immediately following the truncated
// endstream command is a space, to prevent false positives.
const lastByte = stream.peekBytes(end + 1)[end];
if (!isWhiteSpace(lastByte)) {
break;
}
info(
`Found "${bytesToString(TRUNCATED_SIGNATURE)}" when ` +
"searching for endstream command."
);
actualLength = maybeLength;
break;
}
}
if (actualLength < 0) {
throw new FormatError("Missing endstream command.");
}
}
length = actualLength;
lexer.nextChar();
this.shift();
this.shift();
}
this.shift(); // 'endstream'
stream = stream.makeSubStream(startPos, length, dict);
if (cipherTransform) {
stream = cipherTransform.createStream(stream, length);
}
stream = this.filter(stream, dict, length);
stream.dict = dict;
return stream;
}
filter(stream, dict, length) {
let filter = dict.get("F", "Filter");
let params = dict.get("DP", "DecodeParms");
if (filter instanceof Name) {
if (Array.isArray(params)) {
warn("/DecodeParms should not be an Array, when /Filter is a Name.");
}
return this.makeFilter(stream, filter.name, length, params);
}
let maybeLength = length;
if (Array.isArray(filter)) {
const filterArray = filter;
const paramsArray = params;
for (let i = 0, ii = filterArray.length; i < ii; ++i) {
filter = this.xref.fetchIfRef(filterArray[i]);
if (!(filter instanceof Name)) {
throw new FormatError(`Bad filter name "${filter}"`);
}
params = null;
if (Array.isArray(paramsArray) && i in paramsArray) {
params = this.xref.fetchIfRef(paramsArray[i]);
}
stream = this.makeFilter(stream, filter.name, maybeLength, params);
// After the first stream the `length` variable is invalid.
maybeLength = null;
}
}
return stream;
}
makeFilter(stream, name, maybeLength, params) {
// Since the 'Length' entry in the stream dictionary can be completely
// wrong, e.g. zero for non-empty streams, only skip parsing the stream
// when we can be absolutely certain that it actually is empty.
if (maybeLength === 0) {
warn(`Empty "${name}" stream.`);
return new NullStream();
}
const xrefStats = this.xref.stats;
try {
switch (name) {
case "Fl":
case "FlateDecode":
xrefStats.addStreamType(StreamType.FLATE);
if (params) {
return new PredictorStream(
new FlateStream(stream, maybeLength),
maybeLength,
params
);
}
return new FlateStream(stream, maybeLength);
case "LZW":
case "LZWDecode":
xrefStats.addStreamType(StreamType.LZW);
let earlyChange = 1;
if (params) {
if (params.has("EarlyChange")) {
earlyChange = params.get("EarlyChange");
}
return new PredictorStream(
new LZWStream(stream, maybeLength, earlyChange),
maybeLength,
params
);
}
return new LZWStream(stream, maybeLength, earlyChange);
case "DCT":
case "DCTDecode":
xrefStats.addStreamType(StreamType.DCT);
return new JpegStream(stream, maybeLength, params);
case "JPX":
case "JPXDecode":
xrefStats.addStreamType(StreamType.JPX);
return new JpxStream(stream, maybeLength, params);
case "A85":
case "ASCII85Decode":
xrefStats.addStreamType(StreamType.A85);
return new Ascii85Stream(stream, maybeLength);
case "AHx":
case "ASCIIHexDecode":
xrefStats.addStreamType(StreamType.AHX);
return new AsciiHexStream(stream, maybeLength);
case "CCF":
case "CCITTFaxDecode":
xrefStats.addStreamType(StreamType.CCF);
return new CCITTFaxStream(stream, maybeLength, params);
case "RL":
case "RunLengthDecode":
xrefStats.addStreamType(StreamType.RLX);
return new RunLengthStream(stream, maybeLength);
case "JBIG2Decode":
xrefStats.addStreamType(StreamType.JBIG);
return new Jbig2Stream(stream, maybeLength, params);
}
warn(`Filter "${name}" is not supported.`);
return stream;
} catch (ex) {
if (ex instanceof MissingDataException) {
throw ex;
}
warn(`Invalid stream: "${ex}"`);
return new NullStream();
}
}
}
// A '1' in this array means the character is white space. A '1' or
// '2' means the character ends a name or command.
// prettier-ignore
const specialChars = [
1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, // 0x
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 1x
1, 0, 0, 0, 0, 2, 0, 0, 2, 2, 0, 0, 0, 0, 0, 2, // 2x
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 2, 0, // 3x
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 4x
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 2, 0, 0, // 5x
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 6x
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 2, 0, 0, // 7x
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 8x
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 9x
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // ax
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // bx
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // cx
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // dx
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // ex
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 // fx
];
function toHexDigit(ch) {
if (ch >= /* '0' = */ 0x30 && ch /* '9' = */ <= 0x39) {
return ch & 0x0f;
}
if (
(ch >= /* 'A' = */ 0x41 && ch <= /* 'F' = */ 0x46) ||
(ch >= /* 'a' = */ 0x61 && ch <= /* 'f' = */ 0x66)
) {
return (ch & 0x0f) + 9;
}
return -1;
}
class Lexer {
constructor(stream, knownCommands = null) {
this.stream = stream;
this.nextChar();
// While lexing, we build up many strings one char at a time. Using += for
// this can result in lots of garbage strings. It's better to build an
// array of single-char strings and then join() them together at the end.
// And reusing a single array (i.e. |this.strBuf|) over and over for this
// purpose uses less memory than using a new array for each string.
this.strBuf = [];
// The PDFs might have "glued" commands with other commands, operands or
// literals, e.g. "q1". The knownCommands is a dictionary of the valid
// commands and their prefixes. The prefixes are built the following way:
// if there a command that is a prefix of the other valid command or
// literal (e.g. 'f' and 'false') the following prefixes must be included,
// 'fa', 'fal', 'fals'. The prefixes are not needed, if the command has no
// other commands or literals as a prefix. The knowCommands is optional.
this.knownCommands = knownCommands;
this._hexStringNumWarn = 0;
this.beginInlineImagePos = -1;
}
nextChar() {
return (this.currentChar = this.stream.getByte());
}
peekChar() {
return this.stream.peekByte();
}
getNumber() {
let ch = this.currentChar;
let eNotation = false;
let divideBy = 0; // Different from 0 if it's a floating point value.
let sign = 0;
if (ch === /* '-' = */ 0x2d) {
sign = -1;
ch = this.nextChar();
if (ch === /* '-' = */ 0x2d) {
// Ignore double negative (this is consistent with Adobe Reader).
ch = this.nextChar();
}
} else if (ch === /* '+' = */ 0x2b) {
sign = 1;
ch = this.nextChar();
}
if (ch === /* LF = */ 0x0a || ch === /* CR = */ 0x0d) {
// Ignore line-breaks (this is consistent with Adobe Reader).
do {
ch = this.nextChar();
} while (ch === 0x0a || ch === 0x0d);
}
if (ch === /* '.' = */ 0x2e) {
divideBy = 10;
ch = this.nextChar();
}
if (ch < /* '0' = */ 0x30 || ch > /* '9' = */ 0x39) {
if (isWhiteSpace(ch) || ch === /* EOF = */ -1) {
// This is consistent with Adobe Reader (fixes issue9252.pdf).
if (divideBy === 10 && sign === 0) {
warn("Lexer.getNumber - treating a single decimal point as zero.");
return 0;
}
// This is consistent with Adobe Reader (fixes bug1753983.pdf).
if (divideBy === 0 && sign === -1) {
warn("Lexer.getNumber - treating a single minus sign as zero.");
return 0;
}
}
throw new FormatError(
`Invalid number: ${String.fromCharCode(ch)} (charCode ${ch})`
);
}
sign = sign || 1;
let baseValue = ch - 0x30; // '0'
let powerValue = 0;
let powerValueSign = 1;
while ((ch = this.nextChar()) >= 0) {
if (ch >= /* '0' = */ 0x30 && ch <= /* '9' = */ 0x39) {
const currentDigit = ch - 0x30; // '0'
if (eNotation) {
// We are after an 'e' or 'E'.
powerValue = powerValue * 10 + currentDigit;
} else {
if (divideBy !== 0) {
// We are after a point.
divideBy *= 10;
}
baseValue = baseValue * 10 + currentDigit;
}
} else if (ch === /* '.' = */ 0x2e) {
if (divideBy === 0) {
divideBy = 1;
} else {
// A number can have only one dot.
break;
}
} else if (ch === /* '-' = */ 0x2d) {
// Ignore minus signs in the middle of numbers to match
// Adobe's behavior.
warn("Badly formatted number: minus sign in the middle");
} else if (ch === /* 'E' = */ 0x45 || ch === /* 'e' = */ 0x65) {
// 'E' can be either a scientific notation or the beginning of a new
// operator.
ch = this.peekChar();
if (ch === /* '+' = */ 0x2b || ch === /* '-' = */ 0x2d) {
powerValueSign = ch === 0x2d ? -1 : 1;
this.nextChar(); // Consume the sign character.
} else if (ch < /* '0' = */ 0x30 || ch > /* '9' = */ 0x39) {
// The 'E' must be the beginning of a new operator.
break;
}
eNotation = true;
} else {
// The last character doesn't belong to us.
break;
}
}
if (divideBy !== 0) {
baseValue /= divideBy;
}
if (eNotation) {
baseValue *= 10 ** (powerValueSign * powerValue);
}
return sign * baseValue;
}
getString() {
let numParen = 1;
let done = false;
const strBuf = this.strBuf;
strBuf.length = 0;
let ch = this.nextChar();
while (true) {
let charBuffered = false;
switch (ch | 0) {
case -1:
warn("Unterminated string");
done = true;
break;
case 0x28: // '('
++numParen;
strBuf.push("(");
break;
case 0x29: // ')'
if (--numParen === 0) {
this.nextChar(); // consume strings ')'
done = true;
} else {
strBuf.push(")");
}
break;
case 0x5c: // '\\'
ch = this.nextChar();