diff --git a/README.md b/README.md index 1223fb0ac..02c9bed4e 100644 --- a/README.md +++ b/README.md @@ -493,7 +493,7 @@ const secondDonorPdfBytes = ... const firstDonorPdfDoc = await PDFDocument.load(firstDonorPdfBytes) const secondDonorPdfDoc = await PDFDocument.load(secondDonorPdfBytes) -// Copy the 1st page from the first donor document, and +// Copy the 1st page from the first donor document, and // the 743rd page from the second donor document const [firstDonorPage] = await pdfDoc.copyPages(firstDonorPdfDoc, [0]) const [secondDonorPage] = await pdfDoc.copyPages(secondDonorPdfDoc, [742]) @@ -501,7 +501,7 @@ const [secondDonorPage] = await pdfDoc.copyPages(secondDonorPdfDoc, [742]) // Add the first copied page pdfDoc.addPage(firstDonorPage) -// Insert the second copied page to index 0, so it will be the +// Insert the second copied page to index 0, so it will be the // first page in `pdfDoc` pdfDoc.insertPage(0, secondDonorPage) @@ -606,11 +606,11 @@ const preamble = await pdfDoc.embedPage(usConstitutionPdf.getPages()[1], { top: 575, }) -// Get the width/height of the American flag PDF scaled down to 30% of +// Get the width/height of the American flag PDF scaled down to 30% of // its original size const americanFlagDims = americanFlag.scale(0.3) -// Get the width/height of the preamble clipping scaled up to 225% of +// Get the width/height of the preamble clipping scaled up to 225% of // its original size const preambleDims = preamble.scale(2.25) @@ -813,8 +813,8 @@ import { PDFDocument } from 'pdf-lib' const existingPdfBytes = ... // Load a PDFDocument without updating its existing metadata -const pdfDoc = await PDFDocument.load(existingPdfBytes, { - updateMetadata: false +const pdfDoc = await PDFDocument.load(existingPdfBytes, { + updateMetadata: false }) // Print all available metadata fields @@ -1228,7 +1228,7 @@ When working with PDFs, you will frequently come across the terms "character enc const pdfDoc = await PDFDocument.create() const courierFont = await pdfDoc.embedFont(StandardFonts.Courier) const page = pdfDoc.addPage() - page.drawText('Some boring latin text in the Courier font', { + page.drawText('Some boring latin text in the Courier font', { font: courierFont, }) ``` @@ -1248,7 +1248,7 @@ When working with PDFs, you will frequently come across the terms "character enc const ubuntuFont = await pdfDoc.embedFont(fontBytes) const page = pdfDoc.addPage() - page.drawText('Some fancy Unicode text in the ŪЬȕǹƚü font', { + page.drawText('Some fancy Unicode text in the ŪЬȕǹƚü font', { font: ubuntuFont, }) ``` diff --git a/package.json b/package.json index d080bc298..ce32f76dc 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,8 @@ "DkDavid (https://github.com/DkDavid)", "Bj Tecu (https://github.com/btecu)", "Brent McSharry (https://github.com/mcshaz)", - "Tim Knapp (https://github.com/duffyd)" + "Tim Knapp (https://github.com/duffyd)", + "Ching Chang (https://github.com/ChingChang9)" ], "scripts": { "release:latest": "yarn publish --tag latest && yarn pack && yarn release:tag", diff --git a/src/api/PDFPage.ts b/src/api/PDFPage.ts index 5173a1875..39de74d2e 100644 --- a/src/api/PDFPage.ts +++ b/src/api/PDFPage.ts @@ -44,7 +44,6 @@ import { PDFArray, } from 'src/core'; import { - addRandomSuffix, assertEachIs, assertIs, assertMultiple, @@ -700,7 +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 = addRandomSuffix(this.font.name); + this.fontKey = this.doc.context.addRandomSuffix(this.font.name); this.node.setFontDictionary(PDFName.of(this.fontKey), this.font.ref); } @@ -1060,7 +1059,7 @@ export default class PDFPage { assertRangeOrUndefined(options.opacity, 'opacity.opacity', 0, 1); assertIsOneOfOrUndefined(options.blendMode, 'options.blendMode', BlendMode); - const xObjectKey = addRandomSuffix('Image', 10); + const xObjectKey = this.doc.context.addRandomSuffix('Image', 10); this.node.setXObject(PDFName.of(xObjectKey), image.ref); const graphicsStateKey = this.maybeEmbedGraphicsState({ @@ -1135,7 +1134,7 @@ export default class PDFPage { assertRangeOrUndefined(options.opacity, 'opacity.opacity', 0, 1); assertIsOneOfOrUndefined(options.blendMode, 'options.blendMode', BlendMode); - const xObjectKey = addRandomSuffix('EmbeddedPdfPage', 10); + const xObjectKey = this.doc.context.addRandomSuffix('EmbeddedPdfPage', 10); this.node.setXObject(PDFName.of(xObjectKey), embeddedPage.ref); const graphicsStateKey = this.maybeEmbedGraphicsState({ @@ -1592,7 +1591,7 @@ export default class PDFPage { return undefined; } - const key = addRandomSuffix('GS', 10); + const key = this.doc.context.addRandomSuffix('GS', 10); const graphicsState = this.doc.context.obj({ Type: 'ExtGState', diff --git a/src/api/form/PDFField.ts b/src/api/form/PDFField.ts index 3cc7154a7..cd3b3efe4 100644 --- a/src/api/form/PDFField.ts +++ b/src/api/form/PDFField.ts @@ -22,12 +22,7 @@ import { PDFAcroTerminal, AnnotationFlags, } from 'src/core'; -import { - addRandomSuffix, - assertIs, - assertMultiple, - assertOrUndefined, -} from 'src/utils'; +import { assertIs, assertMultiple, assertOrUndefined } from 'src/utils'; import { ImageAlignment } from '../image'; import PDFImage from '../PDFImage'; import { drawImage, rotateInPlace } from '../operations'; @@ -321,9 +316,7 @@ export default class PDFField { ); widget.setRectangle(rect); - if(typeof pageRef !== 'undefined'){ - widget.setP(pageRef); - } + if (pageRef) widget.setP(pageRef); const ac = widget.getOrCreateAppearanceCharacteristics(); if (backgroundColor) { @@ -495,7 +488,7 @@ export default class PDFField { options.y = adj.height - borderWidth - imageDims.height; } - const imageName = addRandomSuffix('Image', 10); + const imageName = this.doc.context.addRandomSuffix('Image', 10); const appearance = [...rotate, ...drawImage(imageName, options)]; //////////// diff --git a/src/api/form/PDFForm.ts b/src/api/form/PDFForm.ts index dfb7d161c..838066582 100644 --- a/src/api/form/PDFForm.ts +++ b/src/api/form/PDFForm.ts @@ -41,7 +41,7 @@ import { PDFName, PDFWidgetAnnotation, } from 'src/core'; -import { addRandomSuffix, assertIs, Cache, assertOrUndefined } from 'src/utils'; +import { assertIs, Cache, assertOrUndefined } from 'src/utils'; export interface FlattenOptions { updateFieldAppearances: boolean; @@ -550,7 +550,7 @@ export default class PDFForm { const page = this.findWidgetPage(widget); const widgetRef = this.findWidgetAppearanceRef(field, widget); - const xObjectKey = addRandomSuffix('FlatWidget', 10); + const xObjectKey = this.doc.context.addRandomSuffix('FlatWidget', 10); page.node.setXObject(PDFName.of(xObjectKey), widgetRef); const rectangle = widget.getRectangle(); diff --git a/src/api/index.ts b/src/api/index.ts index deb2a8d18..677cd389d 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -2,7 +2,7 @@ export * from 'src/api/form'; export * from 'src/api/text'; export * from 'src/api/colors'; export * from 'src/api/errors'; -export * from "src/api/image"; +export * from 'src/api/image'; export * from 'src/api/objects'; export * from 'src/api/operations'; export * from 'src/api/operators'; diff --git a/src/api/operations.ts b/src/api/operations.ts index 05178ef8d..0e46e6fdf 100644 --- a/src/api/operations.ts +++ b/src/api/operations.ts @@ -443,19 +443,19 @@ export const rotateInPlace = (options: { rotation: 0 | 90 | 180 | 270; }) => options.rotation === 0 ? [ - translate(0, 0), - rotateDegrees(0) + translate(0, 0), + rotateDegrees(0) ] : options.rotation === 90 ? [ - translate(options.width, 0), + translate(options.width, 0), rotateDegrees(90) ] : options.rotation === 180 ? [ - translate(options.width, options.height), + translate(options.width, options.height), rotateDegrees(180) ] : options.rotation === 270 ? [ - translate(0, options.height), + translate(0, options.height), rotateDegrees(270) ] : []; // Invalid rotation - noop diff --git a/src/core/PDFContext.ts b/src/core/PDFContext.ts index 7b1bb8cbb..0544aa214 100644 --- a/src/core/PDFContext.ts +++ b/src/core/PDFContext.ts @@ -18,6 +18,7 @@ import PDFOperator from 'src/core/operators/PDFOperator'; import Ops from 'src/core/operators/PDFOperatorNames'; import PDFContentStream from 'src/core/structures/PDFContentStream'; import { typedArrayFor } from 'src/utils'; +import { SimpleRNG } from 'src/utils/rng'; type LookupKey = PDFRef | PDFObject | undefined; @@ -54,6 +55,7 @@ class PDFContext { Info?: PDFObject; ID?: PDFObject; }; + rng: SimpleRNG; private readonly indirectObjects: Map; @@ -66,6 +68,7 @@ class PDFContext { this.trailerInfo = {}; this.indirectObjects = new Map(); + this.rng = SimpleRNG.withSeed(1); } assign(ref: PDFRef, object: PDFObject): void { @@ -287,6 +290,10 @@ class PDFContext { this.popGraphicsStateContentStreamRef = this.register(stream); return this.popGraphicsStateContentStreamRef; } + + addRandomSuffix(prefix: string, suffixLength = 4): string { + return `${prefix}-${Math.floor(this.rng.nextInt() * 10 ** suffixLength)}`; + } } export default PDFContext; diff --git a/src/core/annotation/PDFWidgetAnnotation.ts b/src/core/annotation/PDFWidgetAnnotation.ts index fa3642200..c87903081 100644 --- a/src/core/annotation/PDFWidgetAnnotation.ts +++ b/src/core/annotation/PDFWidgetAnnotation.ts @@ -46,8 +46,8 @@ class PDFWidgetAnnotation extends PDFAnnotation { return undefined; } - setP(page: PDFRef){ - this.dict.set(PDFName.of('P'),page); + setP(page: PDFRef) { + this.dict.set(PDFName.of('P'), page); } setDefaultAppearance(appearance: string) { diff --git a/src/core/embedders/CustomFontEmbedder.ts b/src/core/embedders/CustomFontEmbedder.ts index 5324f8fca..f8d215a6f 100644 --- a/src/core/embedders/CustomFontEmbedder.ts +++ b/src/core/embedders/CustomFontEmbedder.ts @@ -7,7 +7,6 @@ import PDFRef from 'src/core/objects/PDFRef'; import PDFString from 'src/core/objects/PDFString'; import PDFContext from 'src/core/PDFContext'; import { - addRandomSuffix, byAscendingId, Cache, sortedUniq, @@ -106,7 +105,8 @@ class CustomFontEmbedder { } embedIntoContext(context: PDFContext, ref?: PDFRef): Promise { - this.baseFontName = this.customName || addRandomSuffix(this.fontName); + this.baseFontName = + this.customName || context.addRandomSuffix(this.fontName); return this.embedFontDict(context, ref); } diff --git a/src/utils/rng.ts b/src/utils/rng.ts new file mode 100644 index 000000000..73aff2aab --- /dev/null +++ b/src/utils/rng.ts @@ -0,0 +1,21 @@ +/** + * Generates a pseudo random number. Although it is not cryptographically secure + * and uniformly distributed, it is not a concern for the intended use-case, + * which is to generate distinct numbers. + * + * Credit: https://stackoverflow.com/a/19303725/10254049 + */ +export class SimpleRNG { + static withSeed = (seed: number) => new SimpleRNG(seed); + + private seed: number; + + private constructor(seed: number) { + this.seed = seed; + } + + nextInt(): number { + const x = Math.sin(this.seed++) * 10000; + return x - Math.floor(x); + } +} diff --git a/tests/api/PDFDocument.spec.ts b/tests/api/PDFDocument.spec.ts index 1b42e752c..7cf17d0ab 100644 --- a/tests/api/PDFDocument.spec.ts +++ b/tests/api/PDFDocument.spec.ts @@ -142,39 +142,21 @@ describe(`PDFDocument`, () => { }); describe(`embedFont() method`, () => { - it(`serializes the same value on every save when using a custom font name`, async () => { + it(`serializes the same value on every save`, async () => { const customFont = fs.readFileSync('assets/fonts/ubuntu/Ubuntu-B.ttf'); - const customName = 'Custom-Font-Name'; const pdfDoc1 = await PDFDocument.create({ updateMetadata: false }); const pdfDoc2 = await PDFDocument.create({ updateMetadata: false }); pdfDoc1.registerFontkit(fontkit); pdfDoc2.registerFontkit(fontkit); - await pdfDoc1.embedFont(customFont, { customName }); - await pdfDoc2.embedFont(customFont, { customName }); - - const savedDoc1 = await pdfDoc1.save(); - const savedDoc2 = await pdfDoc2.save(); - - expect(savedDoc1).toEqual(savedDoc2); - }); - - it(`does not serialize the same on save when not using a custom font name`, async () => { - const customFont = fs.readFileSync('assets/fonts/ubuntu/Ubuntu-B.ttf'); - const pdfDoc1 = await PDFDocument.create(); - const pdfDoc2 = await PDFDocument.create(); - - pdfDoc1.registerFontkit(fontkit); - pdfDoc2.registerFontkit(fontkit); - await pdfDoc1.embedFont(customFont); await pdfDoc2.embedFont(customFont); const savedDoc1 = await pdfDoc1.save(); const savedDoc2 = await pdfDoc2.save(); - expect(savedDoc1).not.toEqual(savedDoc2); + expect(savedDoc1).toEqual(savedDoc2); }); }); @@ -516,19 +498,21 @@ describe(`copy() method`, () => { srcDoc.setModificationDate(modificationDate); pdfDoc = await srcDoc.copy(); }); - + it(`Returns a pdf with the same number of pages`, async () => { expect(pdfDoc.getPageCount()).toBe(srcDoc.getPageCount()); }); - + it(`Can copy author, creationDate, creator, producer, subject, title, defaultWordBreaks`, async () => { expect(pdfDoc.getAuthor()).toBe(srcDoc.getAuthor()); expect(pdfDoc.getCreationDate()).toStrictEqual(srcDoc.getCreationDate()); expect(pdfDoc.getCreator()).toBe(srcDoc.getCreator()); - expect(pdfDoc.getModificationDate()).toStrictEqual(srcDoc.getModificationDate()); + expect(pdfDoc.getModificationDate()).toStrictEqual( + srcDoc.getModificationDate(), + ); expect(pdfDoc.getProducer()).toBe(srcDoc.getProducer()); expect(pdfDoc.getSubject()).toBe(srcDoc.getSubject()); expect(pdfDoc.getTitle()).toBe(srcDoc.getTitle()); expect(pdfDoc.defaultWordBreaks).toEqual(srcDoc.defaultWordBreaks); }); -}); \ No newline at end of file +}); diff --git a/tests/utils/rng.spec.ts b/tests/utils/rng.spec.ts new file mode 100644 index 000000000..d5310e105 --- /dev/null +++ b/tests/utils/rng.spec.ts @@ -0,0 +1,14 @@ +import { SimpleRNG } from 'src/utils/rng'; + +describe(`psuedo random numbers`, () => { + it(`generates distinct numbers`, () => { + const rng = SimpleRNG.withSeed(1); + expect(rng.nextInt()).not.toEqual(rng.nextInt()); + }); + + it(`generates the same number across different SimpleRNG`, () => { + const rng = SimpleRNG.withSeed(1); + expect(rng.nextInt()).toEqual(0.7098480789645691); + expect(rng.nextInt()).toEqual(0.9742682568175951); + }); +});