Skip to content

Commit 49a1701

Browse files
authored
Fix key reuse bug (Hopding#1078)
* Prevent new XObject keys from conflicting with existing ones * Add test for XObject key reuse * Prevent Font key reuse * Fix other instances of key reuse * Fix another instance * Add PDFDict.uniqueKey() test
1 parent d9c8c95 commit 49a1701

File tree

8 files changed

+159
-54
lines changed

8 files changed

+159
-54
lines changed

scratchpad/index.ts

+14-29
Original file line numberDiff line numberDiff line change
@@ -3,38 +3,23 @@ import { openPdf, Reader } from './open';
33
import { PDFDocument } from 'src/index';
44

55
(async () => {
6-
// This should be a Uint8Array or ArrayBuffer
7-
// This data can be obtained in a number of different ways
8-
// If your running in a Node environment, you could use fs.readFile()
9-
// In the browser, you could make a fetch() call and use res.arrayBuffer()
10-
const existingPdfBytes = fs.readFileSync('assets/pdfs/with_annots.pdf');
6+
const pdfDoc1 = await PDFDocument.create();
7+
const image1 = await pdfDoc1.embedPng(
8+
fs.readFileSync('assets/images/mario_emblem.png'),
9+
);
10+
const page1 = pdfDoc1.addPage();
11+
page1.drawImage(image1, { ...image1.scale(1.0) });
1112

12-
// Load a PDFDocument without updating its existing metadata
13-
const pdfDoc = await PDFDocument.load(existingPdfBytes);
14-
const viewerPrefs = pdfDoc.catalog.getOrCreateViewerPreferences();
13+
const pdfDoc1Bytes = await pdfDoc1.save();
1514

16-
// Print all available viewer preference fields
17-
console.log('HideToolbar:', viewerPrefs.getHideToolbar());
18-
console.log('HideMenubar:', viewerPrefs.getHideMenubar());
19-
console.log('HideWindowUI:', viewerPrefs.getHideWindowUI());
20-
console.log('FitWindow:', viewerPrefs.getFitWindow());
21-
console.log('CenterWindow:', viewerPrefs.getCenterWindow());
22-
console.log('DisplayDocTitle:', viewerPrefs.getDisplayDocTitle());
23-
console.log('NonFullScreenPageMode:', viewerPrefs.getNonFullScreenPageMode());
24-
console.log('ReadingDirection:', viewerPrefs.getReadingDirection());
25-
console.log('PrintScaling:', viewerPrefs.getPrintScaling());
26-
console.log('Duplex:', viewerPrefs.getDuplex());
27-
console.log('PickTrayByPDFSize:', viewerPrefs.getPickTrayByPDFSize());
28-
console.log('PrintPageRange:', viewerPrefs.getPrintPageRange());
29-
console.log('NumCopies:', viewerPrefs.getNumCopies());
15+
const pdfDoc2 = await PDFDocument.load(pdfDoc1Bytes);
16+
const image2 = await pdfDoc2.embedPng(
17+
fs.readFileSync('assets/images/minions_banana_alpha.png'),
18+
);
19+
const page2 = pdfDoc2.getPage(0);
20+
page2.drawImage(image2, { ...image2.scale(0.5), x: 100, y: 100 });
3021

31-
// Serialize the PDFDocument to bytes (a Uint8Array)
32-
const pdfBytes = await pdfDoc.save();
33-
34-
// For example, `pdfBytes` can be:
35-
// • Written to a file in Node
36-
// • Downloaded from the browser
37-
// • Rendered in an <iframe>
22+
const pdfBytes = await pdfDoc2.save();
3823

3924
fs.writeFileSync('out.pdf', pdfBytes);
4025
openPdf('out.pdf', Reader.Preview);

src/api/PDFPage.ts

+10-12
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ export default class PDFPage {
100100
/** The document to which this page belongs. */
101101
readonly doc: PDFDocument;
102102

103-
private fontKey?: string;
103+
private fontKey?: PDFName;
104104
private font?: PDFFont;
105105
private fontSize = 24;
106106
private fontColor = rgb(0, 0, 0) as Color;
@@ -699,8 +699,7 @@ export default class PDFPage {
699699
// TODO: Reuse image Font name if we've already added this image to Resources.Fonts
700700
assertIs(font, 'font', [[PDFFont, 'PDFFont']]);
701701
this.font = font;
702-
this.fontKey = this.doc.context.addRandomSuffix(this.font.name);
703-
this.node.setFontDictionary(PDFName.of(this.fontKey), this.font.ref);
702+
this.fontKey = this.node.newFontDictionary(this.font.name, this.font.ref);
704703
}
705704

706705
/**
@@ -1059,8 +1058,7 @@ export default class PDFPage {
10591058
assertRangeOrUndefined(options.opacity, 'opacity.opacity', 0, 1);
10601059
assertIsOneOfOrUndefined(options.blendMode, 'options.blendMode', BlendMode);
10611060

1062-
const xObjectKey = this.doc.context.addRandomSuffix('Image', 10);
1063-
this.node.setXObject(PDFName.of(xObjectKey), image.ref);
1061+
const xObjectKey = this.node.newXObject('Image', image.ref);
10641062

10651063
const graphicsStateKey = this.maybeEmbedGraphicsState({
10661064
opacity: options.opacity,
@@ -1134,8 +1132,10 @@ export default class PDFPage {
11341132
assertRangeOrUndefined(options.opacity, 'opacity.opacity', 0, 1);
11351133
assertIsOneOfOrUndefined(options.blendMode, 'options.blendMode', BlendMode);
11361134

1137-
const xObjectKey = this.doc.context.addRandomSuffix('EmbeddedPdfPage', 10);
1138-
this.node.setXObject(PDFName.of(xObjectKey), embeddedPage.ref);
1135+
const xObjectKey = this.node.newXObject(
1136+
'EmbeddedPdfPage',
1137+
embeddedPage.ref,
1138+
);
11391139

11401140
const graphicsStateKey = this.maybeEmbedGraphicsState({
11411141
opacity: options.opacity,
@@ -1549,7 +1549,7 @@ export default class PDFPage {
15491549
return { oldFont, oldFontKey, newFont, newFontKey };
15501550
}
15511551

1552-
private getFont(): [PDFFont, string] {
1552+
private getFont(): [PDFFont, PDFName] {
15531553
if (!this.font || !this.fontKey) {
15541554
const font = this.doc.embedStandardFont(StandardFonts.Helvetica);
15551555
this.setFont(font);
@@ -1580,7 +1580,7 @@ export default class PDFPage {
15801580
opacity?: number;
15811581
borderOpacity?: number;
15821582
blendMode?: BlendMode;
1583-
}): string | undefined {
1583+
}): PDFName | undefined {
15841584
const { opacity, borderOpacity, blendMode } = options;
15851585

15861586
if (
@@ -1591,16 +1591,14 @@ export default class PDFPage {
15911591
return undefined;
15921592
}
15931593

1594-
const key = this.doc.context.addRandomSuffix('GS', 10);
1595-
15961594
const graphicsState = this.doc.context.obj({
15971595
Type: 'ExtGState',
15981596
ca: opacity,
15991597
CA: borderOpacity,
16001598
BM: blendMode,
16011599
});
16021600

1603-
this.node.setExtGState(PDFName.of(key), graphicsState);
1601+
const key = this.node.newExtGState('GS', graphicsState);
16041602

16051603
return key;
16061604
}

src/api/form/PDFForm.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -550,11 +550,9 @@ export default class PDFForm {
550550
const page = this.findWidgetPage(widget);
551551
const widgetRef = this.findWidgetAppearanceRef(field, widget);
552552

553-
const xObjectKey = this.doc.context.addRandomSuffix('FlatWidget', 10);
554-
page.node.setXObject(PDFName.of(xObjectKey), widgetRef);
553+
const xObjectKey = page.node.newXObject('FlatWidget', widgetRef);
555554

556555
const rectangle = widget.getRectangle();
557-
558556
const operators = [
559557
pushGraphicsState(),
560558
translate(rectangle.x, rectangle.y),

src/core/objects/PDFDict.ts

+10
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,16 @@ class PDFDict extends PDFObject {
159159
return new Map(this.dict);
160160
}
161161

162+
/** Generate a random key that doesn't exist in current key set */
163+
uniqueKey(tag = ''): PDFName {
164+
const existingKeys = this.keys();
165+
let key = PDFName.of(this.context.addRandomSuffix(tag, 10));
166+
while (existingKeys.includes(key)) {
167+
key = PDFName.of(this.context.addRandomSuffix(tag, 10));
168+
}
169+
return key;
170+
}
171+
162172
clone(context?: PDFContext): PDFDict {
163173
const clone = PDFDict.withContext(context || this.context);
164174
const entries = this.entries();

src/core/structures/PDFPageLeaf.ts

+33
Original file line numberDiff line numberDiff line change
@@ -150,16 +150,49 @@ class PDFPageLeaf extends PDFDict {
150150
Font.set(name, fontDictRef);
151151
}
152152

153+
newFontDictionaryKey(tag: string): PDFName {
154+
const { Font } = this.normalizedEntries();
155+
return Font.uniqueKey(tag);
156+
}
157+
158+
newFontDictionary(tag: string, fontDictRef: PDFRef): PDFName {
159+
const key = this.newFontDictionaryKey(tag);
160+
this.setFontDictionary(key, fontDictRef);
161+
return key;
162+
}
163+
153164
setXObject(name: PDFName, xObjectRef: PDFRef): void {
154165
const { XObject } = this.normalizedEntries();
155166
XObject.set(name, xObjectRef);
156167
}
157168

169+
newXObjectKey(tag: string): PDFName {
170+
const { XObject } = this.normalizedEntries();
171+
return XObject.uniqueKey(tag);
172+
}
173+
174+
newXObject(tag: string, xObjectRef: PDFRef): PDFName {
175+
const key = this.newXObjectKey(tag);
176+
this.setXObject(key, xObjectRef);
177+
return key;
178+
}
179+
158180
setExtGState(name: PDFName, extGStateRef: PDFRef | PDFDict): void {
159181
const { ExtGState } = this.normalizedEntries();
160182
ExtGState.set(name, extGStateRef);
161183
}
162184

185+
newExtGStateKey(tag: string): PDFName {
186+
const { ExtGState } = this.normalizedEntries();
187+
return ExtGState.uniqueKey(tag);
188+
}
189+
190+
newExtGState(tag: string, extGStateRef: PDFRef | PDFDict): PDFName {
191+
const key = this.newExtGStateKey(tag);
192+
this.setExtGState(key, extGStateRef);
193+
return key;
194+
}
195+
163196
ascend(visitor: (node: PDFPageTree | PDFPageLeaf) => any): void {
164197
visitor(this);
165198
const Parent = this.Parent();

tests/api/PDFPage.spec.ts

+60-7
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
1-
import { PDFArray, PDFDocument, PDFName } from 'src/index';
1+
import fs from 'fs';
2+
import { PDFArray, PDFDocument, PDFName, StandardFonts } from 'src/index';
3+
4+
const birdPng = fs.readFileSync('assets/images/greyscale_bird.png');
25

36
describe(`PDFDocument`, () => {
4-
describe.only(`getSize() method`, () => {
5-
it(`Returns the width and height of the the page's MediaBox`, async () => {
7+
describe(`getSize() method`, () => {
8+
it(`returns the width and height of the the page's MediaBox`, async () => {
69
const pdfDoc = await PDFDocument.create();
710
const page = pdfDoc.addPage();
811
page.node.set(PDFName.MediaBox, pdfDoc.context.obj([5, 5, 20, 50]));
912
expect(page.getSize()).toEqual({ width: 15, height: 45 });
1013
});
1114
});
1215

13-
describe.only(`setSize() method`, () => {
14-
it(`Sets the width and height of only the the page's MediaBox when the other boxes are not defined`, async () => {
16+
describe(`setSize() method`, () => {
17+
it(`sets the width and height of only the the page's MediaBox when the other boxes are not defined`, async () => {
1518
const pdfDoc = await PDFDocument.create();
1619
const page = pdfDoc.addPage();
1720

@@ -33,7 +36,7 @@ describe(`PDFDocument`, () => {
3336
expect(page.node.ArtBox()).toBeUndefined();
3437
});
3538

36-
it(`Sets the width and height of the the page's CropBox, BleedBox, TrimBox, and ArtBox when they equal the MediaBox`, async () => {
39+
it(`sets the width and height of the the page's CropBox, BleedBox, TrimBox, and ArtBox when they equal the MediaBox`, async () => {
3740
const pdfDoc = await PDFDocument.create();
3841
const page = pdfDoc.addPage();
3942

@@ -64,7 +67,7 @@ describe(`PDFDocument`, () => {
6467
expect(page.getArtBox()).toEqual({ x: 5, y: 5, width: 15, height: 45 });
6568
});
6669

67-
it(`Does not set the width and height of the the page's CropBox, BleedBox, TrimBox, or ArtBox when they do not equal the MediaBox`, async () => {
70+
it(`does not set the width and height of the the page's CropBox, BleedBox, TrimBox, or ArtBox when they do not equal the MediaBox`, async () => {
6871
const pdfDoc = await PDFDocument.create();
6972
const page = pdfDoc.addPage();
7073

@@ -95,4 +98,54 @@ describe(`PDFDocument`, () => {
9598
expect(page.getArtBox()).toEqual({ x: 0, y: 5, width: 20, height: 25 });
9699
});
97100
});
101+
102+
// https://github.com/Hopding/pdf-lib/issues/1075
103+
it(`drawImage() does not reuse existing XObject keys`, async () => {
104+
const pdfDoc1 = await PDFDocument.create();
105+
const image1 = await pdfDoc1.embedPng(birdPng);
106+
const page1 = pdfDoc1.addPage();
107+
108+
expect(page1.node.normalizedEntries().XObject.keys().length).toEqual(0);
109+
page1.drawImage(image1);
110+
expect(page1.node.normalizedEntries().XObject.keys().length).toEqual(1);
111+
112+
const key1 = page1.node.normalizedEntries().XObject.keys()[0];
113+
114+
const pdfDoc2 = await PDFDocument.load(await pdfDoc1.save());
115+
const image2 = await pdfDoc2.embedPng(birdPng);
116+
const page2 = pdfDoc2.getPage(0);
117+
118+
expect(page2.node.normalizedEntries().XObject.keys().length).toEqual(1);
119+
page2.drawImage(image2);
120+
expect(page2.node.normalizedEntries().XObject.keys().length).toEqual(2);
121+
122+
const key2 = page2.node.normalizedEntries().XObject.keys()[1];
123+
expect(key1).not.toEqual(key2);
124+
expect(page2.node.normalizedEntries().XObject.keys()).toEqual([key1, key2]);
125+
});
126+
127+
// https://github.com/Hopding/pdf-lib/issues/1075
128+
it(`setFont() does not reuse existing Font keys`, async () => {
129+
const pdfDoc1 = await PDFDocument.create();
130+
const font1 = await pdfDoc1.embedFont(StandardFonts.Helvetica);
131+
const page1 = pdfDoc1.addPage();
132+
133+
expect(page1.node.normalizedEntries().Font.keys().length).toEqual(0);
134+
page1.setFont(font1);
135+
expect(page1.node.normalizedEntries().Font.keys().length).toEqual(1);
136+
137+
const key1 = page1.node.normalizedEntries().Font.keys()[0];
138+
139+
const pdfDoc2 = await PDFDocument.load(await pdfDoc1.save());
140+
const font2 = await pdfDoc2.embedFont(StandardFonts.Helvetica);
141+
const page2 = pdfDoc2.getPage(0);
142+
143+
expect(page2.node.normalizedEntries().Font.keys().length).toEqual(1);
144+
page2.setFont(font2);
145+
expect(page2.node.normalizedEntries().Font.keys().length).toEqual(2);
146+
147+
const key2 = page2.node.normalizedEntries().Font.keys()[1];
148+
expect(key1).not.toEqual(key2);
149+
expect(page2.node.normalizedEntries().Font.keys()).toEqual([key1, key2]);
150+
});
98151
});

tests/api/form/PDFRadioGroup.spec.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ describe(`PDFRadioGroup`, () => {
5757
expect(historicalFigures.isOffToggleable()).toBe(false);
5858
});
5959

60-
it.only(`supports mutualExclusion=true`, async () => {
60+
it(`supports mutualExclusion=true`, async () => {
6161
const pdfDoc = await PDFDocument.create();
6262
const page = pdfDoc.addPage();
6363
const form = pdfDoc.getForm();
@@ -103,7 +103,7 @@ describe(`PDFRadioGroup`, () => {
103103
expect((opt.get(3) as PDFHexString).decodeText()).toBe('qux');
104104
});
105105

106-
it.only(`supports mutualExclusion=false`, async () => {
106+
it(`supports mutualExclusion=false`, async () => {
107107
const pdfDoc = await PDFDocument.create();
108108
const page = pdfDoc.addPage();
109109
const form = pdfDoc.getForm();

tests/core/objects/PDFDict.spec.ts

+29-1
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ describe(`PDFDict`, () => {
138138
);
139139
});
140140

141-
it(`return "undefined" if the underlying value is "PDFNull"`, () => {
141+
it(`returns "undefined" if the underlying value is "PDFNull"`, () => {
142142
const dict = context.obj({ foo: null });
143143
dict.set(PDFName.of('Bar'), PDFNull);
144144
context.assign(PDFRef.of(21), PDFNull);
@@ -164,4 +164,32 @@ describe(`PDFDict`, () => {
164164
expect(dict.lookupMaybe(PDFName.of('Bar'), PDFDict)).toBe(undefined);
165165
expect(dict.lookupMaybe(PDFName.of('qux'), PDFDict)).toBe(undefined);
166166
});
167+
168+
// https://github.com/Hopding/pdf-lib/issues/1075
169+
it(`can generate new keys that don't conflict with existing ones`, () => {
170+
const anotherContext = PDFContext.create();
171+
const anotherDict = anotherContext.obj({});
172+
const anotherKey = anotherDict.uniqueKey();
173+
174+
const dict = context.obj({});
175+
expect(dict.keys().length).toBe(0);
176+
177+
dict.set(anotherKey, context.obj('boing'));
178+
expect(dict.keys().length).toBe(1);
179+
180+
const key1 = dict.uniqueKey();
181+
dict.set(key1, context.obj('beep'));
182+
expect(dict.keys().length).toBe(2);
183+
184+
const key2 = dict.uniqueKey();
185+
dict.set(key2, context.obj('boop'));
186+
expect(dict.keys().length).toBe(3);
187+
188+
const key3 = dict.uniqueKey();
189+
dict.set(key3, context.obj('baap'));
190+
expect(dict.keys().length).toBe(4);
191+
192+
expect(new Set(dict.keys()).size).toBe(4);
193+
expect(dict.keys()).toEqual([anotherKey, key1, key2, key3]);
194+
});
167195
});

0 commit comments

Comments
 (0)