diff --git a/.cspell.json b/.cspell.json index c27f9e3734..1e8efa7a1e 100644 --- a/.cspell.json +++ b/.cspell.json @@ -54,7 +54,8 @@ "\\.to\\.include\\.members\\(\\[[^\\]]+]\\)", "/new [a-zA-Z]+\\({[^£]+}\\)/g", "/ [ "Vivamus euismod, sem eget molestie rhoncus, metus dui laoreet lacus, et lobortis est metus ut felis. Aenean imperdiet lacus nunc, vitae molestie orci ultrices nec. Cras egestas maximus diam, vitae efficitur nisl luctus imperdiet. Nulla pellentesque sodales ante, nec facilisis turpis. Vivamus at hendrerit enim, ac dictum lorem. Cras ", ), new Paragraph( - "congue accumsan dui, non pretium sem auctor quis. Nunc a mauris vehicula, elementum ex vitae, sollicitudin eros. Nunc non sapien vitae justo sodales condimentum. Praesent nisl felis, tristique ac odio at, rhoncus porttitor orci. Morbi egestas placerat iaculis. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In at lorem nec neque faucibus ultricies ut in ipsum.", + "congue accumsan dui, non pretium sem auctor quis. Nunc a mauris vehicula, elementum ex vitae, sollicitudin eros. Nunc non sapien vitae justo sodales condimentum. Praesent nisl felis, tristique ac odio at, rhoncus porttitor orci. Morbi egestas placerat iaculis.", + ), + new Paragraph( + "Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In at lorem nec neque faucibus ultricies ut in ipsum.", ), - new Paragraph( "Suspendisse fermentum feugiat augue eu convallis. Maecenas eros velit, efficitur sit amet posuere sed, tristique sit amet nisi. Donec vel convallis justo, id tempor neque. Nunc pulvinar maximus nulla, vitae congue lacus cursus ut. Morbi at mollis est, a vehicula nisi. Cras venenatis nibh vel massa interdum commodo. Nulla mattis ", ), diff --git a/demo/5-images.ts b/demo/5-images.ts index a0b05de3e8..6039d6c832 100644 --- a/demo/5-images.ts +++ b/demo/5-images.ts @@ -2,6 +2,7 @@ import * as fs from "fs"; import { + convertMillimetersToTwip, Document, HorizontalPositionAlign, HorizontalPositionRelativeFrom, @@ -41,6 +42,11 @@ const doc = new Document({ width: 100, height: 100, }, + outline: { + type: "solidFill", + solidFillType: "rgb", + value: "FF0000", + }, }), ], }), @@ -55,6 +61,12 @@ const doc = new Document({ vertical: true, }, }, + outline: { + type: "solidFill", + solidFillType: "rgb", + value: "0000FF", + width: convertMillimetersToTwip(600), + }, }), ], }), diff --git a/src/file/drawing/anchor/anchor.spec.ts b/src/file/drawing/anchor/anchor.spec.ts index 5b5a0da764..215aa087db 100644 --- a/src/file/drawing/anchor/anchor.spec.ts +++ b/src/file/drawing/anchor/anchor.spec.ts @@ -9,10 +9,10 @@ import { TextWrappingType } from "../text-wrap"; import { Anchor } from "./anchor"; const createAnchor = (drawingOptions: IDrawingOptions): Anchor => - new Anchor( - { + new Anchor({ + mediaData: { fileName: "test.png", - stream: new Buffer(""), + stream: Buffer.from(""), transformation: { pixels: { x: 0, @@ -24,7 +24,7 @@ const createAnchor = (drawingOptions: IDrawingOptions): Anchor => }, }, }, - { + transform: { pixels: { x: 100, y: 100, @@ -35,7 +35,7 @@ const createAnchor = (drawingOptions: IDrawingOptions): Anchor => }, }, drawingOptions, - ); + }); describe("Anchor", () => { let anchor: Anchor; diff --git a/src/file/drawing/anchor/anchor.ts b/src/file/drawing/anchor/anchor.ts index d316c26162..e674e4ce20 100644 --- a/src/file/drawing/anchor/anchor.ts +++ b/src/file/drawing/anchor/anchor.ts @@ -6,7 +6,7 @@ import { HorizontalPosition, IFloating, SimplePos, VerticalPosition } from "../f import { Graphic } from "../inline/graphic"; import { TextWrappingType, WrapNone, WrapSquare, WrapTight, WrapTopAndBottom } from "../text-wrap"; import { DocProperties } from "./../doc-properties/doc-properties"; -import { EffectExtent } from "./../effect-extent/effect-extent"; +import { createEffectExtent } from "./../effect-extent/effect-extent"; import { Extent } from "./../extent/extent"; import { GraphicFrameProperties } from "./../graphic-frame/graphic-frame-properties"; import { AnchorAttributes } from "./anchor-attributes"; @@ -37,7 +37,15 @@ import { AnchorAttributes } from "./anchor-attributes"; // // export class Anchor extends XmlComponent { - public constructor(mediaData: IMediaData, transform: IMediaDataTransformation, drawingOptions: IDrawingOptions) { + public constructor({ + mediaData, + transform, + drawingOptions, + }: { + readonly mediaData: IMediaData; + readonly transform: IMediaDataTransformation; + readonly drawingOptions: IDrawingOptions; + }) { super("wp:anchor"); const floating: IFloating = { @@ -69,7 +77,7 @@ export class Anchor extends XmlComponent { this.root.push(new HorizontalPosition(floating.horizontalPosition)); this.root.push(new VerticalPosition(floating.verticalPosition)); this.root.push(new Extent(transform.emus.x, transform.emus.y)); - this.root.push(new EffectExtent()); + this.root.push(createEffectExtent({ top: 0, right: 0, bottom: 0, left: 0 })); if (drawingOptions.floating !== undefined && drawingOptions.floating.wrap !== undefined) { switch (drawingOptions.floating.wrap.type) { @@ -92,6 +100,6 @@ export class Anchor extends XmlComponent { this.root.push(new DocProperties(drawingOptions.docProperties)); this.root.push(new GraphicFrameProperties()); - this.root.push(new Graphic(mediaData, transform)); + this.root.push(new Graphic({ mediaData, transform, outline: drawingOptions.outline })); } } diff --git a/src/file/drawing/drawing.ts b/src/file/drawing/drawing.ts index 7040ef341e..3664da15a2 100644 --- a/src/file/drawing/drawing.ts +++ b/src/file/drawing/drawing.ts @@ -4,18 +4,20 @@ 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"; +import { createInline } from "./inline"; +import { OutlineOptions } from "./inline/graphic/graphic-data/pic/shape-properties/outline/outline"; -export interface IDistance { +export type IDistance = { readonly distT?: number; readonly distB?: number; readonly distL?: number; readonly distR?: number; -} +}; export interface IDrawingOptions { readonly floating?: IFloating; readonly docProperties?: DocPropertiesOptions; + readonly outline?: OutlineOptions; } // @@ -30,14 +32,16 @@ export class Drawing extends XmlComponent { super("w:drawing"); if (!drawingOptions.floating) { - const inline = new Inline({ - mediaData: imageData, - transform: imageData.transformation, - docProperties: drawingOptions.docProperties, - }); - this.root.push(inline); + this.root.push( + createInline({ + mediaData: imageData, + transform: imageData.transformation, + docProperties: drawingOptions.docProperties, + outline: drawingOptions.outline, + }), + ); } else { - this.root.push(new Anchor(imageData, imageData.transformation, drawingOptions)); + this.root.push(new Anchor({ mediaData: imageData, transform: imageData.transformation, drawingOptions })); } } } diff --git a/src/file/drawing/effect-extent/effect-extent-attributes.ts b/src/file/drawing/effect-extent/effect-extent-attributes.ts deleted file mode 100644 index be6245885e..0000000000 --- a/src/file/drawing/effect-extent/effect-extent-attributes.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { XmlAttributeComponent } from "@file/xml-components"; - -export class EffectExtentAttributes extends XmlAttributeComponent<{ - readonly b?: number; - readonly l?: number; - readonly r?: number; - readonly t?: number; -}> { - protected readonly xmlKeys = { - b: "b", - l: "l", - r: "r", - t: "t", - }; -} diff --git a/src/file/drawing/effect-extent/effect-extent.ts b/src/file/drawing/effect-extent/effect-extent.ts index 209da9f1ff..328d71f229 100644 --- a/src/file/drawing/effect-extent/effect-extent.ts +++ b/src/file/drawing/effect-extent/effect-extent.ts @@ -1,17 +1,31 @@ -import { XmlComponent } from "@file/xml-components"; -import { EffectExtentAttributes } from "./effect-extent-attributes"; +import { BuilderElement, XmlComponent } from "@file/xml-components"; -export class EffectExtent extends XmlComponent { - public constructor() { - super("wp:effectExtent"); +export type EffectExtentAttributes = { + readonly top: number; + readonly right: number; + readonly bottom: number; + readonly left: number; +}; - this.root.push( - new EffectExtentAttributes({ - b: 0, - l: 0, - r: 0, - t: 0, - }), - ); - } -} +export const createEffectExtent = ({ top, right, bottom, left }: EffectExtentAttributes): XmlComponent => + new BuilderElement({ + name: "wp:effectExtent", + attributes: { + top: { + key: "t", + value: top, + }, + right: { + key: "r", + value: right, + }, + bottom: { + key: "b", + value: bottom, + }, + left: { + key: "l", + value: left, + }, + }, + }); diff --git a/src/file/drawing/inline/graphic/graphic-data/graphic-data.ts b/src/file/drawing/inline/graphic/graphic-data/graphic-data.ts index 94a1eaea49..0ca40ef9ed 100644 --- a/src/file/drawing/inline/graphic/graphic-data/graphic-data.ts +++ b/src/file/drawing/inline/graphic/graphic-data/graphic-data.ts @@ -3,11 +3,20 @@ import { XmlComponent } from "@file/xml-components"; import { GraphicDataAttributes } from "./graphic-data-attribute"; import { Pic } from "./pic"; +import { OutlineOptions } from "./pic/shape-properties/outline/outline"; export class GraphicData extends XmlComponent { private readonly pic: Pic; - public constructor(mediaData: IMediaData, transform: IMediaDataTransformation) { + public constructor({ + mediaData, + transform, + outline, + }: { + readonly mediaData: IMediaData; + readonly transform: IMediaDataTransformation; + readonly outline?: OutlineOptions; + }) { super("a:graphicData"); this.root.push( @@ -16,7 +25,7 @@ export class GraphicData extends XmlComponent { }), ); - this.pic = new Pic(mediaData, transform); + this.pic = new Pic({ mediaData, transform, outline }); this.root.push(this.pic); } diff --git a/src/file/drawing/inline/graphic/graphic-data/pic/pic.ts b/src/file/drawing/inline/graphic/graphic-data/pic/pic.ts index 8d49a71599..e05c6aa15a 100644 --- a/src/file/drawing/inline/graphic/graphic-data/pic/pic.ts +++ b/src/file/drawing/inline/graphic/graphic-data/pic/pic.ts @@ -6,9 +6,18 @@ import { BlipFill } from "./blip/blip-fill"; import { NonVisualPicProperties } from "./non-visual-pic-properties/non-visual-pic-properties"; import { PicAttributes } from "./pic-attributes"; import { ShapeProperties } from "./shape-properties/shape-properties"; +import { OutlineOptions } from "./shape-properties/outline/outline"; export class Pic extends XmlComponent { - public constructor(mediaData: IMediaData, transform: IMediaDataTransformation) { + public constructor({ + mediaData, + transform, + outline, + }: { + readonly mediaData: IMediaData; + readonly transform: IMediaDataTransformation; + readonly outline?: OutlineOptions; + }) { super("pic:pic"); this.root.push( @@ -19,6 +28,6 @@ export class Pic extends XmlComponent { this.root.push(new NonVisualPicProperties()); this.root.push(new BlipFill(mediaData)); - this.root.push(new ShapeProperties(transform)); + this.root.push(new ShapeProperties({ transform, outline })); } } diff --git a/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/outline/no-fill.spec.ts b/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/outline/no-fill.spec.ts index 13e18ba80b..6def78f9e6 100644 --- a/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/outline/no-fill.spec.ts +++ b/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/outline/no-fill.spec.ts @@ -2,12 +2,12 @@ import { describe, expect, it } from "vitest"; import { Formatter } from "@export/formatter"; -import { NoFill } from "./no-fill"; +import { createNoFill } from "./no-fill"; describe("NoFill", () => { describe("#constructor()", () => { it("should create", () => { - const tree = new Formatter().format(new NoFill()); + const tree = new Formatter().format(createNoFill()); expect(tree).to.deep.equal({ "a:noFill": {}, }); diff --git a/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/outline/no-fill.ts b/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/outline/no-fill.ts index 808a54f84a..22fc607fb6 100644 --- a/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/outline/no-fill.ts +++ b/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/outline/no-fill.ts @@ -1,7 +1,3 @@ -import { XmlComponent } from "@file/xml-components"; +import { BuilderElement, XmlComponent } from "@file/xml-components"; -export class NoFill extends XmlComponent { - public constructor() { - super("a:noFill"); - } -} +export const createNoFill = (): XmlComponent => new BuilderElement({ name: "a:noFill" }); diff --git a/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/outline/outline.spec.ts b/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/outline/outline.spec.ts index 961f3d1595..1716d07457 100644 --- a/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/outline/outline.spec.ts +++ b/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/outline/outline.spec.ts @@ -1,19 +1,66 @@ import { describe, expect, it } from "vitest"; import { Formatter } from "@export/formatter"; -import { Outline } from "./outline"; -describe("Outline", () => { - describe("#constructor()", () => { - it("should create", () => { - const tree = new Formatter().format(new Outline()); - expect(tree).to.deep.equal({ - "a:ln": [ - { - "a:noFill": {}, - }, - ], - }); +import { createOutline } from "./outline"; +import { SchemeColor } from "./scheme-color"; + +describe("createOutline", () => { + it("should create no fill", () => { + const tree = new Formatter().format(createOutline({ type: "noFill" })); + expect(tree).to.deep.equal({ + "a:ln": [ + { + _attr: {}, + }, + { + "a:noFill": {}, + }, + ], + }); + }); + + it("should create solid rgb fill", () => { + const tree = new Formatter().format(createOutline({ type: "solidFill", solidFillType: "rgb", value: "FFFFFF" })); + expect(tree).to.deep.equal({ + "a:ln": [ + { + _attr: {}, + }, + { + "a:solidFill": [ + { + "a:srgbClr": { + _attr: { + val: "FFFFFF", + }, + }, + }, + ], + }, + ], + }); + }); + + it("should create solid scheme fill", () => { + const tree = new Formatter().format(createOutline({ type: "solidFill", solidFillType: "scheme", value: SchemeColor.ACCENT1 })); + expect(tree).to.deep.equal({ + "a:ln": [ + { + _attr: {}, + }, + { + "a:solidFill": [ + { + "a:schemeClr": { + _attr: { + val: "accent1", + }, + }, + }, + ], + }, + ], }); }); }); diff --git a/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/outline/outline.ts b/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/outline/outline.ts index 367934e6a4..ed274299d1 100644 --- a/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/outline/outline.ts +++ b/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/outline/outline.ts @@ -1,11 +1,130 @@ // http://officeopenxml.com/drwSp-outline.php -import { XmlComponent } from "@file/xml-components"; -import { NoFill } from "./no-fill"; +import { BuilderElement, XmlComponent } from "@file/xml-components"; +import { createNoFill } from "./no-fill"; +import { createSolidFill } from "./solid-fill"; +import { SchemeColor } from "./scheme-color"; -export class Outline extends XmlComponent { - public constructor() { - super("a:ln"); +// +// +// +// +// +// +// +// +// +// +// - this.root.push(new NoFill()); - } -} +// +// +// +// +// +// +// +export const LineCap = { + ROUND: "rnd", + SQUARE: "sq", + FLAT: "flat", +} as const; + +// +// +// +// +// +// +// +// +// +export const CompoundLine = { + SINGLE: "sng", + DOUBLE: "dbl", + THICK_THIN: "thickThin", + THIN_THICK: "thinThick", + TRI: "tri", +} as const; + +// +// +// +// +// +// +export const PenAlignment = { + CENTER: "ctr", + INSET: "in", +} as const; + +export type OutlineAttributes = { + readonly width?: number; + readonly cap?: keyof typeof LineCap; + readonly compoundLine?: keyof typeof CompoundLine; + readonly align?: keyof typeof PenAlignment; +}; + +type OutlineNoFill = { + readonly type: "noFill"; +}; + +type OutlineRgbSolidFill = { + readonly type: "solidFill"; + readonly solidFillType: "rgb"; + readonly value: string; +}; + +type OutlineSchemeSolidFill = { + readonly type: "solidFill"; + readonly solidFillType: "scheme"; + readonly value: (typeof SchemeColor)[keyof typeof SchemeColor]; +}; + +type OutlineSolidFill = OutlineRgbSolidFill | OutlineSchemeSolidFill; + +// +// +// +// +// +// +// +type OutlineFillProperties = OutlineNoFill | OutlineSolidFill; + +export type OutlineOptions = OutlineAttributes & OutlineFillProperties; + +export const createOutline = (options: OutlineOptions): XmlComponent => + new BuilderElement({ + name: "a:ln", + attributes: { + width: { + key: "w", + value: options.width, + }, + cap: { + key: "cap", + value: options.cap, + }, + compoundLine: { + key: "cmpd", + value: options.compoundLine, + }, + align: { + key: "algn", + value: options.align, + }, + }, + children: [ + options.type === "noFill" + ? createNoFill() + : options.solidFillType === "rgb" + ? createSolidFill({ + type: "rgb", + value: options.value, + }) + : createSolidFill({ + type: "scheme", + value: options.value, + }), + ], + }); diff --git a/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/outline/rgb-color.ts b/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/outline/rgb-color.ts new file mode 100644 index 0000000000..abc28ffc45 --- /dev/null +++ b/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/outline/rgb-color.ts @@ -0,0 +1,22 @@ +import { BuilderElement, XmlComponent } from "@file/xml-components"; + +type SolidRgbColorOptions = { + readonly value: string; +}; + +// +// +// +// +// +// +export const createSolidRgbColor = (options: SolidRgbColorOptions): XmlComponent => + new BuilderElement({ + name: "a:srgbClr", + attributes: { + value: { + key: "val", + value: options.value, + }, + }, + }); diff --git a/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/outline/scheme-color.ts b/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/outline/scheme-color.ts new file mode 100644 index 0000000000..17004133a2 --- /dev/null +++ b/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/outline/scheme-color.ts @@ -0,0 +1,65 @@ +import { BuilderElement, XmlComponent } from "@file/xml-components"; + +type SchemeColorOptions = { + readonly value: (typeof SchemeColor)[keyof typeof SchemeColor]; +}; + +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// + +// cspell:ignore folHlink, phClr, hlink +export const SchemeColor = { + BG1: "bg1", + TX1: "tx1", + BG2: "bg2", + TX2: "tx2", + ACCENT1: "accent1", + ACCENT2: "accent2", + ACCENT3: "accent3", + ACCENT4: "accent4", + ACCENT5: "accent5", + ACCENT6: "accent6", + HLINK: "hlink", + FOLHLINK: "folHlink", + DK1: "dk1", + LT1: "lt1", + DK2: "dk2", + LT2: "lt2", + PHCLR: "phClr", +} as const; + +// +// +// +// +// +// +export const createSchemeColor = (options: SchemeColorOptions): XmlComponent => + new BuilderElement({ + name: "a:schemeClr", + attributes: { + value: { + key: "val", + value: options.value, + }, + }, + }); diff --git a/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/outline/solid-fill.spec.ts b/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/outline/solid-fill.spec.ts new file mode 100644 index 0000000000..e66a53c4fe --- /dev/null +++ b/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/outline/solid-fill.spec.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from "vitest"; + +import { Formatter } from "@export/formatter"; + +import { createSolidFill } from "./solid-fill"; +import { SchemeColor } from "./scheme-color"; + +describe("createSolidFill", () => { + it("should create of rgb", () => { + const tree = new Formatter().format(createSolidFill({ type: "rgb", value: "FFFFFF" })); + expect(tree).to.deep.equal({ + "a:solidFill": [ + { + "a:srgbClr": { + _attr: { + val: "FFFFFF", + }, + }, + }, + ], + }); + }); + + it("should create of scheme", () => { + const tree = new Formatter().format(createSolidFill({ type: "scheme", value: SchemeColor.TX1 })); + expect(tree).to.deep.equal({ + "a:solidFill": [ + { + "a:schemeClr": { + _attr: { + val: "tx1", + }, + }, + }, + ], + }); + }); +}); diff --git a/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/outline/solid-fill.ts b/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/outline/solid-fill.ts new file mode 100644 index 0000000000..ee3d58777a --- /dev/null +++ b/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/outline/solid-fill.ts @@ -0,0 +1,22 @@ +import { BuilderElement, XmlComponent } from "@file/xml-components"; + +import { createSchemeColor, SchemeColor } from "./scheme-color"; +import { createSolidRgbColor } from "./rgb-color"; + +export type RgbColorOptions = { + readonly type: "rgb"; + readonly value: string; +}; + +export type SchemeColorOptions = { + readonly type: "scheme"; + readonly value: (typeof SchemeColor)[keyof typeof SchemeColor]; +}; + +export type SolidFillOptions = RgbColorOptions | SchemeColorOptions; + +export const createSolidFill = (options: SolidFillOptions): XmlComponent => + new BuilderElement({ + name: "a:solidFill", + children: [options.type === "rgb" ? createSolidRgbColor(options) : createSchemeColor(options)], + }); diff --git a/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/shape-properties.ts b/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/shape-properties.ts index f5f2b8b3e7..8b9effc44d 100644 --- a/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/shape-properties.ts +++ b/src/file/drawing/inline/graphic/graphic-data/pic/shape-properties/shape-properties.ts @@ -2,15 +2,15 @@ import { IMediaDataTransformation } from "@file/media"; import { XmlComponent } from "@file/xml-components"; import { Form } from "./form"; -// import { NoFill } from "./no-fill"; -// import { Outline } from "./outline/outline"; +import { OutlineOptions, createOutline } from "./outline/outline"; import { PresetGeometry } from "./preset-geometry/preset-geometry"; import { ShapePropertiesAttributes } from "./shape-properties-attributes"; +import { createNoFill } from "./outline/no-fill"; export class ShapeProperties extends XmlComponent { private readonly form: Form; - public constructor(transform: IMediaDataTransformation) { + public constructor({ outline, transform }: { readonly outline?: OutlineOptions; readonly transform: IMediaDataTransformation }) { super("pic:spPr"); this.root.push( @@ -23,7 +23,10 @@ export class ShapeProperties extends XmlComponent { this.root.push(this.form); this.root.push(new PresetGeometry()); - // this.root.push(new NoFill()); - // this.root.push(new Outline()); + + if (outline) { + this.root.push(createNoFill()); + this.root.push(createOutline(outline)); + } } } diff --git a/src/file/drawing/inline/graphic/graphic.ts b/src/file/drawing/inline/graphic/graphic.ts index a1e3cd8057..9289eee49c 100644 --- a/src/file/drawing/inline/graphic/graphic.ts +++ b/src/file/drawing/inline/graphic/graphic.ts @@ -2,6 +2,7 @@ import { IMediaData, IMediaDataTransformation } from "@file/media"; import { XmlAttributeComponent, XmlComponent } from "@file/xml-components"; import { GraphicData } from "./graphic-data"; +import { OutlineOptions } from "./graphic-data/pic/shape-properties/outline/outline"; class GraphicAttributes extends XmlAttributeComponent<{ readonly a: string; @@ -14,7 +15,15 @@ class GraphicAttributes extends XmlAttributeComponent<{ export class Graphic extends XmlComponent { private readonly data: GraphicData; - public constructor(mediaData: IMediaData, transform: IMediaDataTransformation) { + public constructor({ + mediaData, + transform, + outline, + }: { + readonly mediaData: IMediaData; + readonly transform: IMediaDataTransformation; + readonly outline?: OutlineOptions; + }) { super("a:graphic"); this.root.push( new GraphicAttributes({ @@ -22,7 +31,7 @@ export class Graphic extends XmlComponent { }), ); - this.data = new GraphicData(mediaData, transform); + this.data = new GraphicData({ mediaData, transform, outline }); this.root.push(this.data); } diff --git a/src/file/drawing/inline/inline-attributes.ts b/src/file/drawing/inline/inline-attributes.ts deleted file mode 100644 index cd61e8953d..0000000000 --- a/src/file/drawing/inline/inline-attributes.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { XmlAttributeComponent } from "@file/xml-components"; -import { IDistance } from "../drawing"; - -// distT, distB etc have no effect on inline images, only floating -export class InlineAttributes extends XmlAttributeComponent { - protected readonly xmlKeys = { - distT: "distT", - distB: "distB", - distL: "distL", - distR: "distR", - }; -} diff --git a/src/file/drawing/inline/inline.spec.ts b/src/file/drawing/inline/inline.spec.ts new file mode 100644 index 0000000000..fca5ed61c9 --- /dev/null +++ b/src/file/drawing/inline/inline.spec.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from "vitest"; + +import { Formatter } from "@export/formatter"; +import { createInline } from "./inline"; + +describe("Inline", () => { + it("should create with default effect extent", () => { + const tree = new Formatter().format( + createInline({ + mediaData: { + fileName: "test.png", + stream: Buffer.from(""), + transformation: { + pixels: { + x: 0, + y: 0, + }, + emus: { + x: 0, + y: 0, + }, + }, + }, + transform: { + pixels: { + x: 100, + y: 100, + }, + emus: { + x: 100, + y: 100, + }, + }, + docProperties: { + name: "test", + description: "test", + title: "test", + }, + outline: { type: "solidFill", solidFillType: "rgb", value: "FFFFFF" }, + }), + ); + + expect(tree).toStrictEqual({ + "wp:inline": expect.arrayContaining([ + { + "wp:effectExtent": { + _attr: { + b: 19050, + l: 19050, + r: 19050, + t: 19050, + }, + }, + }, + ]), + }); + }); +}); diff --git a/src/file/drawing/inline/inline.ts b/src/file/drawing/inline/inline.ts index b5689b05e8..e752c9faa5 100644 --- a/src/file/drawing/inline/inline.ts +++ b/src/file/drawing/inline/inline.ts @@ -1,18 +1,19 @@ // http://officeopenxml.com/drwPicInline.php import { IMediaData, IMediaDataTransformation } from "@file/media"; -import { XmlComponent } from "@file/xml-components"; +import { BuilderElement, XmlComponent } from "@file/xml-components"; import { DocProperties, DocPropertiesOptions } from "./../doc-properties/doc-properties"; -import { EffectExtent } from "./../effect-extent/effect-extent"; +import { createEffectExtent } 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"; +import { OutlineOptions } from "./graphic/graphic-data/pic/shape-properties/outline/outline"; -interface InlineOptions { +type InlineOptions = { readonly mediaData: IMediaData; readonly transform: IMediaDataTransformation; readonly docProperties?: DocPropertiesOptions; -} + readonly outline?: OutlineOptions; +}; // // @@ -28,29 +29,41 @@ interface InlineOptions { // // // -export class Inline extends XmlComponent { - private readonly extent: Extent; - private readonly graphic: Graphic; - - public constructor({ mediaData, transform, docProperties }: InlineOptions) { - super("wp:inline"); - - this.root.push( - new InlineAttributes({ - distT: 0, - distB: 0, - distL: 0, - distR: 0, - }), - ); - - this.extent = new Extent(transform.emus.x, transform.emus.y); - this.graphic = new Graphic(mediaData, transform); - - this.root.push(this.extent); - this.root.push(new EffectExtent()); - this.root.push(new DocProperties(docProperties)); - this.root.push(new GraphicFrameProperties()); - this.root.push(this.graphic); - } -} +export const createInline = ({ mediaData, transform, docProperties, outline }: InlineOptions): XmlComponent => + new BuilderElement({ + name: "wp:inline", + attributes: { + distanceTop: { + key: "distT", + value: 0, + }, + distanceBottom: { + key: "distB", + value: 0, + }, + distanceLeft: { + key: "distL", + value: 0, + }, + distanceRight: { + key: "distR", + value: 0, + }, + }, + children: [ + new Extent(transform.emus.x, transform.emus.y), + createEffectExtent( + outline + ? { + top: (outline.width ?? 9525) * 2, + right: (outline.width ?? 9525) * 2, + bottom: (outline.width ?? 9525) * 2, + left: (outline.width ?? 9525) * 2, + } + : { top: 0, right: 0, bottom: 0, left: 0 }, + ), + new DocProperties(docProperties), + new GraphicFrameProperties(), + new Graphic({ mediaData, transform, outline }), + ], + }); diff --git a/src/file/paragraph/run/image-run.ts b/src/file/paragraph/run/image-run.ts index 838eef43f0..6945ff8e7e 100644 --- a/src/file/paragraph/run/image-run.ts +++ b/src/file/paragraph/run/image-run.ts @@ -3,6 +3,7 @@ import { uniqueId } from "@util/convenience-functions"; import { IContext, IXmlableObject } from "@file/xml-components"; import { DocPropertiesOptions } from "@file/drawing/doc-properties/doc-properties"; +import { OutlineOptions } from "../../drawing/inline/graphic/graphic-data/pic/shape-properties/outline/outline"; import { Drawing, IFloating } from "../../drawing"; import { IMediaTransformation } from "../../media"; import { IMediaData } from "../../media/data"; @@ -13,6 +14,7 @@ export interface IImageOptions { readonly transformation: IMediaTransformation; readonly floating?: IFloating; readonly altText?: DocPropertiesOptions; + readonly outline?: OutlineOptions; } export class ImageRun extends Run { @@ -39,7 +41,11 @@ export class ImageRun extends Run { rotation: options.transformation.rotation ? options.transformation.rotation * 60000 : undefined, }, }; - const drawing = new Drawing(this.imageData, { floating: options.floating, docProperties: options.altText }); + const drawing = new Drawing(this.imageData, { + floating: options.floating, + docProperties: options.altText, + outline: options.outline, + }); this.root.push(drawing); } diff --git a/src/file/track-revision/track-revision-components/deleted-text-run.ts b/src/file/track-revision/track-revision-components/deleted-text-run.ts index e00693c2c0..50cb82ad42 100644 --- a/src/file/track-revision/track-revision-components/deleted-text-run.ts +++ b/src/file/track-revision/track-revision-components/deleted-text-run.ts @@ -1,10 +1,10 @@ import { XmlComponent } from "@file/xml-components"; -import { IRunOptions, RunProperties } from "../../index"; import { Break } from "../../paragraph/run/break"; import { Begin, End, Separate } from "../../paragraph/run/field"; -import { PageNumber } from "../../paragraph/run/run"; +import { IRunOptions, PageNumber } from "../../paragraph/run/run"; import { ChangeAttributes, IChangedAttributesProperties } from "../track-revision"; +import { RunProperties } from "../../paragraph/run/properties"; import { DeletedNumberOfPages, DeletedNumberOfPagesSection, DeletedPage } from "./deleted-page-number"; import { DeletedText } from "./deleted-text"; diff --git a/src/file/xml-components/simple-elements.ts b/src/file/xml-components/simple-elements.ts index dab6f618bc..898c65e567 100644 --- a/src/file/xml-components/simple-elements.ts +++ b/src/file/xml-components/simple-elements.ts @@ -93,6 +93,8 @@ export class BuilderElement extends XmlComponent { this.root.push(new NextAttributeComponent(options.attributes)); } - // TODO: Children + for (const child of options.children ?? []) { + this.root.push(child); + } } }