@annotorious/annotorious 3.8.2 → 3.8.4
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 +1527 -1492
- package/dist/annotorious.es.js.map +1 -1
- package/dist/annotorious.js +1 -1
- package/dist/annotorious.js.map +1 -1
- package/package.json +5 -5
- package/src/model/w3c/W3CImageFormatAdapter.ts +25 -2
- package/src/model/w3c/fragment/FragmentSelector.ts +17 -3
- package/src/model/w3c/svg/SVG.ts +7 -1
- package/src/model/w3c/svg/pathParser.ts +26 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@annotorious/annotorious",
|
|
3
|
-
"version": "3.8.
|
|
3
|
+
"version": "3.8.4",
|
|
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",
|
|
@@ -46,16 +46,16 @@
|
|
|
46
46
|
"@sveltejs/vite-plugin-svelte": "^3.1.2",
|
|
47
47
|
"@tsconfig/svelte": "^5.0.8",
|
|
48
48
|
"@types/rbush": "^4.0.0",
|
|
49
|
-
"jsdom": "^29.1.
|
|
49
|
+
"jsdom": "^29.1.1",
|
|
50
50
|
"svelte": "^4.2.20",
|
|
51
|
-
"svelte-preprocess": "^6.0.
|
|
51
|
+
"svelte-preprocess": "^6.0.5",
|
|
52
52
|
"typescript": "^5.9.3",
|
|
53
53
|
"vite": "^5.4.21",
|
|
54
54
|
"vite-plugin-dts": "^4.5.4",
|
|
55
|
-
"vitest": "^3.2.
|
|
55
|
+
"vitest": "^3.2.6"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@annotorious/core": "3.8.
|
|
58
|
+
"@annotorious/core": "3.8.4",
|
|
59
59
|
"dequal": "^2.0.3",
|
|
60
60
|
"rbush": "^4.0.1",
|
|
61
61
|
"simplify-js": "^1.2.4",
|
|
@@ -33,6 +33,22 @@ export const W3CImageFormat = (
|
|
|
33
33
|
return { parse, serialize }
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
const isSupportedSelector = (selector: any): boolean =>
|
|
37
|
+
selector?.type === 'SvgSelector' ||
|
|
38
|
+
selector?.type === 'FragmentSelector' ||
|
|
39
|
+
isFragmentSelector(selector);
|
|
40
|
+
|
|
41
|
+
const isSupportedTarget = (target: any): boolean => {
|
|
42
|
+
if (typeof target === 'string')
|
|
43
|
+
return isFragmentSelector(target); // Plain string fragment selector
|
|
44
|
+
|
|
45
|
+
const selector = Array.isArray(target.selector)
|
|
46
|
+
? target.selector.find(isSupportedSelector)
|
|
47
|
+
: target.selector;
|
|
48
|
+
|
|
49
|
+
return isSupportedSelector(selector);
|
|
50
|
+
}
|
|
51
|
+
|
|
36
52
|
export const parseW3CImageAnnotation = (
|
|
37
53
|
annotation: W3CAnnotation,
|
|
38
54
|
opts: W3CImageFormatAdapterOpts = { strict: true, invertY: false }
|
|
@@ -49,8 +65,15 @@ export const parseW3CImageAnnotation = (
|
|
|
49
65
|
|
|
50
66
|
const bodies = parseW3CBodies(body || [], annotationId);
|
|
51
67
|
|
|
52
|
-
const w3cTarget = Array.isArray(annotation.target)
|
|
53
|
-
? annotation.target
|
|
68
|
+
const w3cTarget = Array.isArray(annotation.target)
|
|
69
|
+
? annotation.target.find(isSupportedTarget)
|
|
70
|
+
: annotation.target;
|
|
71
|
+
|
|
72
|
+
if (!w3cTarget) {
|
|
73
|
+
return {
|
|
74
|
+
error: Error(`Unsupported target(s): ${JSON.stringify(w3cTarget)}`)
|
|
75
|
+
};
|
|
76
|
+
}
|
|
54
77
|
|
|
55
78
|
const w3cSelector =
|
|
56
79
|
typeof w3cTarget === 'string' ? w3cTarget :
|
|
@@ -11,6 +11,10 @@ export interface FragmentSelector {
|
|
|
11
11
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
const number = '-?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?';
|
|
15
|
+
|
|
16
|
+
const MAX_FRAGMENT_LENGTH = 512; // ReDoS guard
|
|
17
|
+
|
|
14
18
|
export const isFragmentSelector = (
|
|
15
19
|
selector: any
|
|
16
20
|
): boolean => {
|
|
@@ -18,10 +22,15 @@ export const isFragmentSelector = (
|
|
|
18
22
|
return true;
|
|
19
23
|
|
|
20
24
|
if (typeof selector === 'string') {
|
|
25
|
+
if (selector.length > MAX_FRAGMENT_LENGTH) return false;
|
|
26
|
+
|
|
21
27
|
const hashIndex = selector.indexOf('#');
|
|
22
28
|
if (hashIndex < 0) return false;
|
|
23
29
|
|
|
24
|
-
const xywh =
|
|
30
|
+
const xywh = new RegExp(
|
|
31
|
+
`#xywh=((?:pixel|percent):)?(${number}),(${number}),(${number}),(${number})$`,
|
|
32
|
+
'i');
|
|
33
|
+
|
|
25
34
|
return xywh.test(selector);
|
|
26
35
|
}
|
|
27
36
|
|
|
@@ -35,8 +44,13 @@ export const parseFragmentSelector = (
|
|
|
35
44
|
const fragment =
|
|
36
45
|
typeof fragmentOrSelector === 'string' ? fragmentOrSelector : fragmentOrSelector.value;
|
|
37
46
|
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
if (fragment.length > MAX_FRAGMENT_LENGTH) throw new Error('Fragment too long: ' + fragment);
|
|
48
|
+
|
|
49
|
+
const regex = new RegExp(
|
|
50
|
+
`(xywh)=((?:pixel|percent))?:?(${number}),(${number}),(${number}),(${number})$`,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const matches = regex.exec(fragment);
|
|
40
54
|
|
|
41
55
|
if (!matches) throw new Error('Not a MediaFragment: ' + fragment);
|
|
42
56
|
|
package/src/model/w3c/svg/SVG.ts
CHANGED
|
@@ -37,7 +37,13 @@ export const insertSVGNamespace = (originalDoc: Document): Element => {
|
|
|
37
37
|
export const parseSVGXML = (value: string): Element => {
|
|
38
38
|
const parser = new DOMParser();
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
// Parser assumes an <svg /> root element, but W3C spec also
|
|
41
|
+
// allows just the shape element, without SVG doc.
|
|
42
|
+
const wrapped = value.trimStart().startsWith('<svg')
|
|
43
|
+
? value
|
|
44
|
+
: `<svg xmlns="${SVG_NAMESPACE}">${value}</svg>`;
|
|
45
|
+
|
|
46
|
+
const doc = parser.parseFromString(wrapped, 'image/svg+xml');
|
|
41
47
|
|
|
42
48
|
// SVG needs a namespace declaration - check if it's set or insert if not
|
|
43
49
|
const isPrefixDeclared = doc.lookupPrefix(SVG_NAMESPACE); // SVG declared via prefix
|
|
@@ -163,7 +163,6 @@ const parsePathCommands = (d: string) => {
|
|
|
163
163
|
const commands: PathCommand[] = [];
|
|
164
164
|
const cleanPath = d.replace(/,/g, ' ').trim();
|
|
165
165
|
|
|
166
|
-
// Updated regex to include H and V commands
|
|
167
166
|
const commandRegex = /([MmLlHhVvCcZz])\s*([^MmLlHhVvCcZz]*)/g;
|
|
168
167
|
let match;
|
|
169
168
|
|
|
@@ -171,12 +170,33 @@ const parsePathCommands = (d: string) => {
|
|
|
171
170
|
const [, commandLetter, argsString] = match;
|
|
172
171
|
const args = argsString.trim() === '' ? [] :
|
|
173
172
|
argsString.trim().split(/\s+/).map(Number).filter(n => !isNaN(n));
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
173
|
+
|
|
174
|
+
const step = argStep(commandLetter);
|
|
175
|
+
|
|
176
|
+
if (step === 0 || args.length <= step) {
|
|
177
|
+
// Z or single-instance command
|
|
178
|
+
commands.push({ type: commandLetter, args });
|
|
179
|
+
} else {
|
|
180
|
+
// Repeated commands
|
|
181
|
+
for (let i = 0; i < args.length; i += step) {
|
|
182
|
+
const impliedType = i === 0 ? commandLetter
|
|
183
|
+
: commandLetter === 'M' ? 'L'
|
|
184
|
+
: commandLetter === 'm' ? 'l'
|
|
185
|
+
: commandLetter;
|
|
186
|
+
commands.push({ type: impliedType, args: args.slice(i, i + step) });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
179
189
|
}
|
|
180
190
|
|
|
181
191
|
return commands;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const argStep = (cmd: string): number => {
|
|
195
|
+
switch (cmd.toUpperCase()) {
|
|
196
|
+
case 'M': case 'L': return 2;
|
|
197
|
+
case 'H': case 'V': return 1;
|
|
198
|
+
case 'C': return 6;
|
|
199
|
+
case 'Z': return 0;
|
|
200
|
+
default: return 2;
|
|
201
|
+
}
|
|
182
202
|
}
|