@annotorious/annotorious 3.6.1 → 3.6.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.
@@ -1,3 +1,3 @@
1
- import { PolylineGeometry } from './Polyline';
2
- export declare const approximateAsPolygon: (geom: PolylineGeometry) => [number, number][];
1
+ import { PolylineGeometry, PolylinePoint } from './Polyline';
2
+ export declare const approximateAsPolygon: (corners: PolylinePoint[], closed?: boolean) => [number, number][];
3
3
  export declare const computeSVGPath: (geom: PolylineGeometry) => string;
@@ -1,2 +1,3 @@
1
- import { MultiPolygonElement } from '../../core';
1
+ import { MultiPolygonElement, PolylineGeometry } from '../../core';
2
2
  export declare const svgPathToMultiPolygonElement: (d: string) => MultiPolygonElement | undefined;
3
+ export declare const svgPathToPolyline: (d: string) => PolylineGeometry;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@annotorious/annotorious",
3
- "version": "3.6.1",
3
+ "version": "3.6.2",
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,11 +49,10 @@
49
49
  "vitest": "^3.2.4"
50
50
  },
51
51
  "dependencies": {
52
- "@annotorious/core": "3.6.1",
52
+ "@annotorious/core": "3.6.2",
53
53
  "dequal": "^2.0.3",
54
54
  "rbush": "^4.0.1",
55
55
  "simplify-js": "^1.2.4",
56
- "svg-pathdata": "^8.0.0",
57
56
  "uuid": "^11.1.0"
58
57
  }
59
58
  }
@@ -1,6 +1,6 @@
1
1
  import { ShapeType } from '../Shape';
2
2
  import { computePolygonArea, isPointInPolygon, registerShapeUtil, type ShapeUtil } from '../shapeUtils';
3
- import type { Polyline, PolylineGeometry } from './Polyline';
3
+ import type { Polyline, PolylineGeometry, PolylinePoint } from './Polyline';
4
4
 
5
5
  const PolylineUtil: ShapeUtil<Polyline> = {
6
6
 
@@ -10,7 +10,7 @@ const PolylineUtil: ShapeUtil<Polyline> = {
10
10
  if (!geom.closed || geom.points.length < 3)
11
11
  return 0;
12
12
 
13
- const points = approximateAsPolygon(geom);
13
+ const points = approximateAsPolygon(geom.points, geom.closed);
14
14
  return computePolygonArea(points);
15
15
  },
16
16
 
@@ -18,7 +18,7 @@ const PolylineUtil: ShapeUtil<Polyline> = {
18
18
  const geom = polyline.geometry;
19
19
 
20
20
  if (geom.closed) {
21
- const points = approximateAsPolygon(geom);
21
+ const points = approximateAsPolygon(geom.points, geom.closed);
22
22
  return isPointInPolygon(points, x, y);
23
23
  } else {
24
24
  return isPointNearPath(geom, [x, y], buffer);
@@ -27,17 +27,17 @@ const PolylineUtil: ShapeUtil<Polyline> = {
27
27
 
28
28
  };
29
29
 
30
- export const approximateAsPolygon = (geom: PolylineGeometry): [number, number][] => {
30
+ export const approximateAsPolygon = (corners: PolylinePoint[], closed = false): [number, number][] => {
31
31
  const points: [number, number][] = [];
32
32
 
33
- for (let i = 0; i < geom.points.length; i++) {
34
- const currentPoint = geom.points[i];
35
- const nextPoint = geom.points[(i + 1) % geom.points.length];
33
+ for (let i = 0; i < corners.length; i++) {
34
+ const currentPoint = corners[i];
35
+ const nextPoint = corners[(i + 1) % corners.length];
36
36
 
37
37
  points.push(currentPoint.point);
38
38
 
39
39
  // If there's a curve to the next point, approximate it
40
- if (i < geom.points.length - 1 || geom.closed) {
40
+ if (i < corners.length - 1 || closed) {
41
41
  const hasCurve = currentPoint.outHandle || nextPoint.inHandle;
42
42
  if (hasCurve) {
43
43
  const curvePoints = approximateBezierCurve(
@@ -1,6 +1,6 @@
1
- import { boundsFromPoints, multipolygonElementToPath, ShapeType } from '../../core';
1
+ import { boundsFromPoints, computeSVGPath, multipolygonElementToPath, ShapeType } from '../../core';
2
2
  import { parseSVGXML } from './SVG';
3
- import { svgPathToMultiPolygonElement } from './pathParser';
3
+ import { svgPathToMultiPolygonElement, svgPathToPolyline } from './pathParser';
4
4
  import type {
5
5
  Ellipse,
6
6
  EllipseGeometry,
@@ -10,6 +10,8 @@ import type {
10
10
  MultiPolygonGeometry,
11
11
  Polygon,
12
12
  PolygonGeometry,
13
+ Polyline,
14
+ PolylineGeometry,
13
15
  Shape
14
16
  } from '../../core';
15
17
 
@@ -85,7 +87,27 @@ const parseSVGLine = (value: string): Line => {
85
87
  };
86
88
  }
87
89
 
88
- const parseSVGPath = (value: string): Polygon | MultiPolygon => {
90
+ const parseSVGPathToPolyline = (value: string): Polyline => {
91
+ const doc = parseSVGXML(value);
92
+
93
+ const path = doc.nodeName === 'path' ? doc : Array.from(doc.querySelectorAll('path'))[0];
94
+ const d = path?.getAttribute('d');
95
+
96
+ if (!d)
97
+ throw new Error('Could not parse SVG path');
98
+
99
+ const polyline = svgPathToPolyline(d);
100
+
101
+ if (!polyline)
102
+ throw new Error('Could not parse SVG path');
103
+
104
+ return {
105
+ type: ShapeType.POLYLINE,
106
+ geometry: polyline
107
+ }
108
+ }
109
+
110
+ const parseSVGPathToPolygon = (value: string): Polygon | MultiPolygon => {
89
111
  const doc = parseSVGXML(value);
90
112
 
91
113
  const paths = doc.nodeName === 'path' ? [doc] : Array.from(doc.querySelectorAll('path'));
@@ -121,8 +143,10 @@ export const parseSVGSelector = <T extends Shape>(valueOrSelector: SVGSelector |
121
143
 
122
144
  if (value.includes('<polygon points='))
123
145
  return parseSVGPolygon(value) as unknown as T;
146
+ else if (value.includes('<path ') && (value.includes(' C ') || !value.includes('Z')))
147
+ return parseSVGPathToPolyline(value) as unknown as T;
124
148
  else if (value.includes('<path '))
125
- return parseSVGPath(value) as unknown as T;
149
+ return parseSVGPathToPolygon(value) as unknown as T;
126
150
  else if (value.includes('<ellipse '))
127
151
  return parseSVGEllipse(value) as unknown as T;
128
152
  else if (value.includes("<line "))
@@ -164,6 +188,10 @@ export const serializeSVGSelector = (shape: Shape): SVGSelector => {
164
188
  value = `<svg><line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" /></svg>`;
165
189
  break;
166
190
  }
191
+ case ShapeType.POLYLINE: {
192
+ const d = computeSVGPath(shape.geometry as PolylineGeometry);
193
+ value = `<svg><path d="${d}" /></svg>`;
194
+ }
167
195
  }
168
196
 
169
197
  if (value) {
@@ -1,63 +1,52 @@
1
- import { SVGPathData } from 'svg-pathdata';
2
- import { boundsFromPoints, type MultiPolygonElement, type MultiPolygonRing } from '../../core';
1
+ import { approximateAsPolygon, boundsFromPoints } from '../../core';
2
+ import type { MultiPolygonElement, MultiPolygonRing, PolylineGeometry, PolylinePoint } from '../../core';
3
3
 
4
4
  export const svgPathToMultiPolygonElement = (d: string): MultiPolygonElement | undefined => {
5
- const commands = new SVGPathData(d).toAbs().commands;
5
+ const commands = parsePathCommands(d);
6
6
 
7
7
  const rings: MultiPolygonRing[] = [];
8
-
9
8
  let currentRing: [number, number][] = [];
10
- let startPoint: [number, number] | null = null;
9
+ let currentPoint: [number, number] = [0, 0];
11
10
 
12
11
  for (const cmd of commands) {
13
- switch (cmd.type) {
14
- case SVGPathData.MOVE_TO:
12
+ switch (cmd.type.toUpperCase()) {
13
+ case 'M':
15
14
  // Start a new ring
16
15
  if (currentRing.length > 0) {
17
16
  rings.push({ points: currentRing });
18
17
  currentRing = [];
19
18
  }
20
-
21
- startPoint = [cmd.x, cmd.y];
22
- currentRing.push(startPoint);
19
+ currentPoint = [cmd.args[0], cmd.args[1]];
20
+ currentRing.push([...currentPoint]);
23
21
  break;
24
22
 
25
- case SVGPathData.LINE_TO:
26
- currentRing.push([cmd.x, cmd.y]);
23
+ case 'L':
24
+ currentPoint = [cmd.args[0], cmd.args[1]];
25
+ currentRing.push([...currentPoint]);
27
26
  break;
28
27
 
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]);
28
+ case 'H':
29
+ currentPoint = [cmd.args[0], currentPoint[1]];
30
+ currentRing.push([...currentPoint]);
33
31
  break;
34
32
 
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]);
33
+ case 'V':
34
+ currentPoint = [currentPoint[0], cmd.args[0]];
35
+ currentRing.push([...currentPoint]);
39
36
  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;
37
+
38
+ case 'C':
39
+ // For multi-polygon, we only care about the end point
40
+ currentPoint = [cmd.args[4], cmd.args[5]];
41
+ currentRing.push([...currentPoint]);
51
42
  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]);
43
+
44
+ case 'Z':
45
+ // Close the current ring (no action needed since we don't track closed state here)
46
+ break;
47
+
48
+ default:
49
+ console.warn(`Unsupported SVG path command: ${cmd.type}`);
61
50
  break;
62
51
  }
63
52
  }
@@ -69,5 +58,108 @@ export const svgPathToMultiPolygonElement = (d: string): MultiPolygonElement | u
69
58
  const bounds = boundsFromPoints(rings[0].points);
70
59
  return { rings, bounds };
71
60
  }
61
+ }
62
+
63
+ export const svgPathToPolyline = (d: string): PolylineGeometry => {
64
+ const commands = parsePathCommands(d);
65
+
66
+ const points: PolylinePoint[] = [];
67
+
68
+ let currentPoint: [number, number] = [0, 0];
69
+ let closed = false;
70
+
71
+ for (let i = 0; i < commands.length; i++) {
72
+ const cmd = commands[i];
73
+
74
+ switch (cmd.type.toUpperCase()) {
75
+ case 'M':
76
+ currentPoint = [cmd.args[0], cmd.args[1]];
77
+ points.push({
78
+ type: 'CORNER',
79
+ point: [...currentPoint]
80
+ });
81
+ break;
82
+
83
+ case 'L':
84
+ currentPoint = [cmd.args[0], cmd.args[1]];
85
+ points.push({
86
+ type: 'CORNER',
87
+ point: [...currentPoint]
88
+ });
89
+ break;
90
+
91
+ case 'C':
92
+ const cp1: [number, number] = [cmd.args[0], cmd.args[1]];
93
+ const cp2: [number, number] = [cmd.args[2], cmd.args[3]];
94
+ const endPoint: [number, number] = [cmd.args[4], cmd.args[5]];
95
+
96
+ // Set outHandle for the previous point if it doesn't match the point itself
97
+ if (points.length > 0) {
98
+ const prevPoint = points[points.length - 1];
99
+ if (cp1[0] !== prevPoint.point[0] || cp1[1] !== prevPoint.point[1]) {
100
+ prevPoint.type = 'CURVE';
101
+ prevPoint.outHandle = cp1;
102
+ }
103
+ }
104
+
105
+ // Create the end point with inHandle if it doesn't match the point itself
106
+ const newPoint: PolylinePoint = {
107
+ type: cp2[0] !== endPoint[0] || cp2[1] !== endPoint[1] ? 'CURVE' : 'CORNER',
108
+ point: endPoint
109
+ };
110
+
111
+ if (newPoint.type === 'CURVE')
112
+ newPoint.inHandle = cp2;
113
+
114
+ points.push(newPoint);
115
+ currentPoint = endPoint;
116
+ break;
117
+
118
+ case 'Z':
119
+ closed = true;
120
+ break;
121
+
122
+ default:
123
+ console.warn(`Unsupported SVG path command: ${cmd.type}`);
124
+ break;
125
+ }
126
+ }
127
+
128
+ const bounds = boundsFromPoints(approximateAsPolygon(points, closed));
72
129
 
130
+ return {
131
+ points,
132
+ closed,
133
+ bounds
134
+ };
135
+ }
136
+
137
+ interface PathCommand {
138
+
139
+ type: string;
140
+
141
+ args: number[];
142
+
143
+ }
144
+
145
+ const parsePathCommands = (d: string) => {
146
+ const commands: PathCommand[] = [];
147
+ const cleanPath = d.replace(/,/g, ' ').trim();
148
+
149
+ // Updated regex to include H and V commands
150
+ const commandRegex = /([MmLlHhVvCcZz])\s*([^MmLlHhVvCcZz]*)/g;
151
+ let match;
152
+
153
+ while ((match = commandRegex.exec(cleanPath)) !== null) {
154
+ const [, commandLetter, argsString] = match;
155
+ const args = argsString.trim() === '' ? [] :
156
+ argsString.trim().split(/\s+/).map(Number).filter(n => !isNaN(n));
157
+
158
+ commands.push({
159
+ type: commandLetter,
160
+ args
161
+ });
162
+ }
163
+
164
+ return commands;
73
165
  }