add support for floating drawings

- added need elements and test for them
This commit is contained in:
Igor Bulovski
2018-06-08 16:03:04 +02:00
parent ac40a40ec0
commit 97b254ee7b
27 changed files with 746 additions and 21 deletions

View File

@ -0,0 +1,26 @@
import { XmlAttributeComponent } from "file/xml-components";
import { Distance } from "../drawing";
export interface IAnchorAttributes extends Distance {
allowOverlap?: "0" | "1";
behindDoc?: "0" | "1";
layoutInCell?: "0" | "1";
locked?: "0" | "1";
relativeHeight?: number;
simplePos?: "0" | "1";
}
export class AnchorAttributes extends XmlAttributeComponent<IAnchorAttributes> {
protected xmlKeys = {
distT: "distT",
distB: "distB",
distL: "distL",
distR: "distR",
allowOverlap: "allowOverlap",
behindDoc: "behindDoc",
layoutInCell: "layoutInCell",
locked: "locked",
relativeHeight: "relativeHeight",
simplePos: "simplePos",
};
}

View File

@ -0,0 +1,118 @@
import { assert } from "chai";
import { Utility } from "../../../tests/utility";
import { DrawingOptions, TextWrapStyle } from ".././";
import { Anchor } from "./";
function createDrawing(drawingOptions: DrawingOptions) {
return new Anchor(
1,
{
pixels: {
x: 100,
y: 100,
},
emus: {
x: 100 * 9525,
y: 100 * 9525,
},
},
drawingOptions,
);
}
describe("Anchor", () => {
let anchor: Anchor;
describe("#constructor()", () => {
it("should create a Drawing with correct root key", () => {
anchor = createDrawing({});
const newJson = Utility.jsonify(anchor);
assert.equal(newJson.rootKey, "wp:anchor");
assert.equal(newJson.root.length, 10);
});
it("should create a Drawing with all default options", () => {
anchor = createDrawing({});
const newJson = Utility.jsonify(anchor);
assert.equal(newJson.root.length, 10);
const anchorAttributes = newJson.root[0].root;
assert.include(anchorAttributes, {
distT: 0,
distB: 0,
distL: 0,
distR: 0,
simplePos: "0",
allowOverlap: "1",
behindDoc: "0",
locked: "0",
layoutInCell: "1",
relativeHeight: 952500,
});
// 1: simple pos
assert.equal(newJson.root[1].rootKey, "wp:simplePos");
// 2: horizontal position
const horizontalPosition = newJson.root[2];
assert.equal(horizontalPosition.rootKey, "wp:positionH");
assert.include(horizontalPosition.root[0].root, {
relativeFrom: "column",
});
assert.equal(horizontalPosition.root[1].rootKey, "wp:posOffset");
assert.include(horizontalPosition.root[1].root[0], 0);
// 3: vertical position
const verticalPosition = newJson.root[3];
assert.equal(verticalPosition.rootKey, "wp:positionV");
assert.include(verticalPosition.root[0].root, {
relativeFrom: "paragraph",
});
assert.equal(verticalPosition.root[1].rootKey, "wp:posOffset");
assert.include(verticalPosition.root[1].root[0], 0);
// 4: extent
const extent = newJson.root[4];
assert.equal(extent.rootKey, "wp:extent");
assert.include(extent.root[0].root, {
cx: 952500,
cy: 952500,
});
// 5: effect extent
const effectExtent = newJson.root[5];
assert.equal(effectExtent.rootKey, "wp:effectExtent");
// 6 text wrap: none
const textWrap = newJson.root[6];
assert.equal(textWrap.rootKey, "wp:wrapNone");
// 7: doc properties
const docProperties = newJson.root[7];
assert.equal(docProperties.rootKey, "wp:docPr");
// 8: graphic frame properties
const graphicFrame = newJson.root[8];
assert.equal(graphicFrame.rootKey, "wp:cNvGraphicFramePr");
// 9: graphic
const graphic = newJson.root[9];
assert.equal(graphic.rootKey, "a:graphic");
});
it("should create a Drawing with text wrapping", () => {
anchor = createDrawing({
textWrapping: {
textWrapStyle: TextWrapStyle.SQUARE,
},
});
const newJson = Utility.jsonify(anchor);
assert.equal(newJson.root.length, 10);
// 6 text wrap: square
const textWrap = newJson.root[6];
assert.equal(textWrap.rootKey, "wp:wrapSquare");
});
});
});

View File

@ -0,0 +1,88 @@
// http://officeopenxml.com/drwPicFloating.php
import { IMediaDataDimensions } from "file/media";
import { XmlComponent } from "file/xml-components";
import { DocProperties } from "./../doc-properties/doc-properties";
import { EffectExtent } from "./../effect-extent/effect-extent";
import { Extent } from "./../extent/extent";
import { Graphic } from "./../graphic";
import { GraphicFrameProperties } from "./../graphic-frame/graphic-frame-properties";
import { AnchorAttributes } from "./anchor-attributes";
import { DrawingOptions } from "../drawing";
import {
SimplePos,
HorizontalPosition,
VerticalPosition,
Floating,
VerticalPositionRelativeFrom,
HorizontalPositionRelativeFrom,
} from "../floating";
import { WrapNone, TextWrapStyle, WrapSquare, WrapTight, WrapTopAndBottom } from "../text-wrap";
const defaultOptions: Floating = {
allowOverlap: true,
behindDocument: false,
lockAnchor: false,
layoutInCell: true,
verticalPosition: {
relative: VerticalPositionRelativeFrom.PARAGRAPH,
offset: 0,
},
horizontalPosition: {
relative: HorizontalPositionRelativeFrom.COLUMN,
offset: 0,
},
};
export class Anchor extends XmlComponent {
constructor(referenceId: number, dimensions: IMediaDataDimensions, drawingOptions: DrawingOptions) {
super("wp:anchor");
const floating = {
...defaultOptions,
...drawingOptions.floating,
};
this.root.push(
new AnchorAttributes({
distT: 0,
distB: 0,
distL: 0,
distR: 0,
simplePos: "0", // note: word doesn't fully support - so we use 0
allowOverlap: floating.allowOverlap === true ? "1" : "0",
behindDoc: floating.behindDocument === true ? "1" : "0",
locked: floating.lockAnchor === true ? "1" : "0",
layoutInCell: floating.layoutInCell === true ? "1" : "0",
relativeHeight: dimensions.emus.y,
}),
);
this.root.push(new SimplePos());
this.root.push(new HorizontalPosition(floating.horizontalPosition));
this.root.push(new VerticalPosition(floating.verticalPosition));
this.root.push(new Extent(dimensions.emus.x, dimensions.emus.y));
this.root.push(new EffectExtent());
if (drawingOptions.textWrapping != null) {
switch (drawingOptions.textWrapping.textWrapStyle) {
case TextWrapStyle.SQUARE:
this.root.push(new WrapSquare(drawingOptions.textWrapping));
break;
case TextWrapStyle.TIGHT:
this.root.push(new WrapTight(drawingOptions.textWrapping.distanceFromText));
break;
case TextWrapStyle.TOP_AND_BOTTOM:
this.root.push(new WrapTopAndBottom(drawingOptions.textWrapping.distanceFromText));
break;
case TextWrapStyle.NONE:
default:
this.root.push(new WrapNone());
}
} else {
this.root.push(new WrapNone());
}
this.root.push(new DocProperties());
this.root.push(new GraphicFrameProperties());
this.root.push(new Graphic(referenceId, dimensions.emus.x, dimensions.emus.y));
}
}

View File

@ -0,0 +1,2 @@
export * from "./anchor";
export * from "./anchor-attributes";

View File

@ -2,14 +2,12 @@ import { assert } from "chai";
import * as fs from "fs";
import { Utility } from "../../tests/utility";
import { Drawing } from "./";
import { Drawing, DrawingOptions, PlacementPosition } from "./";
describe("Drawing", () => {
let currentBreak: Drawing;
beforeEach(() => {
const path = "./demo/images/image1.jpeg";
currentBreak = new Drawing({
function createDrawing(drawingOptions?: DrawingOptions) {
const path = "./demo/images/image1.jpeg";
return new Drawing(
{
fileName: "test.jpg",
referenceId: 1,
stream: fs.createReadStream(path),
@ -24,14 +22,33 @@ describe("Drawing", () => {
y: 100 * 9525,
},
},
});
});
},
drawingOptions,
);
}
describe("Drawing", () => {
let currentBreak: Drawing;
describe("#constructor()", () => {
it("should create a Drawing with correct root key", () => {
currentBreak = createDrawing();
const newJson = Utility.jsonify(currentBreak);
assert.equal(newJson.rootKey, "w:drawing");
// console.log(JSON.stringify(newJson, null, 2));
});
it("should create a drawing with inline element when there are no options passed", () => {
currentBreak = createDrawing();
const newJson = Utility.jsonify(currentBreak);
assert.equal(newJson.root[0].rootKey, "wp:inline");
});
it("should create a drawing with anchor element when there options are passed", () => {
currentBreak = createDrawing({
position: PlacementPosition.FLOATING,
});
const newJson = Utility.jsonify(currentBreak);
assert.equal(newJson.root[0].rootKey, "wp:anchor");
});
});
});

View File

@ -1,15 +1,49 @@
import { IMediaData } from "file/media";
import { XmlComponent } from "file/xml-components";
import { Inline } from "./inline";
import { Anchor } from "./anchor";
import { TextWrapping } from "./text-wrap";
import { Floating } from "./floating";
export enum PlacementPosition {
INLINE,
FLOATING,
}
export interface Distance {
distT?: number;
distB?: number;
distL?: number;
distR?: number;
}
export interface DrawingOptions {
position?: PlacementPosition;
textWrapping?: TextWrapping;
floating?: Floating;
}
const defaultDrawingOptions: DrawingOptions = {
position: PlacementPosition.INLINE,
};
export class Drawing extends XmlComponent {
constructor(imageData: IMediaData) {
constructor(imageData: IMediaData, drawingOptions?: DrawingOptions) {
super("w:drawing");
if (imageData === undefined) {
throw new Error("imageData cannot be undefined");
}
this.root.push(new Inline(imageData.referenceId, imageData.dimensions));
const mergedOptions = {
...defaultDrawingOptions,
...drawingOptions,
};
if (mergedOptions.position === PlacementPosition.INLINE) {
this.root.push(new Inline(imageData.referenceId, imageData.dimensions));
} else if (mergedOptions.position === PlacementPosition.FLOATING) {
this.root.push(new Anchor(imageData.referenceId, imageData.dimensions, mergedOptions));
}
}
}

View File

@ -0,0 +1,15 @@
import { assert } from "chai";
import { Align } from "./align";
import { Utility } from "../../../tests/utility";
import { VerticalPositionAlign } from ".";
describe("Align", () => {
describe("#constructor()", () => {
it("should create a element with correct root key", () => {
const newJson = Utility.jsonify(new Align(VerticalPositionAlign.CENTER));
assert.equal(newJson.rootKey, "wp:align");
assert.include(newJson.root[0], VerticalPositionAlign.CENTER);
});
});
});

View File

@ -0,0 +1,10 @@
// http://officeopenxml.com/drwPicFloating-position.php
import { XmlComponent } from "file/xml-components";
import { HorizontalPositionAlign, VerticalPositionAlign } from "./floating-position";
export class Align extends XmlComponent {
constructor(value: HorizontalPositionAlign | VerticalPositionAlign) {
super("wp:align");
this.root.push(value);
}
}

View File

@ -0,0 +1,60 @@
// http://officeopenxml.com/drwPicFloating-position.php
export enum HorizontalPositionRelativeFrom {
CHARACTER = "character",
COLUMN = "column",
INSIDE_MARGIN = "insideMargin",
LEFT_MARGIN = "leftMargin",
MARGIN = "margin",
OUTSIDE_MARGIN = "outsideMargin",
PAGE = "page",
RIGHT_MARGIN = "rightMargin",
}
export enum VerticalPositionRelativeFrom {
BOTTOM_MARGIN = "bottomMargin",
INSIDE_MARGIN = "insideMargin",
LINE = "line",
MARGIN = "margin",
OUTSIDE_MARGIN = "outsideMargin",
PAGE = "page",
PARAGRAPH = "paragraph",
TOP_MARGIN = "topMargin",
}
export enum HorizontalPositionAlign {
CENTER = "center",
INSIDE = "inside",
LEFT = "left",
OUTSIDE = "outside",
RIGHT = "right",
}
export enum VerticalPositionAlign {
BOTTOM = "bottom",
CENTER = "center",
INSIDE = "inside",
OUTSIDE = "outside",
TOP = "top",
}
export interface HorizontalPositionOptions {
relative: HorizontalPositionRelativeFrom;
align?: HorizontalPositionAlign;
offset?: number;
}
export interface VerticalPositionOptions {
relative: VerticalPositionRelativeFrom;
align?: VerticalPositionAlign;
offset?: number;
}
export interface Floating {
horizontalPosition: HorizontalPositionOptions;
verticalPosition: VerticalPositionOptions;
allowOverlap?: boolean;
lockAnchor?: boolean;
behindDocument?: boolean;
layoutInCell?: boolean;
}

View File

@ -0,0 +1,41 @@
import { assert } from "chai";
import { HorizontalPosition } from "./horizontal-position";
import { Utility } from "../../../tests/utility";
import { HorizontalPositionRelativeFrom, HorizontalPositionAlign } from ".";
describe("HorizontalPosition", () => {
describe("#constructor()", () => {
it("should create a element with position align", () => {
const newJson = Utility.jsonify(
new HorizontalPosition({
relative: HorizontalPositionRelativeFrom.MARGIN,
align: HorizontalPositionAlign.CENTER,
}),
);
assert.equal(newJson.rootKey, "wp:positionH");
assert.include(newJson.root[0].root, {
relativeFrom: "margin",
});
assert.equal(newJson.root[1].rootKey, "wp:align");
assert.include(newJson.root[1].root, "center");
});
it("should create a element with offset", () => {
const newJson = Utility.jsonify(
new HorizontalPosition({
relative: HorizontalPositionRelativeFrom.MARGIN,
offset: 40,
}),
);
assert.equal(newJson.rootKey, "wp:positionH");
assert.include(newJson.root[0].root, {
relativeFrom: "margin",
});
assert.equal(newJson.root[1].rootKey, "wp:posOffset");
assert.include(newJson.root[1].root[0], 40);
});
});
});

View File

@ -0,0 +1,35 @@
// http://officeopenxml.com/drwPicFloating-position.php
import { XmlComponent, XmlAttributeComponent } from "file/xml-components";
import { HorizontalPositionRelativeFrom, HorizontalPositionOptions } from "./floating-position";
import { Align } from "./align";
import { PositionOffset } from "./position-offset";
interface IHorizontalPositionAttributes {
relativeFrom: HorizontalPositionRelativeFrom;
}
class HorizontalPositionAttributes extends XmlAttributeComponent<IHorizontalPositionAttributes> {
protected xmlKeys = {
relativeFrom: "relativeFrom",
};
}
export class HorizontalPosition extends XmlComponent {
constructor(horizontalPosition: HorizontalPositionOptions) {
super("wp:positionH");
this.root.push(
new HorizontalPositionAttributes({
relativeFrom: horizontalPosition.relative,
}),
);
if (horizontalPosition.align) {
this.root.push(new Align(horizontalPosition.align));
} else if (horizontalPosition.offset !== undefined) {
this.root.push(new PositionOffset(horizontalPosition.offset));
} else {
throw new Error("There is no configuration provided for floating position (Align or offset)");
}
}
}

View File

@ -0,0 +1,4 @@
export * from "./floating-position";
export * from "./simple-pos";
export * from "./horizontal-position";
export * from "./vertical-position";

View File

@ -0,0 +1,14 @@
import { assert } from "chai";
import { PositionOffset } from "./position-offset";
import { Utility } from "../../../tests/utility";
describe("PositionOffset", () => {
describe("#constructor()", () => {
it("should create a element with correct root key", () => {
const newJson = Utility.jsonify(new PositionOffset(50));
assert.equal(newJson.rootKey, "wp:posOffset");
assert.equal(newJson.root[0], 50);
});
});
});

View File

@ -0,0 +1,9 @@
// http://officeopenxml.com/drwPicFloating-position.php
import { XmlComponent } from "file/xml-components";
export class PositionOffset extends XmlComponent {
constructor(offsetValue: number) {
super("wp:posOffset");
this.root.push(offsetValue.toString());
}
}

View File

@ -0,0 +1,17 @@
import { assert } from "chai";
import { SimplePos } from "./simple-pos";
import { Utility } from "../../../tests/utility";
describe("SimplePos", () => {
describe("#constructor()", () => {
it("should create a element with correct root key", () => {
const newJson = Utility.jsonify(new SimplePos());
assert.equal(newJson.rootKey, "wp:simplePos");
assert.include(newJson.root[0].root, {
x: 0,
y: 0,
});
});
});
});

View File

@ -0,0 +1,28 @@
// http://officeopenxml.com/drwPicFloating-position.php
import { XmlComponent, XmlAttributeComponent } from "file/xml-components";
interface ISimplePosAttributes {
x: number;
y: number;
}
class SimplePosAttributes extends XmlAttributeComponent<ISimplePosAttributes> {
protected xmlKeys = {
x: "x",
y: "y",
};
}
export class SimplePos extends XmlComponent {
constructor() {
super("wp:simplePos");
// NOTE: It's not fully supported in Microsoft Word, but this element is needed anyway
this.root.push(
new SimplePosAttributes({
x: 0,
y: 0,
}),
);
}
}

View File

@ -0,0 +1,41 @@
import { assert } from "chai";
import { VerticalPosition } from "./vertical-position";
import { Utility } from "../../../tests/utility";
import { VerticalPositionRelativeFrom, VerticalPositionAlign } from ".";
describe("VerticalPosition", () => {
describe("#constructor()", () => {
it("should create a element with position align", () => {
const newJson = Utility.jsonify(
new VerticalPosition({
relative: VerticalPositionRelativeFrom.MARGIN,
align: VerticalPositionAlign.INSIDE,
}),
);
assert.equal(newJson.rootKey, "wp:positionV");
assert.include(newJson.root[0].root, {
relativeFrom: "margin",
});
assert.equal(newJson.root[1].rootKey, "wp:align");
assert.include(newJson.root[1].root, "inside");
});
it("should create a element with offset", () => {
const newJson = Utility.jsonify(
new VerticalPosition({
relative: VerticalPositionRelativeFrom.MARGIN,
offset: 40,
}),
);
assert.equal(newJson.rootKey, "wp:positionV");
assert.include(newJson.root[0].root, {
relativeFrom: "margin",
});
assert.equal(newJson.root[1].rootKey, "wp:posOffset");
assert.include(newJson.root[1].root[0], 40);
});
});
});

View File

@ -0,0 +1,35 @@
// http://officeopenxml.com/drwPicFloating-position.php
import { XmlComponent, XmlAttributeComponent } from "file/xml-components";
import { VerticalPositionRelativeFrom, VerticalPositionOptions } from "./floating-position";
import { Align } from "./align";
import { PositionOffset } from "./position-offset";
interface IVerticalPositionAttributes {
relativeFrom: VerticalPositionRelativeFrom;
}
class VerticalPositionAttributes extends XmlAttributeComponent<IVerticalPositionAttributes> {
protected xmlKeys = {
relativeFrom: "relativeFrom",
};
}
export class VerticalPosition extends XmlComponent {
constructor(verticalPosition: VerticalPositionOptions) {
super("wp:positionV");
this.root.push(
new VerticalPositionAttributes({
relativeFrom: verticalPosition.relative,
}),
);
if (verticalPosition.align) {
this.root.push(new Align(verticalPosition.align));
} else if (verticalPosition.offset !== undefined) {
this.root.push(new PositionOffset(verticalPosition.offset));
} else {
throw new Error("There is no configuration provided for floating position (Align or offset)");
}
}
}

View File

@ -1 +1,3 @@
export { Drawing } from "./drawing";
export * from "./drawing";
export * from "./text-wrap";
export * from "./floating";

View File

@ -1,11 +1,7 @@
import { XmlAttributeComponent } from "file/xml-components";
import { Distance } from "../drawing";
export interface IInlineAttributes {
distT?: number;
distB?: number;
distL?: number;
distR?: number;
}
export interface IInlineAttributes extends Distance {}
export class InlineAttributes extends XmlAttributeComponent<IInlineAttributes> {
protected xmlKeys = {

View File

@ -0,0 +1,5 @@
export * from "./text-wrapping";
export * from "./wrap-none";
export * from "./wrap-square";
export * from "./wrap-tight";
export * from "./wrap-top-and-bottom";

View File

@ -0,0 +1,22 @@
// http://officeopenxml.com/drwPicFloating-textWrap.php
import { Distance } from "../drawing";
export enum TextWrapStyle {
NONE,
SQUARE,
TIGHT,
TOP_AND_BOTTOM,
}
export enum WrapTextOption {
BOTH_SIDES = "bothSides",
LEFT = "left",
RIGHT = "right",
LARGEST = "largest",
}
export interface TextWrapping {
textWrapStyle: TextWrapStyle;
wrapTextOption?: WrapTextOption;
distanceFromText?: Distance;
}

View File

@ -0,0 +1,8 @@
// http://officeopenxml.com/drwPicFloating-textWrap.php
import { XmlComponent } from "file/xml-components";
export class WrapNone extends XmlComponent {
constructor() {
super("wp:wrapNone");
}
}

View File

@ -0,0 +1,31 @@
// http://officeopenxml.com/drwPicFloating-textWrap.php
import { XmlComponent, XmlAttributeComponent } from "file/xml-components";
import { TextWrapping, WrapTextOption } from ".";
import { Distance } from "../drawing";
interface IWrapSquareAttributes extends Distance {
wrapText?: WrapTextOption;
}
class WrapSquareAttributes extends XmlAttributeComponent<IWrapSquareAttributes> {
protected xmlKeys = {
distT: "distT",
distB: "distB",
distL: "distL",
distR: "distR",
wrapText: "wrapText",
};
}
export class WrapSquare extends XmlComponent {
constructor(textWrapping: TextWrapping) {
super("wp:wrapSquare");
this.root.push(
new WrapSquareAttributes({
wrapText: textWrapping.wrapTextOption || WrapTextOption.BOTH_SIDES,
...textWrapping.distanceFromText,
}),
);
}
}

View File

@ -0,0 +1,33 @@
// http://officeopenxml.com/drwPicFloating-textWrap.php
import { XmlComponent, XmlAttributeComponent } from "file/xml-components";
import { Distance } from "../drawing";
interface IWrapTightAttributes {
distT?: number;
distB?: number;
}
class WrapTightAttributes extends XmlAttributeComponent<IWrapTightAttributes> {
protected xmlKeys = {
distT: "distT",
distB: "distB",
};
}
export class WrapTight extends XmlComponent {
constructor(distanceFromText?: Distance) {
super("wp:wrapTight");
distanceFromText = distanceFromText || {
distT: 0,
distB: 0,
};
this.root.push(
new WrapTightAttributes({
distT: distanceFromText.distT,
distB: distanceFromText.distB,
}),
);
}
}

View File

@ -0,0 +1,33 @@
// http://officeopenxml.com/drwPicFloating-textWrap.php
import { XmlComponent, XmlAttributeComponent } from "file/xml-components";
import { Distance } from "../drawing";
interface IWrapTopAndBottomAttributes {
distT?: number;
distB?: number;
}
class WrapTopAndBottomAttributes extends XmlAttributeComponent<IWrapTopAndBottomAttributes> {
protected xmlKeys = {
distT: "distT",
distB: "distB",
};
}
export class WrapTopAndBottom extends XmlComponent {
constructor(distanceFromText?: Distance) {
super("wp:wrapTopAndBottom");
distanceFromText = distanceFromText || {
distT: 0,
distB: 0,
};
this.root.push(
new WrapTopAndBottomAttributes({
distT: distanceFromText.distT,
distB: distanceFromText.distB,
}),
);
}
}

View File

@ -1,15 +1,16 @@
import { Drawing } from "../../drawing";
import { IMediaData } from "../../media/data";
import { Run } from "../run";
import { DrawingOptions } from "../../drawing/drawing";
export class PictureRun extends Run {
constructor(imageData: IMediaData) {
constructor(imageData: IMediaData, drawingOptions?: DrawingOptions) {
super();
if (imageData === undefined) {
throw new Error("imageData cannot be undefined");
}
this.root.push(new Drawing(imageData));
this.root.push(new Drawing(imageData, drawingOptions));
}
}