Change ImageRun keys to be based on image data content (#2681)

Co-authored-by: Dolan <dolan_miu@hotmail.com>
This commit is contained in:
mustache1up
2024-10-10 21:13:51 -03:00
committed by GitHub
parent 021f1b0c4d
commit e86dbd3398
6 changed files with 102 additions and 31 deletions

9
package-lock.json generated
View File

@ -10,6 +10,7 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/node": "^22.7.5", "@types/node": "^22.7.5",
"hash.js": "^1.1.7",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"nanoid": "^5.0.4", "nanoid": "^5.0.4",
"xml": "^1.0.1", "xml": "^1.0.1",
@ -7513,7 +7514,6 @@
"version": "1.1.7", "version": "1.1.7",
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
"dev": true,
"dependencies": { "dependencies": {
"inherits": "^2.0.3", "inherits": "^2.0.3",
"minimalistic-assert": "^1.0.1" "minimalistic-assert": "^1.0.1"
@ -9252,8 +9252,7 @@
"node_modules/minimalistic-assert": { "node_modules/minimalistic-assert": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
"dev": true
}, },
"node_modules/minimalistic-crypto-utils": { "node_modules/minimalistic-crypto-utils": {
"version": "1.0.1", "version": "1.0.1",
@ -19116,7 +19115,6 @@
"version": "1.1.7", "version": "1.1.7",
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
"dev": true,
"requires": { "requires": {
"inherits": "^2.0.3", "inherits": "^2.0.3",
"minimalistic-assert": "^1.0.1" "minimalistic-assert": "^1.0.1"
@ -20402,8 +20400,7 @@
"minimalistic-assert": { "minimalistic-assert": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
"dev": true
}, },
"minimalistic-crypto-utils": { "minimalistic-crypto-utils": {
"version": "1.0.1", "version": "1.0.1",

View File

@ -55,6 +55,7 @@
], ],
"dependencies": { "dependencies": {
"@types/node": "^22.7.5", "@types/node": "^22.7.5",
"hash.js": "^1.1.7",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"nanoid": "^5.0.4", "nanoid": "^5.0.4",
"xml": "^1.0.1", "xml": "^1.0.1",

View File

@ -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 { Formatter } from "@export/formatter";
import { IViewWrapper } from "@file/document-wrapper"; import { IViewWrapper } from "@file/document-wrapper";
import { File } from "@file/file"; import { File } from "@file/file";
import * as convenienceFunctions from "@util/convenience-functions";
import { ImageRun } from "./image-run"; import { ImageRun } from "./image-run";
describe("ImageRun", () => { describe("ImageRun", () => {
beforeAll(() => {
vi.spyOn(convenienceFunctions, "uniqueId").mockReturnValue("test-unique-id");
});
afterAll(() => {
vi.resetAllMocks();
});
describe("#constructor()", () => { describe("#constructor()", () => {
it("should create with Buffer", () => { it("should create with Buffer", () => {
const currentImageRun = new ImageRun({ const currentImageRun = new ImageRun({
@ -193,7 +184,8 @@ describe("ImageRun", () => {
"a:blip": { "a:blip": {
_attr: { _attr: {
cstate: "none", cstate: "none",
"r:embed": "rId{test-unique-id.png}", "r:embed":
"rId{da39a3ee5e6b4b0d3255bfef95601890afd80709.png}",
}, },
}, },
}, },
@ -445,7 +437,8 @@ describe("ImageRun", () => {
"a:blip": { "a:blip": {
_attr: { _attr: {
cstate: "none", cstate: "none",
"r:embed": "rId{test-unique-id.png}", "r:embed":
"rId{da39a3ee5e6b4b0d3255bfef95601890afd80709.png}",
}, },
}, },
}, },
@ -700,7 +693,8 @@ describe("ImageRun", () => {
"a:blip": { "a:blip": {
_attr: { _attr: {
cstate: "none", cstate: "none",
"r:embed": "rId{test-unique-id.png}", "r:embed":
"rId{da39a3ee5e6b4b0d3255bfef95601890afd80709.png}",
}, },
}, },
}, },
@ -955,7 +949,8 @@ describe("ImageRun", () => {
"a:blip": { "a:blip": {
_attr: { _attr: {
cstate: "none", cstate: "none",
"r:embed": "rId{test-unique-id.png}", "r:embed":
"rId{da39a3ee5e6b4b0d3255bfef95601890afd80709.png}",
}, },
}, },
}, },
@ -1090,7 +1085,8 @@ describe("ImageRun", () => {
{ {
_attr: { _attr: {
cstate: "none", cstate: "none",
"r:embed": "rId{test-unique-id.png}", "r:embed":
"rId{da39a3ee5e6b4b0d3255bfef95601890afd80709.png}",
}, },
}, },
{ {
@ -1106,7 +1102,7 @@ describe("ImageRun", () => {
"asvg:svgBlip": { "asvg:svgBlip": {
_attr: expect.objectContaining({ _attr: expect.objectContaining({
"r:embed": "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` }),
);
});
}); });
}); });

View File

@ -1,4 +1,4 @@
import { uniqueId } from "@util/convenience-functions"; import { hashedId } from "@util/convenience-functions";
import { IContext, IXmlableObject } from "@file/xml-components"; import { IContext, IXmlableObject } from "@file/xml-components";
import { DocPropertiesOptions } from "@file/drawing/doc-properties/doc-properties"; import { DocPropertiesOptions } from "@file/drawing/doc-properties/doc-properties";
@ -76,20 +76,19 @@ const createImageData = (options: IImageOptions, key: string): Pick<IMediaData,
}); });
export class ImageRun extends Run { export class ImageRun extends Run {
private readonly key: string;
private readonly fallbackKey = `${uniqueId()}.png`;
private readonly imageData: IMediaData; private readonly imageData: IMediaData;
public constructor(options: IImageOptions) { public constructor(options: IImageOptions) {
super({}); super({});
this.key = `${uniqueId()}.${options.type}`; const hash = hashedId(options.data);
const key = `${hash}.${options.type}`;
this.imageData = this.imageData =
options.type === "svg" options.type === "svg"
? { ? {
type: options.type, type: options.type,
...createImageData(options, this.key), ...createImageData(options, key),
fallback: { fallback: {
type: options.fallback.type, type: options.fallback.type,
...createImageData( ...createImageData(
@ -97,13 +96,13 @@ export class ImageRun extends Run {
...options.fallback, ...options.fallback,
transformation: options.transformation, transformation: options.transformation,
}, },
this.fallbackKey, `${hashedId(options.fallback.data)}.${options.fallback.type}`,
), ),
}, },
} }
: { : {
type: options.type, type: options.type,
...createImageData(options, this.key), ...createImageData(options, key),
}; };
const drawing = new Drawing(this.imageData, { const drawing = new Drawing(this.imageData, {
floating: options.floating, floating: options.floating,
@ -115,10 +114,10 @@ export class ImageRun extends Run {
} }
public prepForXml(context: IContext): IXmlableObject | undefined { public prepForXml(context: IContext): IXmlableObject | undefined {
context.file.Media.addImage(this.key, this.imageData); context.file.Media.addImage(this.imageData.fileName, this.imageData);
if (this.imageData.type === "svg") { if (this.imageData.type === "svg") {
context.file.Media.addImage(this.fallbackKey, this.imageData.fallback); context.file.Media.addImage(this.imageData.fallback.fileName, this.imageData.fallback);
} }
return super.prepForXml(context); return super.prepForXml(context);

View File

@ -8,6 +8,7 @@ import {
convertMillimetersToTwip, convertMillimetersToTwip,
docPropertiesUniqueNumericIdGen, docPropertiesUniqueNumericIdGen,
uniqueId, uniqueId,
hashedId,
uniqueNumericIdCreator, uniqueNumericIdCreator,
uniqueUuid, uniqueUuid,
} from "./convenience-functions"; } from "./convenience-functions";
@ -72,6 +73,26 @@ describe("Utility", () => {
}); });
}); });
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", () => { describe("#uniqueUuid", () => {
it("should generate a unique pseudorandom ID", () => { it("should generate a unique pseudorandom ID", () => {
expect(uniqueUuid()).to.not.be.empty; expect(uniqueUuid()).to.not.be.empty;

View File

@ -1,4 +1,5 @@
import { nanoid, customAlphabet } from "nanoid/non-secure"; import { nanoid, customAlphabet } from "nanoid/non-secure";
import hash from "hash.js";
// Twip - twentieths of a point // Twip - twentieths of a point
export const convertMillimetersToTwip = (millimeters: number): number => Math.floor((millimeters / 25.4) * 72 * 20); 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 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)(); const generateUuidPart = (count: number): string => customAlphabet("1234567890abcdef", count)();
export const uniqueUuid = (): string => export const uniqueUuid = (): string =>
`${generateUuidPart(8)}-${generateUuidPart(4)}-${generateUuidPart(4)}-${generateUuidPart(4)}-${generateUuidPart(12)}`; `${generateUuidPart(8)}-${generateUuidPart(4)}-${generateUuidPart(4)}-${generateUuidPart(4)}-${generateUuidPart(12)}`;