Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions demo/101-track-change-images.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Track revisions for inline images (ImageRun insertion / deletion wrappers).
// See docs/usage/change-tracking.md.

import * as fs from "fs";
import { AlignmentType, Document, ImageRun, Packer, Paragraph, TextRun } from "docx";

const REVISION_DATE = "2020-10-06T09:00:00Z";
const REVISION_AUTHOR = "Firstname Lastname";

const imagePath = "./demo/images/dog.png";

const doc = new Document({
features: {
trackRevisions: true,
},
sections: [
{
properties: {},
children: [
new Paragraph({
alignment: AlignmentType.CENTER,
children: [
new TextRun({
text: "Track changes: images",
bold: true,
size: 32,
}),
],
}),
new Paragraph({
children: [
new TextRun("Below: an image marked as inserted (w:ins wrapping the run), then an image marked as deleted."),
],
}),
new Paragraph({ text: "" }),
new Paragraph({
children: [
new TextRun({ text: "Inserted image: ", bold: true }),
new ImageRun({
type: "png",
data: fs.readFileSync(imagePath),
transformation: {
width: 120,
height: 120,
},
insertion: {
id: 30,
author: REVISION_AUTHOR,
date: REVISION_DATE,
},
}),
],
}),
new Paragraph({
children: [
new TextRun({ text: "Deleted image: ", bold: true }),
new ImageRun({
type: "png",
data: fs.readFileSync(imagePath),
transformation: {
width: 120,
height: 120,
},
deletion: {
id: 31,
author: REVISION_AUTHOR,
date: REVISION_DATE,
},
}),
],
}),
],
},
],
});

Packer.toBuffer(doc).then((buffer) => {
fs.writeFileSync("101-track-change-images.docx", buffer);
console.log("Document created successfully at 101-track-change-images.docx");
});
84 changes: 84 additions & 0 deletions src/file/paragraph/run/image-run.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1178,4 +1178,88 @@ describe("ImageRun", () => {
);
});
});

it("should wrap the run with w:ins when insertion revision is set", () => {
const base = new ImageRun({
type: "png",
data: Buffer.from(""),
transformation: { width: 100, height: 100 },
});
const withInsertion = new ImageRun({
type: "png",
data: Buffer.from(""),
transformation: { width: 100, height: 100 },
insertion: { id: 7, author: "Firstname Lastname", date: "2026-01-01T12:00:00Z" },
});

const context = {
file: {
Media: {
addImage: vi.fn(),
},
} as unknown as File,
viewWrapper: {} as unknown as IViewWrapper,
stack: [],
};

const baseTree = new Formatter().format(base, context);
const tree = new Formatter().format(withInsertion, context);

expect(tree).to.deep.equal({
"w:ins": [
{
_attr: {
"w:author": "Firstname Lastname",
"w:date": "2026-01-01T12:00:00Z",
"w:id": 7,
},
},
{
"w:r": baseTree["w:r"],
},
],
});
});

it("should wrap the run with w:del when deletion revision is set", () => {
const base = new ImageRun({
type: "png",
data: Buffer.from(""),
transformation: { width: 100, height: 100 },
});
const withDeletion = new ImageRun({
type: "png",
data: Buffer.from(""),
transformation: { width: 100, height: 100 },
deletion: { id: 8, author: "Firstname Lastname", date: "2026-01-01T12:00:00Z" },
});

const context = {
file: {
Media: {
addImage: vi.fn(),
},
} as unknown as File,
viewWrapper: {} as unknown as IViewWrapper,
stack: [],
};

const baseTree = new Formatter().format(base, context);
const tree = new Formatter().format(withDeletion, context);

expect(tree).to.deep.equal({
"w:del": [
{
_attr: {
"w:author": "Firstname Lastname",
"w:date": "2026-01-01T12:00:00Z",
"w:id": 8,
},
},
{
"w:r": baseTree["w:r"],
},
],
});
});
});
47 changes: 39 additions & 8 deletions src/file/paragraph/run/image-run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@
* @module
*/
import type { DocPropertiesOptions } from "@file/drawing/doc-properties/doc-properties";
import type { IContext, IXmlableObject } from "@file/xml-components";
import { ChangeAttributes, type IChangedAttributesProperties } from "@file/track-revision/track-revision";
import { type IContext, type IXmlableObject, XmlComponent } from "@file/xml-components";
import { hashedId } from "@util/convenience-functions";

import { RunProperties } from "./properties";
import { Run } from "./run";
import { Drawing, type IFloating } from "../../drawing";
import type { OutlineOptions } from "../../drawing/inline/graphic/graphic-data/pic/shape-properties/outline/outline";
import type { SolidFillOptions } from "../../drawing/inline/graphic/graphic-data/pic/shape-properties/outline/solid-fill";
import type { IMediaTransformation } from "../../media";
import type { IMediaData } from "../../media/data";
import { Run } from "../run";

/**
* Core options for image configuration.
Expand All @@ -27,6 +29,8 @@ type CoreImageOptions = {
readonly altText?: DocPropertiesOptions;
readonly outline?: OutlineOptions;
readonly solidFill?: SolidFillOptions;
readonly insertion?: IChangedAttributesProperties;
readonly deletion?: IChangedAttributesProperties;
};

type RegularImageOptions = {
Expand Down Expand Up @@ -107,16 +111,14 @@ const createImageData = (options: IImageOptions, key: string): Pick<IMediaData,
* });
* ```
*/
export class ImageRun extends Run {
export class ImageRun extends XmlComponent {
private readonly imageData: IMediaData;

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

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

this.imageData =
const imageData: IMediaData =
options.type === "svg"
? {
type: options.type,
Expand All @@ -136,13 +138,42 @@ export class ImageRun extends Run {
type: options.type,
...createImageData(options, key),
};
const drawing = new Drawing(this.imageData, {

const drawing = new Drawing(imageData, {
floating: options.floating,
docProperties: options.altText,
outline: options.outline,
});

this.root.push(drawing);
const run = new Run({ children: [drawing] });

if (options.insertion) {
super("w:ins");
this.root.push(
new ChangeAttributes({
id: options.insertion.id,
author: options.insertion.author,
date: options.insertion.date,
}),
);
this.addChildElement(run);
} else if (options.deletion) {
super("w:del");
this.root.push(
new ChangeAttributes({
id: options.deletion.id,
author: options.deletion.author,
date: options.deletion.date,
}),
);
this.addChildElement(run);
} else {
super("w:r");
this.root.push(new RunProperties({}));
this.root.push(drawing);
}

this.imageData = imageData;
}

public prepForXml(context: IContext): IXmlableObject | undefined {
Expand Down
Loading