ntk 8.17.4 → 8.17.5
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/lib/fontconfig.js +426 -97
- package/lib/renderingcontext_2d.js +134 -49
- package/lib/text/glyphs.js +34 -13
- package/package.json +1 -1
package/lib/fontconfig.js
CHANGED
|
@@ -93,18 +93,29 @@ export function noFontsError(reason, cause) {
|
|
|
93
93
|
return err;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
// css weight -> fontconfig weight
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
96
|
+
// css weight -> fontconfig weight: fontconfig's own table
|
|
97
|
+
// (FcWeightFromOpenTypeDouble), the one it reads every face's OS/2 weight
|
|
98
|
+
// through, so a pattern's weight sits where a face of that CSS weight does
|
|
99
|
+
const cssToFcWeight = [
|
|
100
|
+
[0, 0],
|
|
101
|
+
[100, 0], // thin
|
|
102
|
+
[200, 40], // extralight
|
|
103
|
+
[300, 50], // light
|
|
104
|
+
[350, 55], // demilight
|
|
105
|
+
[380, 75], // book
|
|
106
|
+
[400, 80], // regular
|
|
107
|
+
[500, 100], // medium
|
|
108
|
+
[600, 180], // demibold
|
|
109
|
+
[700, 200], // bold
|
|
110
|
+
[800, 205], // extrabold
|
|
111
|
+
[900, 210], // black
|
|
112
|
+
[1000, 215] // extrablack
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
// CSS's 400 and 500, and the heaviest weight there is, on fontconfig's scale
|
|
116
|
+
const REGULAR = 80;
|
|
117
|
+
const MEDIUM = 100;
|
|
118
|
+
const HEAVIEST = 215;
|
|
108
119
|
|
|
109
120
|
// formats fontkit can parse. Exported so the font-spec resolver filters a
|
|
110
121
|
// directory listing by exactly the same rule fc-match output is filtered by —
|
|
@@ -239,17 +250,27 @@ function fcName(name) {
|
|
|
239
250
|
return name.replace(/[\\\-:,]/g, '\\$&');
|
|
240
251
|
}
|
|
241
252
|
|
|
242
|
-
|
|
243
|
-
|
|
253
|
+
/**
|
|
254
|
+
* A pattern as fc-match is handed it (`fc`, which is also its cache key),
|
|
255
|
+
* with what CSS's matching needs of it besides: the family names it asks
|
|
256
|
+
* for, the weight on fontconfig's scale (its default, regular, where none
|
|
257
|
+
* is asked for) and whether it asks for italic.
|
|
258
|
+
*/
|
|
259
|
+
function specFor({ family, weight, style }) {
|
|
260
|
+
const names = String(platformFamilies(family || 'sans-serif'))
|
|
244
261
|
.split(',')
|
|
245
262
|
.map((name) => name.trim().replace(/^["']|["']$/g, ''))
|
|
246
|
-
.filter(Boolean)
|
|
247
|
-
|
|
248
|
-
.join(',');
|
|
263
|
+
.filter(Boolean);
|
|
264
|
+
let fc = names.map(fcName).join(',');
|
|
249
265
|
const fcWeight = normalizeWeight(weight);
|
|
250
266
|
if (fcWeight !== undefined) fc += `:weight=${fcWeight}`;
|
|
251
|
-
|
|
252
|
-
|
|
267
|
+
const italic = Boolean(style && style.includes('italic'));
|
|
268
|
+
if (italic) fc += ':slant=italic';
|
|
269
|
+
return { fc, names, weight: fcWeight ?? REGULAR, italic };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function patternFor(pattern) {
|
|
273
|
+
return specFor(pattern).fc;
|
|
253
274
|
}
|
|
254
275
|
|
|
255
276
|
// One command shared by the sync and async paths, so a prewarmed cache entry
|
|
@@ -259,52 +280,317 @@ function patternFor({ family, weight, style }) {
|
|
|
259
280
|
// localized aliases included (`Hiragino Sans`, `ヒラギノ角ゴシック`, and the
|
|
260
281
|
// style-suffixed forms of both are one face), and `--format` joins them with
|
|
261
282
|
// commas. The fields are tab-separated so a comma inside one costs nothing,
|
|
262
|
-
// and `charset` stays last because it is by far the longest.
|
|
283
|
+
// and `charset` stays last because it is by far the longest. Weight, slant
|
|
284
|
+
// and width are what CSS's matching reads (`cssFace`).
|
|
263
285
|
const fcMatchArgs = [
|
|
264
286
|
'-s',
|
|
265
287
|
'--format',
|
|
266
|
-
'%{file}\t%{postscriptname}\t%{family}\t%{charset}\n'
|
|
288
|
+
'%{file}\t%{postscriptname}\t%{family}\t%{weight}\t%{slant}\t%{width}\t%{charset}\n'
|
|
267
289
|
];
|
|
268
290
|
const fcMatchOpts = { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 };
|
|
269
291
|
|
|
292
|
+
/**
|
|
293
|
+
* A weight, slant or width as fontconfig prints it, as `[lo, hi]`: a number
|
|
294
|
+
* is both ends, and a variable font's range, `[80 200]`, its own. Null where
|
|
295
|
+
* there is none.
|
|
296
|
+
*/
|
|
297
|
+
function valueRange(text) {
|
|
298
|
+
const n = text ? Number(text) : NaN;
|
|
299
|
+
if (!Number.isNaN(n)) return [n, n];
|
|
300
|
+
const values = String(text ?? '')
|
|
301
|
+
.replace(/[[\]]/g, ' ')
|
|
302
|
+
.split(/[\s,]+/)
|
|
303
|
+
.filter(Boolean)
|
|
304
|
+
.map(Number);
|
|
305
|
+
if (values.length === 0 || values.some(Number.isNaN)) return null;
|
|
306
|
+
return [Math.min(...values), Math.max(...values)];
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* One face fontconfig named, as a candidate. `charset` is null where it was
|
|
311
|
+
* not asked for (the census's faces) — the face's coverage is not known, and
|
|
312
|
+
* `charsetHas` answers yes, so a fallback opens it and asks the font.
|
|
313
|
+
*/
|
|
314
|
+
function candidate(path, postscriptName, family, weight, slant, width, charset) {
|
|
315
|
+
// `families` keeps fontconfig's whole list, in its order; `family` is
|
|
316
|
+
// the first of them, which is the name fontconfig leads with for the
|
|
317
|
+
// current locale and the one to show in a UI.
|
|
318
|
+
const families = family ? family.split(',').filter(Boolean) : [];
|
|
319
|
+
return {
|
|
320
|
+
path,
|
|
321
|
+
postscriptName,
|
|
322
|
+
family: families[0] || '',
|
|
323
|
+
families,
|
|
324
|
+
charset,
|
|
325
|
+
_ranges: null,
|
|
326
|
+
_style: [weight, slant, width]
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* A candidate's weight, slant and width as ranges (`valueRange`), read the
|
|
332
|
+
* first time they are asked for — a chain's head is, the 150 faces behind
|
|
333
|
+
* it never are. Null where fontconfig did not say.
|
|
334
|
+
*/
|
|
335
|
+
function styleOf(face) {
|
|
336
|
+
if (Array.isArray(face._style)) {
|
|
337
|
+
const [weight, slant, width] = face._style.map(valueRange);
|
|
338
|
+
face._style = weight && slant && width ? { weight, slant, width } : null;
|
|
339
|
+
}
|
|
340
|
+
return face._style;
|
|
341
|
+
}
|
|
342
|
+
|
|
270
343
|
/** fc-match -s output -> candidates, filtered to formats fontkit can parse */
|
|
271
344
|
function parseMatches(out) {
|
|
272
345
|
const list = [];
|
|
273
346
|
for (const line of out.split('\n')) {
|
|
274
|
-
const [path, postscriptName, family, charset] = line.split('\t');
|
|
347
|
+
const [path, postscriptName, family, weight, slant, width, charset] = line.split('\t');
|
|
275
348
|
if (path && supported.test(path)) {
|
|
276
|
-
|
|
277
|
-
// the first of them, which is the name fontconfig leads with for the
|
|
278
|
-
// current locale and the one to show in a UI.
|
|
279
|
-
const families = family ? family.split(',').filter(Boolean) : [];
|
|
280
|
-
list.push({
|
|
281
|
-
path,
|
|
282
|
-
postscriptName,
|
|
283
|
-
family: families[0] || '',
|
|
284
|
-
families,
|
|
285
|
-
charset: charset || '',
|
|
286
|
-
_ranges: null
|
|
287
|
-
});
|
|
349
|
+
list.push(candidate(path, postscriptName, family, weight, slant, width, charset || ''));
|
|
288
350
|
}
|
|
289
351
|
}
|
|
290
352
|
return list;
|
|
291
353
|
}
|
|
292
354
|
|
|
293
355
|
/**
|
|
294
|
-
* fc-match output -> the cached candidate list for a pattern
|
|
295
|
-
*
|
|
296
|
-
*
|
|
356
|
+
* fc-match output -> the cached candidate list for a pattern, its head the
|
|
357
|
+
* face CSS picks (`cssChain`). Throws for output that parses to nothing
|
|
358
|
+
* usable, which is a fontconfig answer rather than a fontconfig failure and
|
|
359
|
+
* so is diagnosed separately.
|
|
297
360
|
*/
|
|
298
|
-
function cacheMatches(
|
|
361
|
+
function cacheMatches(spec, out) {
|
|
299
362
|
const list = parseMatches(out);
|
|
300
363
|
if (list.length === 0) {
|
|
301
364
|
throw noFontsError(
|
|
302
|
-
`fontconfig matched no font ntk can parse for "${fc}" (needs ` +
|
|
365
|
+
`fontconfig matched no font ntk can parse for "${spec.fc}" (needs ` +
|
|
303
366
|
'.ttf/.otf/.woff/.woff2/.ttc/.dfont — bitmap .pcf/.bdf fonts are not usable)'
|
|
304
367
|
);
|
|
305
368
|
}
|
|
306
|
-
|
|
307
|
-
|
|
369
|
+
const chain = cssChain(spec, list);
|
|
370
|
+
sortedCache.set(spec.fc, chain);
|
|
371
|
+
return chain;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// The census: every face fontconfig knows, a line each — file, PostScript
|
|
375
|
+
// name, families, weight, slant and width — from one fc-list a process,
|
|
376
|
+
// started beside the first prewarm's fc-matches (`spawnToFiles`) and read
|
|
377
|
+
// the first time a weight needs it (`cssFace`). 357 KB for 1,900 faces on a
|
|
378
|
+
// Mac, 45 ms to list. It is kept as the bytes it was read as and a family's
|
|
379
|
+
// lines found in it by a byte search: decoded whole, it was a millisecond of
|
|
380
|
+
// the layout that first needed it, and a family is a dozen of its lines.
|
|
381
|
+
const CENSUS_FORMAT = '%{file}\t%{postscriptname}\t%{family}\t%{weight}\t%{slant}\t%{width}\n';
|
|
382
|
+
|
|
383
|
+
// undefined until a prewarm starts it; then its prewarm entry, with `bytes`
|
|
384
|
+
// (undefined until read, null where there is none to read) and `faces`, the
|
|
385
|
+
// family -> faces lookups already made in it
|
|
386
|
+
let census;
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* The census's bytes — waited for synchronously while its child runs. Null
|
|
390
|
+
* where it failed, took too long, or never ran (no shell to run it in), and
|
|
391
|
+
* then for good: a face answered without it must not be answered otherwise
|
|
392
|
+
* a moment later.
|
|
393
|
+
*/
|
|
394
|
+
function censusBytes() {
|
|
395
|
+
if (!census) return null;
|
|
396
|
+
if (census.bytes !== undefined) return census.bytes;
|
|
397
|
+
const deadline = performance.now() + PREWARM_WAIT_MS;
|
|
398
|
+
let status = prewarmStatus(census.base);
|
|
399
|
+
while (status === null && !census.exited && performance.now() < deadline) {
|
|
400
|
+
Atomics.wait(nap, 0, 0, 1);
|
|
401
|
+
status = prewarmStatus(census.base);
|
|
402
|
+
}
|
|
403
|
+
census.bytes = null;
|
|
404
|
+
if (status === 0) {
|
|
405
|
+
try {
|
|
406
|
+
census.bytes = builtin('node:fs').readFileSync(`${census.base}.out`);
|
|
407
|
+
} catch {
|
|
408
|
+
// gone from under us: no census, as if it had failed
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
removePrewarm(census.base);
|
|
412
|
+
return census.bytes;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** once the census can be read without waiting: for a caller that awaits */
|
|
416
|
+
function censusSettled() {
|
|
417
|
+
return census && census.bytes === undefined && !census.exited ? census.promise : undefined;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// a family name as fontconfig compares one: case and blanks aside
|
|
421
|
+
const folded = (name) => name.replace(/\s+/g, '').toLowerCase();
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* The faces of the family fontconfig matched a pattern to, as the census has
|
|
425
|
+
* them — those ntk can open. Null where there is no census.
|
|
426
|
+
*
|
|
427
|
+
* The family is the name `head` was matched by: the first of the pattern's
|
|
428
|
+
* that the face answers to, or — for a generic, or a family fontconfig put
|
|
429
|
+
* another in place of — the name it leads with. Which name matters: `Avenir
|
|
430
|
+
* Next Ultra Light` is a family of two faces, and `Avenir Next` one of
|
|
431
|
+
* twelve that holds both.
|
|
432
|
+
*/
|
|
433
|
+
function familyFaces(names, head) {
|
|
434
|
+
const bytes = censusBytes();
|
|
435
|
+
if (bytes === null) return null;
|
|
436
|
+
let name;
|
|
437
|
+
for (const asked of names.map(folded)) {
|
|
438
|
+
name = head.families.find((f) => folded(f) === asked);
|
|
439
|
+
if (name) break;
|
|
440
|
+
}
|
|
441
|
+
name ??= head.families[0];
|
|
442
|
+
if (!name) return null;
|
|
443
|
+
const key = folded(name);
|
|
444
|
+
let faces = census.faces.get(key);
|
|
445
|
+
if (faces) return faces;
|
|
446
|
+
faces = [];
|
|
447
|
+
// fontconfig spells a name alike in both answers, so a search for it finds
|
|
448
|
+
// every line it is on, and the line's own family list says whether it is
|
|
449
|
+
// one of the face's names or only part of a longer one
|
|
450
|
+
for (let at = bytes.indexOf(name); at !== -1; ) {
|
|
451
|
+
const start = bytes.lastIndexOf(10, at) + 1;
|
|
452
|
+
let end = bytes.indexOf(10, at);
|
|
453
|
+
if (end === -1) end = bytes.length;
|
|
454
|
+
const line = bytes.toString('utf8', start, end);
|
|
455
|
+
const [path, postscriptName, family, weight, slant, width] = line.split('\t');
|
|
456
|
+
const face = candidate(path, postscriptName, family, weight, slant, width, null);
|
|
457
|
+
if (path && supported.test(path) && face.families.some((f) => folded(f) === key)) faces.push(face);
|
|
458
|
+
at = bytes.indexOf(name, end);
|
|
459
|
+
}
|
|
460
|
+
census.faces.set(key, faces);
|
|
461
|
+
return faces;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// CSS Fonts 4 §5.2's order, as ranks on fontconfig's scale, lowest first.
|
|
465
|
+
// Width (font-stretch, normal being 100): the normal width, then narrower
|
|
466
|
+
// ones nearest first, then wider ones.
|
|
467
|
+
function widthRank([lo, hi]) {
|
|
468
|
+
if (lo <= 100 && hi >= 100) return 0;
|
|
469
|
+
return hi < 100 ? 100 - hi : 1000 + lo - 100;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Slant: the one asked for, then oblique (fontconfig's 110), then the other.
|
|
473
|
+
function slantRank(italic, [lo, hi]) {
|
|
474
|
+
const asked = italic ? 100 : 0;
|
|
475
|
+
if (lo <= asked && hi >= asked) return 0;
|
|
476
|
+
return lo >= 105 ? 1 : 2;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Weight, for `weight` asked: between 400 and 500, up to 500 ascending, then
|
|
480
|
+
// lighter descending, then past 500 ascending; below 400 lighter first, above
|
|
481
|
+
// 500 heavier first. A variable face's range holds every weight in it.
|
|
482
|
+
function weightRank(weight, [lo, hi]) {
|
|
483
|
+
if (lo <= weight && hi >= weight) return 0;
|
|
484
|
+
const x = hi < weight ? hi : lo;
|
|
485
|
+
if (weight >= REGULAR && weight <= MEDIUM) {
|
|
486
|
+
if (x > weight && x <= MEDIUM) return x - weight;
|
|
487
|
+
return x < weight ? 1000 + weight - x : 2000 + x - weight;
|
|
488
|
+
}
|
|
489
|
+
if (weight < REGULAR) return x < weight ? weight - x : 1000 + x - weight;
|
|
490
|
+
return x > weight ? x - weight : 1000 + weight - x;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Is `head`, the face fontconfig picked, the one CSS picks — whatever else
|
|
495
|
+
* the family holds?
|
|
496
|
+
*
|
|
497
|
+
* Fontconfig picks the face nearest in weight, after the slant: so no face
|
|
498
|
+
* of the head's slant is nearer the weight asked for than the head, and that
|
|
499
|
+
* is all one answer says about the family. The head is CSS's pick when every
|
|
500
|
+
* weight CSS would try before the head's lies nearer — 500 in Helvetica,
|
|
501
|
+
* whose regular is 80 against 100 asked, where CSS tries 500 alone first.
|
|
502
|
+
* 450 in Arial is not: 500 is as near as its regular, so a medium could be
|
|
503
|
+
* there, and CSS would take it. Then the census decides.
|
|
504
|
+
*
|
|
505
|
+
* Fontconfig ranks width after weight and CSS before it, so a head of
|
|
506
|
+
* another width than normal is never shown to be CSS's: 900 in Helvetica
|
|
507
|
+
* Neue is its condensed black. Nor is an italic head for an upright ask,
|
|
508
|
+
* where CSS tries oblique first.
|
|
509
|
+
*/
|
|
510
|
+
function provenPick(weight, italic, head) {
|
|
511
|
+
const style = styleOf(head);
|
|
512
|
+
if (!style) return false;
|
|
513
|
+
if (widthRank(style.width) !== 0) return false;
|
|
514
|
+
if (!italic && slantRank(false, style.slant) === 2) return false;
|
|
515
|
+
const [lo, hi] = style.weight;
|
|
516
|
+
if (lo <= weight && hi >= weight) return true;
|
|
517
|
+
const x = hi < weight ? hi : lo;
|
|
518
|
+
const near = Math.abs(x - weight);
|
|
519
|
+
// the weights CSS tries before x: from, whether it is open, to, whether it is
|
|
520
|
+
let from, fromOpen, to, toOpen;
|
|
521
|
+
if (weight >= REGULAR && weight <= MEDIUM) {
|
|
522
|
+
if (x > weight && x <= MEDIUM) [from, fromOpen, to, toOpen] = [weight, false, x, true];
|
|
523
|
+
else if (x < weight) [from, fromOpen, to, toOpen] = [x, true, MEDIUM, false];
|
|
524
|
+
else [from, fromOpen, to, toOpen] = [0, false, x, true];
|
|
525
|
+
} else if (weight < REGULAR) {
|
|
526
|
+
if (x < weight) [from, fromOpen, to, toOpen] = [x, true, weight, false];
|
|
527
|
+
else [from, fromOpen, to, toOpen] = [0, false, x, true];
|
|
528
|
+
} else if (x > weight) [from, fromOpen, to, toOpen] = [weight, false, x, true];
|
|
529
|
+
else [from, fromOpen, to, toOpen] = [x, true, HEAVIEST, false];
|
|
530
|
+
return (
|
|
531
|
+
(from > weight - near || (fromOpen && from >= weight - near)) &&
|
|
532
|
+
(to < weight + near || (toOpen && to <= weight + near))
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** a < b, comparing [width, slant, weight] ranks in turn */
|
|
537
|
+
function ranksBefore(a, b) {
|
|
538
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return a[i] < b[i];
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* The face CSS's matching (CSS Fonts 4 §5.2) picks for a pattern, given
|
|
544
|
+
* `head`, the face fontconfig picked: the head itself where it can be shown
|
|
545
|
+
* to be CSS's (`provenPick`), or where there is no census to say otherwise,
|
|
546
|
+
* and the family's best face by width, slant and weight in that order where
|
|
547
|
+
* there is one. Fontconfig picks the face nearest in weight, after the
|
|
548
|
+
* slant and before the width, so 520 in Arial was the regular, which a
|
|
549
|
+
* browser sets in the bold.
|
|
550
|
+
*
|
|
551
|
+
* The head wins ties: a face like it is fontconfig's to choose between. So
|
|
552
|
+
* does a head that is a variable font's instance where CSS picks the font —
|
|
553
|
+
* a weight its axis holds is set on the axis (`instantiate`) whichever face
|
|
554
|
+
* of the file it is opened through.
|
|
555
|
+
*
|
|
556
|
+
* The head is ranked as the census lists it, because an answer describes
|
|
557
|
+
* the face as fontconfig will render it: an upright face matched for an
|
|
558
|
+
* italic ask comes back oblique (90-synthetic.conf slants it), and ranked
|
|
559
|
+
* by that it beat its own family's upright faces — W1 of Hiragino Sans,
|
|
560
|
+
* which has no italics, at 160, where CSS takes W0.
|
|
561
|
+
*/
|
|
562
|
+
function cssFace(spec, head) {
|
|
563
|
+
if (!head || provenPick(spec.weight, spec.italic, head)) return head;
|
|
564
|
+
const faces = familyFaces(spec.names, head);
|
|
565
|
+
if (!faces || faces.length === 0) return head;
|
|
566
|
+
const ranks = (face) => {
|
|
567
|
+
const style = styleOf(face);
|
|
568
|
+
return style && [widthRank(style.width), slantRank(spec.italic, style.slant), weightRank(spec.weight, style.weight)];
|
|
569
|
+
};
|
|
570
|
+
const listed = faces.find((f) => f.path === head.path && f.postscriptName === head.postscriptName);
|
|
571
|
+
let best = null;
|
|
572
|
+
let bestRanks = ranks(listed ?? head);
|
|
573
|
+
for (const face of faces) {
|
|
574
|
+
const r = ranks(face);
|
|
575
|
+
if (r && (bestRanks === null || ranksBefore(r, bestRanks))) [best, bestRanks] = [face, r];
|
|
576
|
+
}
|
|
577
|
+
if (best === null) return head;
|
|
578
|
+
const [lo, hi] = styleOf(best).weight;
|
|
579
|
+
if (lo < hi && best.path === head.path) return head;
|
|
580
|
+
return best;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* A fallback chain with the face CSS picks at its head (`cssFace`). Where
|
|
585
|
+
* that is not fontconfig's head it is moved up from where the chain has it,
|
|
586
|
+
* or, as `-s` trims a face whose coverage the faces before it already give —
|
|
587
|
+
* a family's bold, behind its regular — put there from the census.
|
|
588
|
+
*/
|
|
589
|
+
function cssChain(spec, list) {
|
|
590
|
+
const face = cssFace(spec, list[0]);
|
|
591
|
+
if (face === list[0]) return list;
|
|
592
|
+
const same = (c) => c.path === face.path && c.postscriptName === face.postscriptName;
|
|
593
|
+
return [list.find(same) ?? face, ...list.filter((c) => !same(c))];
|
|
308
594
|
}
|
|
309
595
|
|
|
310
596
|
// fc -> { promise, base }: one child per pattern however many callers ask.
|
|
@@ -356,14 +642,18 @@ function runFcMatch(fc) {
|
|
|
356
642
|
// flag first), then a file base, a kind and a pattern for each job: `sorted`
|
|
357
643
|
// asks for the whole fallback chain, `best` for the one face fontconfig
|
|
358
644
|
// would pick, which it answers in half the time, since it sorts nothing and
|
|
359
|
-
// writes one line (`bestOfPrewarm`).
|
|
360
|
-
//
|
|
361
|
-
// fc-match
|
|
645
|
+
// writes one line (`bestOfPrewarm`). `census` lists every face, with the
|
|
646
|
+
// format in the pattern's place (`CENSUS_FORMAT`). The shell exits once
|
|
647
|
+
// every job has. Nothing but shell builtins runs besides fc-match and
|
|
648
|
+
// fc-list, so a PATH that holds those is enough; without fc-list there is no
|
|
649
|
+
// census, and fontconfig's own picks stand.
|
|
362
650
|
const PREWARM_SCRIPT = [
|
|
363
651
|
...fcMatchArgs.map((_, i) => `a${i}=\${${i + 1}}`),
|
|
364
652
|
`shift ${fcMatchArgs.length}`,
|
|
365
653
|
'while [ $# -gt 2 ]; do ' +
|
|
366
|
-
'if [ "$2" =
|
|
654
|
+
'if [ "$2" = census ]; then ' +
|
|
655
|
+
'(fc-list --format "$3" > "$1.out" 2> "$1.err"; echo $? > "$1.done") & ' +
|
|
656
|
+
'elif [ "$2" = best ]; then ' +
|
|
367
657
|
`(fc-match ${fcMatchArgs
|
|
368
658
|
.slice(1)
|
|
369
659
|
.map((_, i) => `"$a${i + 1}"`)
|
|
@@ -472,16 +762,22 @@ function removePrewarm(base) {
|
|
|
472
762
|
* set up: 634 KB apiece, most of it the coverage of every face fontconfig
|
|
473
763
|
* sorts behind the first.
|
|
474
764
|
*
|
|
765
|
+
* The first child also lists every face, once a process (the census, see
|
|
766
|
+
* `CENSUS_FORMAT`): a job of its own beside the matches, so a weight CSS
|
|
767
|
+
* matches differently from fontconfig costs no spawn of the app's.
|
|
768
|
+
*
|
|
475
769
|
* An entry a pattern, in order; null where files cannot be used, and the
|
|
476
770
|
* caller prewarms the old way.
|
|
477
771
|
*/
|
|
478
772
|
function spawnToFiles(fcs, bests = []) {
|
|
479
773
|
const cp = childProcess();
|
|
480
774
|
const all = [...fcs, ...bests];
|
|
481
|
-
const
|
|
775
|
+
const listing = census === undefined;
|
|
776
|
+
const bases = cp ? Array.from({ length: all.length + (listing ? 1 : 0) }, () => prewarmBase()) : [];
|
|
482
777
|
if (bases.length === 0 || bases[0] === null) return null;
|
|
483
778
|
const jobs = [];
|
|
484
779
|
all.forEach((fc, i) => jobs.push(bases[i], i < fcs.length ? 'sorted' : 'best', fc));
|
|
780
|
+
if (listing) jobs.push(bases[all.length], 'census', CENSUS_FORMAT);
|
|
485
781
|
let child;
|
|
486
782
|
try {
|
|
487
783
|
child = cp.spawn('/bin/sh', ['-c', PREWARM_SCRIPT, 'ntk-fc-match', ...fcMatchArgs, ...jobs], {
|
|
@@ -492,14 +788,22 @@ function spawnToFiles(fcs, bests = []) {
|
|
|
492
788
|
}
|
|
493
789
|
// `promise` resolves once the child is gone, with what it left in the
|
|
494
790
|
// files; `exited` tells a synchronous caller there is nothing to wait for
|
|
495
|
-
const
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
|
|
791
|
+
const entry = (base) => {
|
|
792
|
+
const e = { promise: null, base, answer: undefined, exited: false, gone: null };
|
|
793
|
+
e.promise = new Promise((resolve) => {
|
|
794
|
+
e.gone = resolve;
|
|
499
795
|
});
|
|
500
|
-
|
|
501
|
-
|
|
796
|
+
return e;
|
|
797
|
+
};
|
|
798
|
+
const entries = all.map((fc, i) => {
|
|
799
|
+
const e = entry(bases[i]);
|
|
800
|
+
(i < fcs.length ? inflight : bestInflight).set(fc, e);
|
|
801
|
+
return e;
|
|
502
802
|
});
|
|
803
|
+
if (listing) {
|
|
804
|
+
census = Object.assign(entry(bases[all.length]), { bytes: undefined, faces: new Map() });
|
|
805
|
+
entries.push(census);
|
|
806
|
+
}
|
|
503
807
|
const gone = () => {
|
|
504
808
|
for (const entry of entries) {
|
|
505
809
|
entry.exited = true;
|
|
@@ -551,8 +855,10 @@ function answerSync(fc, entry) {
|
|
|
551
855
|
* layout setting text in the face needs its first line. The rest waits in
|
|
552
856
|
* the file for the first character that falls back, which Latin text in a
|
|
553
857
|
* face that covers it never has.
|
|
858
|
+
*
|
|
859
|
+
* The face is the one CSS picks (`cssFace`), as the chain's head will be.
|
|
554
860
|
*/
|
|
555
|
-
function firstOfPrewarm(entry) {
|
|
861
|
+
function firstOfPrewarm(spec, entry) {
|
|
556
862
|
if (entry.first !== undefined) return entry.first;
|
|
557
863
|
const deadline = performance.now() + PREWARM_WAIT_MS;
|
|
558
864
|
let status = prewarmStatus(entry.base);
|
|
@@ -562,7 +868,7 @@ function firstOfPrewarm(entry) {
|
|
|
562
868
|
}
|
|
563
869
|
// not answered, or not yet: the whole read is what decides, and reports
|
|
564
870
|
if (status !== 0) return null;
|
|
565
|
-
entry.first = firstCandidate(`${entry.base}.out`);
|
|
871
|
+
entry.first = cssFace(spec, firstCandidate(`${entry.base}.out`));
|
|
566
872
|
return entry.first;
|
|
567
873
|
}
|
|
568
874
|
|
|
@@ -576,9 +882,10 @@ function firstOfPrewarm(entry) {
|
|
|
576
882
|
* in about half the time the whole chain takes (15 ms against 30 on a Linux
|
|
577
883
|
* desktop): no sort, one line. A layout that asks for a family warmed while
|
|
578
884
|
* its component rendered waits on the prewarm, and the chain was most of
|
|
579
|
-
* that wait.
|
|
885
|
+
* that wait. Fontconfig's face, that is: the one CSS picks may be another
|
|
886
|
+
* (`cssFace`).
|
|
580
887
|
*/
|
|
581
|
-
function bestOfPrewarm(entry) {
|
|
888
|
+
function bestOfPrewarm(spec, entry) {
|
|
582
889
|
if (entry.first !== undefined) return entry.first;
|
|
583
890
|
const deadline = performance.now() + PREWARM_WAIT_MS;
|
|
584
891
|
let left = readPrewarm(entry.base);
|
|
@@ -589,7 +896,7 @@ function bestOfPrewarm(entry) {
|
|
|
589
896
|
if (left === null) return null;
|
|
590
897
|
removePrewarm(entry.base);
|
|
591
898
|
const [best] = left.status === 0 ? parseMatches(left.out) : [];
|
|
592
|
-
entry.first = best
|
|
899
|
+
entry.first = best ? cssFace(spec, best) : null;
|
|
593
900
|
return entry.first;
|
|
594
901
|
}
|
|
595
902
|
|
|
@@ -665,7 +972,7 @@ function firstCandidate(file) {
|
|
|
665
972
|
* attempt abandoned
|
|
666
973
|
*/
|
|
667
974
|
export function prewarm(pattern = {}) {
|
|
668
|
-
return warm([
|
|
975
|
+
return warm([specFor(pattern)])[0];
|
|
669
976
|
}
|
|
670
977
|
|
|
671
978
|
/**
|
|
@@ -677,36 +984,39 @@ export function prewarm(pattern = {}) {
|
|
|
677
984
|
* up on; never rejects
|
|
678
985
|
*/
|
|
679
986
|
export function prewarmPatterns(patterns) {
|
|
680
|
-
return Promise.all(warm(patterns.map(
|
|
987
|
+
return Promise.all(warm(patterns.map(specFor))).then(() => {});
|
|
681
988
|
}
|
|
682
989
|
|
|
683
990
|
/**
|
|
684
|
-
* `prewarm` for a list of
|
|
991
|
+
* `prewarm` for a list of patterns (`specFor`): the ones neither cached nor
|
|
685
992
|
* already running start together, in one child (`spawnToFiles`). A promise a
|
|
686
993
|
* pattern, resolving once its answer is ready or given up on; none rejects.
|
|
687
994
|
*/
|
|
688
|
-
function warm(
|
|
689
|
-
if (unavailable) return
|
|
995
|
+
function warm(specs, best = null) {
|
|
996
|
+
if (unavailable) return specs.map(() => Promise.resolve());
|
|
997
|
+
const fcs = specs.map((spec) => spec.fc);
|
|
690
998
|
const fresh = [...new Set(fcs)].filter((fc) => !sortedCache.has(fc) && !inflight.has(fc));
|
|
691
999
|
// the best face only beside its own chain: once that is running or read,
|
|
692
1000
|
// the chain's head answers as soon as a best job would
|
|
693
1001
|
const bests = best !== null && fresh.includes(best) && !bestInflight.has(best) ? [best] : [];
|
|
694
1002
|
if (fresh.length > 0) spawnToFiles(fresh, bests);
|
|
695
|
-
return
|
|
1003
|
+
return specs.map((spec) => {
|
|
1004
|
+
const { fc } = spec;
|
|
696
1005
|
if (sortedCache.has(fc)) return Promise.resolve();
|
|
697
1006
|
// a prewarm's answer waits in its files for the first ask
|
|
698
1007
|
const running = inflight.get(fc);
|
|
699
1008
|
if (running?.base) return running.promise;
|
|
700
1009
|
const pending = running?.promise ?? runFcMatch(fc);
|
|
701
|
-
return pending
|
|
702
|
-
(out) => {
|
|
1010
|
+
return pending
|
|
1011
|
+
.then(async (out) => {
|
|
1012
|
+
// the census decides the head: read once it is in, never waited on
|
|
1013
|
+
await censusSettled();
|
|
703
1014
|
if (!sortedCache.has(fc)) {
|
|
704
1015
|
const list = parseMatches(out);
|
|
705
|
-
if (list.length > 0) sortedCache.set(fc, list);
|
|
1016
|
+
if (list.length > 0) sortedCache.set(fc, cssChain(spec, list));
|
|
706
1017
|
}
|
|
707
|
-
}
|
|
708
|
-
() => {}
|
|
709
|
-
);
|
|
1018
|
+
})
|
|
1019
|
+
.catch(() => {});
|
|
710
1020
|
});
|
|
711
1021
|
}
|
|
712
1022
|
|
|
@@ -736,7 +1046,7 @@ export function prewarmFaces(pattern = {}) {
|
|
|
736
1046
|
// the one being asked for, or, warmed ahead, the regular
|
|
737
1047
|
const first = patternFor(pattern.weight === undefined && pattern.style === undefined ? { family, weight: 400, style: 'normal' } : pattern);
|
|
738
1048
|
warm(
|
|
739
|
-
FACES.map(([weight, style]) =>
|
|
1049
|
+
FACES.map(([weight, style]) => specFor({ family, weight, style })),
|
|
740
1050
|
first
|
|
741
1051
|
);
|
|
742
1052
|
}
|
|
@@ -757,14 +1067,18 @@ export function prewarmFaces(pattern = {}) {
|
|
|
757
1067
|
* sync throws do.
|
|
758
1068
|
*
|
|
759
1069
|
* @returns {Promise<Array<{path, postscriptName, family: string,
|
|
760
|
-
* families: string[], charset: string}>>}
|
|
1070
|
+
* families: string[], charset: string|null}>>}
|
|
761
1071
|
*/
|
|
762
1072
|
export async function matchSorted(pattern = {}) {
|
|
763
|
-
const
|
|
1073
|
+
const spec = specFor(pattern);
|
|
1074
|
+
const { fc } = spec;
|
|
764
1075
|
const cached = sortedCache.get(fc);
|
|
765
1076
|
if (cached) return cached;
|
|
766
1077
|
if (unavailable) throw noFontsError(unavailable);
|
|
767
1078
|
|
|
1079
|
+
// A caller that awaits may be the first to ask anything — a font picker
|
|
1080
|
+
// with no source constructed — and nothing else would start the census
|
|
1081
|
+
if (census === undefined) spawnToFiles([]);
|
|
768
1082
|
let out;
|
|
769
1083
|
const pending = inflight.get(fc);
|
|
770
1084
|
if (pending?.base) {
|
|
@@ -780,17 +1094,22 @@ export async function matchSorted(pattern = {}) {
|
|
|
780
1094
|
throw fcMatchError(err);
|
|
781
1095
|
}
|
|
782
1096
|
}
|
|
1097
|
+
// the census decides the head where fontconfig's own cannot be shown to be
|
|
1098
|
+
// CSS's (`cssChain`): read once it is in, so that this never waits on it
|
|
1099
|
+
await censusSettled();
|
|
783
1100
|
// A sync call may have answered this pattern while the child ran. Its list
|
|
784
1101
|
// is the cached one, and candidates memoize their parsed charset, so hand
|
|
785
1102
|
// back what everyone else already holds rather than a fresh copy.
|
|
786
|
-
return sortedCache.get(fc) ?? cacheMatches(
|
|
1103
|
+
return sortedCache.get(fc) ?? cacheMatches(spec, out);
|
|
787
1104
|
}
|
|
788
1105
|
|
|
789
1106
|
/**
|
|
790
1107
|
* Full fontconfig match list for a pattern, best match first — this is the
|
|
791
|
-
* system's font fallback chain
|
|
792
|
-
*
|
|
793
|
-
*
|
|
1108
|
+
* system's font fallback chain, headed by the face CSS's font matching picks
|
|
1109
|
+
* (`cssChain`). Each candidate carries the unicode coverage fontconfig knows
|
|
1110
|
+
* about (`charset`, lazily parsed via `charsetHas`; null for a head the
|
|
1111
|
+
* chain did not have), so a fallback font for a codepoint can be chosen
|
|
1112
|
+
* without opening font files —
|
|
794
1113
|
* and the family name fontconfig already knows, so a *list* of matches can be
|
|
795
1114
|
* shown without opening them either (issue #273: `sans-serif` returns 139
|
|
796
1115
|
* candidates here, and `Font.loadSync` is ~1.2ms a file).
|
|
@@ -800,10 +1119,11 @@ export async function matchSorted(pattern = {}) {
|
|
|
800
1119
|
* a caller that can should use `matchSorted` and not block on the spawn.
|
|
801
1120
|
*
|
|
802
1121
|
* @returns {Array<{path, postscriptName, family: string, families: string[],
|
|
803
|
-
* charset: string}>}
|
|
1122
|
+
* charset: string|null}>}
|
|
804
1123
|
*/
|
|
805
1124
|
export function matchSortedSync(pattern) {
|
|
806
|
-
const
|
|
1125
|
+
const spec = specFor(pattern);
|
|
1126
|
+
const { fc } = spec;
|
|
807
1127
|
const cached = sortedCache.get(fc);
|
|
808
1128
|
if (cached) return cached;
|
|
809
1129
|
if (unavailable) throw noFontsError(unavailable);
|
|
@@ -814,7 +1134,7 @@ export function matchSortedSync(pattern) {
|
|
|
814
1134
|
const pending = inflight.get(fc);
|
|
815
1135
|
if (pending?.base) {
|
|
816
1136
|
const out = answerSync(fc, pending);
|
|
817
|
-
if (out !== null) return sortedCache.get(fc) ?? cacheMatches(
|
|
1137
|
+
if (out !== null) return sortedCache.get(fc) ?? cacheMatches(spec, out);
|
|
818
1138
|
}
|
|
819
1139
|
let out;
|
|
820
1140
|
try {
|
|
@@ -822,7 +1142,7 @@ export function matchSortedSync(pattern) {
|
|
|
822
1142
|
} catch (err) {
|
|
823
1143
|
throw fcMatchError(err);
|
|
824
1144
|
}
|
|
825
|
-
return cacheMatches(
|
|
1145
|
+
return cacheMatches(spec, out);
|
|
826
1146
|
}
|
|
827
1147
|
|
|
828
1148
|
/**
|
|
@@ -832,22 +1152,23 @@ export function matchSortedSync(pattern) {
|
|
|
832
1152
|
* the first character it has to fall back for reads the chain whole.
|
|
833
1153
|
*
|
|
834
1154
|
* @returns {{path, postscriptName, family: string, families: string[],
|
|
835
|
-
* charset: string}}
|
|
1155
|
+
* charset: string|null}}
|
|
836
1156
|
*/
|
|
837
1157
|
export function matchFirstSync(pattern) {
|
|
838
|
-
const
|
|
1158
|
+
const spec = specFor(pattern);
|
|
1159
|
+
const { fc } = spec;
|
|
839
1160
|
const cached = sortedCache.get(fc);
|
|
840
1161
|
if (cached) return cached[0];
|
|
841
1162
|
if (unavailable) throw noFontsError(unavailable);
|
|
842
1163
|
prewarmFaces(pattern);
|
|
843
1164
|
const best = bestInflight.get(fc);
|
|
844
1165
|
if (best) {
|
|
845
|
-
const first = bestOfPrewarm(best);
|
|
1166
|
+
const first = bestOfPrewarm(spec, best);
|
|
846
1167
|
if (first) return first;
|
|
847
1168
|
}
|
|
848
1169
|
const pending = inflight.get(fc);
|
|
849
1170
|
if (pending?.base) {
|
|
850
|
-
const first = firstOfPrewarm(pending);
|
|
1171
|
+
const first = firstOfPrewarm(spec, pending);
|
|
851
1172
|
if (first) return first;
|
|
852
1173
|
}
|
|
853
1174
|
return matchSortedSync(pattern)[0];
|
|
@@ -873,8 +1194,12 @@ export function listFontsSync(pattern) {
|
|
|
873
1194
|
/**
|
|
874
1195
|
* Does a fc-match candidate's charset cover a codepoint?
|
|
875
1196
|
* The charset string is fontconfig's range format: "20-7e a0-ff 131 ...".
|
|
1197
|
+
* A face whose coverage is not known here — one CSS's matching put at a
|
|
1198
|
+
* chain's head, which `-s` had trimmed (`cssChain`) — may: it is opened and
|
|
1199
|
+
* the font asked.
|
|
876
1200
|
*/
|
|
877
1201
|
export function charsetHas(candidate, codepoint) {
|
|
1202
|
+
if (candidate.charset === null) return true;
|
|
878
1203
|
if (candidate._ranges === null) {
|
|
879
1204
|
const ranges = [];
|
|
880
1205
|
for (const part of candidate.charset.split(' ')) {
|
|
@@ -903,15 +1228,18 @@ export function charsetHas(candidate, codepoint) {
|
|
|
903
1228
|
|
|
904
1229
|
/**
|
|
905
1230
|
* A CSS font weight on fontconfig's scale, which is not CSS's: regular is 80
|
|
906
|
-
* there and black is 210. The
|
|
907
|
-
* of
|
|
908
|
-
* from regular to medium. CSS takes 1 to 1000
|
|
909
|
-
* is its nearest end.
|
|
1231
|
+
* there and black is 210. The table is fontconfig's own, and a weight between
|
|
1232
|
+
* two of its entries is as far between their fontconfig weights, so 450 is
|
|
1233
|
+
* 90, halfway from regular to medium. CSS takes 1 to 1000.
|
|
910
1234
|
*
|
|
911
1235
|
* Handed over as it was, 450 was a weight past twice black, and fontconfig
|
|
912
1236
|
* answered with the heaviest face a family has: Arial Bold, where a browser
|
|
913
|
-
* sets 450 in Arial Regular.
|
|
914
|
-
*
|
|
1237
|
+
* sets 450 in Arial Regular.
|
|
1238
|
+
*
|
|
1239
|
+
* Fontconfig's table rather than one of the hundreds alone, because it is
|
|
1240
|
+
* the one a face's own weight went through: a face whose OS/2 weight is 350
|
|
1241
|
+
* is 55 in every fc-match answer, and only the same table puts a pattern
|
|
1242
|
+
* asking for 340 on the lighter side of it (`cssFace`).
|
|
915
1243
|
*
|
|
916
1244
|
* A whole number, because a pattern is a cache key and an fc-match of its
|
|
917
1245
|
* own: the weights an axis is animated through then share the ones they
|
|
@@ -919,13 +1247,14 @@ export function charsetHas(candidate, codepoint) {
|
|
|
919
1247
|
*/
|
|
920
1248
|
function normalizeWeight(weight) {
|
|
921
1249
|
if (weight === undefined) return undefined;
|
|
922
|
-
if (weight === 'normal') return
|
|
923
|
-
if (weight === 'bold') return
|
|
1250
|
+
if (weight === 'normal') return REGULAR;
|
|
1251
|
+
if (weight === 'bold') return 200;
|
|
924
1252
|
const n = parseInt(weight, 10);
|
|
925
1253
|
if (Number.isNaN(n)) return undefined;
|
|
926
|
-
const css = Math.min(
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1254
|
+
const css = Math.min(1000, Math.max(1, n));
|
|
1255
|
+
let i = 1;
|
|
1256
|
+
while (css > cssToFcWeight[i][0]) i++;
|
|
1257
|
+
const [lighterCss, lighter] = cssToFcWeight[i - 1];
|
|
1258
|
+
const [heavierCss, heavier] = cssToFcWeight[i];
|
|
1259
|
+
return Math.round(lighter + ((heavier - lighter) * (css - lighterCss)) / (heavierCss - lighterCss));
|
|
931
1260
|
}
|
|
@@ -387,15 +387,52 @@ function patternSourceOf(app, source) {
|
|
|
387
387
|
* after a `translate` moves with the fill, and one created under a translate
|
|
388
388
|
* and filled without it does not).
|
|
389
389
|
*
|
|
390
|
-
* Returns false when nothing would be painted (a singular matrix
|
|
391
|
-
* anything else, including every
|
|
390
|
+
* Returns false when nothing would be painted (a singular matrix, or one
|
|
391
|
+
* the wire cannot carry), true for anything else, including every
|
|
392
|
+
* plain-colour style. `width` and `height` are the surface's: the style's
|
|
393
|
+
* source origin is kept on it (`sourceOrigin`).
|
|
392
394
|
*/
|
|
393
|
-
function prepareStyle(src, m) {
|
|
395
|
+
function prepareStyle(src, m, width, height) {
|
|
394
396
|
return src instanceof CanvasPattern || src instanceof CanvasGradient
|
|
395
|
-
? src._sync(m)
|
|
397
|
+
? src._sync(m, width, height)
|
|
396
398
|
: true;
|
|
397
399
|
}
|
|
398
400
|
|
|
401
|
+
const NO_ORIGIN = Object.freeze([0, 0]);
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Where a composite samples `src` from, as a device point: each composite
|
|
405
|
+
* that reads a style at its destination's own coordinates passes those
|
|
406
|
+
* coordinates less this as its source offset.
|
|
407
|
+
*
|
|
408
|
+
* A gradient's or a pattern's picture transform is the inverse of the CTM,
|
|
409
|
+
* and sampling it at device coordinates put the CTM's translation, times
|
|
410
|
+
* its downscale, into the transform: a box a fortieth of its size at x
|
|
411
|
+
* 2,200 asked for 88,000, past what 16.16 fixed point carries. `_sync`
|
|
412
|
+
* folds this origin into the transform instead, and puts it where the
|
|
413
|
+
* style's own origin lands, so the translation is small wherever on the
|
|
414
|
+
* surface the style is painted. Everything else — a solid, a picture —
|
|
415
|
+
* samples at device coordinates, from (0, 0).
|
|
416
|
+
*/
|
|
417
|
+
function sourceOrigin(src) {
|
|
418
|
+
return src instanceof CanvasPattern || src instanceof CanvasGradient
|
|
419
|
+
? src._origin
|
|
420
|
+
: NO_ORIGIN;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* The origin `_sync` picks for a style whose own origin lands at device
|
|
425
|
+
* (x, y): the nearest whole pixel, kept on the surface. On it, a composite's
|
|
426
|
+
* source offset — its destination's, less this — is within the surface's
|
|
427
|
+
* size of 0, which the 16-bit field carries whatever the transform; and the
|
|
428
|
+
* transform's translation is the style's coordinate there, which for a
|
|
429
|
+
* style painted on the surface is one the fill samples anyway.
|
|
430
|
+
*/
|
|
431
|
+
function originOn(x, y, width, height) {
|
|
432
|
+
const on = (v, size) => (v > 0 ? Math.min(Math.round(v), size) : 0);
|
|
433
|
+
return [on(x, width), on(y, height)];
|
|
434
|
+
}
|
|
435
|
+
|
|
399
436
|
/**
|
|
400
437
|
* Whether every entry can be written as XRender's 16.16 fixed point, whose
|
|
401
438
|
* whole part is a signed 16-bit number. A picture transform is written that
|
|
@@ -2387,14 +2424,15 @@ class RenderingContext2d {
|
|
|
2387
2424
|
);
|
|
2388
2425
|
}
|
|
2389
2426
|
// src is either a 1x1 repeating solid (offset irrelevant) or a
|
|
2390
|
-
//
|
|
2427
|
+
// gradient/pattern, sampled at the same offset from its own origin
|
|
2428
|
+
const [ox, oy] = sourceOrigin(src);
|
|
2391
2429
|
R.Composite(
|
|
2392
2430
|
op,
|
|
2393
2431
|
src.id,
|
|
2394
2432
|
this.fillMask.id,
|
|
2395
2433
|
this._dst(),
|
|
2396
|
-
out.x,
|
|
2397
|
-
out.y,
|
|
2434
|
+
out.x - ox,
|
|
2435
|
+
out.y - oy,
|
|
2398
2436
|
out.x,
|
|
2399
2437
|
out.y,
|
|
2400
2438
|
out.x,
|
|
@@ -2417,7 +2455,7 @@ class RenderingContext2d {
|
|
|
2417
2455
|
op = op ?? this._op();
|
|
2418
2456
|
alpha = alpha ?? this.globalAlpha;
|
|
2419
2457
|
if (alpha <= 0) return;
|
|
2420
|
-
if (!prepareStyle(src, this._m)) return;
|
|
2458
|
+
if (!prepareStyle(src, this._m, this.width, this.height)) return;
|
|
2421
2459
|
|
|
2422
2460
|
// one box per subpath, so a path holding disjoint ones can be masked as
|
|
2423
2461
|
// the pieces it is rather than as the box around all of them — each the
|
|
@@ -2473,7 +2511,7 @@ class RenderingContext2d {
|
|
|
2473
2511
|
_strokePolys(polys, { src = null } = {}) {
|
|
2474
2512
|
src = src ?? this._strokePicture;
|
|
2475
2513
|
if (this.globalAlpha <= 0) return;
|
|
2476
|
-
if (!prepareStyle(src, this._m)) return;
|
|
2514
|
+
if (!prepareStyle(src, this._m, this.width, this.height)) return;
|
|
2477
2515
|
// approximate transform-aware line width by the average scale factor
|
|
2478
2516
|
const det = this._m[0] * this._m[3] - this._m[1] * this._m[2];
|
|
2479
2517
|
const scale = Math.sqrt(Math.abs(det)) || 1;
|
|
@@ -2841,6 +2879,7 @@ class RenderingContext2d {
|
|
|
2841
2879
|
op === this.Render.PictOp.Over;
|
|
2842
2880
|
const chunk = 4000 * 6;
|
|
2843
2881
|
if (direct) {
|
|
2882
|
+
const [ox, oy] = sourceOrigin(src);
|
|
2844
2883
|
this._cullTris(tris);
|
|
2845
2884
|
if (!tris.length) return;
|
|
2846
2885
|
const cut = this._wireCut(tris);
|
|
@@ -2858,11 +2897,12 @@ class RenderingContext2d {
|
|
|
2858
2897
|
// convention drawGlyphRuns and compositeTraps use — without it every
|
|
2859
2898
|
// non-constant stroke style (a gradient, a pattern) is offset by
|
|
2860
2899
|
// wherever the stroke happens to start, and shifts as it moves.
|
|
2900
|
+
// Less the style's own origin, as every fill samples it.
|
|
2861
2901
|
this.Render.Triangles(
|
|
2862
2902
|
op,
|
|
2863
2903
|
src.id,
|
|
2864
|
-
Math.floor(batch[0]),
|
|
2865
|
-
Math.floor(batch[1]),
|
|
2904
|
+
Math.floor(batch[0]) - ox,
|
|
2905
|
+
Math.floor(batch[1]) - oy,
|
|
2866
2906
|
this._dst(),
|
|
2867
2907
|
this.Render.a8,
|
|
2868
2908
|
batch,
|
|
@@ -3128,7 +3168,7 @@ class RenderingContext2d {
|
|
|
3128
3168
|
_drawGlyphsDevice(op, src, positioned) {
|
|
3129
3169
|
const app = this.window.app;
|
|
3130
3170
|
const R = this.Render;
|
|
3131
|
-
if (!prepareStyle(src, this._m)) return;
|
|
3171
|
+
if (!prepareStyle(src, this._m, this.width, this.height)) return;
|
|
3132
3172
|
if (!this._hasPolyClip) {
|
|
3133
3173
|
// Fast path: a rectangular clip is something the server can do itself.
|
|
3134
3174
|
// Two small requests around the ordinary glyph composite, instead of
|
|
@@ -3148,6 +3188,7 @@ class RenderingContext2d {
|
|
|
3148
3188
|
this._dst(),
|
|
3149
3189
|
positioned,
|
|
3150
3190
|
rect ?? { x: 0, y: 0, w: this.width, h: this.height },
|
|
3191
|
+
sourceOrigin(src),
|
|
3151
3192
|
);
|
|
3152
3193
|
if (rect) this._invalidatePictureClip();
|
|
3153
3194
|
this._markDirty();
|
|
@@ -3185,13 +3226,14 @@ class RenderingContext2d {
|
|
|
3185
3226
|
this.width,
|
|
3186
3227
|
this.height,
|
|
3187
3228
|
);
|
|
3229
|
+
const [ox, oy] = sourceOrigin(src);
|
|
3188
3230
|
R.Composite(
|
|
3189
3231
|
op,
|
|
3190
3232
|
src.id,
|
|
3191
3233
|
this.fillMask.id,
|
|
3192
3234
|
this._dst(),
|
|
3193
|
-
|
|
3194
|
-
|
|
3235
|
+
-ox,
|
|
3236
|
+
-oy,
|
|
3195
3237
|
0,
|
|
3196
3238
|
0,
|
|
3197
3239
|
0,
|
|
@@ -3218,7 +3260,16 @@ class RenderingContext2d {
|
|
|
3218
3260
|
if (rect.w === 0 || rect.h === 0) return;
|
|
3219
3261
|
this._setPictureClip(rect);
|
|
3220
3262
|
}
|
|
3221
|
-
compositeTraps(
|
|
3263
|
+
compositeTraps(
|
|
3264
|
+
app,
|
|
3265
|
+
op,
|
|
3266
|
+
src.id,
|
|
3267
|
+
this._dst(),
|
|
3268
|
+
traps,
|
|
3269
|
+
undefined,
|
|
3270
|
+
undefined,
|
|
3271
|
+
sourceOrigin(src),
|
|
3272
|
+
);
|
|
3222
3273
|
if (rect) this._invalidatePictureClip();
|
|
3223
3274
|
this._markDirty();
|
|
3224
3275
|
return;
|
|
@@ -3253,13 +3304,14 @@ class RenderingContext2d {
|
|
|
3253
3304
|
this.width,
|
|
3254
3305
|
this.height,
|
|
3255
3306
|
);
|
|
3307
|
+
const [ox, oy] = sourceOrigin(src);
|
|
3256
3308
|
R.Composite(
|
|
3257
3309
|
op,
|
|
3258
3310
|
src.id,
|
|
3259
3311
|
this.fillMask.id,
|
|
3260
3312
|
this._dst(),
|
|
3261
|
-
|
|
3262
|
-
|
|
3313
|
+
-ox,
|
|
3314
|
+
-oy,
|
|
3263
3315
|
0,
|
|
3264
3316
|
0,
|
|
3265
3317
|
0,
|
|
@@ -3497,7 +3549,8 @@ class RenderingContext2d {
|
|
|
3497
3549
|
w = x1 - x0;
|
|
3498
3550
|
h = y1 - y0;
|
|
3499
3551
|
}
|
|
3500
|
-
|
|
3552
|
+
const style = this._backgroundPicture;
|
|
3553
|
+
if (!prepareStyle(style, this._m, this.width, this.height)) return;
|
|
3501
3554
|
const op = this._op();
|
|
3502
3555
|
// A rectangular clip is a smaller rectangle to fill, not a mask — this
|
|
3503
3556
|
// was the one drawing path that still built a surface-sized a8 for one
|
|
@@ -3509,13 +3562,14 @@ class RenderingContext2d {
|
|
|
3509
3562
|
w > 0 && h > 0 ? this._boxedComposite({ x, y, w, h }, op) : null;
|
|
3510
3563
|
if (direct && !direct.box) return; // the clip rejects the fill whole
|
|
3511
3564
|
const box = direct ? direct.box : { x, y, w, h };
|
|
3565
|
+
const [ox, oy] = sourceOrigin(style);
|
|
3512
3566
|
this.Render.Composite(
|
|
3513
3567
|
op,
|
|
3514
|
-
|
|
3568
|
+
style.id,
|
|
3515
3569
|
direct ? direct.mask : this._compositeMask(box),
|
|
3516
3570
|
this._dst(),
|
|
3517
|
-
box.x,
|
|
3518
|
-
box.y,
|
|
3571
|
+
box.x - ox,
|
|
3572
|
+
box.y - oy,
|
|
3519
3573
|
box.x,
|
|
3520
3574
|
box.y,
|
|
3521
3575
|
box.x,
|
|
@@ -3667,7 +3721,8 @@ class RenderingContext2d {
|
|
|
3667
3721
|
if (y + h > y1) y1 = y + h;
|
|
3668
3722
|
}
|
|
3669
3723
|
if (!disjointRects(rects)) return false;
|
|
3670
|
-
|
|
3724
|
+
const style = this._backgroundPicture;
|
|
3725
|
+
if (!prepareStyle(style, this._m, this.width, this.height)) return true;
|
|
3671
3726
|
// the box they cover, on the surface: nothing outside it is sampled
|
|
3672
3727
|
const bx = Math.max(0, x0);
|
|
3673
3728
|
const by = Math.max(0, y0);
|
|
@@ -3701,13 +3756,14 @@ class RenderingContext2d {
|
|
|
3701
3756
|
bw,
|
|
3702
3757
|
bh,
|
|
3703
3758
|
);
|
|
3759
|
+
const [ox, oy] = sourceOrigin(style);
|
|
3704
3760
|
R.Composite(
|
|
3705
3761
|
op,
|
|
3706
|
-
|
|
3762
|
+
style.id,
|
|
3707
3763
|
mask,
|
|
3708
3764
|
this._dst(),
|
|
3709
|
-
bx,
|
|
3710
|
-
by,
|
|
3765
|
+
bx - ox,
|
|
3766
|
+
by - oy,
|
|
3711
3767
|
bx,
|
|
3712
3768
|
by,
|
|
3713
3769
|
bx,
|
|
@@ -5156,7 +5212,8 @@ class RenderingContext2d {
|
|
|
5156
5212
|
const R = this.Render;
|
|
5157
5213
|
// the coverage is painted in the current fillStyle, on both branches
|
|
5158
5214
|
// below — a gradient/pattern whose transform collapsed paints nothing
|
|
5159
|
-
|
|
5215
|
+
const style = this._backgroundPicture;
|
|
5216
|
+
if (!prepareStyle(style, this._m, this.width, this.height)) return;
|
|
5160
5217
|
const scaled = dw !== sw || dh !== sh;
|
|
5161
5218
|
if (scaled) {
|
|
5162
5219
|
R.SetPictureTransform(picture.id, [
|
|
@@ -5246,18 +5303,19 @@ class RenderingContext2d {
|
|
|
5246
5303
|
this._setPictureClip(rect);
|
|
5247
5304
|
}
|
|
5248
5305
|
// The scratch mask is surface-sized and surface-aligned — the coverage
|
|
5249
|
-
// went into it at (dx, dy) —
|
|
5250
|
-
//
|
|
5251
|
-
// dw x dh window out of the wrong
|
|
5252
|
-
//
|
|
5253
|
-
// all unless dx and dy are both zero.
|
|
5306
|
+
// went into it at (dx, dy) — so it is sampled at the destination
|
|
5307
|
+
// offset, and a gradient or pattern at that offset from its origin.
|
|
5308
|
+
// Reading the mask from (0, 0) draws a dw x dh window out of the wrong
|
|
5309
|
+
// part of a full-surface picture: the region just cleared to zero, so
|
|
5310
|
+
// nothing paints at all unless dx and dy are both zero.
|
|
5311
|
+
const [ox, oy] = sourceOrigin(style);
|
|
5254
5312
|
R.Composite(
|
|
5255
5313
|
op,
|
|
5256
|
-
|
|
5314
|
+
style.id,
|
|
5257
5315
|
this.fillMask.id,
|
|
5258
5316
|
this._dst(),
|
|
5259
|
-
dx,
|
|
5260
|
-
dy,
|
|
5317
|
+
dx - ox,
|
|
5318
|
+
dy - oy,
|
|
5261
5319
|
dx,
|
|
5262
5320
|
dy,
|
|
5263
5321
|
dx,
|
|
@@ -5269,15 +5327,17 @@ class RenderingContext2d {
|
|
|
5269
5327
|
} else if (!rect || (rect.w > 0 && rect.h > 0)) {
|
|
5270
5328
|
if (rect) this._setPictureClip(rect);
|
|
5271
5329
|
// the source is a 1x1 repeating solid (offset irrelevant) or the fill
|
|
5272
|
-
//
|
|
5273
|
-
//
|
|
5330
|
+
// style, sampled at the destination's offset from its own origin,
|
|
5331
|
+
// exactly as in _fillPolys
|
|
5332
|
+
const src = this._coverageSource();
|
|
5333
|
+
const [ox, oy] = sourceOrigin(src);
|
|
5274
5334
|
R.Composite(
|
|
5275
5335
|
op,
|
|
5276
|
-
|
|
5336
|
+
src.id,
|
|
5277
5337
|
picture.id,
|
|
5278
5338
|
this._dst(),
|
|
5279
|
-
dx,
|
|
5280
|
-
dy,
|
|
5339
|
+
dx - ox,
|
|
5340
|
+
dy - oy,
|
|
5281
5341
|
mx,
|
|
5282
5342
|
my,
|
|
5283
5343
|
dx,
|
|
@@ -5618,6 +5678,9 @@ class CanvasGradient {
|
|
|
5618
5678
|
// what the server currently holds: a fresh gradient picture is
|
|
5619
5679
|
// untransformed, so an untransformed fill costs no extra request
|
|
5620
5680
|
this._applied = [1, 0, 0, 1, 0, 0];
|
|
5681
|
+
// the device point fills sample it from (`sourceOrigin`), set with the
|
|
5682
|
+
// transform it belongs with
|
|
5683
|
+
this._origin = NO_ORIGIN;
|
|
5621
5684
|
|
|
5622
5685
|
this.x0 = p0;
|
|
5623
5686
|
this.y0 = p1;
|
|
@@ -5707,13 +5770,14 @@ class CanvasGradient {
|
|
|
5707
5770
|
/**
|
|
5708
5771
|
* Make the server-side mapping match the CTM this paint runs under. The
|
|
5709
5772
|
* gradient's own coordinates are user space and every fill samples the
|
|
5710
|
-
* source at device coordinates
|
|
5711
|
-
* source coordinate to a gradient one — is the CTM's
|
|
5773
|
+
* source at device coordinates less `_origin`, so the picture transform —
|
|
5774
|
+
* which takes a source coordinate to a gradient one — is the CTM's
|
|
5775
|
+
* inverse, from that origin.
|
|
5712
5776
|
*
|
|
5713
5777
|
* Returns false when the CTM collapses (a zero scale), which paints
|
|
5714
5778
|
* nothing, exactly as the canvas spec says.
|
|
5715
5779
|
*/
|
|
5716
|
-
_sync(ctm) {
|
|
5780
|
+
_sync(ctm, width = 0, height = 0) {
|
|
5717
5781
|
let inv = matInvert(ctm);
|
|
5718
5782
|
if (!inv) return false;
|
|
5719
5783
|
const id = this.id; // lazily creates the picture
|
|
@@ -5721,9 +5785,13 @@ class CanvasGradient {
|
|
|
5721
5785
|
// much nearer its origin
|
|
5722
5786
|
const k = this._scale;
|
|
5723
5787
|
if (k !== 1) inv = inv.map((v) => v / k);
|
|
5788
|
+
// sampled from where user space's origin lands (`sourceOrigin`)
|
|
5789
|
+
const origin = originOn(ctm[4], ctm[5], width, height);
|
|
5790
|
+
inv = matMultiply(inv, [1, 0, 0, 1, origin[0], origin[1]]);
|
|
5724
5791
|
// the transform is 16.16 fixed point too: one that cannot be carried
|
|
5725
5792
|
// fills nothing, rather than throwing the rest of the paint away
|
|
5726
5793
|
if (!fitsFixed(inv)) return false;
|
|
5794
|
+
this._origin = origin;
|
|
5727
5795
|
const a = this._applied;
|
|
5728
5796
|
if (
|
|
5729
5797
|
inv[0] !== a[0] ||
|
|
@@ -5801,6 +5869,7 @@ class CanvasPattern {
|
|
|
5801
5869
|
// what the server currently holds: a fresh picture is untransformed and
|
|
5802
5870
|
// filtered nearest, so an untransformed fill costs no extra request
|
|
5803
5871
|
this._applied = [1, 0, 0, 1, 0, 0];
|
|
5872
|
+
this._origin = NO_ORIGIN;
|
|
5804
5873
|
this._filter = "nearest";
|
|
5805
5874
|
}
|
|
5806
5875
|
|
|
@@ -5844,20 +5913,36 @@ class CanvasPattern {
|
|
|
5844
5913
|
* Make the server-side mapping match `ctm ∘ patternMatrix`. XRender's
|
|
5845
5914
|
* picture transform runs the other way — it takes a coordinate in the
|
|
5846
5915
|
* composite's source space (which every fill here keeps equal to device
|
|
5847
|
-
* space) to a texel — so it is the inverse
|
|
5916
|
+
* space less `_origin`) to a texel — so it is the inverse, from that
|
|
5917
|
+
* origin.
|
|
5848
5918
|
*
|
|
5849
5919
|
* Returns false when that composition collapses (a zero scale), which
|
|
5850
5920
|
* paints nothing, exactly as the canvas spec says.
|
|
5851
5921
|
*/
|
|
5852
|
-
_sync(ctm) {
|
|
5922
|
+
_sync(ctm, width = 0, height = 0) {
|
|
5853
5923
|
const m = matMultiply(ctm, this._m);
|
|
5854
|
-
|
|
5924
|
+
let inv = matInvert(m);
|
|
5855
5925
|
if (!inv) return false;
|
|
5856
|
-
//
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
//
|
|
5926
|
+
// sampled from where the tile's origin lands (`sourceOrigin`)
|
|
5927
|
+
const origin = originOn(m[4], m[5], width, height);
|
|
5928
|
+
inv = matMultiply(inv, [1, 0, 0, 1, origin[0], origin[1]]);
|
|
5929
|
+
// A tile that repeats is the same a whole number of tiles along, so the
|
|
5930
|
+
// translation — the texel the origin samples — is taken to the first
|
|
5931
|
+
// tile. A grid scrolled 100,000 pixels has its origin far off the
|
|
5932
|
+
// surface and samples texels 100,000 along; from the first tile they
|
|
5933
|
+
// are in reach of the wire.
|
|
5934
|
+
const period =
|
|
5935
|
+
this._repeat === 1 ? 1 : this._repeat === 3 ? 2 : 0; // Normal, Reflect
|
|
5936
|
+
if (period && this.width > 0 && this.height > 0) {
|
|
5937
|
+
const pw = this.width * period;
|
|
5938
|
+
const ph = this.height * period;
|
|
5939
|
+
inv[4] -= pw * Math.floor(inv[4] / pw);
|
|
5940
|
+
inv[5] -= ph * Math.floor(inv[5] / ph);
|
|
5941
|
+
}
|
|
5942
|
+
// 16.16 fixed point, as a gradient's is: one that cannot be carried
|
|
5943
|
+
// fills nothing rather than throwing the rest of the paint away
|
|
5860
5944
|
if (!fitsFixed(inv)) return false;
|
|
5945
|
+
this._origin = origin;
|
|
5861
5946
|
const a = this._applied;
|
|
5862
5947
|
if (
|
|
5863
5948
|
inv[0] !== a[0] ||
|
package/lib/text/glyphs.js
CHANGED
|
@@ -583,8 +583,12 @@ export function routeGlyphSize(app, font, size, policy = policyOf(app), textRend
|
|
|
583
583
|
* @param {{x: number, y: number, w: number, h: number}} [bounds] the part of
|
|
584
584
|
* the destination that can show anything — the clip, or the surface. A
|
|
585
585
|
* glyph wholly outside it is not sent (`visibleGlyph`).
|
|
586
|
+
* @param {[number, number]} [srcOrigin] the destination point the source's
|
|
587
|
+
* (0, 0) is aligned with. A gradient or pattern the 2d context has
|
|
588
|
+
* transformed samples from its own origin; anything else is aligned with
|
|
589
|
+
* the destination, from (0, 0).
|
|
586
590
|
*/
|
|
587
|
-
export function drawGlyphRuns(app, op, srcId, dstId, positioned, bounds = null) {
|
|
591
|
+
export function drawGlyphRuns(app, op, srcId, dstId, positioned, bounds = null, srcOrigin = [0, 0]) {
|
|
588
592
|
const policy = policyOf(app);
|
|
589
593
|
let bitmap = positioned;
|
|
590
594
|
let vector = null;
|
|
@@ -602,8 +606,8 @@ export function drawGlyphRuns(app, op, srcId, dstId, positioned, bounds = null)
|
|
|
602
606
|
bitmap.push(positioned[i]);
|
|
603
607
|
}
|
|
604
608
|
}
|
|
605
|
-
if (bitmap.length) drawBitmapGlyphRuns(app, op, srcId, dstId, bitmap, policy, bounds);
|
|
606
|
-
if (vector) drawVectorGlyphRuns(app, op, srcId, dstId, vector, bounds);
|
|
609
|
+
if (bitmap.length) drawBitmapGlyphRuns(app, op, srcId, dstId, bitmap, policy, bounds, srcOrigin);
|
|
610
|
+
if (vector) drawVectorGlyphRuns(app, op, srcId, dstId, vector, bounds, srcOrigin);
|
|
607
611
|
}
|
|
608
612
|
|
|
609
613
|
/**
|
|
@@ -630,7 +634,7 @@ function visibleGlyph(bounds, x, y, size) {
|
|
|
630
634
|
);
|
|
631
635
|
}
|
|
632
636
|
|
|
633
|
-
function drawBitmapGlyphRuns(app, op, srcId, dstId, positioned, policy, bounds) {
|
|
637
|
+
function drawBitmapGlyphRuns(app, op, srcId, dstId, positioned, policy, bounds, srcOrigin) {
|
|
634
638
|
const Render = app.display.Render;
|
|
635
639
|
const items = [];
|
|
636
640
|
let bits = 8;
|
|
@@ -652,8 +656,9 @@ function drawBitmapGlyphRuns(app, op, srcId, dstId, positioned, policy, bounds)
|
|
|
652
656
|
const encoded = encodeGlyphItems(items, bits);
|
|
653
657
|
if (!encoded) return;
|
|
654
658
|
// srcX/srcY align the source with the FIRST glyph's origin (RENDER spec);
|
|
655
|
-
// passing that origin itself
|
|
656
|
-
// coordinates, so gradient fill styles
|
|
659
|
+
// passing that origin itself, less the source's own, makes source
|
|
660
|
+
// coordinates destination coordinates from there, so gradient fill styles
|
|
661
|
+
// line up with canvas space
|
|
657
662
|
Render.CompositeGlyphs(
|
|
658
663
|
encoded.bits,
|
|
659
664
|
op,
|
|
@@ -661,8 +666,8 @@ function drawBitmapGlyphRuns(app, op, srcId, dstId, positioned, policy, bounds)
|
|
|
661
666
|
dstId,
|
|
662
667
|
0,
|
|
663
668
|
encoded.gsid,
|
|
664
|
-
items[0].x,
|
|
665
|
-
items[0].y,
|
|
669
|
+
items[0].x - srcOrigin[0],
|
|
670
|
+
items[0].y - srcOrigin[1],
|
|
666
671
|
encoded.elts
|
|
667
672
|
);
|
|
668
673
|
trimGlyphPages(app, policy, new Set(pages.values()));
|
|
@@ -683,7 +688,7 @@ const MAX_TRAPS_PER_REQUEST = 6000;
|
|
|
683
688
|
* Positions are intentionally NOT rounded to whole pixels — fractional
|
|
684
689
|
* advances and origins keep zoom animations smooth.
|
|
685
690
|
*/
|
|
686
|
-
function drawVectorGlyphRuns(app, op, srcId, dstId, positioned, bounds = null) {
|
|
691
|
+
function drawVectorGlyphRuns(app, op, srcId, dstId, positioned, bounds = null, srcOrigin = [0, 0]) {
|
|
687
692
|
// unrounded glyph origins + ink bounding box
|
|
688
693
|
const placed = [];
|
|
689
694
|
let minX = Infinity;
|
|
@@ -742,7 +747,7 @@ function drawVectorGlyphRuns(app, op, srcId, dstId, positioned, bounds = null) {
|
|
|
742
747
|
bx = x0;
|
|
743
748
|
by = y0;
|
|
744
749
|
}
|
|
745
|
-
compositeTraps(app, op, srcId, dstId, traps, bx, by);
|
|
750
|
+
compositeTraps(app, op, srcId, dstId, traps, bx, by, srcOrigin);
|
|
746
751
|
}
|
|
747
752
|
|
|
748
753
|
/**
|
|
@@ -753,8 +758,10 @@ function drawVectorGlyphRuns(app, op, srcId, dstId, positioned, bounds = null) {
|
|
|
753
758
|
*
|
|
754
759
|
* When bx/by are omitted they are derived from the trapezoid bounds (the
|
|
755
760
|
* trap coordinates are then treated as absolute device coordinates).
|
|
761
|
+
* `srcOrigin` is the destination point the source's (0, 0) is aligned with,
|
|
762
|
+
* as for `drawGlyphRuns`.
|
|
756
763
|
*/
|
|
757
|
-
export function compositeTraps(app, op, srcId, dstId, traps, bx, by) {
|
|
764
|
+
export function compositeTraps(app, op, srcId, dstId, traps, bx, by, srcOrigin = [0, 0]) {
|
|
758
765
|
if (traps.length === 0) return;
|
|
759
766
|
const Render = app.display.Render;
|
|
760
767
|
|
|
@@ -800,8 +807,22 @@ export function compositeTraps(app, op, srcId, dstId, traps, bx, by) {
|
|
|
800
807
|
for (let i = 0; i < traps.length; i += MAX_TRAPS_PER_REQUEST * 6) {
|
|
801
808
|
Render.AddTraps(mask.picture.id, 0, 0, traps.slice(i, i + MAX_TRAPS_PER_REQUEST * 6));
|
|
802
809
|
}
|
|
803
|
-
// src coords = dst coords
|
|
804
|
-
|
|
810
|
+
// src coords = dst coords from the source's origin (see
|
|
811
|
+
// drawBitmapGlyphRuns) so gradients line up
|
|
812
|
+
Render.Composite(
|
|
813
|
+
op,
|
|
814
|
+
srcId,
|
|
815
|
+
mask.picture.id,
|
|
816
|
+
dstId,
|
|
817
|
+
bx - srcOrigin[0],
|
|
818
|
+
by - srcOrigin[1],
|
|
819
|
+
0,
|
|
820
|
+
0,
|
|
821
|
+
bx,
|
|
822
|
+
by,
|
|
823
|
+
bw,
|
|
824
|
+
bh
|
|
825
|
+
);
|
|
805
826
|
releaseScratchMask(app);
|
|
806
827
|
}
|
|
807
828
|
|