abstract-image 3.3.0 → 3.3.2

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.
Files changed (110) hide show
  1. package/CHANGELOG.md +38 -27
  2. package/LICENSE +21 -21
  3. package/README.md +73 -73
  4. package/lib/exporters/__tests__/dxf2d-export-image/test-defs/dxf2d-ellipse.js +379 -379
  5. package/lib/exporters/__tests__/dxf2d-export-image/test-defs/dxf2d-group.js +123 -123
  6. package/lib/exporters/__tests__/dxf2d-export-image/test-defs/dxf2d-line.js +55 -55
  7. package/lib/exporters/__tests__/dxf2d-export-image/test-defs/dxf2d-polygon.js +89 -89
  8. package/lib/exporters/__tests__/dxf2d-export-image/test-defs/dxf2d-polyline.js +79 -79
  9. package/lib/exporters/__tests__/dxf2d-export-image/test-defs/dxf2d-rectangle.js +99 -99
  10. package/lib/exporters/__tests__/dxf2d-export-image/test-defs/dxf2d-text-growth-directions.js +135 -135
  11. package/lib/exporters/__tests__/dxf2d-export-image/test-defs/dxf2d-text.js +63 -63
  12. package/lib/exporters/__tests__/eps-export-image/test-defs/eps-ellipse.js +24 -24
  13. package/lib/exporters/__tests__/eps-export-image/test-defs/eps-empty-text.js +26 -26
  14. package/lib/exporters/__tests__/eps-export-image/test-defs/eps-group.js +31 -31
  15. package/lib/exporters/__tests__/eps-export-image/test-defs/eps-line.js +20 -20
  16. package/lib/exporters/__tests__/eps-export-image/test-defs/eps-polygon.js +34 -34
  17. package/lib/exporters/__tests__/eps-export-image/test-defs/eps-polyline.js +26 -26
  18. package/lib/exporters/__tests__/eps-export-image/test-defs/eps-rectangle.js +20 -20
  19. package/lib/exporters/__tests__/eps-export-image/test-defs/eps-text-growth-directions.js +59 -59
  20. package/lib/exporters/__tests__/eps-export-image/test-defs/eps-text.js +26 -26
  21. package/lib/exporters/__tests__/react-svg-export-image/test-defs/react-svg-binary-url.d.ts +3 -0
  22. package/lib/exporters/__tests__/react-svg-export-image/test-defs/react-svg-binary-url.d.ts.map +1 -0
  23. package/lib/exporters/__tests__/react-svg-export-image/test-defs/react-svg-binary-url.js +33 -0
  24. package/lib/exporters/__tests__/react-svg-export-image/test-defs/react-svg-binary-url.js.map +1 -0
  25. package/lib/exporters/__tests__/svg-export-image.test.d.ts +2 -0
  26. package/lib/exporters/__tests__/svg-export-image.test.d.ts.map +1 -0
  27. package/lib/exporters/__tests__/svg-export-image.test.js +35 -0
  28. package/lib/exporters/__tests__/svg-export-image.test.js.map +1 -0
  29. package/package.json +2 -2
  30. package/src/__stories__/react-svg-export/example-1.stories.tsx +54 -54
  31. package/src/__stories__/svg-export/example-1.stories.tsx +42 -42
  32. package/src/exporters/__tests__/dxf2d-export-image/export-test-def.ts +11 -11
  33. package/src/exporters/__tests__/dxf2d-export-image/export.test.tsx +13 -13
  34. package/src/exporters/__tests__/dxf2d-export-image/test-defs/dxf2d-ellipse.ts +405 -405
  35. package/src/exporters/__tests__/dxf2d-export-image/test-defs/dxf2d-group.ts +166 -166
  36. package/src/exporters/__tests__/dxf2d-export-image/test-defs/dxf2d-line.ts +80 -80
  37. package/src/exporters/__tests__/dxf2d-export-image/test-defs/dxf2d-polygon.ts +114 -114
  38. package/src/exporters/__tests__/dxf2d-export-image/test-defs/dxf2d-polyline.ts +103 -103
  39. package/src/exporters/__tests__/dxf2d-export-image/test-defs/dxf2d-rectangle.ts +125 -125
  40. package/src/exporters/__tests__/dxf2d-export-image/test-defs/dxf2d-text-growth-directions.ts +214 -214
  41. package/src/exporters/__tests__/dxf2d-export-image/test-defs/dxf2d-text.ts +97 -97
  42. package/src/exporters/__tests__/eps-export-image/export-test-def.ts +11 -11
  43. package/src/exporters/__tests__/eps-export-image/export.test.tsx +13 -13
  44. package/src/exporters/__tests__/eps-export-image/test-defs/eps-ellipse.ts +50 -50
  45. package/src/exporters/__tests__/eps-export-image/test-defs/eps-empty-text.ts +60 -60
  46. package/src/exporters/__tests__/eps-export-image/test-defs/eps-group.ts +74 -74
  47. package/src/exporters/__tests__/eps-export-image/test-defs/eps-line.ts +45 -45
  48. package/src/exporters/__tests__/eps-export-image/test-defs/eps-polygon.ts +65 -65
  49. package/src/exporters/__tests__/eps-export-image/test-defs/eps-polyline.ts +58 -58
  50. package/src/exporters/__tests__/eps-export-image/test-defs/eps-rectangle.ts +46 -46
  51. package/src/exporters/__tests__/eps-export-image/test-defs/eps-text-growth-directions.ts +138 -138
  52. package/src/exporters/__tests__/eps-export-image/test-defs/eps-text.ts +60 -60
  53. package/src/exporters/__tests__/exception/png-unsupported.test.tsx +25 -25
  54. package/src/exporters/__tests__/exception/react-svg-direction-exception.test.tsx +65 -65
  55. package/src/exporters/__tests__/exception/svg-direction-exception.test.tsx +65 -65
  56. package/src/exporters/__tests__/png-export-image/export-test-def.ts +11 -11
  57. package/src/exporters/__tests__/png-export-image/export.test.tsx +13 -13
  58. package/src/exporters/__tests__/png-export-image/test-defs/png-createPNG.tsx +26 -26
  59. package/src/exporters/__tests__/react-svg-export-image/export-test-def.tsx +13 -13
  60. package/src/exporters/__tests__/react-svg-export-image/export.test.tsx +13 -13
  61. package/src/exporters/__tests__/react-svg-export-image/test-defs/react-svg-binary-png.tsx +26 -26
  62. package/src/exporters/__tests__/react-svg-export-image/test-defs/react-svg-binary-url.tsx +26 -0
  63. package/src/exporters/__tests__/react-svg-export-image/test-defs/react-svg-binary.tsx +25 -25
  64. package/src/exporters/__tests__/react-svg-export-image/test-defs/react-svg-callback.tsx +60 -60
  65. package/src/exporters/__tests__/react-svg-export-image/test-defs/react-svg-ellipse.tsx +28 -28
  66. package/src/exporters/__tests__/react-svg-export-image/test-defs/react-svg-empty-text.tsx +35 -35
  67. package/src/exporters/__tests__/react-svg-export-image/test-defs/react-svg-group.tsx +44 -44
  68. package/src/exporters/__tests__/react-svg-export-image/test-defs/react-svg-line.tsx +26 -26
  69. package/src/exporters/__tests__/react-svg-export-image/test-defs/react-svg-polygon.tsx +32 -32
  70. package/src/exporters/__tests__/react-svg-export-image/test-defs/react-svg-polyline.tsx +33 -33
  71. package/src/exporters/__tests__/react-svg-export-image/test-defs/react-svg-rectangle.tsx +27 -27
  72. package/src/exporters/__tests__/react-svg-export-image/test-defs/react-svg-subimage.tsx +36 -36
  73. package/src/exporters/__tests__/react-svg-export-image/test-defs/react-svg-text-bold.tsx +50 -50
  74. package/src/exporters/__tests__/react-svg-export-image/test-defs/react-svg-text-growth-directions.tsx +80 -80
  75. package/src/exporters/__tests__/react-svg-export-image/test-defs/react-svg-text-italic.tsx +65 -65
  76. package/src/exporters/__tests__/react-svg-export-image/test-defs/react-svg-text-sub.tsx +35 -35
  77. package/src/exporters/__tests__/react-svg-export-image/test-defs/react-svg-text.tsx +35 -35
  78. package/src/exporters/__tests__/svg-export-image/export-test-def.ts +11 -11
  79. package/src/exporters/__tests__/svg-export-image/export.test.tsx +13 -13
  80. package/src/exporters/__tests__/svg-export-image/test-defs/svg-binary.tsx +25 -25
  81. package/src/exporters/__tests__/svg-export-image/test-defs/svg-ellipse.ts +27 -27
  82. package/src/exporters/__tests__/svg-export-image/test-defs/svg-empty-text.ts +34 -34
  83. package/src/exporters/__tests__/svg-export-image/test-defs/svg-group.ts +44 -44
  84. package/src/exporters/__tests__/svg-export-image/test-defs/svg-line.ts +26 -26
  85. package/src/exporters/__tests__/svg-export-image/test-defs/svg-polygon.ts +32 -32
  86. package/src/exporters/__tests__/svg-export-image/test-defs/svg-polyline.ts +33 -33
  87. package/src/exporters/__tests__/svg-export-image/test-defs/svg-rectangle.ts +27 -27
  88. package/src/exporters/__tests__/svg-export-image/test-defs/svg-text-bold.ts +50 -50
  89. package/src/exporters/__tests__/svg-export-image/test-defs/svg-text-growth-directions.ts +80 -80
  90. package/src/exporters/__tests__/svg-export-image/test-defs/svg-text-italic.ts +65 -65
  91. package/src/exporters/__tests__/svg-export-image/test-defs/svg-text.ts +35 -35
  92. package/src/exporters/dxf2d-export-image.ts +218 -218
  93. package/src/exporters/eps-export-image.ts +154 -154
  94. package/src/exporters/index.ts +3 -3
  95. package/src/exporters/png-export-image.ts +12 -12
  96. package/src/exporters/react-svg-export-image.tsx +315 -315
  97. package/src/exporters/svg-export-image.ts +309 -309
  98. package/src/index.ts +11 -11
  99. package/src/model/__tests__/color/export-test-def.ts +13 -13
  100. package/src/model/__tests__/color/export.test.tsx +14 -14
  101. package/src/model/__tests__/color/test-defs/color-from-string.ts +46 -46
  102. package/src/model/__tests__/color/test-defs/color-to-string.ts +35 -35
  103. package/src/model/__tests__/color/test-defs/color-undefined-2.ts +8 -8
  104. package/src/model/__tests__/color/test-defs/color-undefined.ts +8 -8
  105. package/src/model/abstract-image.ts +25 -25
  106. package/src/model/color.ts +52 -52
  107. package/src/model/component.ts +279 -279
  108. package/src/model/index.ts +5 -5
  109. package/src/model/point.ts +11 -11
  110. package/src/model/size.ts +11 -11
@@ -1,315 +1,315 @@
1
- import * as R from "ramda";
2
- import * as B64 from "base64-js";
3
- import * as React from "react";
4
- import * as AbstractImage from "../model/index";
5
- import { TextEncoder } from "util";
6
-
7
- export interface ReactSvgCallbacks {
8
- readonly onClick?: MouseCallback;
9
- readonly onDoubleClick?: MouseCallback;
10
- readonly onMouseMove?: MouseCallback;
11
- readonly onContextMenu?: MouseCallback;
12
- }
13
-
14
- export type MouseCallback = (id: string | undefined, point: AbstractImage.Point) => void;
15
-
16
- export function createReactSvg(
17
- image: AbstractImage.AbstractImage,
18
- callbacks?: ReactSvgCallbacks
19
- ): React.ReactElement<{}> {
20
- const cb = callbacks || {};
21
- const id = "ai_root";
22
- return (
23
- <svg
24
- id={id}
25
- width={`${image.size.width}px`}
26
- height={`${image.size.height}px`}
27
- viewBox={[0, 0, image.size.width, image.size.height].join(" ")}
28
- onClick={_callback(cb.onClick, id)}
29
- onDoubleClick={_callback(cb.onDoubleClick, id)}
30
- onMouseMove={_callback(cb.onMouseMove, id)}
31
- onContextMenu={_callback(cb.onContextMenu, id)}
32
- >
33
- {R.unnest(
34
- R.addIndex(R.map)(
35
- // tslint:disable-next-line:no-any
36
- (c, i) => _visit(i.toString(), c as any),
37
- image.components
38
- )
39
- )}
40
- </svg>
41
- );
42
- }
43
-
44
- function _callback(callback: MouseCallback | undefined, rootId: string): React.MouseEventHandler<Element> | undefined {
45
- if (!callback) {
46
- return undefined;
47
- }
48
- return (e: React.MouseEvent<Element>) => {
49
- const rect = e.currentTarget.getBoundingClientRect();
50
- const offsetX = e.clientX - rect.left;
51
- const offsetY = e.clientY - rect.top;
52
- const mousePoint = AbstractImage.createPoint(offsetX, offsetY);
53
- const id = getIdAttr(e.target as Element, rootId);
54
- callback(id && id !== "" ? id : undefined, mousePoint);
55
- };
56
- }
57
-
58
- function makeIdAttr(id: string | undefined): string | undefined {
59
- if (!id) {
60
- return undefined;
61
- }
62
- return `ai%${id}`;
63
- }
64
-
65
- function getIdAttr(target: Element | undefined, rootId: string): string | undefined {
66
- if (!target || target.id === rootId) {
67
- return undefined;
68
- }
69
- const id = target.id;
70
- const parts = id.split("%");
71
- if (parts.length !== 2 || parts[0] !== "ai") {
72
- return getIdAttr((target.parentNode as Element) || undefined, rootId);
73
- }
74
- return parts[1];
75
- }
76
-
77
- function _visit(key: string, component: AbstractImage.Component): Array<React.ReactElement<{}>> {
78
- switch (component.type) {
79
- case "group":
80
- return [
81
- <g key={key} name={component.name}>
82
- {R.unnest(
83
- R.addIndex(R.map)(
84
- // tslint:disable-next-line:no-any
85
- (c, i) => _visit(i.toString(), c as any),
86
- component.children
87
- )
88
- )}
89
- </g>,
90
- ];
91
- case "binaryimage":
92
- const url = getImageUrl(component.format, component.data);
93
- return [
94
- <image
95
- key={key}
96
- x={component.topLeft.x}
97
- y={component.topLeft.y}
98
- width={component.bottomRight.x - component.topLeft.x}
99
- height={component.bottomRight.y - component.topLeft.y}
100
- id={makeIdAttr(component.id)}
101
- href={url}
102
- />,
103
- ];
104
- case "line":
105
- return [
106
- <line
107
- id={makeIdAttr(component.id)}
108
- key={key}
109
- x1={component.start.x}
110
- y1={component.start.y}
111
- x2={component.end.x}
112
- y2={component.end.y}
113
- stroke={colorToRgb(component.strokeColor)}
114
- strokeWidth={component.strokeThickness}
115
- strokeOpacity={colorToOpacity(component.strokeColor)}
116
- />,
117
- ];
118
- case "text":
119
- if (!component.text) {
120
- return [];
121
- }
122
- const lineHeight = component.fontSize;
123
-
124
- const shadowStyle = {
125
- textAnchor: getTextAnchor(component.horizontalGrowthDirection),
126
- fontSize: component.fontSize.toString() + "px",
127
- fontWeight: component.fontWeight === "mediumBold" ? "bold" : component.fontWeight,
128
- fontFamily: component.fontFamily,
129
- stroke: colorToRgb(component.strokeColor),
130
- strokeWidth: component.strokeThickness,
131
- };
132
- const style = {
133
- textAnchor: getTextAnchor(component.horizontalGrowthDirection),
134
- fontSize: component.fontSize.toString() + "px",
135
- fontWeight: component.fontWeight === "mediumBold" ? "bold" : component.fontWeight,
136
- fontFamily: component.fontFamily,
137
- fill: colorToRgb(component.textColor),
138
- };
139
- const dy = getBaselineAdjustment(component.verticalGrowthDirection);
140
-
141
- const transform =
142
- "rotate(" +
143
- component.clockwiseRotationDegrees.toString() +
144
- " " +
145
- component.position.x.toString() +
146
- " " +
147
- component.position.y.toString() +
148
- ")";
149
-
150
- const lines: Array<string> = component.text !== null ? component.text.split("\n") : [];
151
- const tSpans = lines.map((t) =>
152
- renderLine(
153
- t,
154
- component.position.x,
155
- component.position.y + (lines.indexOf(t) + dy) * lineHeight,
156
- component.fontSize,
157
- lineHeight
158
- )
159
- );
160
- let cs: Array<React.ReactElement<{}>> = [];
161
- if (component.strokeThickness > 0 && component.strokeColor) {
162
- cs.push(
163
- <text key={key + "shadow"} style={shadowStyle} transform={transform}>
164
- {tSpans}
165
- </text>
166
- );
167
- }
168
- cs.push(
169
- <text key={key} style={style} transform={transform}>
170
- {tSpans}
171
- </text>
172
- );
173
- return cs;
174
- case "ellipse":
175
- const rx = Math.abs(component.bottomRight.x - component.topLeft.x) * 0.5;
176
- const ry = Math.abs(component.bottomRight.y - component.topLeft.y) * 0.5;
177
- const cx = (component.bottomRight.x + component.topLeft.x) * 0.5;
178
- const cy = (component.bottomRight.y + component.topLeft.y) * 0.5;
179
- return [
180
- <ellipse
181
- id={makeIdAttr(component.id)}
182
- key={key}
183
- cx={cx}
184
- cy={cy}
185
- rx={rx}
186
- ry={ry}
187
- stroke={colorToRgb(component.strokeColor)}
188
- strokeWidth={component.strokeThickness}
189
- strokeOpacity={colorToOpacity(component.strokeColor)}
190
- fillOpacity={colorToOpacity(component.fillColor)}
191
- fill={colorToRgb(component.fillColor)}
192
- />,
193
- ];
194
- case "polyline":
195
- let linePoints = component.points.map((p) => p.x.toString() + "," + p.y.toString()).join(" ");
196
- return [
197
- <polyline
198
- id={makeIdAttr(component.id)}
199
- key={key}
200
- points={linePoints}
201
- stroke={colorToRgb(component.strokeColor)}
202
- strokeWidth={component.strokeThickness}
203
- strokeOpacity={colorToOpacity(component.strokeColor)}
204
- fill="none"
205
- />,
206
- ];
207
- case "polygon":
208
- let points = component.points.map((p) => p.x.toString() + "," + p.y.toString()).join(" ");
209
- return [
210
- <polygon
211
- id={makeIdAttr(component.id)}
212
- key={key}
213
- points={points}
214
- stroke={colorToRgb(component.strokeColor)}
215
- strokeWidth={component.strokeThickness}
216
- strokeOpacity={colorToOpacity(component.strokeColor)}
217
- fillOpacity={colorToOpacity(component.fillColor)}
218
- fill={colorToRgb(component.fillColor)}
219
- />,
220
- ];
221
- case "rectangle":
222
- return [
223
- <rect
224
- id={makeIdAttr(component.id)}
225
- key={key}
226
- x={component.topLeft.x}
227
- y={component.topLeft.y}
228
- width={Math.abs(component.bottomRight.x - component.topLeft.x)}
229
- height={Math.abs(component.bottomRight.y - component.topLeft.y)}
230
- stroke={colorToRgb(component.strokeColor)}
231
- strokeWidth={component.strokeThickness}
232
- strokeOpacity={colorToOpacity(component.strokeColor)}
233
- fillOpacity={colorToOpacity(component.fillColor)}
234
- fill={colorToRgb(component.fillColor)}
235
- />,
236
- ];
237
- default:
238
- return [];
239
- }
240
- }
241
-
242
- function getImageUrl(format: AbstractImage.BinaryFormat, data: AbstractImage.ImageData): string {
243
- if (data.type === "url") {
244
- return data.url;
245
- } else if (format === "png") {
246
- const base64 = B64.fromByteArray(data.bytes);
247
- return `data:image/png;base64,${base64}`;
248
- } else {
249
- const svg = String.fromCharCode(...data.bytes).replace('<?xml version="1.0" encoding="utf-8"?>', "");
250
- const bytes = [];
251
- for (let i = 0; i < svg.length; ++i) {
252
- bytes.push(svg.charCodeAt(i));
253
- }
254
- const base64 = B64.fromByteArray(new Uint8Array(bytes));
255
- return `data:image/svg+xml;base64,${base64}`;
256
- }
257
- }
258
-
259
- function renderLine(text: string, x: number, y: number, fontSize: number, lineHeight: number): JSX.Element {
260
- const split = R.unnest<string>(text.split("<sub>").map((t) => t.split("</sub>")));
261
- let inside = false;
262
- const tags: Array<JSX.Element> = [];
263
- for (let i = 0; i < split.length; ++i) {
264
- const splitText = split[i];
265
- if (inside) {
266
- tags.push(
267
- <tspan key={i} baselineShift="sub" style={{ fontSize: (fontSize * 0.8).toString() + "px" }}>
268
- {splitText}
269
- </tspan>
270
- );
271
- } else {
272
- tags.push(<tspan key={i}>{splitText}</tspan>);
273
- }
274
- inside = !inside;
275
- }
276
- return (
277
- <tspan key={text} x={x} y={y} height={lineHeight.toString() + "px"}>
278
- {tags}
279
- </tspan>
280
- );
281
- }
282
-
283
- function getBaselineAdjustment(d: AbstractImage.GrowthDirection): number {
284
- if (d === "up") {
285
- return 0.0;
286
- }
287
- if (d === "uniform") {
288
- return 0.5;
289
- }
290
- if (d === "down") {
291
- return 1.0;
292
- }
293
- throw "Unknown text alignment " + d;
294
- }
295
-
296
- function getTextAnchor(d: AbstractImage.GrowthDirection): "end" | "middle" | "start" {
297
- if (d === "left") {
298
- return "end";
299
- }
300
- if (d === "uniform") {
301
- return "middle";
302
- }
303
- if (d === "right") {
304
- return "start";
305
- }
306
- throw "Unknown text alignment " + d;
307
- }
308
-
309
- function colorToRgb(color: AbstractImage.Color): string {
310
- return "rgb(" + color.r.toString() + "," + color.g.toString() + "," + color.b.toString() + ")";
311
- }
312
-
313
- function colorToOpacity(color: AbstractImage.Color): string {
314
- return (color.a / 255).toString();
315
- }
1
+ import * as R from "ramda";
2
+ import * as B64 from "base64-js";
3
+ import * as React from "react";
4
+ import * as AbstractImage from "../model/index";
5
+ import { TextEncoder } from "util";
6
+
7
+ export interface ReactSvgCallbacks {
8
+ readonly onClick?: MouseCallback;
9
+ readonly onDoubleClick?: MouseCallback;
10
+ readonly onMouseMove?: MouseCallback;
11
+ readonly onContextMenu?: MouseCallback;
12
+ }
13
+
14
+ export type MouseCallback = (id: string | undefined, point: AbstractImage.Point) => void;
15
+
16
+ export function createReactSvg(
17
+ image: AbstractImage.AbstractImage,
18
+ callbacks?: ReactSvgCallbacks
19
+ ): React.ReactElement<{}> {
20
+ const cb = callbacks || {};
21
+ const id = "ai_root";
22
+ return (
23
+ <svg
24
+ id={id}
25
+ width={`${image.size.width}px`}
26
+ height={`${image.size.height}px`}
27
+ viewBox={[0, 0, image.size.width, image.size.height].join(" ")}
28
+ onClick={_callback(cb.onClick, id)}
29
+ onDoubleClick={_callback(cb.onDoubleClick, id)}
30
+ onMouseMove={_callback(cb.onMouseMove, id)}
31
+ onContextMenu={_callback(cb.onContextMenu, id)}
32
+ >
33
+ {R.unnest(
34
+ R.addIndex(R.map)(
35
+ // tslint:disable-next-line:no-any
36
+ (c, i) => _visit(i.toString(), c as any),
37
+ image.components
38
+ )
39
+ )}
40
+ </svg>
41
+ );
42
+ }
43
+
44
+ function _callback(callback: MouseCallback | undefined, rootId: string): React.MouseEventHandler<Element> | undefined {
45
+ if (!callback) {
46
+ return undefined;
47
+ }
48
+ return (e: React.MouseEvent<Element>) => {
49
+ const rect = e.currentTarget.getBoundingClientRect();
50
+ const offsetX = e.clientX - rect.left;
51
+ const offsetY = e.clientY - rect.top;
52
+ const mousePoint = AbstractImage.createPoint(offsetX, offsetY);
53
+ const id = getIdAttr(e.target as Element, rootId);
54
+ callback(id && id !== "" ? id : undefined, mousePoint);
55
+ };
56
+ }
57
+
58
+ function makeIdAttr(id: string | undefined): string | undefined {
59
+ if (!id) {
60
+ return undefined;
61
+ }
62
+ return `ai%${id}`;
63
+ }
64
+
65
+ function getIdAttr(target: Element | undefined, rootId: string): string | undefined {
66
+ if (!target || target.id === rootId) {
67
+ return undefined;
68
+ }
69
+ const id = target.id;
70
+ const parts = id.split("%");
71
+ if (parts.length !== 2 || parts[0] !== "ai") {
72
+ return getIdAttr((target.parentNode as Element) || undefined, rootId);
73
+ }
74
+ return parts[1];
75
+ }
76
+
77
+ function _visit(key: string, component: AbstractImage.Component): Array<React.ReactElement<{}>> {
78
+ switch (component.type) {
79
+ case "group":
80
+ return [
81
+ <g key={key} name={component.name}>
82
+ {R.unnest(
83
+ R.addIndex(R.map)(
84
+ // tslint:disable-next-line:no-any
85
+ (c, i) => _visit(i.toString(), c as any),
86
+ component.children
87
+ )
88
+ )}
89
+ </g>,
90
+ ];
91
+ case "binaryimage":
92
+ const url = getImageUrl(component.format, component.data);
93
+ return [
94
+ <image
95
+ key={key}
96
+ x={component.topLeft.x}
97
+ y={component.topLeft.y}
98
+ width={component.bottomRight.x - component.topLeft.x}
99
+ height={component.bottomRight.y - component.topLeft.y}
100
+ id={makeIdAttr(component.id)}
101
+ href={url}
102
+ />,
103
+ ];
104
+ case "line":
105
+ return [
106
+ <line
107
+ id={makeIdAttr(component.id)}
108
+ key={key}
109
+ x1={component.start.x}
110
+ y1={component.start.y}
111
+ x2={component.end.x}
112
+ y2={component.end.y}
113
+ stroke={colorToRgb(component.strokeColor)}
114
+ strokeWidth={component.strokeThickness}
115
+ strokeOpacity={colorToOpacity(component.strokeColor)}
116
+ />,
117
+ ];
118
+ case "text":
119
+ if (!component.text) {
120
+ return [];
121
+ }
122
+ const lineHeight = component.fontSize;
123
+
124
+ const shadowStyle = {
125
+ textAnchor: getTextAnchor(component.horizontalGrowthDirection),
126
+ fontSize: component.fontSize.toString() + "px",
127
+ fontWeight: component.fontWeight === "mediumBold" ? "bold" : component.fontWeight,
128
+ fontFamily: component.fontFamily,
129
+ stroke: colorToRgb(component.strokeColor),
130
+ strokeWidth: component.strokeThickness,
131
+ };
132
+ const style = {
133
+ textAnchor: getTextAnchor(component.horizontalGrowthDirection),
134
+ fontSize: component.fontSize.toString() + "px",
135
+ fontWeight: component.fontWeight === "mediumBold" ? "bold" : component.fontWeight,
136
+ fontFamily: component.fontFamily,
137
+ fill: colorToRgb(component.textColor),
138
+ };
139
+ const dy = getBaselineAdjustment(component.verticalGrowthDirection);
140
+
141
+ const transform =
142
+ "rotate(" +
143
+ component.clockwiseRotationDegrees.toString() +
144
+ " " +
145
+ component.position.x.toString() +
146
+ " " +
147
+ component.position.y.toString() +
148
+ ")";
149
+
150
+ const lines: Array<string> = component.text !== null ? component.text.split("\n") : [];
151
+ const tSpans = lines.map((t) =>
152
+ renderLine(
153
+ t,
154
+ component.position.x,
155
+ component.position.y + (lines.indexOf(t) + dy) * lineHeight,
156
+ component.fontSize,
157
+ lineHeight
158
+ )
159
+ );
160
+ let cs: Array<React.ReactElement<{}>> = [];
161
+ if (component.strokeThickness > 0 && component.strokeColor) {
162
+ cs.push(
163
+ <text key={key + "shadow"} style={shadowStyle} transform={transform}>
164
+ {tSpans}
165
+ </text>
166
+ );
167
+ }
168
+ cs.push(
169
+ <text key={key} style={style} transform={transform}>
170
+ {tSpans}
171
+ </text>
172
+ );
173
+ return cs;
174
+ case "ellipse":
175
+ const rx = Math.abs(component.bottomRight.x - component.topLeft.x) * 0.5;
176
+ const ry = Math.abs(component.bottomRight.y - component.topLeft.y) * 0.5;
177
+ const cx = (component.bottomRight.x + component.topLeft.x) * 0.5;
178
+ const cy = (component.bottomRight.y + component.topLeft.y) * 0.5;
179
+ return [
180
+ <ellipse
181
+ id={makeIdAttr(component.id)}
182
+ key={key}
183
+ cx={cx}
184
+ cy={cy}
185
+ rx={rx}
186
+ ry={ry}
187
+ stroke={colorToRgb(component.strokeColor)}
188
+ strokeWidth={component.strokeThickness}
189
+ strokeOpacity={colorToOpacity(component.strokeColor)}
190
+ fillOpacity={colorToOpacity(component.fillColor)}
191
+ fill={colorToRgb(component.fillColor)}
192
+ />,
193
+ ];
194
+ case "polyline":
195
+ let linePoints = component.points.map((p) => p.x.toString() + "," + p.y.toString()).join(" ");
196
+ return [
197
+ <polyline
198
+ id={makeIdAttr(component.id)}
199
+ key={key}
200
+ points={linePoints}
201
+ stroke={colorToRgb(component.strokeColor)}
202
+ strokeWidth={component.strokeThickness}
203
+ strokeOpacity={colorToOpacity(component.strokeColor)}
204
+ fill="none"
205
+ />,
206
+ ];
207
+ case "polygon":
208
+ let points = component.points.map((p) => p.x.toString() + "," + p.y.toString()).join(" ");
209
+ return [
210
+ <polygon
211
+ id={makeIdAttr(component.id)}
212
+ key={key}
213
+ points={points}
214
+ stroke={colorToRgb(component.strokeColor)}
215
+ strokeWidth={component.strokeThickness}
216
+ strokeOpacity={colorToOpacity(component.strokeColor)}
217
+ fillOpacity={colorToOpacity(component.fillColor)}
218
+ fill={colorToRgb(component.fillColor)}
219
+ />,
220
+ ];
221
+ case "rectangle":
222
+ return [
223
+ <rect
224
+ id={makeIdAttr(component.id)}
225
+ key={key}
226
+ x={component.topLeft.x}
227
+ y={component.topLeft.y}
228
+ width={Math.abs(component.bottomRight.x - component.topLeft.x)}
229
+ height={Math.abs(component.bottomRight.y - component.topLeft.y)}
230
+ stroke={colorToRgb(component.strokeColor)}
231
+ strokeWidth={component.strokeThickness}
232
+ strokeOpacity={colorToOpacity(component.strokeColor)}
233
+ fillOpacity={colorToOpacity(component.fillColor)}
234
+ fill={colorToRgb(component.fillColor)}
235
+ />,
236
+ ];
237
+ default:
238
+ return [];
239
+ }
240
+ }
241
+
242
+ function getImageUrl(format: AbstractImage.BinaryFormat, data: AbstractImage.ImageData): string {
243
+ if (data.type === "url") {
244
+ return data.url;
245
+ } else if (format === "png") {
246
+ const base64 = B64.fromByteArray(data.bytes);
247
+ return `data:image/png;base64,${base64}`;
248
+ } else {
249
+ const svg = String.fromCharCode(...data.bytes).replace('<?xml version="1.0" encoding="utf-8"?>', "");
250
+ const bytes = [];
251
+ for (let i = 0; i < svg.length; ++i) {
252
+ bytes.push(svg.charCodeAt(i));
253
+ }
254
+ const base64 = B64.fromByteArray(new Uint8Array(bytes));
255
+ return `data:image/svg+xml;base64,${base64}`;
256
+ }
257
+ }
258
+
259
+ function renderLine(text: string, x: number, y: number, fontSize: number, lineHeight: number): JSX.Element {
260
+ const split = R.unnest<string>(text.split("<sub>").map((t) => t.split("</sub>")));
261
+ let inside = false;
262
+ const tags: Array<JSX.Element> = [];
263
+ for (let i = 0; i < split.length; ++i) {
264
+ const splitText = split[i];
265
+ if (inside) {
266
+ tags.push(
267
+ <tspan key={i} baselineShift="sub" style={{ fontSize: (fontSize * 0.8).toString() + "px" }}>
268
+ {splitText}
269
+ </tspan>
270
+ );
271
+ } else {
272
+ tags.push(<tspan key={i}>{splitText}</tspan>);
273
+ }
274
+ inside = !inside;
275
+ }
276
+ return (
277
+ <tspan key={text} x={x} y={y} height={lineHeight.toString() + "px"}>
278
+ {tags}
279
+ </tspan>
280
+ );
281
+ }
282
+
283
+ function getBaselineAdjustment(d: AbstractImage.GrowthDirection): number {
284
+ if (d === "up") {
285
+ return 0.0;
286
+ }
287
+ if (d === "uniform") {
288
+ return 0.5;
289
+ }
290
+ if (d === "down") {
291
+ return 1.0;
292
+ }
293
+ throw "Unknown text alignment " + d;
294
+ }
295
+
296
+ function getTextAnchor(d: AbstractImage.GrowthDirection): "end" | "middle" | "start" {
297
+ if (d === "left") {
298
+ return "end";
299
+ }
300
+ if (d === "uniform") {
301
+ return "middle";
302
+ }
303
+ if (d === "right") {
304
+ return "start";
305
+ }
306
+ throw "Unknown text alignment " + d;
307
+ }
308
+
309
+ function colorToRgb(color: AbstractImage.Color): string {
310
+ return "rgb(" + color.r.toString() + "," + color.g.toString() + "," + color.b.toString() + ")";
311
+ }
312
+
313
+ function colorToOpacity(color: AbstractImage.Color): string {
314
+ return (color.a / 255).toString();
315
+ }