imagescript 3.2.0 → 3.3.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 (59) hide show
  1. package/README.md +8 -3
  2. package/THIRD_PARTY_NOTICES.md +131 -0
  3. package/dist/animation.d.ts +7 -0
  4. package/dist/animation.js +22 -12
  5. package/dist/codecs/crc.js +5 -0
  6. package/dist/codecs/gif.js +30 -52
  7. package/dist/codecs/jpeg.js +9 -27
  8. package/dist/codecs/png.js +22 -10
  9. package/dist/codecs/tga.js +3 -1
  10. package/dist/codecs/tiff.js +7 -2
  11. package/dist/codecs/vp8.js +3 -1
  12. package/dist/codecs/vp8l.js +126 -58
  13. package/dist/codecs/webp.js +11 -9
  14. package/dist/codecs/zlib.js +81 -69
  15. package/dist/frame.d.ts +1 -0
  16. package/dist/frame.js +4 -2
  17. package/dist/gif.d.ts +1 -0
  18. package/dist/gif.js +2 -2
  19. package/dist/image.d.ts +42 -7
  20. package/dist/image.js +224 -113
  21. package/dist/index.js +4 -4
  22. package/dist/render/bidi.js +350 -0
  23. package/dist/render/buffer.js +362 -0
  24. package/dist/render/color.js +164 -0
  25. package/dist/render/colr.js +523 -0
  26. package/dist/render/font.js +173 -241
  27. package/dist/render/hinting.js +12 -4
  28. package/dist/render/layout.js +1553 -0
  29. package/dist/render/linebreak.js +210 -0
  30. package/dist/render/segment.js +106 -0
  31. package/dist/render/shape.js +984 -0
  32. package/dist/render/shapers/arabic.js +284 -0
  33. package/dist/render/shapers/hangul.js +142 -0
  34. package/dist/render/shapers/hebrew.js +49 -0
  35. package/dist/render/shapers/index.js +52 -0
  36. package/dist/render/shapers/indic.js +579 -0
  37. package/dist/render/shapers/khmer.js +104 -0
  38. package/dist/render/shapers/machine.js +129 -0
  39. package/dist/render/shapers/myanmar.js +121 -0
  40. package/dist/render/shapers/syllabic.js +100 -0
  41. package/dist/render/shapers/thai.js +125 -0
  42. package/dist/render/shapers/use.js +235 -0
  43. package/dist/render/svg.js +101 -22
  44. package/dist/render/text.js +373 -103
  45. package/dist/render/unicode-data.js +63 -0
  46. package/dist/render/unicode.js +58 -0
  47. package/dist/text-layout.d.ts +4 -1
  48. package/dist/text-layout.js +2 -2
  49. package/dist/util/alpha.js +43 -0
  50. package/dist/util/blur.js +80 -5
  51. package/dist/util/bytes.js +31 -0
  52. package/dist/util/limits.js +34 -0
  53. package/dist/util/options.js +7 -0
  54. package/dist/util/resample.js +359 -79
  55. package/dist/util/rotate.js +43 -0
  56. package/dist/version.js +1 -1
  57. package/licenses/HarfBuzz.txt +42 -0
  58. package/licenses/Unicode-3.0.txt +39 -0
  59. package/package.json +1 -1
@@ -4,9 +4,22 @@
4
4
  */
5
5
  import { Font } from "./font.js";
6
6
  import { Hinter } from "./hinting.js";
7
+ import { parseColorLayers, parseBitmaps } from "./color.js";
8
+ import { shapeFont, shapeRun, isDefaultIgnorableCodepoint } from "./shape.js";
9
+ import "./shapers/index.js";
10
+ import { classOf, resolve, lineLevels } from "./bidi.js";
11
+ import { lineBreaks } from "./linebreak.js";
12
+ import { graphemeBoundaries, scripts } from "./segment.js";
13
+ import { parsePaintGlyphs, paintExtent, renderPaintGlyph } from "./colr.js";
14
+ import { decode as decodePNG } from "../codecs/png.js";
15
+ import { resample, TRIANGLE } from "../util/resample.js";
7
16
  const SUBPIXELS = 4;
17
+ const MAX_GLYPH_PIXELS = 1 << 26;
8
18
  const fonts = new WeakMap();
9
19
  const hinters = new WeakMap();
20
+ const colorTables = new WeakMap();
21
+ const sizes = new WeakMap();
22
+ const SIZES = 16;
10
23
  function loadFont(data) {
11
24
  let regions = fonts.get(data.buffer);
12
25
  if (!regions)
@@ -17,9 +30,15 @@ function loadFont(data) {
17
30
  regions.set(key, font = new Font(data));
18
31
  return font;
19
32
  }
20
- export function render(data, size, text, layout, hinting = false) {
33
+ function sizedFont(data, size, hinting) {
21
34
  const font = loadFont(data);
22
- const scale = size / font.unitsPerEm;
35
+ let bySize = sizes.get(font);
36
+ if (!bySize)
37
+ sizes.set(font, bySize = new Map());
38
+ const key = `${size}:${hinting}`;
39
+ let sized = bySize.get(key);
40
+ if (sized)
41
+ return sized;
23
42
  let hinter = null;
24
43
  if (hinting && font.outlines === 'glyf') {
25
44
  hinter = hinters.get(font) ?? null;
@@ -28,32 +47,72 @@ export function render(data, size, text, layout, hinting = false) {
28
47
  if (!hinter.hasInstructions)
29
48
  hinter = null;
30
49
  }
31
- hinting = hinter !== null;
32
- const ascent = Math.ceil(font.ascender * scale);
33
- const lineHeight = Math.ceil((font.ascender - font.descender + font.lineGap) * scale);
34
- const glyphs = new Map();
35
- const glyph = (codepoint) => {
36
- let g = glyphs.get(codepoint);
37
- if (!g) {
38
- const index = font.glyphIndex(codepoint);
39
- const hinted = hinter && hinter.glyph(index, size);
40
- if (hinted)
41
- g = { index, path: hinted.path, scale: 1, advance: hinted.advance, ink: inkExtent(hinted.path, 1), bitmaps: [] };
42
- else {
43
- const path = font.outline(index);
44
- const advance = font.advance(index) * scale;
45
- g = { index, path, scale, advance: hinting ? Math.round(advance) : advance, ink: inkExtent(path, scale), bitmaps: [] };
46
- }
47
- glyphs.set(codepoint, g);
48
- }
50
+ let shared = colorTables.get(font);
51
+ if (!shared)
52
+ colorTables.set(font, shared = { layers: parseColorLayers(font), paints: parsePaintGlyphs(font) });
53
+ if (bySize.size >= SIZES)
54
+ bySize.clear();
55
+ bySize.set(key, sized = { font, scale: size / font.unitsPerEm, hinter, layers: shared.layers, bitmaps: parseBitmaps(font, size), paints: shared.paints, shaping: shapeFont(font), glyphs: new Map() });
56
+ return sized;
57
+ }
58
+ function glyphOf(f, index, size) {
59
+ let g = f.glyphs.get(index);
60
+ if (g)
49
61
  return g;
50
- };
51
- const kerning = font.kerning;
52
- const kern = kerning ? (left, right) => {
53
- const k = kerning(left.index, right.index) * scale;
54
- return hinting ? Math.round(k) : k;
55
- } : () => 0;
56
- const lines = wrap(text, layout, glyph, kern);
62
+ const { font, scale, hinter } = f;
63
+ const paint = f.paints !== null && f.paints.paint(index) !== null;
64
+ const bitmap = paint ? null : f.bitmaps?.(index) ?? null, layers = paint || bitmap ? null : f.layers?.(index) ?? null;
65
+ const advance = font.advance(index) * scale;
66
+ g = { index, font: f, path: [], scale, advance: hinter ? Math.round(advance) : advance, ink: null, bitmaps: [], color: null, colors: new Map() };
67
+ if (paint) {
68
+ g.color = { paint: true };
69
+ const extent = paintExtent(font, f.paints, index, glyph => font.outline(glyph));
70
+ g.ink = extent && { left: extent.left * scale, right: extent.right * scale };
71
+ }
72
+ else if (bitmap) {
73
+ const s = size / bitmap.ppem;
74
+ g.color = { bitmap };
75
+ g.ink = { left: bitmap.left * s, right: (bitmap.left + bitmap.width) * s };
76
+ }
77
+ else if (layers) {
78
+ g.color = { layers };
79
+ g.ink = inkExtent(layers.flatMap(layer => font.outline(layer.glyph)), scale);
80
+ }
81
+ else {
82
+ const hinted = hinter && hinter.glyph(index, size);
83
+ if (hinted)
84
+ Object.assign(g, { path: hinted.path, scale: 1, advance: hinted.advance, ink: inkExtent(hinted.path, 1) });
85
+ else {
86
+ g.path = font.outline(index);
87
+ g.ink = inkExtent(g.path, scale);
88
+ }
89
+ }
90
+ f.glyphs.set(index, g);
91
+ return g;
92
+ }
93
+ export async function render(fonts, size, text, layout, hinting = false, foreground = 0xffffffff) {
94
+ try {
95
+ return await layOut(fonts, size, text, layout, hinting, foreground);
96
+ }
97
+ catch (err) {
98
+ if (err instanceof RangeError && !(err instanceof TooLarge))
99
+ throw new Error('Font: damaged or truncated font data', { cause: err });
100
+ throw err;
101
+ }
102
+ }
103
+ const MAX_PIXELS = 1 << 28;
104
+ class TooLarge extends RangeError {
105
+ }
106
+ async function layOut(fonts, size, text, layout, hinting, foreground) {
107
+ const sized = [];
108
+ const font = (i) => sized[i] ??= sizedFont(fonts[i], size, hinting);
109
+ const primary = font(0);
110
+ const ascent = Math.ceil(primary.font.ascender * primary.scale);
111
+ const lineHeight = Math.max(1, Math.ceil((primary.font.ascender - primary.font.descender + primary.font.lineGap) * primary.scale));
112
+ const paragraphs = layout.wrapHardBreaks ? text.split(/\r\n|[\r\n\u0085\u2028\u2029]/) : [text.replace(/\r\n|[\r\n\u0085\u2028\u2029]/g, '')];
113
+ const lines = [];
114
+ for (const paragraph of paragraphs)
115
+ lines.push(...layOutParagraph(analyze(paragraph, fonts.length, font), layout, font, size));
57
116
  let visible = lines;
58
117
  if (isFinite(layout.maxHeight)) {
59
118
  const count = Math.max(1, Math.floor(layout.maxHeight / lineHeight));
@@ -63,29 +122,271 @@ export function render(data, size, text, layout, hinting = false) {
63
122
  visible = lines.slice(start, start + count);
64
123
  }
65
124
  }
66
- const width = Math.ceil(Math.max(0, ...visible.map(line => line.extent)));
125
+ const width = Math.max(1, Math.ceil(Math.max(0, ...visible.map(line => line.extent))));
67
126
  const height = visible.length * lineHeight;
127
+ if (!(width * height <= MAX_PIXELS))
128
+ throw new TooLarge(`Text too large to render (${width}x${height} pixels)`);
68
129
  const coverage = new Uint8Array(width * height);
69
- visible.forEach((line, n) => {
130
+ let color = null;
131
+ for (const [n, line] of visible.entries()) {
70
132
  let offset = layout.horizontalAlign === 'right' ? width - line.extent
71
133
  : layout.horizontalAlign === 'center' ? (width - line.extent) / 2 : 0;
72
- if (hinting)
134
+ if (primary.hinter)
73
135
  offset = Math.round(offset);
74
136
  const baseline = ascent + n * lineHeight;
75
- for (const { x, glyph: g } of line.glyphs) {
137
+ for (const { x, dx, dy, glyph: g } of line.glyphs) {
76
138
  if (!g.ink)
77
139
  continue;
78
- const position = Math.round((offset + x) * SUBPIXELS) / SUBPIXELS;
140
+ const position = g.font.hinter ? Math.round(offset + x + dx) : Math.round((offset + x + dx) * SUBPIXELS) / SUBPIXELS;
141
+ const y = baseline - Math.round(dy);
79
142
  const pixel = Math.floor(position);
80
- const subpixel = Math.round((position - pixel) * SUBPIXELS);
143
+ let subpixel = Math.round((position - pixel) * SUBPIXELS);
144
+ if (g.color) {
145
+ if ('bitmap' in g.color)
146
+ subpixel = 0;
147
+ const key = 'bitmap' in g.color ? '0' : `${subpixel}:${foreground}`;
148
+ let bitmap = g.colors.get(key);
149
+ if (bitmap === undefined)
150
+ g.colors.set(key, bitmap = 'bitmap' in g.color
151
+ ? await scaledBitmap(g.color.bitmap, size / g.color.bitmap.ppem)
152
+ : 'layers' in g.color ? layeredGlyph(g, g.color.layers, subpixel / SUBPIXELS, foreground)
153
+ : paintedGlyph(g, subpixel / SUBPIXELS, foreground));
154
+ if (bitmap)
155
+ blitColor(color ??= new Uint8ClampedArray(width * height * 4), width, height, bitmap, pixel + bitmap.left, y - bitmap.top);
156
+ continue;
157
+ }
81
158
  if (g.bitmaps[subpixel] === undefined)
82
159
  g.bitmaps[subpixel] = rasterizeGlyph(g.path, g.scale, subpixel / SUBPIXELS);
83
160
  const bitmap = g.bitmaps[subpixel];
84
161
  if (bitmap)
85
- blit(coverage, width, height, bitmap, pixel + bitmap.left, baseline - bitmap.top);
162
+ blit(coverage, width, height, bitmap, pixel + bitmap.left, y - bitmap.top);
86
163
  }
164
+ }
165
+ return { width, height, coverage, color };
166
+ }
167
+ function analyze(text, count, font) {
168
+ const codepoints = Array.from(text, char => char.codePointAt(0));
169
+ const n = codepoints.length;
170
+ const has = (f, codepoint) => f.font.glyphIndex(codepoint) !== 0;
171
+ const isColor = (f, codepoint) => {
172
+ const index = f.font.glyphIndex(codepoint);
173
+ return !!(f.paints && f.paints.paint(index) !== null || f.bitmaps?.(index) || f.layers?.(index));
174
+ };
175
+ const choose = (grapheme) => {
176
+ const needed = grapheme.filter(c => !isDefaultIgnorableCodepoint(c));
177
+ const variation = grapheme.find(c => c === 0xfe0f || c === 0xfe0e);
178
+ let first = -1;
179
+ for (let i = 0; i < count; i++) {
180
+ const f = font(i);
181
+ if (!needed.every(c => has(f, c)))
182
+ continue;
183
+ if (first < 0)
184
+ first = i;
185
+ if (!variation || !needed.length || (variation === 0xfe0f) === isColor(f, needed[0]))
186
+ return i;
187
+ }
188
+ return first;
189
+ };
190
+ const normalized = (grapheme, form) => Array.from(String.fromCodePoint(...grapheme).normalize(form), char => char.codePointAt(0));
191
+ const graphemes = graphemeBoundaries(codepoints), fontOf = new Int32Array(n);
192
+ for (let start = 0; start < n;) {
193
+ let end = start + 1;
194
+ while (end < n && !graphemes[end])
195
+ end++;
196
+ const grapheme = codepoints.slice(start, end);
197
+ let chosen = grapheme.length > 1 ? choose(normalized(grapheme, 'NFC')) : choose(grapheme);
198
+ if (chosen < 0)
199
+ chosen = choose(grapheme.length > 1 ? grapheme : normalized(grapheme, 'NFD'));
200
+ if (chosen >= 0)
201
+ fontOf.fill(chosen, start, end);
202
+ else
203
+ for (let k = start; k < end; k++)
204
+ fontOf[k] = Math.max(0, choose([codepoints[k]]));
205
+ start = end;
206
+ }
207
+ return {
208
+ codepoints, fontOf, graphemes,
209
+ bidi: resolve(codepoints.map(classOf), codepoints),
210
+ scripts: scripts(codepoints),
211
+ breaks: lineBreaks(codepoints),
212
+ };
213
+ }
214
+ const CONTEXT = 5;
215
+ function shapeRange(p, start, end, levels, font, size) {
216
+ const runs = [];
217
+ let lastLevel = p.bidi.level;
218
+ const levelAt = (i) => levels[i] >= 0 ? lastLevel = levels[i] : lastLevel;
219
+ for (let i = start; i < end;) {
220
+ const level = levelAt(i), script = p.scripts[i], f = p.fontOf[i];
221
+ let j = i + 1;
222
+ while (j < end && (levels[j] < 0 || levels[j] === level) && p.scripts[j] === script && p.fontOf[j] === f)
223
+ j++;
224
+ runs.push([i, j, level]);
225
+ i = j;
226
+ }
227
+ let highest = 0, lowestOdd = Infinity;
228
+ for (const [, , level] of runs) {
229
+ highest = Math.max(highest, level);
230
+ if (level & 1)
231
+ lowestOdd = Math.min(lowestOdd, level);
232
+ }
233
+ for (let level = highest; level >= lowestOdd; level--)
234
+ for (let i = 0; i < runs.length;) {
235
+ if (runs[i][2] < level) {
236
+ i++;
237
+ continue;
238
+ }
239
+ let j = i;
240
+ while (j < runs.length && runs[j][2] >= level)
241
+ j++;
242
+ const reversed = runs.slice(i, j).reverse();
243
+ for (let k = i; k < j; k++)
244
+ runs[k] = reversed[k - i];
245
+ i = j;
246
+ }
247
+ const glyphs = [], advances = new Map();
248
+ let x = 0, adjustment = 0;
249
+ for (const [rs, re, level] of runs) {
250
+ const f = font(p.fontOf[rs]);
251
+ const before = p.codepoints.slice(Math.max(0, rs - CONTEXT), rs).reverse(), after = p.codepoints.slice(re, re + CONTEXT);
252
+ const clusters = Array.from({ length: re - rs }, (_, k) => rs + k);
253
+ const shaped = shapeRun(f.shaping, p.codepoints.slice(rs, re), p.scripts[rs], level & 1 ? 5 : 4, clusters, { before, after });
254
+ for (const info of shaped) {
255
+ const g = glyphOf(f, info.codepoint, size);
256
+ let advance;
257
+ if (f.hinter) {
258
+ const before = Math.round(adjustment);
259
+ glyphs.push({ x, dx: Math.round(adjustment + info.xOffset * f.scale) - before, dy: Math.round(info.yOffset * f.scale), glyph: g });
260
+ adjustment += (info.xAdvance - f.font.advance(info.codepoint)) * f.scale;
261
+ advance = g.advance + Math.round(adjustment) - before;
262
+ }
263
+ else {
264
+ advance = info.xAdvance * f.scale;
265
+ glyphs.push({ x, dx: info.xOffset * f.scale, dy: info.yOffset * f.scale, glyph: g });
266
+ }
267
+ advances.set(info.cluster, (advances.get(info.cluster) ?? 0) + advance);
268
+ x += advance;
269
+ }
270
+ }
271
+ return { glyphs, width: x, advances };
272
+ }
273
+ const isSpace = (codepoint) => codepoint === 0x20 || codepoint === 0x09 || codepoint === 0x3000 || codepoint >= 0x2000 && codepoint <= 0x200a;
274
+ function layOutParagraph(p, layout, font, size) {
275
+ const n = p.codepoints.length;
276
+ if (!n)
277
+ return [{ glyphs: [], extent: 0 }];
278
+ const whole = shapeRange(p, 0, n, p.bidi.levels, font, size);
279
+ const width = new Float64Array(n), starts = new Uint8Array(n + 1);
280
+ starts[n] = 1;
281
+ for (const [cluster, advance] of whole.advances) {
282
+ width[cluster] = advance;
283
+ starts[cluster] = 1;
284
+ }
285
+ starts[0] = 1;
286
+ const canBreak = (i) => starts[i] === 1 && (layout.wrapStyle === 'char' ? p.graphemes[i] === 1 : p.breaks[i] !== 0);
287
+ const ranges = [];
288
+ let lineStart = 0;
289
+ while (lineStart < n) {
290
+ let fit = -1, emergency = -1, used = 0, trailing = 0;
291
+ for (let i = lineStart; i < n; i++) {
292
+ used += width[i];
293
+ trailing = isSpace(p.codepoints[i]) ? trailing + width[i] : 0;
294
+ const end = i + 1;
295
+ if (!starts[end])
296
+ continue;
297
+ if (p.breaks[end] === 2 && end < n && layout.wrapHardBreaks) {
298
+ fit = end;
299
+ break;
300
+ }
301
+ const fits = used - trailing <= layout.maxWidth;
302
+ if (!fits)
303
+ break;
304
+ if (canBreak(end))
305
+ fit = end;
306
+ if (p.graphemes[end])
307
+ emergency = end;
308
+ }
309
+ if (fit < 0) {
310
+ fit = emergency;
311
+ if (fit < 0) {
312
+ fit = lineStart + 1;
313
+ while (fit < n && !(starts[fit] && p.graphemes[fit]))
314
+ fit++;
315
+ }
316
+ }
317
+ ranges.push([lineStart, fit]);
318
+ lineStart = fit;
319
+ }
320
+ return ranges.map(([start, end]) => {
321
+ const levels = lineLevels(p.bidi, start, end);
322
+ const shifted = new Int8Array(n);
323
+ shifted.set(levels, start);
324
+ const { glyphs } = ranges.length === 1 ? whole : shapeRange(p, start, end, shifted, font, size);
325
+ let extent = 0;
326
+ for (const { x, dx, glyph } of glyphs)
327
+ if (glyph.ink)
328
+ extent = Math.max(extent, x + dx + glyph.ink.right);
329
+ return { glyphs, extent };
87
330
  });
88
- return { width, height, coverage };
331
+ }
332
+ async function scaledBitmap(glyph, scale) {
333
+ let decoded;
334
+ try {
335
+ decoded = await decodePNG(glyph.png);
336
+ }
337
+ catch {
338
+ return null;
339
+ }
340
+ const width = Math.max(1, Math.round(decoded.width * scale)), height = Math.max(1, Math.round(decoded.height * scale));
341
+ const data = new Uint8ClampedArray(width * height * 4);
342
+ resample(new Uint8ClampedArray(decoded.pixels.buffer, decoded.pixels.byteOffset, decoded.pixels.length), decoded.width, decoded.height, width, height, TRIANGLE, data, true);
343
+ premultiply(data);
344
+ return { data, width, height, left: Math.round(glyph.left * scale), top: Math.round(glyph.top * scale) };
345
+ }
346
+ function layeredGlyph(g, layers, offset, foreground) {
347
+ const rasters = layers.map(layer => ({ bitmap: rasterizeGlyph(g.font.font.outline(layer.glyph), g.scale, offset), color: layer.color ?? foreground }));
348
+ let left = Infinity, top = -Infinity, right = -Infinity, bottom = Infinity;
349
+ for (const { bitmap } of rasters)
350
+ if (bitmap) {
351
+ left = Math.min(left, bitmap.left);
352
+ top = Math.max(top, bitmap.top);
353
+ right = Math.max(right, bitmap.left + bitmap.width);
354
+ bottom = Math.min(bottom, bitmap.top - bitmap.height);
355
+ }
356
+ if (right <= left)
357
+ return null;
358
+ const width = right - left, height = top - bottom;
359
+ const data = new Uint8ClampedArray(width * height * 4);
360
+ for (const { bitmap, color } of rasters) {
361
+ if (!bitmap)
362
+ continue;
363
+ const r = color >>> 24, gr = color >> 16 & 255, b = color >> 8 & 255, a = color & 255;
364
+ for (let y = 0; y < bitmap.height; y++)
365
+ for (let x = 0; x < bitmap.width; x++) {
366
+ const coverage = bitmap.data[y * bitmap.width + x];
367
+ if (!coverage)
368
+ continue;
369
+ const o = ((top - bitmap.top + y) * width + bitmap.left - left + x) * 4;
370
+ const sa = coverage * a / 255, keep = 1 - sa / 255;
371
+ data[o] = r * sa / 255 + data[o] * keep;
372
+ data[o + 1] = gr * sa / 255 + data[o + 1] * keep;
373
+ data[o + 2] = b * sa / 255 + data[o + 2] * keep;
374
+ data[o + 3] = sa + data[o + 3] * keep;
375
+ }
376
+ }
377
+ return { data, width, height, left, top };
378
+ }
379
+ function paintedGlyph(g, offset, foreground) {
380
+ const { font } = g.font;
381
+ return renderPaintGlyph(font, g.font.paints, g.index, g.scale, offset, foreground, glyph => font.outline(glyph), (path, clip) => rasterizeGlyph(path, 1, 0, clip));
382
+ }
383
+ function premultiply(rgba) {
384
+ for (let i = 0; i < rgba.length; i += 4) {
385
+ const a = rgba[i + 3] / 255;
386
+ rgba[i] *= a;
387
+ rgba[i + 1] *= a;
388
+ rgba[i + 2] *= a;
389
+ }
89
390
  }
90
391
  function inkExtent(path, scale) {
91
392
  let left = Infinity, right = -Infinity;
@@ -96,76 +397,33 @@ function inkExtent(path, scale) {
96
397
  }
97
398
  return right > left ? { left, right } : null;
98
399
  }
99
- function wrap(text, layout, glyph, kern) {
100
- const lines = [];
101
- const paragraphs = layout.wrapHardBreaks ? text.split(/\r\n|\r|\n/) : [text.replace(/\r\n|\r|\n/g, '')];
102
- const edge = (x, g) => g.ink ? x + g.ink.right : 0;
103
- for (const paragraph of paragraphs) {
104
- let line = { glyphs: [], extent: 0, soft: false };
105
- let x = 0;
106
- let breakAt = -1;
107
- const next = (g) => line.glyphs.length ? x + kern(line.glyphs[line.glyphs.length - 1].glyph, g) : x;
108
- const add = (g) => {
109
- const position = next(g);
110
- line.glyphs.push({ x: position, glyph: g });
111
- line.extent = Math.max(line.extent, edge(position, g));
112
- x = position + g.advance;
113
- };
114
- const newLine = () => {
115
- lines.push(line);
116
- line = { glyphs: [], extent: 0, soft: true };
117
- x = 0;
118
- breakAt = -1;
119
- };
120
- for (const char of paragraph) {
121
- const codepoint = char.codePointAt(0);
122
- const g = glyph(codepoint);
123
- const space = codepoint === 0x20 || codepoint === 0x09;
124
- if (!space && line.glyphs.length && edge(next(g), g) > layout.maxWidth) {
125
- if (layout.wrapStyle === 'word' && breakAt > 0) {
126
- const word = line.glyphs.splice(breakAt);
127
- line.extent = measure(line.glyphs, edge);
128
- newLine();
129
- for (const item of word)
130
- add(item.glyph);
131
- }
132
- else
133
- newLine();
134
- }
135
- if (space && line.soft && !line.glyphs.length)
136
- continue;
137
- add(g);
138
- if (space)
139
- breakAt = line.glyphs.length;
140
- }
141
- lines.push(line);
142
- }
143
- return lines;
144
- }
145
- function measure(glyphs, edge) {
146
- let extent = 0;
147
- for (const { x, glyph } of glyphs)
148
- extent = Math.max(extent, edge(x, glyph));
149
- return extent;
400
+ function clipped(width, height, bitmap, x0, y0) {
401
+ return [Math.max(0, -x0), Math.min(bitmap.width, width - x0), Math.max(0, -y0), Math.min(bitmap.height, height - y0)];
150
402
  }
151
403
  function blit(target, width, height, bitmap, x0, y0) {
152
- for (let y = 0; y < bitmap.height; y++) {
153
- const ty = y0 + y;
154
- if (ty < 0 || ty >= height)
155
- continue;
156
- for (let x = 0; x < bitmap.width; x++) {
157
- const tx = x0 + x;
158
- if (tx < 0 || tx >= width)
159
- continue;
160
- const v = bitmap.data[y * bitmap.width + x];
161
- if (v) {
162
- const o = ty * width + tx;
404
+ const [xs, xe, ys, ye] = clipped(width, height, bitmap, x0, y0);
405
+ for (let y = ys; y < ye; y++)
406
+ for (let x = xs, s = y * bitmap.width + xs, o = (y0 + y) * width + x0 + xs; x < xe; x++, s++, o++) {
407
+ const v = bitmap.data[s];
408
+ if (v)
163
409
  target[o] = Math.min(255, target[o] + v);
164
- }
165
410
  }
166
- }
167
411
  }
168
- export function rasterizeGlyph(path, scale, offset = 0) {
412
+ function blitColor(target, width, height, bitmap, x0, y0) {
413
+ const [xs, xe, ys, ye] = clipped(width, height, bitmap, x0, y0);
414
+ for (let y = ys; y < ye; y++)
415
+ for (let x = xs, s = (y * bitmap.width + xs) * 4, o = ((y0 + y) * width + x0 + xs) * 4; x < xe; x++, s += 4, o += 4) {
416
+ const sa = bitmap.data[s + 3];
417
+ if (!sa)
418
+ continue;
419
+ const keep = 1 - sa / 255;
420
+ target[o] = bitmap.data[s] + target[o] * keep;
421
+ target[o + 1] = bitmap.data[s + 1] + target[o + 1] * keep;
422
+ target[o + 2] = bitmap.data[s + 2] + target[o + 2] * keep;
423
+ target[o + 3] = sa + target[o + 3] * keep;
424
+ }
425
+ }
426
+ export function rasterizeGlyph(path, scale, offset = 0, clip) {
169
427
  if (!path.length)
170
428
  return null;
171
429
  let xmin = Infinity, ymin = Infinity, xmax = -Infinity, ymax = -Infinity;
@@ -181,10 +439,15 @@ export function rasterizeGlyph(path, scale, offset = 0) {
181
439
  if (y > ymax)
182
440
  ymax = y;
183
441
  }
184
- const left = Math.floor(xmin), right = Math.ceil(xmax);
185
- const bottom = Math.floor(ymin), top = Math.ceil(ymax);
442
+ let left = Math.floor(xmin), right = Math.ceil(xmax), bottom = Math.floor(ymin), top = Math.ceil(ymax);
443
+ if (clip) {
444
+ left = Math.max(left, clip.left);
445
+ right = Math.min(right, clip.right);
446
+ bottom = Math.max(bottom, clip.bottom);
447
+ top = Math.min(top, clip.top);
448
+ }
186
449
  const width = right - left, height = top - bottom;
187
- if (width <= 0 || height <= 0)
450
+ if (!(width > 0 && height > 0) || width * height > MAX_GLYPH_PIXELS)
188
451
  return null;
189
452
  const raster = new Rasterizer(width, height);
190
453
  const tx = (x) => x * scale + offset - left;
@@ -239,6 +502,13 @@ export class Rasterizer {
239
502
  line(x0, y0, x1, y1) {
240
503
  if (y0 === y1)
241
504
  return;
505
+ for (const edge of [0, this.width])
506
+ if ((x0 - edge) * (x1 - edge) < 0) {
507
+ const y = y0 + (y1 - y0) * (edge - x0) / (x1 - x0);
508
+ this.line(x0, y0, edge, y);
509
+ this.line(edge, y, x1, y1);
510
+ return;
511
+ }
242
512
  x0 = x0 < 0 ? 0 : x0 > this.width ? this.width : x0;
243
513
  x1 = x1 < 0 ? 0 : x1 > this.width ? this.width : x1;
244
514
  let dir = 1;