ntk 8.12.1 → 8.12.3

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.
@@ -2716,7 +2716,14 @@ class RenderingContext2d {
2716
2716
  if (rect.w === 0 || rect.h === 0) return;
2717
2717
  this._setPictureClip(rect);
2718
2718
  }
2719
- drawGlyphRuns(app, op, src.id, this._dst(), positioned);
2719
+ drawGlyphRuns(
2720
+ app,
2721
+ op,
2722
+ src.id,
2723
+ this._dst(),
2724
+ positioned,
2725
+ rect ?? { x: 0, y: 0, w: this.width, h: this.height },
2726
+ );
2720
2727
  if (rect) this._invalidatePictureClip();
2721
2728
  this._markDirty();
2722
2729
  return;
@@ -2737,6 +2744,7 @@ class RenderingContext2d {
2737
2744
  this._glyphSource.id,
2738
2745
  this.fillMask.id,
2739
2746
  positioned,
2747
+ { x: 0, y: 0, w: this.width, h: this.height },
2740
2748
  );
2741
2749
  R.Composite(
2742
2750
  R.PictOp.In,
package/lib/text/font.js CHANGED
@@ -75,6 +75,12 @@ export default class Font {
75
75
  this.path = path;
76
76
  /** stable cache key for glyph pages and font caches */
77
77
  this.key = `${path}#${postscriptName || fkFont.postscriptName || ''}`;
78
+ // What the face's tables say, read once. fontkit's getters walk to the
79
+ // table on every read, and a text layout asks per run and per word: in
80
+ // a long document they were most of a relayout (see `metrics`).
81
+ this._upem = 0;
82
+ this._vertical = null;
83
+ this._space = -1;
78
84
  }
79
85
 
80
86
  static loadSync(path, postscriptName) {
@@ -214,28 +220,48 @@ export default class Font {
214
220
  }
215
221
 
216
222
  scale(size) {
217
- return size / this.fk.unitsPerEm;
223
+ return size / (this._upem ||= this.fk.unitsPerEm);
218
224
  }
219
225
 
220
226
  /**
221
227
  * Pixel-space metrics for a font size. `ascent`/`capHeight`/`xHeight` are
222
228
  * positive (above baseline), `descent` is positive (below baseline).
229
+ *
230
+ * A new object every call, which a caller may keep or change; the numbers
231
+ * behind it are the face's, read from its tables once.
223
232
  */
224
233
  metrics(size) {
234
+ const v = (this._vertical ??= {
235
+ ascent: this.fk.ascent,
236
+ descent: -this.fk.descent,
237
+ lineGap: this.fk.lineGap,
238
+ capHeight: this.fk.capHeight,
239
+ xHeight: this.fk.xHeight
240
+ });
225
241
  const s = this.scale(size);
226
- const ascent = this.fk.ascent * s;
227
- const descent = -this.fk.descent * s;
228
- const lineGap = this.fk.lineGap * s;
242
+ const ascent = v.ascent * s;
243
+ const descent = v.descent * s;
244
+ const lineGap = v.lineGap * s;
229
245
  return {
230
246
  ascent,
231
247
  descent,
232
248
  lineGap,
233
249
  lineHeight: ascent + descent + lineGap,
234
- capHeight: this.fk.capHeight * s,
235
- xHeight: this.fk.xHeight * s
250
+ capHeight: v.capHeight * s,
251
+ xHeight: v.xHeight * s
236
252
  };
237
253
  }
238
254
 
255
+ /**
256
+ * The advance of U+0020 at `size` — what a run of trailing spaces is
257
+ * worth to a line. Looked up once: it was a cmap lookup for every word a
258
+ * layout ended with a space, which in a paragraph is every word.
259
+ */
260
+ spaceAdvance(size) {
261
+ if (this._space < 0) this._space = this.fk.glyphForCodePoint(0x20).advanceWidth;
262
+ return this._space * this.scale(size);
263
+ }
264
+
239
265
  hasGlyph(codepoint) {
240
266
  return this.fk.hasGlyphForCodePoint(codepoint);
241
267
  }
@@ -571,8 +571,11 @@ export function routeGlyphSize(app, font, size, policy = policyOf(app), textRend
571
571
  * @param {number} srcId source picture (fill color/gradient)
572
572
  * @param {number} dstId destination picture
573
573
  * @param {Array<{run, x, y}>} positioned visual-order runs with baseline origins
574
+ * @param {{x: number, y: number, w: number, h: number}} [bounds] the part of
575
+ * the destination that can show anything — the clip, or the surface. A
576
+ * glyph wholly outside it is not sent (`visibleGlyph`).
574
577
  */
575
- export function drawGlyphRuns(app, op, srcId, dstId, positioned) {
578
+ export function drawGlyphRuns(app, op, srcId, dstId, positioned, bounds = null) {
576
579
  const policy = policyOf(app);
577
580
  let bitmap = positioned;
578
581
  let vector = null;
@@ -590,11 +593,35 @@ export function drawGlyphRuns(app, op, srcId, dstId, positioned) {
590
593
  bitmap.push(positioned[i]);
591
594
  }
592
595
  }
593
- if (bitmap.length) drawBitmapGlyphRuns(app, op, srcId, dstId, bitmap, policy);
594
- if (vector) drawVectorGlyphRuns(app, op, srcId, dstId, vector);
596
+ if (bitmap.length) drawBitmapGlyphRuns(app, op, srcId, dstId, bitmap, policy, bounds);
597
+ if (vector) drawVectorGlyphRuns(app, op, srcId, dstId, vector, bounds);
595
598
  }
596
599
 
597
- function drawBitmapGlyphRuns(app, op, srcId, dstId, positioned, policy) {
600
+ /**
601
+ * Whether a glyph whose origin is at (x, y) can put any ink inside `bounds`,
602
+ * give or take two ems — no glyph reaches further from its origin than that,
603
+ * swashes and all.
604
+ *
605
+ * Not only a saving: the glyphs of a line wider than 32767 device pixels —
606
+ * a minified file in a code editor, a long label in a narrow cell — have
607
+ * origins no request can carry. CompositeGlyphs places its first glyph and
608
+ * every element after it in 16-bit coordinates, and the vector path's trap
609
+ * mask would span the whole line, so drawing such a line threw a RangeError
610
+ * out of the paint however little of it was on screen. What is left after
611
+ * the cull sits inside a surface, and every coordinate in it fits.
612
+ */
613
+ function visibleGlyph(bounds, x, y, size) {
614
+ if (bounds === null) return true;
615
+ const reach = size * 2;
616
+ return (
617
+ x + reach > bounds.x &&
618
+ x - reach < bounds.x + bounds.w &&
619
+ y + reach > bounds.y &&
620
+ y - reach < bounds.y + bounds.h
621
+ );
622
+ }
623
+
624
+ function drawBitmapGlyphRuns(app, op, srcId, dstId, positioned, policy, bounds) {
598
625
  const Render = app.display.Render;
599
626
  const items = [];
600
627
  let bits = 8;
@@ -608,6 +635,7 @@ function drawBitmapGlyphRuns(app, op, srcId, dstId, positioned, policy) {
608
635
  if (page.bits > bits) bits = page.bits;
609
636
  }
610
637
  for (const pos of positionGlyphs(positioned)) {
638
+ if (!visibleGlyph(bounds, pos.x, pos.y, pos.run.size)) continue;
611
639
  const page = pages.get(pos.run);
612
640
  const e = page.entry(pos.glyph.id);
613
641
  items.push({ gs: e.gs, lid: e.lid, adv: e.adv, x: pos.x, y: pos.y });
@@ -646,7 +674,7 @@ const MAX_TRAPS_PER_REQUEST = 6000;
646
674
  * Positions are intentionally NOT rounded to whole pixels — fractional
647
675
  * advances and origins keep zoom animations smooth.
648
676
  */
649
- function drawVectorGlyphRuns(app, op, srcId, dstId, positioned) {
677
+ function drawVectorGlyphRuns(app, op, srcId, dstId, positioned, bounds = null) {
650
678
  // unrounded glyph origins + ink bounding box
651
679
  const placed = [];
652
680
  let minX = Infinity;
@@ -657,6 +685,7 @@ function drawVectorGlyphRuns(app, op, srcId, dstId, positioned) {
657
685
  const gx = cursor + g.dx;
658
686
  const gy = y - g.dy;
659
687
  cursor += g.ax;
688
+ if (!visibleGlyph(bounds, gx, gy, run.size)) continue;
660
689
  const e = run.font.glyphExtents(g.id, run.size);
661
690
  if (!Number.isFinite(e.minX) || e.maxX <= e.minX) continue; // blank glyph
662
691
  placed.push({ run, glyph: g, x: gx, y: gy });
@@ -324,10 +324,8 @@ export class TextLayout {
324
324
  if (last) {
325
325
  const m = TRAILING_WS.exec(last.text);
326
326
  if (m) {
327
- const spaceGlyph = last.span.font.fk.glyphForCodePoint(0x20);
328
327
  const spacing = Number.isFinite(last.span.letterSpacing) ? last.span.letterSpacing : 0;
329
- wsWidth =
330
- m[0].length * (spaceGlyph.advanceWidth * last.span.font.scale(last.span.size) + spacing);
328
+ wsWidth = m[0].length * (last.span.font.spaceAdvance(last.span.size) + spacing);
331
329
  }
332
330
  }
333
331
  return { fragments, width, wsWidth, required, start, end };
package/lib/text/shape.js CHANGED
@@ -25,9 +25,29 @@ function neverSwitches(cp) {
25
25
  */
26
26
  export function embeddingLevels(text, direction = 'auto') {
27
27
  const explicit = direction === 'ltr' || direction === 'rtl' ? direction : null;
28
+ // Text that nothing can make right-to-left is level 0 throughout, which
29
+ // is what the algorithm would work out a character at a time: every
30
+ // layout asks, and most text is Latin, CJK or digits with no bidi at all.
31
+ if (explicit !== 'rtl' && !MAY_REVERSE.test(text)) {
32
+ return {
33
+ levels: new Uint8Array(text.length),
34
+ paragraphs: text.length ? [{ start: 0, end: text.length - 1, level: 0 }] : []
35
+ };
36
+ }
28
37
  return bidi.getEmbeddingLevels(text, explicit);
29
38
  }
30
39
 
40
+ /**
41
+ * What can raise a level in a left-to-right paragraph: a strong
42
+ * right-to-left character (Hebrew, Arabic, Syriac, Thaana, N'Ko and their
43
+ * neighbours, their presentation forms, and the right-to-left blocks of the
44
+ * supplementary planes, found by their high surrogates), an Arabic number,
45
+ * RLM and ALM, and every explicit embedding, override and isolate. Anything
46
+ * else resolves to 0.
47
+ */
48
+ const MAY_REVERSE =
49
+ /[\u0590-\u08FF\u200F\u202A-\u202E\u2066-\u2069\uFB1D-\uFDFF\uFE70-\uFEFF]|[\uD802\uD803\uD83A\uD83B]/;
50
+
31
51
  /**
32
52
  * Compact embedding-levels encoding for the shaping memo key: a single
33
53
  * number when uniform over [start, end) — the common case — else
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ntk",
3
- "version": "8.12.1",
3
+ "version": "8.12.3",
4
4
  "description": "Desktop UI toolkit for X11 with canvas-like 2d and OpenGL rendering",
5
5
  "author": "Andrey Sidorov <sidorares@yandex.ru>",
6
6
  "license": "MIT",