Merge branch 'master' into feat/improve-docs

# Conflicts:
#	src/file/footer-wrapper.spec.ts
This commit is contained in:
Dolan
2019-01-03 02:12:05 +00:00
27 changed files with 360 additions and 208 deletions

17
demo/demo35.ts Normal file
View File

@ -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);
});

23
demo/demo36.ts Normal file
View File

@ -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);
});

18
demo/demo37.ts Normal file
View File

@ -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);
});

View File

@ -29,6 +29,7 @@ import * as docx from "docx";
## Basic Usage ## Basic Usage
```js ```js
var fs = require("fs");
var docx = require("docx"); var docx = require("docx");
// Create document // Create document
@ -41,11 +42,12 @@ paragraph.addRun(new docx.TextRun("Lorem Ipsum Foo Bar"));
doc.addParagraph(paragraph); doc.addParagraph(paragraph);
// Used to export the file into a .docx file // Used to export the file into a .docx file
var exporter = new docx.LocalPacker(doc); var packer = new docx.Packer();
packer.toBuffer(doc).then((buffer) => {
fs.writeFileSync("My First Document.docx", buffer);
});
exporter.pack("My First Document"); // Done! A file called 'My First Document.docx' will be in your file system.
// Done! A file called 'My First Document.docx' will be in your file system if you used LocalPacker
``` ```
## Honoured Mentions ## Honoured Mentions

View File

@ -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);
}
}

View File

@ -3,6 +3,7 @@ import * as xml from "xml";
import { File } from "file"; import { File } from "file";
import { Formatter } from "../formatter"; import { Formatter } from "../formatter";
import { ImageReplacer } from "./image-replacer";
interface IXmlifyedFile { interface IXmlifyedFile {
readonly data: string; readonly data: string;
@ -28,14 +29,15 @@ interface IXmlifyedFileMapping {
export class Compiler { export class Compiler {
private readonly formatter: Formatter; private readonly formatter: Formatter;
private readonly imageReplacer: ImageReplacer;
constructor() { constructor() {
this.formatter = new Formatter(); this.formatter = new Formatter();
this.imageReplacer = new ImageReplacer();
} }
public async compile(file: File): Promise<JSZip> { public async compile(file: File): Promise<JSZip> {
const zip = new JSZip(); const zip = new JSZip();
const xmlifiedFileMapping = this.xmlifyFile(file); const xmlifiedFileMapping = this.xmlifyFile(file);
for (const key in xmlifiedFileMapping) { for (const key in xmlifiedFileMapping) {
@ -59,26 +61,39 @@ export class Compiler {
zip.file(`word/media/${data.fileName}`, mediaData); 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; return zip;
} }
private xmlifyFile(file: File): IXmlifyedFileMapping { private xmlifyFile(file: File): IXmlifyedFileMapping {
file.verifyUpdateFields(); file.verifyUpdateFields();
const documentRelationshipCount = file.DocumentRelationships.RelationshipCount + 1;
return { 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: { 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", path: "word/document.xml",
}, },
Styles: { Styles: {
@ -98,30 +113,66 @@ export class Compiler {
data: xml(this.formatter.format(file.Numbering)), data: xml(this.formatter.format(file.Numbering)),
path: "word/numbering.xml", path: "word/numbering.xml",
}, },
Relationships: {
data: xml(this.formatter.format(file.DocumentRelationships)),
path: "word/_rels/document.xml.rels",
},
FileRelationships: { FileRelationships: {
data: xml(this.formatter.format(file.FileRelationships)), data: xml(this.formatter.format(file.FileRelationships)),
path: "_rels/.rels", path: "_rels/.rels",
}, },
Headers: file.Headers.map((headerWrapper, index) => ({ HeaderRelationships: file.Headers.map((headerWrapper, index) => {
data: xml(this.formatter.format(headerWrapper.Header)), const xmlData = xml(this.formatter.format(headerWrapper.Header));
path: `word/header${index + 1}.xml`, const mediaDatas = this.imageReplacer.getMediaData(xmlData, file.Media);
})),
Footers: file.Footers.map((footerWrapper, index) => ({ mediaDatas.forEach((mediaData, i) => {
data: xml(this.formatter.format(footerWrapper.Footer)), headerWrapper.Relationships.createRelationship(
path: `word/footer${index + 1}.xml`, i,
})), "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
HeaderRelationships: file.Headers.map((headerWrapper, index) => ({ `media/${mediaData.fileName}`,
data: xml(this.formatter.format(headerWrapper.Relationships)), );
path: `word/_rels/header${index + 1}.xml.rels`, });
})),
FooterRelationships: file.Footers.map((footerWrapper, index) => ({ return {
data: xml(this.formatter.format(footerWrapper.Relationships)), data: xml(this.formatter.format(headerWrapper.Relationships)),
path: `word/_rels/footer${index + 1}.xml.rels`, 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: { ContentTypes: {
data: xml(this.formatter.format(file.ContentTypes)), data: xml(this.formatter.format(file.ContentTypes)),
path: "[Content_Types].xml", path: "[Content_Types].xml",

View File

@ -8,7 +8,20 @@ import { Anchor } from "./anchor";
function createDrawing(drawingOptions: IDrawingOptions): Anchor { function createDrawing(drawingOptions: IDrawingOptions): Anchor {
return new Anchor( return new Anchor(
1, {
fileName: "test.png",
stream: new Buffer(""),
dimensions: {
pixels: {
x: 0,
y: 0,
},
emus: {
x: 0,
y: 0,
},
},
},
{ {
pixels: { pixels: {
x: 100, x: 100,

View File

@ -1,5 +1,5 @@
// http://officeopenxml.com/drwPicFloating.php // http://officeopenxml.com/drwPicFloating.php
import { IMediaDataDimensions } from "file/media"; import { IMediaData, IMediaDataDimensions } from "file/media";
import { XmlComponent } from "file/xml-components"; import { XmlComponent } from "file/xml-components";
import { IDrawingOptions } from "../drawing"; import { IDrawingOptions } from "../drawing";
import { import {
@ -34,7 +34,7 @@ const defaultOptions: IFloating = {
}; };
export class Anchor extends XmlComponent { export class Anchor extends XmlComponent {
constructor(referenceId: number, dimensions: IMediaDataDimensions, drawingOptions: IDrawingOptions) { constructor(mediaData: IMediaData, dimensions: IMediaDataDimensions, drawingOptions: IDrawingOptions) {
super("wp:anchor"); super("wp:anchor");
const floating = { const floating = {
@ -83,6 +83,6 @@ export class Anchor extends XmlComponent {
this.root.push(new DocProperties()); this.root.push(new DocProperties());
this.root.push(new GraphicFrameProperties()); 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));
} }
} }

View File

@ -11,7 +11,6 @@ function createDrawing(drawingOptions?: IDrawingOptions): Drawing {
return new Drawing( return new Drawing(
{ {
fileName: "test.jpg", fileName: "test.jpg",
referenceId: 1,
stream: Buffer.from(imageBase64Data, "base64"), stream: Buffer.from(imageBase64Data, "base64"),
path: path, path: path,
dimensions: { dimensions: {

View File

@ -33,20 +33,16 @@ export class Drawing extends XmlComponent {
constructor(imageData: IMediaData, drawingOptions?: IDrawingOptions) { constructor(imageData: IMediaData, drawingOptions?: IDrawingOptions) {
super("w:drawing"); super("w:drawing");
if (imageData === undefined) {
throw new Error("imageData cannot be undefined");
}
const mergedOptions = { const mergedOptions = {
...defaultDrawingOptions, ...defaultDrawingOptions,
...drawingOptions, ...drawingOptions,
}; };
if (mergedOptions.position === PlacementPosition.INLINE) { 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); this.root.push(this.inline);
} else if (mergedOptions.position === PlacementPosition.FLOATING) { } 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));
} }
} }

View File

@ -1,11 +1,13 @@
import { IMediaData } from "file/media";
import { XmlComponent } from "file/xml-components"; import { XmlComponent } from "file/xml-components";
import { GraphicDataAttributes } from "./graphic-data-attribute"; import { GraphicDataAttributes } from "./graphic-data-attribute";
import { Pic } from "./pic"; import { Pic } from "./pic";
export class GraphicData extends XmlComponent { export class GraphicData extends XmlComponent {
private readonly pic: Pic; private readonly pic: Pic;
constructor(referenceId: number, x: number, y: number) { constructor(mediaData: IMediaData, x: number, y: number) {
super("a:graphicData"); super("a:graphicData");
this.root.push( 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); this.root.push(this.pic);
} }

View File

@ -1,12 +1,15 @@
import { IMediaData } from "file/media";
import { XmlComponent } from "file/xml-components"; import { XmlComponent } from "file/xml-components";
import { Blip } from "./blip"; import { Blip } from "./blip";
import { SourceRectangle } from "./source-rectangle"; import { SourceRectangle } from "./source-rectangle";
import { Stretch } from "./stretch"; import { Stretch } from "./stretch";
export class BlipFill extends XmlComponent { export class BlipFill extends XmlComponent {
constructor(referenceId: number) { constructor(mediaData: IMediaData) {
super("pic:blipFill"); super("pic:blipFill");
this.root.push(new Blip(referenceId));
this.root.push(new Blip(mediaData));
this.root.push(new SourceRectangle()); this.root.push(new SourceRectangle());
this.root.push(new Stretch()); this.root.push(new Stretch());
} }

View File

@ -1,3 +1,4 @@
import { IMediaData } from "file/media";
import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; import { XmlAttributeComponent, XmlComponent } from "file/xml-components";
interface IBlipProperties { interface IBlipProperties {
@ -13,11 +14,11 @@ class BlipAttributes extends XmlAttributeComponent<IBlipProperties> {
} }
export class Blip extends XmlComponent { export class Blip extends XmlComponent {
constructor(referenceId: number) { constructor(mediaData: IMediaData) {
super("a:blip"); super("a:blip");
this.root.push( this.root.push(
new BlipAttributes({ new BlipAttributes({
embed: `rId${referenceId}`, embed: `rId{${mediaData.fileName}}`,
cstate: "none", cstate: "none",
}), }),
); );

View File

@ -1,5 +1,7 @@
// http://officeopenxml.com/drwPic.php // http://officeopenxml.com/drwPic.php
import { IMediaData } from "file/media";
import { XmlComponent } from "file/xml-components"; import { XmlComponent } from "file/xml-components";
import { BlipFill } from "./blip/blip-fill"; import { BlipFill } from "./blip/blip-fill";
import { NonVisualPicProperties } from "./non-visual-pic-properties/non-visual-pic-properties"; import { NonVisualPicProperties } from "./non-visual-pic-properties/non-visual-pic-properties";
import { PicAttributes } from "./pic-attributes"; import { PicAttributes } from "./pic-attributes";
@ -8,7 +10,7 @@ import { ShapeProperties } from "./shape-properties/shape-properties";
export class Pic extends XmlComponent { export class Pic extends XmlComponent {
private readonly shapeProperties: ShapeProperties; private readonly shapeProperties: ShapeProperties;
constructor(referenceId: number, x: number, y: number) { constructor(mediaData: IMediaData, x: number, y: number) {
super("pic:pic"); super("pic:pic");
this.root.push( this.root.push(
@ -20,7 +22,7 @@ export class Pic extends XmlComponent {
this.shapeProperties = new ShapeProperties(x, y); this.shapeProperties = new ShapeProperties(x, y);
this.root.push(new NonVisualPicProperties()); this.root.push(new NonVisualPicProperties());
this.root.push(new BlipFill(referenceId)); this.root.push(new BlipFill(mediaData));
this.root.push(new ShapeProperties(x, y)); this.root.push(new ShapeProperties(x, y));
} }

View File

@ -1,4 +1,6 @@
import { IMediaData } from "file/media";
import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; import { XmlAttributeComponent, XmlComponent } from "file/xml-components";
import { GraphicData } from "./graphic-data"; import { GraphicData } from "./graphic-data";
interface IGraphicProperties { interface IGraphicProperties {
@ -14,7 +16,7 @@ class GraphicAttributes extends XmlAttributeComponent<IGraphicProperties> {
export class Graphic extends XmlComponent { export class Graphic extends XmlComponent {
private readonly data: GraphicData; private readonly data: GraphicData;
constructor(referenceId: number, x: number, y: number) { constructor(mediaData: IMediaData, x: number, y: number) {
super("a:graphic"); super("a:graphic");
this.root.push( this.root.push(
new GraphicAttributes({ 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); this.root.push(this.data);
} }

View File

@ -1,5 +1,5 @@
// http://officeopenxml.com/drwPicInline.php // http://officeopenxml.com/drwPicInline.php
import { IMediaDataDimensions } from "file/media"; import { IMediaData, IMediaDataDimensions } from "file/media";
import { XmlComponent } from "file/xml-components"; import { XmlComponent } from "file/xml-components";
import { DocProperties } from "./../doc-properties/doc-properties"; import { DocProperties } from "./../doc-properties/doc-properties";
import { EffectExtent } from "./../effect-extent/effect-extent"; import { EffectExtent } from "./../effect-extent/effect-extent";
@ -12,7 +12,7 @@ export class Inline extends XmlComponent {
private readonly extent: Extent; private readonly extent: Extent;
private readonly graphic: Graphic; private readonly graphic: Graphic;
constructor(referenceId: number, private readonly dimensions: IMediaDataDimensions) { constructor(readonly mediaData: IMediaData, private readonly dimensions: IMediaDataDimensions) {
super("wp:inline"); super("wp:inline");
this.root.push( this.root.push(
@ -25,7 +25,7 @@ export class Inline extends XmlComponent {
); );
this.extent = new Extent(dimensions.emus.x, dimensions.emus.y); 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(this.extent);
this.root.push(new EffectExtent()); this.root.push(new EffectExtent());

View File

@ -1,8 +1,11 @@
import { expect } from "chai"; import { expect } from "chai";
import * as sinon from "sinon";
import { Formatter } from "export/formatter"; import { Formatter } from "export/formatter";
import { File } from "./file"; import { File } from "./file";
import { Paragraph } from "./paragraph";
import { Table } from "./table";
describe("File", () => { describe("File", () => {
describe("#constructor", () => { 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"); 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);
});
});
}); });

View File

@ -17,6 +17,7 @@ import { Image, Media } from "./media";
import { Numbering } from "./numbering"; import { Numbering } from "./numbering";
import { Bookmark, Hyperlink, Paragraph } from "./paragraph"; import { Bookmark, Hyperlink, Paragraph } from "./paragraph";
import { Relationships } from "./relationships"; import { Relationships } from "./relationships";
import { TargetModeType } from "./relationships/relationship/relationship";
import { Settings } from "./settings"; import { Settings } from "./settings";
import { Styles } from "./styles"; import { Styles } from "./styles";
import { ExternalStylesFactory } from "./styles/external-styles-factory"; import { ExternalStylesFactory } from "./styles/external-styles-factory";
@ -147,7 +148,7 @@ export class File {
hyperlink.linkId, hyperlink.linkId,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
link, link,
"External", TargetModeType.EXTERNAL,
); );
return hyperlink; return hyperlink;
} }

View File

@ -2,7 +2,7 @@ import { XmlComponent } from "file/xml-components";
import { FooterReferenceType } from "./document"; import { FooterReferenceType } from "./document";
import { Footer } from "./footer/footer"; import { Footer } from "./footer/footer";
import { Image, IMediaData, Media } from "./media"; import { Image, Media } from "./media";
import { ImageParagraph, Paragraph } from "./paragraph"; import { ImageParagraph, Paragraph } from "./paragraph";
import { Relationships } from "./relationships"; import { Relationships } from "./relationships";
import { Table } from "./table"; import { Table } from "./table";
@ -43,29 +43,8 @@ export class FooterWrapper {
this.footer.addChildElement(childElement); 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 { public createImage(image: Buffer | string | Uint8Array | ArrayBuffer, width?: number, height?: number): void {
// TODO const mediaData = this.media.addMedia(image, width, height);
// tslint:disable-next-line:no-any
const mediaData = this.addImageRelationship(image as any, this.relationships.RelationshipCount, width, height);
this.addImage(new Image(new ImageParagraph(mediaData))); this.addImage(new Image(new ImageParagraph(mediaData)));
} }

View File

@ -9,9 +9,9 @@ import { Table } from "./table";
describe("HeaderWrapper", () => { describe("HeaderWrapper", () => {
describe("#addParagraph", () => { describe("#addParagraph", () => {
it("should call the underlying header's addParagraph", () => { it("should call the underlying header's addParagraph", () => {
const file = new HeaderWrapper(new Media(), 1); const wrapper = new HeaderWrapper(new Media(), 1);
const spy = sinon.spy(file.Header, "addParagraph"); const spy = sinon.spy(wrapper.Header, "addParagraph");
file.addParagraph(new Paragraph()); wrapper.addParagraph(new Paragraph());
expect(spy.called).to.equal(true); expect(spy.called).to.equal(true);
}); });
@ -19,9 +19,9 @@ describe("HeaderWrapper", () => {
describe("#addTable", () => { describe("#addTable", () => {
it("should call the underlying header's addParagraph", () => { it("should call the underlying header's addParagraph", () => {
const file = new HeaderWrapper(new Media(), 1); const wrapper = new HeaderWrapper(new Media(), 1);
const spy = sinon.spy(file.Header, "addTable"); const spy = sinon.spy(wrapper.Header, "addTable");
file.addTable(new Table(1, 1)); wrapper.addTable(new Table(1, 1));
expect(spy.called).to.equal(true); expect(spy.called).to.equal(true);
}); });

View File

@ -2,7 +2,7 @@ import { XmlComponent } from "file/xml-components";
import { HeaderReferenceType } from "./document"; import { HeaderReferenceType } from "./document";
import { Header } from "./header/header"; import { Header } from "./header/header";
import { Image, IMediaData, Media } from "./media"; import { Image, Media } from "./media";
import { ImageParagraph, Paragraph } from "./paragraph"; import { ImageParagraph, Paragraph } from "./paragraph";
import { Relationships } from "./relationships"; import { Relationships } from "./relationships";
import { Table } from "./table"; import { Table } from "./table";
@ -43,29 +43,8 @@ export class HeaderWrapper {
this.header.addChildElement(childElement); 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 { public createImage(image: Buffer | string | Uint8Array | ArrayBuffer, width?: number, height?: number): void {
// TODO const mediaData = this.media.addMedia(image, width, height);
// tslint:disable-next-line:no-any
const mediaData = this.addImageRelationship(image as any, this.relationships.RelationshipCount, width, height);
this.addImage(new Image(new ImageParagraph(mediaData))); this.addImage(new Image(new ImageParagraph(mediaData)));
} }

View File

@ -10,7 +10,6 @@ export interface IMediaDataDimensions {
} }
export interface IMediaData { export interface IMediaData {
readonly referenceId: number;
readonly stream: Buffer | Uint8Array | ArrayBuffer; readonly stream: Buffer | Uint8Array | ArrayBuffer;
readonly path?: string; readonly path?: string;
readonly fileName: string; readonly fileName: string;

View File

@ -1,5 +1,6 @@
// tslint:disable:object-literal-key-quotes // tslint:disable:object-literal-key-quotes
import { expect } from "chai"; import { expect } from "chai";
import { stub } from "sinon";
import { Formatter } from "export/formatter"; import { Formatter } from "export/formatter";
@ -20,16 +21,18 @@ describe("Media", () => {
}); });
it("should ensure the correct relationship id is used when adding image", () => { 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 file = new File();
const image1 = Media.addImage(file, "test"); const image1 = Media.addImage(file, "test");
const tree = new Formatter().format(image1.Paragraph); const tree = new Formatter().format(image1.Paragraph);
const inlineElements = tree["w:p"][1]["w:r"][1]["w:drawing"][0]["wp:inline"]; const inlineElements = tree["w:p"][1]["w:r"][1]["w:drawing"][0]["wp:inline"];
const graphicData = inlineElements.find((x) => x["a:graphic"]); 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({ expect(graphicData["a:graphic"][1]["a:graphicData"][1]["pic:pic"][2]["pic:blipFill"][0]["a:blip"][0]).to.deep.equal({
_attr: { _attr: {
"r:embed": `rId${file.DocumentRelationships.RelationshipCount}`, "r:embed": `rId{testId.png}`,
cstate: "none", 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({ expect(graphicData2["a:graphic"][1]["a:graphicData"][1]["pic:pic"][2]["pic:blipFill"][0]["a:blip"][0]).to.deep.equal({
_attr: { _attr: {
"r:embed": `rId${file.DocumentRelationships.RelationshipCount}`, "r:embed": `rId{testId.png}`,
cstate: "none", cstate: "none",
}, },
}); });
@ -53,9 +56,8 @@ describe("Media", () => {
// tslint:disable-next-line:no-any // tslint:disable-next-line:no-any
(Media as any).generateId = () => "test"; (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.fileName).to.equal("test.png");
expect(image.referenceId).to.equal(1);
expect(image.dimensions).to.deep.equal({ expect(image.dimensions).to.deep.equal({
pixels: { pixels: {
x: 100, x: 100,
@ -74,7 +76,7 @@ describe("Media", () => {
// tslint:disable-next-line:no-any // tslint:disable-next-line:no-any
(Media as any).generateId = () => "test"; (Media as any).generateId = () => "test";
const image = new Media().addMedia("", 1); const image = new Media().addMedia("");
expect(image.stream).to.be.an.instanceof(Uint8Array); expect(image.stream).to.be.an.instanceof(Uint8Array);
}); });
}); });
@ -85,12 +87,11 @@ describe("Media", () => {
(Media as any).generateId = () => "test"; (Media as any).generateId = () => "test";
const media = new Media(); const media = new Media();
media.addMedia("", 1); media.addMedia("");
const image = media.getMedia("test.png"); const image = media.getMedia("test.png");
expect(image.fileName).to.equal("test.png"); expect(image.fileName).to.equal("test.png");
expect(image.referenceId).to.equal(1);
expect(image.dimensions).to.deep.equal({ expect(image.dimensions).to.deep.equal({
pixels: { pixels: {
x: 100, x: 100,
@ -116,7 +117,7 @@ describe("Media", () => {
(Media as any).generateId = () => "test"; (Media as any).generateId = () => "test";
const media = new Media(); const media = new Media();
media.addMedia("", 1); media.addMedia("");
const array = media.Array; const array = media.Array;
expect(array).to.be.an.instanceof(Array); expect(array).to.be.an.instanceof(Array);
@ -124,7 +125,6 @@ describe("Media", () => {
const image = array[0]; const image = array[0];
expect(image.fileName).to.equal("test.png"); expect(image.fileName).to.equal("test.png");
expect(image.referenceId).to.equal(1);
expect(image.dimensions).to.deep.equal({ expect(image.dimensions).to.deep.equal({
pixels: { pixels: {
x: 100, x: 100,

View File

@ -4,11 +4,6 @@ import { ImageParagraph } from "../paragraph";
import { IMediaData } from "./data"; import { IMediaData } from "./data";
import { Image } from "./image"; import { Image } from "./image";
interface IHackedFile {
// tslint:disable-next-line:readonly-keyword
currentRelationshipId: number;
}
export class Media { export class Media {
public static addImage( public static addImage(
file: File, file: File,
@ -18,14 +13,7 @@ export class Media {
drawingOptions?: IDrawingOptions, drawingOptions?: IDrawingOptions,
): Image { ): Image {
// Workaround to expose id without exposing to API // Workaround to expose id without exposing to API
const exposedFile = (file as {}) as IHackedFile; const mediaData = file.Media.addMedia(buffer, width, height);
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}`,
);
return new Image(new ImageParagraph(mediaData, drawingOptions)); return new Image(new ImageParagraph(mediaData, drawingOptions));
} }
@ -57,17 +45,11 @@ export class Media {
return data; return data;
} }
public addMedia( public addMedia(buffer: Buffer | string | Uint8Array | ArrayBuffer, width: number = 100, height: number = 100): IMediaData {
buffer: Buffer | string | Uint8Array | ArrayBuffer,
referenceId: number,
width: number = 100,
height: number = 100,
): IMediaData {
const key = `${Media.generateId()}.png`; const key = `${Media.generateId()}.png`;
return this.createMedia( return this.createMedia(
key, key,
referenceId,
{ {
width: width, width: width,
height: height, height: height,
@ -78,7 +60,6 @@ export class Media {
private createMedia( private createMedia(
key: string, key: string,
relationshipsCount: number,
dimensions: { readonly width: number; readonly height: number }, dimensions: { readonly width: number; readonly height: number },
data: Buffer | string | Uint8Array | ArrayBuffer, data: Buffer | string | Uint8Array | ArrayBuffer,
filePath?: string, filePath?: string,
@ -86,7 +67,6 @@ export class Media {
const newData = typeof data === "string" ? this.convertDataURIToBinary(data) : data; const newData = typeof data === "string" ? this.convertDataURIToBinary(data) : data;
const imageData: IMediaData = { const imageData: IMediaData = {
referenceId: relationshipsCount,
stream: newData, stream: newData,
path: filePath, path: filePath,
fileName: key, fileName: key,

View File

@ -10,10 +10,9 @@ describe("Image", () => {
beforeEach(() => { beforeEach(() => {
image = new ImageParagraph({ image = new ImageParagraph({
referenceId: 0,
stream: new Buffer(""), stream: new Buffer(""),
path: "", path: "",
fileName: "", fileName: "test.png",
dimensions: { dimensions: {
pixels: { pixels: {
x: 10, x: 10,
@ -171,7 +170,7 @@ describe("Image", () => {
{ {
_attr: { _attr: {
cstate: "none", cstate: "none",
"r:embed": "rId0", "r:embed": "rId{test.png}",
}, },
}, },
], ],

View File

@ -17,7 +17,9 @@ export type RelationshipType =
| "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" | "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"
| "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes"; | "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes";
export type TargetModeType = "External"; export enum TargetModeType {
EXTERNAL = "External",
}
export class Relationship extends XmlComponent { export class Relationship extends XmlComponent {
constructor(id: string, type: RelationshipType, target: string, targetMode?: TargetModeType) { constructor(id: string, type: RelationshipType, target: string, targetMode?: TargetModeType) {

View File

@ -5,11 +5,11 @@ import { FooterReferenceType } from "file/document/body/section-properties/foote
import { HeaderReferenceType } from "file/document/body/section-properties/header-reference"; import { HeaderReferenceType } from "file/document/body/section-properties/header-reference";
import { FooterWrapper, IDocumentFooter } from "file/footer-wrapper"; import { FooterWrapper, IDocumentFooter } from "file/footer-wrapper";
import { HeaderWrapper, IDocumentHeader } from "file/header-wrapper"; import { HeaderWrapper, IDocumentHeader } from "file/header-wrapper";
import { convertToXmlComponent, ImportedXmlComponent } from "file/xml-components";
import { Media } from "file/media"; import { Media } from "file/media";
import { TargetModeType } from "file/relationships/relationship/relationship";
import { Styles } from "file/styles"; import { Styles } from "file/styles";
import { ExternalStylesFactory } from "file/styles/external-styles-factory"; import { ExternalStylesFactory } from "file/styles/external-styles-factory";
import { convertToXmlComponent, ImportedXmlComponent } from "file/xml-components";
const schemeToType = { const schemeToType = {
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/header": "header", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header": "header",
@ -23,10 +23,17 @@ interface IDocumentRefs {
readonly footers: Array<{ readonly id: number; readonly type: FooterReferenceType }>; readonly footers: Array<{ readonly id: number; readonly type: FooterReferenceType }>;
} }
enum RelationshipType {
HEADER = "header",
FOOTER = "footer",
IMAGE = "image",
HYPERLINK = "hyperlink",
}
interface IRelationshipFileInfo { interface IRelationshipFileInfo {
readonly id: number; readonly id: number;
readonly target: string; readonly target: string;
readonly type: "header" | "footer" | "image" | "hyperlink"; readonly type: RelationshipType;
} }
// Document Template // Document Template
@ -51,19 +58,69 @@ export class ImportDotx {
const zipContent = await JSZip.loadAsync(data); const zipContent = await JSZip.loadAsync(data);
const stylesContent = await zipContent.files["word/styles.xml"].async("text"); 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 stylesFactory = new ExternalStylesFactory();
const styles = stylesFactory.newInstance(stylesContent); const documentRefs = this.extractDocumentRefs(documentContent);
const documentRelationships = this.findReferenceFiles(relationshipContent);
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 media = new Media(); 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<IDocumentFooter[]> {
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<IDocumentHeader[]> {
const headers: IDocumentHeader[] = []; const headers: IDocumentHeader[] = [];
for (const headerRef of documentRefs.headers) { for (const headerRef of documentRefs.headers) {
const relationFileInfo = documentRelationships.find((rel) => rel.id === headerRef.id); const relationFileInfo = documentRelationships.find((rel) => rel.id === headerRef.id);
if (relationFileInfo === null || !relationFileInfo) { if (relationFileInfo === null || !relationFileInfo) {
@ -83,66 +140,52 @@ export class ImportDotx {
} }
const importedComp = convertToXmlComponent(headerXmlElement) as ImportedXmlComponent; const importedComp = convertToXmlComponent(headerXmlElement) as ImportedXmlComponent;
const header = new HeaderWrapper(media, this.currentRelationshipId++, importedComp); 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 }); headers.push({ type: headerRef.type, header });
} }
const footers: IDocumentFooter[] = []; return headers;
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;
} }
public async addRelationToWrapper( private async addRelationshipToWrapper(
relationhipFile: IRelationshipFileInfo, relationhipFile: IRelationshipFileInfo,
zipContent: JSZip, zipContent: JSZip,
wrapper: HeaderWrapper | FooterWrapper, wrapper: HeaderWrapper | FooterWrapper,
media: Media,
): Promise<void> { ): Promise<void> {
let wrapperImagesReferences: IRelationshipFileInfo[] = [];
let hyperLinkReferences: IRelationshipFileInfo[] = [];
const refFile = zipContent.files[`word/_rels/${relationhipFile.target}.rels`]; const refFile = zipContent.files[`word/_rels/${relationhipFile.target}.rels`];
if (refFile) {
const xmlRef = await refFile.async("text"); if (!refFile) {
wrapperImagesReferences = this.findReferenceFiles(xmlRef).filter((r) => r.type === "image"); return;
hyperLinkReferences = this.findReferenceFiles(xmlRef).filter((r) => r.type === "hyperlink");
} }
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) { for (const r of wrapperImagesReferences) {
const buffer = await zipContent.files[`word/${r.target}`].async("nodebuffer"); 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) { 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 xmlObj = xml2js(xmlData, { compact: true }) as XMLElementCompact;
const relationXmlArray = Array.isArray(xmlObj.Relationships.Relationship) const relationXmlArray = Array.isArray(xmlObj.Relationships.Relationship)
? xmlObj.Relationships.Relationship ? xmlObj.Relationships.Relationship
@ -162,7 +205,7 @@ export class ImportDotx {
return relationships; return relationships;
} }
public extractDocumentRefs(xmlData: string): IDocumentRefs { private extractDocumentRefs(xmlData: string): IDocumentRefs {
const xmlObj = xml2js(xmlData, { compact: true }) as XMLElementCompact; const xmlObj = xml2js(xmlData, { compact: true }) as XMLElementCompact;
const sectionProp = xmlObj["w:document"]["w:body"]["w:sectPr"]; const sectionProp = xmlObj["w:document"]["w:body"]["w:sectPr"];
@ -208,13 +251,14 @@ export class ImportDotx {
return { headers, footers }; return { headers, footers };
} }
public titlePageIsDefined(xmlData: string): boolean { private checkIfTitlePageIsDefined(xmlData: string): boolean {
const xmlObj = xml2js(xmlData, { compact: true }) as XMLElementCompact; const xmlObj = xml2js(xmlData, { compact: true }) as XMLElementCompact;
const sectionProp = xmlObj["w:document"]["w:body"]["w:sectPr"]; const sectionProp = xmlObj["w:document"]["w:body"]["w:sectPr"];
return sectionProp["w:titlePg"] !== undefined; return sectionProp["w:titlePg"] !== undefined;
} }
public parseRefId(str: string): number { private parseRefId(str: string): number {
const match = /^rId(\d+)$/.exec(str); const match = /^rId(\d+)$/.exec(str);
if (match === null) { if (match === null) {
throw new Error("Invalid ref id"); throw new Error("Invalid ref id");