diff --git a/demo/demo35.ts b/demo/demo35.ts new file mode 100644 index 0000000000..a91ad2347d --- /dev/null +++ b/demo/demo35.ts @@ -0,0 +1,17 @@ +// Simple example to add text to a document +// Import from 'docx' rather than '../build' if you install from npm +import * as fs from "fs"; +import { Document, Packer, Paragraph, TextRun } from "../build"; + +var doc = new Document(); +var paragraph = new Paragraph(); +var link = doc.createHyperlink('http://www.example.com', 'Hyperlink'); +link.bold(); +paragraph.addHyperLink(link); +doc.addParagraph(paragraph); + +const packer = new Packer(); + +packer.toBuffer(doc).then((buffer) => { + fs.writeFileSync("My Document.docx", buffer); +}); diff --git a/demo/demo36.ts b/demo/demo36.ts new file mode 100644 index 0000000000..e9db47f25c --- /dev/null +++ b/demo/demo36.ts @@ -0,0 +1,23 @@ +// Add images to header and footer +// Import from 'docx' rather than '../build' if you install from npm +import * as fs from "fs"; +import { Document, Media, Packer, Table } from "../build"; + +const doc = new Document(); +const image = Media.addImage(doc, fs.readFileSync("./demo/images/image1.jpeg")); + +const table = new Table(2, 2); +table.getCell(1, 1).addContent(image.Paragraph); + +// doc.createParagraph("Hello World"); +doc.addTable(table); + +// doc.Header.createImage(fs.readFileSync("./demo/images/pizza.gif")); +doc.Header.addTable(table); +// doc.Footer.createImage(fs.readFileSync("./demo/images/pizza.gif")); + +const packer = new Packer(); + +packer.toBuffer(doc).then((buffer) => { + fs.writeFileSync("My Document.docx", buffer); +}); diff --git a/demo/demo37.ts b/demo/demo37.ts new file mode 100644 index 0000000000..0aaf380d51 --- /dev/null +++ b/demo/demo37.ts @@ -0,0 +1,18 @@ +// Add images to header and footer +// Import from 'docx' rather than '../build' if you install from npm +import * as fs from "fs"; +import { Document, Media, Packer } from "../build"; + +const doc = new Document(); +const image = Media.addImage(doc, fs.readFileSync("./demo/images/image1.jpeg")); +doc.createParagraph("Hello World"); + +doc.Header.addImage(image); +doc.Header.createImage(fs.readFileSync("./demo/images/pizza.gif")); +doc.Header.createImage(fs.readFileSync("./demo/images/image1.jpeg")); + +const packer = new Packer(); + +packer.toBuffer(doc).then((buffer) => { + fs.writeFileSync("My Document.docx", buffer); +}); diff --git a/src/export/packer/image-replacer.ts b/src/export/packer/image-replacer.ts new file mode 100644 index 0000000000..9cb3f950d5 --- /dev/null +++ b/src/export/packer/image-replacer.ts @@ -0,0 +1,17 @@ +import { IMediaData, Media } from "file/media"; + +export class ImageReplacer { + public replace(xmlData: string, mediaData: IMediaData[], offset: number): string { + let currentXmlData = xmlData; + + mediaData.forEach((image, i) => { + currentXmlData = currentXmlData.replace(`{${image.fileName}}`, (offset + i).toString()); + }); + + return currentXmlData; + } + + public getMediaData(xmlData: string, media: Media): IMediaData[] { + return media.Array.filter((image) => xmlData.search(`{${image.fileName}}`) > 0); + } +} diff --git a/src/export/packer/next-compiler.ts b/src/export/packer/next-compiler.ts index cfec78ff23..44af788cb0 100644 --- a/src/export/packer/next-compiler.ts +++ b/src/export/packer/next-compiler.ts @@ -3,6 +3,7 @@ import * as xml from "xml"; import { File } from "file"; import { Formatter } from "../formatter"; +import { ImageReplacer } from "./image-replacer"; interface IXmlifyedFile { readonly data: string; @@ -28,18 +29,15 @@ interface IXmlifyedFileMapping { export class Compiler { private readonly formatter: Formatter; + private readonly imageReplacer: ImageReplacer; constructor() { this.formatter = new Formatter(); + this.imageReplacer = new ImageReplacer(); } public async compile(file: File): Promise { const zip = new JSZip(); - - // Run precompile steps - file.onCompile(); - file.Headers.forEach((header) => header.onCompile()); - const xmlifiedFileMapping = this.xmlifyFile(file); for (const key in xmlifiedFileMapping) { @@ -63,26 +61,39 @@ export class Compiler { zip.file(`word/media/${data.fileName}`, mediaData); } - for (const header of file.Headers) { - for (const data of header.Media.Array) { - zip.file(`word/media/${data.fileName}`, data.stream); - } - } - - for (const footer of file.Footers) { - for (const data of footer.Media.Array) { - zip.file(`word/media/${data.fileName}`, data.stream); - } - } - return zip; } private xmlifyFile(file: File): IXmlifyedFileMapping { file.verifyUpdateFields(); + const documentRelationshipCount = file.DocumentRelationships.RelationshipCount + 1; + return { + Relationships: { + data: (() => { + const xmlData = xml(this.formatter.format(file.Document)); + const mediaDatas = this.imageReplacer.getMediaData(xmlData, file.Media); + + mediaDatas.forEach((mediaData, i) => { + file.DocumentRelationships.createRelationship( + documentRelationshipCount + i, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", + `media/${mediaData.fileName}`, + ); + }); + + return xml(this.formatter.format(file.DocumentRelationships)); + })(), + path: "word/_rels/document.xml.rels", + }, Document: { - data: xml(this.formatter.format(file.Document), true), + data: (() => { + const tempXmlData = xml(this.formatter.format(file.Document), true); + const mediaDatas = this.imageReplacer.getMediaData(tempXmlData, file.Media); + const xmlData = this.imageReplacer.replace(tempXmlData, mediaDatas, documentRelationshipCount); + + return xmlData; + })(), path: "word/document.xml", }, Styles: { @@ -102,30 +113,66 @@ export class Compiler { data: xml(this.formatter.format(file.Numbering)), path: "word/numbering.xml", }, - Relationships: { - data: xml(this.formatter.format(file.DocumentRelationships)), - path: "word/_rels/document.xml.rels", - }, FileRelationships: { data: xml(this.formatter.format(file.FileRelationships)), path: "_rels/.rels", }, - Headers: file.Headers.map((headerWrapper, index) => ({ - data: xml(this.formatter.format(headerWrapper.Header)), - path: `word/header${index + 1}.xml`, - })), - Footers: file.Footers.map((footerWrapper, index) => ({ - data: xml(this.formatter.format(footerWrapper.Footer)), - path: `word/footer${index + 1}.xml`, - })), - HeaderRelationships: file.Headers.map((headerWrapper, index) => ({ - data: xml(this.formatter.format(headerWrapper.Relationships)), - path: `word/_rels/header${index + 1}.xml.rels`, - })), - FooterRelationships: file.Footers.map((footerWrapper, index) => ({ - data: xml(this.formatter.format(footerWrapper.Relationships)), - path: `word/_rels/footer${index + 1}.xml.rels`, - })), + HeaderRelationships: file.Headers.map((headerWrapper, index) => { + const xmlData = xml(this.formatter.format(headerWrapper.Header)); + const mediaDatas = this.imageReplacer.getMediaData(xmlData, file.Media); + + mediaDatas.forEach((mediaData, i) => { + headerWrapper.Relationships.createRelationship( + i, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", + `media/${mediaData.fileName}`, + ); + }); + + return { + data: xml(this.formatter.format(headerWrapper.Relationships)), + path: `word/_rels/header${index + 1}.xml.rels`, + }; + }), + FooterRelationships: file.Footers.map((footerWrapper, index) => { + const xmlData = xml(this.formatter.format(footerWrapper.Footer)); + const mediaDatas = this.imageReplacer.getMediaData(xmlData, file.Media); + + mediaDatas.forEach((mediaData, i) => { + footerWrapper.Relationships.createRelationship( + i, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", + `media/${mediaData.fileName}`, + ); + }); + + return { + data: xml(this.formatter.format(footerWrapper.Relationships)), + path: `word/_rels/footer${index + 1}.xml.rels`, + }; + }), + Headers: file.Headers.map((headerWrapper, index) => { + const tempXmlData = xml(this.formatter.format(headerWrapper.Header)); + const mediaDatas = this.imageReplacer.getMediaData(tempXmlData, file.Media); + // TODO: 0 needs to be changed when headers get relationships of their own + const xmlData = this.imageReplacer.replace(tempXmlData, mediaDatas, 0); + + return { + data: xmlData, + path: `word/header${index + 1}.xml`, + }; + }), + Footers: file.Footers.map((footerWrapper, index) => { + const tempXmlData = xml(this.formatter.format(footerWrapper.Footer)); + const mediaDatas = this.imageReplacer.getMediaData(tempXmlData, file.Media); + // TODO: 0 needs to be changed when headers get relationships of their own + const xmlData = this.imageReplacer.replace(tempXmlData, mediaDatas, 0); + + return { + data: xmlData, + path: `word/footer${index + 1}.xml`, + }; + }), ContentTypes: { data: xml(this.formatter.format(file.ContentTypes)), path: "[Content_Types].xml", diff --git a/src/file/drawing/anchor/anchor.spec.ts b/src/file/drawing/anchor/anchor.spec.ts index 522fa5d228..44ea916a9a 100644 --- a/src/file/drawing/anchor/anchor.spec.ts +++ b/src/file/drawing/anchor/anchor.spec.ts @@ -9,7 +9,6 @@ import { Anchor } from "./anchor"; function createDrawing(drawingOptions: IDrawingOptions): Anchor { return new Anchor( { - referenceId: 1, fileName: "test.png", stream: new Buffer(""), dimensions: { diff --git a/src/file/drawing/drawing.spec.ts b/src/file/drawing/drawing.spec.ts index 6cf3761af8..ea47d74422 100644 --- a/src/file/drawing/drawing.spec.ts +++ b/src/file/drawing/drawing.spec.ts @@ -11,7 +11,6 @@ function createDrawing(drawingOptions?: IDrawingOptions): Drawing { return new Drawing( { fileName: "test.jpg", - referenceId: 1, stream: Buffer.from(imageBase64Data, "base64"), path: path, dimensions: { diff --git a/src/file/file.ts b/src/file/file.ts index 12a1849835..8ff70b9c54 100644 --- a/src/file/file.ts +++ b/src/file/file.ts @@ -13,7 +13,6 @@ import { IFileProperties } from "./file-properties"; import { FooterWrapper, IDocumentFooter } from "./footer-wrapper"; import { FootNotes } from "./footnotes"; import { HeaderWrapper, IDocumentHeader } from "./header-wrapper"; -import { IOnCompile } from "./life-cycles"; import { Image, Media } from "./media"; import { Numbering } from "./numbering"; import { Bookmark, Hyperlink, Paragraph } from "./paragraph"; @@ -26,7 +25,7 @@ import { DefaultStylesFactory } from "./styles/factory"; import { Table } from "./table"; import { TableOfContents } from "./table-of-contents"; -export class File implements IOnCompile { +export class File { // tslint:disable-next-line:readonly-keyword private currentRelationshipId: number = 1; @@ -223,11 +222,6 @@ export class File implements IOnCompile { } } - public onCompile(): void { - // this.media.Array.forEach((media) => { - // }); - } - private addHeaderToDocument(header: HeaderWrapper, type: HeaderReferenceType = HeaderReferenceType.DEFAULT): void { this.headers.push({ header, type }); this.docRelationships.createRelationship( diff --git a/src/file/footer-wrapper.ts b/src/file/footer-wrapper.ts index d56eecac92..6b55730f9f 100644 --- a/src/file/footer-wrapper.ts +++ b/src/file/footer-wrapper.ts @@ -2,7 +2,6 @@ import { XmlComponent } from "file/xml-components"; import { FooterReferenceType } from "./document"; import { Footer } from "./footer/footer"; -import { IOnCompile } from "./life-cycles"; import { Image, Media } from "./media"; import { ImageParagraph, Paragraph } from "./paragraph"; import { Relationships } from "./relationships"; @@ -13,7 +12,7 @@ export interface IDocumentFooter { readonly type: FooterReferenceType; } -export class FooterWrapper implements IOnCompile { +export class FooterWrapper { private readonly footer: Footer; private readonly relationships: Relationships; @@ -45,7 +44,7 @@ export class FooterWrapper implements IOnCompile { } public createImage(image: Buffer | string | Uint8Array | ArrayBuffer, width?: number, height?: number): void { - const mediaData = this.media.addMedia(image, this.relationships.RelationshipCount, width, height); + const mediaData = this.media.addMedia(image, width, height); this.addImage(new Image(new ImageParagraph(mediaData))); } @@ -54,16 +53,6 @@ export class FooterWrapper implements IOnCompile { return this; } - public onCompile(): void { - this.media.Array.forEach((mediaData) => { - this.relationships.createRelationship( - mediaData.referenceId, - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", - `media/${mediaData.fileName}`, - ); - }); - } - public get Footer(): Footer { return this.footer; } diff --git a/src/file/header-wrapper.ts b/src/file/header-wrapper.ts index 001245a924..878d23bc9e 100644 --- a/src/file/header-wrapper.ts +++ b/src/file/header-wrapper.ts @@ -2,7 +2,6 @@ import { XmlComponent } from "file/xml-components"; import { HeaderReferenceType } from "./document"; import { Header } from "./header/header"; -import { IOnCompile } from "./life-cycles"; import { Image, Media } from "./media"; import { ImageParagraph, Paragraph } from "./paragraph"; import { Relationships } from "./relationships"; @@ -13,7 +12,7 @@ export interface IDocumentHeader { readonly type: HeaderReferenceType; } -export class HeaderWrapper implements IOnCompile { +export class HeaderWrapper { private readonly header: Header; private readonly relationships: Relationships; @@ -45,7 +44,7 @@ export class HeaderWrapper implements IOnCompile { } public createImage(image: Buffer | string | Uint8Array | ArrayBuffer, width?: number, height?: number): void { - const mediaData = this.media.addMedia(image, this.relationships.RelationshipCount, width, height); + const mediaData = this.media.addMedia(image, width, height); this.addImage(new Image(new ImageParagraph(mediaData))); } @@ -54,16 +53,6 @@ export class HeaderWrapper implements IOnCompile { return this; } - public onCompile(): void { - this.media.Array.forEach((mediaData) => { - this.relationships.createRelationship( - mediaData.referenceId, - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", - `media/${mediaData.fileName}`, - ); - }); - } - public get Header(): Header { return this.header; } diff --git a/src/file/life-cycles.ts b/src/file/life-cycles.ts deleted file mode 100644 index ac38750a50..0000000000 --- a/src/file/life-cycles.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface IOnCompile { - readonly onCompile: () => void; -} - -// Needed because of: https://github.com/s-panferov/awesome-typescript-loader/issues/432 -/** - * @ignore - */ -export const WORKAROUND4 = ""; diff --git a/src/file/media/data.ts b/src/file/media/data.ts index ce8af83919..c49457b803 100644 --- a/src/file/media/data.ts +++ b/src/file/media/data.ts @@ -10,7 +10,6 @@ export interface IMediaDataDimensions { } export interface IMediaData { - readonly referenceId: number; readonly stream: Buffer | Uint8Array | ArrayBuffer; readonly path?: string; readonly fileName: string; diff --git a/src/file/media/media.spec.ts b/src/file/media/media.spec.ts index 3e833bc17d..04630ff5ed 100644 --- a/src/file/media/media.spec.ts +++ b/src/file/media/media.spec.ts @@ -1,5 +1,6 @@ // tslint:disable:object-literal-key-quotes import { expect } from "chai"; +import { stub } from "sinon"; import { Formatter } from "export/formatter"; @@ -20,16 +21,18 @@ describe("Media", () => { }); it("should ensure the correct relationship id is used when adding image", () => { + // tslint:disable-next-line:no-any + stub(Media as any, "generateId").callsFake(() => "testId"); + const file = new File(); const image1 = Media.addImage(file, "test"); - const tree = new Formatter().format(image1.Paragraph); const inlineElements = tree["w:p"][1]["w:r"][1]["w:drawing"][0]["wp:inline"]; const graphicData = inlineElements.find((x) => x["a:graphic"]); expect(graphicData["a:graphic"][1]["a:graphicData"][1]["pic:pic"][2]["pic:blipFill"][0]["a:blip"][0]).to.deep.equal({ _attr: { - "r:embed": `rId${file.DocumentRelationships.RelationshipCount}`, + "r:embed": `rId{testId.png}`, cstate: "none", }, }); @@ -41,7 +44,7 @@ describe("Media", () => { expect(graphicData2["a:graphic"][1]["a:graphicData"][1]["pic:pic"][2]["pic:blipFill"][0]["a:blip"][0]).to.deep.equal({ _attr: { - "r:embed": `rId${file.DocumentRelationships.RelationshipCount}`, + "r:embed": `rId{testId.png}`, cstate: "none", }, }); @@ -53,9 +56,8 @@ describe("Media", () => { // tslint:disable-next-line:no-any (Media as any).generateId = () => "test"; - const image = new Media().addMedia("", 1); + const image = new Media().addMedia(""); expect(image.fileName).to.equal("test.png"); - expect(image.referenceId).to.equal(1); expect(image.dimensions).to.deep.equal({ pixels: { x: 100, @@ -74,7 +76,7 @@ describe("Media", () => { // tslint:disable-next-line:no-any (Media as any).generateId = () => "test"; - const image = new Media().addMedia("", 1); + const image = new Media().addMedia(""); expect(image.stream).to.be.an.instanceof(Uint8Array); }); }); @@ -85,12 +87,11 @@ describe("Media", () => { (Media as any).generateId = () => "test"; const media = new Media(); - media.addMedia("", 1); + media.addMedia(""); const image = media.getMedia("test.png"); expect(image.fileName).to.equal("test.png"); - expect(image.referenceId).to.equal(1); expect(image.dimensions).to.deep.equal({ pixels: { x: 100, @@ -116,7 +117,7 @@ describe("Media", () => { (Media as any).generateId = () => "test"; const media = new Media(); - media.addMedia("", 1); + media.addMedia(""); const array = media.Array; expect(array).to.be.an.instanceof(Array); @@ -124,7 +125,6 @@ describe("Media", () => { const image = array[0]; expect(image.fileName).to.equal("test.png"); - expect(image.referenceId).to.equal(1); expect(image.dimensions).to.deep.equal({ pixels: { x: 100, diff --git a/src/file/media/media.ts b/src/file/media/media.ts index c1f67b3023..8a160291ca 100644 --- a/src/file/media/media.ts +++ b/src/file/media/media.ts @@ -4,11 +4,6 @@ import { ImageParagraph } from "../paragraph"; import { IMediaData } from "./data"; import { Image } from "./image"; -interface IHackedFile { - // tslint:disable-next-line:readonly-keyword - currentRelationshipId: number; -} - export class Media { public static addImage( file: File, @@ -18,14 +13,7 @@ export class Media { drawingOptions?: IDrawingOptions, ): Image { // Workaround to expose id without exposing to API - const exposedFile = (file as {}) as IHackedFile; - const mediaData = file.Media.addMedia(buffer, exposedFile.currentRelationshipId++, width, height); - file.DocumentRelationships.createRelationship( - mediaData.referenceId, - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", - `media/${mediaData.fileName}`, - ); - + const mediaData = file.Media.addMedia(buffer, width, height); return new Image(new ImageParagraph(mediaData, drawingOptions)); } @@ -57,17 +45,11 @@ export class Media { return data; } - public addMedia( - buffer: Buffer | string | Uint8Array | ArrayBuffer, - referenceId: number, - width: number = 100, - height: number = 100, - ): IMediaData { + public addMedia(buffer: Buffer | string | Uint8Array | ArrayBuffer, width: number = 100, height: number = 100): IMediaData { const key = `${Media.generateId()}.png`; return this.createMedia( key, - referenceId, { width: width, height: height, @@ -78,7 +60,6 @@ export class Media { private createMedia( key: string, - relationshipsCount: number, dimensions: { readonly width: number; readonly height: number }, data: Buffer | string | Uint8Array | ArrayBuffer, filePath?: string, @@ -86,7 +67,6 @@ export class Media { const newData = typeof data === "string" ? this.convertDataURIToBinary(data) : data; const imageData: IMediaData = { - referenceId: relationshipsCount, stream: newData, path: filePath, fileName: key, diff --git a/src/file/paragraph/image.spec.ts b/src/file/paragraph/image.spec.ts index c8289325e5..7dcb6ae148 100644 --- a/src/file/paragraph/image.spec.ts +++ b/src/file/paragraph/image.spec.ts @@ -10,10 +10,9 @@ describe("Image", () => { beforeEach(() => { image = new ImageParagraph({ - referenceId: 0, stream: new Buffer(""), path: "", - fileName: "", + fileName: "test.png", dimensions: { pixels: { x: 10, @@ -171,7 +170,7 @@ describe("Image", () => { { _attr: { cstate: "none", - "r:embed": "rId0", + "r:embed": "rId{test.png}", }, }, ], diff --git a/src/import-dotx/import-dotx.ts b/src/import-dotx/import-dotx.ts index 1f8c6077ec..fdfedc7304 100644 --- a/src/import-dotx/import-dotx.ts +++ b/src/import-dotx/import-dotx.ts @@ -166,10 +166,10 @@ export class ImportDotx { for (const r of wrapperImagesReferences) { const buffer = await zipContent.files[`word/${r.target}`].async("nodebuffer"); - const mediaData = media.addMedia(buffer, r.id); + const mediaData = media.addMedia(buffer); wrapper.Relationships.createRelationship( - mediaData.referenceId, + r.id, "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", `media/${mediaData.fileName}`, );