@annotorious/annotorious 3.2.0 → 3.2.1
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/dist/annotorious.es.js +3158 -2561
- package/dist/annotorious.es.js.map +1 -1
- package/dist/annotorious.js +2 -1
- package/dist/annotorious.js.map +1 -1
- package/dist/model/w3c/svg/SVG.d.ts +1 -0
- package/dist/model/w3c/svg/pathParser.d.ts +2 -0
- package/package.json +3 -2
- package/src/model/w3c/svg/SVG.ts +16 -0
- package/src/model/w3c/svg/SVGSelector.ts +57 -21
- package/src/model/w3c/svg/pathParser.ts +73 -0
|
@@ -2,3 +2,4 @@ export declare const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
|
|
2
2
|
export declare const sanitize: (doc: Element | Document) => Element | Document;
|
|
3
3
|
/** Helper that forces an un-namespaced node to SVG **/
|
|
4
4
|
export declare const insertSVGNamespace: (originalDoc: Document) => Element;
|
|
5
|
+
export declare const parseSVGXML: (value: string) => Element;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@annotorious/annotorious",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.1",
|
|
4
4
|
"description": "Add image annotation functionality to any web page with a few lines of JavaScript",
|
|
5
5
|
"author": "Rainer Simon",
|
|
6
6
|
"license": "BSD-3-Clause",
|
|
@@ -49,8 +49,9 @@
|
|
|
49
49
|
"vitest": "^3.0.8"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@annotorious/core": "3.2.
|
|
52
|
+
"@annotorious/core": "3.2.1",
|
|
53
53
|
"rbush": "^4.0.1",
|
|
54
|
+
"svg-pathdata": "^7.1.0",
|
|
54
55
|
"uuid": "^11.1.0"
|
|
55
56
|
}
|
|
56
57
|
}
|
package/src/model/w3c/svg/SVG.ts
CHANGED
|
@@ -33,3 +33,19 @@ export const insertSVGNamespace = (originalDoc: Document): Element => {
|
|
|
33
33
|
const namespacedDoc = parser.parseFromString(namespaced, "image/svg+xml");
|
|
34
34
|
return namespacedDoc.documentElement;
|
|
35
35
|
}
|
|
36
|
+
|
|
37
|
+
export const parseSVGXML = (value: string): Element => {
|
|
38
|
+
const parser = new DOMParser();
|
|
39
|
+
|
|
40
|
+
const doc = parser.parseFromString(value, 'image/svg+xml');
|
|
41
|
+
|
|
42
|
+
// SVG needs a namespace declaration - check if it's set or insert if not
|
|
43
|
+
const isPrefixDeclared = doc.lookupPrefix(SVG_NAMESPACE); // SVG declared via prefix
|
|
44
|
+
const isDefaultNamespaceSVG = doc.lookupNamespaceURI(null); // SVG declared as default namespace
|
|
45
|
+
|
|
46
|
+
if (isPrefixDeclared || isDefaultNamespaceSVG) {
|
|
47
|
+
return sanitize(doc).firstChild as Element;
|
|
48
|
+
} else {
|
|
49
|
+
return sanitize(insertSVGNamespace(doc)).firstChild as Element;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { boundsFromPoints, multipolygonElementToPath, ShapeType } from '../../core';
|
|
2
|
+
import { parseSVGXML } from './SVG';
|
|
3
|
+
import { svgPathToMultiPolygonElement } from './pathParser';
|
|
4
|
+
import type {
|
|
5
|
+
Ellipse,
|
|
6
|
+
EllipseGeometry,
|
|
7
|
+
MultiPolygon,
|
|
8
|
+
MultiPolygonGeometry,
|
|
9
|
+
Polygon,
|
|
10
|
+
PolygonGeometry,
|
|
11
|
+
Shape
|
|
12
|
+
} from '../../core';
|
|
4
13
|
|
|
5
14
|
export interface SVGSelector {
|
|
6
15
|
|
|
@@ -10,24 +19,8 @@ export interface SVGSelector {
|
|
|
10
19
|
|
|
11
20
|
}
|
|
12
21
|
|
|
13
|
-
const parseSVGXML = (value: string): Element => {
|
|
14
|
-
const parser = new DOMParser();
|
|
15
|
-
|
|
16
|
-
const doc = parser.parseFromString(value, "image/svg+xml");
|
|
17
|
-
|
|
18
|
-
// SVG needs a namespace declaration - check if it's set or insert if not
|
|
19
|
-
const isPrefixDeclared = doc.lookupPrefix(SVG_NAMESPACE); // SVG declared via prefix
|
|
20
|
-
const isDefaultNamespaceSVG = doc.lookupNamespaceURI(null); // SVG declared as default namespace
|
|
21
|
-
|
|
22
|
-
if (isPrefixDeclared || isDefaultNamespaceSVG) {
|
|
23
|
-
return sanitize(doc).firstChild as Element;
|
|
24
|
-
} else {
|
|
25
|
-
return sanitize(insertSVGNamespace(doc)).firstChild as Element;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
22
|
const parseSVGPolygon = (value: string): Polygon => {
|
|
30
|
-
const [
|
|
23
|
+
const [_, __, str] = value.match(/(<polygon points=["|'])([^("|')]*)/) || [];
|
|
31
24
|
const points = str.split(' ').map((p) => p.split(',').map(parseFloat));
|
|
32
25
|
|
|
33
26
|
return {
|
|
@@ -66,17 +59,57 @@ const parseSVGEllipse = (value: string): Ellipse => {
|
|
|
66
59
|
};
|
|
67
60
|
}
|
|
68
61
|
|
|
62
|
+
const parseSVGPath = (value: string): Polygon | MultiPolygon => {
|
|
63
|
+
const doc = parseSVGXML(value);
|
|
64
|
+
|
|
65
|
+
const paths = doc.nodeName === 'path' ? [doc] : Array.from(doc.querySelectorAll('path'));
|
|
66
|
+
const d = paths.map(path => path.getAttribute('d') || '');
|
|
67
|
+
|
|
68
|
+
const polygons = d.map(d => svgPathToMultiPolygonElement(d)!).filter(Boolean);
|
|
69
|
+
|
|
70
|
+
const outerPoints = polygons.reduce<[number, number][]>((points, element) => {
|
|
71
|
+
return [...points, ...element.rings[0].points]
|
|
72
|
+
}, []);
|
|
73
|
+
|
|
74
|
+
const bounds = boundsFromPoints(outerPoints);
|
|
75
|
+
|
|
76
|
+
// No need to create a MultiPolygon if theres only a single element with an outer ring
|
|
77
|
+
const isSinglePolygon = polygons.length === 1 && polygons[0].rings.length === 1;
|
|
78
|
+
return isSinglePolygon ? {
|
|
79
|
+
type: ShapeType.POLYGON,
|
|
80
|
+
geometry: {
|
|
81
|
+
points: outerPoints,
|
|
82
|
+
bounds
|
|
83
|
+
}
|
|
84
|
+
} : {
|
|
85
|
+
type: ShapeType.MULTIPOLYGLON,
|
|
86
|
+
geometry: {
|
|
87
|
+
polygons,
|
|
88
|
+
bounds
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
69
93
|
export const parseSVGSelector = <T extends Shape>(valueOrSelector: SVGSelector | string): T => {
|
|
70
94
|
const value = typeof valueOrSelector === 'string' ? valueOrSelector : valueOrSelector.value;
|
|
71
95
|
|
|
72
96
|
if (value.includes('<polygon points='))
|
|
73
97
|
return parseSVGPolygon(value) as unknown as T;
|
|
98
|
+
else if (value.includes('<path '))
|
|
99
|
+
return parseSVGPath(value) as unknown as T;
|
|
74
100
|
else if (value.includes('<ellipse '))
|
|
75
101
|
return parseSVGEllipse(value) as unknown as T;
|
|
76
102
|
else
|
|
77
103
|
throw 'Unsupported SVG shape: ' + value;
|
|
78
104
|
}
|
|
79
105
|
|
|
106
|
+
const serializeMultiPolygon = (geom: MultiPolygonGeometry) => {
|
|
107
|
+
const paths = geom.polygons.map(elem =>
|
|
108
|
+
`<path fill-rule="evenodd" d="${multipolygonElementToPath(elem)}" />`);
|
|
109
|
+
|
|
110
|
+
return `<g>${paths.join('')}</g>`
|
|
111
|
+
}
|
|
112
|
+
|
|
80
113
|
export const serializeSVGSelector = (shape: Shape): SVGSelector => {
|
|
81
114
|
let value: string | undefined;
|
|
82
115
|
|
|
@@ -86,7 +119,10 @@ export const serializeSVGSelector = (shape: Shape): SVGSelector => {
|
|
|
86
119
|
value = `<svg><polygon points="${points.map((xy) => xy.join(',')).join(' ')}" /></svg>`;
|
|
87
120
|
} else if (shape.type === ShapeType.ELLIPSE) {
|
|
88
121
|
const geom = shape.geometry as EllipseGeometry;
|
|
89
|
-
value = `<svg><ellipse cx="${geom.cx}" cy="${geom.cy}" rx="${geom.rx}" ry="${geom.ry}" /></svg
|
|
122
|
+
value = `<svg><ellipse cx="${geom.cx}" cy="${geom.cy}" rx="${geom.rx}" ry="${geom.ry}" /></svg>`;
|
|
123
|
+
} else if (shape.type === ShapeType.MULTIPOLYGLON) {
|
|
124
|
+
const geom = shape.geometry as MultiPolygonGeometry;
|
|
125
|
+
value = `<svg>${serializeMultiPolygon(geom)}</svg>`;
|
|
90
126
|
}
|
|
91
127
|
|
|
92
128
|
if (value) {
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { SVGPathData } from 'svg-pathdata';
|
|
2
|
+
import { boundsFromPoints, type MultiPolygonElement, type MultiPolygonRing } from '../../core';
|
|
3
|
+
|
|
4
|
+
export const svgPathToMultiPolygonElement = (d: string): MultiPolygonElement | undefined => {
|
|
5
|
+
const commands = new SVGPathData(d).toAbs().commands;
|
|
6
|
+
|
|
7
|
+
const rings: MultiPolygonRing[] = [];
|
|
8
|
+
|
|
9
|
+
let currentRing: [number, number][] = [];
|
|
10
|
+
let startPoint: [number, number] | null = null;
|
|
11
|
+
|
|
12
|
+
for (const cmd of commands) {
|
|
13
|
+
switch (cmd.type) {
|
|
14
|
+
case SVGPathData.MOVE_TO:
|
|
15
|
+
// Start a new ring
|
|
16
|
+
if (currentRing.length > 0) {
|
|
17
|
+
rings.push({ points: currentRing });
|
|
18
|
+
currentRing = [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
startPoint = [cmd.x, cmd.y];
|
|
22
|
+
currentRing.push(startPoint);
|
|
23
|
+
break;
|
|
24
|
+
|
|
25
|
+
case SVGPathData.LINE_TO:
|
|
26
|
+
currentRing.push([cmd.x, cmd.y]);
|
|
27
|
+
break;
|
|
28
|
+
|
|
29
|
+
case SVGPathData.HORIZ_LINE_TO:
|
|
30
|
+
// Get the last Y coordinate since H command only specifies X
|
|
31
|
+
const lastY = currentRing[currentRing.length - 1][1];
|
|
32
|
+
currentRing.push([cmd.x, lastY]);
|
|
33
|
+
break;
|
|
34
|
+
|
|
35
|
+
case SVGPathData.VERT_LINE_TO:
|
|
36
|
+
// Get the last X coordinate since V command only specifies Y
|
|
37
|
+
const lastX = currentRing[currentRing.length - 1][0];
|
|
38
|
+
currentRing.push([lastX, cmd.y]);
|
|
39
|
+
break;
|
|
40
|
+
|
|
41
|
+
/** Might be needed in the future **
|
|
42
|
+
case SVGPathData.CLOSE_PATH:
|
|
43
|
+
if (startPoint &&
|
|
44
|
+
(currentRing[currentRing.length - 1][0] !== startPoint[0] ||
|
|
45
|
+
currentRing[currentRing.length - 1][1] !== startPoint[1])) {
|
|
46
|
+
currentRing.push([...startPoint]);
|
|
47
|
+
}
|
|
48
|
+
rings.push({ points: currentRing });
|
|
49
|
+
currentRing = [];
|
|
50
|
+
startPoint = null;
|
|
51
|
+
break;
|
|
52
|
+
**/
|
|
53
|
+
|
|
54
|
+
// For curve commands, we just add the end point
|
|
55
|
+
case SVGPathData.CURVE_TO:
|
|
56
|
+
case SVGPathData.SMOOTH_CURVE_TO:
|
|
57
|
+
case SVGPathData.QUAD_TO:
|
|
58
|
+
case SVGPathData.SMOOTH_QUAD_TO:
|
|
59
|
+
case SVGPathData.ARC:
|
|
60
|
+
currentRing.push([cmd.x, cmd.y]);
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (currentRing.length > 2)
|
|
66
|
+
rings.push({ points: currentRing });
|
|
67
|
+
|
|
68
|
+
if (rings.length > 0) {
|
|
69
|
+
const bounds = boundsFromPoints(rings[0].points);
|
|
70
|
+
return { rings, bounds };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
}
|