Skip to content

Commit bfc816c

Browse files
committedSep 17, 2022
Add optional onWarning handler
Currently `pdf-lib` uses `console.warn()` to alert about various issues. Here we add an optional `onWarning` parameter that can be passed to the document or parser to provide an alternative mechanism for handling these alerts.
1 parent 4beebbe commit bfc816c

11 files changed

+118
-23
lines changed
 

‎src/api/PDFDocument.ts

+18-5
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ import {
4848
import PDFObject from 'src/core/objects/PDFObject';
4949
import PDFRef from 'src/core/objects/PDFRef';
5050
import { Fontkit } from 'src/types/fontkit';
51-
import { TransformationMatrix } from 'src/types/matrix';
51+
import { OnWarningHandler, TransformationMatrix } from 'src/types';
5252
import {
5353
assertIs,
5454
assertIsOneOfOrUndefined,
@@ -131,6 +131,7 @@ export default class PDFDocument {
131131
ignoreEncryption = false,
132132
parseSpeed = ParseSpeeds.Slow,
133133
throwOnInvalidObject = false,
134+
onWarning,
134135
updateMetadata = true,
135136
capNumbers = false,
136137
} = options;
@@ -146,24 +147,30 @@ export default class PDFDocument {
146147
parseSpeed,
147148
throwOnInvalidObject,
148149
capNumbers,
150+
onWarning,
149151
).parseDocument();
150-
return new PDFDocument(context, ignoreEncryption, updateMetadata);
152+
return new PDFDocument(
153+
context,
154+
ignoreEncryption,
155+
updateMetadata,
156+
onWarning,
157+
);
151158
}
152159

153160
/**
154161
* Create a new [[PDFDocument]].
155162
* @returns Resolves with the newly created document.
156163
*/
157164
static async create(options: CreateOptions = {}) {
158-
const { updateMetadata = true } = options;
165+
const { updateMetadata = true, onWarning } = options;
159166

160167
const context = PDFContext.create();
161168
const pageTree = PDFPageTree.withContext(context);
162169
const pageTreeRef = context.register(pageTree);
163170
const catalog = PDFCatalog.withContextAndPages(context, pageTreeRef);
164171
context.trailerInfo.Root = context.register(catalog);
165172

166-
return new PDFDocument(context, false, updateMetadata);
173+
return new PDFDocument(context, false, updateMetadata, onWarning);
167174
}
168175

169176
/** The low-level context of this document. */
@@ -175,6 +182,9 @@ export default class PDFDocument {
175182
/** Whether or not this document is encrypted. */
176183
readonly isEncrypted: boolean;
177184

185+
/** The function called with details of non-fatal issues. */
186+
readonly onWarning: OnWarningHandler;
187+
178188
/** The default word breaks used in PDFPage.drawText */
179189
defaultWordBreaks: string[] = [' '];
180190

@@ -193,13 +203,16 @@ export default class PDFDocument {
193203
context: PDFContext,
194204
ignoreEncryption: boolean,
195205
updateMetadata: boolean,
206+
onWarning: OnWarningHandler = console.warn.bind(console),
196207
) {
197208
assertIs(context, 'context', [[PDFContext, 'PDFContext']]);
198209
assertIs(ignoreEncryption, 'ignoreEncryption', ['boolean']);
210+
assertIs(onWarning, 'onWarning', ['function']);
199211

200212
this.context = context;
201213
this.catalog = context.lookup(context.trailerInfo.Root) as PDFCatalog;
202214
this.isEncrypted = !!context.lookup(context.trailerInfo.Encrypt);
215+
this.onWarning = onWarning;
203216

204217
this.pageCache = Cache.populatedBy(this.computePages);
205218
this.pageMap = new Map();
@@ -254,7 +267,7 @@ export default class PDFDocument {
254267
getForm(): PDFForm {
255268
const form = this.formCache.access();
256269
if (form.hasXFA()) {
257-
console.warn(
270+
this.onWarning(
258271
'Removing XFA form data as pdf-lib does not support reading or writing XFA',
259272
);
260273
form.deleteXFA();

‎src/api/PDFDocumentOptions.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { EmbeddedFileOptions } from 'src/core/embedders/FileEmbedder';
2-
import { TypeFeatures } from 'src/types/fontkit';
2+
import { OnWarningHandler, TypeFeatures } from 'src/types';
33

44
export enum ParseSpeeds {
55
Fastest = Infinity,
@@ -25,11 +25,13 @@ export interface LoadOptions {
2525
ignoreEncryption?: boolean;
2626
parseSpeed?: ParseSpeeds | number;
2727
throwOnInvalidObject?: boolean;
28+
onWarning?: OnWarningHandler;
2829
updateMetadata?: boolean;
2930
capNumbers?: boolean;
3031
}
3132

3233
export interface CreateOptions {
34+
onWarning?: OnWarningHandler;
3335
updateMetadata?: boolean;
3436
}
3537

‎src/api/form/PDFTextField.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -607,7 +607,7 @@ export default class PDFTextField extends PDFField {
607607
enableCombing() {
608608
if (this.getMaxLength() === undefined) {
609609
const msg = `PDFTextFields must have a max length in order to be combed`;
610-
console.warn(msg);
610+
this.doc.onWarning(msg);
611611
}
612612

613613
this.markAsDirty();

‎src/core/parser/BaseParser.ts

+10-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import ByteStream from 'src/core/parser/ByteStream';
33
import CharCodes from 'src/core/syntax/CharCodes';
44
import { IsDigit, IsNumeric } from 'src/core/syntax/Numeric';
55
import { IsWhitespace } from 'src/core/syntax/Whitespace';
6+
import { OnWarningHandler } from 'src/types';
67
import { charFromCode } from 'src/utils';
78

89
const { Newline, CarriageReturn } = CharCodes;
@@ -11,10 +12,16 @@ const { Newline, CarriageReturn } = CharCodes;
1112
class BaseParser {
1213
protected readonly bytes: ByteStream;
1314
protected readonly capNumbers: boolean;
15+
protected readonly onWarning: OnWarningHandler;
1416

15-
constructor(bytes: ByteStream, capNumbers = false) {
17+
constructor(
18+
bytes: ByteStream,
19+
capNumbers = false,
20+
onWarning = console.warn.bind(console),
21+
) {
1622
this.bytes = bytes;
1723
this.capNumbers = capNumbers;
24+
this.onWarning = onWarning;
1825
}
1926

2027
protected parseRawInt(): number {
@@ -64,11 +71,11 @@ class BaseParser {
6471
if (numberValue > Number.MAX_SAFE_INTEGER) {
6572
if (this.capNumbers) {
6673
const msg = `Parsed number that is too large for some PDF readers: ${value}, using Number.MAX_SAFE_INTEGER instead.`;
67-
console.warn(msg);
74+
this.onWarning(msg);
6875
return Number.MAX_SAFE_INTEGER;
6976
} else {
7077
const msg = `Parsed number that is too large for some PDF readers: ${value}, not capping.`;
71-
console.warn(msg);
78+
this.onWarning(msg);
7279
}
7380
}
7481

‎src/core/parser/PDFObjectParser.ts

+13-4
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { IsDelimiter } from 'src/core/syntax/Delimiters';
2727
import { Keywords } from 'src/core/syntax/Keywords';
2828
import { IsDigit, IsNumeric } from 'src/core/syntax/Numeric';
2929
import { IsWhitespace } from 'src/core/syntax/Whitespace';
30+
import { OnWarningHandler } from 'src/types';
3031
import { charFromCode } from 'src/utils';
3132

3233
// TODO: Throw error if eof is reached before finishing object parse...
@@ -35,18 +36,26 @@ class PDFObjectParser extends BaseParser {
3536
bytes: Uint8Array,
3637
context: PDFContext,
3738
capNumbers?: boolean,
38-
) => new PDFObjectParser(ByteStream.of(bytes), context, capNumbers);
39+
onWarning?: OnWarningHandler,
40+
) =>
41+
new PDFObjectParser(ByteStream.of(bytes), context, capNumbers, onWarning);
3942

4043
static forByteStream = (
4144
byteStream: ByteStream,
4245
context: PDFContext,
4346
capNumbers = false,
44-
) => new PDFObjectParser(byteStream, context, capNumbers);
47+
onWarning?: OnWarningHandler,
48+
) => new PDFObjectParser(byteStream, context, capNumbers, onWarning);
4549

4650
protected readonly context: PDFContext;
4751

48-
constructor(byteStream: ByteStream, context: PDFContext, capNumbers = false) {
49-
super(byteStream, capNumbers);
52+
constructor(
53+
byteStream: ByteStream,
54+
context: PDFContext,
55+
capNumbers = false,
56+
onWarning?: OnWarningHandler,
57+
) {
58+
super(byteStream, capNumbers, onWarning);
5059
this.context = context;
5160
}
5261

‎src/core/parser/PDFObjectStreamParser.ts

+14-3
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,32 @@ import PDFRawStream from 'src/core/objects/PDFRawStream';
55
import PDFRef from 'src/core/objects/PDFRef';
66
import ByteStream from 'src/core/parser/ByteStream';
77
import PDFObjectParser from 'src/core/parser/PDFObjectParser';
8+
import { OnWarningHandler } from 'src/types';
89
import { waitForTick } from 'src/utils';
910

1011
class PDFObjectStreamParser extends PDFObjectParser {
1112
static forStream = (
1213
rawStream: PDFRawStream,
1314
shouldWaitForTick?: () => boolean,
14-
) => new PDFObjectStreamParser(rawStream, shouldWaitForTick);
15+
onWarning?: OnWarningHandler,
16+
) => new PDFObjectStreamParser(rawStream, shouldWaitForTick, onWarning);
1517

1618
private alreadyParsed: boolean;
1719
private readonly shouldWaitForTick: () => boolean;
1820
private readonly firstOffset: number;
1921
private readonly objectCount: number;
2022

21-
constructor(rawStream: PDFRawStream, shouldWaitForTick?: () => boolean) {
22-
super(ByteStream.fromPDFRawStream(rawStream), rawStream.dict.context);
23+
constructor(
24+
rawStream: PDFRawStream,
25+
shouldWaitForTick?: () => boolean,
26+
onWarning?: OnWarningHandler,
27+
) {
28+
super(
29+
ByteStream.fromPDFRawStream(rawStream),
30+
rawStream.dict.context,
31+
undefined,
32+
onWarning,
33+
);
2334

2435
const { dict } = rawStream;
2536

‎src/core/parser/PDFParser.ts

+14-5
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import PDFContext from 'src/core/PDFContext';
2222
import CharCodes from 'src/core/syntax/CharCodes';
2323
import { Keywords } from 'src/core/syntax/Keywords';
2424
import { IsDigit } from 'src/core/syntax/Numeric';
25+
import { OnWarningHandler } from 'src/types';
2526
import { waitForTick } from 'src/utils';
2627

2728
class PDFParser extends PDFObjectParser {
@@ -30,8 +31,15 @@ class PDFParser extends PDFObjectParser {
3031
objectsPerTick?: number,
3132
throwOnInvalidObject?: boolean,
3233
capNumbers?: boolean,
34+
onWarning?: OnWarningHandler,
3335
) =>
34-
new PDFParser(pdfBytes, objectsPerTick, throwOnInvalidObject, capNumbers);
36+
new PDFParser(
37+
pdfBytes,
38+
objectsPerTick,
39+
throwOnInvalidObject,
40+
capNumbers,
41+
onWarning,
42+
);
3543

3644
private readonly objectsPerTick: number;
3745
private readonly throwOnInvalidObject: boolean;
@@ -43,8 +51,9 @@ class PDFParser extends PDFObjectParser {
4351
objectsPerTick = Infinity,
4452
throwOnInvalidObject = false,
4553
capNumbers = false,
54+
onWarning?: OnWarningHandler,
4655
) {
47-
super(ByteStream.of(pdfBytes), PDFContext.create(), capNumbers);
56+
super(ByteStream.of(pdfBytes), PDFContext.create(), capNumbers, onWarning);
4857
this.objectsPerTick = objectsPerTick;
4958
this.throwOnInvalidObject = throwOnInvalidObject;
5059
}
@@ -70,7 +79,7 @@ class PDFParser extends PDFObjectParser {
7079
this.maybeRecoverRoot();
7180

7281
if (this.context.lookup(PDFRef.of(0))) {
73-
console.warn('Removing parsed object: 0 0 R');
82+
this.onWarning('Removing parsed object: 0 0 R');
7483
this.context.delete(PDFRef.of(0));
7584
}
7685

@@ -182,11 +191,11 @@ class PDFParser extends PDFObjectParser {
182191

183192
const msg = `Trying to parse invalid object: ${JSON.stringify(startPos)})`;
184193
if (this.throwOnInvalidObject) throw new Error(msg);
185-
console.warn(msg);
194+
this.onWarning(msg);
186195

187196
const ref = this.parseIndirectObjectHeader();
188197

189-
console.warn(`Invalid object ref: ${ref}`);
198+
this.onWarning(`Invalid object ref: ${ref}`);
190199

191200
this.skipWhitespaceAndComments();
192201
const start = this.bytes.offset();

‎src/types/index.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
1-
export { TransformationMatrix } from 'src/types/matrix';
1+
export * from 'src/types/fontkit';
2+
export * from 'src/types/matrix';
3+
4+
export type OnWarningHandler = (message: string) => void;

‎src/utils/validators.ts

+2
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ export type TypeDescriptor =
9494
| 'boolean'
9595
| 'symbol'
9696
| 'bigint'
97+
| 'function'
9798
| DateConstructor
9899
| ArrayConstructor
99100
| Uint8ArrayConstructor
@@ -109,6 +110,7 @@ export const isType = (value: any, type: TypeDescriptor) => {
109110
if (type === 'boolean') return typeof value === 'boolean';
110111
if (type === 'symbol') return typeof value === 'symbol';
111112
if (type === 'bigint') return typeof value === 'bigint';
113+
if (type === 'function') return typeof value === 'function';
112114
if (type === Date) return value instanceof Date;
113115
if (type === Array) return value instanceof Array;
114116
if (type === Uint8Array) return value instanceof Uint8Array;

‎tests/api/PDFDocument.spec.ts

+14
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,20 @@ describe(`PDFDocument`, () => {
142142
}),
143143
).rejects.toEqual(expectedError);
144144
});
145+
146+
it(`uses the provided onWarning callback`, async () => {
147+
expect.assertions(2);
148+
const onWarning = jest.fn();
149+
await PDFDocument.load(invalidObjectsPdfBytes, {
150+
ignoreEncryption: true,
151+
parseSpeed: ParseSpeeds.Fastest,
152+
onWarning,
153+
});
154+
expect(onWarning).toHaveBeenCalledTimes(2);
155+
expect(onWarning).toHaveBeenCalledWith(
156+
'Trying to parse invalid object: {"line":20,"column":13,"offset":126})',
157+
);
158+
});
145159
});
146160

147161
describe(`embedFont() method`, () => {

‎tests/core/parser/PDFParser.spec.ts

+25
Original file line numberDiff line numberDiff line change
@@ -395,4 +395,29 @@ describe(`PDFParser`, () => {
395395
const object2 = context.lookup(PDFRef.of(2));
396396
expect(object2).toBeInstanceOf(PDFString);
397397
});
398+
399+
it(`uses the provided onWarning callback`, async () => {
400+
expect.assertions(3);
401+
const onWarning = jest.fn();
402+
403+
const input = `
404+
%PDF-1.7
405+
22 0 obj <</Type/Outlines/First ## 0 R/Last ** 0 R/Count 2>> endobj
406+
`;
407+
const parser = PDFParser.forBytesWithOptions(
408+
typedArrayFor(input),
409+
undefined,
410+
undefined,
411+
undefined,
412+
onWarning,
413+
);
414+
await parser.parseDocument();
415+
416+
expect(onWarning).toHaveBeenCalledTimes(2);
417+
expect(onWarning).toHaveBeenNthCalledWith(
418+
1,
419+
'Trying to parse invalid object: {"line":3,"column":53,"offset":18})',
420+
);
421+
expect(onWarning).toHaveBeenNthCalledWith(2, 'Invalid object ref: 22 0 R');
422+
});
398423
});

0 commit comments

Comments
 (0)
Please sign in to comment.