@arraypress/covers 1.0.0 → 1.1.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 +76 -0
- package/README.md +296 -11
- package/package.json +33 -5
- package/recipes/brand.json +1143 -0
- package/schema/recipe.schema.json +7300 -770
- package/src/animate/index.d.ts +19 -0
- package/src/animate/index.js +116 -0
- package/src/builtins.js +4 -0
- package/src/effects/ascii.d.ts +7 -0
- package/src/effects/ascii.js +93 -0
- package/src/effects/buffer.d.ts +60 -0
- package/src/effects/buffer.js +472 -0
- package/src/effects/detail.d.ts +12 -0
- package/src/effects/detail.js +124 -0
- package/src/effects/digital.d.ts +14 -0
- package/src/effects/digital.js +216 -0
- package/src/effects/distort.d.ts +20 -0
- package/src/effects/distort.js +314 -0
- package/src/effects/dither.d.ts +8 -0
- package/src/effects/dither.js +147 -0
- package/src/effects/drop-shadow.d.ts +4 -0
- package/src/effects/drop-shadow.js +45 -0
- package/src/effects/extrude.d.ts +5 -0
- package/src/effects/extrude.js +55 -0
- package/src/effects/film.d.ts +11 -0
- package/src/effects/film.js +204 -0
- package/src/effects/glow.d.ts +8 -0
- package/src/effects/glow.js +56 -0
- package/src/effects/grade.d.ts +30 -0
- package/src/effects/grade.js +274 -0
- package/src/effects/halftone.d.ts +8 -0
- package/src/effects/halftone.js +95 -0
- package/src/effects/index.d.ts +125 -0
- package/src/effects/index.js +277 -0
- package/src/effects/ink.d.ts +12 -0
- package/src/effects/ink.js +70 -0
- package/src/effects/lens.d.ts +14 -0
- package/src/effects/lens.js +277 -0
- package/src/effects/light.d.ts +10 -0
- package/src/effects/light.js +59 -0
- package/src/effects/looks.d.ts +9 -0
- package/src/effects/looks.js +74 -0
- package/src/effects/mask.d.ts +52 -0
- package/src/effects/mask.js +314 -0
- package/src/effects/motion.d.ts +6 -0
- package/src/effects/motion.js +60 -0
- package/src/effects/newsprint.d.ts +6 -0
- package/src/effects/newsprint.js +89 -0
- package/src/effects/optics.d.ts +10 -0
- package/src/effects/optics.js +213 -0
- package/src/effects/options.d.ts +6 -0
- package/src/effects/options.js +106 -0
- package/src/effects/paint-presets.d.ts +10 -0
- package/src/effects/paint-presets.js +47 -0
- package/src/effects/paint.d.ts +49 -0
- package/src/effects/paint.js +66 -0
- package/src/effects/pixels.d.ts +24 -0
- package/src/effects/pixels.js +138 -0
- package/src/effects/polar.d.ts +10 -0
- package/src/effects/polar.js +265 -0
- package/src/effects/region.d.ts +8 -0
- package/src/effects/region.js +136 -0
- package/src/effects/round-border.d.ts +5 -0
- package/src/effects/round-border.js +62 -0
- package/src/effects/screen.d.ts +14 -0
- package/src/effects/screen.js +212 -0
- package/src/effects/sort.d.ts +8 -0
- package/src/effects/sort.js +125 -0
- package/src/effects/symmetry.d.ts +6 -0
- package/src/effects/symmetry.js +74 -0
- package/src/effects/tile.d.ts +7 -0
- package/src/effects/tile.js +55 -0
- package/src/effects/tone.d.ts +16 -0
- package/src/effects/tone.js +117 -0
- package/src/effects/transform.d.ts +35 -0
- package/src/effects/transform.js +127 -0
- package/src/effects/util.d.ts +16 -0
- package/src/effects/util.js +85 -0
- package/src/extract.js +4 -3
- package/src/fonts.d.ts +55 -0
- package/src/fonts.js +246 -53
- package/src/formats/cut.d.ts +10 -0
- package/src/formats/cut.js +77 -0
- package/src/formats/index.d.ts +6 -0
- package/src/formats/index.js +9 -0
- package/src/formats/presets.d.ts +41 -0
- package/src/formats/presets.js +89 -0
- package/src/formats/print.d.ts +25 -0
- package/src/formats/print.js +117 -0
- package/src/formats/zones.d.ts +10 -0
- package/src/formats/zones.js +64 -0
- package/src/geometry.js +1 -1
- package/src/icons.js +43 -9
- package/src/index.d.ts +205 -19
- package/src/index.js +12 -4
- package/src/inter-data.js +27 -0
- package/src/json-recipe.js +181 -23
- package/src/kit/index.d.ts +20 -0
- package/src/kit/index.js +58 -0
- package/src/layer-options.js +23 -4
- package/src/layers/barcode.d.ts +19 -0
- package/src/layers/barcode.js +63 -0
- package/src/layers/code128.d.ts +4 -0
- package/src/layers/code128.js +28 -0
- package/src/layers/expr.d.ts +6 -0
- package/src/layers/expr.js +121 -0
- package/src/layers/geometry.d.ts +6 -0
- package/src/layers/geometry.js +238 -0
- package/src/layers/index.d.ts +18 -0
- package/src/layers/index.js +15 -0
- package/src/layers/perforation.d.ts +14 -0
- package/src/layers/perforation.js +47 -0
- package/src/layers/repeat.d.ts +29 -0
- package/src/layers/repeat.js +90 -0
- package/src/layers/shape.d.ts +46 -0
- package/src/layers/shape.js +115 -0
- package/src/layers/soft.d.ts +5 -0
- package/src/layers/soft.js +43 -0
- package/src/layers/svg.d.ts +23 -0
- package/src/layers/svg.js +109 -0
- package/src/layers/table.d.ts +25 -0
- package/src/layers/table.js +80 -0
- package/src/layers/tear.d.ts +21 -0
- package/src/layers/tear.js +74 -0
- package/src/layers/texture.d.ts +17 -0
- package/src/layers/texture.js +106 -0
- package/src/layers/util.d.ts +8 -0
- package/src/layers/util.js +48 -0
- package/src/layers.js +125 -41
- package/src/lint/image-size.d.ts +4 -0
- package/src/lint/image-size.js +34 -0
- package/src/lint/index.d.ts +16 -0
- package/src/lint/index.js +127 -0
- package/src/lint/rules.d.ts +5 -0
- package/src/lint/rules.js +19 -0
- package/src/node.d.ts +2 -0
- package/src/node.js +20 -0
- package/src/platform/bytes.d.ts +22 -0
- package/src/platform/bytes.js +103 -0
- package/src/platform/compress.d.ts +12 -0
- package/src/platform/compress.js +81 -0
- package/src/platform/harfbuzz.d.ts +9 -0
- package/src/platform/harfbuzz.js +80 -0
- package/src/platform/index.d.ts +8 -0
- package/src/platform/index.js +13 -0
- package/src/platform/load.d.ts +6 -0
- package/src/platform/load.js +72 -0
- package/src/platform/no-harfbuzz.d.ts +2 -0
- package/src/platform/no-harfbuzz.js +11 -0
- package/src/platform/node.d.ts +26 -0
- package/src/platform/node.js +86 -0
- package/src/platform/settings.d.ts +28 -0
- package/src/platform/settings.js +51 -0
- package/src/png.js +77 -28
- package/src/print.js +8 -7
- package/src/raster/index.d.ts +24 -0
- package/src/raster/index.js +171 -0
- package/src/recipes.js +4 -3
- package/src/render/backdrop.d.ts +6 -0
- package/src/render/backdrop.js +55 -0
- package/src/render/content.d.ts +5 -0
- package/src/render/content.js +58 -0
- package/src/render/context.d.ts +82 -0
- package/src/render/grain.d.ts +7 -0
- package/src/render/grain.js +113 -0
- package/src/render/group.d.ts +23 -0
- package/src/render/group.js +67 -0
- package/src/render/key.d.ts +5 -0
- package/src/render/key.js +91 -0
- package/src/render/report.d.ts +5 -0
- package/src/render/report.js +19 -0
- package/src/render/tiles.d.ts +6 -0
- package/src/render/tiles.js +77 -0
- package/src/render/use.d.ts +27 -0
- package/src/render/use.js +73 -0
- package/src/render/wrap.d.ts +14 -0
- package/src/render/wrap.js +199 -0
- package/src/render.js +303 -212
- package/src/text.d.ts +29 -0
- package/src/text.js +22 -15
- package/src/type/auto-color.d.ts +9 -0
- package/src/type/auto-color.js +44 -0
- package/src/type/fallback.d.ts +11 -0
- package/src/type/fallback.js +92 -0
- package/src/type/glyphs.d.ts +24 -0
- package/src/type/glyphs.js +87 -0
- package/src/type/index.d.ts +5 -0
- package/src/type/index.js +10 -0
- package/src/type/kern.d.ts +4 -0
- package/src/type/kern.js +144 -0
- package/src/type/layout-ref.d.ts +10 -0
- package/src/type/layout-ref.js +74 -0
- package/src/type/line.d.ts +11 -0
- package/src/type/line.js +105 -0
- package/src/type/live.d.ts +6 -0
- package/src/type/live.js +39 -0
- package/src/type/name.d.ts +4 -0
- package/src/type/name.js +32 -0
- package/src/type/path.d.ts +18 -0
- package/src/type/path.js +89 -0
- package/src/type/pixel.d.ts +9 -0
- package/src/type/pixel.js +115 -0
- package/src/type/sfnt.d.ts +20 -0
- package/src/type/sfnt.js +258 -0
- package/src/type/spans.d.ts +33 -0
- package/src/type/spans.js +138 -0
- package/src/type/symbols.d.ts +7 -0
- package/src/type/symbols.js +89 -0
- package/src/type/typeset.d.ts +10 -0
- package/src/type/typeset.js +352 -0
- package/src/type/woff2.d.ts +6 -0
- package/src/type/woff2.js +301 -0
- package/src/version.js +4 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typesetting for posters and flyers: what the `text` layer does when it's
|
|
3
|
+
* given any of the 1.1 options (the 1.0 path is untouched, so 1.0 covers
|
|
4
|
+
* render byte-identically).
|
|
5
|
+
*
|
|
6
|
+
* - **Baseline placement**: `x`, `y` put the first baseline where SVG
|
|
7
|
+
* `<text x y>` would, with `align` acting as `text-anchor`.
|
|
8
|
+
* - **Justified to a width**: `width` sets each line to exactly that width,
|
|
9
|
+
* like `textLength` (`lengthAdjust: 'spacingAndGlyphs'` scales the glyphs,
|
|
10
|
+
* the default; `'spacing'` opens the letter-spacing instead).
|
|
11
|
+
* - **Fitting**: `fit: 'width'` sizes the block so its widest line fills
|
|
12
|
+
* `width` (or `maxWidth`); `fit: 'lines'` sizes every line on its own to
|
|
13
|
+
* fill it (the stacked full-width headline); `fit: 'shrink'` shrinks to
|
|
14
|
+
* `minSize` before wrapping further.
|
|
15
|
+
* - **Stretch and slant**: `stretch: [sx, sy]` about the first baseline.
|
|
16
|
+
* - **Variable fonts**: `axes: { wdth: 50, SOFT: 100, WONK: 1 }`.
|
|
17
|
+
* - **Strokes**: `stroke`, `strokeWidth`, `fill: 'none'` (outline type), or
|
|
18
|
+
* `strokes: [{ color, width }]`, drawn widest first under the fill, for
|
|
19
|
+
* keylines.
|
|
20
|
+
* - **Text on a path**: `path` (`circle`, `arc`, `ellipse`, `wave`, or any
|
|
21
|
+
* `d`), `startOffset`, `fitPath` to fill a ring, `dy`.
|
|
22
|
+
* - **Auto contrast**: `color: 'auto'` picks white or near-black (or your
|
|
23
|
+
* `autoColors`) by contrast against whatever is drawn underneath.
|
|
24
|
+
* - **Live text**: `render: 'live'` writes `<text>` with the font embedded
|
|
25
|
+
* by `@font-face` (or linked by URL), laid out the same way, for in-browser
|
|
26
|
+
* speed and selectable text.
|
|
27
|
+
* - **Mixed runs**: `spans`, each with its own font, weight, size, colour.
|
|
28
|
+
* - **Per-glyph control**: `glyphs: { scale, dx, dy, rotate, color }` arrays.
|
|
29
|
+
* - **Fallback**: characters the font lacks come from `fallback`, fonts
|
|
30
|
+
* registered with `fallback: true`, or the built-in symbols.
|
|
31
|
+
* - **Pixel type**: `font: 'pixel'`, a built-in 5×7 face.
|
|
32
|
+
* - **Layout from data**: `fit: 'words'` (a word a line), `fit: 'wrap'`
|
|
33
|
+
* (wrap, then fill the width), `split: 'words'`, and the block's box for
|
|
34
|
+
* later layers as `{layout.<id>.bottom}` and the like.
|
|
35
|
+
*
|
|
36
|
+
* @module type/typeset
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { attrs, esc, num } from '../svg.js';
|
|
40
|
+
import { len, place, anchorAlign, unit } from '../geometry.js';
|
|
41
|
+
import { toHex } from '../color.js';
|
|
42
|
+
import { layout, caseText } from '../text.js';
|
|
43
|
+
import { paint } from '../effects/paint.js';
|
|
44
|
+
import { pos, wrap } from '../layers/util.js';
|
|
45
|
+
import { presetStrokes } from '../effects/paint-presets.js';
|
|
46
|
+
import { extrude } from '../effects/extrude.js';
|
|
47
|
+
import { dashArray } from '../layers/shape.js';
|
|
48
|
+
import { faceFor, mappedPath, setLine } from './line.js';
|
|
49
|
+
import { textPathD, pathSampler } from './path.js';
|
|
50
|
+
import { fontFace } from './live.js';
|
|
51
|
+
import { autoColor } from './auto-color.js';
|
|
52
|
+
import { fallbackFaces, shapeText } from './fallback.js';
|
|
53
|
+
import { controlGlyphs, glyphCount } from './glyphs.js';
|
|
54
|
+
import { loadSpans, spanLines, shapeSpans, spanText } from './spans.js';
|
|
55
|
+
import { recordLayout } from './layout-ref.js';
|
|
56
|
+
|
|
57
|
+
/** Options that switch the text layer onto this typesetter. */
|
|
58
|
+
export const TYPE_OPTIONS = ['x', 'y', 'width', 'justify', 'lengthAdjust', 'fit', 'stretch', 'axes', 'stroke', 'strokeWidth', 'strokes', 'fill', 'path', 'startOffset', 'fitPath', 'dy', 'render', 'autoColors', 'lines', 'spans', 'glyphs', 'fallback', 'split', 'dash', 'extrude', 'maxHeight', 'autoThreshold'];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Whether a text layer needs this typesetter.
|
|
62
|
+
*
|
|
63
|
+
* @param {any} layer
|
|
64
|
+
* @returns {boolean}
|
|
65
|
+
*/
|
|
66
|
+
export function wantsType(layer) {
|
|
67
|
+
return TYPE_OPTIONS.some((k) => layer[k] !== undefined) || layer.color === 'auto' || Array.isArray(layer.content) || layer.font === 'pixel';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A line to set: its text, size, and (with `spans`) its runs.
|
|
72
|
+
*
|
|
73
|
+
* @typedef {{ text: string, size: number, spans?: import('./spans.js').Span[] }} Line
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The `text` layer with 1.1 options.
|
|
78
|
+
*
|
|
79
|
+
* @param {any} layer
|
|
80
|
+
* @param {import('../layers.js').RenderContext} ctx
|
|
81
|
+
* @returns {Promise<import('../layers.js').Markup>}
|
|
82
|
+
*/
|
|
83
|
+
export async function typeset(layer, ctx) {
|
|
84
|
+
const rawContent = Array.isArray(layer.content) ? layer.content.join('\n') : layer.content && typeof layer.content === 'object' ? layer.content.content : layer.content;
|
|
85
|
+
const kase = layer.transform === 'uppercase' || layer.transform === 'lowercase' ? layer.transform : undefined;
|
|
86
|
+
let content = layer.spans ? '' : caseText(String(rawContent ?? ''), kase);
|
|
87
|
+
if (!layer.spans && (layer.split === 'words' || layer.fit === 'words')) content = content.split(/\s+/).filter(Boolean).join('\n');
|
|
88
|
+
const p = ctx.palette;
|
|
89
|
+
const u = unit(ctx.canvas);
|
|
90
|
+
const face = await faceFor(ctx, layer);
|
|
91
|
+
const fallbacks = await fallbackFaces(ctx, layer);
|
|
92
|
+
let size = len(layer.size, ctx.canvas, u * 0.06);
|
|
93
|
+
const ls = layer.letterSpacing ?? -0.02;
|
|
94
|
+
const lh = layer.lineHeight ?? 1.12;
|
|
95
|
+
const baseline = layer.x != null || layer.y != null;
|
|
96
|
+
const anchor = layer.anchor ?? 'bottom-left';
|
|
97
|
+
const align = layer.align ?? (baseline ? 'left' : anchorAlign(anchor));
|
|
98
|
+
const margin = len(layer.margin, ctx.canvas, /** @type {any} */ (ctx).inset ?? u * 0.07);
|
|
99
|
+
const target = layer.width != null ? len(layer.width, ctx.canvas) : undefined;
|
|
100
|
+
const maxWidth = layer.maxWidth != null ? len(layer.maxWidth, ctx.canvas) : baseline ? Infinity : ctx.width - 2 * margin;
|
|
101
|
+
const justifyTo = target ?? (layer.justify ? maxWidth : undefined);
|
|
102
|
+
const adjust = layer.lengthAdjust ?? 'spacingAndGlyphs';
|
|
103
|
+
const id = ctx.id;
|
|
104
|
+
const spans = layer.spans ? await loadSpans(layer, ctx, size) : undefined;
|
|
105
|
+
if (spans) content = spans.map((s) => s.text).join('');
|
|
106
|
+
if (!content.trim()) return { defs: '', body: '' };
|
|
107
|
+
let defs = '';
|
|
108
|
+
|
|
109
|
+
/** Shape a line at a tracking (px), through fallback, spans and per-glyph control. @type {(l: Line, tracking: number, start: number) => import('../text.js').Shaped} */
|
|
110
|
+
const shapeLine = (l, tracking, start) => {
|
|
111
|
+
const run = l.spans ? shapeSpans(l.spans, l.size, tracking - ls * l.size, face, fallbacks) : shapeText(face, l.text, l.size, tracking, fallbacks);
|
|
112
|
+
return controlGlyphs(run, layer.glyphs, { start, size: l.size, capHeight: (face.capHeight / face.upem) * l.size, canvas: ctx.canvas, text: l.text }).run;
|
|
113
|
+
};
|
|
114
|
+
/** @param {Line} l @returns {number} */
|
|
115
|
+
const natural = (l) => shapeLine(l, ls * l.size, 0).width;
|
|
116
|
+
|
|
117
|
+
// ---- Lines and sizes ----
|
|
118
|
+
/** @type {Line[]} */
|
|
119
|
+
let lines;
|
|
120
|
+
if (layer.path) {
|
|
121
|
+
lines = [{ text: content.replace(/\n/g, ' '), size, spans: spans ? spanLines(spans, size, Infinity, fallbacks).flat() : undefined }];
|
|
122
|
+
} else if (spans) {
|
|
123
|
+
const fitting = layer.fit === 'width' || layer.fit === 'lines' || layer.fit === 'words';
|
|
124
|
+
const rows = spanLines(spans, size, fitting ? Infinity : layer.fit === 'wrap' ? justifyTo ?? maxWidth : maxWidth, fallbacks);
|
|
125
|
+
lines = rows.map((r) => ({ text: spanText(r), size, spans: r }));
|
|
126
|
+
lines = fitLines(lines, layer.fit, justifyTo ?? maxWidth, natural);
|
|
127
|
+
} else if (layer.fit === 'width' || layer.fit === 'lines' || layer.fit === 'words') {
|
|
128
|
+
lines = fitLines(content.split('\n').map((t) => ({ text: t, size })), layer.fit, justifyTo ?? maxWidth, natural);
|
|
129
|
+
} else {
|
|
130
|
+
const fitTo = justifyTo ?? maxWidth;
|
|
131
|
+
const set = layout({
|
|
132
|
+
face,
|
|
133
|
+
text: content,
|
|
134
|
+
size,
|
|
135
|
+
minSize: layer.minSize != null ? len(layer.minSize, ctx.canvas) : layer.fit === 'shrink' ? size * 0.5 : undefined,
|
|
136
|
+
letterSpacing: ls,
|
|
137
|
+
// Only `fit: 'wrap'` wraps at the justification width; otherwise `width` justifies what `maxWidth` wrapped.
|
|
138
|
+
maxWidth: layer.fit === 'wrap' ? fitTo : maxWidth,
|
|
139
|
+
maxLines: layer.maxLines ?? (baseline || layer.fit === 'wrap' ? Infinity : 3),
|
|
140
|
+
ellipsis: layer.ellipsis,
|
|
141
|
+
balance: layer.balance ?? !baseline,
|
|
142
|
+
});
|
|
143
|
+
size = set.size;
|
|
144
|
+
lines = set.lines.map((l) => ({ text: l.text, size }));
|
|
145
|
+
// Wrap, then fill: every line at one size, the widest filling the width.
|
|
146
|
+
if (layer.fit === 'wrap') lines = fitLines(lines, 'width', fitTo, natural);
|
|
147
|
+
}
|
|
148
|
+
// A block taller than `maxHeight` shrinks to it.
|
|
149
|
+
if (layer.maxHeight != null && !layer.path) {
|
|
150
|
+
const cap = face.capHeight / face.upem;
|
|
151
|
+
const tall = lines[0].size * cap + lines.slice(1).reduce((h, l) => h + l.size * lh, 0);
|
|
152
|
+
const k = len(layer.maxHeight, ctx.canvas) / tall;
|
|
153
|
+
if (k < 1) lines = lines.map((l) => ({ ...l, size: l.size * k }));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ---- Paint ----
|
|
157
|
+
const strokes = layer.strokes ?? (layer.stroke ? [{ color: layer.stroke, width: layer.strokeWidth ?? '0.3vmin', dash: layer.dash }] : presetStrokes(layer.fill) ?? []);
|
|
158
|
+
const live = (layer.render ?? /** @type {any} */ (ctx).textRender) === 'live';
|
|
159
|
+
|
|
160
|
+
// ---- Text on a path ----
|
|
161
|
+
if (layer.path) {
|
|
162
|
+
const d = textPathD(layer.path, ctx.canvas);
|
|
163
|
+
const sampler = pathSampler(d);
|
|
164
|
+
const line = lines[0];
|
|
165
|
+
let run = shapeLine(line, ls * size, 0);
|
|
166
|
+
if (layer.fitPath) {
|
|
167
|
+
const gaps = Math.max(1, run.glyphs.length);
|
|
168
|
+
const gap = len(layer.fitPath === true ? 0 : layer.fitPath, ctx.canvas);
|
|
169
|
+
run = shapeLine(line, ls * size + (sampler.length - gap - run.width) / gaps, 0);
|
|
170
|
+
}
|
|
171
|
+
const so = layer.startOffset ?? (layer.fitPath ? 0 : 0.5);
|
|
172
|
+
const offset = typeof so === 'number' && so <= 1 && so >= 0 ? so * sampler.length : pos(so, 'x', ctx.canvas);
|
|
173
|
+
const start = layer.fitPath ? offset : align === 'center' || layer.align == null ? offset - run.width / 2 : align === 'right' ? offset - run.width : offset;
|
|
174
|
+
const dy = len(layer.dy, ctx.canvas, 0);
|
|
175
|
+
const adv = (/** @type {number} */ i) => (i + 1 < run.glyphs.length ? run.glyphs[i + 1].x : run.width) - run.glyphs[i].x;
|
|
176
|
+
const mids = run.glyphs.map((g, i) => g.x + adv(i) / 2);
|
|
177
|
+
const index = new Map(run.glyphs.map((g, i) => [g, i]));
|
|
178
|
+
const along = (/** @type {(g: any) => boolean} */ only) => mappedPath(face, run, size, (gx, gy, g) => {
|
|
179
|
+
const i = /** @type {number} */ (index.get(/** @type {any} */ (g)));
|
|
180
|
+
const at = sampler.at(start + mids[i]);
|
|
181
|
+
const a = gx - mids[i];
|
|
182
|
+
const across = gy + dy;
|
|
183
|
+
return [at.x + at.tx * a - at.ty * across, at.y + at.ty * a + at.tx * across];
|
|
184
|
+
}, only);
|
|
185
|
+
const whole = { x: 0, y: 0, width: ctx.width, height: ctx.height };
|
|
186
|
+
const fill = layer.fill === 'none' ? { defs: '', value: 'none' } : layer.color === 'auto' ? { defs: '', value: await autoColor(layer, ctx, whole) } : paint(layer.fill ?? layer.color, p.text, { id: `${id}f`, box: whole, canvas: ctx.canvas });
|
|
187
|
+
defs += fill.defs;
|
|
188
|
+
report(ctx, layer, content, size, face, null, { capHeight: (face.capHeight / face.upem) * size });
|
|
189
|
+
const drawn = drawBuckets(run, along, fill.value, strokes, ctx);
|
|
190
|
+
recordLayout(ctx, layer.id, whole, { x: 0, y: 0, baseline: 0, size, lines: 1 });
|
|
191
|
+
return { defs, body: wrap(layer, finish(drawn, layer, ctx)) };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ---- Measure, then place ----
|
|
195
|
+
// `width` justifies every line, except under a fit that sizes the block to
|
|
196
|
+
// its widest line (`width`, `words`, `wrap`): there the widest fills the
|
|
197
|
+
// width and the others keep their natural width.
|
|
198
|
+
const widestOnly = justifyTo != null && (layer.fit === 'width' || layer.fit === 'words' || layer.fit === 'wrap');
|
|
199
|
+
const naturals = widestOnly ? lines.map(natural) : [];
|
|
200
|
+
const widest = widestOnly ? naturals.indexOf(Math.max(...naturals)) : -1;
|
|
201
|
+
const runs = lines.map((l, i) => {
|
|
202
|
+
const fill = justifyTo != null && (!widestOnly || i === widest);
|
|
203
|
+
const w = fill ? /** @type {number} */ (justifyTo) : widestOnly ? naturals[i] : natural(l);
|
|
204
|
+
return { ...l, w, fill, cap: (face.capHeight / face.upem) * l.size };
|
|
205
|
+
});
|
|
206
|
+
const lineStep = (/** @type {number} */ i) => runs[i].size * lh;
|
|
207
|
+
let x0;
|
|
208
|
+
let y0;
|
|
209
|
+
const blockW = Math.max(...runs.map((r) => r.w));
|
|
210
|
+
if (baseline) {
|
|
211
|
+
x0 = pos(layer.x ?? 0, 'x', ctx.canvas);
|
|
212
|
+
y0 = pos(layer.y ?? 0, 'y', ctx.canvas);
|
|
213
|
+
} else {
|
|
214
|
+
let h = runs[0].cap;
|
|
215
|
+
for (let i = 1; i < runs.length; i++) h += lineStep(i);
|
|
216
|
+
const at = place({ width: blockW, height: h, canvas: ctx.canvas, anchor, margin, position: layer.position, offset: layer.offset });
|
|
217
|
+
x0 = align === 'center' ? at.x + blockW / 2 : align === 'right' ? at.x + blockW : at.x;
|
|
218
|
+
y0 = at.y + runs[0].cap;
|
|
219
|
+
}
|
|
220
|
+
const baselines = [y0];
|
|
221
|
+
for (let i = 1; i < runs.length; i++) baselines.push(baselines[i - 1] + lineStep(i));
|
|
222
|
+
const top = y0 - runs[0].cap;
|
|
223
|
+
const left = align === 'center' ? x0 - blockW / 2 : align === 'right' ? x0 - blockW : x0;
|
|
224
|
+
const box = { x: left, y: top, width: blockW, height: baselines[baselines.length - 1] - top };
|
|
225
|
+
const [sx, sy] = layer.stretch ? (Array.isArray(layer.stretch) ? layer.stretch : [1, layer.stretch]) : [1, 1];
|
|
226
|
+
// The block as drawn (stretched about the first baseline), for lint, `color: 'auto'` and later layers.
|
|
227
|
+
const drawnBox = { x: Math.min(x0 + (box.x - x0) * sx, x0 + (box.x + box.width - x0) * sx), y: Math.min(y0 + (box.y - y0) * sy, y0 + (box.y + box.height - y0) * sy), width: box.width * Math.abs(sx), height: box.height * Math.abs(sy) };
|
|
228
|
+
|
|
229
|
+
const color = layer.color === 'auto' ? await autoColor(layer, ctx, drawnBox) : undefined;
|
|
230
|
+
const fillPaint = layer.fill === 'none' ? { defs: '', value: 'none' } : paint(color ?? layer.fill ?? layer.color, p.text, { id: `${id}f`, box, canvas: ctx.canvas });
|
|
231
|
+
defs += fillPaint.defs;
|
|
232
|
+
recordLayout(ctx, layer.id, drawnBox, { x: x0, y: y0, baseline: y0 + (baselines[baselines.length - 1] - y0) * sy, size: runs[0].size, lines: runs.length });
|
|
233
|
+
|
|
234
|
+
let body;
|
|
235
|
+
/** Narrowest horizontal scale a line was set at (justification squeezes glyphs). */
|
|
236
|
+
let squeeze = 1;
|
|
237
|
+
if (live) {
|
|
238
|
+
const ff = await fontFace(face, ctx);
|
|
239
|
+
if (ff.css) defs += `<style>${ff.css}</style>`;
|
|
240
|
+
const fv = Object.entries(face.variations ?? {}).filter(([t]) => t !== 'wght').map(([t, v]) => `'${t}' ${num(v, 2)}`).join(',');
|
|
241
|
+
const anchorAttr = align === 'center' ? 'middle' : align === 'right' ? 'end' : undefined;
|
|
242
|
+
const strokeAttrs = strokes.length ? { stroke: toHex(strokes[0].color), 'stroke-width': len(strokes[0].width, ctx.canvas), 'paint-order': 'stroke', 'stroke-linejoin': 'round' } : {};
|
|
243
|
+
body = runs.map((r, i) => `<text${attrs({
|
|
244
|
+
x: x0, y: baselines[i], 'font-family': ff.family, 'font-size': r.size,
|
|
245
|
+
'font-weight': layer.weight ?? 600, 'font-style': layer.style && layer.style !== 'normal' ? layer.style : undefined,
|
|
246
|
+
'letter-spacing': ls ? num(ls * r.size, 3) : undefined,
|
|
247
|
+
'text-anchor': anchorAttr,
|
|
248
|
+
textLength: r.fill ? justifyTo : undefined, lengthAdjust: r.fill ? adjust : undefined,
|
|
249
|
+
style: fv ? `font-variation-settings:${fv}` : undefined,
|
|
250
|
+
fill: fillPaint.value, ...strokeAttrs,
|
|
251
|
+
})}>${esc(r.text)}</text>`).join('');
|
|
252
|
+
} else {
|
|
253
|
+
// Each line once; then its glyphs drawn per paint (spans and per-glyph colours).
|
|
254
|
+
let start = 0;
|
|
255
|
+
const set = runs.map((r, i) => {
|
|
256
|
+
const first = start;
|
|
257
|
+
start += glyphCount(r.text);
|
|
258
|
+
const line = setLine({ face, text: r.text, size: r.size, tracking: ls * r.size, x: x0, y: baselines[i], align, width: r.fill ? justifyTo : undefined, adjust, shape: (t) => shapeLine(r, t, first), only: () => false });
|
|
259
|
+
squeeze = Math.min(squeeze, line.scaleX);
|
|
260
|
+
return { ...line, baseline: baselines[i], size: r.size };
|
|
261
|
+
});
|
|
262
|
+
const draw = (/** @type {(g: any) => boolean} */ only) => set.map((l) => mappedPath(face, l.run, l.size, (gx, gy) => [l.left + gx * l.scaleX, l.baseline + gy], only)).join('');
|
|
263
|
+
body = finish(drawBuckets({ glyphs: set.flatMap((l) => l.run.glyphs) }, draw, fillPaint.value, strokes, ctx), layer, ctx);
|
|
264
|
+
}
|
|
265
|
+
// Mirrored (negative) stretch is still that tall.
|
|
266
|
+
report(ctx, layer, content, runs[0].size * Math.abs(sy), face, drawnBox, {
|
|
267
|
+
capHeight: Math.min(...runs.map((r) => r.cap)) * Math.abs(sy),
|
|
268
|
+
squeeze: squeeze * Math.abs(sx),
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
if (layer.stretch) body = `<g transform="translate(${num(x0)} ${num(y0)}) scale(${num(sx, 4)} ${num(sy, 4)}) translate(${num(-x0)} ${num(-y0)})">${body}</g>`;
|
|
272
|
+
// Opacity and blend (renderWrapped has already taken them off a wrapped layer).
|
|
273
|
+
return { defs, body: wrap(layer, body) };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Lines sized to a width: `'width'` (one size, the widest line fills it),
|
|
278
|
+
* `'lines'` and `'words'`… `'lines'` sizes each line on its own; `'words'`
|
|
279
|
+
* (a word a line) is sized like `'width'`.
|
|
280
|
+
*
|
|
281
|
+
* @param {Line[]} lines @param {string} mode @param {number} fitTo
|
|
282
|
+
* @param {(l: Line) => number} natural - A line's width at its size.
|
|
283
|
+
* @returns {Line[]}
|
|
284
|
+
*/
|
|
285
|
+
function fitLines(lines, mode, fitTo, natural) {
|
|
286
|
+
if (mode !== 'width' && mode !== 'lines' && mode !== 'words') return lines;
|
|
287
|
+
if (!Number.isFinite(fitTo)) throw new TypeError('fit needs a width (width or maxWidth)');
|
|
288
|
+
const nat = lines.map(natural);
|
|
289
|
+
if (mode === 'lines') return lines.map((l, i) => ({ ...l, size: (l.size * fitTo) / Math.max(nat[i], 1e-6) }));
|
|
290
|
+
const k = fitTo / Math.max(...nat, 1e-6);
|
|
291
|
+
return lines.map((l) => ({ ...l, size: l.size * k }));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Draw glyphs by paint: strokes under everything (widest first), then the
|
|
296
|
+
* fill, then each colour bucket (spans, per-glyph colours) on its own path.
|
|
297
|
+
*
|
|
298
|
+
* @param {{ glyphs: any[] }} run - Every glyph drawn (for its paint keys).
|
|
299
|
+
* @param {(only: (g: any) => boolean) => string} draw - Path data for the glyphs a filter keeps.
|
|
300
|
+
* @param {string} fill
|
|
301
|
+
* @param {any[]} strokes
|
|
302
|
+
* @param {import('../layers.js').RenderContext} ctx
|
|
303
|
+
* @returns {string}
|
|
304
|
+
*/
|
|
305
|
+
function drawBuckets(run, draw, fill, strokes, ctx) {
|
|
306
|
+
const paints = [...new Set(run.glyphs.map((g) => g.paint).filter(Boolean))];
|
|
307
|
+
if (!paints.length) return drawPaths([draw(() => true)], fill, strokes, ctx);
|
|
308
|
+
let out = strokes.length ? drawPaths([draw(() => true)], 'none', strokes, ctx) : '';
|
|
309
|
+
const plain = draw((g) => !g.paint);
|
|
310
|
+
if (plain && fill !== 'none') out += `<path${attrs({ d: plain, fill })}/>`;
|
|
311
|
+
for (const c of paints) out += `<path${attrs({ d: draw((g) => g.paint === c), fill: c })}/>`;
|
|
312
|
+
return out;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* The text's extrusion (`extrude: { depth, angle, color, to, steps }` or a depth).
|
|
317
|
+
*
|
|
318
|
+
* @param {string} body @param {any} layer @param {import('../layers.js').RenderContext} ctx
|
|
319
|
+
* @returns {string}
|
|
320
|
+
*/
|
|
321
|
+
function finish(body, layer, ctx) {
|
|
322
|
+
if (!layer.extrude) return body;
|
|
323
|
+
const fx = typeof layer.extrude === 'object' ? layer.extrude : { depth: layer.extrude };
|
|
324
|
+
const out = extrude({ kind: 'extrude', ...fx }, { defs: '', body }, ctx, `${ctx.id}t`);
|
|
325
|
+
return `<defs>${out.defs}</defs>${out.body}`;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Stroke copies widest first, then the fill.
|
|
330
|
+
*
|
|
331
|
+
* @param {string[]} ds @param {string} fill @param {any[]} strokes @param {import('../layers.js').RenderContext} ctx
|
|
332
|
+
* @returns {string}
|
|
333
|
+
*/
|
|
334
|
+
function drawPaths(ds, fill, strokes, ctx) {
|
|
335
|
+
const d = ds.join('');
|
|
336
|
+
const sorted = [...strokes].sort((a, b) => len(b.width, ctx.canvas) - len(a.width, ctx.canvas));
|
|
337
|
+
let out = sorted.map((s) => `<path${attrs({ d, fill: s.fill ? toHex(s.fill) : 'none', stroke: toHex(s.color), 'stroke-width': len(s.width, ctx.canvas), 'stroke-linejoin': 'round', 'stroke-linecap': 'round', 'stroke-dasharray': s.dash ? dashArray(s.dash, len(s.width, ctx.canvas), ctx) : undefined })}/>`).join('');
|
|
338
|
+
if (fill !== 'none' || !strokes.length) out += `<path${attrs({ d, fill })}/>`;
|
|
339
|
+
return out;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Note a text block for `lint`: the whole block as drawn (every line,
|
|
344
|
+
* stretched), its smallest capitals, how far justification squeezed it.
|
|
345
|
+
*
|
|
346
|
+
* @param {import('../layers.js').RenderContext} ctx @param {any} layer @param {string} content @param {number} size
|
|
347
|
+
* @param {import('../fonts.js').LoadedFont} face @param {{ x: number, y: number, width: number, height: number } | null} box
|
|
348
|
+
* @param {{ capHeight: number, squeeze?: number }} drawn
|
|
349
|
+
*/
|
|
350
|
+
function report(ctx, layer, content, size, face, box, drawn) {
|
|
351
|
+
/** @type {any} */ (ctx).report?.({ kind: 'text', id: layer.id, content, size, capHeight: drawn.capHeight, box, squeeze: drawn.squeeze ?? 1 });
|
|
352
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// WOFF2 to sfnt.
|
|
2
|
+
|
|
3
|
+
/** A WOFF2 file's flavour, table directory and Brotli payload. */
|
|
4
|
+
export function woff2Directory(buf: Uint8Array): { flavor: number; tables: { tag: string; origLength: number; length: number; transformed: boolean; data: Uint8Array }[]; payload: Uint8Array };
|
|
5
|
+
/** WOFF2 bytes as sfnt, given its decompressed stream (glyf, loca and hmtx rebuilt). */
|
|
6
|
+
export function woff2ToSfnt(buf: Uint8Array, stream: Uint8Array | undefined): Uint8Array;
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WOFF2 to a bare sfnt, in JavaScript: the Brotli stream decompressed (by
|
|
3
|
+
* the platform: Node's zlib, or a `DecompressionStream('brotli')`), then
|
|
4
|
+
* the transformed `glyf`, `loca` and `hmtx` tables rebuilt (W3C WOFF2 §5).
|
|
5
|
+
* Font collections aren't supported.
|
|
6
|
+
*
|
|
7
|
+
* @module type/woff2
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** The 63 known table tags, by index (WOFF2 §5.1). */
|
|
11
|
+
const KNOWN = ['cmap', 'head', 'hhea', 'hmtx', 'maxp', 'name', 'OS/2', 'post', 'cvt ', 'fpgm', 'glyf', 'loca', 'prep', 'CFF ', 'VORG', 'EBDT', 'EBLC', 'gasp', 'hdmx', 'kern', 'LTSH', 'PCLT', 'VDMX', 'vhea', 'vmtx', 'BASE', 'GDEF', 'GPOS', 'GSUB', 'EBSC', 'JSTF', 'MATH', 'CBDT', 'CBLC', 'COLR', 'CPAL', 'SVG ', 'sbix', 'acnt', 'avar', 'bdat', 'bloc', 'bsln', 'cvar', 'fdsc', 'feat', 'fmtx', 'fvar', 'gvar', 'hsty', 'just', 'lcar', 'mort', 'morx', 'opbd', 'prop', 'trak', 'Zapf', 'Silf', 'Glat', 'Gloc', 'Feat', 'Sill'];
|
|
12
|
+
|
|
13
|
+
/** A cursor over bytes. */
|
|
14
|
+
class Reader {
|
|
15
|
+
/** @param {Uint8Array} b @param {number} [at=0] */
|
|
16
|
+
constructor(b, at = 0) { this.b = b; this.v = new DataView(b.buffer, b.byteOffset, b.byteLength); this.at = at; }
|
|
17
|
+
u8() { return this.b[this.at++]; }
|
|
18
|
+
u16() { const x = this.v.getUint16(this.at); this.at += 2; return x; }
|
|
19
|
+
i16() { const x = this.v.getInt16(this.at); this.at += 2; return x; }
|
|
20
|
+
u32() { const x = this.v.getUint32(this.at); this.at += 4; return x; }
|
|
21
|
+
/** UIntBase128. */
|
|
22
|
+
base128() { let x = 0; for (let i = 0; i < 5; i++) { const c = this.u8(); x = x * 128 + (c & 0x7f); if (!(c & 0x80)) return x; } throw new TypeError('WOFF2: bad UIntBase128'); }
|
|
23
|
+
/** 255UInt16. */
|
|
24
|
+
u255() { const c = this.u8(); return c === 253 ? this.u16() : c === 255 ? this.u8() + 253 : c === 254 ? this.u8() + 506 : c; }
|
|
25
|
+
/** @param {number} n */
|
|
26
|
+
bytes(n) { const out = this.b.subarray(this.at, this.at + n); this.at += n; return out; }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A WOFF2 file's header and table directory, and where its Brotli stream is.
|
|
31
|
+
*
|
|
32
|
+
* @param {Uint8Array} buf
|
|
33
|
+
* @returns {{ flavor: number, tables: { tag: string, origLength: number, length: number, transformed: boolean, data: Uint8Array }[], payload: Uint8Array }}
|
|
34
|
+
* @throws {TypeError} On a collection or damaged data.
|
|
35
|
+
*/
|
|
36
|
+
export function woff2Directory(buf) {
|
|
37
|
+
try {
|
|
38
|
+
return directory(buf);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
if (err instanceof RangeError) throw new TypeError('WOFF2: the font data is damaged or cut short');
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** @param {Uint8Array} buf @returns {ReturnType<typeof woff2Directory>} */
|
|
46
|
+
function directory(buf) {
|
|
47
|
+
const r = new Reader(buf);
|
|
48
|
+
if (r.u32() !== 0x774f4632) throw new TypeError('Not a WOFF2 font');
|
|
49
|
+
const flavor = r.u32();
|
|
50
|
+
if (flavor === 0x74746366) throw new TypeError('WOFF2 font collections are not supported');
|
|
51
|
+
r.u32();
|
|
52
|
+
const count = r.u16();
|
|
53
|
+
r.u16(); r.u32();
|
|
54
|
+
const compressedSize = r.u32();
|
|
55
|
+
r.at = 48;
|
|
56
|
+
const tables = [];
|
|
57
|
+
for (let i = 0; i < count; i++) {
|
|
58
|
+
const flags = r.u8();
|
|
59
|
+
const tag = (flags & 0x3f) === 63 ? String.fromCharCode(...r.bytes(4)) : KNOWN[flags & 0x3f];
|
|
60
|
+
const version = flags >> 6;
|
|
61
|
+
const origLength = r.base128();
|
|
62
|
+
const transformed = tag === 'glyf' || tag === 'loca' ? version === 0 : version !== 0;
|
|
63
|
+
const length = transformed ? r.base128() : origLength;
|
|
64
|
+
tables.push({ tag, origLength, length, transformed, data: /** @type {Uint8Array} */ (new Uint8Array(0)) });
|
|
65
|
+
}
|
|
66
|
+
return { flavor, tables, payload: buf.subarray(r.at, r.at + compressedSize) };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* WOFF2 bytes as sfnt.
|
|
71
|
+
*
|
|
72
|
+
* @param {Uint8Array} buf - WOFF2 bytes.
|
|
73
|
+
* @param {Uint8Array | undefined} stream - The decompressed Brotli stream (see {@link woff2Directory}'s `payload`); undefined where nothing could decompress it.
|
|
74
|
+
* @returns {Uint8Array}
|
|
75
|
+
* @throws {TypeError} On a collection, damaged data, or no Brotli decoder.
|
|
76
|
+
*/
|
|
77
|
+
export function woff2ToSfnt(buf, stream) {
|
|
78
|
+
const { flavor, tables } = woff2Directory(buf);
|
|
79
|
+
if (!stream) throw new TypeError('WOFF2 needs Brotli, which this runtime lacks: convert the font to .ttf (scripts/woff2-to-ttf.mjs) or use render: "live"');
|
|
80
|
+
let at = 0;
|
|
81
|
+
for (const t of tables) { t.data = stream.subarray(at, at + t.length); at += t.length; }
|
|
82
|
+
const byTag = new Map(tables.map((t) => [t.tag, t]));
|
|
83
|
+
const glyf = byTag.get('glyf');
|
|
84
|
+
if (glyf?.transformed) {
|
|
85
|
+
const { glyf: g, loca, long } = rebuildGlyf(glyf.data);
|
|
86
|
+
glyf.data = g;
|
|
87
|
+
const l = byTag.get('loca');
|
|
88
|
+
if (l) l.data = loca;
|
|
89
|
+
// The rebuilt glyf outgrew short offsets: the head says long ones now.
|
|
90
|
+
const head = byTag.get('head');
|
|
91
|
+
if (long && head) {
|
|
92
|
+
head.data = new Uint8Array(head.data);
|
|
93
|
+
new DataView(head.data.buffer).setInt16(50, 1);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const hmtx = byTag.get('hmtx');
|
|
97
|
+
if (hmtx?.transformed) hmtx.data = rebuildHmtx(hmtx.data, byTag);
|
|
98
|
+
return assemble(flavor, tables);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The transformed `glyf` table back to `glyf` and `loca` (WOFF2 §5.1–5.3).
|
|
103
|
+
* Glyphs are written plainly (a word a delta), so the table can outgrow
|
|
104
|
+
* the font's short `loca` (offsets to 128 KB): then `loca` is long, and
|
|
105
|
+
* `long` says so (the `head` must agree).
|
|
106
|
+
*
|
|
107
|
+
* @param {Uint8Array} data
|
|
108
|
+
* @returns {{ glyf: Uint8Array, loca: Uint8Array, long: boolean }}
|
|
109
|
+
*/
|
|
110
|
+
function rebuildGlyf(data) {
|
|
111
|
+
const h = new Reader(data);
|
|
112
|
+
h.u16();
|
|
113
|
+
const options = h.u16();
|
|
114
|
+
const numGlyphs = h.u16();
|
|
115
|
+
const indexFormat = h.u16();
|
|
116
|
+
const sizes = Array.from({ length: 7 }, () => h.u32());
|
|
117
|
+
let at = h.at;
|
|
118
|
+
const streams = sizes.map((n) => { const s = new Reader(data.subarray(at, at + n)); at += n; return s; });
|
|
119
|
+
const [nContours, nPoints, flagStream, glyphStream, composite, bboxStream, instructions] = streams;
|
|
120
|
+
const bitmapLength = ((numGlyphs + 31) >> 5) << 2;
|
|
121
|
+
const bboxBitmap = bboxStream.bytes(bitmapLength);
|
|
122
|
+
void options;
|
|
123
|
+
|
|
124
|
+
/** @type {Uint8Array[]} */
|
|
125
|
+
const out = [];
|
|
126
|
+
const offsets = [0];
|
|
127
|
+
let total = 0;
|
|
128
|
+
for (let i = 0; i < numGlyphs; i++) {
|
|
129
|
+
const n = nContours.i16();
|
|
130
|
+
const explicit = (bboxBitmap[i >> 3] >> (7 - (i & 7))) & 1;
|
|
131
|
+
let glyph = new Uint8Array(0);
|
|
132
|
+
if (n === -1) {
|
|
133
|
+
// A composite: its component records, as they are; an explicit bbox.
|
|
134
|
+
const start = composite.at;
|
|
135
|
+
let more = true;
|
|
136
|
+
let hasInstructions = false;
|
|
137
|
+
while (more) {
|
|
138
|
+
const flags = composite.u16();
|
|
139
|
+
composite.u16();
|
|
140
|
+
composite.at += (flags & 0x0001 ? 4 : 2) + (flags & 0x0008 ? 2 : flags & 0x0040 ? 4 : flags & 0x0080 ? 8 : 0);
|
|
141
|
+
more = Boolean(flags & 0x0020);
|
|
142
|
+
if (flags & 0x0100) hasInstructions = true;
|
|
143
|
+
}
|
|
144
|
+
const records = composite.b.subarray(start, composite.at);
|
|
145
|
+
const bbox = [bboxStream.i16(), bboxStream.i16(), bboxStream.i16(), bboxStream.i16()];
|
|
146
|
+
const ilen = hasInstructions ? glyphStream.u255() : 0;
|
|
147
|
+
const ins = instructions.bytes(ilen);
|
|
148
|
+
glyph = new Uint8Array(10 + records.length + (hasInstructions ? 2 + ilen : 0));
|
|
149
|
+
const v = new DataView(glyph.buffer);
|
|
150
|
+
v.setInt16(0, -1);
|
|
151
|
+
bbox.forEach((x, k) => v.setInt16(2 + k * 2, x));
|
|
152
|
+
glyph.set(records, 10);
|
|
153
|
+
if (hasInstructions) { v.setUint16(10 + records.length, ilen); glyph.set(ins, 12 + records.length); }
|
|
154
|
+
} else if (n > 0) {
|
|
155
|
+
const ends = [];
|
|
156
|
+
let points = 0;
|
|
157
|
+
for (let c = 0; c < n; c++) { points += nPoints.u255(); ends.push(points - 1); }
|
|
158
|
+
const flags = flagStream.bytes(points);
|
|
159
|
+
const xs = new Int32Array(points);
|
|
160
|
+
const ys = new Int32Array(points);
|
|
161
|
+
const on = new Uint8Array(points);
|
|
162
|
+
let x = 0;
|
|
163
|
+
let y = 0;
|
|
164
|
+
for (let p = 0; p < points; p++) {
|
|
165
|
+
const [dx, dy] = triplet(flags[p] & 0x7f, glyphStream);
|
|
166
|
+
on[p] = flags[p] >> 7 ? 0 : 1;
|
|
167
|
+
x += dx; y += dy;
|
|
168
|
+
xs[p] = x; ys[p] = y;
|
|
169
|
+
}
|
|
170
|
+
const ilen = glyphStream.u255();
|
|
171
|
+
const ins = instructions.bytes(ilen);
|
|
172
|
+
const bbox = explicit ? [bboxStream.i16(), bboxStream.i16(), bboxStream.i16(), bboxStream.i16()] : [Math.min(...xs), Math.min(...ys), Math.max(...xs), Math.max(...ys)];
|
|
173
|
+
// Written plainly: a flag a point, every delta a word.
|
|
174
|
+
glyph = new Uint8Array(10 + 2 * n + 2 + ilen + points + 4 * points);
|
|
175
|
+
const v = new DataView(glyph.buffer);
|
|
176
|
+
v.setInt16(0, n);
|
|
177
|
+
bbox.forEach((b, k) => v.setInt16(2 + k * 2, b));
|
|
178
|
+
let o = 10;
|
|
179
|
+
for (const e of ends) { v.setUint16(o, e); o += 2; }
|
|
180
|
+
v.setUint16(o, ilen); o += 2;
|
|
181
|
+
glyph.set(ins, o); o += ilen;
|
|
182
|
+
for (let p = 0; p < points; p++) glyph[o++] = on[p];
|
|
183
|
+
for (let p = 0; p < points; p++) { v.setInt16(o, xs[p] - (p ? xs[p - 1] : 0)); o += 2; }
|
|
184
|
+
for (let p = 0; p < points; p++) { v.setInt16(o, ys[p] - (p ? ys[p - 1] : 0)); o += 2; }
|
|
185
|
+
}
|
|
186
|
+
const padded = (glyph.length + 3) & ~3;
|
|
187
|
+
const g = new Uint8Array(padded);
|
|
188
|
+
g.set(glyph);
|
|
189
|
+
out.push(g);
|
|
190
|
+
total += padded;
|
|
191
|
+
offsets.push(total);
|
|
192
|
+
}
|
|
193
|
+
const glyf = new Uint8Array(total);
|
|
194
|
+
let o = 0;
|
|
195
|
+
for (const g of out) { glyf.set(g, o); o += g.length; }
|
|
196
|
+
const long = Boolean(indexFormat) || total > 0x1fffe;
|
|
197
|
+
const loca = new Uint8Array((numGlyphs + 1) * (long ? 4 : 2));
|
|
198
|
+
const lv = new DataView(loca.buffer);
|
|
199
|
+
offsets.forEach((off, k) => (long ? lv.setUint32(k * 4, off) : lv.setUint16(k * 2, off / 2)));
|
|
200
|
+
return { glyf, loca, long: long && !indexFormat };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* One point's delta from the glyph stream (WOFF2 §5.2 triplet encoding).
|
|
205
|
+
*
|
|
206
|
+
* @param {number} flag - 0–127.
|
|
207
|
+
* @param {Reader} s
|
|
208
|
+
* @returns {[number, number]}
|
|
209
|
+
*/
|
|
210
|
+
function triplet(flag, s) {
|
|
211
|
+
const sign = (/** @type {number} */ f, /** @type {number} */ v) => (f & 1 ? v : -v);
|
|
212
|
+
if (flag < 10) return [0, sign(flag, ((flag & 14) << 7) + s.u8())];
|
|
213
|
+
if (flag < 20) return [sign(flag, (((flag - 10) & 14) << 7) + s.u8()), 0];
|
|
214
|
+
if (flag < 84) {
|
|
215
|
+
const b0 = flag - 20;
|
|
216
|
+
const b1 = s.u8();
|
|
217
|
+
return [sign(flag, 1 + (b0 & 0x30) + (b1 >> 4)), sign(flag >> 1, 1 + ((b0 & 0x0c) << 2) + (b1 & 0x0f))];
|
|
218
|
+
}
|
|
219
|
+
if (flag < 120) {
|
|
220
|
+
const b0 = flag - 84;
|
|
221
|
+
const a = s.u8();
|
|
222
|
+
const b = s.u8();
|
|
223
|
+
return [sign(flag, 1 + (Math.floor(b0 / 12) << 8) + a), sign(flag >> 1, 1 + (((b0 % 12) >> 2) << 8) + b)];
|
|
224
|
+
}
|
|
225
|
+
if (flag < 124) {
|
|
226
|
+
const a = s.u8();
|
|
227
|
+
const b2 = s.u8();
|
|
228
|
+
const c = s.u8();
|
|
229
|
+
return [sign(flag, (a << 4) + (b2 >> 4)), sign(flag >> 1, ((b2 & 0x0f) << 8) + c)];
|
|
230
|
+
}
|
|
231
|
+
const a = s.u16();
|
|
232
|
+
const b = s.u16();
|
|
233
|
+
return [sign(flag, a), sign(flag >> 1, b)];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The transformed `hmtx` table back (WOFF2 §5.4): left side bearings from
|
|
238
|
+
* the glyphs' xMin where they were left out.
|
|
239
|
+
*
|
|
240
|
+
* @param {Uint8Array} data
|
|
241
|
+
* @param {Map<string, { data: Uint8Array }>} byTag
|
|
242
|
+
* @returns {Uint8Array}
|
|
243
|
+
*/
|
|
244
|
+
function rebuildHmtx(data, byTag) {
|
|
245
|
+
const r = new Reader(data);
|
|
246
|
+
const flags = r.u8();
|
|
247
|
+
const hhea = /** @type {any} */ (byTag.get('hhea')).data;
|
|
248
|
+
const maxp = /** @type {any} */ (byTag.get('maxp')).data;
|
|
249
|
+
const head = /** @type {any} */ (byTag.get('head')).data;
|
|
250
|
+
const metrics = new DataView(hhea.buffer, hhea.byteOffset).getUint16(34);
|
|
251
|
+
const numGlyphs = new DataView(maxp.buffer, maxp.byteOffset).getUint16(4);
|
|
252
|
+
const longLoca = new DataView(head.buffer, head.byteOffset).getInt16(50) === 1;
|
|
253
|
+
const glyf = /** @type {any} */ (byTag.get('glyf')).data;
|
|
254
|
+
const loca = /** @type {any} */ (byTag.get('loca')).data;
|
|
255
|
+
const lv = new DataView(loca.buffer, loca.byteOffset, loca.byteLength);
|
|
256
|
+
const gv = new DataView(glyf.buffer, glyf.byteOffset, glyf.byteLength);
|
|
257
|
+
const xMin = (/** @type {number} */ g) => {
|
|
258
|
+
const a = longLoca ? lv.getUint32(g * 4) : lv.getUint16(g * 2) * 2;
|
|
259
|
+
const b = longLoca ? lv.getUint32(g * 4 + 4) : lv.getUint16(g * 2 + 2) * 2;
|
|
260
|
+
return b > a ? gv.getInt16(a + 2) : 0;
|
|
261
|
+
};
|
|
262
|
+
const adv = Array.from({ length: metrics }, () => r.u16());
|
|
263
|
+
const lsb = Array.from({ length: numGlyphs }, (_, g) => (g < metrics ? (flags & 1 ? xMin(g) : r.i16()) : 0));
|
|
264
|
+
for (let g = metrics; g < numGlyphs; g++) lsb[g] = flags & 2 ? xMin(g) : r.i16();
|
|
265
|
+
const out = new Uint8Array(metrics * 4 + (numGlyphs - metrics) * 2);
|
|
266
|
+
const v = new DataView(out.buffer);
|
|
267
|
+
for (let g = 0; g < metrics; g++) { v.setUint16(g * 4, adv[g]); v.setInt16(g * 4 + 2, lsb[g]); }
|
|
268
|
+
for (let g = metrics; g < numGlyphs; g++) v.setInt16(metrics * 4 + (g - metrics) * 2, lsb[g]);
|
|
269
|
+
return out;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Tables into an sfnt file (sorted directory, checksums, 4-byte alignment).
|
|
274
|
+
*
|
|
275
|
+
* @param {number} flavor
|
|
276
|
+
* @param {{ tag: string, data: Uint8Array }[]} tables
|
|
277
|
+
* @returns {Uint8Array}
|
|
278
|
+
*/
|
|
279
|
+
function assemble(flavor, tables) {
|
|
280
|
+
const sorted = [...tables].sort((a, b) => (a.tag < b.tag ? -1 : a.tag > b.tag ? 1 : 0));
|
|
281
|
+
const n = sorted.length;
|
|
282
|
+
let pow = 1;
|
|
283
|
+
let log = 0;
|
|
284
|
+
while (pow * 2 <= n) { pow *= 2; log++; }
|
|
285
|
+
const header = 12 + 16 * n;
|
|
286
|
+
const total = sorted.reduce((s, t) => s + ((t.data.length + 3) & ~3), header);
|
|
287
|
+
const out = new Uint8Array(total);
|
|
288
|
+
const v = new DataView(out.buffer);
|
|
289
|
+
v.setUint32(0, flavor); v.setUint16(4, n); v.setUint16(6, pow * 16); v.setUint16(8, log); v.setUint16(10, n * 16 - pow * 16);
|
|
290
|
+
let at = header;
|
|
291
|
+
sorted.forEach((t, i) => {
|
|
292
|
+
out.set(t.data, at);
|
|
293
|
+
let sum = 0;
|
|
294
|
+
for (let k = 0; k < t.data.length; k += 4) sum = (sum + (((out[at + k] << 24) | (out[at + k + 1] << 16) | (out[at + k + 2] << 8) | out[at + k + 3]) >>> 0)) >>> 0;
|
|
295
|
+
const rec = 12 + i * 16;
|
|
296
|
+
for (let k = 0; k < 4; k++) out[rec + k] = t.tag.charCodeAt(k);
|
|
297
|
+
v.setUint32(rec + 4, sum); v.setUint32(rec + 8, at); v.setUint32(rec + 12, t.data.length);
|
|
298
|
+
at += (t.data.length + 3) & ~3;
|
|
299
|
+
});
|
|
300
|
+
return out;
|
|
301
|
+
}
|
package/src/version.js
ADDED