Skip to content

Commit

Permalink
Change ImageRun keys to be based on image data content (#2681)
Browse files Browse the repository at this point in the history
Co-authored-by: Dolan <dolan_miu@hotmail.com>
  • Loading branch information
mustache1up and dolanmiu authored Oct 11, 2024
1 parent 021f1b0 commit e86dbd3
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 31 deletions.
9 changes: 3 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
78 changes: 62 additions & 16 deletions src/file/paragraph/run/image-run.spec.ts
Original file line number Diff line number Diff line change
@@ -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({
Expand Down Expand Up @@ -193,7 +184,8 @@ describe("ImageRun", () => {
"a:blip": {
_attr: {
cstate: "none",
"r:embed": "rId{test-unique-id.png}",
"r:embed":
"rId{da39a3ee5e6b4b0d3255bfef95601890afd80709.png}",
},
},
},
Expand Down Expand Up @@ -445,7 +437,8 @@ describe("ImageRun", () => {
"a:blip": {
_attr: {
cstate: "none",
"r:embed": "rId{test-unique-id.png}",
"r:embed":
"rId{da39a3ee5e6b4b0d3255bfef95601890afd80709.png}",
},
},
},
Expand Down Expand Up @@ -700,7 +693,8 @@ describe("ImageRun", () => {
"a:blip": {
_attr: {
cstate: "none",
"r:embed": "rId{test-unique-id.png}",
"r:embed":
"rId{da39a3ee5e6b4b0d3255bfef95601890afd80709.png}",
},
},
},
Expand Down Expand Up @@ -955,7 +949,8 @@ describe("ImageRun", () => {
"a:blip": {
_attr: {
cstate: "none",
"r:embed": "rId{test-unique-id.png}",
"r:embed":
"rId{da39a3ee5e6b4b0d3255bfef95601890afd80709.png}",
},
},
},
Expand Down Expand Up @@ -1090,7 +1085,8 @@ describe("ImageRun", () => {
{
_attr: {
cstate: "none",
"r:embed": "rId{test-unique-id.png}",
"r:embed":
"rId{da39a3ee5e6b4b0d3255bfef95601890afd80709.png}",
},
},
{
Expand All @@ -1106,7 +1102,7 @@ describe("ImageRun", () => {
"asvg:svgBlip": {
_attr: expect.objectContaining({
"r:embed":
"rId{test-unique-id.svg}",
"rId{da39a3ee5e6b4b0d3255bfef95601890afd80709.svg}",
}),
},
},
Expand All @@ -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` }),
);
});
});
});
17 changes: 8 additions & 9 deletions src/file/paragraph/run/image-run.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -76,34 +76,33 @@ const createImageData = (options: IImageOptions, key: string): Pick<IMediaData,
});

export class ImageRun extends Run {
private readonly key: string;
private readonly fallbackKey = `${uniqueId()}.png`;
private readonly imageData: IMediaData;

public constructor(options: IImageOptions) {
super({});

this.key = `${uniqueId()}.${options.type}`;
const hash = hashedId(options.data);
const key = `${hash}.${options.type}`;

this.imageData =
options.type === "svg"
? {
type: options.type,
...createImageData(options, this.key),
...createImageData(options, key),
fallback: {
type: options.fallback.type,
...createImageData(
{
...options.fallback,
transformation: options.transformation,
},
this.fallbackKey,
`${hashedId(options.fallback.data)}.${options.fallback.type}`,
),
},
}
: {
type: options.type,
...createImageData(options, this.key),
...createImageData(options, key),
};
const drawing = new Drawing(this.imageData, {
floating: options.floating,
Expand All @@ -115,10 +114,10 @@ export class ImageRun extends Run {
}

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") {
context.file.Media.addImage(this.fallbackKey, this.imageData.fallback);
context.file.Media.addImage(this.imageData.fallback.fileName, this.imageData.fallback);
}

return super.prepForXml(context);
Expand Down
21 changes: 21 additions & 0 deletions src/util/convenience-functions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
convertMillimetersToTwip,
docPropertiesUniqueNumericIdGen,
uniqueId,
hashedId,
uniqueNumericIdCreator,
uniqueUuid,
} from "./convenience-functions";
Expand Down Expand Up @@ -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", () => {
it("should generate a unique pseudorandom ID", () => {
expect(uniqueUuid()).to.not.be.empty;
Expand Down
7 changes: 7 additions & 0 deletions src/util/convenience-functions.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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)}`;

0 comments on commit e86dbd3

Please sign in to comment.