@annotorious/annotorious 3.0.0-rc.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/README.md +6 -0
- package/dist/Annotorious.d.ts +15 -0
- package/dist/Annotorious.d.ts.map +1 -0
- package/dist/AnnotoriousOpts.d.ts +14 -0
- package/dist/AnnotoriousOpts.d.ts.map +1 -0
- package/dist/annotation/SVGAnnotationLayer.svelte.d.ts +1 -0
- package/dist/annotation/SVGAnnotationLayerPointerEvent.d.ts +11 -0
- package/dist/annotation/SVGAnnotationLayerPointerEvent.d.ts.map +1 -0
- package/dist/annotation/Transform.d.ts +6 -0
- package/dist/annotation/Transform.d.ts.map +1 -0
- package/dist/annotation/editors/Editor.svelte.d.ts +1 -0
- package/dist/annotation/editors/EditorMount.svelte.d.ts +1 -0
- package/dist/annotation/editors/Handle.d.ts +14 -0
- package/dist/annotation/editors/Handle.d.ts.map +1 -0
- package/dist/annotation/editors/editorsRegistry.d.ts +5 -0
- package/dist/annotation/editors/editorsRegistry.d.ts.map +1 -0
- package/dist/annotation/editors/index.d.ts +7 -0
- package/dist/annotation/editors/index.d.ts.map +1 -0
- package/dist/annotation/editors/polygon/PolygonEditor.svelte.d.ts +1 -0
- package/dist/annotation/editors/polygon/index.d.ts +2 -0
- package/dist/annotation/editors/polygon/index.d.ts.map +1 -0
- package/dist/annotation/editors/rectangle/RectangleEditor.svelte.d.ts +1 -0
- package/dist/annotation/editors/rectangle/index.d.ts +2 -0
- package/dist/annotation/editors/rectangle/index.d.ts.map +1 -0
- package/dist/annotation/index.d.ts +7 -0
- package/dist/annotation/index.d.ts.map +1 -0
- package/dist/annotation/shapes/Ellipse.svelte.d.ts +1 -0
- package/dist/annotation/shapes/Polygon.svelte.d.ts +1 -0
- package/dist/annotation/shapes/Rectangle.svelte.d.ts +1 -0
- package/dist/annotation/shapes/index.d.ts +4 -0
- package/dist/annotation/shapes/index.d.ts.map +1 -0
- package/dist/annotation/tools/DrawingToolConfig.d.ts +8 -0
- package/dist/annotation/tools/DrawingToolConfig.d.ts.map +1 -0
- package/dist/annotation/tools/ToolMount.svelte.d.ts +1 -0
- package/dist/annotation/tools/drawingToolsRegistry.d.ts +17 -0
- package/dist/annotation/tools/drawingToolsRegistry.d.ts.map +1 -0
- package/dist/annotation/tools/index.d.ts +5 -0
- package/dist/annotation/tools/index.d.ts.map +1 -0
- package/dist/annotation/tools/polygon/RubberbandPolygon.svelte.d.ts +1 -0
- package/dist/annotation/tools/polygon/index.d.ts +2 -0
- package/dist/annotation/tools/polygon/index.d.ts.map +1 -0
- package/dist/annotation/tools/rectangle/RubberbandRectangle.svelte.d.ts +1 -0
- package/dist/annotation/tools/rectangle/index.d.ts +2 -0
- package/dist/annotation/tools/rectangle/index.d.ts.map +1 -0
- package/dist/annotation/utils/index.d.ts +4 -0
- package/dist/annotation/utils/index.d.ts.map +1 -0
- package/dist/annotation/utils/math.d.ts +2 -0
- package/dist/annotation/utils/math.d.ts.map +1 -0
- package/dist/annotation/utils/responsive.d.ts +5 -0
- package/dist/annotation/utils/responsive.d.ts.map +1 -0
- package/dist/annotation/utils/styling.d.ts +4 -0
- package/dist/annotation/utils/styling.d.ts.map +1 -0
- package/dist/annotation/utils/touch.d.ts +2 -0
- package/dist/annotation/utils/touch.d.ts.map +1 -0
- package/dist/annotorious.css +1 -0
- package/dist/annotorious.es.js +3890 -0
- package/dist/annotorious.es.js.map +1 -0
- package/dist/annotorious.js +2 -0
- package/dist/annotorious.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/model/core/ImageAnnotation.d.ts +9 -0
- package/dist/model/core/ImageAnnotation.d.ts.map +1 -0
- package/dist/model/core/Shape.d.ts +20 -0
- package/dist/model/core/Shape.d.ts.map +1 -0
- package/dist/model/core/ellipse/Ellipse.d.ts +12 -0
- package/dist/model/core/ellipse/Ellipse.d.ts.map +1 -0
- package/dist/model/core/ellipse/ellipseUtils.d.ts +2 -0
- package/dist/model/core/ellipse/ellipseUtils.d.ts.map +1 -0
- package/dist/model/core/ellipse/index.d.ts +3 -0
- package/dist/model/core/ellipse/index.d.ts.map +1 -0
- package/dist/model/core/index.d.ts +7 -0
- package/dist/model/core/index.d.ts.map +1 -0
- package/dist/model/core/polygon/Polygon.d.ts +9 -0
- package/dist/model/core/polygon/Polygon.d.ts.map +1 -0
- package/dist/model/core/polygon/index.d.ts +3 -0
- package/dist/model/core/polygon/index.d.ts.map +1 -0
- package/dist/model/core/polygon/polygonUtils.d.ts +2 -0
- package/dist/model/core/polygon/polygonUtils.d.ts.map +1 -0
- package/dist/model/core/rectangle/Rectangle.d.ts +12 -0
- package/dist/model/core/rectangle/Rectangle.d.ts.map +1 -0
- package/dist/model/core/rectangle/index.d.ts +3 -0
- package/dist/model/core/rectangle/index.d.ts.map +1 -0
- package/dist/model/core/rectangle/rectangleUtils.d.ts +4 -0
- package/dist/model/core/rectangle/rectangleUtils.d.ts.map +1 -0
- package/dist/model/core/shapeUtils.d.ts +35 -0
- package/dist/model/core/shapeUtils.d.ts.map +1 -0
- package/dist/model/index.d.ts +3 -0
- package/dist/model/index.d.ts.map +1 -0
- package/dist/model/w3c/W3CImageFormatAdapter.d.ts +7 -0
- package/dist/model/w3c/W3CImageFormatAdapter.d.ts.map +1 -0
- package/dist/model/w3c/fragment/FragmentSelector.d.ts +10 -0
- package/dist/model/w3c/fragment/FragmentSelector.d.ts.map +1 -0
- package/dist/model/w3c/fragment/index.d.ts +2 -0
- package/dist/model/w3c/fragment/index.d.ts.map +1 -0
- package/dist/model/w3c/index.d.ts +4 -0
- package/dist/model/w3c/index.d.ts.map +1 -0
- package/dist/model/w3c/svg/SVG.d.ts +5 -0
- package/dist/model/w3c/svg/SVG.d.ts.map +1 -0
- package/dist/model/w3c/svg/SVGSelector.d.ts +9 -0
- package/dist/model/w3c/svg/SVGSelector.d.ts.map +1 -0
- package/dist/model/w3c/svg/index.d.ts +2 -0
- package/dist/model/w3c/svg/index.d.ts.map +1 -0
- package/dist/state/ImageAnnotationStore.d.ts +11 -0
- package/dist/state/ImageAnnotationStore.d.ts.map +1 -0
- package/dist/state/ImageAnnotatorState.d.ts +12 -0
- package/dist/state/ImageAnnotatorState.d.ts.map +1 -0
- package/dist/state/index.d.ts +3 -0
- package/dist/state/index.d.ts.map +1 -0
- package/dist/state/spatialTree.d.ts +21 -0
- package/dist/state/spatialTree.d.ts.map +1 -0
- package/dist/themes/index.d.ts +2 -0
- package/dist/themes/index.d.ts.map +1 -0
- package/dist/themes/smart/index.d.ts +2 -0
- package/dist/themes/smart/index.d.ts.map +1 -0
- package/dist/themes/smart/setTheme.d.ts +3 -0
- package/dist/themes/smart/setTheme.d.ts.map +1 -0
- package/package.json +55 -0
- package/src/Annotorious.css +74 -0
- package/src/Annotorious.ts +158 -0
- package/src/AnnotoriousOpts.ts +40 -0
- package/src/annotation/SVGAnnotationLayer.svelte +169 -0
- package/src/annotation/SVGAnnotationLayerPointerEvent.ts +55 -0
- package/src/annotation/Transform.ts +24 -0
- package/src/annotation/editors/Editor.svelte +61 -0
- package/src/annotation/editors/EditorMount.svelte +44 -0
- package/src/annotation/editors/Handle.ts +21 -0
- package/src/annotation/editors/editorsRegistry.ts +14 -0
- package/src/annotation/editors/index.ts +7 -0
- package/src/annotation/editors/polygon/PolygonEditor.svelte +64 -0
- package/src/annotation/editors/polygon/index.ts +1 -0
- package/src/annotation/editors/rectangle/RectangleEditor.svelte +143 -0
- package/src/annotation/editors/rectangle/index.ts +1 -0
- package/src/annotation/index.ts +7 -0
- package/src/annotation/shapes/Ellipse.svelte +32 -0
- package/src/annotation/shapes/Polygon.svelte +26 -0
- package/src/annotation/shapes/Rectangle.svelte +32 -0
- package/src/annotation/shapes/index.ts +3 -0
- package/src/annotation/tools/DrawingToolConfig.ts +9 -0
- package/src/annotation/tools/ToolMount.svelte +49 -0
- package/src/annotation/tools/drawingToolsRegistry.ts +26 -0
- package/src/annotation/tools/index.ts +4 -0
- package/src/annotation/tools/polygon/RubberbandPolygon.svelte +165 -0
- package/src/annotation/tools/polygon/index.ts +1 -0
- package/src/annotation/tools/rectangle/RubberbandRectangle.svelte +131 -0
- package/src/annotation/tools/rectangle/index.ts +1 -0
- package/src/annotation/utils/index.ts +3 -0
- package/src/annotation/utils/math.ts +6 -0
- package/src/annotation/utils/responsive.ts +56 -0
- package/src/annotation/utils/styling.ts +19 -0
- package/src/annotation/utils/touch.ts +1 -0
- package/src/index.ts +20 -0
- package/src/model/core/ImageAnnotation.ts +14 -0
- package/src/model/core/Shape.ts +37 -0
- package/src/model/core/ellipse/Ellipse.ts +21 -0
- package/src/model/core/ellipse/ellipseUtils.ts +28 -0
- package/src/model/core/ellipse/index.ts +2 -0
- package/src/model/core/index.ts +6 -0
- package/src/model/core/polygon/Polygon.ts +15 -0
- package/src/model/core/polygon/index.ts +2 -0
- package/src/model/core/polygon/polygonUtils.ts +43 -0
- package/src/model/core/rectangle/Rectangle.ts +21 -0
- package/src/model/core/rectangle/index.ts +2 -0
- package/src/model/core/rectangle/rectangleUtils.ts +17 -0
- package/src/model/core/shapeUtils.ts +57 -0
- package/src/model/index.ts +2 -0
- package/src/model/w3c/W3CImageFormatAdapter.ts +83 -0
- package/src/model/w3c/fragment/FragmentSelector.ts +59 -0
- package/src/model/w3c/fragment/index.ts +1 -0
- package/src/model/w3c/index.ts +3 -0
- package/src/model/w3c/svg/SVG.ts +36 -0
- package/src/model/w3c/svg/SVGSelector.ts +99 -0
- package/src/model/w3c/svg/index.ts +1 -0
- package/src/state/ImageAnnotationStore.ts +18 -0
- package/src/state/ImageAnnotatorState.ts +88 -0
- package/src/state/index.ts +2 -0
- package/src/state/spatialTree.ts +108 -0
- package/src/themes/dark/index.css +24 -0
- package/src/themes/index.ts +1 -0
- package/src/themes/light/index.css +30 -0
- package/src/themes/smart/index.ts +1 -0
- package/src/themes/smart/setTheme.ts +46 -0
- package/src/vite-env.d.ts +2 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { writable } from 'svelte/store';
|
|
2
|
+
|
|
3
|
+
const setSize = (image: HTMLImageElement | HTMLCanvasElement, svg: SVGElement) => {
|
|
4
|
+
const { naturalWidth, naturalHeight } = (image as HTMLImageElement);
|
|
5
|
+
|
|
6
|
+
if (!naturalWidth && !naturalHeight) {
|
|
7
|
+
// Might be because a) the image has not loaded yet, or b) because it's not
|
|
8
|
+
// an image element (but maybe a CANVAS etc.)! Allow for both possibilities.
|
|
9
|
+
const { width, height } = image;
|
|
10
|
+
|
|
11
|
+
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
|
12
|
+
|
|
13
|
+
image.addEventListener('load', event => {
|
|
14
|
+
const img = event.target as HTMLImageElement;
|
|
15
|
+
svg.setAttribute('viewBox', `0 0 ${img.naturalWidth} ${img.naturalHeight}`);
|
|
16
|
+
});
|
|
17
|
+
} else {
|
|
18
|
+
svg.setAttribute('viewBox', `0 0 ${naturalWidth} ${naturalHeight}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const enableResponsive = (image: HTMLImageElement | HTMLCanvasElement, svg: SVGSVGElement) => {
|
|
24
|
+
|
|
25
|
+
setSize(image, svg);
|
|
26
|
+
|
|
27
|
+
const { subscribe, set } = writable(1);
|
|
28
|
+
|
|
29
|
+
let resizeObserver: ResizeObserver;
|
|
30
|
+
|
|
31
|
+
if (window.ResizeObserver) {
|
|
32
|
+
resizeObserver = new ResizeObserver(() => {
|
|
33
|
+
const svgBounds = svg.getBoundingClientRect();
|
|
34
|
+
|
|
35
|
+
const { width, height } = svg.viewBox.baseVal;
|
|
36
|
+
|
|
37
|
+
const scale = Math.max(
|
|
38
|
+
svgBounds.width / width,
|
|
39
|
+
svgBounds.height / height
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
set(scale);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
resizeObserver.observe(svg.parentElement);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const destroy = () => {
|
|
49
|
+
if (resizeObserver)
|
|
50
|
+
resizeObserver.disconnect();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { destroy, subscribe };
|
|
54
|
+
|
|
55
|
+
}
|
|
56
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { DrawingStyle } from '@annotorious/core';
|
|
2
|
+
import type { ImageAnnotation } from '../../model';
|
|
3
|
+
|
|
4
|
+
export const computeStyle = (annotation: ImageAnnotation, style?: DrawingStyle | ((a: ImageAnnotation) => DrawingStyle)) => {
|
|
5
|
+
const computed = typeof style === 'function' ? style(annotation) : style;
|
|
6
|
+
|
|
7
|
+
if (computed) {
|
|
8
|
+
const { fill, fillOpacity } = computed;
|
|
9
|
+
|
|
10
|
+
let css = '';
|
|
11
|
+
|
|
12
|
+
if (fill)
|
|
13
|
+
css += `fill:${fill};stroke:${fill};`;
|
|
14
|
+
|
|
15
|
+
css += `fill-opacity:${fillOpacity || '0.25'};`;
|
|
16
|
+
|
|
17
|
+
return css;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const isTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export * from './annotation';
|
|
2
|
+
export * from './model';
|
|
3
|
+
export * from './state';
|
|
4
|
+
export * from './themes';
|
|
5
|
+
export * from './annotation/tools';
|
|
6
|
+
export * from './Annotorious';
|
|
7
|
+
export * from './AnnotoriousOpts';
|
|
8
|
+
|
|
9
|
+
// Re-export essentials from @annotorious/core utilities for convenience
|
|
10
|
+
export * from '@annotorious/core/src/model';
|
|
11
|
+
export * from '@annotorious/core/src/presence';
|
|
12
|
+
export * from '@annotorious/core/src/utils';
|
|
13
|
+
|
|
14
|
+
export type {
|
|
15
|
+
HoverState,
|
|
16
|
+
Selection,
|
|
17
|
+
SelectionState,
|
|
18
|
+
Store,
|
|
19
|
+
StoreObserver
|
|
20
|
+
} from '@annotorious/core/src/state';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Annotation, AnnotationTarget } from '@annotorious/core';
|
|
2
|
+
import type { Shape } from './Shape';
|
|
3
|
+
|
|
4
|
+
export interface ImageAnnotation extends Annotation {
|
|
5
|
+
|
|
6
|
+
target: ImageAnnotationTarget;
|
|
7
|
+
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ImageAnnotationTarget extends AnnotationTarget {
|
|
11
|
+
|
|
12
|
+
selector: Shape
|
|
13
|
+
|
|
14
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { AbstractSelector } from '@annotorious/core';
|
|
2
|
+
|
|
3
|
+
export interface Shape extends AbstractSelector {
|
|
4
|
+
|
|
5
|
+
type: ShapeType;
|
|
6
|
+
|
|
7
|
+
geometry: Geometry;
|
|
8
|
+
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export enum ShapeType {
|
|
12
|
+
|
|
13
|
+
ELLIPSE = 'ELLIPSE',
|
|
14
|
+
|
|
15
|
+
POLYGON = 'POLYGON',
|
|
16
|
+
|
|
17
|
+
RECTANGLE = 'RECTANGLE'
|
|
18
|
+
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface Geometry {
|
|
22
|
+
|
|
23
|
+
bounds: Bounds;
|
|
24
|
+
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface Bounds {
|
|
28
|
+
|
|
29
|
+
minX: number;
|
|
30
|
+
|
|
31
|
+
minY: number;
|
|
32
|
+
|
|
33
|
+
maxX: number;
|
|
34
|
+
|
|
35
|
+
maxY: number;
|
|
36
|
+
|
|
37
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Bounds, Geometry, Shape } from '../Shape';
|
|
2
|
+
|
|
3
|
+
export interface Ellipse extends Shape {
|
|
4
|
+
|
|
5
|
+
geometry: EllipseGeometry;
|
|
6
|
+
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface EllipseGeometry extends Geometry {
|
|
10
|
+
|
|
11
|
+
cx: number;
|
|
12
|
+
|
|
13
|
+
cy: number;
|
|
14
|
+
|
|
15
|
+
rx: number;
|
|
16
|
+
|
|
17
|
+
ry: number;
|
|
18
|
+
|
|
19
|
+
bounds: Bounds;
|
|
20
|
+
|
|
21
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ShapeType } from '../Shape';
|
|
2
|
+
import { registerShapeUtil, type ShapeUtil } from '../shapeUtils';
|
|
3
|
+
import type { Ellipse } from './Ellipse';
|
|
4
|
+
|
|
5
|
+
const EllipseUtil: ShapeUtil<Ellipse> = {
|
|
6
|
+
|
|
7
|
+
area: (e: Ellipse): number => Math.PI * e.geometry.rx * e.geometry.ry,
|
|
8
|
+
|
|
9
|
+
intersects: (e: Ellipse, x: number, y: number): boolean => {
|
|
10
|
+
const { cx, cy, rx, ry } = e.geometry;
|
|
11
|
+
|
|
12
|
+
// For future use
|
|
13
|
+
const rot = 0;
|
|
14
|
+
|
|
15
|
+
const cos = Math.cos(rot);
|
|
16
|
+
const sin = Math.sin(rot);
|
|
17
|
+
|
|
18
|
+
const dx = x - cx;
|
|
19
|
+
const dy = y - cy;
|
|
20
|
+
|
|
21
|
+
const tdx = cos * dx + sin * dy;
|
|
22
|
+
const tdy = sin * dx - cos * dy;
|
|
23
|
+
|
|
24
|
+
return (tdx * tdx) / (rx * rx) + (tdy * tdy) / (ry * ry) <= 1;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
registerShapeUtil(ShapeType.ELLIPSE, EllipseUtil);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Bounds, Geometry, Shape } from '../Shape';
|
|
2
|
+
|
|
3
|
+
export interface Polygon extends Shape {
|
|
4
|
+
|
|
5
|
+
geometry: PolygonGeometry;
|
|
6
|
+
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface PolygonGeometry extends Geometry {
|
|
10
|
+
|
|
11
|
+
points: Array<Array<number>>;
|
|
12
|
+
|
|
13
|
+
bounds: Bounds;
|
|
14
|
+
|
|
15
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ShapeType } from '../Shape';
|
|
2
|
+
import { registerShapeUtil, type ShapeUtil } from '../shapeUtils';
|
|
3
|
+
import type { Polygon } from './Polygon';
|
|
4
|
+
|
|
5
|
+
const PolygonUtil: ShapeUtil<Polygon> = {
|
|
6
|
+
|
|
7
|
+
area: (polygon: Polygon): number => {
|
|
8
|
+
const { points } = polygon.geometry;
|
|
9
|
+
|
|
10
|
+
let area = 0;
|
|
11
|
+
let j = points.length - 1;
|
|
12
|
+
|
|
13
|
+
for (let i = 0; i < points.length; i++) {
|
|
14
|
+
area += (points[j][0] + points[i][0]) * (points[j][1] - points[i][1]);
|
|
15
|
+
j = i;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return Math.abs(0.5 * area);
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
intersects: (polygon: Polygon, x: number, y: number): boolean => {
|
|
22
|
+
// Based on https://wrf.ecse.rpi.edu/Research/Short_Notes/pnpoly.html/pnpoly.html
|
|
23
|
+
const { points } = polygon.geometry;
|
|
24
|
+
|
|
25
|
+
let inside = false;
|
|
26
|
+
|
|
27
|
+
for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
|
|
28
|
+
const xi = points[i][0],
|
|
29
|
+
yi = points[i][1];
|
|
30
|
+
const xj = points[j][0],
|
|
31
|
+
yj = points[j][1];
|
|
32
|
+
|
|
33
|
+
const intersect = yi > y != yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi;
|
|
34
|
+
|
|
35
|
+
if (intersect) inside = !inside;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return inside;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
registerShapeUtil(ShapeType.POLYGON, PolygonUtil);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Bounds, Geometry, Shape } from '../Shape';
|
|
2
|
+
|
|
3
|
+
export interface Rectangle extends Shape {
|
|
4
|
+
|
|
5
|
+
geometry: RectangleGeometry;
|
|
6
|
+
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface RectangleGeometry extends Geometry {
|
|
10
|
+
|
|
11
|
+
x: number;
|
|
12
|
+
|
|
13
|
+
y: number;
|
|
14
|
+
|
|
15
|
+
w: number;
|
|
16
|
+
|
|
17
|
+
h: number;
|
|
18
|
+
|
|
19
|
+
bounds: Bounds;
|
|
20
|
+
|
|
21
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ShapeType } from '../Shape';
|
|
2
|
+
import type { Rectangle } from './Rectangle';
|
|
3
|
+
import { registerShapeUtil, type ShapeUtil } from '../shapeUtils';
|
|
4
|
+
|
|
5
|
+
export const RectangleUtil: ShapeUtil<Rectangle> = {
|
|
6
|
+
|
|
7
|
+
area: (rect: Rectangle): number => rect.geometry.w * rect.geometry.h,
|
|
8
|
+
|
|
9
|
+
intersects: (rect: Rectangle, x: number, y: number): boolean =>
|
|
10
|
+
x >= rect.geometry.x &&
|
|
11
|
+
x <= rect.geometry.x + rect.geometry.w &&
|
|
12
|
+
y >= rect.geometry.y &&
|
|
13
|
+
y <= rect.geometry.y + rect.geometry.h
|
|
14
|
+
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
registerShapeUtil(ShapeType.RECTANGLE, RectangleUtil);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Bounds, Shape, ShapeType } from './Shape';
|
|
2
|
+
|
|
3
|
+
export interface ShapeUtil<T extends Shape> {
|
|
4
|
+
|
|
5
|
+
area: (shape: T) => number;
|
|
6
|
+
|
|
7
|
+
intersects: (shape: T, x: number, y: number) => boolean;
|
|
8
|
+
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const Utils: { [key: string]: ShapeUtil<any> } = {};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Registers a new ShapeUtil for a given shape type.
|
|
15
|
+
* @param type the shape type
|
|
16
|
+
* @param util the ShapeUtil implementation for this shape type
|
|
17
|
+
*/
|
|
18
|
+
export const registerShapeUtil = (type: ShapeType | string, util: ShapeUtil<any>) =>
|
|
19
|
+
(Utils[type] = util);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Computes the area of the given shape. Delegates to the corresponding ShapeUtil.
|
|
23
|
+
* @param shape the shape
|
|
24
|
+
*/
|
|
25
|
+
export const computeArea = (shape: Shape) => Utils[shape.type].area(shape);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Tests if the given shape intersects the given point. Delegates to
|
|
29
|
+
* the corresponding ShapeUtil.
|
|
30
|
+
* @param shape the shape
|
|
31
|
+
* @param x point x coord
|
|
32
|
+
* @param y point y coord
|
|
33
|
+
* @returns true if shape and point intersect
|
|
34
|
+
*/
|
|
35
|
+
export const intersects = (shape: Shape, x: number, y: number): boolean =>
|
|
36
|
+
Utils[shape.type].intersects(shape, x, y);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Computes Bounds from a given list of points.
|
|
40
|
+
* @param points the points
|
|
41
|
+
* @returns the Bounds
|
|
42
|
+
*/
|
|
43
|
+
export const boundsFromPoints = (points: Array<[number, number]>): Bounds => {
|
|
44
|
+
let minX = Infinity;
|
|
45
|
+
let minY = Infinity;
|
|
46
|
+
let maxX = -Infinity;
|
|
47
|
+
let maxY = -Infinity;
|
|
48
|
+
|
|
49
|
+
points.forEach(([x, y]) => {
|
|
50
|
+
minX = Math.min(minX, x);
|
|
51
|
+
minY = Math.min(minY, y);
|
|
52
|
+
maxX = Math.max(maxX, x);
|
|
53
|
+
maxY = Math.max(maxY, y);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return { minX, minY, maxX, maxY };
|
|
57
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
2
|
+
import { parseW3CBodies, serializeW3CBodies } from '@annotorious/core';
|
|
3
|
+
import type { FormatAdapter, ParseResult, W3CAnnotation } from '@annotorious/core';
|
|
4
|
+
import { ShapeType } from '../core';
|
|
5
|
+
import type { ImageAnnotation, RectangleGeometry } from '../core';
|
|
6
|
+
import type { FragmentSelector } from './fragment';
|
|
7
|
+
import { parseFragmentSelector, serializeFragmentSelector } from './fragment';
|
|
8
|
+
import type { SVGSelector } from './svg';
|
|
9
|
+
import { parseSVGSelector, serializeSVGSelector } from './svg';
|
|
10
|
+
|
|
11
|
+
export type W3CImageFormatAdapter = FormatAdapter<ImageAnnotation, W3CAnnotation>;
|
|
12
|
+
|
|
13
|
+
export const W3CImageFormat = (
|
|
14
|
+
source: string,
|
|
15
|
+
invertY: boolean = false
|
|
16
|
+
): W3CImageFormatAdapter => {
|
|
17
|
+
|
|
18
|
+
const parse = (serialized: W3CAnnotation) =>
|
|
19
|
+
parseW3CImageAnnotation(serialized, invertY);
|
|
20
|
+
|
|
21
|
+
const serialize = (annotation: ImageAnnotation) =>
|
|
22
|
+
serializeW3CImageAnnotation(annotation, source);
|
|
23
|
+
|
|
24
|
+
return { parse, serialize }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const parseW3CImageAnnotation = (
|
|
28
|
+
annotation: W3CAnnotation,
|
|
29
|
+
invertY: boolean = false
|
|
30
|
+
): ParseResult<ImageAnnotation> => {
|
|
31
|
+
const annotationId = annotation.id || uuidv4();
|
|
32
|
+
|
|
33
|
+
const bodies = parseW3CBodies(annotation.body, annotationId);
|
|
34
|
+
|
|
35
|
+
const target = Array.isArray(annotation.target) ? annotation.target[0] : annotation.target;
|
|
36
|
+
|
|
37
|
+
const w3cSelector = Array.isArray(target.selector) ? target.selector[0] : target.selector;
|
|
38
|
+
|
|
39
|
+
const selector =
|
|
40
|
+
w3cSelector.type === 'FragmentSelector' ?
|
|
41
|
+
parseFragmentSelector(w3cSelector as FragmentSelector, invertY) :
|
|
42
|
+
w3cSelector.type === 'SvgSelector' ?
|
|
43
|
+
parseSVGSelector(w3cSelector as SVGSelector) : undefined;
|
|
44
|
+
|
|
45
|
+
return selector ? {
|
|
46
|
+
parsed: {
|
|
47
|
+
...annotation,
|
|
48
|
+
id: annotationId,
|
|
49
|
+
bodies,
|
|
50
|
+
target: {
|
|
51
|
+
annotation: annotationId,
|
|
52
|
+
selector
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} : {
|
|
56
|
+
error: Error(`Unknown selector type: ${selector.type}`)
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const serializeW3CImageAnnotation = (
|
|
62
|
+
annotation: ImageAnnotation,
|
|
63
|
+
source: string
|
|
64
|
+
): W3CAnnotation => {
|
|
65
|
+
const shape = annotation.target.selector;
|
|
66
|
+
|
|
67
|
+
const selector =
|
|
68
|
+
shape.type == ShapeType.RECTANGLE ?
|
|
69
|
+
serializeFragmentSelector(shape.geometry as RectangleGeometry) :
|
|
70
|
+
serializeSVGSelector(shape);
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
...annotation,
|
|
74
|
+
'@context': 'http://www.w3.org/ns/anno.jsonld',
|
|
75
|
+
id: annotation.id,
|
|
76
|
+
type: 'Annotation',
|
|
77
|
+
body: serializeW3CBodies(annotation.bodies),
|
|
78
|
+
target: {
|
|
79
|
+
source,
|
|
80
|
+
selector
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { W3CSelector } from '@annotorious/core';
|
|
2
|
+
import { ShapeType } from '../../core';
|
|
3
|
+
import type { Rectangle, RectangleGeometry } from '../../core';
|
|
4
|
+
|
|
5
|
+
export interface FragmentSelector extends W3CSelector {
|
|
6
|
+
|
|
7
|
+
type: 'FragmentSelector';
|
|
8
|
+
|
|
9
|
+
conformsTo: 'http://www.w3.org/TR/media-frags/',
|
|
10
|
+
|
|
11
|
+
value: string;
|
|
12
|
+
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const parseFragmentSelector = (
|
|
16
|
+
fragmentOrSelector: FragmentSelector | string,
|
|
17
|
+
invertY = false
|
|
18
|
+
): Rectangle => {
|
|
19
|
+
|
|
20
|
+
const fragment =
|
|
21
|
+
typeof fragmentOrSelector === 'string' ? fragmentOrSelector : fragmentOrSelector.value;
|
|
22
|
+
|
|
23
|
+
const regex = /^(xywh)=(pixel|percent)?:?(.+?),(.+?),(.+?),(.+)*/g;
|
|
24
|
+
|
|
25
|
+
const matches = [...fragment.matchAll(regex)][0];
|
|
26
|
+
const [_, prefix, unit, a, b, c, d] = matches;
|
|
27
|
+
|
|
28
|
+
if (prefix !== 'xywh') throw new Error('Unsupported MediaFragment: ' + fragment);
|
|
29
|
+
|
|
30
|
+
if (unit && unit !== 'pixel') throw new Error(`Unsupported MediaFragment unit: ${unit}`);
|
|
31
|
+
|
|
32
|
+
const [x, y, w, h] = [a, b, c, d].map(parseFloat);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
type: ShapeType.RECTANGLE,
|
|
36
|
+
geometry: {
|
|
37
|
+
x,
|
|
38
|
+
y,
|
|
39
|
+
w,
|
|
40
|
+
h,
|
|
41
|
+
bounds: {
|
|
42
|
+
minX: x,
|
|
43
|
+
minY: invertY ? y - h : y,
|
|
44
|
+
maxX: x + w,
|
|
45
|
+
maxY: invertY ? y : y + h
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const serializeFragmentSelector = (geometry: RectangleGeometry): FragmentSelector => {
|
|
52
|
+
const { x, y, w, h } = geometry;
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
type: 'FragmentSelector',
|
|
56
|
+
conformsTo: 'http://www.w3.org/TR/media-frags/',
|
|
57
|
+
value: `xywh=pixel:${x},${y},${w},${h}`
|
|
58
|
+
};
|
|
59
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './FragmentSelector';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
|
|
2
|
+
|
|
3
|
+
export const sanitize = (doc: Element | Document) => {
|
|
4
|
+
// Cf. https://github.com/mattkrick/sanitize-svg#readme
|
|
5
|
+
// for the basic approach
|
|
6
|
+
const cleanEl = (el: Element) => {
|
|
7
|
+
Array.from(el.attributes).forEach(attr => {
|
|
8
|
+
if (attr.name.startsWith('on'))
|
|
9
|
+
el.removeAttribute(attr.name)
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Remove script tags
|
|
14
|
+
const scripts = doc.getElementsByTagName('script');
|
|
15
|
+
|
|
16
|
+
Array.from(scripts).reverse().forEach(el =>
|
|
17
|
+
el.parentNode.removeChild(el));
|
|
18
|
+
|
|
19
|
+
Array.from(doc.querySelectorAll('*')).forEach(cleanEl);
|
|
20
|
+
|
|
21
|
+
return doc;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Helper that forces an un-namespaced node to SVG **/
|
|
25
|
+
export const insertSVGNamespace = (originalDoc: Document): Element => {
|
|
26
|
+
// Serialize and parse for the namespace to take effect on every node
|
|
27
|
+
const serializer = new XMLSerializer();
|
|
28
|
+
const str = serializer.serializeToString(originalDoc.documentElement);
|
|
29
|
+
|
|
30
|
+
// Doesn't seem that there's a clean cross-browser way for this...
|
|
31
|
+
const namespaced = str.replace('<svg>', `<svg xmlns="${SVG_NAMESPACE}">`);
|
|
32
|
+
|
|
33
|
+
const parser = new DOMParser();
|
|
34
|
+
const namespacedDoc = parser.parseFromString(namespaced, "image/svg+xml");
|
|
35
|
+
return namespacedDoc.documentElement;
|
|
36
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { W3CSelector } from '@annotorious/core';
|
|
2
|
+
import { boundsFromPoints, ShapeType } from '../../core';
|
|
3
|
+
import type { Ellipse, EllipseGeometry, Polygon, PolygonGeometry, Shape } from '../../core';
|
|
4
|
+
import { SVG_NAMESPACE, insertSVGNamespace, sanitize } from './SVG';
|
|
5
|
+
|
|
6
|
+
export interface SVGSelector extends W3CSelector {
|
|
7
|
+
|
|
8
|
+
type: 'SvgSelector';
|
|
9
|
+
|
|
10
|
+
value: string;
|
|
11
|
+
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const parseSVGXML = (value: string): Element => {
|
|
15
|
+
const parser = new DOMParser();
|
|
16
|
+
|
|
17
|
+
const doc = parser.parseFromString(value, "image/svg+xml");
|
|
18
|
+
|
|
19
|
+
// SVG needs a namespace declaration - check if it's set or insert if not
|
|
20
|
+
const isPrefixDeclared = doc.lookupPrefix(SVG_NAMESPACE); // SVG declared via prefix
|
|
21
|
+
const isDefaultNamespaceSVG = doc.lookupNamespaceURI(null); // SVG declared as default namespace
|
|
22
|
+
|
|
23
|
+
if (isPrefixDeclared || isDefaultNamespaceSVG) {
|
|
24
|
+
return sanitize(doc).firstChild as Element;
|
|
25
|
+
} else {
|
|
26
|
+
return sanitize(insertSVGNamespace(doc)).firstChild as Element;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const parseSVGPolygon = (value: string): Polygon => {
|
|
31
|
+
const [a, b, str] = value.match(/(<polygon points=["|'])([^("|')]*)/) || [];
|
|
32
|
+
|
|
33
|
+
if (!str) return;
|
|
34
|
+
|
|
35
|
+
const points = str.split(' ').map((p) => p.split(',').map(parseFloat));
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
type: ShapeType.POLYGON,
|
|
39
|
+
geometry: {
|
|
40
|
+
points,
|
|
41
|
+
bounds: boundsFromPoints(points as [number, number][])
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const parseSVGEllipse = (value: string): Ellipse => {
|
|
47
|
+
const doc = parseSVGXML(value);
|
|
48
|
+
|
|
49
|
+
const cx = parseFloat(doc.getAttribute('cx'));
|
|
50
|
+
const cy = parseFloat(doc.getAttribute('cy'));
|
|
51
|
+
const rx = parseFloat(doc.getAttribute('rx'));
|
|
52
|
+
const ry = parseFloat(doc.getAttribute('ry'));
|
|
53
|
+
|
|
54
|
+
const bounds = {
|
|
55
|
+
minX: cx - rx,
|
|
56
|
+
minY: cy - ry,
|
|
57
|
+
maxX: cx + rx,
|
|
58
|
+
maxY: cy + ry
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
type: ShapeType.ELLIPSE,
|
|
63
|
+
geometry: {
|
|
64
|
+
cx,
|
|
65
|
+
cy,
|
|
66
|
+
rx,
|
|
67
|
+
ry,
|
|
68
|
+
bounds
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const parseSVGSelector = <T extends Shape>(valueOrSelector: SVGSelector | string): T => {
|
|
74
|
+
const value = typeof valueOrSelector === 'string' ? valueOrSelector : valueOrSelector.value;
|
|
75
|
+
|
|
76
|
+
if (value.includes('<polygon points='))
|
|
77
|
+
return parseSVGPolygon(value) as unknown as T;
|
|
78
|
+
else if (value.includes('<ellipse '))
|
|
79
|
+
return parseSVGEllipse(value) as unknown as T;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const serializeSVGSelector = (shape: Shape): SVGSelector => {
|
|
83
|
+
let value: string;
|
|
84
|
+
|
|
85
|
+
if (shape.type === ShapeType.POLYGON) {
|
|
86
|
+
const geom = shape.geometry as PolygonGeometry;
|
|
87
|
+
const { points } = geom;
|
|
88
|
+
value = `<svg><polygon points="${points.map((xy) => xy.join(',')).join(' ')}" /></svg>`;
|
|
89
|
+
} else if (shape.type === ShapeType.ELLIPSE) {
|
|
90
|
+
const geom = shape.geometry as EllipseGeometry;
|
|
91
|
+
value = `<svg><ellipse cx="${geom.cx}" cy="${geom.cy}" rx="${geom.rx}" ry="${geom.ry}" /></svg>`
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (value) {
|
|
95
|
+
return { type: 'SvgSelector', value };
|
|
96
|
+
} else {
|
|
97
|
+
throw `Unsupported shape type: ${shape.type}`;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './SVGSelector';
|