imagescript 3.2.1 → 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.
- package/README.md +8 -3
- package/THIRD_PARTY_NOTICES.md +131 -0
- package/dist/animation.d.ts +7 -0
- package/dist/animation.js +21 -11
- package/dist/codecs/crc.js +5 -0
- package/dist/codecs/gif.js +10 -35
- package/dist/codecs/jpeg.js +9 -27
- package/dist/codecs/png.js +22 -10
- package/dist/codecs/tga.js +3 -1
- package/dist/codecs/tiff.js +7 -2
- package/dist/codecs/vp8.js +3 -1
- package/dist/codecs/vp8l.js +126 -58
- package/dist/codecs/webp.js +11 -9
- package/dist/codecs/zlib.js +81 -69
- package/dist/frame.d.ts +1 -0
- package/dist/frame.js +4 -2
- package/dist/gif.d.ts +1 -0
- package/dist/gif.js +2 -2
- package/dist/image.d.ts +42 -7
- package/dist/image.js +217 -110
- package/dist/index.js +4 -4
- package/dist/render/bidi.js +350 -0
- package/dist/render/buffer.js +362 -0
- package/dist/render/color.js +164 -0
- package/dist/render/colr.js +523 -0
- package/dist/render/font.js +173 -241
- package/dist/render/hinting.js +12 -4
- package/dist/render/layout.js +1553 -0
- package/dist/render/linebreak.js +210 -0
- package/dist/render/segment.js +106 -0
- package/dist/render/shape.js +984 -0
- package/dist/render/shapers/arabic.js +284 -0
- package/dist/render/shapers/hangul.js +142 -0
- package/dist/render/shapers/hebrew.js +49 -0
- package/dist/render/shapers/index.js +52 -0
- package/dist/render/shapers/indic.js +579 -0
- package/dist/render/shapers/khmer.js +104 -0
- package/dist/render/shapers/machine.js +129 -0
- package/dist/render/shapers/myanmar.js +121 -0
- package/dist/render/shapers/syllabic.js +100 -0
- package/dist/render/shapers/thai.js +125 -0
- package/dist/render/shapers/use.js +235 -0
- package/dist/render/svg.js +101 -22
- package/dist/render/text.js +373 -103
- package/dist/render/unicode-data.js +63 -0
- package/dist/render/unicode.js +58 -0
- package/dist/text-layout.d.ts +4 -1
- package/dist/text-layout.js +2 -2
- package/dist/util/alpha.js +43 -0
- package/dist/util/blur.js +20 -33
- package/dist/util/bytes.js +31 -0
- package/dist/util/limits.js +34 -0
- package/dist/util/options.js +7 -0
- package/dist/util/resample.js +122 -5
- package/dist/util/rotate.js +43 -0
- package/dist/version.js +1 -1
- package/licenses/HarfBuzz.txt +42 -0
- package/licenses/Unicode-3.0.txt +39 -0
- package/package.json +1 -1
package/dist/render/text.js
CHANGED
|
@@ -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
|
-
|
|
33
|
+
function sizedFont(data, size, hinting) {
|
|
21
34
|
const font = loadFont(data);
|
|
22
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
100
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
-
|
|
185
|
-
|
|
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
|
|
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;
|