|
5 | 5 | */ |
6 | 6 | export type PathCommand = [string, ...number[]]; |
7 | 7 |
|
| 8 | +/** a TrueType glyph's points (font units) and instructions, for hinting */ |
| 9 | +export interface GlyfPoints { |
| 10 | + xs: number[]; |
| 11 | + ys: number[]; |
| 12 | + /** on curve flags */ |
| 13 | + on: boolean[]; |
| 14 | + /** index of the last point of every contour */ |
| 15 | + ends: number[]; |
| 16 | + instructions: Uint8Array; |
| 17 | + /** glyph bounding box minimum x (the left side bearing's reference) */ |
| 18 | + xMin: number; |
| 19 | + /** components of composite glyphs (their points are not included in xs/ys) */ |
| 20 | + components: Array<{ glyph: number, dx: number, dy: number, matrix: [number, number, number, number], round: boolean, match: [number, number] | null, useMetrics: boolean }> | null; |
| 21 | +} |
| 22 | + |
8 | 23 | interface CFF { |
9 | 24 | charStrings: Uint8Array[]; |
10 | 25 | globalSubrs: Uint8Array[]; |
@@ -106,6 +121,98 @@ export class Font { |
106 | 121 | return this.u16(hmtx + index * 4); |
107 | 122 | } |
108 | 123 |
|
| 124 | + /** left side bearing in font units */ |
| 125 | + leftSideBearing(glyph: number): number { |
| 126 | + const hmtx = this.tables.hmtx.offset; |
| 127 | + if (glyph < this.numberOfHMetrics) return this.i16(hmtx + glyph * 4 + 2); |
| 128 | + return this.i16(hmtx + this.numberOfHMetrics * 4 + (glyph - this.numberOfHMetrics) * 2); |
| 129 | + } |
| 130 | + |
| 131 | + /** |
| 132 | + * A TrueType glyph's points and instructions (null for empty glyphs or CFF fonts) |
| 133 | + */ |
| 134 | + glyfPoints(glyph: number): GlyfPoints | null { |
| 135 | + if (this.outlines !== 'glyf' || glyph >= this.numGlyphs) return null; |
| 136 | + const loca = this.tables.loca.offset; |
| 137 | + const start = this.indexToLocFormat ? this.u32(loca + glyph * 4) : this.u16(loca + glyph * 2) * 2; |
| 138 | + const end = this.indexToLocFormat ? this.u32(loca + glyph * 4 + 4) : this.u16(loca + glyph * 2 + 2) * 2; |
| 139 | + if (end <= start) return null; |
| 140 | + |
| 141 | + const offset = this.tables.glyf.offset + start; |
| 142 | + const contourCount = this.i16(offset); |
| 143 | + const xMin = this.i16(offset + 2); |
| 144 | + |
| 145 | + if (contourCount < 0) { |
| 146 | + const components: NonNullable<GlyfPoints['components']> = []; |
| 147 | + let p = offset + 10, flags = 0; |
| 148 | + do { |
| 149 | + flags = this.u16(p); |
| 150 | + const component = this.u16(p + 2); |
| 151 | + p += 4; |
| 152 | + let arg1, arg2; |
| 153 | + if (flags & 1) { |
| 154 | + arg1 = flags & 2 ? this.i16(p) : this.u16(p); |
| 155 | + arg2 = flags & 2 ? this.i16(p + 2) : this.u16(p + 2); |
| 156 | + p += 4; |
| 157 | + } else { |
| 158 | + arg1 = flags & 2 ? this.i8(p) : this.u8(p); |
| 159 | + arg2 = flags & 2 ? this.i8(p + 1) : this.u8(p + 1); |
| 160 | + p += 2; |
| 161 | + } |
| 162 | + let matrix: [number, number, number, number] = [1, 0, 0, 1]; |
| 163 | + const f2dot14 = (o: number): number => this.i16(o) / 16384; |
| 164 | + if (flags & 8) { const s = f2dot14(p); matrix = [s, 0, 0, s]; p += 2; } |
| 165 | + else if (flags & 0x40) { matrix = [f2dot14(p), 0, 0, f2dot14(p + 2)]; p += 4; } |
| 166 | + else if (flags & 0x80) { matrix = [f2dot14(p), f2dot14(p + 2), f2dot14(p + 4), f2dot14(p + 6)]; p += 8; } |
| 167 | + components.push({ |
| 168 | + glyph: component, |
| 169 | + dx: flags & 2 ? arg1 : 0, |
| 170 | + dy: flags & 2 ? arg2 : 0, |
| 171 | + matrix, |
| 172 | + round: (flags & 4) !== 0, |
| 173 | + match: flags & 2 ? null : [arg1, arg2], |
| 174 | + useMetrics: (flags & 0x200) !== 0 |
| 175 | + }); |
| 176 | + } while (flags & 0x20); |
| 177 | + const instructions = flags & 0x100 ? this.data.subarray(p + 2, p + 2 + this.u16(p)) : new Uint8Array(0); |
| 178 | + return { xs: [], ys: [], on: [], ends: [], instructions, xMin, components }; |
| 179 | + } |
| 180 | + |
| 181 | + const ends: number[] = []; |
| 182 | + for (let i = 0; i < contourCount; i++) ends.push(this.u16(offset + 10 + i * 2)); |
| 183 | + const pointCount = contourCount ? ends[contourCount - 1] + 1 : 0; |
| 184 | + |
| 185 | + let p = offset + 10 + contourCount * 2; |
| 186 | + const instructionLength = this.u16(p); |
| 187 | + const instructions = this.data.subarray(p + 2, p + 2 + instructionLength); |
| 188 | + p += 2 + instructionLength; |
| 189 | + |
| 190 | + const flags = new Uint8Array(pointCount); |
| 191 | + for (let i = 0; i < pointCount;) { |
| 192 | + const flag = this.u8(p++); |
| 193 | + flags[i++] = flag; |
| 194 | + if (flag & 8) for (let repeat = this.u8(p++); repeat > 0 && i < pointCount; repeat--) flags[i++] = flag; |
| 195 | + } |
| 196 | + |
| 197 | + const xs: number[] = [], ys: number[] = []; |
| 198 | + for (const [coords, short, same] of [[xs, 2, 16], [ys, 4, 32]] as Array<[number[], number, number]>) { |
| 199 | + let value = 0; |
| 200 | + for (let i = 0; i < pointCount; i++) { |
| 201 | + const flag = flags[i]; |
| 202 | + if (flag & short) { |
| 203 | + const delta = this.u8(p++); |
| 204 | + value += flag & same ? delta : -delta; |
| 205 | + } else if (!(flag & same)) { |
| 206 | + value += this.i16(p); |
| 207 | + p += 2; |
| 208 | + } |
| 209 | + coords.push(value); |
| 210 | + } |
| 211 | + } |
| 212 | + |
| 213 | + return { xs, ys, on: Array.from(flags, f => (f & 1) === 1), ends, instructions, xMin, components: null }; |
| 214 | + } |
| 215 | + |
109 | 216 | /** |
110 | 217 | * @param {number} glyph |
111 | 218 | * @return {Array} path commands in font units |
@@ -239,10 +346,10 @@ export class Font { |
239 | 346 | } |
240 | 347 |
|
241 | 348 | // converts a TrueType contour (quadratic, with implied on-curve points) to path commands |
242 | | -function contourToPath(path: PathCommand[], xs: Int32Array, ys: Int32Array, flags: Uint8Array, first: number, last: number): void { |
| 349 | +export function contourToPath(path: PathCommand[], xs: ArrayLike<number>, ys: ArrayLike<number>, flags: ArrayLike<number | boolean>, first: number, last: number): void { |
243 | 350 | if (last < first) return; |
244 | 351 | const count = last - first + 1; |
245 | | - const on = (i: number): number => flags[first + (i % count)] & 1; |
| 352 | + const on = (i: number): boolean => flags[first + (i % count)] === true || ((flags[first + (i % count)] as number) & 1) === 1; |
246 | 353 | const x = (i: number): number => xs[first + (i % count)]; |
247 | 354 | const y = (i: number): number => ys[first + (i % count)]; |
248 | 355 |
|
|
0 commit comments