From 66a1992da0cd7f79bdb8f41af01563fece3d865d Mon Sep 17 00:00:00 2001 From: Dolan Miu Date: Fri, 3 Mar 2023 23:47:50 +0000 Subject: [PATCH] Add ImageRun feature --- demo/85-template-document.ts | 5 ++ demo/assets/simple-template.docx | Bin 16661 -> 16565 bytes src/export/packer/next-compiler.ts | 5 +- src/file/media/media.ts | 1 + src/patcher/content-types-manager.ts | 16 ++++++ src/patcher/from-docx.ts | 82 +++++++++++++++++++++++++-- src/patcher/relationship-manager.ts | 30 ++++++++++ src/patcher/replacer.ts | 9 ++- src/patcher/util.ts | 4 ++ 9 files changed, 141 insertions(+), 11 deletions(-) create mode 100644 src/patcher/content-types-manager.ts create mode 100644 src/patcher/relationship-manager.ts diff --git a/demo/85-template-document.ts b/demo/85-template-document.ts index f015dbe7cf..921a9befbb 100644 --- a/demo/85-template-document.ts +++ b/demo/85-template-document.ts @@ -3,6 +3,7 @@ import * as fs from "fs"; import { HeadingLevel, + ImageRun, Paragraph, patchDocument, PatchType, @@ -40,6 +41,10 @@ patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), { type: PatchType.PARAGRAPH, children: [new TextRun("replaced just as well")], }, + image_test: { + type: PatchType.PARAGRAPH, + children: [new ImageRun({ data: fs.readFileSync("./demo/images/image1.jpeg"), transformation: { width: 100, height: 100 } })], + }, table: { type: PatchType.DOCUMENT, children: [ diff --git a/demo/assets/simple-template.docx b/demo/assets/simple-template.docx index 83f972c48acaf56cb49f81a80939785c46e028c8..a3e71cdf6f1fb731a01ebae47ec08adcba662d77 100644 GIT binary patch delta 6473 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 } + // TODO: Unused public addMedia(data: Buffer | string | Uint8Array | ArrayBuffer, transformation: IMediaTransformation): IMediaData { const key = `${uniqueId()}.png`; diff --git a/src/patcher/content-types-manager.ts b/src/patcher/content-types-manager.ts new file mode 100644 index 0000000000..8032073bfc --- /dev/null +++ b/src/patcher/content-types-manager.ts @@ -0,0 +1,16 @@ +import { Element } from "xml-js"; + +import { getFirstLevelElements } from "./util"; + +export const appendContentType = (element: Element, contentType: string, extension: string): void => { + const relationshipElements = getFirstLevelElements(element, "Types"); + // eslint-disable-next-line functional/immutable-data + relationshipElements.push({ + attributes: { + ContentType: contentType, + Extension: extension, + }, + name: "Default", + type: "element", + }); +}; diff --git a/src/patcher/from-docx.ts b/src/patcher/from-docx.ts index 535336e70d..339ba964ec 100644 --- a/src/patcher/from-docx.ts +++ b/src/patcher/from-docx.ts @@ -3,10 +3,17 @@ import { Element, js2xml } from "xml-js"; import { ParagraphChild } from "@file/paragraph"; import { FileChild } from "@file/file-child"; +import { IMediaData, Media } from "@file/media"; +import { IViewWrapper } from "@file/document-wrapper"; +import { File } from "@file/file"; +import { IContext } from "@file/xml-components"; +import { ImageReplacer } from "@export/packer/image-replacer"; import { replacer } from "./replacer"; import { findLocationOfText } from "./traverser"; import { toJson } from "./util"; +import { appendRelationship, getNextRelationshipIndex } from "./relationship-manager"; +import { appendContentType } from "./content-types-manager"; // eslint-disable-next-line functional/prefer-readonly-type type InputDataType = Buffer | string | number[] | Uint8Array | ArrayBuffer | Blob | NodeJS.ReadableStream; @@ -26,31 +33,94 @@ type FilePatch = { readonly children: readonly FileChild[]; }; +interface IRelationshipReplacement { + readonly key: string; + readonly mediaDatas: readonly IMediaData[]; +} + export type IPatch = ParagraphPatch | FilePatch; export interface PatchDocumentOptions { readonly patches: { readonly [key: string]: IPatch }; } +const imageReplacer = new ImageReplacer(); + export const patchDocument = async (data: InputDataType, options: PatchDocumentOptions): Promise => { const zipContent = await JSZip.loadAsync(data); + const context: IContext = { + file: { + Media: new Media(), + } as unknown as File, + viewWrapper: {} as unknown as IViewWrapper, + stack: [], + }; + const map = new Map(); + // eslint-disable-next-line functional/prefer-readonly-type + const relationshipReplacement: IRelationshipReplacement[] = []; + let hasMedia = false; + for (const [key, value] of Object.entries(zipContent.files)) { const json = toJson(await value.async("text")); - if (key.startsWith("word/")) { + if (key.startsWith("word/") && !key.endsWith(".xml.rels")) { for (const [patchKey, patchValue] of Object.entries(options.patches)) { const patchText = `{{${patchKey}}}`; const renderedParagraphs = findLocationOfText(json, patchText); // TODO: mutates json. Make it immutable - replacer(json, patchValue, patchText, renderedParagraphs); + replacer(json, patchValue, patchText, renderedParagraphs, context); + } + + const mediaDatas = imageReplacer.getMediaData(JSON.stringify(json), context.file.Media); + if (mediaDatas.length > 0) { + hasMedia = true; + // eslint-disable-next-line functional/immutable-data + relationshipReplacement.push({ + key, + mediaDatas, + }); } } map.set(key, json); } + for (const { key, mediaDatas } of relationshipReplacement) { + // eslint-disable-next-line functional/immutable-data + const relationshipsJson = map.get(`word/_rels/${key.split("/").pop()}.rels`); + + if (relationshipsJson) { + const index = getNextRelationshipIndex(relationshipsJson); + const newJson = imageReplacer.replace(JSON.stringify(map.get(key)), mediaDatas, index); + map.set(key, JSON.parse(newJson) as Element); + + for (const { fileName } of mediaDatas) { + appendRelationship( + relationshipsJson, + index, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", + `media/${fileName}`, + ); + } + } + } + + if (hasMedia) { + const contentTypesJson = map.get("[Content_Types].xml"); + + if (!contentTypesJson) { + throw new Error("Could not find content types file"); + } + + appendContentType(contentTypesJson, "image/png", "png"); + appendContentType(contentTypesJson, "image/jpeg", "jpeg"); + appendContentType(contentTypesJson, "image/jpeg", "jpg"); + appendContentType(contentTypesJson, "image/bmp", "bmp"); + appendContentType(contentTypesJson, "image/gif", "gif"); + } + const zip = new JSZip(); for (const [key, value] of map) { @@ -59,13 +129,15 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO zip.file(key, output); } - const zipData = await zip.generateAsync({ + for (const { stream, fileName } of context.file.Media.Array) { + zip.file(`word/media/${fileName}`, stream); + } + + return zip.generateAsync({ type: "nodebuffer", mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", compression: "DEFLATE", }); - - return zipData; }; const toXml = (jsonObj: Element): string => { diff --git a/src/patcher/relationship-manager.ts b/src/patcher/relationship-manager.ts new file mode 100644 index 0000000000..30f3bd425e --- /dev/null +++ b/src/patcher/relationship-manager.ts @@ -0,0 +1,30 @@ +import { Element } from "xml-js"; + +import { RelationshipType } from "@file/relationships/relationship/relationship"; +import { getFirstLevelElements } from "./util"; + +const getIdFromRelationshipId = (relationshipId: string): number => parseInt(relationshipId.substring(3), 10); + +export const getNextRelationshipIndex = (relationships: Element): number => { + const relationshipElements = getFirstLevelElements(relationships, "Relationships"); + + return ( + (relationshipElements + .map((e) => getIdFromRelationshipId(e.attributes?.Id?.toString() ?? "")) + .reduce((acc, curr) => Math.max(acc, curr), 0) ?? 0) + 1 + ); +}; + +export const appendRelationship = (relationships: Element, id: number, type: RelationshipType, target: string): void => { + const relationshipElements = getFirstLevelElements(relationships, "Relationships"); + // eslint-disable-next-line functional/immutable-data + relationshipElements.push({ + attributes: { + Id: `rId${id}`, + Type: type, + Target: target, + }, + name: "Relationship", + type: "element", + }); +}; diff --git a/src/patcher/replacer.ts b/src/patcher/replacer.ts index 488b59760d..45861de8e7 100644 --- a/src/patcher/replacer.ts +++ b/src/patcher/replacer.ts @@ -2,7 +2,7 @@ import { Element } from "xml-js"; import * as xml from "xml"; import { Formatter } from "@export/formatter"; -import { XmlComponent } from "@file/xml-components"; +import { IContext, XmlComponent } from "@file/xml-components"; import { IPatch, PatchType } from "./from-docx"; import { toJson } from "./util"; @@ -19,9 +19,13 @@ export const replacer = ( patch: IPatch, patchText: string, renderedParagraphs: readonly IRenderedParagraphNode[], + context: IContext, ): Element => { for (const renderedParagraph of renderedParagraphs) { - const textJson = patch.children.map((c) => toJson(xml(formatter.format(c as XmlComponent)))).map((c) => c.elements![0]); + const textJson = patch.children + // eslint-disable-next-line no-loop-func + .map((c) => toJson(xml(formatter.format(c as XmlComponent, context)))) + .map((c) => c.elements![0]); if (patch.type === PatchType.DOCUMENT) { const parentElement = goToParentElementFromPath(json, renderedParagraph.path); @@ -30,7 +34,6 @@ export const replacer = ( parentElement.elements?.splice(elementIndex, 1, ...textJson); } else if (patch.type === PatchType.PARAGRAPH) { const paragraphElement = goToElementFromPath(json, renderedParagraph.path); - replaceTokenInParagraphElement({ paragraphElement, renderedParagraph, diff --git a/src/patcher/util.ts b/src/patcher/util.ts index bdb79eb0d0..6f9ed57dd0 100644 --- a/src/patcher/util.ts +++ b/src/patcher/util.ts @@ -24,3 +24,7 @@ export const patchSpaceAttribute = (element: Element): Element => ({ "xml:space": "preserve", }, }); + +// eslint-disable-next-line functional/prefer-readonly-type +export const getFirstLevelElements = (relationships: Element, id: string): Element[] => + relationships.elements?.filter((e) => e.name === id)[0].elements ?? [];