Add image borders (#2472)

* Add image borders

* Fix prettier

* Fix spelling

* Fix spelling

* Finish feature

* Update demo

* Try and fix demo 14
This commit is contained in:
Dolan
2023-12-27 20:20:45 +00:00
committed by GitHub
parent 6c28f8bab0
commit d23f453d28
27 changed files with 571 additions and 138 deletions

View File

@ -54,7 +54,8 @@
"\\.to\\.include\\.members\\(\\[[^\\]]+]\\)",
"/new [a-zA-Z]+\\({[^£]+}\\)/g",
"/<element name=\"[a-z]+\"/gi",
"/<attribute name=\"[a-z]+\"/gi"
"/<attribute name=\"[a-z]+\"/gi",
"/key: \".+\"/"
],
"ignorePaths": ["package.json", "docs/api", "*.docx", "build"],
"allowCompoundWords": true,

View File

@ -92,6 +92,7 @@ rules:
format:
- camelCase
- PascalCase
- UPPER_CASE # for constants
filter:
regex: (^[a-z]+:.+)|_attr|[0-9]
match: false

View File

@ -121,9 +121,11 @@ const createLoremIpsumParagraphs = () => [
"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 ",
),

View File

@ -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),
},
}),
],
}),

View File

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

View File

@ -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";
// <xsd:attribute name="allowOverlap" type="xsd:boolean" use="required"/>
// </xsd:complexType>
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 }));
}
}

View File

@ -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;
}
// <xsd:complexType name="CT_Drawing">
@ -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 }));
}
}
}

View File

@ -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",
};
}

View File

@ -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<EffectExtentAttributes>({
name: "wp:effectExtent",
attributes: {
top: {
key: "t",
value: top,
},
right: {
key: "r",
value: right,
},
bottom: {
key: "b",
value: bottom,
},
left: {
key: "l",
value: left,
},
},
});

View File

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

View File

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

View File

@ -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": {},
});

View File

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

View File

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

View File

@ -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");
// <xsd:complexType name="CT_TextOutlineEffect">
// <xsd:sequence>
// <xsd:group ref="EG_FillProperties" minOccurs="0"/>
// <xsd:group ref="EG_LineDashProperties" minOccurs="0"/>
// <xsd:group ref="EG_LineJoinProperties" minOccurs="0"/>
// </xsd:sequence>
// <xsd:attribute name="w" use="optional" type="a:ST_LineWidth"/>
// <xsd:attribute name="cap" use="optional" type="ST_LineCap"/>
// <xsd:attribute name="cmpd" use="optional" type="ST_CompoundLine"/>
// <xsd:attribute name="algn" use="optional" type="ST_PenAlignment"/>
// </xsd:complexType>
this.root.push(new NoFill());
}
}
// <xsd:simpleType name="ST_LineCap">
// <xsd:restriction base="xsd:string">
// <xsd:enumeration value="rnd"/>
// <xsd:enumeration value="sq"/>
// <xsd:enumeration value="flat"/>
// </xsd:restriction>
// </xsd:simpleType>
export const LineCap = {
ROUND: "rnd",
SQUARE: "sq",
FLAT: "flat",
} as const;
// <xsd:simpleType name="ST_CompoundLine">
// <xsd:restriction base="xsd:string">
// <xsd:enumeration value="sng"/>
// <xsd:enumeration value="dbl"/>
// <xsd:enumeration value="thickThin"/>
// <xsd:enumeration value="thinThick"/>
// <xsd:enumeration value="tri"/>
// </xsd:restriction>
// </xsd:simpleType>
export const CompoundLine = {
SINGLE: "sng",
DOUBLE: "dbl",
THICK_THIN: "thickThin",
THIN_THICK: "thinThick",
TRI: "tri",
} as const;
// <xsd:simpleType name="ST_PenAlignment">
// <xsd:restriction base="xsd:string">
// <xsd:enumeration value="ctr"/>
// <xsd:enumeration value="in"/>
// </xsd:restriction>
// </xsd:simpleType>
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;
// <xsd:group name="EG_FillProperties">
// <xsd:choice>
// <xsd:element name="noFill" type="w:CT_Empty"/>
// <xsd:element name="solidFill" type="CT_SolidColorFillProperties"/>
// <xsd:element name="gradFill" type="CT_GradientFillProperties"/>
// </xsd:choice>
// </xsd:group>
type OutlineFillProperties = OutlineNoFill | OutlineSolidFill;
export type OutlineOptions = OutlineAttributes & OutlineFillProperties;
export const createOutline = (options: OutlineOptions): XmlComponent =>
new BuilderElement<OutlineAttributes>({
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,
}),
],
});

View File

@ -0,0 +1,22 @@
import { BuilderElement, XmlComponent } from "@file/xml-components";
type SolidRgbColorOptions = {
readonly value: string;
};
// <xsd:complexType name="CT_SRgbColor">
// <xsd:sequence>
// <xsd:group ref="EG_ColorTransform" minOccurs="0" maxOccurs="unbounded"/>
// </xsd:sequence>
// <xsd:attribute name="val" type="s:ST_HexColorRGB" use="required"/>
// </xsd:complexType>
export const createSolidRgbColor = (options: SolidRgbColorOptions): XmlComponent =>
new BuilderElement<SolidRgbColorOptions>({
name: "a:srgbClr",
attributes: {
value: {
key: "val",
value: options.value,
},
},
});

View File

@ -0,0 +1,65 @@
import { BuilderElement, XmlComponent } from "@file/xml-components";
type SchemeColorOptions = {
readonly value: (typeof SchemeColor)[keyof typeof SchemeColor];
};
// <xsd:simpleType name="ST_SchemeColorVal">
// <xsd:restriction base="xsd:string">
// <xsd:enumeration value="bg1"/>
// <xsd:enumeration value="tx1"/>
// <xsd:enumeration value="bg2"/>
// <xsd:enumeration value="tx2"/>
// <xsd:enumeration value="accent1"/>
// <xsd:enumeration value="accent2"/>
// <xsd:enumeration value="accent3"/>
// <xsd:enumeration value="accent4"/>
// <xsd:enumeration value="accent5"/>
// <xsd:enumeration value="accent6"/>
// <xsd:enumeration value="hlink"/>
// <xsd:enumeration value="folHlink"/>
// <xsd:enumeration value="dk1"/>
// <xsd:enumeration value="lt1"/>
// <xsd:enumeration value="dk2"/>
// <xsd:enumeration value="lt2"/>
// <xsd:enumeration value="phClr"/>
// </xsd:restriction>
// </xsd:simpleType>
// 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;
// <xsd:complexType name="CT_SchemeColor">
// <xsd:sequence>
// <xsd:group ref="EG_ColorTransform" minOccurs="0" maxOccurs="unbounded"/>
// </xsd:sequence>
// <xsd:attribute name="val" type="ST_SchemeColorVal" use="required"/>
// </xsd:complexType>
export const createSchemeColor = (options: SchemeColorOptions): XmlComponent =>
new BuilderElement<SchemeColorOptions>({
name: "a:schemeClr",
attributes: {
value: {
key: "val",
value: options.value,
},
},
});

View File

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

View File

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

View File

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

View File

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

View File

@ -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<IDistance> {
protected readonly xmlKeys = {
distT: "distT",
distB: "distB",
distL: "distL",
distR: "distR",
};
}

View File

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

View File

@ -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;
};
// <xsd:complexType name="CT_Inline">
// <xsd:sequence>
@ -28,29 +29,41 @@ interface InlineOptions {
// <xsd:attribute name="distL" type="ST_WrapDistance" use="optional"/>
// <xsd:attribute name="distR" type="ST_WrapDistance" use="optional"/>
// </xsd:complexType>
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 }),
],
});

View File

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

View File

@ -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";

View File

@ -93,6 +93,8 @@ export class BuilderElement<T extends AttributeData> extends XmlComponent {
this.root.push(new NextAttributeComponent(options.attributes));
}
// TODO: Children
for (const child of options.children ?? []) {
this.root.push(child);
}
}
}