js-draw 0.3.1 → 0.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 (84) hide show
  1. package/.github/ISSUE_TEMPLATE/translation.md +4 -1
  2. package/CHANGELOG.md +8 -0
  3. package/dist/bundle.js +1 -1
  4. package/dist/src/Editor.d.ts +4 -1
  5. package/dist/src/Editor.js +117 -2
  6. package/dist/src/EditorImage.js +4 -1
  7. package/dist/src/SVGLoader.d.ts +4 -1
  8. package/dist/src/SVGLoader.js +78 -33
  9. package/dist/src/UndoRedoHistory.d.ts +1 -0
  10. package/dist/src/UndoRedoHistory.js +6 -0
  11. package/dist/src/Viewport.d.ts +1 -0
  12. package/dist/src/Viewport.js +12 -4
  13. package/dist/src/commands/lib.d.ts +2 -1
  14. package/dist/src/commands/lib.js +2 -1
  15. package/dist/src/commands/localization.d.ts +1 -0
  16. package/dist/src/commands/localization.js +1 -0
  17. package/dist/src/commands/uniteCommands.d.ts +4 -0
  18. package/dist/src/commands/uniteCommands.js +105 -0
  19. package/dist/src/components/AbstractComponent.d.ts +2 -0
  20. package/dist/src/components/AbstractComponent.js +41 -5
  21. package/dist/src/components/ImageComponent.d.ts +27 -0
  22. package/dist/src/components/ImageComponent.js +129 -0
  23. package/dist/src/components/builders/FreehandLineBuilder.js +2 -2
  24. package/dist/src/components/lib.d.ts +4 -2
  25. package/dist/src/components/lib.js +4 -2
  26. package/dist/src/components/localization.d.ts +2 -0
  27. package/dist/src/components/localization.js +2 -0
  28. package/dist/src/math/LineSegment2.d.ts +2 -0
  29. package/dist/src/math/LineSegment2.js +3 -0
  30. package/dist/src/rendering/localization.d.ts +3 -0
  31. package/dist/src/rendering/localization.js +3 -0
  32. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +7 -0
  33. package/dist/src/rendering/renderers/CanvasRenderer.d.ts +2 -1
  34. package/dist/src/rendering/renderers/CanvasRenderer.js +7 -0
  35. package/dist/src/rendering/renderers/DummyRenderer.d.ts +3 -1
  36. package/dist/src/rendering/renderers/DummyRenderer.js +5 -0
  37. package/dist/src/rendering/renderers/SVGRenderer.d.ts +5 -2
  38. package/dist/src/rendering/renderers/SVGRenderer.js +45 -20
  39. package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +3 -1
  40. package/dist/src/rendering/renderers/TextOnlyRenderer.js +8 -1
  41. package/dist/src/tools/BaseTool.d.ts +3 -1
  42. package/dist/src/tools/BaseTool.js +6 -0
  43. package/dist/src/tools/PasteHandler.d.ts +16 -0
  44. package/dist/src/tools/PasteHandler.js +142 -0
  45. package/dist/src/tools/SelectionTool.d.ts +7 -1
  46. package/dist/src/tools/SelectionTool.js +63 -5
  47. package/dist/src/tools/ToolController.js +36 -27
  48. package/dist/src/tools/lib.d.ts +1 -0
  49. package/dist/src/tools/lib.js +1 -0
  50. package/dist/src/tools/localization.d.ts +3 -0
  51. package/dist/src/tools/localization.js +3 -0
  52. package/dist/src/types.d.ts +13 -2
  53. package/dist/src/types.js +2 -0
  54. package/package.json +1 -1
  55. package/src/Editor.ts +131 -2
  56. package/src/EditorImage.ts +7 -1
  57. package/src/SVGLoader.ts +90 -36
  58. package/src/UndoRedoHistory.test.ts +33 -0
  59. package/src/UndoRedoHistory.ts +8 -0
  60. package/src/Viewport.ts +13 -4
  61. package/src/commands/lib.ts +2 -0
  62. package/src/commands/localization.ts +2 -0
  63. package/src/commands/uniteCommands.test.ts +23 -0
  64. package/src/commands/uniteCommands.ts +121 -0
  65. package/src/components/AbstractComponent.ts +55 -9
  66. package/src/components/ImageComponent.ts +153 -0
  67. package/src/components/builders/FreehandLineBuilder.ts +2 -2
  68. package/src/components/lib.ts +7 -2
  69. package/src/components/localization.ts +4 -0
  70. package/src/math/LineSegment2.test.ts +9 -0
  71. package/src/math/LineSegment2.ts +5 -0
  72. package/src/rendering/localization.ts +6 -0
  73. package/src/rendering/renderers/AbstractRenderer.ts +16 -0
  74. package/src/rendering/renderers/CanvasRenderer.ts +10 -1
  75. package/src/rendering/renderers/DummyRenderer.ts +6 -1
  76. package/src/rendering/renderers/SVGRenderer.ts +50 -21
  77. package/src/rendering/renderers/TextOnlyRenderer.ts +10 -2
  78. package/src/tools/BaseTool.ts +9 -1
  79. package/src/tools/PasteHandler.ts +156 -0
  80. package/src/tools/SelectionTool.ts +80 -8
  81. package/src/tools/ToolController.ts +51 -44
  82. package/src/tools/lib.ts +1 -0
  83. package/src/tools/localization.ts +8 -0
  84. package/src/types.ts +16 -2
@@ -0,0 +1,153 @@
1
+ import LineSegment2 from '../math/LineSegment2';
2
+ import Mat33 from '../math/Mat33';
3
+ import Rect2 from '../math/Rect2';
4
+ import AbstractRenderer, { RenderableImage } from '../rendering/renderers/AbstractRenderer';
5
+ import AbstractComponent from './AbstractComponent';
6
+ import { ImageComponentLocalization } from './localization';
7
+
8
+ // Represents a raster image.
9
+ export default class ImageComponent extends AbstractComponent {
10
+ protected contentBBox: Rect2;
11
+ private image: RenderableImage;
12
+
13
+ public constructor(image: RenderableImage) {
14
+ super('image-component');
15
+ this.image = {
16
+ ...image,
17
+ label: image.label ?? image.image.getAttribute('alt') ?? image.image.getAttribute('aria-label') ?? undefined,
18
+ };
19
+
20
+ const isHTMLImageElem = (elem: HTMLCanvasElement|HTMLImageElement): elem is HTMLImageElement => {
21
+ return elem.getAttribute('src') !== undefined;
22
+ };
23
+ if (isHTMLImageElem(image.image) && !image.image.complete) {
24
+ image.image.onload = () => this.recomputeBBox();
25
+ }
26
+
27
+ this.recomputeBBox();
28
+ }
29
+
30
+ private getImageRect() {
31
+ return new Rect2(0, 0, this.image.image.width, this.image.image.height);
32
+ }
33
+
34
+ private recomputeBBox() {
35
+ this.contentBBox = this.getImageRect();
36
+ this.contentBBox = this.contentBBox.transformedBoundingBox(this.image.transform);
37
+ }
38
+
39
+ // Load from an image. Waits for the image to load if incomplete.
40
+ public static async fromImage(elem: HTMLImageElement, transform: Mat33) {
41
+ if (!elem.complete) {
42
+ await new Promise((resolve, reject) => {
43
+ elem.onload = resolve;
44
+ elem.onerror = reject;
45
+ elem.onabort = reject;
46
+ });
47
+ }
48
+
49
+ let width, height;
50
+ if (
51
+ typeof elem.width === 'number' && typeof elem.height === 'number'
52
+ && elem.width !== 0 && elem.height !== 0
53
+ ) {
54
+ width = elem.width as number;
55
+ height = elem.height as number;
56
+ } else {
57
+ width = elem.clientWidth;
58
+ height = elem.clientHeight;
59
+ }
60
+
61
+ let image;
62
+ let url = elem.src ?? '';
63
+ if (!url.startsWith('data:image/')) {
64
+ // Convert to a data URL:
65
+ const canvas = document.createElement('canvas');
66
+ canvas.width = width;
67
+ canvas.height = height;
68
+
69
+ const ctx = canvas.getContext('2d')!;
70
+ ctx.drawImage(elem, 0, 0, canvas.width, canvas.height);
71
+ url = canvas.toDataURL();
72
+ image = canvas;
73
+ } else {
74
+ image = new Image();
75
+ image.src = url;
76
+ image.width = width;
77
+ image.height = height;
78
+ }
79
+
80
+ return new ImageComponent({
81
+ image,
82
+ base64Url: url,
83
+ transform: transform,
84
+ });
85
+ }
86
+
87
+ public render(canvas: AbstractRenderer, _visibleRect?: Rect2): void {
88
+ canvas.drawImage(this.image);
89
+ }
90
+
91
+ public intersects(lineSegment: LineSegment2): boolean {
92
+ const rect = this.getImageRect();
93
+ const edges = rect.getEdges().map(edge => edge.transformedBy(this.image.transform));
94
+ for (const edge of edges) {
95
+ if (edge.intersects(lineSegment)) {
96
+ return true;
97
+ }
98
+ }
99
+ return false;
100
+ }
101
+
102
+ protected serializeToJSON() {
103
+ return {
104
+ src: this.image.base64Url,
105
+ label: this.image.label,
106
+
107
+ // Store the width and height for bounding box computations while the image is loading.
108
+ width: this.image.image.width,
109
+ height: this.image.image.height,
110
+
111
+ transform: this.image.transform.toArray(),
112
+ };
113
+ }
114
+
115
+ protected applyTransformation(affineTransfm: Mat33) {
116
+ this.image.transform = affineTransfm.rightMul(this.image.transform);
117
+ this.recomputeBBox();
118
+ }
119
+
120
+ public description(localizationTable: ImageComponentLocalization): string {
121
+ return this.image.label ? localizationTable.imageNode(this.image.label) : localizationTable.unlabeledImageNode;
122
+ }
123
+
124
+ protected createClone(): AbstractComponent {
125
+ return new ImageComponent({
126
+ ...this.image,
127
+ });
128
+ }
129
+
130
+ public static deserializeFromJSON(data: any): ImageComponent {
131
+ if (!(typeof data.src === 'string')) {
132
+ throw new Error(`${data} has invalid format! Expected src property.`);
133
+ }
134
+
135
+ const image = new Image();
136
+ image.src = data.src;
137
+ image.width = data.width;
138
+ image.height = data.height;
139
+
140
+ return new ImageComponent({
141
+ image: image,
142
+ base64Url: image.src,
143
+ label: data.label,
144
+ transform: new Mat33(...(data.transform as [
145
+ number, number, number,
146
+ number, number, number,
147
+ number, number, number,
148
+ ])),
149
+ });
150
+ }
151
+ }
152
+
153
+ AbstractComponent.registerComponent('image-component', ImageComponent.deserializeFromJSON);
@@ -329,9 +329,9 @@ export default class FreehandLineBuilder implements ComponentBuilder {
329
329
  return upperBoundary.intersects(lowerBoundary).length > 0;
330
330
  };
331
331
 
332
- // If the boundaries have two intersections, increasing the half vector's length could fix this.
332
+ // If the boundaries have intersections, increasing the half vector's length could fix this.
333
333
  if (boundariesIntersect()) {
334
- halfVec = halfVec.times(2);
334
+ halfVec = halfVec.times(1.1);
335
335
  }
336
336
 
337
337
  // Each starts at startPt ± startVec
@@ -1,12 +1,17 @@
1
1
  export * from './builders/types';
2
2
  export { makeFreehandLineBuilder } from './builders/FreehandLineBuilder';
3
3
 
4
- import AbstractComponent from './AbstractComponent';
4
+ export * from './AbstractComponent';
5
+ export { default as AbstractComponent } from './AbstractComponent';
5
6
  import Stroke from './Stroke';
6
7
  import Text from './Text';
8
+ import ImageComponent from './ImageComponent';
7
9
 
8
10
  export {
9
- AbstractComponent,
10
11
  Stroke,
11
12
  Text,
13
+
14
+ Text as TextComponent,
15
+ Stroke as StrokeComponent,
16
+ ImageComponent,
12
17
  };
@@ -1,11 +1,15 @@
1
1
  export interface ImageComponentLocalization {
2
+ unlabeledImageNode: string;
2
3
  text: (text: string)=> string;
4
+ imageNode: (description: string)=> string;
3
5
  stroke: string;
4
6
  svgObject: string;
5
7
  }
6
8
 
7
9
  export const defaultComponentLocalization: ImageComponentLocalization = {
10
+ unlabeledImageNode: 'Unlabeled image node',
8
11
  stroke: 'Stroke',
9
12
  svgObject: 'SVG Object',
10
13
  text: (text) => `Text object: ${text}`,
14
+ imageNode: (description: string) => `Image: ${description}`,
11
15
  };
@@ -1,6 +1,7 @@
1
1
  import LineSegment2 from './LineSegment2';
2
2
  import { loadExpectExtensions } from '../testing/loadExpectExtensions';
3
3
  import { Vec2 } from './Vec2';
4
+ import Mat33 from './Mat33';
4
5
 
5
6
  loadExpectExtensions();
6
7
 
@@ -89,4 +90,12 @@ describe('Line2', () => {
89
90
  const line = new LineSegment2(Vec2.of(1, 1), Vec2.of(2, 4));
90
91
  expect(line.closestPointTo(Vec2.of(5, 2))).objEq(Vec2.of(2, 4));
91
92
  });
93
+
94
+ it('Should translate when translated by a translation matrix', () => {
95
+ const line = new LineSegment2(Vec2.of(-1, 1), Vec2.of(2, 100));
96
+ expect(line.transformedBy(Mat33.translation(Vec2.of(1, -2)))).toMatchObject({
97
+ p1: Vec2.of(0, -1),
98
+ p2: Vec2.of(3, 98),
99
+ });
100
+ });
92
101
  });
@@ -1,3 +1,4 @@
1
+ import Mat33 from './Mat33';
1
2
  import Rect2 from './Rect2';
2
3
  import { Vec2, Point2 } from './Vec2';
3
4
 
@@ -149,6 +150,10 @@ export default class LineSegment2 {
149
150
  }
150
151
  }
151
152
 
153
+ public transformedBy(affineTransfm: Mat33): LineSegment2 {
154
+ return new LineSegment2(affineTransfm.transformVec2(this.p1), affineTransfm.transformVec2(this.p2));
155
+ }
156
+
152
157
  public toString() {
153
158
  return `LineSegment(${this.p1.toString()}, ${this.p2.toString()})`;
154
159
  }
@@ -2,13 +2,19 @@
2
2
  export interface TextRendererLocalization {
3
3
  pathNodeCount(pathCount: number): string;
4
4
  textNodeCount(nodeCount: number): string;
5
+ imageNodeCount(nodeCount: number): string;
5
6
  textNode(content: string): string;
7
+ unlabeledImageNode: string;
8
+ imageNode(label: string): string;
6
9
  rerenderAsText: string;
7
10
  }
8
11
 
9
12
  export const defaultTextRendererLocalization: TextRendererLocalization = {
10
13
  pathNodeCount: (count: number) => `There are ${count} visible path objects.`,
11
14
  textNodeCount: (count: number) => `There are ${count} visible text nodes.`,
15
+ imageNodeCount: (nodeCount: number) => `There are ${nodeCount} visible image nodes.`,
12
16
  textNode: (content: string) => `Text: ${content}`,
17
+ imageNode: (label: string) => `Image: ${label}`,
18
+ unlabeledImageNode: 'Unlabeled image',
13
19
  rerenderAsText: 'Re-render as text',
14
20
  };
@@ -14,6 +14,21 @@ export interface RenderablePathSpec {
14
14
  path?: Path;
15
15
  }
16
16
 
17
+ export interface RenderableImage {
18
+ transform: Mat33;
19
+
20
+ // An Image or HTMLCanvasElement. If an Image, it must be loaded from the same origin as this
21
+ // (and should have `src=this.base64Url`).
22
+ image: HTMLImageElement|HTMLCanvasElement;
23
+
24
+ // All images that can be drawn **must** have a base64 URL in the form
25
+ // data:image/[format];base64,[data here]
26
+ // If `image` is an Image, this should be equivalent to `image.src`.
27
+ base64Url: string;
28
+
29
+ label?: string;
30
+ }
31
+
17
32
  export default abstract class AbstractRenderer {
18
33
  // If null, this' transformation is linked to the Viewport
19
34
  private selfTransform: Mat33|null = null;
@@ -41,6 +56,7 @@ export default abstract class AbstractRenderer {
41
56
  controlPoint: Point2, endPoint: Point2,
42
57
  ): void;
43
58
  public abstract drawText(text: string, transform: Mat33, style: TextStyle): void;
59
+ public abstract drawImage(image: RenderableImage): void;
44
60
 
45
61
  // Returns true iff the given rectangle is so small, rendering anything within
46
62
  // it has no effect on the image.
@@ -6,7 +6,7 @@ import { Point2, Vec2 } from '../../math/Vec2';
6
6
  import Vec3 from '../../math/Vec3';
7
7
  import Viewport from '../../Viewport';
8
8
  import RenderingStyle from '../RenderingStyle';
9
- import AbstractRenderer, { RenderablePathSpec } from './AbstractRenderer';
9
+ import AbstractRenderer, { RenderableImage, RenderablePathSpec } from './AbstractRenderer';
10
10
 
11
11
  export default class CanvasRenderer extends AbstractRenderer {
12
12
  private ignoreObjectsAboveLevel: number|null = null;
@@ -168,6 +168,15 @@ export default class CanvasRenderer extends AbstractRenderer {
168
168
  this.ctx.restore();
169
169
  }
170
170
 
171
+ public drawImage(image: RenderableImage) {
172
+ this.ctx.save();
173
+ const transform = this.getCanvasToScreenTransform().rightMul(image.transform);
174
+ this.transformBy(transform);
175
+
176
+ this.ctx.drawImage(image.image, 0, 0);
177
+ this.ctx.restore();
178
+ }
179
+
171
180
  private clipLevels: number[] = [];
172
181
  public startObject(boundingBox: Rect2, clip: boolean) {
173
182
  if (this.isTooSmallToRender(boundingBox)) {
@@ -7,7 +7,7 @@ import { Point2, Vec2 } from '../../math/Vec2';
7
7
  import Vec3 from '../../math/Vec3';
8
8
  import Viewport from '../../Viewport';
9
9
  import RenderingStyle from '../RenderingStyle';
10
- import AbstractRenderer from './AbstractRenderer';
10
+ import AbstractRenderer, { RenderableImage } from './AbstractRenderer';
11
11
 
12
12
  export default class DummyRenderer extends AbstractRenderer {
13
13
  // Variables that track the state of what's been rendered
@@ -17,6 +17,7 @@ export default class DummyRenderer extends AbstractRenderer {
17
17
  public lastPoint: Point2|null = null;
18
18
  public objectNestingLevel: number = 0;
19
19
  public lastText: string|null = null;
20
+ public lastImage: RenderableImage|null = null;
20
21
 
21
22
  // List of points drawn since the last clear.
22
23
  public pointBuffer: Point2[] = [];
@@ -44,6 +45,7 @@ export default class DummyRenderer extends AbstractRenderer {
44
45
  this.renderedPathCount = 0;
45
46
  this.pointBuffer = [];
46
47
  this.lastText = null;
48
+ this.lastImage = null;
47
49
 
48
50
  // Ensure all objects finished rendering
49
51
  if (this.objectNestingLevel > 0) {
@@ -96,6 +98,9 @@ export default class DummyRenderer extends AbstractRenderer {
96
98
  public drawText(text: string, _transform: Mat33, _style: TextStyle): void {
97
99
  this.lastText = text;
98
100
  }
101
+ public drawImage(image: RenderableImage) {
102
+ this.lastImage = image;
103
+ }
99
104
 
100
105
  public startObject(boundingBox: Rect2, _clip: boolean) {
101
106
  super.startObject(boundingBox);
@@ -9,7 +9,7 @@ import { Point2, Vec2 } from '../../math/Vec2';
9
9
  import { svgAttributesDataKey, SVGLoaderUnknownAttribute, SVGLoaderUnknownStyleAttribute, svgStyleAttributesDataKey } from '../../SVGLoader';
10
10
  import Viewport from '../../Viewport';
11
11
  import RenderingStyle from '../RenderingStyle';
12
- import AbstractRenderer, { RenderablePathSpec } from './AbstractRenderer';
12
+ import AbstractRenderer, { RenderableImage, RenderablePathSpec } from './AbstractRenderer';
13
13
 
14
14
  const svgNameSpace = 'http://www.w3.org/2000/svg';
15
15
  export default class SVGRenderer extends AbstractRenderer {
@@ -19,13 +19,18 @@ export default class SVGRenderer extends AbstractRenderer {
19
19
 
20
20
  private overwrittenAttrs: Record<string, string|null> = {};
21
21
 
22
- public constructor(private elem: SVGSVGElement, viewport: Viewport) {
22
+ // Renders onto `elem`. If `sanitize`, don't render potentially untrusted data.
23
+ public constructor(private elem: SVGSVGElement, viewport: Viewport, private sanitize: boolean = false) {
23
24
  super(viewport);
24
25
  this.clear();
25
26
  }
26
27
 
27
28
  // Sets an attribute on the root SVG element.
28
29
  public setRootSVGAttribute(name: string, value: string|null) {
30
+ if (this.sanitize) {
31
+ return;
32
+ }
33
+
29
34
  // Make the original value of the attribute restorable on clear
30
35
  if (!(name in this.overwrittenAttrs)) {
31
36
  this.overwrittenAttrs[name] = this.elem.getAttribute(name);
@@ -43,18 +48,21 @@ export default class SVGRenderer extends AbstractRenderer {
43
48
  }
44
49
 
45
50
  public clear() {
46
- // Restore all alltributes
47
- for (const attrName in this.overwrittenAttrs) {
48
- const value = this.overwrittenAttrs[attrName];
49
-
50
- if (value) {
51
- this.elem.setAttribute(attrName, value);
52
- } else {
53
- this.elem.removeAttribute(attrName);
51
+ this.lastPathString = [];
52
+
53
+ if (!this.sanitize) {
54
+ // Restore all all attributes
55
+ for (const attrName in this.overwrittenAttrs) {
56
+ const value = this.overwrittenAttrs[attrName];
57
+
58
+ if (value) {
59
+ this.elem.setAttribute(attrName, value);
60
+ } else {
61
+ this.elem.removeAttribute(attrName);
62
+ }
54
63
  }
64
+ this.overwrittenAttrs = {};
55
65
  }
56
- this.overwrittenAttrs = {};
57
- this.lastPathString = [];
58
66
  }
59
67
 
60
68
  // Push [this.fullPath] to the SVG
@@ -91,26 +99,31 @@ export default class SVGRenderer extends AbstractRenderer {
91
99
  this.lastPathString.push(path.toString());
92
100
  }
93
101
 
94
- public drawText(text: string, transform: Mat33, style: TextStyle): void {
95
- transform = this.getCanvasToScreenTransform().rightMul(transform);
96
-
102
+ // Apply [elemTransform] to [elem].
103
+ private transformFrom(elemTransform: Mat33, elem: SVGElement) {
104
+ let transform = this.getCanvasToScreenTransform().rightMul(elemTransform);
97
105
  const translation = transform.transformVec2(Vec2.zero);
98
106
  transform = transform.rightMul(Mat33.translation(translation.times(-1)));
99
107
 
100
- const textElem = document.createElementNS(svgNameSpace, 'text');
101
- textElem.appendChild(document.createTextNode(text));
102
- textElem.style.transform = `matrix(
108
+ elem.style.transform = `matrix(
103
109
  ${transform.a1}, ${transform.b1},
104
110
  ${transform.a2}, ${transform.b2},
105
111
  ${transform.a3}, ${transform.b3}
106
112
  )`;
113
+ elem.setAttribute('x', `${toRoundedString(translation.x)}`);
114
+ elem.setAttribute('y', `${toRoundedString(translation.y)}`);
115
+ }
116
+
117
+ public drawText(text: string, transform: Mat33, style: TextStyle): void {
118
+ const textElem = document.createElementNS(svgNameSpace, 'text');
119
+ textElem.appendChild(document.createTextNode(text));
120
+ this.transformFrom(transform, textElem);
121
+
107
122
  textElem.style.fontFamily = style.fontFamily;
108
123
  textElem.style.fontVariant = style.fontVariant ?? '';
109
124
  textElem.style.fontWeight = style.fontWeight ?? '';
110
125
  textElem.style.fontSize = style.size + 'px';
111
126
  textElem.style.fill = style.renderingStyle.fill.toHexString();
112
- textElem.setAttribute('x', `${toRoundedString(translation.x)}`);
113
- textElem.setAttribute('y', `${toRoundedString(translation.y)}`);
114
127
 
115
128
  if (style.renderingStyle.stroke) {
116
129
  const strokeStyle = style.renderingStyle.stroke;
@@ -122,6 +135,18 @@ export default class SVGRenderer extends AbstractRenderer {
122
135
  this.objectElems?.push(textElem);
123
136
  }
124
137
 
138
+ public drawImage(image: RenderableImage) {
139
+ const svgImgElem = document.createElementNS(svgNameSpace, 'image');
140
+ svgImgElem.setAttribute('href', image.base64Url);
141
+ svgImgElem.setAttribute('width', image.image.getAttribute('width') ?? '');
142
+ svgImgElem.setAttribute('height', image.image.getAttribute('height') ?? '');
143
+ svgImgElem.setAttribute('aria-label', image.image.getAttribute('aria-label') ?? image.image.getAttribute('alt') ?? '');
144
+ this.transformFrom(image.transform, svgImgElem);
145
+
146
+ this.elem.appendChild(svgImgElem);
147
+ this.objectElems?.push(svgImgElem);
148
+ }
149
+
125
150
  public startObject(boundingBox: Rect2) {
126
151
  super.startObject(boundingBox);
127
152
 
@@ -137,7 +162,7 @@ export default class SVGRenderer extends AbstractRenderer {
137
162
  // Don't extend paths across objects
138
163
  this.addPathToSVG();
139
164
 
140
- if (loaderData) {
165
+ if (loaderData && !this.sanitize) {
141
166
  // Restore any attributes unsupported by the app.
142
167
  for (const elem of this.objectElems ?? []) {
143
168
  const attrs = loaderData[svgAttributesDataKey] as SVGLoaderUnknownAttribute[]|undefined;
@@ -181,6 +206,10 @@ export default class SVGRenderer extends AbstractRenderer {
181
206
 
182
207
  // Renders a **copy** of the given element.
183
208
  public drawSVGElem(elem: SVGElement) {
209
+ if (this.sanitize) {
210
+ return;
211
+ }
212
+
184
213
  this.elem.appendChild(elem.cloneNode(true));
185
214
  }
186
215
 
@@ -6,7 +6,7 @@ import Vec3 from '../../math/Vec3';
6
6
  import Viewport from '../../Viewport';
7
7
  import { TextRendererLocalization } from '../localization';
8
8
  import RenderingStyle from '../RenderingStyle';
9
- import AbstractRenderer from './AbstractRenderer';
9
+ import AbstractRenderer, { RenderableImage } from './AbstractRenderer';
10
10
 
11
11
  // Outputs a description of what was rendered.
12
12
 
@@ -14,6 +14,7 @@ export default class TextOnlyRenderer extends AbstractRenderer {
14
14
  private descriptionBuilder: string[] = [];
15
15
  private pathCount: number = 0;
16
16
  private textNodeCount: number = 0;
17
+ private imageNodeCount: number = 0;
17
18
 
18
19
  public constructor(viewport: Viewport, private localizationTable: TextRendererLocalization) {
19
20
  super(viewport);
@@ -33,7 +34,8 @@ export default class TextOnlyRenderer extends AbstractRenderer {
33
34
  public getDescription(): string {
34
35
  return [
35
36
  this.localizationTable.pathNodeCount(this.pathCount),
36
- this.localizationTable.textNodeCount(this.textNodeCount),
37
+ ...(this.textNodeCount > 0 ? this.localizationTable.textNodeCount(this.textNodeCount) : []),
38
+ ...(this.imageNodeCount > 0 ? this.localizationTable.imageNodeCount(this.imageNodeCount) : []),
37
39
  ...this.descriptionBuilder
38
40
  ].join('\n');
39
41
  }
@@ -55,6 +57,12 @@ export default class TextOnlyRenderer extends AbstractRenderer {
55
57
  this.descriptionBuilder.push(this.localizationTable.textNode(text));
56
58
  this.textNodeCount ++;
57
59
  }
60
+ public drawImage(image: RenderableImage) {
61
+ const label = image.label ? this.localizationTable.imageNode(image.label) : this.localizationTable.unlabeledImageNode;
62
+
63
+ this.descriptionBuilder.push(label);
64
+ this.imageNodeCount ++;
65
+ }
58
66
  public isTooSmallToRender(rect: Rect2): boolean {
59
67
  return rect.maxDimension < 15 / this.getSizeOfCanvasPixelOnScreen();
60
68
  }
@@ -1,4 +1,4 @@
1
- import { PointerEvtListener, WheelEvt, PointerEvt, EditorNotifier, EditorEventType, KeyPressEvent, KeyUpEvent } from '../types';
1
+ import { PointerEvtListener, WheelEvt, PointerEvt, EditorNotifier, EditorEventType, KeyPressEvent, KeyUpEvent, PasteEvent, CopyEvent } from '../types';
2
2
  import ToolEnabledGroup from './ToolEnabledGroup';
3
3
 
4
4
  export default abstract class BaseTool implements PointerEvtListener {
@@ -17,6 +17,14 @@ export default abstract class BaseTool implements PointerEvtListener {
17
17
  return false;
18
18
  }
19
19
 
20
+ public onCopy(_event: CopyEvent): boolean {
21
+ return false;
22
+ }
23
+
24
+ public onPaste(_event: PasteEvent): boolean {
25
+ return false;
26
+ }
27
+
20
28
  public onKeyPress(_event: KeyPressEvent): boolean {
21
29
  return false;
22
30
  }