Skip to content

Commit

Permalink
feat: minify support compound glyphs
Browse files Browse the repository at this point in the history
  • Loading branch information
qq15725 committed Mar 12, 2024
1 parent 926a6e1 commit 41dc152
Show file tree
Hide file tree
Showing 5 changed files with 50 additions and 43 deletions.
49 changes: 22 additions & 27 deletions src/minify/minify-glyphs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { componentFlags } from '../sfnt'
import type { HMetric, Sfnt, VMetric } from '../sfnt'

export interface MinimizedGlyph extends HMetric, VMetric {
rawGlyphIndex: number
glyphIndex: number
unicodes: Array<number>
view: DataView
Expand All @@ -11,7 +12,7 @@ export function minifyGlyphs(sfnt: Sfnt, subset: string) {
const { cmap, loca, hmtx, vmtx, glyf } = sfnt
const unicodeGlyphIndexMap = cmap.getUnicodeGlyphIndexMap()
const glyphIndexUnicodesMap = new Map<number, Set<number>>()
unicodeGlyphIndexMap.forEach((unicode, glyphIndex) => {
unicodeGlyphIndexMap.forEach((glyphIndex, unicode) => {
let unicodes = glyphIndexUnicodesMap.get(glyphIndex)
if (!unicodes) glyphIndexUnicodesMap.set(glyphIndex, unicodes = new Set())
unicodes.add(unicode)
Expand All @@ -23,64 +24,58 @@ export function minifyGlyphs(sfnt: Sfnt, subset: string) {
new Set(
subset.split('')
.map(str => str.codePointAt(0))
.filter(unicode => unicode !== undefined && unicodeGlyphIndexMap.has(unicode))
.sort(),
.filter(unicode => unicode !== undefined && unicodeGlyphIndexMap.has(unicode)) as Array<number>,
),
) as Array<number>
).sort((a, b) => a - b)

const glyphIndexToGlyph = (glyphIndex: number) => {
const glyphs: Array<MinimizedGlyph> = []

const addGlyph = (glyphIndex: number) => {
const hMetric = hMetrics[glyphIndex]
const vMetric = vMetrics?.[glyphIndex] ?? { advanceHeight: 0, topSideBearing: 0 }
const start = locations[glyphIndex]
const end = locations[glyphIndex + 1] ?? start
return {
const glyph = {
...hMetric,
...vMetric,
glyphIndex,
unicodes: Array.from(glyphIndexUnicodesMap.get(glyphIndex)!) ?? [],
rawGlyphIndex: glyphIndex,
glyphIndex: glyphs.length,
unicodes: Array.from(glyphIndexUnicodesMap.get(glyphIndex) ?? []),
view: new DataView(
glyf.buffer,
glyf.byteOffset + start,
end - start,
),
}
glyphs.push(glyph)
return glyph
}

const glyphs: Array<MinimizedGlyph> = unicodes.map(unicode => glyphIndexToGlyph(unicodeGlyphIndexMap.get(unicode)!))
addGlyph(0)

unicodes.forEach(unicode => addGlyph(unicodeGlyphIndexMap.get(unicode)!))

const compoundGlyphs: Array<MinimizedGlyph> = []
glyphs.forEach(glyph => {
glyphs.slice().forEach(glyph => {
const { view } = glyph
if (!view.byteLength) return
const numberOfContours = view.getInt16(0)
if (numberOfContours >= 0) return
let offset = 10
let flags
do {
flags = view.getUint16(offset)
const glyphIndex = view.getUint16(offset + 2)
const glyphIndexOffset = offset + 2
const glyphIndex = view.getUint16(glyphIndexOffset)
offset += 4
if (componentFlags.ARG_1_AND_2_ARE_WORDS & flags) offset += 4
else offset += 2
if (componentFlags.WE_HAVE_A_SCALE & flags) offset += 2
else if (componentFlags.WE_HAVE_AN_X_AND_Y_SCALE & flags) offset += 4
else if (componentFlags.WE_HAVE_A_TWO_BY_TWO & flags) offset += 8
compoundGlyphs.push(glyphIndexToGlyph(glyphIndex))
const glyph = addGlyph(glyphIndex)
view.setUint16(glyphIndexOffset, glyph.glyphIndex)
} while (componentFlags.MORE_COMPONENTS & flags)
})

compoundGlyphs.forEach(glyph => glyphs.push(glyph))

glyphs.unshift({
...hMetrics[0],
...(vMetrics?.[0] ?? { advanceHeight: 0, topSideBearing: 0 }),
glyphIndex: 0,
unicodes: [0],
view: new DataView(
glyf.buffer,
glyf.byteOffset + locations[0],
locations[1] - locations[0],
),
})

return glyphs
}
4 changes: 2 additions & 2 deletions src/minify/minify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ export function minify<T extends (Ttf | Woff | ArrayBuffer)>(source: T, subset:
let sfnt: Sfnt
let outputFormat: 'ttf' | 'woff' | 'ttf-buffer' | 'woff-buffer'
if (source instanceof Ttf) {
sfnt = source.sfnt
sfnt = source.sfnt.clone()
outputFormat = 'ttf'
} else if (source instanceof Woff) {
sfnt = source.sfnt
sfnt = source.sfnt.clone()
outputFormat = 'woff'
} else {
const view = toDataView(source)
Expand Down
12 changes: 4 additions & 8 deletions src/sfnt/cmap-subtables/cmap-subtable-format0.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,14 @@ export class CmapSubtableFormat0 extends Entity {
}

static from(unicodeGlyphIndexMap: Map<number, number>): CmapSubtableFormat0 {
const unicodes: Array<number> = []
unicodeGlyphIndexMap.forEach((glyphIndex, unicode) => {
if (unicode < 256 && glyphIndex < 256) {
unicodes.push(unicode)
}
})
const table = new CmapSubtableFormat0()
table.format = 0
table.length = table.byteLength
table.language = 0
unicodes.sort((a, b) => a - b).forEach(unicode => {
table.writeUint8(unicodeGlyphIndexMap.get(unicode)!, Number(6 + unicode))
unicodeGlyphIndexMap.forEach((glyphIndex, unicode) => {
if (unicode < 256 && glyphIndex < 256) {
table.writeUint8(glyphIndex, 6 + unicode)
}
})
return table
}
Expand Down
6 changes: 3 additions & 3 deletions src/sfnt/cmap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,18 @@ export class Cmap extends SfntTable {
@Entity.column({ type: 'uint16' }) declare numberSubtables: number

static from(unicodeGlyphIndexMap: Map<number, number>): Cmap {
const has2Byte = Array.from(unicodeGlyphIndexMap.keys()).some(unicode => Number(unicode) > 0xFFFF)
const has2Byte = Array.from(unicodeGlyphIndexMap.keys()).some(unicode => unicode > 0xFFFF)
const table4 = CmapSubtableFormat4.from(unicodeGlyphIndexMap)
const table0 = CmapSubtableFormat0.from(unicodeGlyphIndexMap)
const table12 = has2Byte ? CmapSubtableFormat12.from(unicodeGlyphIndexMap) : undefined
const offset4 = 4 + (has2Byte ? 32 : 24)
const offset4 = 4 + (table12 ? 32 : 24)
const offset0 = offset4 + table4.byteLength
const offset12 = offset0 + table0.byteLength
const subtables = [
{ platformID: 0, platformSpecificID: 3, offset: offset4 }, // subtable 4, unicode
{ platformID: 1, platformSpecificID: 0, offset: offset0 }, // subtable 0, mac standard
{ platformID: 3, platformSpecificID: 1, offset: offset4 }, // subtable 4, windows standard
has2Byte && { platformID: 3, platformSpecificID: 10, offset: offset12 }, // hasGLyphsOver2Bytes
table12 && { platformID: 3, platformSpecificID: 10, offset: offset12 }, // hasGLyphsOver2Bytes
].filter(Boolean) as Array<CmapSubtable>
const cmap = new Cmap(
new ArrayBuffer(
Expand Down
22 changes: 19 additions & 3 deletions src/sfnt/sfnt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,20 @@ export class Sfnt {
//
}

clone() {
return new Sfnt(this.tables.map(({ tag, view }) => {
return {
tag,
view: new DataView(
view.buffer.slice(
view.byteOffset,
view.byteOffset + view.byteLength,
),
),
}
}))
}

delete(tag: SfntTableTag): this {
this.tableViews.delete(tag)
const index = this.tables.findIndex(table => table.tag === tag)
Expand All @@ -50,9 +64,11 @@ export class Sfnt {
if (!view) {
const Table = Sfnt.registeredTableViews.get(tag) as any
if (Table) {
const rawView = this.tables.find(table => table.tag === tag)!.view
view = new Table(rawView.buffer, rawView.byteOffset, rawView.byteLength).setSfnt(this) as any
this.set(tag, view!)
const rawView = this.tables.find(table => table.tag === tag)?.view
if (rawView) {
view = new Table(rawView.buffer, rawView.byteOffset, rawView.byteLength).setSfnt(this) as any
this.set(tag, view!)
}
}
}
return view
Expand Down

0 comments on commit 41dc152

Please sign in to comment.