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 d311cf310c..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,14 +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(); - const xmlifiedFileMapping = this.xmlifyFile(file); for (const key in xmlifiedFileMapping) { @@ -59,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: { @@ -98,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 a42d914243..44ea916a9a 100644 --- a/src/file/drawing/anchor/anchor.spec.ts +++ b/src/file/drawing/anchor/anchor.spec.ts @@ -8,7 +8,20 @@ import { Anchor } from "./anchor"; function createDrawing(drawingOptions: IDrawingOptions): Anchor { return new Anchor( - 1, + { + fileName: "test.png", + stream: new Buffer(""), + dimensions: { + pixels: { + x: 0, + y: 0, + }, + emus: { + x: 0, + y: 0, + }, + }, + }, { pixels: { x: 100, diff --git a/src/file/drawing/anchor/anchor.ts b/src/file/drawing/anchor/anchor.ts index d76a11402c..a000e9b311 100644 --- a/src/file/drawing/anchor/anchor.ts +++ b/src/file/drawing/anchor/anchor.ts @@ -1,5 +1,5 @@ // http://officeopenxml.com/drwPicFloating.php -import { IMediaDataDimensions } from "file/media"; +import { IMediaData, IMediaDataDimensions } from "file/media"; import { XmlComponent } from "file/xml-components"; import { IDrawingOptions } from "../drawing"; import { @@ -34,7 +34,7 @@ const defaultOptions: IFloating = { }; export class Anchor extends XmlComponent { - constructor(referenceId: number, dimensions: IMediaDataDimensions, drawingOptions: IDrawingOptions) { + constructor(mediaData: IMediaData, dimensions: IMediaDataDimensions, drawingOptions: IDrawingOptions) { super("wp:anchor"); const floating = { @@ -83,6 +83,6 @@ export class Anchor extends XmlComponent { this.root.push(new DocProperties()); this.root.push(new GraphicFrameProperties()); - this.root.push(new Graphic(referenceId, dimensions.emus.x, dimensions.emus.y)); + this.root.push(new Graphic(mediaData, dimensions.emus.x, dimensions.emus.y)); } } 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/drawing/drawing.ts b/src/file/drawing/drawing.ts index d1087462fb..e2786cd8e9 100644 --- a/src/file/drawing/drawing.ts +++ b/src/file/drawing/drawing.ts @@ -33,20 +33,16 @@ export class Drawing extends XmlComponent { constructor(imageData: IMediaData, drawingOptions?: IDrawingOptions) { super("w:drawing"); - if (imageData === undefined) { - throw new Error("imageData cannot be undefined"); - } - const mergedOptions = { ...defaultDrawingOptions, ...drawingOptions, }; if (mergedOptions.position === PlacementPosition.INLINE) { - this.inline = new Inline(imageData.referenceId, imageData.dimensions); + this.inline = new Inline(imageData, imageData.dimensions); this.root.push(this.inline); } else if (mergedOptions.position === PlacementPosition.FLOATING) { - this.root.push(new Anchor(imageData.referenceId, imageData.dimensions, mergedOptions)); + this.root.push(new Anchor(imageData, imageData.dimensions, mergedOptions)); } } diff --git a/src/file/drawing/inline/graphic/graphic-data/graphic-data.ts b/src/file/drawing/inline/graphic/graphic-data/graphic-data.ts index 78606fd399..ce04f68179 100644 --- a/src/file/drawing/inline/graphic/graphic-data/graphic-data.ts +++ b/src/file/drawing/inline/graphic/graphic-data/graphic-data.ts @@ -1,11 +1,13 @@ +import { IMediaData } from "file/media"; import { XmlComponent } from "file/xml-components"; + import { GraphicDataAttributes } from "./graphic-data-attribute"; import { Pic } from "./pic"; export class GraphicData extends XmlComponent { private readonly pic: Pic; - constructor(referenceId: number, x: number, y: number) { + constructor(mediaData: IMediaData, x: number, y: number) { super("a:graphicData"); this.root.push( @@ -14,7 +16,7 @@ export class GraphicData extends XmlComponent { }), ); - this.pic = new Pic(referenceId, x, y); + this.pic = new Pic(mediaData, x, y); this.root.push(this.pic); } diff --git a/src/file/drawing/inline/graphic/graphic-data/pic/blip/blip-fill.ts b/src/file/drawing/inline/graphic/graphic-data/pic/blip/blip-fill.ts index c4630ddee3..ea0ee27023 100644 --- a/src/file/drawing/inline/graphic/graphic-data/pic/blip/blip-fill.ts +++ b/src/file/drawing/inline/graphic/graphic-data/pic/blip/blip-fill.ts @@ -1,12 +1,15 @@ +import { IMediaData } from "file/media"; import { XmlComponent } from "file/xml-components"; + import { Blip } from "./blip"; import { SourceRectangle } from "./source-rectangle"; import { Stretch } from "./stretch"; export class BlipFill extends XmlComponent { - constructor(referenceId: number) { + constructor(mediaData: IMediaData) { super("pic:blipFill"); - this.root.push(new Blip(referenceId)); + + this.root.push(new Blip(mediaData)); this.root.push(new SourceRectangle()); this.root.push(new Stretch()); } diff --git a/src/file/drawing/inline/graphic/graphic-data/pic/blip/blip.ts b/src/file/drawing/inline/graphic/graphic-data/pic/blip/blip.ts index bd279b2fae..f45b4d8316 100644 --- a/src/file/drawing/inline/graphic/graphic-data/pic/blip/blip.ts +++ b/src/file/drawing/inline/graphic/graphic-data/pic/blip/blip.ts @@ -1,3 +1,4 @@ +import { IMediaData } from "file/media"; import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; interface IBlipProperties { @@ -13,11 +14,11 @@ class BlipAttributes extends XmlAttributeComponent { } export class Blip extends XmlComponent { - constructor(referenceId: number) { + constructor(mediaData: IMediaData) { super("a:blip"); this.root.push( new BlipAttributes({ - embed: `rId${referenceId}`, + embed: `rId{${mediaData.fileName}}`, cstate: "none", }), ); diff --git a/src/file/drawing/inline/graphic/graphic-data/pic/pic.ts b/src/file/drawing/inline/graphic/graphic-data/pic/pic.ts index 14ccf63f02..cce8116677 100644 --- a/src/file/drawing/inline/graphic/graphic-data/pic/pic.ts +++ b/src/file/drawing/inline/graphic/graphic-data/pic/pic.ts @@ -1,5 +1,7 @@ // http://officeopenxml.com/drwPic.php +import { IMediaData } from "file/media"; import { XmlComponent } from "file/xml-components"; + import { BlipFill } from "./blip/blip-fill"; import { NonVisualPicProperties } from "./non-visual-pic-properties/non-visual-pic-properties"; import { PicAttributes } from "./pic-attributes"; @@ -8,7 +10,7 @@ import { ShapeProperties } from "./shape-properties/shape-properties"; export class Pic extends XmlComponent { private readonly shapeProperties: ShapeProperties; - constructor(referenceId: number, x: number, y: number) { + constructor(mediaData: IMediaData, x: number, y: number) { super("pic:pic"); this.root.push( @@ -20,7 +22,7 @@ export class Pic extends XmlComponent { this.shapeProperties = new ShapeProperties(x, y); this.root.push(new NonVisualPicProperties()); - this.root.push(new BlipFill(referenceId)); + this.root.push(new BlipFill(mediaData)); this.root.push(new ShapeProperties(x, y)); } diff --git a/src/file/drawing/inline/graphic/graphic.ts b/src/file/drawing/inline/graphic/graphic.ts index 0228b78dfb..52d11d8e42 100644 --- a/src/file/drawing/inline/graphic/graphic.ts +++ b/src/file/drawing/inline/graphic/graphic.ts @@ -1,4 +1,6 @@ +import { IMediaData } from "file/media"; import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; + import { GraphicData } from "./graphic-data"; interface IGraphicProperties { @@ -14,7 +16,7 @@ class GraphicAttributes extends XmlAttributeComponent { export class Graphic extends XmlComponent { private readonly data: GraphicData; - constructor(referenceId: number, x: number, y: number) { + constructor(mediaData: IMediaData, x: number, y: number) { super("a:graphic"); this.root.push( new GraphicAttributes({ @@ -22,7 +24,7 @@ export class Graphic extends XmlComponent { }), ); - this.data = new GraphicData(referenceId, x, y); + this.data = new GraphicData(mediaData, x, y); this.root.push(this.data); } diff --git a/src/file/drawing/inline/inline.ts b/src/file/drawing/inline/inline.ts index f36dd19cf3..7c40e7c3b3 100644 --- a/src/file/drawing/inline/inline.ts +++ b/src/file/drawing/inline/inline.ts @@ -1,5 +1,5 @@ // http://officeopenxml.com/drwPicInline.php -import { IMediaDataDimensions } from "file/media"; +import { IMediaData, IMediaDataDimensions } from "file/media"; import { XmlComponent } from "file/xml-components"; import { DocProperties } from "./../doc-properties/doc-properties"; import { EffectExtent } from "./../effect-extent/effect-extent"; @@ -12,7 +12,7 @@ export class Inline extends XmlComponent { private readonly extent: Extent; private readonly graphic: Graphic; - constructor(referenceId: number, private readonly dimensions: IMediaDataDimensions) { + constructor(readonly mediaData: IMediaData, private readonly dimensions: IMediaDataDimensions) { super("wp:inline"); this.root.push( @@ -25,7 +25,7 @@ export class Inline extends XmlComponent { ); this.extent = new Extent(dimensions.emus.x, dimensions.emus.y); - this.graphic = new Graphic(referenceId, dimensions.emus.x, dimensions.emus.y); + this.graphic = new Graphic(mediaData, dimensions.emus.x, dimensions.emus.y); this.root.push(this.extent); this.root.push(new EffectExtent()); diff --git a/src/file/file.spec.ts b/src/file/file.spec.ts index 47a1a15d27..e69c7418c7 100644 --- a/src/file/file.spec.ts +++ b/src/file/file.spec.ts @@ -1,8 +1,11 @@ import { expect } from "chai"; +import * as sinon from "sinon"; import { Formatter } from "export/formatter"; import { File } from "./file"; +import { Paragraph } from "./paragraph"; +import { Table } from "./table"; describe("File", () => { describe("#constructor", () => { @@ -55,4 +58,24 @@ describe("File", () => { expect(tree["w:body"][1]["w:sectPr"][9]["w:footerReference"][0]._attr["w:type"]).to.equal("even"); }); }); + + describe("#addParagraph", () => { + it("should call the underlying header's addParagraph", () => { + const file = new File(); + const spy = sinon.spy(file.Document, "addParagraph"); + file.addParagraph(new Paragraph()); + + expect(spy.called).to.equal(true); + }); + }); + + describe("#addTable", () => { + it("should call the underlying header's addParagraph", () => { + const wrapper = new File(); + const spy = sinon.spy(wrapper.Document, "addTable"); + wrapper.addTable(new Table(1, 1)); + + expect(spy.called).to.equal(true); + }); + }); }); diff --git a/src/file/file.ts b/src/file/file.ts index 8958c4ccd3..8ff70b9c54 100644 --- a/src/file/file.ts +++ b/src/file/file.ts @@ -17,6 +17,7 @@ import { Image, Media } from "./media"; import { Numbering } from "./numbering"; import { Bookmark, Hyperlink, Paragraph } from "./paragraph"; import { Relationships } from "./relationships"; +import { TargetModeType } from "./relationships/relationship/relationship"; import { Settings } from "./settings"; import { Styles } from "./styles"; import { ExternalStylesFactory } from "./styles/external-styles-factory"; @@ -147,7 +148,7 @@ export class File { hyperlink.linkId, "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink", link, - "External", + TargetModeType.EXTERNAL, ); return hyperlink; } diff --git a/src/file/footer-wrapper.spec.ts b/src/file/footer-wrapper.spec.ts new file mode 100644 index 0000000000..ccea54cc6c --- /dev/null +++ b/src/file/footer-wrapper.spec.ts @@ -0,0 +1,29 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; + +import { FooterWrapper } from "./footer-wrapper"; +import { Media } from "./media"; +import { Paragraph } from "./paragraph"; +import { Table } from "./table"; + +describe("FooterWrapper", () => { + describe("#addParagraph", () => { + it("should call the underlying header's addParagraph", () => { + const file = new FooterWrapper(new Media(), 1); + const spy = sinon.spy(file.Footer, "addParagraph"); + file.addParagraph(new Paragraph()); + + expect(spy.called).to.equal(true); + }); + }); + + describe("#addTable", () => { + it("should call the underlying header's addParagraph", () => { + const wrapper = new FooterWrapper(new Media(), 1); + const spy = sinon.spy(wrapper.Footer, "addTable"); + wrapper.addTable(new Table(1, 1)); + + expect(spy.called).to.equal(true); + }); + }); +}); diff --git a/src/file/footer-wrapper.ts b/src/file/footer-wrapper.ts index f9fecf6a50..6b55730f9f 100644 --- a/src/file/footer-wrapper.ts +++ b/src/file/footer-wrapper.ts @@ -2,7 +2,7 @@ import { XmlComponent } from "file/xml-components"; import { FooterReferenceType } from "./document"; import { Footer } from "./footer/footer"; -import { Image, IMediaData, Media } from "./media"; +import { Image, Media } from "./media"; import { ImageParagraph, Paragraph } from "./paragraph"; import { Relationships } from "./relationships"; import { Table } from "./table"; @@ -43,29 +43,8 @@ export class FooterWrapper { this.footer.addChildElement(childElement); } - public addImageRelationship(image: Buffer, refId: number, width?: number, height?: number): IMediaData { - const mediaData = this.media.addMedia(image, refId, width, height); - this.relationships.createRelationship( - mediaData.referenceId, - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", - `media/${mediaData.fileName}`, - ); - return mediaData; - } - - public addHyperlinkRelationship(target: string, refId: number, targetMode?: "External" | undefined): void { - this.relationships.createRelationship( - refId, - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink", - target, - targetMode, - ); - } - public createImage(image: Buffer | string | Uint8Array | ArrayBuffer, width?: number, height?: number): void { - // TODO - // tslint:disable-next-line:no-any - const mediaData = this.addImageRelationship(image as any, this.relationships.RelationshipCount, width, height); + const mediaData = this.media.addMedia(image, width, height); this.addImage(new Image(new ImageParagraph(mediaData))); } diff --git a/src/file/header-wrapper.spec.ts b/src/file/header-wrapper.spec.ts index 4985887e33..da4f35c043 100644 --- a/src/file/header-wrapper.spec.ts +++ b/src/file/header-wrapper.spec.ts @@ -9,9 +9,9 @@ import { Table } from "./table"; describe("HeaderWrapper", () => { describe("#addParagraph", () => { it("should call the underlying header's addParagraph", () => { - const file = new HeaderWrapper(new Media(), 1); - const spy = sinon.spy(file.Header, "addParagraph"); - file.addParagraph(new Paragraph()); + const wrapper = new HeaderWrapper(new Media(), 1); + const spy = sinon.spy(wrapper.Header, "addParagraph"); + wrapper.addParagraph(new Paragraph()); expect(spy.called).to.equal(true); }); @@ -19,9 +19,9 @@ describe("HeaderWrapper", () => { describe("#addTable", () => { it("should call the underlying header's addParagraph", () => { - const file = new HeaderWrapper(new Media(), 1); - const spy = sinon.spy(file.Header, "addTable"); - file.addTable(new Table(1, 1)); + const wrapper = new HeaderWrapper(new Media(), 1); + const spy = sinon.spy(wrapper.Header, "addTable"); + wrapper.addTable(new Table(1, 1)); expect(spy.called).to.equal(true); }); diff --git a/src/file/header-wrapper.ts b/src/file/header-wrapper.ts index 5730b5eaa6..878d23bc9e 100644 --- a/src/file/header-wrapper.ts +++ b/src/file/header-wrapper.ts @@ -2,7 +2,7 @@ import { XmlComponent } from "file/xml-components"; import { HeaderReferenceType } from "./document"; import { Header } from "./header/header"; -import { Image, IMediaData, Media } from "./media"; +import { Image, Media } from "./media"; import { ImageParagraph, Paragraph } from "./paragraph"; import { Relationships } from "./relationships"; import { Table } from "./table"; @@ -43,29 +43,8 @@ export class HeaderWrapper { this.header.addChildElement(childElement); } - public addImageRelationship(image: Buffer, refId: number, width?: number, height?: number): IMediaData { - const mediaData = this.media.addMedia(image, refId, width, height); - this.relationships.createRelationship( - mediaData.referenceId, - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", - `media/${mediaData.fileName}`, - ); - return mediaData; - } - - public addHyperlinkRelationship(target: string, refId: number, targetMode?: "External" | undefined): void { - this.relationships.createRelationship( - refId, - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink", - target, - targetMode, - ); - } - public createImage(image: Buffer | string | Uint8Array | ArrayBuffer, width?: number, height?: number): void { - // TODO - // tslint:disable-next-line:no-any - const mediaData = this.addImageRelationship(image as any, this.relationships.RelationshipCount, width, height); + const mediaData = this.media.addMedia(image, width, height); this.addImage(new Image(new ImageParagraph(mediaData))); } 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/file/relationships/relationship/relationship.ts b/src/file/relationships/relationship/relationship.ts index 92553a38f3..82672644c9 100644 --- a/src/file/relationships/relationship/relationship.ts +++ b/src/file/relationships/relationship/relationship.ts @@ -17,7 +17,9 @@ export type RelationshipType = | "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" | "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes"; -export type TargetModeType = "External"; +export enum TargetModeType { + EXTERNAL = "External", +} export class Relationship extends XmlComponent { constructor(id: string, type: RelationshipType, target: string, targetMode?: TargetModeType) { diff --git a/src/import-dotx/import-dotx.ts b/src/import-dotx/import-dotx.ts index 9ece4db3f1..fdfedc7304 100644 --- a/src/import-dotx/import-dotx.ts +++ b/src/import-dotx/import-dotx.ts @@ -5,11 +5,11 @@ import { FooterReferenceType } from "file/document/body/section-properties/foote import { HeaderReferenceType } from "file/document/body/section-properties/header-reference"; import { FooterWrapper, IDocumentFooter } from "file/footer-wrapper"; import { HeaderWrapper, IDocumentHeader } from "file/header-wrapper"; -import { convertToXmlComponent, ImportedXmlComponent } from "file/xml-components"; - import { Media } from "file/media"; +import { TargetModeType } from "file/relationships/relationship/relationship"; import { Styles } from "file/styles"; import { ExternalStylesFactory } from "file/styles/external-styles-factory"; +import { convertToXmlComponent, ImportedXmlComponent } from "file/xml-components"; const schemeToType = { "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header": "header", @@ -23,10 +23,17 @@ interface IDocumentRefs { readonly footers: Array<{ readonly id: number; readonly type: FooterReferenceType }>; } +enum RelationshipType { + HEADER = "header", + FOOTER = "footer", + IMAGE = "image", + HYPERLINK = "hyperlink", +} + interface IRelationshipFileInfo { readonly id: number; readonly target: string; - readonly type: "header" | "footer" | "image" | "hyperlink"; + readonly type: RelationshipType; } // Document Template @@ -51,19 +58,69 @@ export class ImportDotx { const zipContent = await JSZip.loadAsync(data); const stylesContent = await zipContent.files["word/styles.xml"].async("text"); + const documentContent = await zipContent.files["word/document.xml"].async("text"); + const relationshipContent = await zipContent.files["word/_rels/document.xml.rels"].async("text"); + const stylesFactory = new ExternalStylesFactory(); - const styles = stylesFactory.newInstance(stylesContent); - - const documentContent = zipContent.files["word/document.xml"]; - const documentRefs: IDocumentRefs = this.extractDocumentRefs(await documentContent.async("text")); - const titlePageIsDefined = this.titlePageIsDefined(await documentContent.async("text")); - - const relationshipContent = zipContent.files["word/_rels/document.xml.rels"]; - const documentRelationships: IRelationshipFileInfo[] = this.findReferenceFiles(await relationshipContent.async("text")); + const documentRefs = this.extractDocumentRefs(documentContent); + const documentRelationships = this.findReferenceFiles(relationshipContent); const media = new Media(); + const templateDocument: IDocumentTemplate = { + headers: await this.createHeaders(zipContent, documentRefs, documentRelationships, media), + footers: await this.createFooters(zipContent, documentRefs, documentRelationships, media), + currentRelationshipId: this.currentRelationshipId, + styles: stylesFactory.newInstance(stylesContent), + titlePageIsDefined: this.checkIfTitlePageIsDefined(documentContent), + }; + + return templateDocument; + } + + private async createFooters( + zipContent: JSZip, + documentRefs: IDocumentRefs, + documentRelationships: IRelationshipFileInfo[], + media: Media, + ): Promise { + const footers: IDocumentFooter[] = []; + + for (const footerRef of documentRefs.footers) { + const relationFileInfo = documentRelationships.find((rel) => rel.id === footerRef.id); + + if (relationFileInfo === null || !relationFileInfo) { + throw new Error(`Can not find target file for id ${footerRef.id}`); + } + + const xmlData = await zipContent.files[`word/${relationFileInfo.target}`].async("text"); + const xmlObj = xml2js(xmlData, { compact: false, captureSpacesBetweenElements: true }) as XMLElement; + let footerXmlElement: XMLElement | undefined; + for (const xmlElm of xmlObj.elements || []) { + if (xmlElm.name === "w:ftr") { + footerXmlElement = xmlElm; + } + } + if (footerXmlElement === undefined) { + continue; + } + const importedComp = convertToXmlComponent(footerXmlElement) as ImportedXmlComponent; + const footer = new FooterWrapper(media, this.currentRelationshipId++, importedComp); + await this.addRelationshipToWrapper(relationFileInfo, zipContent, footer, media); + footers.push({ type: footerRef.type, footer }); + } + + return footers; + } + + private async createHeaders( + zipContent: JSZip, + documentRefs: IDocumentRefs, + documentRelationships: IRelationshipFileInfo[], + media: Media, + ): Promise { const headers: IDocumentHeader[] = []; + for (const headerRef of documentRefs.headers) { const relationFileInfo = documentRelationships.find((rel) => rel.id === headerRef.id); if (relationFileInfo === null || !relationFileInfo) { @@ -83,66 +140,52 @@ export class ImportDotx { } const importedComp = convertToXmlComponent(headerXmlElement) as ImportedXmlComponent; const header = new HeaderWrapper(media, this.currentRelationshipId++, importedComp); - await this.addRelationToWrapper(relationFileInfo, zipContent, header); + // await this.addMedia(zipContent, media, documentRefs, documentRelationships); + await this.addRelationshipToWrapper(relationFileInfo, zipContent, header, media); headers.push({ type: headerRef.type, header }); } - const footers: IDocumentFooter[] = []; - for (const footerRef of documentRefs.footers) { - const relationFileInfo = documentRelationships.find((rel) => rel.id === footerRef.id); - if (relationFileInfo === null || !relationFileInfo) { - throw new Error(`Can not find target file for id ${footerRef.id}`); - } - const xmlData = await zipContent.files[`word/${relationFileInfo.target}`].async("text"); - const xmlObj = xml2js(xmlData, { compact: false, captureSpacesBetweenElements: true }) as XMLElement; - let footerXmlElement: XMLElement | undefined; - for (const xmlElm of xmlObj.elements || []) { - if (xmlElm.name === "w:ftr") { - footerXmlElement = xmlElm; - } - } - if (footerXmlElement === undefined) { - continue; - } - const importedComp = convertToXmlComponent(footerXmlElement) as ImportedXmlComponent; - const footer = new FooterWrapper(media, this.currentRelationshipId++, importedComp); - await this.addRelationToWrapper(relationFileInfo, zipContent, footer); - footers.push({ type: footerRef.type, footer }); - } - - const templateDocument: IDocumentTemplate = { - headers, - footers, - currentRelationshipId: this.currentRelationshipId, - styles, - titlePageIsDefined, - }; - return templateDocument; + return headers; } - public async addRelationToWrapper( + private async addRelationshipToWrapper( relationhipFile: IRelationshipFileInfo, zipContent: JSZip, wrapper: HeaderWrapper | FooterWrapper, + media: Media, ): Promise { - let wrapperImagesReferences: IRelationshipFileInfo[] = []; - let hyperLinkReferences: IRelationshipFileInfo[] = []; const refFile = zipContent.files[`word/_rels/${relationhipFile.target}.rels`]; - if (refFile) { - const xmlRef = await refFile.async("text"); - wrapperImagesReferences = this.findReferenceFiles(xmlRef).filter((r) => r.type === "image"); - hyperLinkReferences = this.findReferenceFiles(xmlRef).filter((r) => r.type === "hyperlink"); + + if (!refFile) { + return; } + + const xmlRef = await refFile.async("text"); + const wrapperImagesReferences = this.findReferenceFiles(xmlRef).filter((r) => r.type === RelationshipType.IMAGE); + const hyperLinkReferences = this.findReferenceFiles(xmlRef).filter((r) => r.type === RelationshipType.HYPERLINK); + for (const r of wrapperImagesReferences) { const buffer = await zipContent.files[`word/${r.target}`].async("nodebuffer"); - wrapper.addImageRelationship(buffer, r.id); + const mediaData = media.addMedia(buffer); + + wrapper.Relationships.createRelationship( + r.id, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", + `media/${mediaData.fileName}`, + ); } + for (const r of hyperLinkReferences) { - wrapper.addHyperlinkRelationship(r.target, r.id, "External"); + wrapper.Relationships.createRelationship( + r.id, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink", + r.target, + TargetModeType.EXTERNAL, + ); } } - public findReferenceFiles(xmlData: string): IRelationshipFileInfo[] { + private findReferenceFiles(xmlData: string): IRelationshipFileInfo[] { const xmlObj = xml2js(xmlData, { compact: true }) as XMLElementCompact; const relationXmlArray = Array.isArray(xmlObj.Relationships.Relationship) ? xmlObj.Relationships.Relationship @@ -162,7 +205,7 @@ export class ImportDotx { return relationships; } - public extractDocumentRefs(xmlData: string): IDocumentRefs { + private extractDocumentRefs(xmlData: string): IDocumentRefs { const xmlObj = xml2js(xmlData, { compact: true }) as XMLElementCompact; const sectionProp = xmlObj["w:document"]["w:body"]["w:sectPr"]; @@ -208,13 +251,14 @@ export class ImportDotx { return { headers, footers }; } - public titlePageIsDefined(xmlData: string): boolean { + private checkIfTitlePageIsDefined(xmlData: string): boolean { const xmlObj = xml2js(xmlData, { compact: true }) as XMLElementCompact; const sectionProp = xmlObj["w:document"]["w:body"]["w:sectPr"]; + return sectionProp["w:titlePg"] !== undefined; } - public parseRefId(str: string): number { + private parseRefId(str: string): number { const match = /^rId(\d+)$/.exec(str); if (match === null) { throw new Error("Invalid ref id");