Repository: Hopding/pdf-lib
Pull request #1032 (2021-10-14 15:58:09)

Improved memory usage
We have been using pdf-lib for the last 4 months to generate our PDFs on the WEB and on the server. I noticed that for a PDF with 180 pages and high resolution PNGs, the RAM usage increases a lot (~6GB). 

My code change ensures that after a `page.drawImage` you can directly call an `image.embed()`.

This decodes and encodes the image immediately and remove the decoded image from RAM immediately.
With this change I have the same performance but a maximum RAM usage of 220MB.

Usage without these changes:
![image](https://user-images.githubusercontent.com/14217001/137353918-ecf42a78-dd5f-4257-b4c6-399fb8e746c1.png)
Max RAM usage:
![image](https://user-images.githubusercontent.com/14217001/137353948-b793bce0-2c0c-4bd5-90ff-0aa53476658b.png)


Usage with these changes:
![image](https://user-images.githubusercontent.com/14217001/137353970-c40b6046-0879-494c-8f95-06c90a3245bf.png)
Max RAM usage:
![image](https://user-images.githubusercontent.com/14217001/137353994-18fc7e40-a9a8-4814-89e6-8b17b72f4cfa.png)


ritteratzuken: Hello @Hopding,

i am pleased that the amendment is accepted. I have adapted the commit as you requested. 

The unneeded field "alreadyEmbedded" has been removed.
(https://github.com/Hopding/pdf-lib/pull/1032/files#diff-2e6b05e8490d2c6cc7ca78f78e8fa58bcaef509fbc97754da0808bca0f9d47ffL38)

Additionally, I made the embed() method parellel safe.
(https://github.com/Hopding/pdf-lib/pull/1032/files#diff-2e6b05e8490d2c6cc7ca78f78e8fa58bcaef509fbc97754da0808bca0f9d47ffR131)

Patch:
diff --git a/scratchpad/index.ts b/scratchpad/index.ts
index 6263ac2..370b950 100644
--- a/scratchpad/index.ts
+++ b/scratchpad/index.ts
@@ -3,38 +3,23 @@ import { openPdf, Reader } from './open';
 import { PDFDocument } from 'src/index';
 
 (async () => {
-  // This should be a Uint8Array or ArrayBuffer
-  // This data can be obtained in a number of different ways
-  // If your running in a Node environment, you could use fs.readFile()
-  // In the browser, you could make a fetch() call and use res.arrayBuffer()
-  const existingPdfBytes = fs.readFileSync('assets/pdfs/with_annots.pdf');
+  const pdfDoc1 = await PDFDocument.create();
+  const image1 = await pdfDoc1.embedPng(
+    fs.readFileSync('assets/images/mario_emblem.png'),
+  );
+  const page1 = pdfDoc1.addPage();
+  page1.drawImage(image1, { ...image1.scale(1.0) });
 
-  // Load a PDFDocument without updating its existing metadata
-  const pdfDoc = await PDFDocument.load(existingPdfBytes);
-  const viewerPrefs = pdfDoc.catalog.getOrCreateViewerPreferences();
+  const pdfDoc1Bytes = await pdfDoc1.save();
 
-  // Print all available viewer preference fields
-  console.log('HideToolbar:', viewerPrefs.getHideToolbar());
-  console.log('HideMenubar:', viewerPrefs.getHideMenubar());
-  console.log('HideWindowUI:', viewerPrefs.getHideWindowUI());
-  console.log('FitWindow:', viewerPrefs.getFitWindow());
-  console.log('CenterWindow:', viewerPrefs.getCenterWindow());
-  console.log('DisplayDocTitle:', viewerPrefs.getDisplayDocTitle());
-  console.log('NonFullScreenPageMode:', viewerPrefs.getNonFullScreenPageMode());
-  console.log('ReadingDirection:', viewerPrefs.getReadingDirection());
-  console.log('PrintScaling:', viewerPrefs.getPrintScaling());
-  console.log('Duplex:', viewerPrefs.getDuplex());
-  console.log('PickTrayByPDFSize:', viewerPrefs.getPickTrayByPDFSize());
-  console.log('PrintPageRange:', viewerPrefs.getPrintPageRange());
-  console.log('NumCopies:', viewerPrefs.getNumCopies());
+  const pdfDoc2 = await PDFDocument.load(pdfDoc1Bytes);
+  const image2 = await pdfDoc2.embedPng(
+    fs.readFileSync('assets/images/minions_banana_alpha.png'),
+  );
+  const page2 = pdfDoc2.getPage(0);
+  page2.drawImage(image2, { ...image2.scale(0.5), x: 100, y: 100 });
 
-  // Serialize the PDFDocument to bytes (a Uint8Array)
-  const pdfBytes = await pdfDoc.save();
-
-  // For example, `pdfBytes` can be:
-  //   • Written to a file in Node
-  //   • Downloaded from the browser
-  //   • Rendered in an <iframe>
+  const pdfBytes = await pdfDoc2.save();
 
   fs.writeFileSync('out.pdf', pdfBytes);
   openPdf('out.pdf', Reader.Preview);
diff --git a/src/api/PDFImage.ts b/src/api/PDFImage.ts
index f2f8ce1..2b33cbc 100644
--- a/src/api/PDFImage.ts
+++ b/src/api/PDFImage.ts
@@ -35,8 +35,8 @@ export default class PDFImage implements Embeddable {
   /** The height of this image in pixels. */
   readonly height: number;
 
-  private alreadyEmbedded = false;
-  private readonly embedder: ImageEmbedder;
+  private embedder: ImageEmbedder | undefined;
+  private embedderTask: Promise<PDFRef> | undefined;
 
   private constructor(ref: PDFRef, doc: PDFDocument, embedder: ImageEmbedder) {
     assertIs(ref, 'ref', [[PDFRef, 'PDFRef']]);
@@ -125,9 +125,15 @@ export default class PDFImage implements Embeddable {
    * @returns Resolves when the embedding is complete.
    */
   async embed(): Promise<void> {
-    if (!this.alreadyEmbedded) {
-      await this.embedder.embedIntoContext(this.doc.context, this.ref);
-      this.alreadyEmbedded = true;
+    const embedder = this.embedder;
+    if (!embedder) return;
+
+    let embedderTask = this.embedderTask;
+    if (!embedderTask) {
+      embedderTask = embedder.embedIntoContext(this.doc.context, this.ref);
+      this.embedderTask = embedderTask;
     }
+    await embedderTask;
+    this.embedder = undefined;
   }
 }
diff --git a/src/api/PDFPage.ts b/src/api/PDFPage.ts
index 39de74d..7333440 100644
--- a/src/api/PDFPage.ts
+++ b/src/api/PDFPage.ts
@@ -100,7 +100,7 @@ export default class PDFPage {
   /** The document to which this page belongs. */
   readonly doc: PDFDocument;
 
-  private fontKey?: string;
+  private fontKey?: PDFName;
   private font?: PDFFont;
   private fontSize = 24;
   private fontColor = rgb(0, 0, 0) as Color;
@@ -699,8 +699,7 @@ export default class PDFPage {
     // TODO: Reuse image Font name if we've already added this image to Resources.Fonts
     assertIs(font, 'font', [[PDFFont, 'PDFFont']]);
     this.font = font;
-    this.fontKey = this.doc.context.addRandomSuffix(this.font.name);
-    this.node.setFontDictionary(PDFName.of(this.fontKey), this.font.ref);
+    this.fontKey = this.node.newFontDictionary(this.font.name, this.font.ref);
   }
 
   /**
@@ -1059,8 +1058,7 @@ export default class PDFPage {
     assertRangeOrUndefined(options.opacity, 'opacity.opacity', 0, 1);
     assertIsOneOfOrUndefined(options.blendMode, 'options.blendMode', BlendMode);
 
-    const xObjectKey = this.doc.context.addRandomSuffix('Image', 10);
-    this.node.setXObject(PDFName.of(xObjectKey), image.ref);
+    const xObjectKey = this.node.newXObject('Image', image.ref);
 
     const graphicsStateKey = this.maybeEmbedGraphicsState({
       opacity: options.opacity,
@@ -1134,8 +1132,10 @@ export default class PDFPage {
     assertRangeOrUndefined(options.opacity, 'opacity.opacity', 0, 1);
     assertIsOneOfOrUndefined(options.blendMode, 'options.blendMode', BlendMode);
 
-    const xObjectKey = this.doc.context.addRandomSuffix('EmbeddedPdfPage', 10);
-    this.node.setXObject(PDFName.of(xObjectKey), embeddedPage.ref);
+    const xObjectKey = this.node.newXObject(
+      'EmbeddedPdfPage',
+      embeddedPage.ref,
+    );
 
     const graphicsStateKey = this.maybeEmbedGraphicsState({
       opacity: options.opacity,
@@ -1549,7 +1549,7 @@ export default class PDFPage {
     return { oldFont, oldFontKey, newFont, newFontKey };
   }
 
-  private getFont(): [PDFFont, string] {
+  private getFont(): [PDFFont, PDFName] {
     if (!this.font || !this.fontKey) {
       const font = this.doc.embedStandardFont(StandardFonts.Helvetica);
       this.setFont(font);
@@ -1580,7 +1580,7 @@ export default class PDFPage {
     opacity?: number;
     borderOpacity?: number;
     blendMode?: BlendMode;
-  }): string | undefined {
+  }): PDFName | undefined {
     const { opacity, borderOpacity, blendMode } = options;
 
     if (
@@ -1591,8 +1591,6 @@ export default class PDFPage {
       return undefined;
     }
 
-    const key = this.doc.context.addRandomSuffix('GS', 10);
-
     const graphicsState = this.doc.context.obj({
       Type: 'ExtGState',
       ca: opacity,
@@ -1600,7 +1598,7 @@ export default class PDFPage {
       BM: blendMode,
     });
 
-    this.node.setExtGState(PDFName.of(key), graphicsState);
+    const key = this.node.newExtGState('GS', graphicsState);
 
     return key;
   }
diff --git a/src/api/form/PDFForm.ts b/src/api/form/PDFForm.ts
index 8380665..e92c833 100644
--- a/src/api/form/PDFForm.ts
+++ b/src/api/form/PDFForm.ts
@@ -550,11 +550,9 @@ export default class PDFForm {
         const page = this.findWidgetPage(widget);
         const widgetRef = this.findWidgetAppearanceRef(field, widget);
 
-        const xObjectKey = this.doc.context.addRandomSuffix('FlatWidget', 10);
-        page.node.setXObject(PDFName.of(xObjectKey), widgetRef);
+        const xObjectKey = page.node.newXObject('FlatWidget', widgetRef);
 
         const rectangle = widget.getRectangle();
-
         const operators = [
           pushGraphicsState(),
           translate(rectangle.x, rectangle.y),
diff --git a/src/core/objects/PDFDict.ts b/src/core/objects/PDFDict.ts
index d855d90..f7ba7c7 100644
--- a/src/core/objects/PDFDict.ts
+++ b/src/core/objects/PDFDict.ts
@@ -159,6 +159,16 @@ class PDFDict extends PDFObject {
     return new Map(this.dict);
   }
 
+  /** Generate a random key that doesn't exist in current key set */
+  uniqueKey(tag = ''): PDFName {
+    const existingKeys = this.keys();
+    let key = PDFName.of(this.context.addRandomSuffix(tag, 10));
+    while (existingKeys.includes(key)) {
+      key = PDFName.of(this.context.addRandomSuffix(tag, 10));
+    }
+    return key;
+  }
+
   clone(context?: PDFContext): PDFDict {
     const clone = PDFDict.withContext(context || this.context);
     const entries = this.entries();
diff --git a/src/core/structures/PDFPageLeaf.ts b/src/core/structures/PDFPageLeaf.ts
index 26f65b1..0015c59 100644
--- a/src/core/structures/PDFPageLeaf.ts
+++ b/src/core/structures/PDFPageLeaf.ts
@@ -150,16 +150,49 @@ class PDFPageLeaf extends PDFDict {
     Font.set(name, fontDictRef);
   }
 
+  newFontDictionaryKey(tag: string): PDFName {
+    const { Font } = this.normalizedEntries();
+    return Font.uniqueKey(tag);
+  }
+
+  newFontDictionary(tag: string, fontDictRef: PDFRef): PDFName {
+    const key = this.newFontDictionaryKey(tag);
+    this.setFontDictionary(key, fontDictRef);
+    return key;
+  }
+
   setXObject(name: PDFName, xObjectRef: PDFRef): void {
     const { XObject } = this.normalizedEntries();
     XObject.set(name, xObjectRef);
   }
 
+  newXObjectKey(tag: string): PDFName {
+    const { XObject } = this.normalizedEntries();
+    return XObject.uniqueKey(tag);
+  }
+
+  newXObject(tag: string, xObjectRef: PDFRef): PDFName {
+    const key = this.newXObjectKey(tag);
+    this.setXObject(key, xObjectRef);
+    return key;
+  }
+
   setExtGState(name: PDFName, extGStateRef: PDFRef | PDFDict): void {
     const { ExtGState } = this.normalizedEntries();
     ExtGState.set(name, extGStateRef);
   }
 
+  newExtGStateKey(tag: string): PDFName {
+    const { ExtGState } = this.normalizedEntries();
+    return ExtGState.uniqueKey(tag);
+  }
+
+  newExtGState(tag: string, extGStateRef: PDFRef | PDFDict): PDFName {
+    const key = this.newExtGStateKey(tag);
+    this.setExtGState(key, extGStateRef);
+    return key;
+  }
+
   ascend(visitor: (node: PDFPageTree | PDFPageLeaf) => any): void {
     visitor(this);
     const Parent = this.Parent();


Test patch:
diff --git a/tests/api/PDFDocument.spec.ts b/tests/api/PDFDocument.spec.ts
index 7cf17d0..89b04a6 100644
--- a/tests/api/PDFDocument.spec.ts
+++ b/tests/api/PDFDocument.spec.ts
@@ -16,6 +16,8 @@ import {
   ViewerPreferences,
 } from 'src/index';
 
+const examplePngImage = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9TxaoVBzuIdMhQnSyIijhKFYtgobQVWnUwufQLmjQkKS6OgmvBwY/FqoOLs64OroIg+AHi5uak6CIl/i8ptIjx4Lgf7+497t4BQqPCVLNrAlA1y0jFY2I2tyr2vKIfAgLoRVhipp5IL2bgOb7u4ePrXZRneZ/7cwwoeZMBPpF4jumGRbxBPLNp6Zz3iUOsJCnE58TjBl2Q+JHrsstvnIsOCzwzZGRS88QhYrHYwXIHs5KhEk8TRxRVo3wh67LCeYuzWqmx1j35C4N5bSXNdZphxLGEBJIQIaOGMiqwEKVVI8VEivZjHv4Rx58kl0yuMhg5FlCFCsnxg//B727NwtSkmxSMAd0vtv0xCvTsAs26bX8f23bzBPA/A1da219tALOfpNfbWuQIGNwGLq7bmrwHXO4Aw0+6ZEiO5KcpFArA+xl9Uw4YugX61tzeWvs4fQAy1NXyDXBwCIwVKXvd492Bzt7+PdPq7wcdn3KFLu4iBAAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAlFJREFUeNrt289r02AYB/Dvk6Sl4EDKpllTlFKsnUdBHXgUBEHwqHj2IJ72B0zwKHhxJ08i/gDxX/AiRfSkBxELXTcVxTa2s2xTsHNN8ngQbQL70RZqG/Z9b29JnvflkydP37whghG3ZaegoxzfwB5vBCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgwB5rstWPtnP0LqBX/vZNyLF6vVrpN/hucewhb4g+B2AyAwiwY7NGOXijviS9vBeYh6CEP4edBLDADCAAAQhAAAIQgAAEIAABCDAUAFF/GIN1DM+PBYCo/ohMXDQ1WPjoeUZH1mMBEEh0oqLGvsHCy0S4NzWVWotJBogbvZB+brDwQT7UWSmXy5sxyQB9HQEROdVv4HQ+vx+QmS4iXsWmCK7Usu8AhOqAXMzlcn3VgWTbugQgEYrxMkZ/gyUPgnuhe2C6/Stxvdeg2ezMJERvhOuoZ+JBrNYBRuDdBtDuXkDM25nCHLbZSv9X6A4VHU+DpwCcbvbjcetLtTaOANtuirrux08HM0euisjDEMKC7RQuq+C+pVJqpzx3NZ3+eeBza9I0rWJgyHnxg2sAJrqnaHUzFcyN60Jox13hprv8aNopZBS4GcqWWVHM+lAkN0zY7ncgkYBukRoKLPpiXVj9UFkfV4Bdl8Jf60u3IMZZAG/6iLuhkDvaSZ74VqtUx3kp3NN7gUZt8RmA43a2eEY1OCfQ04AcBpAGkAKwpkBLIG8BfQE/eNJsvG/G4VlARj0BfjDBx2ECEIAABCAAAQhAAAIQgAAE+P/tN8YvpvbTDBOlAAAAAElFTkSuQmCC';
+
 const unencryptedPdfBytes = fs.readFileSync('assets/pdfs/normal.pdf');
 const oldEncryptedPdfBytes1 = fs.readFileSync('assets/pdfs/encrypted_old.pdf');
 
@@ -469,6 +471,67 @@ describe(`PDFDocument`, () => {
       expect(JSNames.lookup(2, PDFHexString).decodeText()).toEqual('second');
     });
   });
+
+  describe(`embedPng() method`, () => {
+    it(`It should still be possible to modify the PDFDocument even if a image has already been embedded`, async () => {
+      const pdfDoc = await PDFDocument.create();
+      const pdfPage = pdfDoc.addPage();
+
+      const noErrorFunc = async () => {
+        const embeddedImage = await pdfDoc.embedPng(examplePngImage);
+        await pdfPage.drawImage(embeddedImage);
+        await embeddedImage.embed();
+
+        const pdfPage2 = pdfDoc.addPage();
+        await pdfPage2.drawImage(embeddedImage);
+
+        pdfDoc.setTitle('Unit Test');
+      };
+
+      await expect(noErrorFunc()).resolves.not.toThrowError();
+    });
+  });
+
+  describe(`save() method`, () => {
+
+    it(`The save() function may be called several times with different changes`, async () => {
+      const pdfDoc = await PDFDocument.create();
+      const pdfPage = pdfDoc.addPage();
+      const embeddedImage = await pdfDoc.embedPng(examplePngImage);
+
+      const noErrorFunc = async () => {
+        await pdfPage.drawImage(embeddedImage);
+        await embeddedImage.embed();
+
+        const pdfPage2 = pdfDoc.addPage();
+        await pdfPage2.drawImage(embeddedImage);
+
+        pdfDoc.setTitle('Unit Test');
+
+        const pdfBytes1 = await pdfDoc.save();
+        expect(pdfBytes1.byteLength).toBeGreaterThan(0);
+
+        const pdfPage3 = pdfDoc.addPage();
+        await pdfPage3.drawImage(embeddedImage);
+
+        pdfDoc.setTitle('Unit Test 2. change');
+
+        const pdfBytes2 = await pdfDoc.save();
+        expect(pdfBytes2.byteLength).toBeGreaterThan(0);
+        expect(pdfBytes1.byteLength).not.toEqual(pdfBytes2.byteLength);
+
+        const pdf1 = await PDFDocument.load(pdfBytes1);
+        expect(pdf1.getTitle()).toEqual('Unit Test');
+        expect(pdf1.getPageCount()).toEqual(2);
+
+        const pdf2 = await PDFDocument.load(pdfBytes2);
+        expect(pdf2.getTitle()).toEqual('Unit Test 2. change');
+        expect(pdf2.getPageCount()).toEqual(3);
+      };
+
+      await expect(noErrorFunc()).resolves.not.toThrowError();
+    });
+  });
 });
 
 describe(`copy() method`, () => {
diff --git a/tests/api/PDFImage.spec.ts b/tests/api/PDFImage.spec.ts
new file mode 100644
index 0000000..5ec65ba
--- /dev/null
+++ b/tests/api/PDFImage.spec.ts
@@ -0,0 +1,62 @@
+import { PDFDocument, PDFImage } from "src/api";
+import { PngEmbedder } from "src/core";
+import { toUint8Array } from "src/utils";
+
+
+const examplePngImage = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9TxaoVBzuIdMhQnSyIijhKFYtgobQVWnUwufQLmjQkKS6OgmvBwY/FqoOLs64OroIg+AHi5uak6CIl/i8ptIjx4Lgf7+497t4BQqPCVLNrAlA1y0jFY2I2tyr2vKIfAgLoRVhipp5IL2bgOb7u4ePrXZRneZ/7cwwoeZMBPpF4jumGRbxBPLNp6Zz3iUOsJCnE58TjBl2Q+JHrsstvnIsOCzwzZGRS88QhYrHYwXIHs5KhEk8TRxRVo3wh67LCeYuzWqmx1j35C4N5bSXNdZphxLGEBJIQIaOGMiqwEKVVI8VEivZjHv4Rx58kl0yuMhg5FlCFCsnxg//B727NwtSkmxSMAd0vtv0xCvTsAs26bX8f23bzBPA/A1da219tALOfpNfbWuQIGNwGLq7bmrwHXO4Aw0+6ZEiO5KcpFArA+xl9Uw4YugX61tzeWvs4fQAy1NXyDXBwCIwVKXvd492Bzt7+PdPq7wcdn3KFLu4iBAAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAlFJREFUeNrt289r02AYB/Dvk6Sl4EDKpllTlFKsnUdBHXgUBEHwqHj2IJ72B0zwKHhxJ08i/gDxX/AiRfSkBxELXTcVxTa2s2xTsHNN8ngQbQL70RZqG/Z9b29JnvflkydP37whghG3ZaegoxzfwB5vBCAAAQhAAAIQgAAEIAABCEAAAhCAAAQgwB5rstWPtnP0LqBX/vZNyLF6vVrpN/hucewhb4g+B2AyAwiwY7NGOXijviS9vBeYh6CEP4edBLDADCAAAQhAAAIQgAAEIAABCDAUAFF/GIN1DM+PBYCo/ohMXDQ1WPjoeUZH1mMBEEh0oqLGvsHCy0S4NzWVWotJBogbvZB+brDwQT7UWSmXy5sxyQB9HQEROdVv4HQ+vx+QmS4iXsWmCK7Usu8AhOqAXMzlcn3VgWTbugQgEYrxMkZ/gyUPgnuhe2C6/Stxvdeg2ezMJERvhOuoZ+JBrNYBRuDdBtDuXkDM25nCHLbZSv9X6A4VHU+DpwCcbvbjcetLtTaOANtuirrux08HM0euisjDEMKC7RQuq+C+pVJqpzx3NZ3+eeBza9I0rWJgyHnxg2sAJrqnaHUzFcyN60Jox13hprv8aNopZBS4GcqWWVHM+lAkN0zY7ncgkYBukRoKLPpiXVj9UFkfV4Bdl8Jf60u3IMZZAG/6iLuhkDvaSZ74VqtUx3kp3NN7gUZt8RmA43a2eEY1OCfQ04AcBpAGkAKwpkBLIG8BfQE/eNJsvG/G4VlARj0BfjDBx2ECEIAABCAAAQhAAAIQgAAE+P/tN8YvpvbTDBOlAAAAAElFTkSuQmCC";
+
+describe(`PDFImage`, () => {
+
+  describe(`embed() method`, () => {
+    it(`The embedder field should be cleared after the first call to embed()`, async () => {
+      const pdfDoc = await PDFDocument.create();
+
+      const bytes = toUint8Array(examplePngImage);
+      const embedder = await PngEmbedder.for(bytes);
+      const ref = pdfDoc.context.nextRef();
+      const pdfImage = PDFImage.of(ref, pdfDoc, embedder);
+
+      const embedderVariable = "embedder";
+      expect(pdfImage[embedderVariable]).toBeDefined();
+      await pdfImage.embed();
+      expect(pdfImage[embedderVariable]).toBeUndefined();
+    });
+    it(`The method embed() may be called several times without an error`, async () => {
+      const pdfDoc = await PDFDocument.create();
+
+      const bytes = toUint8Array(examplePngImage);
+      const embedder = await PngEmbedder.for(bytes);
+      const ref = pdfDoc.context.nextRef();
+      const pdfImage = PDFImage.of(ref, pdfDoc, embedder);
+
+      const noErrorFunc = async () => {
+        await pdfImage.embed();
+        await pdfImage.embed();
+      }
+
+      await expect(noErrorFunc()).resolves.not.toThrowError();
+    });
+    it(`The method embed() may be called parallel without causing an error`, async () => {
+      const pdfDoc = await PDFDocument.create();
+
+      const bytes = toUint8Array(examplePngImage);
+      const embedder = await PngEmbedder.for(bytes);
+      const ref = pdfDoc.context.nextRef();
+      const pdfImage = PDFImage.of(ref, pdfDoc, embedder);
+
+      const noErrorFunc = async () => {
+        const embedderTaskVariable = "embedderTask";
+        const undefinedTask = pdfImage[embedderTaskVariable];
+        expect(undefinedTask).toBeUndefined();
+        const task1 = pdfImage.embed();
+        const firstTask = pdfImage[embedderTaskVariable];
+        const task2 = pdfImage.embed();
+        const secondTask = pdfImage[embedderTaskVariable];
+        await Promise.all([task1, task2]);
+        expect(firstTask).toEqual(secondTask);
+      }
+
+      await expect(noErrorFunc()).resolves.not.toThrowError();
+    });
+  });
+});
\ No newline at end of file
diff --git a/tests/api/PDFPage.spec.ts b/tests/api/PDFPage.spec.ts
index 1f0ff1f..3a5f0a9 100644
--- a/tests/api/PDFPage.spec.ts
+++ b/tests/api/PDFPage.spec.ts
@@ -1,8 +1,11 @@
-import { PDFArray, PDFDocument, PDFName } from 'src/index';
+import fs from 'fs';
+import { PDFArray, PDFDocument, PDFName, StandardFonts } from 'src/index';
+
+const birdPng = fs.readFileSync('assets/images/greyscale_bird.png');
 
 describe(`PDFDocument`, () => {
-  describe.only(`getSize() method`, () => {
-    it(`Returns the width and height of the the page's MediaBox`, async () => {
+  describe(`getSize() method`, () => {
+    it(`returns the width and height of the the page's MediaBox`, async () => {
       const pdfDoc = await PDFDocument.create();
       const page = pdfDoc.addPage();
       page.node.set(PDFName.MediaBox, pdfDoc.context.obj([5, 5, 20, 50]));
@@ -10,8 +13,8 @@ describe(`PDFDocument`, () => {
     });
   });
 
-  describe.only(`setSize() method`, () => {
-    it(`Sets the width and height of only the the page's MediaBox when the other boxes are not defined`, async () => {
+  describe(`setSize() method`, () => {
+    it(`sets the width and height of only the the page's MediaBox when the other boxes are not defined`, async () => {
       const pdfDoc = await PDFDocument.create();
       const page = pdfDoc.addPage();
 
@@ -33,7 +36,7 @@ describe(`PDFDocument`, () => {
       expect(page.node.ArtBox()).toBeUndefined();
     });
 
-    it(`Sets the width and height of the the page's CropBox, BleedBox, TrimBox, and ArtBox when they equal the MediaBox`, async () => {
+    it(`sets the width and height of the the page's CropBox, BleedBox, TrimBox, and ArtBox when they equal the MediaBox`, async () => {
       const pdfDoc = await PDFDocument.create();
       const page = pdfDoc.addPage();
 
@@ -64,7 +67,7 @@ describe(`PDFDocument`, () => {
       expect(page.getArtBox()).toEqual({ x: 5, y: 5, width: 15, height: 45 });
     });
 
-    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 () => {
+    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 () => {
       const pdfDoc = await PDFDocument.create();
       const page = pdfDoc.addPage();
 
@@ -95,4 +98,54 @@ describe(`PDFDocument`, () => {
       expect(page.getArtBox()).toEqual({ x: 0, y: 5, width: 20, height: 25 });
     });
   });
+
+  // https://github.com/Hopding/pdf-lib/issues/1075
+  it(`drawImage() does not reuse existing XObject keys`, async () => {
+    const pdfDoc1 = await PDFDocument.create();
+    const image1 = await pdfDoc1.embedPng(birdPng);
+    const page1 = pdfDoc1.addPage();
+
+    expect(page1.node.normalizedEntries().XObject.keys().length).toEqual(0);
+    page1.drawImage(image1);
+    expect(page1.node.normalizedEntries().XObject.keys().length).toEqual(1);
+
+    const key1 = page1.node.normalizedEntries().XObject.keys()[0];
+
+    const pdfDoc2 = await PDFDocument.load(await pdfDoc1.save());
+    const image2 = await pdfDoc2.embedPng(birdPng);
+    const page2 = pdfDoc2.getPage(0);
+
+    expect(page2.node.normalizedEntries().XObject.keys().length).toEqual(1);
+    page2.drawImage(image2);
+    expect(page2.node.normalizedEntries().XObject.keys().length).toEqual(2);
+
+    const key2 = page2.node.normalizedEntries().XObject.keys()[1];
+    expect(key1).not.toEqual(key2);
+    expect(page2.node.normalizedEntries().XObject.keys()).toEqual([key1, key2]);
+  });
+
+  // https://github.com/Hopding/pdf-lib/issues/1075
+  it(`setFont() does not reuse existing Font keys`, async () => {
+    const pdfDoc1 = await PDFDocument.create();
+    const font1 = await pdfDoc1.embedFont(StandardFonts.Helvetica);
+    const page1 = pdfDoc1.addPage();
+
+    expect(page1.node.normalizedEntries().Font.keys().length).toEqual(0);
+    page1.setFont(font1);
+    expect(page1.node.normalizedEntries().Font.keys().length).toEqual(1);
+
+    const key1 = page1.node.normalizedEntries().Font.keys()[0];
+
+    const pdfDoc2 = await PDFDocument.load(await pdfDoc1.save());
+    const font2 = await pdfDoc2.embedFont(StandardFonts.Helvetica);
+    const page2 = pdfDoc2.getPage(0);
+
+    expect(page2.node.normalizedEntries().Font.keys().length).toEqual(1);
+    page2.setFont(font2);
+    expect(page2.node.normalizedEntries().Font.keys().length).toEqual(2);
+
+    const key2 = page2.node.normalizedEntries().Font.keys()[1];
+    expect(key1).not.toEqual(key2);
+    expect(page2.node.normalizedEntries().Font.keys()).toEqual([key1, key2]);
+  });
 });
diff --git a/tests/api/form/PDFRadioGroup.spec.ts b/tests/api/form/PDFRadioGroup.spec.ts
index 51d0576..59315c9 100644
--- a/tests/api/form/PDFRadioGroup.spec.ts
+++ b/tests/api/form/PDFRadioGroup.spec.ts
@@ -57,7 +57,7 @@ describe(`PDFRadioGroup`, () => {
     expect(historicalFigures.isOffToggleable()).toBe(false);
   });
 
-  it.only(`supports mutualExclusion=true`, async () => {
+  it(`supports mutualExclusion=true`, async () => {
     const pdfDoc = await PDFDocument.create();
     const page = pdfDoc.addPage();
     const form = pdfDoc.getForm();
@@ -103,7 +103,7 @@ describe(`PDFRadioGroup`, () => {
     expect((opt.get(3) as PDFHexString).decodeText()).toBe('qux');
   });
 
-  it.only(`supports mutualExclusion=false`, async () => {
+  it(`supports mutualExclusion=false`, async () => {
     const pdfDoc = await PDFDocument.create();
     const page = pdfDoc.addPage();
     const form = pdfDoc.getForm();
diff --git a/tests/core/objects/PDFDict.spec.ts b/tests/core/objects/PDFDict.spec.ts
index f14faff..0eb880a 100644
--- a/tests/core/objects/PDFDict.spec.ts
+++ b/tests/core/objects/PDFDict.spec.ts
@@ -138,7 +138,7 @@ describe(`PDFDict`, () => {
     );
   });
 
-  it(`return "undefined" if the underlying value is "PDFNull"`, () => {
+  it(`returns "undefined" if the underlying value is "PDFNull"`, () => {
     const dict = context.obj({ foo: null });
     dict.set(PDFName.of('Bar'), PDFNull);
     context.assign(PDFRef.of(21), PDFNull);
@@ -164,4 +164,32 @@ describe(`PDFDict`, () => {
     expect(dict.lookupMaybe(PDFName.of('Bar'), PDFDict)).toBe(undefined);
     expect(dict.lookupMaybe(PDFName.of('qux'), PDFDict)).toBe(undefined);
   });
+
+  // https://github.com/Hopding/pdf-lib/issues/1075
+  it(`can generate new keys that don't conflict with existing ones`, () => {
+    const anotherContext = PDFContext.create();
+    const anotherDict = anotherContext.obj({});
+    const anotherKey = anotherDict.uniqueKey();
+
+    const dict = context.obj({});
+    expect(dict.keys().length).toBe(0);
+
+    dict.set(anotherKey, context.obj('boing'));
+    expect(dict.keys().length).toBe(1);
+
+    const key1 = dict.uniqueKey();
+    dict.set(key1, context.obj('beep'));
+    expect(dict.keys().length).toBe(2);
+
+    const key2 = dict.uniqueKey();
+    dict.set(key2, context.obj('boop'));
+    expect(dict.keys().length).toBe(3);
+
+    const key3 = dict.uniqueKey();
+    dict.set(key3, context.obj('baap'));
+    expect(dict.keys().length).toBe(4);
+
+    expect(new Set(dict.keys()).size).toBe(4);
+    expect(dict.keys()).toEqual([anotherKey, key1, key2, key3]);
+  });
 });