ntk 8.14.4 → 8.14.6
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/cliprect.js +23 -8
- package/lib/path.js +85 -7
- package/lib/renderingcontext_2d.js +74 -11
- package/lib/text/font.js +38 -2
- package/lib/text/fontmanager.js +24 -0
- package/lib/text/layout.js +62 -8
- package/lib/text/marks.js +3 -3
- package/lib/text/pairs.js +88 -0
- package/package.json +1 -1
package/lib/cliprect.js
CHANGED
|
@@ -101,9 +101,19 @@ export function clipTrianglesToRect(tris, x0, y0, x1, y1) {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
/**
|
|
104
|
-
* A flat `[x, y, w, h, …]` list of rectangles
|
|
105
|
-
* height)`, dropping the ones with nothing left: core X's
|
|
106
|
-
* bits. The list itself when every rectangle is already
|
|
104
|
+
* A flat `[x, y, w, h, …]` list of rectangles on whole pixels and cut to
|
|
105
|
+
* `[0, width) × [0, height)`, dropping the ones with nothing left: core X's
|
|
106
|
+
* rectangles are 16 bits. The list itself when every rectangle is already
|
|
107
|
+
* whole and on the surface.
|
|
108
|
+
*
|
|
109
|
+
* Each edge is rounded on its own. The wire truncates a rectangle's x and
|
|
110
|
+
* its width separately, so two rectangles meeting at a fractional edge —
|
|
111
|
+
* the selection bands of two lines whose height is the face's, 15.13px —
|
|
112
|
+
* left a row of neither between them every seventh or eighth line, and one
|
|
113
|
+
* that crossed the surface's edge was cut a pixel differently from the
|
|
114
|
+
* same rectangle moved: a scroll's copy and a repaint disagreed. Rounded
|
|
115
|
+
* edges meet whoever they are shared with, and move with a whole-pixel
|
|
116
|
+
* shift.
|
|
107
117
|
*/
|
|
108
118
|
export function rectsOnSurface(flat, width, height) {
|
|
109
119
|
let out = null;
|
|
@@ -112,16 +122,21 @@ export function rectsOnSurface(flat, width, height) {
|
|
|
112
122
|
const y = flat[i + 1];
|
|
113
123
|
const w = flat[i + 2];
|
|
114
124
|
const h = flat[i + 3];
|
|
125
|
+
const whole =
|
|
126
|
+
Number.isInteger(x) &&
|
|
127
|
+
Number.isInteger(y) &&
|
|
128
|
+
Number.isInteger(w) &&
|
|
129
|
+
Number.isInteger(h);
|
|
115
130
|
const inside = x >= 0 && y >= 0 && x + w <= width && y + h <= height;
|
|
116
|
-
if (inside) {
|
|
131
|
+
if (whole && inside) {
|
|
117
132
|
if (out) out.push(x, y, w, h);
|
|
118
133
|
continue;
|
|
119
134
|
}
|
|
120
135
|
if (!out) out = flat.slice(0, i);
|
|
121
|
-
const x0 = Math.max(x, 0);
|
|
122
|
-
const y0 = Math.max(y, 0);
|
|
123
|
-
const x1 = Math.min(x + w, width);
|
|
124
|
-
const y1 = Math.min(y + h, height);
|
|
136
|
+
const x0 = Math.max(Math.round(x), 0);
|
|
137
|
+
const y0 = Math.max(Math.round(y), 0);
|
|
138
|
+
const x1 = Math.min(Math.round(x + w), width);
|
|
139
|
+
const y1 = Math.min(Math.round(y + h), height);
|
|
125
140
|
if (x1 > x0 && y1 > y0) out.push(x0, y0, x1 - x0, y1 - y0);
|
|
126
141
|
}
|
|
127
142
|
return out ?? flat;
|
package/lib/path.js
CHANGED
|
@@ -491,13 +491,19 @@ function normalizeRadii(w, h, radii) {
|
|
|
491
491
|
br = take(radii[2]);
|
|
492
492
|
bl = take(radii[3]);
|
|
493
493
|
}
|
|
494
|
-
//
|
|
494
|
+
// Scale down so opposite corners never overlap: by the side's length over
|
|
495
|
+
// the radii along it, which is 0 for a side of no length, and the radii go
|
|
496
|
+
// with it. Only radii of nothing leave a side alone — `|| 1` guarded that
|
|
497
|
+
// 0 / 0 and took a real 0 for one as well, so a box of no height kept its
|
|
498
|
+
// corners, and a clip to it was two lobes of arc a radius above and below
|
|
499
|
+
// it that let what it should have hidden through.
|
|
500
|
+
const fit = (side, radii) => (radii > 0 ? side / radii : 1);
|
|
495
501
|
const s = Math.min(
|
|
496
502
|
1,
|
|
497
|
-
w
|
|
498
|
-
h
|
|
499
|
-
w
|
|
500
|
-
h
|
|
503
|
+
fit(w, tl.x + tr.x),
|
|
504
|
+
fit(h, tr.y + br.y),
|
|
505
|
+
fit(w, br.x + bl.x),
|
|
506
|
+
fit(h, bl.y + tl.y)
|
|
501
507
|
);
|
|
502
508
|
const f = (r) => ({ x: Math.max(0, r.x * s), y: Math.max(0, r.y * s) });
|
|
503
509
|
return [f(tl), f(tr), f(br), f(bl)];
|
|
@@ -803,6 +809,62 @@ function addCubic(pts, x0, y0, x1, y1, x2, y2, x3, y3, tol2, depth) {
|
|
|
803
809
|
addCubic(pts, mx, my, bcx, bcy, cx, cy, x3, y3, tol2, depth + 1);
|
|
804
810
|
}
|
|
805
811
|
|
|
812
|
+
/**
|
|
813
|
+
* Which way a cubic leaves (x0, y0), or arrives at (x, y) with `end`: the
|
|
814
|
+
* control point next to that end, or the next one along where it sits on
|
|
815
|
+
* the end itself. Null for a cubic that is a single point.
|
|
816
|
+
*/
|
|
817
|
+
function cubicTangent(x0, y0, x1, y1, x2, y2, x, y, end) {
|
|
818
|
+
if (end) {
|
|
819
|
+
if (x !== x2 || y !== y2) return [x - x2, y - y2];
|
|
820
|
+
if (x !== x1 || y !== y1) return [x - x1, y - y1];
|
|
821
|
+
return x !== x0 || y !== y0 ? [x - x0, y - y0] : null;
|
|
822
|
+
}
|
|
823
|
+
if (x1 !== x0 || y1 !== y0) return [x1 - x0, y1 - y0];
|
|
824
|
+
if (x2 !== x0 || y2 !== y0) return [x2 - x0, y2 - y0];
|
|
825
|
+
return x !== x0 || y !== y0 ? [x - x0, y - y0] : null;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* The tangent at a curve command's end or start, in the flattened
|
|
830
|
+
* polyline's space; (x0, y0) is where the command begins, already there.
|
|
831
|
+
*/
|
|
832
|
+
function commandTangent(c, x0, y0, m, end) {
|
|
833
|
+
const p = (x, y) => (m ? matApply(m, x, y) : [x, y]);
|
|
834
|
+
const [x, y] = p(c.x, c.y);
|
|
835
|
+
if (c.type === 'Q') {
|
|
836
|
+
const [qx, qy] = p(c.x1, c.y1);
|
|
837
|
+
return cubicTangent(
|
|
838
|
+
x0, y0,
|
|
839
|
+
x0 + (2 / 3) * (qx - x0), y0 + (2 / 3) * (qy - y0),
|
|
840
|
+
x + (2 / 3) * (qx - x), y + (2 / 3) * (qy - y),
|
|
841
|
+
x, y,
|
|
842
|
+
end
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
const [x1, y1] = p(c.x1, c.y1);
|
|
846
|
+
const [x2, y2] = p(c.x2, c.y2);
|
|
847
|
+
return cubicTangent(x0, y0, x1, y1, x2, y2, x, y, end);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* The tangents a flattened subpath leaves its first point and arrives at its
|
|
852
|
+
* last with, where that end is a curve — null where it is a line, whose
|
|
853
|
+
* chord already says which way the path heads. Worked out on demand from the
|
|
854
|
+
* commands `flattenPath` kept: only a stroke whose ends meet asks.
|
|
855
|
+
*/
|
|
856
|
+
export function subpathHead(poly) {
|
|
857
|
+
return poly.head
|
|
858
|
+
? commandTangent(poly.head, poly.pts[0], poly.pts[1], poly.m, false)
|
|
859
|
+
: null;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
export function subpathTail(poly) {
|
|
863
|
+
return poly.tail
|
|
864
|
+
? commandTangent(poly.tail, poly.tailX, poly.tailY, poly.m, true)
|
|
865
|
+
: null;
|
|
866
|
+
}
|
|
867
|
+
|
|
806
868
|
/**
|
|
807
869
|
* Chords of the arc a tagged cubic stands for, appended to `pts`. Returns
|
|
808
870
|
* false — flatten the cubic instead — when the path's current point is not
|
|
@@ -858,6 +920,13 @@ function addArcChords(pts, cmd, m, tol, x0, y0) {
|
|
|
858
920
|
* geometry (see addArcChords); genuine beziers — hand-built paths, SVG curve
|
|
859
921
|
* commands, font outlines — take the adaptive bisection route.
|
|
860
922
|
*
|
|
923
|
+
* A subpath that begins or ends on a curve also keeps the command it began
|
|
924
|
+
* or ended with, for `subpathHead` and `subpathTail`: flattening turns a
|
|
925
|
+
* curve's end into a chord, which points half a chord's angle away from the
|
|
926
|
+
* curve, and the stroker asks for the curve's own tangent where that
|
|
927
|
+
* difference shows (a ring's seam). Kept rather than worked out, because a
|
|
928
|
+
* fill never asks.
|
|
929
|
+
*
|
|
861
930
|
* @returns {Array<{pts: number[], closed: boolean}>} flat [x0,y0,x1,y1,…]
|
|
862
931
|
*/
|
|
863
932
|
export function flattenPath(cmds, m = null, tol = FLATTEN_TOLERANCE) {
|
|
@@ -872,19 +941,24 @@ export function flattenPath(cmds, m = null, tol = FLATTEN_TOLERANCE) {
|
|
|
872
941
|
case 'M': {
|
|
873
942
|
if (pts && pts.pts.length >= 4) polys.push(pts);
|
|
874
943
|
const [x, y] = p(c.x, c.y);
|
|
875
|
-
pts = { pts: [x, y], closed: false };
|
|
944
|
+
pts = { pts: [x, y], closed: false, head: null, tail: null, tailX: 0, tailY: 0, m };
|
|
876
945
|
break;
|
|
877
946
|
}
|
|
878
947
|
case 'L': {
|
|
879
948
|
if (!pts) break;
|
|
880
949
|
const [x, y] = p(c.x, c.y);
|
|
881
950
|
pts.pts.push(x, y);
|
|
951
|
+
pts.tail = null;
|
|
882
952
|
break;
|
|
883
953
|
}
|
|
884
954
|
case 'C': {
|
|
885
955
|
if (!pts) break;
|
|
886
956
|
const x0 = pts.pts[pts.pts.length - 2];
|
|
887
957
|
const y0 = pts.pts[pts.pts.length - 1];
|
|
958
|
+
if (pts.pts.length === 2) pts.head = c;
|
|
959
|
+
pts.tail = c;
|
|
960
|
+
pts.tailX = x0;
|
|
961
|
+
pts.tailY = y0;
|
|
888
962
|
if (c.arc && addArcChords(pts.pts, c, m, tol, x0, y0)) break;
|
|
889
963
|
const [x1, y1] = p(c.x1, c.y1);
|
|
890
964
|
const [x2, y2] = p(c.x2, c.y2);
|
|
@@ -896,6 +970,10 @@ export function flattenPath(cmds, m = null, tol = FLATTEN_TOLERANCE) {
|
|
|
896
970
|
if (!pts) break;
|
|
897
971
|
const x0 = pts.pts[pts.pts.length - 2];
|
|
898
972
|
const y0 = pts.pts[pts.pts.length - 1];
|
|
973
|
+
if (pts.pts.length === 2) pts.head = c;
|
|
974
|
+
pts.tail = c;
|
|
975
|
+
pts.tailX = x0;
|
|
976
|
+
pts.tailY = y0;
|
|
899
977
|
const [qx, qy] = p(c.x1, c.y1);
|
|
900
978
|
const [x, y] = p(c.x, c.y);
|
|
901
979
|
// degree elevation: quadratic -> cubic
|
|
@@ -916,7 +994,7 @@ export function flattenPath(cmds, m = null, tol = FLATTEN_TOLERANCE) {
|
|
|
916
994
|
polys.push(pts);
|
|
917
995
|
}
|
|
918
996
|
// a new subpath continues from the subpath start
|
|
919
|
-
pts = { pts: [pts.pts[0], pts.pts[1]], closed: false };
|
|
997
|
+
pts = { pts: [pts.pts[0], pts.pts[1]], closed: false, head: null, tail: null, tailX: 0, tailY: 0, m };
|
|
920
998
|
}
|
|
921
999
|
break;
|
|
922
1000
|
}
|
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
matInvert,
|
|
23
23
|
matIsIdentity,
|
|
24
24
|
matMultiply,
|
|
25
|
+
subpathHead,
|
|
26
|
+
subpathTail,
|
|
25
27
|
} from "./path.js";
|
|
26
28
|
import Picture from "./picture.js";
|
|
27
29
|
import { formatForDepth } from "./pictformat.js";
|
|
@@ -524,16 +526,23 @@ class RenderingContext2d {
|
|
|
524
526
|
);
|
|
525
527
|
}
|
|
526
528
|
if (typeof window.on === "function") {
|
|
527
|
-
|
|
529
|
+
// Kept, so `destroy` can take them off again: a surface drawn through
|
|
530
|
+
// `render()` gets a context a call, and each one left three listeners
|
|
531
|
+
// on the pixmap — holding the dead context — for as long as the
|
|
532
|
+
// surface lived. A fade redraws its group surface every frame.
|
|
533
|
+
this._onBacking = () => this._bindTarget();
|
|
528
534
|
// masks are sized to the drawable — recreate them after a resize
|
|
529
|
-
|
|
535
|
+
this._onResize = () => this._dropMasks();
|
|
530
536
|
// window-backed pictures are freed server-side with the window
|
|
531
|
-
|
|
537
|
+
this._onDestroyed = () => {
|
|
532
538
|
if (this._picture && this._target === this.window) {
|
|
533
539
|
this._picture.forget();
|
|
534
540
|
this._picture = null;
|
|
535
541
|
}
|
|
536
|
-
}
|
|
542
|
+
};
|
|
543
|
+
window.on("_backing", this._onBacking);
|
|
544
|
+
window.on("resize", this._onResize);
|
|
545
|
+
window.on("_destroyed", this._onDestroyed);
|
|
537
546
|
}
|
|
538
547
|
|
|
539
548
|
this.fillMask = null;
|
|
@@ -876,6 +885,13 @@ class RenderingContext2d {
|
|
|
876
885
|
if (this._destroyed) return;
|
|
877
886
|
this._destroyed = true;
|
|
878
887
|
gcRegistry.unregister(this);
|
|
888
|
+
const drawable = this.window;
|
|
889
|
+
if (this._onBacking && typeof drawable?.removeListener === "function") {
|
|
890
|
+
drawable.removeListener("_backing", this._onBacking);
|
|
891
|
+
drawable.removeListener("resize", this._onResize);
|
|
892
|
+
drawable.removeListener("_destroyed", this._onDestroyed);
|
|
893
|
+
this._onBacking = this._onResize = this._onDestroyed = null;
|
|
894
|
+
}
|
|
879
895
|
|
|
880
896
|
this._setClips([]);
|
|
881
897
|
this._dropMasks();
|
|
@@ -2401,6 +2417,32 @@ class RenderingContext2d {
|
|
|
2401
2417
|
addDisk(pts[pts.length - 1][0], pts[pts.length - 1][1]);
|
|
2402
2418
|
}
|
|
2403
2419
|
};
|
|
2420
|
+
// An open subpath that ends where it began, heading the way it began —
|
|
2421
|
+
// a full circle from arc(), which leaves its subpath open. Stroked
|
|
2422
|
+
// open, each end got a cap square to its own chord, and a flattened
|
|
2423
|
+
// curve's first and last chords point a chord's angle apart: a wedge
|
|
2424
|
+
// of ink went missing outside the seam, a hairline across every
|
|
2425
|
+
// stroked ring, and doubled inside it. Where the two ends meet head to
|
|
2426
|
+
// tail the caps and a join are the same ink, so it is stroked as the
|
|
2427
|
+
// loop it is. A path that comes back to its start at a corner keeps
|
|
2428
|
+
// its caps, as the spec gives them.
|
|
2429
|
+
const smoothLoop = (poly, pts) => {
|
|
2430
|
+
const n = pts.length;
|
|
2431
|
+
if (n < 4) return false;
|
|
2432
|
+
const [fx, fy] = pts[0];
|
|
2433
|
+
const [lx, ly] = pts[n - 1];
|
|
2434
|
+
if (Math.abs(fx - lx) > 1e-6 || Math.abs(fy - ly) > 1e-6) return false;
|
|
2435
|
+
const [hx, hy] = (poly.head && subpathHead(poly)) ?? [
|
|
2436
|
+
pts[1][0] - fx,
|
|
2437
|
+
pts[1][1] - fy,
|
|
2438
|
+
];
|
|
2439
|
+
const [tx, ty] = (poly.tail && subpathTail(poly)) ?? [
|
|
2440
|
+
lx - pts[n - 2][0],
|
|
2441
|
+
ly - pts[n - 2][1],
|
|
2442
|
+
];
|
|
2443
|
+
const lengths = Math.hypot(hx, hy) * Math.hypot(tx, ty);
|
|
2444
|
+
return lengths > 0 && (hx * tx + hy * ty) / lengths > 1 - 1e-9;
|
|
2445
|
+
};
|
|
2404
2446
|
// dash boundaries can duplicate run endpoints; collapse them
|
|
2405
2447
|
const cleanRun = (run) => {
|
|
2406
2448
|
const out = [run[0]];
|
|
@@ -2433,12 +2475,13 @@ class RenderingContext2d {
|
|
|
2433
2475
|
pts.push([fx, fy]);
|
|
2434
2476
|
}
|
|
2435
2477
|
if (pts.length < 2) continue;
|
|
2478
|
+
const closed = poly.closed || smoothLoop(poly, pts);
|
|
2436
2479
|
|
|
2437
2480
|
if (!dash) {
|
|
2438
|
-
extrudeRun(pts,
|
|
2481
|
+
extrudeRun(pts, closed);
|
|
2439
2482
|
continue;
|
|
2440
2483
|
}
|
|
2441
|
-
const dashed = dashPolyline(pts,
|
|
2484
|
+
const dashed = dashPolyline(pts, closed, dash, dashOffset, view);
|
|
2442
2485
|
if (!dashed) {
|
|
2443
2486
|
extrudeRun(pts, poly.closed);
|
|
2444
2487
|
} else if (dashed.closedLoop) {
|
|
@@ -3056,6 +3099,25 @@ class RenderingContext2d {
|
|
|
3056
3099
|
* for the whole list and then draws the rectangles through here */
|
|
3057
3100
|
_fillRect(x, y, w, h) {
|
|
3058
3101
|
if (matIsIdentity(this._m)) {
|
|
3102
|
+
// On whole pixels, each edge rounded on its own (see rectsOnSurface):
|
|
3103
|
+
// the wire truncates x and width separately, so rectangles that met
|
|
3104
|
+
// at a fractional edge left a row of neither between them. Whole, a
|
|
3105
|
+
// rectangle under a rectangular clip also keeps the bounded
|
|
3106
|
+
// composite below instead of building a mask.
|
|
3107
|
+
if (
|
|
3108
|
+
!Number.isInteger(x) ||
|
|
3109
|
+
!Number.isInteger(y) ||
|
|
3110
|
+
!Number.isInteger(w) ||
|
|
3111
|
+
!Number.isInteger(h)
|
|
3112
|
+
) {
|
|
3113
|
+
const x0 = Math.round(x);
|
|
3114
|
+
const y0 = Math.round(y);
|
|
3115
|
+
w = Math.round(x + w) - x0;
|
|
3116
|
+
h = Math.round(y + h) - y0;
|
|
3117
|
+
x = x0;
|
|
3118
|
+
y = y0;
|
|
3119
|
+
if (!(w > 0 && h > 0)) return;
|
|
3120
|
+
}
|
|
3059
3121
|
// X coordinates and sizes are 16 bits, and a coordinate past 32,767
|
|
3060
3122
|
// throws out of the paint (lib/cliprect.js): a rectangle that reaches
|
|
3061
3123
|
// past the surface is filled as the part of it the surface has.
|
|
@@ -3224,13 +3286,14 @@ class RenderingContext2d {
|
|
|
3224
3286
|
let x1 = -Infinity;
|
|
3225
3287
|
let y1 = -Infinity;
|
|
3226
3288
|
for (let i = 0; i < flat.length; i += 4) {
|
|
3227
|
-
//
|
|
3228
|
-
|
|
3229
|
-
const
|
|
3289
|
+
// on whole pixels, each edge rounded as `_fillRect` rounds it, and the
|
|
3290
|
+
// part of it on the surface: the wire's coordinates are 16 bits
|
|
3291
|
+
const left = Math.round(flat[i]);
|
|
3292
|
+
const top = Math.round(flat[i + 1]);
|
|
3230
3293
|
const x = Math.max(0, left);
|
|
3231
3294
|
const y = Math.max(0, top);
|
|
3232
|
-
const w = Math.min(this.width,
|
|
3233
|
-
const h = Math.min(this.height,
|
|
3295
|
+
const w = Math.min(this.width, Math.round(flat[i] + flat[i + 2])) - x;
|
|
3296
|
+
const h = Math.min(this.height, Math.round(flat[i + 1] + flat[i + 3])) - y;
|
|
3234
3297
|
if (w <= 0 || h <= 0) continue;
|
|
3235
3298
|
rects.push(x, y, w, h);
|
|
3236
3299
|
if (x < x0) x0 = x;
|
package/lib/text/font.js
CHANGED
|
@@ -3,6 +3,7 @@ import * as fontkit from 'fontkit';
|
|
|
3
3
|
import { flatten, rasterizePath } from '../rasterize.js';
|
|
4
4
|
import { NO_ANCHOR, tolerateAnchors, watchAnchors } from './anchors.js';
|
|
5
5
|
import { marksCover, standInMarks } from './marks.js';
|
|
6
|
+
import { pairGlyphs, pairsCover } from './pairs.js';
|
|
6
7
|
|
|
7
8
|
// Axis coordinates are rounded to this many decimals before anything is
|
|
8
9
|
// instantiated or cached. Every distinct coordinate is a font in its own
|
|
@@ -111,6 +112,28 @@ function answerWithCharacters(fk) {
|
|
|
111
112
|
Object.defineProperty(fk, '__ntkCharacters', { value: true });
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
/**
|
|
116
|
+
* A face's cap height or x-height, as its OS/2 table states it, or else the
|
|
117
|
+
* top of its `H` or `x`. A table older than version 2 has neither field, and
|
|
118
|
+
* a face with no OS/2 table at all — Apple's Courier.ttc — has no table to
|
|
119
|
+
* state them: fontkit answered 0 for one and nothing for the other, and an
|
|
120
|
+
* `ex` in either came out half an em (CSS Values 4, 6.1.1 lets the height
|
|
121
|
+
* of a lowercase glyph stand in, as browsers measure it). NaN where the face
|
|
122
|
+
* has no such glyph either, which is the "half an em" case the spec keeps.
|
|
123
|
+
*/
|
|
124
|
+
function statedOrGlyph(fk, field, codePoint) {
|
|
125
|
+
const os2 = fk['OS/2'];
|
|
126
|
+
const stated = os2 && os2.version >= 2 ? os2[field] : undefined;
|
|
127
|
+
if (stated > 0) return stated;
|
|
128
|
+
try {
|
|
129
|
+
if (!fk.hasGlyphForCodePoint(codePoint)) return NaN;
|
|
130
|
+
const top = fk.glyphForCodePoint(codePoint).bbox.maxY;
|
|
131
|
+
return top > 0 ? top : NaN;
|
|
132
|
+
} catch {
|
|
133
|
+
return NaN;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
114
137
|
export default class Font {
|
|
115
138
|
constructor(fkFont, path, postscriptName) {
|
|
116
139
|
answerWithCharacters(fkFont);
|
|
@@ -132,6 +155,8 @@ export default class Font {
|
|
|
132
155
|
// whether the face's mark attachment is watched for a NULL anchor
|
|
133
156
|
// (./anchors.js): set at its first shaping, false once one has been met
|
|
134
157
|
this._anchors = false;
|
|
158
|
+
// the glyphs its pairs can move (./pairs.js), read when first asked
|
|
159
|
+
this._pairs = undefined;
|
|
135
160
|
}
|
|
136
161
|
|
|
137
162
|
static loadSync(path, postscriptName) {
|
|
@@ -286,8 +311,8 @@ export default class Font {
|
|
|
286
311
|
ascent: this.fk.ascent,
|
|
287
312
|
descent: -this.fk.descent,
|
|
288
313
|
lineGap: this.fk.lineGap,
|
|
289
|
-
capHeight: this.fk
|
|
290
|
-
xHeight: this.fk
|
|
314
|
+
capHeight: statedOrGlyph(this.fk, 'capHeight', 0x48),
|
|
315
|
+
xHeight: statedOrGlyph(this.fk, 'xHeight', 0x78)
|
|
291
316
|
});
|
|
292
317
|
const s = this.scale(size);
|
|
293
318
|
const ascent = v.ascent * s;
|
|
@@ -334,6 +359,17 @@ export default class Font {
|
|
|
334
359
|
return this._drawable;
|
|
335
360
|
}
|
|
336
361
|
|
|
362
|
+
/**
|
|
363
|
+
* Whether two glyphs side by side can be set otherwise than apart: the
|
|
364
|
+
* left one moved against what follows it, or the right one against what
|
|
365
|
+
* comes before it (./pairs.js). True where the tables cannot tell.
|
|
366
|
+
*/
|
|
367
|
+
mayKern(left, right) {
|
|
368
|
+
if (this._pairs === undefined) this._pairs = pairGlyphs(this.fk);
|
|
369
|
+
const pairs = this._pairs;
|
|
370
|
+
return pairs === null || pairsCover(pairs.left, left) || pairsCover(pairs.right, right);
|
|
371
|
+
}
|
|
372
|
+
|
|
337
373
|
hasGlyph(codepoint) {
|
|
338
374
|
return this.drawable && this.fk.hasGlyphForCodePoint(codepoint);
|
|
339
375
|
}
|
package/lib/text/fontmanager.js
CHANGED
|
@@ -235,6 +235,8 @@ export default class FontManager {
|
|
|
235
235
|
this._shapeCount = 0;
|
|
236
236
|
// whether two letters shape otherwise together than apart (`_meets`)
|
|
237
237
|
this._meetings = new Map();
|
|
238
|
+
// how much two letters kern across a break opportunity (`_kernAcross`)
|
|
239
|
+
this._kerns = new Map();
|
|
238
240
|
// a paragraph's layout up to the line fill, for its next width
|
|
239
241
|
// (text/paragraphs.js, TextLayout)
|
|
240
242
|
this._paragraphs = new ParagraphCache();
|
|
@@ -580,6 +582,28 @@ export default class FontManager {
|
|
|
580
582
|
return meets;
|
|
581
583
|
}
|
|
582
584
|
|
|
585
|
+
/**
|
|
586
|
+
* How much wider the two letters of `pair`, cut at `cut`, are set
|
|
587
|
+
* together than apart in this style: their kerning, negative for a pair
|
|
588
|
+
* set closer. 0 where they do not kern, and where shaping them together
|
|
589
|
+
* changes a glyph — a ligature, a contextual form — which no offset
|
|
590
|
+
* between the two stands for. Kept, as `_meets` keeps its answers.
|
|
591
|
+
*/
|
|
592
|
+
_kernAcross(pair, cut, style, levelsKey = '0') {
|
|
593
|
+
const key = `${shapeGroupOf(style, levelsKey)}|${cut}|${pair}`;
|
|
594
|
+
let kern = this._kerns.get(key);
|
|
595
|
+
if (kern !== undefined) return kern;
|
|
596
|
+
const whole = this._shapeCached(pair, style, levelsKey);
|
|
597
|
+
const left = this._shapeCached(pair.slice(0, cut), style, sliceKey(levelsKey, 0, cut));
|
|
598
|
+
const right = this._shapeCached(pair.slice(cut), style, sliceKey(levelsKey, cut, pair.length));
|
|
599
|
+
const ids = (...shaped) => shaped.flatMap((s) => s.runs.flatMap((r) => r.glyphs.map((g) => g.id))).join(',');
|
|
600
|
+
kern = whole.width - left.width - right.width;
|
|
601
|
+
if (Math.abs(kern) <= 1e-6 || ids(whole) !== ids(left, right)) kern = 0;
|
|
602
|
+
if (this._kerns.size >= SHAPE_GENERATION) this._kerns.clear();
|
|
603
|
+
this._kerns.set(key, kern);
|
|
604
|
+
return kern;
|
|
605
|
+
}
|
|
606
|
+
|
|
583
607
|
_shapeCached(text, style, levelsKey = '0') {
|
|
584
608
|
// A hit in this generation is the whole cost of a word a paragraph has
|
|
585
609
|
// shaped before — a relayout at a new width asks for every word again,
|
package/lib/text/layout.js
CHANGED
|
@@ -21,10 +21,16 @@ const UNSPACED =
|
|
|
21
21
|
/** How wide a line's tokens are, the white space it ends on left out. */
|
|
22
22
|
function tokensWidth(toks) {
|
|
23
23
|
let width = 0;
|
|
24
|
-
for (
|
|
24
|
+
for (let i = 0; i < toks.length; i++) width += toks[i].width + (i ? kernBefore(toks[i]) : 0);
|
|
25
25
|
return toks.length ? width - toks[toks.length - 1].wsWidth : 0;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/** The kerning between a token and the one before it on its line
|
|
29
|
+
* (`TextLayout._kernBefore`); none for a token a line starts with. */
|
|
30
|
+
function kernBefore(token) {
|
|
31
|
+
return token.kernBefore ?? 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
28
34
|
function isWsGlyph(g) {
|
|
29
35
|
return g.codePoints.length > 0 && g.codePoints.every((cp) => WS.has(cp));
|
|
30
36
|
}
|
|
@@ -121,7 +127,7 @@ export class TextLayout {
|
|
|
121
127
|
curWidth = 0;
|
|
122
128
|
};
|
|
123
129
|
for (let token of tokens) {
|
|
124
|
-
while (wraps && curWidth + token.width - token.wsWidth > maxWidth) {
|
|
130
|
+
while (wraps && curWidth + (cur.length ? kernBefore(token) : 0) + token.width - token.wsWidth > maxWidth) {
|
|
125
131
|
if (cur.length > 0) {
|
|
126
132
|
flush();
|
|
127
133
|
} else if (whole && !UNSPACED.test(text.slice(token.start, token.end))) {
|
|
@@ -135,8 +141,8 @@ export class TextLayout {
|
|
|
135
141
|
token = rest;
|
|
136
142
|
}
|
|
137
143
|
}
|
|
144
|
+
curWidth += token.width + (cur.length ? kernBefore(token) : 0);
|
|
138
145
|
cur.push(token);
|
|
139
|
-
curWidth += token.width;
|
|
140
146
|
if (token.required) flush();
|
|
141
147
|
}
|
|
142
148
|
if (cur.length) lineTokens.push(cur);
|
|
@@ -185,16 +191,24 @@ export class TextLayout {
|
|
|
185
191
|
// .start/.end are absolute code-unit ranges into the full text, kept
|
|
186
192
|
// for caret positioning (caretPosition / indexAt)
|
|
187
193
|
let entries = [];
|
|
188
|
-
for (
|
|
194
|
+
for (let t = 0; t < toks.length; t++) {
|
|
195
|
+
const token = toks[t];
|
|
196
|
+
// the kerning with the token before it goes before its first run
|
|
197
|
+
let kern = t ? kernBefore(token) : 0;
|
|
189
198
|
for (const frag of token.fragments) {
|
|
190
199
|
for (const run of frag.shaped.runs) {
|
|
191
|
-
|
|
200
|
+
const entry = {
|
|
192
201
|
run,
|
|
193
202
|
span: frag.span,
|
|
194
203
|
level: run.level,
|
|
195
204
|
start: frag.start + run.start,
|
|
196
205
|
end: frag.start + run.end
|
|
197
|
-
}
|
|
206
|
+
};
|
|
207
|
+
if (kern) {
|
|
208
|
+
entry.kern = kern;
|
|
209
|
+
kern = 0;
|
|
210
|
+
}
|
|
211
|
+
entries.push(entry);
|
|
198
212
|
}
|
|
199
213
|
}
|
|
200
214
|
}
|
|
@@ -234,6 +248,7 @@ export class TextLayout {
|
|
|
234
248
|
const runs = [];
|
|
235
249
|
let x = 0;
|
|
236
250
|
for (const e of entries) {
|
|
251
|
+
if (e.kern) x += e.kern;
|
|
237
252
|
const positioned = { x, width: e.run.width, run: e.run, span: e.span, start: e.start, end: e.end };
|
|
238
253
|
if (e.ellipsis) positioned.ellipsis = true;
|
|
239
254
|
runs.push(positioned);
|
|
@@ -396,10 +411,48 @@ export class TextLayout {
|
|
|
396
411
|
tokens.push(this._makeToken(text, prev, text.length, levels, spanAt, false));
|
|
397
412
|
}
|
|
398
413
|
}
|
|
414
|
+
for (let i = 1; i < tokens.length; i++) {
|
|
415
|
+
const kern = this._kernBefore(text, tokens[i - 1], tokens[i], levels);
|
|
416
|
+
if (kern) tokens[i].kernBefore = kern;
|
|
417
|
+
}
|
|
399
418
|
|
|
400
419
|
return { spans, text, baseLevel, tokens };
|
|
401
420
|
}
|
|
402
421
|
|
|
422
|
+
/**
|
|
423
|
+
* The kerning between the last letter of one token and the first of the
|
|
424
|
+
* next, where the two are on one line. Each token is shaped apart, so a
|
|
425
|
+
* pair either side of a break opportunity was never kerned — Trebuchet MS
|
|
426
|
+
* sets a space closer to an A, a T or a Y — and a line of it came out
|
|
427
|
+
* wider than a browser's, which shapes a line whole: shaping breaks only
|
|
428
|
+
* at an inline box's margin, border or padding (CSS Text 3, 7.3). Only
|
|
429
|
+
* between letters shaped alike at one left-to-right level; a line that
|
|
430
|
+
* breaks there never meets it.
|
|
431
|
+
*/
|
|
432
|
+
_kernBefore(text, prev, next, levels) {
|
|
433
|
+
if (prev.required) return 0;
|
|
434
|
+
const a = prev.fragments[prev.fragments.length - 1];
|
|
435
|
+
const b = next.fragments[0];
|
|
436
|
+
if (!a || !b || a.shaping !== b.shaping || a.span.shapeApart || b.span.shapeApart) return 0;
|
|
437
|
+
const at = a.start + a.text.length;
|
|
438
|
+
if (at !== b.start) return 0;
|
|
439
|
+
// two glyphs the face's tables cannot move against each other are set
|
|
440
|
+
// together as apart, and asked about no further: most faces kern no
|
|
441
|
+
// space, and a paragraph meets one at every word
|
|
442
|
+
const l = a.shaped.runs[a.shaped.runs.length - 1];
|
|
443
|
+
const r = b.shaped.runs[0];
|
|
444
|
+
if (!l || !r || l.font !== r.font) return 0;
|
|
445
|
+
const lg = l.glyphs[l.glyphs.length - 1];
|
|
446
|
+
const rg = r.glyphs[0];
|
|
447
|
+
if (!lg || !rg || (l.font.mayKern && !l.font.mayKern(lg.id, rg.id))) return 0;
|
|
448
|
+
const low = text.charCodeAt(at - 1);
|
|
449
|
+
const before = low >= 0xdc00 && low < 0xe000 && at >= 2 ? 2 : 1;
|
|
450
|
+
const after = text.codePointAt(at) > 0xffff ? 2 : 1;
|
|
451
|
+
const key = normalizedLevels(levels, at - before, at + after);
|
|
452
|
+
if (key.includes(',') || Number(key) & 1) return 0;
|
|
453
|
+
return this.fonts._kernAcross(text.slice(at - before, at + after), before, a.shaping, key);
|
|
454
|
+
}
|
|
455
|
+
|
|
403
456
|
/**
|
|
404
457
|
* Whether the letters that meet at an edge between two spans of a group
|
|
405
458
|
* shape otherwise together than apart: their glyphs or their advances
|
|
@@ -537,9 +590,10 @@ export class TextLayout {
|
|
|
537
590
|
for (const token of toks) {
|
|
538
591
|
// trailing whitespace does not count against the budget, exactly as it
|
|
539
592
|
// does not count during the greedy fill
|
|
540
|
-
|
|
593
|
+
const kern = kept.length ? kernBefore(token) : 0;
|
|
594
|
+
if (used + kern + token.width - token.wsWidth <= budget) {
|
|
541
595
|
kept.push(token);
|
|
542
|
-
used += token.width;
|
|
596
|
+
used += kern + token.width;
|
|
543
597
|
continue;
|
|
544
598
|
}
|
|
545
599
|
const [head] = this._forceBreak(token, budget - used);
|
package/lib/text/marks.js
CHANGED
|
@@ -102,7 +102,7 @@ export function marksCover(bits, id) {
|
|
|
102
102
|
* base)` reads the 16-bit offset stored at `pos` and answers where it
|
|
103
103
|
* points, `base` being what the format measures it from.
|
|
104
104
|
*/
|
|
105
|
-
function table(fk, tag) {
|
|
105
|
+
export function table(fk, tag) {
|
|
106
106
|
const entry = fk.directory?.tables?.[tag];
|
|
107
107
|
if (!entry) return null;
|
|
108
108
|
const stream = fk._getTableStream(tag);
|
|
@@ -163,7 +163,7 @@ function coverLookup(t, lookupList, index, bits) {
|
|
|
163
163
|
* Where the coverage a subtable is entered from starts, or -1 for a
|
|
164
164
|
* subtable this does not know.
|
|
165
165
|
*/
|
|
166
|
-
function firstCoverage(t, type, sub) {
|
|
166
|
+
export function firstCoverage(t, type, sub) {
|
|
167
167
|
const format = t.u16(sub);
|
|
168
168
|
switch (type) {
|
|
169
169
|
case 1: // single adjustment
|
|
@@ -191,7 +191,7 @@ function firstCoverage(t, type, sub) {
|
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
function addCoverage(t, coverage, bits) {
|
|
194
|
+
export function addCoverage(t, coverage, bits) {
|
|
195
195
|
const format = t.u16(coverage);
|
|
196
196
|
const count = t.u16(coverage + 2);
|
|
197
197
|
if (format === 1) {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// The glyphs a face can set closer or further from the glyph beside them,
|
|
2
|
+
// read from its tables' bytes the way ./marks.js reads its marks: a pair of
|
|
3
|
+
// letters that neither can be is set together as it is set apart, and the
|
|
4
|
+
// layout need not shape the two to find that out (`TextLayout._kernBefore`).
|
|
5
|
+
//
|
|
6
|
+
// A pair's left glyph can be moved by a pair adjustment it enters, by a
|
|
7
|
+
// cursive attachment, by a context it is the first input of, or by the
|
|
8
|
+
// pairs of a legacy `kern` table; its right glyph only by a context it is
|
|
9
|
+
// the first input of, with the left glyph behind it. Every GPOS lookup is
|
|
10
|
+
// read, whatever feature names it — a superset of what fontkit applies,
|
|
11
|
+
// which only widens what counts.
|
|
12
|
+
//
|
|
13
|
+
// Null — ask the shaper — for a face shaped through AAT's morx, a `kern`
|
|
14
|
+
// table in a format this does not read, or tables it cannot read to the end.
|
|
15
|
+
|
|
16
|
+
import { addCoverage, firstCoverage, table } from './marks.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {object} fk a fontkit font
|
|
20
|
+
* @returns {{ left: Uint8Array, right: Uint8Array }|null} a bit per glyph id,
|
|
21
|
+
* set for every glyph that can be moved against the glyph after it
|
|
22
|
+
* (`left`) or before it (`right`); null where this cannot tell
|
|
23
|
+
*/
|
|
24
|
+
export function pairGlyphs(fk) {
|
|
25
|
+
try {
|
|
26
|
+
if (fk.directory?.tables?.morx) return null;
|
|
27
|
+
const left = new Uint8Array(8192);
|
|
28
|
+
const right = new Uint8Array(8192);
|
|
29
|
+
const gpos = table(fk, 'GPOS');
|
|
30
|
+
if (gpos) {
|
|
31
|
+
const lookupList = gpos.offset(8, 0);
|
|
32
|
+
for (let index = 0, n = gpos.u16(lookupList); index < n; index++) {
|
|
33
|
+
if (!coverPairs(gpos, lookupList, index, left, right)) return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const kern = table(fk, 'kern');
|
|
37
|
+
if (kern && !kernLefts(kern, left)) return null;
|
|
38
|
+
return { left, right };
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Whether `bits` (from `pairGlyphs`) has glyph `id`. */
|
|
45
|
+
export function pairsCover(bits, id) {
|
|
46
|
+
return (bits[id >> 3] & (1 << (id & 7))) !== 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Adds what a lookup can move against a neighbour; false for one it cannot read. */
|
|
50
|
+
function coverPairs(t, lookupList, index, left, right) {
|
|
51
|
+
const lookup = t.offset(lookupList + 2 + index * 2, lookupList);
|
|
52
|
+
const type = t.u16(lookup);
|
|
53
|
+
for (let i = 0, n = t.u16(lookup + 4); i < n; i++) {
|
|
54
|
+
let sub = t.offset(lookup + 6 + i * 2, lookup);
|
|
55
|
+
let subType = type;
|
|
56
|
+
if (subType === 9) {
|
|
57
|
+
if (t.u16(sub) !== 1) return false;
|
|
58
|
+
subType = t.u16(sub + 2);
|
|
59
|
+
sub += t.u32(sub + 4);
|
|
60
|
+
}
|
|
61
|
+
// a single adjustment moves a glyph whatever is beside it, and a mark
|
|
62
|
+
// attaches with no advance of its own
|
|
63
|
+
if (subType !== 2 && subType !== 3 && subType !== 7 && subType !== 8) continue;
|
|
64
|
+
const coverage = firstCoverage(t, subType, sub);
|
|
65
|
+
if (coverage < 0) return false;
|
|
66
|
+
addCoverage(t, coverage, left);
|
|
67
|
+
if (subType === 7 || subType === 8) addCoverage(t, coverage, right);
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Adds the left glyphs of a Microsoft `kern` table's pairs; false for a
|
|
73
|
+
* table or subtable format this does not read. */
|
|
74
|
+
function kernLefts(t, left) {
|
|
75
|
+
if (t.u16(0) !== 0) return false;
|
|
76
|
+
let at = 4;
|
|
77
|
+
for (let i = 0, n = t.u16(2); i < n; i++) {
|
|
78
|
+
const length = t.u16(at + 2);
|
|
79
|
+
const format = t.u16(at + 4) >> 8;
|
|
80
|
+
if (format !== 0) return false;
|
|
81
|
+
for (let j = 0, pairs = t.u16(at + 6); j < pairs; j++) {
|
|
82
|
+
const id = t.u16(at + 14 + j * 6);
|
|
83
|
+
left[id >> 3] |= 1 << (id & 7);
|
|
84
|
+
}
|
|
85
|
+
at += length;
|
|
86
|
+
}
|
|
87
|
+
return true;
|
|
88
|
+
}
|