Change ImageRun keys to be based on image data content (#2681)
Co-authored-by: Dolan <dolan_miu@hotmail.com>
This commit is contained in:
9
package-lock.json
generated
9
package-lock.json
generated
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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` }),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
@ -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)}`;
|
||||||
|
Reference in New Issue
Block a user