From e86dbd3398069f70574e5b783a2204b431ddc562 Mon Sep 17 00:00:00 2001 From: mustache1up Date: Thu, 10 Oct 2024 21:13:51 -0300 Subject: [PATCH] Change ImageRun keys to be based on image data content (#2681) Co-authored-by: Dolan --- package-lock.json | 9 +-- package.json | 1 + src/file/paragraph/run/image-run.spec.ts | 78 +++++++++++++++++++----- src/file/paragraph/run/image-run.ts | 17 +++--- src/util/convenience-functions.spec.ts | 21 +++++++ src/util/convenience-functions.ts | 7 +++ 6 files changed, 102 insertions(+), 31 deletions(-) diff --git a/package-lock.json b/package-lock.json index b0e1dfabb2..bdceb2efc7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "@types/node": "^22.7.5", + "hash.js": "^1.1.7", "jszip": "^3.10.1", "nanoid": "^5.0.4", "xml": "^1.0.1", @@ -7513,7 +7514,6 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dev": true, "dependencies": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" @@ -9252,8 +9252,7 @@ "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" }, "node_modules/minimalistic-crypto-utils": { "version": "1.0.1", @@ -19116,7 +19115,6 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dev": true, "requires": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" @@ -20402,8 +20400,7 @@ "minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" }, "minimalistic-crypto-utils": { "version": "1.0.1", diff --git a/package.json b/package.json index a4a00a126f..68786f7af0 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ ], "dependencies": { "@types/node": "^22.7.5", + "hash.js": "^1.1.7", "jszip": "^3.10.1", "nanoid": "^5.0.4", "xml": "^1.0.1", diff --git a/src/file/paragraph/run/image-run.spec.ts b/src/file/paragraph/run/image-run.spec.ts index f0fb8161f1..13fbf6bc8f 100644 --- a/src/file/paragraph/run/image-run.spec.ts +++ b/src/file/paragraph/run/image-run.spec.ts @@ -1,21 +1,12 @@ -import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { Formatter } from "@export/formatter"; import { IViewWrapper } from "@file/document-wrapper"; import { File } from "@file/file"; -import * as convenienceFunctions from "@util/convenience-functions"; import { ImageRun } from "./image-run"; describe("ImageRun", () => { - beforeAll(() => { - vi.spyOn(convenienceFunctions, "uniqueId").mockReturnValue("test-unique-id"); - }); - - afterAll(() => { - vi.resetAllMocks(); - }); - describe("#constructor()", () => { it("should create with Buffer", () => { const currentImageRun = new ImageRun({ @@ -193,7 +184,8 @@ describe("ImageRun", () => { "a:blip": { _attr: { cstate: "none", - "r:embed": "rId{test-unique-id.png}", + "r:embed": + "rId{da39a3ee5e6b4b0d3255bfef95601890afd80709.png}", }, }, }, @@ -445,7 +437,8 @@ describe("ImageRun", () => { "a:blip": { _attr: { cstate: "none", - "r:embed": "rId{test-unique-id.png}", + "r:embed": + "rId{da39a3ee5e6b4b0d3255bfef95601890afd80709.png}", }, }, }, @@ -700,7 +693,8 @@ describe("ImageRun", () => { "a:blip": { _attr: { cstate: "none", - "r:embed": "rId{test-unique-id.png}", + "r:embed": + "rId{da39a3ee5e6b4b0d3255bfef95601890afd80709.png}", }, }, }, @@ -955,7 +949,8 @@ describe("ImageRun", () => { "a:blip": { _attr: { cstate: "none", - "r:embed": "rId{test-unique-id.png}", + "r:embed": + "rId{da39a3ee5e6b4b0d3255bfef95601890afd80709.png}", }, }, }, @@ -1090,7 +1085,8 @@ describe("ImageRun", () => { { _attr: { cstate: "none", - "r:embed": "rId{test-unique-id.png}", + "r:embed": + "rId{da39a3ee5e6b4b0d3255bfef95601890afd80709.png}", }, }, { @@ -1106,7 +1102,7 @@ describe("ImageRun", () => { "asvg:svgBlip": { _attr: expect.objectContaining({ "r:embed": - "rId{test-unique-id.svg}", + "rId{da39a3ee5e6b4b0d3255bfef95601890afd80709.svg}", }), }, }, @@ -1131,5 +1127,55 @@ describe("ImageRun", () => { ], }); }); + + it("using same data twice should use same media key", () => { + const imageRunStringData = new ImageRun({ + type: "png", + data: "DATA", + transformation: { + width: 100, + height: 100, + rotation: 42, + }, + }); + + const imageRunBufferData = new ImageRun({ + type: "png", + data: Buffer.from("DATA"), + transformation: { + width: 200, + height: 200, + rotation: 45, + }, + }); + + const addImageSpy = vi.fn(); + const context = { + file: { + Media: { + addImage: addImageSpy, + }, + } as unknown as File, + viewWrapper: {} as unknown as IViewWrapper, + stack: [], + }; + + new Formatter().format(imageRunStringData, context); + new Formatter().format(imageRunBufferData, context); + + const expectedHash = "580393f5a94fb469585f5dd2a6859a4aab899f37"; + + expect(addImageSpy).toHaveBeenCalledTimes(2); + expect(addImageSpy).toHaveBeenNthCalledWith( + 1, + `${expectedHash}.png`, + expect.objectContaining({ fileName: `${expectedHash}.png` }), + ); + expect(addImageSpy).toHaveBeenNthCalledWith( + 2, + `${expectedHash}.png`, + expect.objectContaining({ fileName: `${expectedHash}.png` }), + ); + }); }); }); diff --git a/src/file/paragraph/run/image-run.ts b/src/file/paragraph/run/image-run.ts index b2dfa1280e..b73d5ee874 100644 --- a/src/file/paragraph/run/image-run.ts +++ b/src/file/paragraph/run/image-run.ts @@ -1,4 +1,4 @@ -import { uniqueId } from "@util/convenience-functions"; +import { hashedId } from "@util/convenience-functions"; import { IContext, IXmlableObject } from "@file/xml-components"; import { DocPropertiesOptions } from "@file/drawing/doc-properties/doc-properties"; @@ -76,20 +76,19 @@ const createImageData = (options: IImageOptions, key: string): Pick { }); }); + describe("#hashedId", () => { + it("should generate a hex string", () => { + expect(hashedId("")).to.equal("da39a3ee5e6b4b0d3255bfef95601890afd80709"); + }); + + it("should work with string, Uint8Array, Buffer and ArrayBuffer", () => { + const stringInput = "DATA"; + const uint8ArrayInput = new Uint8Array(new TextEncoder().encode(stringInput)); + const bufferInput = Buffer.from(uint8ArrayInput); + const arrayBufferInput = uint8ArrayInput.buffer; + + const expectedHash = "580393f5a94fb469585f5dd2a6859a4aab899f37"; + + expect(hashedId(stringInput)).to.equal(expectedHash); + expect(hashedId(uint8ArrayInput)).to.equal(expectedHash); + expect(hashedId(bufferInput)).to.equal(expectedHash); + expect(hashedId(arrayBufferInput)).to.equal(expectedHash); + }); + }); + describe("#uniqueUuid", () => { it("should generate a unique pseudorandom ID", () => { expect(uniqueUuid()).to.not.be.empty; diff --git a/src/util/convenience-functions.ts b/src/util/convenience-functions.ts index bf3c5774a3..7e9a69bf90 100644 --- a/src/util/convenience-functions.ts +++ b/src/util/convenience-functions.ts @@ -1,4 +1,5 @@ import { nanoid, customAlphabet } from "nanoid/non-secure"; +import hash from "hash.js"; // Twip - twentieths of a point export const convertMillimetersToTwip = (millimeters: number): number => Math.floor((millimeters / 25.4) * 72 * 20); @@ -24,6 +25,12 @@ export const bookmarkUniqueNumericIdGen = (): UniqueNumericIdCreator => uniqueNu export const uniqueId = (): string => nanoid().toLowerCase(); +export const hashedId = (data: Buffer | string | Uint8Array | ArrayBuffer): string => + hash + .sha1() + .update(data instanceof ArrayBuffer ? new Uint8Array(data) : data) + .digest("hex"); + const generateUuidPart = (count: number): string => customAlphabet("1234567890abcdef", count)(); export const uniqueUuid = (): string => `${generateUuidPart(8)}-${generateUuidPart(4)}-${generateUuidPart(4)}-${generateUuidPart(4)}-${generateUuidPart(12)}`;