diff --git a/.github/workflows/demos.yml b/.github/workflows/demos.yml index 4444adec84..8f9675f05e 100644 --- a/.github/workflows/demos.yml +++ b/.github/workflows/demos.yml @@ -73,15 +73,15 @@ jobs: with: xml-file: build/extracted-doc/word/document.xml xml-schema-file: ooxml-schemas/microsoft/wml-2010.xsd - # - name: Run Demo - # run: npm run ts-node -- ./demo/5-images.ts - # - name: Extract Word Document - # run: npm run extract - # - name: Validate XML - # uses: ChristophWurst/xmllint-action@v1 - # with: - # xml-file: build/extracted-doc/word/document.xml - # xml-schema-file: ooxml-schemas/microsoft/wml-2010.xsd + - name: Run Demo + run: npm run ts-node -- ./demo/5-images.ts + - name: Extract Word Document + run: npm run extract + - name: Validate XML + uses: ChristophWurst/xmllint-action@v1 + with: + xml-file: build/extracted-doc/word/document.xml + xml-schema-file: ooxml-schemas/microsoft/wml-2010.xsd - name: Run Demo run: npm run ts-node -- ./demo/6-page-borders.ts - name: Extract Word Document diff --git a/demo/5-images.ts b/demo/5-images.ts index 3d1da1633c..e6496266fe 100644 --- a/demo/5-images.ts +++ b/demo/5-images.ts @@ -25,6 +25,11 @@ const doc = new Document({ width: 100, height: 100, }, + altText: { + title: "This is an ultimate title", + description: "This is an ultimate image", + name: "My Ultimate Image", + }, }), ], }), diff --git a/docs/usage/images.md b/docs/usage/images.md index 218d4e260c..1326a780b2 100644 --- a/docs/usage/images.md +++ b/docs/usage/images.md @@ -252,13 +252,36 @@ const image = new ImageRun({ }); ``` +## Alternative Text + +Specifies common non-visual DrawingML properties. A name, title and description for a picture can be specified. + +```ts +const image = new ImageRun({ + data: fs.readFileSync("./demo/images/pizza.gif"), + altText: { + title: "This is an ultimate title", + description: "This is an ultimate image", + name: "My Ultimate Image", + }, +}); +``` + +### Options + +| Property | Type | Notes | Possible Values | +| ----------- | -------- | -------- | ------------------------------------ | +| name | `string` | Required | `Specimen A` | +| title | `string` | Required | `My awesome title of my image` | +| description | `string` | Required | `My awesome description of my image` | + ## Examples ### Add image to the document Importing Images from file system path -[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/5-images.ts ':include') +[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/5-images.ts ":include") _Source: https://github.com/dolanmiu/docx/blob/master/demo/5-images.ts_ @@ -266,7 +289,7 @@ _Source: https://github.com/dolanmiu/docx/blob/master/demo/5-images.ts_ Example showing how to add image to headers and footers -[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/9-images-in-header-and-footer.ts ':include') +[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/9-images-in-header-and-footer.ts ":include") _Source: https://github.com/dolanmiu/docx/blob/master/demo/9-images-in-header-and-footer.ts_ @@ -274,6 +297,6 @@ _Source: https://github.com/dolanmiu/docx/blob/master/demo/9-images-in-header-an Example showing how to float images on top of text and optimally give a `margin` -[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/38-text-wrapping.ts ':include') +[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/38-text-wrapping.ts ":include") _Source: https://github.com/dolanmiu/docx/blob/master/demo/38-text-wrapping.ts_ diff --git a/src/file/drawing/anchor/anchor.spec.ts b/src/file/drawing/anchor/anchor.spec.ts index 17ccb0f165..d2da912761 100644 --- a/src/file/drawing/anchor/anchor.spec.ts +++ b/src/file/drawing/anchor/anchor.spec.ts @@ -1,4 +1,8 @@ -import { assert } from "chai"; +import { assert, expect } from "chai"; +import { SinonStub, stub } from "sinon"; + +import { Formatter } from "@export/formatter"; +import * as convenienceFunctions from "@util/convenience-functions"; import { Utility } from "tests/utility"; @@ -36,6 +40,14 @@ const createAnchor = (drawingOptions: IDrawingOptions): Anchor => ); describe("Anchor", () => { + before(() => { + stub(convenienceFunctions, "uniqueNumericId").callsFake(() => 0); + }); + + after(() => { + (convenienceFunctions.uniqueNumericId as SinonStub).restore(); + }); + let anchor: Anchor; describe("#constructor()", () => { @@ -362,5 +374,236 @@ describe("Anchor", () => { relativeHeight: 120, }); }); + + it("should create a Drawing with doc properties", () => { + anchor = createAnchor({ + floating: { + verticalPosition: { + offset: 0, + }, + horizontalPosition: { + offset: 0, + }, + zIndex: 120, + }, + docProperties: { + name: "test", + description: "test", + title: "test", + }, + }); + const tree = new Formatter().format(anchor); + expect(tree).to.deep.equal({ + "wp:anchor": [ + { + _attr: { + allowOverlap: "1", + behindDoc: "0", + distB: 0, + distL: 0, + distR: 0, + distT: 0, + layoutInCell: "1", + locked: "0", + relativeHeight: 120, + simplePos: "0", + }, + }, + { + "wp:simplePos": { + _attr: { + x: 0, + y: 0, + }, + }, + }, + { + "wp:positionH": [ + { + _attr: { + relativeFrom: "page", + }, + }, + { + "wp:posOffset": ["0"], + }, + ], + }, + { + "wp:positionV": [ + { + _attr: { + relativeFrom: "page", + }, + }, + { + "wp:posOffset": ["0"], + }, + ], + }, + { + "wp:extent": { + _attr: { + cx: 952500, + cy: 952500, + }, + }, + }, + { + "wp:effectExtent": { + _attr: { + b: 0, + l: 0, + r: 0, + t: 0, + }, + }, + }, + { + "wp:wrapNone": {}, + }, + { + "wp:docPr": { + _attr: { + descr: "test", + id: 0, + name: "test", + title: "test", + }, + }, + }, + { + "wp:cNvGraphicFramePr": [ + { + "a:graphicFrameLocks": { + _attr: { + noChangeAspect: 1, + "xmlns:a": "http://schemas.openxmlformats.org/drawingml/2006/main", + }, + }, + }, + ], + }, + { + "a:graphic": [ + { + _attr: { + "xmlns:a": "http://schemas.openxmlformats.org/drawingml/2006/main", + }, + }, + { + "a:graphicData": [ + { + _attr: { + uri: "http://schemas.openxmlformats.org/drawingml/2006/picture", + }, + }, + { + "pic:pic": [ + { + _attr: { + "xmlns:pic": "http://schemas.openxmlformats.org/drawingml/2006/picture", + }, + }, + { + "pic:nvPicPr": [ + { + "pic:cNvPr": { + _attr: { + descr: "", + id: 0, + name: "", + }, + }, + }, + { + "pic:cNvPicPr": [ + { + "a:picLocks": { + _attr: { + noChangeArrowheads: 1, + noChangeAspect: 1, + }, + }, + }, + ], + }, + ], + }, + { + "pic:blipFill": [ + { + "a:blip": { + _attr: { + cstate: "none", + "r:embed": "rId{test.png}", + }, + }, + }, + { + "a:srcRect": {}, + }, + { + "a:stretch": [ + { + "a:fillRect": {}, + }, + ], + }, + ], + }, + { + "pic:spPr": [ + { + _attr: { + bwMode: "auto", + }, + }, + { + "a:xfrm": [ + { + _attr: {}, + }, + { + "a:off": { + _attr: { + x: 0, + y: 0, + }, + }, + }, + { + "a:ext": { + _attr: { + cx: 952500, + cy: 952500, + }, + }, + }, + ], + }, + { + "a:prstGeom": [ + { + _attr: { + prst: "rect", + }, + }, + { + "a:avLst": {}, + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }); + }); }); }); diff --git a/src/file/drawing/anchor/anchor.ts b/src/file/drawing/anchor/anchor.ts index 154965cdd7..d316c26162 100644 --- a/src/file/drawing/anchor/anchor.ts +++ b/src/file/drawing/anchor/anchor.ts @@ -90,7 +90,7 @@ export class Anchor extends XmlComponent { this.root.push(new WrapNone()); } - this.root.push(new DocProperties()); + this.root.push(new DocProperties(drawingOptions.docProperties)); this.root.push(new GraphicFrameProperties()); this.root.push(new Graphic(mediaData, transform)); } diff --git a/src/file/drawing/doc-properties/doc-properties-attributes.ts b/src/file/drawing/doc-properties/doc-properties-attributes.ts deleted file mode 100644 index a65aa9352b..0000000000 --- a/src/file/drawing/doc-properties/doc-properties-attributes.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { XmlAttributeComponent } from "@file/xml-components"; - -export class DocPropertiesAttributes extends XmlAttributeComponent<{ - readonly id?: number; - readonly name?: string; - readonly descr?: string; -}> { - protected readonly xmlKeys = { - id: "id", - name: "name", - descr: "descr", - }; -} diff --git a/src/file/drawing/doc-properties/doc-properties.ts b/src/file/drawing/doc-properties/doc-properties.ts index 45aaf919b6..0724cd9513 100644 --- a/src/file/drawing/doc-properties/doc-properties.ts +++ b/src/file/drawing/doc-properties/doc-properties.ts @@ -1,15 +1,36 @@ -import { XmlComponent } from "@file/xml-components"; -import { DocPropertiesAttributes } from "./doc-properties-attributes"; +import { XmlAttributeComponent, XmlComponent } from "@file/xml-components"; +import { uniqueNumericId } from "@util/convenience-functions"; + +class DocPropertiesAttributes extends XmlAttributeComponent<{ + readonly id?: number; + readonly name?: string; + readonly description?: string; + readonly title?: string; +}> { + protected readonly xmlKeys = { + id: "id", + name: "name", + description: "descr", + title: "title", + }; +} + +export interface DocPropertiesOptions { + readonly name: string; + readonly description: string; + readonly title: string; +} export class DocProperties extends XmlComponent { - public constructor() { + public constructor({ name, description, title }: DocPropertiesOptions = { name: "", description: "", title: "" }) { super("wp:docPr"); this.root.push( new DocPropertiesAttributes({ - id: 0, - name: "", - descr: "", + id: uniqueNumericId(), + name, + description, + title, }), ); } diff --git a/src/file/drawing/drawing.spec.ts b/src/file/drawing/drawing.spec.ts index b5b48d80bd..4b01e84a81 100644 --- a/src/file/drawing/drawing.spec.ts +++ b/src/file/drawing/drawing.spec.ts @@ -1,6 +1,8 @@ import { expect } from "chai"; +import { SinonStub, stub } from "sinon"; import { Formatter } from "@export/formatter"; +import * as convenienceFunctions from "@util/convenience-functions"; import { Drawing, IDrawingOptions } from "./drawing"; @@ -26,6 +28,14 @@ const createDrawing = (drawingOptions?: IDrawingOptions): Drawing => ); describe("Drawing", () => { + before(() => { + stub(convenienceFunctions, "uniqueNumericId").callsFake(() => 0); + }); + + after(() => { + (convenienceFunctions.uniqueNumericId as SinonStub).restore(); + }); + let currentBreak: Drawing; describe("#constructor()", () => { @@ -68,6 +78,7 @@ describe("Drawing", () => { descr: "", id: 0, name: "", + title: "", }, }, }, @@ -298,6 +309,7 @@ describe("Drawing", () => { descr: "", id: 0, name: "", + title: "", }, }, }, diff --git a/src/file/drawing/drawing.ts b/src/file/drawing/drawing.ts index 8fc08ff454..4bb83a9462 100644 --- a/src/file/drawing/drawing.ts +++ b/src/file/drawing/drawing.ts @@ -1,6 +1,8 @@ import { IMediaData } from "@file/media"; import { XmlComponent } from "@file/xml-components"; + import { Anchor } from "./anchor"; +import { DocPropertiesOptions } from "./doc-properties/doc-properties"; import { IFloating } from "./floating"; import { Inline } from "./inline"; @@ -13,6 +15,7 @@ export interface IDistance { export interface IDrawingOptions { readonly floating?: IFloating; + readonly docProperties?: DocPropertiesOptions; } // @@ -29,7 +32,11 @@ export class Drawing extends XmlComponent { super("w:drawing"); if (!drawingOptions.floating) { - this.inline = new Inline(imageData, imageData.transformation); + this.inline = new Inline({ + mediaData: imageData, + transform: imageData.transformation, + docProperties: drawingOptions.docProperties, + }); this.root.push(this.inline); } else { this.root.push(new Anchor(imageData, imageData.transformation, drawingOptions)); diff --git a/src/file/drawing/inline/inline.ts b/src/file/drawing/inline/inline.ts index b76781d465..b5689b05e8 100644 --- a/src/file/drawing/inline/inline.ts +++ b/src/file/drawing/inline/inline.ts @@ -1,13 +1,19 @@ // http://officeopenxml.com/drwPicInline.php import { IMediaData, IMediaDataTransformation } from "@file/media"; import { XmlComponent } from "@file/xml-components"; -import { DocProperties } from "./../doc-properties/doc-properties"; +import { DocProperties, DocPropertiesOptions } from "./../doc-properties/doc-properties"; import { EffectExtent } from "./../effect-extent/effect-extent"; import { Extent } from "./../extent/extent"; import { GraphicFrameProperties } from "./../graphic-frame/graphic-frame-properties"; import { Graphic } from "./../inline/graphic"; import { InlineAttributes } from "./inline-attributes"; +interface InlineOptions { + readonly mediaData: IMediaData; + readonly transform: IMediaDataTransformation; + readonly docProperties?: DocPropertiesOptions; +} + // // // @@ -26,7 +32,7 @@ export class Inline extends XmlComponent { private readonly extent: Extent; private readonly graphic: Graphic; - public constructor(mediaData: IMediaData, transform: IMediaDataTransformation) { + public constructor({ mediaData, transform, docProperties }: InlineOptions) { super("wp:inline"); this.root.push( @@ -43,7 +49,7 @@ export class Inline extends XmlComponent { this.root.push(this.extent); this.root.push(new EffectExtent()); - this.root.push(new DocProperties()); + this.root.push(new DocProperties(docProperties)); this.root.push(new GraphicFrameProperties()); this.root.push(this.graphic); } diff --git a/src/file/paragraph/paragraph.spec.ts b/src/file/paragraph/paragraph.spec.ts index 327ebf381f..ad32517207 100644 --- a/src/file/paragraph/paragraph.spec.ts +++ b/src/file/paragraph/paragraph.spec.ts @@ -25,6 +25,7 @@ describe("Paragraph", () => { after(() => { (convenienceFunctions.uniqueId as SinonStub).restore(); + (convenienceFunctions.uniqueNumericId as SinonStub).restore(); }); describe("#constructor()", () => { diff --git a/src/file/paragraph/run/image-run.spec.ts b/src/file/paragraph/run/image-run.spec.ts index c59ddd1ff4..37bd72d753 100644 --- a/src/file/paragraph/run/image-run.spec.ts +++ b/src/file/paragraph/run/image-run.spec.ts @@ -11,10 +11,12 @@ import { ImageRun } from "./image-run"; describe("ImageRun", () => { before(() => { stub(convenienceFunctions, "uniqueId").callsFake(() => "test-unique-id"); + stub(convenienceFunctions, "uniqueNumericId").callsFake(() => 0); }); after(() => { (convenienceFunctions.uniqueId as SinonStub).restore(); + (convenienceFunctions.uniqueNumericId as SinonStub).restore(); }); describe("#constructor()", () => { @@ -125,6 +127,7 @@ describe("ImageRun", () => { descr: "", id: 0, name: "", + title: "", }, }, }, @@ -375,6 +378,7 @@ describe("ImageRun", () => { descr: "", id: 0, name: "", + title: "", }, }, }, @@ -629,6 +633,7 @@ describe("ImageRun", () => { descr: "", id: 0, name: "", + title: "", }, }, }, @@ -886,6 +891,7 @@ describe("ImageRun", () => { descr: "", id: 0, name: "", + title: "", }, }, }, diff --git a/src/file/paragraph/run/image-run.ts b/src/file/paragraph/run/image-run.ts index 849f7c6306..df8dcda705 100644 --- a/src/file/paragraph/run/image-run.ts +++ b/src/file/paragraph/run/image-run.ts @@ -1,6 +1,7 @@ import { uniqueId } from "@util/convenience-functions"; import { IContext, IXmlableObject } from "@file/xml-components"; +import { DocPropertiesOptions } from "@file/drawing/doc-properties/doc-properties"; import { Drawing, IFloating } from "../../drawing"; import { IMediaTransformation } from "../../media"; @@ -11,6 +12,7 @@ export interface IImageOptions { readonly data: Buffer | string | Uint8Array | ArrayBuffer; readonly transformation: IMediaTransformation; readonly floating?: IFloating; + readonly altText?: DocPropertiesOptions; } export class ImageRun extends Run { @@ -37,7 +39,7 @@ export class ImageRun extends Run { rotation: options.transformation.rotation ? options.transformation.rotation * 60000 : undefined, }, }; - const drawing = new Drawing(this.imageData, { floating: options.floating }); + const drawing = new Drawing(this.imageData, { floating: options.floating, docProperties: options.altText }); this.root.push(drawing); } @@ -49,15 +51,16 @@ export class ImageRun extends Run { } private convertDataURIToBinary(dataURI: string): Uint8Array { - // https://gist.github.com/borismus/1032746 - // https://github.com/mafintosh/base64-to-uint8array - const BASE64_MARKER = ";base64,"; - - const base64Index = dataURI.indexOf(BASE64_MARKER) + BASE64_MARKER.length; - 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(base64Index)) + atob(dataURI.substring(base64IndexWithOffset)) .split("") .map((c) => c.charCodeAt(0)), );