Skip to content

Commit 6438acc

Browse files
committed
Add promise-queue.
1 parent 216bf47 commit 6438acc

File tree

1 file changed

+66
-49
lines changed

1 file changed

+66
-49
lines changed

src/pdfassembler.ts

Lines changed: 66 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from 'pdfjs-dist/lib/shared/util';
1414

1515
import { deflate } from 'pako';
16+
import * as queue from 'promise-queue';
1617

1718
export type TypedArray = Int8Array | Uint8Array | Int16Array | Uint16Array |
1819
Int32Array | Uint32Array | Uint8ClampedArray | Float32Array | Float64Array;
@@ -21,52 +22,57 @@ export type BinaryFile = Blob | File | ArrayBuffer | TypedArray;
2122

2223
export class PDFAssembler {
2324
pdfManager: any = null;
25+
userPassword = '';
26+
ownerPassword = '';
2427
nextNodeNum = 1;
2528
pdfTree: any = Object.create(null);
2629
recoveryMode = false;
2730
objCache: any = Object.create(null);
2831
objCacheQueue: any = Object.create(null);
29-
promiseQueue: Promise<any> = Promise.resolve(true);
32+
pdfManagerArrays = [];
33+
pdfAssemblerArrays = [];
34+
promiseQueue: any = new queue(1);
3035
indent: boolean|string|number = false;
3136
compress = true;
3237
encrypt = false; // not yet implemented
3338
groupPages = true;
3439
pageGroupSize = 16;
3540
pdfVersion = '1.7';
3641

37-
constructor(inputData?: BinaryFile|Object) {
42+
constructor(inputData?: BinaryFile|Object, userPassword = '') {
43+
if (userPassword.length) { this.userPassword = userPassword; }
3844
if (typeof inputData === 'object') {
3945
if (inputData instanceof Blob || inputData instanceof ArrayBuffer || inputData instanceof Uint8Array) {
40-
this.promiseQueue = this.toArrayBuffer(inputData)
41-
.then(arrayBuffer => this.pdfManager = new LocalPdfManager(1, arrayBuffer, '', {}, ''))
46+
this.promiseQueue.add(() => this.toArrayBuffer(inputData)
47+
.then(arrayBuffer => this.pdfManager = new LocalPdfManager(1, arrayBuffer, userPassword, {}, ''))
4248
.then(() => this.pdfManager.ensureDoc('checkHeader', []))
4349
.then(() => this.pdfManager.ensureDoc('parseStartXRef', []))
4450
.then(() => this.pdfManager.ensureDoc('parse', [this.recoveryMode]))
4551
.then(() => this.pdfManager.ensureDoc('numPages'))
4652
.then(() => this.pdfManager.ensureDoc('fingerprint'))
47-
.then(() => this.pdfTree['/Root'] = this.resolveNodeRefs())
4853
.then(() => {
54+
this.pdfTree['/Root'] = this.resolveNodeRefs();
4955
const infoDict = new Dict();
5056
infoDict._map = this.pdfManager.pdfDocument.documentInfo;
5157
this.pdfTree['/Info'] = this.resolveNodeRefs(infoDict);
5258
delete this.pdfTree['/Info']['/IsAcroFormPresent'];
5359
delete this.pdfTree['/Info']['/IsXFAPresent'];
5460
delete this.pdfTree['/Info']['/PDFFormatVersion'];
55-
this.pdfTree['/Info']['/Producer'] = '(pdfAssembler — www.pdfcircus.com)';
61+
this.pdfTree['/Info']['/Producer'] = '(PDF Assembler — www.pdfcircus.com)';
5662
this.pdfTree['/Info']['/ModDate'] = '(' + this.toPdfDate() + ')';
63+
this.flattenPageTree();
5764
})
58-
.then(() => this.flattenPageTree());
65+
);
5966
} else {
6067
this.pdfTree = inputData;
6168
}
6269
} else {
6370
this.pdfTree = {
6471
'documentInfo': {},
6572
'/Info': {
66-
'/Producer': '(pdfAssembler (www.pdfcircus.com))',
73+
'/Producer': '(PDF Assembler — www.pdfcircus.com)',
6774
'/CreationDate': '(' + this.toPdfDate() + ')',
6875
'/ModDate': '(' + this.toPdfDate() + ')',
69-
'/Trapped': '/False',
7076
},
7177
'/Root': {
7278
'/Type': '/Catalog',
@@ -89,16 +95,15 @@ export class PDFAssembler {
8995
}
9096

9197
get pdfDocument(): Promise<PDFDocument> {
92-
return this.promiseQueue.then(() => this.pdfManager && this.pdfManager.pdfDocument);
98+
return this.promiseQueue.add(() => Promise.resolve(this.pdfManager && this.pdfManager.pdfDocument));
9399
}
94100

95101
get numPages(): Promise<number> {
96-
return this.promiseQueue.then(() => this.pdfTree['/Root']['/Pages']['/Count']);
97-
// this.pdfManager && this.pdfManager.pdfDocument && this.pdfManager.pdfDocument.numPages
102+
return this.promiseQueue.add(() => Promise.resolve(this.pdfTree['/Root']['/Pages']['/Count']));
98103
}
99104

100105
get pdfObject() {
101-
return this.promiseQueue.then(() => this.pdfTree);
106+
return this.promiseQueue.add(() => Promise.resolve(this.pdfTree));
102107
}
103108

104109
toArrayBuffer(file: BinaryFile): Promise<ArrayBuffer> {
@@ -155,11 +160,18 @@ export class PDFAssembler {
155160
} else if (typeof node === 'string') {
156161
return `(${node})`;
157162
} else if (node instanceof Array) {
158-
const arrayNode = [];
159-
node.forEach((element, index) => arrayNode.push(
160-
this.resolveNodeRefs(element, index, arrayNode, contents)
161-
));
162-
return arrayNode;
163+
const existingArrayIndex = this.pdfManagerArrays.indexOf(node);
164+
if (existingArrayIndex > -1) {
165+
return this.pdfAssemblerArrays[existingArrayIndex];
166+
} else {
167+
const newArrayNode = [];
168+
this.pdfManagerArrays.push(node);
169+
this.pdfAssemblerArrays.push(newArrayNode);
170+
node.forEach((element, index) => newArrayNode.push(
171+
this.resolveNodeRefs(element, index, newArrayNode, contents)
172+
));
173+
return newArrayNode;
174+
}
163175
} else if (typeof node === 'object' && node !== null) {
164176
const objectNode: any = Object.create(null);
165177
let source = null;
@@ -183,16 +195,28 @@ export class PDFAssembler {
183195
}
184196
}
185197
if (!objectNode.stream) {
186-
const checkStream = (streamSource) => {
187-
if (streamSource instanceof Stream || streamSource instanceof DecryptStream) {
188-
source = streamSource;
198+
for (const checkSource of [
199+
node, node.stream, node.stream && node.stream.str,
200+
node.str, node.str && node.str.str
201+
]) {
202+
if (checkSource instanceof Stream || checkSource instanceof DecryptStream) {
203+
source = checkSource;
204+
break;
189205
}
190-
};
191-
if (!source) { checkStream(node); }
192-
if (!source) { checkStream(node.stream); }
193-
if (!source) { checkStream(node.stream && node.stream.str); }
194-
if (!source) { checkStream(node.str); }
195-
if (!source) { checkStream(node.str && node.str.str); }
206+
}
207+
// const checkStream = (streamSource) => {
208+
// if (!source && (
209+
// streamSource instanceof Stream ||
210+
// streamSource instanceof DecryptStream
211+
// )) {
212+
// source = streamSource;
213+
// }
214+
// };
215+
// checkStream(node);
216+
// checkStream(node.stream);
217+
// checkStream(node.stream && node.stream.str);
218+
// checkStream(node.str);
219+
// checkStream(node.str && node.str.str);
196220
if (source) {
197221
source.reset();
198222
objectNode.stream = source.getBytes();
@@ -203,17 +227,8 @@ export class PDFAssembler {
203227
if (contents || objectNode['/Subtype'] === '/XML' ||
204228
(objectNode.stream && objectNode.stream.every(byte => byte < 128))
205229
) {
206-
// if (contents) { objectNode.contents = objectNode.stream; }
207-
// TODO: remove unneeded spaces in command streams
208-
// (but NOT in text strings inside command streams)
209-
// --or-- split command streams into command arrays?
230+
// TODO: split command stream into array of commands?
210231
objectNode.stream = bytesToString(objectNode.stream);
211-
// .replace(/\s*\n\s*/g, '\n').replace(/\s*\r\s*/g, '\n')
212-
// .replace(/\s\s+/g, ' ').replace(/\s\s+/g, ' ').replace(/\s*\/\s*/g, '/')
213-
// .replace(/\s*\(\s*/g, '(').replace(/\s*\)\s*/g, ')')
214-
// .replace(/\s*\[\s*/g, '[').replace(/\s*\]\s*/g, ']')
215-
// .replace(/\s*\{\s*/g, '{').replace(/\s*\}\s*/g, '}')
216-
// .replace(/[ \t\v\f]*<\s*/g, '<').replace(/\s*>[ \t\v\f]*/g, '>');
217232
}
218233
delete objectNode['/Length'];
219234
}
@@ -364,7 +379,7 @@ export class PDFAssembler {
364379
}
365380

366381
assemblePdf(nameOrOutputFormat = 'output.pdf'): Promise<File|ArrayBuffer|Uint8Array> {
367-
return this.promiseQueue = this.promiseQueue.then(() => {
382+
return this.promiseQueue.add(() => new Promise((resolve, reject) => {
368383
const stringByteMap = [ // encodes string chars by byte code
369384
'\\000', '\\001', '\\002', '\\003', '\\004', '\\005', '\\006', '\\007',
370385
'\\b', '\\t', '\\n', '\\013', '\\f', '\\r', '\\016', '\\017',
@@ -390,8 +405,10 @@ export class PDFAssembler {
390405
const space = !this.indent ? '' :
391406
typeof this.indent === 'number' ? ' '.repeat(this.indent) :
392407
typeof this.indent === 'string' ? this.indent :
393-
'\t'; // this.indent = truthy
394-
const newline = !this.indent ? '' : '\n';
408+
'\t'; // if this.indent == truthy
409+
// const newline = !this.indent ? '' : '\n';
410+
const newline = '\n';
411+
// TODO: If no indent, break lines longer than 255 characters
395412
this.flattenPageTree();
396413
this.groupPageTree();
397414
this.resetObjectIds();
@@ -402,15 +419,15 @@ export class PDFAssembler {
402419
if (nextIndent === true) { nextIndent = newline + space.repeat(depth); }
403420
let pdfObject = '';
404421

405-
// detect and encode names and strings
422+
// detect and encode name or string
406423
if (typeof jsObject === 'string') {
407424
const firstChar = jsObject[0], lastChar = jsObject[jsObject.length - 1];
408-
if (firstChar === '/') {
425+
if (firstChar === '/') { // name
409426
// encode name chars: NUL, TAB, LF, FF, CR, space, #, %, (, ), /, <, >, [, ], {, }
410427
const encodeChar = (char: string) => '\0\t\n\f\r #%()/<>[]{}'.indexOf(char) === -1 ?
411428
char : `#${`0${char.charCodeAt(0).toString(16)}`.slice(-2)}`;
412429
pdfObject = `/${jsObject.slice(1).replace(/./g, encodeChar)}`;
413-
} else if (firstChar === '(' && lastChar === ')') {
430+
} else if (firstChar === '(' && lastChar === ')') { // string
414431
const byteArray = Array.from(arraysToBytes(jsObject.slice(1, -1)));
415432
const stringEncode = byteArray.map((byte: number) => stringByteMap[byte]).join('');
416433
if (stringEncode.length < byteArray.length * 2) {
@@ -423,7 +440,7 @@ export class PDFAssembler {
423440
pdfObject = jsObject;
424441
}
425442

426-
// convert true, false, and null to string
443+
// convert true, false, null, or number to string
427444
} else if (typeof jsObject !== 'object' || jsObject === null) {
428445
pdfObject = jsObject === null || jsObject === undefined ? 'null' :
429446
jsObject === true ? 'true' :
@@ -499,7 +516,7 @@ export class PDFAssembler {
499516
// if nextIndent is set, indent item
500517
nextIndent ? nextIndent :
501518
// otherwise, check if item is first in an array, or starts with a delimiter character
502-
// if not (nextIndent = ''), add a space to separate it from the previous item
519+
// if not (if nextIndent = ''), add a space to separate it from the previous item
503520
nextIndent === false || ['/', '[', '(', '<'].includes(pdfObject[0]) ? '' : ' ';
504521
return prefix + pdfObject;
505522
};
@@ -531,13 +548,13 @@ export class PDFAssembler {
531548
`%%EOF\n`;
532549
const pdfData = arraysToBytes([header, ...indirectObjects.filter(o => o), xref, trailer]);
533550
switch (nameOrOutputFormat) {
534-
case 'ArrayBuffer': return pdfData.buffer;
535-
case 'Uint8Array': return pdfData;
551+
case 'ArrayBuffer': resolve(pdfData.buffer); break;
552+
case 'Uint8Array': resolve(pdfData); break;
536553
default:
537554
if (nameOrOutputFormat.slice(-4) !== '.pdf') { nameOrOutputFormat += '.pdf'; }
538-
return new File([pdfData], nameOrOutputFormat, { type: 'application/pdf' });
555+
resolve(new File([pdfData], nameOrOutputFormat, { type: 'application/pdf' }));
539556
}
540-
});
557+
}));
541558
}
542559

543560
// utility functions from js.pdf:

0 commit comments

Comments
 (0)