diff --git a/demo/5-images.ts b/demo/5-images.ts
index 6039d6c832..7953b939cd 100644
--- a/demo/5-images.ts
+++ b/demo/5-images.ts
@@ -21,6 +21,7 @@ const doc = new Document({
new Paragraph({
children: [
new ImageRun({
+ type: "jpg",
data: fs.readFileSync("./demo/images/image1.jpeg"),
transformation: {
width: 100,
@@ -37,6 +38,7 @@ const doc = new Document({
new Paragraph({
children: [
new ImageRun({
+ type: "png",
data: fs.readFileSync("./demo/images/dog.png").toString("base64"),
transformation: {
width: 100,
@@ -53,6 +55,7 @@ const doc = new Document({
new Paragraph({
children: [
new ImageRun({
+ type: "jpg",
data: fs.readFileSync("./demo/images/cat.jpg"),
transformation: {
width: 100,
@@ -73,6 +76,7 @@ const doc = new Document({
new Paragraph({
children: [
new ImageRun({
+ type: "bmp",
data: fs.readFileSync("./demo/images/parrots.bmp"),
transformation: {
width: 150,
@@ -88,6 +92,7 @@ const doc = new Document({
new Paragraph({
children: [
new ImageRun({
+ type: "gif",
data: fs.readFileSync("./demo/images/pizza.gif"),
transformation: {
width: 200,
@@ -103,6 +108,7 @@ const doc = new Document({
new Paragraph({
children: [
new ImageRun({
+ type: "gif",
data: fs.readFileSync("./demo/images/pizza.gif"),
transformation: {
width: 200,
@@ -124,6 +130,7 @@ const doc = new Document({
new Paragraph({
children: [
new ImageRun({
+ type: "jpg",
data: fs.readFileSync("./demo/images/cat.jpg"),
transformation: {
width: 200,
@@ -143,6 +150,22 @@ const doc = new Document({
}),
],
}),
+ new Paragraph({
+ children: [
+ new ImageRun({
+ type: "svg",
+ data: fs.readFileSync("./demo/images/linux-svg.svg"),
+ transformation: {
+ width: 200,
+ height: 200,
+ },
+ fallback: {
+ type: "png",
+ data: fs.readFileSync("./demo/images/linux-png.png"),
+ },
+ }),
+ ],
+ }),
],
},
],
diff --git a/demo/images/linux-png.png b/demo/images/linux-png.png
new file mode 100644
index 0000000000..f4e2d5ad27
Binary files /dev/null and b/demo/images/linux-png.png differ
diff --git a/demo/images/linux-svg.svg b/demo/images/linux-svg.svg
new file mode 100644
index 0000000000..da9140f12d
--- /dev/null
+++ b/demo/images/linux-svg.svg
@@ -0,0 +1,183 @@
+
+
+
+
diff --git a/docs/_coverpage.md b/docs/_coverpage.md
index b8a546f3a4..665583fc68 100644
--- a/docs/_coverpage.md
+++ b/docs/_coverpage.md
@@ -4,7 +4,7 @@
- Simple, declarative API
- 80+ usage examples
-- Battle tested, mature, 99.9%+ coverage
+- Battle tested, mature, 100% coverage (yes, every line is tested)
[GitHub](https://github.com/dolanmiu/docx)
[Get Started](#Welcome)
diff --git a/docs/usage/images.md b/docs/usage/images.md
index 1326a780b2..8fa763b3bf 100644
--- a/docs/usage/images.md
+++ b/docs/usage/images.md
@@ -6,6 +6,7 @@ To create a `floating` image on top of text:
```ts
const image = new ImageRun({
+ type: 'gif',
data: fs.readFileSync("./demo/images/pizza.gif"),
transformation: {
width: 200,
@@ -26,6 +27,7 @@ By default with no arguments, its an `inline` image:
```ts
const image = new ImageRun({
+ type: 'gif',
data: fs.readFileSync("./demo/images/pizza.gif"),
transformation: {
width: 100,
@@ -59,6 +61,7 @@ const doc = new Document({
new Paragraph({
children: [
new ImageRun({
+ type: [IMAGE_TYPE],
data: [IMAGE_BUFFER],
transformation: {
width: [IMAGE_SIZE],
@@ -97,6 +100,7 @@ To change the position the image to be on top of the text, simply add the `float
```ts
const image = new ImageRun({
+ type: 'png',
data: buffer,
transformation: {
width: 903,
@@ -115,6 +119,7 @@ const image = new ImageRun({
```ts
const image = new ImageRun({
+ type: 'png',
data: buffer,
transformation: {
width: 903,
@@ -180,6 +185,7 @@ For example:
```ts
const image = new ImageRun({
+ type: 'gif',
data: fs.readFileSync("./demo/images/pizza.gif"),
transformation: {
width: 200,
@@ -228,6 +234,7 @@ For example:
```ts
const image = new ImageRun({
+ type: 'gif',
data: fs.readFileSync("./demo/images/pizza.gif"),
transformation: {
width: 200,
@@ -258,6 +265,7 @@ Specifies common non-visual DrawingML properties. A name, title and description
```ts
const image = new ImageRun({
+ type: 'gif',
data: fs.readFileSync("./demo/images/pizza.gif"),
altText: {
title: "This is an ultimate title",
diff --git a/docs/usage/patcher.md b/docs/usage/patcher.md
index 6f76b1d180..fecc709dcf 100644
--- a/docs/usage/patcher.md
+++ b/docs/usage/patcher.md
@@ -76,7 +76,7 @@ patchDocument(fs.readFileSync("My Document.docx"), {
],
link: "https://www.google.co.uk",
}),
- new ImageRun({ data: fs.readFileSync("./demo/images/dog.png"), transformation: { width: 100, height: 100 } }),
+ new ImageRun({ type: 'png', data: fs.readFileSync("./demo/images/dog.png"), transformation: { width: 100, height: 100 } }),
],
}),
],
diff --git a/src/export/packer/image-replacer.spec.ts b/src/export/packer/image-replacer.spec.ts
index f9f5f33e47..33ff33f4cf 100644
--- a/src/export/packer/image-replacer.spec.ts
+++ b/src/export/packer/image-replacer.spec.ts
@@ -12,7 +12,8 @@ describe("ImageReplacer", () => {
"test {test-image.png} test",
[
{
- stream: Buffer.from(""),
+ type: "png",
+ data: Buffer.from(""),
fileName: "test-image.png",
transformation: {
pixels: {
diff --git a/src/export/packer/next-compiler.spec.ts b/src/export/packer/next-compiler.spec.ts
index 6713ee2734..753389de44 100644
--- a/src/export/packer/next-compiler.spec.ts
+++ b/src/export/packer/next-compiler.spec.ts
@@ -150,12 +150,25 @@ describe("Compiler", () => {
new Paragraph({
children: [
new ImageRun({
+ type: "png",
data: Buffer.from("", "base64"),
transformation: {
width: 100,
height: 100,
},
}),
+ new ImageRun({
+ type: "svg",
+ data: Buffer.from("", "base64"),
+ transformation: {
+ width: 100,
+ height: 100,
+ },
+ fallback: {
+ type: "png",
+ data: Buffer.from("", "base64"),
+ },
+ }),
],
}),
],
@@ -165,7 +178,8 @@ describe("Compiler", () => {
vi.spyOn(compiler["imageReplacer"], "getMediaData").mockReturnValue([
{
- stream: Buffer.from(""),
+ type: "png",
+ data: Buffer.from(""),
fileName: "test",
transformation: {
pixels: {
@@ -178,6 +192,36 @@ describe("Compiler", () => {
},
},
},
+ {
+ type: "svg",
+ data: Buffer.from(""),
+ fileName: "test",
+ transformation: {
+ pixels: {
+ x: 100,
+ y: 100,
+ },
+ emus: {
+ x: 100,
+ y: 100,
+ },
+ },
+ fallback: {
+ type: "png",
+ data: Buffer.from(""),
+ fileName: "test",
+ transformation: {
+ pixels: {
+ x: 100,
+ y: 100,
+ },
+ emus: {
+ x: 100,
+ y: 100,
+ },
+ },
+ },
+ },
]);
compiler.compile(file);
diff --git a/src/export/packer/next-compiler.ts b/src/export/packer/next-compiler.ts
index 0b0434a0b2..b7df84c899 100644
--- a/src/export/packer/next-compiler.ts
+++ b/src/export/packer/next-compiler.ts
@@ -62,8 +62,13 @@ export class Compiler {
}
}
- for (const { stream, fileName } of file.Media.Array) {
- zip.file(`word/media/${fileName}`, stream);
+ for (const data of file.Media.Array) {
+ if (data.type !== "svg") {
+ zip.file(`word/media/${data.fileName}`, data.data);
+ } else {
+ zip.file(`word/media/${data.fileName}`, data.data);
+ zip.file(`word/media/${data.fallback.fileName}`, data.fallback.data);
+ }
}
for (const { data: buffer, name, fontKey } of file.FontTable.fontOptionsWithKey) {
diff --git a/src/file/content-types/content-types.spec.ts b/src/file/content-types/content-types.spec.ts
index a0203d0dae..1fac77993e 100644
--- a/src/file/content-types/content-types.spec.ts
+++ b/src/file/content-types/content-types.spec.ts
@@ -25,12 +25,13 @@ describe("ContentTypes", () => {
expect(tree["Types"][3]).to.deep.equal({ Default: { _attr: { ContentType: "image/jpeg", Extension: "jpg" } } });
expect(tree["Types"][4]).to.deep.equal({ Default: { _attr: { ContentType: "image/bmp", Extension: "bmp" } } });
expect(tree["Types"][5]).to.deep.equal({ Default: { _attr: { ContentType: "image/gif", Extension: "gif" } } });
- expect(tree["Types"][6]).to.deep.equal({
+ expect(tree["Types"][6]).to.deep.equal({ Default: { _attr: { ContentType: "image/svg+xml", Extension: "svg" } } });
+ expect(tree["Types"][7]).to.deep.equal({
Default: { _attr: { ContentType: "application/vnd.openxmlformats-package.relationships+xml", Extension: "rels" } },
});
- expect(tree["Types"][7]).to.deep.equal({ Default: { _attr: { ContentType: "application/xml", Extension: "xml" } } });
+ expect(tree["Types"][8]).to.deep.equal({ Default: { _attr: { ContentType: "application/xml", Extension: "xml" } } });
- expect(tree["Types"][8]).to.deep.equal({
+ expect(tree["Types"][9]).to.deep.equal({
Default: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.obfuscatedFont",
@@ -38,7 +39,7 @@ describe("ContentTypes", () => {
},
},
});
- expect(tree["Types"][9]).to.deep.equal({
+ expect(tree["Types"][10]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml",
@@ -46,7 +47,7 @@ describe("ContentTypes", () => {
},
},
});
- expect(tree["Types"][10]).to.deep.equal({
+ expect(tree["Types"][11]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml",
@@ -54,7 +55,7 @@ describe("ContentTypes", () => {
},
},
});
- expect(tree["Types"][11]).to.deep.equal({
+ expect(tree["Types"][12]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-package.core-properties+xml",
@@ -62,7 +63,7 @@ describe("ContentTypes", () => {
},
},
});
- expect(tree["Types"][12]).to.deep.equal({
+ expect(tree["Types"][13]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.custom-properties+xml",
@@ -70,7 +71,7 @@ describe("ContentTypes", () => {
},
},
});
- expect(tree["Types"][13]).to.deep.equal({
+ expect(tree["Types"][14]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.extended-properties+xml",
@@ -78,7 +79,7 @@ describe("ContentTypes", () => {
},
},
});
- expect(tree["Types"][14]).to.deep.equal({
+ expect(tree["Types"][15]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml",
@@ -86,7 +87,7 @@ describe("ContentTypes", () => {
},
},
});
- expect(tree["Types"][15]).to.deep.equal({
+ expect(tree["Types"][16]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml",
@@ -94,7 +95,7 @@ describe("ContentTypes", () => {
},
},
});
- expect(tree["Types"][16]).to.deep.equal({
+ expect(tree["Types"][17]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml",
@@ -111,7 +112,7 @@ describe("ContentTypes", () => {
contentTypes.addFooter(102);
const tree = new Formatter().format(contentTypes);
- expect(tree["Types"][19]).to.deep.equal({
+ expect(tree["Types"][20]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml",
@@ -120,7 +121,7 @@ describe("ContentTypes", () => {
},
});
- expect(tree["Types"][20]).to.deep.equal({
+ expect(tree["Types"][21]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml",
@@ -137,7 +138,7 @@ describe("ContentTypes", () => {
contentTypes.addHeader(202);
const tree = new Formatter().format(contentTypes);
- expect(tree["Types"][19]).to.deep.equal({
+ expect(tree["Types"][20]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml",
@@ -146,7 +147,7 @@ describe("ContentTypes", () => {
},
});
- expect(tree["Types"][20]).to.deep.equal({
+ expect(tree["Types"][21]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml",
diff --git a/src/file/content-types/content-types.ts b/src/file/content-types/content-types.ts
index 399581f7d6..fc7cc8a8b0 100644
--- a/src/file/content-types/content-types.ts
+++ b/src/file/content-types/content-types.ts
@@ -18,6 +18,7 @@ export class ContentTypes extends XmlComponent {
this.root.push(new Default("image/jpeg", "jpg"));
this.root.push(new Default("image/bmp", "bmp"));
this.root.push(new Default("image/gif", "gif"));
+ this.root.push(new Default("image/svg+xml", "svg"));
this.root.push(new Default("application/vnd.openxmlformats-package.relationships+xml", "rels"));
this.root.push(new Default("application/xml", "xml"));
this.root.push(new Default("application/vnd.openxmlformats-officedocument.obfuscatedFont", "odttf"));
diff --git a/src/file/drawing/anchor/anchor.spec.ts b/src/file/drawing/anchor/anchor.spec.ts
index 215aa087db..ab0c9cb401 100644
--- a/src/file/drawing/anchor/anchor.spec.ts
+++ b/src/file/drawing/anchor/anchor.spec.ts
@@ -11,8 +11,9 @@ import { Anchor } from "./anchor";
const createAnchor = (drawingOptions: IDrawingOptions): Anchor =>
new Anchor({
mediaData: {
+ type: "png",
fileName: "test.png",
- stream: Buffer.from(""),
+ data: Buffer.from(""),
transformation: {
pixels: {
x: 0,
diff --git a/src/file/drawing/drawing.spec.ts b/src/file/drawing/drawing.spec.ts
index 60fe335792..73bb1fcd15 100644
--- a/src/file/drawing/drawing.spec.ts
+++ b/src/file/drawing/drawing.spec.ts
@@ -11,8 +11,9 @@ const imageBase64Data = `iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAACzVBMVEU
const createDrawing = (drawingOptions?: IDrawingOptions): Drawing =>
new Drawing(
{
+ type: "jpg",
fileName: "test.jpg",
- stream: Buffer.from(imageBase64Data, "base64"),
+ data: Buffer.from(imageBase64Data, "base64"),
transformation: {
pixels: {
x: 100,
diff --git a/src/file/drawing/inline/graphic/graphic-data/pic/blip/blip-extentions.ts b/src/file/drawing/inline/graphic/graphic-data/pic/blip/blip-extentions.ts
new file mode 100644
index 0000000000..909d87f80b
--- /dev/null
+++ b/src/file/drawing/inline/graphic/graphic-data/pic/blip/blip-extentions.ts
@@ -0,0 +1,35 @@
+import { BuilderElement, XmlComponent } from "@file/xml-components";
+import { IMediaData } from "@file/media";
+
+const createSvgBlip = (mediaData: IMediaData): XmlComponent =>
+ new BuilderElement({
+ name: "asvg:svgBlip",
+ attributes: {
+ asvg: {
+ key: "xmlns:asvg",
+ value: "http://schemas.microsoft.com/office/drawing/2016/SVG/main",
+ },
+ embed: {
+ key: "r:embed",
+ value: `rId{${mediaData.fileName}}`,
+ },
+ },
+ });
+
+const createExtention = (mediaData: IMediaData): XmlComponent =>
+ new BuilderElement({
+ name: "a:ext",
+ attributes: {
+ uri: {
+ key: "uri",
+ value: "{96DAC541-7B7A-43D3-8B79-37D633B846F1}",
+ },
+ },
+ children: [createSvgBlip(mediaData)],
+ });
+
+export const createExtentionList = (mediaData: IMediaData): XmlComponent =>
+ new BuilderElement({
+ name: "a:extLst",
+ children: [createExtention(mediaData)],
+ });
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 00b90fcef5..5f9bacebed 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,7 +1,7 @@
import { IMediaData } from "@file/media";
import { XmlComponent } from "@file/xml-components";
-import { Blip } from "./blip";
+import { createBlip } from "./blip";
import { SourceRectangle } from "./source-rectangle";
import { Stretch } from "./stretch";
@@ -9,7 +9,7 @@ export class BlipFill extends XmlComponent {
public constructor(mediaData: IMediaData) {
super("pic:blipFill");
- this.root.push(new Blip(mediaData));
+ this.root.push(createBlip(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 8db38d6762..d322272737 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,24 +1,24 @@
import { IMediaData } from "@file/media";
-import { XmlAttributeComponent, XmlComponent } from "@file/xml-components";
+import { BuilderElement, XmlComponent } from "@file/xml-components";
+import { createExtentionList } from "./blip-extentions";
-class BlipAttributes extends XmlAttributeComponent<{
+type BlipAttributes = {
readonly embed: string;
readonly cstate: string;
-}> {
- protected readonly xmlKeys = {
- embed: "r:embed",
- cstate: "cstate",
- };
-}
+};
-export class Blip extends XmlComponent {
- public constructor(mediaData: IMediaData) {
- super("a:blip");
- this.root.push(
- new BlipAttributes({
- embed: `rId{${mediaData.fileName}}`,
- cstate: "none",
- }),
- );
- }
-}
+export const createBlip = (mediaData: IMediaData): XmlComponent =>
+ new BuilderElement({
+ name: "a:blip",
+ attributes: {
+ embed: {
+ key: "r:embed",
+ value: `rId{${mediaData.type === "svg" ? mediaData.fallback.fileName : mediaData.fileName}}`,
+ },
+ cstate: {
+ key: "cstate",
+ value: "none",
+ },
+ },
+ children: mediaData.type === "svg" ? [createExtentionList(mediaData)] : [],
+ });
diff --git a/src/file/drawing/inline/inline.spec.ts b/src/file/drawing/inline/inline.spec.ts
index fca5ed61c9..1d2a791507 100644
--- a/src/file/drawing/inline/inline.spec.ts
+++ b/src/file/drawing/inline/inline.spec.ts
@@ -8,8 +8,9 @@ describe("Inline", () => {
const tree = new Formatter().format(
createInline({
mediaData: {
+ type: "png",
fileName: "test.png",
- stream: Buffer.from(""),
+ data: Buffer.from(""),
transformation: {
pixels: {
x: 0,
diff --git a/src/file/file.spec.ts b/src/file/file.spec.ts
index ac0098a379..7f4cca4ae2 100644
--- a/src/file/file.spec.ts
+++ b/src/file/file.spec.ts
@@ -436,7 +436,44 @@ describe("File", () => {
it("should work with external styles", () => {
const doc = new File({
sections: [],
- externalStyles: "",
+ externalStyles: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
});
expect(doc.Styles).to.not.be.undefined;
diff --git a/src/file/file.ts b/src/file/file.ts
index 5aee17d7d1..47dd895166 100644
--- a/src/file/file.ts
+++ b/src/file/file.ts
@@ -84,7 +84,7 @@ export class File {
this.media = new Media();
- if (options.externalStyles) {
+ if (options.externalStyles !== undefined) {
const stylesFactory = new ExternalStylesFactory();
this.styles = stylesFactory.newInstance(options.externalStyles);
} else if (options.styles) {
diff --git a/src/file/media/data.ts b/src/file/media/data.ts
index eb5ae4629d..1cdd76f2a7 100644
--- a/src/file/media/data.ts
+++ b/src/file/media/data.ts
@@ -14,11 +14,25 @@ export interface IMediaDataTransformation {
readonly rotation?: number;
}
-export interface IMediaData {
- readonly stream: Buffer | Uint8Array | ArrayBuffer;
+type CoreMediaData = {
readonly fileName: string;
readonly transformation: IMediaDataTransformation;
-}
+ readonly data: Buffer | Uint8Array | ArrayBuffer;
+};
+
+type RegularMediaData = {
+ readonly type: "jpg" | "png" | "gif" | "bmp";
+};
+
+type SvgMediaData = {
+ readonly type: "svg";
+ /**
+ * Required in case the Word processor does not support SVG.
+ */
+ readonly fallback: RegularMediaData & CoreMediaData;
+};
+
+export type IMediaData = (RegularMediaData | SvgMediaData) & CoreMediaData;
// Needed because of: https://github.com/s-panferov/awesome-typescript-loader/issues/432
/**
diff --git a/src/file/media/media.spec.ts b/src/file/media/media.spec.ts
index 1e59502726..8cc0d0f741 100644
--- a/src/file/media/media.spec.ts
+++ b/src/file/media/media.spec.ts
@@ -19,7 +19,8 @@ describe("Media", () => {
const media = new Media();
media.addImage("test2.png", {
- stream: Buffer.from(""),
+ type: "png",
+ data: Buffer.from(""),
fileName: "test.png",
transformation: {
pixels: {
diff --git a/src/file/paragraph/run/image-run.spec.ts b/src/file/paragraph/run/image-run.spec.ts
index 77eff04070..f0fb8161f1 100644
--- a/src/file/paragraph/run/image-run.spec.ts
+++ b/src/file/paragraph/run/image-run.spec.ts
@@ -19,6 +19,7 @@ describe("ImageRun", () => {
describe("#constructor()", () => {
it("should create with Buffer", () => {
const currentImageRun = new ImageRun({
+ type: "png",
data: Buffer.from(""),
transformation: {
width: 200,
@@ -39,8 +40,7 @@ describe("ImageRun", () => {
const tree = new Formatter().format(currentImageRun, {
file: {
Media: {
- // eslint-disable-next-line @typescript-eslint/no-empty-function
- addImage: () => {},
+ addImage: vi.fn(),
},
} as unknown as File,
viewWrapper: {} as unknown as IViewWrapper,
@@ -271,6 +271,7 @@ describe("ImageRun", () => {
it("should create with string", () => {
const currentImageRun = new ImageRun({
+ type: "png",
data: "",
transformation: {
width: 200,
@@ -291,8 +292,7 @@ describe("ImageRun", () => {
const tree = new Formatter().format(currentImageRun, {
file: {
Media: {
- // eslint-disable-next-line @typescript-eslint/no-empty-function
- addImage: () => {},
+ addImage: vi.fn(),
},
} as unknown as File,
viewWrapper: {} as unknown as IViewWrapper,
@@ -522,10 +522,10 @@ describe("ImageRun", () => {
});
it("should return UInt8Array if atob is present", () => {
- // eslint-disable-next-line functional/immutable-data
- global.atob = () => "atob result";
+ vi.spyOn(global, "atob").mockReturnValue("atob result");
const currentImageRun = new ImageRun({
+ type: "png",
data: "",
transformation: {
width: 200,
@@ -546,8 +546,7 @@ describe("ImageRun", () => {
const tree = new Formatter().format(currentImageRun, {
file: {
Media: {
- // eslint-disable-next-line @typescript-eslint/no-empty-function
- addImage: () => {},
+ addImage: vi.fn(),
},
} as unknown as File,
viewWrapper: {} as unknown as IViewWrapper,
@@ -775,16 +774,13 @@ describe("ImageRun", () => {
},
],
});
-
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, functional/immutable-data
- (global as any).atob = undefined;
});
it("should use data as is if its not a string", () => {
- // eslint-disable-next-line functional/immutable-data
- global.atob = () => "atob result";
+ vi.spyOn(global, "atob").mockReturnValue("atob result");
const currentImageRun = new ImageRun({
+ type: "png",
data: "",
transformation: {
width: 200,
@@ -805,8 +801,7 @@ describe("ImageRun", () => {
const tree = new Formatter().format(currentImageRun, {
file: {
Media: {
- // eslint-disable-next-line @typescript-eslint/no-empty-function
- addImage: () => {},
+ addImage: vi.fn(),
},
} as unknown as File,
viewWrapper: {} as unknown as IViewWrapper,
@@ -1034,9 +1029,107 @@ describe("ImageRun", () => {
},
],
});
+ });
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, functional/immutable-data
- (global as any).atob = undefined;
+ it("should strip base64 marker", () => {
+ const spy = vi.spyOn(global, "atob").mockReturnValue("atob result");
+
+ new ImageRun({
+ type: "png",
+ data: ";base64,",
+ transformation: {
+ width: 200,
+ height: 200,
+ rotation: 45,
+ },
+ });
+
+ expect(spy).toBeCalledWith("");
+ });
+
+ it("should work with svgs", () => {
+ const currentImageRun = new ImageRun({
+ type: "svg",
+ data: Buffer.from(""),
+ transformation: {
+ width: 200,
+ height: 200,
+ },
+ fallback: {
+ type: "png",
+ data: Buffer.from(""),
+ },
+ });
+
+ const tree = new Formatter().format(currentImageRun, {
+ file: {
+ Media: {
+ addImage: vi.fn(),
+ },
+ } as unknown as File,
+ viewWrapper: {} as unknown as IViewWrapper,
+ stack: [],
+ });
+
+ expect(tree).toStrictEqual({
+ "w:r": [
+ {
+ "w:drawing": [
+ {
+ "wp:inline": expect.arrayContaining([
+ {
+ "a:graphic": expect.arrayContaining([
+ {
+ "a:graphicData": expect.arrayContaining([
+ {
+ "pic:pic": expect.arrayContaining([
+ {
+ "pic:blipFill": expect.arrayContaining([
+ {
+ "a:blip": [
+ {
+ _attr: {
+ cstate: "none",
+ "r:embed": "rId{test-unique-id.png}",
+ },
+ },
+ {
+ "a:extLst": [
+ {
+ "a:ext": [
+ {
+ _attr: {
+ uri: "{96DAC541-7B7A-43D3-8B79-37D633B846F1}",
+ },
+ },
+ {
+ "asvg:svgBlip": {
+ _attr: expect.objectContaining({
+ "r:embed":
+ "rId{test-unique-id.svg}",
+ }),
+ },
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ]),
+ },
+ ]),
+ },
+ ]),
+ },
+ ]),
+ },
+ ]),
+ },
+ ],
+ },
+ ],
+ });
});
});
});
diff --git a/src/file/paragraph/run/image-run.ts b/src/file/paragraph/run/image-run.ts
index 7a674a13c8..b2dfa1280e 100644
--- a/src/file/paragraph/run/image-run.ts
+++ b/src/file/paragraph/run/image-run.ts
@@ -9,38 +9,102 @@ import { IMediaTransformation } from "../../media";
import { IMediaData } from "../../media/data";
import { Run } from "../run";
-export interface IImageOptions {
- readonly data: Buffer | string | Uint8Array | ArrayBuffer;
+type CoreImageOptions = {
readonly transformation: IMediaTransformation;
readonly floating?: IFloating;
readonly altText?: DocPropertiesOptions;
readonly outline?: OutlineOptions;
-}
+};
+
+type RegularImageOptions = {
+ readonly type: "jpg" | "png" | "gif" | "bmp";
+ readonly data: Buffer | string | Uint8Array | ArrayBuffer;
+};
+
+type SvgMediaOptions = {
+ readonly type: "svg";
+ readonly data: Buffer | string | Uint8Array | ArrayBuffer;
+ /**
+ * Required in case the Word processor does not support SVG.
+ */
+ readonly fallback: RegularImageOptions;
+};
+
+export type IImageOptions = (RegularImageOptions | SvgMediaOptions) & CoreImageOptions;
+
+const convertDataURIToBinary = (dataURI: string): Uint8Array => {
+ if (typeof atob === "function") {
+ // https://gist.github.com/borismus/1032746
+ // https://github.com/mafintosh/base64-to-uint8array
+ const BASE64_MARKER = ";base64,";
+ const base64Index = dataURI.indexOf(BASE64_MARKER);
+
+ const base64IndexWithOffset = base64Index === -1 ? 0 : base64Index + BASE64_MARKER.length;
+
+ return new Uint8Array(
+ atob(dataURI.substring(base64IndexWithOffset))
+ .split("")
+ .map((c) => c.charCodeAt(0)),
+ );
+ /* c8 ignore next 6 */
+ } else {
+ // Not possible to test this branch in NodeJS
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
+ const b = require("buf" + "fer");
+ return new b.Buffer(dataURI, "base64");
+ }
+};
+
+const standardizeData = (data: string | Buffer | Uint8Array | ArrayBuffer): Buffer | Uint8Array | ArrayBuffer =>
+ typeof data === "string" ? convertDataURIToBinary(data) : data;
+
+const createImageData = (options: IImageOptions, key: string): Pick => ({
+ data: standardizeData(options.data),
+ fileName: key,
+ transformation: {
+ pixels: {
+ x: Math.round(options.transformation.width),
+ y: Math.round(options.transformation.height),
+ },
+ emus: {
+ x: Math.round(options.transformation.width * 9525),
+ y: Math.round(options.transformation.height * 9525),
+ },
+ flip: options.transformation.flip,
+ rotation: options.transformation.rotation ? options.transformation.rotation * 60000 : undefined,
+ },
+});
export class ImageRun extends Run {
- private readonly key = `${uniqueId()}.png`;
+ private readonly key: string;
+ private readonly fallbackKey = `${uniqueId()}.png`;
private readonly imageData: IMediaData;
public constructor(options: IImageOptions) {
super({});
- const newData = typeof options.data === "string" ? this.convertDataURIToBinary(options.data) : options.data;
- this.imageData = {
- stream: newData,
- fileName: this.key,
- transformation: {
- pixels: {
- x: Math.round(options.transformation.width),
- y: Math.round(options.transformation.height),
- },
- emus: {
- x: Math.round(options.transformation.width * 9525),
- y: Math.round(options.transformation.height * 9525),
- },
- flip: options.transformation.flip,
- rotation: options.transformation.rotation ? options.transformation.rotation * 60000 : undefined,
- },
- };
+ this.key = `${uniqueId()}.${options.type}`;
+
+ this.imageData =
+ options.type === "svg"
+ ? {
+ type: options.type,
+ ...createImageData(options, this.key),
+ fallback: {
+ type: options.fallback.type,
+ ...createImageData(
+ {
+ ...options.fallback,
+ transformation: options.transformation,
+ },
+ this.fallbackKey,
+ ),
+ },
+ }
+ : {
+ type: options.type,
+ ...createImageData(options, this.key),
+ };
const drawing = new Drawing(this.imageData, {
floating: options.floating,
docProperties: options.altText,
@@ -53,29 +117,10 @@ export class ImageRun extends Run {
public prepForXml(context: IContext): IXmlableObject | undefined {
context.file.Media.addImage(this.key, this.imageData);
+ if (this.imageData.type === "svg") {
+ context.file.Media.addImage(this.fallbackKey, this.imageData.fallback);
+ }
+
return super.prepForXml(context);
}
-
- private convertDataURIToBinary(dataURI: string): Uint8Array {
- if (typeof atob === "function") {
- // https://gist.github.com/borismus/1032746
- // https://github.com/mafintosh/base64-to-uint8array
- const BASE64_MARKER = ";base64,";
- const base64Index = dataURI.indexOf(BASE64_MARKER);
-
- const base64IndexWithOffset = base64Index === -1 ? 0 : base64Index + BASE64_MARKER.length;
-
- return new Uint8Array(
- atob(dataURI.substring(base64IndexWithOffset))
- .split("")
- .map((c) => c.charCodeAt(0)),
- );
- /* c8 ignore next 6 */
- } else {
- // Not possible to test this branch in NodeJS
- // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
- const b = require("buf" + "fer");
- return new b.Buffer(dataURI, "base64");
- }
- }
}
diff --git a/src/file/paragraph/run/run-components/text.ts b/src/file/paragraph/run/run-components/text.ts
index d6bc671dbb..db6c8d77b0 100644
--- a/src/file/paragraph/run/run-components/text.ts
+++ b/src/file/paragraph/run/run-components/text.ts
@@ -23,11 +23,9 @@ export class Text extends XmlComponent {
if (typeof options === "string") {
this.root.push(new TextAttributes({ space: SpaceType.PRESERVE }));
this.root.push(options);
- return this;
} else {
this.root.push(new TextAttributes({ space: options.space ?? SpaceType.DEFAULT }));
this.root.push(options.text);
- return this;
}
}
}
diff --git a/src/patcher/from-docx.spec.ts b/src/patcher/from-docx.spec.ts
index 088d0b715d..1b939bad17 100644
--- a/src/patcher/from-docx.spec.ts
+++ b/src/patcher/from-docx.spec.ts
@@ -254,6 +254,7 @@ describe("from-docx", () => {
link: "https://www.google.co.uk",
}),
new ImageRun({
+ type: "png",
data: Buffer.from(""),
transformation: { width: 100, height: 100 },
}),
@@ -266,6 +267,7 @@ describe("from-docx", () => {
type: PatchType.PARAGRAPH,
children: [
new ImageRun({
+ type: "png",
data: Buffer.from(""),
transformation: { width: 100, height: 100 },
}),
@@ -310,6 +312,7 @@ describe("from-docx", () => {
type: PatchType.PARAGRAPH,
children: [
new ImageRun({
+ type: "png",
data: Buffer.from(""),
transformation: { width: 100, height: 100 },
}),
@@ -354,6 +357,7 @@ describe("from-docx", () => {
type: PatchType.PARAGRAPH,
children: [
new ImageRun({
+ type: "png",
data: Buffer.from(""),
transformation: { width: 100, height: 100 },
}),
@@ -391,6 +395,7 @@ describe("from-docx", () => {
type: PatchType.PARAGRAPH,
children: [
new ImageRun({
+ type: "png",
data: Buffer.from(""),
transformation: { width: 100, height: 100 },
}),
diff --git a/src/patcher/from-docx.ts b/src/patcher/from-docx.ts
index 11c4c9c4ce..1098bf05ea 100644
--- a/src/patcher/from-docx.ts
+++ b/src/patcher/from-docx.ts
@@ -215,7 +215,7 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO
zip.file(key, value);
}
- for (const { stream, fileName } of file.Media.Array) {
+ for (const { data: stream, fileName } of file.Media.Array) {
zip.file(`word/media/${fileName}`, stream);
}
diff --git a/vite.config.ts b/vite.config.ts
index d2fdcd127e..e5d830db50 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -62,10 +62,10 @@ export default defineConfig({
provider: "v8",
reporter: ["text", "json", "html"],
thresholds: {
- statements: 99.98,
- branches: 99.15,
+ statements: 100,
+ branches: 99.35,
functions: 100,
- lines: 99.98,
+ lines: 100,
},
exclude: [
...configDefaults.exclude,