#815 Rotation and flipping images

This commit is contained in:
Dolan
2021-03-15 02:41:37 +00:00
parent a3febae8a3
commit bd40b11ce4
33 changed files with 615 additions and 126 deletions

View File

@ -42,7 +42,7 @@ describe("ImageReplacer", () => {
{
stream: Buffer.from(""),
fileName: "test-image.png",
dimensions: {
transformation: {
pixels: {
x: 100,
y: 100,

View File

@ -11,7 +11,7 @@ function createAnchor(drawingOptions: IDrawingOptions): Anchor {
{
fileName: "test.png",
stream: new Buffer(""),
dimensions: {
transformation: {
pixels: {
x: 0,
y: 0,

View File

@ -1,5 +1,5 @@
// http://officeopenxml.com/drwPicFloating.php
import { IMediaData, IMediaDataDimensions } from "file/media";
import { IMediaData, IMediaDataTransformation } from "file/media";
import { XmlComponent } from "file/xml-components";
import { IDrawingOptions } from "../drawing";
import { HorizontalPosition, IFloating, SimplePos, VerticalPosition } from "../floating";
@ -12,7 +12,7 @@ import { GraphicFrameProperties } from "./../graphic-frame/graphic-frame-propert
import { AnchorAttributes } from "./anchor-attributes";
export class Anchor extends XmlComponent {
constructor(mediaData: IMediaData, dimensions: IMediaDataDimensions, drawingOptions: IDrawingOptions) {
constructor(mediaData: IMediaData, transform: IMediaDataTransformation, drawingOptions: IDrawingOptions) {
super("wp:anchor");
const floating: IFloating = {
@ -36,14 +36,14 @@ export class Anchor extends XmlComponent {
behindDoc: floating.behindDocument === true ? "1" : "0",
locked: floating.lockAnchor === true ? "1" : "0",
layoutInCell: floating.layoutInCell === true ? "1" : "0",
relativeHeight: floating.zIndex ? floating.zIndex : dimensions.emus.y,
relativeHeight: floating.zIndex ? floating.zIndex : transform.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 Extent(transform.emus.x, transform.emus.y));
this.root.push(new EffectExtent());
if (drawingOptions.floating !== undefined && drawingOptions.floating.wrap !== undefined) {
@ -67,6 +67,6 @@ export class Anchor extends XmlComponent {
this.root.push(new DocProperties());
this.root.push(new GraphicFrameProperties());
this.root.push(new Graphic(mediaData, dimensions.emus.x, dimensions.emus.y));
this.root.push(new Graphic(mediaData, transform));
}
}

View File

@ -13,7 +13,7 @@ function createDrawing(drawingOptions?: IDrawingOptions): Drawing {
fileName: "test.jpg",
stream: Buffer.from(imageBase64Data, "base64"),
path: path,
dimensions: {
transformation: {
pixels: {
x: 100,
y: 100,
@ -165,6 +165,9 @@ describe("Drawing", () => {
},
{
"a:xfrm": [
{
_attr: {},
},
{
"a:ext": {
_attr: {
@ -392,6 +395,9 @@ describe("Drawing", () => {
},
{
"a:xfrm": [
{
_attr: {},
},
{
"a:ext": {
_attr: {

View File

@ -22,10 +22,10 @@ export class Drawing extends XmlComponent {
super("w:drawing");
if (!drawingOptions.floating) {
this.inline = new Inline(imageData, imageData.dimensions);
this.inline = new Inline(imageData, imageData.transformation);
this.root.push(this.inline);
} else {
this.root.push(new Anchor(imageData, imageData.dimensions, drawingOptions));
this.root.push(new Anchor(imageData, imageData.transformation, drawingOptions));
}
}

View File

@ -1,4 +1,4 @@
import { IMediaData } from "file/media";
import { IMediaData, IMediaDataTransformation } from "file/media";
import { XmlComponent } from "file/xml-components";
import { GraphicDataAttributes } from "./graphic-data-attribute";
@ -7,7 +7,7 @@ import { Pic } from "./pic";
export class GraphicData extends XmlComponent {
private readonly pic: Pic;
constructor(mediaData: IMediaData, x: number, y: number) {
constructor(mediaData: IMediaData, transform: IMediaDataTransformation) {
super("a:graphicData");
this.root.push(
@ -16,7 +16,7 @@ export class GraphicData extends XmlComponent {
}),
);
this.pic = new Pic(mediaData, x, y);
this.pic = new Pic(mediaData, transform);
this.root.push(this.pic);
}

View File

@ -1,5 +1,5 @@
// http://officeopenxml.com/drwPic.php
import { IMediaData } from "file/media";
import { IMediaData, IMediaDataTransformation } from "file/media";
import { XmlComponent } from "file/xml-components";
import { BlipFill } from "./blip/blip-fill";
@ -10,7 +10,7 @@ import { ShapeProperties } from "./shape-properties/shape-properties";
export class Pic extends XmlComponent {
private readonly shapeProperties: ShapeProperties;
constructor(mediaData: IMediaData, x: number, y: number) {
constructor(mediaData: IMediaData, transform: IMediaDataTransformation) {
super("pic:pic");
this.root.push(
@ -19,11 +19,11 @@ export class Pic extends XmlComponent {
}),
);
this.shapeProperties = new ShapeProperties(x, y);
this.shapeProperties = new ShapeProperties(transform);
this.root.push(new NonVisualPicProperties());
this.root.push(new BlipFill(mediaData));
this.root.push(new ShapeProperties(x, y));
this.root.push(new ShapeProperties(transform));
}
public setXY(x: number, y: number): void {

View File

@ -0,0 +1,93 @@
import { expect } from "chai";
import { Formatter } from "export/formatter";
import { Form } from "./form/form";
describe("Form", () => {
describe("#constructor()", () => {
it("should create", () => {
const tree = new Formatter().format(
new Form({
pixels: {
x: 100,
y: 100,
},
emus: {
x: 100,
y: 100,
},
}),
);
expect(tree).to.deep.equal({
"a:xfrm": [
{
_attr: {},
},
{
"a:ext": {
_attr: {
cx: 100,
cy: 100,
},
},
},
{
"a:off": {
_attr: {
x: 0,
y: 0,
},
},
},
],
});
});
it("should create with flip", () => {
const tree = new Formatter().format(
new Form({
pixels: {
x: 100,
y: 100,
},
emus: {
x: 100,
y: 100,
},
flip: {
vertical: true,
horizontal: true,
},
}),
);
expect(tree).to.deep.equal({
"a:xfrm": [
{
_attr: {
flipH: true,
flipV: true,
},
},
{
"a:ext": {
_attr: {
cx: 100,
cy: 100,
},
},
},
{
"a:off": {
_attr: {
x: 0,
y: 0,
},
},
},
],
});
});
});
});

View File

@ -1,15 +1,38 @@
// http://officeopenxml.com/drwSp-size.php
import { XmlComponent } from "file/xml-components";
// http://officeopenxml.com/drwSp-rotate.php
import { IMediaDataTransformation } from "file/media";
import { XmlAttributeComponent, XmlComponent } from "file/xml-components";
import { Extents } from "./extents/extents";
import { Offset } from "./offset/off";
export class FormAttributes extends XmlAttributeComponent<{
readonly flipVertical?: boolean;
readonly flipHorizontal?: boolean;
readonly rotation?: number;
}> {
protected readonly xmlKeys = {
flipVertical: "flipV",
flipHorizontal: "flipH",
rotation: "rot",
};
}
export class Form extends XmlComponent {
private readonly extents: Extents;
constructor(x: number, y: number) {
constructor(options: IMediaDataTransformation) {
super("a:xfrm");
this.extents = new Extents(x, y);
this.root.push(
new FormAttributes({
flipVertical: options.flip?.vertical,
flipHorizontal: options.flip?.horizontal,
rotation: options.rotation,
}),
);
this.extents = new Extents(options.emus.x, options.emus.y);
this.root.push(this.extents);
this.root.push(new Offset());

View File

@ -1,7 +0,0 @@
import { XmlComponent } from "file/xml-components";
export class NoFill extends XmlComponent {
constructor() {
super("a:noFill");
}
}

View File

@ -0,0 +1,16 @@
import { expect } from "chai";
import { Formatter } from "export/formatter";
import { NoFill } from "./no-fill";
describe("NoFill", () => {
describe("#constructor()", () => {
it("should create", () => {
const tree = new Formatter().format(new NoFill());
expect(tree).to.deep.equal({
"a:noFill": {},
});
});
});
});

View File

@ -0,0 +1,19 @@
import { expect } from "chai";
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": {},
},
],
});
});
});
});

View File

@ -1,4 +1,5 @@
// http://officeopenxml.com/drwSp-SpPr.php
import { IMediaDataTransformation } from "file/media";
import { XmlComponent } from "file/xml-components";
import { Form } from "./form";
// import { NoFill } from "./no-fill";
@ -9,7 +10,7 @@ import { ShapePropertiesAttributes } from "./shape-properties-attributes";
export class ShapeProperties extends XmlComponent {
private readonly form: Form;
constructor(x: number, y: number) {
constructor(transform: IMediaDataTransformation) {
super("pic:spPr");
this.root.push(
@ -18,7 +19,7 @@ export class ShapeProperties extends XmlComponent {
}),
);
this.form = new Form(x, y);
this.form = new Form(transform);
this.root.push(this.form);
this.root.push(new PresetGeometry());

View File

@ -1,4 +1,4 @@
import { IMediaData } from "file/media";
import { IMediaData, IMediaDataTransformation } from "file/media";
import { XmlAttributeComponent, XmlComponent } from "file/xml-components";
import { GraphicData } from "./graphic-data";
@ -14,7 +14,7 @@ class GraphicAttributes extends XmlAttributeComponent<{
export class Graphic extends XmlComponent {
private readonly data: GraphicData;
constructor(mediaData: IMediaData, x: number, y: number) {
constructor(mediaData: IMediaData, transform: IMediaDataTransformation) {
super("a:graphic");
this.root.push(
new GraphicAttributes({
@ -22,7 +22,7 @@ export class Graphic extends XmlComponent {
}),
);
this.data = new GraphicData(mediaData, x, y);
this.data = new GraphicData(mediaData, transform);
this.root.push(this.data);
}

View File

@ -1,5 +1,5 @@
// http://officeopenxml.com/drwPicInline.php
import { IMediaData, IMediaDataDimensions } from "file/media";
import { IMediaData, IMediaDataTransformation } from "file/media";
import { XmlComponent } from "file/xml-components";
import { DocProperties } from "./../doc-properties/doc-properties";
import { EffectExtent } from "./../effect-extent/effect-extent";
@ -12,7 +12,7 @@ export class Inline extends XmlComponent {
private readonly extent: Extent;
private readonly graphic: Graphic;
constructor(mediaData: IMediaData, private readonly dimensions: IMediaDataDimensions) {
constructor(mediaData: IMediaData, private readonly transform: IMediaDataTransformation) {
super("wp:inline");
this.root.push(
@ -24,8 +24,8 @@ export class Inline extends XmlComponent {
}),
);
this.extent = new Extent(dimensions.emus.x, dimensions.emus.y);
this.graphic = new Graphic(mediaData, dimensions.emus.x, dimensions.emus.y);
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());
@ -35,8 +35,8 @@ export class Inline extends XmlComponent {
}
public scale(factorX: number, factorY: number): void {
const newX = Math.round(this.dimensions.emus.x * factorX);
const newY = Math.round(this.dimensions.emus.y * factorY);
const newX = Math.round(this.transform.emus.x * factorX);
const newY = Math.round(this.transform.emus.y * factorY);
this.extent.setXY(newX, newY);
this.graphic.setXY(newX, newY);

View File

@ -1,4 +1,4 @@
export interface IMediaDataDimensions {
export interface IMediaDataTransformation {
readonly pixels: {
readonly x: number;
readonly y: number;
@ -7,13 +7,18 @@ export interface IMediaDataDimensions {
readonly x: number;
readonly y: number;
};
readonly flip?: {
readonly vertical?: boolean;
readonly horizontal?: boolean;
};
readonly rotation?: number;
}
export interface IMediaData {
readonly stream: Buffer | Uint8Array | ArrayBuffer;
readonly path?: string;
readonly fileName: string;
readonly dimensions: IMediaDataDimensions;
readonly transformation: IMediaDataTransformation;
}
// Needed because of: https://github.com/s-panferov/awesome-typescript-loader/issues/432

View File

@ -21,7 +21,14 @@ describe("Media", () => {
describe("#addImage", () => {
it("should add image", () => {
const file = new File();
const image = Media.addImage(file, "");
const image = Media.addImage({
document: file,
data: "",
transformation: {
width: 100,
height: 100,
},
});
let tree = new Formatter().format(new Paragraph(image));
expect(tree["w:p"]).to.be.an.instanceof(Array);
@ -34,7 +41,14 @@ describe("Media", () => {
// tslint:disable-next-line:no-any
const file = new File();
const image1 = Media.addImage(file, "test");
const image1 = Media.addImage({
document: file,
data: "test",
transformation: {
width: 100,
height: 100,
},
});
const tree = new Formatter().format(new Paragraph(image1));
const inlineElements = tree["w:p"][0]["w:r"][0]["w:drawing"][0]["wp:inline"];
const graphicData = inlineElements.find((x) => x["a:graphic"]);
@ -46,7 +60,14 @@ describe("Media", () => {
},
});
const image2 = Media.addImage(file, "test");
const image2 = Media.addImage({
document: file,
data: "test",
transformation: {
width: 100,
height: 100,
},
});
const tree2 = new Formatter().format(new Paragraph(image2));
const inlineElements2 = tree2["w:p"][0]["w:r"][0]["w:drawing"][0]["wp:inline"];
const graphicData2 = inlineElements2.find((x) => x["a:graphic"]);
@ -62,24 +83,32 @@ describe("Media", () => {
describe("#addMedia", () => {
it("should add media", () => {
const image = new Media().addMedia("");
const image = new Media().addMedia("", {
width: 100,
height: 100,
});
expect(image.fileName).to.equal("test.png");
expect(image.dimensions).to.deep.equal({
expect(image.transformation).to.deep.equal({
pixels: {
x: 100,
y: 100,
},
flip: undefined,
emus: {
x: 952500,
y: 952500,
},
rotation: undefined,
});
});
it("should return UInt8Array if atob is present", () => {
global.atob = () => "atob result";
const image = new Media().addMedia("");
const image = new Media().addMedia("", {
width: 100,
height: 100,
});
expect(image.stream).to.be.an.instanceof(Uint8Array);
// tslint:disable-next-line: no-any
@ -89,7 +118,10 @@ describe("Media", () => {
it("should use data as is if its not a string", () => {
global.atob = () => "atob result";
const image = new Media().addMedia(Buffer.from(""));
const image = new Media().addMedia(Buffer.from(""), {
width: 100,
height: 100,
});
expect(image.stream).to.be.an.instanceof(Uint8Array);
// tslint:disable-next-line: no-any
@ -100,20 +132,25 @@ describe("Media", () => {
describe("#getMedia", () => {
it("should get media", () => {
const media = new Media();
media.addMedia("");
media.addMedia("", {
width: 100,
height: 100,
});
const image = media.getMedia("test.png");
expect(image.fileName).to.equal("test.png");
expect(image.dimensions).to.deep.equal({
expect(image.transformation).to.deep.equal({
pixels: {
x: 100,
y: 100,
},
flip: undefined,
emus: {
x: 952500,
y: 952500,
},
rotation: undefined,
});
});
@ -127,7 +164,15 @@ describe("Media", () => {
describe("#Array", () => {
it("Get images as array", () => {
const media = new Media();
media.addMedia("");
media.addMedia("", {
width: 100,
height: 100,
flip: {
vertical: true,
horizontal: true,
},
rotation: 90,
});
const array = media.Array;
expect(array).to.be.an.instanceof(Array);
@ -135,15 +180,20 @@ describe("Media", () => {
const image = array[0];
expect(image.fileName).to.equal("test.png");
expect(image.dimensions).to.deep.equal({
expect(image.transformation).to.deep.equal({
pixels: {
x: 100,
y: 100,
},
flip: {
vertical: true,
horizontal: true,
},
emus: {
x: 952500,
y: 952500,
},
rotation: 5400000,
});
});
});

View File

@ -1,22 +1,31 @@
import { uniqueId } from "convenience-functions";
import { IDrawingOptions } from "../drawing";
import { IFloating } from "../drawing";
import { File } from "../file";
import { PictureRun } from "../paragraph";
import { IMediaData } from "./data";
// import { Image } from "./image";
interface IMediaTransformation {
readonly width: number;
readonly height: number;
readonly flip?: {
readonly vertical?: boolean;
readonly horizontal?: boolean;
};
readonly rotation?: number;
}
export class Media {
public static addImage(
file: File,
buffer: Buffer | string | Uint8Array | ArrayBuffer,
width?: number,
height?: number,
drawingOptions?: IDrawingOptions,
): PictureRun {
public static addImage(options: {
readonly document: File;
readonly data: Buffer | string | Uint8Array | ArrayBuffer;
readonly transformation: IMediaTransformation;
readonly floating?: IFloating;
}): PictureRun {
// Workaround to expose id without exposing to API
const mediaData = file.Media.addMedia(buffer, width, height);
return new PictureRun(mediaData, drawingOptions);
const mediaData = options.document.Media.addMedia(options.data, options.transformation);
return new PictureRun(mediaData, { floating: options.floating });
}
private readonly map: Map<string, IMediaData>;
@ -35,22 +44,13 @@ export class Media {
return data;
}
public addMedia(buffer: Buffer | string | Uint8Array | ArrayBuffer, width: number = 100, height: number = 100): IMediaData {
const key = `${uniqueId()}.png`;
return this.createMedia(
key,
{
width: width,
height: height,
},
buffer,
);
public addMedia(buffer: Buffer | string | Uint8Array | ArrayBuffer, transformation: IMediaTransformation): IMediaData {
return this.createMedia(`${uniqueId()}.png`, transformation, buffer);
}
private createMedia(
key: string,
dimensions: { readonly width: number; readonly height: number },
transformation: IMediaTransformation,
data: Buffer | string | Uint8Array | ArrayBuffer,
filePath?: string,
): IMediaData {
@ -60,15 +60,17 @@ export class Media {
stream: newData,
path: filePath,
fileName: key,
dimensions: {
transformation: {
pixels: {
x: Math.round(dimensions.width),
y: Math.round(dimensions.height),
x: Math.round(transformation.width),
y: Math.round(transformation.height),
},
emus: {
x: Math.round(dimensions.width * 9525),
y: Math.round(dimensions.height * 9525),
x: Math.round(transformation.width * 9525),
y: Math.round(transformation.height * 9525),
},
flip: transformation.flip,
rotation: transformation.rotation ? transformation.rotation * 60000 : undefined,
},
};

View File

@ -159,7 +159,10 @@ export class ImportDotx {
for (const r of wrapperImagesReferences) {
const buffer = await zipContent.files[`word/${r.target}`].async("nodebuffer");
const mediaData = media.addMedia(buffer);
const mediaData = media.addMedia(buffer, {
width: 100,
height: 100,
});
wrapper.Relationships.createRelationship(
r.id,