@gilav21/shadcn-angular-rte 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -27
- package/fesm2022/{gilav21-shadcn-angular-rte-docx-parser-B8BeJWtY.mjs → gilav21-shadcn-angular-rte-docx-parser-DiBPweI4.mjs} +63 -38
- package/fesm2022/gilav21-shadcn-angular-rte-docx-parser-DiBPweI4.mjs.map +1 -0
- package/fesm2022/{gilav21-shadcn-angular-rte-docx-to-editor-html-Cl9eIhkC.mjs → gilav21-shadcn-angular-rte-docx-to-editor-html-BreagmS_.mjs} +43 -33
- package/fesm2022/gilav21-shadcn-angular-rte-docx-to-editor-html-BreagmS_.mjs.map +1 -0
- package/fesm2022/{gilav21-shadcn-angular-rte-gilav21-shadcn-angular-rte-CfjU_y-y.mjs → gilav21-shadcn-angular-rte-gilav21-shadcn-angular-rte-BnEAQOuU.mjs} +3561 -1901
- package/fesm2022/gilav21-shadcn-angular-rte-gilav21-shadcn-angular-rte-BnEAQOuU.mjs.map +1 -0
- package/fesm2022/gilav21-shadcn-angular-rte-markup-safety-qJk8EoPk.mjs +69 -0
- package/fesm2022/gilav21-shadcn-angular-rte-markup-safety-qJk8EoPk.mjs.map +1 -0
- package/fesm2022/{gilav21-shadcn-angular-rte-pdf-readable-DmE-CIxv.mjs → gilav21-shadcn-angular-rte-pdf-readable-JvCpuNQH.mjs} +445 -124
- package/fesm2022/gilav21-shadcn-angular-rte-pdf-readable-JvCpuNQH.mjs.map +1 -0
- package/fesm2022/gilav21-shadcn-angular-rte.mjs +1 -1
- package/package.json +6 -2
- package/schematics/collection.json +10 -0
- package/schematics/ng-add/index.cjs +169 -0
- package/schematics/ng-add/schema.json +13 -0
- package/styles.css +2 -0
- package/types/gilav21-shadcn-angular-rte.d.ts +365 -114
- package/types/gilav21-shadcn-angular-rte.d.ts.map +1 -1
- package/fesm2022/gilav21-shadcn-angular-rte-docx-parser-B8BeJWtY.mjs.map +0 -1
- package/fesm2022/gilav21-shadcn-angular-rte-docx-to-editor-html-Cl9eIhkC.mjs.map +0 -1
- package/fesm2022/gilav21-shadcn-angular-rte-gilav21-shadcn-angular-rte-CfjU_y-y.mjs.map +0 -1
- package/fesm2022/gilav21-shadcn-angular-rte-pdf-readable-DmE-CIxv.mjs.map +0 -1
- package/theme.css +0 -159
|
@@ -1,4 +1,79 @@
|
|
|
1
1
|
import { B as ByteSink, z as zlibInflate } from './gilav21-shadcn-angular-rte-inflate-laXxypva.mjs';
|
|
2
|
+
import { a as cssFontFamily, e as escapeAttribute, s as safeLinkHref } from './gilav21-shadcn-angular-rte-markup-safety-qJk8EoPk.mjs';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_PARSER_LIMITS = Object.freeze({
|
|
5
|
+
maxImagePixels: 50_000_000,
|
|
6
|
+
maxDecodedBytesTotal: 1024 * 1024 * 1024,
|
|
7
|
+
maxNestingDepth: 32,
|
|
8
|
+
maxFormDrawsPerPage: 10_000,
|
|
9
|
+
maxPages: 10_000,
|
|
10
|
+
maxOle2StreamBytes: 256 * 1024 * 1024,
|
|
11
|
+
});
|
|
12
|
+
/**
|
|
13
|
+
* Marks `node` visited. Returns false when it already was, so a walk over a
|
|
14
|
+
* shared or cyclic reference graph visits each node once.
|
|
15
|
+
*/
|
|
16
|
+
function visitOnce(seen, node) {
|
|
17
|
+
if (seen.has(node))
|
|
18
|
+
return false;
|
|
19
|
+
seen.add(node);
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Guards recursive drawing of shared content (form XObjects): a node is
|
|
24
|
+
* skipped when it is already on the current draw path (a cycle), when the path
|
|
25
|
+
* is at the depth cap, or when the draw budget is spent. A node that is merely
|
|
26
|
+
* shared — drawn again from a different path — is drawn every time, because the
|
|
27
|
+
* path is a recursion stack, not an "ever drawn" set.
|
|
28
|
+
*/
|
|
29
|
+
class DrawPath {
|
|
30
|
+
constructor(maxDepth, maxDraws) {
|
|
31
|
+
this.maxDepth = maxDepth;
|
|
32
|
+
this.maxDraws = maxDraws;
|
|
33
|
+
this.path = new Set();
|
|
34
|
+
this.draws = 0;
|
|
35
|
+
}
|
|
36
|
+
/** Runs `draw` inside `node` unless a guard trips; returns whether it ran. */
|
|
37
|
+
enter(node, draw) {
|
|
38
|
+
if (this.path.has(node) || this.path.size >= this.maxDepth || this.draws >= this.maxDraws) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
this.draws++;
|
|
42
|
+
this.path.add(node);
|
|
43
|
+
try {
|
|
44
|
+
draw();
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
this.path.delete(node);
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** A byte budget shared by every stream decode in one parse. */
|
|
53
|
+
class DecodeBudget {
|
|
54
|
+
constructor(total) {
|
|
55
|
+
this.remaining = total;
|
|
56
|
+
}
|
|
57
|
+
/** Charges `bytes`; returns false (charging nothing) when they do not fit. */
|
|
58
|
+
take(bytes) {
|
|
59
|
+
if (bytes > this.remaining)
|
|
60
|
+
return false;
|
|
61
|
+
this.remaining -= bytes;
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
/** True once nothing is left, so a caller can refuse work before doing it. */
|
|
65
|
+
get spent() {
|
|
66
|
+
return this.remaining <= 0;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Gives up what is left. A charge that did not fit means the work it paid
|
|
70
|
+
* for was already done; spending the rest stops every later decode of the
|
|
71
|
+
* same size from doing that work again just to be refused.
|
|
72
|
+
*/
|
|
73
|
+
spendRemainder() {
|
|
74
|
+
this.remaining = 0;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
2
77
|
|
|
3
78
|
/**
|
|
4
79
|
* Ceiling for a single decoded PDF stream. A stream's filter chain can expand
|
|
@@ -369,8 +444,12 @@ function decodeCCITTFax(data, parms, reader) {
|
|
|
369
444
|
const rows = reader.getNumber(parms['Rows']) || 0;
|
|
370
445
|
const encodedByteAlign = !!parms['EncodedByteAlign'];
|
|
371
446
|
const blackIs1 = !!parms['BlackIs1'];
|
|
447
|
+
// Columns and Rows are the file's claim, not the image's size: a row that
|
|
448
|
+
// alone exceeds the pixel cap is refused, and rows stop where the cap does.
|
|
449
|
+
if (columns <= 0 || columns > reader.limits.maxImagePixels)
|
|
450
|
+
return new Uint8Array(0);
|
|
372
451
|
const rowBytes = Math.ceil(columns / 8);
|
|
373
|
-
const maxRows = rows > 0 ? rows : 10000;
|
|
452
|
+
const maxRows = Math.min(rows > 0 ? rows : 10000, Math.floor(reader.limits.maxImagePixels / columns));
|
|
374
453
|
const output = [];
|
|
375
454
|
const bitReader = new CCITTBitReader(data);
|
|
376
455
|
if (k < 0) {
|
|
@@ -399,7 +478,9 @@ function decodeCCITTGroup3Row(reader, columns) {
|
|
|
399
478
|
const line = new Uint8Array(columns);
|
|
400
479
|
let col = 0;
|
|
401
480
|
let isWhite = true;
|
|
402
|
-
|
|
481
|
+
// Past the end of the data every bit reads 0, which is no code at all, so a
|
|
482
|
+
// row cut short would otherwise alternate zero-length runs forever.
|
|
483
|
+
while (col < columns && !reader.exhausted) {
|
|
403
484
|
const tree = isWhite ? whiteTree : blackTree;
|
|
404
485
|
const runLen = ccittDecodeRun(reader, tree);
|
|
405
486
|
const fill = isWhite ? 0 : 1;
|
|
@@ -575,13 +656,24 @@ function paethPredictor(a, b, c) {
|
|
|
575
656
|
}
|
|
576
657
|
// ── PDF structure parser ────────────────────────────────────────────────
|
|
577
658
|
class PdfReader {
|
|
578
|
-
|
|
659
|
+
/**
|
|
660
|
+
* `limits` holds the caps the reader and its renderers apply to what the
|
|
661
|
+
* file declares — image size, nesting, page count, form draws and total
|
|
662
|
+
* decoded bytes.
|
|
663
|
+
*/
|
|
664
|
+
constructor(buffer, limits = DEFAULT_PARSER_LIMITS) {
|
|
665
|
+
this.limits = limits;
|
|
579
666
|
this.objects = new Map();
|
|
580
667
|
this.parsedObjects = new Map();
|
|
581
668
|
this.compressedObjects = new Map();
|
|
582
669
|
this.trailer = null;
|
|
670
|
+
/** References being parsed right now: an object whose /Length leads back to itself must not recurse. */
|
|
671
|
+
this.resolving = new Set();
|
|
672
|
+
/** Arrays and dictionaries open right now in the value being parsed. */
|
|
673
|
+
this.nesting = 0;
|
|
583
674
|
this.data = new Uint8Array(buffer);
|
|
584
675
|
this.text = new TextDecoder('latin1').decode(this.data);
|
|
676
|
+
this.decodeBudget = new DecodeBudget(limits.maxDecodedBytesTotal);
|
|
585
677
|
}
|
|
586
678
|
parse() {
|
|
587
679
|
this.findXRef();
|
|
@@ -614,12 +706,21 @@ class PdfReader {
|
|
|
614
706
|
match ??= this.findLastStartxref(this.text.slice(-8192));
|
|
615
707
|
if (!match)
|
|
616
708
|
throw new Error('Could not find startxref in PDF');
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
709
|
+
this.walkXRefChain(Number.parseInt(match[1], 10));
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Reads the newest xref section, then each /Prev behind it. A section
|
|
713
|
+
* already read ends the walk, so a /Prev that points back at itself or at a
|
|
714
|
+
* later section cannot loop, and the walk needs no call stack however long
|
|
715
|
+
* the chain of incremental updates is.
|
|
716
|
+
*/
|
|
717
|
+
walkXRefChain(newest) {
|
|
718
|
+
const seen = new Set();
|
|
719
|
+
let offset = newest;
|
|
720
|
+
while (offset !== null && visitOnce(seen, offset)) {
|
|
721
|
+
offset = this.text.substring(offset, offset + 4) === 'xref'
|
|
722
|
+
? this.parseTraditionalXRef(offset)
|
|
723
|
+
: this.parseXRefStream(offset);
|
|
623
724
|
}
|
|
624
725
|
}
|
|
625
726
|
/**
|
|
@@ -724,23 +825,27 @@ class PdfReader {
|
|
|
724
825
|
this.objects.set(key, { offset: entryOffset, gen });
|
|
725
826
|
}
|
|
726
827
|
}
|
|
828
|
+
/** Adopts the section's trailer; returns the /Prev offset it names, if any. */
|
|
727
829
|
parseTrailerDict(offset) {
|
|
728
830
|
const trailerPos = this.text.indexOf('trailer', offset);
|
|
729
831
|
if (trailerPos === -1)
|
|
730
|
-
return;
|
|
832
|
+
return null;
|
|
731
833
|
const dictStart = this.text.indexOf('<<', trailerPos);
|
|
732
834
|
if (dictStart === -1)
|
|
733
|
-
return;
|
|
835
|
+
return null;
|
|
734
836
|
const result = this.parseObjectAt(dictStart);
|
|
735
837
|
if (result.obj.type !== 'dict')
|
|
736
|
-
return;
|
|
838
|
+
return null;
|
|
737
839
|
const dict = result.obj.value;
|
|
738
840
|
this.trailer ??= dict;
|
|
841
|
+
return this.prevOffset(dict);
|
|
842
|
+
}
|
|
843
|
+
prevOffset(dict) {
|
|
739
844
|
const prev = dict['Prev'];
|
|
740
|
-
|
|
741
|
-
this.parseTraditionalXRef(prev.value);
|
|
742
|
-
}
|
|
845
|
+
return prev?.type === 'number' ? prev.value : null;
|
|
743
846
|
}
|
|
847
|
+
/** Smallest an entry can be: 10 + 1 + 5 + 1 + 1 characters, with no terminator. */
|
|
848
|
+
static { this.MIN_XREF_ENTRY_CHARS = 18; }
|
|
744
849
|
parseTraditionalXRef(offset) {
|
|
745
850
|
let pos = offset + 4;
|
|
746
851
|
pos = this.skipWhitespace(pos);
|
|
@@ -754,14 +859,18 @@ class PdfReader {
|
|
|
754
859
|
const count = Number.parseInt(headerMatch[2], 10);
|
|
755
860
|
pos += headerMatch[0].length;
|
|
756
861
|
pos = this.skipWhitespace(pos);
|
|
757
|
-
|
|
862
|
+
// The count is the file's claim; the bytes left bound what it can hold.
|
|
863
|
+
const holdable = Math.ceil((this.text.length - pos) / PdfReader.MIN_XREF_ENTRY_CHARS);
|
|
864
|
+
for (let i = 0; i < Math.min(count, holdable); i++) {
|
|
865
|
+
if (this.text.startsWith('trailer', pos))
|
|
866
|
+
break;
|
|
758
867
|
this.parseXRefEntry(this.text.substring(pos, pos + 20), startObj, i);
|
|
759
868
|
pos += 20;
|
|
760
869
|
while (pos < this.text.length && (this.text[pos] === '\r' || this.text[pos] === '\n' || this.text[pos] === ' '))
|
|
761
870
|
pos++;
|
|
762
871
|
}
|
|
763
872
|
}
|
|
764
|
-
this.parseTrailerDict(offset);
|
|
873
|
+
return this.parseTrailerDict(offset);
|
|
765
874
|
}
|
|
766
875
|
readXRefField(decoded, bytePos, fieldOffset, fieldWidth) {
|
|
767
876
|
let value = 0;
|
|
@@ -809,18 +918,13 @@ class PdfReader {
|
|
|
809
918
|
}
|
|
810
919
|
}
|
|
811
920
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
if (this.text.substring(prevOffset, prevOffset + 4) === 'xref') {
|
|
818
|
-
this.parseTraditionalXRef(prevOffset);
|
|
819
|
-
}
|
|
820
|
-
else {
|
|
821
|
-
this.parseXRefStream(prevOffset);
|
|
822
|
-
}
|
|
921
|
+
/** An xref stream entry is three fields of 0-8 bytes; all-zero widths would consume no input. */
|
|
922
|
+
static isValidXRefWidths(w) {
|
|
923
|
+
return w.length >= 3
|
|
924
|
+
&& w.every(width => Number.isInteger(width) && width >= 0 && width <= 8)
|
|
925
|
+
&& w[0] + w[1] + w[2] > 0;
|
|
823
926
|
}
|
|
927
|
+
/** Adopts the section's entries and trailer; returns the /Prev offset it names, if any. */
|
|
824
928
|
parseXRefStream(offset) {
|
|
825
929
|
const result = this.parseObjectAt(offset);
|
|
826
930
|
const obj = result.obj;
|
|
@@ -833,10 +937,12 @@ class PdfReader {
|
|
|
833
937
|
if (wArr?.type !== 'array')
|
|
834
938
|
throw new Error('Missing /W in xref stream');
|
|
835
939
|
const w = wArr.value.map(o => o.value);
|
|
940
|
+
if (!PdfReader.isValidXRefWidths(w))
|
|
941
|
+
throw new Error('Invalid /W in xref stream');
|
|
836
942
|
const indexArr = this.getXRefIndexArray(dict);
|
|
837
943
|
const decoded = this.decodeStreamData(dict, streamData);
|
|
838
944
|
this.processXRefStreamEntries(decoded, w, indexArr);
|
|
839
|
-
this.
|
|
945
|
+
return this.prevOffset(dict);
|
|
840
946
|
}
|
|
841
947
|
skipWhitespace(pos) {
|
|
842
948
|
while (pos < this.text.length && ' \t\r\n\0\f'.includes(this.text[pos]))
|
|
@@ -847,10 +953,8 @@ class PdfReader {
|
|
|
847
953
|
const lengthObj = dict['Length'];
|
|
848
954
|
if (!lengthObj)
|
|
849
955
|
return 0;
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
}
|
|
853
|
-
return lengthObj.value;
|
|
956
|
+
const length = lengthObj.type === 'ref' ? this.resolveRef(lengthObj) : lengthObj;
|
|
957
|
+
return typeof length.value === 'number' ? length.value : 0;
|
|
854
958
|
}
|
|
855
959
|
parseObjectAt(offset) {
|
|
856
960
|
let pos = offset;
|
|
@@ -890,7 +994,7 @@ class PdfReader {
|
|
|
890
994
|
const ch = this.text[pos];
|
|
891
995
|
const ch2 = pos + 1 < this.text.length ? this.text[pos + 1] : '';
|
|
892
996
|
if (ch === '<' && ch2 === '<') {
|
|
893
|
-
return this.parseDict(pos);
|
|
997
|
+
return this.parseNested(pos, () => this.parseDict(pos));
|
|
894
998
|
}
|
|
895
999
|
if (ch === '<') {
|
|
896
1000
|
return this.parseHexString(pos);
|
|
@@ -902,7 +1006,7 @@ class PdfReader {
|
|
|
902
1006
|
return this.parseName(pos);
|
|
903
1007
|
}
|
|
904
1008
|
if (ch === '[') {
|
|
905
|
-
return this.parseArray(pos);
|
|
1009
|
+
return this.parseNested(pos, () => this.parseArray(pos));
|
|
906
1010
|
}
|
|
907
1011
|
if (ch === 't' && this.text.substring(pos, pos + 4) === 'true') {
|
|
908
1012
|
return { obj: { type: 'boolean', value: true }, endPos: pos + 4 };
|
|
@@ -932,6 +1036,56 @@ class PdfReader {
|
|
|
932
1036
|
}
|
|
933
1037
|
return { obj: { type: 'number', value: num }, endPos: afterNum };
|
|
934
1038
|
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Parses the container at `pos` unless it would open past the nesting cap.
|
|
1041
|
+
* One that would is skipped whole and read as null: following it down costs
|
|
1042
|
+
* a stack frame per level, and a file can nest to its own length.
|
|
1043
|
+
*/
|
|
1044
|
+
parseNested(pos, parse) {
|
|
1045
|
+
if (this.nesting >= this.limits.maxNestingDepth) {
|
|
1046
|
+
return { obj: { type: 'null', value: null }, endPos: this.skipContainer(pos) };
|
|
1047
|
+
}
|
|
1048
|
+
this.nesting++;
|
|
1049
|
+
try {
|
|
1050
|
+
return parse();
|
|
1051
|
+
}
|
|
1052
|
+
finally {
|
|
1053
|
+
this.nesting--;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
/** The end of the dictionary or array opening at `pos`, found without recursion. */
|
|
1057
|
+
skipContainer(pos) {
|
|
1058
|
+
let depth = 0;
|
|
1059
|
+
let i = pos;
|
|
1060
|
+
while (i < this.text.length) {
|
|
1061
|
+
const ch = this.text[i];
|
|
1062
|
+
const pair = ch + this.text.charAt(i + 1);
|
|
1063
|
+
if (ch === '(') {
|
|
1064
|
+
i = this.parseLiteralString(i).endPos;
|
|
1065
|
+
}
|
|
1066
|
+
else if (pair === '<<' || pair === '>>') {
|
|
1067
|
+
depth += pair === '<<' ? 1 : -1;
|
|
1068
|
+
i += 2;
|
|
1069
|
+
}
|
|
1070
|
+
else if (ch === '<') {
|
|
1071
|
+
// Not parseHexString: malformed hex is skipped, not decoded.
|
|
1072
|
+
const close = this.text.indexOf('>', i);
|
|
1073
|
+
i = close === -1 ? this.text.length : close + 1;
|
|
1074
|
+
}
|
|
1075
|
+
else {
|
|
1076
|
+
depth += PdfReader.bracketDelta(ch);
|
|
1077
|
+
i++;
|
|
1078
|
+
}
|
|
1079
|
+
if (depth <= 0)
|
|
1080
|
+
return i;
|
|
1081
|
+
}
|
|
1082
|
+
return i;
|
|
1083
|
+
}
|
|
1084
|
+
static bracketDelta(ch) {
|
|
1085
|
+
if (ch === '[')
|
|
1086
|
+
return 1;
|
|
1087
|
+
return ch === ']' ? -1 : 0;
|
|
1088
|
+
}
|
|
935
1089
|
parseDict(pos) {
|
|
936
1090
|
pos += 2;
|
|
937
1091
|
const dict = {};
|
|
@@ -1115,9 +1269,17 @@ class PdfReader {
|
|
|
1115
1269
|
this.parseObjectStream(compressed.streamObjNum);
|
|
1116
1270
|
return this.parsedObjects.get(key) ?? { type: 'null', value: null };
|
|
1117
1271
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1272
|
+
if (this.resolving.has(key))
|
|
1273
|
+
return { type: 'null', value: null };
|
|
1274
|
+
this.resolving.add(key);
|
|
1275
|
+
try {
|
|
1276
|
+
const result = this.parseObjectAt(entry.offset);
|
|
1277
|
+
this.parsedObjects.set(key, result.obj);
|
|
1278
|
+
return result.obj;
|
|
1279
|
+
}
|
|
1280
|
+
finally {
|
|
1281
|
+
this.resolving.delete(key);
|
|
1282
|
+
}
|
|
1121
1283
|
}
|
|
1122
1284
|
/**
|
|
1123
1285
|
* Follow a chain of indirect references to the object it ends at.
|
|
@@ -1236,6 +1398,8 @@ class PdfReader {
|
|
|
1236
1398
|
const filterObj = dict['Filter'];
|
|
1237
1399
|
if (!filterObj)
|
|
1238
1400
|
return rawStream;
|
|
1401
|
+
if (this.decodeBudget.spent)
|
|
1402
|
+
return new Uint8Array(0);
|
|
1239
1403
|
const filters = this.resolveFilterNames(filterObj);
|
|
1240
1404
|
const decodeParms = this.getDecodeParmsArray(dict, filters.length);
|
|
1241
1405
|
let result = rawStream;
|
|
@@ -1250,7 +1414,14 @@ class PdfReader {
|
|
|
1250
1414
|
return rawStream;
|
|
1251
1415
|
}
|
|
1252
1416
|
}
|
|
1253
|
-
|
|
1417
|
+
// A stream referenced from many places is decoded again at each one; the
|
|
1418
|
+
// budget is what keeps those repeats from adding up to gigabytes. Past
|
|
1419
|
+
// it the stream reads as empty, and the budget is spent so the next
|
|
1420
|
+
// reference is refused before it decodes anything.
|
|
1421
|
+
if (this.decodeBudget.take(result.length))
|
|
1422
|
+
return result;
|
|
1423
|
+
this.decodeBudget.spendRemainder();
|
|
1424
|
+
return new Uint8Array(0);
|
|
1254
1425
|
}
|
|
1255
1426
|
getStreamData(obj) {
|
|
1256
1427
|
const resolved = this.resolveDeep(obj);
|
|
@@ -1296,21 +1467,32 @@ class PdfReader {
|
|
|
1296
1467
|
return this.collectPages(pagesRef);
|
|
1297
1468
|
}
|
|
1298
1469
|
static { this.INHERITABLE_PAGE_KEYS = ['Resources', 'MediaBox', 'CropBox', 'Rotate']; }
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
return [this.resolveDeep(node)];
|
|
1307
|
-
}
|
|
1308
|
-
if (!kids)
|
|
1309
|
-
return [];
|
|
1310
|
-
const nextInherited = this.mergeInheritablePageKeys(dict, inherited);
|
|
1470
|
+
/**
|
|
1471
|
+
* Walks the page tree depth-first, in page order. Each node is visited once,
|
|
1472
|
+
* so a /Kids that lists an ancestor, or the same child many times, yields
|
|
1473
|
+
* every distinct page once; the walk keeps its own stack, so a tree of any
|
|
1474
|
+
* depth costs no call stack.
|
|
1475
|
+
*/
|
|
1476
|
+
collectPages(root) {
|
|
1311
1477
|
const pages = [];
|
|
1312
|
-
|
|
1313
|
-
|
|
1478
|
+
const seen = new Set();
|
|
1479
|
+
const pending = [{ node: root, inherited: {} }];
|
|
1480
|
+
for (let next = pending.pop(); next && pages.length < this.limits.maxPages; next = pending.pop()) {
|
|
1481
|
+
const resolved = this.resolveDeep(next.node);
|
|
1482
|
+
if (!visitOnce(seen, resolved))
|
|
1483
|
+
continue;
|
|
1484
|
+
const dict = this.getDict(resolved);
|
|
1485
|
+
const kids = dict['Kids'];
|
|
1486
|
+
if (this.getString(dict['Type']) === 'Page' || (!kids && dict['Contents'])) {
|
|
1487
|
+
this.applyInheritedPageKeys(dict, next.inherited);
|
|
1488
|
+
pages.push(resolved);
|
|
1489
|
+
}
|
|
1490
|
+
else if (kids) {
|
|
1491
|
+
const inherited = this.mergeInheritablePageKeys(dict, next.inherited);
|
|
1492
|
+
// Pushed last-first, so the first kid is popped first.
|
|
1493
|
+
for (const kid of this.getArray(kids).reverse())
|
|
1494
|
+
pending.push({ node: kid, inherited });
|
|
1495
|
+
}
|
|
1314
1496
|
}
|
|
1315
1497
|
return pages;
|
|
1316
1498
|
}
|
|
@@ -1417,11 +1599,32 @@ function pdfStringToUnicode(raw, fontInfo) {
|
|
|
1417
1599
|
const { result, charCodes } = decodeSingleByteString(raw, fontInfo?.toUnicode);
|
|
1418
1600
|
return { text: result, charCodes };
|
|
1419
1601
|
}
|
|
1602
|
+
const WHITESPACE = /\s/;
|
|
1603
|
+
/**
|
|
1604
|
+
* The text between each `begin` marker (plus the one whitespace character that
|
|
1605
|
+
* must follow it) and the next `end`, in order. Found with indexOf: a lazy
|
|
1606
|
+
* regex retries from every unterminated `begin` and rescans to the end of the
|
|
1607
|
+
* text each time, which is quadratic. Once no `end` is left, no later section
|
|
1608
|
+
* can close either, so the scan stops.
|
|
1609
|
+
*/
|
|
1610
|
+
function* markedSections(text, begin, end) {
|
|
1611
|
+
let from = 0;
|
|
1612
|
+
for (let start = text.indexOf(begin, from); start !== -1; start = text.indexOf(begin, from)) {
|
|
1613
|
+
const bodyStart = start + begin.length + 1;
|
|
1614
|
+
if (!WHITESPACE.test(text.charAt(bodyStart - 1))) {
|
|
1615
|
+
from = start + 1;
|
|
1616
|
+
continue;
|
|
1617
|
+
}
|
|
1618
|
+
const close = text.indexOf(end, bodyStart);
|
|
1619
|
+
if (close === -1)
|
|
1620
|
+
return;
|
|
1621
|
+
yield text.slice(bodyStart, close);
|
|
1622
|
+
from = close + end.length;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1420
1625
|
function parseBfCharEntries(text, map) {
|
|
1421
|
-
const
|
|
1422
|
-
|
|
1423
|
-
while ((match = bfCharRe.exec(text)) !== null) {
|
|
1424
|
-
for (const entry of match[1].trim().split('\n')) {
|
|
1626
|
+
for (const section of markedSections(text, 'beginbfchar', 'endbfchar')) {
|
|
1627
|
+
for (const entry of section.trim().split('\n')) {
|
|
1425
1628
|
const parts = /<([0-9a-fA-F]+)>\s*<([0-9a-fA-F]+)>/.exec(entry.trim());
|
|
1426
1629
|
if (!parts)
|
|
1427
1630
|
continue;
|
|
@@ -1468,10 +1671,8 @@ function parseBfRangeEntry(trimmed, map) {
|
|
|
1468
1671
|
}
|
|
1469
1672
|
}
|
|
1470
1673
|
function parseBfRangeEntries(text, map) {
|
|
1471
|
-
const
|
|
1472
|
-
|
|
1473
|
-
while ((match = bfRangeRe.exec(text)) !== null) {
|
|
1474
|
-
for (const entry of match[1].trim().split('\n')) {
|
|
1674
|
+
for (const section of markedSections(text, 'beginbfrange', 'endbfrange')) {
|
|
1675
|
+
for (const entry of section.trim().split('\n')) {
|
|
1475
1676
|
parseBfRangeEntry(entry.trim(), map);
|
|
1476
1677
|
}
|
|
1477
1678
|
}
|
|
@@ -2709,9 +2910,15 @@ function buildFormCtm(ctx, dict) {
|
|
|
2709
2910
|
const m = matrixArr.map(o => ctx.reader.getNumber(o));
|
|
2710
2911
|
return multiplyMatrix$1(m, ctx.gs.ctm);
|
|
2711
2912
|
}
|
|
2913
|
+
/**
|
|
2914
|
+
* Draws a form XObject unless it is already being drawn higher up (a cycle),
|
|
2915
|
+
* the path is at the nesting cap, or the page's draw budget is spent. A form
|
|
2916
|
+
* shared between sibling draws is drawn every time.
|
|
2917
|
+
*/
|
|
2712
2918
|
function processFormXObject(parentCtx, formObj) {
|
|
2713
|
-
|
|
2714
|
-
|
|
2919
|
+
parentCtx.formDraws.enter(formObj, () => drawFormXObject(parentCtx, formObj));
|
|
2920
|
+
}
|
|
2921
|
+
function drawFormXObject(parentCtx, formObj) {
|
|
2715
2922
|
const dict = formObj.value;
|
|
2716
2923
|
if (!formObj.stream)
|
|
2717
2924
|
return;
|
|
@@ -2745,7 +2952,7 @@ function buildFormContext(parentCtx, fontInfoMap, xObjectDict, resources, formCt
|
|
|
2745
2952
|
lineCap: parentCtx.gs.lineCap, lineJoin: parentCtx.gs.lineJoin,
|
|
2746
2953
|
},
|
|
2747
2954
|
operandStack: [], pageIndex: parentCtx.pageIndex,
|
|
2748
|
-
|
|
2955
|
+
formDraws: parentCtx.formDraws, compatibilityMode: 0,
|
|
2749
2956
|
mcidStack: [], ocgOffSet: parentCtx.ocgOffSet ?? new Set(),
|
|
2750
2957
|
ocgHiddenDepth: 0, perFragment: parentCtx.perFragment,
|
|
2751
2958
|
patternFillImage: null,
|
|
@@ -3739,7 +3946,7 @@ function buildExtractionContext(reader, resources, fontDict, xObjectDict, initia
|
|
|
3739
3946
|
dashArray: [], dashPhase: 0, lineCap: 0, lineJoin: 0,
|
|
3740
3947
|
},
|
|
3741
3948
|
operandStack: [], pageIndex,
|
|
3742
|
-
|
|
3949
|
+
formDraws: new DrawPath(reader.limits.maxNestingDepth, reader.limits.maxFormDrawsPerPage), compatibilityMode: 0,
|
|
3743
3950
|
mcidStack: [], ocgOffSet: new Set(),
|
|
3744
3951
|
ocgHiddenDepth: 0, perFragment: options?.perFragment ?? false,
|
|
3745
3952
|
patternFillImage: null,
|
|
@@ -3795,6 +4002,16 @@ function resolveImageFilterName(reader, dict) {
|
|
|
3795
4002
|
const last = reader.resolveDeep(lastEntry);
|
|
3796
4003
|
return last.type === 'name' ? last.value : '';
|
|
3797
4004
|
}
|
|
4005
|
+
const ALLOWED_BITS_PER_COMPONENT = new Set([1, 2, 4, 8, 16]);
|
|
4006
|
+
/**
|
|
4007
|
+
* Whether the declared raster is one we will expand into pixel rows, PNG and
|
|
4008
|
+
* base64 (roughly 24 bytes per source byte). The size is the file's claim, so it
|
|
4009
|
+
* is checked before anything is allocated for it.
|
|
4010
|
+
*/
|
|
4011
|
+
function isDecodableRaster(reader, width, height, dict) {
|
|
4012
|
+
const bpc = reader.getNumber(dict['BitsPerComponent']) || 8;
|
|
4013
|
+
return ALLOWED_BITS_PER_COMPONENT.has(bpc) && width * height <= reader.limits.maxImagePixels;
|
|
4014
|
+
}
|
|
3798
4015
|
function buildImageDataUrl(filterName, xObj, reader, width, height, dict) {
|
|
3799
4016
|
if (filterName === 'DCTDecode') {
|
|
3800
4017
|
if (!xObj.stream)
|
|
@@ -3806,6 +4023,8 @@ function buildImageDataUrl(filterName, xObj, reader, width, height, dict) {
|
|
|
3806
4023
|
return null;
|
|
3807
4024
|
return `data:image/jp2;base64,${uint8ArrayToBase64(xObj.stream)}`;
|
|
3808
4025
|
}
|
|
4026
|
+
if (!isDecodableRaster(reader, width, height, dict))
|
|
4027
|
+
return null;
|
|
3809
4028
|
const decoded = reader.getStreamData(xObj);
|
|
3810
4029
|
if (decoded.length === 0)
|
|
3811
4030
|
return null;
|
|
@@ -4620,7 +4839,7 @@ function buildItemStyles(item, bodyFont, bodySize) {
|
|
|
4620
4839
|
if (!isDefaultColor(item.color))
|
|
4621
4840
|
styles.push(`color: ${item.color}`);
|
|
4622
4841
|
if (item.fontFamily && item.fontFamily !== bodyFont)
|
|
4623
|
-
styles.push(`font-family:
|
|
4842
|
+
styles.push(`font-family: ${cssFontFamily(item.fontFamily)}`);
|
|
4624
4843
|
const sizeRatio = bodySize > 0 ? item.fontSize / bodySize : 1;
|
|
4625
4844
|
if (sizeRatio < 0.85 || sizeRatio > 1.15) {
|
|
4626
4845
|
const editorSize = Math.round(sizeRatio * 12);
|
|
@@ -4642,7 +4861,7 @@ function wrapItemHtml(item, text, bodyFont, bodySize, linkUri) {
|
|
|
4642
4861
|
let fragment = text;
|
|
4643
4862
|
const styles = buildItemStyles(item, bodyFont, bodySize);
|
|
4644
4863
|
if (styles.length > 0) {
|
|
4645
|
-
fragment = `<span style="${styles.join('; ')}">${fragment}</span>`;
|
|
4864
|
+
fragment = `<span style="${escapeAttribute(styles.join('; '))}">${fragment}</span>`;
|
|
4646
4865
|
}
|
|
4647
4866
|
if (item.textRise > 1) {
|
|
4648
4867
|
fragment = `<sup>${fragment}</sup>`;
|
|
@@ -4654,10 +4873,10 @@ function wrapItemHtml(item, text, bodyFont, bodySize, linkUri) {
|
|
|
4654
4873
|
fragment = `<em>${fragment}</em>`;
|
|
4655
4874
|
if (item.bold)
|
|
4656
4875
|
fragment = `<strong>${fragment}</strong>`;
|
|
4657
|
-
|
|
4658
|
-
|
|
4876
|
+
const safeUri = linkUri ? safeLinkHref(linkUri) : undefined;
|
|
4877
|
+
if (safeUri !== undefined) {
|
|
4659
4878
|
const linkColor = isDefaultColor(item.color) ? '#1a0dab' : item.color;
|
|
4660
|
-
fragment = `<a href="${safeUri}" target="_blank" rel="noopener noreferrer" style="color:${linkColor};text-decoration:underline">${fragment}</a>`;
|
|
4879
|
+
fragment = `<a href="${escapeAttribute(safeUri)}" target="_blank" rel="noopener noreferrer" style="color:${linkColor};text-decoration:underline">${fragment}</a>`;
|
|
4661
4880
|
}
|
|
4662
4881
|
return fragment;
|
|
4663
4882
|
}
|
|
@@ -4756,7 +4975,21 @@ function detectColumns(lines) {
|
|
|
4756
4975
|
const BULLET_PATTERN = /^[\u2022\u2023\u25E6\u2043\u2219\u25CF\u25CB\u25AA\u25AB\u2013\u2014\-*]\s*/;
|
|
4757
4976
|
const BULLET_CHARS = /^[\u2022\u2023\u25E6\u2043\u2219\u25CF\u25CB\u25AA\u25AB\u2013\u2014\-*]+$/;
|
|
4758
4977
|
const NUMBERED_PATTERN = /^(\d{1,3})[.)]\s+/;
|
|
4759
|
-
const
|
|
4978
|
+
const RTL_NUMBER_TAIL = /[.)]\d{1,3}$/;
|
|
4979
|
+
/**
|
|
4980
|
+
* Where a trailing right-to-left list number starts — whitespace, then ".12"
|
|
4981
|
+
* or ")12" at the end of the text — or -1. Matched from the number backwards:
|
|
4982
|
+
* a `\s+` regex retried from every space of a long run is quadratic.
|
|
4983
|
+
*/
|
|
4984
|
+
function rtlNumberTailStart(text) {
|
|
4985
|
+
const tail = RTL_NUMBER_TAIL.exec(text);
|
|
4986
|
+
if (!tail)
|
|
4987
|
+
return -1;
|
|
4988
|
+
let start = tail.index;
|
|
4989
|
+
while (start > 0 && WHITESPACE.test(text.charAt(start - 1)))
|
|
4990
|
+
start--;
|
|
4991
|
+
return start === tail.index ? -1 : start;
|
|
4992
|
+
}
|
|
4760
4993
|
function stripBulletItems(line) {
|
|
4761
4994
|
const filtered = line.items.filter(item => {
|
|
4762
4995
|
const t = item.text.trim();
|
|
@@ -4795,9 +5028,9 @@ function stripNumberedPrefix(line) {
|
|
|
4795
5028
|
const trimmed = item.text.replace(NUMBERED_PATTERN, '');
|
|
4796
5029
|
if (trimmed !== item.text)
|
|
4797
5030
|
return { ...item, text: trimmed };
|
|
4798
|
-
const
|
|
4799
|
-
if (
|
|
4800
|
-
return { ...item, text:
|
|
5031
|
+
const rtlStart = rtlNumberTailStart(item.text);
|
|
5032
|
+
if (rtlStart >= 0)
|
|
5033
|
+
return { ...item, text: item.text.slice(0, rtlStart) };
|
|
4801
5034
|
return item;
|
|
4802
5035
|
}),
|
|
4803
5036
|
y: line.y,
|
|
@@ -4980,7 +5213,7 @@ function appendToParagraph(line, bodySize, bodyFont, state, options, rtl) {
|
|
|
4980
5213
|
detectParagraphIndent(state);
|
|
4981
5214
|
}
|
|
4982
5215
|
function isNumberedLine(lineText, rtl) {
|
|
4983
|
-
return NUMBERED_PATTERN.test(lineText) || (rtl &&
|
|
5216
|
+
return NUMBERED_PATTERN.test(lineText) || (rtl && rtlNumberTailStart(lineText) >= 0);
|
|
4984
5217
|
}
|
|
4985
5218
|
function processHtmlLine(line, lineText, bodySize, bodyFont, state, options) {
|
|
4986
5219
|
const { structureMap } = options;
|
|
@@ -5791,39 +6024,44 @@ function resolvePageIndex(reader, pageRef, pages) {
|
|
|
5791
6024
|
return findPageIndexByDict(reader, dict, pages);
|
|
5792
6025
|
}
|
|
5793
6026
|
function processStructKDict(opts) {
|
|
5794
|
-
const {
|
|
6027
|
+
const { walk, kDict, effectivePage, depth, mcids, children, elemRef } = opts;
|
|
5795
6028
|
const mcidObj = kDict['MCID'];
|
|
5796
6029
|
if (mcidObj) {
|
|
5797
|
-
const mcidVal = reader.getNumber(mcidObj);
|
|
6030
|
+
const mcidVal = walk.reader.getNumber(mcidObj);
|
|
5798
6031
|
const mcidPageRef = kDict['Pg'];
|
|
5799
|
-
const mcidPage = mcidPageRef ? resolvePageIndex(reader, mcidPageRef, pages) : effectivePage;
|
|
6032
|
+
const mcidPage = mcidPageRef ? resolvePageIndex(walk.reader, mcidPageRef, walk.pages) : effectivePage;
|
|
5800
6033
|
mcids.push({ mcid: mcidVal, page: mcidPage >= 0 ? mcidPage : effectivePage });
|
|
5801
6034
|
}
|
|
5802
6035
|
else {
|
|
5803
|
-
const child = parseStructureElement(
|
|
6036
|
+
const child = parseStructureElement(walk, elemRef, effectivePage, depth + 1);
|
|
5804
6037
|
if (child)
|
|
5805
6038
|
children.push(child);
|
|
5806
6039
|
}
|
|
5807
6040
|
}
|
|
5808
|
-
function processStructKArrayItem(
|
|
5809
|
-
const itemResolved = reader.resolveDeep(item);
|
|
6041
|
+
function processStructKArrayItem(walk, item, effectivePage, depth, mcids, children) {
|
|
6042
|
+
const itemResolved = walk.reader.resolveDeep(item);
|
|
5810
6043
|
if (itemResolved.type === 'number') {
|
|
5811
6044
|
mcids.push({ mcid: itemResolved.value, page: effectivePage });
|
|
5812
6045
|
}
|
|
5813
6046
|
else if (itemResolved.type === 'dict') {
|
|
5814
6047
|
const itemDict = itemResolved.value;
|
|
5815
|
-
processStructKDict({
|
|
6048
|
+
processStructKDict({ walk, kDict: itemDict, effectivePage, depth, mcids, children, elemRef: item });
|
|
5816
6049
|
}
|
|
5817
6050
|
}
|
|
5818
|
-
function parseStructureElement(
|
|
6051
|
+
function parseStructureElement(walk, elemObj, parentPageIdx, depth) {
|
|
5819
6052
|
if (depth > 50)
|
|
5820
6053
|
return null;
|
|
6054
|
+
const { reader, pages } = walk;
|
|
5821
6055
|
const resolved = reader.resolveDeep(elemObj);
|
|
5822
6056
|
if (resolved.type === 'number') {
|
|
5823
6057
|
return { type: 'MCID', mcids: [{ mcid: resolved.value, page: parentPageIdx }], children: [] };
|
|
5824
6058
|
}
|
|
5825
6059
|
if (resolved.type !== 'dict')
|
|
5826
6060
|
return null;
|
|
6061
|
+
// An element has one parent; a second reference to it is a shared or cyclic
|
|
6062
|
+
// node, and expanding it again costs a copy of its whole subtree each time.
|
|
6063
|
+
if (!visitOnce(walk.seen, resolved))
|
|
6064
|
+
return null;
|
|
5827
6065
|
const dict = resolved.value;
|
|
5828
6066
|
const typeStr = reader.getString(dict['Type']);
|
|
5829
6067
|
if (typeStr && typeStr !== 'StructElem')
|
|
@@ -5843,11 +6081,11 @@ function parseStructureElement(reader, elemObj, pages, parentPageIdx, depth) {
|
|
|
5843
6081
|
}
|
|
5844
6082
|
else if (kResolved.type === 'dict') {
|
|
5845
6083
|
const kDict = kResolved.value;
|
|
5846
|
-
processStructKDict({
|
|
6084
|
+
processStructKDict({ walk, kDict, effectivePage, depth, mcids, children, elemRef: kResolved });
|
|
5847
6085
|
}
|
|
5848
6086
|
else if (kResolved.type === 'array') {
|
|
5849
6087
|
for (const item of kResolved.value) {
|
|
5850
|
-
processStructKArrayItem(
|
|
6088
|
+
processStructKArrayItem(walk, item, effectivePage, depth, mcids, children);
|
|
5851
6089
|
}
|
|
5852
6090
|
}
|
|
5853
6091
|
return { type: sType, mcids, children };
|
|
@@ -5863,8 +6101,8 @@ function collectMcidTypes(node, result) {
|
|
|
5863
6101
|
collectMcidTypes(child, result);
|
|
5864
6102
|
}
|
|
5865
6103
|
}
|
|
5866
|
-
function collectMcidTypesFromElement(
|
|
5867
|
-
const node = parseStructureElement(
|
|
6104
|
+
function collectMcidTypesFromElement(walk, item, mcidToType) {
|
|
6105
|
+
const node = parseStructureElement(walk, item, 0, 0);
|
|
5868
6106
|
if (node)
|
|
5869
6107
|
collectMcidTypes(node, mcidToType);
|
|
5870
6108
|
}
|
|
@@ -5880,14 +6118,15 @@ function parseStructureTree(reader, pages) {
|
|
|
5880
6118
|
if (!kObj)
|
|
5881
6119
|
return emptyResult;
|
|
5882
6120
|
const mcidToType = new Map();
|
|
6121
|
+
const walk = { reader, pages, seen: new Set() };
|
|
5883
6122
|
const kResolved = reader.resolveDeep(kObj);
|
|
5884
6123
|
if (kResolved.type === 'array') {
|
|
5885
6124
|
for (const item of kResolved.value) {
|
|
5886
|
-
collectMcidTypesFromElement(
|
|
6125
|
+
collectMcidTypesFromElement(walk, item, mcidToType);
|
|
5887
6126
|
}
|
|
5888
6127
|
}
|
|
5889
6128
|
else {
|
|
5890
|
-
collectMcidTypesFromElement(
|
|
6129
|
+
collectMcidTypesFromElement(walk, kObj, mcidToType);
|
|
5891
6130
|
}
|
|
5892
6131
|
return { mcidToType, hasStructure: mcidToType.size > 0 };
|
|
5893
6132
|
}
|
|
@@ -5924,7 +6163,11 @@ function isStructureListItem(type) {
|
|
|
5924
6163
|
return type === 'LI' || type === 'LBody' || type === 'Lbl';
|
|
5925
6164
|
}
|
|
5926
6165
|
// ── Main export ─────────────────────────────────────────────────────────
|
|
5927
|
-
|
|
6166
|
+
/**
|
|
6167
|
+
* Parses a PDF into flowing HTML and plain text.
|
|
6168
|
+
* Input is untrusted; structures exceeding internal limits are skipped.
|
|
6169
|
+
*/
|
|
6170
|
+
async function parsePdf(buffer, limits = DEFAULT_PARSER_LIMITS) {
|
|
5928
6171
|
const header = new Uint8Array(buffer, 0, Math.min(5, buffer.byteLength));
|
|
5929
6172
|
if (header.length < 5 ||
|
|
5930
6173
|
header[0] !== 0x25 ||
|
|
@@ -5934,7 +6177,7 @@ async function parsePdf(buffer) {
|
|
|
5934
6177
|
header[4] !== 0x2D) {
|
|
5935
6178
|
throw new Error('Not a valid PDF file.');
|
|
5936
6179
|
}
|
|
5937
|
-
const reader = new PdfReader(buffer);
|
|
6180
|
+
const reader = new PdfReader(buffer, limits);
|
|
5938
6181
|
try {
|
|
5939
6182
|
reader.parse();
|
|
5940
6183
|
}
|
|
@@ -6036,14 +6279,18 @@ function extractAllPages(reader, pdfPages, structureMap) {
|
|
|
6036
6279
|
}
|
|
6037
6280
|
return { pages, allTextParts, allHtmlParts, allImageOnly };
|
|
6038
6281
|
}
|
|
6039
|
-
|
|
6282
|
+
/**
|
|
6283
|
+
* Parses a PDF into one HTML fragment per page, plus the outline.
|
|
6284
|
+
* Input is untrusted; structures exceeding internal limits are skipped.
|
|
6285
|
+
*/
|
|
6286
|
+
async function parsePdfPaged(buffer, limits = DEFAULT_PARSER_LIMITS) {
|
|
6040
6287
|
const header = new Uint8Array(buffer, 0, Math.min(5, buffer.byteLength));
|
|
6041
6288
|
if (header.length < 5 ||
|
|
6042
6289
|
header[0] !== 0x25 || header[1] !== 0x50 || header[2] !== 0x44 ||
|
|
6043
6290
|
header[3] !== 0x46 || header[4] !== 0x2D) {
|
|
6044
6291
|
throw new Error('Not a valid PDF file.');
|
|
6045
6292
|
}
|
|
6046
|
-
const reader = new PdfReader(buffer);
|
|
6293
|
+
const reader = new PdfReader(buffer, limits);
|
|
6047
6294
|
try {
|
|
6048
6295
|
reader.parse();
|
|
6049
6296
|
}
|
|
@@ -6080,7 +6327,7 @@ function parseOutline(reader, pdfPages) {
|
|
|
6080
6327
|
if (!firstRef)
|
|
6081
6328
|
return [];
|
|
6082
6329
|
const pageMap = buildPageObjNumMap(reader, pdfPages);
|
|
6083
|
-
return parseOutlineItems(reader, firstRef, pageMap, 0);
|
|
6330
|
+
return parseOutlineItems(reader, firstRef, pageMap, 0, new Set());
|
|
6084
6331
|
}
|
|
6085
6332
|
catch {
|
|
6086
6333
|
return [];
|
|
@@ -6131,11 +6378,14 @@ function resolveOutlinePageIndex(reader, item, pageMap) {
|
|
|
6131
6378
|
return -1;
|
|
6132
6379
|
}
|
|
6133
6380
|
}
|
|
6134
|
-
|
|
6381
|
+
/**
|
|
6382
|
+
* `visited` spans the whole outline tree, not one sibling list: two items
|
|
6383
|
+
* pointing at the same child list would otherwise each expand it in full.
|
|
6384
|
+
*/
|
|
6385
|
+
function parseOutlineItems(reader, firstRef, pageMap, depth, visited) {
|
|
6135
6386
|
if (depth > 10)
|
|
6136
6387
|
return [];
|
|
6137
6388
|
const items = [];
|
|
6138
|
-
const visited = new Set();
|
|
6139
6389
|
let currentRef = firstRef;
|
|
6140
6390
|
while (currentRef) {
|
|
6141
6391
|
const key = currentRef.type === 'ref' ? currentRef.value : '';
|
|
@@ -6150,7 +6400,7 @@ function parseOutlineItems(reader, firstRef, pageMap, depth) {
|
|
|
6150
6400
|
let children = [];
|
|
6151
6401
|
const childFirst = itemDict['First'];
|
|
6152
6402
|
if (childFirst) {
|
|
6153
|
-
children = parseOutlineItems(reader, childFirst, pageMap, depth + 1);
|
|
6403
|
+
children = parseOutlineItems(reader, childFirst, pageMap, depth + 1, visited);
|
|
6154
6404
|
}
|
|
6155
6405
|
items.push({ title, pageIndex, children });
|
|
6156
6406
|
const nextRef = itemDict['Next'];
|
|
@@ -7247,6 +7497,42 @@ function escapeHtml$1(str) {
|
|
|
7247
7497
|
.replaceAll('>', '>')
|
|
7248
7498
|
.replaceAll('"', '"');
|
|
7249
7499
|
}
|
|
7500
|
+
/**
|
|
7501
|
+
* A font name taken from the PDF as a quoted CSS string. The CSS is emitted inside
|
|
7502
|
+
* a `<style>` element, so quotes and backslashes are escaped, control characters
|
|
7503
|
+
* dropped and `<` hex-escaped: nothing in the name can end the string, the rule or
|
|
7504
|
+
* the style element.
|
|
7505
|
+
*/
|
|
7506
|
+
function cssQuotedFontName(name) {
|
|
7507
|
+
let quoted = '';
|
|
7508
|
+
for (const ch of name) {
|
|
7509
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
7510
|
+
if (code < 0x20 || code === 0x7f)
|
|
7511
|
+
continue;
|
|
7512
|
+
if (ch === '\\' || ch === "'")
|
|
7513
|
+
quoted += `\\${ch}`;
|
|
7514
|
+
else if (ch === '<')
|
|
7515
|
+
quoted += String.raw `\3c `;
|
|
7516
|
+
else
|
|
7517
|
+
quoted += ch;
|
|
7518
|
+
}
|
|
7519
|
+
return `'${quoted}'`;
|
|
7520
|
+
}
|
|
7521
|
+
const SAFE_LINK_PROTOCOLS = new Set(['http:', 'https:', 'mailto:']);
|
|
7522
|
+
/**
|
|
7523
|
+
* The link annotation's URI as the browser will parse it when it is an absolute
|
|
7524
|
+
* http, https or mailto URL; `undefined` for any other scheme or a relative URI.
|
|
7525
|
+
*/
|
|
7526
|
+
function safeLinkUri(uri) {
|
|
7527
|
+
try {
|
|
7528
|
+
const url = new URL(uri);
|
|
7529
|
+
return SAFE_LINK_PROTOCOLS.has(url.protocol) ? url.href : undefined;
|
|
7530
|
+
}
|
|
7531
|
+
catch {
|
|
7532
|
+
// Not an absolute URL; a relative URI has no page to resolve against here.
|
|
7533
|
+
return undefined;
|
|
7534
|
+
}
|
|
7535
|
+
}
|
|
7250
7536
|
function resolveFontDescriptor(reader, fontObjDict) {
|
|
7251
7537
|
const direct = fontObjDict['FontDescriptor'];
|
|
7252
7538
|
if (direct)
|
|
@@ -7537,6 +7823,21 @@ function buildFallbackGlyph(fb, unicode, targetAdvance, unitsPerEm) {
|
|
|
7537
7823
|
// Serif font family keywords for generic fallback detection
|
|
7538
7824
|
const SERIF_KEYWORDS = ['times', 'georgia', 'cambria', 'garamond', 'palatino', 'bodoni', 'baskerville', 'bookman', 'century'];
|
|
7539
7825
|
const MONO_KEYWORDS = ['courier', 'mono', 'consolas', 'menlo', 'lucidaconsole', 'inconsolata'];
|
|
7826
|
+
const STYLE_SUFFIX = /(?:Bold|Italic|Regular|Medium|Light|Condensed|Black|Heavy|Oblique|Book|Demi|Semi|Ultra|Thin|Extra|PS)$/i;
|
|
7827
|
+
/**
|
|
7828
|
+
* Drops one trailing style word and the whitespace before it ("Arial Bold" →
|
|
7829
|
+
* "Arial"). Found from the word backwards: a `\s+` regex retried from every
|
|
7830
|
+
* space of a long run of spaces is quadratic.
|
|
7831
|
+
*/
|
|
7832
|
+
function stripStyleSuffix(name) {
|
|
7833
|
+
const word = STYLE_SUFFIX.exec(name);
|
|
7834
|
+
if (!word)
|
|
7835
|
+
return name;
|
|
7836
|
+
let end = word.index;
|
|
7837
|
+
while (end > 0 && /\s/.test(name.charAt(end - 1)))
|
|
7838
|
+
end--;
|
|
7839
|
+
return end === word.index ? name : name.slice(0, end);
|
|
7840
|
+
}
|
|
7540
7841
|
/**
|
|
7541
7842
|
* Dynamically extract a CSS font-family from any PDF BaseFontName.
|
|
7542
7843
|
* No static map needed — works for any font by parsing the name.
|
|
@@ -7561,9 +7862,8 @@ function getSystemFontFamily(baseFontName) {
|
|
|
7561
7862
|
// Clean up dashes/underscores/commas to spaces
|
|
7562
7863
|
name = name.replaceAll(/[-_,]+/g, ' ').trim();
|
|
7563
7864
|
// Remove style/weight suffixes (run twice to catch consecutive: "PS Bold" → "")
|
|
7564
|
-
|
|
7565
|
-
name = name
|
|
7566
|
-
name = name.replaceAll(stylePat, '').trim();
|
|
7865
|
+
name = stripStyleSuffix(name).trim();
|
|
7866
|
+
name = stripStyleSuffix(name).trim();
|
|
7567
7867
|
// Title-case all-lowercase names: "arialuni" → "Arialuni"
|
|
7568
7868
|
if (name === name.toLowerCase() && name.length > 0) {
|
|
7569
7869
|
name = name[0].toUpperCase() + name.slice(1);
|
|
@@ -7575,7 +7875,7 @@ function getSystemFontFamily(baseFontName) {
|
|
|
7575
7875
|
generic = 'serif';
|
|
7576
7876
|
else if (MONO_KEYWORDS.some(k => lower.includes(k)))
|
|
7577
7877
|
generic = 'monospace';
|
|
7578
|
-
return
|
|
7878
|
+
return `${cssQuotedFontName(name)}, ${generic}`;
|
|
7579
7879
|
}
|
|
7580
7880
|
function usesSystemFallback(entry) {
|
|
7581
7881
|
return !entry.reEncodedData;
|
|
@@ -7644,7 +7944,7 @@ function fallbackChainFor(entry) {
|
|
|
7644
7944
|
generic = 'monospace';
|
|
7645
7945
|
else if (FAMILY_SERIF_HINT.test(name))
|
|
7646
7946
|
generic = 'serif';
|
|
7647
|
-
const original = entry.familyName ?
|
|
7947
|
+
const original = entry.familyName ? `${cssQuotedFontName(entry.familyName)},` : '';
|
|
7648
7948
|
return `${original}${generic}`;
|
|
7649
7949
|
}
|
|
7650
7950
|
function resolveFontFamily(entry, isSymbolFont, familyPrefix = '') {
|
|
@@ -7660,7 +7960,7 @@ function resolveFontFamily(entry, isSymbolFont, familyPrefix = '') {
|
|
|
7660
7960
|
if (entry.fontData && fontFormatToMime(entry.fontData.format)) {
|
|
7661
7961
|
return `${embeddedId},${fallbackChainFor(entry)}`;
|
|
7662
7962
|
}
|
|
7663
|
-
return entry.familyName
|
|
7963
|
+
return entry.familyName ? cssQuotedFontName(entry.familyName) : 'serif';
|
|
7664
7964
|
}
|
|
7665
7965
|
class FontRegistry {
|
|
7666
7966
|
constructor(familyPrefix = '') {
|
|
@@ -8785,12 +9085,18 @@ class PixelPerfectProcessor {
|
|
|
8785
9085
|
this.reader = reader;
|
|
8786
9086
|
this.fontRegistry = fontRegistry;
|
|
8787
9087
|
this.zoom = zoom;
|
|
9088
|
+
this.formDraws = this.newFormDraws();
|
|
8788
9089
|
// general.cc:326-327: text_scale_factor1 = max(zoom, font_size_multiplier)
|
|
8789
9090
|
this.textScaleFactor1 = Math.max(zoom, FONT_SIZE_MULTIPLIER);
|
|
8790
9091
|
this.textScaleFactor2 = zoom / this.textScaleFactor1;
|
|
8791
9092
|
}
|
|
9093
|
+
newFormDraws() {
|
|
9094
|
+
const { maxNestingDepth, maxFormDrawsPerPage } = this.reader.limits;
|
|
9095
|
+
return new DrawPath(maxNestingDepth, maxFormDrawsPerPage);
|
|
9096
|
+
}
|
|
8792
9097
|
processPage(pageObj, _pageIndex) {
|
|
8793
9098
|
this.resetState();
|
|
9099
|
+
this.formDraws = this.newFormDraws();
|
|
8794
9100
|
this.lines.length = 0;
|
|
8795
9101
|
this.pathRects.length = 0;
|
|
8796
9102
|
this.imageItems.length = 0;
|
|
@@ -9571,13 +9877,13 @@ class PixelPerfectProcessor {
|
|
|
9571
9877
|
const objDict = this.reader.getDict(ref);
|
|
9572
9878
|
const subtype = this.reader.getString(objDict['Subtype']);
|
|
9573
9879
|
if (subtype === 'Form') {
|
|
9574
|
-
this.
|
|
9880
|
+
this.formDraws.enter(this.reader.resolveDeep(ref), () => this.drawFormXObject(ref, resources, pageWidth, pageHeight));
|
|
9575
9881
|
}
|
|
9576
9882
|
else if (subtype === 'Image') {
|
|
9577
9883
|
this.processImageXObject(ref, objDict);
|
|
9578
9884
|
}
|
|
9579
9885
|
}
|
|
9580
|
-
|
|
9886
|
+
drawFormXObject(ref, parentResources, pageWidth, pageHeight) {
|
|
9581
9887
|
const objDict = this.reader.getDict(ref);
|
|
9582
9888
|
const formResources = objDict['Resources']
|
|
9583
9889
|
? this.reader.getDict(objDict['Resources'])
|
|
@@ -9864,14 +10170,17 @@ function renderImage(img, z) {
|
|
|
9864
10170
|
const w = round$1(img.renderWidth * z);
|
|
9865
10171
|
const h = round$1(img.renderHeight * z);
|
|
9866
10172
|
return `<img class="bi" style="position:absolute;left:${left}px;bottom:${bottom}px;` +
|
|
9867
|
-
`width:${w}px;height:${h}px;" src="${img.dataUrl}"/>`;
|
|
10173
|
+
`width:${w}px;height:${h}px;" src="${escapeHtml$1(img.dataUrl)}"/>`;
|
|
9868
10174
|
}
|
|
9869
10175
|
function renderAnnotation(ann, z) {
|
|
10176
|
+
const href = safeLinkUri(ann.uri);
|
|
10177
|
+
if (!href)
|
|
10178
|
+
return '';
|
|
9870
10179
|
const left = round$1(ann.x * z);
|
|
9871
10180
|
const bottom = round$1(ann.y * z);
|
|
9872
10181
|
const w = round$1(ann.width * z);
|
|
9873
10182
|
const h = round$1(ann.height * z);
|
|
9874
|
-
return `<a class="l" href="${escapeHtml$1(
|
|
10183
|
+
return `<a class="l" href="${escapeHtml$1(href)}">` +
|
|
9875
10184
|
`<div style="position:absolute;left:${left}px;bottom:${bottom}px;` +
|
|
9876
10185
|
`width:${w}px;height:${h}px;background-color:rgba(255,255,255,0.000001);border-style:none;"></div></a>`;
|
|
9877
10186
|
}
|
|
@@ -9957,14 +10266,18 @@ function assemblePageHtml(result, pageIndex, mgr, fontRegistry, z) {
|
|
|
9957
10266
|
};
|
|
9958
10267
|
}
|
|
9959
10268
|
// ── Main Entry Point ──────────────────────────────────────────────────
|
|
9960
|
-
|
|
10269
|
+
/**
|
|
10270
|
+
* Renders a PDF page by page with absolutely positioned text, for pixel-faithful display.
|
|
10271
|
+
* Input is untrusted; structures exceeding internal limits are skipped.
|
|
10272
|
+
*/
|
|
10273
|
+
async function renderPixelPerfectPaged(buffer, options, limits = DEFAULT_PARSER_LIMITS) {
|
|
9961
10274
|
const header = new Uint8Array(buffer, 0, Math.min(5, buffer.byteLength));
|
|
9962
10275
|
if (header.length < 5 ||
|
|
9963
10276
|
header[0] !== 0x25 || header[1] !== 0x50 || header[2] !== 0x44 ||
|
|
9964
10277
|
header[3] !== 0x46 || header[4] !== 0x2D) {
|
|
9965
10278
|
throw new Error('Not a valid PDF file.');
|
|
9966
10279
|
}
|
|
9967
|
-
const reader = new PdfReader(buffer);
|
|
10280
|
+
const reader = new PdfReader(buffer, limits);
|
|
9968
10281
|
try {
|
|
9969
10282
|
reader.parse();
|
|
9970
10283
|
}
|
|
@@ -14365,8 +14678,9 @@ function emitRun(run, dominant, ctx) {
|
|
|
14365
14678
|
html = `<sup>${html}</sup>`;
|
|
14366
14679
|
if (style.baselineShift === 'sub')
|
|
14367
14680
|
html = `<sub>${html}</sub>`;
|
|
14368
|
-
|
|
14369
|
-
|
|
14681
|
+
const href = style.link ? safeLinkHref(style.link) : undefined;
|
|
14682
|
+
if (href !== undefined)
|
|
14683
|
+
html = `<a href="${escapeHtml(href)}" target="_blank" rel="noopener">${html}</a>`;
|
|
14370
14684
|
return html;
|
|
14371
14685
|
}
|
|
14372
14686
|
function dominantRunStyle(lines) {
|
|
@@ -14493,11 +14807,17 @@ function colorValue(color) {
|
|
|
14493
14807
|
const normalized = color.toLowerCase();
|
|
14494
14808
|
return normalized === '#000000' || normalized === '#000' ? '' : color;
|
|
14495
14809
|
}
|
|
14810
|
+
/**
|
|
14811
|
+
* The declarations as the VALUE of a double-quoted `style` attribute. Escaped
|
|
14812
|
+
* here, at the one place every style attribute is built, because a font name
|
|
14813
|
+
* can hold `';…` (character references without their semicolon), which a
|
|
14814
|
+
* browser decodes back into a quote and a `;` and so into a further declaration.
|
|
14815
|
+
*/
|
|
14496
14816
|
function styleAttr(pairs) {
|
|
14497
|
-
return pairs
|
|
14817
|
+
return escapeHtml(pairs
|
|
14498
14818
|
.filter(([, value]) => value !== '')
|
|
14499
14819
|
.map(([prop, value]) => `${prop}:${value}`)
|
|
14500
|
-
.join(';');
|
|
14820
|
+
.join(';'));
|
|
14501
14821
|
}
|
|
14502
14822
|
function attrString(styleValue, dir) {
|
|
14503
14823
|
const stylePart = styleValue ? ` style="${styleValue}"` : '';
|
|
@@ -14604,17 +14924,17 @@ function registerPageFonts(reader, pageObj, registry, fonts) {
|
|
|
14604
14924
|
try {
|
|
14605
14925
|
const pageDict = reader.getDict(pageObj);
|
|
14606
14926
|
const resources = pageDict['Resources'] ? reader.getDict(pageDict['Resources']) : {};
|
|
14607
|
-
registerResourceFonts(reader, resources, registry, fonts, 0);
|
|
14927
|
+
registerResourceFonts(reader, resources, registry, fonts, { depth: 0, seen: new Set() });
|
|
14608
14928
|
}
|
|
14609
14929
|
catch {
|
|
14610
14930
|
// A malformed resource dict must not abort the page's text extraction.
|
|
14611
14931
|
}
|
|
14612
14932
|
}
|
|
14613
|
-
function registerResourceFonts(reader, resources, registry, fonts,
|
|
14614
|
-
if (depth > MAX_RESOURCE_DEPTH)
|
|
14933
|
+
function registerResourceFonts(reader, resources, registry, fonts, walk) {
|
|
14934
|
+
if (walk.depth > MAX_RESOURCE_DEPTH || !visitOnce(walk.seen, resources))
|
|
14615
14935
|
return;
|
|
14616
14936
|
registerFontDict(reader, resources, registry, fonts);
|
|
14617
|
-
descendIntoXObjects(reader, resources, registry, fonts,
|
|
14937
|
+
descendIntoXObjects(reader, resources, registry, fonts, walk);
|
|
14618
14938
|
}
|
|
14619
14939
|
function registerFontDict(reader, resources, registry, fonts) {
|
|
14620
14940
|
if (!resources['Font'])
|
|
@@ -14642,14 +14962,14 @@ function registerSingleFont(reader, name, ref, registry, fonts) {
|
|
|
14642
14962
|
// A single broken font program must not abort the import.
|
|
14643
14963
|
}
|
|
14644
14964
|
}
|
|
14645
|
-
function descendIntoXObjects(reader, resources, registry, fonts,
|
|
14965
|
+
function descendIntoXObjects(reader, resources, registry, fonts, walk) {
|
|
14646
14966
|
if (!resources['XObject'])
|
|
14647
14967
|
return;
|
|
14648
14968
|
const xObjects = reader.getDict(resources['XObject']);
|
|
14649
14969
|
for (const ref of Object.values(xObjects)) {
|
|
14650
14970
|
const nested = resolveXObjectResources(reader, ref);
|
|
14651
14971
|
if (nested)
|
|
14652
|
-
registerResourceFonts(reader, nested, registry, fonts, depth + 1);
|
|
14972
|
+
registerResourceFonts(reader, nested, registry, fonts, { depth: walk.depth + 1, seen: walk.seen });
|
|
14653
14973
|
}
|
|
14654
14974
|
}
|
|
14655
14975
|
function resolveXObjectResources(reader, ref) {
|
|
@@ -14876,10 +15196,11 @@ let importCounter = 0;
|
|
|
14876
15196
|
* the original document (fonts, colors, alignment, spacing) without absolute
|
|
14877
15197
|
* positioning. The returned `fontFaceCss` must be added to the host document
|
|
14878
15198
|
* (e.g. a `<style>` in `<head>`) for embedded fonts to render.
|
|
15199
|
+
* Input is untrusted; structures exceeding internal limits are skipped.
|
|
14879
15200
|
*/
|
|
14880
|
-
async function parsePdfReadable(buffer, options) {
|
|
15201
|
+
async function parsePdfReadable(buffer, options, limits = DEFAULT_PARSER_LIMITS) {
|
|
14881
15202
|
const opts = normalizeOptions(options);
|
|
14882
|
-
const reader = createValidatedReader(buffer);
|
|
15203
|
+
const reader = createValidatedReader(buffer, limits);
|
|
14883
15204
|
const pages = reader.getPages();
|
|
14884
15205
|
if (pages.length === 0) {
|
|
14885
15206
|
throw new Error('PDF contains no pages.');
|
|
@@ -14912,7 +15233,7 @@ function normalizeOptions(options) {
|
|
|
14912
15233
|
fontFamilyPrefix: options?.fontFamilyPrefix ?? `pdf${importCounter.toString(36)}-`,
|
|
14913
15234
|
};
|
|
14914
15235
|
}
|
|
14915
|
-
function createValidatedReader(buffer) {
|
|
15236
|
+
function createValidatedReader(buffer, limits) {
|
|
14916
15237
|
const header = new Uint8Array(buffer, 0, Math.min(5, buffer.byteLength));
|
|
14917
15238
|
const isPdf = header.length === 5 &&
|
|
14918
15239
|
header[0] === 0x25 && header[1] === 0x50 && header[2] === 0x44 &&
|
|
@@ -14920,7 +15241,7 @@ function createValidatedReader(buffer) {
|
|
|
14920
15241
|
if (!isPdf) {
|
|
14921
15242
|
throw new Error('Not a valid PDF file.');
|
|
14922
15243
|
}
|
|
14923
|
-
const reader = new PdfReader(buffer);
|
|
15244
|
+
const reader = new PdfReader(buffer, limits);
|
|
14924
15245
|
try {
|
|
14925
15246
|
reader.parse();
|
|
14926
15247
|
}
|
|
@@ -15005,4 +15326,4 @@ function emitImageOnly(extracted, opts) {
|
|
|
15005
15326
|
}
|
|
15006
15327
|
|
|
15007
15328
|
export { parsePdfReadable };
|
|
15008
|
-
//# sourceMappingURL=gilav21-shadcn-angular-rte-pdf-readable-
|
|
15329
|
+
//# sourceMappingURL=gilav21-shadcn-angular-rte-pdf-readable-JvCpuNQH.mjs.map
|