diff --git a/.cspell.json b/.cspell.json index c27f9e37349..1e8efa7a1ed 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 a0b05de3e8c..6039d6c8323 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 5b5a0da764c..215aa087db5 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 d316c26162c..e674e4ce20f 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 7040ef341e0..3664da15a2c 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 be6245885e6..00000000000 --- 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 209da9f1ff9..328d71f2297 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 94a1eaea492..0ca40ef9ed6 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 8d49a715993..e05c6aa15a3 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 13e18ba80b6..6def78f9e62 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 808a54f84ad..22fc607fb63 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 961f3d1595e..1716d07457b 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 367934e6a45..ed274299d12 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 00000000000..abc28ffc45a --- /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 00000000000..17004133a2a --- /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 00000000000..e66a53c4fe9 --- /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 00000000000..ee3d58777a9 --- /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 f5f2b8b3e79..8b9effc44d9 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 a1e3cd80573..9289eee49c0 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 cd61e8953d4..00000000000 --- 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 00000000000..fca5ed61c97 --- /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 b5689b05e8e..e752c9faa52 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 838eef43f0b..6945ff8e7eb 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 e00693c2c08..50cb82ad425 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 dab6f618bcb..898c65e567e 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); + } } }