imagescript 3.2.1 → 3.4.0

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 (101) hide show
  1. package/README.md +35 -10
  2. package/THIRD_PARTY_NOTICES.md +298 -2
  3. package/dist/animation.d.ts +25 -8
  4. package/dist/animation.js +44 -13
  5. package/dist/codecs/av1/cdef.js +197 -0
  6. package/dist/codecs/av1/data.js +141 -0
  7. package/dist/codecs/av1/decode.js +1769 -0
  8. package/dist/codecs/av1/filmgrain.js +219 -0
  9. package/dist/codecs/av1/frame.js +114 -0
  10. package/dist/codecs/av1/ipred.js +491 -0
  11. package/dist/codecs/av1/itx.js +950 -0
  12. package/dist/codecs/av1/loopfilter.js +149 -0
  13. package/dist/codecs/av1/msac.js +161 -0
  14. package/dist/codecs/av1/obu.js +516 -0
  15. package/dist/codecs/av1/postfilter.js +28 -0
  16. package/dist/codecs/av1/restoration.js +193 -0
  17. package/dist/codecs/av1/tables.js +160 -0
  18. package/dist/codecs/avif.js +581 -0
  19. package/dist/codecs/bmp.js +317 -0
  20. package/dist/codecs/crc.js +5 -0
  21. package/dist/codecs/gif.js +10 -35
  22. package/dist/codecs/hdr.d.ts +25 -0
  23. package/dist/codecs/hdr.js +201 -0
  24. package/dist/codecs/ico.js +48 -0
  25. package/dist/codecs/jpeg.js +11 -28
  26. package/dist/codecs/jxl/bits.js +126 -0
  27. package/dist/codecs/jxl/blend.js +117 -0
  28. package/dist/codecs/jxl/color.js +116 -0
  29. package/dist/codecs/jxl/dct.js +404 -0
  30. package/dist/codecs/jxl/entropy.js +556 -0
  31. package/dist/codecs/jxl/features.js +325 -0
  32. package/dist/codecs/jxl/filters.js +119 -0
  33. package/dist/codecs/jxl/frame-header.js +195 -0
  34. package/dist/codecs/jxl/frame.js +319 -0
  35. package/dist/codecs/jxl/headers.js +249 -0
  36. package/dist/codecs/jxl/idct.js +484 -0
  37. package/dist/codecs/jxl/modular.js +688 -0
  38. package/dist/codecs/jxl/quant.js +219 -0
  39. package/dist/codecs/jxl/upsample.js +52 -0
  40. package/dist/codecs/jxl/vardct.js +472 -0
  41. package/dist/codecs/jxl.js +89 -0
  42. package/dist/codecs/netpbm.js +239 -0
  43. package/dist/codecs/png.js +22 -10
  44. package/dist/codecs/qoi.js +150 -0
  45. package/dist/codecs/tga.js +3 -1
  46. package/dist/codecs/tiff.js +7 -2
  47. package/dist/codecs/vp8-enc-dsp.js +932 -0
  48. package/dist/codecs/vp8-enc-tables.js +78 -0
  49. package/dist/codecs/vp8-enc.js +1930 -0
  50. package/dist/codecs/vp8.js +3 -1
  51. package/dist/codecs/vp8l.js +126 -58
  52. package/dist/codecs/webp.js +47 -11
  53. package/dist/codecs/zlib.js +81 -69
  54. package/dist/frame.d.ts +1 -0
  55. package/dist/frame.js +4 -2
  56. package/dist/gif.d.ts +1 -0
  57. package/dist/gif.js +2 -2
  58. package/dist/image-type.d.ts +15 -1
  59. package/dist/image-type.js +45 -2
  60. package/dist/image.d.ts +89 -13
  61. package/dist/image.js +266 -113
  62. package/dist/index.d.ts +3 -2
  63. package/dist/index.js +4 -4
  64. package/dist/render/bidi.js +350 -0
  65. package/dist/render/buffer.js +362 -0
  66. package/dist/render/color.js +164 -0
  67. package/dist/render/colr.js +523 -0
  68. package/dist/render/font.js +173 -241
  69. package/dist/render/hinting.js +38 -22
  70. package/dist/render/layout.js +1553 -0
  71. package/dist/render/linebreak.js +210 -0
  72. package/dist/render/segment.js +106 -0
  73. package/dist/render/shape.js +984 -0
  74. package/dist/render/shapers/arabic.js +284 -0
  75. package/dist/render/shapers/hangul.js +142 -0
  76. package/dist/render/shapers/hebrew.js +49 -0
  77. package/dist/render/shapers/index.js +52 -0
  78. package/dist/render/shapers/indic.js +579 -0
  79. package/dist/render/shapers/khmer.js +104 -0
  80. package/dist/render/shapers/machine.js +129 -0
  81. package/dist/render/shapers/myanmar.js +121 -0
  82. package/dist/render/shapers/syllabic.js +100 -0
  83. package/dist/render/shapers/thai.js +125 -0
  84. package/dist/render/shapers/use.js +235 -0
  85. package/dist/render/svg.js +102 -23
  86. package/dist/render/text.js +373 -103
  87. package/dist/render/unicode-data.js +63 -0
  88. package/dist/render/unicode.js +50 -0
  89. package/dist/text-layout.d.ts +4 -1
  90. package/dist/text-layout.js +2 -2
  91. package/dist/util/alpha.js +43 -0
  92. package/dist/util/blur.js +20 -33
  93. package/dist/util/bytes.js +40 -0
  94. package/dist/util/limits.js +34 -0
  95. package/dist/util/options.js +7 -0
  96. package/dist/util/resample.js +126 -7
  97. package/dist/util/rotate.js +43 -0
  98. package/dist/version.js +1 -1
  99. package/licenses/HarfBuzz.txt +42 -0
  100. package/licenses/Unicode-3.0.txt +39 -0
  101. package/package.json +25 -2
@@ -0,0 +1,235 @@
1
+ /*!
2
+ * The Universal Shaping Engine, ported from HarfBuzz (hb-ot-shaper-use.cc, hb-ot-shaper-use-machine.rl)
3
+ * Copyright © 2015 Mozilla Foundation, © 2015 Google, Inc., Old MIT license (see THIRD_PARTY_NOTICES.md and
4
+ * licenses/HarfBuzz.txt)
5
+ */
6
+ import { isLigated, isSubstituted, ligComp, isUnicodeMark, isMark } from "../buffer.js";
7
+ import { compose, unicodeCategory } from "../shape.js";
8
+ import { useCategory } from "../unicode-data.js";
9
+ import { value } from "../unicode.js";
10
+ import { lazyScanner, cat, seq, alt, opt, star, plus, any } from "./machine.js";
11
+ import { forEachSyllable, insertDottedCircles, vowelConstraintsPreprocess } from "./syllabic.js";
12
+ import { dataCreate as arabicDataCreate, setupMasksArabic } from "./arabic.js";
13
+ const CATEGORIES = {
14
+ O: 0, B: 1, N: 4, GB: 5, CGJ: 6, SUB: 11, H: 12, HN: 13, ZWNJ: 14, WJ: 16, R: 18, CS: 43, IS: 44, Sk: 48, G: 49,
15
+ J: 50, SB: 51, SE: 52, HVM: 53, HM: 54, HR: 55, RK: 56, FAbv: 24, FBlw: 25, FPst: 26, MAbv: 27, MBlw: 28,
16
+ MPst: 29, MPre: 30, CMAbv: 31, CMBlw: 32, VAbv: 33, VBlw: 34, VPst: 35, VPre: 22, VMAbv: 37, VMBlw: 38,
17
+ VMPst: 39, VMPre: 23, SMAbv: 41, SMBlw: 42, FMAbv: 45, FMBlw: 46, FMPst: 47,
18
+ };
19
+ const findSyllables = lazyScanner(() => {
20
+ const s = (...c) => star(cat(...c));
21
+ const h = cat(12, 53, 44, 48);
22
+ const consonantModifiers = seq(s(31), s(32), star(alt(seq(h, cat(1)), cat(11)), s(31), s(32)));
23
+ const medialConsonants = seq(opt(cat(30)), opt(cat(27)), opt(cat(28)), opt(cat(29)));
24
+ const dependentVowels = alt(seq(s(22), s(33), s(34), s(35)), cat(12));
25
+ const vowelModifiers = seq(opt(cat(53)), s(23), s(37), s(38), s(39));
26
+ const finalConsonants = seq(s(24), s(25), s(26));
27
+ const finalModifiers = alt(seq(s(45), s(46)), opt(cat(47)));
28
+ const complexSyllableStart = seq(opt(cat(18, 43)), cat(1, 5));
29
+ const complexSyllableMiddle = seq(consonantModifiers, medialConsonants, dependentVowels, vowelModifiers, star(cat(48), cat(1)));
30
+ const complexSyllableTail = seq(complexSyllableMiddle, finalConsonants, finalModifiers);
31
+ const numberJoinerTerminatedClusterTail = seq(star(cat(13), cat(4)), cat(13));
32
+ const numeralClusterTail = plus(cat(13), cat(4));
33
+ const symbolClusterTail = alt(seq(plus(cat(41)), s(42)), plus(cat(42)));
34
+ const viramaTerminatedClusterTail = seq(consonantModifiers, cat(44, 56));
35
+ const viramaTerminatedCluster = seq(complexSyllableStart, viramaTerminatedClusterTail);
36
+ const sakotTerminatedClusterTail = seq(complexSyllableMiddle, cat(48));
37
+ const sakotTerminatedCluster = seq(complexSyllableStart, sakotTerminatedClusterTail);
38
+ const standardCluster = seq(complexSyllableStart, complexSyllableTail);
39
+ const tail = alt(complexSyllableTail, sakotTerminatedClusterTail, symbolClusterTail, viramaTerminatedClusterTail);
40
+ const brokenCluster = seq(opt(cat(18)), alt(tail, numberJoinerTerminatedClusterTail, numeralClusterTail));
41
+ const numberJoinerTerminatedCluster = seq(cat(4), numberJoinerTerminatedClusterTail);
42
+ const numeralCluster = seq(cat(4), opt(numeralClusterTail));
43
+ const symbolCluster = seq(cat(0, 5, 51), opt(tail));
44
+ const g = seq(cat(49), opt(cat(55)), opt(cat(54)), s(52));
45
+ const hieroglyphCluster = seq(s(51), g, star(cat(50), s(51), opt(g)));
46
+ const zwnj = opt(cat(14));
47
+ return [
48
+ [seq(viramaTerminatedCluster, zwnj), 0],
49
+ [seq(sakotTerminatedCluster, zwnj), 1],
50
+ [seq(standardCluster, zwnj), 2],
51
+ [seq(numberJoinerTerminatedCluster, zwnj), 3],
52
+ [seq(numeralCluster, zwnj), 4],
53
+ [seq(symbolCluster, zwnj), 5],
54
+ [seq(hieroglyphCluster, zwnj), 6],
55
+ [cat(47), 8],
56
+ [seq(brokenCluster, zwnj), 7],
57
+ [any, 8],
58
+ ];
59
+ });
60
+ function setupSyllables(plan, _font, buffer) {
61
+ const info = buffer.info, kept = [];
62
+ for (let i = 0; i < info.length; i++) {
63
+ if (info[i].category === 6)
64
+ continue;
65
+ if (info[i].category === 14) {
66
+ let j = i + 1;
67
+ while (j < info.length && info[j].category === 6)
68
+ j++;
69
+ if (j < info.length && isUnicodeMark(info[j]))
70
+ continue;
71
+ }
72
+ kept.push(i);
73
+ }
74
+ let serial = 1;
75
+ for (const [start, end, type] of findSyllables(kept.map(i => info[i].category))) {
76
+ const from = kept[start], to = end < kept.length ? kept[end] : info.length;
77
+ for (let i = from; i < to; i++)
78
+ info[i].syllable = serial << 4 | type;
79
+ if (type === 7)
80
+ buffer.hasBrokenSyllable = true;
81
+ if (++serial === 16)
82
+ serial = 1;
83
+ }
84
+ setupRphfMask(plan, buffer);
85
+ setupTopographicalMasks(plan, buffer);
86
+ }
87
+ function setupRphfMask(plan, buffer) {
88
+ const mask = plan.data.rphfMask;
89
+ if (!mask)
90
+ return;
91
+ const info = buffer.info;
92
+ forEachSyllable(buffer, (start, end) => {
93
+ const limit = info[start].category === 18 ? 1 : Math.min(3, end - start);
94
+ for (let i = start; i < start + limit; i++)
95
+ info[i].mask |= mask;
96
+ });
97
+ }
98
+ const TOPOGRAPHICAL = ['isol', 'init', 'medi', 'fina'];
99
+ function setupTopographicalMasks(plan, buffer) {
100
+ if (plan.data.arabic)
101
+ return;
102
+ const masks = TOPOGRAPHICAL.map(tag => {
103
+ const m = plan.map.get1Mask(tag);
104
+ return m === plan.map.globalMask ? 0 : m;
105
+ });
106
+ const all = masks.reduce((a, b) => a | b, 0);
107
+ if (!all)
108
+ return;
109
+ const others = ~all, info = buffer.info;
110
+ let lastStart = 0, lastForm = 4;
111
+ forEachSyllable(buffer, (start, end) => {
112
+ const type = info[start].syllable & 0x0f;
113
+ if (type === 6 || type === 8)
114
+ lastForm = 4;
115
+ else {
116
+ const join = lastForm === 3 || lastForm === 0;
117
+ if (join) {
118
+ lastForm = lastForm === 3 ? 2 : 1;
119
+ for (let i = lastStart; i < start; i++)
120
+ info[i].mask = info[i].mask & others | masks[lastForm];
121
+ }
122
+ lastForm = join ? 3 : 0;
123
+ for (let i = start; i < end; i++)
124
+ info[i].mask = info[i].mask & others | masks[lastForm];
125
+ }
126
+ lastStart = start;
127
+ });
128
+ }
129
+ function clearSubstitutionFlags(_plan, _font, buffer) {
130
+ for (const info of buffer.info)
131
+ info.glyphProps &= ~16;
132
+ }
133
+ function recordRphf(plan, _font, buffer) {
134
+ const mask = plan.data.rphfMask;
135
+ if (!mask)
136
+ return;
137
+ const info = buffer.info;
138
+ forEachSyllable(buffer, (start, end) => {
139
+ for (let i = start; i < end && info[i].mask & mask; i++)
140
+ if (isSubstituted(info[i])) {
141
+ info[i].category = 18;
142
+ break;
143
+ }
144
+ });
145
+ }
146
+ function recordPref(_plan, _font, buffer) {
147
+ const info = buffer.info;
148
+ forEachSyllable(buffer, (start, end) => {
149
+ for (let i = start; i < end; i++)
150
+ if (isSubstituted(info[i])) {
151
+ info[i].category = 22;
152
+ break;
153
+ }
154
+ });
155
+ }
156
+ const isHalant = (info) => (info.category === 12 || info.category === 53 || info.category === 44) && !isLigated(info);
157
+ const POST_BASE = new Set([24, 25, 26, 45, 46, 47, 27, 28, 29, 30, 33, 34, 35, 22, 37,
158
+ 38, 39, 23]);
159
+ const REORDERED = new Set([0, 1, 2, 5, 7]);
160
+ function reorderSyllable(buffer, start, end) {
161
+ const info = buffer.info;
162
+ if (!REORDERED.has(info[start].syllable & 0x0f))
163
+ return;
164
+ if (info[start].category === 18 && end - start > 1)
165
+ for (let i = start + 1; i < end; i++) {
166
+ const postBase = POST_BASE.has(info[i].category) || isHalant(info[i]);
167
+ if (postBase || i === end - 1) {
168
+ if (postBase)
169
+ i--;
170
+ buffer.mergeClusters(start, i + 1);
171
+ const t = info[start];
172
+ for (let k = start; k < i; k++)
173
+ info[k] = info[k + 1];
174
+ info[i] = t;
175
+ break;
176
+ }
177
+ }
178
+ let j = start;
179
+ for (let i = start; i < end; i++) {
180
+ const category = info[i].category;
181
+ if (isHalant(info[i]))
182
+ j = i + 1;
183
+ else if ((category === 22 || category === 23) && ligComp(info[i]) === 0 && j < i) {
184
+ buffer.mergeClusters(j, i + 1);
185
+ const t = info[i];
186
+ for (let k = i; k > j; k--)
187
+ info[k] = info[k - 1];
188
+ info[j] = t;
189
+ }
190
+ }
191
+ }
192
+ function reorder(_plan, font, buffer) {
193
+ insertDottedCircles(font, buffer, 7, 1, 18, -1);
194
+ forEachSyllable(buffer, (start, end) => reorderSyllable(buffer, start, end));
195
+ }
196
+ function collectFeatures(planner) {
197
+ const map = planner.map;
198
+ map.addGsubPause(setupSyllables);
199
+ map.enableFeature('locl', 64);
200
+ map.enableFeature('ccmp', 64);
201
+ map.enableFeature('nukt', 64);
202
+ map.enableFeature('akhn', 8 | 64);
203
+ map.addGsubPause(clearSubstitutionFlags);
204
+ map.addFeature('rphf', 8 | 64);
205
+ map.addGsubPause(recordRphf);
206
+ map.addGsubPause(clearSubstitutionFlags);
207
+ map.enableFeature('pref', 8 | 64);
208
+ map.addGsubPause(recordPref);
209
+ for (const tag of ['rkrf', 'abvf', 'blwf', 'half', 'pstf', 'vatu', 'cjct'])
210
+ map.enableFeature(tag, 8 | 64);
211
+ map.addGsubPause(reorder);
212
+ map.addGsubPause(null);
213
+ for (const tag of TOPOGRAPHICAL)
214
+ map.addFeature(tag);
215
+ map.addGsubPause(null);
216
+ for (const tag of ['abvs', 'blws', 'haln', 'pres', 'psts'])
217
+ map.enableFeature(tag, 8);
218
+ }
219
+ const ARABIC_JOINING = new Set(['Adlm', 'Arab', 'Chrs', 'Rohg', 'Mand', 'Mani', 'Mong', 'Nkoo', 'Ougr', 'Phag', 'Phlp', 'Sogd', 'Syrc']);
220
+ export const useShaper = {
221
+ collectFeatures,
222
+ dataCreate: (plan) => ({ rphfMask: plan.map.get1Mask('rphf'), arabic: ARABIC_JOINING.has(plan.script) ? arabicDataCreate(plan) : null }),
223
+ preprocessText: (_plan, buffer) => vowelConstraintsPreprocess(buffer),
224
+ compose: (_c, a, b) => isMark(unicodeCategory(a)) ? 0 : compose(a, b),
225
+ setupMasks: (plan, buffer) => {
226
+ const use = plan.data;
227
+ if (use.arabic)
228
+ setupMasksArabic(use.arabic, buffer, plan.script);
229
+ for (const info of buffer.info)
230
+ info.category = CATEGORIES[value(useCategory, info.codepoint)];
231
+ },
232
+ normalization: 3,
233
+ zeroWidthMarks: 1,
234
+ fallbackPosition: false,
235
+ };
@@ -173,16 +173,76 @@ function parseXML(text) {
173
173
  }
174
174
  return root;
175
175
  }
176
+ function urlReference(value) {
177
+ let close = -1;
178
+ for (let open = value.indexOf('url('); open >= 0; open = value.indexOf('url(', open + 4)) {
179
+ if (close < open && (close = value.indexOf(')', open)) < 0)
180
+ return null;
181
+ let inner = value.slice(open + 4, close).trim();
182
+ if (inner.startsWith('"') || inner.startsWith("'"))
183
+ inner = inner.slice(1);
184
+ if (inner.endsWith('"') || inner.endsWith("'"))
185
+ inner = inner.slice(0, -1);
186
+ inner = inner.trim();
187
+ if (/^#[^'"\s]+$/.test(inner))
188
+ return inner.slice(1);
189
+ }
190
+ return null;
191
+ }
192
+ function stripComments(text) {
193
+ let out = '';
194
+ for (let i = 0;;) {
195
+ const start = text.indexOf('/*', i);
196
+ if (start < 0)
197
+ return out + text.slice(i);
198
+ out += text.slice(i, start);
199
+ const end = text.indexOf('*/', start + 2);
200
+ if (end < 0)
201
+ return out;
202
+ i = end + 2;
203
+ }
204
+ }
205
+ function blockEnd(text, open) {
206
+ let depth = 0;
207
+ for (let i = open; i < text.length; i++) {
208
+ if (text[i] === '{')
209
+ depth++;
210
+ else if (text[i] === '}' && !--depth)
211
+ return i;
212
+ }
213
+ return text.length;
214
+ }
176
215
  function parseStyleSheet(text) {
177
216
  const rules = [];
178
- text = text.replace(/\/\*[\s\S]*?\*\//g, '');
217
+ text = stripComments(text);
218
+ const cache = {};
219
+ const next = (char, i) => {
220
+ const cached = cache[char];
221
+ if (cached === undefined || cached !== -1 && cached < i)
222
+ cache[char] = text.indexOf(char, i);
223
+ return cache[char];
224
+ };
179
225
  let order = 0;
180
- const blocks = /([^{}@]+)\{([^{}]*)\}|@[^{;]*(\{(?:[^{}]*\{[^{}]*\})*[^{}]*\}|;)/g;
181
- for (const [, selectors, body] of text.matchAll(blocks)) {
182
- if (selectors === undefined)
226
+ for (let i = 0; i < text.length;) {
227
+ const start = i, open = next('{', i), close = next('}', i), at = next('@', i);
228
+ if (open < 0)
229
+ break;
230
+ if (close >= 0 && close < open) {
231
+ i = close + 1;
232
+ continue;
233
+ }
234
+ if (at >= 0 && at < open) {
235
+ const semicolon = next(';', at);
236
+ i = semicolon >= 0 && semicolon < open ? semicolon + 1 : blockEnd(text, open) + 1;
237
+ continue;
238
+ }
239
+ const end = blockEnd(text, open);
240
+ const body = text.slice(open + 1, end);
241
+ i = end + 1;
242
+ if (body.includes('{'))
183
243
  continue;
184
244
  const declarations = parseDeclarations(body);
185
- for (const selector of selectors.split(',')) {
245
+ for (const selector of text.slice(start, open).split(',')) {
186
246
  const parsed = parseSelector(selector.trim());
187
247
  if (parsed)
188
248
  rules.push({ ...parsed, order: order++, declarations });
@@ -279,7 +339,7 @@ function parseSelector(selector) {
279
339
  }
280
340
  function parseDeclarations(text) {
281
341
  const declarations = { normal: {}, important: {} };
282
- text = text.replace(/\/\*[\s\S]*?\*\//g, '');
342
+ text = stripComments(text);
283
343
  for (const part of text.split(';')) {
284
344
  const colon = part.indexOf(':');
285
345
  if (colon === -1)
@@ -312,7 +372,10 @@ function validProperty(name, value) {
312
372
  return true;
313
373
  }
314
374
  }
375
+ const MAX_INSTANCES = 100000;
376
+ const MAX_DASHES = 1000000;
315
377
  class Document {
378
+ instances;
316
379
  ids;
317
380
  active;
318
381
  root;
@@ -320,26 +383,28 @@ class Document {
320
383
  constructor(root) {
321
384
  this.ids = new Map();
322
385
  this.active = new Set();
386
+ this.instances = 0;
323
387
  const styles = [];
324
- const walk = (node) => {
388
+ const stack = [root];
389
+ while (stack.length) {
390
+ const node = stack.pop();
325
391
  if (node.attrs.id && !this.ids.has(node.attrs.id))
326
392
  this.ids.set(node.attrs.id, node);
327
393
  if (node.name === 'style' && node.svg && (!node.attrs.type || node.attrs.type === 'text/css'))
328
394
  styles.push(node.text);
329
- for (const child of node.children) {
330
- child.parent = node;
331
- walk(child);
395
+ for (let i = node.children.length - 1; i >= 0; i--) {
396
+ node.children[i].parent = node;
397
+ stack.push(node.children[i]);
332
398
  }
333
- };
334
- walk(root);
399
+ }
335
400
  this.root = root;
336
401
  this.rules = parseStyleSheet(styles.join('\n'));
337
402
  }
338
403
  reference(value) {
339
404
  if (!value)
340
405
  return null;
341
- const match = /url\(\s*['"]?\s*#([^)'"]+?)\s*['"]?\s*\)/.exec(value) || /^\s*#(.+?)\s*$/.exec(value);
342
- return match ? this.ids.get(match[1]) || null : null;
406
+ const id = urlReference(value) ?? (value.trim().startsWith('#') ? value.trim().slice(1) : null);
407
+ return id ? this.ids.get(id) || null : null;
343
408
  }
344
409
  href(node) {
345
410
  return node.attrs.href || node.attrs['xlink:href'];
@@ -356,7 +421,7 @@ class Document {
356
421
  const presentation = {};
357
422
  for (const name in node.attrs)
358
423
  if (PROPERTIES.has(name))
359
- presentation[name] = node.attrs[name];
424
+ presentation[name] = node.attrs[name].trim();
360
425
  apply(presentation);
361
426
  const rules = this.rules.filter(rule => rule.match(node));
362
427
  const inline = node.attrs.style ? parseDeclarations(node.attrs.style) : null;
@@ -406,6 +471,8 @@ class Document {
406
471
  renderNode(node, context, root = false) {
407
472
  if (!node.svg || UNSUPPORTED.has(node.name) || context.depth > 32 || !this.passesConditions(node))
408
473
  return;
474
+ if (context.instanced && ++this.instances > MAX_INSTANCES)
475
+ throw new Error(`SVG: more than ${MAX_INSTANCES} elements rendered through <use> references`);
409
476
  const style = this.computeStyle(node, context.style);
410
477
  if (style.display === 'none')
411
478
  return;
@@ -513,7 +580,7 @@ class Document {
513
580
  outer = multiply(outer, [1, 0, 0, 1, parseLength(target.attrs.x, ctx.viewport, 16, 'x'), parseLength(target.attrs.y, ctx.viewport, 16, 'y')]);
514
581
  const inner = viewBox ? multiply(outer, viewBoxTransform(viewBox, width, height, target.attrs.preserveAspectRatio)) : outer;
515
582
  const opacity = clamp01(parseNumber(style.opacity, 1));
516
- const innerContext = { ...ctx, matrix: inner, style, viewport: viewBox ? { width: viewBox[2], height: viewBox[3] } : { width, height } };
583
+ const innerContext = { ...ctx, matrix: inner, style, viewport: viewBox ? { width: viewBox[2], height: viewBox[3] } : { width, height }, instanced: true };
517
584
  const draw = (layer) => /^(visible|auto)$/.test(target.attrs.overflow || '')
518
585
  ? this.renderChildren(target, { ...innerContext, layer })
519
586
  : this.clipped({ ...ctx, matrix: outer, layer }, 0, 0, width, height, inner => this.renderChildren(target, { ...innerContext, layer: inner }));
@@ -526,7 +593,7 @@ class Document {
526
593
  }
527
594
  const computed = target.computed, computedParent = target.computedParent;
528
595
  target.computed = null;
529
- this.renderNode(target, { ...ctx, matrix });
596
+ this.renderNode(target, { ...ctx, matrix, instanced: true });
530
597
  target.computed = computed;
531
598
  target.computedParent = computedParent;
532
599
  }
@@ -1142,9 +1209,8 @@ function shapePath(node, viewport, fontSize) {
1142
1209
  }
1143
1210
  function parsePath(d) {
1144
1211
  const path = [];
1145
- const tokens = /([MmZzLlHhVvCcSsQqTtAa])|([-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?)/g;
1146
1212
  const items = [];
1147
- for (const m of d.matchAll(tokens))
1213
+ for (const m of d.matchAll(PATH_TOKENS))
1148
1214
  items.push(m[1] ? { command: m[1] } : m[2]);
1149
1215
  let i = 0;
1150
1216
  let command = null;
@@ -1398,6 +1464,14 @@ function transformPoints(points, m) {
1398
1464
  function dash(subpaths, pattern, offset) {
1399
1465
  const total = pattern.reduce((a, b) => a + b, 0);
1400
1466
  const result = [];
1467
+ let length = 0;
1468
+ for (const { points, closed } of subpaths) {
1469
+ const pts = closed ? [...points, points[0], points[1]] : points;
1470
+ for (let i = 0; i + 3 < pts.length; i += 2)
1471
+ length += Math.hypot(pts[i + 2] - pts[i], pts[i + 3] - pts[i + 1]);
1472
+ }
1473
+ if (!(length / total * pattern.length <= MAX_DASHES))
1474
+ return subpaths;
1401
1475
  for (const { points, closed } of subpaths) {
1402
1476
  const pts = closed ? [...points, points[0], points[1]] : points;
1403
1477
  let index = 0;
@@ -1620,10 +1694,15 @@ function viewBoxTransform([vx, vy, vw, vh], width, height, preserve = 'xMidYMid
1620
1694
  return [sx, 0, 0, sy, tx, ty];
1621
1695
  }
1622
1696
  const UNITS = { px: 1, pt: 4 / 3, pc: 16, mm: 96 / 25.4, cm: 96 / 2.54, in: 96 };
1697
+ const NUMBER = String.raw `[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?`;
1698
+ const LENGTH = new RegExp(`^(${NUMBER})\\s*(px|pt|pc|mm|cm|in|em|ex|%)?$`);
1699
+ const NUMBER_OR_PERCENTAGE = new RegExp(`^(${NUMBER})\\s*(%?)$`);
1700
+ const NUMBERS = new RegExp(NUMBER, 'g');
1701
+ const PATH_TOKENS = new RegExp(`([MmZzLlHhVvCcSsQqTtAa])|(${NUMBER})`, 'g');
1623
1702
  function parseLength(value, viewport, fontSize = 16, axis = 'x') {
1624
1703
  if (value === undefined || value === null)
1625
1704
  return 0;
1626
- const match = /^\s*([-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?)\s*(px|pt|pc|mm|cm|in|em|ex|%)?\s*$/.exec(String(value));
1705
+ const match = LENGTH.exec(String(value).trim());
1627
1706
  if (!match)
1628
1707
  return 0;
1629
1708
  const number = parseFloat(match[1]);
@@ -1650,13 +1729,13 @@ function parseNumber(value, fallback) {
1650
1729
  function parseStrictNumber(value) {
1651
1730
  if (value === undefined || value === null)
1652
1731
  return null;
1653
- const match = /^\s*([-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?)\s*(%?)\s*$/.exec(String(value));
1732
+ const match = NUMBER_OR_PERCENTAGE.exec(String(value).trim());
1654
1733
  if (!match)
1655
1734
  return null;
1656
1735
  return match[2] ? parseFloat(match[1]) / 100 : parseFloat(match[1]);
1657
1736
  }
1658
1737
  function parseNumberList(value) {
1659
- return [...String(value).matchAll(/[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?/g)].map(m => parseFloat(m[0]));
1738
+ return [...String(value).matchAll(NUMBERS)].map(m => parseFloat(m[0]));
1660
1739
  }
1661
1740
  function parseDashArray(value, viewport, fontSize) {
1662
1741
  if (!value || value === 'none')
@@ -1686,7 +1765,7 @@ function parseColor(value) {
1686
1765
  return [0, 2, 4, 6].slice(0, hex.length / 2).map(i => parseInt(hex.slice(i, i + 2), 16) / 255).concat(hex.length === 6 ? [1] : []);
1687
1766
  return null;
1688
1767
  }
1689
- match = /^(rgba?|hsla?)\(\s*([^)]*)\)$/.exec(value);
1768
+ match = /^(rgba?|hsla?)\(([^)]*)\)$/.exec(value);
1690
1769
  if (match) {
1691
1770
  const parts = match[2].split(/\s*[,/]\s*|\s+/).filter(Boolean);
1692
1771
  if (parts.length < 3)