diff --git a/demo/101-track-change-images.ts b/demo/101-track-change-images.ts new file mode 100644 index 0000000000..9fba528bd2 --- /dev/null +++ b/demo/101-track-change-images.ts @@ -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"); +}); diff --git a/src/file/paragraph/run/image-run.spec.ts b/src/file/paragraph/run/image-run.spec.ts index bf9b0f3a77..3afd964b3d 100644 --- a/src/file/paragraph/run/image-run.spec.ts +++ b/src/file/paragraph/run/image-run.spec.ts @@ -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"], + }, + ], + }); + }); }); diff --git a/src/file/paragraph/run/image-run.ts b/src/file/paragraph/run/image-run.ts index 6aac2e989a..a077a206e2 100644 --- a/src/file/paragraph/run/image-run.ts +++ b/src/file/paragraph/run/image-run.ts @@ -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. @@ -27,6 +29,8 @@ type CoreImageOptions = { readonly altText?: DocPropertiesOptions; readonly outline?: OutlineOptions; readonly solidFill?: SolidFillOptions; + readonly insertion?: IChangedAttributesProperties; + readonly deletion?: IChangedAttributesProperties; }; type RegularImageOptions = { @@ -107,16 +111,14 @@ const createImageData = (options: IImageOptions, key: string): Pick