@hidemikimura/receipt-html-to-pdf 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/README.md +18 -10
- package/dist/receipt-html-to-pdf.min.js +15 -14
- package/dist/receipt-html-to-pdf.min.js.map +4 -4
- package/examples/cdn.html +2 -2
- package/package.json +1 -1
- package/skills/receipt-html-to-pdf/SKILL.md +22 -5
- package/skills/receipt-html-to-pdf/references/receipt-format.md +3 -1
- package/src/font/gsub.js +229 -0
- package/src/index.js +22 -1
- package/src/pacer.js +50 -0
- package/src/page.js +101 -8
- package/src/paginate.js +19 -0
- package/src/pdf/content.js +44 -2
- package/src/pdf/shading.js +113 -0
- package/src/walker/gradient.js +193 -0
- package/src/walker/image.js +77 -0
- package/src/walker/svg-path.js +342 -0
- package/src/walker/text.js +29 -2
- package/src/walker/walk.js +273 -24
- package/types/font/gsub.d.ts +47 -0
- package/types/index.d.ts +28 -1
- package/types/pacer.d.ts +11 -0
- package/types/page.d.ts +4 -1
- package/types/paginate.d.ts +1 -0
- package/types/pdf/content.d.ts +18 -1
- package/types/pdf/shading.d.ts +44 -0
- package/types/walker/gradient.d.ts +25 -0
- package/types/walker/image.d.ts +36 -0
- package/types/walker/svg-path.d.ts +31 -0
- package/types/walker/text.d.ts +4 -0
- package/types/walker/walk.d.ts +50 -1
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* SVG のパスデータと基本図形を、PDF に出せる形(絶対座標の M / L / C / Z)へ正規化する。
|
|
4
|
+
*
|
|
5
|
+
* 円弧(A)は 3 次ベジェへ、二次ベジェ(Q / T)も 3 次へ変換する。
|
|
6
|
+
* 座標は要素のユーザー単位のまま(変換行列は描画時に cm で適用する)。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {['M', number, number]|['L', number, number]|['C', number, number, number, number, number, number]|['Z']} PathSeg
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* `d` 属性を絶対座標の M / L / C / Z 列にする。
|
|
15
|
+
* @param {string} d
|
|
16
|
+
* @returns {PathSeg[]}
|
|
17
|
+
*/
|
|
18
|
+
export function parsePathData(d) {
|
|
19
|
+
/** @type {PathSeg[]} */
|
|
20
|
+
const out = [];
|
|
21
|
+
const tokens = tokenize(d);
|
|
22
|
+
let i = 0;
|
|
23
|
+
let x = 0;
|
|
24
|
+
let y = 0;
|
|
25
|
+
let startX = 0;
|
|
26
|
+
let startY = 0;
|
|
27
|
+
// 直前の制御点(S / T の反射用)
|
|
28
|
+
/** @type {[number, number]|null} */
|
|
29
|
+
let lastC = null;
|
|
30
|
+
/** @type {[number, number]|null} */
|
|
31
|
+
let lastQ = null;
|
|
32
|
+
let cmd = '';
|
|
33
|
+
|
|
34
|
+
const num = () => {
|
|
35
|
+
const t = tokens[i++];
|
|
36
|
+
return typeof t === 'number' ? t : NaN;
|
|
37
|
+
};
|
|
38
|
+
const hasNum = () => typeof tokens[i] === 'number';
|
|
39
|
+
|
|
40
|
+
while (i < tokens.length) {
|
|
41
|
+
if (typeof tokens[i] === 'string') cmd = /** @type {string} */ (tokens[i++]);
|
|
42
|
+
else if (!cmd) break; // 数値から始まる不正なデータ
|
|
43
|
+
const rel = cmd === cmd.toLowerCase();
|
|
44
|
+
const C = cmd.toUpperCase();
|
|
45
|
+
const ox = rel ? x : 0;
|
|
46
|
+
const oy = rel ? y : 0;
|
|
47
|
+
|
|
48
|
+
if (C === 'M') {
|
|
49
|
+
x = num() + ox;
|
|
50
|
+
y = num() + oy;
|
|
51
|
+
out.push(['M', x, y]);
|
|
52
|
+
startX = x;
|
|
53
|
+
startY = y;
|
|
54
|
+
lastC = lastQ = null;
|
|
55
|
+
// 続く座標対は L / l 扱い
|
|
56
|
+
cmd = rel ? 'l' : 'L';
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (C === 'Z') {
|
|
60
|
+
out.push(['Z']);
|
|
61
|
+
x = startX;
|
|
62
|
+
y = startY;
|
|
63
|
+
lastC = lastQ = null;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (C === 'L') {
|
|
67
|
+
x = num() + ox;
|
|
68
|
+
y = num() + oy;
|
|
69
|
+
out.push(['L', x, y]);
|
|
70
|
+
lastC = lastQ = null;
|
|
71
|
+
} else if (C === 'H') {
|
|
72
|
+
x = num() + ox;
|
|
73
|
+
out.push(['L', x, y]);
|
|
74
|
+
lastC = lastQ = null;
|
|
75
|
+
} else if (C === 'V') {
|
|
76
|
+
y = num() + oy;
|
|
77
|
+
out.push(['L', x, y]);
|
|
78
|
+
lastC = lastQ = null;
|
|
79
|
+
} else if (C === 'C') {
|
|
80
|
+
const x1 = num() + ox;
|
|
81
|
+
const y1 = num() + oy;
|
|
82
|
+
const x2 = num() + ox;
|
|
83
|
+
const y2 = num() + oy;
|
|
84
|
+
x = num() + ox;
|
|
85
|
+
y = num() + oy;
|
|
86
|
+
out.push(['C', x1, y1, x2, y2, x, y]);
|
|
87
|
+
lastC = [x2, y2];
|
|
88
|
+
lastQ = null;
|
|
89
|
+
} else if (C === 'S') {
|
|
90
|
+
const rx = lastC ? 2 * x - lastC[0] : x;
|
|
91
|
+
const ry = lastC ? 2 * y - lastC[1] : y;
|
|
92
|
+
const x2 = num() + ox;
|
|
93
|
+
const y2 = num() + oy;
|
|
94
|
+
x = num() + ox;
|
|
95
|
+
y = num() + oy;
|
|
96
|
+
out.push(['C', rx, ry, x2, y2, x, y]);
|
|
97
|
+
lastC = [x2, y2];
|
|
98
|
+
lastQ = null;
|
|
99
|
+
} else if (C === 'Q' || C === 'T') {
|
|
100
|
+
let qx;
|
|
101
|
+
let qy;
|
|
102
|
+
if (C === 'Q') {
|
|
103
|
+
qx = num() + ox;
|
|
104
|
+
qy = num() + oy;
|
|
105
|
+
} else {
|
|
106
|
+
qx = lastQ ? 2 * x - lastQ[0] : x;
|
|
107
|
+
qy = lastQ ? 2 * y - lastQ[1] : y;
|
|
108
|
+
}
|
|
109
|
+
const px = num() + ox;
|
|
110
|
+
const py = num() + oy;
|
|
111
|
+
// 二次 → 三次
|
|
112
|
+
out.push(['C', x + (2 / 3) * (qx - x), y + (2 / 3) * (qy - y), px + (2 / 3) * (qx - px), py + (2 / 3) * (qy - py), px, py]);
|
|
113
|
+
x = px;
|
|
114
|
+
y = py;
|
|
115
|
+
lastQ = [qx, qy];
|
|
116
|
+
lastC = null;
|
|
117
|
+
} else if (C === 'A') {
|
|
118
|
+
const rx = num();
|
|
119
|
+
const ry = num();
|
|
120
|
+
const rot = num();
|
|
121
|
+
const large = num();
|
|
122
|
+
const sweep = num();
|
|
123
|
+
const px = num() + ox;
|
|
124
|
+
const py = num() + oy;
|
|
125
|
+
for (const seg of arcToCurves(x, y, px, py, rx, ry, rot, large !== 0, sweep !== 0)) out.push(seg);
|
|
126
|
+
x = px;
|
|
127
|
+
y = py;
|
|
128
|
+
lastC = lastQ = null;
|
|
129
|
+
} else {
|
|
130
|
+
break; // 未知のコマンド
|
|
131
|
+
}
|
|
132
|
+
if (!hasNum() && typeof tokens[i] !== 'string') break;
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* `d` を数値とコマンド文字に分解する。
|
|
139
|
+
* @param {string} d
|
|
140
|
+
* @returns {(string|number)[]}
|
|
141
|
+
*/
|
|
142
|
+
function tokenize(d) {
|
|
143
|
+
/** @type {(string|number)[]} */
|
|
144
|
+
const out = [];
|
|
145
|
+
const re = /([MmLlHhVvCcSsQqTtAaZz])|(-?(?:\d*\.\d+|\d+)(?:[eE][-+]?\d+)?)/g;
|
|
146
|
+
let m;
|
|
147
|
+
while ((m = re.exec(d))) {
|
|
148
|
+
if (m[1]) out.push(m[1]);
|
|
149
|
+
else out.push(parseFloat(/** @type {string} */ (m[2])));
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 楕円円弧を 3 次ベジェ列にする(SVG 仕様 F.6 の実装)。
|
|
156
|
+
* @param {number} x1 @param {number} y1 @param {number} x2 @param {number} y2
|
|
157
|
+
* @param {number} rx @param {number} ry @param {number} rotDeg
|
|
158
|
+
* @param {boolean} large @param {boolean} sweep
|
|
159
|
+
* @returns {PathSeg[]}
|
|
160
|
+
*/
|
|
161
|
+
export function arcToCurves(x1, y1, x2, y2, rx, ry, rotDeg, large, sweep) {
|
|
162
|
+
if (x1 === x2 && y1 === y2) return [];
|
|
163
|
+
rx = Math.abs(rx);
|
|
164
|
+
ry = Math.abs(ry);
|
|
165
|
+
if (rx === 0 || ry === 0) return [['L', x2, y2]];
|
|
166
|
+
|
|
167
|
+
const phi = (rotDeg * Math.PI) / 180;
|
|
168
|
+
const cosP = Math.cos(phi);
|
|
169
|
+
const sinP = Math.sin(phi);
|
|
170
|
+
const dx = (x1 - x2) / 2;
|
|
171
|
+
const dy = (y1 - y2) / 2;
|
|
172
|
+
const x1p = cosP * dx + sinP * dy;
|
|
173
|
+
const y1p = -sinP * dx + cosP * dy;
|
|
174
|
+
|
|
175
|
+
// 半径が小さすぎる場合は広げる(仕様 F.6.6)
|
|
176
|
+
const lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry);
|
|
177
|
+
if (lambda > 1) {
|
|
178
|
+
const s = Math.sqrt(lambda);
|
|
179
|
+
rx *= s;
|
|
180
|
+
ry *= s;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const sign = large === sweep ? -1 : 1;
|
|
184
|
+
const num = rx * rx * ry * ry - rx * rx * y1p * y1p - ry * ry * x1p * x1p;
|
|
185
|
+
const den = rx * rx * y1p * y1p + ry * ry * x1p * x1p;
|
|
186
|
+
const co = sign * Math.sqrt(Math.max(0, num / den));
|
|
187
|
+
const cxp = (co * rx * y1p) / ry;
|
|
188
|
+
const cyp = (-co * ry * x1p) / rx;
|
|
189
|
+
const cx = cosP * cxp - sinP * cyp + (x1 + x2) / 2;
|
|
190
|
+
const cy = sinP * cxp + cosP * cyp + (y1 + y2) / 2;
|
|
191
|
+
|
|
192
|
+
const angle = (/** @type {number} */ ux, /** @type {number} */ uy, /** @type {number} */ vx, /** @type {number} */ vy) => {
|
|
193
|
+
const dot = ux * vx + uy * vy;
|
|
194
|
+
const len = Math.hypot(ux, uy) * Math.hypot(vx, vy);
|
|
195
|
+
let a = Math.acos(Math.min(1, Math.max(-1, dot / (len || 1))));
|
|
196
|
+
if (ux * vy - uy * vx < 0) a = -a;
|
|
197
|
+
return a;
|
|
198
|
+
};
|
|
199
|
+
const theta1 = angle(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry);
|
|
200
|
+
let dTheta = angle((x1p - cxp) / rx, (y1p - cyp) / ry, (-x1p - cxp) / rx, (-y1p - cyp) / ry);
|
|
201
|
+
if (!sweep && dTheta > 0) dTheta -= 2 * Math.PI;
|
|
202
|
+
if (sweep && dTheta < 0) dTheta += 2 * Math.PI;
|
|
203
|
+
|
|
204
|
+
// 1 区間あたり 90 度以下になるよう分割する
|
|
205
|
+
const parts = Math.max(1, Math.ceil(Math.abs(dTheta / (Math.PI / 2))));
|
|
206
|
+
const delta = dTheta / parts;
|
|
207
|
+
const t = ((4 / 3) * Math.tan(delta / 4));
|
|
208
|
+
/** @type {PathSeg[]} */
|
|
209
|
+
const out = [];
|
|
210
|
+
let th = theta1;
|
|
211
|
+
for (let i = 0; i < parts; i++) {
|
|
212
|
+
const th2 = th + delta;
|
|
213
|
+
const cos1 = Math.cos(th);
|
|
214
|
+
const sin1 = Math.sin(th);
|
|
215
|
+
const cos2 = Math.cos(th2);
|
|
216
|
+
const sin2 = Math.sin(th2);
|
|
217
|
+
/** 楕円上の点とその接線から制御点を作る */
|
|
218
|
+
const map = (/** @type {number} */ ex, /** @type {number} */ ey) => [cosP * rx * ex - sinP * ry * ey + cx, sinP * rx * ex + cosP * ry * ey + cy];
|
|
219
|
+
const [px1, py1] = map(cos1, sin1);
|
|
220
|
+
const [px2, py2] = map(cos2, sin2);
|
|
221
|
+
const [c1x, c1y] = map(cos1 - t * sin1, sin1 + t * cos1);
|
|
222
|
+
const [c2x, c2y] = map(cos2 + t * sin2, sin2 - t * cos2);
|
|
223
|
+
void px1;
|
|
224
|
+
void py1;
|
|
225
|
+
out.push(['C', /** @type {number} */ (c1x), /** @type {number} */ (c1y), /** @type {number} */ (c2x), /** @type {number} */ (c2y), /** @type {number} */ (px2), /** @type {number} */ (py2)]);
|
|
226
|
+
th = th2;
|
|
227
|
+
}
|
|
228
|
+
return out;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* 基本図形をパスにする。対応しない要素は null。
|
|
233
|
+
* @param {Element} el
|
|
234
|
+
* @param {(name: string) => string} attr 属性値(プレゼンテーション属性は computed style を優先しない純粋な幾何属性)
|
|
235
|
+
* @returns {PathSeg[]|null}
|
|
236
|
+
*/
|
|
237
|
+
export function shapeToPath(el, attr) {
|
|
238
|
+
const n = (/** @type {string} */ name, /** @type {number} */ dflt = 0) => {
|
|
239
|
+
const v = parseFloat(attr(name));
|
|
240
|
+
return Number.isFinite(v) ? v : dflt;
|
|
241
|
+
};
|
|
242
|
+
switch (el.tagName) {
|
|
243
|
+
case 'path': {
|
|
244
|
+
const d = attr('d');
|
|
245
|
+
return d ? parsePathData(d) : [];
|
|
246
|
+
}
|
|
247
|
+
case 'rect': {
|
|
248
|
+
const x = n('x');
|
|
249
|
+
const y = n('y');
|
|
250
|
+
const w = n('width');
|
|
251
|
+
const h = n('height');
|
|
252
|
+
if (w <= 0 || h <= 0) return [];
|
|
253
|
+
let rx = attr('rx') === '' || attr('rx') === 'auto' ? NaN : n('rx');
|
|
254
|
+
let ry = attr('ry') === '' || attr('ry') === 'auto' ? NaN : n('ry');
|
|
255
|
+
if (Number.isNaN(rx) && Number.isNaN(ry)) return rectPath(x, y, w, h);
|
|
256
|
+
if (Number.isNaN(rx)) rx = /** @type {number} */ (ry);
|
|
257
|
+
if (Number.isNaN(ry)) ry = /** @type {number} */ (rx);
|
|
258
|
+
rx = Math.min(rx, w / 2);
|
|
259
|
+
ry = Math.min(ry, h / 2);
|
|
260
|
+
if (rx <= 0 || ry <= 0) return rectPath(x, y, w, h);
|
|
261
|
+
return roundRectPath(x, y, w, h, rx, ry);
|
|
262
|
+
}
|
|
263
|
+
case 'circle': {
|
|
264
|
+
const r = n('r');
|
|
265
|
+
if (r <= 0) return [];
|
|
266
|
+
return ellipsePath(n('cx'), n('cy'), r, r);
|
|
267
|
+
}
|
|
268
|
+
case 'ellipse': {
|
|
269
|
+
const rx = n('rx');
|
|
270
|
+
const ry = n('ry');
|
|
271
|
+
if (rx <= 0 || ry <= 0) return [];
|
|
272
|
+
return ellipsePath(n('cx'), n('cy'), rx, ry);
|
|
273
|
+
}
|
|
274
|
+
case 'line':
|
|
275
|
+
return [
|
|
276
|
+
['M', n('x1'), n('y1')],
|
|
277
|
+
['L', n('x2'), n('y2')],
|
|
278
|
+
];
|
|
279
|
+
case 'polyline':
|
|
280
|
+
case 'polygon': {
|
|
281
|
+
const pts = attr('points')
|
|
282
|
+
.split(/[\s,]+/)
|
|
283
|
+
.map(parseFloat)
|
|
284
|
+
.filter((v) => Number.isFinite(v));
|
|
285
|
+
if (pts.length < 4) return [];
|
|
286
|
+
/** @type {PathSeg[]} */
|
|
287
|
+
const out = [['M', /** @type {number} */ (pts[0]), /** @type {number} */ (pts[1])]];
|
|
288
|
+
for (let i = 2; i + 1 < pts.length; i += 2) out.push(['L', /** @type {number} */ (pts[i]), /** @type {number} */ (pts[i + 1])]);
|
|
289
|
+
if (el.tagName === 'polygon') out.push(['Z']);
|
|
290
|
+
return out;
|
|
291
|
+
}
|
|
292
|
+
default:
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** @returns {PathSeg[]} */
|
|
298
|
+
function rectPath(/** @type {number} */ x, /** @type {number} */ y, /** @type {number} */ w, /** @type {number} */ h) {
|
|
299
|
+
return [
|
|
300
|
+
['M', x, y],
|
|
301
|
+
['L', x + w, y],
|
|
302
|
+
['L', x + w, y + h],
|
|
303
|
+
['L', x, y + h],
|
|
304
|
+
['Z'],
|
|
305
|
+
];
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const K = 0.5522847498307936; // 4/3·(√2−1)
|
|
309
|
+
|
|
310
|
+
/** @returns {PathSeg[]} */
|
|
311
|
+
function roundRectPath(/** @type {number} */ x, /** @type {number} */ y, /** @type {number} */ w, /** @type {number} */ h, /** @type {number} */ rx, /** @type {number} */ ry) {
|
|
312
|
+
const cx = rx * K;
|
|
313
|
+
const cy = ry * K;
|
|
314
|
+
const r = x + w;
|
|
315
|
+
const b = y + h;
|
|
316
|
+
return [
|
|
317
|
+
['M', x + rx, y],
|
|
318
|
+
['L', r - rx, y],
|
|
319
|
+
['C', r - rx + cx, y, r, y + ry - cy, r, y + ry],
|
|
320
|
+
['L', r, b - ry],
|
|
321
|
+
['C', r, b - ry + cy, r - rx + cx, b, r - rx, b],
|
|
322
|
+
['L', x + rx, b],
|
|
323
|
+
['C', x + rx - cx, b, x, b - ry + cy, x, b - ry],
|
|
324
|
+
['L', x, y + ry],
|
|
325
|
+
['C', x, y + ry - cy, x + rx - cx, y, x + rx, y],
|
|
326
|
+
['Z'],
|
|
327
|
+
];
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** @returns {PathSeg[]} */
|
|
331
|
+
function ellipsePath(/** @type {number} */ cx, /** @type {number} */ cy, /** @type {number} */ rx, /** @type {number} */ ry) {
|
|
332
|
+
const ox = rx * K;
|
|
333
|
+
const oy = ry * K;
|
|
334
|
+
return [
|
|
335
|
+
['M', cx + rx, cy],
|
|
336
|
+
['C', cx + rx, cy + oy, cx + ox, cy + ry, cx, cy + ry],
|
|
337
|
+
['C', cx - ox, cy + ry, cx - rx, cy + oy, cx - rx, cy],
|
|
338
|
+
['C', cx - rx, cy - oy, cx - ox, cy - ry, cx, cy - ry],
|
|
339
|
+
['C', cx + ox, cy - ry, cx + rx, cy - oy, cx + rx, cy],
|
|
340
|
+
['Z'],
|
|
341
|
+
];
|
|
342
|
+
}
|
package/src/walker/text.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Range を 1 文字ずつ張って各グリフの矩形を実測し、行ごとにグリフ列(GID・ペン位置)を返す。
|
|
5
5
|
* ブラウザのカーニング・letter-spacing・禁則・両端揃えの結果がそのまま位置に反映される。
|
|
6
6
|
*/
|
|
7
|
+
import { buildSubstitution } from '../font/gsub.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* @typedef {object} MeasuredLine
|
|
@@ -24,6 +25,7 @@
|
|
|
24
25
|
* @property {'normal'|'italic'} fstyle
|
|
25
26
|
* @property {number} size px
|
|
26
27
|
* @property {'font'|'measure'|'auto'} textMeasure
|
|
28
|
+
* @property {string[]} features ブラウザが有効にしている OpenType 機能タグ(GSUB の単一置換を再現する)
|
|
27
29
|
* @property {(w: import('../index.js').ConversionWarning) => void} warn
|
|
28
30
|
* @property {Element} element
|
|
29
31
|
*/
|
|
@@ -73,12 +75,37 @@ export function measureText(node, style, o) {
|
|
|
73
75
|
}
|
|
74
76
|
cur.top = Math.min(cur.top, top);
|
|
75
77
|
cur.bottom = Math.max(cur.bottom, rect.bottom);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
+
// GSUB(単一置換): ブラウザが有効にした機能と同じ置換を適用する
|
|
79
|
+
const subst = o.features.length ? substitutionFor(font.parsed, o.features) : null;
|
|
80
|
+
const outGid = subst ? subst(gid) : gid;
|
|
81
|
+
const advance = ((font.parsed.advances[outGid] ?? 0) * o.size) / font.parsed.unitsPerEm;
|
|
82
|
+
cur.glyphs.push({ gid: outGid, cp: cpForUnicode, x: rect.left, advance });
|
|
78
83
|
}
|
|
79
84
|
return lines;
|
|
80
85
|
}
|
|
81
86
|
|
|
87
|
+
/** @type {WeakMap<import('../font/parse.js').ParsedFont, Map<string, ((gid: number) => number)|null>>} */
|
|
88
|
+
const substCache = new WeakMap();
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* (フォント, 機能集合) ごとの置換関数。結果は null も含めてキャッシュする。
|
|
92
|
+
* @param {import('../font/parse.js').ParsedFont} parsed
|
|
93
|
+
* @param {string[]} features
|
|
94
|
+
* @returns {((gid: number) => number)|null}
|
|
95
|
+
*/
|
|
96
|
+
function substitutionFor(parsed, features) {
|
|
97
|
+
let byKey = substCache.get(parsed);
|
|
98
|
+
if (!byKey) {
|
|
99
|
+
byKey = new Map();
|
|
100
|
+
substCache.set(parsed, byKey);
|
|
101
|
+
}
|
|
102
|
+
const key = [...features].sort().join(',');
|
|
103
|
+
if (byKey.has(key)) return byKey.get(key) ?? null;
|
|
104
|
+
const fn = buildSubstitution(parsed, features);
|
|
105
|
+
byKey.set(key, fn);
|
|
106
|
+
return fn;
|
|
107
|
+
}
|
|
108
|
+
|
|
82
109
|
/**
|
|
83
110
|
* 1 文字分の矩形のうち幅を持つものを選ぶ。行末の折り返し位置では 2 つ返ることがある。
|
|
84
111
|
* @param {DOMRectList} rects
|