abstract-document 19.1.25 → 19.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/lib/abstract-document-exporters/docx2/render-image.d.ts.map +1 -1
- package/lib/abstract-document-exporters/docx2/render-image.js +222 -143
- package/lib/abstract-document-exporters/docx2/render-image.js.map +1 -1
- package/lib/abstract-document-exporters/docx2/render.d.ts.map +1 -1
- package/lib/abstract-document-exporters/docx2/render.js +65 -33
- package/lib/abstract-document-exporters/docx2/render.js.map +1 -1
- package/package.json +3 -3
- package/src/abstract-document-exporters/docx2/render-image.ts +331 -161
- package/src/abstract-document-exporters/docx2/render.ts +75 -33
|
@@ -3,180 +3,350 @@ import { ImageRun, IMediaTransformation } from "docx";
|
|
|
3
3
|
import { TextStyle } from "../../abstract-document/styles/text-style.js";
|
|
4
4
|
import { Image } from "../../abstract-document/atoms/image.js";
|
|
5
5
|
import { Resources } from "../../abstract-document/index.js";
|
|
6
|
-
import { fromBase64 } from "../shared/base-64.js";
|
|
6
|
+
import { fromBase64, rawSvgPrefix } from "../shared/base-64.js";
|
|
7
|
+
import * as AD from "../../abstract-document/index.js";
|
|
8
|
+
|
|
9
|
+
const abstractDocPointsToDocxPxRatio = 1; // Set to 1 for now to minimize impact. Can be adjusted to better match PDF image dimensions
|
|
10
|
+
|
|
11
|
+
const emptyPNG =
|
|
12
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO6pH7sAAAAASUVORK5CYII=";
|
|
13
|
+
|
|
14
|
+
const fallbackImage = {
|
|
15
|
+
type: "png" as const,
|
|
16
|
+
data: fromBase64(emptyPNG),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function renderImage(
|
|
20
|
+
image: Image,
|
|
21
|
+
textStyle: TextStyle,
|
|
22
|
+
resources: Resources.Resources
|
|
23
|
+
): ImageRun {
|
|
7
24
|
|
|
8
|
-
export function renderImage(image: Image, textStyle: TextStyle, resources: Resources.Resources): ImageRun {
|
|
9
25
|
const aImage = image.imageResource.abstractImage;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
26
|
+
|
|
27
|
+
const firstComp = aImage.components[0];
|
|
28
|
+
const resource =
|
|
29
|
+
!aImage.size.width &&
|
|
30
|
+
!aImage.size.height &&
|
|
31
|
+
firstComp?.type === "binaryimage" &&
|
|
32
|
+
firstComp.data.type === "url"
|
|
33
|
+
? resources.imageResources?.[firstComp.data.url] ?? image.imageResource
|
|
34
|
+
: image.imageResource;
|
|
35
|
+
|
|
36
|
+
const rect = resourceRect(resource, {
|
|
37
|
+
width: image.width,
|
|
38
|
+
height: image.height,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const imgW = resource.abstractImage.size.width || 1;
|
|
42
|
+
const imgH = resource.abstractImage.size.height || 1;
|
|
43
|
+
|
|
44
|
+
const scaleX = rect.width / imgW;
|
|
45
|
+
const scaleY = rect.height / imgH;
|
|
46
|
+
const scale = Math.min(scaleX, scaleY);
|
|
47
|
+
|
|
48
|
+
const drawnW = imgW * scale;
|
|
49
|
+
const drawnH = imgH * scale;
|
|
50
|
+
|
|
51
|
+
const transformation: IMediaTransformation = { width: drawnW * abstractDocPointsToDocxPxRatio, height: drawnH * abstractDocPointsToDocxPxRatio};
|
|
52
|
+
|
|
53
|
+
const directImage = tryCreateDirectImageRun(resource, transformation, resources);
|
|
54
|
+
if (directImage) {
|
|
55
|
+
return directImage;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const svg = abstractImageToSvg(resource.abstractImage, resources, textStyle);
|
|
59
|
+
|
|
60
|
+
return new ImageRun({
|
|
61
|
+
type: "svg",
|
|
62
|
+
data: Buffer.from(svg, "utf8"),
|
|
63
|
+
transformation,
|
|
64
|
+
fallback: fallbackImage,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function tryCreateDirectImageRun(
|
|
69
|
+
resource: AD.ImageResource.ImageResource,
|
|
70
|
+
transformation: IMediaTransformation,
|
|
71
|
+
resources: Resources.Resources
|
|
72
|
+
): ImageRun | undefined {
|
|
73
|
+
if (resource.abstractImage.components.length !== 1) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const component = resource.abstractImage.components[0];
|
|
78
|
+
if (component.type !== "binaryimage") {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return binaryImageToImageRun(component, transformation, resources);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function binaryImageToImageRun(
|
|
86
|
+
component: AbstractImage.BinaryImage,
|
|
87
|
+
transformation: IMediaTransformation,
|
|
88
|
+
resources: Resources.Resources
|
|
89
|
+
): ImageRun | undefined {
|
|
90
|
+
const format = component.format.toLowerCase();
|
|
91
|
+
|
|
92
|
+
if (component.data.type === "bytes") {
|
|
93
|
+
if (format === "png" || format === "jpg" || format === "jpeg") {
|
|
94
|
+
return new ImageRun({
|
|
95
|
+
type: format === "png" ? "png" : "jpg",
|
|
96
|
+
data: Buffer.from(
|
|
97
|
+
component.data.bytes.buffer,
|
|
98
|
+
component.data.bytes.byteOffset,
|
|
99
|
+
component.data.bytes.byteLength
|
|
100
|
+
),
|
|
101
|
+
transformation,
|
|
102
|
+
} as any);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (format === "svg") {
|
|
106
|
+
return new ImageRun({
|
|
107
|
+
type: "svg",
|
|
108
|
+
data: Buffer.from(component.data.bytes),
|
|
109
|
+
transformation,
|
|
110
|
+
fallback: fallbackImage,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (component.data.type === "url") {
|
|
116
|
+
const nestedResource = resources.imageResources?.[component.data.url];
|
|
117
|
+
|
|
118
|
+
if (nestedResource) {
|
|
119
|
+
const direct = tryCreateDirectImageRun(nestedResource, transformation, resources);
|
|
120
|
+
return direct;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (component.data.url.startsWith(rawSvgPrefix)) {
|
|
124
|
+
const svg = decodeURIComponent(component.data.url.slice(rawSvgPrefix.length));
|
|
125
|
+
|
|
126
|
+
return new ImageRun({
|
|
127
|
+
type: "svg",
|
|
128
|
+
data: Buffer.from(svg, "utf8"),
|
|
129
|
+
transformation,
|
|
130
|
+
fallback: fallbackImage,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const match = /^data:(.+?);base64,(.*)$/.exec(component.data.url);
|
|
135
|
+
if (match) {
|
|
136
|
+
const mimeType = match[1].toLowerCase();
|
|
137
|
+
const data = fromBase64(match[2]);
|
|
138
|
+
|
|
139
|
+
if (mimeType.includes("png")) {
|
|
140
|
+
return new ImageRun({ data, transformation, type: "png" } as any);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (mimeType.includes("jpeg") || mimeType.includes("jpg")) {
|
|
144
|
+
return new ImageRun({ data, transformation, type: "jpg" } as any);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (mimeType.includes("svg")) {
|
|
148
|
+
return new ImageRun({ type: "svg", data, transformation, fallback: fallbackImage});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function abstractImageToSvg(
|
|
157
|
+
image: AbstractImage.AbstractImage,
|
|
158
|
+
resources: Resources.Resources,
|
|
159
|
+
textStyle: TextStyle
|
|
160
|
+
): string {
|
|
161
|
+
const width = image.size.width || 1;
|
|
162
|
+
const height = image.size.height || 1;
|
|
163
|
+
|
|
164
|
+
const children = image.components
|
|
165
|
+
.map((component) => componentToSvg(component, resources, textStyle, 0))
|
|
166
|
+
.filter(Boolean)
|
|
167
|
+
.join("");
|
|
168
|
+
|
|
169
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">${children}</svg>`;
|
|
14
170
|
}
|
|
15
171
|
|
|
16
|
-
function
|
|
172
|
+
function componentToSvg(
|
|
17
173
|
component: AbstractImage.Component,
|
|
18
|
-
rectWidth: number,
|
|
19
|
-
rectHeight: number,
|
|
20
|
-
_textStyle: TextStyle,
|
|
21
174
|
resources: Resources.Resources,
|
|
175
|
+
textStyle: TextStyle,
|
|
22
176
|
circuitBreaker: number
|
|
23
|
-
):
|
|
177
|
+
): string {
|
|
24
178
|
if (++circuitBreaker > 20) {
|
|
25
|
-
return
|
|
179
|
+
return "";
|
|
26
180
|
}
|
|
181
|
+
|
|
27
182
|
switch (component.type) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
183
|
+
case "group":
|
|
184
|
+
return component.children
|
|
185
|
+
.map((child) => componentToSvg(child, resources, textStyle, circuitBreaker))
|
|
186
|
+
.join("");
|
|
187
|
+
|
|
188
|
+
case "subimage": {
|
|
189
|
+
const scaleX = component.size.width / (component.image.size.width || 1);
|
|
190
|
+
const scaleY = component.size.height / (component.image.size.height || 1);
|
|
191
|
+
const scale = Math.min(scaleX, scaleY);
|
|
192
|
+
|
|
193
|
+
const children = component.image.components
|
|
194
|
+
.map((child) => componentToSvg(child, resources, textStyle, circuitBreaker))
|
|
195
|
+
.join("");
|
|
196
|
+
|
|
197
|
+
return `<g transform="translate(${component.topLeft.x} ${component.topLeft.y}) scale(${scale})">${children}</g>`;
|
|
198
|
+
}
|
|
199
|
+
|
|
31
200
|
case "binaryimage":
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return new ImageRun({ data: fromBase64(match[1]), transformation });
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
break;
|
|
66
|
-
//else if (format === "svg") {
|
|
67
|
-
// const svg = new TextDecoder().decode(component.data);
|
|
68
|
-
// const imageWidth = component.bottomRight.x - component.topLeft.x;
|
|
69
|
-
// const imageHeight = component.bottomRight.y - component.topLeft.y;
|
|
70
|
-
// svgToPdfKit(pdf, svg, component.topLeft.x, component.topLeft.y, {
|
|
71
|
-
// width: imageWidth,
|
|
72
|
-
// height: imageHeight,
|
|
73
|
-
// preserveAspectRatio: "xMinYMin",
|
|
74
|
-
// //fontCallback: component.overrideSvgFont
|
|
75
|
-
// // ? (_family: string, _bold: boolean, _italic: boolean) => {
|
|
76
|
-
// // return getFontNameStyle(textStyle);
|
|
77
|
-
// // }
|
|
78
|
-
// // : undefined,
|
|
79
|
-
// // fontCallback: (_family: string, _bold: boolean, _italic: boolean) => {
|
|
80
|
-
// // return getFontNameStyle(textStyle);
|
|
81
|
-
// // }
|
|
82
|
-
// });
|
|
83
|
-
// }
|
|
84
|
-
//break;
|
|
85
|
-
// case "subimage":
|
|
86
|
-
// break;
|
|
87
|
-
// case "line":
|
|
88
|
-
// pdf
|
|
89
|
-
// .lineWidth(component.strokeThickness)
|
|
90
|
-
// .moveTo(component.start.x, component.start.y)
|
|
91
|
-
// .lineTo(component.end.x, component.end.y)
|
|
92
|
-
// .strokeOpacity(colorToOpacity(component.strokeColor))
|
|
93
|
-
// .stroke(colorToRgb(component.strokeColor));
|
|
94
|
-
// break;
|
|
95
|
-
// case "polyline":
|
|
96
|
-
// for (let i = 0; i < component.points.length; ++i) {
|
|
97
|
-
// const p = component.points[i];
|
|
98
|
-
// if (i === 0) {
|
|
99
|
-
// pdf.moveTo(p.x, p.y);
|
|
100
|
-
// } else {
|
|
101
|
-
// pdf.lineTo(p.x, p.y);
|
|
102
|
-
// }
|
|
103
|
-
// }
|
|
104
|
-
// pdf
|
|
105
|
-
// .lineWidth(component.strokeThickness)
|
|
106
|
-
// .strokeOpacity(colorToOpacity(component.strokeColor))
|
|
107
|
-
// .stroke(colorToRgb(component.strokeColor));
|
|
108
|
-
// break;
|
|
109
|
-
// case "text":
|
|
110
|
-
// if (component.clockwiseRotationDegrees !== 0) {
|
|
111
|
-
// pdf.save();
|
|
112
|
-
// pdf.rotate(component.clockwiseRotationDegrees, {
|
|
113
|
-
// origin: [component.position.x, component.position.y],
|
|
114
|
-
// });
|
|
115
|
-
// }
|
|
116
|
-
// pdf.font(component.fontFamily).fontSize(component.fontSize);
|
|
117
|
-
// const stringWidth = pdf.widthOfString(component.text);
|
|
118
|
-
// const stringHeight = pdf.currentLineHeight();
|
|
119
|
-
// const dx =
|
|
120
|
-
// component.horizontalGrowthDirection === "left"
|
|
121
|
-
// ? -stringWidth
|
|
122
|
-
// : component.horizontalGrowthDirection === "uniform"
|
|
123
|
-
// ? -stringWidth * 0.5
|
|
124
|
-
// : 0;
|
|
125
|
-
// const dy =
|
|
126
|
-
// component.verticalGrowthDirection === "up"
|
|
127
|
-
// ? -stringHeight
|
|
128
|
-
// : component.verticalGrowthDirection === "uniform"
|
|
129
|
-
// ? -stringHeight * 0.5
|
|
130
|
-
// : 0;
|
|
131
|
-
// pdf
|
|
132
|
-
// .font(component.fontFamily)
|
|
133
|
-
// .fontSize(component.fontSize)
|
|
134
|
-
// .fillColor(colorToRgb(component.textColor))
|
|
135
|
-
// .text(component.text, component.position.x + dx, component.position.y + dy, { lineBreak: false });
|
|
136
|
-
// if (component.clockwiseRotationDegrees !== 0) {
|
|
137
|
-
// pdf.restore();
|
|
138
|
-
// }
|
|
139
|
-
// break;
|
|
140
|
-
// case "ellipse":
|
|
141
|
-
// const width = component.bottomRight.x - component.topLeft.x;
|
|
142
|
-
// const height = component.bottomRight.y - component.topLeft.y;
|
|
143
|
-
// const centerX = component.topLeft.x + width * 0.5;
|
|
144
|
-
// const centerY = component.topLeft.y + height * 0.5;
|
|
145
|
-
// pdf
|
|
146
|
-
// .lineWidth(component.strokeThickness)
|
|
147
|
-
// .ellipse(centerX, centerY, width * 0.5, height * 0.5)
|
|
148
|
-
// .strokeOpacity(colorToOpacity(component.strokeColor))
|
|
149
|
-
// .fillOpacity(colorToOpacity(component.fillColor))
|
|
150
|
-
// .fillAndStroke(colorToRgb(component.fillColor), colorToRgb(component.strokeColor));
|
|
151
|
-
// break;
|
|
152
|
-
// case "polygon":
|
|
153
|
-
// pdf
|
|
154
|
-
// .lineWidth(component.strokeThickness)
|
|
155
|
-
// .strokeOpacity(colorToOpacity(component.strokeColor))
|
|
156
|
-
// .fillOpacity(colorToOpacity(component.fillColor))
|
|
157
|
-
// .polygon(...component.points.map((p) => [p.x, p.y]))
|
|
158
|
-
// .fillAndStroke(colorToRgb(component.fillColor), colorToRgb(component.strokeColor));
|
|
159
|
-
// break;
|
|
160
|
-
// case "rectangle":
|
|
161
|
-
// const rWidth = component.bottomRight.x - component.topLeft.x;
|
|
162
|
-
// const rHeight = component.bottomRight.y - component.topLeft.y;
|
|
163
|
-
// pdf
|
|
164
|
-
// .lineWidth(component.strokeThickness)
|
|
165
|
-
// .strokeOpacity(colorToOpacity(component.strokeColor))
|
|
166
|
-
// .fillOpacity(colorToOpacity(component.fillColor))
|
|
167
|
-
// .rect(component.topLeft.x, component.topLeft.y, rWidth, rHeight)
|
|
168
|
-
// .fillAndStroke(colorToRgb(component.fillColor), colorToRgb(component.strokeColor));
|
|
169
|
-
// break;
|
|
201
|
+
return binaryImageToSvg(component, resources, textStyle, circuitBreaker);
|
|
202
|
+
|
|
203
|
+
case "line":
|
|
204
|
+
return `<line x1="${component.start.x}" y1="${component.start.y}" x2="${component.end.x}" y2="${component.end.y}" ${strokeAttrs(component.strokeColor, component.strokeThickness, component.strokeDashStyle)} />`;
|
|
205
|
+
|
|
206
|
+
case "polyline":
|
|
207
|
+
return `<polyline points="${component.points.map((p) => `${p.x},${p.y}`).join(" ")}" fill="none" ${strokeAttrs(component.strokeColor, component.strokeThickness, component.strokeDashStyle)} />`;
|
|
208
|
+
|
|
209
|
+
case "polygon":
|
|
210
|
+
return `<polygon points="${component.points.map((p) => `${p.x},${p.y}`).join(" ")}" ${fillAttrs(component.fillColor)} ${strokeAttrs(component.strokeColor, component.strokeThickness, component.strokeDashStyle)} />`;
|
|
211
|
+
|
|
212
|
+
case "rectangle": {
|
|
213
|
+
const width = component.bottomRight.x - component.topLeft.x;
|
|
214
|
+
const height = component.bottomRight.y - component.topLeft.y;
|
|
215
|
+
|
|
216
|
+
return `<rect x="${component.topLeft.x}" y="${component.topLeft.y}" width="${width}" height="${height}" ${fillAttrs(component.fillColor)} ${strokeAttrs(component.strokeColor, component.strokeThickness, component.strokeDashStyle)} />`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
case "ellipse": {
|
|
220
|
+
const width = component.bottomRight.x - component.topLeft.x;
|
|
221
|
+
const height = component.bottomRight.y - component.topLeft.y;
|
|
222
|
+
const cx = component.topLeft.x + width * 0.5;
|
|
223
|
+
const cy = component.topLeft.y + height * 0.5;
|
|
224
|
+
|
|
225
|
+
return `<ellipse cx="${cx}" cy="${cy}" rx="${width * 0.5}" ry="${height * 0.5}" ${fillAttrs(component.fillColor)} ${strokeAttrs(component.strokeColor, component.strokeThickness, component.strokeDashStyle)} />`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
case "text":
|
|
229
|
+
return textComponentToSvg(component);
|
|
230
|
+
|
|
170
231
|
default:
|
|
171
|
-
return
|
|
232
|
+
return "";
|
|
172
233
|
}
|
|
173
|
-
return undefined;
|
|
174
234
|
}
|
|
175
235
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
236
|
+
function binaryImageToSvg(
|
|
237
|
+
component: AbstractImage.BinaryImage,
|
|
238
|
+
resources: Resources.Resources,
|
|
239
|
+
textStyle: TextStyle,
|
|
240
|
+
circuitBreaker: number
|
|
241
|
+
): string {
|
|
242
|
+
const width = component.bottomRight.x - component.topLeft.x;
|
|
243
|
+
const height = component.bottomRight.y - component.topLeft.y;
|
|
244
|
+
const format = component.format.toLowerCase();
|
|
245
|
+
|
|
246
|
+
if (component.data.type === "url") {
|
|
247
|
+
const imageResource = resources.imageResources?.[component.data.url];
|
|
248
|
+
|
|
249
|
+
if (imageResource) {
|
|
250
|
+
const scaleX = width / (imageResource.abstractImage.size.width || 1);
|
|
251
|
+
const scaleY = height / (imageResource.abstractImage.size.height || 1);
|
|
252
|
+
const scale = Math.min(scaleX, scaleY);
|
|
253
|
+
|
|
254
|
+
const children = imageResource.abstractImage.components
|
|
255
|
+
.map((child) => componentToSvg(child, resources, textStyle, circuitBreaker))
|
|
256
|
+
.join("");
|
|
257
|
+
|
|
258
|
+
return `<g transform="translate(${component.topLeft.x} ${component.topLeft.y}) scale(${scale})">${children}</g>`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (component.data.url.startsWith(rawSvgPrefix)) {
|
|
262
|
+
const svg = decodeURIComponent(component.data.url.slice(rawSvgPrefix.length));
|
|
263
|
+
return `<g transform="translate(${component.topLeft.x} ${component.topLeft.y})">${svg}</g>`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return `<image x="${component.topLeft.x}" y="${component.topLeft.y}" width="${width}" height="${height}" href="${escapeXml(component.data.url)}" />`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (component.data.type === "bytes") {
|
|
270
|
+
if (format === "png" || format === "jpg" || format === "jpeg") {
|
|
271
|
+
const mime = format === "png" ? "image/png" : "image/jpeg";
|
|
272
|
+
const base64 = Buffer.from(
|
|
273
|
+
component.data.bytes.buffer,
|
|
274
|
+
component.data.bytes.byteOffset,
|
|
275
|
+
component.data.bytes.byteLength
|
|
276
|
+
).toString("base64");
|
|
277
|
+
|
|
278
|
+
return `<image x="${component.topLeft.x}" y="${component.topLeft.y}" width="${width}" height="${height}" href="data:${mime};base64,${base64}" />`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (format === "svg") {
|
|
282
|
+
const svg = new TextDecoder().decode(component.data.bytes);
|
|
283
|
+
return `<g transform="translate(${component.topLeft.x} ${component.topLeft.y})">${svg}</g>`;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return "";
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function textComponentToSvg(component: AbstractImage.Text): string {
|
|
291
|
+
const rotation =
|
|
292
|
+
component.clockwiseRotationDegrees !== 0
|
|
293
|
+
? ` transform="rotate(${component.clockwiseRotationDegrees} ${component.position.x} ${component.position.y})"`
|
|
294
|
+
: "";
|
|
295
|
+
|
|
296
|
+
const anchor =
|
|
297
|
+
component.horizontalGrowthDirection === "left"
|
|
298
|
+
? "end"
|
|
299
|
+
: component.horizontalGrowthDirection === "uniform"
|
|
300
|
+
? "middle"
|
|
301
|
+
: "start";
|
|
302
|
+
|
|
303
|
+
const dominantBaseline =
|
|
304
|
+
component.verticalGrowthDirection === "up"
|
|
305
|
+
? "text-after-edge"
|
|
306
|
+
: component.verticalGrowthDirection === "uniform"
|
|
307
|
+
? "middle"
|
|
308
|
+
: "text-before-edge";
|
|
179
309
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
310
|
+
return `<text x="${component.position.x}" y="${component.position.y}" font-family="${escapeXml(component.fontFamily)}" font-size="${component.fontSize}" font-weight="${component.fontWeight}" font-style="${component.italic ? "italic" : "normal"}" fill="${colorToCss(component.textColor)}" text-anchor="${anchor}" dominant-baseline="${dominantBaseline}"${rotation}>${escapeXml(component.text)}</text>`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function fillAttrs(color: AbstractImage.Color): string {
|
|
314
|
+
return `fill="${colorToCss(color)}" fill-opacity="${colorToOpacity(color)}"`;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function strokeAttrs(
|
|
318
|
+
color: AbstractImage.Color,
|
|
319
|
+
thickness: number,
|
|
320
|
+
dashStyle: AbstractImage.DashStyle
|
|
321
|
+
): string {
|
|
322
|
+
const dashArray =
|
|
323
|
+
dashStyle.dashes.length > 0
|
|
324
|
+
? ` stroke-dasharray="${dashStyle.dashes.filter((dash) => dash !== 0).join(" ")}" stroke-dashoffset="${dashStyle.offset}"`
|
|
325
|
+
: "";
|
|
326
|
+
|
|
327
|
+
return `stroke="${colorToCss(color)}" stroke-opacity="${colorToOpacity(color)}" stroke-width="${thickness}"${dashArray}`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function colorToOpacity(color: AbstractImage.Color): number {
|
|
331
|
+
return color.a / 255;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function colorToCss(color: AbstractImage.Color): string {
|
|
335
|
+
return `rgb(${color.r},${color.g},${color.b})`;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function escapeXml(value: string): string {
|
|
339
|
+
return value
|
|
340
|
+
.replace(/&/g, "&")
|
|
341
|
+
.replace(/"/g, """)
|
|
342
|
+
.replace(/</g, "<")
|
|
343
|
+
.replace(/>/g, ">");
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function resourceRect(resource: AD.ImageResource.ImageResource, rect: AbstractImage.Size): AbstractImage.Size {
|
|
347
|
+
const ai = resource.abstractImage;
|
|
348
|
+
const rectWidth = rect.width || ai.size.width * (rect.height / (ai.size.height || 1));
|
|
349
|
+
const rectHeight = rect.height || ai.size.height * (rect.width / (ai.size.width || 1));
|
|
350
|
+
|
|
351
|
+
return { width: rectWidth, height: rectHeight };
|
|
352
|
+
}
|