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