chore(v2): vendor dependencies for offline/China builds

go mod vendor pins onnxruntime_go v1.12.1, Gio and the rest into v2/vendor
so go run/build work without hitting proxy.golang.org (blocked/slow in
China). Verified: CGO_ENABLED=1 go build -mod=vendor ./internal/spike and
GOOS=windows go build -mod=vendor ./internal/ui both pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ila
2026-07-23 16:35:01 +08:00
co-authored by Claude Opus 4.8
parent 97c1c4a974
commit f58728cddd
972 changed files with 597802 additions and 0 deletions
+55
View File
@@ -0,0 +1,55 @@
This project is provided under the terms of the UNLICENSE or
the BSD license denoted by the following SPDX identifier:
SPDX-License-Identifier: Unlicense OR BSD-3-Clause
You may use the project under the terms of either license.
Both licenses are reproduced below.
----
The BSD 3 Clause License
Copyright 2021 The go-text authors
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
---
---
The UNLICENSE
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org/>
---
+3
View File
@@ -0,0 +1,3 @@
# di
di is a library that converts bi-directional text into uni-directional text
+130
View File
@@ -0,0 +1,130 @@
package di
import (
"github.com/go-text/typesetting/harfbuzz"
)
// Direction indicates the layout direction of a piece of text.
type Direction uint8
const (
// DirectionLTR is for Left-to-Right text.
DirectionLTR Direction = iota
// DirectionRTL is for Right-to-Left text.
DirectionRTL
// DirectionTTB is for Top-to-Bottom text.
DirectionTTB
// DirectionBTT is for Bottom-to-Top text.
DirectionBTT
)
const (
progression Direction = 1 << iota
// axisVertical is the bit for the axis, 0 for horizontal, 1 for vertical
axisVertical
// If this flag is set, the orientation is chosen
// using the [verticalSideways] flag.
// Otherwise, the segmenter will resolve the orientation based
// on unicode properties
verticalOrientationSet
// verticalSideways is set for 'sideways', unset for 'upright'
// It implies BVerticalOrientationSet is set
verticalSideways
)
// IsVertical returns whether d is laid out on a vertical
// axis. If the return value is false, d is on the horizontal
// axis.
func (d Direction) IsVertical() bool { return d&axisVertical != 0 }
// Axis returns the layout axis for d.
func (d Direction) Axis() Axis {
if d.IsVertical() {
return Vertical
}
return Horizontal
}
// SwitchAxis switches from horizontal to vertical (and vice versa), preserving
// the progression.
func (d Direction) SwitchAxis() Direction { return d ^ axisVertical }
// Progression returns the text layout progression for d.
func (d Direction) Progression() Progression {
if d&progression == 0 {
return FromTopLeft
}
return TowardTopLeft
}
// SetProgression sets the progression, preserving the others bits.
func (d *Direction) SetProgression(p Progression) {
if p == FromTopLeft {
*d &= ^progression
} else {
*d |= progression
}
}
// Axis indicates the axis of layout for a piece of text.
type Axis bool
const (
Horizontal Axis = false
Vertical Axis = true
)
// Progression indicates how text is read within its Axis relative
// to the top left corner.
type Progression bool
const (
// FromTopLeft indicates text in which a reader starts reading
// at the top left corner of the text and moves away from it.
// DirectionLTR and DirectionTTB are examples of FromTopLeft
// Progression.
FromTopLeft Progression = false
// TowardTopLeft indicates text in which a reader starts reading
// at the opposite end of the text's Axis from the top left corner
// and moves towards it. DirectionRTL and DirectionBTT are examples
// of TowardTopLeft progression.
TowardTopLeft Progression = true
)
// HasVerticalOrientation returns true if the direction has set up
// an orientation for vertical text (typically using [SetSideways] or [SetUpright])
func (d Direction) HasVerticalOrientation() bool { return d&verticalOrientationSet != 0 }
// IsSideways returns true if the direction is vertical with a 'sideways'
// orientation.
//
// When shaping vertical text, 'sideways' means that the glyphs are rotated
// by 90°, clock-wise. This flag should be used by renderers to properly
// rotate the glyphs when drawing.
func (d Direction) IsSideways() bool { return d.IsVertical() && d&verticalSideways != 0 }
// SetSideways makes d vertical with 'sideways' or 'upright' orientation, preserving only the
// progression.
func (d *Direction) SetSideways(sideways bool) {
*d |= axisVertical | verticalOrientationSet
if sideways {
*d |= verticalSideways
} else {
*d &= ^verticalSideways
}
}
// Harfbuzz returns the equivalent direction used by harfbuzz.
func (d Direction) Harfbuzz() harfbuzz.Direction {
switch d & (progression | axisVertical) {
case DirectionRTL:
return harfbuzz.RightToLeft
case DirectionBTT:
return harfbuzz.BottomToTop
case DirectionTTB:
return harfbuzz.TopToBottom
default:
return harfbuzz.LeftToRight
}
}
+6
View File
@@ -0,0 +1,6 @@
# font
font is a library that handles loading and utilizing Opentype fonts.
`font/opentype` implements the low level parsing of a font file and its tables,
and `font` provides an higher level API usable by shapers and renderers.
+322
View File
@@ -0,0 +1,322 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package font
import (
"encoding/binary"
"github.com/go-text/typesetting/font/opentype/tables"
)
// Kernx represents a 'kern' or 'kerx' kerning table.
// It supports both Microsoft and Apple formats.
type Kernx []KernSubtable
func newKernxFromKerx(kerx tables.Kerx) Kernx {
if len(kerx.Tables) == 0 {
return nil
}
out := make(Kernx, len(kerx.Tables))
for i, ta := range kerx.Tables {
out[i] = newKerxSubtable(ta)
}
return out
}
func newKernxFromKern(kern tables.Kern) Kernx {
if len(kern.Tables) == 0 {
return nil
}
out := make(Kernx, len(kern.Tables))
for i, ta := range kern.Tables {
out[i] = newKernSubtable(ta)
}
return out
}
// KernSubtable represents a 'kern' or 'kerx' subtable.
type KernSubtable struct {
Data interface{ isKernSubtable() }
// high bit of the Coverage field, following 'kerx' conventions
coverage byte
// IsExtended is [true] for AAT `kerx` subtables, false for 'kern' subtables
IsExtended bool
// 0 for scalar values
TupleCount int
}
func newKernSubtable(table tables.KernSubtable) (out KernSubtable) {
out.IsExtended = false
switch table := table.(type) {
case tables.OTKernSubtableHeader:
// synthesize a coverage flag following kerx conventions
const (
Horizontal = 0x01
CrossStream = 0x04
)
if table.Coverage&Horizontal == 0 { // vertical
out.coverage |= kerxVertical
}
if table.Coverage&CrossStream != 0 {
out.coverage |= kerxCrossStream
}
case tables.AATKernSubtableHeader:
out.coverage = table.Coverage
out.TupleCount = int(table.TupleCount)
}
switch data := table.Data().(type) {
case tables.KernData0:
out.Data = newKern0(data)
case tables.KernData1:
out.Data = newKern1(data)
case tables.KernData2:
out.Data = newKern2(data)
case tables.KernData3:
out.Data = Kern3(data)
}
return out
}
func newKerxSubtable(table tables.KerxSubtable) (out KernSubtable) {
out.IsExtended = true
out.TupleCount = int(table.TupleCount)
out.coverage = byte(table.Coverage >> 8) // high bit only
switch data := table.Data.(type) {
case tables.KerxData0:
out.Data = newKern0x(data)
case tables.KerxData1:
out.Data = newKern1x(data)
case tables.KerxData2:
out.Data = Kern2(data)
case tables.KerxData4:
out.Data = newKern4(data)
case tables.KerxData6:
out.Data = Kern6(data)
}
return out
}
func (Kern0) isKernSubtable() {}
func (Kern1) isKernSubtable() {}
func (Kern2) isKernSubtable() {}
func (Kern3) isKernSubtable() {}
func (Kern4) isKernSubtable() {}
func (Kern6) isKernSubtable() {}
var (
_ SimpleKerns = Kern0(nil)
_ SimpleKerns = (*Kern2)(nil)
_ SimpleKerns = (*Kern3)(nil)
_ SimpleKerns = (*Kern6)(nil)
)
// SimpleKerns store a compact form of the kerning values,
// which is restricted to (one direction) kerning pairs.
// It is only implemented by [Kern0], [Kern2], [Kern3] and [Kern6],
// where [Kern1] and [Kern4] requires a state machine to be interpreted.
type SimpleKerns interface {
// KernPair return the kern value for the given pair, or zero.
// The value is expressed in glyph units and
// is negative when glyphs should be closer.
KernPair(left, right GID) int16
}
// kernx coverage flags
const (
kerxBackwards = 1 << (12 - 8)
kerxVariation = 1 << (13 - 8)
kerxCrossStream = 1 << (14 - 8)
kerxVertical = 1 << (15 - 8)
)
// IsHorizontal returns true if the subtable has horizontal kerning values.
func (k KernSubtable) IsHorizontal() bool { return k.coverage&kerxVertical == 0 }
// IsBackwards returns true if state-table based should process the glyphs backwards.
func (k KernSubtable) IsBackwards() bool { return k.coverage&kerxBackwards != 0 }
// IsCrossStream returns true if the subtable has cross-stream kerning values.
func (k KernSubtable) IsCrossStream() bool { return k.coverage&kerxCrossStream != 0 }
// IsVariation returns true if the subtable has variation kerning values.
func (k KernSubtable) IsVariation() bool { return k.coverage&kerxVariation != 0 }
type Kern0 []tables.Kernx0Record
func newKern0(k tables.KernData0) Kern0 { return k.Pairs }
func newKern0x(k tables.KerxData0) Kern0 { return k.Pairs }
func kernPair(records []tables.Kernx0Record, left, right GID) int16 {
key := uint32(left)<<16 | uint32(right)
low, high := 0, len(records)
for low < high {
mid := low + (high-low)/2 // avoid overflow when computing mid
p := recordKey(records[mid])
if key < p {
high = mid
} else if key > p {
low = mid + 1
} else {
return records[mid].Value
}
}
return 0
}
func recordKey(kp tables.Kernx0Record) uint32 { return uint32(kp.Left)<<16 | uint32(kp.Right) }
func (kd Kern0) KernPair(left, right GID) int16 { return kernPair(kd, left, right) }
type Kern1 struct {
Values []int16 // After successful parsing, may be safely indexed by AATStateEntry.AsKernxIndex() from `Machine`
Machine AATStateTable
}
// convert from non extended to extended
func newKern1(k tables.KernData1) Kern1 {
class := tables.AATLoopkup8{
AATLoopkup8Data: tables.AATLoopkup8Data{
FirstGlyph: k.ClassTable.StartGlyph,
Values: make([]uint16, len(k.ClassTable.Values)),
},
}
for i, b := range k.ClassTable.Values {
class.Values[i] = uint16(b)
}
states := make([][]uint16, len(k.States))
for i, row := range k.States {
v := make([]uint16, len(row))
for j, b := range row {
v[j] = uint16(b)
}
states[i] = v
}
return Kern1{
Values: k.Values,
Machine: AATStateTable{
nClass: uint32(k.StateSize),
Class: class,
states: states,
entries: k.Entries,
},
}
}
func newKern1x(k tables.KerxData1) Kern1 {
return Kern1{Values: k.Values, Machine: newAATStableTable(k.AATStateTableExt)}
}
type Kern2 tables.KerxData2
// convert from non extended to extended
func newKern2(k tables.KernData2) Kern2 {
return Kern2{
Left: tables.AATLoopkup8{AATLoopkup8Data: k.Left},
Right: tables.AATLoopkup8{AATLoopkup8Data: k.Right},
KerningStart: tables.Offset32(k.KerningStart),
KerningData: k.KerningData,
}
}
func (kd Kern2) KernPair(left, right GID) int16 {
l, _ := kd.Left.Class(tables.GlyphID(left))
r, _ := kd.Right.Class(tables.GlyphID(right))
index := int(l) + int(r)
if len(kd.KerningData) < index+2 || index < int(kd.KerningStart) {
return 0
}
kernVal := binary.BigEndian.Uint16(kd.KerningData[index:])
return int16(kernVal)
}
type Kern3 tables.KernData3
func (kd Kern3) KernPair(left, right GID) int16 {
if int(left) >= len(kd.LeftClass) || int(right) >= len(kd.RightClass) { // should not happend
return 0
}
lc, rc := int(kd.LeftClass[left]), int(kd.RightClass[right])
index := kd.KernIndex[lc*int(kd.RightClassCount)+rc] // sanitized during parsing
return kd.Kernings[index] // sanitized during parsing
}
type Kern4 struct {
Anchors tables.KerxAnchors
Machine AATStateTable
flags uint32
}
func newKern4(k tables.KerxData4) Kern4 {
return Kern4{
Machine: newAATStableTable(k.AATStateTableExt),
Anchors: k.Anchors,
flags: k.Flags,
}
}
// ActionType returns 0, 1 or 2 .
func (k Kern4) ActionType() uint8 {
const ActionType = 0xC0000000 // A two-bit field containing the action type.
return uint8(k.flags & ActionType >> 30)
}
type Kern6 tables.KerxData6
func (kd Kern6) KernPair(left, right GID) int16 {
l := kd.Row.ClassUint32(tables.GlyphID(left))
r := kd.Column.ClassUint32(tables.GlyphID(right))
index := int(l) + int(r)
if len(kd.Kernings) <= index {
return 0
}
return kd.Kernings[index]
}
// --------------------------------------- state machine ---------------------------------------
// AATStateTable supports both regular and extended AAT state machines
type AATStateTable struct {
nClass uint32
Class tables.AATLookup
states [][]uint16 // each sub array has length stateSize
entries []tables.AATStateEntry // length is the maximum state + 1
}
func newAATStableTable(k tables.AATStateTableExt) AATStateTable {
return AATStateTable{
nClass: k.StateSize,
Class: k.Class,
states: k.States,
entries: k.Entries,
}
}
// GetClass return the class for the given glyph, with the correct default value.
func (st *AATStateTable) GetClass(glyph GID) uint16 {
if glyph == 0xFFFF { // deleted glyph
return 2 // class deleted
}
c, ok := st.Class.Class(tables.GlyphID(glyph))
if !ok {
return 1 // class out of bounds
}
return c // class for a state table can't be uint32
}
// GetEntry return the entry for the given state and class,
// and handle invalid values (by returning an empty entry).
func (st *AATStateTable) GetEntry(state, class uint16) tables.AATStateEntry {
if uint32(class) >= st.nClass {
class = 1 // class out of bounds
}
if int(state) >= len(st.states) {
return tables.AATStateEntry{}
}
entry := st.states[state][class] // access check when parsing
return st.entries[entry] // access check when parsing
}
+108
View File
@@ -0,0 +1,108 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package font
import "github.com/go-text/typesetting/font/opentype/tables"
type Morx []MorxChain
func newMorx(table tables.Morx) Morx {
if len(table.Chains) == 0 {
return nil
}
out := make(Morx, len(table.Chains))
for i, c := range table.Chains {
out[i] = newMorxChain(c)
}
return out
}
type MorxChain struct {
Features []tables.AATFeature
Subtables []MorxSubtable
DefaultFlags uint32
}
func newMorxChain(table tables.MorxChain) (out MorxChain) {
out.DefaultFlags = table.Flags
out.Features = table.Features
out.Subtables = make([]MorxSubtable, len(table.Subtables))
for i, s := range table.Subtables {
out.Subtables[i] = newMorxSubtable(s)
}
return out
}
type MorxSubtable struct {
Data interface{ isMorxSubtable() }
Coverage uint8 // high byte of the coverage flag
Flags uint32 // Mask identifying which subtable this is.
}
func (MorxRearrangementSubtable) isMorxSubtable() {}
func (MorxContextualSubtable) isMorxSubtable() {}
func (MorxLigatureSubtable) isMorxSubtable() {}
func (MorxNonContextualSubtable) isMorxSubtable() {}
func (MorxInsertionSubtable) isMorxSubtable() {}
func newMorxSubtable(table tables.MorxChainSubtable) (out MorxSubtable) {
out.Coverage = table.Coverage
out.Flags = table.SubFeatureFlags
switch data := table.Data.(type) {
case tables.MorxSubtableRearrangement:
out.Data = MorxRearrangementSubtable(newAATStableTable(data.AATStateTableExt))
case tables.MorxSubtableContextual:
out.Data = MorxContextualSubtable{
Machine: newAATStableTable(data.AATStateTableExt),
Substitutions: data.Substitutions.Substitutions,
}
case tables.MorxSubtableLigature:
s := MorxLigatureSubtable{
Machine: newAATStableTable(data.AATStateTableExt),
LigatureAction: data.LigActions,
Components: data.Components,
Ligatures: make([]GID, len(data.Ligatures)),
}
for i, g := range data.Ligatures {
s.Ligatures[i] = GID(g)
}
out.Data = s
case tables.MorxSubtableNonContextual:
out.Data = MorxNonContextualSubtable{Class: data.Class}
case tables.MorxSubtableInsertion:
s := MorxInsertionSubtable{
Machine: newAATStableTable(data.AATStateTableExt),
Insertions: make([]GID, len(data.Insertions)),
}
for i, g := range data.Insertions {
s.Insertions[i] = GID(g)
}
out.Data = s
}
return out
}
type MorxRearrangementSubtable AATStateTable
type MorxContextualSubtable struct {
Substitutions []tables.AATLookup
Machine AATStateTable
}
type MorxLigatureSubtable struct {
LigatureAction []uint32
Components []uint16
Ligatures []GID
Machine AATStateTable
}
type MorxNonContextualSubtable struct {
Class tables.AATLookup // the lookup value is interpreted as a GlyphIndex
}
type MorxInsertionSubtable struct {
// After successul parsing, this array may be safely
// indexed by the indexes and counts from Machine entries.
Insertions []GID
Machine AATStateTable
}
+476
View File
@@ -0,0 +1,476 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package font
import (
"errors"
"fmt"
"math"
ot "github.com/go-text/typesetting/font/opentype"
"github.com/go-text/typesetting/font/opentype/tables"
)
// sbix
type sbix []tables.Strike
func newSbix(table tables.Sbix) sbix { return table.Strikes }
// chooseStrike selects the best match for the given resolution.
// It returns nil only if the table is empty
func (sb sbix) chooseStrike(xPpem, yPpem uint16) *tables.Strike {
if len(sb) == 0 {
return nil
}
request := maxu16(xPpem, yPpem)
if request == 0 {
request = math.MaxUint16 // choose largest strike
}
var (
bestIndex = 0
bestPpem = sb[0].Ppem
)
for i, s := range sb {
ppem := s.Ppem
if request <= ppem && ppem < bestPpem || request > bestPpem && ppem > bestPpem {
bestIndex = i
bestPpem = ppem
}
}
return &sb[bestIndex]
}
func (sb sbix) availableSizes(horizontal *tables.Hhea, avgWidth, upem uint16) []BitmapSize {
out := make([]BitmapSize, 0, len(sb))
for _, size := range sb {
v := strikeSizeMetrics(size, horizontal, avgWidth, upem)
// only use strikes with valid PPEM values
if v.XPpem == 0 || v.YPpem == 0 {
continue
}
out = append(out, v)
}
return out
}
func strikeSizeMetrics(b tables.Strike, hori *tables.Hhea, avgWidth, upem uint16) (out BitmapSize) {
out.XPpem, out.YPpem = b.Ppem, b.Ppem
out.Height = mulDiv(uint16(hori.Ascender-hori.Descender+hori.LineGap), b.Ppem, upem)
inferBitmapWidth(&out, avgWidth, upem)
return out
}
// ---------------------------- bitmap ----------------------------
func loadBitmap(ld *ot.Loader, tagLoc, tagData ot.Tag) (bitmap, error) {
raw, err := ld.RawTable(tagLoc)
if err != nil {
return nil, err
}
loc, _, err := tables.ParseCBLC(raw)
if err != nil {
return nil, err
}
imageTable, err := ld.RawTable(tagData)
if err != nil {
return nil, err
}
return newBitmap(loc, imageTable)
}
// CBLC/CBDT or EBLC/EBDT or BLOC/BDAT
type bitmap []bitmapStrike
func newBitmap(table tables.EBLC, imageTable []byte) (bitmap, error) {
out := make(bitmap, len(table.BitmapSizes))
for i, strike := range table.BitmapSizes {
subtables := table.IndexSubTables[i]
out[i] = bitmapStrike{
subTables: make([]bitmapSubtable, len(subtables)),
hori: strike.Hori,
vert: strike.Vert,
ppemX: uint16(strike.PpemX),
ppemY: uint16(strike.PpemY),
}
for j, subtable := range subtables {
var err error
out[i].subTables[j], err = newBitmapSubtable(subtable, imageTable)
if err != nil {
return nil, err
}
}
}
return out, nil
}
func (t bitmap) availableSizes(avgWidth, upem uint16) []BitmapSize {
out := make([]BitmapSize, 0, len(t))
for _, size := range t {
v := size.sizeMetrics(avgWidth, upem)
// only use strikes with valid PPEM values
if v.XPpem == 0 || v.YPpem == 0 {
continue
}
out = append(out, v)
}
return out
}
type bitmapStrike struct {
subTables []bitmapSubtable
hori, vert tables.SbitLineMetrics
ppemX, ppemY uint16
}
// chooseStrike selects the best match for the given resolution.
// It returns nil only if the table is empty
func (bt bitmap) chooseStrike(xPpem, yPpem uint16) *bitmapStrike {
if len(bt) == 0 {
return nil
}
request := maxu16(xPpem, yPpem)
if request == 0 {
request = math.MaxUint16 // choose largest strike
}
var (
bestIndex = 0
bestPpem = maxu16(bt[0].ppemX, bt[0].ppemY)
)
for i, s := range bt {
ppem := maxu16(s.ppemX, s.ppemY)
if request <= ppem && ppem < bestPpem || request > bestPpem && ppem > bestPpem {
bestIndex = i
bestPpem = ppem
}
}
return &bt[bestIndex]
}
func (b *bitmapStrike) sizeMetrics(avgWidth, upem uint16) (out BitmapSize) {
out.XPpem, out.YPpem = b.ppemX, b.ppemY
ascender := int16(b.hori.Ascender)
descender := int16(b.hori.Descender)
maxBeforeBl := b.hori.MaxBeforeBL
minAfterBl := b.hori.MinAfterBL
/* Due to fuzzy wording in the EBLC documentation, we find both */
/* positive and negative values for `descender'. Additionally, */
/* many fonts have both `ascender' and `descender' set to zero */
/* (which is definitely wrong). MS Windows simply ignores all */
/* those values... For these reasons we apply some heuristics */
/* to get a reasonable, non-zero value for the height. */
if descender > 0 {
if minAfterBl < 0 {
descender = -descender
}
} else if descender == 0 {
if ascender == 0 {
/* sanitize buggy ascender and descender values */
if maxBeforeBl != 0 || minAfterBl != 0 {
ascender = int16(maxBeforeBl)
descender = int16(minAfterBl)
} else {
ascender = int16(out.YPpem)
descender = 0
}
}
}
if h := ascender - descender; h > 0 {
out.Height = uint16(h)
} else {
out.Height = out.YPpem
}
inferBitmapWidth(&out, avgWidth, upem)
return out
}
func inferBitmapWidth(size *BitmapSize, avgWidth, upem uint16) {
size.Width = uint16((uint32(avgWidth)*uint32(size.XPpem) + uint32(upem/2)) / uint32(upem))
}
// return nil when not found
func (b *bitmapStrike) findTable(glyph gID) *bitmapSubtable {
for i, subtable := range b.subTables {
if subtable.first <= glyph && glyph <= subtable.last {
return &b.subTables[i]
}
}
return nil
}
type bitmapSubtable struct {
first gID // First glyph ID of this range.
last gID // Last glyph ID of this range (inclusive).
imageFormat uint16
index bitmapIndex
}
func newBitmapSubtable(header tables.BitmapSubtable, dataTable []byte) (bitmapSubtable, error) {
out := bitmapSubtable{
first: header.FirstGlyph,
last: header.LastGlyph,
imageFormat: header.ImageFormat,
}
if L, E := len(dataTable), int(header.ImageDataOffset); L < E {
return bitmapSubtable{}, errors.New("invalid bitmap table (EOF)")
}
imageData := dataTable[header.ImageDataOffset:]
var err error
switch index := header.IndexData.(type) {
case tables.IndexData1:
out.index, err = parseIndexSubTable1(header, index, imageData)
case tables.IndexData2:
out.index, err = parseIndexSubTable2(header, index, imageData)
case tables.IndexData3:
out.index, err = parseIndexSubTable3(header, index, imageData)
case tables.IndexData4:
out.index, err = parseIndexSubTable4(header, index, imageData)
case tables.IndexData5:
out.index, err = parseIndexSubTable5(header, index, imageData)
}
return out, err
}
func (subT *bitmapSubtable) image(glyph gID) *bitmapImage {
return subT.index.imageFor(glyph, subT.first, subT.last)
}
type bitmapIndex interface {
// first, last is the range of the subtable
imageFor(glyph gID, first, last gID) *bitmapImage
}
type bitmapImage struct {
image []byte
metrics tables.SmallGlyphMetrics
}
type indexSubTable1And3 struct {
// length lastGlyph - firstGlyph + 1, elements may be nil
glyphs []bitmapImage
format uint16
}
func (idx indexSubTable1And3) imageFor(gid gID, first, last gID) *bitmapImage {
if gid < first || gid > last {
return nil
}
return &idx.glyphs[gid-first]
}
// imageData starts at the image (table[imageDataOffset:])
func parseIndexSubTable1(header tables.BitmapSubtable, index tables.IndexData1, imageData []byte) (indexSubTable1And3, error) {
out := indexSubTable1And3{
format: header.ImageFormat,
glyphs: make([]bitmapImage, len(index.SbitOffsets)-1),
}
for i := range out.glyphs {
if index.SbitOffsets[i] == index.SbitOffsets[i+1] {
continue
}
var err error
out.glyphs[i], err = parseBitmapDataMetrics(imageData, index.SbitOffsets[i], index.SbitOffsets[i+1], header.ImageFormat)
if err != nil {
return out, fmt.Errorf("invalid bitmap index format 1: %s", err)
}
}
return out, nil
}
func parseIndexSubTable3(header tables.BitmapSubtable, index tables.IndexData3, imageData []byte) (indexSubTable1And3, error) {
out := indexSubTable1And3{
format: header.ImageFormat,
glyphs: make([]bitmapImage, len(index.SbitOffsets)-1),
}
for i := range out.glyphs {
if index.SbitOffsets[i] == index.SbitOffsets[i+1] {
continue
}
var err error
out.glyphs[i], err = parseBitmapDataMetrics(imageData, tables.Offset32(index.SbitOffsets[i]), tables.Offset32(index.SbitOffsets[i+1]), header.ImageFormat)
if err != nil {
return out, fmt.Errorf("invalid bitmap index format 1: %s", err)
}
}
return out, nil
}
type bitmapDataStandalone []byte
type indexSubTable2 struct {
glyphs []bitmapDataStandalone
format uint16
metrics tables.BigGlyphMetrics
}
func (idx indexSubTable2) imageFor(gid gID, first, last gID) *bitmapImage {
if gid < first || gid > last {
return nil
}
return &bitmapImage{image: idx.glyphs[gid-first], metrics: idx.metrics.SmallGlyphMetrics}
}
// imageData starts at the image (table[imageDataOffset:])
func parseIndexSubTable2(header tables.BitmapSubtable, index tables.IndexData2, imageData []byte) (indexSubTable2, error) {
out := indexSubTable2{
format: header.ImageFormat,
metrics: index.BigMetrics,
glyphs: make([]bitmapDataStandalone, int(header.LastGlyph)-int(header.FirstGlyph)+1),
}
for i := range out.glyphs {
var err error
out.glyphs[i], err = parseBitmapDataStandalone(imageData, index.ImageSize*uint32(i), index.ImageSize*uint32(i+1), header.ImageFormat)
if err != nil {
return out, fmt.Errorf("invalid bitmap index format 2: %s", err)
}
}
return out, nil
}
type indexedBitmapGlyph struct {
data bitmapImage
glyph gID
}
type indexSubTable4 struct {
glyphs []indexedBitmapGlyph
format uint16
}
func (idx indexSubTable4) imageFor(gid gID, first, last gID) *bitmapImage {
if gid < first || gid > last {
return nil
}
for i, g := range idx.glyphs {
if g.glyph == gid {
return &idx.glyphs[i].data
}
}
return nil
}
// imageData starts at the image (table[imageDataOffset:])
func parseIndexSubTable4(header tables.BitmapSubtable, index tables.IndexData4, imageData []byte) (indexSubTable4, error) {
out := indexSubTable4{
format: header.ImageFormat,
glyphs: make([]indexedBitmapGlyph, len(index.GlyphArray)-1),
}
for i := range out.glyphs {
current, next := index.GlyphArray[i], index.GlyphArray[i+1]
out.glyphs[i].glyph = current.GlyphID
var err error
out.glyphs[i].data, err = parseBitmapDataMetrics(imageData, tables.Offset32(current.SbitOffset), tables.Offset32(next.SbitOffset), header.ImageFormat)
if err != nil {
return out, fmt.Errorf("invalid bitmap index format 4: %s", err)
}
}
return out, nil
}
type indexSubTable5 struct {
glyphIndexes []gID // sorted by glyph index
glyphs []bitmapDataStandalone // corresponding to glyphIndexes
format uint16
metrics tables.BigGlyphMetrics
}
func (idx indexSubTable5) imageFor(gid gID, first, last gID) *bitmapImage {
if gid < first || gid > last {
return nil
}
// binary search
for i, j := 0, len(idx.glyphIndexes); i < j; {
h := i + (j-i)/2
entry := idx.glyphIndexes[h]
if gid < entry {
j = h
} else if entry < gid {
i = h + 1
} else {
return &bitmapImage{image: idx.glyphs[h], metrics: idx.metrics.SmallGlyphMetrics}
}
}
return nil
}
// imageData starts at the image (table[imageDataOffset:])
func parseIndexSubTable5(header tables.BitmapSubtable, index tables.IndexData5, imageData []byte) (indexSubTable5, error) {
out := indexSubTable5{
format: header.ImageFormat,
metrics: index.BigMetrics,
glyphIndexes: index.GlyphIdArray,
glyphs: make([]bitmapDataStandalone, len(index.GlyphIdArray)),
}
for i := range out.glyphs {
var err error
out.glyphs[i], err = parseBitmapDataStandalone(imageData, index.ImageSize*uint32(i), (index.ImageSize+1)*uint32(i), header.ImageFormat)
if err != nil {
return out, fmt.Errorf("invalid bitmap index format 5: %s", err)
}
}
return out, nil
}
func parseBitmapDataMetrics(imageData []byte, start, end tables.Offset32, imageFormat uint16) (bitmapImage, error) {
if len(imageData) < int(end) || start > end {
return bitmapImage{}, errors.New("invalid bitmap data table (EOF)")
}
imageData = imageData[start:end]
switch imageFormat {
case 1, 6, 7, 8, 9:
return bitmapImage{}, fmt.Errorf("valid but currently not implemented bitmap image format: %d", imageFormat)
case 2:
data, _, err := tables.ParseBitmapData2(imageData)
return bitmapImage{metrics: data.SmallGlyphMetrics, image: data.Image}, err
case 17:
data, _, err := tables.ParseBitmapData17(imageData)
return bitmapImage{metrics: data.SmallGlyphMetrics, image: data.Image}, err
case 18:
data, _, err := tables.ParseBitmapData18(imageData)
return bitmapImage{metrics: data.SmallGlyphMetrics, image: data.Image}, err
default:
return bitmapImage{}, fmt.Errorf("unsupported bitmap image format: %d", imageFormat)
}
}
func parseBitmapDataStandalone(imageData []byte, start, end uint32, format uint16) (bitmapDataStandalone, error) {
if len(imageData) < int(end) || start > end {
return nil, fmt.Errorf("invalid bitmap data table (EOF for [%d,%d])", start, end)
}
imageData = imageData[start:end]
switch format {
case 4:
return nil, fmt.Errorf("valid but currently not implemented bitmap image format: %d", format)
case 5:
data, _, err := tables.ParseBitmapData5(imageData)
return data.Image, err
case 19:
data, _, err := tables.ParseBitmapData19(imageData)
return data.Image, err
default:
return nil, fmt.Errorf("unsupported bitmap image format: %d", format)
}
}
func maxu16(a, b uint16) uint16 {
if a > b {
return a
}
return b
}
func mulDiv(a, b, c uint16) uint16 {
return uint16(uint32(a) * uint32(b) / uint32(c))
}
+41
View File
@@ -0,0 +1,41 @@
package font
type glyphExtents struct {
valid bool
extents GlyphExtents
}
type extentsCache []glyphExtents
func (ec extentsCache) get(gid GID) (GlyphExtents, bool) {
if int(gid) >= len(ec) {
return GlyphExtents{}, false
}
ge := ec[gid]
return ge.extents, ge.valid
}
func (ec extentsCache) set(gid GID, extents GlyphExtents) {
if int(gid) >= len(ec) {
return
}
ec[gid].valid = true
ec[gid].extents = extents
}
func (ec extentsCache) reset() {
for i := range ec {
ec[i] = glyphExtents{}
}
}
func (f *Face) GlyphExtents(glyph GID) (GlyphExtents, bool) {
if e, ok := f.extentsCache.get(glyph); ok {
return e, ok
}
e, ok := f.glyphExtentsRaw(glyph)
if ok {
f.extentsCache.set(glyph, e)
}
return e, ok
}
+260
View File
@@ -0,0 +1,260 @@
package cff
import (
"encoding/binary"
"errors"
"fmt"
ps "github.com/go-text/typesetting/font/cff/interpreter"
"github.com/go-text/typesetting/font/opentype/tables"
)
//go:generate ../../../../typesetting-utils/generators/binarygen/cmd/generator . _src.go
// CFF2 represents a parsed 'CFF2' Opentype table.
type CFF2 struct {
fdSelect fdSelect // maybe nil if there is only one font dict
// Charstrings contains the actual glyph definition.
// It has a length of numGlyphs and is indexed by glyph ID.
// See `LoadGlyph` for a way to intepret the glyph data.
Charstrings [][]byte
globalSubrs [][]byte
// array of length 1 if fdSelect is nil
// otherwise, it can be safely indexed by `fdSelect` output
fonts []privateFonts
VarStore tables.ItemVarStore // optional
}
type privateFonts struct {
localSubrs [][]byte
defaultVSIndex int32
}
// ParseCFF2 parses 'src', which must be the content of a 'CFF2' Opentype table.
//
// See also https://learn.microsoft.com/en-us/typography/opentype/spec/cff2
func ParseCFF2(src []byte) (*CFF2, error) {
if L := len(src); L < 5 {
return nil, fmt.Errorf("reading header: EOF: expected length: 5, got %d", L)
}
var header header2
header.mustParse(src)
topDictEnd := int(header.headerSize) + int(header.topDictLength)
if L := len(src); L < topDictEnd {
return nil, fmt.Errorf("reading topDict: EOF: expected length: %d, got %d", topDictEnd, L)
}
topDictSrc := src[header.headerSize:topDictEnd]
var (
tp topDict2
psi ps.Machine
)
if err := psi.Run(topDictSrc, nil, nil, &tp); err != nil {
return nil, fmt.Errorf("reading top dict: %s", err)
}
var (
out CFF2
err error
)
out.globalSubrs, err = parseIndex2(src, topDictEnd)
if err != nil {
return nil, err
}
// parse charstrings
out.Charstrings, err = parseIndex2(src, int(tp.charStrings))
if err != nil {
return nil, err
}
fdIndex, err := parseIndex2(src, int(tp.fdArray))
if err != nil {
return nil, err
}
out.fonts = make([]privateFonts, len(fdIndex))
// private dict reference
for i, font := range fdIndex {
var fd fontDict2
err = psi.Run(font, nil, nil, &fd)
if err != nil {
return nil, fmt.Errorf("reading font dict: %s", err)
}
end := int(fd.privateDictOffset + fd.privateDictSize)
if L := len(src); L < end {
return nil, fmt.Errorf("reading private dict: EOF: expected length: %d, got %d", end, L)
}
// parse private dict
var pd privateDict2
err = psi.Run(src[fd.privateDictOffset:end], nil, nil, &pd)
if err != nil {
return nil, fmt.Errorf("reading private dict: %s", err)
}
out.fonts[i].defaultVSIndex = pd.vsindex
// if required, parse the local subroutines
if pd.subrsOffset != 0 {
out.fonts[i].localSubrs, err = parseIndex2(src, int(pd.subrsOffset))
if err != nil {
return nil, err
}
}
}
if len(fdIndex) > 1 {
// parse the fdSelect
if L := len(src); L < int(tp.fdSelect) {
return nil, fmt.Errorf("reading fdSelect: EOF: expected length: %d, got %d", tp.fdSelect, L)
}
out.fdSelect, _, err = parseFdSelect(src[tp.fdSelect:], len(out.Charstrings))
if err != nil {
return nil, err
}
// sanitize fdSelect outputs
indexExtent := out.fdSelect.extent()
if len(fdIndex) < indexExtent {
return nil, fmt.Errorf("invalid number of font dicts: %d (for %d)", len(fdIndex), indexExtent)
}
}
// parse variation store
if tp.vstore != 0 {
// See https://learn.microsoft.com/en-us/typography/opentype/spec/cff2#variationstore-data-contents
if E, L := int(tp.vstore)+2, len(src); L < E {
return nil, fmt.Errorf("reading variation store: EOF: expected length: %d, got %d", E, L)
}
size := int(binary.BigEndian.Uint16(src[tp.vstore:]))
end := int(tp.vstore) + 2 + size
if L := len(src); L < end {
return nil, fmt.Errorf("reading variation store: EOF: expected length: %d, got %d", end, L)
}
vstore := src[tp.vstore+2 : end]
out.VarStore, _, err = tables.ParseItemVarStore(vstore)
if err != nil {
return nil, err
}
}
return &out, nil
}
func parseIndex2(src []byte, offset int) ([][]byte, error) {
if L := len(src); L < offset+5 {
return nil, fmt.Errorf("reading INDEX: EOF: expected length: %d, got %d", offset+5, L)
}
var is indexStart
is.mustParse(src[offset:])
out, _, err := parseIndexContent(src[offset+5:], is)
return out, err
}
type topDict2 struct {
charStrings int32 // offset
fdArray int32 // offset
fdSelect int32 // offset
vstore int32 // offset
}
func (tp *topDict2) Context() ps.Context { return ps.TopDict }
func (tp *topDict2) Apply(state *ps.Machine, op ps.Operator) error {
switch op {
case ps.Operator{Operator: 7, IsEscaped: true}: // FontMatrix
// skip
state.ArgStack.Clear()
return nil
case ps.Operator{Operator: 17, IsEscaped: false}: // CharStrings
if state.ArgStack.Top < 1 {
return fmt.Errorf("invalid number of arguments for operator %s in Top Dict", op)
}
tp.charStrings = int32(state.ArgStack.Pop())
case ps.Operator{Operator: 36, IsEscaped: true}: // FDArray
if state.ArgStack.Top < 1 {
return fmt.Errorf("invalid number of arguments for operator %s in Top Dict", op)
}
tp.fdArray = int32(state.ArgStack.Pop())
case ps.Operator{Operator: 37, IsEscaped: true}: // FDSelect
if state.ArgStack.Top < 1 {
return fmt.Errorf("invalid number of arguments for operator %s in Top Dict", op)
}
tp.fdSelect = int32(state.ArgStack.Pop())
case ps.Operator{Operator: 24, IsEscaped: false}: // vstore
if state.ArgStack.Top < 1 {
return fmt.Errorf("invalid number of arguments for operator %s in Top Dict", op)
}
tp.vstore = int32(state.ArgStack.Pop())
default:
return fmt.Errorf("invalid operator %s in Top Dict", op)
}
return nil
}
type fontDict2 struct {
privateDictSize int32
privateDictOffset int32
}
func (fd *fontDict2) Context() ps.Context { return ps.TopDict }
func (fd *fontDict2) Apply(state *ps.Machine, op ps.Operator) error {
switch op {
case ps.Operator{Operator: 18, IsEscaped: false}: // Private
if state.ArgStack.Top < 2 {
return fmt.Errorf("invalid number of arguments for operator %s in Font Dict", op)
}
fd.privateDictOffset = int32(state.ArgStack.Pop())
fd.privateDictSize = int32(state.ArgStack.Pop())
return nil
default:
return fmt.Errorf("invalid operator %s in Font Dict", op)
}
}
// privateDict2 contains fields specific to the Private DICT context.
type privateDict2 struct {
subrsOffset int32
vsindex int32 // itemVariationData index in the VariationStore structure table.
}
func (privateDict2) Context() ps.Context { return ps.PrivateDict }
// The Private DICT operators are defined by 5176.CFF.pdf Table 23 "Private
// DICT Operators".
func (priv *privateDict2) Apply(state *ps.Machine, op ps.Operator) error {
if !op.IsEscaped { // 1-byte operators.
switch op.Operator {
case 6, 7, 8, 9: // "BlueValues" "OtherBlues" "FamilyBlues" "FamilyOtherBlues"
return state.ArgStack.PopN(-2)
case 10, 11: // "StdHW" "StdVW"
return state.ArgStack.PopN(1)
case 19: // "Subrs" pop 1
if state.ArgStack.Top < 1 {
return errors.New("invalid stack size for 'subrs' in private Dict charstring")
}
priv.subrsOffset = int32(state.ArgStack.Pop())
return nil
case 22: // "vsindex"
if state.ArgStack.Top < 1 {
return fmt.Errorf("invalid stack size for %s in private Dict", op)
}
priv.vsindex = int32(state.ArgStack.Pop())
return nil
case 23: // "blend"
return nil
}
} else { // 2-byte operators. The first byte is the escape byte.
switch op.Operator {
case 9, 10, 11, 17, 18: // "BlueScale" "BlueShift" "BlueFuzz" "LanguageGroup" "ExpansionFactor"
return state.ArgStack.PopN(1)
case 12, 13: // "StemSnapH" "StemSnapV"
return state.ArgStack.PopN(-2)
}
}
return errors.New("invalid operand in private Dict charstring")
}
+151
View File
@@ -0,0 +1,151 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package cff
import (
"encoding/binary"
"fmt"
)
// Code generated by binarygen from cff2_src.go. DO NOT EDIT
func (item *header2) mustParse(src []byte) {
_ = src[4] // early bound checking
item.majorVersion = src[0]
item.minorVersion = src[1]
item.headerSize = src[2]
item.topDictLength = binary.BigEndian.Uint16(src[3:])
}
func (item *indexStart) mustParse(src []byte) {
_ = src[4] // early bound checking
item.count = binary.BigEndian.Uint32(src[0:])
item.offSize = src[4]
}
func parseFdSelect(src []byte, fdsCount int) (fdSelect, int, error) {
var item fdSelect
if L := len(src); L < 1 {
return item, 0, fmt.Errorf("reading fdSelect: "+"EOF: expected length: 1, got %d", L)
}
format := uint8(src[0])
var (
read int
err error
)
switch format {
case 0:
item, read, err = parseFdSelect0(src[0:], fdsCount)
case 3:
item, read, err = parseFdSelect3(src[0:])
case 4:
item, read, err = parseFdSelect4(src[0:])
default:
err = fmt.Errorf("unsupported fdSelect format %d", format)
}
if err != nil {
return item, 0, fmt.Errorf("reading fdSelect: %s", err)
}
return item, read, nil
}
func parseFdSelect0(src []byte, fdsCount int) (fdSelect0, int, error) {
var item fdSelect0
n := 0
if L := len(src); L < 1 {
return item, 0, fmt.Errorf("reading fdSelect0: "+"EOF: expected length: 1, got %d", L)
}
item.format = src[0]
n += 1
{
L := int(1 + fdsCount)
if len(src) < L {
return item, 0, fmt.Errorf("reading fdSelect0: "+"EOF: expected length: %d, got %d", L, len(src))
}
item.fds = src[1:L]
n = L
}
return item, n, nil
}
func parseFdSelect3(src []byte) (fdSelect3, int, error) {
var item fdSelect3
n := 0
if L := len(src); L < 3 {
return item, 0, fmt.Errorf("reading fdSelect3: "+"EOF: expected length: 3, got %d", L)
}
_ = src[2] // early bound checking
item.format = src[0]
item.nRanges = binary.BigEndian.Uint16(src[1:])
n += 3
{
arrayLength := int(item.nRanges)
if L := len(src); L < 3+arrayLength*3 {
return item, 0, fmt.Errorf("reading fdSelect3: "+"EOF: expected length: %d, got %d", 3+arrayLength*3, L)
}
item.ranges = make([]range3, arrayLength) // allocation guarded by the previous check
for i := range item.ranges {
item.ranges[i].mustParse(src[3+i*3:])
}
n += arrayLength * 3
}
if L := len(src); L < n+2 {
return item, 0, fmt.Errorf("reading fdSelect3: "+"EOF: expected length: n + 2, got %d", L)
}
item.sentinel = binary.BigEndian.Uint16(src[n:])
n += 2
return item, n, nil
}
func parseFdSelect4(src []byte) (fdSelect4, int, error) {
var item fdSelect4
n := 0
if L := len(src); L < 5 {
return item, 0, fmt.Errorf("reading fdSelect4: "+"EOF: expected length: 5, got %d", L)
}
_ = src[4] // early bound checking
item.format = src[0]
item.nRanges = binary.BigEndian.Uint32(src[1:])
n += 5
{
arrayLength := int(item.nRanges)
if L := len(src); L < 5+arrayLength*6 {
return item, 0, fmt.Errorf("reading fdSelect4: "+"EOF: expected length: %d, got %d", 5+arrayLength*6, L)
}
item.ranges = make([]range4, arrayLength) // allocation guarded by the previous check
for i := range item.ranges {
item.ranges[i].mustParse(src[5+i*6:])
}
n += arrayLength * 6
}
if L := len(src); L < n+4 {
return item, 0, fmt.Errorf("reading fdSelect4: "+"EOF: expected length: n + 4, got %d", L)
}
item.sentinel = binary.BigEndian.Uint32(src[n:])
n += 4
return item, n, nil
}
func (item *range3) mustParse(src []byte) {
_ = src[2] // early bound checking
item.first = binary.BigEndian.Uint16(src[0:])
item.fd = src[2]
}
func (item *range4) mustParse(src []byte) {
_ = src[5] // early bound checking
item.first = binary.BigEndian.Uint32(src[0:])
item.fd = binary.BigEndian.Uint16(src[4:])
}
+162
View File
@@ -0,0 +1,162 @@
package cff
import (
"errors"
"github.com/go-text/typesetting/font/opentype/tables"
)
//go:generate ../../../../../typesetting-utils/generators/binarygen/cmd/generator . _src.go
type header2 struct {
majorVersion uint8 // Format major version. Set to 2.
minorVersion uint8 // Format minor version. Set to zero.
headerSize uint8 // Header size (bytes).
topDictLength uint16 // Length of Top DICT structure in bytes.
}
type indexStart struct {
count uint32 // Number of objects stored in INDEX
offSize uint8 // Offset array element size
// then
// offset []Offset
// data []byte
}
//lint:ignore U1000 this type is required so that the code generator add a ParseFdSelect function
type dummy struct {
fd fdSelect
}
// fdSelect holds a CFF font's Font Dict Select data.
type fdSelect interface {
isFdSelect()
fontDictIndex(glyph tables.GlyphID) (byte, error)
// return the maximum index + 1 (it's the length of an array
// which can be safely indexed by the indexes)
extent() int
}
func (fdSelect0) isFdSelect() {}
func (fdSelect3) isFdSelect() {}
func (fdSelect4) isFdSelect() {}
type fdSelect0 struct {
format uint8 `unionTag:"0"` // Set to 0
fds []uint8 // [nGlyphs] FD selector array
}
var errGlyph = errors.New("invalid glyph index")
func (fds fdSelect0) fontDictIndex(glyph tables.GlyphID) (byte, error) {
if int(glyph) >= len(fds.fds) {
return 0, errGlyph
}
return fds.fds[glyph], nil
}
func (fds fdSelect0) extent() int {
max := -1
for _, b := range fds.fds {
if int(b) > max {
max = int(b)
}
}
return max + 1
}
type fdSelect3 struct {
format uint8 `unionTag:"3"` // Set to 3
nRanges uint16 // Number of ranges
ranges []range3 `arrayCount:"ComputedField-nRanges"` // [nRanges] Array of Range3 records (see below)
sentinel uint16 // Sentinel GID
}
type range3 struct {
first tables.GlyphID // First glyph index in range
fd uint8 // FD index for all glyphs in range
}
func (fds fdSelect3) fontDictIndex(x tables.GlyphID) (byte, error) {
lo, hi := 0, len(fds.ranges)
for lo < hi {
i := (lo + hi) / 2
r := fds.ranges[i]
xlo := r.first
if x < xlo {
hi = i
continue
}
xhi := fds.sentinel
if i < len(fds.ranges)-1 {
xhi = fds.ranges[i+1].first
}
if xhi <= x {
lo = i + 1
continue
}
return r.fd, nil
}
return 0, errGlyph
}
func (fds fdSelect3) extent() int {
max := -1
for _, b := range fds.ranges {
if int(b.fd) > max {
max = int(b.fd)
}
}
return max + 1
}
type fdSelect4 struct {
format uint8 `unionTag:"4"` // Set to 4
nRanges uint32 // Number of ranges
ranges []range4 `arrayCount:"ComputedField-nRanges"` // [nRanges] Array of Range4 records (see below)
sentinel uint32 // Sentinel GID
}
type range4 struct {
first uint32 // First glyph index in range
fd uint16 // FD index for all glyphs in range
}
func (fds fdSelect4) fontDictIndex(x tables.GlyphID) (byte, error) {
fd, err := fds.fontDictIndex32(uint32(x))
return byte(fd), err
}
func (fds fdSelect4) fontDictIndex32(x uint32) (uint16, error) {
lo, hi := 0, len(fds.ranges)
for lo < hi {
i := (lo + hi) / 2
r := fds.ranges[i]
xlo := r.first
if x < xlo {
hi = i
continue
}
xhi := fds.sentinel
if i < len(fds.ranges)-1 {
xhi = fds.ranges[i+1].first
}
if xhi <= x {
lo = i + 1
continue
}
return r.fd, nil
}
return 0, errGlyph
}
func (fds fdSelect4) extent() int {
max := -1
for _, b := range fds.ranges {
if int(b.fd) > max {
max = int(b.fd)
}
}
return max + 1
}
+423
View File
@@ -0,0 +1,423 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package cff
var (
charsetISOAdobe = [229]uint16{
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32,
33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65,
66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98,
99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131,
132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164,
165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197,
198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228,
}
charsetExpert = [166]uint16{
0, 1, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 13, 14, 15, 99, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 27, 28, 249, 250, 251, 252, 253, 254,
255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 109, 110, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286,
287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 158, 155,
163, 319, 320, 321, 322, 323, 324, 325, 326, 150, 164, 169, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348,
349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378,
}
charsetExpertSubset = [87]uint16{
0, 1, 231, 232, 235, 236, 237, 238, 13, 14, 15, 99, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 27, 28, 249, 250, 251, 253, 254,
255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 109, 110, 267, 268, 269, 270, 272, 300, 301, 302, 305, 314, 315, 158, 155, 163, 320,
321, 322, 323, 324, 325, 326, 150, 164, 169, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346,
}
)
var stdStrings = [391]string{
".notdef",
"space",
"exclam",
"quotedbl",
"numbersign",
"dollar",
"percent",
"ampersand",
"quoteright",
"parenleft",
"parenright",
"asterisk",
"plus",
"comma",
"hyphen",
"period",
"slash",
"zero",
"one",
"two",
"three",
"four",
"five",
"six",
"seven",
"eight",
"nine",
"colon",
"semicolon",
"less",
"equal",
"greater",
"question",
"at",
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"K",
"L",
"M",
"N",
"O",
"P",
"Q",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z",
"bracketleft",
"backslash",
"bracketright",
"asciicircum",
"underscore",
"quoteleft",
"a",
"b",
"c",
"d",
"e",
"f",
"g",
"h",
"i",
"j",
"k",
"l",
"m",
"n",
"o",
"p",
"q",
"r",
"s",
"t",
"u",
"v",
"w",
"x",
"y",
"z",
"braceleft",
"bar",
"braceright",
"asciitilde",
"exclamdown",
"cent",
"sterling",
"fraction",
"yen",
"florin",
"section",
"currency",
"quotesingle",
"quotedblleft",
"guillemotleft",
"guilsinglleft",
"guilsinglright",
"fi",
"fl",
"endash",
"dagger",
"daggerdbl",
"periodcentered",
"paragraph",
"bullet",
"quotesinglbase",
"quotedblbase",
"quotedblright",
"guillemotright",
"ellipsis",
"perthousand",
"questiondown",
"grave",
"acute",
"circumflex",
"tilde",
"macron",
"breve",
"dotaccent",
"dieresis",
"ring",
"cedilla",
"hungarumlaut",
"ogonek",
"caron",
"emdash",
"AE",
"ordfeminine",
"Lslash",
"Oslash",
"OE",
"ordmasculine",
"ae",
"dotlessi",
"lslash",
"oslash",
"oe",
"germandbls",
"onesuperior",
"logicalnot",
"mu",
"trademark",
"Eth",
"onehalf",
"plusminus",
"Thorn",
"onequarter",
"divide",
"brokenbar",
"degree",
"thorn",
"threequarters",
"twosuperior",
"registered",
"minus",
"eth",
"multiply",
"threesuperior",
"copyright",
"Aacute",
"Acircumflex",
"Adieresis",
"Agrave",
"Aring",
"Atilde",
"Ccedilla",
"Eacute",
"Ecircumflex",
"Edieresis",
"Egrave",
"Iacute",
"Icircumflex",
"Idieresis",
"Igrave",
"Ntilde",
"Oacute",
"Ocircumflex",
"Odieresis",
"Ograve",
"Otilde",
"Scaron",
"Uacute",
"Ucircumflex",
"Udieresis",
"Ugrave",
"Yacute",
"Ydieresis",
"Zcaron",
"aacute",
"acircumflex",
"adieresis",
"agrave",
"aring",
"atilde",
"ccedilla",
"eacute",
"ecircumflex",
"edieresis",
"egrave",
"iacute",
"icircumflex",
"idieresis",
"igrave",
"ntilde",
"oacute",
"ocircumflex",
"odieresis",
"ograve",
"otilde",
"scaron",
"uacute",
"ucircumflex",
"udieresis",
"ugrave",
"yacute",
"ydieresis",
"zcaron",
"exclamsmall",
"Hungarumlautsmall",
"dollaroldstyle",
"dollarsuperior",
"ampersandsmall",
"Acutesmall",
"parenleftsuperior",
"parenrightsuperior",
"twodotenleader",
"onedotenleader",
"zerooldstyle",
"oneoldstyle",
"twooldstyle",
"threeoldstyle",
"fouroldstyle",
"fiveoldstyle",
"sixoldstyle",
"sevenoldstyle",
"eightoldstyle",
"nineoldstyle",
"commasuperior",
"threequartersemdash",
"periodsuperior",
"questionsmall",
"asuperior",
"bsuperior",
"centsuperior",
"dsuperior",
"esuperior",
"isuperior",
"lsuperior",
"msuperior",
"nsuperior",
"osuperior",
"rsuperior",
"ssuperior",
"tsuperior",
"ff",
"ffi",
"ffl",
"parenleftinferior",
"parenrightinferior",
"Circumflexsmall",
"hyphensuperior",
"Gravesmall",
"Asmall",
"Bsmall",
"Csmall",
"Dsmall",
"Esmall",
"Fsmall",
"Gsmall",
"Hsmall",
"Ismall",
"Jsmall",
"Ksmall",
"Lsmall",
"Msmall",
"Nsmall",
"Osmall",
"Psmall",
"Qsmall",
"Rsmall",
"Ssmall",
"Tsmall",
"Usmall",
"Vsmall",
"Wsmall",
"Xsmall",
"Ysmall",
"Zsmall",
"colonmonetary",
"onefitted",
"rupiah",
"Tildesmall",
"exclamdownsmall",
"centoldstyle",
"Lslashsmall",
"Scaronsmall",
"Zcaronsmall",
"Dieresissmall",
"Brevesmall",
"Caronsmall",
"Dotaccentsmall",
"Macronsmall",
"figuredash",
"hypheninferior",
"Ogoneksmall",
"Ringsmall",
"Cedillasmall",
"questiondownsmall",
"oneeighth",
"threeeighths",
"fiveeighths",
"seveneighths",
"onethird",
"twothirds",
"zerosuperior",
"foursuperior",
"fivesuperior",
"sixsuperior",
"sevensuperior",
"eightsuperior",
"ninesuperior",
"zeroinferior",
"oneinferior",
"twoinferior",
"threeinferior",
"fourinferior",
"fiveinferior",
"sixinferior",
"seveninferior",
"eightinferior",
"nineinferior",
"centinferior",
"dollarinferior",
"periodinferior",
"commainferior",
"Agravesmall",
"Aacutesmall",
"Acircumflexsmall",
"Atildesmall",
"Adieresissmall",
"Aringsmall",
"AEsmall",
"Ccedillasmall",
"Egravesmall",
"Eacutesmall",
"Ecircumflexsmall",
"Edieresissmall",
"Igravesmall",
"Iacutesmall",
"Icircumflexsmall",
"Idieresissmall",
"Ethsmall",
"Ntildesmall",
"Ogravesmall",
"Oacutesmall",
"Ocircumflexsmall",
"Otildesmall",
"Odieresissmall",
"OEsmall",
"Oslashsmall",
"Ugravesmall",
"Uacutesmall",
"Ucircumflexsmall",
"Udieresissmall",
"Yacutesmall",
"Thornsmall",
"Ydieresissmall",
"001.000",
"001.001",
"001.002",
"001.003",
"Black",
"Bold",
"Book",
"Light",
"Medium",
"Regular",
"Roman",
"Semibold",
}
+315
View File
@@ -0,0 +1,315 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package cff
import (
"errors"
"fmt"
ps "github.com/go-text/typesetting/font/cff/interpreter"
ot "github.com/go-text/typesetting/font/opentype"
"github.com/go-text/typesetting/font/opentype/tables"
)
// LoadGlyph parses the glyph charstring to compute segments and path bounds.
// It returns an error if the glyph is invalid or if decoding the charstring fails.
func (f *CFF) LoadGlyph(glyph tables.GlyphID) ([]ot.Segment, ps.PathBounds, error) {
if int(glyph) >= len(f.Charstrings) {
return nil, ps.PathBounds{}, errGlyph
}
var (
psi ps.Machine
loader type2CharstringHandler
index byte = 0
err error
)
if f.fdSelect != nil {
index, err = f.fdSelect.fontDictIndex(glyph)
if err != nil {
return nil, ps.PathBounds{}, err
}
}
subrs := f.localSubrs[index]
err = psi.Run(f.Charstrings[glyph], subrs, f.globalSubrs, &loader)
return loader.cs.Segments, loader.cs.Bounds, err
}
// type2CharstringHandler implements operators needed to fetch Type2 charstring metrics
type type2CharstringHandler struct {
cs ps.CharstringReader
// found in private DICT, needed since we can't differenciate
// no width set from 0 width
// `width` must be initialized to default width
nominalWidthX float64
width float64
}
func (type2CharstringHandler) Context() ps.Context { return ps.Type2Charstring }
func (met *type2CharstringHandler) Apply(state *ps.Machine, op ps.Operator) error {
var err error
if !op.IsEscaped {
switch op.Operator {
case 11: // return
return state.Return() // do not clear the arg stack
case 14: // endchar
if state.ArgStack.Top > 0 { // width is optional
met.width = met.nominalWidthX + state.ArgStack.Vals[0]
}
met.cs.ClosePath()
return ps.ErrInterrupt
case 10: // callsubr
return ps.LocalSubr(state) // do not clear the arg stack
case 29: // callgsubr
return ps.GlobalSubr(state) // do not clear the arg stack
case 21: // rmoveto
if state.ArgStack.Top > 2 { // width is optional
met.width = met.nominalWidthX + state.ArgStack.Vals[0]
}
err = met.cs.Rmoveto(state)
case 22: // hmoveto
if state.ArgStack.Top > 1 { // width is optional
met.width = met.nominalWidthX + state.ArgStack.Vals[0]
}
err = met.cs.Hmoveto(state)
case 4: // vmoveto
if state.ArgStack.Top > 1 { // width is optional
met.width = met.nominalWidthX + state.ArgStack.Vals[0]
}
err = met.cs.Vmoveto(state)
case 1, 18: // hstem, hstemhm
met.cs.Hstem(state)
case 3, 23: // vstem, vstemhm
met.cs.Vstem(state)
case 19, 20: // hintmask, cntrmask
// variable number of arguments, but always even
// for xxxmask, if there are arguments on the stack, then this is an impliied stem
if state.ArgStack.Top&1 != 0 {
met.width = met.nominalWidthX + state.ArgStack.Vals[0]
}
met.cs.Hintmask(state)
// the stack is managed by the previous call
return nil
case 5: // rlineto
met.cs.Rlineto(state)
case 6: // hlineto
met.cs.Hlineto(state)
case 7: // vlineto
met.cs.Vlineto(state)
case 8: // rrcurveto
met.cs.Rrcurveto(state)
case 24: // rcurveline
err = met.cs.Rcurveline(state)
case 25: // rlinecurve
err = met.cs.Rlinecurve(state)
case 26: // vvcurveto
met.cs.Vvcurveto(state)
case 27: // hhcurveto
met.cs.Hhcurveto(state)
case 30: // vhcurveto
met.cs.Vhcurveto(state)
case 31: // hvcurveto
met.cs.Hvcurveto(state)
default:
// no other operands are allowed before the ones handled above
err = fmt.Errorf("invalid operator %s in charstring", op)
}
} else {
switch op.Operator {
case 34: // hflex
err = met.cs.Hflex(state)
case 35: // flex
err = met.cs.Flex(state)
case 36: // hflex1
err = met.cs.Hflex1(state)
case 37: // flex1
err = met.cs.Flex1(state)
default:
// no other operands are allowed before the ones handled above
err = fmt.Errorf("invalid operator %s in charstring", op)
}
}
state.ArgStack.Clear()
return err
}
// ---------------------------- CFF2 format ----------------------------
// LoadGlyph parses the glyph charstring to compute segments and path bounds.
// It returns an error if the glyph is invalid or if decoding the charstring fails.
//
// [coords] must either have the same length as the variations axis, or be empty,
// and be normalized
func (f *CFF2) LoadGlyph(glyph tables.GlyphID, coords []tables.Coord) ([]ot.Segment, ps.PathBounds, error) {
if int(glyph) >= len(f.Charstrings) {
return nil, ps.PathBounds{}, errGlyph
}
var (
psi ps.Machine
loader cff2CharstringHandler
index byte = 0
err error
)
if f.fdSelect != nil {
index, err = f.fdSelect.fontDictIndex(glyph)
if err != nil {
return nil, ps.PathBounds{}, err
}
}
font := f.fonts[index]
loader.coords = coords
loader.vars = f.VarStore
loader.setVSIndex(int(font.defaultVSIndex))
err = psi.Run(f.Charstrings[glyph], font.localSubrs, f.globalSubrs, &loader)
return loader.cs.Segments, loader.cs.Bounds, err
}
// cff2CharstringHandler implements operators needed to fetch CFF2 charstring metrics
type cff2CharstringHandler struct {
cs ps.CharstringReader
coords []tables.Coord // normalized variation coordinates
vars tables.ItemVarStore
// the currently active ItemVariationData subtable (default to 0)
scalars []float32 // computed from the currently active ItemVariationData subtable
}
func (cff2CharstringHandler) Context() ps.Context { return ps.Type2Charstring }
func (met *cff2CharstringHandler) setVSIndex(index int) error {
// if the font has variations, always build the scalar
// slice, even if no variations are activated by the user:
// the blend operator needs to know how many args to skip.
if len(met.vars.ItemVariationDatas) == 0 {
return nil
}
if index >= len(met.vars.ItemVariationDatas) {
return fmt.Errorf("invalid 'vsindex' %d", index)
}
vars := met.vars.ItemVariationDatas[index]
k := int32(len(vars.RegionIndexes)) // number of regions
met.scalars = append(met.scalars[:0], make([]float32, k)...)
for i, regionIndex := range vars.RegionIndexes {
region := met.vars.VariationRegionList.VariationRegions[regionIndex]
met.scalars[i] = region.Evaluate(met.coords)
}
return nil
}
func (met *cff2CharstringHandler) blend(state *ps.Machine) error {
// blend requires n*(k+1) + 1 arguments
if state.ArgStack.Top < 1 {
return errors.New("missing n argument for blend operator")
}
n := int32(state.ArgStack.Pop())
k := int32(len(met.scalars))
if state.ArgStack.Top < n*(k+1) {
return errors.New("missing arguments for blend operator")
}
// actually apply the deltas only if the user has activated variations
if len(met.coords) != 0 {
args := state.ArgStack.Vals[state.ArgStack.Top-n*(k+1) : state.ArgStack.Top]
// the first n values are the 'default' arguments
for i := int32(0); i < n; i++ {
baseValue := args[i]
deltas := args[n+i*k : n+(i+1)*k] // all the regions, for one operand
v := 0.
for ik, delta := range deltas {
v += float64(met.scalars[ik]) * delta
}
args[i] = baseValue + v // update the stack with the blended value
}
}
// clear the stack, keeping only n arguments
state.ArgStack.Top -= n * k
return nil
}
func (met *cff2CharstringHandler) Apply(state *ps.Machine, op ps.Operator) error {
var err error
if !op.IsEscaped {
switch op.Operator {
case 1, 18: // hstem, hstemhm
met.cs.Hstem(state)
case 3, 23: // vstem, vstemhm
met.cs.Vstem(state)
case 4: // vmoveto
err = met.cs.Vmoveto(state)
case 5: // rlineto
met.cs.Rlineto(state)
case 6: // hlineto
met.cs.Hlineto(state)
case 7: // vlineto
met.cs.Vlineto(state)
case 8: // rrcurveto
met.cs.Rrcurveto(state)
case 10: // callsubr
return ps.LocalSubr(state) // do not clear the arg stack
case 15: // vsindex
if state.ArgStack.Top < 1 {
return errors.New("missing argument for vsindex operator")
}
err = met.setVSIndex(int(state.ArgStack.Pop()))
case 16: // blend
return met.blend(state) // do not clear the arg stack
case 19, 20: // hintmask, cntrmask
// variable number of arguments, but always even
// for xxxmask, if there are arguments on the stack, then this is an impliied stem
met.cs.Hintmask(state)
// the stack is managed by the previous call
return nil
case 21: // rmoveto
err = met.cs.Rmoveto(state)
case 22: // hmoveto
err = met.cs.Hmoveto(state)
case 24: // rcurveline
err = met.cs.Rcurveline(state)
case 25: // rlinecurve
err = met.cs.Rlinecurve(state)
case 26: // vvcurveto
met.cs.Vvcurveto(state)
case 27: // hhcurveto
met.cs.Hhcurveto(state)
case 29: // callgsubr
return ps.GlobalSubr(state) // do not clear the arg stack
case 30: // vhcurveto
met.cs.Vhcurveto(state)
case 31: // hvcurveto
met.cs.Hvcurveto(state)
default:
// no other operands are allowed before the ones handled above
err = fmt.Errorf("invalid operator %s in charstring", op)
}
} else {
switch op.Operator {
case 34: // hflex
err = met.cs.Hflex(state)
case 35: // flex
err = met.cs.Flex(state)
case 36: // hflex1
err = met.cs.Hflex1(state)
case 37: // flex1
err = met.cs.Flex1(state)
default:
// no other operands are allowed before the ones handled above
err = fmt.Errorf("invalid operator %s in charstring", op)
}
}
state.ArgStack.Clear()
return err
}
@@ -0,0 +1,586 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package psinterpreter
import (
"errors"
"fmt"
"math"
ot "github.com/go-text/typesetting/font/opentype"
)
// PathBounds represents a control bounds for
// a glyph outline (in font units).
type PathBounds struct {
Min, Max Point
}
// Enlarge enlarges the bounds to include pt
func (b *PathBounds) Enlarge(pt Point) {
if pt.X < b.Min.X {
b.Min.X = pt.X
}
if pt.X > b.Max.X {
b.Max.X = pt.X
}
if pt.Y < b.Min.Y {
b.Min.Y = pt.Y
}
if pt.Y > b.Max.Y {
b.Max.Y = pt.Y
}
}
// ToExtents converts a path bounds to the corresponding glyph extents.
func (b *PathBounds) ToExtents() ot.GlyphExtents {
xBearing, yBearing := math.Round(b.Min.X), math.Round(b.Max.Y)
return ot.GlyphExtents{
XBearing: float32(xBearing),
YBearing: float32(yBearing),
Width: float32(math.Round(b.Max.X - xBearing)),
Height: float32(math.Round(b.Min.Y - yBearing)),
}
}
// Point is a 2D Point in font units.
type Point struct{ X, Y float64 }
// Move translates the Point.
func (p *Point) Move(dx, dy float64) {
p.X += dx
p.Y += dy
}
func (p Point) toSP() ot.SegmentPoint {
return ot.SegmentPoint{X: float32(p.X), Y: float32(p.Y)}
}
// CharstringReader provides implementation
// of the operators found in a font charstring.
type CharstringReader struct {
// Acumulated segments for the glyph outlines
Segments []ot.Segment
// Acumulated bounds for the glyph outlines
Bounds PathBounds
vstemCount int32
hstemCount int32
hintmaskSize int32
CurrentPoint Point
firstPoint Point // first point in path, required to check if a path is closed
isPathOpen bool
seenHintmask bool
// bounds for an empty path is {0,0,0,0}
// however, for the first point in the path,
// we must not compare the coordinates with {0,0,0,0}
seenPoint bool
}
// enlarges the current bounds to include the Point (x,y).
func (out *CharstringReader) updateBounds(pt Point) {
if !out.seenPoint {
out.Bounds.Min, out.Bounds.Max = pt, pt
out.seenPoint = true
return
}
out.Bounds.Enlarge(pt)
}
func (out *CharstringReader) Hstem(state *Machine) {
out.hstemCount += state.ArgStack.Top / 2
}
func (out *CharstringReader) Vstem(state *Machine) {
out.vstemCount += state.ArgStack.Top / 2
}
func (out *CharstringReader) determineHintmaskSize(state *Machine) {
if !out.seenHintmask {
out.vstemCount += state.ArgStack.Top / 2
out.hintmaskSize = (out.hstemCount + out.vstemCount + 7) >> 3
out.seenHintmask = true
}
}
func (out *CharstringReader) Hintmask(state *Machine) {
out.determineHintmaskSize(state)
state.SkipBytes(out.hintmaskSize)
}
func (out *CharstringReader) move(pt Point) {
out.ensureClosePath()
out.CurrentPoint.Move(pt.X, pt.Y)
out.isPathOpen = false
out.firstPoint = out.CurrentPoint
out.Segments = append(out.Segments, ot.Segment{
Op: ot.SegmentOpMoveTo,
Args: [3]ot.SegmentPoint{out.CurrentPoint.toSP()},
})
}
// pt is in absolute coordinates
func (out *CharstringReader) line(pt Point) {
if !out.isPathOpen {
out.isPathOpen = true
out.updateBounds(out.CurrentPoint)
}
out.CurrentPoint = pt
out.updateBounds(pt)
out.Segments = append(out.Segments, ot.Segment{
Op: ot.SegmentOpLineTo,
Args: [3]ot.SegmentPoint{pt.toSP()},
})
}
func (out *CharstringReader) curve(pt1, pt2, pt3 Point) {
if !out.isPathOpen {
out.isPathOpen = true
out.updateBounds(out.CurrentPoint)
}
/* include control Points */
out.updateBounds(pt1)
out.updateBounds(pt2)
out.CurrentPoint = pt3
out.updateBounds(pt3)
out.Segments = append(out.Segments, ot.Segment{
Op: ot.SegmentOpCubeTo,
Args: [3]ot.SegmentPoint{pt1.toSP(), pt2.toSP(), pt3.toSP()},
})
}
func (out *CharstringReader) doubleCurve(pt1, pt2, pt3, pt4, pt5, pt6 Point) {
out.curve(pt1, pt2, pt3)
out.curve(pt4, pt5, pt6)
}
func (out *CharstringReader) ensureClosePath() {
if out.firstPoint != out.CurrentPoint {
out.Segments = append(out.Segments, ot.Segment{
Op: ot.SegmentOpLineTo,
Args: [3]ot.SegmentPoint{out.firstPoint.toSP()},
})
}
}
// ------------------------------------------------------------
// LocalSubr pops the subroutine index and call it
func LocalSubr(state *Machine) error {
if state.ArgStack.Top < 1 {
return errors.New("invalid callsubr operator (empty stack)")
}
index := int32(state.ArgStack.Pop())
return state.CallSubroutine(index, true)
}
// GlobalSubr pops the subroutine index and call it
func GlobalSubr(state *Machine) error {
if state.ArgStack.Top < 1 {
return errors.New("invalid callgsubr operator (empty stack)")
}
index := int32(state.ArgStack.Pop())
return state.CallSubroutine(index, false)
}
// ClosePath closes the current contour, adding
// a segment to the first point if needed.
func (out *CharstringReader) ClosePath() {
out.ensureClosePath()
out.isPathOpen = false
}
func (out *CharstringReader) Rmoveto(state *Machine) error {
if state.ArgStack.Top < 2 {
return errors.New("invalid rmoveto operator")
}
y := state.ArgStack.Pop()
x := state.ArgStack.Pop()
out.move(Point{x, y})
return nil
}
func (out *CharstringReader) Vmoveto(state *Machine) error {
if state.ArgStack.Top < 1 {
return errors.New("invalid vmoveto operator")
}
y := state.ArgStack.Pop()
out.move(Point{0, y})
return nil
}
func (out *CharstringReader) Hmoveto(state *Machine) error {
if state.ArgStack.Top < 1 {
return errors.New("invalid hmoveto operator")
}
x := state.ArgStack.Pop()
out.move(Point{x, 0})
return nil
}
func (out *CharstringReader) Rlineto(state *Machine) {
for i := int32(0); i+2 <= state.ArgStack.Top; i += 2 {
newPoint := out.CurrentPoint
newPoint.Move(state.ArgStack.Vals[i], state.ArgStack.Vals[i+1])
out.line(newPoint)
}
state.ArgStack.Clear()
}
func (out *CharstringReader) Hlineto(state *Machine) {
var i int32
for ; i+2 <= state.ArgStack.Top; i += 2 {
newPoint := out.CurrentPoint
newPoint.X += state.ArgStack.Vals[i]
out.line(newPoint)
newPoint.Y += state.ArgStack.Vals[i+1]
out.line(newPoint)
}
if i < state.ArgStack.Top {
newPoint := out.CurrentPoint
newPoint.X += state.ArgStack.Vals[i]
out.line(newPoint)
}
}
func (out *CharstringReader) Vlineto(state *Machine) {
var i int32
for ; i+2 <= state.ArgStack.Top; i += 2 {
newPoint := out.CurrentPoint
newPoint.Y += state.ArgStack.Vals[i]
out.line(newPoint)
newPoint.X += state.ArgStack.Vals[i+1]
out.line(newPoint)
}
if i < state.ArgStack.Top {
newPoint := out.CurrentPoint
newPoint.Y += state.ArgStack.Vals[i]
out.line(newPoint)
}
}
// RelativeCurveTo draws a curve with controls points computed from
// the current point and `arg1`, `arg2`, `arg3`
func (out *CharstringReader) RelativeCurveTo(arg1, arg2, arg3 Point) {
pt1 := out.CurrentPoint
pt1.Move(arg1.X, arg1.Y)
pt2 := pt1
pt2.Move(arg2.X, arg2.Y)
pt3 := pt2
pt3.Move(arg3.X, arg3.Y)
out.curve(pt1, pt2, pt3)
}
func (out *CharstringReader) Rrcurveto(state *Machine) {
for i := int32(0); i+6 <= state.ArgStack.Top; i += 6 {
out.RelativeCurveTo(
Point{state.ArgStack.Vals[i], state.ArgStack.Vals[i+1]},
Point{state.ArgStack.Vals[i+2], state.ArgStack.Vals[i+3]},
Point{state.ArgStack.Vals[i+4], state.ArgStack.Vals[i+5]},
)
}
}
func (out *CharstringReader) Hhcurveto(state *Machine) {
var (
i int32
pt1 = out.CurrentPoint
)
if (state.ArgStack.Top & 1) != 0 {
pt1.Y += (state.ArgStack.Vals[i])
i++
}
for ; i+4 <= state.ArgStack.Top; i += 4 {
pt1.X += state.ArgStack.Vals[i]
pt2 := pt1
pt2.Move(state.ArgStack.Vals[i+1], state.ArgStack.Vals[i+2])
pt3 := pt2
pt3.X += state.ArgStack.Vals[i+3]
out.curve(pt1, pt2, pt3)
pt1 = out.CurrentPoint
}
}
func (out *CharstringReader) Vhcurveto(state *Machine) {
var i int32
if (state.ArgStack.Top % 8) >= 4 {
pt1 := out.CurrentPoint
pt1.Y += state.ArgStack.Vals[i]
pt2 := pt1
pt2.Move(state.ArgStack.Vals[i+1], state.ArgStack.Vals[i+2])
pt3 := pt2
pt3.X += state.ArgStack.Vals[i+3]
i += 4
for ; i+8 <= state.ArgStack.Top; i += 8 {
out.curve(pt1, pt2, pt3)
pt1 = out.CurrentPoint
pt1.X += (state.ArgStack.Vals[i])
pt2 = pt1
pt2.Move(state.ArgStack.Vals[i+1], state.ArgStack.Vals[i+2])
pt3 = pt2
pt3.Y += (state.ArgStack.Vals[i+3])
out.curve(pt1, pt2, pt3)
pt1 = pt3
pt1.Y += (state.ArgStack.Vals[i+4])
pt2 = pt1
pt2.Move(state.ArgStack.Vals[i+5], state.ArgStack.Vals[i+6])
pt3 = pt2
pt3.X += (state.ArgStack.Vals[i+7])
}
if i < state.ArgStack.Top {
pt3.Y += (state.ArgStack.Vals[i])
}
out.curve(pt1, pt2, pt3)
} else {
for ; i+8 <= state.ArgStack.Top; i += 8 {
pt1 := out.CurrentPoint
pt1.Y += (state.ArgStack.Vals[i])
pt2 := pt1
pt2.Move(state.ArgStack.Vals[i+1], state.ArgStack.Vals[i+2])
pt3 := pt2
pt3.X += (state.ArgStack.Vals[i+3])
out.curve(pt1, pt2, pt3)
pt1 = pt3
pt1.X += (state.ArgStack.Vals[i+4])
pt2 = pt1
pt2.Move(state.ArgStack.Vals[i+5], state.ArgStack.Vals[i+6])
pt3 = pt2
pt3.Y += (state.ArgStack.Vals[i+7])
if (state.ArgStack.Top-i < 16) && ((state.ArgStack.Top & 1) != 0) {
pt3.X += (state.ArgStack.Vals[i+8])
}
out.curve(pt1, pt2, pt3)
}
}
}
func (out *CharstringReader) Hvcurveto(state *Machine) {
var i int32
if (state.ArgStack.Top % 8) >= 4 {
pt1 := out.CurrentPoint
pt1.X += (state.ArgStack.Vals[i])
pt2 := pt1
pt2.Move(state.ArgStack.Vals[i+1], state.ArgStack.Vals[i+2])
pt3 := pt2
pt3.Y += (state.ArgStack.Vals[i+3])
i += 4
for ; i+8 <= state.ArgStack.Top; i += 8 {
out.curve(pt1, pt2, pt3)
pt1 = out.CurrentPoint
pt1.Y += (state.ArgStack.Vals[i])
pt2 = pt1
pt2.Move(state.ArgStack.Vals[i+1], state.ArgStack.Vals[i+2])
pt3 = pt2
pt3.X += (state.ArgStack.Vals[i+3])
out.curve(pt1, pt2, pt3)
pt1 = pt3
pt1.X += state.ArgStack.Vals[i+4]
pt2 = pt1
pt2.Move(state.ArgStack.Vals[i+5], state.ArgStack.Vals[i+6])
pt3 = pt2
pt3.Y += state.ArgStack.Vals[i+7]
}
if i < state.ArgStack.Top {
pt3.X += (state.ArgStack.Vals[i])
}
out.curve(pt1, pt2, pt3)
} else {
for ; i+8 <= state.ArgStack.Top; i += 8 {
pt1 := out.CurrentPoint
pt1.X += (state.ArgStack.Vals[i])
pt2 := pt1
pt2.Move(state.ArgStack.Vals[i+1], state.ArgStack.Vals[i+2])
pt3 := pt2
pt3.Y += (state.ArgStack.Vals[i+3])
out.curve(pt1, pt2, pt3)
pt1 = pt3
pt1.Y += (state.ArgStack.Vals[i+4])
pt2 = pt1
pt2.Move(state.ArgStack.Vals[i+5], state.ArgStack.Vals[i+6])
pt3 = pt2
pt3.X += (state.ArgStack.Vals[i+7])
if (state.ArgStack.Top-i < 16) && ((state.ArgStack.Top & 1) != 0) {
pt3.Y += state.ArgStack.Vals[i+8]
}
out.curve(pt1, pt2, pt3)
}
}
}
func (out *CharstringReader) Rcurveline(state *Machine) error {
argCount := state.ArgStack.Top
if argCount < 8 {
return fmt.Errorf("expected at least 8 operands for <rcurveline>, got %d", argCount)
}
var i int32
curveLimit := argCount - 2
for ; i+6 <= curveLimit; i += 6 {
pt1 := out.CurrentPoint
pt1.Move(state.ArgStack.Vals[i], state.ArgStack.Vals[i+1])
pt2 := pt1
pt2.Move(state.ArgStack.Vals[i+2], state.ArgStack.Vals[i+3])
pt3 := pt2
pt3.Move(state.ArgStack.Vals[i+4], state.ArgStack.Vals[i+5])
out.curve(pt1, pt2, pt3)
}
pt1 := out.CurrentPoint
pt1.Move(state.ArgStack.Vals[i], state.ArgStack.Vals[i+1])
out.line(pt1)
return nil
}
func (out *CharstringReader) Rlinecurve(state *Machine) error {
argCount := state.ArgStack.Top
if argCount < 8 {
return fmt.Errorf("expected at least 8 operands for <rlinecurve>, got %d", argCount)
}
var i int32
lineLimit := argCount - 6
for ; i+2 <= lineLimit; i += 2 {
pt1 := out.CurrentPoint
pt1.Move(state.ArgStack.Vals[i], state.ArgStack.Vals[i+1])
out.line(pt1)
}
pt1 := out.CurrentPoint
pt1.Move(state.ArgStack.Vals[i], state.ArgStack.Vals[i+1])
pt2 := pt1
pt2.Move(state.ArgStack.Vals[i+2], state.ArgStack.Vals[i+3])
pt3 := pt2
pt3.Move(state.ArgStack.Vals[i+4], state.ArgStack.Vals[i+5])
out.curve(pt1, pt2, pt3)
return nil
}
func (out *CharstringReader) Vvcurveto(state *Machine) {
var i int32
pt1 := out.CurrentPoint
if (state.ArgStack.Top & 1) != 0 {
pt1.X += state.ArgStack.Vals[i]
i++
}
for ; i+4 <= state.ArgStack.Top; i += 4 {
pt1.Y += state.ArgStack.Vals[i]
pt2 := pt1
pt2.Move(state.ArgStack.Vals[i+1], state.ArgStack.Vals[i+2])
pt3 := pt2
pt3.Y += state.ArgStack.Vals[i+3]
out.curve(pt1, pt2, pt3)
pt1 = out.CurrentPoint
}
}
func (out *CharstringReader) Hflex(state *Machine) error {
if state.ArgStack.Top != 7 {
return fmt.Errorf("expected 7 operands for <hflex>, got %d", state.ArgStack.Top)
}
pt1 := out.CurrentPoint
pt1.X += state.ArgStack.Vals[0]
pt2 := pt1
pt2.Move(state.ArgStack.Vals[1], state.ArgStack.Vals[2])
pt3 := pt2
pt3.X += state.ArgStack.Vals[3]
pt4 := pt3
pt4.X += state.ArgStack.Vals[4]
pt5 := pt4
pt5.X += state.ArgStack.Vals[5]
pt5.Y = pt1.Y
pt6 := pt5
pt6.X += state.ArgStack.Vals[6]
out.doubleCurve(pt1, pt2, pt3, pt4, pt5, pt6)
return nil
}
func (out *CharstringReader) Flex(state *Machine) error {
if state.ArgStack.Top != 13 {
return fmt.Errorf("expected 13 operands for <flex>, got %d", state.ArgStack.Top)
}
pt1 := out.CurrentPoint
pt1.Move(state.ArgStack.Vals[0], state.ArgStack.Vals[1])
pt2 := pt1
pt2.Move(state.ArgStack.Vals[2], state.ArgStack.Vals[3])
pt3 := pt2
pt3.Move(state.ArgStack.Vals[4], state.ArgStack.Vals[5])
pt4 := pt3
pt4.Move(state.ArgStack.Vals[6], state.ArgStack.Vals[7])
pt5 := pt4
pt5.Move(state.ArgStack.Vals[8], state.ArgStack.Vals[9])
pt6 := pt5
pt6.Move(state.ArgStack.Vals[10], state.ArgStack.Vals[11])
out.doubleCurve(pt1, pt2, pt3, pt4, pt5, pt6)
return nil
}
func (out *CharstringReader) Hflex1(state *Machine) error {
if state.ArgStack.Top != 9 {
return fmt.Errorf("expected 9 operands for <hflex1>, got %d", state.ArgStack.Top)
}
pt1 := out.CurrentPoint
pt1.Move(state.ArgStack.Vals[0], state.ArgStack.Vals[1])
pt2 := pt1
pt2.Move(state.ArgStack.Vals[2], state.ArgStack.Vals[3])
pt3 := pt2
pt3.X += state.ArgStack.Vals[4]
pt4 := pt3
pt4.X += state.ArgStack.Vals[5]
pt5 := pt4
pt5.Move(state.ArgStack.Vals[6], state.ArgStack.Vals[7])
pt6 := pt5
pt6.X += state.ArgStack.Vals[8]
pt6.Y = out.CurrentPoint.Y
out.doubleCurve(pt1, pt2, pt3, pt4, pt5, pt6)
return nil
}
func (out *CharstringReader) Flex1(state *Machine) error {
if state.ArgStack.Top != 11 {
return fmt.Errorf("expected 11 operands for <flex1>, got %d", state.ArgStack.Top)
}
var d Point
for i := 0; i < 10; i += 2 {
d.Move(state.ArgStack.Vals[i], state.ArgStack.Vals[i+1])
}
pt1 := out.CurrentPoint
pt1.Move(state.ArgStack.Vals[0], state.ArgStack.Vals[1])
pt2 := pt1
pt2.Move(state.ArgStack.Vals[2], state.ArgStack.Vals[3])
pt3 := pt2
pt3.Move(state.ArgStack.Vals[4], state.ArgStack.Vals[5])
pt4 := pt3
pt4.Move(state.ArgStack.Vals[6], state.ArgStack.Vals[7])
pt5 := pt4
pt5.Move(state.ArgStack.Vals[8], state.ArgStack.Vals[9])
pt6 := pt5
if math.Abs(d.X) > math.Abs(d.Y) {
pt6.X += state.ArgStack.Vals[10]
pt6.Y = out.CurrentPoint.Y
} else {
pt6.X = out.CurrentPoint.X
pt6.Y += state.ArgStack.Vals[10]
}
out.doubleCurve(pt1, pt2, pt3, pt4, pt5, pt6)
return nil
}
@@ -0,0 +1,370 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
// Package psinterpreter implement a Postscript interpreter
// required to parse .CFF files, and Type1 and Type2 Charstrings.
// This package provides the low-level mechanisms needed to
// read such formats; the data is consumed in higher level packages,
// which implement `PsOperatorHandler`.
// It also provides helpers to interpret glyph outline descriptions,
// shared between Type1 and CFF font formats.
//
// See https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf
package psinterpreter
import (
"encoding/binary"
"errors"
"fmt"
"strconv"
)
var (
// ErrInterrupt signals the interpreter to stop early, without erroring.
ErrInterrupt = errors.New("interruption")
errInvalidCFFTable = errors.New("invalid ps instructions")
errUnsupportedRealNumberEncoding = errors.New("unsupported real number encoding")
be = binary.BigEndian
)
const (
// psArgStackSize is the argument stack size for a PostScript interpreter,
// set to 513 in CFF2
// See https://learn.microsoft.com/en-us/typography/opentype/spec/cff2#appendixD
psArgStackSize = 513
// Similarly, Appendix B says "Subr nesting, stack limit 10".
psCallStackSize = 10
maxRealNumberStrLen = 64 // Maximum length in bytes of the "-123.456E-7" representation.
)
// Context is the flavour of the Postcript language.
type Context uint32
const (
TopDict Context = iota // Top dict in CFF files
PrivateDict // Private dict in CFF files
Type2Charstring // Charstring in CFF files
Type1Charstring // Charstring in Type1 font files
)
type ArgStack struct {
Vals [psArgStackSize]float64 // we have to use float64 to properly store floats and int32 values
// Effecive size currently in use. The first value to
// pop is at index Top-1
Top int32
}
// Uint16 returns the top level value as uint16,
// without popping the stack.
func (a *ArgStack) Uint16() uint16 { return uint16(a.Vals[a.Top-1]) }
// Pop returns the top level value and decrease `Top`
// It will panic if the stack is empty.
func (a *ArgStack) Pop() float64 {
a.Top--
return a.Vals[a.Top]
}
// Clear clears the stack
func (a *ArgStack) Clear() { a.Top = 0 }
// PopN check and remove the n top levels entries.
// Passing a negative `numPop` clears all the stack.
func (a *ArgStack) PopN(numPop int32) error {
if a.Top < numPop {
return fmt.Errorf("invalid number of operands in PS stack: %d", numPop)
}
if numPop < 0 { // pop all
a.Top = 0
} else {
a.Top -= numPop
}
return nil
}
// Machine is a PostScript interpreter.
// A same interpreter may be re-used using muliples `Run` calls.
type Machine struct {
localSubrs [][]byte
globalSubrs [][]byte
instructions []byte
callStack struct {
vals [psCallStackSize][]byte // parent instructions
top int32 // effecive size currently in use
}
ArgStack ArgStack
parseNumberBuf [maxRealNumberStrLen]byte
ctx Context
}
// SkipBytes skips the next `count` bytes from the instructions, and clears the argument stack.
// It does nothing if `count` exceed the length of the instructions.
func (p *Machine) SkipBytes(count int32) {
if int(count) >= len(p.instructions) {
return
}
p.instructions = p.instructions[count:]
p.ArgStack.Clear()
}
// 5176.CFF.pdf section 4 "DICT Data" says that "Two-byte operators have an
// initial escape byte of 12".
const escapeByte = 12
// Run runs the instructions in the PostScript context asked by `handler`.
// `localSubrs` and `globalSubrs` contains the subroutines that may be called in the instructions.
func (p *Machine) Run(instructions []byte, localSubrs, globalSubrs [][]byte, handler OperatorHandler) error {
p.ctx = handler.Context()
p.instructions = instructions
p.localSubrs = localSubrs
p.globalSubrs = globalSubrs
p.ArgStack.Top = 0
p.callStack.top = 0
for len(p.instructions) > 0 {
// Push a numeric operand on the stack, if applicable.
if hasResult, err := p.parseNumber(); hasResult {
if err != nil {
return err
}
continue
}
// Otherwise, execute an operator.
b := p.instructions[0]
p.instructions = p.instructions[1:]
// check for the escape byte
escaped := b == escapeByte
if escaped {
if len(p.instructions) <= 0 {
return errInvalidCFFTable
}
b = p.instructions[0]
p.instructions = p.instructions[1:]
}
err := handler.Apply(p, Operator{Operator: b, IsEscaped: escaped})
if err == ErrInterrupt { // stop cleanly
return nil
}
if err != nil {
return err
}
}
return nil
}
// See 5176.CFF.pdf section 4 "DICT Data".
func (p *Machine) parseNumber() (hasResult bool, err error) {
number := 0.
switch b := p.instructions[0]; {
case b == 28:
if len(p.instructions) < 3 {
return true, errInvalidCFFTable
}
number, hasResult = float64(int16(be.Uint16(p.instructions[1:]))), true
p.instructions = p.instructions[3:]
case b == 29 && p.ctx != Type2Charstring:
if len(p.instructions) < 5 {
return true, errInvalidCFFTable
}
number, hasResult = float64(int32(be.Uint32(p.instructions[1:]))), true
p.instructions = p.instructions[5:]
case b == 30 && p.ctx != Type2Charstring && p.ctx != Type1Charstring:
// Parse a real number. This isn't listed in 5176.CFF.pdf Table 3
// "Operand Encoding" but that table lists integer encodings. Further
// down the page it says "A real number operand is provided in addition
// to integer operands. This operand begins with a byte value of 30
// followed by a variable-length sequence of bytes."
s := p.parseNumberBuf[:0]
p.instructions = p.instructions[1:]
loop:
for {
if len(p.instructions) == 0 {
return true, errInvalidCFFTable
}
by := p.instructions[0]
p.instructions = p.instructions[1:]
// Process by's two nibbles, high then low.
for i := 0; i < 2; i++ {
nib := by >> 4
by = by << 4
if nib == 0x0f {
f, err := strconv.ParseFloat(string(s), 32)
if err != nil {
return true, errInvalidCFFTable
}
number, hasResult = float64(f), true
break loop
}
if nib == 0x0d {
return true, errInvalidCFFTable
}
if len(s)+maxNibbleDefsLength > len(p.parseNumberBuf) {
return true, errUnsupportedRealNumberEncoding
}
s = append(s, nibbleDefs[nib]...)
}
}
case b < 32:
// not a number: no-op.
case b < 247:
p.instructions = p.instructions[1:]
number, hasResult = float64(b)-139, true
case b < 251:
if len(p.instructions) < 2 {
return true, errInvalidCFFTable
}
b1 := p.instructions[1]
p.instructions = p.instructions[2:]
number, hasResult = float64(+int32(b-247)*256+int32(b1)+108), true
case b < 255:
if len(p.instructions) < 2 {
return true, errInvalidCFFTable
}
b1 := p.instructions[1]
p.instructions = p.instructions[2:]
number, hasResult = float64(-int32(b-251)*256-int32(b1)-108), true
case b == 255 && (p.ctx == Type2Charstring || p.ctx == Type1Charstring):
if len(p.instructions) < 5 {
return true, errInvalidCFFTable
}
intValue := int32(be.Uint32(p.instructions[1:]))
if p.ctx == Type2Charstring {
// 5177.Type2.pdf section 3.2 "Charstring Number Encoding" says "If the
// charstring byte contains the value 255... [this] number is
// interpreted as a Fixed; that is, a signed number with 16 bits of
// fraction".
//
// we just round the 16.16 fixed point number to the closest integer value
number = float64(intValue) / (1 << 16)
hasResult = true
} else {
number, hasResult = float64(intValue), true
}
p.instructions = p.instructions[5:]
}
if hasResult {
if p.ArgStack.Top == psArgStackSize {
return true, errInvalidCFFTable
}
p.ArgStack.Vals[p.ArgStack.Top] = number
p.ArgStack.Top++
}
return hasResult, nil
}
const maxNibbleDefsLength = len("E-")
// nibbleDefs encodes 5176.CFF.pdf Table 5 "Nibble Definitions".
var nibbleDefs = [16]string{
0x00: "0",
0x01: "1",
0x02: "2",
0x03: "3",
0x04: "4",
0x05: "5",
0x06: "6",
0x07: "7",
0x08: "8",
0x09: "9",
0x0a: ".",
0x0b: "E",
0x0c: "E-",
0x0d: "",
0x0e: "-",
0x0f: "",
}
// subrBias returns the subroutine index bias as per 5177.Type2.pdf section 4.7
// "Subroutine Operators".
func subrBias(numSubroutines int) int32 {
if numSubroutines < 1240 {
return 107
}
if numSubroutines < 33900 {
return 1131
}
return 32768
}
// CallSubroutine calls the subroutine, identified by its index, as found
// in the instructions (that is, before applying the subroutine biased).
// `isLocal` controls whether the local or global subroutines are used.
// No argument stack modification is performed.
func (p *Machine) CallSubroutine(index int32, isLocal bool) error {
subrs := p.globalSubrs
if isLocal {
subrs = p.localSubrs
}
// no bias in type1 fonts
if p.ctx == Type2Charstring {
index += subrBias(len(subrs))
}
if index < 0 || int(index) >= len(subrs) {
return fmt.Errorf("invalid subroutine index %d (for length %d)", index, len(subrs))
}
if p.callStack.top == psCallStackSize {
return errors.New("maximum call stack size reached")
}
// save the current instructions
p.callStack.vals[p.callStack.top] = p.instructions
p.callStack.top++
// activate the subroutine
p.instructions = subrs[index]
return nil
}
// Return returns from a subroutine call.
func (p *Machine) Return() error {
if p.callStack.top <= 0 {
return errors.New("no subroutine has been called")
}
p.callStack.top--
// restore the previous instructions
p.instructions = p.callStack.vals[p.callStack.top]
return nil
}
// Operator is a postcript command, which may be escaped.
type Operator struct {
Operator byte
IsEscaped bool
}
func (p Operator) String() string {
if p.IsEscaped {
return fmt.Sprintf("2-byte operator (12 %d)", p.Operator)
}
return fmt.Sprintf("1-byte operator (%d)", p.Operator)
}
// OperatorHandler defines the behaviour of an operator.
type OperatorHandler interface {
// Context defines the precise behaviour of the interpreter,
// which has small nuances depending on the context.
Context() Context
// Apply implements the operator defined by `operator` (which is the second byte if `escaped` is true).
//
// Returning `ErrInterrupt` stop the parsing of the instructions, without reporting an error.
// It can be used as an optimization.
Apply(state *Machine, operator Operator) error
}
+709
View File
@@ -0,0 +1,709 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package cff
// code is adapted from golang.org/x/image/font/sfnt
import (
"encoding/binary"
"errors"
"fmt"
ps "github.com/go-text/typesetting/font/cff/interpreter"
"github.com/go-text/typesetting/font/opentype"
)
var errUnsupportedCFFVersion = errors.New("unsupported CFF version")
// CFF represents a parsed CFF font, as found in the 'CFF ' Opentype table.
type CFF struct {
userStrings userStrings
fdSelect fdSelect // only valid for CIDFonts
charset []uint16 // indexed by glyph ID
cidFontName string
// Charstrings contains the actual glyph definition.
// It has a length of numGlyphs and is indexed by glyph ID.
// See `LoadGlyph` for a way to intepret the glyph data.
Charstrings [][]byte
fontName []byte // name from the Name INDEX
globalSubrs [][]byte
// array of length 1 for non CIDFonts
// For CIDFonts, it can be safely indexed by `fdSelect` output
localSubrs [][][]byte
}
// Parse parses a .cff font file.
// Although CFF enables multiple font or CIDFont programs to be bundled together in a
// single file, embedded CFF font file in PDF or in TrueType/OpenType fonts
// shall consist of exactly one font or CIDFont. Thus, this function
// returns an error if the file contains more than one font.
func Parse(file []byte) (*CFF, error) {
// read 4 bytes to check if its a supported CFF file
if L := len(file); L < 4 {
return nil, fmt.Errorf("EOF: expected length: %d, got %d", 4, L)
}
if file[0] != 1 || file[1] != 0 || file[2] != 4 {
return nil, errUnsupportedCFFVersion
}
p := cffParser{src: file, offset: 4}
out, err := p.parse()
if err != nil {
return nil, err
}
if len(out) > 1 {
return nil, errors.New("only one font is allowed CFF table")
}
return &out[0], nil
}
// GlyphName returns the name of the glyph or an empty string if not found.
func (f *CFF) GlyphName(glyph opentype.GID) string {
if f.fdSelect != nil || int(glyph) >= len(f.charset) {
return ""
}
out, _ := f.userStrings.getString(f.charset[glyph])
return out
}
// since SID = 0 means .notdef, we use a reserved value
// to mean unset
const unsetSID = uint16(0xFFFF)
type userStrings [][]byte
// return either the predefined string or the user defined one
func (u userStrings) getString(sid uint16) (string, error) {
if sid == unsetSID {
return "", nil
}
if sid < 391 {
return stdStrings[sid], nil
}
sid -= 391
if int(sid) >= len(u) {
return "", fmt.Errorf("invalid glyph index %d", sid)
}
return string(u[sid]), nil
}
// Compact Font Format (CFF) fonts are written in PostScript, a stack-based
// programming language.
//
// A fundamental concept is a DICT, or a key-value map, expressed in reverse
// Polish notation. For example, this sequence of operations:
// - push the number 379
// - version operator
// - push the number 392
// - Notice operator
// - etc
// - push the number 100
// - push the number 0
// - push the number 500
// - push the number 800
// - FontBBox operator
// - etc
//
// defines a DICT that maps "version" to the String ID (SID) 379, "Notice" to
// the SID 392, "FontBBox" to the four numbers [100, 0, 500, 800], etc.
//
// The first 391 String IDs (starting at 0) are predefined as per the CFF spec
// Appendix A, in 5176.CFF.pdf referenced below. For example, 379 means
// "001.000". String ID 392 is not predefined, and is mapped by a separate
// structure, the "String INDEX", inside the CFF data. (String ID 391 is also
// not predefined. Specifically for go-opentype-testdata/data/toys/CFFTest.otf, 391 means
// "uni4E2D", as this font contains a glyph for U+4E2D).
//
// The actual glyph vectors are similarly encoded (in PostScript), in a format
// called Type 2 Charstrings. The wire encoding is similar to but not exactly
// the same as CFF's. For example, the byte 0x05 means FontBBox for CFF DICTs,
// but means rlineto (relative line-to) for Type 2 Charstrings. See
// 5176.CFF.pdf Appendix H and 5177.Type2.pdf Appendix A in the PDF files
// referenced below.
//
// The relevant specifications are:
// - http://wwwimages.adobe.com/content/dam/Adobe/en/devnet/font/pdfs/5176.CFF.pdf
// - http://wwwimages.adobe.com/content/dam/Adobe/en/devnet/font/pdfs/5177.Type2.pdf
type cffParser struct {
src []byte // whole input
offset int // current position
}
func (p *cffParser) parse() ([]CFF, error) {
// header was checked prior to this call
// Parse the Name INDEX.
fontNames, err := p.parseNames()
if err != nil {
return nil, err
}
topDicts, err := p.parseTopDicts()
if err != nil {
return nil, err
}
// 5176.CFF.pdf section 8 "Top DICT INDEX" says that the count here
// should match the count of the Name INDEX
if len(topDicts) != len(fontNames) {
return nil, fmt.Errorf("top DICT length doest not match Names (%d, %d)", len(topDicts),
len(fontNames))
}
// parse the String INDEX.
strs, err := p.parseUserStrings()
if err != nil {
return nil, err
}
out := make([]CFF, len(topDicts))
// use the strings to fetch the PSInfo
for i, topDict := range topDicts {
out[i].fontName = fontNames[i]
out[i].userStrings = strs
// skip PSInfo, and cidFontName
out[i].cidFontName, err = strs.getString(topDict.cidFontName)
if err != nil {
return nil, err
}
}
// Parse the Global Subrs [Subroutines] INDEX,
// shared among all fonts.
globalSubrs, err := p.parseIndex()
if err != nil {
return nil, err
}
for i, topDict := range topDicts {
out[i].globalSubrs = globalSubrs
// Parse the CharStrings INDEX, whose location was found in the Top DICT.
if err = p.seek(topDict.charStringsOffset); err != nil {
return nil, err
}
out[i].Charstrings, err = p.parseIndex()
if err != nil {
return nil, err
}
numGlyphs := uint16(len(out[i].Charstrings))
out[i].charset, err = p.parseCharset(topDict.charsetOffset, numGlyphs)
if err != nil {
return nil, err
}
// skip encoding
if !topDict.isCIDFont {
// Parse the Private DICT, whose location was found in the Top DICT.
var localSubrs [][]byte
localSubrs, err = p.parsePrivateDICT(topDict.privateDictOffset, topDict.privateDictLength)
if err != nil {
return nil, err
}
out[i].localSubrs = [][][]byte{localSubrs}
} else {
// Parse the Font Dict Select data, whose location was found in the Top
// DICT.
out[i].fdSelect, err = p.parseFDSelect(topDict.fdSelect, numGlyphs)
if err != nil {
return nil, err
}
indexExtent := out[i].fdSelect.extent()
// Parse the Font Dicts. Each one contains its own Private DICT.
if err = p.seek(topDict.fdArray); err != nil {
return nil, err
}
topDicts, err := p.parseTopDicts()
if err != nil {
return nil, err
}
if len(topDicts) < indexExtent {
return nil, fmt.Errorf("invalid number of font dicts: %d (for %d)",
len(topDicts), indexExtent)
}
multiSubrs := make([][][]byte, len(topDicts))
for i, topDict := range topDicts {
multiSubrs[i], err = p.parsePrivateDICT(topDict.privateDictOffset, topDict.privateDictLength)
if err != nil {
return nil, err
}
}
out[i].localSubrs = multiSubrs
}
}
return out, nil
}
func (p *cffParser) parseTopDicts() ([]topDict, error) {
// Parse the Top DICT INDEX.
instructions, err := p.parseIndex()
if err != nil {
return nil, err
}
out := make([]topDict, len(instructions)) // guarded by uint16 max size
var psi ps.Machine
for i, buf := range instructions {
topDict := &out[i]
// set default value before parsing
topDict.underlinePosition = -100
topDict.underlineThickness = 50
topDict.version = unsetSID
topDict.notice = unsetSID
topDict.fullName = unsetSID
topDict.familyName = unsetSID
topDict.weight = unsetSID
topDict.cidFontName = unsetSID
if err = psi.Run(buf, nil, nil, topDict); err != nil {
return nil, err
}
}
return out, nil
}
// src does NOT includes header, but starts at the array offset
// also returns the length read from 'src'
func parseIndexContent(src []byte, header indexStart) ([][]byte, int, error) {
if header.count == 0 {
return nil, 0, nil
}
oSize := int(header.offSize)
offsetArraySize := int(header.count+1) * oSize
if L := len(src); L < offsetArraySize {
return nil, 0, fmt.Errorf("reading INDEX offsets: EOF: expected length: %d, got %d", offsetArraySize, L)
}
out := make([][]byte, header.count)
data := src[offsetArraySize:]
prev := 0
for i := range out {
// In the same paragraph, "Therefore the first element of the offset
// array is always 1" before correcting for the off-by-1.
loc := int(bigEndian(src[(i+1)*oSize : (i+2)*oSize]))
// Locations are off by 1 byte. 5176.CFF.pdf section 5 "INDEX Data"
// says that "Offsets in the offset array are relative to the byte that
// precedes the object data... This ensures that every object has a
// corresponding offset which is always nonzero".
if loc == 0 {
return nil, 0, errors.New("invalid INDEX locations (0)")
}
loc--
if loc < prev { // Check that locations are increasing
return nil, 0, errors.New("invalid INDEX locations (not increasing)")
}
// Check that locations are in bounds, that is offsetsLength + loc <= len(src)
if int(loc) > len(data) {
return nil, 0, errors.New("invalid INDEX locations (out of bounds)")
}
out[i] = data[prev:loc]
prev = loc
}
return out, offsetArraySize + prev, nil
}
// parse the general form of an index
func (p *cffParser) parseIndex() ([][]byte, error) {
count, offSize, err := p.parseIndexHeader()
if err != nil {
return nil, err
}
out, read, err := parseIndexContent(p.src[p.offset:], indexStart{count: uint32(count), offSize: offSize})
p.offset += read
return out, err
}
// parse the Name INDEX
func (p *cffParser) parseNames() ([][]byte, error) {
return p.parseIndex()
}
// parse the String INDEX
func (p *cffParser) parseUserStrings() (userStrings, error) {
index, err := p.parseIndex()
return userStrings(index), err
}
// Parse the charset data, whose location was found in the Top DICT.
func (p *cffParser) parseCharset(charsetOffset int32, numGlyphs uint16) ([]uint16, error) {
// Predefined charset may have offset of 0 to 2 // Table 22
var charset []uint16
switch charsetOffset {
case 0: // ISOAdobe
charset = charsetISOAdobe[:]
case 1: // Expert
charset = charsetExpert[:]
case 2: // ExpertSubset
charset = charsetExpertSubset[:]
default: // custom
if err := p.seek(charsetOffset); err != nil {
return nil, err
}
buf, err := p.read(1)
if err != nil {
return nil, err
}
charset = make([]uint16, numGlyphs)
switch buf[0] { // format
case 0:
buf, err = p.read(2 * (int(numGlyphs) - 1)) // ".notdef" is omited, and has an implicit SID of 0
if err != nil {
return nil, err
}
for i := uint16(1); i < numGlyphs; i++ {
charset[i] = binary.BigEndian.Uint16(buf[2*i-2:])
}
case 1:
for i := uint16(1); i < numGlyphs; {
buf, err = p.read(3)
if err != nil {
return nil, err
}
first, nLeft := binary.BigEndian.Uint16(buf), uint16(buf[2])
for j := uint16(0); j <= nLeft && i < numGlyphs; j++ {
charset[i] = first + j
i++
}
}
case 2:
for i := uint16(1); i < numGlyphs; {
buf, err = p.read(4)
if err != nil {
return nil, err
}
first, nLeft := binary.BigEndian.Uint16(buf), binary.BigEndian.Uint16(buf[2:])
for j := uint16(0); j <= nLeft && i < numGlyphs; j++ {
charset[i] = first + j
i++
}
}
default:
return nil, fmt.Errorf("invalid custom charset format %d", buf[0])
}
}
return charset, nil
}
// parseFDSelect parses the Font Dict Select data as per 5176.CFF.pdf section
// 19 "FDSelect".
func (p *cffParser) parseFDSelect(offset int32, numGlyphs uint16) (fdSelect, error) {
if err := p.seek(offset); err != nil {
return nil, err
}
out, _, err := parseFdSelect(p.src[offset:], int(numGlyphs))
if err != nil {
return nil, err
}
return out, err
}
// Parse Private DICT and the Local Subrs [Subroutines] INDEX
func (p *cffParser) parsePrivateDICT(offset, length int32) ([][]byte, error) {
if length == 0 {
return nil, nil
}
if err := p.seek(offset); err != nil {
return nil, err
}
buf, err := p.read(int(length))
if err != nil {
return nil, err
}
var (
psi ps.Machine
priv privateDict
)
if err = psi.Run(buf, nil, nil, &priv); err != nil {
return nil, err
}
if priv.subrsOffset == 0 {
return nil, nil
}
// "The local subrs offset is relative to the beginning of the Private DICT data"
if err = p.seek(offset + priv.subrsOffset); err != nil {
return nil, errors.New("invalid local subroutines offset")
}
subrs, err := p.parseIndex()
if err != nil {
return nil, err
}
return subrs, nil
}
// read returns the n bytes from p.offset and advances p.offset by n.
func (p *cffParser) read(n int) ([]byte, error) {
if n < 0 || len(p.src) < p.offset+n {
return nil, errors.New("invalid CFF font file (EOF)")
}
out := p.src[p.offset : p.offset+n]
p.offset += n
return out, nil
}
func (p *cffParser) seek(offset int32) error {
if offset < 0 || len(p.src) < int(offset) {
return errors.New("invalid CFF font file (EOF)")
}
p.offset = int(offset)
return nil
}
func bigEndian(b []byte) uint32 {
switch len(b) {
case 1:
return uint32(b[0])
case 2:
return uint32(b[0])<<8 | uint32(b[1])
case 3:
return uint32(b[0])<<16 | uint32(b[1])<<8 | uint32(b[2])
case 4:
return uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3])
}
panic("unreachable")
}
func (p *cffParser) parseIndexHeader() (count uint16, offSize uint8, err error) {
buf, err := p.read(2)
if err != nil {
return 0, 0, err
}
count = binary.BigEndian.Uint16(buf)
// 5176.CFF.pdf section 5 "INDEX Data" says that "An empty INDEX is
// represented by a count field with a 0 value and no additional fields.
// Thus, the total size of an empty INDEX is 2 bytes".
if count == 0 {
return count, 0, nil
}
buf, err = p.read(1)
if err != nil {
return 0, 0, err
}
offSize = buf[0]
if offSize < 1 || 4 < offSize {
return 0, 0, fmt.Errorf("invalid offset size %d", offSize)
}
return count, offSize, nil
}
// topDict contains fields specific to the Top DICT context.
type topDict struct {
// SIDs, to be decoded using the string index
version, notice, fullName, familyName, weight uint16
isFixedPitch bool
italicAngle, underlinePosition, underlineThickness float32
charsetOffset int32
encodingOffset int32
charStringsOffset int32
fdArray int32
fdSelect int32
isCIDFont bool
cidFontName uint16
privateDictOffset int32
privateDictLength int32
}
func (tp *topDict) Context() ps.Context { return ps.TopDict }
func (tp *topDict) Apply(state *ps.Machine, op ps.Operator) error {
ops := topDictOperators[0]
if op.IsEscaped {
ops = topDictOperators[1]
}
if int(op.Operator) >= len(ops) {
return fmt.Errorf("invalid operator %s in Top Dict", op)
}
opFunc := ops[op.Operator]
if opFunc.run == nil {
return fmt.Errorf("invalid operator %s in Top Dict", op)
}
if state.ArgStack.Top < opFunc.numPop {
return fmt.Errorf("invalid number of arguments for operator %s in Top Dict", op)
}
err := opFunc.run(tp, state)
if err != nil {
return err
}
err = state.ArgStack.PopN(opFunc.numPop)
return err
}
// The Top DICT operators are defined by 5176.CFF.pdf Table 9 "Top DICT
// Operator Entries" and Table 10 "CIDFont Operator Extensions".
type topDictOperator struct {
// run is the function that implements the operator. Nil means that we
// ignore the operator, other than popping its arguments off the stack.
run func(*topDict, *ps.Machine) error
// numPop is the number of stack values to pop. -1 means "array" and -2
// means "delta" as per 5176.CFF.pdf Table 6 "Operand Types".
numPop int32
}
func topDictNoOp(*topDict, *ps.Machine) error { return nil }
var topDictOperators = [2][]topDictOperator{
// 1-byte operators.
{
0: {func(t *topDict, s *ps.Machine) error {
t.version = s.ArgStack.Uint16()
return nil
}, +1 /*version*/},
1: {func(t *topDict, s *ps.Machine) error {
t.notice = s.ArgStack.Uint16()
return nil
}, +1 /*Notice*/},
2: {func(t *topDict, s *ps.Machine) error {
t.fullName = s.ArgStack.Uint16()
return nil
}, +1 /*FullName*/},
3: {func(t *topDict, s *ps.Machine) error {
t.familyName = s.ArgStack.Uint16()
return nil
}, +1 /*FamilyName*/},
4: {func(t *topDict, s *ps.Machine) error {
t.weight = s.ArgStack.Uint16()
return nil
}, +1 /*Weight*/},
5: {topDictNoOp, -1 /*FontBBox*/},
13: {topDictNoOp, +1 /*UniqueID*/},
14: {topDictNoOp, -1 /*XUID*/},
15: {func(t *topDict, s *ps.Machine) error {
t.charsetOffset = int32(s.ArgStack.Vals[s.ArgStack.Top-1])
return nil
}, +1 /*charset*/},
16: {func(t *topDict, s *ps.Machine) error {
t.encodingOffset = int32(s.ArgStack.Vals[s.ArgStack.Top-1])
return nil
}, +1 /*Encoding*/},
17: {func(t *topDict, s *ps.Machine) error {
t.charStringsOffset = int32(s.ArgStack.Vals[s.ArgStack.Top-1])
return nil
}, +1 /*CharStrings*/},
18: {func(t *topDict, s *ps.Machine) error {
t.privateDictLength = int32(s.ArgStack.Vals[s.ArgStack.Top-2])
t.privateDictOffset = int32(s.ArgStack.Vals[s.ArgStack.Top-1])
return nil
}, +2 /*Private*/},
},
// 2-byte operators. The first byte is the escape byte.
{
0: {topDictNoOp, +1 /*Copyright*/},
1: {func(t *topDict, s *ps.Machine) error {
t.isFixedPitch = s.ArgStack.Vals[s.ArgStack.Top-1] == 1
return nil
}, +1 /*isFixedPitch*/},
2: {func(t *topDict, s *ps.Machine) error {
t.italicAngle = float32(s.ArgStack.Vals[s.ArgStack.Top-1])
return nil
}, +1 /*ItalicAngle*/},
3: {func(t *topDict, s *ps.Machine) error {
t.underlinePosition = float32(s.ArgStack.Vals[s.ArgStack.Top-1])
return nil
}, +1 /*UnderlinePosition*/},
4: {func(t *topDict, s *ps.Machine) error {
t.underlineThickness = float32(s.ArgStack.Vals[s.ArgStack.Top-1])
return nil
}, +1 /*UnderlineThickness*/},
5: {topDictNoOp, +1 /*PaintType*/},
6: {func(_ *topDict, i *ps.Machine) error {
if version := int(i.ArgStack.Vals[i.ArgStack.Top-1]); version != 2 {
return fmt.Errorf("charstring type %d not supported", version)
}
return nil
}, +1 /*CharstringType*/},
7: {topDictNoOp, -1 /*FontMatrix*/},
8: {topDictNoOp, +1 /*StrokeWidth*/},
20: {topDictNoOp, +1 /*SyntheticBase*/},
21: {topDictNoOp, +1 /*PostScript*/},
22: {topDictNoOp, +1 /*BaseFontName*/},
23: {topDictNoOp, -2 /*BaseFontBlend*/},
30: {func(t *topDict, _ *ps.Machine) error {
t.isCIDFont = true
return nil
}, +3 /*ROS*/},
31: {topDictNoOp, +1 /*CIDFontVersion*/},
32: {topDictNoOp, +1 /*CIDFontRevision*/},
33: {topDictNoOp, +1 /*CIDFontType*/},
34: {topDictNoOp, +1 /*CIDCount*/},
35: {topDictNoOp, +1 /*UIDBase*/},
36: {func(t *topDict, s *ps.Machine) error {
t.fdArray = int32(s.ArgStack.Vals[s.ArgStack.Top-1])
return nil
}, +1 /*FDArray*/},
37: {func(t *topDict, s *ps.Machine) error {
t.fdSelect = int32(s.ArgStack.Vals[s.ArgStack.Top-1])
return nil
}, +1 /*FDSelect*/},
38: {func(t *topDict, s *ps.Machine) error {
t.cidFontName = s.ArgStack.Uint16()
return nil
}, +1 /*FontName*/},
},
}
// privateDict contains fields specific to the Private DICT context.
type privateDict struct {
subrsOffset int32
defaultWidthX, nominalWidthX float64
}
func (privateDict) Context() ps.Context { return ps.PrivateDict }
// The Private DICT operators are defined by 5176.CFF.pdf Table 23 "Private
// DICT Operators".
func (priv *privateDict) Apply(state *ps.Machine, op ps.Operator) error {
if !op.IsEscaped { // 1-byte operators.
switch op.Operator {
case 6, 7, 8, 9: // "BlueValues" "OtherBlues" "FamilyBlues" "FamilyOtherBlues"
return state.ArgStack.PopN(-2)
case 10, 11: // "StdHW" "StdVW"
return state.ArgStack.PopN(1)
case 20: // "defaultWidthX"
if state.ArgStack.Top < 1 {
return errors.New("invalid stack size for 'defaultWidthX' in private Dict charstring")
}
priv.defaultWidthX = state.ArgStack.Vals[state.ArgStack.Top-1]
return state.ArgStack.PopN(1)
case 21: // "nominalWidthX"
if state.ArgStack.Top < 1 {
return errors.New("invalid stack size for 'nominalWidthX' in private Dict charstring")
}
priv.nominalWidthX = state.ArgStack.Vals[state.ArgStack.Top-1]
return state.ArgStack.PopN(1)
case 19: // "Subrs" pop 1
if state.ArgStack.Top < 1 {
return errors.New("invalid stack size for 'subrs' in private Dict charstring")
}
priv.subrsOffset = int32(state.ArgStack.Vals[state.ArgStack.Top-1])
return state.ArgStack.PopN(1)
}
} else { // 2-byte operators. The first byte is the escape byte.
switch op.Operator {
case 9, 10, 11, 14, 17, 18, 19: // "BlueScale" "BlueShift" "BlueFuzz" "ForceBold" "LanguageGroup" "ExpansionFactor" "initialRandomSeed"
return state.ArgStack.PopN(1)
case 12, 13: // "StemSnapH" "StemSnapV"
return state.ArgStack.PopN(-2)
}
}
return errors.New("invalid operand in private Dict charstring")
}
+891
View File
@@ -0,0 +1,891 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package font
import (
"encoding/binary"
"errors"
"sort"
"github.com/go-text/typesetting/font/opentype/tables"
)
// This file implements the logic needed to use a cmap.
var (
_ Cmap = cmap0(nil)
_ Cmap = cmap4(nil)
_ Cmap = (*cmap6or10)(nil)
_ Cmap = cmap12(nil)
_ Cmap = cmap13(nil)
_ CmapIter = (*cmap0Iter)(nil)
_ CmapIter = (*cmap4Iter)(nil)
_ CmapIter = (*cmap6Or10Iter)(nil)
_ CmapIter = (*cmap12Iter)(nil)
_ CmapIter = (*cmap13Iter)(nil)
)
// CmapIter is an iterator over a Cmap.
type CmapIter interface {
// Next returns true if the iterator still has data to yield
Next() bool
// Char must be called only when `Next` has returned `true`
Char() (rune, GID)
}
// Cmap stores a compact representation of a cmap,
// offering both on-demand rune lookup and full rune range.
// It is conceptually equivalent to a map[rune]GID, but is often
// implemented more efficiently.
type Cmap interface {
// Iter returns a new iterator over the cmap
// Multiple iterators may be used over the same cmap
// The returned interface is garanted not to be nil.
Iter() CmapIter
// Lookup avoid the construction of a map and provides
// an alternative when only few runes need to be fetched.
// It returns a default value and false when no glyph is provided.
Lookup(rune) (GID, bool)
}
// ProcessCmap sanitize the given 'cmap' subtable, and select the best encoding
// when several subtables are given.
// When present, the variation selectors are returned.
// [os2FontPage] is used for legacy arabic fonts.
//
// The returned values are copied from the input 'cmap', meaning they do not
// retain any reference on the input storage.
func ProcessCmap(cmap tables.Cmap, os2FontPage tables.FontPage) (Cmap, UnicodeVariations, error) {
var (
candidateIds []cmapID
candidates []Cmap
uv UnicodeVariations
)
for _, table := range cmap.Records {
id := cmapID{platform: table.PlatformID, encoding: table.EncodingID}
switch table := table.Subtable.(type) {
case tables.CmapSubtable0:
candidates = append(candidates, newCmap0(table))
candidateIds = append(candidateIds, id)
case tables.CmapSubtable2:
// we dont support this deprecated format
continue
case tables.CmapSubtable4:
cmap, err := newCmap4(table)
if err != nil {
return nil, nil, err
}
candidates = append(candidates, cmap)
candidateIds = append(candidateIds, id)
case tables.CmapSubtable6:
candidates = append(candidates, newCmap6(table))
candidateIds = append(candidateIds, id)
case tables.CmapSubtable10:
candidates = append(candidates, newCmap10(table))
candidateIds = append(candidateIds, id)
case tables.CmapSubtable12:
candidates = append(candidates, newCmap12(table))
candidateIds = append(candidateIds, id)
case tables.CmapSubtable13:
candidates = append(candidates, newCmap13(table))
candidateIds = append(candidateIds, id)
case tables.CmapSubtable14:
// quoting the spec :
// This subtable format must only be used under platform ID 0 and encoding ID 5.
if !(id.platform == 0 && id.encoding == 5) {
return nil, nil, errors.New("invalid cmap subtable format 14 platform or encoding")
}
uv = newUnicodeVariations(table)
}
}
// now find the best cmap, following harfbuzz/src/hb-ot-cmap-table.hh
// Prefer symbol if available.
if index := findSubtable(cmapID{tables.PlatformMicrosoft, tables.PEMicrosoftSymbolCs}, candidateIds); index != -1 {
cm := candidates[index]
switch os2FontPage {
case tables.FPNone:
cm = remaperSymbol{cm}
case tables.FPSimpArabic:
cm = remaperPUASimp{cm}
case tables.FPTradArabic:
cm = remaperPUATrad{cm}
}
return cm, uv, nil
}
/* 32-bit subtables. */
if index := findSubtable(cmapID{tables.PlatformMicrosoft, tables.PEMicrosoftUcs4}, candidateIds); index != -1 {
return candidates[index], uv, nil
}
if index := findSubtable(cmapID{tables.PlatformUnicode, tables.PEUnicodeFull13}, candidateIds); index != -1 {
return candidates[index], uv, nil
}
if index := findSubtable(cmapID{tables.PlatformUnicode, tables.PEUnicodeFull}, candidateIds); index != -1 {
return candidates[index], uv, nil
}
/* 16-bit subtables. */
if index := findSubtable(cmapID{tables.PlatformMicrosoft, tables.PEMicrosoftUnicodeCs}, candidateIds); index != -1 {
return candidates[index], uv, nil
}
if index := findSubtable(cmapID{tables.PlatformUnicode, tables.PEUnicodeBMP}, candidateIds); index != -1 {
return candidates[index], uv, nil
}
if index := findSubtable(cmapID{tables.PlatformUnicode, 2}, candidateIds); index != -1 { // deprecated
return candidates[index], uv, nil
}
if index := findSubtable(cmapID{tables.PlatformUnicode, 1}, candidateIds); index != -1 { // deprecated
return candidates[index], uv, nil
}
if index := findSubtable(cmapID{tables.PlatformUnicode, 0}, candidateIds); index != -1 { // deprecated
return candidates[index], uv, nil
}
/* MacRoman subtable. */
if index := findSubtable(cmapID{tables.PlatformMac, 0}, candidateIds); index != -1 {
cm := candidates[index]
return remaperMacroman{cm}, uv, nil
}
/* Any other Mac subtable; we just map ASCII for these. */
if index := findSubtable(cmapID{tables.PlatformMac, 0xFFFF}, candidateIds); index != -1 {
cm := candidates[index]
return remaperAscii{cm}, uv, nil
}
// uuh... fallback to the first cmap and hope for the best
if len(candidates) != 0 {
return candidates[0], uv, nil
}
return nil, nil, errors.New("unsupported cmap table")
}
// cmapID groups the platform and encoding of a Cmap subtable.
type cmapID struct {
platform tables.PlatformID
encoding tables.EncodingID
}
func (c cmapID) key(ignoreEncoding bool) uint32 {
if ignoreEncoding {
c.encoding = 0
}
return uint32(c.platform)<<16 | uint32(c.encoding)
}
// findSubtable returns the cmap index for the given platform and encoding, or -1 if not found.
// as a special case, if [id.encoding] is 0xFFFF, encoding is ignored
func findSubtable(id cmapID, cmaps []cmapID) int {
ignoreEncoding := id.encoding == 0xFFFF
key := id.key(ignoreEncoding)
// binary search
for i, j := 0, len(cmaps); i < j; {
h := i + (j-i)/2
entryKey := cmaps[h].key(ignoreEncoding)
if key < entryKey {
j = h
} else if entryKey < key {
i = h + 1
} else {
return h
}
}
return -1
}
// ---------------------------------- Format 0 ----------------------------------
// use Macintosh encoding, storing indexIntoEncoding -> glyphIndex
type cmap0 map[rune]uint8
func newCmap0(cm tables.CmapSubtable0) cmap0 {
out := make(cmap0)
for b, gid := range cm.GlyphIdArray {
if b == 0 {
continue
}
out[tables.DecodeMacintoshByte(byte(b))] = gid
}
return out
}
type cmap0Iter struct {
data cmap0
keys []rune
pos int
}
func (it *cmap0Iter) Next() bool {
return it.pos < len(it.keys)
}
func (it *cmap0Iter) Char() (rune, GID) {
r := it.keys[it.pos]
it.pos++
return r, GID(it.data[r])
}
func (s cmap0) Iter() CmapIter {
keys := make([]rune, 0, len(s))
for k := range s {
keys = append(keys, k)
}
return &cmap0Iter{data: s, keys: keys}
}
func (s cmap0) Lookup(r rune) (GID, bool) {
v, ok := s[r] // will be 0 if r is not in s
return GID(v), ok
}
// ---------------------------------- Format 4 ----------------------------------
// if indexes is nil, delta is used
type cmapEntry16 struct {
// we prefere not to keep a link to a buffer (via an offset)
// and eagerly resolve it
indexes []tables.GlyphID // length end - start + 1
end, start uint16
delta uint16 // arithmetic modulo 0xFFFF
}
type cmap4 []cmapEntry16
func newCmap4(cm tables.CmapSubtable4) (cmap4, error) {
segCount := len(cm.EndCode)
out := make(cmap4, segCount)
for i := range out {
entry := cmapEntry16{
end: cm.EndCode[i],
start: cm.StartCode[i],
delta: cm.IdDelta[i],
}
idRangeOffset := int(cm.IdRangeOffsets[i])
// some fonts use 0xFFFF for idRangeOff for the last segment
if entry.start != 0xFFFF && idRangeOffset != 0 {
// we resolve the indexes
entry.indexes = make([]tables.GlyphID, entry.end-entry.start+1)
indexStart := idRangeOffset/2 + i - segCount
if len(cm.GlyphIDArray) < 2*(indexStart+len(entry.indexes)) {
return nil, errors.New("invalid cmap subtable format 4 glyphs array length")
}
for j := range entry.indexes {
index := indexStart + j
entry.indexes[j] = tables.GlyphID(binary.BigEndian.Uint16(cm.GlyphIDArray[2*index:]))
}
}
out[i] = entry
}
return out, nil
}
type cmap4Iter struct {
data cmap4
pos1 int // into data
pos2 int // either into data[pos1].indexes or an offset between start and end
}
func (it *cmap4Iter) Next() bool {
return it.pos1 < len(it.data)
}
func (it *cmap4Iter) Char() (r rune, gy GID) {
entry := it.data[it.pos1]
if entry.indexes == nil {
r = rune(it.pos2 + int(entry.start))
gy = GID(uint16(it.pos2) + entry.start + entry.delta)
if uint16(it.pos2) == entry.end-entry.start {
// we have read the last glyph in this part
it.pos2 = 0
it.pos1++
} else {
it.pos2++
}
} else { // pos2 is the array index
r = rune(it.pos2) + rune(entry.start)
gy = GID(entry.indexes[it.pos2])
if gy != 0 {
gy += GID(entry.delta)
}
if it.pos2 == len(entry.indexes)-1 {
// we have read the last glyph in this part
it.pos2 = 0
it.pos1++
} else {
it.pos2++
}
}
return r, gy
}
func (s cmap4) Iter() CmapIter { return &cmap4Iter{data: s} }
func (s cmap4) Lookup(r rune) (GID, bool) {
if uint32(r) > 0xffff {
return 0, false
}
// binary search
c := uint16(r)
for i, j := 0, len(s); i < j; {
h := i + (j-i)/2
entry := s[h]
if c < entry.start {
j = h
} else if entry.end < c {
i = h + 1
} else if entry.indexes == nil {
return GID(c + entry.delta), true
} else {
glyph := entry.indexes[c-entry.start]
if glyph == 0 {
return 0, false
}
return GID(uint16(glyph) + entry.delta), true
}
}
return 0, false
}
// ---------------------------------- Format 6 and 10 ----------------------------------
type cmap6or10 struct {
entries []tables.GlyphID
firstCode rune
}
func newCmap6(cm tables.CmapSubtable6) cmap6or10 {
return cmap6or10{entries: cm.GlyphIdArray, firstCode: rune(cm.FirstCode)}
}
func newCmap10(cm tables.CmapSubtable10) cmap6or10 {
return cmap6or10{entries: cm.GlyphIdArray, firstCode: rune(cm.StartCharCode)}
}
type cmap6Or10Iter struct {
data cmap6or10
pos int // index into data.entries
}
func (it *cmap6Or10Iter) Next() bool {
return it.pos < len(it.data.entries)
}
func (it *cmap6Or10Iter) Char() (rune, GID) {
entry := it.data.entries[it.pos]
r := rune(it.pos) + it.data.firstCode
gy := GID(entry)
it.pos++
return r, gy
}
func (s cmap6or10) Iter() CmapIter {
return &cmap6Or10Iter{data: s}
}
func (s cmap6or10) Lookup(r rune) (GID, bool) {
if r < s.firstCode {
return 0, false
}
c := int(r - s.firstCode)
if c >= len(s.entries) {
return 0, false
}
return GID(s.entries[c]), true
}
// ---------------------------------- Format 12 ----------------------------------
type cmap12 []tables.SequentialMapGroup
func newCmap12(cm tables.CmapSubtable12) cmap12 { return cm.Groups }
type cmap12Iter struct {
data cmap12
pos1 int // into data
pos2 int // offset from start
}
func (it *cmap12Iter) Next() bool { return it.pos1 < len(it.data) }
func (it *cmap12Iter) Char() (r rune, gy GID) {
entry := it.data[it.pos1]
r = rune(it.pos2 + int(entry.StartCharCode))
gy = GID(it.pos2 + int(entry.StartGlyphID))
if uint32(it.pos2) == entry.EndCharCode-entry.StartCharCode {
// we have read the last glyph in this part
it.pos2 = 0
it.pos1++
} else {
it.pos2++
}
return r, gy
}
func (s cmap12) Iter() CmapIter { return &cmap12Iter{data: s} }
func (s cmap12) Lookup(r rune) (GID, bool) {
c := uint32(r)
// binary search
for i, j := 0, len(s); i < j; {
h := i + (j-i)/2
entry := s[h]
if c < entry.StartCharCode {
j = h
} else if entry.EndCharCode < c {
i = h + 1
} else {
return GID(c - entry.StartCharCode + entry.StartGlyphID), true
}
}
return 0, false
}
// ---------------------------------- Format 13 ----------------------------------
type cmap13 []tables.SequentialMapGroup
func newCmap13(cm tables.CmapSubtable13) cmap13 { return cm.Groups }
type cmap13Iter struct {
data cmap13
pos1 int // into data
pos2 int // offset from start
}
func (it *cmap13Iter) Next() bool {
return it.pos1 < len(it.data)
}
func (it *cmap13Iter) Char() (r rune, gy GID) {
entry := it.data[it.pos1]
r = rune(it.pos2 + int(entry.StartCharCode))
gy = GID(entry.StartGlyphID)
if uint32(it.pos2) == entry.EndCharCode-entry.StartCharCode {
// we have read the last glyph in this part
it.pos2 = 0
it.pos1++
} else {
it.pos2++
}
return r, gy
}
func (s cmap13) Iter() CmapIter { return &cmap13Iter{data: s} }
func (s cmap13) Lookup(r rune) (GID, bool) {
c := uint32(r)
// binary search
for i, j := 0, len(s); i < j; {
h := i + (j-i)/2
entry := s[h]
if c < entry.StartCharCode {
j = h
} else if entry.EndCharCode < c {
i = h + 1
} else {
return GID(entry.StartGlyphID), true
}
}
return 0, false
}
// -------------------------------- Unicode selectors --------------------------------
type unicodeRange struct {
start rune
additionalCount uint8 // 0 for a singleton range
}
type uvsMapping struct {
unicode rune
glyphID tables.GlyphID
}
type variationSelector struct {
defaultUVS []unicodeRange
nonDefaultUVS []uvsMapping
varSelector rune
}
func (vs variationSelector) getGlyph(r rune) (GID, uint8) {
// binary search
for i, j := 0, len(vs.defaultUVS); i < j; {
h := i + (j-i)/2
entry := vs.defaultUVS[h]
if r < entry.start {
j = h
} else if entry.start+rune(entry.additionalCount) < r {
i = h + 1
} else {
return 0, VariantUseDefault
}
}
for i, j := 0, len(vs.nonDefaultUVS); i < j; {
h := i + (j-i)/2
entry := vs.nonDefaultUVS[h].unicode
if r < entry {
j = h
} else if entry < r {
i = h + 1
} else {
return GID(vs.nonDefaultUVS[h].glyphID), VariantFound
}
}
return 0, VariantNotFound
}
// same as binary.BigEndian.Uint32, but for 24 bit uint
func parseUint24(b [3]byte) rune {
return rune(b[0])<<16 | rune(b[1])<<8 | rune(b[2])
}
type UnicodeVariations []variationSelector
func newUnicodeVariations(cm tables.CmapSubtable14) UnicodeVariations {
out := make([]variationSelector, len(cm.VarSelectors))
for i, sel := range cm.VarSelectors {
vs := variationSelector{
varSelector: parseUint24(sel.VarSelector),
defaultUVS: make([]unicodeRange, len(sel.DefaultUVS.Ranges)),
nonDefaultUVS: make([]uvsMapping, len(sel.NonDefaultUVS.Ranges)),
}
for i, r := range sel.DefaultUVS.Ranges {
vs.defaultUVS[i] = unicodeRange{start: parseUint24(r.StartUnicodeValue), additionalCount: r.AdditionalCount}
}
for i, r := range sel.NonDefaultUVS.Ranges {
vs.nonDefaultUVS[i] = uvsMapping{unicode: parseUint24(r.UnicodeValue), glyphID: r.GlyphID}
}
out[i] = vs
}
return out
}
const (
// VariantNotFound is returned when the font does not have a glyph for
// the given rune and selector.
VariantNotFound = iota
// VariantUseDefault is returned when the regular glyph should be used (ignoring the selector).
VariantUseDefault
// VariantFound is returned when the font has a variant for the glyph and selector.
VariantFound
)
// GetGlyphVariant returns the glyph index to used to [r] combined with [selector],
// with one of the tri-state flags [VariantNotFound, VariantUseDefault, VariantFound]
func (t UnicodeVariations) GetGlyphVariant(r, selector rune) (GID, uint8) {
// binary search
for i, j := 0, len(t); i < j; {
h := i + (j-i)/2
entryKey := t[h].varSelector
if selector < entryKey {
j = h
} else if entryKey < selector {
i = h + 1
} else {
return t[h].getGlyph(r)
}
}
return 0, VariantNotFound
}
// Handle legacy font with remap
// TODO: the Iter() and RuneRanges() method does not include the additional mapping
type remaperSymbol struct {
Cmap
}
func (rs remaperSymbol) Lookup(r rune) (GID, bool) {
// try without map first
if g, ok := rs.Cmap.Lookup(r); ok {
return g, true
}
if r <= 0x00FF {
/* For symbol-encoded OpenType fonts, we duplicate the
* U+F000..F0FF range at U+0000..U+00FF. That's what
* Windows seems to do, and that's hinted about at:
* https://docs.microsoft.com/en-us/typography/opentype/spec/recom
* under "Non-Standard (Symbol) Fonts". */
mapped := 0xF000 + r
return rs.Lookup(mapped)
}
return 0, false
}
type remaperPUASimp struct {
Cmap
}
func (rs remaperPUASimp) Lookup(r rune) (GID, bool) {
// try without map first
if g, ok := rs.Cmap.Lookup(r); ok {
return g, true
}
if mapped := puaSimpLookup(r); mapped != 0 {
return rs.Lookup(rune(mapped))
}
return 0, false
}
type remaperPUATrad struct {
Cmap
}
func (rs remaperPUATrad) Lookup(r rune) (GID, bool) {
// try without map first
if g, ok := rs.Cmap.Lookup(r); ok {
return g, true
}
if mapped := puaTradLookup(r); mapped != 0 {
return rs.Lookup(rune(mapped))
}
return 0, false
}
type remaperAscii struct {
Cmap
}
func lookupAscii(cmap Cmap, r rune) (GID, bool) {
if r < 0x80 {
return cmap.Lookup(r)
}
return 0, false
}
func (rs remaperAscii) Lookup(r rune) (GID, bool) { return lookupAscii(rs.Cmap, r) }
type remaperMacroman struct {
Cmap
}
func (rs remaperMacroman) Lookup(r rune) (GID, bool) {
if g, ok := lookupAscii(rs.Cmap, r); ok {
return g, ok
}
if mapped := unicodeToMacroman(r); mapped != 0 {
return rs.Cmap.Lookup(mapped)
}
return 0, false
}
// assume u is not in ASCII range
func unicodeToMacroman(u rune) rune {
mapping := [...]struct {
unicode uint16
macroman uint8
}{
{0x00A0, 0xCA},
{0x00A1, 0xC1},
{0x00A2, 0xA2},
{0x00A3, 0xA3},
{0x00A5, 0xB4},
{0x00A7, 0xA4},
{0x00A8, 0xAC},
{0x00A9, 0xA9},
{0x00AA, 0xBB},
{0x00AB, 0xC7},
{0x00AC, 0xC2},
{0x00AE, 0xA8},
{0x00AF, 0xF8},
{0x00B0, 0xA1},
{0x00B1, 0xB1},
{0x00B4, 0xAB},
{0x00B5, 0xB5},
{0x00B6, 0xA6},
{0x00B7, 0xE1},
{0x00B8, 0xFC},
{0x00BA, 0xBC},
{0x00BB, 0xC8},
{0x00BF, 0xC0},
{0x00C0, 0xCB},
{0x00C1, 0xE7},
{0x00C2, 0xE5},
{0x00C3, 0xCC},
{0x00C4, 0x80},
{0x00C5, 0x81},
{0x00C6, 0xAE},
{0x00C7, 0x82},
{0x00C8, 0xE9},
{0x00C9, 0x83},
{0x00CA, 0xE6},
{0x00CB, 0xE8},
{0x00CC, 0xED},
{0x00CD, 0xEA},
{0x00CE, 0xEB},
{0x00CF, 0xEC},
{0x00D1, 0x84},
{0x00D2, 0xF1},
{0x00D3, 0xEE},
{0x00D4, 0xEF},
{0x00D5, 0xCD},
{0x00D6, 0x85},
{0x00D8, 0xAF},
{0x00D9, 0xF4},
{0x00DA, 0xF2},
{0x00DB, 0xF3},
{0x00DC, 0x86},
{0x00DF, 0xA7},
{0x00E0, 0x88},
{0x00E1, 0x87},
{0x00E2, 0x89},
{0x00E3, 0x8B},
{0x00E4, 0x8A},
{0x00E5, 0x8C},
{0x00E6, 0xBE},
{0x00E7, 0x8D},
{0x00E8, 0x8F},
{0x00E9, 0x8E},
{0x00EA, 0x90},
{0x00EB, 0x91},
{0x00EC, 0x93},
{0x00ED, 0x92},
{0x00EE, 0x94},
{0x00EF, 0x95},
{0x00F1, 0x96},
{0x00F2, 0x98},
{0x00F3, 0x97},
{0x00F4, 0x99},
{0x00F5, 0x9B},
{0x00F6, 0x9A},
{0x00F7, 0xD6},
{0x00F8, 0xBF},
{0x00F9, 0x9D},
{0x00FA, 0x9C},
{0x00FB, 0x9E},
{0x00FC, 0x9F},
{0x00FF, 0xD8},
{0x0131, 0xF5},
{0x0152, 0xCE},
{0x0153, 0xCF},
{0x0178, 0xD9},
{0x0192, 0xC4},
{0x02C6, 0xF6},
{0x02C7, 0xFF},
{0x02D8, 0xF9},
{0x02D9, 0xFA},
{0x02DA, 0xFB},
{0x02DB, 0xFE},
{0x02DC, 0xF7},
{0x02DD, 0xFD},
{0x03A9, 0xBD},
{0x03C0, 0xB9},
{0x2013, 0xD0},
{0x2014, 0xD1},
{0x2018, 0xD4},
{0x2019, 0xD5},
{0x201A, 0xE2},
{0x201C, 0xD2},
{0x201D, 0xD3},
{0x201E, 0xE3},
{0x2020, 0xA0},
{0x2021, 0xE0},
{0x2022, 0xA5},
{0x2026, 0xC9},
{0x2030, 0xE4},
{0x2039, 0xDC},
{0x203A, 0xDD},
{0x2044, 0xDA},
{0x20AC, 0xDB},
{0x2122, 0xAA},
{0x2202, 0xB6},
{0x2206, 0xC6},
{0x220F, 0xB8},
{0x2211, 0xB7},
{0x221A, 0xC3},
{0x221E, 0xB0},
{0x222B, 0xBA},
{0x2248, 0xC5},
{0x2260, 0xAD},
{0x2264, 0xB2},
{0x2265, 0xB3},
{0x25CA, 0xD7},
{0xF8FF, 0xF0},
{0xFB01, 0xDE},
{0xFB02, 0xDF},
}
i := sort.Search(len(mapping), func(i int) bool { return u <= rune(mapping[i].unicode) })
if i < len(mapping) && rune(mapping[i].unicode) == u {
return rune(mapping[i].macroman)
}
return 0
}
// ---------------------------- efficent rune set support -----------------------------------------
// CmapRuneRanger is implemented by cmaps whose coverage is defined in terms
// of rune ranges
type CmapRuneRanger interface {
// RuneRanges returns a list of (start, end) rune pairs, both included.
// `dst` is an optional buffer used to reduce allocations
RuneRanges(dst [][2]rune) [][2]rune
}
var (
_ CmapRuneRanger = cmap4(nil)
_ CmapRuneRanger = (*cmap6or10)(nil)
_ CmapRuneRanger = cmap12(nil)
_ CmapRuneRanger = cmap13(nil)
)
func (cm cmap4) RuneRanges(dst [][2]rune) [][2]rune {
if cap(dst) < len(cm) {
dst = make([][2]rune, 0, len(cm))
}
dst = dst[:0]
for _, e := range cm {
start, end := rune(e.start), rune(e.end)
if L := len(dst); L != 0 && dst[L-1][1] == start {
// grow the previous range
dst[L-1][1] = end
} else {
dst = append(dst, [2]rune{start, end})
}
}
return dst
}
func (cm *cmap6or10) RuneRanges(dst [][2]rune) [][2]rune {
if cap(dst) < 1 {
dst = [][2]rune{{}}
}
dst = dst[:1]
dst[0] = [2]rune{cm.firstCode, cm.firstCode + rune(len(cm.entries)) - 1}
return dst
}
func (cm cmap12) RuneRanges(dst [][2]rune) [][2]rune {
if cap(dst) < len(cm) {
dst = make([][2]rune, 0, len(cm))
}
dst = dst[:0]
for _, e := range cm {
start, end := rune(e.StartCharCode), rune(e.EndCharCode)
if L := len(dst); L != 0 && dst[L-1][1] == start {
// grow the previous range
dst[L-1][1] = end
} else {
dst = append(dst, [2]rune{start, end})
}
}
return dst
}
func (cm cmap13) RuneRanges(dst [][2]rune) [][2]rune { return cmap12(cm).RuneRanges(dst) }
+106
View File
@@ -0,0 +1,106 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package font
// Code generated by typesettings-utils/generators/unicodedata/cmd/main.go DO NOT EDIT.
var puaSimpUint16 = [320]uint16{
0, 0, 0, 0, 0, 0, 0, 0, 61728, 61729, 61730, 0, 0, 61733, 0, 0, 61736, 61737, 61738, 61739,
61790, 61741, 61742, 61743, 61872, 61873, 61874, 61875, 61876, 61877, 61878, 61879, 61880, 61881, 61754, 61755, 0, 61757, 0, 61759,
0, 0, 0, 61787, 61788, 61789, 0, 0, 0, 0, 0, 61731, 0, 0, 0, 0, 0, 0, 0, 61732,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 61734, 0, 0, 0, 0, 0, 0, 0, 61735,
0, 0, 0, 0, 61740, 0, 0, 0, 0, 0, 0, 61755, 0, 0, 0, 61759, 0, 61869, 61765, 61763,
61883, 61767, 61882, 61761, 61770, 61865, 61772, 61774, 61777, 61780, 61783, 61784, 61785, 61786, 61792, 61794, 61796, 61798, 61800, 61801,
61802, 61806, 61810, 61696, 61696, 61696, 61696, 61696, 61791, 61813, 61816, 61818, 61820, 61822, 61921, 61860, 61861, 61868, 61864, 61895,
61896, 61899, 61892, 61893, 61898, 61897, 61894, 61696, 61696, 61696, 61696, 61696, 61696, 61696, 61696, 61696, 61696, 61696, 61696, 0,
61744, 61745, 61746, 61747, 61748, 61749, 61750, 61751, 61752, 61753, 0, 61790, 61790, 0, 0, 0, 0, 0, 0, 0,
61708, 61709, 61710, 61711, 61756, 61758, 0, 0, 0, 0, 0, 0, 0, 61765, 61766, 61763, 61764, 61883, 61883, 61767,
61768, 61882, 61871, 61870, 61870, 61761, 61762, 61770, 61770, 61769, 61769, 61865, 61866, 61772, 61772, 61771, 61771, 61774, 61774, 61773,
61773, 61777, 61776, 61775, 61775, 61780, 61779, 61778, 61778, 61783, 61782, 61781, 61781, 61784, 61784, 61785, 61785, 61786, 61786, 61792,
61792, 61794, 61794, 61793, 61793, 61796, 61796, 61795, 61795, 61798, 61798, 61797, 61797, 61800, 61800, 61799, 61799, 61801, 61801, 61801,
61801, 61802, 61802, 61802, 61802, 61806, 61805, 61803, 61804, 61810, 61809, 61807, 61808, 61813, 61813, 61811, 61812, 61816, 61816, 61814,
61815, 61818, 61818, 61817, 61817, 61820, 61820, 61819, 61819, 61822, 61822, 61821, 61821, 61921, 61921, 61823, 61823, 61860, 61859, 61857,
61858, 61861, 61861, 61868, 61867, 61864, 61863, 61862, 61862, 61888, 61889, 61886, 61887, 61890, 61891, 61885, 61884, 0, 0, 0,
}
var puaSimpUint8 = [136]uint8{
84, 86, 85, 85, 85, 85, 85, 213, 16, 34, 34, 34, 34, 34, 35, 34, 34, 34, 34, 34,
34, 34, 34, 34, 36, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 82, 16,
0, 0, 0, 0, 1, 2, 3, 4, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0,
0, 6, 0, 7, 0, 0, 8, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 11, 12, 13, 14, 15, 16, 17, 18, 19,
20, 21, 0, 0, 0, 22, 0, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39,
}
// Total size 776 B.
func puaSimpBits2(a []uint8, i int) uint8 {
return (a[i>>2] >> ((i & 3) << 1)) & 0b11
}
func puaSimpBits4(a []uint8, i int) uint8 {
return (a[i>>1] >> ((i & 1) << 2)) & 0b1111
}
func puaSimpLookup(u rune) uint16 {
if 0 <= u && u < 65277 {
return puaSimpUint16[int((int(puaSimpUint8[40+int(int((int(puaSimpBits4(puaSimpUint8[8:], int((int(puaSimpBits2(puaSimpUint8[:], int(((u>>3)>>4)>>4))))<<4)+int(((u>>3)>>4)&15))))<<4)+int((u>>3)&15))]))<<3)+int(u&7)]
} else {
return 0
}
}
var puaTradUint16 = [400]uint16{
0, 0, 0, 0, 61984, 61985, 61986, 0, 0, 61989, 0, 0, 61992, 61993, 61994, 61995, 62046, 61997, 61998, 61999,
0, 0, 62010, 62011, 0, 62013, 0, 62015, 0, 0, 0, 62043, 0, 62045, 0, 0, 0, 0, 0, 61987,
0, 0, 0, 61988, 0, 0, 0, 61990, 0, 0, 0, 61991, 61996, 0, 0, 0, 0, 0, 0, 62011,
0, 0, 0, 62015, 0, 62165, 62021, 62019, 62170, 62023, 62169, 62017, 62028, 62161, 62032, 62036, 62040, 62048, 62052, 62053,
62055, 62057, 62059, 62064, 62068, 62072, 62078, 62114, 62115, 62122, 62126, 61952, 61952, 61952, 61952, 61952, 62047, 62130, 62134, 62138,
62142, 62146, 62150, 62154, 62155, 62164, 62160, 62183, 62184, 62187, 62180, 62181, 62186, 62185, 62182, 61952, 61952, 61952, 61952, 0,
62000, 62001, 62002, 62003, 62004, 62005, 62006, 62007, 62008, 62009, 0, 62046, 62046, 0, 0, 0, 61964, 61965, 61966, 61967,
62012, 62014, 0, 0, 61954, 0, 61981, 0, 0, 0, 61955, 0, 61982, 0, 61956, 0, 0, 0, 62111, 0,
0, 0, 0, 61970, 61971, 61972, 61957, 0, 61980, 0, 0, 0, 0, 0, 61958, 0, 61983, 0, 0, 0,
0, 0, 62191, 0, 62188, 62189, 62192, 0, 0, 0, 61973, 0, 0, 62098, 0, 0, 61974, 0, 0, 62099,
0, 0, 62101, 0, 0, 61975, 0, 0, 62100, 0, 0, 0, 62080, 62081, 62082, 62102, 0, 62083, 62084, 62085,
62103, 0, 0, 0, 62106, 0, 62107, 0, 62108, 0, 0, 0, 61976, 0, 0, 0, 0, 62086, 62087, 62088,
62109, 61978, 62089, 62090, 62091, 62110, 62093, 62094, 0, 62104, 0, 0, 0, 0, 62095, 62096, 62097, 62105, 0, 0,
61977, 0, 0, 0, 0, 0, 62075, 62077, 61968, 0, 0, 0, 0, 62021, 62022, 62019, 62020, 62170, 62171, 62023,
62024, 62169, 62168, 62166, 62167, 62017, 62018, 62028, 62027, 62025, 62026, 62161, 62162, 62032, 62031, 62029, 62030, 62036, 62035, 62033,
62034, 62040, 62039, 62037, 62038, 62048, 62044, 62041, 62042, 62052, 62051, 62049, 62050, 62053, 62054, 62055, 62056, 62057, 62058, 62059,
62060, 62064, 62063, 62061, 62062, 62068, 62067, 62065, 62066, 62072, 62071, 62069, 62070, 62078, 62076, 62073, 62074, 62114, 62113, 62079,
62193, 62118, 62117, 62115, 62116, 62122, 62121, 62119, 62120, 62126, 62125, 62123, 62124, 62130, 62129, 62127, 62128, 62134, 62133, 62131,
62132, 62138, 62137, 62135, 62136, 62142, 62141, 62139, 62140, 62146, 62145, 62143, 62144, 62150, 62149, 62147, 62148, 62154, 62153, 62151,
62152, 62155, 62156, 62164, 62163, 62160, 62159, 62157, 62158, 62176, 62177, 62174, 62175, 62178, 62179, 62172, 62173, 0, 0, 0,
}
var puaTradUint8 = [328]uint8{
16, 34, 34, 34, 35, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34,
34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 66, 16, 50, 68, 68, 68, 68, 68, 68,
68, 68, 68, 68, 101, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 71, 68, 68, 68,
68, 68, 68, 68, 152, 186, 76, 77, 68, 254, 16, 50, 0, 0, 0, 0, 0, 0, 0, 0,
1, 2, 3, 4, 0, 0, 5, 6, 0, 0, 0, 0, 0, 0, 7, 8, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 10, 0,
0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 14, 15,
16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 23, 23, 29, 30, 31, 32, 33,
0, 0, 0, 0, 0, 0, 0, 34, 0, 0, 0, 35, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 36, 37, 38, 0, 0, 0, 0, 0, 0, 0, 39, 0, 0, 40, 41, 42, 0, 43,
44, 0, 0, 45, 46, 0, 47, 48, 49, 0, 0, 0, 0, 50, 0, 0, 51, 52, 0, 53,
54, 55, 56, 57, 58, 0, 0, 0, 0, 0, 59, 60, 61, 62, 63, 64, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 0, 0, 66,
0, 0, 67, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 68, 69, 70, 71,
72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91,
92, 93, 94, 95, 96, 97, 98, 99,
}
// Total size 1128 B.
func puaTradBits4(a []uint8, i int) uint8 {
return (a[i>>1] >> ((i & 1) << 2)) & 0b1111
}
func puaTradLookup(u rune) uint16 {
if 0 <= u && u < 65277 {
return puaTradUint16[int((int(puaTradUint8[72+int(int((int(puaTradBits4(puaTradUint8[32:], int((int(puaTradBits4(puaTradUint8[:], int(((u>>2)>>4)>>4))))<<4)+int(((u>>2)>>4)&15))))<<4)+int((u>>2)&15))]))<<2)+int(u&3)]
} else {
return 0
}
}
+70
View File
@@ -0,0 +1,70 @@
package font
// Code generated by typesetting-utils/generators/cache/gen.go. DO NOT EDIT.
/* Implements caches for integers key->value functions.
*
* The cache is a fixed-size array of 8-bit, 16-bit or 32-bit integers,
* typically 256 elements.
*
* The key is split into two parts: the cache index (high bits)
* and the rest (low bits).
*
* The memory layout is the following :
* KEY = <key bits - cache bits><cache bits>
* VALUE = <key bits - cache bits><value bits>
* with the constraints
* KEY in [0, 2^key bits[
* VALUE in [0, 2^value bits[
*
* The cache index is used to index into the array. The array
* member is an integer that is used BOTH
* to store the low bits of the key, and the value.
*
* The value is stored in the least significant bits of the integer.
* The low bits of the key are stored in the most significant bits
* of the integer.
*
* A cache hit is detected by comparing the low bits of the key
* with the high bits of the integer at the array position indexed
* by the high bits of the key. If they match, the value is extracted
* from the least significant bits of the integer and returned.
* Otherwise, a cache miss is reported.
*
* Cache operations (storage and retrieval) involve just a few
* arithmetic operations and a single memory access.
*/
// cache21_19_8 is a cache for integer (key, value) pairs,
// with 0 <= key < 2097152 and 0 <= value < 524288
type cache21_19_8 [1 << 8]uint32
// clear should be used as init function
func (c *cache21_19_8) clear() {
for i := range c {
c[i] = ^uint32(0)
}
}
func (c cache21_19_8) get(key uint32) (uint32, bool) {
k := key & ((1 << 8) - 1)
v := c[k]
if v == ^uint32(0) || (v>>19) != uint32(key>>8) {
return 0, false
}
return v & ((1 << 19) - 1), true
}
func (c *cache21_19_8) set(key uint32, value uint32) {
if (key>>21) != 0 || (value>>19) != 0 { /* overflows */
return
}
c.setUnchecked(key, value)
}
// assumes key < 2097152 and value < 524288
func (c *cache21_19_8) setUnchecked(key uint32, value uint32) {
k := key & ((1 << 8) - 1)
v := (uint32(key>>8) << 19) | value
c[k] = v
}
+37
View File
@@ -0,0 +1,37 @@
package font
import (
"errors"
"fmt"
"github.com/go-text/typesetting/font/opentype/tables"
)
// Support for COLR and CPAL tables
// CPAL is the 'CPAL' table,
// with [numPalettes]x[numPaletteEntries] colors.
// CPAL[0] is the default palette
type CPAL [][]tables.ColorRecord
func newCPAL(table tables.CPAL) (CPAL, error) {
numPalettes := len(table.ColorRecordIndices)
numColors := len(table.ColorRecordsArray)
// "The first palette, palette index 0, is the default palette.
// A minimum of one palette must be provided in the CPAL table if the table is present.
// Palettes must have a minimum of one color record. An empty CPAL table,
// with no palettes and no color records is not permitted."
if numPalettes == 0 {
return nil, errors.New("empty CPAL table")
}
out := make(CPAL, numPalettes)
for i, startIndex := range table.ColorRecordIndices {
endIndex := int(startIndex) + int(table.NumPaletteEntries)
if endIndex > numColors {
return nil, fmt.Errorf("invalid CPAL table (expected at least %d colors, got %d)", endIndex, numColors)
}
out[i] = table.ColorRecordsArray[startIndex:endIndex]
}
return out, nil
}
+667
View File
@@ -0,0 +1,667 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
// Package font provides an high level API to access
// Opentype font properties.
// See packages [opentype] and [opentype/tables] for a lower level, more detailled API.
package font
import (
"errors"
"fmt"
"math"
"github.com/go-text/typesetting/font/cff"
ot "github.com/go-text/typesetting/font/opentype"
"github.com/go-text/typesetting/font/opentype/tables"
)
type (
// GID is used to identify glyphs in a font.
// It is mostly internal to the font and should not be confused with
// Unicode code points.
// Note that, despite Opentype font files using uint16, we choose to use uint32,
// to allow room for future extension.
GID = ot.GID
// Tag represents an open-type name.
// These are technically uint32's, but are usually
// displayed in ASCII as they are all acronyms.
// See https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6.html#Overview
Tag = ot.Tag
// VarCoord stores font variation coordinates,
// which are real numbers in [-1;1], stored as fixed 2.14 integer.
VarCoord = tables.Coord
// Resource is a combination of io.Reader, io.Seeker and io.ReaderAt.
// This interface is satisfied by most things that you'd want
// to parse, for example *os.File, io.SectionReader or *bytes.Reader.
Resource = ot.Resource
// GlyphExtents exposes extent values, measured in font units.
// Note that height is negative in coordinate systems that grow up.
GlyphExtents = ot.GlyphExtents
)
// ParseTTF parse an Opentype font file (.otf, .ttf).
// See ParseTTC for support for collections.
func ParseTTF(file Resource) (*Face, error) {
ld, err := ot.NewLoader(file)
if err != nil {
return nil, err
}
ft, err := NewFont(ld)
if err != nil {
return nil, err
}
return NewFace(ft), nil
}
// ParseTTC parse an Opentype font file, with support for collections.
// Single font files are supported, returning a slice with length 1.
func ParseTTC(file Resource) ([]*Face, error) {
lds, err := ot.NewLoaders(file)
if err != nil {
return nil, err
}
out := make([]*Face, len(lds))
for i, ld := range lds {
ft, err := NewFont(ld)
if err != nil {
return nil, fmt.Errorf("reading font %d of collection: %s", i, err)
}
out[i] = NewFace(ft)
}
return out, nil
}
// EmptyGlyph represents an invisible glyph, which should not be drawn,
// but whose advance and offsets should still be accounted for when rendering.
const EmptyGlyph GID = math.MaxUint32
// FontExtents exposes font-wide extent values, measured in font units.
// Note that typically ascender is positive and descender negative in coordinate systems that grow up.
type FontExtents struct {
Ascender float32 // Typographic ascender.
Descender float32 // Typographic descender.
LineGap float32 // Suggested line spacing gap.
}
// LineMetric identifies one metric about the font.
type LineMetric uint8
const (
// Distance above the baseline of the top of the underline.
// Since most fonts have underline positions beneath the baseline, this value is typically negative.
UnderlinePosition LineMetric = iota
// Suggested thickness to draw for the underline.
UnderlineThickness
// Distance above the baseline of the top of the strikethrough.
StrikethroughPosition
// Suggested thickness to draw for the strikethrough.
StrikethroughThickness
SuperscriptEmYSize
SuperscriptEmXOffset
SubscriptEmYSize
SubscriptEmYOffset
SubscriptEmXOffset
CapHeight
XHeight
)
// FontID represents an identifier of a font (possibly in a collection),
// and an optional variable instance.
type FontID struct {
File string // The filename or identifier of the font file.
// The index of the face in a collection. It is always 0 for
// single font files.
Index uint16
// For variable fonts, stores 1 + the instance index.
// It is set to 0 to ignore variations, or for non variable fonts.
Instance uint16
}
// Font represents one Opentype font file (or one sub font of a collection).
// It is an educated view of the underlying font file, optimized for quick access
// to information required by text layout engines.
//
// All its methods are read-only and a [*Font] object is thus safe for concurrent use.
type Font struct {
// Cmap is the 'cmap' table
Cmap Cmap
cmapVar UnicodeVariations
hhea *tables.Hhea
vhea *tables.Vhea
vorg *tables.VORG // optional
cff *cff.CFF // optional
cff2 *cff.CFF2 // optional
post post // optional
svg svg // optional
glyf tables.Glyf
hmtx tables.Hmtx
vmtx tables.Vmtx
bitmap bitmap
sbix sbix
STAT *STAT // optional
COLR *tables.COLR1 // color glyphs, optional
CPAL CPAL // color glyphs, optional
os2 os2
names tables.Name
head tables.Head
// Optional, only present in variable fonts
fvar fvar // optional
hvar *tables.HVAR // optional
vvar *tables.VVAR // optional
avar tables.Avar
mvar mvar
gvar gvar
// Advanced layout tables.
GDEF tables.GDEF // An absent table has a nil GlyphClassDef
Trak tables.Trak
Ankr tables.Ankr
Feat tables.Feat
Ltag tables.Ltag
Morx Morx
Kern Kernx
Kerx Kernx
GSUB GSUB // An absent table has a nil slice of lookups
GPOS GPOS // An absent table has a nil slice of lookups
upem uint16 // cached value
nGlyphs int
}
// NewFont loads all the font tables, sanitizing them.
// An error is returned only when required tables 'cmap', 'head', 'maxp' are invalid (or missing).
// More control on errors is available by using package [tables].
func NewFont(ld *ot.Loader) (*Font, error) {
var (
out Font
err error
)
// 'cmap' handling depend on os2
raw, _ := ld.RawTable(ot.MustNewTag("OS/2"))
os2, _, _ := tables.ParseOs2(raw)
fontPage := os2.FontPage()
out.os2, _ = newOs2(os2)
raw, err = ld.RawTable(ot.MustNewTag("cmap"))
if err != nil {
return nil, err
}
tb, _, err := tables.ParseCmap(raw)
if err != nil {
return nil, err
}
out.Cmap, out.cmapVar, err = ProcessCmap(tb, fontPage)
if err != nil {
return nil, err
}
out.head, _, err = LoadHeadTable(ld, nil)
if err != nil {
return nil, err
}
raw, err = ld.RawTable(ot.MustNewTag("maxp"))
if err != nil {
return nil, err
}
maxp, _, err := tables.ParseMaxp(raw)
if err != nil {
return nil, err
}
out.nGlyphs = int(maxp.NumGlyphs)
// We considerer all the following tables as optional,
// since, in practice, users won't have much control on the
// font files they use
//
// Ignoring the errors on `RawTable` is OK : it will trigger an error on the next tables.ParseXXX,
// which in turn will return a zero value
raw, _ = ld.RawTable(ot.MustNewTag("fvar"))
fvar, _, _ := tables.ParseFvar(raw)
out.fvar = newFvar(fvar)
raw, _ = ld.RawTable(ot.MustNewTag("avar"))
out.avar, _, _ = tables.ParseAvar(raw)
out.upem = out.head.Upem()
raw, _ = ld.RawTable(ot.MustNewTag("glyf"))
locaRaw, _ := ld.RawTable(ot.MustNewTag("loca"))
loca, err := tables.ParseLoca(locaRaw, out.nGlyphs, out.head.IndexToLocFormat == 1)
if err == nil { // ParseGlyf panics if len(loca) == 0
out.glyf, _ = tables.ParseGlyf(raw, loca)
}
out.bitmap = selectBitmapTable(ld)
raw, _ = ld.RawTable(ot.MustNewTag("sbix"))
sbix, _, _ := tables.ParseSbix(raw, out.nGlyphs)
out.sbix = newSbix(sbix)
out.cff, _ = loadCff(ld, out.nGlyphs)
out.cff2, _ = loadCff2(ld, out.nGlyphs, len(out.fvar))
raw, _ = ld.RawTable(ot.MustNewTag("post"))
post, _, _ := tables.ParsePost(raw)
out.post, _ = newPost(post)
raw, _ = ld.RawTable(ot.MustNewTag("SVG "))
svg, _, _ := tables.ParseSVG(raw)
out.svg, _ = newSvg(svg)
raw, _ = ld.RawTable(ot.MustNewTag("COLR"))
if colr, err := tables.ParseCOLR(raw); err == nil {
out.COLR = &colr
// color table without CPAL is broken
raw, _ = ld.RawTable(ot.MustNewTag("CPAL"))
cpal, _, _ := tables.ParseCPAL(raw)
out.CPAL, err = newCPAL(cpal)
if err != nil {
return nil, err
}
}
raw, _ = ld.RawTable(ot.MustNewTag("STAT"))
stat, _, err := tables.ParseSTAT(raw)
if err == nil {
out.STAT = &stat
}
out.hhea, out.hmtx, _ = loadHmtx(ld, out.nGlyphs)
out.vhea, out.vmtx, _ = loadVmtx(ld, out.nGlyphs)
if axisCount := len(out.fvar); axisCount != 0 {
raw, _ = ld.RawTable(ot.MustNewTag("MVAR"))
mvar, _, _ := tables.ParseMVAR(raw)
out.mvar, _ = newMvar(mvar, axisCount)
raw, _ = ld.RawTable(ot.MustNewTag("gvar"))
gvar, _, _ := tables.ParseGvar(raw)
out.gvar, _ = newGvar(gvar, out.glyf)
raw, _ = ld.RawTable(ot.MustNewTag("HVAR"))
hvar, _, err := tables.ParseHVAR(raw)
if err == nil {
out.hvar = &hvar
}
raw, _ = ld.RawTable(ot.MustNewTag("VVAR"))
vvar, _, err := tables.ParseVVAR(raw)
if err == nil {
out.vvar = &vvar
}
}
raw, _ = ld.RawTable(ot.MustNewTag("VORG"))
vorg, _, err := tables.ParseVORG(raw)
if err == nil {
out.vorg = &vorg
}
raw, _ = ld.RawTable(ot.MustNewTag("name"))
out.names, _, _ = tables.ParseName(raw)
// layout tables
gsubRaw, _ := ld.RawTable(ot.MustNewTag("GSUB"))
layout, _, err := tables.ParseLayout(gsubRaw)
// harfbuzz relies on GSUB.Loookups being nil when the table is absent
if err == nil {
out.GSUB, _ = newGSUB(layout)
}
gposRaw, _ := ld.RawTable(ot.MustNewTag("GPOS"))
layout, _, err = tables.ParseLayout(gposRaw)
// harfbuzz relies on GPOS.Loookups being nil when the table is absent
if err == nil {
out.GPOS, _ = newGPOS(layout)
}
out.GDEF, _ = loadGDEF(ld, len(out.fvar), gsubRaw, gposRaw)
raw, _ = ld.RawTable(ot.MustNewTag("morx"))
morx, _, _ := tables.ParseMorx(raw, out.nGlyphs)
out.Morx = newMorx(morx)
raw, _ = ld.RawTable(ot.MustNewTag("kerx"))
kerx, _, _ := tables.ParseKerx(raw, out.nGlyphs)
out.Kerx = newKernxFromKerx(kerx)
raw, _ = ld.RawTable(ot.MustNewTag("kern"))
kern, _, _ := tables.ParseKern(raw)
out.Kern = newKernxFromKern(kern)
raw, _ = ld.RawTable(ot.MustNewTag("ankr"))
out.Ankr, _, _ = tables.ParseAnkr(raw, out.nGlyphs)
raw, _ = ld.RawTable(ot.MustNewTag("trak"))
out.Trak, _, _ = tables.ParseTrak(raw)
raw, _ = ld.RawTable(ot.MustNewTag("feat"))
out.Feat, _, _ = tables.ParseFeat(raw)
raw, _ = ld.RawTable(ot.MustNewTag("ltag"))
out.Ltag, _, _ = tables.ParseLtag(raw)
return &out, nil
}
// see harfbuzz/src/hb-ot-layout.cc
func isGDEFBlocklisted(gdef, gsub, gpos []byte) bool {
id := uint64(len(gdef))<<42 | uint64(len(gsub))<<21 | uint64(len(gpos))
switch id {
/* sha1sum:c5ee92f0bca4bfb7d06c4d03e8cf9f9cf75d2e8a Windows 7? timesi.ttf */
case 442<<42 | 2874<<21 | 42038,
/* sha1sum:37fc8c16a0894ab7b749e35579856c73c840867b Windows 7? timesbi.ttf */
430<<42 | 2874<<21 | 40662,
/* sha1sum:19fc45110ea6cd3cdd0a5faca256a3797a069a80 Windows 7 timesi.ttf */
442<<42 | 2874<<21 | 39116,
/* sha1sum:6d2d3c9ed5b7de87bc84eae0df95ee5232ecde26 Windows 7 timesbi.ttf */
430<<42 | 2874<<21 | 39374,
/* sha1sum:8583225a8b49667c077b3525333f84af08c6bcd8 OS X 10.11.3 Times New Roman Italic.ttf */
490<<42 | 3046<<21 | 41638,
/* sha1sum:ec0f5a8751845355b7c3271d11f9918a966cb8c9 OS X 10.11.3 Times New Roman Bold Italic.ttf */
478<<42 | 3046<<21 | 41902,
/* sha1sum:96eda93f7d33e79962451c6c39a6b51ee893ce8c tahoma.ttf from Windows 8 */
898<<42 | 12554<<21 | 46470,
/* sha1sum:20928dc06014e0cd120b6fc942d0c3b1a46ac2bc tahomabd.ttf from Windows 8 */
910<<42 | 12566<<21 | 47732,
/* sha1sum:4f95b7e4878f60fa3a39ca269618dfde9721a79e tahoma.ttf from Windows 8.1 */
928<<42 | 23298<<21 | 59332,
/* sha1sum:6d400781948517c3c0441ba42acb309584b73033 tahomabd.ttf from Windows 8.1 */
940<<42 | 23310<<21 | 60732,
/* tahoma.ttf v6.04 from Windows 8.1 x64, see https://bugzilla.mozilla.org/show_bug.cgi?id=1279925 */
964<<42 | 23836<<21 | 60072,
/* tahomabd.ttf v6.04 from Windows 8.1 x64, see https://bugzilla.mozilla.org/show_bug.cgi?id=1279925 */
976<<42 | 23832<<21 | 61456,
/* sha1sum:e55fa2dfe957a9f7ec26be516a0e30b0c925f846 tahoma.ttf from Windows 10 */
994<<42 | 24474<<21 | 60336,
/* sha1sum:7199385abb4c2cc81c83a151a7599b6368e92343 tahomabd.ttf from Windows 10 */
1006<<42 | 24470<<21 | 61740,
/* tahoma.ttf v6.91 from Windows 10 x64, see https://bugzilla.mozilla.org/show_bug.cgi?id=1279925 */
1006<<42 | 24576<<21 | 61346,
/* tahomabd.ttf v6.91 from Windows 10 x64, see https://bugzilla.mozilla.org/show_bug.cgi?id=1279925 */
1018<<42 | 24572<<21 | 62828,
/* sha1sum:b9c84d820c49850d3d27ec498be93955b82772b5 tahoma.ttf from Windows 10 AU */
1006<<42 | 24576<<21 | 61352,
/* sha1sum:2bdfaab28174bdadd2f3d4200a30a7ae31db79d2 tahomabd.ttf from Windows 10 AU */
1018<<42 | 24572<<21 | 62834,
/* sha1sum:b0d36cf5a2fbe746a3dd277bffc6756a820807a7 Tahoma.ttf from Mac OS X 10.9 */
832<<42 | 7324<<21 | 47162,
/* sha1sum:12fc4538e84d461771b30c18b5eb6bd434e30fba Tahoma Bold.ttf from Mac OS X 10.9 */
844<<42 | 7302<<21 | 45474,
/* sha1sum:eb8afadd28e9cf963e886b23a30b44ab4fd83acc himalaya.ttf from Windows 7 */
180<<42 | 13054<<21 | 7254,
/* sha1sum:73da7f025b238a3f737aa1fde22577a6370f77b0 himalaya.ttf from Windows 8 */
192<<42 | 12638<<21 | 7254,
/* sha1sum:6e80fd1c0b059bbee49272401583160dc1e6a427 himalaya.ttf from Windows 8.1 */
192<<42 | 12690<<21 | 7254,
/* 8d9267aea9cd2c852ecfb9f12a6e834bfaeafe44 cantarell-fonts-0.0.21/otf/Cantarell-Regular.otf */
/* 983988ff7b47439ab79aeaf9a45bd4a2c5b9d371 cantarell-fonts-0.0.21/otf/Cantarell-Oblique.otf */
188<<42 | 248<<21 | 3852,
/* 2c0c90c6f6087ffbfea76589c93113a9cbb0e75f cantarell-fonts-0.0.21/otf/Cantarell-Bold.otf */
/* 55461f5b853c6da88069ffcdf7f4dd3f8d7e3e6b cantarell-fonts-0.0.21/otf/Cantarell-Bold-Oblique.otf */
188<<42 | 264<<21 | 3426,
/* d125afa82a77a6475ac0e74e7c207914af84b37a padauk-2.80/Padauk.ttf RHEL 7.2 */
1058<<42 | 47032<<21 | 11818,
/* 0f7b80437227b90a577cc078c0216160ae61b031 padauk-2.80/Padauk-Bold.ttf RHEL 7.2*/
1046<<42 | 47030<<21 | 12600,
/* d3dde9aa0a6b7f8f6a89ef1002e9aaa11b882290 padauk-2.80/Padauk.ttf Ubuntu 16.04 */
1058<<42 | 71796<<21 | 16770,
/* 5f3c98ccccae8a953be2d122c1b3a77fd805093f padauk-2.80/Padauk-Bold.ttf Ubuntu 16.04 */
1046<<42 | 71790<<21 | 17862,
/* 6c93b63b64e8b2c93f5e824e78caca555dc887c7 padauk-2.80/Padauk-book.ttf */
1046<<42 | 71788<<21 | 17112,
/* d89b1664058359b8ec82e35d3531931125991fb9 padauk-2.80/Padauk-bookbold.ttf */
1058<<42 | 71794<<21 | 17514,
/* 824cfd193aaf6234b2b4dc0cf3c6ef576c0d00ef padauk-3.0/Padauk-book.ttf */
1330<<42 | 109904<<21 | 57938,
/* 91fcc10cf15e012d27571e075b3b4dfe31754a8a padauk-3.0/Padauk-bookbold.ttf */
1330<<42 | 109904<<21 | 58972,
/* sha1sum: c26e41d567ed821bed997e937bc0c41435689e85 Padauk.ttf
* "Padauk Regular" "Version 2.5", see https://crbug.com/681813 */
1004<<42 | 59092<<21 | 14836,
/* 88d2006ca084f04af2df1954ed714a8c71e8400f Courier New.ttf from macOS 15 */
588<<42 | 5078<<21 | 14418,
/* 608e3ebb6dd1aee521cff08eb07d500a2c59df68 Courier New Bold.ttf from macOS 15 */
588<<42 | 5078<<21 | 14238,
/* d13221044ff054efd78f1cd8631b853c3ce85676 cour.ttf from Windows 10 */
894<<42 | 17162<<21 | 33960,
/* 68ed4a22d8067fcf1622ac6f6e2f4d3a2e3ec394 courbd.ttf from Windows 10 */
894<<42 | 17154<<21 | 34472,
/* 4cdb0259c96b7fd7c103821bb8f08f7cc6b211d7 cour.ttf from Windows 8.1 */
816<<42 | 7868<<21 | 17052,
/* 920483d8a8ed37f7f0afdabbe7f679aece7c75d8 courbd.ttf from Windows 8.1 */
816<<42 | 7868<<21 | 17138:
return true
}
return false
}
var bhedTag = ot.MustNewTag("bhed")
// LoadHeadTable loads the 'head' or the 'bhed' table.
//
// If a 'bhed' Apple table is present, it replaces the 'head' one.
//
// [buffer] may be provided to reduce allocations; the returned [tables.Head] is guaranteed
// not to retain any reference on [buffer].
// If [buffer] is nil or has not enough capacity, a new slice is allocated (and returned).
func LoadHeadTable(ld *ot.Loader, buffer []byte) (tables.Head, []byte, error) {
var err error
// check 'bhed' first
if ld.HasTable(bhedTag) {
buffer, err = ld.RawTableTo(bhedTag, buffer)
} else {
buffer, err = ld.RawTableTo(ot.MustNewTag("head"), buffer)
}
if err != nil {
return tables.Head{}, nil, errors.New("missing required head (or bhed) table")
}
out, _, err := tables.ParseHead(buffer)
return out, buffer, err
}
// return nil if no table is valid (or present)
func selectBitmapTable(ld *ot.Loader) bitmap {
color, err := loadBitmap(ld, ot.MustNewTag("CBLC"), ot.MustNewTag("CBDT"))
if err == nil {
return color
}
gray, err := loadBitmap(ld, ot.MustNewTag("EBLC"), ot.MustNewTag("EBDT"))
if err == nil {
return gray
}
apple, err := loadBitmap(ld, ot.MustNewTag("bloc"), ot.MustNewTag("bdat"))
if err == nil {
return apple
}
return nil
}
// return nil if the table is missing or invalid
func loadCff(ld *ot.Loader, numGlyphs int) (*cff.CFF, error) {
raw, err := ld.RawTable(ot.MustNewTag("CFF "))
if err != nil {
return nil, err
}
cff, err := cff.Parse(raw)
if err != nil {
return nil, err
}
if N := len(cff.Charstrings); N != numGlyphs {
return nil, fmt.Errorf("invalid number of glyphs in CFF table (%d != %d)", N, numGlyphs)
}
return cff, nil
}
// return nil if the table is missing or invalid
func loadCff2(ld *ot.Loader, numGlyphs, axisCount int) (*cff.CFF2, error) {
raw, err := ld.RawTable(ot.MustNewTag("CFF2"))
if err != nil {
return nil, err
}
cff2, err := cff.ParseCFF2(raw)
if err != nil {
return nil, err
}
if N := len(cff2.Charstrings); N != numGlyphs {
return nil, fmt.Errorf("invalid number of glyphs in CFF table (%d != %d)", N, numGlyphs)
}
if got := cff2.VarStore.AxisCount(); got != -1 && got != axisCount {
return nil, fmt.Errorf("invalid number of axis in CFF table (%d != %d)", got, axisCount)
}
return cff2, nil
}
func loadHVtmx(hheaRaw, htmxRaw []byte, numGlyphs int) (*tables.Hhea, tables.Hmtx, error) {
hhea, _, err := tables.ParseHhea(hheaRaw)
if err != nil {
return nil, tables.Hmtx{}, err
}
hmtx, _, err := tables.ParseHmtx(htmxRaw, int(hhea.NumOfLongMetrics), numGlyphs-int(hhea.NumOfLongMetrics))
if err != nil {
return nil, tables.Hmtx{}, err
}
return &hhea, hmtx, nil
}
func loadHmtx(ld *ot.Loader, numGlyphs int) (*tables.Hhea, tables.Hmtx, error) {
rawHead, err := ld.RawTable(ot.MustNewTag("hhea"))
if err != nil {
return nil, tables.Hmtx{}, err
}
rawMetrics, err := ld.RawTable(ot.MustNewTag("hmtx"))
if err != nil {
return nil, tables.Hmtx{}, err
}
return loadHVtmx(rawHead, rawMetrics, numGlyphs)
}
func loadVmtx(ld *ot.Loader, numGlyphs int) (*tables.Hhea, tables.Hmtx, error) {
rawHead, err := ld.RawTable(ot.MustNewTag("vhea"))
if err != nil {
return nil, tables.Hmtx{}, err
}
rawMetrics, err := ld.RawTable(ot.MustNewTag("vmtx"))
if err != nil {
return nil, tables.Hmtx{}, err
}
return loadHVtmx(rawHead, rawMetrics, numGlyphs)
}
func loadGDEF(ld *ot.Loader, axisCount int, gsub, gpos []byte) (tables.GDEF, error) {
raw, err := ld.RawTable(ot.MustNewTag("GDEF"))
if err != nil {
return tables.GDEF{}, err
}
// Nuke the GDEF tables of to avoid unwanted width-zeroing.
if isGDEFBlocklisted(raw, gsub, gpos) {
return tables.GDEF{}, nil
}
GDEF, _, err := tables.ParseGDEF(raw)
if err != nil {
return tables.GDEF{}, err
}
err = sanitizeGDEF(GDEF, axisCount)
if err != nil {
return tables.GDEF{}, err
}
return GDEF, nil
}
// Face is a font with user-provided settings.
// Contrary to the [*Font] objects, Faces are NOT safe for concurrent use.
// A Face caches glyph extents and rune to glyph mapping, and should be reused when possible.
//
// Also note that an empty [Face] is invalid : the [NewFace] constructor is required to properly init caches.
type Face struct {
*Font
extentsCache extentsCache
cmapCache cache21_19_8
coords []tables.Coord
xPpem, yPpem uint16
}
// NewFace wraps [font] and initializes glyph caches.
func NewFace(font *Font) *Face {
out := &Face{Font: font, extentsCache: make(extentsCache, font.nGlyphs)}
out.cmapCache.clear()
return out
}
// NominalGlyph returns the glyph used to represent the given rune,
// or false if not found.
// Note that it only looks into the cmap, without taking account substitutions
// nor variation selectors.
func (f *Face) NominalGlyph(ch rune) (GID, bool) {
if g, ok := f.cmapCache.get(uint32(ch)); ok {
return GID(g), ok
}
g, ok := f.Cmap.Lookup(ch)
if ok {
f.cmapCache.set(uint32(ch), uint32(g))
}
return g, ok
}
// Ppem returns the horizontal and vertical pixels-per-em (ppem), used to select bitmap sizes.
func (f *Face) Ppem() (x, y uint16) { return f.xPpem, f.yPpem }
// SetPpem applies horizontal and vertical pixels-per-em (ppem).
func (f *Face) SetPpem(x, y uint16) {
f.xPpem, f.yPpem = x, y
// invalid the cache
f.extentsCache.reset()
}
// Coords return a read-only slice of the current variable coordinates, expressed in normalized units.
// It is empty for non variable fonts.
func (f *Face) Coords() []tables.Coord { return f.coords }
// SetCoords applies a list of variation coordinates, expressed in normalized units.
// Use [NormalizeVariations] to convert from design (user) space units.
func (f *Face) SetCoords(coords []tables.Coord) {
f.coords = coords
// invalid the cache
f.extentsCache.reset()
}
+367
View File
@@ -0,0 +1,367 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package font
import (
"bytes"
"encoding/binary"
"fmt"
"image"
"image/jpeg"
"image/png"
ot "github.com/go-text/typesetting/font/opentype"
"github.com/go-text/typesetting/font/opentype/tables"
"golang.org/x/image/tiff"
)
type contourPoint struct {
SegmentPoint
isOnCurve bool
isEndPoint bool // this point is the last of the current contour
isExplicit bool // this point is referenced, i.e., explicit deltas specified */
}
func (c *contourPoint) translate(x, y float32) {
c.X += x
c.Y += y
}
func (c *contourPoint) transform(matrix [4]float32) {
px := c.X*matrix[0] + c.Y*matrix[2]
c.Y = c.X*matrix[1] + c.Y*matrix[3]
c.X = px
}
const (
phantomLeft = iota
phantomRight
phantomTop
phantomBottom
phantomCount
)
type glyphSet map[tables.GlyphID]struct{}
func (f *Face) getPointsForGlyph(gid tables.GlyphID) []contourPoint {
var out []contourPoint
seenGlyphs := make(glyphSet) // used to deny loops
f.getPointsForGlyphRec(gid, 0, seenGlyphs, &out)
return out
}
const maxCompositeNesting = 20 // protect against malicious fonts
// use the `glyf` table to fetch the contour points,
// applying variation if needed.
// for composite, recursively calls itself; allPoints includes phantom points and will be at least of length 4
func (f *Face) getPointsForGlyphRec(gid tables.GlyphID, currentDepth int, currentGlyphs glyphSet, allPoints *[]contourPoint /* OUT */) {
// adapted from harfbuzz/src/OT/glyf/Glyph.hh
if currentDepth > maxCompositeNesting || int(gid) >= len(f.glyf) {
return
}
g := f.glyf[gid]
var points []contourPoint
if data, ok := g.Data.(tables.SimpleGlyph); ok {
points = getContourPoints(data) // fetch the "real" points
} else { // zeros values are enough
points = make([]contourPoint, pointNumbersCount(g))
}
// init phantom point
points = append(points, make([]contourPoint, phantomCount)...)
phantoms := points[len(points)-phantomCount:]
hDelta := float32(g.XMin - f.hmtx.SideBearing(gid))
vOrig := float32(g.YMax + f.vmtx.SideBearing(gid))
hAdv := float32(f.getBaseAdvance(gid, f.hmtx, false))
vAdv := float32(f.getBaseAdvance(gid, f.vmtx, true))
phantoms[phantomLeft].X = hDelta
phantoms[phantomRight].X = hAdv + hDelta
phantoms[phantomTop].Y = vOrig
phantoms[phantomBottom].Y = vOrig - vAdv
if f.isVar() {
f.gvar.applyDeltasToPoints(gid, f.coords, points)
}
switch data := g.Data.(type) {
case tables.SimpleGlyph:
*allPoints = append(*allPoints, points...)
case tables.CompositeGlyph:
for compIndex, item := range data.Glyphs {
if _, has := currentGlyphs[item.GlyphIndex]; has {
continue
}
currentGlyphs[item.GlyphIndex] = struct{}{}
// recurse on component
var compPoints []contourPoint
f.getPointsForGlyphRec(item.GlyphIndex, currentDepth+1, currentGlyphs, &compPoints)
LC := len(compPoints)
if LC < phantomCount { // in case of max depth reached
delete(currentGlyphs, item.GlyphIndex)
return
}
/* Copy phantom points from component if USE_MY_METRICS flag set */
if item.HasUseMyMetrics() {
copy(phantoms, compPoints[LC-phantomCount:])
}
/* Apply component transformation & translation */
transformPoints(&item, compPoints)
/* Apply translation from gvar */
tx, ty := points[compIndex].X, points[compIndex].Y
for i := range compPoints {
compPoints[i].translate(tx, ty)
}
if item.IsAnchored() {
p1, p2 := item.ArgsAsIndices()
if p1 < len(*allPoints) && p2 < LC {
tx, ty := (*allPoints)[p1].X-compPoints[p2].X, (*allPoints)[p1].Y-compPoints[p2].Y
for i := range compPoints {
compPoints[i].translate(tx, ty)
}
}
}
*allPoints = append(*allPoints, compPoints[0:LC-phantomCount]...)
delete(currentGlyphs, item.GlyphIndex)
}
*allPoints = append(*allPoints, phantoms...)
default: // no data for the glyph
*allPoints = append(*allPoints, phantoms...)
}
// apply at top level
if currentDepth == 0 {
/* Undocumented rasterizer behavior:
* Shift points horizontally by the updated left side bearing */
tx := -phantoms[phantomLeft].X
for i := range *allPoints {
(*allPoints)[i].translate(tx, 0)
}
}
}
// does not includes phantom points
func pointNumbersCount(g tables.Glyph) int {
switch g := g.Data.(type) {
case tables.SimpleGlyph:
return len(g.Points)
case tables.CompositeGlyph:
/* pseudo component points for each component in composite glyph */
return len(g.Glyphs)
}
return 0
}
// return all the contour points, without phantoms
func getContourPoints(sg tables.SimpleGlyph) []contourPoint {
const flagOnCurve = 1 << 0 // 0x0001
points := make([]contourPoint, len(sg.Points))
for _, end := range sg.EndPtsOfContours {
points[end].isEndPoint = true
}
for i, p := range sg.Points {
points[i].X, points[i].Y = float32(p.X), float32(p.Y)
points[i].isOnCurve = p.Flag&flagOnCurve != 0
}
return points
}
func extentsFromPoints(allPoints []contourPoint) (ext GlyphExtents) {
truePoints := allPoints[:len(allPoints)-phantomCount]
if len(truePoints) == 0 {
// zero extent for the empty glyph
return ext
}
minX, minY := truePoints[0].X, truePoints[0].Y
maxX, maxY := minX, minY
for _, p := range truePoints {
minX = minF(minX, p.X)
minY = minF(minY, p.Y)
maxX = maxF(maxX, p.X)
maxY = maxF(maxY, p.Y)
}
ext.XBearing = minX
ext.YBearing = maxY
ext.Width = maxX - minX
ext.Height = minY - maxY
return ext
}
// walk through the contour points of the given glyph to compute its extends and its phantom points
// As an optimization, if `computeExtents` is false, the extents computation is skipped (a zero value is returned).
func (f *Face) getGlyfPoints(gid tables.GlyphID, computeExtents bool) (ext GlyphExtents, ph [phantomCount]contourPoint) {
if int(gid) >= len(f.glyf) {
return
}
allPoints := f.getPointsForGlyph(gid)
copy(ph[:], allPoints[len(allPoints)-phantomCount:])
if computeExtents {
ext = extentsFromPoints(allPoints)
}
return ext, ph
}
func min16(a, b int16) int16 {
if a < b {
return a
}
return b
}
func max16(a, b int16) int16 {
if a > b {
return a
}
return b
}
func minC(a, b VarCoord) VarCoord {
if a < b {
return a
}
return b
}
func maxC(a, b VarCoord) VarCoord {
if a > b {
return a
}
return b
}
func minF(a, b float32) float32 {
if a < b {
return a
}
return b
}
func maxF(a, b float32) float32 {
if a > b {
return a
}
return b
}
func transformPoints(c *tables.CompositeGlyphPart, points []contourPoint) {
var transX, transY float32
if !c.IsAnchored() {
arg1, arg2 := c.ArgsAsTranslation()
transX, transY = float32(arg1), float32(arg2)
}
scale := c.Scale
// shortcut identity transform
if transX == 0 && transY == 0 && scale == [4]float32{1, 0, 0, 1} {
return
}
if c.IsScaledOffsets() {
for i := range points {
points[i].translate(transX, transY)
points[i].transform(scale)
}
} else {
for i := range points {
points[i].transform(scale)
points[i].translate(transX, transY)
}
}
}
func getGlyphExtents(g tables.Glyph, metrics tables.Hmtx, gid gID) GlyphExtents {
var extents GlyphExtents
/* Undocumented rasterizer behavior: shift glyph to the left by (lsb - xMin), i.e., xMin = lsb */
/* extents.XBearing = hb_min (glyph_header.xMin, glyph_header.xMax); */
extents.XBearing = float32(metrics.SideBearing(gid))
extents.YBearing = float32(max16(g.YMin, g.YMax))
extents.Width = float32(max16(g.XMin, g.XMax) - min16(g.XMin, g.XMax))
extents.Height = float32(min16(g.YMin, g.YMax) - max16(g.YMin, g.YMax))
return extents
}
// sbix
var (
dupe = ot.MustNewTag("dupe")
// tagPNG identifies bitmap glyph with png format
tagPNG = ot.MustNewTag("png ")
// tagTIFF identifies bitmap glyph with tiff format
tagTIFF = ot.MustNewTag("tiff")
// tagJPG identifies bitmap glyph with jpg format
tagJPG = ot.MustNewTag("jpg ")
)
// strikeGlyph return the data for [glyph], or a zero value if not found.
func strikeGlyph(b *tables.Strike, glyph gID, recursionLevel int) tables.BitmapGlyphData {
const maxRecursionLevel = 8
if int(glyph) >= len(b.GlyphDatas) {
return tables.BitmapGlyphData{}
}
out := b.GlyphDatas[glyph]
if out.GraphicType == dupe {
if len(out.Data) < 2 || recursionLevel > maxRecursionLevel {
return tables.BitmapGlyphData{}
}
glyph = gID(binary.BigEndian.Uint16(out.Data))
return strikeGlyph(b, glyph, recursionLevel+1)
}
return out
}
// decodeBitmapConfig parse the data to find the width and height
func decodeBitmapConfig(b tables.BitmapGlyphData) (width, height int, format BitmapFormat, err error) {
var config image.Config
switch b.GraphicType {
case tagPNG:
format = PNG
config, err = png.DecodeConfig(bytes.NewReader(b.Data))
case tagTIFF:
format = TIFF
config, err = tiff.DecodeConfig(bytes.NewReader(b.Data))
case tagJPG:
format = JPG
config, err = jpeg.DecodeConfig(bytes.NewReader(b.Data))
default:
err = fmt.Errorf("unsupported graphic type in sbix table: %s", b.GraphicType)
}
if err != nil {
return 0, 0, 0, err
}
return config.Width, config.Height, format, nil
}
// return the extents computed from the data
// should only be called on valid, non nil glyph data
func bitmapGlyphExtents(b tables.BitmapGlyphData) (out GlyphExtents, ok bool) {
width, height, _, err := decodeBitmapConfig(b)
if err != nil {
return out, false
}
out.XBearing = float32(b.OriginOffsetX)
out.YBearing = float32(height) + float32(b.OriginOffsetY)
out.Width = float32(width)
out.Height = -float32(height)
return out, true
}
+455
View File
@@ -0,0 +1,455 @@
package font
import (
"strings"
ot "github.com/go-text/typesetting/font/opentype"
"github.com/go-text/typesetting/font/opentype/tables"
)
// name values corresponding to the xxxConsts arrays
var (
styleStrings [len(styleConsts)]string
weightStrings [len(weightConsts)]string
stretchStrings [len(stretchConsts)]string
)
func init() {
for i, v := range styleConsts {
styleStrings[i] = v.name
}
for i, v := range weightConsts {
weightStrings[i] = v.name
}
for i, v := range stretchConsts {
stretchStrings[i] = v.name
}
}
var styleConsts = [...]struct {
name string
value Style
}{
{"italic", StyleItalic},
{"kursiv", StyleItalic},
{"oblique", StyleItalic}, // map Oblique to Italic
}
var weightConsts = [...]struct {
name string
value Weight
}{
{"thin", WeightThin},
{"extralight", WeightExtraLight},
{"ultralight", WeightExtraLight},
{"light", WeightLight},
{"demilight", (WeightLight + WeightNormal) / 2},
{"semilight", (WeightLight + WeightNormal) / 2},
{"book", WeightNormal - 20},
{"regular", WeightNormal},
{"normal", WeightNormal},
{"medium", WeightMedium},
{"demibold", WeightSemibold},
{"demi", WeightSemibold},
{"semibold", WeightSemibold},
{"extrabold", WeightExtraBold},
{"superbold", WeightExtraBold},
{"ultrabold", WeightExtraBold},
{"bold", WeightBold},
{"ultrablack", WeightBlack + 20},
{"superblack", WeightBlack + 20},
{"extrablack", WeightBlack + 20},
{"black", WeightBlack},
{"heavy", WeightBlack},
}
var stretchConsts = [...]struct {
name string
value Stretch
}{
{"ultracondensed", StretchUltraCondensed},
{"extracondensed", StretchExtraCondensed},
{"semicondensed", StretchSemiCondensed},
{"condensed", StretchCondensed},
{"normal", StretchNormal},
{"semiexpanded", StretchSemiExpanded},
{"extraexpanded", StretchExtraExpanded},
{"ultraexpanded", StretchUltraExpanded},
{"expanded", StretchExpanded},
{"extended", StretchExpanded},
}
// Style (also called slant) allows italic or oblique faces to be selected.
type Style uint8
// note that we use the 0 value to indicate no style has been found yet
const (
// A face that is neither italic not obliqued.
StyleNormal Style = iota + 1
// A form that is generally cursive in nature or slanted.
// This groups what is usually called Italic or Oblique.
StyleItalic
)
// Weight is the degree of blackness or stroke thickness of a font.
// This value ranges from 100.0 to 900.0, with 400.0 as normal.
type Weight float32
const (
// Thin weight (100), the thinnest value.
WeightThin Weight = 100
// Extra light weight (200).
WeightExtraLight Weight = 200
// Light weight (300).
WeightLight Weight = 300
// Normal (400).
WeightNormal Weight = 400
// Medium weight (500, higher than normal).
WeightMedium Weight = 500
// Semibold weight (600).
WeightSemibold Weight = 600
// Bold weight (700).
WeightBold Weight = 700
// Extra-bold weight (800).
WeightExtraBold Weight = 800
// Black weight (900), the thickest value.
WeightBlack Weight = 900
)
// Stretch is the width of a font as an approximate fraction of the normal width.
// Widths range from 0.5 to 2.0 inclusive, with 1.0 as the normal width.
type Stretch float32
const (
// Ultra-condensed width (50%), the narrowest possible.
StretchUltraCondensed Stretch = 0.5
// Extra-condensed width (62.5%).
StretchExtraCondensed Stretch = 0.625
// Condensed width (75%).
StretchCondensed Stretch = 0.75
// Semi-condensed width (87.5%).
StretchSemiCondensed Stretch = 0.875
// Normal width (100%).
StretchNormal Stretch = 1.0
// Semi-expanded width (112.5%).
StretchSemiExpanded Stretch = 1.125
// Expanded width (125%).
StretchExpanded Stretch = 1.25
// Extra-expanded width (150%).
StretchExtraExpanded Stretch = 1.5
// Ultra-expanded width (200%), the widest possible.
StretchUltraExpanded Stretch = 2.0
)
// Aspect stores the properties that specify which font in a family to use:
// style, weight, and stretchiness.
type Aspect struct {
Style Style
Weight Weight
Stretch Stretch
}
// aspect returns the [aspect] of the font,
// defaulting to regular style.
func (fd *fontDescriptor) aspect() Aspect {
// use rawAspect and additionalStyle to infer the Aspect
out := fd.rawAspect() // load the aspect properties ...
// ... try to fill the missing one with the "style"
out.inferFromStyle(fd.additionalStyle())
// ... and finally add default to regular values :
// StyleNormal, WeightNormal, StretchNormal
out.SetDefaults()
return out
}
// some fonts includes aspect information in a string description,
// usually called "style"
// inferFromStyle scans such a string and fills the missing fields,
func (as *Aspect) inferFromStyle(additionalStyle string) {
additionalStyle = NormalizeFamily(additionalStyle)
if as.Style == 0 {
if index := stringContainsConst(additionalStyle, styleStrings[:]); index != -1 {
as.Style = styleConsts[index].value
}
}
if as.Weight == 0 {
if index := stringContainsConst(additionalStyle, weightStrings[:]); index != -1 {
as.Weight = weightConsts[index].value
}
}
if as.Stretch == 0 {
if index := stringContainsConst(additionalStyle, stretchStrings[:]); index != -1 {
as.Stretch = stretchConsts[index].value
}
}
}
// SetDefaults replace unspecified values by the default values: StyleNormal, WeightNormal, StretchNormal
func (as *Aspect) SetDefaults() {
if as.Style == 0 {
as.Style = StyleNormal
}
if as.Stretch == 0 {
as.Stretch = StretchNormal
}
if as.Weight == 0 {
as.Weight = WeightNormal
}
}
func (fd *fontDescriptor) additionalStyle() string {
var style string
if fd.os2 != nil && fd.os2.fsSelection&256 != 0 {
style = fd.names.Name(namePreferredSubfamily)
if style == "" {
style = fd.names.Name(nameFontSubfamily)
}
} else {
style = fd.names.Name(nameWWSSubfamily)
if style == "" {
style = fd.names.Name(namePreferredSubfamily)
}
if style == "" {
style = fd.names.Name(nameFontSubfamily)
}
}
style = strings.TrimSpace(style)
return style
}
func (fd *fontDescriptor) rawAspect() Aspect {
var (
style Style
weight Weight
stretch Stretch
)
if fd.os2 != nil {
// We have an OS/2 table; use the `fsSelection' field. Bit 9
// indicates an oblique font face. This flag has been
// introduced in version 1.5 of the OpenType specification.
if fd.os2.fsSelection&(1<<9) != 0 || fd.os2.fsSelection&1 != 0 {
style = StyleItalic
}
weight = Weight(fd.os2.usWeightClass)
switch fd.os2.usWidthClass {
case 1:
stretch = StretchUltraCondensed
case 2:
stretch = StretchExtraCondensed
case 3:
stretch = StretchCondensed
case 4:
stretch = StretchSemiCondensed
case 5:
stretch = StretchNormal
case 6:
stretch = StretchSemiExpanded
case 7:
stretch = StretchExpanded
case 8:
stretch = StretchExtraExpanded
case 9:
stretch = StretchUltraExpanded
}
} else {
// this is an old Mac font, use the header field
if isItalic := fd.head.MacStyle&2 != 0; isItalic {
style = StyleItalic
}
if isBold := fd.head.MacStyle&1 != 0; isBold {
weight = WeightBold
}
}
return Aspect{style, weight, stretch}
}
var rp = strings.NewReplacer(" ", "", "\t", "")
// NormalizeFamily removes spaces and lower the given string.
func NormalizeFamily(family string) string { return rp.Replace(strings.ToLower(family)) }
// returns the index in `constants` of a constant contained in `str`,
// or -1
func stringContainsConst(str string, constants []string) int {
for i, c := range constants {
if strings.Contains(str, c) {
return i
}
}
return -1
}
const (
nameFontFamily tables.NameID = 1
nameFontSubfamily tables.NameID = 2
namePreferredFamily tables.NameID = 16 // or Typographic Family
namePreferredSubfamily tables.NameID = 17 // or Typographic Subfamily
nameWWSFamily tables.NameID = 21 //
nameWWSSubfamily tables.NameID = 22 //
)
type os2Desc struct {
usWeightClass uint16
usWidthClass uint16
fsSelection uint16
}
func newOS2Desc(os tables.Os2) *os2Desc {
return &os2Desc{
usWeightClass: os.USWeightClass,
usWidthClass: os.USWidthClass,
fsSelection: os.FsSelection,
}
}
// fontDescriptor provides access to family and aspect
type fontDescriptor struct {
// these tables are required both in Family
// and Aspect
os2 *os2Desc // optional
names tables.Name
head tables.Head
}
func newFontDescriptor(ld *ot.Loader, buffer []byte) (fontDescriptor, []byte) {
var desc fontDescriptor
// load tables, all considered optional
buffer, _ = ld.RawTableTo(ot.MustNewTag("OS/2"), buffer)
if os2, _, err := tables.ParseOs2(buffer); err == nil {
desc.os2 = newOS2Desc(os2)
}
desc.head, buffer, _ = LoadHeadTable(ld, buffer)
buffer, _ = ld.RawTableTo(ot.MustNewTag("name"), buffer)
desc.names, _, _ = tables.ParseName(buffer)
return desc, buffer
}
// family returns the font family name.
func (fd *fontDescriptor) family() string {
var family string
if fd.os2 != nil && fd.os2.fsSelection&256 != 0 {
family = fd.names.Name(namePreferredFamily)
if family == "" {
family = fd.names.Name(nameFontFamily)
}
} else {
family = fd.names.Name(nameWWSFamily)
if family == "" {
family = fd.names.Name(namePreferredFamily)
}
if family == "" {
family = fd.names.Name(nameFontFamily)
}
}
return family
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
func approximatelyEqual(x, y int) bool { return abs(x-y)*33 <= max(abs(x), abs(y)) }
// IsMonospace returns 'true' if the font is monospace,
// by inspecting the horizontal advances of its glyphs.
func (fd *Font) IsMonospace() bool {
// code adapted from fontconfig
// try the fast shortcuts
if fd.post.isFixedPitch {
return true
}
if fd.hmtx.IsEmpty() {
// we can't be sure, so be conservative
return false
}
if len(fd.hmtx.Metrics) == 1 {
return true
}
// directly read the advances in the 'hmtx' table
var firstAdvance int
for gid, metric := range fd.hmtx.Metrics {
if gid == 0 { // ignore the 'unset' glyph, which may be different
continue
}
advance := int(metric.AdvanceWidth)
if advance == 0 { // do not count zero as a proper width
continue
}
if firstAdvance == 0 {
firstAdvance = advance
continue
}
if approximatelyEqual(advance, firstAdvance) {
continue
}
// two distinct advances : the font is not monospace
return false
}
return true
}
// Description provides font metadata.
type Description struct {
Family string
Aspect Aspect
}
// Describe provides access to family and aspect.
//
// 'buffer' may be provided to reduce allocations.
//
// It provides an efficient API, loading only the mininum
// tables required. See also the method [Font.Describe]
// if you already have loaded the font.
func Describe(ld *ot.Loader, buffer []byte) (Description, []byte) {
desc, buffer := newFontDescriptor(ld, buffer)
return Description{desc.family(), desc.aspect()}, buffer
}
// Describe provides access to family and aspect.
//
// See also the package level function [Describe],
// which is more efficient if you only need the font
// metadata.
func (ft *Font) Describe() Description {
desc := fontDescriptor{ft.os2.os2Desc, ft.names, ft.head}
return Description{desc.family(), desc.aspect()}
}
type STAT = tables.STAT
+419
View File
@@ -0,0 +1,419 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package font
import (
"math"
ot "github.com/go-text/typesetting/font/opentype"
"github.com/go-text/typesetting/font/opentype/tables"
)
type gID = tables.GlyphID
func (f *Font) GetGlyphContourPoint(glyph GID, pointIndex uint16) (x, y int32, ok bool) {
// harfbuzz seems not to implement this feature
return 0, 0, false
}
// GlyphName returns the name of the given glyph, or an empty
// string if the glyph is invalid or has no name.
func (f *Font) GlyphName(glyph GID) string {
if postNames := f.post.names; postNames != nil {
if name := postNames.glyphName(glyph); name != "" {
return name
}
}
if f.cff != nil {
return f.cff.GlyphName(glyph)
}
return ""
}
// Upem returns the units per em of the font file.
// This value is only relevant for scalable fonts.
func (f *Font) Upem() uint16 { return f.upem }
var (
metricsTagHorizontalAscender = ot.MustNewTag("hasc")
metricsTagHorizontalDescender = ot.MustNewTag("hdsc")
metricsTagHorizontalLineGap = ot.MustNewTag("hlgp")
metricsTagVerticalAscender = ot.MustNewTag("vasc")
metricsTagVerticalDescender = ot.MustNewTag("vdsc")
metricsTagVerticalLineGap = ot.MustNewTag("vlgp")
)
func fixAscenderDescender(value float32, metricsTag Tag) float32 {
if metricsTag == metricsTagHorizontalAscender || metricsTag == metricsTagVerticalAscender {
return float32(math.Abs(float64(value)))
}
if metricsTag == metricsTagHorizontalDescender || metricsTag == metricsTagVerticalDescender {
return float32(-math.Abs(float64(value)))
}
return value
}
func (f *Font) getPositionCommon(metricTag Tag, varCoords []VarCoord) (float32, bool) {
deltaVar := f.mvar.getVar(metricTag, varCoords)
switch metricTag {
case metricsTagHorizontalAscender:
if f.os2.useTypoMetrics {
return fixAscenderDescender(float32(f.os2.sTypoAscender)+deltaVar, metricTag), true
} else if f.hhea != nil {
return fixAscenderDescender(float32(f.hhea.Ascender)+deltaVar, metricTag), true
}
case metricsTagHorizontalDescender:
if f.os2.useTypoMetrics {
return fixAscenderDescender(float32(f.os2.sTypoDescender)+deltaVar, metricTag), true
} else if f.hhea != nil {
return fixAscenderDescender(float32(f.hhea.Descender)+deltaVar, metricTag), true
}
case metricsTagHorizontalLineGap:
if f.os2.useTypoMetrics {
return fixAscenderDescender(float32(f.os2.sTypoLineGap)+deltaVar, metricTag), true
} else if f.hhea != nil {
return fixAscenderDescender(float32(f.hhea.LineGap)+deltaVar, metricTag), true
}
case metricsTagVerticalAscender:
if f.vhea != nil {
return fixAscenderDescender(float32(f.vhea.Ascender)+deltaVar, metricTag), true
}
case metricsTagVerticalDescender:
if f.vhea != nil {
return fixAscenderDescender(float32(f.vhea.Descender)+deltaVar, metricTag), true
}
case metricsTagVerticalLineGap:
if f.vhea != nil {
return fixAscenderDescender(float32(f.vhea.LineGap)+deltaVar, metricTag), true
}
}
return 0, false
}
// FontHExtents returns the extents of the font for horizontal text, or false
// it not available, in font units.
func (f *Face) FontHExtents() (FontExtents, bool) {
var (
out FontExtents
ok1, ok2, ok3 bool
)
out.Ascender, ok1 = f.Font.getPositionCommon(metricsTagHorizontalAscender, f.coords)
out.Descender, ok2 = f.Font.getPositionCommon(metricsTagHorizontalDescender, f.coords)
out.LineGap, ok3 = f.Font.getPositionCommon(metricsTagHorizontalLineGap, f.coords)
return out, ok1 && ok2 && ok3
}
// FontVExtents is the same as `FontHExtents`, but for vertical text.
func (f *Face) FontVExtents() (FontExtents, bool) {
var (
out FontExtents
ok1, ok2, ok3 bool
)
out.Ascender, ok1 = f.Font.getPositionCommon(metricsTagVerticalAscender, f.coords)
out.Descender, ok2 = f.Font.getPositionCommon(metricsTagVerticalDescender, f.coords)
out.LineGap, ok3 = f.Font.getPositionCommon(metricsTagVerticalLineGap, f.coords)
return out, ok1 && ok2 && ok3
}
var (
tagStrikeoutSize = ot.MustNewTag("strs")
tagStrikeoutOffset = ot.MustNewTag("stro")
tagUnderlineSize = ot.MustNewTag("unds")
tagUnderlineOffset = ot.MustNewTag("undo")
tagSuperscriptYSize = ot.MustNewTag("spys")
tagSuperscriptXOffset = ot.MustNewTag("spxo")
tagSubscriptYSize = ot.MustNewTag("sbys")
tagSubscriptYOffset = ot.MustNewTag("sbyo")
tagSubscriptXOffset = ot.MustNewTag("sbxo")
tagXHeight = ot.MustNewTag("xhgt")
tagCapHeight = ot.MustNewTag("cpht")
)
// return the height from baseline (in font units)
func (f *Face) runeHeight(r rune) float32 {
gid, ok := f.NominalGlyph(r)
if !ok {
return 0
}
extents, ok := f.GlyphExtents(gid)
if !ok {
return 0
}
return extents.YBearing
}
// LineMetric returns the metric identified by `metric` (in fonts units).
func (f *Face) LineMetric(metric LineMetric) float32 {
switch metric {
case UnderlinePosition:
return f.post.underlinePosition + f.mvar.getVar(tagUnderlineOffset, f.coords)
case UnderlineThickness:
return f.post.underlineThickness + f.mvar.getVar(tagUnderlineSize, f.coords)
case StrikethroughPosition:
return float32(f.os2.yStrikeoutPosition) + f.mvar.getVar(tagStrikeoutOffset, f.coords)
case StrikethroughThickness:
return float32(f.os2.yStrikeoutSize) + f.mvar.getVar(tagStrikeoutSize, f.coords)
case SuperscriptEmYSize:
return float32(f.os2.ySuperscriptYSize) + f.mvar.getVar(tagSuperscriptYSize, f.coords)
case SuperscriptEmXOffset:
return float32(f.os2.ySuperscriptXOffset) + f.mvar.getVar(tagSuperscriptXOffset, f.coords)
case SubscriptEmYSize:
return float32(f.os2.ySubscriptYSize) + f.mvar.getVar(tagSubscriptYSize, f.coords)
case SubscriptEmYOffset:
return float32(f.os2.ySubscriptYOffset) + f.mvar.getVar(tagSubscriptYOffset, f.coords)
case SubscriptEmXOffset:
return float32(f.os2.ySubscriptXOffset) + f.mvar.getVar(tagSubscriptXOffset, f.coords)
case CapHeight:
if f.os2.version < 2 {
// sCapHeight may be set equal to the top of the unscaled and unhinted glyph
// bounding box of the glyph encoded at U+0048 (LATIN CAPITAL LETTER H).
return f.runeHeight('H')
}
return float32(f.os2.sCapHeight) + f.mvar.getVar(tagCapHeight, f.coords)
case XHeight:
if f.os2.version < 2 {
// sxHeight equal to the top of the unscaled and unhinted glyph bounding box
// of the glyph encoded at U+0078 (LATIN SMALL LETTER X).
return f.runeHeight('x')
}
return float32(f.os2.sxHeigh) + f.mvar.getVar(tagXHeight, f.coords)
default:
return 0
}
}
// NominalGlyph returns the glyph used to represent the given rune,
// or false if not found.
// Note that it only looks into the cmap, without taking account substitutions
// nor variation selectors.
func (f *Font) NominalGlyph(ch rune) (GID, bool) { return f.Cmap.Lookup(ch) }
// VariationGlyph retrieves the glyph ID for a specified Unicode code point
// followed by a specified Variation Selector code point, or false if not found
func (f *Font) VariationGlyph(ch, varSelector rune) (GID, bool) {
gid, kind := f.cmapVar.GetGlyphVariant(ch, varSelector)
switch kind {
case VariantNotFound:
return 0, false
case VariantFound:
return gid, true
default: // VariantUseDefault
return f.NominalGlyph(ch)
}
}
// do not take into account variations
func (f *Font) getBaseAdvance(gid gID, table tables.Hmtx, isVertical bool) int16 {
/* If `table` is empty, it means we don't have the metrics table
* for this direction: return default advance. Otherwise, it means that the
* glyph index is out of bound: return zero. */
if table.IsEmpty() {
if isVertical {
return int16(f.upem)
}
return int16(f.upem / 2)
}
return table.Advance(gid)
}
func clamp(v float32) float32 {
if v < 0 {
v = 0
}
return v
}
func (f *Face) getGlyphAdvanceVar(gid gID, isVertical bool) float32 {
_, phantoms := f.getGlyfPoints(gid, false)
if isVertical {
return clamp(phantoms[phantomTop].Y - phantoms[phantomBottom].Y)
}
return clamp(phantoms[phantomRight].X - phantoms[phantomLeft].X)
}
func (f *Face) HorizontalAdvance(gid GID) float32 {
advance := f.getBaseAdvance(gID(gid), f.hmtx, false)
if !f.isVar() {
return float32(advance)
}
if f.hvar != nil {
return float32(advance) + f.hvar.AdvanceDelta(gID(gid), f.coords)
}
return f.getGlyphAdvanceVar(gID(gid), false)
}
// return `true` is the font is variable and `Coords` is valid
func (f *Face) isVar() bool {
return len(f.coords) != 0 && len(f.coords) == len(f.Font.fvar)
}
// HasVerticalMetrics returns true if a the 'vmtx' table is present.
// If not, client should avoid calls to [VerticalAdvance], which will returns a
// defaut value.
func (f *Font) HasVerticalMetrics() bool { return !f.vmtx.IsEmpty() }
func (f *Face) VerticalAdvance(gid GID) float32 {
// return the opposite of the advance from the font
advance := f.getBaseAdvance(gID(gid), f.vmtx, true)
if !f.isVar() {
return -float32(advance)
}
if f.vvar != nil {
return -float32(advance) - f.vvar.AdvanceDelta(gID(gid), f.coords)
}
return -f.getGlyphAdvanceVar(gID(gid), true)
}
func (f *Font) GlyphHOrigin(GID) (x, y int32, found bool) {
// zero is the right value here
return 0, 0, true
}
func (f *Face) GlyphVOrigin(glyph GID) (x, y float32) {
// First, set the x value to half the advance width.
x = f.HorizontalAdvance(glyph) / 2
// If there is VORG, always use it. It uses VVAR for variations if necessary.
if f.vorg != nil {
y = float32(f.vorg.YOrigin(gID(glyph)))
if f.isVar() && f.vvar != nil {
y += f.vvar.VorgDelta(gID(glyph), f.coords)
}
return x, y
}
// If and only if `vmtx` is present and it's a `glyf` font,
// we use the top phantom point, deduced from vmtx,glyf[,gvar].
if !f.vmtx.IsEmpty() && f.glyf != nil {
y = f.getVOriginWithVar(gID(glyph))
return x, y
}
// Otherwise, use glyph extents to center the glyph vertically.
// If getting glyph extents failed, just use the font ascender.
fontExtents, _ := f.FontHExtents()
fontAdvance := fontExtents.Ascender - fontExtents.Descender
if extents, ok := f.getExtentsFromGlyf(gID(glyph)); ok {
diff := fontAdvance - -extents.Height
y = extents.YBearing + float32(int(diff)/2)
return x, y
}
y = fontExtents.Ascender
return x, y
}
func (f *Face) getVOriginWithVar(gid gID) float32 {
if int(gid) >= f.nGlyphs {
return 0
}
_, phantoms := f.getGlyfPoints(gid, false)
return phantoms[phantomTop].Y
}
func (f *Face) getExtentsFromGlyf(glyph gID) (GlyphExtents, bool) {
if int(glyph) >= len(f.glyf) {
return GlyphExtents{}, false
}
if f.isVar() { // we have to compute the outline points and apply variations
extents, _ := f.getGlyfPoints(glyph, true)
return extents, true
}
return getGlyphExtents(f.glyf[glyph], f.hmtx, glyph), true
}
func (f *Font) getExtentsFromBitmap(glyph gID, xPpem, yPpem uint16) (GlyphExtents, bool) {
strike := f.bitmap.chooseStrike(xPpem, yPpem)
if strike == nil || strike.ppemX == 0 || strike.ppemY == 0 {
return GlyphExtents{}, false
}
subtable := strike.findTable(glyph)
if subtable == nil {
return GlyphExtents{}, false
}
image := subtable.image(glyph)
if image == nil {
return GlyphExtents{}, false
}
extents := GlyphExtents{
XBearing: float32(image.metrics.BearingX),
YBearing: float32(image.metrics.BearingY),
Width: float32(image.metrics.Width),
Height: -float32(image.metrics.Height),
}
/* convert to font units. */
xScale := float32(f.upem) / float32(strike.ppemX)
yScale := float32(f.upem) / float32(strike.ppemY)
extents.XBearing *= xScale
extents.YBearing *= yScale
extents.Width *= xScale
extents.Height *= yScale
return extents, true
}
func (f *Font) getExtentsFromSbix(glyph gID, xPpem, yPpem uint16) (GlyphExtents, bool) {
strike := f.sbix.chooseStrike(xPpem, yPpem)
if strike == nil || strike.Ppem == 0 {
return GlyphExtents{}, false
}
data := strikeGlyph(strike, glyph, 0)
if data.GraphicType == 0 {
return GlyphExtents{}, false
}
extents, ok := bitmapGlyphExtents(data)
/* convert to font units. */
scale := float32(f.upem) / float32(strike.Ppem)
extents.XBearing *= scale
extents.YBearing *= scale
extents.Width *= scale
extents.Height *= scale
return extents, ok
}
func (f *Font) getExtentsFromCff1(glyph gID) (GlyphExtents, bool) {
if f.cff == nil {
return GlyphExtents{}, false
}
_, bounds, err := f.cff.LoadGlyph(glyph)
if err != nil {
return GlyphExtents{}, false
}
return bounds.ToExtents(), true
}
func (f *Face) getExtentsFromCff2(glyph gID) (GlyphExtents, bool) {
if f.cff2 == nil {
return GlyphExtents{}, false
}
_, bounds, err := f.cff2.LoadGlyph(glyph, f.coords)
if err != nil {
return GlyphExtents{}, false
}
return bounds.ToExtents(), true
}
func (f *Face) glyphExtentsRaw(glyph GID) (GlyphExtents, bool) {
out, ok := f.getExtentsFromSbix(gID(glyph), f.xPpem, f.yPpem)
if ok {
return out, ok
}
out, ok = f.getExtentsFromBitmap(gID(glyph), f.xPpem, f.yPpem)
if ok {
return out, ok
}
out, ok = f.getExtentsFromGlyf(gID(glyph))
if ok {
return out, ok
}
out, ok = f.getExtentsFromCff2(gID(glyph))
if ok {
return out, ok
}
out, ok = f.getExtentsFromCff1(gID(glyph))
return out, ok
}
+87
View File
@@ -0,0 +1,87 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
// Package opentype provides the low level routines
// required to read and write Opentype font files, including collections.
//
// This package is designed to provide an efficient, lazy, reading API.
//
// For the parsing of the various tables, see package [tables].
package opentype
type Tag uint32
// NewTag returns the tag for <abcd>.
func NewTag(a, b, c, d byte) Tag {
return Tag(uint32(d) | uint32(c)<<8 | uint32(b)<<16 | uint32(a)<<24)
}
// MustNewTag gives you the Tag corresponding to the acronym.
// This function will panic if the string passed in is not 4 bytes long.
func MustNewTag(str string) Tag {
if len(str) != 4 {
panic("invalid tag: must be exactly 4 bytes")
}
_ = str[3]
return NewTag(str[0], str[1], str[2], str[3])
}
// String return the ASCII form of the tag.
func (t Tag) String() string {
return string([]byte{
byte(t >> 24),
byte(t >> 16),
byte(t >> 8),
byte(t),
})
}
type GID uint32
type GlyphExtents struct {
XBearing float32 // Left side of glyph from origin
YBearing float32 // Top side of glyph from origin
Width float32 // Distance from left to right side
Height float32 // Distance from top to bottom side
}
type SegmentOp uint8
const (
SegmentOpMoveTo SegmentOp = iota
SegmentOpLineTo
SegmentOpQuadTo
SegmentOpCubeTo
)
type SegmentPoint struct {
X, Y float32 // expressed in fonts units
}
// Move translates the point.
func (pt *SegmentPoint) Move(dx, dy float32) {
pt.X += dx
pt.Y += dy
}
type Segment struct {
Op SegmentOp
// Args is up to three (x, y) coordinates, depending on the
// operation.
// The Y axis increases up.
Args [3]SegmentPoint
}
// ArgsSlice returns the effective slice of points
// used (whose length is between 1 and 3).
func (s *Segment) ArgsSlice() []SegmentPoint {
switch s.Op {
case SegmentOpMoveTo, SegmentOpLineTo:
return s.Args[0:1]
case SegmentOpQuadTo:
return s.Args[0:2]
case SegmentOpCubeTo:
return s.Args[0:3]
default:
panic("unreachable")
}
}
+371
View File
@@ -0,0 +1,371 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package opentype
import (
"compress/zlib"
"encoding/binary"
"errors"
"fmt"
"io"
"sort"
)
var (
// TrueType is the first four bytes of an OpenType file containing a TrueType font
TrueType = Tag(0x00010000)
// AppleTrueType is the first four bytes of an OpenType file containing a TrueType font
// (specifically one designed for Apple products, it's recommended to use TrueType instead)
AppleTrueType = MustNewTag("true")
// PostScript1 is the first four bytes of an OpenType file containing a PostScript 1 font
PostScript1 = MustNewTag("typ1")
// OpenType is the first four bytes of an OpenType file containing a PostScript Type 2 font
// as specified by OpenType
OpenType = MustNewTag("OTTO")
// signatureWOFF is the magic number at the start of a WOFF file.
signatureWOFF = MustNewTag("wOFF")
ttcTag = MustNewTag("ttcf")
errInvalidDfont = errors.New("invalid dfont")
)
// dfontResourceDataOffset is the assumed value of a dfont file's resource data
// offset.
//
// https://github.com/kreativekorp/ksfl/wiki/Macintosh-Resource-File-Format
// says that "A Mac OS resource file... [starts with an] offset from start of
// file to start of resource data section... [usually] 0x0100". In theory,
// 0x00000100 isn't always a magic number for identifying dfont files. In
// practice, it seems to work.
const dfontResourceDataOffset = 0x00000100
type Resource interface {
Read([]byte) (int, error)
ReadAt([]byte, int64) (int, error)
Seek(int64, int) (int64, error)
}
// tableSection represents a table within the font file.
type tableSection struct {
offset uint32 // Offset into the file this table starts.
length uint32 // Length of this table within the file.
zLength uint32 // Uncompressed length of this table.
}
// Loader is the low level font reader, providing
// full control over table loading.
type Loader struct {
file Resource // source, needed to parse each table
tables map[Tag]tableSection // header only, contents is processed on demand
// Type represents the kind of this font being loaded.
// It is one of TrueType, TrueTypeApple, PostScript1, OpenType
Type Tag
}
// NewLoader reads the `file` header and returns
// a new lazy ot.
// `file` will be used to parse tables, and should not be close.
func NewLoader(file Resource) (*Loader, error) {
return parseOneFont(file, 0, false)
}
// NewLoaders is the same as `NewLoader`, but supports collections.
func NewLoaders(file Resource) ([]*Loader, error) {
_, err := file.Seek(0, io.SeekStart) // file might have been used before
if err != nil {
return nil, err
}
var bytes [4]byte
_, err = file.Read(bytes[:])
if err != nil {
return nil, err
}
magic := NewTag(bytes[0], bytes[1], bytes[2], bytes[3])
file.Seek(0, io.SeekStart)
var (
pr *Loader
offsets []uint32
relativeOffset bool
)
switch magic {
case signatureWOFF, TrueType, OpenType, PostScript1, AppleTrueType:
pr, err = parseOneFont(file, 0, false)
case ttcTag:
offsets, err = parseTTCHeader(file)
case dfontResourceDataOffset:
offsets, err = parseDfont(file)
relativeOffset = true
default:
return nil, fmt.Errorf("unsupported font format %v", bytes)
}
if err != nil {
return nil, err
}
// only one font
if pr != nil {
return []*Loader{pr}, nil
}
// collection
out := make([]*Loader, len(offsets))
for i, o := range offsets {
out[i], err = parseOneFont(file, o, relativeOffset)
if err != nil {
return nil, err
}
}
return out, nil
}
// dst is an optional storage which may be provided to reduce allocations.
func (pr *Loader) findTableBuffer(s tableSection, dst []byte) ([]byte, error) {
if s.length != 0 && s.length < s.zLength {
zbuf := io.NewSectionReader(pr.file, int64(s.offset), int64(s.length))
r, err := zlib.NewReader(zbuf)
if err != nil {
return nil, err
}
defer r.Close()
if cap(dst) < int(s.zLength) {
dst = make([]byte, s.zLength)
}
dst = dst[0:s.zLength]
if _, err := io.ReadFull(r, dst); err != nil {
return nil, err
}
} else {
if cap(dst) < int(s.length) {
dst = make([]byte, s.length)
}
dst = dst[0:s.length]
if _, err := pr.file.ReadAt(dst, int64(s.offset)); err != nil {
return nil, err
}
}
return dst, nil
}
// HasTable returns true if [table] is present.
func (pr *Loader) HasTable(table Tag) bool {
_, has := pr.tables[table]
return has
}
// Tables returns all the tables found in the file,
// as a sorted slice.
func (ld *Loader) Tables() []Tag {
out := make([]Tag, 0, len(ld.tables))
for tag := range ld.tables {
out = append(out, tag)
}
sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
return out
}
// RawTable returns the binary content of the given table,
// or an error if not found.
func (pr *Loader) RawTable(tag Tag) ([]byte, error) {
return pr.RawTableTo(tag, nil)
}
// RawTable writes the binary content of the given table to [dst], returning it,
// or an error if not found.
func (pr *Loader) RawTableTo(tag Tag, dst []byte) ([]byte, error) {
s, found := pr.tables[tag]
if !found {
return nil, fmt.Errorf("missing table %s", tag)
}
return pr.findTableBuffer(s, dst)
}
func parseOneFont(file Resource, offset uint32, relativeOffset bool) (parser *Loader, err error) {
_, err = file.Seek(int64(offset), io.SeekStart)
if err != nil {
return nil, fmt.Errorf("invalid offset: %s", err)
}
var bytes [4]byte
_, err = file.Read(bytes[:])
if err != nil {
return nil, err
}
magic := NewTag(bytes[0], bytes[1], bytes[2], bytes[3])
switch magic {
case signatureWOFF:
parser, err = parseWOFF(file, offset, relativeOffset)
case TrueType, OpenType, PostScript1, AppleTrueType:
parser, err = parseOTF(file, offset, relativeOffset)
case ttcTag, dfontResourceDataOffset: // no more collections allowed here
return nil, errors.New("collections not allowed")
default:
return nil, fmt.Errorf("unknown font format tag %v", bytes)
}
if err != nil {
return nil, err
}
return parser, nil
}
// support for collections
const maxNumFonts = 2048 // security implementation limit
// returns the offsets of each font
func parseTTCHeader(r io.Reader) ([]uint32, error) {
// The https://www.microsoft.com/typography/otspec/otff.htm "Font
// Collections" section describes the TTC header.
var buf [12]byte
if _, err := r.Read(buf[:]); err != nil {
return nil, err
}
// skip versions
numFonts := binary.BigEndian.Uint32(buf[8:])
if numFonts == 0 {
return nil, errors.New("empty font collection")
}
if numFonts > maxNumFonts {
return nil, fmt.Errorf("number of fonts (%d) in collection exceed implementation limit (%d)",
numFonts, maxNumFonts)
}
offsetsBytes := make([]byte, numFonts*4)
_, err := io.ReadFull(r, offsetsBytes)
if err != nil {
return nil, err
}
return parseUint32s(offsetsBytes, int(numFonts)), nil
}
// parseDfont parses a dfont resource map, as per
// https://github.com/kreativekorp/ksfl/wiki/Macintosh-Resource-File-Format
//
// That unofficial wiki page lists all of its fields as *signed* integers,
// which looks unusual. The actual file format might use *unsigned* integers in
// various places, but until we have either an official specification or an
// actual dfont file where this matters, we'll use signed integers and treat
// negative values as invalid.
func parseDfont(r Resource) ([]uint32, error) {
var buf [16]byte
if _, err := r.Read(buf[:]); err != nil {
return nil, err
}
resourceMapOffset := binary.BigEndian.Uint32(buf[4:])
resourceMapLength := binary.BigEndian.Uint32(buf[12:])
const (
// (maxTableOffset + maxTableLength) will not overflow an int32.
maxTableLength = 1 << 29
maxTableOffset = 1 << 29
)
if resourceMapOffset > maxTableOffset || resourceMapLength > maxTableLength {
return nil, errors.New("unsupported table offset or length")
}
const headerSize = 28
if resourceMapLength < headerSize {
return nil, errInvalidDfont
}
_, err := r.ReadAt(buf[:2], int64(resourceMapOffset+24))
if err != nil {
return nil, err
}
typeListOffset := int64(int16(binary.BigEndian.Uint16(buf[:])))
if typeListOffset < headerSize || resourceMapLength < uint32(typeListOffset)+2 {
return nil, errInvalidDfont
}
_, err = r.ReadAt(buf[:2], int64(resourceMapOffset)+typeListOffset)
if err != nil {
return nil, err
}
typeCount := int(binary.BigEndian.Uint16(buf[:])) // The number of types, minus one.
if typeCount == 0xFFFF {
return nil, errInvalidDfont
}
typeCount += 1
const tSize = 8
if tSize*uint32(typeCount) > resourceMapLength-uint32(typeListOffset)-2 {
return nil, errInvalidDfont
}
typeList := make([]byte, tSize*typeCount)
_, err = r.ReadAt(typeList, int64(resourceMapOffset)+typeListOffset+2)
if err != nil {
return nil, err
}
numFonts, resourceListOffset := 0, 0
for i := 0; i < typeCount; i++ {
if binary.BigEndian.Uint32(typeList[tSize*i:]) != 0x73666e74 { // "sfnt".
continue
}
numFonts = int(int16(binary.BigEndian.Uint16(typeList[tSize*i+4:])))
if numFonts < 0 {
return nil, errInvalidDfont
}
// https://github.com/kreativekorp/ksfl/wiki/Macintosh-Resource-File-Format
// says that the value in the wire format is "the number of
// resources of this type, minus one."
numFonts++
resourceListOffset = int(int16(binary.BigEndian.Uint16((typeList[tSize*i+6:]))))
if resourceListOffset < 0 {
return nil, errInvalidDfont
}
}
if numFonts == 0 {
return nil, errInvalidDfont
}
if numFonts > maxNumFonts {
return nil, fmt.Errorf("number of fonts (%d) in collection exceed implementation limit (%d)",
numFonts, maxNumFonts)
}
const rSize = 12
o, n := uint32(int(typeListOffset)+resourceListOffset), rSize*uint32(numFonts)
if o > resourceMapLength || n > resourceMapLength-o {
return nil, errInvalidDfont
}
offsetsBytes := make([]byte, n)
_, err = r.ReadAt(offsetsBytes, int64(resourceMapOffset+o))
if err != nil {
return nil, err
}
offsets := make([]uint32, numFonts)
for i := range offsets {
o := 0xffffff & binary.BigEndian.Uint32(offsetsBytes[rSize*i+4:])
// Offsets are relative to the resource data start, not the file start.
// A particular resource's data also starts with a 4-byte length, which
// we skip.
o += dfontResourceDataOffset + 4
if o > maxTableOffset {
return nil, errors.New("unsupported table offset or length")
}
offsets[i] = o
}
return offsets, nil
}
// data length must have been checked
func parseUint32s(data []byte, count int) []uint32 {
out := make([]uint32, count)
for i := range out {
out[i] = binary.BigEndian.Uint32(data[4*i:])
}
return out
}
+99
View File
@@ -0,0 +1,99 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package opentype
import (
"encoding/binary"
"errors"
"fmt"
"io"
)
// An Entry in an OpenType table.
type otfEntry struct {
Tag Tag
CheckSum uint32
Offset uint32
Length uint32
}
const (
otfHeaderSize = 12
otfEntrySize = 16
)
func readOTFHeader(r io.Reader) (flavor Tag, numTables uint16, err error) {
var buf [otfHeaderSize]byte
if _, err := r.Read(buf[:]); err != nil {
return 0, 0, fmt.Errorf("invalid OpenType header: %s", err)
}
return NewTag(buf[0], buf[1], buf[2], buf[3]), binary.BigEndian.Uint16(buf[4:6]), nil
}
func readOTFEntry(r io.Reader) (otfEntry, error) {
var (
buf [otfEntrySize]byte
entry otfEntry
)
if _, err := io.ReadFull(r, buf[:]); err != nil {
return entry, fmt.Errorf("invalid directory entry: %s", err)
}
entry.Tag = Tag(binary.BigEndian.Uint32(buf[0:4]))
entry.CheckSum = binary.BigEndian.Uint32(buf[4:8])
entry.Offset = binary.BigEndian.Uint32(buf[8:12])
entry.Length = binary.BigEndian.Uint32(buf[12:16])
return entry, nil
}
// parseOTF reads an OpenTyp (.otf) or TrueType (.ttf) file and returns a Font.
// If the parsing fails, then an error is returned and Font will be nil.
// `offset` is the beginning of the ressource in the file (non zero for collections)
// `relativeOffset` is true when the table offset are expresed relatively to the ressource start
// (that is, `offset`) rather than to the file start.
func parseOTF(file Resource, offset uint32, relativeOffset bool) (*Loader, error) {
_, err := file.Seek(int64(offset), io.SeekStart)
if err != nil {
return nil, fmt.Errorf("invalid offset: %s", err)
}
flavor, numTables, err := readOTFHeader(file)
if err != nil {
return nil, err
}
pr := &Loader{
file: file,
tables: make(map[Tag]tableSection, numTables),
Type: flavor,
}
for i := 0; i < int(numTables); i++ {
entry, err := readOTFEntry(file)
if err != nil {
return nil, err
}
if _, found := pr.tables[entry.Tag]; found {
// ignore duplicate tables – the first one wins
continue
}
sec := tableSection{
offset: entry.Offset,
length: entry.Length,
}
// adapt the relative offsets
if relativeOffset {
sec.offset += offset
if sec.offset < offset { // check for overflow
return nil, errors.New("unsupported table offset or length")
}
}
pr.tables[entry.Tag] = sec
}
return pr, nil
}
+96
View File
@@ -0,0 +1,96 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package opentype
import (
"encoding/binary"
"errors"
"io"
)
type woffEntry struct {
Tag Tag
Offset uint32
CompLength uint32
OrigLength uint32
OrigChecksum uint32
}
const (
woffHeaderSize = 44 // for the full header, but we only read Flavor and NumTables
woffEntrySize = 20
)
func readWOFFHeader(r io.Reader) (flavor Tag, numTables uint16, err error) {
var buf [woffHeaderSize]byte
if _, err := io.ReadFull(r, buf[:]); err != nil {
return 0, 0, err
}
return NewTag(buf[4], buf[5], buf[6], buf[7]), binary.BigEndian.Uint16(buf[12:14]), nil
}
func readWOFFEntry(r io.Reader) (woffEntry, error) {
var (
buf [woffEntrySize]byte
entry woffEntry
)
if _, err := io.ReadFull(r, buf[:]); err != nil {
return entry, err
}
entry.Tag = NewTag(buf[0], buf[1], buf[2], buf[3])
entry.Offset = binary.BigEndian.Uint32(buf[4:8])
entry.CompLength = binary.BigEndian.Uint32(buf[8:12])
entry.OrigLength = binary.BigEndian.Uint32(buf[12:16])
entry.OrigChecksum = binary.BigEndian.Uint32(buf[16:20])
return entry, nil
}
// `offset` is the beginning of the ressource in the file (non zero for collections)
// `relativeOffset` is true when the table offset are expresed relatively ot the ressource
// (that is, `offset`) rather than to the file
func parseWOFF(file Resource, offset uint32, relativeOffset bool) (*Loader, error) {
_, err := file.Seek(int64(offset), io.SeekStart)
if err != nil {
return nil, err
}
flavor, numTables, err := readWOFFHeader(file)
if err != nil {
return nil, err
}
fontParser := &Loader{
file: file,
tables: make(map[Tag]tableSection, numTables),
Type: flavor,
}
for i := 0; i < int(numTables); i++ {
entry, err := readWOFFEntry(file)
if err != nil {
return nil, err
}
if _, found := fontParser.tables[entry.Tag]; found {
// ignore duplicate tables – the first one wins
continue
}
sec := tableSection{
offset: entry.Offset,
length: entry.CompLength,
zLength: entry.OrigLength,
}
// adapt the relative offsets
if relativeOffset {
sec.offset += offset
if sec.offset < offset { // check for overflow
return nil, errors.New("unsupported table offset or length")
}
}
fontParser.tables[entry.Tag] = sec
}
return fontParser, nil
}
@@ -0,0 +1,342 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import (
"encoding/binary"
"fmt"
)
// Code generated by binarygen from aat_ankr_src.go. DO NOT EDIT
func (item *AnkrAnchor) mustParse(src []byte) {
_ = src[3] // early bound checking
item.X = int16(binary.BigEndian.Uint16(src[0:]))
item.Y = int16(binary.BigEndian.Uint16(src[2:]))
}
func (item *LookupRecord2) mustParse(src []byte) {
_ = src[5] // early bound checking
item.LastGlyph = binary.BigEndian.Uint16(src[0:])
item.FirstGlyph = binary.BigEndian.Uint16(src[2:])
item.Value = binary.BigEndian.Uint16(src[4:])
}
func ParseAATLookup(src []byte, valuesCount int) (AATLookup, int, error) {
var item AATLookup
if L := len(src); L < 2 {
return item, 0, fmt.Errorf("reading AATLookup: "+"EOF: expected length: 2, got %d", L)
}
format := uint16(binary.BigEndian.Uint16(src[0:]))
var (
read int
err error
)
switch format {
case 0:
item, read, err = ParseAATLoopkup0(src[0:], valuesCount)
case 10:
item, read, err = ParseAATLoopkup10(src[0:])
case 2:
item, read, err = ParseAATLoopkup2(src[0:])
case 4:
item, read, err = ParseAATLoopkup4(src[0:])
case 6:
item, read, err = ParseAATLoopkup6(src[0:])
case 8:
item, read, err = ParseAATLoopkup8(src[0:])
default:
err = fmt.Errorf("unsupported AATLookup format %d", format)
}
if err != nil {
return item, 0, fmt.Errorf("reading AATLookup: %s", err)
}
return item, read, nil
}
func ParseAATLookupRecord4(src []byte, parentSrc []byte) (AATLookupRecord4, int, error) {
var item AATLookupRecord4
n := 0
if L := len(src); L < 6 {
return item, 0, fmt.Errorf("reading AATLookupRecord4: "+"EOF: expected length: 6, got %d", L)
}
_ = src[5] // early bound checking
item.LastGlyph = binary.BigEndian.Uint16(src[0:])
item.FirstGlyph = binary.BigEndian.Uint16(src[2:])
offsetValues := int(binary.BigEndian.Uint16(src[4:]))
n += 6
{
if offsetValues != 0 { // ignore null offset
if L := len(parentSrc); L < offsetValues {
return item, 0, fmt.Errorf("reading AATLookupRecord4: "+"EOF: expected length: %d, got %d", offsetValues, L)
}
arrayLength := int(item.nValues())
if L := len(parentSrc); L < offsetValues+arrayLength*2 {
return item, 0, fmt.Errorf("reading AATLookupRecord4: "+"EOF: expected length: %d, got %d", offsetValues+arrayLength*2, L)
}
item.Values = make([]uint16, arrayLength) // allocation guarded by the previous check
for i := range item.Values {
item.Values[i] = binary.BigEndian.Uint16(parentSrc[offsetValues+i*2:])
}
offsetValues += arrayLength * 2
}
}
return item, n, nil
}
func ParseAATLoopkup0(src []byte, valuesCount int) (AATLoopkup0, int, error) {
var item AATLoopkup0
n := 0
if L := len(src); L < 2 {
return item, 0, fmt.Errorf("reading AATLoopkup0: "+"EOF: expected length: 2, got %d", L)
}
item.version = binary.BigEndian.Uint16(src[0:])
n += 2
{
if L := len(src); L < 2+valuesCount*2 {
return item, 0, fmt.Errorf("reading AATLoopkup0: "+"EOF: expected length: %d, got %d", 2+valuesCount*2, L)
}
item.Values = make([]uint16, valuesCount) // allocation guarded by the previous check
for i := range item.Values {
item.Values[i] = binary.BigEndian.Uint16(src[2+i*2:])
}
n += valuesCount * 2
}
return item, n, nil
}
func ParseAATLoopkup10(src []byte) (AATLoopkup10, int, error) {
var item AATLoopkup10
n := 0
if L := len(src); L < 8 {
return item, 0, fmt.Errorf("reading AATLoopkup10: "+"EOF: expected length: 8, got %d", L)
}
_ = src[7] // early bound checking
item.version = binary.BigEndian.Uint16(src[0:])
item.unitSize = binary.BigEndian.Uint16(src[2:])
item.FirstGlyph = binary.BigEndian.Uint16(src[4:])
arrayLengthValues := int(binary.BigEndian.Uint16(src[6:]))
n += 8
{
if L := len(src); L < 8+arrayLengthValues*2 {
return item, 0, fmt.Errorf("reading AATLoopkup10: "+"EOF: expected length: %d, got %d", 8+arrayLengthValues*2, L)
}
item.Values = make([]uint16, arrayLengthValues) // allocation guarded by the previous check
for i := range item.Values {
item.Values[i] = binary.BigEndian.Uint16(src[8+i*2:])
}
n += arrayLengthValues * 2
}
return item, n, nil
}
func ParseAATLoopkup2(src []byte) (AATLoopkup2, int, error) {
var item AATLoopkup2
n := 0
if L := len(src); L < 12 {
return item, 0, fmt.Errorf("reading AATLoopkup2: "+"EOF: expected length: 12, got %d", L)
}
_ = src[11] // early bound checking
item.version = binary.BigEndian.Uint16(src[0:])
item.binSearchHeader.mustParse(src[2:])
n += 12
{
arrayLength := int(item.nUnits)
if L := len(src); L < 12+arrayLength*6 {
return item, 0, fmt.Errorf("reading AATLoopkup2: "+"EOF: expected length: %d, got %d", 12+arrayLength*6, L)
}
item.Records = make([]LookupRecord2, arrayLength) // allocation guarded by the previous check
for i := range item.Records {
item.Records[i].mustParse(src[12+i*6:])
}
n += arrayLength * 6
}
return item, n, nil
}
func ParseAATLoopkup4(src []byte) (AATLoopkup4, int, error) {
var item AATLoopkup4
n := 0
if L := len(src); L < 12 {
return item, 0, fmt.Errorf("reading AATLoopkup4: "+"EOF: expected length: 12, got %d", L)
}
_ = src[11] // early bound checking
item.version = binary.BigEndian.Uint16(src[0:])
item.binSearchHeader.mustParse(src[2:])
n += 12
{
arrayLength := int(item.nUnits - 1)
offset := 12
for i := 0; i < arrayLength; i++ {
elem, read, err := ParseAATLookupRecord4(src[offset:], src)
if err != nil {
return item, 0, fmt.Errorf("reading AATLoopkup4: %s", err)
}
item.Records = append(item.Records, elem)
offset += read
}
n = offset
}
return item, n, nil
}
func ParseAATLoopkup6(src []byte) (AATLoopkup6, int, error) {
var item AATLoopkup6
n := 0
if L := len(src); L < 12 {
return item, 0, fmt.Errorf("reading AATLoopkup6: "+"EOF: expected length: 12, got %d", L)
}
_ = src[11] // early bound checking
item.version = binary.BigEndian.Uint16(src[0:])
item.binSearchHeader.mustParse(src[2:])
n += 12
{
arrayLength := int(item.nUnits)
if L := len(src); L < 12+arrayLength*4 {
return item, 0, fmt.Errorf("reading AATLoopkup6: "+"EOF: expected length: %d, got %d", 12+arrayLength*4, L)
}
item.Records = make([]loopkupRecord6, arrayLength) // allocation guarded by the previous check
for i := range item.Records {
item.Records[i].mustParse(src[12+i*4:])
}
n += arrayLength * 4
}
return item, n, nil
}
func ParseAATLoopkup8(src []byte) (AATLoopkup8, int, error) {
var item AATLoopkup8
n := 0
if L := len(src); L < 2 {
return item, 0, fmt.Errorf("reading AATLoopkup8: "+"EOF: expected length: 2, got %d", L)
}
item.version = binary.BigEndian.Uint16(src[0:])
n += 2
{
var (
err error
read int
)
item.AATLoopkup8Data, read, err = ParseAATLoopkup8Data(src[2:])
if err != nil {
return item, 0, fmt.Errorf("reading AATLoopkup8: %s", err)
}
n += read
}
return item, n, nil
}
func ParseAATLoopkup8Data(src []byte) (AATLoopkup8Data, int, error) {
var item AATLoopkup8Data
n := 0
if L := len(src); L < 4 {
return item, 0, fmt.Errorf("reading AATLoopkup8Data: "+"EOF: expected length: 4, got %d", L)
}
_ = src[3] // early bound checking
item.FirstGlyph = binary.BigEndian.Uint16(src[0:])
arrayLengthValues := int(binary.BigEndian.Uint16(src[2:]))
n += 4
{
if L := len(src); L < 4+arrayLengthValues*2 {
return item, 0, fmt.Errorf("reading AATLoopkup8Data: "+"EOF: expected length: %d, got %d", 4+arrayLengthValues*2, L)
}
item.Values = make([]uint16, arrayLengthValues) // allocation guarded by the previous check
for i := range item.Values {
item.Values[i] = binary.BigEndian.Uint16(src[4+i*2:])
}
n += arrayLengthValues * 2
}
return item, n, nil
}
func ParseAnkr(src []byte, valuesCount int) (Ankr, int, error) {
var item Ankr
n := 0
if L := len(src); L < 12 {
return item, 0, fmt.Errorf("reading Ankr: "+"EOF: expected length: 12, got %d", L)
}
_ = src[11] // early bound checking
item.version = binary.BigEndian.Uint16(src[0:])
item.flags = binary.BigEndian.Uint16(src[2:])
offsetLookupTable := int(binary.BigEndian.Uint32(src[4:]))
offsetGlyphDataTable := int(binary.BigEndian.Uint32(src[8:]))
n += 12
{
if offsetLookupTable != 0 { // ignore null offset
if L := len(src); L < offsetLookupTable {
return item, 0, fmt.Errorf("reading Ankr: "+"EOF: expected length: %d, got %d", offsetLookupTable, L)
}
var (
err error
read int
)
item.lookupTable, read, err = ParseAATLookup(src[offsetLookupTable:], valuesCount)
if err != nil {
return item, 0, fmt.Errorf("reading Ankr: %s", err)
}
offsetLookupTable += read
}
}
{
if offsetGlyphDataTable != 0 { // ignore null offset
if L := len(src); L < offsetGlyphDataTable {
return item, 0, fmt.Errorf("reading Ankr: "+"EOF: expected length: %d, got %d", offsetGlyphDataTable, L)
}
item.glyphDataTable = src[offsetGlyphDataTable:]
}
}
return item, n, nil
}
func ParseAnkrAnchor(src []byte) (AnkrAnchor, int, error) {
var item AnkrAnchor
n := 0
if L := len(src); L < 4 {
return item, 0, fmt.Errorf("reading AnkrAnchor: "+"EOF: expected length: 4, got %d", L)
}
item.mustParse(src)
n += 4
return item, n, nil
}
func (item *binSearchHeader) mustParse(src []byte) {
_ = src[9] // early bound checking
item.unitSize = binary.BigEndian.Uint16(src[0:])
item.nUnits = binary.BigEndian.Uint16(src[2:])
item.searchRange = binary.BigEndian.Uint16(src[4:])
item.entrySelector = binary.BigEndian.Uint16(src[6:])
item.rangeShift = binary.BigEndian.Uint16(src[8:])
}
func (item *loopkupRecord6) mustParse(src []byte) {
_ = src[3] // early bound checking
item.Glyph = binary.BigEndian.Uint16(src[0:])
item.Value = binary.BigEndian.Uint16(src[2:])
}
@@ -0,0 +1,44 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import "encoding/binary"
// Ankr is the anchor point table
// See - https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6ankr.html
type Ankr struct {
version uint16 // Version number (set to zero)
flags uint16 // Flags (currently unused; set to zero)
// Offset to the table's lookup table; currently this is always 0x0000000C
// The lookup table returns uint16 offset from the beginning of the glyph data table, not indices.
lookupTable AATLookup `offsetSize:"Offset32"`
// Offset to the glyph data table
glyphDataTable []byte `offsetSize:"Offset32" arrayCount:"ToEnd"`
}
// GetAnchor return the i-th anchor for `glyph`, or {0,0} if not found.
func (ank Ankr) GetAnchor(glyph GlyphID, index int) (anchor AnkrAnchor) {
offset, ok := ank.lookupTable.Class(glyph)
if !ok || int(offset)+4 >= len(ank.glyphDataTable) {
return anchor
}
count := int(binary.BigEndian.Uint32(ank.glyphDataTable[offset:]))
if index >= count {
return anchor // invalid index
}
indexStart := int(offset) + 4 + 4*index
if len(ank.glyphDataTable) < indexStart+4 {
return anchor // invalid table
}
anchor.X = int16(binary.BigEndian.Uint16(ank.glyphDataTable[indexStart:]))
anchor.Y = int16(binary.BigEndian.Uint16(ank.glyphDataTable[indexStart+2:]))
return anchor
}
// AnkrAnchor is a point within the coordinate space of a given glyph
// independent of the control points used to render the glyph
type AnkrAnchor struct {
X, Y int16
}
@@ -0,0 +1,452 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import (
"encoding/binary"
"fmt"
)
// AAT layout
// State table header, without the actual data
// See https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6Tables.html
type AATStateTable struct {
StateSize uint16 // Size of a state, in bytes. The size is limited to 8 bits, although the field is 16 bits for alignment.
ClassTable ClassTable `offsetSize:"Offset16"` // Byte offset from the beginning of the state table to the class subtable.
stateArray Offset16 // Byte offset from the beginning of the state table to the state array.
entryTable Offset16 // Byte offset from the beginning of the state table to the entry subtable.
States [][]uint8 `isOpaque:""`
Entries []AATStateEntry `isOpaque:""` // entry data are empty
}
func (state *AATStateTable) parseStates(src []byte) error {
if state.stateArray > state.entryTable {
return fmt.Errorf("invalid AAT state offsets (%d > %d)", state.stateArray, state.entryTable)
}
if L := len(src); L < int(state.entryTable) {
return fmt.Errorf("EOF: expected length: %d, got %d", state.entryTable, L)
}
states := src[state.stateArray:state.entryTable]
nC := int(state.StateSize)
// Ensure pre-defined classes fit.
if nC < 4 {
return fmt.Errorf("invalid number of classes in AAT state table: %d", nC)
}
state.States = make([][]uint8, len(states)/nC)
for i := range state.States {
state.States[i] = states[i*nC : (i+1)*nC]
}
return nil
}
func (state *AATStateTable) parseEntries(src []byte) (int, error) {
// find max index
var maxi uint8
for _, l := range state.States {
for _, stateIndex := range l {
if stateIndex > maxi {
maxi = stateIndex
}
}
}
src = src[state.entryTable:] // checked in parseStates
count := int(maxi) + 1
var err error
state.Entries, err = parseAATStateEntries(src, count, 0)
if err != nil {
return 0, err
}
// newState is an offset: convert back to index
for i, entry := range state.Entries {
state.Entries[i].NewState = uint16((int(entry.NewState) - int(state.stateArray)) / int(state.StateSize))
}
// the own header data stop at the entryTable offset
return 8, err
}
// src starts at the entryTable
func parseAATStateEntries(src []byte, count, entryDataSize int) ([]AATStateEntry, error) {
entrySize := 4 + entryDataSize
if L := len(src); L < count*entrySize {
return nil, fmt.Errorf("EOF: expected length: %d, got %d", count*entrySize, L)
}
out := make([]AATStateEntry, count)
for i := range out {
out[i].NewState = binary.BigEndian.Uint16(src[i*entrySize:])
out[i].Flags = binary.BigEndian.Uint16(src[i*entrySize+2:])
copy(out[i].data[:], src[i*entrySize+4:(i+1)*entrySize])
}
return out, nil
}
// ClassTable is the same as AATLookup8, but with no format and with bytes instead of uint16s
type ClassTable struct {
StartGlyph GlyphID
Values []byte `arrayCount:"FirstUint16"`
}
// Extended state table, including the data
// See https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6Tables.html - State tables
// binarygen: argument=entryDataSize int
type AATStateTableExt struct {
StateSize uint32 // Size of a state, in bytes. The size is limited to 8 bits, although the field is 16 bits for alignment.
Class AATLookup `offsetSize:"Offset32"` // Byte offset from the beginning of the state table to the class subtable.
stateArray Offset32 // Byte offset from the beginning of the state table to the state array.
entryTable Offset32 // Byte offset from the beginning of the state table to the entry subtable.
States [][]uint16 `isOpaque:""` // each sub array has length stateSize
Entries []AATStateEntry `isOpaque:""` // length is the maximum state + 1
}
func (state *AATStateTableExt) parseStates(src []byte, _, _ int) error {
if state.stateArray > state.entryTable {
return fmt.Errorf("invalid AAT state offsets (%d > %d)", state.stateArray, state.entryTable)
}
if L := len(src); L < int(state.entryTable) {
return fmt.Errorf("EOF: expected length: %d, got %d", state.entryTable, L)
}
statesArray := src[state.stateArray:state.entryTable]
states, err := ParseUint16s(statesArray, len(statesArray)/2)
if err != nil {
return err
}
nC := int(state.StateSize)
// Ensure pre-defined classes fit.
if nC < 4 {
return fmt.Errorf("invalid number of classes in AAT state table: %d", nC)
}
state.States = make([][]uint16, len(states)/nC)
for i := range state.States {
state.States[i] = states[i*nC : (i+1)*nC]
}
return nil
}
func (state *AATStateTableExt) parseEntries(src []byte, _, entryDataSize int) (int, error) {
// find max index
var maxi uint16
for _, l := range state.States {
for _, stateIndex := range l {
if stateIndex > maxi {
maxi = stateIndex
}
}
}
src = src[state.entryTable:] // checked in parseStates
count := int(maxi) + 1
var err error
state.Entries, err = parseAATStateEntries(src, count, entryDataSize)
// the own header data stop at the entryTable offset
return 16, err
}
// AATStateEntry is shared between old and extended state tables,
// and between the different kind of entries.
// See the various AsXXX() methods.
type AATStateEntry struct {
NewState uint16
Flags uint16 // Table specific.
data [4]byte // Table specific.
}
// AsMorxContextual reads the internal data for entries in morx contextual subtable.
// The returned indexes use 0xFFFF as empty value.
func (e AATStateEntry) AsMorxContextual() (markIndex, currentIndex uint16) {
markIndex = binary.BigEndian.Uint16(e.data[:])
currentIndex = binary.BigEndian.Uint16(e.data[2:])
return
}
// AsMorxInsertion reads the internal data for entries in morx insertion subtable.
// The returned indexes use 0xFFFF as empty value.
func (e AATStateEntry) AsMorxInsertion() (currentIndex, markedIndex uint16) {
currentIndex = binary.BigEndian.Uint16(e.data[:])
markedIndex = binary.BigEndian.Uint16(e.data[2:])
return
}
// AsMorxLigature reads the internal data for entries in morx ligature subtable.
func (e AATStateEntry) AsMorxLigature() (ligActionIndex uint16) {
return binary.BigEndian.Uint16(e.data[:])
}
// AsKernxIndex reads the internal data for entries in 'kern/x' subtable format 1 or 4.
// An entry with no index returns 0xFFFF
func (e AATStateEntry) AsKernxIndex() uint16 {
// for kern table, during parsing, we store the resolved index
// at the same place as kerx tables
return binary.BigEndian.Uint16(e.data[:])
}
type binSearchHeader struct {
unitSize uint16
nUnits uint16
searchRange uint16 // The value of unitSize times the largest power of 2 that is less than or equal to the value of nUnits.
entrySelector uint16 // The log base 2 of the largest power of 2 less than or equal to the value of nUnits.
rangeShift uint16 // The value of unitSize times the difference of the value of nUnits minus the largest power of 2 less than or equal to the value of nUnits.
}
// AATLookup is conceptually a map[GlyphID]uint16, but it may
// be implemented more efficiently.
type AATLookup interface {
AatLookupMixed
isAATLookup()
// Class returns the class ID for the provided glyph, or (0, false)
// for glyphs not covered by this class.
Class(g GlyphID) (uint16, bool)
}
func (AATLoopkup0) isAATLookup() {}
func (AATLoopkup2) isAATLookup() {}
func (AATLoopkup4) isAATLookup() {}
func (AATLoopkup6) isAATLookup() {}
func (AATLoopkup8) isAATLookup() {}
func (AATLoopkup10) isAATLookup() {}
type AATLoopkup0 struct {
version uint16 `unionTag:"0"`
Values []uint16 `arrayCount:""`
}
type AATLoopkup2 struct {
version uint16 `unionTag:"2"`
binSearchHeader
Records []LookupRecord2 `arrayCount:"ComputedField-nUnits"`
}
type LookupRecord2 struct {
LastGlyph GlyphID
FirstGlyph GlyphID
Value uint16
}
type AATLoopkup4 struct {
version uint16 `unionTag:"4"`
binSearchHeader
// Do not include the termination segment
Records []AATLookupRecord4 `arrayCount:"ComputedField-nUnits-1"`
}
type AATLookupRecord4 struct {
LastGlyph GlyphID
FirstGlyph GlyphID
// offset to an array of []uint16 (or []uint32 for extended) with length last - first + 1
Values []uint16 `offsetSize:"Offset16" offsetRelativeTo:"Parent" arrayCount:"ComputedField-nValues()"`
}
func (lk AATLookupRecord4) nValues() int { return int(lk.LastGlyph) - int(lk.FirstGlyph) + 1 }
type AATLoopkup6 struct {
version uint16 `unionTag:"6"`
binSearchHeader
Records []loopkupRecord6 `arrayCount:"ComputedField-nUnits"`
}
type loopkupRecord6 struct {
Glyph GlyphID
Value uint16
}
type AATLoopkup8 struct {
version uint16 `unionTag:"8"`
AATLoopkup8Data
}
type AATLoopkup8Data struct {
FirstGlyph GlyphID
Values []uint16 `arrayCount:"FirstUint16"`
}
type AATLoopkup10 struct {
version uint16 `unionTag:"10"`
unitSize uint16
FirstGlyph GlyphID
Values []uint16 `arrayCount:"FirstUint16"`
}
// extended versions
// AATLookupExt is the same as AATLookup, but class values are uint32
type AATLookupExt interface {
AatLookupMixed
isAATLookupExt()
// Class returns the class ID for the provided glyph, or (0, false)
// for glyphs not covered by this class.
Class(g GlyphID) (uint32, bool)
}
func (AATLoopkupExt0) isAATLookupExt() {}
func (AATLoopkupExt2) isAATLookupExt() {}
func (AATLoopkupExt4) isAATLookupExt() {}
func (AATLoopkupExt6) isAATLookupExt() {}
func (AATLoopkupExt8) isAATLookupExt() {}
func (AATLoopkupExt10) isAATLookupExt() {}
type AATLoopkupExt0 struct {
version uint16 `unionTag:"0"`
Values []uint32 `arrayCount:""`
}
type AATLoopkupExt2 struct {
version uint16 `unionTag:"2"`
binSearchHeader
Records []lookupRecordExt2 `arrayCount:"ComputedField-nUnits"`
}
type lookupRecordExt2 struct {
LastGlyph GlyphID
FirstGlyph GlyphID
Value uint32
}
type AATLoopkupExt4 struct {
version uint16 `unionTag:"4"`
binSearchHeader
// the values pointed by the record are uint32
Records []loopkupRecordExt4 `arrayCount:"ComputedField-nUnits"`
}
type loopkupRecordExt4 struct {
LastGlyph GlyphID
FirstGlyph GlyphID
// offset to an array of []uint16 (or []uint32 for extended) with length last - first + 1
Values []uint32 `offsetSize:"Offset16" offsetRelativeTo:"Parent" arrayCount:"ComputedField-nValues()"`
}
func (lk loopkupRecordExt4) nValues() int { return int(lk.LastGlyph) - int(lk.FirstGlyph) + 1 }
type AATLoopkupExt6 struct {
version uint16 `unionTag:"6"`
binSearchHeader
Records []loopkupRecordExt6 `arrayCount:"ComputedField-nUnits"`
}
type loopkupRecordExt6 struct {
Glyph GlyphID
Value uint32
}
type AATLoopkupExt8 AATLoopkup8
type AATLoopkupExt10 struct {
version uint16 `unionTag:"10"`
unitSize uint16
FirstGlyph GlyphID
Values []uint32 `arrayCount:"FirstUint16"`
}
func (src AATLoopkup0) Coverage() [][2]GlyphID {
return [][2]GlyphID{{0, GlyphID(len(src.Values) - 1)}}
}
func (src AATLoopkupExt0) Coverage() [][2]GlyphID {
return [][2]GlyphID{{0, GlyphID(len(src.Values) - 1)}}
}
func (src AATLoopkup2) Coverage() [][2]GlyphID {
out := make([][2]GlyphID, 0, len(src.Records))
for _, record := range src.Records {
if record.FirstGlyph == 0xFFFF {
continue
}
out = append(out, [2]GlyphID{record.FirstGlyph, record.LastGlyph})
}
return out
}
func (src AATLoopkupExt2) Coverage() [][2]GlyphID {
out := make([][2]GlyphID, 0, len(src.Records))
for _, record := range src.Records {
if record.FirstGlyph == 0xFFFF {
continue
}
out = append(out, [2]GlyphID{record.FirstGlyph, record.LastGlyph})
}
return out
}
func (src AATLoopkup4) Coverage() [][2]GlyphID {
out := make([][2]GlyphID, 0, len(src.Records))
for _, record := range src.Records {
if record.FirstGlyph == 0xFFFF {
continue
}
out = append(out, [2]GlyphID{record.FirstGlyph, record.LastGlyph})
}
return out
}
func (src AATLoopkupExt4) Coverage() [][2]GlyphID {
out := make([][2]GlyphID, 0, len(src.Records))
for _, record := range src.Records {
if record.FirstGlyph == 0xFFFF {
continue
}
out = append(out, [2]GlyphID{record.FirstGlyph, record.LastGlyph})
}
return out
}
func (src AATLoopkup6) Coverage() [][2]GlyphID {
out := make([][2]GlyphID, 0, len(src.Records))
for _, record := range src.Records {
if record.Glyph == 0xFFFF {
continue
}
out = append(out, [2]GlyphID{record.Glyph, record.Glyph})
}
return out
}
func (src AATLoopkupExt6) Coverage() [][2]GlyphID {
out := make([][2]GlyphID, 0, len(src.Records))
for _, record := range src.Records {
if record.Glyph == 0xFFFF {
continue
}
out = append(out, [2]GlyphID{record.Glyph, record.Glyph})
}
return out
}
func (src AATLoopkup8) Coverage() [][2]GlyphID {
if len(src.Values) == 0 || src.FirstGlyph == 0xFFFF {
return nil
}
return [][2]GlyphID{{src.FirstGlyph, src.FirstGlyph + GlyphID(len(src.Values)-1)}}
}
func (src AATLoopkupExt8) Coverage() [][2]GlyphID {
if len(src.Values) == 0 || src.FirstGlyph == 0xFFFF {
return nil
}
return [][2]GlyphID{{src.FirstGlyph, src.FirstGlyph + GlyphID(len(src.Values)-1)}}
}
func (src AATLoopkup10) Coverage() [][2]GlyphID {
if len(src.Values) == 0 || src.FirstGlyph == 0xFFFF {
return nil
}
return [][2]GlyphID{{src.FirstGlyph, src.FirstGlyph + GlyphID(len(src.Values)-1)}}
}
func (src AATLoopkupExt10) Coverage() [][2]GlyphID {
if len(src.Values) == 0 || src.FirstGlyph == 0xFFFF {
return nil
}
return [][2]GlyphID{{src.FirstGlyph, src.FirstGlyph + GlyphID(len(src.Values)-1)}}
}
@@ -0,0 +1,82 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import (
"encoding/binary"
"fmt"
)
// Code generated by binarygen from aat_feat_src.go. DO NOT EDIT
func (item *FeatureSettingName) mustParse(src []byte) {
_ = src[3] // early bound checking
item.Setting = binary.BigEndian.Uint16(src[0:])
item.NameIndex = binary.BigEndian.Uint16(src[2:])
}
func ParseFeat(src []byte) (Feat, int, error) {
var item Feat
n := 0
if L := len(src); L < 12 {
return item, 0, fmt.Errorf("reading Feat: "+"EOF: expected length: 12, got %d", L)
}
_ = src[11] // early bound checking
item.version = binary.BigEndian.Uint32(src[0:])
item.featureNameCount = binary.BigEndian.Uint16(src[4:])
item.none1 = binary.BigEndian.Uint16(src[6:])
item.none2 = binary.BigEndian.Uint32(src[8:])
n += 12
{
arrayLength := int(item.featureNameCount)
offset := 12
for i := 0; i < arrayLength; i++ {
elem, read, err := ParseFeatureName(src[offset:], src)
if err != nil {
return item, 0, fmt.Errorf("reading Feat: %s", err)
}
item.Names = append(item.Names, elem)
offset += read
}
n = offset
}
return item, n, nil
}
func ParseFeatureName(src []byte, parentSrc []byte) (FeatureName, int, error) {
var item FeatureName
n := 0
if L := len(src); L < 12 {
return item, 0, fmt.Errorf("reading FeatureName: "+"EOF: expected length: 12, got %d", L)
}
_ = src[11] // early bound checking
item.Feature = binary.BigEndian.Uint16(src[0:])
item.nSettings = binary.BigEndian.Uint16(src[2:])
offsetSettingTable := int(binary.BigEndian.Uint32(src[4:]))
item.FeatureFlags = binary.BigEndian.Uint16(src[8:])
item.NameIndex = binary.BigEndian.Uint16(src[10:])
n += 12
{
if offsetSettingTable != 0 { // ignore null offset
if L := len(parentSrc); L < offsetSettingTable {
return item, 0, fmt.Errorf("reading FeatureName: "+"EOF: expected length: %d, got %d", offsetSettingTable, L)
}
arrayLength := int(item.nSettings)
if L := len(parentSrc); L < offsetSettingTable+arrayLength*4 {
return item, 0, fmt.Errorf("reading FeatureName: "+"EOF: expected length: %d, got %d", offsetSettingTable+arrayLength*4, L)
}
item.SettingTable = make([]FeatureSettingName, arrayLength) // allocation guarded by the previous check
for i := range item.SettingTable {
item.SettingTable[i].mustParse(parentSrc[offsetSettingTable+i*4:])
}
offsetSettingTable += arrayLength * 4
}
}
return item, n, nil
}
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
// Feat is the feature name table.
// See - https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6feat.html
type Feat struct {
version uint32 // Version number of the feature name table (0x00010000 for the current version).
featureNameCount uint16 // The number of entries in the feature name array.
none1 uint16 // Reserved (set to zero).
none2 uint32 // Reserved (set to zero).
Names []FeatureName `arrayCount:"ComputedField-featureNameCount"` // The feature name array.
}
type FeatureName struct {
Feature uint16 // Feature type.
nSettings uint16 // The number of records in the setting name array.
SettingTable []FeatureSettingName `offsetSize:"Offset32" offsetRelativeTo:"Parent" arrayCount:"ComputedField-nSettings"` // Offset in bytes from the beginning of the 'feat' table to this feature's setting name array. The actual type of record this offset refers to will depend on the exclusivity value, as described below.
FeatureFlags uint16 // Single-bit flags associated with the feature type.
NameIndex uint16 // The name table index for the feature's name. This index has values greater than 255 and less than 32768.
}
type FeatureSettingName struct {
Setting uint16 // The setting.
NameIndex uint16 // The name table index for the setting's name. The nameIndex must be greater than 255 and less than 32768.
}
@@ -0,0 +1,642 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import (
"encoding/binary"
"fmt"
)
// Code generated by binarygen from aat_kerx_src.go. DO NOT EDIT
func (item *KAAnchor) mustParse(src []byte) {
_ = src[3] // early bound checking
item.Mark = binary.BigEndian.Uint16(src[0:])
item.Current = binary.BigEndian.Uint16(src[2:])
}
func (item *KAControl) mustParse(src []byte) {
_ = src[3] // early bound checking
item.Mark = binary.BigEndian.Uint16(src[0:])
item.Current = binary.BigEndian.Uint16(src[2:])
}
func (item *KACoordinates) mustParse(src []byte) {
_ = src[7] // early bound checking
item.MarkX = int16(binary.BigEndian.Uint16(src[0:]))
item.MarkY = int16(binary.BigEndian.Uint16(src[2:]))
item.CurrentX = int16(binary.BigEndian.Uint16(src[4:]))
item.CurrentY = int16(binary.BigEndian.Uint16(src[6:]))
}
func (item *Kernx0Record) mustParse(src []byte) {
_ = src[5] // early bound checking
item.Left = binary.BigEndian.Uint16(src[0:])
item.Right = binary.BigEndian.Uint16(src[2:])
item.Value = int16(binary.BigEndian.Uint16(src[4:]))
}
func ParseAATLookupExt(src []byte, valuesCount int) (AATLookupExt, int, error) {
var item AATLookupExt
if L := len(src); L < 2 {
return item, 0, fmt.Errorf("reading AATLookupExt: "+"EOF: expected length: 2, got %d", L)
}
format := uint16(binary.BigEndian.Uint16(src[0:]))
var (
read int
err error
)
switch format {
case 0:
item, read, err = ParseAATLoopkupExt0(src[0:], valuesCount)
case 10:
item, read, err = ParseAATLoopkupExt10(src[0:])
case 2:
item, read, err = ParseAATLoopkupExt2(src[0:])
case 4:
item, read, err = ParseAATLoopkupExt4(src[0:])
case 6:
item, read, err = ParseAATLoopkupExt6(src[0:])
case 8:
item, read, err = ParseAATLoopkupExt8(src[0:])
default:
err = fmt.Errorf("unsupported AATLookupExt format %d", format)
}
if err != nil {
return item, 0, fmt.Errorf("reading AATLookupExt: %s", err)
}
return item, read, nil
}
func ParseAATLoopkupExt0(src []byte, valuesCount int) (AATLoopkupExt0, int, error) {
var item AATLoopkupExt0
n := 0
if L := len(src); L < 2 {
return item, 0, fmt.Errorf("reading AATLoopkupExt0: "+"EOF: expected length: 2, got %d", L)
}
item.version = binary.BigEndian.Uint16(src[0:])
n += 2
{
if L := len(src); L < 2+valuesCount*4 {
return item, 0, fmt.Errorf("reading AATLoopkupExt0: "+"EOF: expected length: %d, got %d", 2+valuesCount*4, L)
}
item.Values = make([]uint32, valuesCount) // allocation guarded by the previous check
for i := range item.Values {
item.Values[i] = binary.BigEndian.Uint32(src[2+i*4:])
}
n += valuesCount * 4
}
return item, n, nil
}
func ParseAATLoopkupExt10(src []byte) (AATLoopkupExt10, int, error) {
var item AATLoopkupExt10
n := 0
if L := len(src); L < 8 {
return item, 0, fmt.Errorf("reading AATLoopkupExt10: "+"EOF: expected length: 8, got %d", L)
}
_ = src[7] // early bound checking
item.version = binary.BigEndian.Uint16(src[0:])
item.unitSize = binary.BigEndian.Uint16(src[2:])
item.FirstGlyph = binary.BigEndian.Uint16(src[4:])
arrayLengthValues := int(binary.BigEndian.Uint16(src[6:]))
n += 8
{
if L := len(src); L < 8+arrayLengthValues*4 {
return item, 0, fmt.Errorf("reading AATLoopkupExt10: "+"EOF: expected length: %d, got %d", 8+arrayLengthValues*4, L)
}
item.Values = make([]uint32, arrayLengthValues) // allocation guarded by the previous check
for i := range item.Values {
item.Values[i] = binary.BigEndian.Uint32(src[8+i*4:])
}
n += arrayLengthValues * 4
}
return item, n, nil
}
func ParseAATLoopkupExt2(src []byte) (AATLoopkupExt2, int, error) {
var item AATLoopkupExt2
n := 0
if L := len(src); L < 12 {
return item, 0, fmt.Errorf("reading AATLoopkupExt2: "+"EOF: expected length: 12, got %d", L)
}
_ = src[11] // early bound checking
item.version = binary.BigEndian.Uint16(src[0:])
item.binSearchHeader.mustParse(src[2:])
n += 12
{
arrayLength := int(item.nUnits)
if L := len(src); L < 12+arrayLength*8 {
return item, 0, fmt.Errorf("reading AATLoopkupExt2: "+"EOF: expected length: %d, got %d", 12+arrayLength*8, L)
}
item.Records = make([]lookupRecordExt2, arrayLength) // allocation guarded by the previous check
for i := range item.Records {
item.Records[i].mustParse(src[12+i*8:])
}
n += arrayLength * 8
}
return item, n, nil
}
func ParseAATLoopkupExt4(src []byte) (AATLoopkupExt4, int, error) {
var item AATLoopkupExt4
n := 0
if L := len(src); L < 12 {
return item, 0, fmt.Errorf("reading AATLoopkupExt4: "+"EOF: expected length: 12, got %d", L)
}
_ = src[11] // early bound checking
item.version = binary.BigEndian.Uint16(src[0:])
item.binSearchHeader.mustParse(src[2:])
n += 12
{
arrayLength := int(item.nUnits)
offset := 12
for i := 0; i < arrayLength; i++ {
elem, read, err := parseLoopkupRecordExt4(src[offset:], src)
if err != nil {
return item, 0, fmt.Errorf("reading AATLoopkupExt4: %s", err)
}
item.Records = append(item.Records, elem)
offset += read
}
n = offset
}
return item, n, nil
}
func ParseAATLoopkupExt6(src []byte) (AATLoopkupExt6, int, error) {
var item AATLoopkupExt6
n := 0
if L := len(src); L < 12 {
return item, 0, fmt.Errorf("reading AATLoopkupExt6: "+"EOF: expected length: 12, got %d", L)
}
_ = src[11] // early bound checking
item.version = binary.BigEndian.Uint16(src[0:])
item.binSearchHeader.mustParse(src[2:])
n += 12
{
arrayLength := int(item.nUnits)
if L := len(src); L < 12+arrayLength*6 {
return item, 0, fmt.Errorf("reading AATLoopkupExt6: "+"EOF: expected length: %d, got %d", 12+arrayLength*6, L)
}
item.Records = make([]loopkupRecordExt6, arrayLength) // allocation guarded by the previous check
for i := range item.Records {
item.Records[i].mustParse(src[12+i*6:])
}
n += arrayLength * 6
}
return item, n, nil
}
func ParseAATLoopkupExt8(src []byte) (AATLoopkupExt8, int, error) {
var item AATLoopkupExt8
n := 0
if L := len(src); L < 2 {
return item, 0, fmt.Errorf("reading AATLoopkupExt8: "+"EOF: expected length: 2, got %d", L)
}
item.version = binary.BigEndian.Uint16(src[0:])
n += 2
{
var (
err error
read int
)
item.AATLoopkup8Data, read, err = ParseAATLoopkup8Data(src[2:])
if err != nil {
return item, 0, fmt.Errorf("reading AATLoopkupExt8: %s", err)
}
n += read
}
return item, n, nil
}
func ParseAATStateTableExt(src []byte, valuesCount int, entryDataSize int) (AATStateTableExt, int, error) {
var item AATStateTableExt
n := 0
if L := len(src); L < 16 {
return item, 0, fmt.Errorf("reading AATStateTableExt: "+"EOF: expected length: 16, got %d", L)
}
_ = src[15] // early bound checking
item.StateSize = binary.BigEndian.Uint32(src[0:])
offsetClass := int(binary.BigEndian.Uint32(src[4:]))
item.stateArray = Offset32(binary.BigEndian.Uint32(src[8:]))
item.entryTable = Offset32(binary.BigEndian.Uint32(src[12:]))
n += 16
{
if offsetClass != 0 { // ignore null offset
if L := len(src); L < offsetClass {
return item, 0, fmt.Errorf("reading AATStateTableExt: "+"EOF: expected length: %d, got %d", offsetClass, L)
}
var (
err error
read int
)
item.Class, read, err = ParseAATLookup(src[offsetClass:], valuesCount)
if err != nil {
return item, 0, fmt.Errorf("reading AATStateTableExt: %s", err)
}
offsetClass += read
}
}
{
err := item.parseStates(src[:], valuesCount, entryDataSize)
if err != nil {
return item, 0, fmt.Errorf("reading AATStateTableExt: %s", err)
}
}
{
read, err := item.parseEntries(src[:], valuesCount, entryDataSize)
if err != nil {
return item, 0, fmt.Errorf("reading AATStateTableExt: %s", err)
}
n = read
}
return item, n, nil
}
func ParseKerx(src []byte, valuesCount int) (Kerx, int, error) {
var item Kerx
n := 0
if L := len(src); L < 8 {
return item, 0, fmt.Errorf("reading Kerx: "+"EOF: expected length: 8, got %d", L)
}
_ = src[7] // early bound checking
item.version = binary.BigEndian.Uint16(src[0:])
item.padding = binary.BigEndian.Uint16(src[2:])
item.nTables = binary.BigEndian.Uint32(src[4:])
n += 8
{
arrayLength := int(item.nTables)
offset := 8
for i := 0; i < arrayLength; i++ {
elem, read, err := ParseKerxSubtable(src[offset:], valuesCount)
if err != nil {
return item, 0, fmt.Errorf("reading Kerx: %s", err)
}
item.Tables = append(item.Tables, elem)
offset += read
}
n = offset
}
return item, n, nil
}
func ParseKerxAnchorAnchors(src []byte, anchorsCount int) (KerxAnchorAnchors, int, error) {
var item KerxAnchorAnchors
n := 0
{
if L := len(src); L < anchorsCount*4 {
return item, 0, fmt.Errorf("reading KerxAnchorAnchors: "+"EOF: expected length: %d, got %d", anchorsCount*4, L)
}
item.Anchors = make([]KAAnchor, anchorsCount) // allocation guarded by the previous check
for i := range item.Anchors {
item.Anchors[i].mustParse(src[i*4:])
}
n += anchorsCount * 4
}
return item, n, nil
}
func ParseKerxAnchorControls(src []byte, anchorsCount int) (KerxAnchorControls, int, error) {
var item KerxAnchorControls
n := 0
{
if L := len(src); L < anchorsCount*4 {
return item, 0, fmt.Errorf("reading KerxAnchorControls: "+"EOF: expected length: %d, got %d", anchorsCount*4, L)
}
item.Anchors = make([]KAControl, anchorsCount) // allocation guarded by the previous check
for i := range item.Anchors {
item.Anchors[i].mustParse(src[i*4:])
}
n += anchorsCount * 4
}
return item, n, nil
}
func ParseKerxAnchorCoordinates(src []byte, anchorsCount int) (KerxAnchorCoordinates, int, error) {
var item KerxAnchorCoordinates
n := 0
{
if L := len(src); L < anchorsCount*8 {
return item, 0, fmt.Errorf("reading KerxAnchorCoordinates: "+"EOF: expected length: %d, got %d", anchorsCount*8, L)
}
item.Anchors = make([]KACoordinates, anchorsCount) // allocation guarded by the previous check
for i := range item.Anchors {
item.Anchors[i].mustParse(src[i*8:])
}
n += anchorsCount * 8
}
return item, n, nil
}
func ParseKerxData0(src []byte, tupleCount int) (KerxData0, int, error) {
var item KerxData0
n := 0
if L := len(src); L < 16 {
return item, 0, fmt.Errorf("reading KerxData0: "+"EOF: expected length: 16, got %d", L)
}
_ = src[15] // early bound checking
item.nPairs = binary.BigEndian.Uint32(src[0:])
item.searchRange = binary.BigEndian.Uint32(src[4:])
item.entrySelector = binary.BigEndian.Uint32(src[8:])
item.rangeShift = binary.BigEndian.Uint32(src[12:])
n += 16
{
arrayLength := int(item.nPairs)
if L := len(src); L < 16+arrayLength*6 {
return item, 0, fmt.Errorf("reading KerxData0: "+"EOF: expected length: %d, got %d", 16+arrayLength*6, L)
}
item.Pairs = make([]Kernx0Record, arrayLength) // allocation guarded by the previous check
for i := range item.Pairs {
item.Pairs[i].mustParse(src[16+i*6:])
}
n += arrayLength * 6
}
var err error
n, err = item.parseEnd(src, tupleCount)
if err != nil {
return item, 0, fmt.Errorf("reading KerxData0: %s", err)
}
return item, n, nil
}
func ParseKerxData1(src []byte, tupleCount int, valuesCount int) (KerxData1, int, error) {
var item KerxData1
n := 0
{
var (
err error
read int
)
item.AATStateTableExt, read, err = ParseAATStateTableExt(src[0:], int(valuesCount), int(2))
if err != nil {
return item, 0, fmt.Errorf("reading KerxData1: %s", err)
}
n += read
}
if L := len(src); L < n+4 {
return item, 0, fmt.Errorf("reading KerxData1: "+"EOF: expected length: n + 4, got %d", L)
}
item.valueTable = Offset32(binary.BigEndian.Uint32(src[n:]))
n += 4
{
err := item.parseValues(src[:], tupleCount, valuesCount)
if err != nil {
return item, 0, fmt.Errorf("reading KerxData1: %s", err)
}
}
return item, n, nil
}
func ParseKerxData2(src []byte, parentSrc []byte, valuesCount int) (KerxData2, int, error) {
var item KerxData2
n := 0
if L := len(src); L < 16 {
return item, 0, fmt.Errorf("reading KerxData2: "+"EOF: expected length: 16, got %d", L)
}
_ = src[15] // early bound checking
item.rowWidth = binary.BigEndian.Uint32(src[0:])
offsetLeft := int(binary.BigEndian.Uint32(src[4:]))
offsetRight := int(binary.BigEndian.Uint32(src[8:]))
item.KerningStart = Offset32(binary.BigEndian.Uint32(src[12:]))
n += 16
{
if offsetLeft != 0 { // ignore null offset
if L := len(parentSrc); L < offsetLeft {
return item, 0, fmt.Errorf("reading KerxData2: "+"EOF: expected length: %d, got %d", offsetLeft, L)
}
var (
err error
read int
)
item.Left, read, err = ParseAATLookup(parentSrc[offsetLeft:], valuesCount)
if err != nil {
return item, 0, fmt.Errorf("reading KerxData2: %s", err)
}
offsetLeft += read
}
}
{
if offsetRight != 0 { // ignore null offset
if L := len(parentSrc); L < offsetRight {
return item, 0, fmt.Errorf("reading KerxData2: "+"EOF: expected length: %d, got %d", offsetRight, L)
}
var (
err error
read int
)
item.Right, read, err = ParseAATLookup(parentSrc[offsetRight:], valuesCount)
if err != nil {
return item, 0, fmt.Errorf("reading KerxData2: %s", err)
}
offsetRight += read
}
}
{
item.KerningData = src[0:]
}
return item, n, nil
}
func ParseKerxData4(src []byte, valuesCount int) (KerxData4, int, error) {
var item KerxData4
n := 0
{
var (
err error
read int
)
item.AATStateTableExt, read, err = ParseAATStateTableExt(src[0:], int(valuesCount), int(2))
if err != nil {
return item, 0, fmt.Errorf("reading KerxData4: %s", err)
}
n += read
}
if L := len(src); L < n+4 {
return item, 0, fmt.Errorf("reading KerxData4: "+"EOF: expected length: n + 4, got %d", L)
}
item.Flags = binary.BigEndian.Uint32(src[n:])
n += 4
{
err := item.parseAnchors(src[:], valuesCount)
if err != nil {
return item, 0, fmt.Errorf("reading KerxData4: %s", err)
}
}
return item, n, nil
}
func ParseKerxData6(src []byte, parentSrc []byte, tupleCount int, valuesCount int) (KerxData6, int, error) {
var item KerxData6
n := 0
if L := len(src); L < 24 {
return item, 0, fmt.Errorf("reading KerxData6: "+"EOF: expected length: 24, got %d", L)
}
_ = src[23] // early bound checking
item.flags = binary.BigEndian.Uint32(src[0:])
item.rowCount = binary.BigEndian.Uint16(src[4:])
item.columnCount = binary.BigEndian.Uint16(src[6:])
item.rowIndexTableOffset = binary.BigEndian.Uint32(src[8:])
item.columnIndexTableOffset = binary.BigEndian.Uint32(src[12:])
item.kerningArrayOffset = binary.BigEndian.Uint32(src[16:])
item.kerningVectorOffset = binary.BigEndian.Uint32(src[20:])
n += 24
{
err := item.parseRow(src[:], parentSrc, tupleCount, valuesCount)
if err != nil {
return item, 0, fmt.Errorf("reading KerxData6: %s", err)
}
}
{
err := item.parseColumn(src[:], parentSrc, tupleCount, valuesCount)
if err != nil {
return item, 0, fmt.Errorf("reading KerxData6: %s", err)
}
}
{
err := item.parseKernings(src[:], parentSrc, tupleCount, valuesCount)
if err != nil {
return item, 0, fmt.Errorf("reading KerxData6: %s", err)
}
}
return item, n, nil
}
func ParseKerxSubtable(src []byte, valuesCount int) (KerxSubtable, int, error) {
var item KerxSubtable
n := 0
if L := len(src); L < 12 {
return item, 0, fmt.Errorf("reading KerxSubtable: "+"EOF: expected length: 12, got %d", L)
}
_ = src[11] // early bound checking
item.length = binary.BigEndian.Uint32(src[0:])
item.Coverage = binary.BigEndian.Uint16(src[4:])
item.padding = src[6]
item.version = kerxSTVersion(src[7])
item.TupleCount = binary.BigEndian.Uint32(src[8:])
n += 12
{
var (
read int
err error
)
switch item.version {
case kerxSTVersion0:
item.Data, read, err = ParseKerxData0(src[12:], int(item.TupleCount))
case kerxSTVersion1:
item.Data, read, err = ParseKerxData1(src[12:], int(item.TupleCount), int(valuesCount))
case kerxSTVersion2:
item.Data, read, err = ParseKerxData2(src[12:], src, int(valuesCount))
case kerxSTVersion4:
item.Data, read, err = ParseKerxData4(src[12:], int(valuesCount))
case kerxSTVersion6:
item.Data, read, err = ParseKerxData6(src[12:], src, int(item.TupleCount), int(valuesCount))
default:
err = fmt.Errorf("unsupported KerxDataVersion %d", item.version)
}
if err != nil {
return item, 0, fmt.Errorf("reading KerxSubtable: %s", err)
}
n += read
}
var err error
n, err = item.parseEnd(src, valuesCount)
if err != nil {
return item, 0, fmt.Errorf("reading KerxSubtable: %s", err)
}
return item, n, nil
}
func (item *lookupRecordExt2) mustParse(src []byte) {
_ = src[7] // early bound checking
item.LastGlyph = binary.BigEndian.Uint16(src[0:])
item.FirstGlyph = binary.BigEndian.Uint16(src[2:])
item.Value = binary.BigEndian.Uint32(src[4:])
}
func (item *loopkupRecordExt6) mustParse(src []byte) {
_ = src[5] // early bound checking
item.Glyph = binary.BigEndian.Uint16(src[0:])
item.Value = binary.BigEndian.Uint32(src[2:])
}
func parseLoopkupRecordExt4(src []byte, parentSrc []byte) (loopkupRecordExt4, int, error) {
var item loopkupRecordExt4
n := 0
if L := len(src); L < 6 {
return item, 0, fmt.Errorf("reading loopkupRecordExt4: "+"EOF: expected length: 6, got %d", L)
}
_ = src[5] // early bound checking
item.LastGlyph = binary.BigEndian.Uint16(src[0:])
item.FirstGlyph = binary.BigEndian.Uint16(src[2:])
offsetValues := int(binary.BigEndian.Uint16(src[4:]))
n += 6
{
if offsetValues != 0 { // ignore null offset
if L := len(parentSrc); L < offsetValues {
return item, 0, fmt.Errorf("reading loopkupRecordExt4: "+"EOF: expected length: %d, got %d", offsetValues, L)
}
arrayLength := int(item.nValues())
if L := len(parentSrc); L < offsetValues+arrayLength*4 {
return item, 0, fmt.Errorf("reading loopkupRecordExt4: "+"EOF: expected length: %d, got %d", offsetValues+arrayLength*4, L)
}
item.Values = make([]uint32, arrayLength) // allocation guarded by the previous check
for i := range item.Values {
item.Values[i] = binary.BigEndian.Uint32(parentSrc[offsetValues+i*4:])
}
offsetValues += arrayLength * 4
}
}
return item, n, nil
}
@@ -0,0 +1,320 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import (
"encoding/binary"
"fmt"
)
// Kerx is the extended kerning table
// See https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6kerx.html
type Kerx struct {
version uint16 // The version number of the extended kerning table (currently 2, 3, or 4).
padding uint16 // Unused; set to zero.
nTables uint32 // The number of subtables included in the extended kerning table.
Tables []KerxSubtable `arrayCount:"ComputedField-nTables"`
}
// extended versions
// binarygen: argument=valuesCount int
type KerxSubtable struct {
length uint32 // The length of this subtable in bytes, including this header.
Coverage uint16 // Circumstances under which this table is used.
padding byte // unused
version kerxSTVersion
TupleCount uint32 // The tuple count. This value is only used with variation fonts and should be 0 for all other fonts. The subtable's tupleCount will be ignored if the 'kerx' table version is less than 4.
Data KerxData `unionField:"version" arguments:"tupleCount=.TupleCount, valuesCount=valuesCount"`
}
// check and return the subtable length
func (ks *KerxSubtable) parseEnd(src []byte, _ int) (int, error) {
if L := len(src); L < int(ks.length) {
return 0, fmt.Errorf("EOF: expected length: %d, got %d", ks.length, L)
}
return int(ks.length), nil
}
type kerxSTVersion byte
const (
kerxSTVersion0 kerxSTVersion = iota
kerxSTVersion1
kerxSTVersion2
_
kerxSTVersion4
_
kerxSTVersion6
)
type KerxData interface {
isKerxData()
}
func (KerxData0) isKerxData() {}
func (KerxData1) isKerxData() {}
func (KerxData2) isKerxData() {}
func (KerxData4) isKerxData() {}
func (KerxData6) isKerxData() {}
// binarygen: argument=tupleCount int
type KerxData0 struct {
nPairs uint32
searchRange uint32
entrySelector uint32
rangeShift uint32
Pairs []Kernx0Record `arrayCount:"ComputedField-nPairs"`
}
// resolve offset for variable fonts
func (kd *KerxData0) parseEnd(src []byte, tupleCount int) (int, error) {
if tupleCount != 0 { // interpret values as offset
for i, pair := range kd.Pairs {
if L, E := len(src), int(uint16(pair.Value))+2; L < E {
return 0, fmt.Errorf("EOF: expected length: %d, got %d", E, L)
}
kd.Pairs[i].Value = int16(binary.BigEndian.Uint16(src[pair.Value:]))
}
}
return len(src), nil
}
type Kernx0Record struct {
Left, Right GlyphID
Value int16
}
// Kernx1 state entry flags
const (
Kerx1Push = 0x8000 // If set, push this glyph on the kerning stack.
Kerx1DontAdvance = 0x4000 // If set, don't advance to the next glyph before going to the new state.
Kerx1Reset = 0x2000 // If set, reset the kerning data (clear the stack)
Kern1Offset = 0x3FFF // Byte offset from beginning of subtable to the value table for the glyphs on the kerning stack.
)
// binarygen: argument=tupleCount int
// binarygen: argument=valuesCount int
type KerxData1 struct {
AATStateTableExt `arguments:"valuesCount=valuesCount, entryDataSize=2"`
valueTable Offset32
Values []int16 `isOpaque:""`
}
// From Apple 'kern' spec:
// Each pops one glyph from the kerning stack and applies the kerning value to it.
// The end of the list is marked by an odd value...
func parseKernx1Values(src []byte, entries []AATStateEntry, valueTableOffset, tupleCount int) ([]int16, error) {
// find the maximum index need in the values array
var maxi uint16
for _, entry := range entries {
if index := entry.AsKernxIndex(); index != 0xFFFF && index > maxi {
maxi = index
}
}
if tupleCount == 0 {
tupleCount = 1
}
nbUint16Min := tupleCount * int(maxi+1)
if L, E := len(src), valueTableOffset+2*nbUint16Min; L < E {
return nil, fmt.Errorf("EOF: expected length: %d, got %d", E, L)
}
src = src[valueTableOffset:]
out := make([]int16, 0, nbUint16Min)
for len(src) >= 2 { // gracefully handle missing odd value
v := int16(binary.BigEndian.Uint16(src))
out = append(out, v)
src = src[2:]
if len(out) >= nbUint16Min && v&1 != 0 {
break
}
}
return out, nil
}
func (kx *KerxData1) parseValues(src []byte, tupleCount, _ int) error {
var err error
kx.Values, err = parseKernx1Values(src, kx.Entries, int(kx.valueTable), tupleCount)
return err
}
type KerxData2 struct {
rowWidth uint32 // The number of bytes in each row of the kerning value array
Left AATLookup `offsetSize:"Offset32" offsetRelativeTo:"Parent"` // Offset from beginning of this subtable to the left-hand offset table.
Right AATLookup `offsetSize:"Offset32" offsetRelativeTo:"Parent"` // Offset from beginning of this subtable to right-hand offset table.
KerningStart Offset32 // Offset from beginning of this subtable to the start of the kerning array.
KerningData []byte `subsliceStart:"AtStart" arrayCount:"ToEnd"` // indexed by Left + Right
}
// binarygen: argument=valuesCount int
type KerxData4 struct {
AATStateTableExt `arguments:"valuesCount=valuesCount,entryDataSize=2"`
Flags uint32
Anchors KerxAnchors `isOpaque:""`
}
func (kd KerxData4) nAnchors() int {
// find the maximum index need in the actions array
var maxi uint16
for _, entry := range kd.Entries {
if index := entry.AsKernxIndex(); index != 0xFFFF && index > maxi {
maxi = index
}
}
return int(maxi) + 1
}
func (kd *KerxData4) parseAnchors(src []byte, _ int) error {
nAnchors := kd.nAnchors()
const Offset = 0x00FFFFFF // Masks the offset in bytes from the beginning of the subtable to the beginning of the control point table.
controlOffset := int(kd.Flags & Offset)
if L := len(src); L < controlOffset {
return fmt.Errorf("EOF: expected length: %d, got %d", controlOffset, L)
}
var err error
switch kd.ActionType() {
case 0:
kd.Anchors, _, err = ParseKerxAnchorControls(src[controlOffset:], nAnchors)
case 1:
kd.Anchors, _, err = ParseKerxAnchorAnchors(src[controlOffset:], nAnchors)
case 2:
kd.Anchors, _, err = ParseKerxAnchorCoordinates(src[controlOffset:], nAnchors)
default:
return fmt.Errorf("invalid Kerx4 anchor format %d", kd.ActionType())
}
return err
}
// ActionType returns 0, 1 or 2, according to the anchor format :
// - 0 : KerxAnchorControls
// - 1 : KerxAnchorAnchors
// - 2 : KerxAnchorCoordinates
func (kd KerxData4) ActionType() uint8 {
const ActionType = 0xC0000000 // A two-bit field containing the action type.
return uint8((kd.Flags & ActionType) >> 30)
}
type KerxAnchors interface {
isKerxAnchors()
}
func (KerxAnchorControls) isKerxAnchors() {}
func (KerxAnchorAnchors) isKerxAnchors() {}
func (KerxAnchorCoordinates) isKerxAnchors() {}
type KerxAnchorControls struct {
Anchors []KAControl
}
type KerxAnchorAnchors struct {
Anchors []KAAnchor
}
type KerxAnchorCoordinates struct {
Anchors []KACoordinates
}
type KAControl struct {
Mark, Current uint16
}
type KAAnchor struct {
Mark, Current uint16
}
type KACoordinates struct {
MarkX, MarkY, CurrentX, CurrentY int16
}
// binarygen: argument=tupleCount int
// binarygen: argument=valuesCount int
type KerxData6 struct {
flags uint32 // Flags for this subtable. See below.
rowCount uint16 // The number of rows in the kerning value array
columnCount uint16 // The number of columns in the kerning value array
rowIndexTableOffset uint32 // Offset from beginning of this subtable to the row index lookup table.
columnIndexTableOffset uint32 // Offset from beginning of this subtable to column index offset table.
kerningArrayOffset uint32 // Offset from beginning of this subtable to the start of the kerning array.
kerningVectorOffset uint32 // Offset from beginning of this subtable to the start of the kerning vectors. This value is only present if the tupleCount for this subtable is 1 or more.
Row AatLookupMixed `isOpaque:"" offsetRelativeTo:"Parent"` // Values are pre-multiplied by `columnCount`
Column AatLookupMixed `isOpaque:"" offsetRelativeTo:"Parent"`
// with rowCount * columnCount
// for tuples the values are estParseKerx (Not yet run).the first element of the tuple
Kernings []int16 `isOpaque:"" offsetRelativeTo:"Parent"`
}
func (kd *KerxData6) parseRow(_, parentSrc []byte, _, valuesCount int) error {
isExtended := kd.flags&1 != 0
if L := len(parentSrc); L < int(kd.rowIndexTableOffset) {
return fmt.Errorf("EOF: expected length: %d, got %d", kd.rowIndexTableOffset, L)
}
var err error
if isExtended {
kd.Row, _, err = ParseAATLookupExt(parentSrc[kd.rowIndexTableOffset:], valuesCount)
} else {
kd.Row, _, err = ParseAATLookup(parentSrc[kd.rowIndexTableOffset:], valuesCount)
}
return err
}
func (kd *KerxData6) parseColumn(_, parentSrc []byte, _, valuesCount int) error {
isExtended := kd.flags&1 != 0
if L := len(parentSrc); L < int(kd.columnIndexTableOffset) {
return fmt.Errorf("EOF: expected length: %d, got %d", kd.columnIndexTableOffset, L)
}
var err error
if isExtended {
kd.Column, _, err = ParseAATLookupExt(parentSrc[kd.columnIndexTableOffset:], valuesCount)
} else {
kd.Column, _, err = ParseAATLookup(parentSrc[kd.columnIndexTableOffset:], valuesCount)
}
return err
}
func (kd *KerxData6) parseKernings(_, parentSrc []byte, tupleCount, _ int) error {
isExtended := kd.flags&1 != 0
length := int(kd.rowCount) * int(kd.columnCount)
var tmp []uint32
if isExtended {
if L, E := len(parentSrc), int(kd.kerningArrayOffset)+length*4; L < E {
return fmt.Errorf("EOF: expected length: %d, got %d", E, L)
}
tmp = make([]uint32, length)
for i := range tmp {
tmp[i] = binary.BigEndian.Uint32(parentSrc[int(kd.kerningArrayOffset)+4*i:])
}
} else {
if L, E := len(parentSrc), int(kd.kerningArrayOffset)+length*2; L < E {
return fmt.Errorf("EOF: expected length: %d, got %d", E, L)
}
tmp = make([]uint32, length)
for i := range tmp {
tmp[i] = uint32(binary.BigEndian.Uint16(parentSrc[int(kd.kerningArrayOffset)+2*i:]))
}
}
kd.Kernings = make([]int16, len(tmp))
if tupleCount != 0 { // interpret kern values as offset
// If the tupleCount is 1 or more, then the kerning array contains offsets from the beginning
// of the kerningVectors table to a tupleCount-dimensional vector of FUnits controlling the kerning.
for i, v := range tmp {
kerningOffset := int(kd.kerningVectorOffset) + int(v)
if L := len(parentSrc); L < kerningOffset+2 {
return fmt.Errorf("EOF: expected length: %d, got %d", kerningOffset+2, L)
}
kd.Kernings[i] = int16(binary.BigEndian.Uint16(parentSrc[kerningOffset:]))
}
} else {
// a kerning value greater than an int16 should not happen
for i, v := range tmp {
kd.Kernings[i] = int16(v)
}
}
return nil
}
//lint:ignore U1000 this type is required so that the code generator add a ParseAATLookupExt function
type dummy struct {
A AATLookupExt
}
@@ -0,0 +1,49 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import (
"encoding/binary"
"fmt"
)
// Code generated by binarygen from aat_ltag_src.go. DO NOT EDIT
func ParseLtag(src []byte) (Ltag, int, error) {
var item Ltag
n := 0
if L := len(src); L < 12 {
return item, 0, fmt.Errorf("reading Ltag: "+"EOF: expected length: 12, got %d", L)
}
_ = src[11] // early bound checking
item.version = binary.BigEndian.Uint32(src[0:])
item.flags = binary.BigEndian.Uint32(src[4:])
item.numTags = binary.BigEndian.Uint32(src[8:])
n += 12
{
arrayLength := int(item.numTags)
if L := len(src); L < 12+arrayLength*4 {
return item, 0, fmt.Errorf("reading Ltag: "+"EOF: expected length: %d, got %d", 12+arrayLength*4, L)
}
item.tagRange = make([]stringRange, arrayLength) // allocation guarded by the previous check
for i := range item.tagRange {
item.tagRange[i].mustParse(src[12+i*4:])
}
n += arrayLength * 4
}
{
item.stringData = src[0:]
n = len(src)
}
return item, n, nil
}
func (item *stringRange) mustParse(src []byte) {
_ = src[3] // early bound checking
item.offset = binary.BigEndian.Uint16(src[0:])
item.length = binary.BigEndian.Uint16(src[2:])
}
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import "github.com/go-text/typesetting/language"
// Ltag is the language tags table
// See https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6ltag.html
type Ltag struct {
version uint32 // Table version; currently 1
flags uint32 // Table flags; currently none defined
numTags uint32 // Number of language tags which follow
tagRange []stringRange `arrayCount:"ComputedField-numTags"` // Range for each tag's string
stringData []byte `subsliceStart:"AtStart" arrayCount:"ToEnd"`
}
type stringRange struct {
offset uint16 // Offset from the start of the table to the beginning of the string
length uint16 // String length (in bytes)
}
func (lt Ltag) Language(i uint16) language.Language {
r := lt.tagRange[i]
return language.NewLanguage(string(lt.stringData[r.offset : r.offset+r.length]))
}
@@ -0,0 +1,330 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import (
"encoding/binary"
"fmt"
)
// Code generated by binarygen from aat_mortx_src.go. DO NOT EDIT
func (item *AATFeature) mustParse(src []byte) {
_ = src[11] // early bound checking
item.FeatureType = binary.BigEndian.Uint16(src[0:])
item.FeatureSetting = binary.BigEndian.Uint16(src[2:])
item.EnableFlags = binary.BigEndian.Uint32(src[4:])
item.DisableFlags = binary.BigEndian.Uint32(src[8:])
}
func ParseMorx(src []byte, valuesCount int) (Morx, int, error) {
var item Morx
n := 0
if L := len(src); L < 8 {
return item, 0, fmt.Errorf("reading Morx: "+"EOF: expected length: 8, got %d", L)
}
_ = src[7] // early bound checking
item.version = binary.BigEndian.Uint16(src[0:])
item.unused = binary.BigEndian.Uint16(src[2:])
item.nChains = binary.BigEndian.Uint32(src[4:])
n += 8
{
arrayLength := int(item.nChains)
offset := 8
for i := 0; i < arrayLength; i++ {
elem, read, err := ParseMorxChain(src[offset:], valuesCount)
if err != nil {
return item, 0, fmt.Errorf("reading Morx: %s", err)
}
item.Chains = append(item.Chains, elem)
offset += read
}
n = offset
}
return item, n, nil
}
func ParseMorxChain(src []byte, valuesCount int) (MorxChain, int, error) {
var item MorxChain
n := 0
if L := len(src); L < 16 {
return item, 0, fmt.Errorf("reading MorxChain: "+"EOF: expected length: 16, got %d", L)
}
_ = src[15] // early bound checking
item.Flags = binary.BigEndian.Uint32(src[0:])
item.chainLength = binary.BigEndian.Uint32(src[4:])
item.nFeatureEntries = binary.BigEndian.Uint32(src[8:])
item.nSubtable = binary.BigEndian.Uint32(src[12:])
n += 16
{
arrayLength := int(item.nFeatureEntries)
if L := len(src); L < 16+arrayLength*12 {
return item, 0, fmt.Errorf("reading MorxChain: "+"EOF: expected length: %d, got %d", 16+arrayLength*12, L)
}
item.Features = make([]AATFeature, arrayLength) // allocation guarded by the previous check
for i := range item.Features {
item.Features[i].mustParse(src[16+i*12:])
}
n += arrayLength * 12
}
{
arrayLength := int(item.nSubtable)
offset := n
for i := 0; i < arrayLength; i++ {
elem, read, err := ParseMorxChainSubtable(src[offset:], valuesCount)
if err != nil {
return item, 0, fmt.Errorf("reading MorxChain: %s", err)
}
item.Subtables = append(item.Subtables, elem)
offset += read
}
n = offset
}
return item, n, nil
}
func ParseMorxChainSubtable(src []byte, valuesCount int) (MorxChainSubtable, int, error) {
var item MorxChainSubtable
n := 0
if L := len(src); L < 12 {
return item, 0, fmt.Errorf("reading MorxChainSubtable: "+"EOF: expected length: 12, got %d", L)
}
_ = src[11] // early bound checking
item.length = binary.BigEndian.Uint32(src[0:])
item.Coverage = src[4]
item.ignored[0] = src[5]
item.ignored[1] = src[6]
item.version = MorxSubtableVersion(src[7])
item.SubFeatureFlags = binary.BigEndian.Uint32(src[8:])
n += 12
{
var (
read int
err error
)
switch item.version {
case MorxSubtableVersionContextual:
item.Data, read, err = ParseMorxSubtableContextual(src[12:], valuesCount)
case MorxSubtableVersionInsertion:
item.Data, read, err = ParseMorxSubtableInsertion(src[12:], valuesCount)
case MorxSubtableVersionLigature:
item.Data, read, err = ParseMorxSubtableLigature(src[12:], valuesCount)
case MorxSubtableVersionNonContextual:
item.Data, read, err = ParseMorxSubtableNonContextual(src[12:], valuesCount)
case MorxSubtableVersionRearrangement:
item.Data, read, err = ParseMorxSubtableRearrangement(src[12:], valuesCount)
default:
err = fmt.Errorf("unsupported MorxSubtableVersion %d", item.version)
}
if err != nil {
return item, 0, fmt.Errorf("reading MorxChainSubtable: %s", err)
}
n += read
}
var err error
n, err = item.parseEnd(src, valuesCount)
if err != nil {
return item, 0, fmt.Errorf("reading MorxChainSubtable: %s", err)
}
return item, n, nil
}
func ParseMorxSubtableContextual(src []byte, valuesCount int) (MorxSubtableContextual, int, error) {
var item MorxSubtableContextual
n := 0
{
var (
err error
read int
)
item.AATStateTableExt, read, err = ParseAATStateTableExt(src[0:], int(valuesCount), int(4))
if err != nil {
return item, 0, fmt.Errorf("reading MorxSubtableContextual: %s", err)
}
n += read
}
if L := len(src); L < n+4 {
return item, 0, fmt.Errorf("reading MorxSubtableContextual: "+"EOF: expected length: n + 4, got %d", L)
}
offsetSubstitutions := int(binary.BigEndian.Uint32(src[n:]))
n += 4
{
if offsetSubstitutions != 0 { // ignore null offset
if L := len(src); L < offsetSubstitutions {
return item, 0, fmt.Errorf("reading MorxSubtableContextual: "+"EOF: expected length: %d, got %d", offsetSubstitutions, L)
}
var err error
item.Substitutions, _, err = ParseSubstitutionsTable(src[offsetSubstitutions:], int(item.nSubs()), int(valuesCount))
if err != nil {
return item, 0, fmt.Errorf("reading MorxSubtableContextual: %s", err)
}
}
}
return item, n, nil
}
func ParseMorxSubtableInsertion(src []byte, valuesCount int) (MorxSubtableInsertion, int, error) {
var item MorxSubtableInsertion
n := 0
{
var (
err error
read int
)
item.AATStateTableExt, read, err = ParseAATStateTableExt(src[0:], int(valuesCount), int(4))
if err != nil {
return item, 0, fmt.Errorf("reading MorxSubtableInsertion: %s", err)
}
n += read
}
if L := len(src); L < n+4 {
return item, 0, fmt.Errorf("reading MorxSubtableInsertion: "+"EOF: expected length: n + 4, got %d", L)
}
offsetInsertions := int(binary.BigEndian.Uint32(src[n:]))
n += 4
{
if offsetInsertions != 0 { // ignore null offset
if L := len(src); L < offsetInsertions {
return item, 0, fmt.Errorf("reading MorxSubtableInsertion: "+"EOF: expected length: %d, got %d", offsetInsertions, L)
}
arrayLength := int(item.nInsertions())
if L := len(src); L < offsetInsertions+arrayLength*2 {
return item, 0, fmt.Errorf("reading MorxSubtableInsertion: "+"EOF: expected length: %d, got %d", offsetInsertions+arrayLength*2, L)
}
item.Insertions = make([]uint16, arrayLength) // allocation guarded by the previous check
for i := range item.Insertions {
item.Insertions[i] = binary.BigEndian.Uint16(src[offsetInsertions+i*2:])
}
offsetInsertions += arrayLength * 2
}
}
return item, n, nil
}
func ParseMorxSubtableLigature(src []byte, valuesCount int) (MorxSubtableLigature, int, error) {
var item MorxSubtableLigature
n := 0
{
var (
err error
read int
)
item.AATStateTableExt, read, err = ParseAATStateTableExt(src[0:], int(valuesCount), int(2))
if err != nil {
return item, 0, fmt.Errorf("reading MorxSubtableLigature: %s", err)
}
n += read
}
if L := len(src); L < n+12 {
return item, 0, fmt.Errorf("reading MorxSubtableLigature: "+"EOF: expected length: n + 12, got %d", L)
}
_ = src[n+11] // early bound checking
item.ligActionOffset = Offset32(binary.BigEndian.Uint32(src[n:]))
item.componentOffset = Offset32(binary.BigEndian.Uint32(src[n+4:]))
item.ligatureOffset = Offset32(binary.BigEndian.Uint32(src[n+8:]))
n += 12
{
err := item.parseLigActions(src[:], valuesCount)
if err != nil {
return item, 0, fmt.Errorf("reading MorxSubtableLigature: %s", err)
}
}
{
err := item.parseComponents(src[:], valuesCount)
if err != nil {
return item, 0, fmt.Errorf("reading MorxSubtableLigature: %s", err)
}
}
{
err := item.parseLigatures(src[:], valuesCount)
if err != nil {
return item, 0, fmt.Errorf("reading MorxSubtableLigature: %s", err)
}
}
return item, n, nil
}
func ParseMorxSubtableNonContextual(src []byte, valuesCount int) (MorxSubtableNonContextual, int, error) {
var item MorxSubtableNonContextual
n := 0
{
var (
err error
read int
)
item.Class, read, err = ParseAATLookup(src[0:], valuesCount)
if err != nil {
return item, 0, fmt.Errorf("reading MorxSubtableNonContextual: %s", err)
}
n += read
}
return item, n, nil
}
func ParseMorxSubtableRearrangement(src []byte, valuesCount int) (MorxSubtableRearrangement, int, error) {
var item MorxSubtableRearrangement
n := 0
{
var (
err error
read int
)
item.AATStateTableExt, read, err = ParseAATStateTableExt(src[0:], int(valuesCount), int(0))
if err != nil {
return item, 0, fmt.Errorf("reading MorxSubtableRearrangement: %s", err)
}
n += read
}
return item, n, nil
}
func ParseSubstitutionsTable(src []byte, substitutionsCount int, valuesCount int) (SubstitutionsTable, int, error) {
var item SubstitutionsTable
n := 0
{
if L := len(src); L < substitutionsCount*4 {
return item, 0, fmt.Errorf("reading SubstitutionsTable: "+"EOF: expected length: %d, got %d", substitutionsCount*4, L)
}
item.Substitutions = make([]AATLookup, substitutionsCount) // allocation guarded by the previous check
for i := range item.Substitutions {
offset := int(binary.BigEndian.Uint32(src[i*4:]))
// ignore null offsets
if offset == 0 {
continue
}
if L := len(src); L < offset {
return item, 0, fmt.Errorf("reading SubstitutionsTable: "+"EOF: expected length: %d, got %d", offset, L)
}
var err error
item.Substitutions[i], _, err = ParseAATLookup(src[offset:], valuesCount)
if err != nil {
return item, 0, fmt.Errorf("reading SubstitutionsTable: %s", err)
}
}
n += substitutionsCount * 4
}
return item, n, nil
}
@@ -0,0 +1,302 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import (
"encoding/binary"
"errors"
"fmt"
)
// Morx is the extended glyph metamorphosis table
// See https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6morx.html
type Morx struct {
version uint16 // Version number of the extended glyph metamorphosis table (either 2 or 3)
unused uint16 // Set to 0
nChains uint32 // Number of metamorphosis chains contained in this table.
Chains []MorxChain `arrayCount:"ComputedField-nChains"`
}
// MorxChain is a set of subtables
type MorxChain struct {
Flags uint32 // The default specification for subtables.
chainLength uint32 // Total byte count, including this header; must be a multiple of 4.
nFeatureEntries uint32 // Number of feature subtable entries.
nSubtable uint32 // The number of subtables in the chain.
Features []AATFeature `arrayCount:"ComputedField-nFeatureEntries"`
Subtables []MorxChainSubtable `arrayCount:"ComputedField-nSubtable"`
}
type AATFeature struct {
FeatureType uint16
FeatureSetting uint16
EnableFlags uint32 // Flags for the settings that this feature and setting enables.
DisableFlags uint32 // Complement of flags for the settings that this feature and setting disable.
}
type MorxChainSubtable struct {
length uint32 // Total subtable length, including this header.
// Coverage flags and subtable type.
Coverage byte
ignored [2]byte
version MorxSubtableVersion
SubFeatureFlags uint32 // The 32-bit mask identifying which subtable this is (the subtable being executed if the AND of this value and the processed defaultFlags is nonzero)
Data MorxSubtable `unionField:"version"`
}
// check and return the subtable length
func (mc *MorxChainSubtable) parseEnd(src []byte, _ int) (int, error) {
if L := len(src); L < int(mc.length) {
return 0, fmt.Errorf("EOF: expected length: %d, got %d", mc.length, L)
}
return int(mc.length), nil
}
// MorxSubtableVersion indicates the kind of 'morx' subtable.
// See the constants.
type MorxSubtableVersion uint8
const (
MorxSubtableVersionRearrangement MorxSubtableVersion = iota
MorxSubtableVersionContextual
MorxSubtableVersionLigature
_ // reserved
MorxSubtableVersionNonContextual
MorxSubtableVersionInsertion
)
type MorxSubtable interface {
isMorxSubtable()
}
func (MorxSubtableRearrangement) isMorxSubtable() {}
func (MorxSubtableContextual) isMorxSubtable() {}
func (MorxSubtableLigature) isMorxSubtable() {}
func (MorxSubtableNonContextual) isMorxSubtable() {}
func (MorxSubtableInsertion) isMorxSubtable() {}
// binarygen: argument=valuesCount int
type MorxSubtableRearrangement struct {
AATStateTableExt `arguments:"valuesCount=valuesCount,entryDataSize=0"`
}
// binarygen: argument=valuesCount int
type MorxSubtableContextual struct {
AATStateTableExt `arguments:"valuesCount=valuesCount,entryDataSize=4"`
// Byte offset from the beginning of the state subtable to the beginning of the substitution tables :
// each value of the array is itself an offet to a aatLookupTable, and the number of
// items is computed from the header
Substitutions SubstitutionsTable `offsetSize:"Offset32" arguments:"substitutionsCount=.nSubs(), valuesCount=valuesCount"`
}
type SubstitutionsTable struct {
Substitutions []AATLookup `offsetsArray:"Offset32"`
}
func (ct *MorxSubtableContextual) nSubs() int {
// find the maximum index need in the substitution array
var maxi uint16
for _, entry := range ct.Entries {
markIndex, currentIndex := entry.AsMorxContextual()
if markIndex != 0xFFFF && markIndex > maxi {
maxi = markIndex
}
if currentIndex != 0xFFFF && currentIndex > maxi {
maxi = currentIndex
}
}
return int(maxi) + 1
}
// binarygen: argument=valuesCount int
type MorxSubtableLigature struct {
AATStateTableExt `arguments:"valuesCount=valuesCount, entryDataSize=2"`
ligActionOffset Offset32 // Byte offset from stateHeader to the start of the ligature action table.
componentOffset Offset32 // Byte offset from stateHeader to the start of the component table.
ligatureOffset Offset32 // Byte offset from stateHeader to the start of the actual ligature lists.
LigActions []uint32 `isOpaque:""`
Components []uint16 `isOpaque:""`
Ligatures []GlyphID `isOpaque:""`
}
// MorxLigatureSubtable flags
const (
// Push this glyph onto the component stack for
// eventual processing.
MLSetComponent = 0x8000
// Leave the glyph pointer at this glyph for the
// next iteration.
MLDontAdvance = 0x4000
// Use the ligActionIndex to process a ligature group.
MLPerformAction = 0x2000
// Byte offset from beginning of subtable to the
// ligature action list. This value must be a
// multiple of 4.
MLOffset = 0x3FFF
// This is the last action in the list. This also
// implies storage.
MLActionLast = 1 << 31
// Store the ligature at the current cumulated index
// in the ligature table in place of the marked
// (i.e. currently-popped) glyph.
MLActionStore = 1 << 30
// A 30-bit value which is sign-extended to 32-bits
// and added to the glyph ID, resulting in an index
// into the component table.
MLActionOffset = 0x3FFFFFFF
)
// the LigActions length is not specified. Instead, we have to parse uint32 one by one
// until we reach last action or reach EOF
func (lig *MorxSubtableLigature) parseLigActions(src []byte, _ int) error {
// fetch the maximum start index
maxIndex := -1
for _, entry := range lig.Entries {
if entry.Flags&MLPerformAction == 0 {
continue
}
if index := int(entry.AsMorxLigature()); index > maxIndex {
maxIndex = index
}
}
if L := len(src); L < int(lig.ligActionOffset)+4*int(maxIndex+1) {
return fmt.Errorf("EOF: expected length: %d, got %d", lig.ligActionOffset, L)
}
// fetch the action table, up to the last entry
src = src[lig.ligActionOffset:]
for len(src) >= 4 { // stop gracefully if the last action was not found
action := binary.BigEndian.Uint32(src)
lig.LigActions = append(lig.LigActions, action)
src = src[4:]
// dont break before maxIndex
if len(lig.LigActions) > maxIndex && action&MLActionLast != 0 {
break
}
}
return nil
}
func (lig *MorxSubtableLigature) parseComponents(src []byte, _ int) error {
// we rely on offset being sorted, which seems to be the case in practice
if lig.componentOffset > lig.ligatureOffset {
return errors.New("unsupported non sorted offsets")
}
if L := len(src); L < int(lig.componentOffset) {
return fmt.Errorf("EOF: expected length: %d, got %d", lig.componentOffset, L)
}
src = src[lig.componentOffset:]
componentCount := (lig.ligatureOffset - lig.componentOffset) / 2
lig.Components = make([]uint16, componentCount)
for i := range lig.Components {
lig.Components[i] = binary.BigEndian.Uint16(src[2*i:])
}
return nil
}
func (lig *MorxSubtableLigature) parseLigatures(src []byte, _ int) error {
if L := len(src); L < int(lig.ligatureOffset) {
return fmt.Errorf("EOF: expected length: %d, got %d", lig.ligatureOffset, L)
}
src = src[lig.ligatureOffset:]
ligatureCount := len(src) / 2
lig.Ligatures = make([]GlyphID, ligatureCount)
for i := range lig.Ligatures {
lig.Ligatures[i] = GlyphID(binary.BigEndian.Uint16(src[2*i:]))
}
return nil
}
type MorxSubtableNonContextual struct {
// The lookup value is interpreted as a GlyphIndex
Class AATLookup
}
// binarygen: argument=valuesCount int
type MorxSubtableInsertion struct {
AATStateTableExt `arguments:"valuesCount=valuesCount,entryDataSize=4"`
Insertions []GlyphID `offsetSize:"Offset32" arrayCount:"ComputedField-nInsertions()"` // Byte offset from stateHeader to the start of the insertion glyph table.
}
// MorxInsertionSubtable flags
const (
// If set, mark the current glyph.
MISetMark = 0x8000
// If set, don't advance to the next glyph before
// going to the new state. This does not mean
// that the glyph pointed to is the same one as
// before. If you've made insertions immediately
// downstream of the current glyph, the next glyph
// processed would in fact be the first one
// inserted.
MIDontAdvance = 0x4000
// If set, and the currentInsertList is nonzero,
// then the specified glyph list will be inserted
// as a kashida-like insertion, either before or
// after the current glyph (depending on the state
// of the currentInsertBefore flag). If clear, and
// the currentInsertList is nonzero, then the
// specified glyph list will be inserted as a
// split-vowel-like insertion, either before or
// after the current glyph (depending on the state
// of the currentInsertBefore flag).
MICurrentIsKashidaLike = 0x2000
// If set, and the markedInsertList is nonzero,
// then the specified glyph list will be inserted
// as a kashida-like insertion, either before or
// after the marked glyph (depending on the state
// of the markedInsertBefore flag). If clear, and
// the markedInsertList is nonzero, then the
// specified glyph list will be inserted as a
// split-vowel-like insertion, either before or
// after the marked glyph (depending on the state
// of the markedInsertBefore flag).
MIMarkedIsKashidaLike = 0x1000
// If set, specifies that insertions are to be made
// to the left of the current glyph. If clear,
// they're made to the right of the current glyph.
MICurrentInsertBefore = 0x0800
// If set, specifies that insertions are to be
// made to the left of the marked glyph. If clear,
// they're made to the right of the marked glyph.
MIMarkedInsertBefore = 0x0400
// This 5-bit field is treated as a count of the
// number of glyphs to insert at the current
// position. Since zero means no insertions, the
// largest number of insertions at any given
// current location is 31 glyphs.
MICurrentInsertCount = 0x3E0
// This 5-bit field is treated as a count of the
// number of glyphs to insert at the marked
// position. Since zero means no insertions, the
// largest number of insertions at any given
// marked location is 31 glyphs.
MIMarkedInsertCount = 0x001F
)
func (msi *MorxSubtableInsertion) nInsertions() int {
// find the maximum index needed in the insertions array,
// taking into account the number of insertions
var maxi uint16
for _, entry := range msi.Entries {
currentIndex, markedIndex := entry.AsMorxInsertion()
if currentIndex != 0xFFFF {
indexEnd := currentIndex + (entry.Flags&MICurrentInsertCount)>>5
if indexEnd > maxi {
maxi = indexEnd
}
}
if markedIndex != 0xFFFF {
indexEnd := markedIndex + entry.Flags&MIMarkedInsertCount
if indexEnd > maxi {
maxi = indexEnd
}
}
}
return int(maxi)
}
@@ -0,0 +1,272 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import (
"sort"
)
// This file implements routines used to simplify acces to the tables
// data.
func (lk AATLoopkup0) Class(g GlyphID) (uint16, bool) {
if int(g) >= len(lk.Values) {
return 0, false
}
return lk.Values[g], true
}
func (lk AATLoopkup2) Class(g GlyphID) (uint16, bool) {
// 'adapted' from golang/x/image/font/sfnt
c := lk.Records
num := len(c)
if num == 0 {
return 0, false
}
// classRange is an array of startGlyphID, endGlyphID and target class ID.
// Ranges are non-overlapping.
// E.g. 130, 135, 1 137, 137, 5 etc
idx := sort.Search(num, func(i int) bool { return g <= c[i].FirstGlyph })
// idx either points to a matching start, or to the next range (or idx==num)
// e.g. with the range example from above: 130 points to 130-135 range, 133 points to 137-137 range
// check if gi is the start of a range, but only if sort.Search returned a valid result
if idx < num {
if class := c[idx]; g == c[idx].FirstGlyph {
return class.Value, true
}
}
// check if gi is in previous range
if idx > 0 {
idx--
if class := c[idx]; g >= class.FirstGlyph && g <= class.LastGlyph {
return class.Value, true
}
}
return 0, false
}
func (lk AATLoopkup4) Class(g GlyphID) (uint16, bool) {
// binary search
for i, j := 0, len(lk.Records); i < j; {
h := i + (j-i)/2
entry := lk.Records[h]
if g < entry.FirstGlyph {
j = h
} else if entry.LastGlyph < g {
i = h + 1
} else {
return entry.Values[g-entry.FirstGlyph], true
}
}
return 0, false
}
func (lk AATLoopkup6) Class(g GlyphID) (uint16, bool) {
// binary search
for i, j := 0, len(lk.Records); i < j; {
h := i + (j-i)/2
entry := lk.Records[h]
if g < entry.Glyph {
j = h
} else if entry.Glyph < g {
i = h + 1
} else {
return entry.Value, true
}
}
return 0, false
}
func (lk AATLoopkup8Data) Class(g GlyphID) (uint16, bool) {
if g < lk.FirstGlyph || g >= lk.FirstGlyph+GlyphID(len(lk.Values)) {
return 0, false
}
return lk.Values[g-lk.FirstGlyph], true
}
func (lk AATLoopkup10) Class(g GlyphID) (uint16, bool) {
if g < lk.FirstGlyph || g >= lk.FirstGlyph+GlyphID(len(lk.Values)) {
return 0, false
}
return lk.Values[g-lk.FirstGlyph], true
}
func (lk AATLoopkupExt0) Class(g GlyphID) (uint32, bool) {
if int(g) >= len(lk.Values) {
return 0, false
}
return lk.Values[g], true
}
func (lk AATLoopkupExt2) Class(g GlyphID) (uint32, bool) {
// 'adapted' from golang/x/image/font/sfnt
c := lk.Records
num := len(c)
if num == 0 {
return 0, false
}
// classRange is an array of startGlyphID, endGlyphID and target class ID.
// Ranges are non-overlapping.
// E.g. 130, 135, 1 137, 137, 5 etc
idx := sort.Search(num, func(i int) bool { return g <= c[i].FirstGlyph })
// idx either points to a matching start, or to the next range (or idx==num)
// e.g. with the range example from above: 130 points to 130-135 range, 133 points to 137-137 range
// check if gi is the start of a range, but only if sort.Search returned a valid result
if idx < num {
if class := c[idx]; g == c[idx].FirstGlyph {
return class.Value, true
}
}
// check if gi is in previous range
if idx > 0 {
idx--
if class := c[idx]; g >= class.FirstGlyph && g <= class.LastGlyph {
return class.Value, true
}
}
return 0, false
}
func (lk AATLoopkupExt4) Class(g GlyphID) (uint32, bool) {
// binary search
for i, j := 0, len(lk.Records); i < j; {
h := i + (j-i)/2
entry := lk.Records[h]
if g < entry.FirstGlyph {
j = h
} else if entry.LastGlyph < g {
i = h + 1
} else {
return entry.Values[g-entry.FirstGlyph], true
}
}
return 0, false
}
func (lk AATLoopkupExt6) Class(g GlyphID) (uint32, bool) {
// binary search
for i, j := 0, len(lk.Records); i < j; {
h := i + (j-i)/2
entry := lk.Records[h]
if g < entry.Glyph {
j = h
} else if entry.Glyph < g {
i = h + 1
} else {
return entry.Value, true
}
}
return 0, false
}
func (lk AATLoopkupExt8) Class(g GlyphID) (uint32, bool) {
v, ok := AATLoopkup8(lk).Class(g)
return uint32(v), ok
}
func (lk AATLoopkupExt10) Class(g GlyphID) (uint32, bool) {
if g < lk.FirstGlyph || g >= lk.FirstGlyph+GlyphID(len(lk.Values)) {
return 0, false
}
return lk.Values[g-lk.FirstGlyph], true
}
type AatLookupMixed interface {
// Returns 0 if not supported
ClassUint32(GlyphID) uint32
// Coverage returns the glyphs covered by this lookup as
// a list of inclusive ranges
Coverage() [][2]GlyphID
}
func (lk AATLoopkup0) ClassUint32(g GlyphID) uint32 {
v, _ := lk.Class(g)
return uint32(v)
}
func (lk AATLoopkup2) ClassUint32(g GlyphID) uint32 {
v, _ := lk.Class(g)
return uint32(v)
}
func (lk AATLoopkup4) ClassUint32(g GlyphID) uint32 {
v, _ := lk.Class(g)
return uint32(v)
}
func (lk AATLoopkup6) ClassUint32(g GlyphID) uint32 {
v, _ := lk.Class(g)
return uint32(v)
}
func (lk AATLoopkup8) ClassUint32(g GlyphID) uint32 {
v, _ := lk.Class(g)
return uint32(v)
}
func (lk AATLoopkup10) ClassUint32(g GlyphID) uint32 {
v, _ := lk.Class(g)
return uint32(v)
}
func (lk AATLoopkupExt0) ClassUint32(g GlyphID) uint32 {
v, _ := lk.Class(g)
return v
}
func (lk AATLoopkupExt2) ClassUint32(g GlyphID) uint32 {
v, _ := lk.Class(g)
return v
}
func (lk AATLoopkupExt4) ClassUint32(g GlyphID) uint32 {
v, _ := lk.Class(g)
return v
}
func (lk AATLoopkupExt6) ClassUint32(g GlyphID) uint32 {
v, _ := lk.Class(g)
return v
}
func (lk AATLoopkupExt8) ClassUint32(g GlyphID) uint32 {
v, _ := lk.Class(g)
return v
}
func (lk AATLoopkupExt10) ClassUint32(g GlyphID) uint32 {
v, _ := lk.Class(g)
return v
}
// GetFeature performs a binary seach into the names, using `Feature` as key,
// returning `nil` if not found.
func (ft Feat) GetFeature(feature uint16) *FeatureName {
for i, j := 0, len(ft.Names); i < j; {
h := i + (j-i)/2
entry := ft.Names[h].Feature
if feature < entry {
j = h
} else if entry < feature {
i = h + 1
} else {
return &ft.Names[h]
}
}
return nil
}
// IsExclusive returns true if the feature settings are mutually exclusive.
func (feature *FeatureName) IsExclusive() bool {
const Exclusive = 0x8000
return feature.FeatureFlags&Exclusive != 0
}
@@ -0,0 +1,135 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import (
"encoding/binary"
"fmt"
)
// Code generated by binarygen from aat_trak_src.go. DO NOT EDIT
func ParseTrackData(src []byte, parentSrc []byte) (TrackData, int, error) {
var item TrackData
n := 0
if L := len(src); L < 8 {
return item, 0, fmt.Errorf("reading TrackData: "+"EOF: expected length: 8, got %d", L)
}
_ = src[7] // early bound checking
item.nTracks = binary.BigEndian.Uint16(src[0:])
item.nSizes = binary.BigEndian.Uint16(src[2:])
offsetSizeTable := int(binary.BigEndian.Uint32(src[4:]))
n += 8
{
if offsetSizeTable != 0 { // ignore null offset
if L := len(parentSrc); L < offsetSizeTable {
return item, 0, fmt.Errorf("reading TrackData: "+"EOF: expected length: %d, got %d", offsetSizeTable, L)
}
arrayLength := int(item.nSizes)
if L := len(parentSrc); L < offsetSizeTable+arrayLength*4 {
return item, 0, fmt.Errorf("reading TrackData: "+"EOF: expected length: %d, got %d", offsetSizeTable+arrayLength*4, L)
}
item.SizeTable = make([]float32, arrayLength) // allocation guarded by the previous check
for i := range item.SizeTable {
item.SizeTable[i] = Float1616FromUint(binary.BigEndian.Uint32(parentSrc[offsetSizeTable+i*4:]))
}
offsetSizeTable += arrayLength * 4
}
}
{
arrayLength := int(item.nTracks)
offset := 8
for i := 0; i < arrayLength; i++ {
elem, read, err := ParseTrackTableEntry(src[offset:], parentSrc, int(item.nSizes))
if err != nil {
return item, 0, fmt.Errorf("reading TrackData: %s", err)
}
item.TrackTable = append(item.TrackTable, elem)
offset += read
}
n = offset
}
return item, n, nil
}
func ParseTrackTableEntry(src []byte, grandParentSrc []byte, perSizeTrackingCount int) (TrackTableEntry, int, error) {
var item TrackTableEntry
n := 0
if L := len(src); L < 8 {
return item, 0, fmt.Errorf("reading TrackTableEntry: "+"EOF: expected length: 8, got %d", L)
}
_ = src[7] // early bound checking
item.Track = Float1616FromUint(binary.BigEndian.Uint32(src[0:]))
item.NameIndex = binary.BigEndian.Uint16(src[4:])
offsetPerSizeTracking := int(binary.BigEndian.Uint16(src[6:]))
n += 8
{
if offsetPerSizeTracking != 0 { // ignore null offset
if L := len(grandParentSrc); L < offsetPerSizeTracking {
return item, 0, fmt.Errorf("reading TrackTableEntry: "+"EOF: expected length: %d, got %d", offsetPerSizeTracking, L)
}
if L := len(grandParentSrc); L < offsetPerSizeTracking+perSizeTrackingCount*2 {
return item, 0, fmt.Errorf("reading TrackTableEntry: "+"EOF: expected length: %d, got %d", offsetPerSizeTracking+perSizeTrackingCount*2, L)
}
item.PerSizeTracking = make([]int16, perSizeTrackingCount) // allocation guarded by the previous check
for i := range item.PerSizeTracking {
item.PerSizeTracking[i] = int16(binary.BigEndian.Uint16(grandParentSrc[offsetPerSizeTracking+i*2:]))
}
offsetPerSizeTracking += perSizeTrackingCount * 2
}
}
return item, n, nil
}
func ParseTrak(src []byte) (Trak, int, error) {
var item Trak
n := 0
if L := len(src); L < 12 {
return item, 0, fmt.Errorf("reading Trak: "+"EOF: expected length: 12, got %d", L)
}
_ = src[11] // early bound checking
item.version = binary.BigEndian.Uint32(src[0:])
item.format = binary.BigEndian.Uint16(src[4:])
offsetHoriz := int(binary.BigEndian.Uint16(src[6:]))
offsetVert := int(binary.BigEndian.Uint16(src[8:]))
item.reserved = binary.BigEndian.Uint16(src[10:])
n += 12
{
if offsetHoriz != 0 { // ignore null offset
if L := len(src); L < offsetHoriz {
return item, 0, fmt.Errorf("reading Trak: "+"EOF: expected length: %d, got %d", offsetHoriz, L)
}
var err error
item.Horiz, _, err = ParseTrackData(src[offsetHoriz:], src)
if err != nil {
return item, 0, fmt.Errorf("reading Trak: %s", err)
}
}
}
{
if offsetVert != 0 { // ignore null offset
if L := len(src); L < offsetVert {
return item, 0, fmt.Errorf("reading Trak: "+"EOF: expected length: %d, got %d", offsetVert, L)
}
var err error
item.Vert, _, err = ParseTrackData(src[offsetVert:], src)
if err != nil {
return item, 0, fmt.Errorf("reading Trak: %s", err)
}
}
}
return item, n, nil
}
@@ -0,0 +1,137 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
// Trak is the tracking table.
// See - https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6trak.html
type Trak struct {
version uint32 // Version number of the tracking table (0x00010000 for the current version).
format uint16 // Format of the tracking table (set to 0).
Horiz TrackData `offsetSize:"Offset16"` // Offset from start of tracking table to TrackData for horizontal text (or 0 if none).
Vert TrackData `offsetSize:"Offset16"` // Offset from start of tracking table to TrackData for vertical text (or 0 if none).
reserved uint16 // Reserved. Set to 0.
}
// IsEmpty return `true` it the table has no entries.
func (t Trak) IsEmpty() bool {
return len(t.Horiz.TrackTable)+len(t.Vert.TrackTable) == 0
}
type TrackData struct {
nTracks uint16 // Number of separate tracks included in this table.
nSizes uint16 // Number of point sizes included in this table.
SizeTable []Float1616 `offsetSize:"Offset32" offsetRelativeTo:"Parent" arrayCount:"ComputedField-nSizes"` // Offset from start of the tracking table to the start of the size subtable.
TrackTable []TrackTableEntry `arrayCount:"ComputedField-nTracks" arguments:"perSizeTrackingCount=.nSizes"` // Array[nTracks] of TrackTableEntry records.
}
// GetTracking selects the tracking for the given `track` and applies it
// for `ptem`. It returns 0 if not found.
func (td TrackData) GetTracking(ptem float32, track float32) float32 {
count := len(td.TrackTable)
if count == 0 {
return 0
} else if count == 1 {
return td.TrackTable[0].value(ptem, td.SizeTable)
}
// At least two entries.
i := 0
j := count - 1
// Find the two entries that track is between.
for i+1 < count && td.TrackTable[i+1].Track <= track {
i++
}
for j > 0 && td.TrackTable[j-1].Track >= track {
j--
}
// Exact match.
if i == j {
return td.TrackTable[i].value(ptem, td.SizeTable)
}
// Interpolate.
t0 := td.TrackTable[i].Track
t1 := td.TrackTable[j].Track
t := (track - t0) / (t1 - t0)
a := td.TrackTable[i].value(ptem, td.SizeTable)
b := td.TrackTable[j].value(ptem, td.SizeTable)
return a + t*(b-a)
}
type TrackTableEntry struct {
Track Float1616 // Track value for this record.
NameIndex uint16 // The 'name' table index for this track (a short word or phrase like "loose" or "very tight"). NameIndex has a value greater than 255 and less than 32768.
PerSizeTracking []int16 `offsetSize:"Offset16" offsetRelativeTo:"GrandParent"` // in font units, with length len(SizeTable)
}
func (entry *TrackTableEntry) value(ptem float32, sizeTable []Float1616) float32 {
values := entry.PerSizeTracking
nSizes := len(sizeTable)
// Choose size.
if nSizes == 0 {
return 0
}
if nSizes == 1 {
return float32(values[0])
}
// At least two entries.
var i int
for i = 0; i < nSizes; i++ {
if sizeTable[i] >= ptem {
break
}
}
// Boundary conditions.
if i == 0 {
return float32(values[0])
}
if i == nSizes {
return float32(values[nSizes-1])
}
// Exact match.
if sizeTable[i] == ptem {
return float32(values[i])
}
// Interpolate.
return entry.interpolateAt(i-1, ptem, sizeTable)
}
// idx is assumed to verify idx <= len(sizeTable) - 2
func (td *TrackTableEntry) interpolateAt(idx int, ptem float32, sizeTable []Float1616) float32 {
values := td.PerSizeTracking
s0 := sizeTable[idx]
s1 := sizeTable[idx+1]
v0 := float32(values[idx])
v1 := float32(values[idx+1])
// Deal with font bugs.
if s1 < s0 {
s0, s1 = s1, s0
v0, v1 = v1, v0
}
if ptem < s0 {
return v0
}
if ptem > s1 {
return v1
}
if s0 == s1 {
return (v0 + v1) * 0.5
}
t := (ptem - s0) / (s1 - s0)
return v0 + t*(v1-v0)
}
@@ -0,0 +1,742 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import (
"encoding/binary"
"fmt"
)
// Code generated by binarygen from cmap_src.go. DO NOT EDIT
func (item *CmapSubtable0) mustParse(src []byte) {
_ = src[261] // early bound checking
item.format = binary.BigEndian.Uint16(src[0:])
item.length = binary.BigEndian.Uint16(src[2:])
item.language = binary.BigEndian.Uint16(src[4:])
item.GlyphIdArray[0] = src[6]
item.GlyphIdArray[1] = src[7]
item.GlyphIdArray[2] = src[8]
item.GlyphIdArray[3] = src[9]
item.GlyphIdArray[4] = src[10]
item.GlyphIdArray[5] = src[11]
item.GlyphIdArray[6] = src[12]
item.GlyphIdArray[7] = src[13]
item.GlyphIdArray[8] = src[14]
item.GlyphIdArray[9] = src[15]
item.GlyphIdArray[10] = src[16]
item.GlyphIdArray[11] = src[17]
item.GlyphIdArray[12] = src[18]
item.GlyphIdArray[13] = src[19]
item.GlyphIdArray[14] = src[20]
item.GlyphIdArray[15] = src[21]
item.GlyphIdArray[16] = src[22]
item.GlyphIdArray[17] = src[23]
item.GlyphIdArray[18] = src[24]
item.GlyphIdArray[19] = src[25]
item.GlyphIdArray[20] = src[26]
item.GlyphIdArray[21] = src[27]
item.GlyphIdArray[22] = src[28]
item.GlyphIdArray[23] = src[29]
item.GlyphIdArray[24] = src[30]
item.GlyphIdArray[25] = src[31]
item.GlyphIdArray[26] = src[32]
item.GlyphIdArray[27] = src[33]
item.GlyphIdArray[28] = src[34]
item.GlyphIdArray[29] = src[35]
item.GlyphIdArray[30] = src[36]
item.GlyphIdArray[31] = src[37]
item.GlyphIdArray[32] = src[38]
item.GlyphIdArray[33] = src[39]
item.GlyphIdArray[34] = src[40]
item.GlyphIdArray[35] = src[41]
item.GlyphIdArray[36] = src[42]
item.GlyphIdArray[37] = src[43]
item.GlyphIdArray[38] = src[44]
item.GlyphIdArray[39] = src[45]
item.GlyphIdArray[40] = src[46]
item.GlyphIdArray[41] = src[47]
item.GlyphIdArray[42] = src[48]
item.GlyphIdArray[43] = src[49]
item.GlyphIdArray[44] = src[50]
item.GlyphIdArray[45] = src[51]
item.GlyphIdArray[46] = src[52]
item.GlyphIdArray[47] = src[53]
item.GlyphIdArray[48] = src[54]
item.GlyphIdArray[49] = src[55]
item.GlyphIdArray[50] = src[56]
item.GlyphIdArray[51] = src[57]
item.GlyphIdArray[52] = src[58]
item.GlyphIdArray[53] = src[59]
item.GlyphIdArray[54] = src[60]
item.GlyphIdArray[55] = src[61]
item.GlyphIdArray[56] = src[62]
item.GlyphIdArray[57] = src[63]
item.GlyphIdArray[58] = src[64]
item.GlyphIdArray[59] = src[65]
item.GlyphIdArray[60] = src[66]
item.GlyphIdArray[61] = src[67]
item.GlyphIdArray[62] = src[68]
item.GlyphIdArray[63] = src[69]
item.GlyphIdArray[64] = src[70]
item.GlyphIdArray[65] = src[71]
item.GlyphIdArray[66] = src[72]
item.GlyphIdArray[67] = src[73]
item.GlyphIdArray[68] = src[74]
item.GlyphIdArray[69] = src[75]
item.GlyphIdArray[70] = src[76]
item.GlyphIdArray[71] = src[77]
item.GlyphIdArray[72] = src[78]
item.GlyphIdArray[73] = src[79]
item.GlyphIdArray[74] = src[80]
item.GlyphIdArray[75] = src[81]
item.GlyphIdArray[76] = src[82]
item.GlyphIdArray[77] = src[83]
item.GlyphIdArray[78] = src[84]
item.GlyphIdArray[79] = src[85]
item.GlyphIdArray[80] = src[86]
item.GlyphIdArray[81] = src[87]
item.GlyphIdArray[82] = src[88]
item.GlyphIdArray[83] = src[89]
item.GlyphIdArray[84] = src[90]
item.GlyphIdArray[85] = src[91]
item.GlyphIdArray[86] = src[92]
item.GlyphIdArray[87] = src[93]
item.GlyphIdArray[88] = src[94]
item.GlyphIdArray[89] = src[95]
item.GlyphIdArray[90] = src[96]
item.GlyphIdArray[91] = src[97]
item.GlyphIdArray[92] = src[98]
item.GlyphIdArray[93] = src[99]
item.GlyphIdArray[94] = src[100]
item.GlyphIdArray[95] = src[101]
item.GlyphIdArray[96] = src[102]
item.GlyphIdArray[97] = src[103]
item.GlyphIdArray[98] = src[104]
item.GlyphIdArray[99] = src[105]
item.GlyphIdArray[100] = src[106]
item.GlyphIdArray[101] = src[107]
item.GlyphIdArray[102] = src[108]
item.GlyphIdArray[103] = src[109]
item.GlyphIdArray[104] = src[110]
item.GlyphIdArray[105] = src[111]
item.GlyphIdArray[106] = src[112]
item.GlyphIdArray[107] = src[113]
item.GlyphIdArray[108] = src[114]
item.GlyphIdArray[109] = src[115]
item.GlyphIdArray[110] = src[116]
item.GlyphIdArray[111] = src[117]
item.GlyphIdArray[112] = src[118]
item.GlyphIdArray[113] = src[119]
item.GlyphIdArray[114] = src[120]
item.GlyphIdArray[115] = src[121]
item.GlyphIdArray[116] = src[122]
item.GlyphIdArray[117] = src[123]
item.GlyphIdArray[118] = src[124]
item.GlyphIdArray[119] = src[125]
item.GlyphIdArray[120] = src[126]
item.GlyphIdArray[121] = src[127]
item.GlyphIdArray[122] = src[128]
item.GlyphIdArray[123] = src[129]
item.GlyphIdArray[124] = src[130]
item.GlyphIdArray[125] = src[131]
item.GlyphIdArray[126] = src[132]
item.GlyphIdArray[127] = src[133]
item.GlyphIdArray[128] = src[134]
item.GlyphIdArray[129] = src[135]
item.GlyphIdArray[130] = src[136]
item.GlyphIdArray[131] = src[137]
item.GlyphIdArray[132] = src[138]
item.GlyphIdArray[133] = src[139]
item.GlyphIdArray[134] = src[140]
item.GlyphIdArray[135] = src[141]
item.GlyphIdArray[136] = src[142]
item.GlyphIdArray[137] = src[143]
item.GlyphIdArray[138] = src[144]
item.GlyphIdArray[139] = src[145]
item.GlyphIdArray[140] = src[146]
item.GlyphIdArray[141] = src[147]
item.GlyphIdArray[142] = src[148]
item.GlyphIdArray[143] = src[149]
item.GlyphIdArray[144] = src[150]
item.GlyphIdArray[145] = src[151]
item.GlyphIdArray[146] = src[152]
item.GlyphIdArray[147] = src[153]
item.GlyphIdArray[148] = src[154]
item.GlyphIdArray[149] = src[155]
item.GlyphIdArray[150] = src[156]
item.GlyphIdArray[151] = src[157]
item.GlyphIdArray[152] = src[158]
item.GlyphIdArray[153] = src[159]
item.GlyphIdArray[154] = src[160]
item.GlyphIdArray[155] = src[161]
item.GlyphIdArray[156] = src[162]
item.GlyphIdArray[157] = src[163]
item.GlyphIdArray[158] = src[164]
item.GlyphIdArray[159] = src[165]
item.GlyphIdArray[160] = src[166]
item.GlyphIdArray[161] = src[167]
item.GlyphIdArray[162] = src[168]
item.GlyphIdArray[163] = src[169]
item.GlyphIdArray[164] = src[170]
item.GlyphIdArray[165] = src[171]
item.GlyphIdArray[166] = src[172]
item.GlyphIdArray[167] = src[173]
item.GlyphIdArray[168] = src[174]
item.GlyphIdArray[169] = src[175]
item.GlyphIdArray[170] = src[176]
item.GlyphIdArray[171] = src[177]
item.GlyphIdArray[172] = src[178]
item.GlyphIdArray[173] = src[179]
item.GlyphIdArray[174] = src[180]
item.GlyphIdArray[175] = src[181]
item.GlyphIdArray[176] = src[182]
item.GlyphIdArray[177] = src[183]
item.GlyphIdArray[178] = src[184]
item.GlyphIdArray[179] = src[185]
item.GlyphIdArray[180] = src[186]
item.GlyphIdArray[181] = src[187]
item.GlyphIdArray[182] = src[188]
item.GlyphIdArray[183] = src[189]
item.GlyphIdArray[184] = src[190]
item.GlyphIdArray[185] = src[191]
item.GlyphIdArray[186] = src[192]
item.GlyphIdArray[187] = src[193]
item.GlyphIdArray[188] = src[194]
item.GlyphIdArray[189] = src[195]
item.GlyphIdArray[190] = src[196]
item.GlyphIdArray[191] = src[197]
item.GlyphIdArray[192] = src[198]
item.GlyphIdArray[193] = src[199]
item.GlyphIdArray[194] = src[200]
item.GlyphIdArray[195] = src[201]
item.GlyphIdArray[196] = src[202]
item.GlyphIdArray[197] = src[203]
item.GlyphIdArray[198] = src[204]
item.GlyphIdArray[199] = src[205]
item.GlyphIdArray[200] = src[206]
item.GlyphIdArray[201] = src[207]
item.GlyphIdArray[202] = src[208]
item.GlyphIdArray[203] = src[209]
item.GlyphIdArray[204] = src[210]
item.GlyphIdArray[205] = src[211]
item.GlyphIdArray[206] = src[212]
item.GlyphIdArray[207] = src[213]
item.GlyphIdArray[208] = src[214]
item.GlyphIdArray[209] = src[215]
item.GlyphIdArray[210] = src[216]
item.GlyphIdArray[211] = src[217]
item.GlyphIdArray[212] = src[218]
item.GlyphIdArray[213] = src[219]
item.GlyphIdArray[214] = src[220]
item.GlyphIdArray[215] = src[221]
item.GlyphIdArray[216] = src[222]
item.GlyphIdArray[217] = src[223]
item.GlyphIdArray[218] = src[224]
item.GlyphIdArray[219] = src[225]
item.GlyphIdArray[220] = src[226]
item.GlyphIdArray[221] = src[227]
item.GlyphIdArray[222] = src[228]
item.GlyphIdArray[223] = src[229]
item.GlyphIdArray[224] = src[230]
item.GlyphIdArray[225] = src[231]
item.GlyphIdArray[226] = src[232]
item.GlyphIdArray[227] = src[233]
item.GlyphIdArray[228] = src[234]
item.GlyphIdArray[229] = src[235]
item.GlyphIdArray[230] = src[236]
item.GlyphIdArray[231] = src[237]
item.GlyphIdArray[232] = src[238]
item.GlyphIdArray[233] = src[239]
item.GlyphIdArray[234] = src[240]
item.GlyphIdArray[235] = src[241]
item.GlyphIdArray[236] = src[242]
item.GlyphIdArray[237] = src[243]
item.GlyphIdArray[238] = src[244]
item.GlyphIdArray[239] = src[245]
item.GlyphIdArray[240] = src[246]
item.GlyphIdArray[241] = src[247]
item.GlyphIdArray[242] = src[248]
item.GlyphIdArray[243] = src[249]
item.GlyphIdArray[244] = src[250]
item.GlyphIdArray[245] = src[251]
item.GlyphIdArray[246] = src[252]
item.GlyphIdArray[247] = src[253]
item.GlyphIdArray[248] = src[254]
item.GlyphIdArray[249] = src[255]
item.GlyphIdArray[250] = src[256]
item.GlyphIdArray[251] = src[257]
item.GlyphIdArray[252] = src[258]
item.GlyphIdArray[253] = src[259]
item.GlyphIdArray[254] = src[260]
item.GlyphIdArray[255] = src[261]
}
func ParseCmap(src []byte) (Cmap, int, error) {
var item Cmap
n := 0
if L := len(src); L < 4 {
return item, 0, fmt.Errorf("reading Cmap: "+"EOF: expected length: 4, got %d", L)
}
_ = src[3] // early bound checking
item.version = binary.BigEndian.Uint16(src[0:])
item.numTables = binary.BigEndian.Uint16(src[2:])
n += 4
{
arrayLength := int(item.numTables)
offset := 4
for i := 0; i < arrayLength; i++ {
elem, read, err := ParseEncodingRecord(src[offset:], src)
if err != nil {
return item, 0, fmt.Errorf("reading Cmap: %s", err)
}
item.Records = append(item.Records, elem)
offset += read
}
n = offset
}
return item, n, nil
}
func ParseCmapSubtable(src []byte) (CmapSubtable, int, error) {
var item CmapSubtable
if L := len(src); L < 2 {
return item, 0, fmt.Errorf("reading CmapSubtable: "+"EOF: expected length: 2, got %d", L)
}
format := uint16(binary.BigEndian.Uint16(src[0:]))
var (
read int
err error
)
switch format {
case 0:
item, read, err = ParseCmapSubtable0(src[0:])
case 10:
item, read, err = ParseCmapSubtable10(src[0:])
case 12:
item, read, err = ParseCmapSubtable12(src[0:])
case 13:
item, read, err = ParseCmapSubtable13(src[0:])
case 14:
item, read, err = ParseCmapSubtable14(src[0:])
case 2:
item, read, err = ParseCmapSubtable2(src[0:])
case 4:
item, read, err = ParseCmapSubtable4(src[0:])
case 6:
item, read, err = ParseCmapSubtable6(src[0:])
default:
err = fmt.Errorf("unsupported CmapSubtable format %d", format)
}
if err != nil {
return item, 0, fmt.Errorf("reading CmapSubtable: %s", err)
}
return item, read, nil
}
func ParseCmapSubtable0(src []byte) (CmapSubtable0, int, error) {
var item CmapSubtable0
n := 0
if L := len(src); L < 262 {
return item, 0, fmt.Errorf("reading CmapSubtable0: "+"EOF: expected length: 262, got %d", L)
}
item.mustParse(src)
n += 262
return item, n, nil
}
func ParseCmapSubtable10(src []byte) (CmapSubtable10, int, error) {
var item CmapSubtable10
n := 0
if L := len(src); L < 20 {
return item, 0, fmt.Errorf("reading CmapSubtable10: "+"EOF: expected length: 20, got %d", L)
}
_ = src[19] // early bound checking
item.format = binary.BigEndian.Uint16(src[0:])
item.reserved = binary.BigEndian.Uint16(src[2:])
item.length = binary.BigEndian.Uint32(src[4:])
item.language = binary.BigEndian.Uint32(src[8:])
item.StartCharCode = binary.BigEndian.Uint32(src[12:])
arrayLengthGlyphIdArray := int(binary.BigEndian.Uint32(src[16:]))
n += 20
{
if L := len(src); L < 20+arrayLengthGlyphIdArray*2 {
return item, 0, fmt.Errorf("reading CmapSubtable10: "+"EOF: expected length: %d, got %d", 20+arrayLengthGlyphIdArray*2, L)
}
item.GlyphIdArray = make([]uint16, arrayLengthGlyphIdArray) // allocation guarded by the previous check
for i := range item.GlyphIdArray {
item.GlyphIdArray[i] = binary.BigEndian.Uint16(src[20+i*2:])
}
n += arrayLengthGlyphIdArray * 2
}
return item, n, nil
}
func ParseCmapSubtable12(src []byte) (CmapSubtable12, int, error) {
var item CmapSubtable12
n := 0
if L := len(src); L < 16 {
return item, 0, fmt.Errorf("reading CmapSubtable12: "+"EOF: expected length: 16, got %d", L)
}
_ = src[15] // early bound checking
item.format = binary.BigEndian.Uint16(src[0:])
item.reserved = binary.BigEndian.Uint16(src[2:])
item.length = binary.BigEndian.Uint32(src[4:])
item.language = binary.BigEndian.Uint32(src[8:])
arrayLengthGroups := int(binary.BigEndian.Uint32(src[12:]))
n += 16
{
if L := len(src); L < 16+arrayLengthGroups*12 {
return item, 0, fmt.Errorf("reading CmapSubtable12: "+"EOF: expected length: %d, got %d", 16+arrayLengthGroups*12, L)
}
item.Groups = make([]SequentialMapGroup, arrayLengthGroups) // allocation guarded by the previous check
for i := range item.Groups {
item.Groups[i].mustParse(src[16+i*12:])
}
n += arrayLengthGroups * 12
}
return item, n, nil
}
func ParseCmapSubtable13(src []byte) (CmapSubtable13, int, error) {
var item CmapSubtable13
n := 0
if L := len(src); L < 16 {
return item, 0, fmt.Errorf("reading CmapSubtable13: "+"EOF: expected length: 16, got %d", L)
}
_ = src[15] // early bound checking
item.format = binary.BigEndian.Uint16(src[0:])
item.reserved = binary.BigEndian.Uint16(src[2:])
item.length = binary.BigEndian.Uint32(src[4:])
item.language = binary.BigEndian.Uint32(src[8:])
arrayLengthGroups := int(binary.BigEndian.Uint32(src[12:]))
n += 16
{
if L := len(src); L < 16+arrayLengthGroups*12 {
return item, 0, fmt.Errorf("reading CmapSubtable13: "+"EOF: expected length: %d, got %d", 16+arrayLengthGroups*12, L)
}
item.Groups = make([]SequentialMapGroup, arrayLengthGroups) // allocation guarded by the previous check
for i := range item.Groups {
item.Groups[i].mustParse(src[16+i*12:])
}
n += arrayLengthGroups * 12
}
return item, n, nil
}
func ParseCmapSubtable14(src []byte) (CmapSubtable14, int, error) {
var item CmapSubtable14
n := 0
if L := len(src); L < 10 {
return item, 0, fmt.Errorf("reading CmapSubtable14: "+"EOF: expected length: 10, got %d", L)
}
_ = src[9] // early bound checking
item.format = binary.BigEndian.Uint16(src[0:])
item.length = binary.BigEndian.Uint32(src[2:])
arrayLengthVarSelectors := int(binary.BigEndian.Uint32(src[6:]))
n += 10
{
offset := 10
for i := 0; i < arrayLengthVarSelectors; i++ {
elem, read, err := ParseVariationSelector(src[offset:], src)
if err != nil {
return item, 0, fmt.Errorf("reading CmapSubtable14: %s", err)
}
item.VarSelectors = append(item.VarSelectors, elem)
offset += read
}
n = offset
}
return item, n, nil
}
func ParseCmapSubtable2(src []byte) (CmapSubtable2, int, error) {
var item CmapSubtable2
n := 0
if L := len(src); L < 2 {
return item, 0, fmt.Errorf("reading CmapSubtable2: "+"EOF: expected length: 2, got %d", L)
}
item.format = binary.BigEndian.Uint16(src[0:])
n += 2
{
item.rawData = src[2:]
n = len(src)
}
return item, n, nil
}
func ParseCmapSubtable4(src []byte) (CmapSubtable4, int, error) {
var item CmapSubtable4
n := 0
if L := len(src); L < 14 {
return item, 0, fmt.Errorf("reading CmapSubtable4: "+"EOF: expected length: 14, got %d", L)
}
_ = src[13] // early bound checking
item.format = binary.BigEndian.Uint16(src[0:])
item.length = binary.BigEndian.Uint16(src[2:])
item.language = binary.BigEndian.Uint16(src[4:])
item.segCountX2 = binary.BigEndian.Uint16(src[6:])
item.searchRange = binary.BigEndian.Uint16(src[8:])
item.entrySelector = binary.BigEndian.Uint16(src[10:])
item.rangeShift = binary.BigEndian.Uint16(src[12:])
n += 14
{
arrayLength := int(item.segCountX2 / 2)
if L := len(src); L < 14+arrayLength*2 {
return item, 0, fmt.Errorf("reading CmapSubtable4: "+"EOF: expected length: %d, got %d", 14+arrayLength*2, L)
}
item.EndCode = make([]uint16, arrayLength) // allocation guarded by the previous check
for i := range item.EndCode {
item.EndCode[i] = binary.BigEndian.Uint16(src[14+i*2:])
}
n += arrayLength * 2
}
if L := len(src); L < n+2 {
return item, 0, fmt.Errorf("reading CmapSubtable4: "+"EOF: expected length: n + 2, got %d", L)
}
item.reservedPad = binary.BigEndian.Uint16(src[n:])
n += 2
{
arrayLength := int(item.segCountX2 / 2)
if L := len(src); L < n+arrayLength*2 {
return item, 0, fmt.Errorf("reading CmapSubtable4: "+"EOF: expected length: %d, got %d", n+arrayLength*2, L)
}
item.StartCode = make([]uint16, arrayLength) // allocation guarded by the previous check
for i := range item.StartCode {
item.StartCode[i] = binary.BigEndian.Uint16(src[n+i*2:])
}
n += arrayLength * 2
}
{
arrayLength := int(item.segCountX2 / 2)
if L := len(src); L < n+arrayLength*2 {
return item, 0, fmt.Errorf("reading CmapSubtable4: "+"EOF: expected length: %d, got %d", n+arrayLength*2, L)
}
item.IdDelta = make([]uint16, arrayLength) // allocation guarded by the previous check
for i := range item.IdDelta {
item.IdDelta[i] = binary.BigEndian.Uint16(src[n+i*2:])
}
n += arrayLength * 2
}
{
arrayLength := int(item.segCountX2 / 2)
if L := len(src); L < n+arrayLength*2 {
return item, 0, fmt.Errorf("reading CmapSubtable4: "+"EOF: expected length: %d, got %d", n+arrayLength*2, L)
}
item.IdRangeOffsets = make([]uint16, arrayLength) // allocation guarded by the previous check
for i := range item.IdRangeOffsets {
item.IdRangeOffsets[i] = binary.BigEndian.Uint16(src[n+i*2:])
}
n += arrayLength * 2
}
{
item.GlyphIDArray = src[n:]
n = len(src)
}
return item, n, nil
}
func ParseCmapSubtable6(src []byte) (CmapSubtable6, int, error) {
var item CmapSubtable6
n := 0
if L := len(src); L < 10 {
return item, 0, fmt.Errorf("reading CmapSubtable6: "+"EOF: expected length: 10, got %d", L)
}
_ = src[9] // early bound checking
item.format = binary.BigEndian.Uint16(src[0:])
item.length = binary.BigEndian.Uint16(src[2:])
item.language = binary.BigEndian.Uint16(src[4:])
item.FirstCode = binary.BigEndian.Uint16(src[6:])
arrayLengthGlyphIdArray := int(binary.BigEndian.Uint16(src[8:]))
n += 10
{
if L := len(src); L < 10+arrayLengthGlyphIdArray*2 {
return item, 0, fmt.Errorf("reading CmapSubtable6: "+"EOF: expected length: %d, got %d", 10+arrayLengthGlyphIdArray*2, L)
}
item.GlyphIdArray = make([]uint16, arrayLengthGlyphIdArray) // allocation guarded by the previous check
for i := range item.GlyphIdArray {
item.GlyphIdArray[i] = binary.BigEndian.Uint16(src[10+i*2:])
}
n += arrayLengthGlyphIdArray * 2
}
return item, n, nil
}
func ParseDefaultUVSTable(src []byte) (DefaultUVSTable, int, error) {
var item DefaultUVSTable
n := 0
if L := len(src); L < 4 {
return item, 0, fmt.Errorf("reading DefaultUVSTable: "+"EOF: expected length: 4, got %d", L)
}
arrayLengthRanges := int(binary.BigEndian.Uint32(src[0:]))
n += 4
{
if L := len(src); L < 4+arrayLengthRanges*4 {
return item, 0, fmt.Errorf("reading DefaultUVSTable: "+"EOF: expected length: %d, got %d", 4+arrayLengthRanges*4, L)
}
item.Ranges = make([]UnicodeRange, arrayLengthRanges) // allocation guarded by the previous check
for i := range item.Ranges {
item.Ranges[i].mustParse(src[4+i*4:])
}
n += arrayLengthRanges * 4
}
return item, n, nil
}
func ParseEncodingRecord(src []byte, parentSrc []byte) (EncodingRecord, int, error) {
var item EncodingRecord
n := 0
if L := len(src); L < 8 {
return item, 0, fmt.Errorf("reading EncodingRecord: "+"EOF: expected length: 8, got %d", L)
}
_ = src[7] // early bound checking
item.PlatformID = PlatformID(binary.BigEndian.Uint16(src[0:]))
item.EncodingID = EncodingID(binary.BigEndian.Uint16(src[2:]))
offsetSubtable := int(binary.BigEndian.Uint32(src[4:]))
n += 8
{
if offsetSubtable != 0 { // ignore null offset
if L := len(parentSrc); L < offsetSubtable {
return item, 0, fmt.Errorf("reading EncodingRecord: "+"EOF: expected length: %d, got %d", offsetSubtable, L)
}
var (
err error
read int
)
item.Subtable, read, err = ParseCmapSubtable(parentSrc[offsetSubtable:])
if err != nil {
return item, 0, fmt.Errorf("reading EncodingRecord: %s", err)
}
offsetSubtable += read
}
}
return item, n, nil
}
func ParseUVSMappingTable(src []byte) (UVSMappingTable, int, error) {
var item UVSMappingTable
n := 0
if L := len(src); L < 4 {
return item, 0, fmt.Errorf("reading UVSMappingTable: "+"EOF: expected length: 4, got %d", L)
}
arrayLengthRanges := int(binary.BigEndian.Uint32(src[0:]))
n += 4
{
if L := len(src); L < 4+arrayLengthRanges*5 {
return item, 0, fmt.Errorf("reading UVSMappingTable: "+"EOF: expected length: %d, got %d", 4+arrayLengthRanges*5, L)
}
item.Ranges = make([]UvsMappingRecord, arrayLengthRanges) // allocation guarded by the previous check
for i := range item.Ranges {
item.Ranges[i].mustParse(src[4+i*5:])
}
n += arrayLengthRanges * 5
}
return item, n, nil
}
func ParseVariationSelector(src []byte, parentSrc []byte) (VariationSelector, int, error) {
var item VariationSelector
n := 0
if L := len(src); L < 11 {
return item, 0, fmt.Errorf("reading VariationSelector: "+"EOF: expected length: 11, got %d", L)
}
_ = src[10] // early bound checking
item.VarSelector[0] = src[0]
item.VarSelector[1] = src[1]
item.VarSelector[2] = src[2]
offsetDefaultUVS := int(binary.BigEndian.Uint32(src[3:]))
offsetNonDefaultUVS := int(binary.BigEndian.Uint32(src[7:]))
n += 11
{
if offsetDefaultUVS != 0 { // ignore null offset
if L := len(parentSrc); L < offsetDefaultUVS {
return item, 0, fmt.Errorf("reading VariationSelector: "+"EOF: expected length: %d, got %d", offsetDefaultUVS, L)
}
var err error
item.DefaultUVS, _, err = ParseDefaultUVSTable(parentSrc[offsetDefaultUVS:])
if err != nil {
return item, 0, fmt.Errorf("reading VariationSelector: %s", err)
}
}
}
{
if offsetNonDefaultUVS != 0 { // ignore null offset
if L := len(parentSrc); L < offsetNonDefaultUVS {
return item, 0, fmt.Errorf("reading VariationSelector: "+"EOF: expected length: %d, got %d", offsetNonDefaultUVS, L)
}
var err error
item.NonDefaultUVS, _, err = ParseUVSMappingTable(parentSrc[offsetNonDefaultUVS:])
if err != nil {
return item, 0, fmt.Errorf("reading VariationSelector: %s", err)
}
}
}
return item, n, nil
}
func (item *SequentialMapGroup) mustParse(src []byte) {
_ = src[11] // early bound checking
item.StartCharCode = binary.BigEndian.Uint32(src[0:])
item.EndCharCode = binary.BigEndian.Uint32(src[4:])
item.StartGlyphID = binary.BigEndian.Uint32(src[8:])
}
func (item *UnicodeRange) mustParse(src []byte) {
_ = src[3] // early bound checking
item.StartUnicodeValue[0] = src[0]
item.StartUnicodeValue[1] = src[1]
item.StartUnicodeValue[2] = src[2]
item.AdditionalCount = src[3]
}
func (item *UvsMappingRecord) mustParse(src []byte) {
_ = src[4] // early bound checking
item.UnicodeValue[0] = src[0]
item.UnicodeValue[1] = src[1]
item.UnicodeValue[2] = src[2]
item.GlyphID = binary.BigEndian.Uint16(src[3:])
}
@@ -0,0 +1,132 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
// Cmap is the Character to Glyph Index Mapping table
// See https://learn.microsoft.com/en-us/typography/opentype/spec/cmap
type Cmap struct {
version uint16 // Table version number (0).
numTables uint16 // Number of encoding tables that follow.
Records []EncodingRecord `arrayCount:"ComputedField-numTables"`
}
type EncodingRecord struct {
PlatformID PlatformID // Platform ID.
EncodingID EncodingID // Platform-specific encoding ID.
Subtable CmapSubtable `offsetSize:"Offset32" offsetRelativeTo:"Parent"` // Byte offset from beginning of table to the subtable for this encoding.
}
// CmapSubtable is the union type for the various cmap formats
type CmapSubtable interface {
isCmapSubtable()
}
func (CmapSubtable0) isCmapSubtable() {}
func (CmapSubtable2) isCmapSubtable() {}
func (CmapSubtable4) isCmapSubtable() {}
func (CmapSubtable6) isCmapSubtable() {}
func (CmapSubtable10) isCmapSubtable() {}
func (CmapSubtable12) isCmapSubtable() {}
func (CmapSubtable13) isCmapSubtable() {}
func (CmapSubtable14) isCmapSubtable() {}
type CmapSubtable0 struct {
format uint16 `unionTag:"0"` // Format number is set to 0.
length uint16 // This is the length in bytes of the subtable.
language uint16
GlyphIdArray [256]uint8 // An array that maps character codes to glyph index values.
}
type CmapSubtable2 struct {
format uint16 `unionTag:"2"` // Format number is set to 2.
rawData []byte `arrayCount:"ToEnd"`
}
type CmapSubtable4 struct {
format uint16 `unionTag:"4"` // Format number is set to 4.
length uint16 // This is the length in bytes of the subtable.
language uint16
segCountX2 uint16 // 2 × segCount.
searchRange uint16 // Maximum power of 2 less than or equal to segCount, times 2 ((2**floor(log2(segCount))) * 2, where “**” is an exponentiation operator)
entrySelector uint16 // Log2 of the maximum power of 2 less than or equal to numTables (log2(searchRange/2), which is equal to floor(log2(segCount)))
rangeShift uint16 // segCount times 2, minus searchRange ((segCount * 2) - searchRange)
EndCode []uint16 `arrayCount:"ComputedField-segCountX2 / 2"` // [segCount]uint16 End characterCode for each segment, last=0xFFFF.
reservedPad uint16 // Set to 0.
StartCode []uint16 `arrayCount:"ComputedField-segCountX2 / 2"` // [segCount]uint16 Start character code for each segment.
IdDelta []uint16 `arrayCount:"ComputedField-segCountX2 / 2"` // [segCount]int16 Delta for all character codes in segment.
IdRangeOffsets []uint16 `arrayCount:"ComputedField-segCountX2 / 2"` // [segCount]uint16 Offsets into glyphIdArray or 0
GlyphIDArray []byte `arrayCount:"ToEnd"` // glyphIdArray : uint16[] glyph index array (arbitrary length)
}
type CmapSubtable6 struct {
format uint16 `unionTag:"6"` // Format number is set to 6.
length uint16 // This is the length in bytes of the subtable.
language uint16
FirstCode uint16 // First character code of subrange.
GlyphIdArray []GlyphID `arrayCount:"FirstUint16"` // Array of glyph index values for character codes in the range.
}
type CmapSubtable10 struct {
format uint16 `unionTag:"10"` // Subtable format; set to 10.
reserved uint16 // Reserved; set to 0
length uint32 // Byte length of this subtable (including the header)
language uint32
StartCharCode uint32 // First character code covered
GlyphIdArray []GlyphID `arrayCount:"FirstUint32"` // Array of glyph indices for the character codes covered
}
type CmapSubtable12 struct {
format uint16 `unionTag:"12"` // Subtable format; set to 12.
reserved uint16 // Reserved; set to 0
length uint32 // Byte length of this subtable (including the header)
language uint32 // For requirements on use of the language field, see “Use of the language field in 'cmap' subtables” in this document.
Groups []SequentialMapGroup `arrayCount:"FirstUint32"` // Array of SequentialMapGroup records.
}
type SequentialMapGroup struct {
StartCharCode uint32 // First character code in this group
EndCharCode uint32 // Last character code in this group
StartGlyphID uint32 // Glyph index corresponding to the starting character code
}
type CmapSubtable13 struct {
format uint16 `unionTag:"13"` // Subtable format; set to 13.
reserved uint16 // Reserved; set to 0
length uint32 // Byte length of this subtable (including the header)
language uint32 // For requirements on use of the language field, see “Use of the language field in 'cmap' subtables” in this document.
Groups []SequentialMapGroup `arrayCount:"FirstUint32"` // Array of SequentialMapGroup records.
}
type CmapSubtable14 struct {
format uint16 `unionTag:"14"` // Subtable format. Set to 14.
length uint32 // Byte length of this subtable (including this header)
VarSelectors []VariationSelector `arrayCount:"FirstUint32"` // [numVarSelectorRecords] Array of VariationSelector records.
}
type VariationSelector struct {
VarSelector [3]byte // uint24 Variation selector
DefaultUVS DefaultUVSTable `offsetSize:"Offset32" offsetRelativeTo:"Parent"` // Offset from the start of the format 14 subtable to Default UVS Table. May be 0.
NonDefaultUVS UVSMappingTable `offsetSize:"Offset32" offsetRelativeTo:"Parent"` // Offset from the start of the format 14 subtable to Non-Default UVS Table. May be 0.
}
// DefaultUVSTable is used in Cmap format 14
// See https://learn.microsoft.com/en-us/typography/opentype/spec/cmap#default-uvs-table
type DefaultUVSTable struct {
Ranges []UnicodeRange `arrayCount:"FirstUint32"`
}
type UnicodeRange struct {
StartUnicodeValue [3]byte // uint24 First value in this range
AdditionalCount uint8 // Number of additional values in this range
}
// UVSMappingTable is used in Cmap format 14
// See https://learn.microsoft.com/en-us/typography/opentype/spec/cmap#non-default-uvs-table
type UVSMappingTable struct {
Ranges []UvsMappingRecord `arrayCount:"FirstUint32"`
}
type UvsMappingRecord struct {
UnicodeValue [3]byte // uint24 Base Unicode value of the UVS
GlyphID GlyphID // Glyph ID of the UVS
}
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
type BitmapSubtable struct {
FirstGlyph GlyphID // First glyph ID of this range.
LastGlyph GlyphID // Last glyph ID of this range (inclusive).
IndexSubHeader
}
// EBLC is the Embedded Bitmap Location Table
// See - https://learn.microsoft.com/fr-fr/typography/opentype/spec/eblc
type EBLC = CBLC
// Bloc is the bitmap location table
// See - https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6bloc.html
type Bloc = CBLC
// PaintColrLayersResolved is a simili PaintTable, build
// from COLR version 0 table.
type PaintColrLayersResolved []Layer
func (PaintColrLayersResolved) isPaintTable() {}
@@ -0,0 +1,363 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import (
"encoding/binary"
"fmt"
)
// Code generated by binarygen from glyphs_bitmap_src.go. DO NOT EDIT
func (item *BigGlyphMetrics) mustParse(src []byte) {
_ = src[7] // early bound checking
item.SmallGlyphMetrics.mustParse(src[0:])
item.vertBearingX = int8(src[5])
item.vertBearingY = int8(src[6])
item.vertAdvance = src[7]
}
func (item *BitmapSize) mustParse(src []byte) {
_ = src[47] // early bound checking
item.indexSubTableArrayOffset = Offset32(binary.BigEndian.Uint32(src[0:]))
item.indexTablesSize = binary.BigEndian.Uint32(src[4:])
item.numberOfIndexSubTables = binary.BigEndian.Uint32(src[8:])
item.colorRef = binary.BigEndian.Uint32(src[12:])
item.Hori.mustParse(src[16:])
item.Vert.mustParse(src[28:])
item.startGlyphIndex = binary.BigEndian.Uint16(src[40:])
item.endGlyphIndex = binary.BigEndian.Uint16(src[42:])
item.PpemX = src[44]
item.PpemY = src[45]
item.bitDepth = src[46]
item.flags = int8(src[47])
}
func (item *GlyphIdOffsetPair) mustParse(src []byte) {
_ = src[3] // early bound checking
item.GlyphID = binary.BigEndian.Uint16(src[0:])
item.SbitOffset = Offset16(binary.BigEndian.Uint16(src[2:]))
}
func (item *IndexData2) mustParse(src []byte) {
_ = src[11] // early bound checking
item.ImageSize = binary.BigEndian.Uint32(src[0:])
item.BigMetrics.mustParse(src[4:])
}
func (item *IndexSubTableHeader) mustParse(src []byte) {
_ = src[7] // early bound checking
item.FirstGlyph = binary.BigEndian.Uint16(src[0:])
item.LastGlyph = binary.BigEndian.Uint16(src[2:])
item.additionalOffsetToIndexSubtable = Offset32(binary.BigEndian.Uint32(src[4:]))
}
func ParseBitmapData17(src []byte) (BitmapData17, int, error) {
var item BitmapData17
n := 0
if L := len(src); L < 9 {
return item, 0, fmt.Errorf("reading BitmapData17: "+"EOF: expected length: 9, got %d", L)
}
_ = src[8] // early bound checking
item.SmallGlyphMetrics.mustParse(src[0:])
arrayLengthImage := int(binary.BigEndian.Uint32(src[5:]))
n += 9
{
L := int(9 + arrayLengthImage)
if len(src) < L {
return item, 0, fmt.Errorf("reading BitmapData17: "+"EOF: expected length: %d, got %d", L, len(src))
}
item.Image = src[9:L]
n = L
}
return item, n, nil
}
func ParseBitmapData18(src []byte) (BitmapData18, int, error) {
var item BitmapData18
n := 0
if L := len(src); L < 12 {
return item, 0, fmt.Errorf("reading BitmapData18: "+"EOF: expected length: 12, got %d", L)
}
_ = src[11] // early bound checking
item.BigGlyphMetrics.mustParse(src[0:])
arrayLengthImage := int(binary.BigEndian.Uint32(src[8:]))
n += 12
{
L := int(12 + arrayLengthImage)
if len(src) < L {
return item, 0, fmt.Errorf("reading BitmapData18: "+"EOF: expected length: %d, got %d", L, len(src))
}
item.Image = src[12:L]
n = L
}
return item, n, nil
}
func ParseBitmapData19(src []byte) (BitmapData19, int, error) {
var item BitmapData19
n := 0
if L := len(src); L < 4 {
return item, 0, fmt.Errorf("reading BitmapData19: "+"EOF: expected length: 4, got %d", L)
}
arrayLengthImage := int(binary.BigEndian.Uint32(src[0:]))
n += 4
{
L := int(4 + arrayLengthImage)
if len(src) < L {
return item, 0, fmt.Errorf("reading BitmapData19: "+"EOF: expected length: %d, got %d", L, len(src))
}
item.Image = src[4:L]
n = L
}
return item, n, nil
}
func ParseBitmapData2(src []byte) (BitmapData2, int, error) {
var item BitmapData2
n := 0
if L := len(src); L < 5 {
return item, 0, fmt.Errorf("reading BitmapData2: "+"EOF: expected length: 5, got %d", L)
}
item.SmallGlyphMetrics.mustParse(src[0:])
n += 5
{
item.Image = src[5:]
n = len(src)
}
return item, n, nil
}
func ParseBitmapData5(src []byte) (BitmapData5, int, error) {
var item BitmapData5
n := 0
{
item.Image = src[0:]
n = len(src)
}
return item, n, nil
}
func ParseCBLC(src []byte) (CBLC, int, error) {
var item CBLC
n := 0
if L := len(src); L < 8 {
return item, 0, fmt.Errorf("reading CBLC: "+"EOF: expected length: 8, got %d", L)
}
_ = src[7] // early bound checking
item.majorVersion = binary.BigEndian.Uint16(src[0:])
item.minorVersion = binary.BigEndian.Uint16(src[2:])
arrayLengthBitmapSizes := int(binary.BigEndian.Uint32(src[4:]))
n += 8
{
if L := len(src); L < 8+arrayLengthBitmapSizes*48 {
return item, 0, fmt.Errorf("reading CBLC: "+"EOF: expected length: %d, got %d", 8+arrayLengthBitmapSizes*48, L)
}
item.BitmapSizes = make([]BitmapSize, arrayLengthBitmapSizes) // allocation guarded by the previous check
for i := range item.BitmapSizes {
item.BitmapSizes[i].mustParse(src[8+i*48:])
}
n += arrayLengthBitmapSizes * 48
}
{
err := item.parseIndexSubTables(src[:])
if err != nil {
return item, 0, fmt.Errorf("reading CBLC: %s", err)
}
}
return item, n, nil
}
func ParseIndexData1(src []byte, sbitOffsetsCount int) (IndexData1, int, error) {
var item IndexData1
n := 0
{
if L := len(src); L < sbitOffsetsCount*4 {
return item, 0, fmt.Errorf("reading IndexData1: "+"EOF: expected length: %d, got %d", sbitOffsetsCount*4, L)
}
item.SbitOffsets = make([]Offset32, sbitOffsetsCount) // allocation guarded by the previous check
for i := range item.SbitOffsets {
item.SbitOffsets[i] = Offset32(binary.BigEndian.Uint32(src[i*4:]))
}
n += sbitOffsetsCount * 4
}
return item, n, nil
}
func ParseIndexData2(src []byte) (IndexData2, int, error) {
var item IndexData2
n := 0
if L := len(src); L < 12 {
return item, 0, fmt.Errorf("reading IndexData2: "+"EOF: expected length: 12, got %d", L)
}
item.mustParse(src)
n += 12
return item, n, nil
}
func ParseIndexData3(src []byte, sbitOffsetsCount int) (IndexData3, int, error) {
var item IndexData3
n := 0
{
if L := len(src); L < sbitOffsetsCount*2 {
return item, 0, fmt.Errorf("reading IndexData3: "+"EOF: expected length: %d, got %d", sbitOffsetsCount*2, L)
}
item.SbitOffsets = make([]Offset16, sbitOffsetsCount) // allocation guarded by the previous check
for i := range item.SbitOffsets {
item.SbitOffsets[i] = Offset16(binary.BigEndian.Uint16(src[i*2:]))
}
n += sbitOffsetsCount * 2
}
return item, n, nil
}
func ParseIndexData4(src []byte) (IndexData4, int, error) {
var item IndexData4
n := 0
if L := len(src); L < 4 {
return item, 0, fmt.Errorf("reading IndexData4: "+"EOF: expected length: 4, got %d", L)
}
item.numGlyphs = binary.BigEndian.Uint32(src[0:])
n += 4
{
arrayLength := int(item.numGlyphs + 1)
if L := len(src); L < 4+arrayLength*4 {
return item, 0, fmt.Errorf("reading IndexData4: "+"EOF: expected length: %d, got %d", 4+arrayLength*4, L)
}
item.GlyphArray = make([]GlyphIdOffsetPair, arrayLength) // allocation guarded by the previous check
for i := range item.GlyphArray {
item.GlyphArray[i].mustParse(src[4+i*4:])
}
n += arrayLength * 4
}
return item, n, nil
}
func ParseIndexData5(src []byte) (IndexData5, int, error) {
var item IndexData5
n := 0
if L := len(src); L < 16 {
return item, 0, fmt.Errorf("reading IndexData5: "+"EOF: expected length: 16, got %d", L)
}
_ = src[15] // early bound checking
item.ImageSize = binary.BigEndian.Uint32(src[0:])
item.BigMetrics.mustParse(src[4:])
arrayLengthGlyphIdArray := int(binary.BigEndian.Uint32(src[12:]))
n += 16
{
if L := len(src); L < 16+arrayLengthGlyphIdArray*2 {
return item, 0, fmt.Errorf("reading IndexData5: "+"EOF: expected length: %d, got %d", 16+arrayLengthGlyphIdArray*2, L)
}
item.GlyphIdArray = make([]uint16, arrayLengthGlyphIdArray) // allocation guarded by the previous check
for i := range item.GlyphIdArray {
item.GlyphIdArray[i] = binary.BigEndian.Uint16(src[16+i*2:])
}
n += arrayLengthGlyphIdArray * 2
}
return item, n, nil
}
func ParseIndexSubHeader(src []byte, sbitOffsetsCount int) (IndexSubHeader, int, error) {
var item IndexSubHeader
n := 0
if L := len(src); L < 8 {
return item, 0, fmt.Errorf("reading IndexSubHeader: "+"EOF: expected length: 8, got %d", L)
}
_ = src[7] // early bound checking
item.indexFormat = indexVersion(binary.BigEndian.Uint16(src[0:]))
item.ImageFormat = binary.BigEndian.Uint16(src[2:])
item.ImageDataOffset = Offset32(binary.BigEndian.Uint32(src[4:]))
n += 8
{
var (
read int
err error
)
switch item.indexFormat {
case indexVersion1:
item.IndexData, read, err = ParseIndexData1(src[8:], sbitOffsetsCount)
case indexVersion2:
item.IndexData, read, err = ParseIndexData2(src[8:])
case indexVersion3:
item.IndexData, read, err = ParseIndexData3(src[8:], sbitOffsetsCount)
case indexVersion4:
item.IndexData, read, err = ParseIndexData4(src[8:])
case indexVersion5:
item.IndexData, read, err = ParseIndexData5(src[8:])
default:
err = fmt.Errorf("unsupported IndexDataVersion %d", item.indexFormat)
}
if err != nil {
return item, 0, fmt.Errorf("reading IndexSubHeader: %s", err)
}
n += read
}
return item, n, nil
}
func ParseIndexSubTableArray(src []byte, subtablesCount int) (IndexSubTableArray, int, error) {
var item IndexSubTableArray
n := 0
{
if L := len(src); L < subtablesCount*8 {
return item, 0, fmt.Errorf("reading IndexSubTableArray: "+"EOF: expected length: %d, got %d", subtablesCount*8, L)
}
item.Subtables = make([]IndexSubTableHeader, subtablesCount) // allocation guarded by the previous check
for i := range item.Subtables {
item.Subtables[i].mustParse(src[i*8:])
}
n += subtablesCount * 8
}
return item, n, nil
}
func (item *SbitLineMetrics) mustParse(src []byte) {
_ = src[11] // early bound checking
item.Ascender = int8(src[0])
item.Descender = int8(src[1])
item.widthMax = src[2]
item.caretSlopeNumerator = int8(src[3])
item.caretSlopeDenominator = int8(src[4])
item.caretOffset = int8(src[5])
item.minOriginSB = int8(src[6])
item.minAdvanceSB = int8(src[7])
item.MaxBeforeBL = int8(src[8])
item.MinAfterBL = int8(src[9])
item.pad1 = int8(src[10])
item.pad2 = int8(src[11])
}
func (item *SmallGlyphMetrics) mustParse(src []byte) {
_ = src[4] // early bound checking
item.Height = src[0]
item.Width = src[1]
item.BearingX = int8(src[2])
item.BearingY = int8(src[3])
item.Advance = src[4]
}
@@ -0,0 +1,190 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import "fmt"
// CBLC is the Color Bitmap Location Table
// See - https://learn.microsoft.com/fr-fr/typography/opentype/spec/cblc
type CBLC struct {
majorVersion uint16 // Major version of the CBLC table, = 3.
minorVersion uint16 // Minor version of the CBLC table, = 0.
BitmapSizes []BitmapSize `arrayCount:"FirstUint32"` // BitmapSize records array.
IndexSubTables [][]BitmapSubtable `isOpaque:""` // with same length as [BitmapSizes]
}
func (cb *CBLC) parseIndexSubTables(src []byte) error {
cb.IndexSubTables = make([][]BitmapSubtable, len(cb.BitmapSizes))
for i, size := range cb.BitmapSizes {
start := int(size.indexSubTableArrayOffset)
if L := len(src); L < start {
return fmt.Errorf("EOF: expected length: %d, got %d", start, L)
}
subtables, _, err := ParseIndexSubTableArray(src[start:], int(size.numberOfIndexSubTables))
if err != nil {
return err
}
sizeSubtables := make([]BitmapSubtable, len(subtables.Subtables))
for j, subtable := range subtables.Subtables {
numGlyphs := int(subtable.LastGlyph) - int(subtable.FirstGlyph) + 1
subtableStart := start + int(subtable.additionalOffsetToIndexSubtable)
sizeSubtables[j].FirstGlyph = subtable.FirstGlyph
sizeSubtables[j].LastGlyph = subtable.LastGlyph
sizeSubtables[j].IndexSubHeader, _, err = ParseIndexSubHeader(src[subtableStart:], numGlyphs+1)
if err != nil {
return err
}
}
cb.IndexSubTables[i] = sizeSubtables
}
return nil
}
type BitmapSize struct {
indexSubTableArrayOffset Offset32 // Offset to index subtable from beginning of CBLC.
indexTablesSize uint32 // Number of bytes in corresponding index subtables and array.
numberOfIndexSubTables uint32 // There is an index subtable for each range or format change.
colorRef uint32 // Not used; set to 0.
Hori SbitLineMetrics // Line metrics for text rendered horizontally.
Vert SbitLineMetrics // Line metrics for text rendered vertically.
startGlyphIndex uint16 // Lowest glyph index for this size.
endGlyphIndex uint16 // Highest glyph index for this size.
PpemX uint8 // Horizontal pixels per em.
PpemY uint8 // Vertical pixels per em.
bitDepth uint8 // In addtition to already defined bitDepth values 1, 2, 4, and 8 supported by existing implementations, the value of 32 is used to identify color bitmaps with 8 bit per pixel RGBA channels.
flags int8 // Vertical or horizontal (see the Bitmap Flags section of the EBLC table chapter).
}
type SbitLineMetrics struct {
Ascender int8
Descender int8
widthMax uint8
caretSlopeNumerator int8
caretSlopeDenominator int8
caretOffset int8
minOriginSB int8
minAdvanceSB int8
MaxBeforeBL int8
MinAfterBL int8
pad1 int8
pad2 int8
}
type IndexSubTableArray struct {
Subtables []IndexSubTableHeader
}
type IndexSubTableHeader struct {
FirstGlyph GlyphID // First glyph ID of this range.
LastGlyph GlyphID // Last glyph ID of this range (inclusive).
additionalOffsetToIndexSubtable Offset32 // Add to indexSubTableArrayOffset to get offset from beginning of EBLC.
}
type IndexSubHeader struct {
indexFormat indexVersion // Format of this IndexSubTable.
ImageFormat uint16 // Format of EBDT image data.
ImageDataOffset Offset32 // Offset to image data in EBDT table.
IndexData IndexData `unionField:"indexFormat"`
}
type indexVersion uint16
const (
indexVersion1 indexVersion = iota + 1
indexVersion2
indexVersion3
indexVersion4
indexVersion5
)
type IndexData interface {
isIndexData()
}
func (IndexData1) isIndexData() {}
func (IndexData2) isIndexData() {}
func (IndexData3) isIndexData() {}
func (IndexData4) isIndexData() {}
func (IndexData5) isIndexData() {}
type IndexData1 struct {
// sizeOfArray = (lastGlyph - firstGlyph + 1) + 1 + 1 pad if needed
// sbitOffsets[glyphIndex] + imageDataOffset = glyphData
SbitOffsets []Offset32
}
type IndexData2 struct {
ImageSize uint32 // All the glyphs are of the same size.
BigMetrics BigGlyphMetrics // All glyphs have the same metrics; glyph data may be compressed, byte-aligned, or bit-aligned.
}
type IndexData3 struct {
// sizeOfArray = (lastGlyph - firstGlyph + 1) + 1 + 1 pad if needed
// sbitOffets[glyphIndex] + imageDataOffset = glyphData
SbitOffsets []Offset16
}
type IndexData4 struct {
numGlyphs uint32 // Array length.
GlyphArray []GlyphIdOffsetPair `arrayCount:"ComputedField-numGlyphs+1"` //[numGlyphs + 1] One per glyph.
}
type GlyphIdOffsetPair struct {
GlyphID GlyphID // Glyph ID of glyph present.
SbitOffset Offset16 // Location in EBDT.
}
type IndexData5 struct {
ImageSize uint32 // All glyphs have the same data size.
BigMetrics BigGlyphMetrics // All glyphs have the same metrics.
GlyphIdArray []GlyphID `arrayCount:"FirstUint32"` // [numGlyphs] One per glyph, sorted by glyph ID.
}
// ------------------------- actual data : shared by EBDT / CBDT / BDAT -------------------------
// for now, we simplify the implementation to two cases:
// - data, metrics (small)
// - data only
type SmallGlyphMetrics struct {
Height uint8 // Number of rows of data.
Width uint8 // Number of columns of data.
BearingX int8 // Distance in pixels from the horizontal origin to the left edge of the bitmap.
BearingY int8 // Distance in pixels from the horizontal origin to the top edge of the bitmap.
Advance uint8 // Horizontal advance width in pixels.
}
type BigGlyphMetrics struct {
SmallGlyphMetrics
vertBearingX int8 // Distance in pixels from the vertical origin to the left edge of the bitmap.
vertBearingY int8 // Distance in pixels from the vertical origin to the top edge of the bitmap.
vertAdvance uint8 // Vertical advance width in pixels.
}
// Format 2: small metrics, bit-aligned data
type BitmapData2 struct {
SmallGlyphMetrics
Image []byte `arrayCount:"ToEnd"`
}
// Format 5: metrics in CBLC table, bit-aligned image data only
type BitmapData5 struct {
Image []byte `arrayCount:"ToEnd"`
}
// Format 17: small metrics, PNG image data
type BitmapData17 struct {
SmallGlyphMetrics
Image []byte `arrayCount:"FirstUint32"`
}
// Format 18: big metrics, PNG image data
type BitmapData18 struct {
BigGlyphMetrics
Image []byte `arrayCount:"FirstUint32"`
}
// Format 19: metrics in CBLC table, PNG image data
type BitmapData19 struct {
Image []byte `arrayCount:"FirstUint32"`
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,577 @@
package tables
import (
"fmt"
"sort"
)
func ParseCOLR(src []byte) (COLR1, error) {
header, _, err := parseColr0(src)
if err != nil {
return COLR1{}, err
}
switch header.Version {
case 0:
return COLR1{colr0: header}, nil
case 1:
out, _, err := ParseCOLR1(src)
return out, err
default:
return COLR1{}, fmt.Errorf("unsupported version for COLR: %d", header.Version)
}
}
// https://learn.microsoft.com/en-us/typography/opentype/spec/colr#colr-table-formats
type colr0 struct {
Version uint16 // Table version number
numBaseGlyphRecords uint16 // Number of BaseGlyph records.
baseGlyphRecords []baseGlyph `arrayCount:"ComputedField-numBaseGlyphRecords" offsetSize:"Offset32"` // Offset to baseGlyphRecords array, from beginning of COLR table.
layerRecords []Layer `arrayCount:"ComputedField-numLayerRecords" offsetSize:"Offset32"` // Offset to layerRecords array, from beginning of COLR table.
numLayerRecords uint16 // Number of Layer records.
}
func (cl colr0) paintForGlyph(gi GlyphID) (PaintColrLayersResolved, bool) {
num := len(cl.baseGlyphRecords)
idx := sort.Search(num, func(i int) bool { return gi <= cl.baseGlyphRecords[i].GlyphID })
if idx >= num {
return nil, false
}
entry := cl.baseGlyphRecords[idx]
if gi != entry.GlyphID {
return nil, false
}
return cl.layerRecords[entry.FirstLayerIndex : entry.FirstLayerIndex+entry.NumLayers], true
}
type COLR1 struct {
colr0
baseGlyphList baseGlyphList `offsetSize:"Offset32"` // Offset to BaseGlyphList table, from beginning of COLR table.
LayerList LayerList `offsetSize:"Offset32"` // Offset to LayerList table, from beginning of COLR table (may be NULL).
ClipList ClipList `offsetSize:"Offset32"` // Offset to ClipList table, from beginning of COLR table (may be NULL).
VarIndexMap *DeltaSetMapping `offsetSize:"Offset32"` // Offset to DeltaSetIndexMap table, from beginning of COLR table (may be NULL).
ItemVariationStore *ItemVarStore `offsetSize:"Offset32"` // Offset to ItemVariationStore, from beginning of COLR table (may be NULL).
}
func (cl *COLR1) Search(gid GlyphID) (PaintTable, bool) {
if cl == nil {
return nil, false
}
// "Applications that support COLR version 1 should give preference to the version 1 color glyph.
// For applications that support COLR version 1, the application should search for a base glyph ID first in the BaseGlyphList.
// Then, if not found, search in the baseGlyphRecords array, if present."
if paint, ok := cl.baseGlyphList.paintForGlyph(gid); ok {
return paint, true
}
return cl.colr0.paintForGlyph(gid)
}
type baseGlyph struct {
GlyphID GlyphID // Glyph ID of the base glyph.
FirstLayerIndex uint16 // Index (base 0) into the layerRecords array.
NumLayers uint16 // Number of color layers associated with this glyph.
}
type Layer struct {
GlyphID GlyphID // Glyph ID of the glyph used for a given layer.
PaletteIndex uint16 // Index (base 0) for a palette entry in the CPAL table.
}
type baseGlyphList struct {
paintRecords []baseGlyphPaintRecord `arrayCount:"FirstUint32"` // numBaseGlyphPaintRecords
}
func (bl baseGlyphList) paintForGlyph(gi GlyphID) (PaintTable, bool) {
num := len(bl.paintRecords)
idx := sort.Search(num, func(i int) bool { return gi <= bl.paintRecords[i].GlyphID })
if idx >= num {
return nil, false
}
entry := bl.paintRecords[idx]
if gi != entry.GlyphID {
return nil, false
}
return entry.Paint, true
}
type baseGlyphPaintRecord struct {
GlyphID GlyphID // Glyph ID of the base glyph.
Paint PaintTable `offsetSize:"Offset32" offsetRelativeTo:"Parent"` // Offset to a Paint table, from beginning of BaseGlyphList table.
}
type LayerList struct {
paintTables []PaintTable `arrayCount:"FirstUint32" offsetsArray:"Offset32"` // Offsets to Paint tables, from beginning of LayerList table.
}
// Resolve returns an error for invalid (out of bounds) indices
func (ll LayerList) Resolve(paint PaintColrLayers) ([]PaintTable, error) {
last := paint.FirstLayerIndex + uint32(paint.NumLayers)
if L := len(ll.paintTables); int(last) > L {
return nil, fmt.Errorf("out of bounds PaintColrLayers: expected %d, got %d", last, L)
}
return ll.paintTables[paint.FirstLayerIndex:last], nil
}
type ClipList struct {
format uint8 // Set to 1.
clips []Clip `arrayCount:"FirstUint32"` // Clip records. Sorted by startGlyphID.
}
func (cl ClipList) Search(g GlyphID) (ClipBox, bool) {
// binary search
for i, j := 0, len(cl.clips); i < j; {
h := i + (j-i)/2
entry := cl.clips[h]
if g < entry.StartGlyphID {
j = h
} else if entry.EndGlyphID < g {
i = h + 1
} else {
return entry.ClipBox, true
}
}
return nil, false
}
type Clip struct {
StartGlyphID GlyphID // First glyph ID in the range.
EndGlyphID GlyphID // Last glyph ID in the range.
ClipBox ClipBox `offsetSize:"Offset24" offsetRelativeTo:"Parent"` // Offset to a ClipBox table, from beginning of ClipList table.
}
type ClipBox interface {
isClipBox()
}
func (ClipBoxFormat1) isClipBox() {}
func (ClipBoxFormat2) isClipBox() {}
// static clip box
type ClipBoxFormat1 struct {
format byte `unionTag:"1"`
XMin int16 // Minimum x of clip box.
YMin int16 // Minimum y of clip box.
XMax int16 // Maximum x of clip box.
YMax int16 // Maximum y of clip box.
}
// variable clip box
type ClipBoxFormat2 struct {
format byte `unionTag:"2"`
XMin int16 // Minimum x of clip box. For variation, use varIndexBase + 0.
YMin int16 // Minimum y of clip box. For variation, use varIndexBase + 1.
XMax int16 // Maximum x of clip box. For variation, use varIndexBase + 2.
YMax int16 // Maximum y of clip box. For variation, use varIndexBase + 3.
VarIndexBase uint32 // Base index into DeltaSetIndexMap.
}
type ColorStop struct {
StopOffset Fixed214 // Position on a color line.
PaletteIndex uint16 // Index for a CPAL palette entry.
Alpha Fixed214 // Alpha value.
}
type VarColorStop struct {
StopOffset Fixed214 // Position on a color line. For variation, use varIndexBase + 0.
PaletteIndex uint16 // Index for a CPAL palette entry.
Alpha Fixed214 // Alpha value. For variation, use varIndexBase + 1.
VarIndexBase uint32 // Base index into DeltaSetIndexMap
}
type Extend uint8
const (
ExtendPad Extend = iota // Use nearest color stop.
ExtendRepeat // Repeat from farthest color stop.
ExtendReflect // Mirror color line from nearest end.
)
type ColorLine struct {
Extend Extend // An Extend enum value.
ColorStops []ColorStop `arrayCount:"FirstUint16"` // [numStops]
}
type VarColorLine struct {
Extend Extend // An Extend enum value.
ColorStops []VarColorStop `arrayCount:"FirstUint16"` // [numStops] Allows for variations.
}
type Affine2x3 struct {
Xx Float1616 // x-component of transformed x-basis vector.
Yx Float1616 // y-component of transformed x-basis vector.
Xy Float1616 // x-component of transformed y-basis vector.
Yy Float1616 // y-component of transformed y-basis vector.
Dx Float1616 // Translation in x direction.
Dy Float1616 // Translation in y direction.
}
type VarAffine2x3 struct {
Xx Float1616 // x-component of transformed x-basis vector. For variation, use varIndexBase + 0.
Yx Float1616 // y-component of transformed x-basis vector. For variation, use varIndexBase + 1.
Xy Float1616 // x-component of transformed y-basis vector. For variation, use varIndexBase + 2.
Yy Float1616 // y-component of transformed y-basis vector. For variation, use varIndexBase + 3.
Dx Float1616 // Translation in x direction. For variation, use varIndexBase + 4.
Dy Float1616 // Translation in y direction. For variation, use varIndexBase + 5.
VarIndexBase uint32 // Base index into DeltaSetIndexMap.
}
type PaintTable interface {
isPaintTable()
}
func (PaintColrLayers) isPaintTable() {}
func (PaintSolid) isPaintTable() {}
func (PaintVarSolid) isPaintTable() {}
func (PaintLinearGradient) isPaintTable() {}
func (PaintVarLinearGradient) isPaintTable() {}
func (PaintRadialGradient) isPaintTable() {}
func (PaintVarRadialGradient) isPaintTable() {}
func (PaintSweepGradient) isPaintTable() {}
func (PaintVarSweepGradient) isPaintTable() {}
func (PaintGlyph) isPaintTable() {}
func (PaintColrGlyph) isPaintTable() {}
func (PaintTransform) isPaintTable() {}
func (PaintVarTransform) isPaintTable() {}
func (PaintTranslate) isPaintTable() {}
func (PaintVarTranslate) isPaintTable() {}
func (PaintScale) isPaintTable() {}
func (PaintVarScale) isPaintTable() {}
func (PaintScaleAroundCenter) isPaintTable() {}
func (PaintVarScaleAroundCenter) isPaintTable() {}
func (PaintScaleUniform) isPaintTable() {}
func (PaintVarScaleUniform) isPaintTable() {}
func (PaintScaleUniformAroundCenter) isPaintTable() {}
func (PaintVarScaleUniformAroundCenter) isPaintTable() {}
func (PaintRotate) isPaintTable() {}
func (PaintVarRotate) isPaintTable() {}
func (PaintRotateAroundCenter) isPaintTable() {}
func (PaintVarRotateAroundCenter) isPaintTable() {}
func (PaintSkew) isPaintTable() {}
func (PaintVarSkew) isPaintTable() {}
func (PaintSkewAroundCenter) isPaintTable() {}
func (PaintVarSkewAroundCenter) isPaintTable() {}
func (PaintComposite) isPaintTable() {}
// (format 1)
type PaintColrLayers struct {
format byte `unionTag:"1"`
NumLayers uint8 // Number of offsets to paint tables to read from LayerList.
FirstLayerIndex uint32 // Index (base 0) into the LayerList.
}
// (format 2)
type PaintSolid struct {
format byte `unionTag:"2"`
PaletteIndex uint16 // Index for a CPAL palette entry.
Alpha Fixed214 // Alpha value.
}
// (format 3)
type PaintVarSolid struct {
format byte `unionTag:"3"`
PaletteIndex uint16 // Index for a CPAL palette entry.
Alpha Fixed214 // Alpha value. For variation, use varIndexBase + 0.
VarIndexBase uint32 // Base index into DeltaSetIndexMap.
}
// (format 4)
type PaintLinearGradient struct {
format byte `unionTag:"4"`
ColorLine ColorLine `offsetSize:"Offset24"` // Offset to ColorLine table, from beginning of PaintLinearGradient table.
X0 int16 // Start point (p₀) x coordinate.
Y0 int16 // Start point (p₀) y coordinate.
X1 int16 // End point (p₁) x coordinate.
Y1 int16 // End point (p₁) y coordinate.
X2 int16 // Rotation point (p₂) x coordinate.
Y2 int16 // Rotation point (p₂) y coordinate.
}
// (format 5)
type PaintVarLinearGradient struct {
format byte `unionTag:"5"`
ColorLine VarColorLine `offsetSize:"Offset24"` // Offset to VarColorLine table, from beginning of PaintVarLinearGradient table.
X0 int16 // Start point (p₀) x coordinate. For variation, use varIndexBase + 0.
Y0 int16 // Start point (p₀) y coordinate. For variation, use varIndexBase + 1.
X1 int16 // End point (p₁) x coordinate. For variation, use varIndexBase + 2.
Y1 int16 // End point (p₁) y coordinate. For variation, use varIndexBase + 3.
X2 int16 // Rotation point (p₂) x coordinate. For variation, use varIndexBase + 4.
Y2 int16 // Rotation point (p₂) y coordinate. For variation, use varIndexBase + 5.
VarIndexBase uint32 // Base index into DeltaSetIndexMap.
}
// (format 6)
type PaintRadialGradient struct {
format byte `unionTag:"6"`
ColorLine ColorLine `offsetSize:"Offset24"` // Offset to ColorLine table, from beginning of PaintRadialGradient table.
X0 int16 // Start circle center x coordinate.
Y0 int16 // Start circle center y coordinate.
Radius0 uint16 // Start circle radius.
X1 int16 // End circle center x coordinate.
Y1 int16 // End circle center y coordinate.
Radius1 uint16 // End circle radius.
}
// (format 7)
type PaintVarRadialGradient struct {
format byte `unionTag:"7"`
ColorLine VarColorLine `offsetSize:"Offset24"` // Offset to VarColorLine table, from beginning of PaintVarRadialGradient table.
X0 int16 // Start circle center x coordinate. For variation, use varIndexBase + 0.
Y0 int16 // Start circle center y coordinate. For variation, use varIndexBase + 1.
Radius0 uint16 // Start circle radius. For variation, use varIndexBase + 2.
X1 int16 // End circle center x coordinate. For variation, use varIndexBase + 3.
Y1 int16 // End circle center y coordinate. For variation, use varIndexBase + 4.
Radius1 uint16 // End circle radius. For variation, use varIndexBase + 5.
VarIndexBase uint32 // Base index into DeltaSetIndexMap.
}
// (format 8)
type PaintSweepGradient struct {
format byte `unionTag:"8"`
ColorLine ColorLine `offsetSize:"Offset24"` // Offset to ColorLine table, from beginning of PaintSweepGradient table.
CenterX int16 // Center x coordinate.
CenterY int16 // Center y coordinate.
StartAngle Fixed214 // Start of the angular range of the gradient: add 1.0 and multiply by 180° to retrieve counter-clockwise degrees.
EndAngle Fixed214 // End of the angular range of the gradient: add 1.0 and multiply by 180° to retrieve counter-clockwise degrees.
}
// (format 9)
type PaintVarSweepGradient struct {
format byte `unionTag:"9"`
ColorLine VarColorLine `offsetSize:"Offset24"` // Offset to VarColorLine table, from beginning of PaintVarSweepGradient table.
CenterX int16 // Center x coordinate. For variation, use varIndexBase + 0.
CenterY int16 // Center y coordinate. For variation, use varIndexBase + 1.
StartAngle Fixed214 // Start of the angular range of the gradient: add 1.0 and multiply by 180° to retrieve counter-clockwise degrees. For variation, use varIndexBase + 2.
EndAngle Fixed214 // End of the angular range of the gradient: add 1.0 and multiply by 180° to retrieve counter-clockwise degrees. For variation, use varIndexBase + 3.
VarIndexBase uint32 // Base index into DeltaSetIndexMap.
}
// (format 10)
type PaintGlyph struct {
format byte `unionTag:"10"`
Paint PaintTable `offsetSize:"Offset24"` // Offset to a Paint table, from beginning of PaintGlyph table.
GlyphID uint16 // Glyph ID for the source outline.
}
// (format 11)
type PaintColrGlyph struct {
format byte `unionTag:"11"`
GlyphID uint16 // Glyph ID for a BaseGlyphList base glyph.
}
// (format 12)
type PaintTransform struct {
format byte `unionTag:"12"`
Paint PaintTable `offsetSize:"Offset24"` // Offset to a Paint subtable, from beginning of PaintTransform table.
Transform Affine2x3 `offsetSize:"Offset24"` // Offset to an Affine2x3 table, from beginning of PaintTransform table.
}
// (format 13)
type PaintVarTransform struct {
format byte `unionTag:"13"`
Paint PaintTable `offsetSize:"Offset24"` // Offset to a Paint subtable, from beginning of PaintVarTransform table.
Transform VarAffine2x3 `offsetSize:"Offset24"` // Offset to a VarAffine2x3 table, from beginning of PaintVarTransform table.
}
// (format 14)
type PaintTranslate struct {
format byte `unionTag:"14"`
Paint PaintTable `offsetSize:"Offset24"` // Offset to a Paint subtable, from beginning of PaintTranslate table.
Dx int16 // Translation in x direction.
Dy int16 // Translation in y direction.
}
// (format 15)
type PaintVarTranslate struct {
format byte `unionTag:"15"`
Paint PaintTable `offsetSize:"Offset24"` // Offset to a Paint subtable, from beginning of PaintVarTranslate table.
Dx int16 // Translation in x direction. For variation, use varIndexBase + 0.
Dy int16 // Translation in y direction. For variation, use varIndexBase + 1.
VarIndexBase uint32 // Base index into DeltaSetIndexMap.
}
// (format 16)
type PaintScale struct {
format byte `unionTag:"16"`
Paint PaintTable `offsetSize:"Offset24"` // Offset to a Paint subtable, from beginning of PaintScale table.
ScaleX Fixed214 // Scale factor in x direction.
ScaleY Fixed214 // Scale factor in y direction.
}
// (format 17)
type PaintVarScale struct {
format byte `unionTag:"17"`
Paint PaintTable `offsetSize:"Offset24"` // Offset to a Paint subtable, from beginning of PaintVarScale table.
ScaleX Fixed214 // Scale factor in x direction. For variation, use varIndexBase + 0.
ScaleY Fixed214 // Scale factor in y direction. For variation, use varIndexBase + 1.
VarIndexBase uint32 // Base index into DeltaSetIndexMap.
}
// (format 18)
type PaintScaleAroundCenter struct {
format byte `unionTag:"18"`
Paint PaintTable `offsetSize:"Offset24"` // Offset to a Paint subtable, from beginning of PaintScaleAroundCenter table.
ScaleX Fixed214 // Scale factor in x direction.
ScaleY Fixed214 // Scale factor in y direction.
CenterX int16 // x coordinate for the center of scaling.
CenterY int16 // y coordinate for the center of scaling.
}
// (format 19)
type PaintVarScaleAroundCenter struct {
format byte `unionTag:"19"`
Paint PaintTable `offsetSize:"Offset24"` // Offset to a Paint subtable, from beginning of PaintVarScaleAroundCenter table.
ScaleX Fixed214 // Scale factor in x direction. For variation, use varIndexBase + 0.
ScaleY Fixed214 // Scale factor in y direction. For variation, use varIndexBase + 1.
CenterX int16 // x coordinate for the center of scaling. For variation, use varIndexBase + 2.
CenterY int16 // y coordinate for the center of scaling. For variation, use varIndexBase + 3.
VarIndexBase uint32 // Base index into DeltaSetIndexMap.
}
// (format 20)
type PaintScaleUniform struct {
format byte `unionTag:"20"`
Paint PaintTable `offsetSize:"Offset24"` // Offset to a Paint subtable, from beginning of PaintScaleUniform table.
Scale Fixed214 // Scale factor in x and y directions.
}
// (format 21)
type PaintVarScaleUniform struct {
format byte `unionTag:"21"`
Paint PaintTable `offsetSize:"Offset24"` // Offset to a Paint subtable, from beginning of PaintVarScaleUniform table.
Scale Fixed214 // Scale factor in x and y directions. For variation, use varIndexBase + 0.
VarIndexBase uint32 // Base index into DeltaSetIndexMap.
}
// (format 22)
type PaintScaleUniformAroundCenter struct {
format byte `unionTag:"22"`
Paint PaintTable `offsetSize:"Offset24"` // Offset to a Paint subtable, from beginning of PaintScaleUniformAroundCenter table.
Scale Fixed214 // Scale factor in x and y directions.
CenterX int16 // x coordinate for the center of scaling.
CenterY int16 // y coordinate for the center of scaling.
}
// (format 23)
type PaintVarScaleUniformAroundCenter struct {
format byte `unionTag:"23"`
Paint PaintTable `offsetSize:"Offset24"` // Offset to a Paint subtable, from beginning of PaintVarScaleUniformAroundCenter table.
Scale Fixed214 // Scale factor in x and y directions. For variation, use varIndexBase + 0.
CenterX int16 // x coordinate for the center of scaling. For variation, use varIndexBase + 1.
CenterY int16 // y coordinate for the center of scaling. For variation, use varIndexBase + 2.
VarIndexBase uint32 // Base index into DeltaSetIndexMap.
}
// (format 24)
type PaintRotate struct {
format byte `unionTag:"24"`
Paint PaintTable `offsetSize:"Offset24"` // Offset to a Paint subtable, from beginning of PaintRotate table.
Angle Fixed214 // Rotation angle, 180° in counter-clockwise degrees per 1.0 of value.
}
// (format 25)
type PaintVarRotate struct {
format byte `unionTag:"25"`
Paint PaintTable `offsetSize:"Offset24"` // Offset to a Paint subtable, from beginning of PaintVarRotate table.
Angle Fixed214 // Rotation angle, 180° in counter-clockwise degrees per 1.0 of value. For variation, use varIndexBase + 0.
VarIndexBase uint32 // Base index into DeltaSetIndexMap.
}
// (format 26)
type PaintRotateAroundCenter struct {
format byte `unionTag:"26"`
Paint PaintTable `offsetSize:"Offset24"` // Offset to a Paint subtable, from beginning of PaintRotateAroundCenter table.
Angle Fixed214 // Rotation angle, 180° in counter-clockwise degrees per 1.0 of value.
CenterX int16 // x coordinate for the center of rotation.
CenterY int16 // y coordinate for the center of rotation.
}
// (format 27)
type PaintVarRotateAroundCenter struct {
format byte `unionTag:"27"`
Paint PaintTable `offsetSize:"Offset24"` // Offset to a Paint subtable, from beginning of PaintVarRotateAroundCenter table.
Angle Fixed214 // Rotation angle, 180° in counter-clockwise degrees per 1.0 of value. For variation, use varIndexBase + 0.
CenterX int16 // x coordinate for the center of rotation. For variation, use varIndexBase + 1.
CenterY int16 // y coordinate for the center of rotation. For variation, use varIndexBase + 2.
VarIndexBase uint32 // Base index into DeltaSetIndexMap.
}
// (format 28)
type PaintSkew struct {
format byte `unionTag:"28"`
Paint PaintTable `offsetSize:"Offset24"` // Offset to a Paint subtable, from beginning of PaintSkew table.
XSkewAngle Fixed214 // Angle of skew in the direction of the x-axis, 180° in counter-clockwise degrees per 1.0 of value.
YSkewAngle Fixed214 // Angle of skew in the direction of the y-axis, 180° in counter-clockwise degrees per 1.0 of value.
}
// (format 29)
type PaintVarSkew struct {
format byte `unionTag:"29"`
Paint PaintTable `offsetSize:"Offset24"` // Offset to a Paint subtable, from beginning of PaintVarSkew table.
XSkewAngle Fixed214 // Angle of skew in the direction of the x-axis, 180° in counter-clockwise degrees per 1.0 of value. For variation, use varIndexBase + 0.
YSkewAngle Fixed214 // Angle of skew in the direction of the y-axis, 180° in counter-clockwise degrees per 1.0 of value. For variation, use varIndexBase + 1.
VarIndexBase uint32 // Base index into DeltaSetIndexMap.
}
// (format 30)
type PaintSkewAroundCenter struct {
format byte `unionTag:"30"`
Paint PaintTable `offsetSize:"Offset24"` // Offset to a Paint subtable, from beginning of PaintSkewAroundCenter table.
XSkewAngle Fixed214 // Angle of skew in the direction of the x-axis, 180° in counter-clockwise degrees per 1.0 of value.
YSkewAngle Fixed214 // Angle of skew in the direction of the y-axis, 180° in counter-clockwise degrees per 1.0 of value.
CenterX int16 // x coordinate for the center of rotation.
CenterY int16 // y coordinate for the center of rotation.
}
// (format 31)
type PaintVarSkewAroundCenter struct {
format byte `unionTag:"31"`
Paint PaintTable `offsetSize:"Offset24"` // Offset to a Paint subtable, from beginning of PaintVarSkewAroundCenter table.
XSkewAngle Fixed214 // Angle of skew in the direction of the x-axis, 180° in counter-clockwise degrees per 1.0 of value. For variation, use varIndexBase + 0.
YSkewAngle Fixed214 // Angle of skew in the direction of the y-axis, 180° in counter-clockwise degrees per 1.0 of value. For variation, use varIndexBase + 1.
CenterX int16 // x coordinate for the center of rotation. For variation, use varIndexBase + 2.
CenterY int16 // y coordinate for the center of rotation. For variation, use varIndexBase + 3.
VarIndexBase uint32 // Base index into DeltaSetIndexMap.
}
// (format 32)
type PaintComposite struct {
format byte `unionTag:"32"`
SourcePaint PaintTable `offsetSize:"Offset24"` // Offset to a source Paint table, from beginning of PaintComposite table.
CompositeMode CompositeMode // A CompositeMode enumeration value.
BackdropPaint PaintTable `offsetSize:"Offset24"` // Offset to a backdrop Paint table, from beginning of PaintComposite table.
}
type CompositeMode uint8
const (
// Porter-Duff modes
CompositeClear CompositeMode = iota // Clear
CompositeSrc // Source (“Copy” in Composition & Blending Level 1)
CompositeDest // Destination
CompositeSrcOver // Source Over
CompositeDestOver // Destination Over
CompositeSrcIn // Source In
CompositeDestIn // Destination In
CompositeSrcOut // Source Out
CompositeDestOut // Destination Out
CompositeSrcAtop // Source Atop
CompositeDestAtop // Destination Atop
CompositeXor // XOR
CompositePlus // Plus (“Lighter” in Composition & Blending Level 1)
// Separable color blend modes:
CompositeScreen // screen
CompositeOverlay // overlay
CompositeDarken // darken
CompositeLighten // lighten
CompositeColorDodge // color-dodge
CompositeColorBurn // color-burn
CompositeHardLight // hard-light
CompositeSoftLight // soft-light
CompositeDifference // difference
CompositeExclusion // exclusion
CompositeMultiply // multiply
// Non-separable color blend modes:
CompositeHslHue // hue
CompositeHslSaturation // saturation
CompositeHslColor // color
CompositeHslLuminosity // luminosity
)
@@ -0,0 +1,67 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import (
"encoding/binary"
"fmt"
)
// Code generated by binarygen from glyphs_cpal_src.go. DO NOT EDIT
func (item *ColorRecord) mustParse(src []byte) {
_ = src[3] // early bound checking
item.Blue = src[0]
item.Green = src[1]
item.Red = src[2]
item.Alpha = src[3]
}
func ParseCPAL(src []byte) (CPAL, int, error) {
var item CPAL
n := 0
if L := len(src); L < 12 {
return item, 0, fmt.Errorf("reading CPAL: "+"EOF: expected length: 12, got %d", L)
}
_ = src[11] // early bound checking
item.Version = binary.BigEndian.Uint16(src[0:])
item.NumPaletteEntries = binary.BigEndian.Uint16(src[2:])
item.numPalettes = binary.BigEndian.Uint16(src[4:])
item.numColorRecords = binary.BigEndian.Uint16(src[6:])
offsetColorRecordsArray := int(binary.BigEndian.Uint32(src[8:]))
n += 12
{
if offsetColorRecordsArray != 0 { // ignore null offset
if L := len(src); L < offsetColorRecordsArray {
return item, 0, fmt.Errorf("reading CPAL: "+"EOF: expected length: %d, got %d", offsetColorRecordsArray, L)
}
arrayLength := int(item.numColorRecords)
if L := len(src); L < offsetColorRecordsArray+arrayLength*4 {
return item, 0, fmt.Errorf("reading CPAL: "+"EOF: expected length: %d, got %d", offsetColorRecordsArray+arrayLength*4, L)
}
item.ColorRecordsArray = make([]ColorRecord, arrayLength) // allocation guarded by the previous check
for i := range item.ColorRecordsArray {
item.ColorRecordsArray[i].mustParse(src[offsetColorRecordsArray+i*4:])
}
offsetColorRecordsArray += arrayLength * 4
}
}
{
arrayLength := int(item.numPalettes)
if L := len(src); L < 12+arrayLength*2 {
return item, 0, fmt.Errorf("reading CPAL: "+"EOF: expected length: %d, got %d", 12+arrayLength*2, L)
}
item.ColorRecordIndices = make([]uint16, arrayLength) // allocation guarded by the previous check
for i := range item.ColorRecordIndices {
item.ColorRecordIndices[i] = binary.BigEndian.Uint16(src[12+i*2:])
}
n += arrayLength * 2
}
return item, n, nil
}
@@ -0,0 +1,20 @@
package tables
// https://learn.microsoft.com/en-us/typography/opentype/spec/cpal
//
// For now, only the CPAL version 0 is supported.
type CPAL struct {
Version uint16 // Table version number
NumPaletteEntries uint16 // Number of palette entries in each palette.
numPalettes uint16 // Number of palettes in the table.
numColorRecords uint16 // Total number of color records, combined for all palettes.
ColorRecordsArray []ColorRecord `arrayCount:"ComputedField-numColorRecords" offsetSize:"Offset32"` // Offset from the beginning of CPAL table to the first ColorRecord.
ColorRecordIndices []uint16 `arrayCount:"ComputedField-numPalettes"` // [numPalettes] Index of each palette’s first color record in the combined color record array.
}
type ColorRecord struct {
Blue uint8 // Blue value (B0).
Green uint8 // Green value (B1).
Red uint8 // Red value (B2).
Alpha uint8 // Alpha value (B3).
}
@@ -0,0 +1,135 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import (
"encoding/binary"
"fmt"
)
// Code generated by binarygen from glyphs_glyf_src.go. DO NOT EDIT
func (item *CompositeGlyphPart) mustParse(src []byte) {
_ = src[23] // early bound checking
item.Flags = binary.BigEndian.Uint16(src[0:])
item.GlyphIndex = binary.BigEndian.Uint16(src[2:])
item.arg1 = binary.BigEndian.Uint16(src[4:])
item.arg2 = binary.BigEndian.Uint16(src[6:])
item.Scale[0] = float32(binary.BigEndian.Uint32(src[8:]))
item.Scale[1] = float32(binary.BigEndian.Uint32(src[12:]))
item.Scale[2] = float32(binary.BigEndian.Uint32(src[16:]))
item.Scale[3] = float32(binary.BigEndian.Uint32(src[20:]))
}
func (item *GlyphContourPoint) mustParse(src []byte) {
_ = src[4] // early bound checking
item.Flag = src[0]
item.X = int16(binary.BigEndian.Uint16(src[1:]))
item.Y = int16(binary.BigEndian.Uint16(src[3:]))
}
func ParseCompositeGlyph(src []byte) (CompositeGlyph, int, error) {
var item CompositeGlyph
n := 0
{
err := item.parseGlyphs(src[:])
if err != nil {
return item, 0, fmt.Errorf("reading CompositeGlyph: %s", err)
}
}
{
err := item.parseInstructions(src[:])
if err != nil {
return item, 0, fmt.Errorf("reading CompositeGlyph: %s", err)
}
}
return item, n, nil
}
func ParseCompositeGlyphPart(src []byte) (CompositeGlyphPart, int, error) {
var item CompositeGlyphPart
n := 0
if L := len(src); L < 24 {
return item, 0, fmt.Errorf("reading CompositeGlyphPart: "+"EOF: expected length: 24, got %d", L)
}
item.mustParse(src)
n += 24
return item, n, nil
}
func ParseGlyph(src []byte) (Glyph, int, error) {
var item Glyph
n := 0
if L := len(src); L < 10 {
return item, 0, fmt.Errorf("reading Glyph: "+"EOF: expected length: 10, got %d", L)
}
_ = src[9] // early bound checking
item.numberOfContours = int16(binary.BigEndian.Uint16(src[0:]))
item.XMin = int16(binary.BigEndian.Uint16(src[2:]))
item.YMin = int16(binary.BigEndian.Uint16(src[4:]))
item.XMax = int16(binary.BigEndian.Uint16(src[6:]))
item.YMax = int16(binary.BigEndian.Uint16(src[8:]))
n += 10
{
err := item.parseData(src[10:])
if err != nil {
return item, 0, fmt.Errorf("reading Glyph: %s", err)
}
}
return item, n, nil
}
func ParseGlyphContourPoint(src []byte) (GlyphContourPoint, int, error) {
var item GlyphContourPoint
n := 0
if L := len(src); L < 5 {
return item, 0, fmt.Errorf("reading GlyphContourPoint: "+"EOF: expected length: 5, got %d", L)
}
item.mustParse(src)
n += 5
return item, n, nil
}
func ParseSimpleGlyph(src []byte, endPtsOfContoursCount int) (SimpleGlyph, int, error) {
var item SimpleGlyph
n := 0
{
if L := len(src); L < endPtsOfContoursCount*2 {
return item, 0, fmt.Errorf("reading SimpleGlyph: "+"EOF: expected length: %d, got %d", endPtsOfContoursCount*2, L)
}
item.EndPtsOfContours = make([]uint16, endPtsOfContoursCount) // allocation guarded by the previous check
for i := range item.EndPtsOfContours {
item.EndPtsOfContours[i] = binary.BigEndian.Uint16(src[i*2:])
}
n += endPtsOfContoursCount * 2
}
if L := len(src); L < n+2 {
return item, 0, fmt.Errorf("reading SimpleGlyph: "+"EOF: expected length: n + 2, got %d", L)
}
arrayLengthInstructions := int(binary.BigEndian.Uint16(src[n:]))
n += 2
{
L := int(n + arrayLengthInstructions)
if len(src) < L {
return item, 0, fmt.Errorf("reading SimpleGlyph: "+"EOF: expected length: %d, got %d", L, len(src))
}
item.Instructions = src[n:L]
n = L
}
{
err := item.parsePoints(src[n:], endPtsOfContoursCount)
if err != nil {
return item, 0, fmt.Errorf("reading SimpleGlyph: %s", err)
}
}
return item, n, nil
}
@@ -0,0 +1,350 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import (
"encoding/binary"
"errors"
"fmt"
)
// shared with gvar, sbix, eblc
// return an error only if data is not long enough
func ParseLoca(src []byte, numGlyphs int, isLong bool) (out []uint32, err error) {
var size int
if isLong {
size = (numGlyphs + 1) * 4
} else {
size = (numGlyphs + 1) * 2
}
if L := len(src); L < size {
return nil, fmt.Errorf("reading Loca: EOF: expected length: %d, got %d", size, L)
}
out = make([]uint32, numGlyphs+1)
if isLong {
for i := range out {
out[i] = binary.BigEndian.Uint32(src[4*i:])
}
} else {
for i := range out {
out[i] = 2 * uint32(binary.BigEndian.Uint16(src[2*i:])) // The actual local offset divided by 2 is stored.
}
}
return out, nil
}
// Glyph Data
type Glyf []Glyph
// ParseGlyf parses the 'glyf' table.
// locaOffsets has length numGlyphs + 1, and is returned by ParseLoca
func ParseGlyf(src []byte, locaOffsets []uint32) (Glyf, error) {
out := make(Glyf, len(locaOffsets)-1)
var err error
for i := range out {
start, end := locaOffsets[i], locaOffsets[i+1]
// If a glyph has no outline, then loca[n] = loca [n+1].
if start == end {
continue
}
out[i], _, err = ParseGlyph(src[start:end])
if err != nil {
return nil, err
}
}
return out, nil
}
type Glyph struct {
numberOfContours int16 // If the number of contours is greater than or equal to zero, this is a simple glyph. If negative, this is a composite glyph — the value -1 should be used for composite glyphs.
XMin int16 // Minimum x for coordinate data.
YMin int16 // Minimum y for coordinate data.
XMax int16 // Maximum x for coordinate data.
YMax int16 // Maximum y for coordinate data.
Data GlyphData `isOpaque:"" subsliceStart:"AtCurrent"`
}
func (gl *Glyph) parseData(src []byte) (err error) {
if gl.numberOfContours >= 0 { // simple glyph
gl.Data, _, err = ParseSimpleGlyph(src, int(gl.numberOfContours))
} else { // composite glyph
gl.Data, _, err = ParseCompositeGlyph(src)
}
return err
}
type GlyphData interface {
isGlyphData()
}
func (SimpleGlyph) isGlyphData() {}
func (CompositeGlyph) isGlyphData() {}
type SimpleGlyph struct {
EndPtsOfContours []uint16 // [numberOfContours] Array of point indices for the last point of each contour, in increasing numeric order.
Instructions []byte `arrayCount:"FirstUint16"` // [instructionLength] Array of instruction byte code for the glyph.
Points []GlyphContourPoint `isOpaque:"" subsliceStart:"AtCurrent"`
}
type GlyphContourPoint struct {
Flag uint8
X, Y int16
}
const (
xShortVector = 0x02
xIsSameOrPositiveXShortVector = 0x10
yShortVector = 0x04
yIsSameOrPositiveYShortVector = 0x20
)
func (sg *SimpleGlyph) parsePoints(src []byte, _ int) error {
if len(sg.EndPtsOfContours) == 0 {
return nil
}
numPoints := int(sg.EndPtsOfContours[len(sg.EndPtsOfContours)-1]) + 1
const repeatFlag = 0x08
sg.Points = make([]GlyphContourPoint, numPoints)
// read flags
// to avoid costly length check, we also precompute the expected data size for coordinates
var (
coordinatesLengthX, coordinatesLengthY int
cursor int
L = len(src)
)
for i := 0; i < numPoints; i++ {
if L <= cursor {
return errors.New("invalid simple glyph data flags (EOF)")
}
flag := src[cursor]
sg.Points[i].Flag = flag
cursor++
localLengthX, localLengthY := 0, 0
if flag&xShortVector != 0 {
localLengthX = 1
} else if flag&xIsSameOrPositiveXShortVector == 0 {
localLengthX = 2
}
if flag&yShortVector != 0 {
localLengthY = 1
} else if flag&yIsSameOrPositiveYShortVector == 0 {
localLengthY = 2
}
if flag&repeatFlag != 0 {
if L <= cursor {
return errors.New("invalid simple glyph data flags (EOF)")
}
repeatCount := int(src[cursor])
cursor++
if i+repeatCount+1 > numPoints { // gracefully handle out of bounds
repeatCount = numPoints - i - 1
}
subSlice := sg.Points[i+1 : i+repeatCount+1]
for j := range subSlice {
subSlice[j].Flag = flag
}
i += repeatCount
localLengthX += repeatCount * localLengthX
localLengthY += repeatCount * localLengthY
}
coordinatesLengthX += localLengthX
coordinatesLengthY += localLengthY
}
src = src[cursor:]
if L, E := len(src), coordinatesLengthX+coordinatesLengthY; L < E {
return fmt.Errorf("EOF: expected length: %d, got %d", E, L)
}
dataX, dataY := src[:coordinatesLengthX], src[coordinatesLengthX:coordinatesLengthX+coordinatesLengthY]
// read x and y coordinates
parseGlyphContourPoints(dataX, dataY, sg.Points)
return nil
}
// returns the position after the read and the relative coordinate
// the input slice has already been checked for length
func readContourPoint(flag byte, data []byte, pos int, shortFlag, sameFlag uint8) (int, int16) {
var v int16
if flag&shortFlag != 0 {
val := data[pos]
pos++
if flag&sameFlag != 0 {
v += int16(val)
} else {
v -= int16(val)
}
} else if flag&sameFlag == 0 {
val := binary.BigEndian.Uint16(data[pos:])
pos += 2
v += int16(val)
}
return pos, v
}
// update the points in place
func parseGlyphContourPoints(dataX, dataY []byte, points []GlyphContourPoint) {
var (
posX, posY int // position into data
vX, offsetX, vY, offsetY int16 // coordinates are relative to the previous
)
for i, p := range points {
posX, offsetX = readContourPoint(p.Flag, dataX, posX, xShortVector, xIsSameOrPositiveXShortVector)
vX += offsetX
points[i].X = vX
posY, offsetY = readContourPoint(p.Flag, dataY, posY, yShortVector, yIsSameOrPositiveYShortVector)
vY += offsetY
points[i].Y = vY
}
}
type CompositeGlyph struct {
Glyphs []CompositeGlyphPart `isOpaque:""`
Instructions []byte `isOpaque:""`
}
const arg1And2AreWords = 1
func (cg *CompositeGlyph) parseGlyphs(src []byte) error {
const (
_ = 1 << iota
_
_
weHaveAScale
_
moreComponents
weHaveAnXAndYScale
weHaveATwoByTwo
weHaveInstructions
)
var flags uint16
for do := true; do; do = flags&moreComponents != 0 {
var part CompositeGlyphPart
if L := len(src); L < 4 {
return fmt.Errorf("EOF: expected length: %d, got %d", 4, L)
}
flags = binary.BigEndian.Uint16(src)
part.Flags = flags
part.GlyphIndex = GlyphID(binary.BigEndian.Uint16(src[2:]))
if flags&arg1And2AreWords != 0 { // 16 bits
if L, E := len(src), 4+4; L < E {
return fmt.Errorf("EOF: expected length: %d, got %d", E, L)
}
part.arg1 = binary.BigEndian.Uint16(src[4:])
part.arg2 = binary.BigEndian.Uint16(src[6:])
src = src[8:]
} else {
if L, E := len(src), 4+2; L < E {
return fmt.Errorf("EOF: expected length: %d, got %d", E, L)
}
part.arg1 = uint16(src[4])
part.arg2 = uint16(src[5])
src = src[6:]
}
part.Scale[0], part.Scale[3] = 1, 1
if flags&weHaveAScale != 0 {
if L := len(src); L < 2 {
return fmt.Errorf("EOF: expected length: %d, got %d", 2, L)
}
part.Scale[0] = Float214FromUint(binary.BigEndian.Uint16(src))
part.Scale[3] = part.Scale[0]
src = src[2:]
} else if flags&weHaveAnXAndYScale != 0 {
if L := len(src); L < 4 {
return fmt.Errorf("EOF: expected length: %d, got %d", 4, L)
}
part.Scale[0] = Float214FromUint(binary.BigEndian.Uint16(src))
part.Scale[3] = Float214FromUint(binary.BigEndian.Uint16(src[2:]))
src = src[4:]
} else if flags&weHaveATwoByTwo != 0 {
if L := len(src); L < 8 {
return fmt.Errorf("EOF: expected length: %d, got %d", 8, L)
}
part.Scale[0] = Float214FromUint(binary.BigEndian.Uint16(src))
part.Scale[1] = Float214FromUint(binary.BigEndian.Uint16(src[2:]))
part.Scale[2] = Float214FromUint(binary.BigEndian.Uint16(src[4:]))
part.Scale[3] = Float214FromUint(binary.BigEndian.Uint16(src[6:]))
src = src[8:]
}
cg.Glyphs = append(cg.Glyphs, part)
}
if flags&weHaveInstructions != 0 {
if L := len(src); L < 2 {
return fmt.Errorf("EOF: expected length: 2, got %d", L)
}
E := int(binary.BigEndian.Uint16(src))
if L := len(src); L < E {
return fmt.Errorf("EOF: expected length: %d, got %d", E, len(src))
}
cg.Instructions = src[0:E]
}
return nil
}
// already handled in parseGlyphs
func (cg *CompositeGlyph) parseInstructions(_ []byte) error { return nil }
type CompositeGlyphPart struct {
Flags uint16
GlyphIndex GlyphID
// raw value before interpretation:
// arg1 and arg2 may be either :
// - unsigned, when used as indices into the contour point list
// (see ArgsAsIndices)
// - signed, when used as translation in the transformation matrix
// (see ArgsAsTranslation)
arg1, arg2 uint16
// Scale is a matrix x, 01, 10, y ; default to identity
Scale [4]float32
}
func (c *CompositeGlyphPart) HasUseMyMetrics() bool {
const useMyMetrics = 0x0200
return c.Flags&useMyMetrics != 0
}
// return true if arg1 and arg2 indicated an anchor point,
// not offsets
func (c *CompositeGlyphPart) IsAnchored() bool {
const argsAreXyValues = 0x0002
return c.Flags&argsAreXyValues == 0
}
func (c *CompositeGlyphPart) IsScaledOffsets() bool {
const (
scaledComponentOffset = 0x0800
unscaledComponentOffset = 0x1000
)
return c.Flags&(scaledComponentOffset|unscaledComponentOffset) == scaledComponentOffset
}
func (c *CompositeGlyphPart) ArgsAsTranslation() (int16, int16) {
// arg1 and arg2 are interpreted as signed integers here
// the conversion depends on the original size (8 or 16 bits)
if c.Flags&arg1And2AreWords != 0 {
return int16(c.arg1), int16(c.arg2)
}
return int16(int8(uint8(c.arg1))), int16(int8(uint8(c.arg2)))
}
func (c *CompositeGlyphPart) ArgsAsIndices() (int, int) {
// arg1 and arg2 are interpreted as unsigned integers here
return int(c.arg1), int(c.arg2)
}
@@ -0,0 +1,110 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import (
"encoding/binary"
"fmt"
)
// Code generated by binarygen from glyphs_misc_src.go. DO NOT EDIT
func ParseSVG(src []byte) (SVG, int, error) {
var item SVG
n := 0
if L := len(src); L < 10 {
return item, 0, fmt.Errorf("reading SVG: "+"EOF: expected length: 10, got %d", L)
}
_ = src[9] // early bound checking
item.version = binary.BigEndian.Uint16(src[0:])
offsetSVGDocumentList := int(binary.BigEndian.Uint32(src[2:]))
item.reserved = binary.BigEndian.Uint32(src[6:])
n += 10
{
if offsetSVGDocumentList != 0 { // ignore null offset
if L := len(src); L < offsetSVGDocumentList {
return item, 0, fmt.Errorf("reading SVG: "+"EOF: expected length: %d, got %d", offsetSVGDocumentList, L)
}
var err error
item.SVGDocumentList, _, err = ParseSVGDocumentList(src[offsetSVGDocumentList:])
if err != nil {
return item, 0, fmt.Errorf("reading SVG: %s", err)
}
}
}
return item, n, nil
}
func ParseSVGDocumentList(src []byte) (SVGDocumentList, int, error) {
var item SVGDocumentList
n := 0
if L := len(src); L < 2 {
return item, 0, fmt.Errorf("reading SVGDocumentList: "+"EOF: expected length: 2, got %d", L)
}
arrayLengthDocumentRecords := int(binary.BigEndian.Uint16(src[0:]))
n += 2
{
if L := len(src); L < 2+arrayLengthDocumentRecords*12 {
return item, 0, fmt.Errorf("reading SVGDocumentList: "+"EOF: expected length: %d, got %d", 2+arrayLengthDocumentRecords*12, L)
}
item.DocumentRecords = make([]SVGDocumentRecord, arrayLengthDocumentRecords) // allocation guarded by the previous check
for i := range item.DocumentRecords {
item.DocumentRecords[i].mustParse(src[2+i*12:])
}
n += arrayLengthDocumentRecords * 12
}
{
item.SVGRawData = src[0:]
n = len(src)
}
return item, n, nil
}
func ParseVORG(src []byte) (VORG, int, error) {
var item VORG
n := 0
if L := len(src); L < 8 {
return item, 0, fmt.Errorf("reading VORG: "+"EOF: expected length: 8, got %d", L)
}
_ = src[7] // early bound checking
item.majorVersion = binary.BigEndian.Uint16(src[0:])
item.minorVersion = binary.BigEndian.Uint16(src[2:])
item.DefaultVertOriginY = int16(binary.BigEndian.Uint16(src[4:]))
arrayLengthVertOriginYMetrics := int(binary.BigEndian.Uint16(src[6:]))
n += 8
{
if L := len(src); L < 8+arrayLengthVertOriginYMetrics*4 {
return item, 0, fmt.Errorf("reading VORG: "+"EOF: expected length: %d, got %d", 8+arrayLengthVertOriginYMetrics*4, L)
}
item.VertOriginYMetrics = make([]VertOriginYMetric, arrayLengthVertOriginYMetrics) // allocation guarded by the previous check
for i := range item.VertOriginYMetrics {
item.VertOriginYMetrics[i].mustParse(src[8+i*4:])
}
n += arrayLengthVertOriginYMetrics * 4
}
return item, n, nil
}
func (item *SVGDocumentRecord) mustParse(src []byte) {
_ = src[11] // early bound checking
item.StartGlyphID = binary.BigEndian.Uint16(src[0:])
item.EndGlyphID = binary.BigEndian.Uint16(src[2:])
item.SvgDocOffset = Offset32(binary.BigEndian.Uint32(src[4:]))
item.SvgDocLength = binary.BigEndian.Uint32(src[8:])
}
func (item *VertOriginYMetric) mustParse(src []byte) {
_ = src[3] // early bound checking
item.GlyphIndex = binary.BigEndian.Uint16(src[0:])
item.VertOriginY = int16(binary.BigEndian.Uint16(src[2:]))
}
@@ -0,0 +1,61 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
// SVG is the SVG (Scalable Vector Graphics) table.
// See - https://learn.microsoft.com/fr-fr/typography/opentype/spec/svg
type SVG struct {
version uint16 // Table version (starting at 0). Set to 0.
SVGDocumentList SVGDocumentList `offsetSize:"Offset32"` // Offset to the SVG Document List, from the start of the SVG table. Must be non-zero.
reserved uint32 // Set to 0.
}
type SVGDocumentList struct {
DocumentRecords []SVGDocumentRecord `arrayCount:"FirstUint16"` // [numEntries] Array of SVG document records.
SVGRawData []byte `subsliceStart:"AtStart" arrayCount:"ToEnd"`
}
// Each SVG document record specifies a range of glyph IDs (from startGlyphID to endGlyphID, inclusive), and the location of its associated SVG document in the SVG table.
type SVGDocumentRecord struct {
StartGlyphID GlyphID // The first glyph ID for the range covered by this record.
EndGlyphID GlyphID // The last glyph ID for the range covered by this record.
SvgDocOffset Offset32 // Offset from the beginning of the SVGDocumentList to an SVG document. Must be non-zero.
SvgDocLength uint32 // Length of the SVG document data. Must be non-zero.
}
// CFF is the Compact Font Format Table.
// Since it used its own format, quite different from the regular Opentype format,
// its interpretation is handled externally (see font/cff).
// See also https://learn.microsoft.com/fr-fr/typography/opentype/spec/cff
type CFF = []byte
// VORG is the Vertical Origin Table
// See - https://learn.microsoft.com/fr-fr/typography/opentype/spec/vorg
type VORG struct {
majorVersion uint16 // Major version (starting at 1). Set to 1.
minorVersion uint16 // Minor version (starting at 0). Set to 0.
DefaultVertOriginY int16 // The y coordinate of a glyph’s vertical origin, in the font’s design coordinate system, to be used if no entry is present for the glyph in the vertOriginYMetrics array.
VertOriginYMetrics []VertOriginYMetric `arrayCount:"FirstUint16"`
}
// YOrigin returns the vertical origin for [glyph].
func (t *VORG) YOrigin(glyph GlyphID) int16 {
// binary search
for i, j := 0, len(t.VertOriginYMetrics); i < j; {
h := i + (j-i)/2
entry := t.VertOriginYMetrics[h]
if glyph < entry.GlyphIndex {
j = h
} else if entry.GlyphIndex < glyph {
i = h + 1
} else {
return entry.VertOriginY
}
}
return t.DefaultVertOriginY
}
type VertOriginYMetric struct {
GlyphIndex GlyphID // Glyph index.
VertOriginY int16 // Y coordinate, in the font’s design coordinate system, of the vertical origin of glyph with index glyphIndex.
}
@@ -0,0 +1,92 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import (
"encoding/binary"
"fmt"
)
// Code generated by binarygen from glyphs_sbix_src.go. DO NOT EDIT
func ParseBitmapGlyphData(src []byte) (BitmapGlyphData, int, error) {
var item BitmapGlyphData
n := 0
if L := len(src); L < 8 {
return item, 0, fmt.Errorf("reading BitmapGlyphData: "+"EOF: expected length: 8, got %d", L)
}
_ = src[7] // early bound checking
item.OriginOffsetX = int16(binary.BigEndian.Uint16(src[0:]))
item.OriginOffsetY = int16(binary.BigEndian.Uint16(src[2:]))
item.GraphicType = Tag(binary.BigEndian.Uint32(src[4:]))
n += 8
{
item.Data = src[8:]
n = len(src)
}
return item, n, nil
}
func ParseSbix(src []byte, numGlyphs int) (Sbix, int, error) {
var item Sbix
n := 0
if L := len(src); L < 8 {
return item, 0, fmt.Errorf("reading Sbix: "+"EOF: expected length: 8, got %d", L)
}
_ = src[7] // early bound checking
item.version = binary.BigEndian.Uint16(src[0:])
item.Flags = binary.BigEndian.Uint16(src[2:])
arrayLengthStrikes := int(binary.BigEndian.Uint32(src[4:]))
n += 8
{
if L := len(src); L < 8+arrayLengthStrikes*4 {
return item, 0, fmt.Errorf("reading Sbix: "+"EOF: expected length: %d, got %d", 8+arrayLengthStrikes*4, L)
}
item.Strikes = make([]Strike, arrayLengthStrikes) // allocation guarded by the previous check
for i := range item.Strikes {
offset := int(binary.BigEndian.Uint32(src[8+i*4:]))
// ignore null offsets
if offset == 0 {
continue
}
if L := len(src); L < offset {
return item, 0, fmt.Errorf("reading Sbix: "+"EOF: expected length: %d, got %d", offset, L)
}
var err error
item.Strikes[i], _, err = ParseStrike(src[offset:], numGlyphs)
if err != nil {
return item, 0, fmt.Errorf("reading Sbix: %s", err)
}
}
n += arrayLengthStrikes * 4
}
return item, n, nil
}
func ParseStrike(src []byte, numGlyphs int) (Strike, int, error) {
var item Strike
n := 0
if L := len(src); L < 4 {
return item, 0, fmt.Errorf("reading Strike: "+"EOF: expected length: 4, got %d", L)
}
_ = src[3] // early bound checking
item.Ppem = binary.BigEndian.Uint16(src[0:])
item.Ppi = binary.BigEndian.Uint16(src[2:])
n += 4
{
err := item.parseGlyphDatas(src[:], numGlyphs)
if err != nil {
return item, 0, fmt.Errorf("reading Strike: %s", err)
}
}
return item, n, nil
}
@@ -0,0 +1,62 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import (
"fmt"
)
// Sbix is the Standard Bitmap Graphics Table
// See - https://learn.microsoft.com/fr-fr/typography/opentype/spec/sbix
type Sbix struct {
version uint16 // Table version number — set to 1
// Bit 0: Set to 1.
// Bit 1: Draw outlines.
// Bits 2 to 15: reserved (set to 0).
Flags uint16
Strikes []Strike `arrayCount:"FirstUint32" offsetsArray:"Offset32"` // [numStrikes] Offsets from the beginning of the 'sbix' table to data for each individual bitmap strike.
}
// Strike stores one size of bitmap glyphs in the 'sbix' table.
// binarygen: argument=numGlyphs int
type Strike struct {
Ppem uint16 // The PPEM size for which this strike was designed.
Ppi uint16 // The device pixel density (in PPI) for which this strike was designed. (E.g., 96 PPI, 192 PPI.)
GlyphDatas []BitmapGlyphData `isOpaque:""` //[numGlyphs+1] Offset from the beginning of the strike data header to bitmap data for an individual glyph ID.
}
func (st *Strike) parseGlyphDatas(src []byte, numGlyphs int) error {
const headerSize = 4
offsets, err := ParseLoca(src[headerSize:], numGlyphs, true)
if err != nil {
return err
}
st.GlyphDatas = make([]BitmapGlyphData, numGlyphs)
for i := range st.GlyphDatas {
start, end := offsets[i], offsets[i+1]
if start == end { // no data
continue
}
if start > end {
return fmt.Errorf("invalid strike offsets %d > %d", start, end)
}
if L := len(src); L < int(end) {
return fmt.Errorf("EOF: expected length: %d, got %d", end, L)
}
st.GlyphDatas[i], _, err = ParseBitmapGlyphData(src[start:end])
if err != nil {
return err
}
}
return nil
}
type BitmapGlyphData struct {
OriginOffsetX int16 // The horizontal (x-axis) position of the left edge of the bitmap graphic in relation to the glyph design space origin.
OriginOffsetY int16 // The vertical (y-axis) position of the bottom edge of the bitmap graphic in relation to the glyph design space origin.
GraphicType Tag // Indicates the format of the embedded graphic data: one of 'jpg ', 'png ' or 'tiff', or the special format 'dupe'.
Data []byte `arrayCount:"ToEnd"` // The actual embedded graphic data. The total length is inferred from sequential entries in the glyphDataOffsets array and the fixed size (8 bytes) of the preceding fields.
}
@@ -0,0 +1,43 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import (
"encoding/binary"
"fmt"
)
// Code generated by binarygen from head_src.go. DO NOT EDIT
func (item *Head) mustParse(src []byte) {
_ = src[53] // early bound checking
item.majorVersion = binary.BigEndian.Uint16(src[0:])
item.minorVersion = binary.BigEndian.Uint16(src[2:])
item.fontRevision = binary.BigEndian.Uint32(src[4:])
item.checksumAdjustment = binary.BigEndian.Uint32(src[8:])
item.magicNumber = binary.BigEndian.Uint32(src[12:])
item.flags = binary.BigEndian.Uint16(src[16:])
item.UnitsPerEm = binary.BigEndian.Uint16(src[18:])
item.created = binary.BigEndian.Uint64(src[20:])
item.modified = binary.BigEndian.Uint64(src[28:])
item.XMin = int16(binary.BigEndian.Uint16(src[36:]))
item.YMin = int16(binary.BigEndian.Uint16(src[38:]))
item.XMax = int16(binary.BigEndian.Uint16(src[40:]))
item.YMax = int16(binary.BigEndian.Uint16(src[42:]))
item.MacStyle = binary.BigEndian.Uint16(src[44:])
item.lowestRecPPEM = binary.BigEndian.Uint16(src[46:])
item.fontDirectionHint = int16(binary.BigEndian.Uint16(src[48:]))
item.IndexToLocFormat = int16(binary.BigEndian.Uint16(src[50:]))
item.glyphDataFormat = int16(binary.BigEndian.Uint16(src[52:]))
}
func ParseHead(src []byte) (Head, int, error) {
var item Head
n := 0
if L := len(src); L < 54 {
return item, 0, fmt.Errorf("reading Head: "+"EOF: expected length: 54, got %d", L)
}
item.mustParse(src)
n += 54
return item, n, nil
}
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
// TableHead contains critical information about the rest of the font.
// https://learn.microsoft.com/en-us/typography/opentype/spec/head
// https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6head.html
// https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6bhed.html
type Head struct {
majorVersion uint16
minorVersion uint16
fontRevision uint32
checksumAdjustment uint32
magicNumber uint32
flags uint16
UnitsPerEm uint16
created longdatetime
modified longdatetime
XMin int16
YMin int16
XMax int16
YMax int16
MacStyle uint16
lowestRecPPEM uint16
fontDirectionHint int16
IndexToLocFormat int16
glyphDataFormat int16
}
// Upem returns a sanitize version of the 'UnitsPerEm' field.
func (head *Head) Upem() uint16 {
if head.UnitsPerEm < 16 || head.UnitsPerEm > 16384 {
return 1000
}
return head.UnitsPerEm
}
@@ -0,0 +1,43 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import (
"encoding/binary"
"fmt"
)
// Code generated by binarygen from hhea_vhea_src.go. DO NOT EDIT
func (item *Hhea) mustParse(src []byte) {
_ = src[35] // early bound checking
item.majorVersion = binary.BigEndian.Uint16(src[0:])
item.minorVersion = binary.BigEndian.Uint16(src[2:])
item.Ascender = int16(binary.BigEndian.Uint16(src[4:]))
item.Descender = int16(binary.BigEndian.Uint16(src[6:]))
item.LineGap = int16(binary.BigEndian.Uint16(src[8:]))
item.AdvanceMax = binary.BigEndian.Uint16(src[10:])
item.MinFirstSideBearing = int16(binary.BigEndian.Uint16(src[12:]))
item.MinSecondSideBearing = int16(binary.BigEndian.Uint16(src[14:]))
item.MaxExtent = int16(binary.BigEndian.Uint16(src[16:]))
item.CaretSlopeRise = int16(binary.BigEndian.Uint16(src[18:]))
item.CaretSlopeRun = int16(binary.BigEndian.Uint16(src[20:]))
item.CaretOffset = int16(binary.BigEndian.Uint16(src[22:]))
item.reserved[0] = binary.BigEndian.Uint16(src[24:])
item.reserved[1] = binary.BigEndian.Uint16(src[26:])
item.reserved[2] = binary.BigEndian.Uint16(src[28:])
item.reserved[3] = binary.BigEndian.Uint16(src[30:])
item.metricDataformat = int16(binary.BigEndian.Uint16(src[32:]))
item.NumOfLongMetrics = binary.BigEndian.Uint16(src[34:])
}
func ParseHhea(src []byte) (Hhea, int, error) {
var item Hhea
n := 0
if L := len(src); L < 36 {
return item, 0, fmt.Errorf("reading Hhea: "+"EOF: expected length: 36, got %d", L)
}
item.mustParse(src)
n += 36
return item, n, nil
}
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
// https://learn.microsoft.com/en-us/typography/opentype/spec/hhea
type Hhea struct {
majorVersion uint16
minorVersion uint16
Ascender int16
Descender int16
LineGap int16
AdvanceMax uint16
MinFirstSideBearing int16
MinSecondSideBearing int16
MaxExtent int16
CaretSlopeRise int16
CaretSlopeRun int16
CaretOffset int16
reserved [4]uint16
metricDataformat int16
NumOfLongMetrics uint16
}
// https://learn.microsoft.com/en-us/typography/opentype/spec/vhea
type Vhea = Hhea
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import (
"encoding/binary"
"fmt"
)
// Code generated by binarygen from hmtx_vmtx_src.go. DO NOT EDIT
func (item *LongHorMetric) mustParse(src []byte) {
_ = src[3] // early bound checking
item.AdvanceWidth = int16(binary.BigEndian.Uint16(src[0:]))
item.LeftSideBearing = int16(binary.BigEndian.Uint16(src[2:]))
}
func ParseHmtx(src []byte, metricsCount int, leftSideBearingsCount int) (Hmtx, int, error) {
var item Hmtx
n := 0
{
if L := len(src); L < metricsCount*4 {
return item, 0, fmt.Errorf("reading Hmtx: "+"EOF: expected length: %d, got %d", metricsCount*4, L)
}
item.Metrics = make([]LongHorMetric, metricsCount) // allocation guarded by the previous check
for i := range item.Metrics {
item.Metrics[i].mustParse(src[i*4:])
}
n += metricsCount * 4
}
{
if L := len(src); L < n+leftSideBearingsCount*2 {
return item, 0, fmt.Errorf("reading Hmtx: "+"EOF: expected length: %d, got %d", n+leftSideBearingsCount*2, L)
}
item.LeftSideBearings = make([]int16, leftSideBearingsCount) // allocation guarded by the previous check
for i := range item.LeftSideBearings {
item.LeftSideBearings[i] = int16(binary.BigEndian.Uint16(src[n+i*2:]))
}
n += leftSideBearingsCount * 2
}
return item, n, nil
}
@@ -0,0 +1,47 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
// https://learn.microsoft.com/en-us/typography/opentype/spec/hmtx
type Hmtx struct {
Metrics []LongHorMetric `arrayCount:""`
// avances are padded with the last value
// and side bearings are given
LeftSideBearings []int16 `arrayCount:""`
}
func (table Hmtx) IsEmpty() bool {
return len(table.Metrics)+len(table.LeftSideBearings) == 0
}
// Advance returns the base side bearing, defaulting to 0 for invalid glyph index
func (table Hmtx) Advance(gid GlyphID) int16 {
LM, LS := len(table.Metrics), len(table.LeftSideBearings)
index := int(gid)
if index < LM {
return table.Metrics[index].AdvanceWidth
} else if index < LS+LM { // return the last value
return table.Metrics[len(table.Metrics)-1].AdvanceWidth
}
return 0
}
// SideBearing returns the base side bearing, defaulting to 0 for invalid glyph index
func (table Hmtx) SideBearing(gid GlyphID) int16 {
LM, LS := len(table.Metrics), len(table.LeftSideBearings)
index := int(gid)
if index < LM {
return table.Metrics[index].LeftSideBearing
} else if index < LS+LM {
return table.LeftSideBearings[index-LM]
} else {
return 0
}
}
type LongHorMetric struct {
AdvanceWidth, LeftSideBearing int16
}
// https://learn.microsoft.com/en-us/typography/opentype/spec/vmtx
type Vmtx = Hmtx
+101
View File
@@ -0,0 +1,101 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import (
"encoding/binary"
"errors"
"fmt"
)
// Kern is the kern table. It has multiple header format, defined in Apple AAT and Microsoft OT
// specs, but the subtable data actually are the same.
//
// Microsoft (OT) format
//
// version uint16 : Table version number (0)
// nTables uint16 : Number of subtables in the kerning table.
//
// Apple (AAT) old format
//
// version uint16 : The version number of the kerning table (0x0001 for the current version).
// nTables uint16 : The number of subtables included in the kerning table.
//
// Apple (AAT) new format
//
// version uint32 : The version number of the kerning table (0x00010000 for the current version).
// nTables uint32 : The number of subtables included in the kerning table.
//
// See - https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6kern.html
// and - https://learn.microsoft.com/fr-fr/typography/opentype/spec/kern
type Kern struct {
version uint16
Tables []KernSubtable
}
// We apply the following logic:
// - read the first uint16 -> it's always the major version
// - if it's 0, we have a Miscrosoft table
// - if it's 1, we have an Apple table. We read the next uint16,
// to differentiate between the old and the new Apple format.
func ParseKern(src []byte) (Kern, int, error) {
if L := len(src); L < 4 {
return Kern{}, 0, fmt.Errorf("reading Kern: "+"EOF: expected length: 4, got %d", L)
}
var numTables uint32
major := binary.BigEndian.Uint16(src)
switch major {
case 0:
numTables = uint32(binary.BigEndian.Uint16(src[2:]))
src = src[4:]
case 1:
nextUint16 := binary.BigEndian.Uint16(src[2:])
if nextUint16 == 0 {
// either new format or old format with 0 subtables, the later being invalid (or at least useless)
if len(src) < 8 {
return Kern{}, 0, errors.New("invalid kern table version 1 (EOF)")
}
numTables = binary.BigEndian.Uint32(src[4:])
src = src[8:]
} else {
// old format
numTables = uint32(nextUint16)
src = src[4:]
}
default:
return Kern{}, 0, fmt.Errorf("unsupported kern table version: %d", major)
}
out := make([]KernSubtable, numTables)
var (
err error
nbRead int
isOT = major == 0
)
for i := range out {
if L := len(src); L < nbRead {
return Kern{}, 0, fmt.Errorf("reading Kern: "+"EOF: expected length: %d, got %d", nbRead, L)
}
src = src[nbRead:]
if isOT {
out[i], nbRead, err = ParseOTKernSubtableHeader(src)
} else {
out[i], nbRead, err = ParseAATKernSubtableHeader(src)
}
if err != nil {
return Kern{}, 0, err
}
}
return Kern{
version: major,
Tables: out,
}, 0, nil
}
func (k AATKernSubtableHeader) Data() KernData { return k.data }
func (k OTKernSubtableHeader) Data() KernData { return k.data }
@@ -0,0 +1,342 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import (
"encoding/binary"
"fmt"
)
// Code generated by binarygen from kern_src.go. DO NOT EDIT
func ParseAATKernSubtableHeader(src []byte) (AATKernSubtableHeader, int, error) {
var item AATKernSubtableHeader
n := 0
if L := len(src); L < 8 {
return item, 0, fmt.Errorf("reading AATKernSubtableHeader: "+"EOF: expected length: 8, got %d", L)
}
_ = src[7] // early bound checking
item.length = binary.BigEndian.Uint32(src[0:])
item.Coverage = src[4]
item.version = kernSTVersion(src[5])
item.TupleCount = binary.BigEndian.Uint16(src[6:])
n += 8
{
var (
read int
err error
)
switch item.version {
case kernSTVersion0:
item.data, read, err = ParseKernData0(src[8:])
case kernSTVersion1:
item.data, read, err = ParseKernData1(src[8:])
case kernSTVersion2:
item.data, read, err = ParseKernData2(src[8:], src)
case kernSTVersion3:
item.data, read, err = ParseKernData3(src[8:])
default:
err = fmt.Errorf("unsupported KernDataVersion %d", item.version)
}
if err != nil {
return item, 0, fmt.Errorf("reading AATKernSubtableHeader: %s", err)
}
n += read
}
var err error
n, err = item.parseEnd(src)
if err != nil {
return item, 0, fmt.Errorf("reading AATKernSubtableHeader: %s", err)
}
return item, n, nil
}
func ParseAATStateTable(src []byte) (AATStateTable, int, error) {
var item AATStateTable
n := 0
if L := len(src); L < 8 {
return item, 0, fmt.Errorf("reading AATStateTable: "+"EOF: expected length: 8, got %d", L)
}
_ = src[7] // early bound checking
item.StateSize = binary.BigEndian.Uint16(src[0:])
offsetClassTable := int(binary.BigEndian.Uint16(src[2:]))
item.stateArray = Offset16(binary.BigEndian.Uint16(src[4:]))
item.entryTable = Offset16(binary.BigEndian.Uint16(src[6:]))
n += 8
{
if offsetClassTable != 0 { // ignore null offset
if L := len(src); L < offsetClassTable {
return item, 0, fmt.Errorf("reading AATStateTable: "+"EOF: expected length: %d, got %d", offsetClassTable, L)
}
var err error
item.ClassTable, _, err = ParseClassTable(src[offsetClassTable:])
if err != nil {
return item, 0, fmt.Errorf("reading AATStateTable: %s", err)
}
}
}
{
err := item.parseStates(src[:])
if err != nil {
return item, 0, fmt.Errorf("reading AATStateTable: %s", err)
}
}
{
read, err := item.parseEntries(src[:])
if err != nil {
return item, 0, fmt.Errorf("reading AATStateTable: %s", err)
}
n = read
}
return item, n, nil
}
func ParseClassTable(src []byte) (ClassTable, int, error) {
var item ClassTable
n := 0
if L := len(src); L < 4 {
return item, 0, fmt.Errorf("reading ClassTable: "+"EOF: expected length: 4, got %d", L)
}
_ = src[3] // early bound checking
item.StartGlyph = binary.BigEndian.Uint16(src[0:])
arrayLengthValues := int(binary.BigEndian.Uint16(src[2:]))
n += 4
{
L := int(4 + arrayLengthValues)
if len(src) < L {
return item, 0, fmt.Errorf("reading ClassTable: "+"EOF: expected length: %d, got %d", L, len(src))
}
item.Values = src[4:L]
n = L
}
return item, n, nil
}
func ParseKernData0(src []byte) (KernData0, int, error) {
var item KernData0
n := 0
if L := len(src); L < 8 {
return item, 0, fmt.Errorf("reading KernData0: "+"EOF: expected length: 8, got %d", L)
}
_ = src[7] // early bound checking
item.nPairs = binary.BigEndian.Uint16(src[0:])
item.searchRange = binary.BigEndian.Uint16(src[2:])
item.entrySelector = binary.BigEndian.Uint16(src[4:])
item.rangeShift = binary.BigEndian.Uint16(src[6:])
n += 8
{
arrayLength := int(item.nPairs)
if L := len(src); L < 8+arrayLength*6 {
return item, 0, fmt.Errorf("reading KernData0: "+"EOF: expected length: %d, got %d", 8+arrayLength*6, L)
}
item.Pairs = make([]Kernx0Record, arrayLength) // allocation guarded by the previous check
for i := range item.Pairs {
item.Pairs[i].mustParse(src[8+i*6:])
}
n += arrayLength * 6
}
return item, n, nil
}
func ParseKernData1(src []byte) (KernData1, int, error) {
var item KernData1
n := 0
{
var (
err error
read int
)
item.AATStateTable, read, err = ParseAATStateTable(src[0:])
if err != nil {
return item, 0, fmt.Errorf("reading KernData1: %s", err)
}
n += read
}
if L := len(src); L < n+2 {
return item, 0, fmt.Errorf("reading KernData1: "+"EOF: expected length: n + 2, got %d", L)
}
item.valueTable = binary.BigEndian.Uint16(src[n:])
n += 2
{
err := item.parseValues(src[:])
if err != nil {
return item, 0, fmt.Errorf("reading KernData1: %s", err)
}
}
return item, n, nil
}
func ParseKernData2(src []byte, parentSrc []byte) (KernData2, int, error) {
var item KernData2
n := 0
if L := len(src); L < 8 {
return item, 0, fmt.Errorf("reading KernData2: "+"EOF: expected length: 8, got %d", L)
}
_ = src[7] // early bound checking
item.rowWidth = binary.BigEndian.Uint16(src[0:])
offsetLeft := int(binary.BigEndian.Uint16(src[2:]))
offsetRight := int(binary.BigEndian.Uint16(src[4:]))
item.KerningStart = Offset16(binary.BigEndian.Uint16(src[6:]))
n += 8
{
if offsetLeft != 0 { // ignore null offset
if L := len(parentSrc); L < offsetLeft {
return item, 0, fmt.Errorf("reading KernData2: "+"EOF: expected length: %d, got %d", offsetLeft, L)
}
var err error
item.Left, _, err = ParseAATLoopkup8Data(parentSrc[offsetLeft:])
if err != nil {
return item, 0, fmt.Errorf("reading KernData2: %s", err)
}
}
}
{
if offsetRight != 0 { // ignore null offset
if L := len(parentSrc); L < offsetRight {
return item, 0, fmt.Errorf("reading KernData2: "+"EOF: expected length: %d, got %d", offsetRight, L)
}
var err error
item.Right, _, err = ParseAATLoopkup8Data(parentSrc[offsetRight:])
if err != nil {
return item, 0, fmt.Errorf("reading KernData2: %s", err)
}
}
}
{
err := item.parseKerningData(src[:], parentSrc)
if err != nil {
return item, 0, fmt.Errorf("reading KernData2: %s", err)
}
}
return item, n, nil
}
func ParseKernData3(src []byte) (KernData3, int, error) {
var item KernData3
n := 0
if L := len(src); L < 6 {
return item, 0, fmt.Errorf("reading KernData3: "+"EOF: expected length: 6, got %d", L)
}
_ = src[5] // early bound checking
item.glyphCount = binary.BigEndian.Uint16(src[0:])
item.kernValueCount = src[2]
item.leftClassCount = src[3]
item.RightClassCount = src[4]
item.flags = src[5]
n += 6
{
arrayLength := int(item.kernValueCount)
if L := len(src); L < 6+arrayLength*2 {
return item, 0, fmt.Errorf("reading KernData3: "+"EOF: expected length: %d, got %d", 6+arrayLength*2, L)
}
item.Kernings = make([]int16, arrayLength) // allocation guarded by the previous check
for i := range item.Kernings {
item.Kernings[i] = int16(binary.BigEndian.Uint16(src[6+i*2:]))
}
n += arrayLength * 2
}
{
arrayLength := int(item.glyphCount)
L := int(n + arrayLength)
if len(src) < L {
return item, 0, fmt.Errorf("reading KernData3: "+"EOF: expected length: %d, got %d", L, len(src))
}
item.LeftClass = src[n:L]
n = L
}
{
arrayLength := int(item.glyphCount)
L := int(n + arrayLength)
if len(src) < L {
return item, 0, fmt.Errorf("reading KernData3: "+"EOF: expected length: %d, got %d", L, len(src))
}
item.RightClass = src[n:L]
n = L
}
{
arrayLength := int(item.nKernIndex())
L := int(n + arrayLength)
if len(src) < L {
return item, 0, fmt.Errorf("reading KernData3: "+"EOF: expected length: %d, got %d", L, len(src))
}
item.KernIndex = src[n:L]
n = L
}
var err error
n, err = item.parseEnd(src)
if err != nil {
return item, 0, fmt.Errorf("reading KernData3: %s", err)
}
return item, n, nil
}
func ParseOTKernSubtableHeader(src []byte) (OTKernSubtableHeader, int, error) {
var item OTKernSubtableHeader
n := 0
if L := len(src); L < 6 {
return item, 0, fmt.Errorf("reading OTKernSubtableHeader: "+"EOF: expected length: 6, got %d", L)
}
_ = src[5] // early bound checking
item.version = binary.BigEndian.Uint16(src[0:])
item.length = binary.BigEndian.Uint16(src[2:])
item.format = kernSTVersion(src[4])
item.Coverage = src[5]
n += 6
{
var (
read int
err error
)
switch item.format {
case kernSTVersion0:
item.data, read, err = ParseKernData0(src[6:])
case kernSTVersion1:
item.data, read, err = ParseKernData1(src[6:])
case kernSTVersion2:
item.data, read, err = ParseKernData2(src[6:], src)
case kernSTVersion3:
item.data, read, err = ParseKernData3(src[6:])
default:
err = fmt.Errorf("unsupported KernDataVersion %d", item.format)
}
if err != nil {
return item, 0, fmt.Errorf("reading OTKernSubtableHeader: %s", err)
}
n += read
}
var err error
n, err = item.parseEnd(src)
if err != nil {
return item, 0, fmt.Errorf("reading OTKernSubtableHeader: %s", err)
}
return item, n, nil
}
@@ -0,0 +1,143 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import (
"encoding/binary"
"errors"
"fmt"
)
type KernSubtable interface {
// Data returns the actual kerning data
Data() KernData
}
type OTKernSubtableHeader struct {
version uint16 // Kern subtable version number
length uint16 // Length of the subtable, in bytes (including this header).
format kernSTVersion // What type of information is contained in this table.
Coverage byte // What type of information is contained in this table.
data KernData `unionField:"format"`
}
// check and return the length
func (st *OTKernSubtableHeader) parseEnd(src []byte) (int, error) {
if L, E := len(src), int(st.length); L < E {
return 0, fmt.Errorf("EOF: expected length: %d, got %d", E, L)
}
return int(st.length), nil
}
type AATKernSubtableHeader struct {
length uint32 // The length of this subtable in bytes, including this header.
Coverage byte // Circumstances under which this table is used.
version kernSTVersion
TupleCount uint16 // The tuple count. This value is only used with variation fonts and should be 0 for all other fonts. The subtable's tupleCount will be ignored if the 'kerx' table version is less than 4.
data KernData `unionField:"version"`
}
// check and return the length
func (st *AATKernSubtableHeader) parseEnd(src []byte) (int, error) {
if L, E := len(src), int(st.length); L < E {
return 0, fmt.Errorf("EOF: expected length: %d, got %d", E, L)
}
return int(st.length), nil
}
type KernData interface {
isKernData()
}
func (KernData0) isKernData() {}
func (KernData1) isKernData() {}
func (KernData2) isKernData() {}
func (KernData3) isKernData() {}
type kernSTVersion byte
const (
kernSTVersion0 kernSTVersion = iota
kernSTVersion1
kernSTVersion2
kernSTVersion3
)
type KernData0 struct {
nPairs uint16 // The number of kerning pairs in this subtable.
searchRange uint16 // The largest power of two less than or equal to the value of nPairs, multiplied by the size in bytes of an entry in the subtable.
entrySelector uint16 // This is calculated as log2 of the largest power of two less than or equal to the value of nPairs. This value indicates how many iterations of the search loop have to be made. For example, in a list of eight items, there would be three iterations of the loop.
rangeShift uint16 // The value of nPairs minus the largest power of two less than or equal to nPairs. This is multiplied b
Pairs []Kernx0Record `arrayCount:"ComputedField-nPairs"`
}
type KernData1 struct {
AATStateTable
valueTable uint16 // Offset in bytes from the beginning of the subtable to the beginning of the kerning table.
Values []int16 `isOpaque:""`
}
func (kd *KernData1) parseValues(src []byte) error {
valuesOffset := int(kd.valueTable)
// start by resolving offset -> index
for i := range kd.Entries {
entry := &kd.Entries[i]
offset := int(entry.Flags & Kern1Offset)
if offset == 0 || offset < valuesOffset {
binary.BigEndian.PutUint16(entry.data[:], 0xFFFF)
} else {
index := uint16((offset - valuesOffset) / 2)
binary.BigEndian.PutUint16(entry.data[:], index)
}
}
var err error
kd.Values, err = parseKernx1Values(src, kd.Entries, valuesOffset, 0)
return err
}
type KernData2 struct {
rowWidth uint16 // The width, in bytes, of a row in the subtable.
Left AATLoopkup8Data `offsetSize:"Offset16" offsetRelativeTo:"Parent"`
Right AATLoopkup8Data `offsetSize:"Offset16" offsetRelativeTo:"Parent"`
KerningStart Offset16 // Offset from beginning of this subtable to the start of the kerning array.
KerningData []byte `isOpaque:"" offsetRelativeTo:"Parent"` // indexed by Left + Right
}
func (kd *KernData2) parseKerningData(_ []byte, parentSrc []byte) error {
kd.KerningData = parentSrc
return nil
}
type KernData3 struct {
glyphCount uint16 // The number of glyphs in this font.
kernValueCount uint8 // The number of kerning values.
leftClassCount uint8 // The number of left-hand classes.
RightClassCount uint8 // The number of right-hand classes.
flags uint8 // Set to zero (reserved for future use).
Kernings []int16 `arrayCount:"ComputedField-kernValueCount"`
LeftClass []uint8 `arrayCount:"ComputedField-glyphCount"`
RightClass []uint8 `arrayCount:"ComputedField-glyphCount"`
KernIndex []uint8 `arrayCount:"ComputedField-nKernIndex()"`
}
func (kd *KernData3) nKernIndex() int { return int(kd.leftClassCount) * int(kd.RightClassCount) }
// sanitize index and class values
func (kd *KernData3) parseEnd(_ []byte) (int, error) {
for _, index := range kd.KernIndex {
if index >= kd.kernValueCount {
return 0, errors.New("invalid kern subtable format 3 index value")
}
}
for i := range kd.LeftClass {
if kd.LeftClass[i] >= kd.leftClassCount {
return 0, errors.New("invalid kern subtable format 3 left class value")
}
if kd.RightClass[i] >= kd.RightClassCount {
return 0, errors.New("invalid kern subtable format 3 right class value")
}
}
return 0, nil
}
@@ -0,0 +1,75 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import (
"encoding/binary"
"fmt"
)
// Code generated by binarygen from maxp_src.go. DO NOT EDIT
func ParseMaxp(src []byte) (Maxp, int, error) {
var item Maxp
n := 0
if L := len(src); L < 6 {
return item, 0, fmt.Errorf("reading Maxp: "+"EOF: expected length: 6, got %d", L)
}
_ = src[5] // early bound checking
item.version = maxpVersion(binary.BigEndian.Uint32(src[0:]))
item.NumGlyphs = binary.BigEndian.Uint16(src[4:])
n += 6
{
var (
read int
err error
)
switch item.version {
case maxpVersion05:
item.data, read, err = parseMaxpData05(src[6:])
case maxpVersion1:
item.data, read, err = parseMaxpData1(src[6:])
default:
err = fmt.Errorf("unsupported maxpDataVersion %d", item.version)
}
if err != nil {
return item, 0, fmt.Errorf("reading Maxp: %s", err)
}
n += read
}
return item, n, nil
}
func (item *maxpData1) mustParse(src []byte) {
item.rawData[0] = binary.BigEndian.Uint16(src[0:])
item.rawData[1] = binary.BigEndian.Uint16(src[2:])
item.rawData[2] = binary.BigEndian.Uint16(src[4:])
item.rawData[3] = binary.BigEndian.Uint16(src[6:])
item.rawData[4] = binary.BigEndian.Uint16(src[8:])
item.rawData[5] = binary.BigEndian.Uint16(src[10:])
item.rawData[6] = binary.BigEndian.Uint16(src[12:])
item.rawData[7] = binary.BigEndian.Uint16(src[14:])
item.rawData[8] = binary.BigEndian.Uint16(src[16:])
item.rawData[9] = binary.BigEndian.Uint16(src[18:])
item.rawData[10] = binary.BigEndian.Uint16(src[20:])
item.rawData[11] = binary.BigEndian.Uint16(src[22:])
item.rawData[12] = binary.BigEndian.Uint16(src[24:])
}
func parseMaxpData05([]byte) (maxpData05, int, error) {
var item maxpData05
n := 0
return item, n, nil
}
func parseMaxpData1(src []byte) (maxpData1, int, error) {
var item maxpData1
n := 0
if L := len(src); L < 26 {
return item, 0, fmt.Errorf("reading maxpData1: "+"EOF: expected length: 26, got %d", L)
}
item.mustParse(src)
n += 26
return item, n, nil
}
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
// https://learn.microsoft.com/en-us/typography/opentype/spec/Maxp
type Maxp struct {
version maxpVersion
NumGlyphs uint16
data maxpData `unionField:"version"`
}
type maxpVersion uint32
const (
maxpVersion05 maxpVersion = 0x00005000
maxpVersion1 maxpVersion = 0x00010000
)
type maxpData interface {
isMaxpVersion()
}
func (maxpData05) isMaxpVersion() {}
func (maxpData1) isMaxpVersion() {}
type maxpData05 struct{}
type maxpData1 struct {
rawData [13]uint16
}
@@ -0,0 +1,57 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import (
"encoding/binary"
"fmt"
)
// Code generated by binarygen from name_src.go. DO NOT EDIT
func ParseName(src []byte) (Name, int, error) {
var item Name
n := 0
if L := len(src); L < 6 {
return item, 0, fmt.Errorf("reading Name: "+"EOF: expected length: 6, got %d", L)
}
_ = src[5] // early bound checking
item.version = binary.BigEndian.Uint16(src[0:])
item.count = binary.BigEndian.Uint16(src[2:])
offsetStringData := int(binary.BigEndian.Uint16(src[4:]))
n += 6
{
if offsetStringData != 0 { // ignore null offset
if L := len(src); L < offsetStringData {
return item, 0, fmt.Errorf("reading Name: "+"EOF: expected length: %d, got %d", offsetStringData, L)
}
item.stringData = src[offsetStringData:]
}
}
{
arrayLength := int(item.count)
if L := len(src); L < 6+arrayLength*12 {
return item, 0, fmt.Errorf("reading Name: "+"EOF: expected length: %d, got %d", 6+arrayLength*12, L)
}
item.nameRecords = make([]nameRecord, arrayLength) // allocation guarded by the previous check
for i := range item.nameRecords {
item.nameRecords[i].mustParse(src[6+i*12:])
}
n += arrayLength * 12
}
return item, n, nil
}
func (item *nameRecord) mustParse(src []byte) {
_ = src[11] // early bound checking
item.platformID = PlatformID(binary.BigEndian.Uint16(src[0:]))
item.encodingID = EncodingID(binary.BigEndian.Uint16(src[2:]))
item.languageID = LanguageID(binary.BigEndian.Uint16(src[4:]))
item.nameID = NameID(binary.BigEndian.Uint16(src[6:]))
item.length = binary.BigEndian.Uint16(src[8:])
item.stringOffset = binary.BigEndian.Uint16(src[10:])
}
@@ -0,0 +1,183 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import (
"encoding/binary"
"unicode/utf16"
)
const (
PlatformUnicode PlatformID = iota
PlatformMac
PlatformIso // deprecated
PlatformMicrosoft
PlatformCustom
_
_
PlatformAdobe // artificial
)
const (
PEUnicodeDefault = EncodingID(0)
PEUnicodeBMP = EncodingID(3)
PEUnicodeFull = EncodingID(4)
PEUnicodeFull13 = EncodingID(6)
PEMacRoman = PEUnicodeDefault
PEMicrosoftSymbolCs = EncodingID(0)
PEMicrosoftUnicodeCs = EncodingID(1)
PEMicrosoftUcs4 = EncodingID(10)
)
const (
plMacEnglish = LanguageID(0)
plUnicodeDefault = LanguageID(0)
plMicrosoftEnglish = LanguageID(0x0409)
)
// Naming table
// See https://learn.microsoft.com/en-us/typography/opentype/spec/name
type Name struct {
version uint16
count uint16
stringData []byte `offsetSize:"Offset16" arrayCount:"ToEnd"`
nameRecords []nameRecord `arrayCount:"ComputedField-count"`
}
type nameRecord struct {
platformID PlatformID
encodingID EncodingID
languageID LanguageID
nameID NameID
length uint16
stringOffset uint16
}
// selectRecord return the entry for `name` or nil if not found.
func (names Name) selectRecord(name NameID) *nameRecord {
var (
foundAppleRoman = -1
foundAppleEnglish = -1
foundWin = -1
foundUnicode = -1
isEnglish = false
)
for n, rec := range names.nameRecords {
// According to the OpenType 1.3 specification, only Microsoft or
// Apple platform IDs might be used in the `name' table. The
// `Unicode' platform is reserved for the `cmap' table, and the
// `ISO' one is deprecated.
//
// However, the Apple TrueType specification doesn't say the same
// thing and goes to suggest that all Unicode `name' table entries
// should be coded in UTF-16.
if rec.nameID == name && rec.length > 0 {
switch rec.platformID {
case PlatformUnicode, PlatformIso:
// there is `languageID' to check there. We should use this
// field only as a last solution when nothing else is
// available.
foundUnicode = n
case PlatformMac:
// This is a bit special because some fonts will use either
// an English language id, or a Roman encoding id, to indicate
// the English version of its font name.
if rec.languageID == plMacEnglish {
foundAppleEnglish = n
} else if rec.encodingID == PEMacRoman {
foundAppleRoman = n
}
case PlatformMicrosoft:
// we only take a non-English name when there is nothing
// else available in the font
if foundWin == -1 || (rec.languageID&0x3FF) == 0x009 {
switch rec.encodingID {
case PEMicrosoftSymbolCs, PEMicrosoftUnicodeCs, PEMicrosoftUcs4:
isEnglish = (rec.languageID & 0x3FF) == 0x009
foundWin = n
}
}
}
}
}
foundApple := foundAppleRoman
if foundAppleEnglish >= 0 {
foundApple = foundAppleEnglish
}
// some fonts contain invalid Unicode or Macintosh formatted entries;
// we will thus favor names encoded in Windows formats if available
// (provided it is an English name)
if foundWin >= 0 && !(foundApple >= 0 && !isEnglish) {
return &names.nameRecords[foundWin]
} else if foundApple >= 0 {
return &names.nameRecords[foundApple]
} else if foundUnicode >= 0 {
return &names.nameRecords[foundUnicode]
}
return nil
}
// Name returns the entry at [name], encoded in UTF-8 when possible,
// or an empty string if not found
func (names Name) Name(name NameID) string {
if record := names.selectRecord(name); record != nil {
return names.decodeRecord(*record)
}
return ""
}
// decode is a best-effort attempt to get an UTF-8 encoded version of
// Value. Only MicrosoftUnicode (3,1 ,X), MacRomain (1,0,X) and Unicode platform
// strings are supported.
func (names Name) decodeRecord(n nameRecord) string {
end := int(n.stringOffset) + int(n.length)
if end > len(names.stringData) {
// invalid record
return ""
}
value := names.stringData[n.stringOffset:end]
if n.platformID == PlatformUnicode ||
(n.platformID == PlatformMicrosoft &&
(n.encodingID == PEMicrosoftUnicodeCs || n.encodingID == PEMicrosoftUcs4 || n.encodingID == PEMicrosoftSymbolCs)) {
return decodeUtf16(value)
}
if n.platformID == PlatformMac && n.encodingID == PEMacRoman {
return DecodeMacintosh(value)
}
// no encoding detected, hope for utf8
return string(value)
}
// decode a big ending, no BOM utf16 string
func decodeUtf16(b []byte) string {
ints := make([]uint16, len(b)/2)
for i := range ints {
ints[i] = binary.BigEndian.Uint16(b[2*i:])
}
return string(utf16.Decode(ints))
}
// Support for the old macintosh encoding
var macintoshEncoding = [256]rune{
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 196, 197, 199, 201, 209, 214, 220, 225, 224, 226, 228, 227, 229, 231, 233, 232, 234, 235, 237, 236, 238, 239, 241, 243, 242, 244, 246, 245, 250, 249, 251, 252, 8224, 176, 162, 163, 167, 8226, 182, 223, 174, 169, 8482, 180, 168, 8800, 198, 216, 8734, 177, 8804, 8805, 165, 181, 8706, 8721, 8719, 960, 8747, 170, 186, 937, 230, 248, 191, 161, 172, 8730, 402, 8776, 8710, 171, 187, 8230, 160, 192, 195, 213, 338, 339, 8211, 8212, 8220, 8221, 8216, 8217, 247, 9674, 255, 376, 8260, 8364,
8249, 8250, 64257, 64258, 8225, 183, 8218, 8222, 8240, 194, 202, 193, 203, 200, 205, 206, 207, 204, 211, 212, 63743, 210, 218, 219, 217, 305, 710, 732, 175, 728, 729, 730, 184, 733, 731, 711,
}
// DecodeMacintoshByte returns the rune for the given byte
func DecodeMacintoshByte(b byte) rune { return macintoshEncoding[b] }
// DecodeMacintosh decode a Macintosh encoded string
func DecodeMacintosh(encoded []byte) string {
out := make([]rune, len(encoded))
for i, b := range encoded {
out[i] = macintoshEncoding[b]
}
return string(out)
}
@@ -0,0 +1,66 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import (
"encoding/binary"
"fmt"
)
// Code generated by binarygen from os2_src.go. DO NOT EDIT
func ParseOs2(src []byte) (Os2, int, error) {
var item Os2
n := 0
if L := len(src); L < 78 {
return item, 0, fmt.Errorf("reading Os2: "+"EOF: expected length: 78, got %d", L)
}
_ = src[77] // early bound checking
item.Version = binary.BigEndian.Uint16(src[0:])
item.XAvgCharWidth = binary.BigEndian.Uint16(src[2:])
item.USWeightClass = binary.BigEndian.Uint16(src[4:])
item.USWidthClass = binary.BigEndian.Uint16(src[6:])
item.fSType = binary.BigEndian.Uint16(src[8:])
item.YSubscriptXSize = int16(binary.BigEndian.Uint16(src[10:]))
item.YSubscriptYSize = int16(binary.BigEndian.Uint16(src[12:]))
item.YSubscriptXOffset = int16(binary.BigEndian.Uint16(src[14:]))
item.YSubscriptYOffset = int16(binary.BigEndian.Uint16(src[16:]))
item.YSuperscriptXSize = int16(binary.BigEndian.Uint16(src[18:]))
item.YSuperscriptYSize = int16(binary.BigEndian.Uint16(src[20:]))
item.YSuperscriptXOffset = int16(binary.BigEndian.Uint16(src[22:]))
item.ySuperscriptYOffset = int16(binary.BigEndian.Uint16(src[24:]))
item.YStrikeoutSize = int16(binary.BigEndian.Uint16(src[26:]))
item.YStrikeoutPosition = int16(binary.BigEndian.Uint16(src[28:]))
item.sFamilyClass = int16(binary.BigEndian.Uint16(src[30:]))
item.panose[0] = src[32]
item.panose[1] = src[33]
item.panose[2] = src[34]
item.panose[3] = src[35]
item.panose[4] = src[36]
item.panose[5] = src[37]
item.panose[6] = src[38]
item.panose[7] = src[39]
item.panose[8] = src[40]
item.panose[9] = src[41]
item.ulCharRange[0] = binary.BigEndian.Uint32(src[42:])
item.ulCharRange[1] = binary.BigEndian.Uint32(src[46:])
item.ulCharRange[2] = binary.BigEndian.Uint32(src[50:])
item.ulCharRange[3] = binary.BigEndian.Uint32(src[54:])
item.achVendID = Tag(binary.BigEndian.Uint32(src[58:]))
item.FsSelection = binary.BigEndian.Uint16(src[62:])
item.USFirstCharIndex = binary.BigEndian.Uint16(src[64:])
item.USLastCharIndex = binary.BigEndian.Uint16(src[66:])
item.STypoAscender = int16(binary.BigEndian.Uint16(src[68:]))
item.STypoDescender = int16(binary.BigEndian.Uint16(src[70:]))
item.STypoLineGap = int16(binary.BigEndian.Uint16(src[72:]))
item.usWinAscent = binary.BigEndian.Uint16(src[74:])
item.usWinDescent = binary.BigEndian.Uint16(src[76:])
n += 78
{
item.HigherVersionData = src[78:]
n = len(src)
}
return item, n, nil
}
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
// OS/2 and Windows Metrics Table
// See https://learn.microsoft.com/en-us/typography/opentype/spec/os2
type Os2 struct {
Version uint16
XAvgCharWidth uint16
USWeightClass uint16
USWidthClass uint16
fSType uint16
YSubscriptXSize int16
YSubscriptYSize int16
YSubscriptXOffset int16
YSubscriptYOffset int16
YSuperscriptXSize int16
YSuperscriptYSize int16
YSuperscriptXOffset int16
ySuperscriptYOffset int16
YStrikeoutSize int16
YStrikeoutPosition int16
sFamilyClass int16
panose [10]byte
ulCharRange [4]uint32
achVendID Tag
FsSelection uint16
USFirstCharIndex uint16
USLastCharIndex uint16
STypoAscender int16
STypoDescender int16
STypoLineGap int16
usWinAscent uint16
usWinDescent uint16
HigherVersionData []byte `arrayCount:"ToEnd"`
}
func (os *Os2) FontPage() FontPage {
if os.Version == 0 {
return FontPage(os.FsSelection & 0xFF00)
}
return FPNone
}
// See https://docs.microsoft.com/en-us/typography/legacy/legacy_arabic_fonts
// https://github.com/Microsoft/Font-Validator/blob/520aaae/OTFontFileVal/val_OS2.cs#L644-L681
type FontPage uint16
const (
FPNone FontPage = 0
FPHebrew FontPage = 0xB100 /* Hebrew Windows 3.1 font page */
FPSimpArabic FontPage = 0xB200 /* Simplified Arabic Windows 3.1 font page */
FPTradArabic FontPage = 0xB300 /* Traditional Arabic Windows 3.1 font page */
FPOemArabic FontPage = 0xB400 /* OEM Arabic Windows 3.1 font page */
FPSimpFarsi FontPage = 0xBA00 /* Simplified Farsi Windows 3.1 font page */
FPTradFarsi FontPage = 0xBB00 /* Traditional Farsi Windows 3.1 font page */
FPThai FontPage = 0xDE00 /* Thai Windows 3.1 font page */
)
@@ -0,0 +1,576 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import (
"encoding/binary"
"fmt"
)
// Code generated by binarygen from ot_gdef_src.go. DO NOT EDIT
func (item *CaretValue1) mustParse(src []byte) {
_ = src[3] // early bound checking
item.caretValueFormat = binary.BigEndian.Uint16(src[0:])
item.Coordinate = int16(binary.BigEndian.Uint16(src[2:]))
}
func (item *CaretValue2) mustParse(src []byte) {
_ = src[3] // early bound checking
item.caretValueFormat = binary.BigEndian.Uint16(src[0:])
item.CaretValuePointIndex = binary.BigEndian.Uint16(src[2:])
}
func (item *ClassRangeRecord) mustParse(src []byte) {
_ = src[5] // early bound checking
item.StartGlyphID = binary.BigEndian.Uint16(src[0:])
item.EndGlyphID = binary.BigEndian.Uint16(src[2:])
item.Class = binary.BigEndian.Uint16(src[4:])
}
func ParseAttachList(src []byte) (AttachList, int, error) {
var item AttachList
n := 0
if L := len(src); L < 4 {
return item, 0, fmt.Errorf("reading AttachList: "+"EOF: expected length: 4, got %d", L)
}
_ = src[3] // early bound checking
offsetCoverage := int(binary.BigEndian.Uint16(src[0:]))
arrayLengthAttachPoints := int(binary.BigEndian.Uint16(src[2:]))
n += 4
{
if offsetCoverage != 0 { // ignore null offset
if L := len(src); L < offsetCoverage {
return item, 0, fmt.Errorf("reading AttachList: "+"EOF: expected length: %d, got %d", offsetCoverage, L)
}
var (
err error
read int
)
item.Coverage, read, err = ParseCoverage(src[offsetCoverage:])
if err != nil {
return item, 0, fmt.Errorf("reading AttachList: %s", err)
}
offsetCoverage += read
}
}
{
if L := len(src); L < 4+arrayLengthAttachPoints*2 {
return item, 0, fmt.Errorf("reading AttachList: "+"EOF: expected length: %d, got %d", 4+arrayLengthAttachPoints*2, L)
}
item.AttachPoints = make([]AttachPoint, arrayLengthAttachPoints) // allocation guarded by the previous check
for i := range item.AttachPoints {
offset := int(binary.BigEndian.Uint16(src[4+i*2:]))
// ignore null offsets
if offset == 0 {
continue
}
if L := len(src); L < offset {
return item, 0, fmt.Errorf("reading AttachList: "+"EOF: expected length: %d, got %d", offset, L)
}
var err error
item.AttachPoints[i], _, err = ParseAttachPoint(src[offset:])
if err != nil {
return item, 0, fmt.Errorf("reading AttachList: %s", err)
}
}
n += arrayLengthAttachPoints * 2
}
return item, n, nil
}
func ParseAttachPoint(src []byte) (AttachPoint, int, error) {
var item AttachPoint
n := 0
if L := len(src); L < 2 {
return item, 0, fmt.Errorf("reading AttachPoint: "+"EOF: expected length: 2, got %d", L)
}
arrayLengthPointIndices := int(binary.BigEndian.Uint16(src[0:]))
n += 2
{
if L := len(src); L < 2+arrayLengthPointIndices*2 {
return item, 0, fmt.Errorf("reading AttachPoint: "+"EOF: expected length: %d, got %d", 2+arrayLengthPointIndices*2, L)
}
item.PointIndices = make([]uint16, arrayLengthPointIndices) // allocation guarded by the previous check
for i := range item.PointIndices {
item.PointIndices[i] = binary.BigEndian.Uint16(src[2+i*2:])
}
n += arrayLengthPointIndices * 2
}
return item, n, nil
}
func ParseCaretValue(src []byte) (CaretValue, int, error) {
var item CaretValue
if L := len(src); L < 2 {
return item, 0, fmt.Errorf("reading CaretValue: "+"EOF: expected length: 2, got %d", L)
}
format := uint16(binary.BigEndian.Uint16(src[0:]))
var (
read int
err error
)
switch format {
case 1:
item, read, err = ParseCaretValue1(src[0:])
case 2:
item, read, err = ParseCaretValue2(src[0:])
case 3:
item, read, err = ParseCaretValue3(src[0:])
default:
err = fmt.Errorf("unsupported CaretValue format %d", format)
}
if err != nil {
return item, 0, fmt.Errorf("reading CaretValue: %s", err)
}
return item, read, nil
}
func ParseCaretValue1(src []byte) (CaretValue1, int, error) {
var item CaretValue1
n := 0
if L := len(src); L < 4 {
return item, 0, fmt.Errorf("reading CaretValue1: "+"EOF: expected length: 4, got %d", L)
}
item.mustParse(src)
n += 4
return item, n, nil
}
func ParseCaretValue2(src []byte) (CaretValue2, int, error) {
var item CaretValue2
n := 0
if L := len(src); L < 4 {
return item, 0, fmt.Errorf("reading CaretValue2: "+"EOF: expected length: 4, got %d", L)
}
item.mustParse(src)
n += 4
return item, n, nil
}
func ParseCaretValue3(src []byte) (CaretValue3, int, error) {
var item CaretValue3
n := 0
if L := len(src); L < 6 {
return item, 0, fmt.Errorf("reading CaretValue3: "+"EOF: expected length: 6, got %d", L)
}
_ = src[5] // early bound checking
item.caretValueFormat = binary.BigEndian.Uint16(src[0:])
item.Coordinate = int16(binary.BigEndian.Uint16(src[2:]))
item.deviceOffset = Offset16(binary.BigEndian.Uint16(src[4:]))
n += 6
{
err := item.parseDevice(src[:])
if err != nil {
return item, 0, fmt.Errorf("reading CaretValue3: %s", err)
}
}
return item, n, nil
}
func ParseClassDef(src []byte) (ClassDef, int, error) {
var item ClassDef
if L := len(src); L < 2 {
return item, 0, fmt.Errorf("reading ClassDef: "+"EOF: expected length: 2, got %d", L)
}
format := uint16(binary.BigEndian.Uint16(src[0:]))
var (
read int
err error
)
switch format {
case 1:
item, read, err = ParseClassDef1(src[0:])
case 2:
item, read, err = ParseClassDef2(src[0:])
default:
err = fmt.Errorf("unsupported ClassDef format %d", format)
}
if err != nil {
return item, 0, fmt.Errorf("reading ClassDef: %s", err)
}
return item, read, nil
}
func ParseClassDef1(src []byte) (ClassDef1, int, error) {
var item ClassDef1
n := 0
if L := len(src); L < 6 {
return item, 0, fmt.Errorf("reading ClassDef1: "+"EOF: expected length: 6, got %d", L)
}
_ = src[5] // early bound checking
item.format = binary.BigEndian.Uint16(src[0:])
item.StartGlyphID = binary.BigEndian.Uint16(src[2:])
arrayLengthClassValueArray := int(binary.BigEndian.Uint16(src[4:]))
n += 6
{
if L := len(src); L < 6+arrayLengthClassValueArray*2 {
return item, 0, fmt.Errorf("reading ClassDef1: "+"EOF: expected length: %d, got %d", 6+arrayLengthClassValueArray*2, L)
}
item.ClassValueArray = make([]uint16, arrayLengthClassValueArray) // allocation guarded by the previous check
for i := range item.ClassValueArray {
item.ClassValueArray[i] = binary.BigEndian.Uint16(src[6+i*2:])
}
n += arrayLengthClassValueArray * 2
}
return item, n, nil
}
func ParseClassDef2(src []byte) (ClassDef2, int, error) {
var item ClassDef2
n := 0
if L := len(src); L < 4 {
return item, 0, fmt.Errorf("reading ClassDef2: "+"EOF: expected length: 4, got %d", L)
}
_ = src[3] // early bound checking
item.format = binary.BigEndian.Uint16(src[0:])
arrayLengthClassRangeRecords := int(binary.BigEndian.Uint16(src[2:]))
n += 4
{
if L := len(src); L < 4+arrayLengthClassRangeRecords*6 {
return item, 0, fmt.Errorf("reading ClassDef2: "+"EOF: expected length: %d, got %d", 4+arrayLengthClassRangeRecords*6, L)
}
item.ClassRangeRecords = make([]ClassRangeRecord, arrayLengthClassRangeRecords) // allocation guarded by the previous check
for i := range item.ClassRangeRecords {
item.ClassRangeRecords[i].mustParse(src[4+i*6:])
}
n += arrayLengthClassRangeRecords * 6
}
return item, n, nil
}
func ParseCoverage(src []byte) (Coverage, int, error) {
var item Coverage
if L := len(src); L < 2 {
return item, 0, fmt.Errorf("reading Coverage: "+"EOF: expected length: 2, got %d", L)
}
format := uint16(binary.BigEndian.Uint16(src[0:]))
var (
read int
err error
)
switch format {
case 1:
item, read, err = ParseCoverage1(src[0:])
case 2:
item, read, err = ParseCoverage2(src[0:])
default:
err = fmt.Errorf("unsupported Coverage format %d", format)
}
if err != nil {
return item, 0, fmt.Errorf("reading Coverage: %s", err)
}
return item, read, nil
}
func ParseCoverage1(src []byte) (Coverage1, int, error) {
var item Coverage1
n := 0
if L := len(src); L < 4 {
return item, 0, fmt.Errorf("reading Coverage1: "+"EOF: expected length: 4, got %d", L)
}
_ = src[3] // early bound checking
item.format = binary.BigEndian.Uint16(src[0:])
arrayLengthGlyphs := int(binary.BigEndian.Uint16(src[2:]))
n += 4
{
if L := len(src); L < 4+arrayLengthGlyphs*2 {
return item, 0, fmt.Errorf("reading Coverage1: "+"EOF: expected length: %d, got %d", 4+arrayLengthGlyphs*2, L)
}
item.Glyphs = make([]uint16, arrayLengthGlyphs) // allocation guarded by the previous check
for i := range item.Glyphs {
item.Glyphs[i] = binary.BigEndian.Uint16(src[4+i*2:])
}
n += arrayLengthGlyphs * 2
}
return item, n, nil
}
func ParseCoverage2(src []byte) (Coverage2, int, error) {
var item Coverage2
n := 0
if L := len(src); L < 4 {
return item, 0, fmt.Errorf("reading Coverage2: "+"EOF: expected length: 4, got %d", L)
}
_ = src[3] // early bound checking
item.format = binary.BigEndian.Uint16(src[0:])
arrayLengthRanges := int(binary.BigEndian.Uint16(src[2:]))
n += 4
{
if L := len(src); L < 4+arrayLengthRanges*6 {
return item, 0, fmt.Errorf("reading Coverage2: "+"EOF: expected length: %d, got %d", 4+arrayLengthRanges*6, L)
}
item.Ranges = make([]RangeRecord, arrayLengthRanges) // allocation guarded by the previous check
for i := range item.Ranges {
item.Ranges[i].mustParse(src[4+i*6:])
}
n += arrayLengthRanges * 6
}
return item, n, nil
}
func ParseGDEF(src []byte) (GDEF, int, error) {
var item GDEF
n := 0
if L := len(src); L < 12 {
return item, 0, fmt.Errorf("reading GDEF: "+"EOF: expected length: 12, got %d", L)
}
_ = src[11] // early bound checking
item.majorVersion = binary.BigEndian.Uint16(src[0:])
item.minorVersion = binary.BigEndian.Uint16(src[2:])
offsetGlyphClassDef := int(binary.BigEndian.Uint16(src[4:]))
offsetAttachList := int(binary.BigEndian.Uint16(src[6:]))
offsetLigCaretList := int(binary.BigEndian.Uint16(src[8:]))
offsetMarkAttachClass := int(binary.BigEndian.Uint16(src[10:]))
n += 12
{
if offsetGlyphClassDef != 0 { // ignore null offset
if L := len(src); L < offsetGlyphClassDef {
return item, 0, fmt.Errorf("reading GDEF: "+"EOF: expected length: %d, got %d", offsetGlyphClassDef, L)
}
var (
err error
read int
)
item.GlyphClassDef, read, err = ParseClassDef(src[offsetGlyphClassDef:])
if err != nil {
return item, 0, fmt.Errorf("reading GDEF: %s", err)
}
offsetGlyphClassDef += read
}
}
{
if offsetAttachList != 0 { // ignore null offset
if L := len(src); L < offsetAttachList {
return item, 0, fmt.Errorf("reading GDEF: "+"EOF: expected length: %d, got %d", offsetAttachList, L)
}
var err error
item.AttachList, _, err = ParseAttachList(src[offsetAttachList:])
if err != nil {
return item, 0, fmt.Errorf("reading GDEF: %s", err)
}
}
}
{
if offsetLigCaretList != 0 { // ignore null offset
if L := len(src); L < offsetLigCaretList {
return item, 0, fmt.Errorf("reading GDEF: "+"EOF: expected length: %d, got %d", offsetLigCaretList, L)
}
var err error
item.LigCaretList, _, err = ParseLigCaretList(src[offsetLigCaretList:])
if err != nil {
return item, 0, fmt.Errorf("reading GDEF: %s", err)
}
}
}
{
if offsetMarkAttachClass != 0 { // ignore null offset
if L := len(src); L < offsetMarkAttachClass {
return item, 0, fmt.Errorf("reading GDEF: "+"EOF: expected length: %d, got %d", offsetMarkAttachClass, L)
}
var (
err error
read int
)
item.MarkAttachClass, read, err = ParseClassDef(src[offsetMarkAttachClass:])
if err != nil {
return item, 0, fmt.Errorf("reading GDEF: %s", err)
}
offsetMarkAttachClass += read
}
}
{
err := item.parseMarkGlyphSetsDef(src[:])
if err != nil {
return item, 0, fmt.Errorf("reading GDEF: %s", err)
}
}
{
read, err := item.parseItemVarStore(src[:])
if err != nil {
return item, 0, fmt.Errorf("reading GDEF: %s", err)
}
n = read
}
return item, n, nil
}
func ParseLigCaretList(src []byte) (LigCaretList, int, error) {
var item LigCaretList
n := 0
if L := len(src); L < 4 {
return item, 0, fmt.Errorf("reading LigCaretList: "+"EOF: expected length: 4, got %d", L)
}
_ = src[3] // early bound checking
offsetCoverage := int(binary.BigEndian.Uint16(src[0:]))
arrayLengthLigGlyphs := int(binary.BigEndian.Uint16(src[2:]))
n += 4
{
if offsetCoverage != 0 { // ignore null offset
if L := len(src); L < offsetCoverage {
return item, 0, fmt.Errorf("reading LigCaretList: "+"EOF: expected length: %d, got %d", offsetCoverage, L)
}
var (
err error
read int
)
item.Coverage, read, err = ParseCoverage(src[offsetCoverage:])
if err != nil {
return item, 0, fmt.Errorf("reading LigCaretList: %s", err)
}
offsetCoverage += read
}
}
{
if L := len(src); L < 4+arrayLengthLigGlyphs*2 {
return item, 0, fmt.Errorf("reading LigCaretList: "+"EOF: expected length: %d, got %d", 4+arrayLengthLigGlyphs*2, L)
}
item.LigGlyphs = make([]LigGlyph, arrayLengthLigGlyphs) // allocation guarded by the previous check
for i := range item.LigGlyphs {
offset := int(binary.BigEndian.Uint16(src[4+i*2:]))
// ignore null offsets
if offset == 0 {
continue
}
if L := len(src); L < offset {
return item, 0, fmt.Errorf("reading LigCaretList: "+"EOF: expected length: %d, got %d", offset, L)
}
var err error
item.LigGlyphs[i], _, err = ParseLigGlyph(src[offset:])
if err != nil {
return item, 0, fmt.Errorf("reading LigCaretList: %s", err)
}
}
n += arrayLengthLigGlyphs * 2
}
return item, n, nil
}
func ParseLigGlyph(src []byte) (LigGlyph, int, error) {
var item LigGlyph
n := 0
if L := len(src); L < 2 {
return item, 0, fmt.Errorf("reading LigGlyph: "+"EOF: expected length: 2, got %d", L)
}
arrayLengthCaretValues := int(binary.BigEndian.Uint16(src[0:]))
n += 2
{
if L := len(src); L < 2+arrayLengthCaretValues*2 {
return item, 0, fmt.Errorf("reading LigGlyph: "+"EOF: expected length: %d, got %d", 2+arrayLengthCaretValues*2, L)
}
item.CaretValues = make([]CaretValue, arrayLengthCaretValues) // allocation guarded by the previous check
for i := range item.CaretValues {
offset := int(binary.BigEndian.Uint16(src[2+i*2:]))
// ignore null offsets
if offset == 0 {
continue
}
if L := len(src); L < offset {
return item, 0, fmt.Errorf("reading LigGlyph: "+"EOF: expected length: %d, got %d", offset, L)
}
var err error
item.CaretValues[i], _, err = ParseCaretValue(src[offset:])
if err != nil {
return item, 0, fmt.Errorf("reading LigGlyph: %s", err)
}
}
n += arrayLengthCaretValues * 2
}
return item, n, nil
}
func ParseMarkGlyphSets(src []byte) (MarkGlyphSets, int, error) {
var item MarkGlyphSets
n := 0
if L := len(src); L < 4 {
return item, 0, fmt.Errorf("reading MarkGlyphSets: "+"EOF: expected length: 4, got %d", L)
}
_ = src[3] // early bound checking
item.format = binary.BigEndian.Uint16(src[0:])
arrayLengthCoverages := int(binary.BigEndian.Uint16(src[2:]))
n += 4
{
if L := len(src); L < 4+arrayLengthCoverages*4 {
return item, 0, fmt.Errorf("reading MarkGlyphSets: "+"EOF: expected length: %d, got %d", 4+arrayLengthCoverages*4, L)
}
item.Coverages = make([]Coverage, arrayLengthCoverages) // allocation guarded by the previous check
for i := range item.Coverages {
offset := int(binary.BigEndian.Uint32(src[4+i*4:]))
// ignore null offsets
if offset == 0 {
continue
}
if L := len(src); L < offset {
return item, 0, fmt.Errorf("reading MarkGlyphSets: "+"EOF: expected length: %d, got %d", offset, L)
}
var err error
item.Coverages[i], _, err = ParseCoverage(src[offset:])
if err != nil {
return item, 0, fmt.Errorf("reading MarkGlyphSets: %s", err)
}
}
n += arrayLengthCoverages * 4
}
return item, n, nil
}
func (item *RangeRecord) mustParse(src []byte) {
_ = src[5] // early bound checking
item.StartGlyphID = binary.BigEndian.Uint16(src[0:])
item.EndGlyphID = binary.BigEndian.Uint16(src[2:])
item.StartCoverageIndex = binary.BigEndian.Uint16(src[4:])
}
@@ -0,0 +1,111 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import (
"encoding/binary"
"fmt"
)
type GDEF struct {
majorVersion uint16 // Major version of the GDEF table, = 1
minorVersion uint16 // Minor version of the GDEF table, = 0, 2, 3
GlyphClassDef ClassDef `offsetSize:"Offset16"` // Offset to class definition table for glyph type, from beginning of GDEF header (may be NULL)
AttachList AttachList `offsetSize:"Offset16"` // Offset to attachment point list table, from beginning of GDEF header (may be NULL)
LigCaretList LigCaretList `offsetSize:"Offset16"` // Offset to ligature caret list table, from beginning of GDEF header (may be NULL)
MarkAttachClass ClassDef `offsetSize:"Offset16"` // Offset to class definition table for mark attachment type, from beginning of GDEF header (may be NULL)
MarkGlyphSetsDef MarkGlyphSets `isOpaque:""` // Offset to the table of mark glyph set definitions, from beginning of GDEF header (may be NULL)
ItemVarStore ItemVarStore `isOpaque:""` // Offset to the Item Variation Store table, from beginning of GDEF header (may be NULL)
}
func (gdef *GDEF) parseMarkGlyphSetsDef(src []byte) error {
const headerSize = 12
if gdef.minorVersion < 2 {
return nil
}
if L := len(src); L < headerSize+2 {
return fmt.Errorf("EOF: expected length: %d, got %d", headerSize+2, L)
}
offset := binary.BigEndian.Uint16(src[headerSize:])
if offset != 0 {
var err error
gdef.MarkGlyphSetsDef, _, err = ParseMarkGlyphSets(src[offset:])
if err != nil {
return err
}
}
return nil
}
func (gdef *GDEF) parseItemVarStore(src []byte) (int, error) {
const headerSize = 12 + 2
if gdef.minorVersion < 3 {
return 0, nil
}
if L := len(src); L < headerSize+4 {
return 0, fmt.Errorf("EOF: expected length: %d, got %d", headerSize+4, L)
}
offset := binary.BigEndian.Uint32(src[headerSize:])
if offset != 0 {
var err error
gdef.ItemVarStore, _, err = ParseItemVarStore(src[offset:])
if err != nil {
return 0, err
}
}
return headerSize + 4, nil
}
type AttachList struct {
Coverage Coverage `offsetSize:"Offset16"` // Offset to Coverage table - from beginning of AttachList table
AttachPoints []AttachPoint `arrayCount:"FirstUint16" offsetsArray:"Offset16"` // [glyphCount] Array of offsets to AttachPoint tables-from beginning of AttachList table-in Coverage Index order
}
type AttachPoint struct {
PointIndices []uint16 `arrayCount:"FirstUint16"` // [pointCount] Array of contour point indices -in increasing numerical order
}
type LigCaretList struct {
Coverage Coverage `offsetSize:"Offset16"` // Offset to Coverage table - from beginning of LigCaretList table
LigGlyphs []LigGlyph `arrayCount:"FirstUint16" offsetsArray:"Offset16"` // [ligGlyphCount] Array of offsets to LigGlyph tables, from beginning of LigCaretList table —in Coverage Index order
}
type LigGlyph struct {
CaretValues []CaretValue `arrayCount:"FirstUint16" offsetsArray:"Offset16"` // [caretCount] Array of offsets to CaretValue tables, from beginning of LigGlyph table — in increasing coordinate order
}
type CaretValue interface {
isCaretValue()
}
func (CaretValue1) isCaretValue() {}
func (CaretValue2) isCaretValue() {}
func (CaretValue3) isCaretValue() {}
type CaretValue1 struct {
caretValueFormat uint16 `unionTag:"1"` // Format identifier: format = 1
Coordinate int16 // X or Y value, in design units
}
type CaretValue2 struct {
caretValueFormat uint16 `unionTag:"2"` // Format identifier: format = 2
CaretValuePointIndex uint16 // Contour point index on glyph
}
type CaretValue3 struct {
caretValueFormat uint16 `unionTag:"3"` // Format identifier: format = 3
Coordinate int16 // X or Y value, in design units
deviceOffset Offset16 // Offset to Device table (non-variable font) / Variation Index table (variable font) for X or Y value-from beginning of CaretValue table
Device DeviceTable `isOpaque:""`
}
func (cv *CaretValue3) parseDevice(src []byte) (err error) {
cv.Device, err = parseDeviceTable(src, uint16(cv.deviceOffset))
return err
}
type MarkGlyphSets struct {
format uint16 // Format identifier == 1
Coverages []Coverage `arrayCount:"FirstUint16" offsetsArray:"Offset32"` // [markGlyphSetCount] Array of offsets to mark glyph set coverage tables, from the start of the MarkGlyphSets table.
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,276 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import (
"fmt"
)
type SinglePos struct {
Data SinglePosData
}
type SinglePosData interface {
isSinglePosData()
Cov() Coverage
}
func (SinglePosData1) isSinglePosData() {}
func (SinglePosData2) isSinglePosData() {}
type SinglePosData1 struct {
format uint16 `unionTag:"1"`
coverage Coverage `offsetSize:"Offset16"` // Offset to Coverage table, from beginning of SinglePos subtable.
ValueFormat ValueFormat // Defines the types of data in the ValueRecord.
ValueRecord ValueRecord `isOpaque:""` // Defines positioning value(s) — applied to all glyphs in the Coverage table.
}
func (sp *SinglePosData1) parseValueRecord(src []byte) (err error) {
sp.ValueRecord, _, err = parseValueRecord(sp.ValueFormat, src, 6)
return err
}
type SinglePosData2 struct {
format uint16 `unionTag:"2"`
coverage Coverage `offsetSize:"Offset16"` // Offset to Coverage table, from beginning of SinglePos subtable.
ValueFormat ValueFormat // Defines the types of data in the ValueRecords.
valueCount uint16 // Number of ValueRecords — must equal glyphCount in the Coverage table.
ValueRecords []ValueRecord `isOpaque:""` //[valueCount] Array of ValueRecords — positioning values applied to glyphs.
}
func (sp *SinglePosData2) parseValueRecords(src []byte) (err error) {
offset := 8
sp.ValueRecords = make([]ValueRecord, sp.valueCount)
for i := range sp.ValueRecords {
sp.ValueRecords[i], offset, err = parseValueRecord(sp.ValueFormat, src, offset)
if err != nil {
return err
}
}
return err
}
type PairPos struct {
Data PairPosData
}
type PairPosData interface {
isPairPosData()
Cov() Coverage
}
func (PairPosData1) isPairPosData() {}
func (PairPosData2) isPairPosData() {}
type PairPosData1 struct {
format uint16 `unionTag:"1"`
coverage Coverage `offsetSize:"Offset16"` // Offset to Coverage table, from beginning of PairPos subtable.
ValueFormat1 ValueFormat // Defines the types of data in valueRecord1 — for the first glyph in the pair (may be zero).
ValueFormat2 ValueFormat // Defines the types of data in valueRecord2 — for the second glyph in the pair (may be zero).
PairSets []PairSet `arrayCount:"FirstUint16" offsetsArray:"Offset16" arguments:"valueFormat1=.ValueFormat1, valueFormat2=.ValueFormat2"` //[pairSetCount] Array of offsets to PairSet tables. Offsets are from beginning of PairPos subtable, ordered by Coverage Index.
}
// binarygen: argument=valueFormat1 ValueFormat
// binarygen: argument=valueFormat2 ValueFormat
type PairSet struct {
pairValueCount uint16 // Number of PairValueRecords
// we store the compressed form to avoid wasting to much memory
data pairValueRecords `isOpaque:""`
}
func (ps *PairSet) parseData(src []byte, fmt1, fmt2 ValueFormat) error {
recNbUint16 := 1 + fmt1.size() + fmt2.size() // in uint16
if exp := 2 + recNbUint16*2*int(ps.pairValueCount); len(src) < exp { //
return fmt.Errorf("EOF: expected length: %d, got %d", exp, len(src))
}
ps.data = pairValueRecords{data: src, fmt1: fmt1, fmt2: fmt2}
return nil
}
type PairPosData2 struct {
format uint16 `unionTag:"2"`
coverage Coverage `offsetSize:"Offset16"` // Offset to Coverage table, from beginning of PairPos subtable.
ValueFormat1 ValueFormat // Defines the types of data in valueRecord1 — for the first glyph in the pair (may be zero).
ValueFormat2 ValueFormat // Defines the types of data in valueRecord2 — for the second glyph in the pair (may be zero).
ClassDef1 ClassDef `offsetSize:"Offset16"` // Offset to ClassDef table, from beginning of PairPos subtable — for the first glyph of the pair.
ClassDef2 ClassDef `offsetSize:"Offset16"` // Offset to ClassDef table, from beginning of PairPos subtable — for the second glyph of the pair.
class1Count uint16 // Number of classes in classDef1 table — includes Class 0.
class2Count uint16 // Number of classes in classDef2 table — includes Class 0.
classData []byte `subsliceStart:"AtStart" arrayCount:"ToEnd"`
}
// Record returns the record for the given classes, which must come from ClassDef1
// and ClassDef2
func (pp *PairPosData2) Record(class1, class2 uint16) Class2Record {
const headerSize = 16 // including posFormat and coverageOffset
size2 := (pp.ValueFormat1.size() + pp.ValueFormat2.size()) * 2
size1 := int(pp.class2Count) * size2
offset := headerSize + size1*int(class1) + size2*int(class2)
v1, newOffset, _ := parseValueRecord(pp.ValueFormat1, pp.classData, offset)
v2, _, _ := parseValueRecord(pp.ValueFormat2, pp.classData, newOffset)
return Class2Record{v1, v2}
}
// DeviceTableHeader is the common header for DeviceTable
// See https://learn.microsoft.com/fr-fr/typography/opentype/spec/chapter2#device-and-variationindex-tables
type DeviceTableHeader struct {
first uint16
second uint16
deltaFormat uint16 // Format of deltaValue array data
}
type Anchor interface {
isAnchor()
}
type EntryExit struct {
EntryAnchor Anchor
ExitAnchor Anchor
}
type CursivePos struct {
posFormat uint16 // Format identifier: format = 1
coverage Coverage `offsetSize:"Offset16"` // Offset to Coverage table, from beginning of CursivePos subtable.
entryExitRecords []entryExitRecord `arrayCount:"FirstUint16"` //[entryExitCount] Array of EntryExit records, in Coverage index order.
EntryExits []EntryExit `isOpaque:""`
}
type entryExitRecord struct {
entryAnchorOffset Offset16 // Offset to entryAnchor table, from beginning of CursivePos subtable (may be NULL).
exitAnchorOffset Offset16 // Offset to exitAnchor table, from beginning of CursivePos subtable (may be NULL).
}
func (cp *CursivePos) parseEntryExits(src []byte) error {
cp.EntryExits = make([]EntryExit, len(cp.entryExitRecords))
var err error
for i, rec := range cp.entryExitRecords {
if rec.entryAnchorOffset != 0 {
if L := len(src); L < int(rec.entryAnchorOffset) {
return fmt.Errorf("EOF: expected length: %d, got %d", rec.entryAnchorOffset, L)
}
cp.EntryExits[i].EntryAnchor, _, err = ParseAnchor(src[rec.entryAnchorOffset:])
if err != nil {
return err
}
}
if rec.exitAnchorOffset != 0 {
if L := len(src); L < int(rec.exitAnchorOffset) {
return fmt.Errorf("EOF: expected length: %d, got %d", rec.exitAnchorOffset, L)
}
cp.EntryExits[i].ExitAnchor, _, err = ParseAnchor(src[rec.exitAnchorOffset:])
if err != nil {
return err
}
}
}
return nil
}
type MarkBasePos struct {
posFormat uint16 // Format identifier: format = 1
markCoverage Coverage `offsetSize:"Offset16"` // Offset to markCoverage table, from beginning of MarkBasePos subtable.
BaseCoverage Coverage `offsetSize:"Offset16"` // Offset to baseCoverage table, from beginning of MarkBasePos subtable.
markClassCount uint16 // Number of classes defined for marks
MarkArray MarkArray `offsetSize:"Offset16"` // Offset to MarkArray table, from beginning of MarkBasePos subtable.
BaseArray BaseArray `offsetSize:"Offset16" arguments:"offsetsCount=.markClassCount"` // Offset to BaseArray table, from beginning of MarkBasePos subtable.
}
type BaseArray struct {
baseRecords []anchorOffsets `arrayCount:"FirstUint16"` // [markClassCount] Array of offsets (one per mark class) to Anchor tables. Offsets are from beginning of BaseArray table, ordered by class (offsets may be NULL).
data []byte `arrayCount:"ToEnd" subsliceStart:"AtStart"`
}
func (ba BaseArray) Anchors() AnchorMatrix { return AnchorMatrix{ba.baseRecords, ba.data} }
type anchorOffsets struct {
offsets []Offset16 // Array of offsets to Anchor tables, with external length
}
type MarkLigPos struct {
posFormat uint16 // Format identifier: format = 1
MarkCoverage Coverage `offsetSize:"Offset16"` // Offset to markCoverage table, from beginning of MarkLigPos subtable.
LigatureCoverage Coverage `offsetSize:"Offset16"` // Offset to ligatureCoverage table, from beginning of MarkLigPos subtable.
MarkClassCount uint16 // Number of defined mark classes
MarkArray MarkArray `offsetSize:"Offset16"` // Offset to MarkArray table, from beginning of MarkLigPos subtable.
LigatureArray LigatureArray `offsetSize:"Offset16" arguments:"offsetsCount=.MarkClassCount"` // Offset to LigatureArray table, from beginning of MarkLigPos subtable.
}
type LigatureArray struct {
LigatureAttachs []LigatureAttach `arrayCount:"FirstUint16" offsetsArray:"Offset16"` // [ligatureCount] Array of offsets to LigatureAttach tables. Offsets are from beginning of LigatureArray table, ordered by ligatureCoverage index.
}
type LigatureAttach struct {
// [componentCount] Array of Component records, ordered in writing direction.
// Each element is an array of offsets (one per class, length = [markClassCount]) to Anchor tables. Offsets are from beginning of LigatureAttach table, ordered by class (offsets may be NULL).
componentRecords []anchorOffsets `arrayCount:"FirstUint16"`
data []byte `arrayCount:"ToEnd" subsliceStart:"AtStart"`
}
func (la LigatureAttach) Anchors() AnchorMatrix { return AnchorMatrix{la.componentRecords, la.data} }
type MarkMarkPos struct {
PosFormat uint16 // Format identifier: format = 1
Mark1Coverage Coverage `offsetSize:"Offset16"` // Offset to Combining Mark Coverage table, from beginning of MarkMarkPos subtable.
Mark2Coverage Coverage `offsetSize:"Offset16"` // Offset to Base Mark Coverage table, from beginning of MarkMarkPos subtable.
MarkClassCount uint16 // Number of Combining Mark classes defined
Mark1Array MarkArray `offsetSize:"Offset16"` // Offset to MarkArray table for mark1, from beginning of MarkMarkPos subtable.
Mark2Array Mark2Array `offsetSize:"Offset16" arguments:"offsetsCount=.MarkClassCount"` // Offset to Mark2Array table for mark2, from beginning of MarkMarkPos subtable.
}
type Mark2Array struct {
// [mark2Count] Array of Mark2Records, in Coverage order.
// Each element if an array of offsets (one per class, length = [markClassCount]) to Anchor tables. Offsets are from beginning of Mark2Array table, in class order (offsets may be NULL).
mark2Records []anchorOffsets `arrayCount:"FirstUint16"`
data []byte `arrayCount:"ToEnd" subsliceStart:"AtStart"`
}
func (ma Mark2Array) Anchors() AnchorMatrix { return AnchorMatrix{ma.mark2Records, ma.data} }
type ContextualPos struct {
Data ContextualPosITF
}
type ContextualPosITF interface {
isContextualPosITF()
Cov() Coverage
}
type (
ContextualPos1 SequenceContextFormat1
ContextualPos2 SequenceContextFormat2
ContextualPos3 SequenceContextFormat3
)
func (ContextualPos1) isContextualPosITF() {}
func (ContextualPos2) isContextualPosITF() {}
func (ContextualPos3) isContextualPosITF() {}
type ChainedContextualPos struct {
Data ChainedContextualPosITF
}
type ChainedContextualPosITF interface {
isChainedContextualPosITF()
Cov() Coverage
}
type (
ChainedContextualPos1 ChainedSequenceContextFormat1
ChainedContextualPos2 ChainedSequenceContextFormat2
ChainedContextualPos3 ChainedSequenceContextFormat3
)
func (ChainedContextualPos1) isChainedContextualPosITF() {}
func (ChainedContextualPos2) isChainedContextualPosITF() {}
func (ChainedContextualPos3) isChainedContextualPosITF() {}
type ExtensionPos Extension
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,117 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
type SingleSubs struct {
Data SingleSubstData
}
type SingleSubstData interface {
isSingleSubstData()
Cov() Coverage
}
func (SingleSubstData1) isSingleSubstData() {}
func (SingleSubstData2) isSingleSubstData() {}
type SingleSubstData1 struct {
format uint16 `unionTag:"1"`
Coverage Coverage `offsetSize:"Offset16"` // Offset to Coverage table, from beginning of substitution subtable
DeltaGlyphID int16 // Add to original glyph ID to get substitute glyph ID
}
type SingleSubstData2 struct {
format uint16 `unionTag:"2"`
Coverage Coverage `offsetSize:"Offset16"` // Offset to Coverage table, from beginning of substitution subtable
SubstituteGlyphIDs []GlyphID `arrayCount:"FirstUint16"` //[glyphCount] Array of substitute glyph IDs — ordered by Coverage index
}
type MultipleSubs struct {
substFormat uint16 // Format identifier: format = 1
Coverage Coverage `offsetSize:"Offset16"` // Offset to Coverage table, from beginning of substitution subtable
Sequences []Sequence `arrayCount:"FirstUint16" offsetsArray:"Offset16"`
//[sequenceCount] Array of offsets to Sequence tables. Offsets are from beginning of substitution subtable, ordered by Coverage index
}
type Sequence struct {
SubstituteGlyphIDs []GlyphID `arrayCount:"FirstUint16"` // [glyphCount] String of glyph IDs to substitute
}
type AlternateSubs struct {
substFormat uint16 // Format identifier: format = 1
Coverage Coverage `offsetSize:"Offset16"` // Offset to Coverage table, from beginning of substitution subtable
AlternateSets []AlternateSet `arrayCount:"FirstUint16" offsetsArray:"Offset16"`
}
type AlternateSet struct {
AlternateGlyphIDs []GlyphID `arrayCount:"FirstUint16"` // Array of alternate glyph IDs, in arbitrary order
}
type LigatureSubs struct {
substFormat uint16 // Format identifier: format = 1
Coverage Coverage `offsetSize:"Offset16"` // Offset to Coverage table, from beginning of substitution subtable
LigatureSets []LigatureSet `arrayCount:"FirstUint16" offsetsArray:"Offset16"` //[ligatureSetCount] Array of offsets to LigatureSet tables. Offsets are from beginning of substitution subtable, ordered by Coverage index
}
// All ligatures beginning with the same glyph
type LigatureSet struct {
Ligatures []Ligature `arrayCount:"FirstUint16" offsetsArray:"Offset16"` // [LigatureCount] Array of offsets to Ligature tables. Offsets are from beginning of LigatureSet table, ordered by preference.
}
// Glyph components for one ligature
type Ligature struct {
LigatureGlyph GlyphID // glyph ID of ligature to substitute
componentCount uint16 // Number of components in the ligature
ComponentGlyphIDs []GlyphID `arrayCount:"ComputedField-componentCount-1"` // [componentCount - 1] Array of component glyph IDs — start with the second component, ordered in writing direction
}
type ContextualSubs struct {
Data ContextualSubsITF
}
type ContextualSubsITF interface {
isContextualSubsITF()
Cov() Coverage
}
type (
ContextualSubs1 SequenceContextFormat1
ContextualSubs2 SequenceContextFormat2
ContextualSubs3 SequenceContextFormat3
)
func (ContextualSubs1) isContextualSubsITF() {}
func (ContextualSubs2) isContextualSubsITF() {}
func (ContextualSubs3) isContextualSubsITF() {}
type ChainedContextualSubs struct {
Data ChainedContextualSubsITF
}
type ChainedContextualSubsITF interface {
isChainedContextualSubsITF()
Cov() Coverage
}
type (
ChainedContextualSubs1 ChainedSequenceContextFormat1
ChainedContextualSubs2 ChainedSequenceContextFormat2
ChainedContextualSubs3 ChainedSequenceContextFormat3
)
func (ChainedContextualSubs1) isChainedContextualSubsITF() {}
func (ChainedContextualSubs2) isChainedContextualSubsITF() {}
func (ChainedContextualSubs3) isChainedContextualSubsITF() {}
type ExtensionSubs Extension
type ReverseChainSingleSubs struct {
substFormat uint16 // Format identifier: format = 1
coverage Coverage `offsetSize:"Offset16"` // Offset to Coverage table, from beginning of substitution subtable.
BacktrackCoverages []Coverage `arrayCount:"FirstUint16" offsetsArray:"Offset16"` //[backtrackGlyphCount] Array of offsets to coverage tables in backtrack sequence, in glyph sequence order.
LookaheadCoverages []Coverage `arrayCount:"FirstUint16" offsetsArray:"Offset16"` //[lookaheadGlyphCount] Array of offsets to coverage tables in lookahead sequence, in glyph sequence order.
SubstituteGlyphIDs []GlyphID `arrayCount:"FirstUint16"` //[glyphCount] Array of substitute glyph IDs — ordered by Coverage index.
}
@@ -0,0 +1,814 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import (
"encoding/binary"
"errors"
"fmt"
"math/bits"
)
// The following are types shared by GSUB and GPOS tables
// Coverage specifies all the glyphs affected by a substitution or
// positioning operation described in a subtable.
// Conceptually is it a []GlyphIndex, with an Index method,
// but it may be implemented for efficiently.
// See https://learn.microsoft.com/typography/opentype/spec/chapter2#lookup-table
type Coverage interface {
isCov()
// Index returns the index of the provided glyph, or
// `false` if the glyph is not covered by this lookup.
// Note: this method is injective: two distincts, covered glyphs are mapped
// to distincts indices.
Index(GlyphID) (int, bool)
// Len return the number of glyphs covered.
// It is 0 for empty coverages.
// For non empty Coverages, it is also 1 + (maximum index returned)
Len() int
}
func (Coverage1) isCov() {}
func (Coverage2) isCov() {}
type Coverage1 struct {
format uint16 `unionTag:"1"`
Glyphs []GlyphID `arrayCount:"FirstUint16"`
}
type Coverage2 struct {
format uint16 `unionTag:"2"`
Ranges []RangeRecord `arrayCount:"FirstUint16"`
}
type RangeRecord struct {
StartGlyphID GlyphID // First glyph ID in the range
EndGlyphID GlyphID // Last glyph ID in the range
StartCoverageIndex uint16 // Coverage Index of first glyph ID in range
}
// ClassDef stores a value for a set of GlyphIDs.
// Conceptually it is a map[GlyphID]uint16, but it may
// be implemented more efficiently.
type ClassDef interface {
isClassDef()
Class(gi GlyphID) (uint16, bool)
// Extent returns the maximum class ID + 1. This is the length
// required for an array to be indexed by the class values.
Extent() int
}
func (ClassDef1) isClassDef() {}
func (ClassDef2) isClassDef() {}
type ClassDef1 struct {
format uint16 `unionTag:"1"`
StartGlyphID GlyphID // First glyph ID of the classValueArray
ClassValueArray []uint16 `arrayCount:"FirstUint16"` //[glyphCount] Array of Class Values — one per glyph ID
}
type ClassDef2 struct {
format uint16 `unionTag:"2"`
ClassRangeRecords []ClassRangeRecord `arrayCount:"FirstUint16"` //[glyphCount] Array of Class Values — one per glyph ID
}
type ClassRangeRecord struct {
StartGlyphID GlyphID // First glyph ID in the range
EndGlyphID GlyphID // Last glyph ID in the range
Class uint16 // Applied to all glyphs in the range
}
// Lookups
type SequenceLookupRecord struct {
SequenceIndex uint16 // Index (zero-based) into the input glyph sequence
LookupListIndex uint16 // Index (zero-based) into the LookupList
}
type SequenceContextFormat1 struct {
format uint16 `unionTag:"1"`
coverage Coverage `offsetSize:"Offset16"` // Offset to Coverage table, from beginning of SequenceContextFormat1 table
SeqRuleSet []SequenceRuleSet `arrayCount:"FirstUint16" offsetsArray:"Offset16"` //[seqRuleSetCount] Array of offsets to SequenceRuleSet tables, from beginning of SequenceContextFormat1 table (offsets may be NULL)
}
func (sc *SequenceContextFormat1) sanitize(lookupCount uint16) error {
for _, set := range sc.SeqRuleSet {
for _, rule := range set.SeqRule {
if err := rule.sanitize(lookupCount); err != nil {
return err
}
}
}
return nil
}
type SequenceRuleSet struct {
SeqRule []SequenceRule `arrayCount:"FirstUint16" offsetsArray:"Offset16"` // Array of offsets to SequenceRule tables, from beginning of the SequenceRuleSet table
}
type SequenceRule struct {
glyphCount uint16 // Number of glyphs in the input glyph sequence
seqLookupCount uint16 // Number of SequenceLookupRecords
InputSequence []GlyphID `arrayCount:"ComputedField-glyphCount-1"` //[glyphCount - 1] Array of input glyph IDs—starting with the second glyph
SeqLookupRecords []SequenceLookupRecord `arrayCount:"ComputedField-seqLookupCount"` //[seqLookupCount] Array of Sequence lookup records
}
func (sr *SequenceRule) sanitize(lookupCount uint16) error {
for _, rec := range sr.SeqLookupRecords {
if rec.SequenceIndex >= sr.glyphCount {
return fmt.Errorf("invalid sequence lookup table (input index %d >= %d)", rec.SequenceIndex, sr.glyphCount)
}
if rec.LookupListIndex >= lookupCount {
return fmt.Errorf("invalid sequence lookup table (lookup index %d >= %d)", rec.LookupListIndex, lookupCount)
}
}
return nil
}
type SequenceContextFormat2 struct {
format uint16 `unionTag:"2"`
coverage Coverage `offsetSize:"Offset16"` // Offset to Coverage table, from beginning of SequenceContextFormat2 table
ClassDef ClassDef `offsetSize:"Offset16"` // Offset to ClassDef table, from beginning of SequenceContextFormat2 table
ClassSeqRuleSet []ClassSequenceRuleSet `arrayCount:"FirstUint16" offsetsArray:"Offset16"` //[classSeqRuleSetCount] Array of offsets to ClassSequenceRuleSet tables, from beginning of SequenceContextFormat2 table (may be NULL)
}
// ClassSequenceRuleSet has the same binary format as SequenceRuleSet,
// and using the same type simplifies later processing.
type ClassSequenceRuleSet = SequenceRuleSet
type SequenceContextFormat3 struct {
format uint16 `unionTag:"3"`
glyphCount uint16 // Number of glyphs in the input sequence
seqLookupCount uint16 // Number of SequenceLookupRecords
Coverages []Coverage `arrayCount:"ComputedField-glyphCount" offsetsArray:"Offset16"` //[glyphCount] Array of offsets to Coverage tables, from beginning of SequenceContextFormat3 subtable
SeqLookupRecords []SequenceLookupRecord `arrayCount:"ComputedField-seqLookupCount"` //[seqLookupCount] Array of SequenceLookupRecords
}
type ChainedSequenceContextFormat1 struct {
format uint16 `unionTag:"1"`
coverage Coverage `offsetSize:"Offset16"` // Offset to Coverage table, from beginning of ChainSequenceContextFormat1 table
ChainedSeqRuleSet []ChainedSequenceRuleSet `arrayCount:"FirstUint16" offsetsArray:"Offset16"` //[chainedSeqRuleSetCount] Array of offsets to ChainedSeqRuleSet tables, from beginning of ChainedSequenceContextFormat1 table (may be NULL)
}
type ChainedSequenceRuleSet struct {
ChainedSeqRules []ChainedSequenceRule `arrayCount:"FirstUint16" offsetsArray:"Offset16"` // Array of offsets to SequenceRule tables, from beginning of the SequenceRuleSet table
}
type ChainedSequenceRule struct {
BacktrackSequence []GlyphID `arrayCount:"FirstUint16"` //[backtrackGlyphCount] Array of backtrack glyph IDs
inputGlyphCount uint16 // Number of glyphs in the input sequence
InputSequence []GlyphID `arrayCount:"ComputedField-inputGlyphCount-1"` //[inputGlyphCount - 1] Array of input glyph IDs—start with second glyph
LookaheadSequence []GlyphID `arrayCount:"FirstUint16"` //[lookaheadGlyphCount] Array of lookahead glyph IDs
SeqLookupRecords []SequenceLookupRecord `arrayCount:"FirstUint16"` //[seqLookupCount] Array of SequenceLookupRecords
}
type ChainedSequenceContextFormat2 struct {
format uint16 `unionTag:"2"`
coverage Coverage `offsetSize:"Offset16"` // Offset to Coverage table, from beginning of ChainedSequenceContextFormat2 table
BacktrackClassDef ClassDef `offsetSize:"Offset16"` // Offset to ClassDef table containing backtrack sequence context, from beginning of ChainedSequenceContextFormat2 table
InputClassDef ClassDef `offsetSize:"Offset16"` // Offset to ClassDef table containing input sequence context, from beginning of ChainedSequenceContextFormat2 table
LookaheadClassDef ClassDef `offsetSize:"Offset16"` // Offset to ClassDef table containing lookahead sequence context, from beginning of ChainedSequenceContextFormat2 table
ChainedClassSeqRuleSet []ChainedClassSequenceRuleSet `arrayCount:"FirstUint16" offsetsArray:"Offset16"` //[chainedClassSeqRuleSetCount] Array of offsets to ChainedClassSequenceRuleSet tables, from beginning of ChainedSequenceContextFormat2 table (may be NULL)
}
// ChainedClassSequenceRuleSet has the same binary format as ChainedSequenceRuleSet,
// and using the same type simplifies later processing.
type ChainedClassSequenceRuleSet = ChainedSequenceRuleSet
type ChainedSequenceContextFormat3 struct {
format uint16 `unionTag:"3"`
BacktrackCoverages []Coverage `arrayCount:"FirstUint16" offsetsArray:"Offset16"` //[backtrackGlyphCount] Array of offsets to coverage tables for the backtrack sequence
InputCoverages []Coverage `arrayCount:"FirstUint16" offsetsArray:"Offset16"` //[inputGlyphCount] Array of offsets to coverage tables for the input sequence
LookaheadCoverages []Coverage `arrayCount:"FirstUint16" offsetsArray:"Offset16"` //[lookaheadGlyphCount] Array of offsets to coverage tables for the lookahead sequence
SeqLookupRecords []SequenceLookupRecord `arrayCount:"FirstUint16"` //[seqLookupCount] Array of SequenceLookupRecords
}
type Extension struct {
substFormat uint16 // Format identifier. Set to 1.
ExtensionLookupType uint16 // Lookup type of subtable referenced by extensionOffset (that is, the extension subtable).
ExtensionOffset Offset32 // Offset to the extension subtable, of lookup type extensionLookupType, relative to the start of the ExtensionSubstFormat1 subtable.
RawData []byte `subsliceStart:"AtStart" arrayCount:"ToEnd"`
}
// GSUB is the Glyph Substitution (GSUB) table.
// It provides data for substition of glyphs for appropriate rendering of scripts,
// such as cursively-connecting forms in Arabic script,
// or for advanced typographic effects, such as ligatures.
// See https://learn.microsoft.com/fr-fr/typography/opentype/spec/gsub
type GSUB Layout
// GSUBLookup is one lookup subtable data
type GSUBLookup interface {
isGSUBLookup()
// Coverage returns the coverage of the lookup subtable.
// For ContextualSubs3 and ChainedContextualSubs3, its the coverage of the first input.
Cov() Coverage
}
func (SingleSubs) isGSUBLookup() {}
func (MultipleSubs) isGSUBLookup() {}
func (AlternateSubs) isGSUBLookup() {}
func (LigatureSubs) isGSUBLookup() {}
func (ContextualSubs) isGSUBLookup() {}
func (ChainedContextualSubs) isGSUBLookup() {}
func (ExtensionSubs) isGSUBLookup() {}
func (ReverseChainSingleSubs) isGSUBLookup() {}
func (ms MultipleSubs) Sanitize() error {
if exp, got := ms.Coverage.Len(), len(ms.Sequences); exp != got {
return fmt.Errorf("GSUB: invalid MultipleSubs sequences count (%d != %d)", exp, got)
}
return nil
}
func (ls LigatureSubs) Sanitize() error {
if exp, got := ls.Coverage.Len(), len(ls.LigatureSets); exp != got {
return fmt.Errorf("GSUB: invalid LigatureSubs sets count (%d != %d)", exp, got)
}
return nil
}
func (cs ContextualSubs) Sanitize(lookupCount uint16) error {
if f1, isFormat1 := cs.Data.(ContextualSubs1); isFormat1 {
return (*SequenceContextFormat1)(&f1).sanitize(lookupCount)
}
return nil
}
func (rs ReverseChainSingleSubs) Sanitize() error {
if exp, got := rs.coverage.Len(), len(rs.SubstituteGlyphIDs); exp != got {
return fmt.Errorf("GSUB: invalid ReverseChainSingleSubs glyphs count (%d != %d)", exp, got)
}
return nil
}
func (ext ExtensionSubs) Resolve() (GSUBLookup, error) {
if L, E := len(ext.RawData), int(ext.ExtensionOffset); L < E {
return nil, fmt.Errorf("EOF: expected length: %d, got %d", E, L)
}
lk, err := parseGSUBLookup(ext.RawData[ext.ExtensionOffset:], ext.ExtensionLookupType)
if err != nil {
return nil, err
}
if _, isExt := lk.(ExtensionSubs); isExt {
return nil, errors.New("invalid extension substitution table")
}
return lk, nil
}
func parseGSUBLookup(src []byte, lookupType uint16) (out GSUBLookup, err error) {
switch lookupType {
case 1: // Single (format 1.1 1.2) Replace one glyph with one glyph
out, _, err = ParseSingleSubs(src)
case 2: // Multiple (format 2.1) Replace one glyph with more than one glyph
out, _, err = ParseMultipleSubs(src)
case 3: // Alternate (format 3.1) Replace one glyph with one of many glyphs
out, _, err = ParseAlternateSubs(src)
case 4: // Ligature (format 4.1) Replace multiple glyphs with one glyph
out, _, err = ParseLigatureSubs(src)
case 5: // Context (format 5.1 5.2 5.3) Replace one or more glyphs in context
out, _, err = ParseContextualSubs(src)
case 6: // Chaining Context (format 6.1 6.2 6.3) Replace one or more glyphs in chained context
out, _, err = ParseChainedContextualSubs(src)
case 7: // Extension Substitution (format 7.1) Extension mechanism for other substitutions
out, _, err = ParseExtensionSubs(src)
case 8: // Reverse chaining context single (format 8.1)
out, _, err = ParseReverseChainSingleSubs(src)
default:
err = fmt.Errorf("invalid GSUB Loopkup type %d", lookupType)
}
return out, err
}
// AsGSUBLookups returns the GSUB lookup subtables.
func (lk Lookup) AsGSUBLookups() ([]GSUBLookup, error) {
var err error
out := make([]GSUBLookup, len(lk.subtableOffsets))
for i, offset := range lk.subtableOffsets {
if L := len(lk.rawData); L < int(offset) {
return nil, fmt.Errorf("EOF: expected length: %d, got %d", offset, L)
}
out[i], err = parseGSUBLookup(lk.rawData[offset:], lk.lookupType)
if err != nil {
return nil, err
}
}
return out, nil
}
// ------------------------ GPOS common data structures ------------------------
// GPOS is the Glyph Positioning (GPOS) table.
// It provides precise control over glyph placement
// for sophisticated text layout and rendering in each script
// and language system that a font supports.
// See https://learn.microsoft.com/fr-fr/typography/opentype/spec/gpos
type GPOS Layout
type GPOSLookup interface {
isGPOSLookup()
// Coverage returns the coverage of the lookup subtable.
// For ContextualPos3 and ChainedContextualPos3, its the coverage of the first input.
Cov() Coverage
}
func (SinglePos) isGPOSLookup() {}
func (PairPos) isGPOSLookup() {}
func (CursivePos) isGPOSLookup() {}
func (MarkBasePos) isGPOSLookup() {}
func (MarkLigPos) isGPOSLookup() {}
func (MarkMarkPos) isGPOSLookup() {}
func (ContextualPos) isGPOSLookup() {}
func (ChainedContextualPos) isGPOSLookup() {}
func (ExtensionPos) isGPOSLookup() {}
func (sp *SinglePos) Sanitize() error {
if f2, isFormat2 := sp.Data.(SinglePosData2); isFormat2 {
if exp, got := f2.coverage.Len(), len(f2.ValueRecords); exp != got {
return fmt.Errorf("GPOS: invalid SinglePos values count (%d != %d)", exp, got)
}
}
return nil
}
func (pp *PairPos) Sanitize() error {
if f1, isFormat1 := pp.Data.(PairPosData1); isFormat1 {
// there are fonts with to much PairSets : accept it
if exp, got := f1.coverage.Len(), len(f1.PairSets); exp > got {
return fmt.Errorf("GPOS: invalid PairPos1 sets count (%d > %d)", exp, got)
}
} else if f2, isFormat2 := pp.Data.(PairPosData2); isFormat2 {
if exp, got := f2.ClassDef1.Extent(), int(f2.class1Count); exp != got {
return fmt.Errorf("GPOS: invalid PairPos2 class1 count (%d != %d)", exp, got)
}
if exp, got := f2.ClassDef2.Extent(), int(f2.class2Count); exp != got {
return fmt.Errorf("GPOS: invalid PairPos2 class2 count (%d != %d)", exp, got)
}
}
return nil
}
func (mp *MarkBasePos) Sanitize() error {
if exp, got := mp.markCoverage.Len(), len(mp.MarkArray.MarkRecords); exp != got {
return fmt.Errorf("GPOS: invalid MarkBasePos marks count (%d != %d)", exp, got)
}
if exp, got := mp.BaseCoverage.Len(), len(mp.BaseArray.baseRecords); exp != got {
return fmt.Errorf("GPOS: invalid MarkBasePos marks count (%d != %d)", exp, got)
}
if err := mp.BaseArray.Anchors().sanitizeOffsets(); err != nil {
return err
}
return nil
}
func (mp *MarkLigPos) Sanitize() error {
if exp, got := mp.MarkCoverage.Len(), len(mp.MarkArray.MarkAnchors); exp != got {
return fmt.Errorf("GPOS: invalid MarkBasePos marks count (%d != %d)", exp, got)
}
if exp, got := mp.LigatureCoverage.Len(), len(mp.LigatureArray.LigatureAttachs); exp != got {
return fmt.Errorf("GPOS: invalid MarkBasePos marks count (%d != %d)", exp, got)
}
return nil
}
func (cs *ContextualPos) Sanitize(lookupCount uint16) error {
if f1, isFormat1 := cs.Data.(ContextualPos1); isFormat1 {
return (*SequenceContextFormat1)(&f1).sanitize(lookupCount)
}
return nil
}
func (ext ExtensionPos) Resolve() (GPOSLookup, error) {
if L, E := len(ext.RawData), int(ext.ExtensionOffset); L < E {
return nil, fmt.Errorf("EOF: expected length: %d, got %d", E, L)
}
lk, err := parseGPOSLookup(ext.RawData[ext.ExtensionOffset:], ext.ExtensionLookupType)
if err != nil {
return nil, err
}
if _, isExt := lk.(ExtensionPos); isExt {
return nil, errors.New("invalid extension positioning table")
}
return lk, nil
}
func parseGPOSLookup(src []byte, lookupType uint16) (out GPOSLookup, err error) {
switch lookupType {
case 1: // Single adjustment Adjust position of a single glyph
out, _, err = ParseSinglePos(src)
case 2: // Pair adjustment Adjust position of a pair of glyphs
out, _, err = ParsePairPos(src)
case 3: // Cursive attachment Attach cursive glyphs
out, _, err = ParseCursivePos(src)
case 4: // MarkToBase attachment Attach a combining mark to a base glyph
out, _, err = ParseMarkBasePos(src)
case 5: // MarkToLigature attachment Attach a combining mark to a ligature
out, _, err = ParseMarkLigPos(src)
case 6: // MarkToMark attachment Attach a combining mark to another mark
out, _, err = ParseMarkMarkPos(src)
case 7: // Context positioning Position one or more glyphs in context
out, _, err = ParseContextualPos(src)
case 8: // Chained Context positioning Position one or more glyphs in chained context
out, _, err = ParseChainedContextualPos(src)
case 9: // Extension positioning Extension mechanism for other positionings
out, _, err = ParseExtensionPos(src)
default:
err = fmt.Errorf("invalid GPOS Loopkup type %d", lookupType)
}
return out, err
}
// AsGPOSLookups returns the GPOS lookup subtables
func (lk Lookup) AsGPOSLookups() ([]GPOSLookup, error) {
var err error
out := make([]GPOSLookup, len(lk.subtableOffsets))
for i, offset := range lk.subtableOffsets {
if L := len(lk.rawData); L < int(offset) {
return nil, fmt.Errorf("EOF: expected length: %d, got %d", offset, L)
}
out[i], err = parseGPOSLookup(lk.rawData[offset:], lk.lookupType)
if err != nil {
return nil, err
}
}
return out, nil
}
// ValueFormat is a mask indicating which field
// are set in a GPOS [ValueRecord].
// It is often shared between many records.
type ValueFormat uint16
// number of fields present
func (f ValueFormat) size() int { return bits.OnesCount16(uint16(f)) }
const (
XPlacement ValueFormat = 1 << iota // Includes horizontal adjustment for placement
YPlacement // Includes vertical adjustment for placement
XAdvance // Includes horizontal adjustment for advance
YAdvance // Includes vertical adjustment for advance
XPlaDevice // Includes horizontal Device table for placement
YPlaDevice // Includes vertical Device table for placement
XAdvDevice // Includes horizontal Device table for advance
YAdvDevice // Includes vertical Device table for advance
// Mask for having any Device table
Devices = XPlaDevice | YPlaDevice | XAdvDevice | YAdvDevice
)
// ValueRecord has optional fields
type ValueRecord struct {
XPlacement int16 // Horizontal adjustment for placement, in design units.
YPlacement int16 // Vertical adjustment for placement, in design units.
XAdvance int16 // Horizontal adjustment for advance, in design units — only used for horizontal layout.
YAdvance int16 // Vertical adjustment for advance, in design units — only used for vertical layout.
XPlaDevice DeviceTable // Offset to Device table (non-variable font) / VariationIndex table (variable font) for horizontal placement, from beginning of the immediate parent table (SinglePos or PairPosFormat2 lookup subtable, PairSet table within a PairPosFormat1 lookup subtable) — may be NULL.
YPlaDevice DeviceTable // Offset to Device table (non-variable font) / VariationIndex table (variable font) for vertical placement, from beginning of the immediate parent table (SinglePos or PairPosFormat2 lookup subtable, PairSet table within a PairPosFormat1 lookup subtable) — may be NULL.
XAdvDevice DeviceTable // Offset to Device table (non-variable font) / VariationIndex table (variable font) for horizontal advance, from beginning of the immediate parent table (SinglePos or PairPosFormat2 lookup subtable, PairSet table within a PairPosFormat1 lookup subtable) — may be NULL.
YAdvDevice DeviceTable // Offset to Device table (non-variable font) / VariationIndex table (variable font) for vertical advance, from beginning of the immediate parent table (SinglePos or PairPosFormat2 lookup subtable, PairSet table within a PairPosFormat1 lookup su
}
// [data] must start at the immediate parent table, [offset] indicating
// the start of the record in it.
// Returns [offset] + the number of bytes read from [offset]
// Note that a [format] with value 0, is supported, resulting in a no-op
func parseValueRecord(format ValueFormat, data []byte, offset int) (out ValueRecord, _ int, err error) {
if L := len(data); L < offset {
return out, 0, fmt.Errorf("EOF: expected length: %d, got %d", offset, L)
}
size := format.size() // number of fields present
if size == 0 { // return early
return out, offset, nil
}
// start by parsing the list of values
values, err := ParseUint16s(data[offset:], size)
if err != nil {
return out, 0, fmt.Errorf("invalid value record: %s", err)
}
// follow the order
cursor := 0
if format&XPlacement != 0 {
out.XPlacement = int16(values[cursor])
cursor++
}
if format&YPlacement != 0 {
out.YPlacement = int16(values[cursor])
cursor++
}
if format&XAdvance != 0 {
out.XAdvance = int16(values[cursor])
cursor++
}
if format&YAdvance != 0 {
out.YAdvance = int16(values[cursor])
cursor++
}
if format&XPlaDevice != 0 {
if devOffset := values[cursor]; devOffset != 0 {
out.XPlaDevice, err = parseDeviceTable(data, devOffset)
if err != nil {
return out, 0, err
}
}
cursor++
}
if format&YPlaDevice != 0 {
if devOffset := values[cursor]; devOffset != 0 {
out.YPlaDevice, err = parseDeviceTable(data, devOffset)
if err != nil {
return out, 0, err
}
}
cursor++
}
if format&XAdvDevice != 0 {
if devOffset := values[cursor]; devOffset != 0 {
out.XAdvDevice, err = parseDeviceTable(data, devOffset)
if err != nil {
return out, 0, err
}
}
cursor++
}
if format&YAdvDevice != 0 {
if devOffset := values[cursor]; devOffset != 0 {
out.YAdvDevice, err = parseDeviceTable(data, devOffset)
if err != nil {
return out, 0, err
}
}
cursor++ // useless actually
}
return out, offset + 2*size, err
}
type pairValueRecords struct {
data []byte // start with the item count
fmt1, fmt2 ValueFormat
}
// panic if index is out of range
func (ps pairValueRecords) get(index int) (out PairValueRecord, err error) {
recLen := 1 + ps.fmt1.size() + ps.fmt2.size()
offset := 2 + 2*index*recLen
out.SecondGlyph = GlyphID(binary.BigEndian.Uint16(ps.data[offset:]))
v1, newOffset, err := parseValueRecord(ps.fmt1, ps.data, offset+2)
if err != nil {
return out, fmt.Errorf("invalid pair set table: %s", err)
}
v2, _, err := parseValueRecord(ps.fmt2, ps.data, newOffset)
if err != nil {
return out, fmt.Errorf("invalid pair set table: %s", err)
}
out.ValueRecord1 = v1
out.ValueRecord2 = v2
return out, nil
}
// DeviceTable is either an DeviceHinting for standard fonts,
// or a DeviceVariation for variable fonts.
type DeviceTable interface {
isDevice()
}
func (DeviceHinting) isDevice() {}
func (DeviceVariation) isDevice() {}
type DeviceHinting struct {
// with length endSize - startSize + 1
Values []int8
// correction range, in ppem
StartSize, EndSize uint16
}
type DeviceVariation VariationStoreIndex
func parseDeviceTable(src []byte, offset uint16) (DeviceTable, error) {
if L := len(src); L < int(offset)+6 {
return nil, fmt.Errorf("EOF: expected length: %d, got %d", offset+6, L)
}
var header DeviceTableHeader
header.mustParse(src[offset:])
switch format := header.deltaFormat; format {
case 1, 2, 3:
var out DeviceHinting
out.StartSize, out.EndSize = header.first, header.second
if out.EndSize < out.StartSize {
return nil, errors.New("invalid positionning device subtable")
}
nbPerUint16 := 16 / (1 << format) // 8, 4 or 2
outLength := int(out.EndSize - out.StartSize + 1)
var count int
if outLength%nbPerUint16 == 0 {
count = outLength / nbPerUint16
} else {
// add padding
count = outLength/nbPerUint16 + 1
}
uint16s, err := ParseUint16s(src[offset+6:], count)
if err != nil {
return nil, err
}
out.Values = make([]int8, count*nbPerUint16) // handle rounding error by reslicing after
switch format {
case 1:
for i, u := range uint16s {
uint16As2Bits(out.Values[i*8:], u)
}
case 2:
for i, u := range uint16s {
uint16As4Bits(out.Values[i*4:], u)
}
case 3:
for i, u := range uint16s {
uint16As8Bits(out.Values[i*2:], u)
}
}
out.Values = out.Values[:outLength]
return out, nil
case 0x8000:
return DeviceVariation{DeltaSetOuter: header.first, DeltaSetInner: header.second}, nil
default:
return nil, fmt.Errorf("unsupported positionning device subtable: %d", format)
}
}
type PairValueRecord struct {
SecondGlyph GlyphID // Glyph ID of second glyph in the pair (first glyph is listed in the Coverage table).
ValueRecord1 ValueRecord // Positioning data for the first glyph in the pair.
ValueRecord2 ValueRecord // Positioning data for the second glyph in the pair.
}
type Class1Record []Class2Record //[class2Count] Array of Class2 records, ordered by classes in classDef2.
type Class2Record struct {
ValueRecord1 ValueRecord // Positioning for first glyph — empty if valueFormat1 = 0.
ValueRecord2 ValueRecord // Positioning for second glyph — empty if valueFormat2 = 0.
}
func (AnchorFormat1) isAnchor() {}
func (AnchorFormat2) isAnchor() {}
func (AnchorFormat3) isAnchor() {}
type AnchorFormat1 struct {
anchorFormat uint16 `unionTag:"1"`
XCoordinate int16 // Horizontal value, in design units
YCoordinate int16 // Vertical value, in design units
}
type AnchorFormat2 struct {
anchorFormat uint16 `unionTag:"2"`
XCoordinate int16 // Horizontal value, in design units
YCoordinate int16 // Vertical value, in design units
AnchorPoint uint16 // Index to glyph contour point
}
type AnchorFormat3 struct {
anchorFormat uint16 `unionTag:"3"`
XCoordinate int16 // Horizontal value, in design units
YCoordinate int16 // Vertical value, in design units
xDeviceOffset Offset16 // Offset to Device table (non-variable font) / VariationIndex table (variable font) for X coordinate, from beginning of Anchor table (may be NULL)
yDeviceOffset Offset16 // Offset to Device table (non-variable font) / VariationIndex table (variable font) for Y coordinate, from beginning of Anchor table (may be NULL)
XDevice DeviceTable `isOpaque:""` // Offset to Device table (non-variable font) / VariationIndex table (variable font) for X coordinate, from beginning of Anchor table (may be NULL)
YDevice DeviceTable `isOpaque:""` // Offset to Device table (non-variable font) / VariationIndex table (variable font) for Y coordinate, from beginning of Anchor table (may be NULL)
}
func (af *AnchorFormat3) parseXDevice(src []byte) error {
if af.xDeviceOffset == 0 {
return nil
}
var err error
af.XDevice, err = parseDeviceTable(src, uint16(af.xDeviceOffset))
return err
}
func (af *AnchorFormat3) parseYDevice(src []byte) error {
if af.yDeviceOffset == 0 {
return nil
}
var err error
af.YDevice, err = parseDeviceTable(src, uint16(af.yDeviceOffset))
return err
}
// AnchorMatrix is a compact representation of a [][]Anchor
type AnchorMatrix struct {
records []anchorOffsets
data []byte
}
func (am AnchorMatrix) Len() int { return len(am.records) }
func (am AnchorMatrix) sanitizeOffsets() error {
for _, list := range am.records {
for _, offset := range list.offsets {
if offset == 0 {
continue
}
if L := len(am.data); L < int(offset) {
return fmt.Errorf("EOF: expected length: %d, got %d", offset, L)
}
}
}
return nil
}
func (am AnchorMatrix) Anchor(index, class int) Anchor {
if len(am.records) < index {
return nil
}
offsets := am.records[index].offsets
if len(offsets) < class {
return nil
}
offset := offsets[class]
if offset == 0 {
return nil
}
anchor, _, _ := ParseAnchor(am.data[offset:]) // offset is sanitized
return anchor
}
type MarkArray struct {
MarkRecords []MarkRecord `arrayCount:"FirstUint16"` //[markCount] Array of MarkRecords, ordered by corresponding glyphs in the associated mark Coverage table.
MarkAnchors []Anchor `isOpaque:""` // with same length as MarkRecords
}
func (ma *MarkArray) parseMarkAnchors(src []byte) error {
ma.MarkAnchors = make([]Anchor, len(ma.MarkRecords))
var err error
for i, rec := range ma.MarkRecords {
if L := len(src); L < int(rec.markAnchorOffset) {
return fmt.Errorf("EOF: expected length: %d, got %d", rec.markAnchorOffset, L)
}
ma.MarkAnchors[i], _, err = ParseAnchor(src[rec.markAnchorOffset:])
if err != nil {
return err
}
}
return nil
}
type MarkRecord struct {
MarkClass uint16 // Class defined for the associated mark.
markAnchorOffset Offset16 // Offset to Anchor table, from beginning of MarkArray table.
}
// ------------------------------ parsing helpers ------------------------------
// write 8 elements
func uint16As2Bits(dst []int8, u uint16) {
const mask = 0xFE // 11111110
dst[0] = int8((0-uint8(u>>15&1))&mask | uint8(u>>14&1))
dst[1] = int8((0-uint8(u>>13&1))&mask | uint8(u>>12&1))
dst[2] = int8((0-uint8(u>>11&1))&mask | uint8(u>>10&1))
dst[3] = int8((0-uint8(u>>9&1))&mask | uint8(u>>8&1))
dst[4] = int8((0-uint8(u>>7&1))&mask | uint8(u>>6&1))
dst[5] = int8((0-uint8(u>>5&1))&mask | uint8(u>>4&1))
dst[6] = int8((0-uint8(u>>3&1))&mask | uint8(u>>2&1))
dst[7] = int8((0-uint8(u>>1&1))&mask | uint8(u>>0&1))
}
// write 4 elements
func uint16As4Bits(dst []int8, u uint16) {
const mask = 0xF8 // 11111000
dst[0] = int8((0-uint8(u>>15&1))&mask | uint8(u>>12&0x07))
dst[1] = int8((0-uint8(u>>11&1))&mask | uint8(u>>8&0x07))
dst[2] = int8((0-uint8(u>>7&1))&mask | uint8(u>>4&0x07))
dst[3] = int8((0-uint8(u>>3&1))&mask | uint8(u>>0&0x07))
}
// write 2 elements
func uint16As8Bits(dst []int8, u uint16) {
dst[0] = int8(u >> 8)
dst[1] = int8(u)
}
// ParseUint16s interprets data as a (big endian) uint16 slice.
// It returns an error if [data] is not long enough for the given [count].
func ParseUint16s(src []byte, count int) ([]uint16, error) {
if L := len(src); L < 2*count {
return nil, fmt.Errorf("EOF: expected length: %d, got %d", 2*count, L)
}
out := make([]uint16, count)
for i := range out {
out[i] = binary.BigEndian.Uint16(src[2*i:])
}
return out, nil
}
@@ -0,0 +1,506 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import (
"encoding/binary"
"fmt"
)
// Code generated by binarygen from ot_layout_src.go. DO NOT EDIT
func (item *ConditionFormat1) mustParse(src []byte) {
_ = src[7] // early bound checking
item.format = binary.BigEndian.Uint16(src[0:])
item.AxisIndex = binary.BigEndian.Uint16(src[2:])
item.FilterRangeMinValue = Coord(binary.BigEndian.Uint16(src[4:]))
item.FilterRangeMaxValue = Coord(binary.BigEndian.Uint16(src[6:]))
}
func ParseConditionFormat1(src []byte) (ConditionFormat1, int, error) {
var item ConditionFormat1
n := 0
if L := len(src); L < 8 {
return item, 0, fmt.Errorf("reading ConditionFormat1: "+"EOF: expected length: 8, got %d", L)
}
item.mustParse(src)
n += 8
return item, n, nil
}
func ParseConditionSet(src []byte) (ConditionSet, int, error) {
var item ConditionSet
n := 0
if L := len(src); L < 2 {
return item, 0, fmt.Errorf("reading ConditionSet: "+"EOF: expected length: 2, got %d", L)
}
arrayLengthConditions := int(binary.BigEndian.Uint16(src[0:]))
n += 2
{
if L := len(src); L < 2+arrayLengthConditions*4 {
return item, 0, fmt.Errorf("reading ConditionSet: "+"EOF: expected length: %d, got %d", 2+arrayLengthConditions*4, L)
}
item.Conditions = make([]ConditionFormat1, arrayLengthConditions) // allocation guarded by the previous check
for i := range item.Conditions {
offset := int(binary.BigEndian.Uint32(src[2+i*4:]))
// ignore null offsets
if offset == 0 {
continue
}
if L := len(src); L < offset {
return item, 0, fmt.Errorf("reading ConditionSet: "+"EOF: expected length: %d, got %d", offset, L)
}
var err error
item.Conditions[i], _, err = ParseConditionFormat1(src[offset:])
if err != nil {
return item, 0, fmt.Errorf("reading ConditionSet: %s", err)
}
}
n += arrayLengthConditions * 4
}
return item, n, nil
}
func ParseFeature(src []byte) (Feature, int, error) {
var item Feature
n := 0
if L := len(src); L < 4 {
return item, 0, fmt.Errorf("reading Feature: "+"EOF: expected length: 4, got %d", L)
}
_ = src[3] // early bound checking
item.featureParamsOffset = binary.BigEndian.Uint16(src[0:])
arrayLengthLookupListIndices := int(binary.BigEndian.Uint16(src[2:]))
n += 4
{
if L := len(src); L < 4+arrayLengthLookupListIndices*2 {
return item, 0, fmt.Errorf("reading Feature: "+"EOF: expected length: %d, got %d", 4+arrayLengthLookupListIndices*2, L)
}
item.LookupListIndices = make([]uint16, arrayLengthLookupListIndices) // allocation guarded by the previous check
for i := range item.LookupListIndices {
item.LookupListIndices[i] = binary.BigEndian.Uint16(src[4+i*2:])
}
n += arrayLengthLookupListIndices * 2
}
return item, n, nil
}
func ParseFeatureList(src []byte) (FeatureList, int, error) {
var item FeatureList
n := 0
if L := len(src); L < 2 {
return item, 0, fmt.Errorf("reading FeatureList: "+"EOF: expected length: 2, got %d", L)
}
arrayLengthRecords := int(binary.BigEndian.Uint16(src[0:]))
n += 2
{
if L := len(src); L < 2+arrayLengthRecords*6 {
return item, 0, fmt.Errorf("reading FeatureList: "+"EOF: expected length: %d, got %d", 2+arrayLengthRecords*6, L)
}
item.Records = make([]TagOffsetRecord, arrayLengthRecords) // allocation guarded by the previous check
for i := range item.Records {
item.Records[i].mustParse(src[2+i*6:])
}
n += arrayLengthRecords * 6
}
{
err := item.parseFeatures(src[:])
if err != nil {
return item, 0, fmt.Errorf("reading FeatureList: %s", err)
}
}
return item, n, nil
}
func ParseFeatureTableSubstitution(src []byte) (FeatureTableSubstitution, int, error) {
var item FeatureTableSubstitution
n := 0
if L := len(src); L < 6 {
return item, 0, fmt.Errorf("reading FeatureTableSubstitution: "+"EOF: expected length: 6, got %d", L)
}
_ = src[5] // early bound checking
item.majorVersion = binary.BigEndian.Uint16(src[0:])
item.minorVersion = binary.BigEndian.Uint16(src[2:])
arrayLengthSubstitutions := int(binary.BigEndian.Uint16(src[4:]))
n += 6
{
offset := 6
for i := 0; i < arrayLengthSubstitutions; i++ {
elem, read, err := ParseFeatureTableSubstitutionRecord(src[offset:], src)
if err != nil {
return item, 0, fmt.Errorf("reading FeatureTableSubstitution: %s", err)
}
item.Substitutions = append(item.Substitutions, elem)
offset += read
}
n = offset
}
return item, n, nil
}
func ParseFeatureTableSubstitutionRecord(src []byte, parentSrc []byte) (FeatureTableSubstitutionRecord, int, error) {
var item FeatureTableSubstitutionRecord
n := 0
if L := len(src); L < 6 {
return item, 0, fmt.Errorf("reading FeatureTableSubstitutionRecord: "+"EOF: expected length: 6, got %d", L)
}
_ = src[5] // early bound checking
item.FeatureIndex = binary.BigEndian.Uint16(src[0:])
offsetAlternateFeature := int(binary.BigEndian.Uint32(src[2:]))
n += 6
{
if offsetAlternateFeature != 0 { // ignore null offset
if L := len(parentSrc); L < offsetAlternateFeature {
return item, 0, fmt.Errorf("reading FeatureTableSubstitutionRecord: "+"EOF: expected length: %d, got %d", offsetAlternateFeature, L)
}
var err error
item.AlternateFeature, _, err = ParseFeature(parentSrc[offsetAlternateFeature:])
if err != nil {
return item, 0, fmt.Errorf("reading FeatureTableSubstitutionRecord: %s", err)
}
}
}
return item, n, nil
}
func ParseFeatureVariation(src []byte) (FeatureVariation, int, error) {
var item FeatureVariation
n := 0
if L := len(src); L < 8 {
return item, 0, fmt.Errorf("reading FeatureVariation: "+"EOF: expected length: 8, got %d", L)
}
_ = src[7] // early bound checking
item.majorVersion = binary.BigEndian.Uint16(src[0:])
item.minorVersion = binary.BigEndian.Uint16(src[2:])
arrayLengthFeatureVariationRecords := int(binary.BigEndian.Uint32(src[4:]))
n += 8
{
offset := 8
for i := 0; i < arrayLengthFeatureVariationRecords; i++ {
elem, read, err := ParseFeatureVariationRecord(src[offset:], src)
if err != nil {
return item, 0, fmt.Errorf("reading FeatureVariation: %s", err)
}
item.FeatureVariationRecords = append(item.FeatureVariationRecords, elem)
offset += read
}
n = offset
}
return item, n, nil
}
func ParseFeatureVariationRecord(src []byte, parentSrc []byte) (FeatureVariationRecord, int, error) {
var item FeatureVariationRecord
n := 0
if L := len(src); L < 8 {
return item, 0, fmt.Errorf("reading FeatureVariationRecord: "+"EOF: expected length: 8, got %d", L)
}
_ = src[7] // early bound checking
offsetConditionSet := int(binary.BigEndian.Uint32(src[0:]))
offsetSubstitutions := int(binary.BigEndian.Uint32(src[4:]))
n += 8
{
if offsetConditionSet != 0 { // ignore null offset
if L := len(parentSrc); L < offsetConditionSet {
return item, 0, fmt.Errorf("reading FeatureVariationRecord: "+"EOF: expected length: %d, got %d", offsetConditionSet, L)
}
var err error
item.ConditionSet, _, err = ParseConditionSet(parentSrc[offsetConditionSet:])
if err != nil {
return item, 0, fmt.Errorf("reading FeatureVariationRecord: %s", err)
}
}
}
{
if offsetSubstitutions != 0 { // ignore null offset
if L := len(parentSrc); L < offsetSubstitutions {
return item, 0, fmt.Errorf("reading FeatureVariationRecord: "+"EOF: expected length: %d, got %d", offsetSubstitutions, L)
}
var err error
item.Substitutions, _, err = ParseFeatureTableSubstitution(parentSrc[offsetSubstitutions:])
if err != nil {
return item, 0, fmt.Errorf("reading FeatureVariationRecord: %s", err)
}
}
}
return item, n, nil
}
func ParseLangSys(src []byte) (LangSys, int, error) {
var item LangSys
n := 0
if L := len(src); L < 6 {
return item, 0, fmt.Errorf("reading LangSys: "+"EOF: expected length: 6, got %d", L)
}
_ = src[5] // early bound checking
item.lookupOrderOffset = binary.BigEndian.Uint16(src[0:])
item.RequiredFeatureIndex = binary.BigEndian.Uint16(src[2:])
arrayLengthFeatureIndices := int(binary.BigEndian.Uint16(src[4:]))
n += 6
{
if L := len(src); L < 6+arrayLengthFeatureIndices*2 {
return item, 0, fmt.Errorf("reading LangSys: "+"EOF: expected length: %d, got %d", 6+arrayLengthFeatureIndices*2, L)
}
item.FeatureIndices = make([]uint16, arrayLengthFeatureIndices) // allocation guarded by the previous check
for i := range item.FeatureIndices {
item.FeatureIndices[i] = binary.BigEndian.Uint16(src[6+i*2:])
}
n += arrayLengthFeatureIndices * 2
}
return item, n, nil
}
func ParseLayout(src []byte) (Layout, int, error) {
var item Layout
n := 0
if L := len(src); L < 10 {
return item, 0, fmt.Errorf("reading Layout: "+"EOF: expected length: 10, got %d", L)
}
_ = src[9] // early bound checking
item.majorVersion = binary.BigEndian.Uint16(src[0:])
item.minorVersion = binary.BigEndian.Uint16(src[2:])
offsetScriptList := int(binary.BigEndian.Uint16(src[4:]))
offsetFeatureList := int(binary.BigEndian.Uint16(src[6:]))
offsetLookupList := int(binary.BigEndian.Uint16(src[8:]))
n += 10
{
if offsetScriptList != 0 { // ignore null offset
if L := len(src); L < offsetScriptList {
return item, 0, fmt.Errorf("reading Layout: "+"EOF: expected length: %d, got %d", offsetScriptList, L)
}
var err error
item.ScriptList, _, err = ParseScriptList(src[offsetScriptList:])
if err != nil {
return item, 0, fmt.Errorf("reading Layout: %s", err)
}
}
}
{
if offsetFeatureList != 0 { // ignore null offset
if L := len(src); L < offsetFeatureList {
return item, 0, fmt.Errorf("reading Layout: "+"EOF: expected length: %d, got %d", offsetFeatureList, L)
}
var err error
item.FeatureList, _, err = ParseFeatureList(src[offsetFeatureList:])
if err != nil {
return item, 0, fmt.Errorf("reading Layout: %s", err)
}
}
}
{
if offsetLookupList != 0 { // ignore null offset
if L := len(src); L < offsetLookupList {
return item, 0, fmt.Errorf("reading Layout: "+"EOF: expected length: %d, got %d", offsetLookupList, L)
}
var err error
item.LookupList, _, err = parseLookupList(src[offsetLookupList:])
if err != nil {
return item, 0, fmt.Errorf("reading Layout: %s", err)
}
}
}
{
read, err := item.parseFeatureVariations(src[:])
if err != nil {
return item, 0, fmt.Errorf("reading Layout: %s", err)
}
n = read
}
return item, n, nil
}
func ParseLookup(src []byte) (Lookup, int, error) {
var item Lookup
n := 0
if L := len(src); L < 6 {
return item, 0, fmt.Errorf("reading Lookup: "+"EOF: expected length: 6, got %d", L)
}
_ = src[5] // early bound checking
item.lookupType = binary.BigEndian.Uint16(src[0:])
item.LookupFlag = binary.BigEndian.Uint16(src[2:])
arrayLengthSubtableOffsets := int(binary.BigEndian.Uint16(src[4:]))
n += 6
{
if L := len(src); L < 6+arrayLengthSubtableOffsets*2 {
return item, 0, fmt.Errorf("reading Lookup: "+"EOF: expected length: %d, got %d", 6+arrayLengthSubtableOffsets*2, L)
}
item.subtableOffsets = make([]Offset16, arrayLengthSubtableOffsets) // allocation guarded by the previous check
for i := range item.subtableOffsets {
item.subtableOffsets[i] = Offset16(binary.BigEndian.Uint16(src[6+i*2:]))
}
n += arrayLengthSubtableOffsets * 2
}
if L := len(src); L < n+2 {
return item, 0, fmt.Errorf("reading Lookup: "+"EOF: expected length: n + 2, got %d", L)
}
item.MarkFilteringSet = binary.BigEndian.Uint16(src[n:])
n += 2
{
item.rawData = src[0:]
n = len(src)
}
return item, n, nil
}
func ParseScript(src []byte) (Script, int, error) {
var item Script
n := 0
if L := len(src); L < 4 {
return item, 0, fmt.Errorf("reading Script: "+"EOF: expected length: 4, got %d", L)
}
_ = src[3] // early bound checking
offsetDefaultLangSys := int(binary.BigEndian.Uint16(src[0:]))
arrayLengthLangSysRecords := int(binary.BigEndian.Uint16(src[2:]))
n += 4
{
if offsetDefaultLangSys != 0 { // ignore null offset
if L := len(src); L < offsetDefaultLangSys {
return item, 0, fmt.Errorf("reading Script: "+"EOF: expected length: %d, got %d", offsetDefaultLangSys, L)
}
var tmpDefaultLangSys LangSys
var err error
tmpDefaultLangSys, _, err = ParseLangSys(src[offsetDefaultLangSys:])
if err != nil {
return item, 0, fmt.Errorf("reading Script: %s", err)
}
item.DefaultLangSys = &tmpDefaultLangSys
}
}
{
if L := len(src); L < 4+arrayLengthLangSysRecords*6 {
return item, 0, fmt.Errorf("reading Script: "+"EOF: expected length: %d, got %d", 4+arrayLengthLangSysRecords*6, L)
}
item.LangSysRecords = make([]TagOffsetRecord, arrayLengthLangSysRecords) // allocation guarded by the previous check
for i := range item.LangSysRecords {
item.LangSysRecords[i].mustParse(src[4+i*6:])
}
n += arrayLengthLangSysRecords * 6
}
{
err := item.parseLangSys(src[:])
if err != nil {
return item, 0, fmt.Errorf("reading Script: %s", err)
}
}
return item, n, nil
}
func ParseScriptList(src []byte) (ScriptList, int, error) {
var item ScriptList
n := 0
if L := len(src); L < 2 {
return item, 0, fmt.Errorf("reading ScriptList: "+"EOF: expected length: 2, got %d", L)
}
arrayLengthRecords := int(binary.BigEndian.Uint16(src[0:]))
n += 2
{
if L := len(src); L < 2+arrayLengthRecords*6 {
return item, 0, fmt.Errorf("reading ScriptList: "+"EOF: expected length: %d, got %d", 2+arrayLengthRecords*6, L)
}
item.Records = make([]TagOffsetRecord, arrayLengthRecords) // allocation guarded by the previous check
for i := range item.Records {
item.Records[i].mustParse(src[2+i*6:])
}
n += arrayLengthRecords * 6
}
{
err := item.parseScripts(src[:])
if err != nil {
return item, 0, fmt.Errorf("reading ScriptList: %s", err)
}
}
return item, n, nil
}
func (item *TagOffsetRecord) mustParse(src []byte) {
_ = src[5] // early bound checking
item.Tag = Tag(binary.BigEndian.Uint32(src[0:]))
item.Offset = binary.BigEndian.Uint16(src[4:])
}
func parseLookupList(src []byte) (lookupList, int, error) {
var item lookupList
n := 0
if L := len(src); L < 2 {
return item, 0, fmt.Errorf("reading lookupList: "+"EOF: expected length: 2, got %d", L)
}
arrayLengthLookups := int(binary.BigEndian.Uint16(src[0:]))
n += 2
{
if L := len(src); L < 2+arrayLengthLookups*2 {
return item, 0, fmt.Errorf("reading lookupList: "+"EOF: expected length: %d, got %d", 2+arrayLengthLookups*2, L)
}
item.Lookups = make([]Lookup, arrayLengthLookups) // allocation guarded by the previous check
for i := range item.Lookups {
offset := int(binary.BigEndian.Uint16(src[2+i*2:]))
// ignore null offsets
if offset == 0 {
continue
}
if L := len(src); L < offset {
return item, 0, fmt.Errorf("reading lookupList: "+"EOF: expected length: %d, got %d", offset, L)
}
var err error
item.Lookups[i], _, err = ParseLookup(src[offset:])
if err != nil {
return item, 0, fmt.Errorf("reading lookupList: %s", err)
}
}
n += arrayLengthLookups * 2
}
return item, n, nil
}
@@ -0,0 +1,172 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import (
"encoding/binary"
"fmt"
)
// Layout represents the common layout table used by GPOS and GSUB.
// The Features field contains all the features for this layout. However,
// the script and language determines which feature is used.
//
// See https://learn.microsoft.com/typography/opentype/spec/chapter2#organization
// See https://learn.microsoft.com/typography/opentype/spec/gpos
// See https://www.microsoft.com/typography/otspec/GSUB.htm
type Layout struct {
majorVersion uint16 // Major version of the GPOS table, = 1
minorVersion uint16 // Minor version of the GPOS table, = 0 or 1
ScriptList ScriptList `offsetSize:"Offset16"` // Offset to ScriptList table, from beginning of GPOS table
FeatureList FeatureList `offsetSize:"Offset16"` // Offset to FeatureList table, from beginning of GPOS table
LookupList lookupList `offsetSize:"Offset16"` // Offset to LookupList table, from beginning of GPOS table
FeatureVariations *FeatureVariation `isOpaque:""` // Offset to FeatureVariations table, from beginning of GPOS table (may be NULL)
}
func (lt *Layout) parseFeatureVariations(src []byte) (int, error) {
const layoutHeaderSize = 2 + 2 + 2 + 2 + 2
if lt.minorVersion != 1 {
return 0, nil
}
if L := len(src); L < layoutHeaderSize+4 {
return 0, fmt.Errorf("reading Layout: EOF: expected length: 4, got %d", L)
}
offset := binary.BigEndian.Uint32(src[layoutHeaderSize:])
if offset == 0 {
return 4, nil
}
if L := len(src); L < int(offset) {
return 0, fmt.Errorf("reading Layout: EOF: expected length: %d, got %d", offset, L)
}
fv, _, err := ParseFeatureVariation(src[offset:])
if err != nil {
return 0, err
}
lt.FeatureVariations = &fv
return 4, nil
}
type TagOffsetRecord struct {
Tag Tag // 4-byte script tag identifier
Offset uint16 // Offset to object from beginning of list
}
type ScriptList struct {
Records []TagOffsetRecord `arrayCount:"FirstUint16"` // Array of ScriptRecords, listed alphabetically by script tag
Scripts []Script `isOpaque:""`
}
func (sl *ScriptList) parseScripts(src []byte) error {
sl.Scripts = make([]Script, len(sl.Records))
for i, rec := range sl.Records {
var err error
if L := len(src); L < int(rec.Offset) {
return fmt.Errorf("EOF: expected length: %d, got %d", rec.Offset, L)
}
sl.Scripts[i], _, err = ParseScript(src[rec.Offset:])
if err != nil {
return err
}
}
return nil
}
type Script struct {
DefaultLangSys *LangSys `offsetSize:"Offset16"` // Offset to default LangSys table, from beginning of Script table — may be NULL
LangSysRecords []TagOffsetRecord `arrayCount:"FirstUint16"` // [langSysCount] Array of LangSysRecords, listed alphabetically by LangSys tag
LangSys []LangSys `isOpaque:""` // same length as langSysRecords
}
func (sc *Script) parseLangSys(src []byte) error {
sc.LangSys = make([]LangSys, len(sc.LangSysRecords))
for i, rec := range sc.LangSysRecords {
var err error
if L := len(src); L < int(rec.Offset) {
return fmt.Errorf("EOF: expected length: %d, got %d", rec.Offset, L)
}
sc.LangSys[i], _, err = ParseLangSys(src[rec.Offset:])
if err != nil {
return err
}
}
return nil
}
type LangSys struct {
lookupOrderOffset uint16 // = NULL (reserved for an offset to a reordering table)
RequiredFeatureIndex uint16 // Index of a feature required for this language system; if no required features = 0xFFFF
FeatureIndices []uint16 `arrayCount:"FirstUint16"` // [featureIndexCount] Array of indices into the FeatureList, in arbitrary order
}
type FeatureList struct {
Records []TagOffsetRecord `arrayCount:"FirstUint16"` // Array of FeatureRecords — zero-based (first feature has FeatureIndex = 0), listed alphabetically by feature tag
Features []Feature `isOpaque:""`
}
func (fl *FeatureList) parseFeatures(src []byte) error {
fl.Features = make([]Feature, len(fl.Records))
for i, rec := range fl.Records {
var err error
if L := len(src); L < int(rec.Offset) {
return fmt.Errorf("EOF: expected length: %d, got %d", rec.Offset, L)
}
fl.Features[i], _, err = ParseFeature(src[rec.Offset:])
if err != nil {
return err
}
}
return nil
}
type Feature struct {
featureParamsOffset uint16 // Offset from start of Feature table to FeatureParams table, if defined for the feature and present, else NULL
LookupListIndices []uint16 `arrayCount:"FirstUint16"` // [lookupIndexCount] Array of indices into the LookupList — zero-based (first lookup is LookupListIndex = 0)
}
type lookupList struct {
Lookups []Lookup `arrayCount:"FirstUint16" offsetsArray:"Offset16"` // Array of offsets to Lookup tables, from beginning of LookupList — zero based (first lookup is Lookup index = 0)
}
// Lookup is the common format for GSUB and GPOS lookups
type Lookup struct {
lookupType uint16 // Different enumerations for GSUB and GPOS
LookupFlag uint16 // Lookup qualifiers
subtableOffsets []Offset16 `arrayCount:"FirstUint16"` // [subTableCount] Array of offsets to lookup subtables, from beginning of Lookup table
MarkFilteringSet uint16 // Index (base 0) into GDEF mark glyph sets structure. This field is only present if the USE_MARK_FILTERING_SET lookup flag is set.
rawData []byte `subsliceStart:"AtStart" arrayCount:"ToEnd"`
}
type FeatureVariation struct {
majorVersion uint16 // Major version of the FeatureVariations table — set to 1.
minorVersion uint16 // Minor version of the FeatureVariations table — set to 0.
FeatureVariationRecords []FeatureVariationRecord `arrayCount:"FirstUint32"` //[featureVariationRecordCount] Array of feature variation records.
}
type FeatureVariationRecord struct {
ConditionSet ConditionSet `offsetSize:"Offset32" offsetRelativeTo:"Parent"` // Offset to a condition set table, from beginning of FeatureVariations table.
Substitutions FeatureTableSubstitution `offsetSize:"Offset32" offsetRelativeTo:"Parent"` // Offset to a feature table substitution table, from beginning of the FeatureVariations table.
}
type ConditionSet struct {
// uint16 conditionCount Number of Conditions for this condition set.
Conditions []ConditionFormat1 `arrayCount:"FirstUint16" offsetsArray:"Offset32"` // [conditionCount] Array of offsets to condition tables, from beginning of the ConditionSet table.
}
type ConditionFormat1 struct {
format uint16 // Format, = 1
AxisIndex uint16 // Index (zero-based) for the variation axis within the 'fvar' table.
FilterRangeMinValue Coord // Minimum value of the font variation instances that satisfy this condition.
FilterRangeMaxValue Coord // Maximum value of the font variation instances that satisfy this condition.
}
type FeatureTableSubstitution struct {
majorVersion uint16 // Major version of the feature table substitution table — set to 1
minorVersion uint16 // Minor version of the feature table substitution table — set to 0.
Substitutions []FeatureTableSubstitutionRecord `arrayCount:"FirstUint16"` // [substitutionCount] Array of feature table substitution records.
}
type FeatureTableSubstitutionRecord struct {
FeatureIndex uint16 // The feature table index to match.
AlternateFeature Feature `offsetSize:"Offset32" offsetRelativeTo:"Parent"` // Offset to an alternate feature table, from start of the FeatureTableSubstitution table.
}
@@ -0,0 +1,283 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import (
"sort"
)
func (c Coverage1) Index(gi GlyphID) (int, bool) {
num := len(c.Glyphs)
idx := sort.Search(num, func(i int) bool { return gi <= c.Glyphs[i] })
if idx < num && c.Glyphs[idx] == gi {
return idx, true
}
return 0, false
}
func (cl Coverage1) Len() int { return len(cl.Glyphs) }
func (c Coverage2) Index(gi GlyphID) (int, bool) {
num := len(c.Ranges)
if num == 0 {
return 0, false
}
idx := sort.Search(num, func(i int) bool { return gi <= c.Ranges[i].StartGlyphID })
// idx either points to a matching start, or to the next range (or idx==num)
// e.g. with the range example from above: 130 points to 130-135 range, 133 points to 137-137 range
// check if gi is the start of a range, but only if sort.Search returned a valid result
if idx < num {
if rang := c.Ranges[idx]; gi == rang.StartGlyphID {
return int(rang.StartCoverageIndex), true
}
}
// check if gi is in previous range
if idx > 0 {
idx--
if rang := c.Ranges[idx]; gi >= rang.StartGlyphID && gi <= rang.EndGlyphID {
return int(rang.StartCoverageIndex) + int(gi-rang.StartGlyphID), true
}
}
return 0, false
}
func (cr Coverage2) Len() int {
size := 0
for _, r := range cr.Ranges {
size += int(r.EndGlyphID - r.StartGlyphID + 1)
}
return size
}
func (cl ClassDef1) Class(gi GlyphID) (uint16, bool) {
if gi < cl.StartGlyphID || gi >= cl.StartGlyphID+GlyphID(len(cl.ClassValueArray)) {
return 0, false
}
return cl.ClassValueArray[gi-cl.StartGlyphID], true
}
func (cl ClassDef1) Extent() int {
max := uint16(0)
for _, cid := range cl.ClassValueArray {
if cid >= max {
max = cid
}
}
return int(max) + 1
}
func (cl ClassDef2) Class(g GlyphID) (uint16, bool) {
// 'adapted' from golang/x/image/font/sfnt
c := cl.ClassRangeRecords
num := len(c)
if num == 0 {
return 0, false
}
// classRange is an array of startGlyphID, endGlyphID and target class ID.
// Ranges are non-overlapping.
// E.g. 130, 135, 1 137, 137, 5 etc
idx := sort.Search(num, func(i int) bool { return g <= c[i].StartGlyphID })
// idx either points to a matching start, or to the next range (or idx==num)
// e.g. with the range example from above: 130 points to 130-135 range, 133 points to 137-137 range
// check if gi is the start of a range, but only if sort.Search returned a valid result
if idx < num {
if class := c[idx]; g == c[idx].StartGlyphID {
return class.Class, true
}
}
// check if gi is in previous range
if idx > 0 {
idx--
if class := c[idx]; g >= class.StartGlyphID && g <= class.EndGlyphID {
return class.Class, true
}
}
return 0, false
}
func (cl ClassDef2) Extent() int {
max := uint16(0)
for _, r := range cl.ClassRangeRecords {
if r.Class >= max {
max = r.Class
}
}
return int(max) + 1
}
// ------------------------------------ layout getters ------------------------------------
// FindLanguage looks for [language] and return its index into the [LangSys] slice,
// or -1 if the tag is not found.
func (sc Script) FindLanguage(language Tag) int {
// LangSys is sorted: binary search
low, high := 0, len(sc.LangSysRecords)
for low < high {
mid := low + (high-low)/2 // avoid overflow when computing mid
p := sc.LangSysRecords[mid].Tag
if language < p {
high = mid
} else if language > p {
low = mid + 1
} else {
return mid
}
}
return -1
}
// GetLangSys return the language at [index]. It [index] is out of range (for example with 0xFFFF),
// it returns [DefaultLangSys] (which may be empty)
func (sc Script) GetLangSys(index uint16) LangSys {
if int(index) >= len(sc.LangSys) {
if sc.DefaultLangSys != nil {
return *sc.DefaultLangSys
}
return LangSys{RequiredFeatureIndex: 0xFFFF}
}
return sc.LangSys[index]
}
// --------------------------------------- gsub ---------------------------------------
func (d SingleSubstData1) Cov() Coverage { return d.Coverage }
func (d SingleSubstData2) Cov() Coverage { return d.Coverage }
func (cs ContextualSubs1) Cov() Coverage { return cs.coverage }
func (cs ContextualSubs2) Cov() Coverage { return cs.coverage }
func (cs ContextualSubs3) Cov() Coverage {
if len(cs.Coverages) == 0 { // return an empty, valid Coverage
return Coverage1{}
}
return cs.Coverages[0]
}
func (cc ChainedContextualSubs1) Cov() Coverage { return cc.coverage }
func (cc ChainedContextualSubs2) Cov() Coverage { return cc.coverage }
func (cc ChainedContextualSubs3) Cov() Coverage {
if len(cc.InputCoverages) == 0 { // return an empty, valid Coverage
return Coverage1{}
}
return cc.InputCoverages[0]
}
func (lk SingleSubs) Cov() Coverage { return lk.Data.Cov() }
func (lk MultipleSubs) Cov() Coverage { return lk.Coverage }
func (lk AlternateSubs) Cov() Coverage { return lk.Coverage }
func (lk LigatureSubs) Cov() Coverage { return lk.Coverage }
func (lk ContextualSubs) Cov() Coverage { return lk.Data.Cov() }
func (lk ChainedContextualSubs) Cov() Coverage { return lk.Data.Cov() }
func (lk ExtensionSubs) Cov() Coverage { return nil } // not used anyway
func (lk ReverseChainSingleSubs) Cov() Coverage { return lk.coverage }
// --------------------------------------- gpos ---------------------------------------
func (d SinglePosData1) Cov() Coverage { return d.coverage }
func (d SinglePosData2) Cov() Coverage { return d.coverage }
func (d PairPosData1) Cov() Coverage { return d.coverage }
func (d PairPosData2) Cov() Coverage { return d.coverage }
func (cs ContextualPos1) Cov() Coverage { return cs.coverage }
func (cs ContextualPos2) Cov() Coverage { return cs.coverage }
func (cs ContextualPos3) Cov() Coverage {
if len(cs.Coverages) == 0 { // return an empty, valid Coverage
return Coverage1{}
}
return cs.Coverages[0]
}
func (cc ChainedContextualPos1) Cov() Coverage { return cc.coverage }
func (cc ChainedContextualPos2) Cov() Coverage { return cc.coverage }
func (cc ChainedContextualPos3) Cov() Coverage {
if len(cc.InputCoverages) == 0 { // return an empty, valid Coverage
return Coverage1{}
}
return cc.InputCoverages[0]
}
func (lk SinglePos) Cov() Coverage { return lk.Data.Cov() }
func (lk PairPos) Cov() Coverage { return lk.Data.Cov() }
func (lk CursivePos) Cov() Coverage { return lk.coverage }
func (lk MarkBasePos) Cov() Coverage { return lk.markCoverage }
func (lk MarkLigPos) Cov() Coverage { return lk.MarkCoverage }
func (lk MarkMarkPos) Cov() Coverage { return lk.Mark1Coverage }
func (lk ContextualPos) Cov() Coverage { return lk.Data.Cov() }
func (lk ChainedContextualPos) Cov() Coverage { return lk.Data.Cov() }
func (lk ExtensionPos) Cov() Coverage { return nil } // not used anyway
// FindGlyph performs a binary search in the list, returning the record for `secondGlyph`,
// or `nil` if not found.
func (ps PairSet) FindGlyph(secondGlyph GlyphID) (PairValueRecord, bool) {
low, high := 0, int(ps.pairValueCount)
for low < high {
mid := low + (high-low)/2 // avoid overflow when computing mid
rec, err := ps.data.get(mid)
if err != nil { // argh...
return PairValueRecord{}, false
}
p := rec.SecondGlyph
if secondGlyph < p {
high = mid
} else if secondGlyph > p {
low = mid + 1
} else {
return rec, true
}
}
return PairValueRecord{}, false
}
// GetDelta returns the hint for the given `ppem`, scaled by `scale`.
// It returns 0 for out of range `ppem` values.
func (dev DeviceHinting) GetDelta(ppem uint16, scale int32) int32 {
if ppem == 0 {
return 0
}
if ppem < dev.StartSize || ppem > dev.EndSize {
return 0
}
pixels := dev.Values[ppem-dev.StartSize]
return int32(pixels) * (scale / int32(ppem))
}
// -------------------------------------- gdef --------------------------------------
// GlyphProps is a 16-bit integer where the lower 8-bit have bits representing
// glyph class, and high 8-bit the mark attachment type (if any).
type GlyphProps = uint16
const (
GPBaseGlyph GlyphProps = 1 << (iota + 1)
GPLigature
GPMark
)
// GlyphProps return a summary of the glyph properties.
func (gd *GDEF) GlyphProps(glyph GlyphID) GlyphProps {
klass, _ := gd.GlyphClassDef.Class(glyph)
switch klass {
case 1:
return GPBaseGlyph
case 2:
return GPLigature
case 3:
var klass uint16 // it is actually a byte
if gd.MarkAttachClass != nil {
klass, _ = gd.MarkAttachClass.Class(glyph)
}
return GlyphProps(klass)<<8 | GPMark
default:
return 0
}
}
@@ -0,0 +1,94 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import (
"encoding/binary"
"fmt"
)
// Code generated by binarygen from post_src.go. DO NOT EDIT
func ParsePost(src []byte) (Post, int, error) {
var item Post
n := 0
if L := len(src); L < 32 {
return item, 0, fmt.Errorf("reading Post: "+"EOF: expected length: 32, got %d", L)
}
_ = src[31] // early bound checking
item.version = postVersion(binary.BigEndian.Uint32(src[0:]))
item.italicAngle = binary.BigEndian.Uint32(src[4:])
item.UnderlinePosition = int16(binary.BigEndian.Uint16(src[8:]))
item.UnderlineThickness = int16(binary.BigEndian.Uint16(src[10:]))
item.IsFixedPitch = binary.BigEndian.Uint32(src[12:])
item.memoryUsage[0] = binary.BigEndian.Uint32(src[16:])
item.memoryUsage[1] = binary.BigEndian.Uint32(src[20:])
item.memoryUsage[2] = binary.BigEndian.Uint32(src[24:])
item.memoryUsage[3] = binary.BigEndian.Uint32(src[28:])
n += 32
{
var (
read int
err error
)
switch item.version {
case postVersion10:
item.Names, read, err = ParsePostNames10(src[32:])
case postVersion20:
item.Names, read, err = ParsePostNames20(src[32:])
case postVersion30:
item.Names, read, err = ParsePostNames30(src[32:])
default:
err = fmt.Errorf("unsupported PostNamesVersion %d", item.version)
}
if err != nil {
return item, 0, fmt.Errorf("reading Post: %s", err)
}
n += read
}
return item, n, nil
}
func ParsePostNames10([]byte) (PostNames10, int, error) {
var item PostNames10
n := 0
return item, n, nil
}
func ParsePostNames20(src []byte) (PostNames20, int, error) {
var item PostNames20
n := 0
if L := len(src); L < 2 {
return item, 0, fmt.Errorf("reading PostNames20: "+"EOF: expected length: 2, got %d", L)
}
arrayLengthGlyphNameIndexes := int(binary.BigEndian.Uint16(src[0:]))
n += 2
{
if L := len(src); L < 2+arrayLengthGlyphNameIndexes*2 {
return item, 0, fmt.Errorf("reading PostNames20: "+"EOF: expected length: %d, got %d", 2+arrayLengthGlyphNameIndexes*2, L)
}
item.GlyphNameIndexes = make([]uint16, arrayLengthGlyphNameIndexes) // allocation guarded by the previous check
for i := range item.GlyphNameIndexes {
item.GlyphNameIndexes[i] = binary.BigEndian.Uint16(src[2+i*2:])
}
n += arrayLengthGlyphNameIndexes * 2
}
{
err := item.parseStrings(src[n:])
if err != nil {
return item, 0, fmt.Errorf("reading PostNames20: %s", err)
}
}
return item, n, nil
}
func ParsePostNames30([]byte) (PostNames30, int, error) {
var item PostNames30
n := 0
return item, n, nil
}
@@ -0,0 +1,65 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import "fmt"
// PostScript table
// See https://learn.microsoft.com/en-us/typography/opentype/spec/post
type Post struct {
version postVersion
italicAngle uint32
// UnderlinePosition is the suggested distance of the top of the
// underline from the baseline (negative values indicate below baseline).
UnderlinePosition int16
// Suggested values for the underline thickness.
UnderlineThickness int16
// IsFixedPitch indicates that the font is not proportionally spaced
// (i.e. monospaced).
IsFixedPitch uint32
memoryUsage [4]uint32
Names PostNames `unionField:"version"`
}
type PostNames interface {
isPostNames()
}
func (PostNames10) isPostNames() {}
func (PostNames20) isPostNames() {}
func (PostNames30) isPostNames() {}
type postVersion uint32
const (
postVersion10 postVersion = 0x00010000
postVersion20 postVersion = 0x00020000
postVersion30 postVersion = 0x00030000
)
type PostNames10 struct{}
type PostNames20 struct {
GlyphNameIndexes []uint16 `arrayCount:"FirstUint16"` // size numGlyph
Strings []string `isOpaque:"" subsliceStart:"AtCurrent"`
}
// see https://learn.microsoft.com/en-us/typography/opentype/spec/post#version-20
func (ps *PostNames20) parseStrings(src []byte) error {
// "Strings are in Pascal string format, meaning that the first byte of
// a given string is a length: the number of characters in that string.
// The length byte is not included; for example, a length byte of 8 indicates
// that the 8 bytes following the length byte comprise the string character data."
for i := 0; i < len(src); {
length := int(src[i]) // read the length
end := i + 1 + length
if L := len(src); L < end {
return fmt.Errorf("invalid Postscript names tables format 20: EOF: expected %d, got %d", end, L)
}
ps.Strings = append(ps.Strings, string(src[i+1:end]))
i = end
}
return nil
}
type PostNames30 PostNames10
@@ -0,0 +1,73 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import "github.com/go-text/typesetting/font/opentype"
//go:generate ../../../../typesetting-utils/generators/binarygen/cmd/generator . _src.go
type GlyphID = uint16
// NameID is the ID for entries in the font table.
type NameID uint16
type Tag = opentype.Tag
// Float1616 is a float32, represented in
// fixed 16.16 format in font files.
type Float1616 = float32
func Float1616FromUint(v uint32) Float1616 {
// value are actually signed integers
return Float1616(int32(v)) / (1 << 16)
}
func Float1616ToUint(f Float1616) uint32 {
return uint32(int32(f * (1 << 16)))
}
// Fixed214 is a number stored as a fixed 2.14 integer
type Fixed214 = Coord
func Float214FromUint(v uint16) float32 {
// value are actually signed integers
return float32(int16(v)) / (1 << 14)
}
// Coord is a real number in [-1;1], stored as a fixed 2.14 integer
type Coord int16
func NewCoord(c float64) Coord {
return Coord(c * (1 << 14))
}
func abs(c Coord) Coord {
if c < 0 {
return -c
}
return c
}
func readUint24(b []byte) uint32 {
_ = b[2] // bounds check hint to compiler; see golang.org/issue/14808
return uint32(b[2]) | uint32(b[1])<<8 | uint32(b[0])<<16
}
// Number of seconds since 12:00 midnight that started January 1st 1904 in GMT/UTC time zone.
type longdatetime = uint64
// PlatformID represents the platform id for entries in the name table.
type PlatformID uint16
// EncodingID represents the platform specific id for entries in the name table.
// The most common values are provided as constants.
type EncodingID uint16
// LanguageID represents the language used by an entry in the name table
type LanguageID uint16
// Offset16 is an offset into the input byte slice
type Offset16 uint16
// Offset32 is an offset into the input byte slice
type Offset32 uint32
@@ -0,0 +1,716 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import (
"encoding/binary"
"fmt"
)
// Code generated by binarygen from xvar_src.go. DO NOT EDIT
func (item *AxisRecord) mustParse(src []byte) {
_ = src[7] // early bound checking
item.Tag = Tag(binary.BigEndian.Uint32(src[0:]))
item.NameID = NameID(binary.BigEndian.Uint16(src[4:]))
item.Ordering = binary.BigEndian.Uint16(src[6:])
}
func (item *AxisValue1) mustParse(src []byte) {
_ = src[11] // early bound checking
item.format = binary.BigEndian.Uint16(src[0:])
item.axisIndex = binary.BigEndian.Uint16(src[2:])
item.flags = binary.BigEndian.Uint16(src[4:])
item.valueNameID = NameID(binary.BigEndian.Uint16(src[6:]))
item.value = Float1616FromUint(binary.BigEndian.Uint32(src[8:]))
}
func (item *AxisValue2) mustParse(src []byte) {
_ = src[19] // early bound checking
item.format = binary.BigEndian.Uint16(src[0:])
item.axisIndex = binary.BigEndian.Uint16(src[2:])
item.flags = binary.BigEndian.Uint16(src[4:])
item.valueNameID = NameID(binary.BigEndian.Uint16(src[6:]))
item.nominalValue = Float1616FromUint(binary.BigEndian.Uint32(src[8:]))
item.rangeMinValue = Float1616FromUint(binary.BigEndian.Uint32(src[12:]))
item.rangeMaxValue = Float1616FromUint(binary.BigEndian.Uint32(src[16:]))
}
func (item *AxisValue3) mustParse(src []byte) {
_ = src[15] // early bound checking
item.format = binary.BigEndian.Uint16(src[0:])
item.axisIndex = binary.BigEndian.Uint16(src[2:])
item.flags = binary.BigEndian.Uint16(src[4:])
item.valueNameID = NameID(binary.BigEndian.Uint16(src[6:]))
item.value = Float1616FromUint(binary.BigEndian.Uint32(src[8:]))
item.linkedValue = Float1616FromUint(binary.BigEndian.Uint32(src[12:]))
}
func (item *AxisValueMap) mustParse(src []byte) {
_ = src[3] // early bound checking
item.FromCoordinate = Coord(binary.BigEndian.Uint16(src[0:]))
item.ToCoordinate = Coord(binary.BigEndian.Uint16(src[2:]))
}
func (item *AxisValueRecord) mustParse(src []byte) {
_ = src[5] // early bound checking
item.axisIndex = binary.BigEndian.Uint16(src[0:])
item.value = Float1616FromUint(binary.BigEndian.Uint32(src[2:]))
}
func ParseAvar(src []byte) (Avar, int, error) {
var item Avar
n := 0
if L := len(src); L < 8 {
return item, 0, fmt.Errorf("reading Avar: "+"EOF: expected length: 8, got %d", L)
}
_ = src[7] // early bound checking
item.majorVersion = binary.BigEndian.Uint16(src[0:])
item.minorVersion = binary.BigEndian.Uint16(src[2:])
item.reserved = binary.BigEndian.Uint16(src[4:])
arrayLengthAxisSegmentMaps := int(binary.BigEndian.Uint16(src[6:]))
n += 8
{
offset := 8
for i := 0; i < arrayLengthAxisSegmentMaps; i++ {
elem, read, err := ParseSegmentMaps(src[offset:])
if err != nil {
return item, 0, fmt.Errorf("reading Avar: %s", err)
}
item.AxisSegmentMaps = append(item.AxisSegmentMaps, elem)
offset += read
}
n = offset
}
return item, n, nil
}
func ParseAxisValue(src []byte) (AxisValue, int, error) {
var item AxisValue
if L := len(src); L < 2 {
return item, 0, fmt.Errorf("reading AxisValue: "+"EOF: expected length: 2, got %d", L)
}
format := uint16(binary.BigEndian.Uint16(src[0:]))
var (
read int
err error
)
switch format {
case 1:
item, read, err = ParseAxisValue1(src[0:])
case 2:
item, read, err = ParseAxisValue2(src[0:])
case 3:
item, read, err = ParseAxisValue3(src[0:])
case 4:
item, read, err = ParseAxisValue4(src[0:])
default:
err = fmt.Errorf("unsupported AxisValue format %d", format)
}
if err != nil {
return item, 0, fmt.Errorf("reading AxisValue: %s", err)
}
return item, read, nil
}
func ParseAxisValue1(src []byte) (AxisValue1, int, error) {
var item AxisValue1
n := 0
if L := len(src); L < 12 {
return item, 0, fmt.Errorf("reading AxisValue1: "+"EOF: expected length: 12, got %d", L)
}
item.mustParse(src)
n += 12
return item, n, nil
}
func ParseAxisValue2(src []byte) (AxisValue2, int, error) {
var item AxisValue2
n := 0
if L := len(src); L < 20 {
return item, 0, fmt.Errorf("reading AxisValue2: "+"EOF: expected length: 20, got %d", L)
}
item.mustParse(src)
n += 20
return item, n, nil
}
func ParseAxisValue3(src []byte) (AxisValue3, int, error) {
var item AxisValue3
n := 0
if L := len(src); L < 16 {
return item, 0, fmt.Errorf("reading AxisValue3: "+"EOF: expected length: 16, got %d", L)
}
item.mustParse(src)
n += 16
return item, n, nil
}
func ParseAxisValue4(src []byte) (AxisValue4, int, error) {
var item AxisValue4
n := 0
if L := len(src); L < 8 {
return item, 0, fmt.Errorf("reading AxisValue4: "+"EOF: expected length: 8, got %d", L)
}
_ = src[7] // early bound checking
item.format = binary.BigEndian.Uint16(src[0:])
item.axisCount = binary.BigEndian.Uint16(src[2:])
item.flags = binary.BigEndian.Uint16(src[4:])
item.valueNameID = NameID(binary.BigEndian.Uint16(src[6:]))
n += 8
{
arrayLength := int(item.axisCount)
if L := len(src); L < 8+arrayLength*6 {
return item, 0, fmt.Errorf("reading AxisValue4: "+"EOF: expected length: %d, got %d", 8+arrayLength*6, L)
}
item.axisValues = make([]AxisValueRecord, arrayLength) // allocation guarded by the previous check
for i := range item.axisValues {
item.axisValues[i].mustParse(src[8+i*6:])
}
n += arrayLength * 6
}
return item, n, nil
}
func ParseAxisValueArray(src []byte, valuesCount int) (AxisValueArray, int, error) {
var item AxisValueArray
n := 0
{
if L := len(src); L < valuesCount*2 {
return item, 0, fmt.Errorf("reading AxisValueArray: "+"EOF: expected length: %d, got %d", valuesCount*2, L)
}
item.Values = make([]AxisValue, valuesCount) // allocation guarded by the previous check
for i := range item.Values {
offset := int(binary.BigEndian.Uint16(src[i*2:]))
// ignore null offsets
if offset == 0 {
continue
}
if L := len(src); L < offset {
return item, 0, fmt.Errorf("reading AxisValueArray: "+"EOF: expected length: %d, got %d", offset, L)
}
var err error
item.Values[i], _, err = ParseAxisValue(src[offset:])
if err != nil {
return item, 0, fmt.Errorf("reading AxisValueArray: %s", err)
}
}
n += valuesCount * 2
}
return item, n, nil
}
func ParseFvar(src []byte) (Fvar, int, error) {
var item Fvar
n := 0
if L := len(src); L < 16 {
return item, 0, fmt.Errorf("reading Fvar: "+"EOF: expected length: 16, got %d", L)
}
_ = src[15] // early bound checking
item.majorVersion = binary.BigEndian.Uint16(src[0:])
item.minorVersion = binary.BigEndian.Uint16(src[2:])
item.axesArrayOffset = Offset16(binary.BigEndian.Uint16(src[4:]))
item.reserved = binary.BigEndian.Uint16(src[6:])
item.axisCount = binary.BigEndian.Uint16(src[8:])
item.axisSize = binary.BigEndian.Uint16(src[10:])
item.instanceCount = binary.BigEndian.Uint16(src[12:])
item.instanceSize = binary.BigEndian.Uint16(src[14:])
n += 16
{
err := item.parseFvarRecords(src[:])
if err != nil {
return item, 0, fmt.Errorf("reading Fvar: %s", err)
}
}
return item, n, nil
}
func ParseFvarRecords(src []byte, axisCount int, instanceCount int, instanceSize int) (FvarRecords, int, error) {
var item FvarRecords
n := 0
{
if L := len(src); L < axisCount*20 {
return item, 0, fmt.Errorf("reading FvarRecords: "+"EOF: expected length: %d, got %d", axisCount*20, L)
}
item.Axis = make([]VariationAxisRecord, axisCount) // allocation guarded by the previous check
for i := range item.Axis {
item.Axis[i].mustParse(src[i*20:])
}
n += axisCount * 20
}
{
err := item.parseInstances(src[n:], axisCount, instanceCount, instanceSize)
if err != nil {
return item, 0, fmt.Errorf("reading FvarRecords: %s", err)
}
}
return item, n, nil
}
func ParseGlyphVariationData(src []byte, axisCount int) (GlyphVariationData, int, error) {
var item GlyphVariationData
n := 0
if L := len(src); L < 4 {
return item, 0, fmt.Errorf("reading GlyphVariationData: "+"EOF: expected length: 4, got %d", L)
}
_ = src[3] // early bound checking
item.tupleVariationCount = binary.BigEndian.Uint16(src[0:])
offsetSerializedData := int(binary.BigEndian.Uint16(src[2:]))
n += 4
{
if offsetSerializedData != 0 { // ignore null offset
if L := len(src); L < offsetSerializedData {
return item, 0, fmt.Errorf("reading GlyphVariationData: "+"EOF: expected length: %d, got %d", offsetSerializedData, L)
}
item.SerializedData = src[offsetSerializedData:]
}
}
{
arrayLength := int(item.tupleVariationCount & 0x0FFF)
offset := 4
for i := 0; i < arrayLength; i++ {
elem, read, err := ParseTupleVariationHeader(src[offset:], axisCount)
if err != nil {
return item, 0, fmt.Errorf("reading GlyphVariationData: %s", err)
}
item.TupleVariationHeaders = append(item.TupleVariationHeaders, elem)
offset += read
}
n = offset
}
return item, n, nil
}
func ParseGvar(src []byte) (Gvar, int, error) {
var item Gvar
n := 0
if L := len(src); L < 20 {
return item, 0, fmt.Errorf("reading Gvar: "+"EOF: expected length: 20, got %d", L)
}
_ = src[19] // early bound checking
item.majorVersion = binary.BigEndian.Uint16(src[0:])
item.minorVersion = binary.BigEndian.Uint16(src[2:])
item.axisCount = binary.BigEndian.Uint16(src[4:])
item.sharedTupleCount = binary.BigEndian.Uint16(src[6:])
offsetSharedTuples := int(binary.BigEndian.Uint32(src[8:]))
item.glyphCount = binary.BigEndian.Uint16(src[12:])
item.flags = binary.BigEndian.Uint16(src[14:])
item.glyphVariationDataArrayOffset = Offset32(binary.BigEndian.Uint32(src[16:]))
n += 20
{
if offsetSharedTuples != 0 { // ignore null offset
if L := len(src); L < offsetSharedTuples {
return item, 0, fmt.Errorf("reading Gvar: "+"EOF: expected length: %d, got %d", offsetSharedTuples, L)
}
var err error
item.SharedTuples, _, err = ParseSharedTuples(src[offsetSharedTuples:], int(item.sharedTupleCount), int(item.axisCount))
if err != nil {
return item, 0, fmt.Errorf("reading Gvar: %s", err)
}
}
}
{
err := item.parseGlyphVariationDataOffsets(src[20:])
if err != nil {
return item, 0, fmt.Errorf("reading Gvar: %s", err)
}
}
{
err := item.parseGlyphVariationDatas(src[:])
if err != nil {
return item, 0, fmt.Errorf("reading Gvar: %s", err)
}
}
return item, n, nil
}
func ParseHVAR(src []byte) (HVAR, int, error) {
var item HVAR
n := 0
if L := len(src); L < 20 {
return item, 0, fmt.Errorf("reading HVAR: "+"EOF: expected length: 20, got %d", L)
}
_ = src[19] // early bound checking
item.majorVersion = binary.BigEndian.Uint16(src[0:])
item.minorVersion = binary.BigEndian.Uint16(src[2:])
offsetItemVariationStore := int(binary.BigEndian.Uint32(src[4:]))
offsetAdvanceWidthMapping := int(binary.BigEndian.Uint32(src[8:]))
offsetLsbMapping := int(binary.BigEndian.Uint32(src[12:]))
offsetRsbMapping := int(binary.BigEndian.Uint32(src[16:]))
n += 20
{
if offsetItemVariationStore != 0 { // ignore null offset
if L := len(src); L < offsetItemVariationStore {
return item, 0, fmt.Errorf("reading HVAR: "+"EOF: expected length: %d, got %d", offsetItemVariationStore, L)
}
var err error
item.ItemVariationStore, _, err = ParseItemVarStore(src[offsetItemVariationStore:])
if err != nil {
return item, 0, fmt.Errorf("reading HVAR: %s", err)
}
}
}
{
if offsetAdvanceWidthMapping != 0 { // ignore null offset
if L := len(src); L < offsetAdvanceWidthMapping {
return item, 0, fmt.Errorf("reading HVAR: "+"EOF: expected length: %d, got %d", offsetAdvanceWidthMapping, L)
}
var err error
item.AdvanceWidthMapping, _, err = ParseDeltaSetMapping(src[offsetAdvanceWidthMapping:])
if err != nil {
return item, 0, fmt.Errorf("reading HVAR: %s", err)
}
}
}
{
if offsetLsbMapping != 0 { // ignore null offset
if L := len(src); L < offsetLsbMapping {
return item, 0, fmt.Errorf("reading HVAR: "+"EOF: expected length: %d, got %d", offsetLsbMapping, L)
}
var tmpLsbMapping DeltaSetMapping
var err error
tmpLsbMapping, _, err = ParseDeltaSetMapping(src[offsetLsbMapping:])
if err != nil {
return item, 0, fmt.Errorf("reading HVAR: %s", err)
}
item.LsbMapping = &tmpLsbMapping
}
}
{
if offsetRsbMapping != 0 { // ignore null offset
if L := len(src); L < offsetRsbMapping {
return item, 0, fmt.Errorf("reading HVAR: "+"EOF: expected length: %d, got %d", offsetRsbMapping, L)
}
var tmpRsbMapping DeltaSetMapping
var err error
tmpRsbMapping, _, err = ParseDeltaSetMapping(src[offsetRsbMapping:])
if err != nil {
return item, 0, fmt.Errorf("reading HVAR: %s", err)
}
item.RsbMapping = &tmpRsbMapping
}
}
return item, n, nil
}
func ParseInstanceRecord(src []byte, coordinatesCount int) (InstanceRecord, int, error) {
var item InstanceRecord
n := 0
if L := len(src); L < 4 {
return item, 0, fmt.Errorf("reading InstanceRecord: "+"EOF: expected length: 4, got %d", L)
}
_ = src[3] // early bound checking
item.SubfamilyNameID = binary.BigEndian.Uint16(src[0:])
item.flags = binary.BigEndian.Uint16(src[2:])
n += 4
{
if L := len(src); L < 4+coordinatesCount*4 {
return item, 0, fmt.Errorf("reading InstanceRecord: "+"EOF: expected length: %d, got %d", 4+coordinatesCount*4, L)
}
item.Coordinates = make([]float32, coordinatesCount) // allocation guarded by the previous check
for i := range item.Coordinates {
item.Coordinates[i] = Float1616FromUint(binary.BigEndian.Uint32(src[4+i*4:]))
}
n += coordinatesCount * 4
}
{
read, err := item.parsePostScriptNameID(src[n:], coordinatesCount)
if err != nil {
return item, 0, fmt.Errorf("reading InstanceRecord: %s", err)
}
n += read
}
return item, n, nil
}
func ParseMVAR(src []byte) (MVAR, int, error) {
var item MVAR
n := 0
if L := len(src); L < 12 {
return item, 0, fmt.Errorf("reading MVAR: "+"EOF: expected length: 12, got %d", L)
}
_ = src[11] // early bound checking
item.majorVersion = binary.BigEndian.Uint16(src[0:])
item.minorVersion = binary.BigEndian.Uint16(src[2:])
item.reserved = binary.BigEndian.Uint16(src[4:])
item.valueRecordSize = binary.BigEndian.Uint16(src[6:])
item.valueRecordCount = binary.BigEndian.Uint16(src[8:])
offsetItemVariationStore := int(binary.BigEndian.Uint16(src[10:]))
n += 12
{
if offsetItemVariationStore != 0 { // ignore null offset
if L := len(src); L < offsetItemVariationStore {
return item, 0, fmt.Errorf("reading MVAR: "+"EOF: expected length: %d, got %d", offsetItemVariationStore, L)
}
var err error
item.ItemVariationStore, _, err = ParseItemVarStore(src[offsetItemVariationStore:])
if err != nil {
return item, 0, fmt.Errorf("reading MVAR: %s", err)
}
}
}
{
err := item.parseValueRecords(src[12:])
if err != nil {
return item, 0, fmt.Errorf("reading MVAR: %s", err)
}
}
return item, n, nil
}
func ParseSTAT(src []byte) (STAT, int, error) {
var item STAT
n := 0
if L := len(src); L < 20 {
return item, 0, fmt.Errorf("reading STAT: "+"EOF: expected length: 20, got %d", L)
}
_ = src[19] // early bound checking
item.majorVersion = binary.BigEndian.Uint16(src[0:])
item.minorVersion = binary.BigEndian.Uint16(src[2:])
item.designAxisSize = binary.BigEndian.Uint16(src[4:])
item.designAxisCount = binary.BigEndian.Uint16(src[6:])
offsetDesignAxes := int(binary.BigEndian.Uint32(src[8:]))
item.axisValueCount = binary.BigEndian.Uint16(src[12:])
offsetAxisValues := int(binary.BigEndian.Uint32(src[14:]))
item.elidedFallbackNameID = binary.BigEndian.Uint16(src[18:])
n += 20
{
if offsetDesignAxes != 0 { // ignore null offset
if L := len(src); L < offsetDesignAxes {
return item, 0, fmt.Errorf("reading STAT: "+"EOF: expected length: %d, got %d", offsetDesignAxes, L)
}
arrayLength := int(item.designAxisCount)
if L := len(src); L < offsetDesignAxes+arrayLength*8 {
return item, 0, fmt.Errorf("reading STAT: "+"EOF: expected length: %d, got %d", offsetDesignAxes+arrayLength*8, L)
}
item.designAxes = make([]AxisRecord, arrayLength) // allocation guarded by the previous check
for i := range item.designAxes {
item.designAxes[i].mustParse(src[offsetDesignAxes+i*8:])
}
offsetDesignAxes += arrayLength * 8
}
}
{
if offsetAxisValues != 0 { // ignore null offset
if L := len(src); L < offsetAxisValues {
return item, 0, fmt.Errorf("reading STAT: "+"EOF: expected length: %d, got %d", offsetAxisValues, L)
}
var err error
item.axisValues, _, err = ParseAxisValueArray(src[offsetAxisValues:], int(item.axisValueCount))
if err != nil {
return item, 0, fmt.Errorf("reading STAT: %s", err)
}
}
}
return item, n, nil
}
func ParseSegmentMaps(src []byte) (SegmentMaps, int, error) {
var item SegmentMaps
n := 0
if L := len(src); L < 2 {
return item, 0, fmt.Errorf("reading SegmentMaps: "+"EOF: expected length: 2, got %d", L)
}
arrayLengthAxisValueMaps := int(binary.BigEndian.Uint16(src[0:]))
n += 2
{
if L := len(src); L < 2+arrayLengthAxisValueMaps*4 {
return item, 0, fmt.Errorf("reading SegmentMaps: "+"EOF: expected length: %d, got %d", 2+arrayLengthAxisValueMaps*4, L)
}
item.AxisValueMaps = make([]AxisValueMap, arrayLengthAxisValueMaps) // allocation guarded by the previous check
for i := range item.AxisValueMaps {
item.AxisValueMaps[i].mustParse(src[2+i*4:])
}
n += arrayLengthAxisValueMaps * 4
}
return item, n, nil
}
func ParseSharedTuples(src []byte, sharedTuplesCount int, valuesCount int) (SharedTuples, int, error) {
var item SharedTuples
n := 0
{
offset := 0
for i := 0; i < sharedTuplesCount; i++ {
elem, read, err := ParseTuple(src[offset:], valuesCount)
if err != nil {
return item, 0, fmt.Errorf("reading SharedTuples: %s", err)
}
item.SharedTuples = append(item.SharedTuples, elem)
offset += read
}
n = offset
}
return item, n, nil
}
func ParseTuple(src []byte, valuesCount int) (Tuple, int, error) {
var item Tuple
n := 0
{
if L := len(src); L < valuesCount*2 {
return item, 0, fmt.Errorf("reading Tuple: "+"EOF: expected length: %d, got %d", valuesCount*2, L)
}
item.Values = make([]Coord, valuesCount) // allocation guarded by the previous check
for i := range item.Values {
item.Values[i] = Coord(binary.BigEndian.Uint16(src[i*2:]))
}
n += valuesCount * 2
}
return item, n, nil
}
func ParseTupleVariationHeader(src []byte, axisCount int) (TupleVariationHeader, int, error) {
var item TupleVariationHeader
n := 0
if L := len(src); L < 4 {
return item, 0, fmt.Errorf("reading TupleVariationHeader: "+"EOF: expected length: 4, got %d", L)
}
_ = src[3] // early bound checking
item.VariationDataSize = binary.BigEndian.Uint16(src[0:])
item.tupleIndex = binary.BigEndian.Uint16(src[2:])
n += 4
{
read, err := item.parsePeakTuple(src[4:], axisCount)
if err != nil {
return item, 0, fmt.Errorf("reading TupleVariationHeader: %s", err)
}
n += read
}
{
read, err := item.parseIntermediateTuples(src[n:], axisCount)
if err != nil {
return item, 0, fmt.Errorf("reading TupleVariationHeader: %s", err)
}
n += read
}
return item, n, nil
}
func ParseVVAR(src []byte) (VVAR, int, error) {
var item VVAR
n := 0
{
var (
err error
read int
)
item.HVAR, read, err = ParseHVAR(src[0:])
if err != nil {
return item, 0, fmt.Errorf("reading VVAR: %s", err)
}
n += read
}
if L := len(src); L < n+4 {
return item, 0, fmt.Errorf("reading VVAR: "+"EOF: expected length: n + 4, got %d", L)
}
offsetVOrgMapping := int(binary.BigEndian.Uint32(src[n:]))
n += 4
{
if offsetVOrgMapping != 0 { // ignore null offset
if L := len(src); L < offsetVOrgMapping {
return item, 0, fmt.Errorf("reading VVAR: "+"EOF: expected length: %d, got %d", offsetVOrgMapping, L)
}
var tmpVOrgMapping DeltaSetMapping
var err error
tmpVOrgMapping, _, err = ParseDeltaSetMapping(src[offsetVOrgMapping:])
if err != nil {
return item, 0, fmt.Errorf("reading VVAR: %s", err)
}
item.VOrgMapping = &tmpVOrgMapping
}
}
return item, n, nil
}
func ParseVarValueRecord(src []byte) (VarValueRecord, int, error) {
var item VarValueRecord
n := 0
if L := len(src); L < 8 {
return item, 0, fmt.Errorf("reading VarValueRecord: "+"EOF: expected length: 8, got %d", L)
}
item.mustParse(src)
n += 8
return item, n, nil
}
func (item *VarValueRecord) mustParse(src []byte) {
_ = src[7] // early bound checking
item.ValueTag = Tag(binary.BigEndian.Uint32(src[0:]))
item.Index.mustParse(src[4:])
}
func (item *VariationAxisRecord) mustParse(src []byte) {
_ = src[19] // early bound checking
item.Tag = Tag(binary.BigEndian.Uint32(src[0:]))
item.Minimum = Float1616FromUint(binary.BigEndian.Uint32(src[4:]))
item.Default = Float1616FromUint(binary.BigEndian.Uint32(src[8:]))
item.Maximum = Float1616FromUint(binary.BigEndian.Uint32(src[12:]))
item.flags = binary.BigEndian.Uint16(src[16:])
item.strid = NameID(binary.BigEndian.Uint16(src[18:]))
}
func (item *VariationStoreIndex) mustParse(src []byte) {
_ = src[3] // early bound checking
item.DeltaSetOuter = binary.BigEndian.Uint16(src[0:])
item.DeltaSetInner = binary.BigEndian.Uint16(src[2:])
}
@@ -0,0 +1,689 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package tables
import (
"encoding/binary"
"errors"
"fmt"
"math"
)
// ------------------------------------ fvar ------------------------------------
// Fvar is the Font Variations Table.
// See - https://learn.microsoft.com/fr-fr/typography/opentype/spec/fvar
type Fvar struct {
majorVersion uint16 // Major version number of the font variations table — set to 1.
minorVersion uint16 // Minor version number of the font variations table — set to 0.
axesArrayOffset Offset16 // Offset in bytes from the beginning of the table to the start of the VariationAxisRecord array.
reserved uint16 // This field is permanently reserved. Set to 2.
axisCount uint16 // The number of variation axes in the font (the number of records in the axes array).
axisSize uint16 // The size in bytes of each VariationAxisRecord — set to 20 (0x0014) for this version.
instanceCount uint16 // The number of named instances defined in the font (the number of records in the instances array).
instanceSize uint16 // The size in bytes of each InstanceRecord — set to either axisCount * sizeof(Fixed) + 4, or to axisCount * sizeof(Fixed) + 6.
FvarRecords `isOpaque:""`
}
func (fv *Fvar) parseFvarRecords(src []byte) (err error) {
if L := len(src); L < int(fv.axesArrayOffset) {
return fmt.Errorf("EOF: expected length: %d, got %d", fv.axesArrayOffset, L)
}
fv.FvarRecords, _, err = ParseFvarRecords(src[fv.axesArrayOffset:], int(fv.axisCount), int(fv.instanceCount), int(fv.axisCount))
return
}
// binarygen: argument=instanceCount int
// binarygen: argument=instanceSize int
type FvarRecords struct {
Axis []VariationAxisRecord
Instances []InstanceRecord `isOpaque:"" subsliceStart:"AtCurrent"`
}
func (fvr *FvarRecords) parseInstances(src []byte, axisCount, instanceCount, instanceSize int) error {
if L := len(src); L < instanceCount*instanceSize {
return fmt.Errorf("EOF: expected length: %d, got %d", instanceCount*instanceSize, L)
}
fvr.Instances = make([]InstanceRecord, instanceCount)
for i := range fvr.Instances {
var err error
fvr.Instances[i], _, err = ParseInstanceRecord(src[instanceSize*i:], axisCount)
if err != nil {
return err
}
}
return nil
}
type VariationAxisRecord struct {
Tag Tag // Tag identifying the design variation for the axis.
Minimum Float1616 // mininum value on the variation axis that the font covers
Default Float1616 // default position on the axis
Maximum Float1616 // maximum value on the variation axis that the font covers
flags uint16 // Axis qualifiers — see details below.
strid NameID // name entry in the font's ‘name’ table
}
type InstanceRecord struct {
SubfamilyNameID uint16 // The name ID for entries in the 'name' table that provide subfamily names for this instance.
flags uint16 // Reserved for future use — set to 0.
Coordinates []Float1616 // [axisCount] The coordinates array for this instance.
PostScriptNameID uint16 `isOpaque:"" subsliceStart:"AtCurrent"` // Optional. The name ID for entries in the 'name' table that provide PostScript names for this instance.
}
func (ir *InstanceRecord) parsePostScriptNameID(src []byte, _ int) (int, error) {
if len(src) >= 2 {
ir.PostScriptNameID = binary.BigEndian.Uint16(src)
return 2, nil
}
return 0, nil
}
type ItemVarStore struct {
format uint16 // Format — set to 1
VariationRegionList VariationRegionList `offsetSize:"Offset32"` // Offset in bytes from the start of the item variation store to the variation region list.
ItemVariationDatas []ItemVariationData `arrayCount:"FirstUint16" offsetsArray:"Offset32"` // [itemVariationDataCount] Offsets in bytes from the start of the item variation store to each item variation data subtable.
}
// GetDelta uses the variation [store] and the selected instance coordinates [coords]
// to compute the value at [index].
func (store ItemVarStore) GetDelta(index VariationStoreIndex, coords []Coord) float32 {
if int(index.DeltaSetOuter) >= len(store.ItemVariationDatas) {
return 0
}
varData := store.ItemVariationDatas[index.DeltaSetOuter]
if int(index.DeltaSetInner) >= len(varData.DeltaSets) {
return 0
}
deltaSet := varData.DeltaSets[index.DeltaSetInner]
var delta float32
for i, regionIndex := range varData.RegionIndexes {
region := store.VariationRegionList.VariationRegions[regionIndex]
v := region.Evaluate(coords)
delta += float32(deltaSet[i]) * v
}
return delta
}
// AxisCount returns the number of axis found in the
// var store, which must be the same as the one in the 'fvar' table.
// It returns -1 if the store is empty
func (vs *ItemVarStore) AxisCount() int {
if vs.format == 0 {
return -1
}
return int(vs.VariationRegionList.axisCount)
}
type VariationRegionList struct {
axisCount uint16 // The number of variation axes for this font. This must be the same number as axisCount in the 'fvar' table.
VariationRegions []VariationRegion `arrayCount:"FirstUint16" arguments:"regionAxesCount=.axisCount"` // [regionCount] Array of variation regions.
}
type VariationRegion struct {
// Array of region axis coordinates records, in the order of axes given in the 'fvar' table.
// Each RegionAxisCoordinates record provides coordinate values for a region along a single axis:
RegionAxes []RegionAxisCoordinates // [axisCount]
}
// Evaluate returns the scalar factor of the region
func (vr VariationRegion) Evaluate(coords []Coord) float32 {
v := float32(1)
for axis, coord := range coords {
factor := vr.RegionAxes[axis].evaluate(coord)
v *= factor
}
return v
}
type RegionAxisCoordinates struct {
StartCoord Coord // The region start coordinate value for the current axis.
PeakCoord Coord // The region peak coordinate value for the current axis.
EndCoord Coord // The region end coordinate value for the current axis.
}
// evaluate returns the factor corresponding to the given [coord],
// interpolating between start and end.
func (reg RegionAxisCoordinates) evaluate(coord Coord) float32 {
start, peak, end := reg.StartCoord, reg.PeakCoord, reg.EndCoord
if peak == 0 || coord == peak {
return 1
} else if coord == 0 { // Faster
return 0
}
if coord <= start || end <= coord {
return 0
}
// Interpolate
if coord < peak {
return float32(coord-start) / float32(peak-start)
}
return float32(end-coord) / float32(end-peak)
}
type ItemVariationData struct {
itemCount uint16 // The number of delta sets for distinct items.
wordDeltaCount uint16 // A packed field: the high bit is a flag—see details below.
regionIndexCount uint16 // The number of variation regions referenced.
RegionIndexes []uint16 `arrayCount:"ComputedField-regionIndexCount"` //[regionIndexCount] Array of indices into the variation region list for the regions referenced by this item variation data table.
DeltaSets [][]int16 `isOpaque:"" subsliceStart:"AtCurrent"` //[itemCount] Delta-set rows.
}
func (ivd *ItemVariationData) parseDeltaSets(src []byte) error {
const (
LONG_WORDS = 0x8000 // Flag indicating that “word” deltas are long (int32)
WORD_DELTA_COUNT_MASK = 0x7FFF // Count of “word” delt
)
if ivd.wordDeltaCount&LONG_WORDS != 0 {
return errors.New("LONG_WORDS not implemented in DeltaSets")
}
itemCount := int(ivd.itemCount)
shortDeltaCount := int(WORD_DELTA_COUNT_MASK & ivd.wordDeltaCount)
regionIndexCount := int(ivd.regionIndexCount)
rowLength := shortDeltaCount + regionIndexCount
if L := len(src); L < itemCount*rowLength {
return fmt.Errorf("EOF: expected length: %d, got %d", itemCount*rowLength, L)
}
if shortDeltaCount > regionIndexCount {
return errors.New("invalid item variation data subtable")
}
ivd.DeltaSets = make([][]int16, itemCount)
for i := range ivd.DeltaSets {
vi := make([]int16, regionIndexCount)
j := 0
for ; j < shortDeltaCount; j++ {
vi[j] = int16(binary.BigEndian.Uint16(src[2*j:]))
}
for ; j < regionIndexCount; j++ {
vi[j] = int16(int8(src[shortDeltaCount+j]))
}
ivd.DeltaSets[i] = vi
src = src[rowLength:]
}
return nil
}
// ------------------------------------ GVAR ------------------------------------
// See - https://learn.microsoft.com/fr-fr/typography/opentype/spec/gvar
type Gvar struct {
majorVersion uint16 // Major version number of the glyph variations table — set to 1.
minorVersion uint16 // Minor version number of the glyph variations table — set to 0.
axisCount uint16 // The number of variation axes for this font. This must be the same number as axisCount in the 'fvar' table.
sharedTupleCount uint16 // The number of shared tuple records. Shared tuple records can be referenced within glyph variation data tables for multiple glyphs, as opposed to other tuple records stored directly within a glyph variation data table.
SharedTuples `offsetSize:"Offset32" arguments:"sharedTuplesCount=.sharedTupleCount,valuesCount=.axisCount"` // Offset from the start of this table to the shared tuple records.
glyphCount uint16 // The number of glyphs in this font. This must match the number of glyphs stored elsewhere in the font.
flags uint16 // Bit-field that gives the format of the offset array that follows. If bit 0 is clear, the offsets are uint16; if bit 0 is set, the offsets are uint32.
glyphVariationDataArrayOffset Offset32 // Offset from the start of this table to the array of GlyphVariationData tables.
glyphVariationDataOffsets []uint32 `isOpaque:"" subsliceStart:"AtCurrent"` // [glyphCount + 1]Offset16 or Offset32 Offsets from the start of the GlyphVariationData array to each GlyphVariationData table.
GlyphVariationDatas []GlyphVariationData `isOpaque:""`
}
func (gv *Gvar) parseGlyphVariationDataOffsets(src []byte) error {
var err error
gv.glyphVariationDataOffsets, err = ParseLoca(src, int(gv.glyphCount), gv.flags&1 != 0)
return err
}
func (gv *Gvar) parseGlyphVariationDatas(src []byte) error {
gv.GlyphVariationDatas = make([]GlyphVariationData, gv.glyphCount)
startArray := uint32(gv.glyphVariationDataArrayOffset)
for i := range gv.GlyphVariationDatas {
start, end := int(startArray+gv.glyphVariationDataOffsets[i]), int(startArray+gv.glyphVariationDataOffsets[i+1])
if start == end {
continue
}
if start > end {
return fmt.Errorf("invalid offsets %d > %d", start, end)
}
if L := len(src); L < end {
return fmt.Errorf("EOF: expected length: %d, got %d", end, L)
}
var err error
gv.GlyphVariationDatas[i], _, err = ParseGlyphVariationData(src[start:end], int(gv.axisCount))
if err != nil {
return err
}
}
return nil
}
type SharedTuples struct {
SharedTuples []Tuple // [sharedTupleCount] Array of tuple records shared across all glyph variation data tables.
}
type Tuple struct {
Values []Coord // [axisCount] Coordinate array specifying a position within the font’s variation space. The number of elements must match the axisCount specified in the 'fvar' table.
}
type GlyphVariationData struct {
tupleVariationCount uint16 // A packed field. The high 4 bits are flags, and the low 12 bits are the number of tuple variation tables for this glyph. The number of tuple variation tables can be any number between 1 and 4095.
SerializedData []byte `offsetSize:"Offset16" arrayCount:"ToEnd"` // Offset from the start of the GlyphVariationData table to the serialized data
TupleVariationHeaders []TupleVariationHeader `arrayCount:"ComputedField-tupleVariationCount&0x0FFF"` //[tupleCount] Array of tuple variation headers.
}
// HasSharedPointNumbers returns true if the 'sharedPointNumbers' is on.
func (gv *GlyphVariationData) HasSharedPointNumbers() bool {
const sharedPointNumbers = 0x8000
return gv.tupleVariationCount&sharedPointNumbers != 0
}
// binarygen: argument=axisCount int
type TupleVariationHeader struct {
VariationDataSize uint16 // The size in bytes of the serialized data for this tuple variation table.
tupleIndex uint16 // A packed field. The high 4 bits are flags (see below). The low 12 bits are an index into a shared tuple records array.
// Peak tuple record for this tuple variation table — optional, determined by flags in the tupleIndex value.
// Note that this must always be included in the 'cvar' table.
PeakTuple Tuple `isOpaque:"" subsliceStart:"AtCurrent"`
IntermediateTuples [2]Tuple `isOpaque:"" subsliceStart:"AtCurrent"` // Intermediate start/end tuple record for this tuple variation table — optional, determined by flags in the tupleIndex value.
}
func (tv *TupleVariationHeader) parsePeakTuple(src []byte, axisCount int) (read int, err error) {
const embeddedPeakTuple = 0x8000
if hasPeak := tv.tupleIndex&embeddedPeakTuple != 0; hasPeak {
tv.PeakTuple, read, err = ParseTuple(src, axisCount)
if err != nil {
return 0, err
}
}
return
}
func (tv *TupleVariationHeader) parseIntermediateTuples(src []byte, axisCount int) (read int, err error) {
const intermediateRegion = 0x4000
if hasRegions := tv.tupleIndex&intermediateRegion != 0; hasRegions {
tv.IntermediateTuples[0], read, err = ParseTuple(src, axisCount)
if err != nil {
return 0, err
}
tv.IntermediateTuples[1], _, err = ParseTuple(src[read:], axisCount)
read *= 2
}
return
}
// HasPrivatePointNumbers returns true if the flag 'privatePointNumbers' is on
func (t *TupleVariationHeader) HasPrivatePointNumbers() bool {
const privatePointNumbers = 0x2000
return t.tupleIndex&privatePointNumbers != 0
}
// Index returns the tuple index, after masking
func (t *TupleVariationHeader) Index() uint16 {
const TupleIndexMask = 0x0FFF
return t.tupleIndex & TupleIndexMask
}
// ---------------------------------- HVAR/VVAR ----------------------------------
// See - https://learn.microsoft.com/fr-fr/typography/opentype/spec/hvar
type HVAR struct {
majorVersion uint16 // Major version number of the horizontal metrics variations table — set to 1.
minorVersion uint16 // Minor version number of the horizontal metrics variations table — set to 0.
ItemVariationStore ItemVarStore `offsetSize:"Offset32"` // Offset in bytes from the start of this table to the item variation store table.
AdvanceWidthMapping DeltaSetMapping `offsetSize:"Offset32"` // Offset in bytes from the start of this table to the delta-set index mapping for advance widths (may be NULL).
LsbMapping *DeltaSetMapping `offsetSize:"Offset32"` // Offset in bytes from the start of this table to the delta-set index mapping for left side bearings (may be NULL).
RsbMapping *DeltaSetMapping `offsetSize:"Offset32"` // Offset in bytes from the start of this table to the delta-set index mapping for right side bearings (may be NULL).
}
func (t *HVAR) AdvanceDelta(glyph GlyphID, coords []Coord) float32 {
index := t.AdvanceWidthMapping.Index(glyph)
return t.ItemVariationStore.GetDelta(index, coords)
}
// VariationStoreIndex reference an item in the variation store
type VariationStoreIndex struct {
DeltaSetOuter, DeltaSetInner uint16
}
type DeltaSetMapping struct {
format uint8 // DeltaSetIndexMap format: 0 or 1
entryFormat uint8 // A packed field that describes the compressed representation of delta-set indices. See details below.
// uint16 or uint32 mapCount : The number of mapping entries.
Map []VariationStoreIndex `isOpaque:"" subsliceStart:"AtCurrent"`
}
// Index returns the [VariationStoreIndex] for the given index.
func (m DeltaSetMapping) Index(glyph GlyphID) VariationStoreIndex {
// If a mapping table is not provided, glyph indices are used as implicit delta-set indices.
// [...] the delta-set outer-level index is zero, and the glyph ID is used as the inner-level index.
if len(m.Map) == 0 {
return VariationStoreIndex{DeltaSetInner: uint16(glyph)}
}
// If a given glyph ID is greater than mapCount - 1, then the last entry is used.
if int(glyph) >= len(m.Map) {
glyph = GlyphID(len(m.Map) - 1)
}
return m.Map[glyph]
}
func (ds *DeltaSetMapping) parseMap(src []byte) error {
var mapCount int
switch ds.format {
case 0:
if L := len(src); L < 2 {
return fmt.Errorf("EOF: expected length: %d, got %d", 2, L)
}
mapCount = int(binary.BigEndian.Uint16(src))
src = src[2:]
case 1:
if L := len(src); L < 4 {
return fmt.Errorf("EOF: expected length: %d, got %d", 4, L)
}
mapCount = int(binary.BigEndian.Uint32(src))
src = src[4:]
default:
return fmt.Errorf("unsupported DeltaSetMapping format %d", ds.format)
}
const (
INNER_INDEX_BIT_COUNT_MASK = 0x0F // Mask for the low 4 bits, which give the count of bits minus one that are used in each entry for the inner-level index.
MAP_ENTRY_SIZE_MASK = 0x30 // Mask for bits that indicate the size in bytes minus one of each entry.
)
innerBitSize := ds.entryFormat&INNER_INDEX_BIT_COUNT_MASK + 1
entrySize := int((ds.entryFormat&MAP_ENTRY_SIZE_MASK)>>4 + 1)
if entrySize > 4 || len(src) < entrySize*mapCount {
return fmt.Errorf("invalid delta-set mapping (length %d, entrySize %d, mapCount %d)", len(src), entrySize, mapCount)
}
ds.Map = make([]VariationStoreIndex, mapCount)
for i := range ds.Map {
var v uint32
for _, b := range src[entrySize*i : entrySize*(i+1)] { // 1 to 4 bytes
v = v<<8 + uint32(b)
}
ds.Map[i].DeltaSetOuter = uint16(v >> innerBitSize)
ds.Map[i].DeltaSetInner = uint16(v & (1<<innerBitSize - 1))
}
return nil
}
// See - https://learn.microsoft.com/fr-fr/typography/opentype/spec/vvar
type VVAR struct {
HVAR
VOrgMapping *DeltaSetMapping `offsetSize:"Offset32"` // Offset in bytes from the start of this table to the delta-set index mapping for Y coordinates of vertical origins (may be NULL).
}
func (vv *VVAR) VorgDelta(glyph GlyphID, coords []Coord) float32 {
if vv.VOrgMapping == nil {
return 0
}
varidx := vv.VOrgMapping.Index(glyph)
return vv.ItemVariationStore.GetDelta(varidx, coords)
}
// ------------------------------------ avar ------------------------------------
// avar — Axis Variations Table
type Avar struct {
majorVersion uint16 // Major version number of the axis variations table — set to 1.
minorVersion uint16 // Minor version number of the axis variations table — set to 0.
reserved uint16 // Permanently reserved; set to zero.
AxisSegmentMaps []SegmentMaps `arrayCount:"FirstUint16"` //[axisCount] The segment maps array — one segment map for each axis, in the order of axes specified in the 'fvar' table.
}
type SegmentMaps struct {
// [positionMapCount] The array of axis value map records for this axis.
// Each axis value map record provides a single axis-value mapping correspondence.
AxisValueMaps []AxisValueMap `arrayCount:"FirstUint16"`
}
func (sm SegmentMaps) Map(value Coord) Coord {
// copied from harfbuzz/src/hb-ot-var-avar-table.hh
l := sm.AxisValueMaps
// The following special-cases are not part of OpenType, which requires
// that at least -1, 0, and +1 must be mapped. But we include these as
// part of a better error recovery scheme.
if len(l) == 0 {
return value
} else if len(l) == 1 {
return value - l[0].FromCoordinate + l[0].ToCoordinate
}
// At least two mappings now.
// CoreText is wild...
// PingFangUI avar needs all this special-casing...
// So we implement an extended version of the spec here,
// which is more robust and more likely to be compatible with
// the wild.
const p1 = Coord(1 << 14)
const m1 = -p1
start := 0
end := len(l)
if l[start].FromCoordinate == m1 && l[start].ToCoordinate == m1 && l[start+1].FromCoordinate == m1 {
start++
}
if l[end-1].FromCoordinate == p1 && l[end-1].ToCoordinate == p1 && l[end-2].FromCoordinate == p1 {
end--
}
// Look for exact match first, and do lots of special-casing.
var i int
for i = start; i < end; i++ {
if value == l[i].FromCoordinate {
break
}
}
if i < end {
// There's at least one exact match. See if there are more.
j := i
for ; j+1 < end; j++ {
if value != l[j+1].FromCoordinate {
break
}
}
// [i,j] inclusive are all exact matches:
// If there's only one, return it. This is the only spec-compliant case.
if i == j {
return l[i].ToCoordinate
}
// If there's exactly three, return the middle one.
if i+2 == j {
return l[i+1].ToCoordinate
}
// Ignore the middle ones. Return the one mapping closer to 0.
if value < 0 {
return l[j].ToCoordinate
}
if value > 0 {
return l[i].ToCoordinate
}
// Mapping 0 ? CoreText seems confused. It seems to prefer 0 here...
// So we'll just return the smallest one. lol
if abs(l[i].ToCoordinate) < abs(l[j].ToCoordinate) {
return l[i].ToCoordinate
}
return l[j].ToCoordinate
}
// There's at least two and we're not an exact match. Prepare to lerp.
// Find the segment we're in.
for i = start; i < end; i++ {
if value < l[i].FromCoordinate {
break
}
}
if i == 0 {
// Value before all segments; Shift.
return value - l[0].FromCoordinate + l[0].ToCoordinate
}
if i == end {
// Value after all segments; Shift.
return value - l[end-1].FromCoordinate + l[end-1].ToCoordinate
}
// Actually interpolate.
before := l[i-1]
after := l[i]
denom := float64(after.FromCoordinate - before.FromCoordinate) // Can't be zero by now.
return before.ToCoordinate + Coord(math.Round(float64(after.ToCoordinate-before.ToCoordinate)*float64(value-before.FromCoordinate))/denom)
}
type AxisValueMap struct {
FromCoordinate Coord // A normalized coordinate value obtained using default normalization.
ToCoordinate Coord // The modified, normalized coordinate value.
}
// ----------------------------------------- MVAR -----------------------------------------
type MVAR struct {
majorVersion uint16 // Major version number of the metrics variations table — set to 1.
minorVersion uint16 // Minor version number of the metrics variations table — set to 0.
reserved uint16 // Not used; set to 0.
valueRecordSize uint16 // The size in bytes of each value record — must be greater than zero.
valueRecordCount uint16 // The number of value records — may be zero.
ItemVariationStore ItemVarStore `offsetSize:"Offset16"` // Offset in bytes from the start of this table to the item variation store table. If valueRecordCount is zero, set to zero; if valueRecordCount is greater than zero, must be greater than zero.
ValueRecords []VarValueRecord `isOpaque:"" subsliceStart:"AtCurrent"` // [valueRecordCount] Array of value records that identify target items and the associated delta-set index for each. The valueTag records must be in binary order of their valueTag field.
}
// Quoting the spec:
// "The valueRecordSize field indicates the size of each value record.
// Future, minor version updates of the MVAR table may define compatible
// extensions to the value record format with additional fields.
// Implementations must use the valueRecordSize field to determine the start of each record."
func (mv *MVAR) parseValueRecords(src []byte) error {
expectedL := int(mv.valueRecordSize) * int(mv.valueRecordCount)
if L := len(src); L < expectedL {
return fmt.Errorf("EOF: expected length: %d, got %d", expectedL, L)
}
mv.ValueRecords = make([]VarValueRecord, mv.valueRecordCount)
for i := range mv.ValueRecords {
mv.ValueRecords[i].mustParse(src[int(mv.valueRecordSize)*i:])
}
return nil
}
type VarValueRecord struct {
ValueTag Tag // Four-byte tag identifying a font-wide measure.
Index VariationStoreIndex // A delta-set index — used to select an item variation data subtable within the item variation store.
}
// ------------------------------------------------- STAT -------------------------------------------------
// STAT is the Style Attributes Table
// See https://learn.microsoft.com/en-us/typography/opentype/spec/stat
type STAT struct {
majorVersion uint16 // Major version number of the style attributes table — set to 1.
minorVersion uint16 // Minor version number of the style attributes table — set to 2.
designAxisSize uint16 // The size in bytes of each axis record.
designAxisCount uint16 // The number of axis records. In a font with an 'fvar' table, this value must be greater than or equal to the axisCount value in the 'fvar' table. In all fonts, must be greater than zero if axisValueCount is greater than zero.
designAxes []AxisRecord `offsetSize:"Offset32" arrayCount:"ComputedField-designAxisCount"` // Offset in bytes from the beginning of the STAT table to the start of the design axes array. If designAxisCount is zero, set to zero; if designAxisCount is greater than zero, must be greater than zero.
axisValueCount uint16 // The number of axis value tables.
axisValues AxisValueArray `offsetSize:"Offset32" arguments:"valuesCount=.axisValueCount"` // Offset in bytes from the beginning of the STAT table to the start of the design axes value offsets array. If axisValueCount is zero, set to zero; if axisValueCount is greater than zero, must be greater than zero.
elidedFallbackNameID uint16 // Name ID used as fallback when projection of names into a particular font model produces a subfamily name containing only elidable elements.
}
func (st *STAT) getAxisIndex(tag Tag) (uint16, bool) {
for index, record := range st.designAxes {
if record.Tag == tag {
return uint16(index), true
}
}
return 0, false
}
func (st STAT) Value(tag Tag) (float32, bool) {
axisIndex, ok := st.getAxisIndex(tag)
if !ok {
return 0, false
}
for _, axisValue := range st.axisValues.Values {
if axisValue.index() == axisIndex {
return axisValue.valueFor(axisIndex), true
}
}
return 0, false
}
type AxisRecord struct {
Tag Tag // A tag identifying the axis of design variation.
NameID NameID // The name ID for entries in the 'name' table that provide a display string for this axis.
Ordering uint16 // A value that applications can use to determine primary sorting of face names, or for ordering of labels when composing family or face names.
}
type AxisValueArray struct {
Values []AxisValue `offsetsArray:"Offset16"` // Offset in bytes from the beginning of the STAT table to the start of the design axes value offsets array. If axisValueCount is zero, set to zero; if axisValueCount is greater than zero, must be greater than zero.
}
type AxisValue interface {
name() NameID
index() uint16
valueFor(index uint16) float32
}
func (av AxisValue1) name() NameID { return av.valueNameID }
func (av AxisValue2) name() NameID { return av.valueNameID }
func (av AxisValue3) name() NameID { return av.valueNameID }
func (av AxisValue4) name() NameID { return av.valueNameID }
func (av AxisValue1) index() uint16 { return av.axisIndex }
func (av AxisValue2) index() uint16 { return av.axisIndex }
func (av AxisValue3) index() uint16 { return av.axisIndex }
func (av AxisValue4) index() uint16 { return 0xFFFF }
func (av AxisValue1) valueFor(index uint16) float32 { return av.value }
func (av AxisValue2) valueFor(index uint16) float32 { return av.nominalValue }
func (av AxisValue3) valueFor(index uint16) float32 { return av.value }
func (av AxisValue4) valueFor(index uint16) float32 {
return av.axisValues[index].value
}
type AxisValue1 struct {
format uint16 `unionTag:"1"` // Format identifier — set to 1.
axisIndex uint16 // Zero-base index into the axis record array identifying the axis of design variation to which the axis value table applies. Must be less than designAxisCount.
flags uint16 // Flags — see below for details.
valueNameID NameID // The name ID for entries in the 'name' table that provide a display string for this attribute value.
value Float1616 // A numeric value for this attribute value.
}
type AxisValue2 struct {
format uint16 `unionTag:"2"` // Format identifier — set to 2.
axisIndex uint16 // Zero-base index into the axis record array identifying the axis of design variation to which the axis value table applies. Must be less than designAxisCount.
flags uint16 // Flags — see below for details.
valueNameID NameID // The name ID for entries in the 'name' table that provide a display string for this attribute value.
nominalValue Float1616 // A nominal numeric value for this attribute value.
rangeMinValue Float1616 // The minimum value for a range associated with the specified name ID.
rangeMaxValue Float1616 // The maximum value for a range associated with the specified name ID
}
type AxisValue3 struct {
format uint16 `unionTag:"3"` // Format identifier — set to 3.
axisIndex uint16 // Zero-base index into the axis record array identifying the axis of design variation to which the axis value table applies. Must be less than designAxisCount.
flags uint16 // Flags — see below for details.
valueNameID NameID // The name ID for entries in the 'name' table that provide a display string for this attribute value.
value Float1616 // A numeric value for this attribute value.
linkedValue Float1616 // The numeric value for a style-linked mapping from this value.
}
type AxisValue4 struct {
format uint16 `unionTag:"4"` // Format identifier — set to 4.
axisCount uint16 // The total number of axes contributing to this axis-values combination.
flags uint16 // Flags — see below for details.
valueNameID NameID // The name ID for entries in the 'name' table that provide a display string for this combination of axis values.
axisValues []AxisValueRecord `arrayCount:"ComputedField-axisCount"` //[axisCount] Array of AxisValue records that provide the combination of axis values, one for each contributing axis.
}
type AxisValueRecord struct {
axisIndex uint16 // Zero-base index into the axis record array identifying the axis to which this value applies. Must be less than designAxisCount.
value Float1616 // A numeric value for this attribute value.
}
+80
View File
@@ -0,0 +1,80 @@
package opentype
import (
"encoding/binary"
"math"
)
// Table is one opentype binary table and its tag.
type Table struct {
Content []byte
Tag Tag
}
// WriteTTF creates a single Truetype font file (.ttf) from the given [tables] slice,
// which must be sorted by Tag
func WriteTTF(tables []Table) []byte {
introLength := uint32(otfHeaderSize + len(tables)*otfEntrySize)
buffer := make([]byte, introLength)
writeTTFHeader(len(tables), buffer)
tableOffset := introLength // the actual content will start after the header + table directory
for i, table := range tables {
cs := checksum(table.Content)
tableLength := uint32(len(table.Content))
slice := buffer[otfHeaderSize+i*otfEntrySize:]
binary.BigEndian.PutUint32(slice, uint32(table.Tag))
binary.BigEndian.PutUint32(slice[4:], cs)
binary.BigEndian.PutUint32(slice[8:], tableOffset)
binary.BigEndian.PutUint32(slice[12:], tableLength)
// update the offset
tableOffset = tableOffset + tableLength
}
// append the actual table content :
// allocate only once
buffer = append(buffer, make([]byte, tableOffset-introLength)...)
tableOffset = introLength
for _, table := range tables {
copy(buffer[tableOffset:], table.Content)
tableOffset = tableOffset + uint32(len(table.Content))
}
return buffer
}
// out is assumed to have a length >= ttfHeaderSize
func writeTTFHeader(nTables int, out []byte) {
log2 := math.Floor(math.Log2(float64(nTables)))
// Maximum power of 2 less than or equal to numTables, times 16 ((2**floor(log2(numTables))) * 16, where “**” is an exponentiation operator).
searchRange := math.Pow(2, log2) * 16
// Log2 of the maximum power of 2 less than or equal to numTables (log2(searchRange/16), which is equal to floor(log2(numTables))).
entrySelector := log2
// numTables times 16, minus searchRange ((numTables * 16) - searchRange).
rangeShift := nTables*16 - int(searchRange)
binary.BigEndian.PutUint32(out[:], uint32(TrueType))
binary.BigEndian.PutUint16(out[4:], uint16(nTables))
binary.BigEndian.PutUint16(out[6:], uint16(searchRange))
binary.BigEndian.PutUint16(out[8:], uint16(entrySelector))
binary.BigEndian.PutUint16(out[10:], uint16(rangeShift))
}
func checksum(table []byte) uint32 {
// "To accommodate data with a length that is not a multiple of four,
// the above algorithm must be modified to treat the data as though
// it contains zero padding to a length that is a multiple of four."
if r := len(table) % 4; r != 0 {
table = append(table, make([]byte, r)...)
}
var sum uint32
for i := 0; i < len(table)/4; i++ {
sum += binary.BigEndian.Uint32(table[i*4:])
}
return sum
}
+69
View File
@@ -0,0 +1,69 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package font
import (
"encoding/binary"
"errors"
"github.com/go-text/typesetting/font/opentype/tables"
)
type os2 struct {
version uint16
xAvgCharWidth uint16
*os2Desc
useTypoMetrics bool // true if the field sTypoAscender, sTypoDescender and sTypoLineGap are valid.
ySubscriptXSize float32
ySubscriptYSize float32
ySubscriptXOffset float32
ySubscriptYOffset float32
ySuperscriptXSize float32
ySuperscriptYSize float32
ySuperscriptXOffset float32
yStrikeoutSize float32
yStrikeoutPosition float32
sTypoAscender float32
sTypoDescender float32
sTypoLineGap float32
sxHeigh float32
sCapHeight float32
}
func newOs2(os tables.Os2) (os2, error) {
out := os2{
version: os.Version,
xAvgCharWidth: os.XAvgCharWidth,
os2Desc: newOS2Desc(os),
ySubscriptXSize: float32(os.YSubscriptXSize),
ySubscriptYSize: float32(os.YSubscriptYSize),
ySubscriptXOffset: float32(os.YSubscriptXOffset),
ySubscriptYOffset: float32(os.YSubscriptYOffset),
ySuperscriptXSize: float32(os.YSuperscriptXSize),
ySuperscriptYSize: float32(os.YSuperscriptYSize),
ySuperscriptXOffset: float32(os.YSuperscriptXOffset),
yStrikeoutSize: float32(os.YStrikeoutSize),
yStrikeoutPosition: float32(os.YStrikeoutPosition),
sTypoAscender: float32(os.STypoAscender),
sTypoDescender: float32(os.STypoDescender),
sTypoLineGap: float32(os.STypoLineGap),
}
// add addition info for version >= 2
if os.Version >= 2 {
if len(os.HigherVersionData) < 12 {
return os2{}, errors.New("invalid table os2")
}
out.sxHeigh = float32(binary.BigEndian.Uint16(os.HigherVersionData[8:]))
out.sCapHeight = float32(binary.BigEndian.Uint16(os.HigherVersionData[10:]))
}
const useTypoMetrics = 1 << 7
use := os.FsSelection&useTypoMetrics != 0
hasData := os.USWeightClass != 0 || os.USWidthClass != 0 || os.USFirstCharIndex != 0 || os.USLastCharIndex != 0
out.useTypoMetrics = use && hasData
return out, nil
}
+255
View File
@@ -0,0 +1,255 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package font
import "github.com/go-text/typesetting/font/opentype/tables"
// shared between GSUB and GPOS
type Layout struct {
Scripts []Script
Features []Feature
FeatureVariations []tables.FeatureVariationRecord
}
func newLayout(table tables.Layout) Layout {
fCount := len(table.FeatureList.Features)
out := Layout{
Scripts: make([]Script, len(table.ScriptList.Scripts)),
Features: make([]Feature, fCount),
}
for i, s := range table.ScriptList.Scripts {
if langSys := s.DefaultLangSys; langSys != nil {
sanitizeLangSys(langSys, fCount)
}
for i := range s.LangSys {
sanitizeLangSys(&s.LangSys[i], fCount)
}
out.Scripts[i] = Script{
Script: s,
Tag: table.ScriptList.Records[i].Tag,
}
}
for i, f := range table.FeatureList.Features {
out.Features[i] = Feature{
Feature: f,
Tag: table.FeatureList.Records[i].Tag,
}
}
if table.FeatureVariations != nil {
out.FeatureVariations = table.FeatureVariations.FeatureVariationRecords
}
return out
}
func sanitizeLangSys(langSys *tables.LangSys, featuresCount int) {
if int(langSys.RequiredFeatureIndex) >= featuresCount {
// invalid index : replace it by the sentinel value
langSys.RequiredFeatureIndex = 0xFFFF
}
}
type Script struct {
tables.Script
Tag Tag
}
type Feature struct {
tables.Feature
Tag Tag
}
// FindScript looks for [script] and return its index into the Scripts slice,
// or -1 if the tag is not found.
func (la *Layout) FindScript(script Tag) int {
// Scripts is sorted: binary search
low, high := 0, len(la.Scripts)
for low < high {
mid := low + (high-low)/2 // avoid overflow when computing mid
p := la.Scripts[mid].Tag
if script < p {
high = mid
} else if script > p {
low = mid + 1
} else {
return mid
}
}
return -1
}
// FindVariationIndex returns the first feature variation matching
// the specified variation coordinates, as an index in the
// `FeatureVariations` field.
// It returns `-1` if not found.
func (la *Layout) FindVariationIndex(coords []VarCoord) int {
for i, record := range la.FeatureVariations {
if evaluateVarRec(record, coords) {
return i
}
}
return -1
}
// returns `true` if the feature is concerned by the `coords`
func evaluateVarRec(fv tables.FeatureVariationRecord, coords []VarCoord) bool {
for _, c := range fv.ConditionSet.Conditions {
if !evaluateCondition(c, coords) {
return false
}
}
return true
}
// returns `true` if `coords` match the condition `c`
func evaluateCondition(c tables.ConditionFormat1, coords []VarCoord) bool {
var coord VarCoord
if int(c.AxisIndex) < len(coords) {
coord = coords[c.AxisIndex]
}
return c.FilterRangeMinValue <= coord && coord <= c.FilterRangeMaxValue
}
// FindFeatureIndex fetches the index for a given feature tag in the GSUB or GPOS table.
// Returns false if not found
func (la *Layout) FindFeatureIndex(featureTag Tag) (uint16, bool) {
for i, feat := range la.Features { // i fits in uint16
if featureTag == feat.Tag {
return uint16(i), true
}
}
return 0, false
}
// ---------------------------------- GSUB ----------------------------------
type GSUB struct {
Layout
Lookups []GSUBLookup
}
type LookupOptions struct {
// Lookup qualifiers.
Flag uint16
// Index (base 0) into GDEF mark glyph sets structure,
// meaningfull only if UseMarkFilteringSet is set.
MarkFilteringSet uint16
}
const UseMarkFilteringSet = 1 << 4
// Props returns a 32-bit integer where the lower 16-bit is `Flag` and
// the higher 16-bit is `MarkFilteringSet` if the lookup uses one.
func (lo LookupOptions) Props() uint32 {
flag := uint32(lo.Flag)
if lo.Flag&UseMarkFilteringSet != 0 {
flag |= uint32(lo.MarkFilteringSet) << 16
}
return flag
}
type GSUBLookup struct {
LookupOptions
Subtables []tables.GSUBLookup
}
func newGSUB(table tables.Layout) (GSUB, error) {
out := GSUB{
Layout: newLayout(table),
Lookups: make([]GSUBLookup, len(table.LookupList.Lookups)),
}
for i, lk := range table.LookupList.Lookups {
subtables, err := lk.AsGSUBLookups()
if err != nil {
return GSUB{}, err
}
for j, subtable := range subtables {
// start by resolving extension
if ext, isExt := subtable.(tables.ExtensionSubs); isExt {
subtables[j], err = ext.Resolve()
if err != nil {
return GSUB{}, err
}
}
// sanitize each lookup
switch subtable := subtable.(type) {
case tables.MultipleSubs:
err = subtable.Sanitize()
case tables.LigatureSubs:
err = subtable.Sanitize()
case tables.ContextualSubs:
err = subtable.Sanitize(uint16(len(out.Lookups)))
case tables.ReverseChainSingleSubs:
err = subtable.Sanitize()
}
if err != nil {
return GSUB{}, err
}
}
out.Lookups[i] = GSUBLookup{
LookupOptions: LookupOptions{
Flag: lk.LookupFlag,
MarkFilteringSet: lk.MarkFilteringSet,
},
Subtables: subtables,
}
}
return out, nil
}
type GPOS struct {
Layout
Lookups []GPOSLookup
}
type GPOSLookup struct {
LookupOptions
Subtables []tables.GPOSLookup
}
func newGPOS(table tables.Layout) (GPOS, error) {
out := GPOS{
Layout: newLayout(table),
Lookups: make([]GPOSLookup, len(table.LookupList.Lookups)),
}
for i, lk := range table.LookupList.Lookups {
subtables, err := lk.AsGPOSLookups()
if err != nil {
return GPOS{}, err
}
for j, subtable := range subtables {
// start by resolving extension
if ext, isExt := subtable.(tables.ExtensionPos); isExt {
subtables[j], err = ext.Resolve()
if err != nil {
return GPOS{}, err
}
}
// sanitize each lookup
switch subtable := subtable.(type) {
case tables.SinglePos:
err = subtable.Sanitize()
case tables.PairPos:
err = subtable.Sanitize()
case tables.MarkBasePos:
err = subtable.Sanitize()
case tables.MarkLigPos:
err = subtable.Sanitize()
case tables.ContextualPos:
err = subtable.Sanitize(uint16(len(out.Lookups)))
}
if err != nil {
return GPOS{}, err
}
}
out.Lookups[i] = GPOSLookup{
LookupOptions: LookupOptions{
Flag: lk.LookupFlag,
MarkFilteringSet: lk.MarkFilteringSet,
},
Subtables: subtables,
}
}
return out, nil
}
+359
View File
@@ -0,0 +1,359 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package font
import (
"errors"
"github.com/go-text/typesetting/font/opentype/tables"
)
const numBuiltInPostNames = len(builtInPostNames)
// names is the built-in post table names listed at
// https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6post.html
var builtInPostNames = [...]string{
".notdef",
".null",
"nonmarkingreturn",
"space",
"exclam",
"quotedbl",
"numbersign",
"dollar",
"percent",
"ampersand",
"quotesingle",
"parenleft",
"parenright",
"asterisk",
"plus",
"comma",
"hyphen",
"period",
"slash",
"zero",
"one",
"two",
"three",
"four",
"five",
"six",
"seven",
"eight",
"nine",
"colon",
"semicolon",
"less",
"equal",
"greater",
"question",
"at",
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"K",
"L",
"M",
"N",
"O",
"P",
"Q",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z",
"bracketleft",
"backslash",
"bracketright",
"asciicircum",
"underscore",
"grave",
"a",
"b",
"c",
"d",
"e",
"f",
"g",
"h",
"i",
"j",
"k",
"l",
"m",
"n",
"o",
"p",
"q",
"r",
"s",
"t",
"u",
"v",
"w",
"x",
"y",
"z",
"braceleft",
"bar",
"braceright",
"asciitilde",
"Adieresis",
"Aring",
"Ccedilla",
"Eacute",
"Ntilde",
"Odieresis",
"Udieresis",
"aacute",
"agrave",
"acircumflex",
"adieresis",
"atilde",
"aring",
"ccedilla",
"eacute",
"egrave",
"ecircumflex",
"edieresis",
"iacute",
"igrave",
"icircumflex",
"idieresis",
"ntilde",
"oacute",
"ograve",
"ocircumflex",
"odieresis",
"otilde",
"uacute",
"ugrave",
"ucircumflex",
"udieresis",
"dagger",
"degree",
"cent",
"sterling",
"section",
"bullet",
"paragraph",
"germandbls",
"registered",
"copyright",
"trademark",
"acute",
"dieresis",
"notequal",
"AE",
"Oslash",
"infinity",
"plusminus",
"lessequal",
"greaterequal",
"yen",
"mu",
"partialdiff",
"summation",
"product",
"pi",
"integral",
"ordfeminine",
"ordmasculine",
"Omega",
"ae",
"oslash",
"questiondown",
"exclamdown",
"logicalnot",
"radical",
"florin",
"approxequal",
"Delta",
"guillemotleft",
"guillemotright",
"ellipsis",
"nonbreakingspace",
"Agrave",
"Atilde",
"Otilde",
"OE",
"oe",
"endash",
"emdash",
"quotedblleft",
"quotedblright",
"quoteleft",
"quoteright",
"divide",
"lozenge",
"ydieresis",
"Ydieresis",
"fraction",
"currency",
"guilsinglleft",
"guilsinglright",
"fi",
"fl",
"daggerdbl",
"periodcentered",
"quotesinglbase",
"quotedblbase",
"perthousand",
"Acircumflex",
"Ecircumflex",
"Aacute",
"Edieresis",
"Egrave",
"Iacute",
"Icircumflex",
"Idieresis",
"Igrave",
"Oacute",
"Ocircumflex",
"apple",
"Ograve",
"Uacute",
"Ucircumflex",
"Ugrave",
"dotlessi",
"circumflex",
"tilde",
"macron",
"breve",
"dotaccent",
"ring",
"cedilla",
"hungarumlaut",
"ogonek",
"caron",
"Lslash",
"lslash",
"Scaron",
"scaron",
"Zcaron",
"zcaron",
"brokenbar",
"Eth",
"eth",
"Yacute",
"yacute",
"Thorn",
"thorn",
"minus",
"multiply",
"onesuperior",
"twosuperior",
"threesuperior",
"onehalf",
"onequarter",
"threequarters",
"franc",
"Gbreve",
"gbreve",
"Idotaccent",
"Scedilla",
"scedilla",
"Cacute",
"cacute",
"Ccaron",
"ccaron",
"dcroat",
}
type post struct {
// suggested distance of the top of the
// underline from the baseline (negative values indicate below baseline).
underlinePosition float32
// suggested values for the underline thickness.
underlineThickness float32
names postGlyphNames
isFixedPitch bool
}
func newPost(pst tables.Post) (post, error) {
out := post{
underlinePosition: float32(pst.UnderlinePosition),
underlineThickness: float32(pst.UnderlineThickness),
isFixedPitch: pst.IsFixedPitch != 0,
}
switch names := pst.Names.(type) {
case tables.PostNames10:
out.names = postNames10or30{}
case tables.PostNames20:
n := postNames20(names)
if err := n.sanitize(); err != nil {
return out, err
}
out.names = n
case tables.PostNames30:
// no-op, do not use the post name tables
}
return out, nil
}
// postGlyphNames stores the names of a 'post' table.
type postGlyphNames interface {
// GlyphName return the postscript name of a
// glyph, or an empty string if it not found
glyphName(x GID) string
}
type postNames10or30 struct{}
func (p postNames10or30) glyphName(x GID) string {
if int(x) >= numBuiltInPostNames {
return ""
}
// https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6post.html
return builtInPostNames[x]
}
type postNames20 tables.PostNames20
func (p postNames20) glyphName(x GID) string {
if int(x) >= len(p.GlyphNameIndexes) {
return ""
}
u := int(p.GlyphNameIndexes[x])
if u < numBuiltInPostNames {
return builtInPostNames[u]
}
u -= numBuiltInPostNames
return p.Strings[u]
}
// check that all the indexes are valid
func (p postNames20) sanitize() error {
var maxIndex uint16
// find the maximum
for _, u := range p.GlyphNameIndexes {
// https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6post.html
// says that "32768 through 65535 are reserved for future use".
if u > 32767 {
return errors.New("invalid index in Postscript names table format 20")
}
if u > maxIndex {
maxIndex = u
}
}
if int(maxIndex) >= numBuiltInPostNames && len(p.Strings) < (int(maxIndex)-numBuiltInPostNames) {
return errors.New("invalid index in Postscript names table format 20")
}
return nil
}
+479
View File
@@ -0,0 +1,479 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package font
import (
"bytes"
"compress/gzip"
"errors"
"fmt"
"io"
ot "github.com/go-text/typesetting/font/opentype"
"github.com/go-text/typesetting/font/opentype/tables"
)
var (
errEmptySbixTable = errors.New("empty 'sbix' table")
errEmptyBitmapTable = errors.New("empty bitmap table")
)
type (
Segment = ot.Segment
SegmentPoint = ot.SegmentPoint
)
// GlyphData describe how to draw a glyph.
// It is either an GlyphOutline, GlyphSVG or GlyphBitmap.
type GlyphData interface {
isGlyphData()
}
func (GlyphOutline) isGlyphData() {}
func (GlyphSVG) isGlyphData() {}
func (GlyphBitmap) isGlyphData() {}
func (GlyphColor) isGlyphData() {}
// GlyphOutline exposes the path to draw for
// vector glyph.
// Coordinates are expressed in fonts units.
type GlyphOutline struct {
Segments []Segment
}
// Sideways updates the coordinates of the outline by applying
// a 90° clockwise rotation, and adding [yOffset] afterwards.
//
// When used for vertical text, pass
// -Glyph.YOffset, converted in font units, as [yOffset]
// (a positive value to lift the glyph up).
func (o GlyphOutline) Sideways(yOffset float32) {
for i := range o.Segments {
target := o.Segments[i].Args[:]
target[0].X, target[0].Y = target[0].Y, -target[0].X+yOffset
target[1].X, target[1].Y = target[1].Y, -target[1].X+yOffset
target[2].X, target[2].Y = target[2].Y, -target[2].X+yOffset
}
}
// GlyphSVG is an SVG description for the glyph,
// as found in Opentype SVG table.
type GlyphSVG struct {
// The SVG image content, decompressed if needed.
// The actual glyph description is an SVG element
// with id="glyph<GID>" (as in id="glyph12"),
// and several glyphs may share the same Source
Source []byte
// According to the specification, a fallback outline
// should be specified for each SVG glyphs
Outline GlyphOutline
}
type GlyphBitmap struct {
// The actual image content, whose interpretation depends
// on the Format field.
Data []byte
Format BitmapFormat
Width, Height int // number of columns and rows
// Outline may be specified to be drawn with bitmap
Outline *GlyphOutline
}
// BitmapFormat identifies the format on the glyph
// raw data. Across the various font files, many formats
// may be encountered : black and white bitmaps, PNG, TIFF, JPG.
type BitmapFormat uint8
const (
_ BitmapFormat = iota
// The [GlyphBitmap.Data] slice stores a black or white (0/1)
// bit image, whose length L satisfies
// L * 8 >= [GlyphBitmap.Width] * [GlyphBitmap.Height]
BlackAndWhite
// The [GlyphBitmap.Data] slice stores a PNG encoded image
PNG
// The [GlyphBitmap.Data] slice stores a JPG encoded image
JPG
// The [GlyphBitmap.Data] slice stores a TIFF encoded image
TIFF
)
// BitmapSize expose the size of bitmap glyphs.
// One font may contain several sizes.
type BitmapSize struct {
Height, Width uint16
XPpem, YPpem uint16
}
// GlyphColor describe a colored glyph, as found in
// COLR tables
type GlyphColor struct {
Paint tables.PaintTable
}
func (sb sbix) glyphData(gid gID, xPpem, yPpem uint16) (GlyphBitmap, error) {
st := sb.chooseStrike(xPpem, yPpem)
if st == nil {
return GlyphBitmap{}, errEmptySbixTable
}
glyph := strikeGlyph(st, gid, 0)
if glyph.GraphicType == 0 {
return GlyphBitmap{}, fmt.Errorf("no glyph %d in 'sbix' table for resolution (%d, %d)", gid, xPpem, yPpem)
}
out := GlyphBitmap{Data: glyph.Data}
var err error
out.Width, out.Height, out.Format, err = decodeBitmapConfig(glyph)
return out, err
}
func (bt bitmap) glyphData(gid gID, xPpem, yPpem uint16) (GlyphBitmap, error) {
st := bt.chooseStrike(xPpem, yPpem)
if st == nil || st.ppemX == 0 || st.ppemY == 0 {
return GlyphBitmap{}, errEmptyBitmapTable
}
subtable := st.findTable(gid)
if subtable == nil {
return GlyphBitmap{}, fmt.Errorf("no glyph %d in bitmap table for resolution (%d, %d)", gid, xPpem, yPpem)
}
glyph := subtable.image(gid)
if glyph == nil {
return GlyphBitmap{}, fmt.Errorf("no glyph %d in bitmap table for resolution (%d, %d)", gid, xPpem, yPpem)
}
out := GlyphBitmap{
Data: glyph.image,
Width: int(glyph.metrics.Width),
Height: int(glyph.metrics.Height),
}
switch subtable.imageFormat {
case 17, 18, 19: // PNG
out.Format = PNG
case 2, 5:
out.Format = BlackAndWhite
// ensure data length
L := out.Width * out.Height // in bits
if len(out.Data)*8 < L {
return GlyphBitmap{}, fmt.Errorf("EOF in glyph bitmap: expected %d, got %d", L, len(out.Data)*8)
}
default:
return GlyphBitmap{}, fmt.Errorf("unsupported format %d in bitmap table", subtable.imageFormat)
}
return out, nil
}
func (s svg) glyphData(gid gID) (GlyphSVG, bool) {
data, ok := s.rawGlyphData(gid)
if !ok {
return GlyphSVG{}, false
}
// un-compress if needed
if r, err := gzip.NewReader(bytes.NewReader(data)); err == nil {
var buf bytes.Buffer
if _, err := io.Copy(&buf, r); err == nil {
data = buf.Bytes()
}
}
return GlyphSVG{Source: data}, true
}
// this file converts from font format for glyph outlines to
// segments that rasterizer will consume
//
// adapted from snft/truetype.go
func midPoint(p, q SegmentPoint) SegmentPoint {
return SegmentPoint{
X: (p.X + q.X) / 2,
Y: (p.Y + q.Y) / 2,
}
}
// build the segments from the resolved contour points
func buildSegments(points []contourPoint) []Segment {
if len(points) == 0 {
return nil
}
var (
firstOnCurveValid, firstOffCurveValid, lastOffCurveValid bool
firstOnCurve, firstOffCurve, lastOffCurve SegmentPoint
)
out := make([]Segment, 0, len(points)+2)
for _, point := range points {
p := point.SegmentPoint
if !firstOnCurveValid {
if point.isOnCurve {
firstOnCurve = p
firstOnCurveValid = true
out = append(out, Segment{
Op: ot.SegmentOpMoveTo,
Args: [3]SegmentPoint{p},
})
} else if !firstOffCurveValid {
firstOffCurve = p
firstOffCurveValid = true
if !point.isEndPoint {
continue
}
} else {
firstOnCurve = midPoint(firstOffCurve, p)
firstOnCurveValid = true
lastOffCurve = p
lastOffCurveValid = true
out = append(out, Segment{
Op: ot.SegmentOpMoveTo,
Args: [3]SegmentPoint{firstOnCurve},
})
}
} else if !lastOffCurveValid {
if !point.isOnCurve {
lastOffCurve = p
lastOffCurveValid = true
if !point.isEndPoint {
continue
}
} else {
out = append(out, Segment{
Op: ot.SegmentOpLineTo,
Args: [3]SegmentPoint{p},
})
}
} else {
if !point.isOnCurve {
out = append(out, Segment{
Op: ot.SegmentOpQuadTo,
Args: [3]SegmentPoint{
lastOffCurve,
midPoint(lastOffCurve, p),
},
})
lastOffCurve = p
lastOffCurveValid = true
} else {
out = append(out, Segment{
Op: ot.SegmentOpQuadTo,
Args: [3]SegmentPoint{lastOffCurve, p},
})
lastOffCurveValid = false
}
}
if point.isEndPoint {
// closing the contour
switch {
case !firstOffCurveValid && !lastOffCurveValid:
out = append(out, Segment{
Op: ot.SegmentOpLineTo,
Args: [3]SegmentPoint{firstOnCurve},
})
case !firstOffCurveValid && lastOffCurveValid:
out = append(out, Segment{
Op: ot.SegmentOpQuadTo,
Args: [3]SegmentPoint{lastOffCurve, firstOnCurve},
})
case firstOffCurveValid && !lastOffCurveValid:
out = append(out, Segment{
Op: ot.SegmentOpQuadTo,
Args: [3]SegmentPoint{firstOffCurve, firstOnCurve},
})
case firstOffCurveValid && lastOffCurveValid:
out = append(out, Segment{
Op: ot.SegmentOpQuadTo,
Args: [3]SegmentPoint{
lastOffCurve,
midPoint(lastOffCurve, firstOffCurve),
},
},
Segment{
Op: ot.SegmentOpQuadTo,
Args: [3]SegmentPoint{firstOffCurve, firstOnCurve},
},
)
}
firstOnCurveValid = false
firstOffCurveValid = false
lastOffCurveValid = false
}
}
return out
}
type errGlyphOutOfRange int
func (e errGlyphOutOfRange) Error() string {
return fmt.Sprintf("out of range glyph %d", e)
}
// apply variation when needed
func (f *Face) glyphDataFromGlyf(glyph gID) (GlyphOutline, error) {
if int(glyph) >= len(f.glyf) {
return GlyphOutline{}, errGlyphOutOfRange(glyph)
}
points := f.getPointsForGlyph(glyph)
segments := buildSegments(points[:len(points)-phantomCount])
return GlyphOutline{Segments: segments}, nil
}
var (
errNoCFFTable error = errors.New("no CFF table")
errNoCFF2Table error = errors.New("no CFF2 table")
)
func (f *Font) glyphDataFromCFF1(glyph gID) (GlyphOutline, error) {
if f.cff == nil {
return GlyphOutline{}, errNoCFFTable
}
segments, _, err := f.cff.LoadGlyph(glyph)
if err != nil {
return GlyphOutline{}, err
}
return GlyphOutline{Segments: segments}, nil
}
func (f *Face) glyphDataFromCFF2(glyph gID) (GlyphOutline, error) {
if f.cff2 == nil {
return GlyphOutline{}, errNoCFF2Table
}
segments, _, err := f.cff2.LoadGlyph(glyph, f.coords)
if err != nil {
return GlyphOutline{}, err
}
return GlyphOutline{Segments: segments}, nil
}
// -------------------------- Public API --------------------------
// BitmapSizes returns the size of bitmap glyphs present in the font.
func (font *Font) BitmapSizes() []BitmapSize {
upem := font.head.UnitsPerEm
avgWidth := font.os2.xAvgCharWidth
// handle invalid head/os2 tables
if upem == 0 || font.os2.version == 0xFFFF {
avgWidth = 1
upem = 1
}
// adapted from freetype tt_face_load_sbit
if font.bitmap != nil {
return font.bitmap.availableSizes(avgWidth, upem)
}
if hori := font.hhea; hori != nil {
return font.sbix.availableSizes(hori, avgWidth, upem)
}
return nil
}
// GlyphData returns the glyph content for [gid], or nil if
// not found.
//
// See also the various GlyphDataXXX methods, for more control
// on the types of glyphs loaded.
func (f *Face) GlyphData(gid GID) GlyphData {
g := gID(gid)
// since outline may be specified for SVG and bitmaps, check it at the end
// check color first over SVG, since it is more optimized format
if out, ok := f.GlyphDataColor(g); ok {
return out
}
if out, ok := f.GlyphDataBitmap(g); ok {
return out
}
if out, ok := f.GlyphDataSVG(g); ok {
return out
}
if out, ok := f.GlyphDataOutline(g); ok {
return out
}
return nil
}
// GlyphDataOutline looks for glyph data in 'glyf', 'CFF ' and 'CFF2' tables.
//
// It is a bit faster than calling [Face.GlyphData] and may be used for instance
// when rendering colored glyphs (from the 'COLR' table).
func (f *Face) GlyphDataOutline(gid gID) (GlyphOutline, bool) {
out, err := f.glyphDataFromCFF1(gid)
if err == nil {
return out, true
}
out, err = f.glyphDataFromCFF2(gid)
if err == nil {
return out, true
}
out, err = f.glyphDataFromGlyf(gid)
if err == nil {
return out, true
}
return GlyphOutline{}, false
}
// GlyphDataSVG looks for glyph data in the 'SVG ' table.
func (f *Face) GlyphDataSVG(gid gID) (GlyphSVG, bool) {
outS, ok := f.svg.glyphData(gid)
if !ok {
return GlyphSVG{}, false
}
// Spec :
// For every SVG glyph description, there must be a corresponding TrueType,
// CFF or CFF2 glyph description in the font.
outS.Outline, _ = f.GlyphDataOutline(gid)
return outS, true
}
// GlyphDataColor looks for glyph data in the 'COLR' table.
func (f *Face) GlyphDataColor(gid gID) (GlyphColor, bool) {
v, ok := f.COLR.Search(gid)
return GlyphColor{v}, ok
}
// GlyphDataBitmap looks for glyph data in the 'sbix', 'CBDT', 'EBDT' and 'BDAT' tables.
func (f *Face) GlyphDataBitmap(gid gID) (GlyphBitmap, bool) {
outB, err := f.sbix.glyphData(gid, f.xPpem, f.yPpem)
if err == nil {
outline, ok := f.GlyphDataOutline(gid)
if ok {
outB.Outline = &outline
}
return outB, true
}
outB, err = f.bitmap.glyphData(gid, f.xPpem, f.yPpem)
if err == nil {
outline, ok := f.GlyphDataOutline(gid)
if ok {
outB.Outline = &outline
}
return outB, true
}
return GlyphBitmap{}, false
}
+54
View File
@@ -0,0 +1,54 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package font
import (
"fmt"
"github.com/go-text/typesetting/font/opentype/tables"
)
type svg []svgDocument
func newSvg(table tables.SVG) (svg, error) {
rawData := table.SVGDocumentList.SVGRawData
out := make(svg, len(table.SVGDocumentList.DocumentRecords))
for i, rec := range table.SVGDocumentList.DocumentRecords {
start, end := rec.SvgDocOffset, rec.SvgDocOffset+tables.Offset32(rec.SvgDocLength)
if len(rawData) < int(end) {
return nil, fmt.Errorf("invalid svg table (EOF: expected %d, got %d)", end, len(rawData))
}
out[i] = svgDocument{
first: rec.StartGlyphID,
last: rec.EndGlyphID,
svg: rawData[start:end],
}
}
return out, nil
}
type svgDocument struct {
// svg document
// each glyph description must be written
// in an element with id=glyphXXX
svg []byte
first gID // The first glyph ID in the range described by this index entry.
last gID // The last glyph ID in the range described by this index entry. Must be >= startGlyphID.
}
// rawGlyphData returns the SVG document for [gid], or false.
func (s svg) rawGlyphData(gid gID) ([]byte, bool) {
// binary search
for i, j := 0, len(s); i < j; {
h := i + (j-i)/2
entry := s[h]
if gid < entry.first {
j = h
} else if entry.last < gid {
i = h + 1
} else {
return entry.svg, true
}
}
return nil, false
}
+596
View File
@@ -0,0 +1,596 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package font
import (
"encoding/binary"
"errors"
"fmt"
"math"
"github.com/go-text/typesetting/font/opentype/tables"
)
// axis records
type fvar []tables.VariationAxisRecord
func newFvar(table tables.Fvar) fvar { return table.FvarRecords.Axis }
type mvar struct {
store tables.ItemVarStore
values []tables.VarValueRecord
}
func newMvar(mv tables.MVAR, axisCount int) (mvar, error) {
if got := mv.ItemVariationStore.AxisCount(); got != axisCount {
return mvar{}, fmt.Errorf("mvar: invalid number of axis (%d != %d)", got, axisCount)
}
return mvar{mv.ItemVariationStore, mv.ValueRecords}, nil
}
// return 0 if `tag` is not found
func (mv mvar) getVar(tag Tag, coords []VarCoord) float32 {
// binary search
for i, j := 0, len(mv.values); i < j; {
h := i + (j-i)/2
entry := mv.values[h]
if tag < entry.ValueTag {
j = h
} else if entry.ValueTag < tag {
i = h + 1
} else {
return mv.store.GetDelta(entry.Index, coords)
}
}
return 0
}
// ---------------------------------- gvar ----------------------------------
type gvar struct {
sharedTuples [][]VarCoord // with size tupleCount x axisCount
variations [][]tupleVariation // with length glyphCount
sharedTupleActiveIdx []int // with length tupleCount
}
func newGvar(table tables.Gvar, glyf tables.Glyf) (gvar, error) {
if len(table.GlyphVariationDatas) != len(glyf) {
return gvar{}, fmt.Errorf("invalid 'gvar' table: mismatch in glyphs count")
}
out := gvar{
sharedTuples: make([][]VarCoord, len(table.SharedTuples.SharedTuples)),
variations: make([][]tupleVariation, len(table.GlyphVariationDatas)),
sharedTupleActiveIdx: make([]int, len(table.SharedTuples.SharedTuples)),
}
for i, ts := range table.SharedTuples.SharedTuples {
out.sharedTuples[i] = ts.Values
}
for i, vs := range table.GlyphVariationDatas {
tvs := make([]tupleVariation, len(vs.TupleVariationHeaders))
for j, header := range vs.TupleVariationHeaders {
tvs[j].TupleVariationHeader = header
}
pointsNumberCountAll := pointNumbersCount(glyf[i]) + phantomCount
err := parseGlyphVariationSerializedData(vs.SerializedData,
vs.HasSharedPointNumbers(), pointsNumberCountAll, false, tvs)
if err != nil {
return out, err
}
out.variations[i] = tvs
}
// For shared tuples that only have one axis active, share the index of
// that axis as a cache. This will speed up caclulateScalar() a lot
// for fonts with lots of axes and many "monovar" tuples.
for i, tuple := range out.sharedTuples {
idx := -1
for j, peak := range tuple {
if peak != 0 {
if idx != -1 { // two peaks or more, do not cache
idx = -1
break
}
idx = j
}
}
out.sharedTupleActiveIdx[i] = idx
}
return out, nil
}
type tupleVariation struct {
tables.TupleVariationHeader
pointNumbers []uint16 // nil means allPointsNumbers
// length 2*len(pointNumbers) for gvar table or 2*allPointsNumbers if zero
deltas []int16
}
// sharedTuples has length tupleCount x axisCount
// sharedTupleActiveIdx has length tupleCount
func (t tupleVariation) calculateScalar(coords []VarCoord, sharedTuples [][]VarCoord, sharedTupleActiveIdx []int) float32 {
startIdx, endIdx := 0, len(coords)
peakTuple := t.PeakTuple.Values
if peakTuple == nil { // no peak specified -> use shared tuple
index := t.Index()
if int(index) >= len(sharedTuples) { // should not happend
return 0.
}
peakTuple = sharedTuples[index]
// use the cache to restrict the range
if v := sharedTupleActiveIdx[index]; v != -1 {
startIdx = v
endIdx = startIdx + 1
}
}
startTuple, endTuple := t.IntermediateTuples[0].Values, t.IntermediateTuples[1].Values
hasIntermediate := startTuple != nil
var scalar float32 = 1.
for i := startIdx; i < endIdx; i++ {
v, peak := coords[i], peakTuple[i]
if peak == 0 || v == peak {
continue
}
if hasIntermediate {
start := startTuple[i]
end := endTuple[i]
if start > peak || peak > end || (start < 0 && end > 0 && peak != 0) {
continue
}
if v < start || v > end {
return 0.
}
if v < peak {
if peak != start {
scalar *= float32(v-start) / float32(peak-start)
}
} else {
if peak != end {
scalar *= float32(end-v) / float32(end-peak)
}
}
} else if v == 0 || v < minC(0, peak) || v > maxC(0, peak) {
return 0.
} else {
scalar *= float32(v) / float32(peak)
}
}
return scalar
}
// complete `out`, which contains the parsed tuple headers.
// pointNumbersCountAll is used when the tuple variation data provides deltas for all glyph points
func parseGlyphVariationSerializedData(data []byte, hasSharedPoints bool, pointNumbersCountAll int, isCvar bool, out []tupleVariation) error {
var (
sharedPointNumbers []uint16
err error
)
if hasSharedPoints {
sharedPointNumbers, data, err = parsePointNumbers(data)
if err != nil {
return err
}
}
for i, h := range out {
// adjust for the next iteration
if len(data) < int(h.VariationDataSize) {
return errors.New("invalid glyph variation serialized data (EOF)")
}
nextData := data[h.VariationDataSize:]
// default to shared points
privatePointNumbers := sharedPointNumbers
if h.HasPrivatePointNumbers() {
privatePointNumbers, data, err = parsePointNumbers(data)
if err != nil {
return err
}
}
// the number of point is precised or defaut to all the points
pointCount := pointNumbersCountAll
if privatePointNumbers != nil {
pointCount = len(privatePointNumbers)
}
out[i].pointNumbers = privatePointNumbers
if !isCvar {
pointCount *= 2 // for X and Y
}
out[i].deltas, err = unpackDeltas(data, pointCount)
if err != nil {
return err
}
data = nextData
}
return nil
}
// the returned slice is nil if all glyph points are used
func parsePointNumbers(data []byte) ([]uint16, []byte, error) {
count, data, err := getPackedPointCount(data)
if err != nil {
return nil, nil, err
}
if count == 0 {
return nil, data, nil
}
var lastPoint uint16
points := make([]uint16, 0, count) // max value of count is 32767
for len(points) < int(count) { // loop through the runs
if len(data) == 0 {
return nil, nil, errors.New("invalid glyph variation points numbers (EOF)")
}
control := data[0]
is16bit := control&0x80 != 0
runLength := int(control&0x7F + 1)
if is16bit {
pts, err := tables.ParseUint16s(data[1:], runLength)
if err != nil {
return nil, nil, fmt.Errorf("invalid glyph variation points numbers: %s", err)
}
for _, pt := range pts {
actualValue := pt + lastPoint
points = append(points, actualValue)
lastPoint = actualValue
}
data = data[1+2*runLength:]
} else {
if len(data) < 1+runLength {
return nil, nil, errors.New("invalid glyph variation points numbers (EOF)")
}
for _, b := range data[1 : 1+runLength] {
actualValue := uint16(b) + lastPoint
points = append(points, actualValue)
lastPoint = actualValue
}
data = data[1+runLength:]
}
}
return points, data, nil
}
// return the remaining data and special case of 00
func getPackedPointCount(data []byte) (uint16, []byte, error) {
const highOrderBit byte = 1 << 7
if len(data) < 1 {
return 0, nil, errors.New("invalid glyph variation points numbers (EOF)")
}
if data[0] == 0 {
return 0, data[1:], nil
} else if data[0]&highOrderBit == 0 {
count := uint16(data[0])
return count, data[1:], nil
} else {
if len(data) < 2 {
return 0, nil, errors.New("invalid glyph variation points numbers (EOF)")
}
count := uint16(data[0]&^highOrderBit)<<8 | uint16(data[1])
return count, data[2:], nil
}
}
func unpackDeltas(data []byte, pointNumbersCount int) ([]int16, error) {
const (
deltasAreZero = 0x80
deltasAreWords = 0x40
deltaRunCountMask = 0x3F
)
out := make([]int16, pointNumbersCount)
nbRead := 0 // number of point read : out[:nbRead] is valid
// The data is read until the expected logic count of deltas is obtained.
for nbRead < pointNumbersCount {
if len(data) == 0 {
return nil, errors.New("invalid packed deltas (EOF)")
}
control := data[0]
count := control&deltaRunCountMask + 1
if isZero := control&deltasAreZero != 0; isZero {
// no additional value to read, just fill with zeros
nbRead += int(count)
data = data[1:]
} else {
// we want to fill out[nbRead:nbRead+count-1], that is we must have
// nbRead+count-1 < pointNumbersCount, ie
// nbRead+count <= pointNumbersCount
if got := nbRead + int(count); got > pointNumbersCount {
return nil, fmt.Errorf("invalid packed deltas (expected %d point numbers, got %d)", pointNumbersCount, got)
}
isInt16 := control&deltasAreWords != 0
if isInt16 {
if len(data) < 1+2*int(count) {
return nil, errors.New("invalid packed deltas (EOF)")
}
for i := byte(0); i < count; i++ { // count < 64 -> no overflow
out[nbRead] = int16(binary.BigEndian.Uint16(data[1+2*i:]))
nbRead++
}
data = data[1+2*count:]
} else {
if len(data) < 1+int(count) {
return nil, errors.New("invalid packed deltas (EOF)")
}
for i := byte(0); i < count; i++ { // count < 64 -> no overflow
out[nbRead] = int16(int8(data[1+i]))
nbRead++
}
data = data[1+count:]
}
}
}
return out, nil
}
// update `points` in place
func (gvar gvar) applyDeltasToPoints(glyph gID, coords []VarCoord, points []contourPoint) {
// adapted from harfbuzz/src/hb-ot-var-gvar-table.hh
const phantomOnly = false
if int(glyph) >= len(gvar.variations) { // should not happend
return
}
// save original points for inferred delta calculation
origPoints := append([]contourPoint(nil), points...)
// flag is used to indicate referenced point
deltas := make([]contourPoint, len(points))
varData := gvar.variations[glyph]
for _, tuple := range varData {
scalar := tuple.calculateScalar(coords, gvar.sharedTuples, gvar.sharedTupleActiveIdx)
if scalar == 0 {
continue
}
L := len(tuple.deltas)
applyToAll := tuple.pointNumbers == nil
xDeltas, yDeltas := tuple.deltas[:L/2], tuple.deltas[L/2:]
// reset the current deltas
for i := range deltas {
deltas[i] = contourPoint{}
}
for i := range xDeltas {
ptIndex := uint16(i)
if !applyToAll {
ptIndex = tuple.pointNumbers[i]
}
deltas[ptIndex].isExplicit = true
deltas[ptIndex].X += float32(xDeltas[i]) * scalar
deltas[ptIndex].Y += float32(yDeltas[i]) * scalar
}
/* infer deltas for unreferenced points */
if !applyToAll && !phantomOnly {
startPoint, endPoint := 0, 0
for {
for endPoint < len(points) && !points[endPoint].isEndPoint {
endPoint++
}
if endPoint == len(points) {
break
}
// check the number of unreferenced points in a contour.
// If no unref points or no ref points, nothing to do.
unrefCount := 0
for _, p := range deltas[startPoint : endPoint+1] {
if !p.isExplicit {
unrefCount++
}
}
j := startPoint
if unrefCount == 0 || unrefCount > endPoint-startPoint {
goto noMoreGaps
}
for {
/* Locate the next gap of unreferenced points between two referenced points prev and next.
* Note that a gap may wrap around at left (startPoint) and/or at right (endPoint).
*/
var prev, next, i int
for {
i = j
j = nextIndex(i, startPoint, endPoint)
if deltas[i].isExplicit && !deltas[j].isExplicit {
break
}
}
prev, j = i, i
for {
i = j
j = nextIndex(i, startPoint, endPoint)
if !deltas[i].isExplicit && deltas[j].isExplicit {
break
}
}
next = j
/* Infer deltas for all unref points in the gap between prev and next */
i = prev
for {
i = nextIndex(i, startPoint, endPoint)
if i == next {
break
}
deltas[i].X = inferDelta(origPoints[i].X, origPoints[prev].X, origPoints[next].X, deltas[prev].X, deltas[next].X)
deltas[i].Y = inferDelta(origPoints[i].Y, origPoints[prev].Y, origPoints[next].Y, deltas[prev].Y, deltas[next].Y)
unrefCount--
if unrefCount == 0 {
goto noMoreGaps
}
}
}
noMoreGaps:
startPoint = endPoint + 1
endPoint = startPoint
}
}
// apply specified / inferred deltas to points
for i, d := range deltas {
points[i].translate(d.X, d.Y)
}
}
}
func nextIndex(i, start, end int) int {
if i >= end {
return start
}
return i + 1
}
func inferDelta(targetVal, prevVal, nextVal, prevDelta, nextDelta float32) float32 {
if prevVal == nextVal {
if prevDelta == nextDelta {
return prevDelta
}
return 0
} else if targetVal <= minF(prevVal, nextVal) {
if prevVal < nextVal {
return prevDelta
}
return nextDelta
} else if targetVal >= maxF(prevVal, nextVal) {
if prevVal > nextVal {
return prevDelta
}
return nextDelta
}
// linear interpolation
r := (targetVal - prevVal) / (nextVal - prevVal)
return prevDelta + r*(nextDelta-prevDelta)
}
func sanitizeGDEF(table tables.GDEF, axisCount int) error {
// check axis count
if got := table.ItemVarStore.AxisCount(); got != -1 && got != axisCount {
return fmt.Errorf("GDEF: invalid number of axis (%d != %d)", axisCount, got)
}
// check LigCarets length
if table.LigCaretList.Coverage != nil {
expected := table.LigCaretList.Coverage.Len()
got := len(table.LigCaretList.LigGlyphs)
if expected != got {
return fmt.Errorf("GDEF: invalid number of lig gyphs (%d != %d)", expected, got)
}
}
return nil
}
// ------------------------------------- external API -------------------------------------
// Variation defines a value for a wanted variation axis.
type Variation struct {
Tag Tag // Variation-axis identifier tag
Value float32 // In design units
}
// SetVariations applies a list of font-variation settings to a font,
// defaulting to the values given in the `fvar` table.
// Note that passing an empty slice will instead remove the coordinates.
func (face *Face) SetVariations(variations []Variation) {
if len(variations) == 0 {
face.SetCoords(nil)
return
}
fv := face.Font.fvar
if len(fv) == 0 { // the font is not variable...
face.SetCoords(nil)
return
}
designCoords := fv.getDesignCoordsDefault(variations)
face.SetCoords(face.NormalizeVariations(designCoords))
}
// getDesignCoordsDefault returns the design coordinates corresponding to the given pairs of axis/value.
// The default value of the axis is used when not specified in the variations.
func (fv fvar) getDesignCoordsDefault(variations []Variation) []float32 {
designCoords := make([]float32, len(fv))
// start with default values
for i, axis := range fv {
designCoords[i] = axis.Default
}
fv.getDesignCoords(variations, designCoords)
return designCoords
}
// getDesignCoords updates the design coordinates, with the given pairs of axis/value.
// It will panic if `designCoords` has not the length expected by the table, that is the number of axis.
func (fv fvar) getDesignCoords(variations []Variation, designCoords []float32) {
for _, variation := range variations {
// allow for multiple axis with the same tag
for index, axis := range fv {
if axis.Tag == variation.Tag {
designCoords[index] = variation.Value
}
}
}
}
// normalize based on the [min,def,max] values for the axis to be [-1,0,1].
func (fv fvar) normalizeCoordinates(coords []float32) []VarCoord {
normalized := make([]VarCoord, len(coords))
for i, a := range fv {
coord := coords[i]
// out of range: clamping
if coord > a.Maximum {
coord = a.Maximum
} else if coord < a.Minimum {
coord = a.Minimum
}
if coord < a.Default {
coord = -(coord - a.Default) / (a.Minimum - a.Default)
} else if coord > a.Default {
coord = (coord - a.Default) / (a.Maximum - a.Default)
} else {
coord = 0
}
normalized[i] = VarCoord(math.Round(float64(coord * 16384))) // 1 << 14
}
return normalized
}
// NormalizeVariations normalize the given design-space coordinates. The minimum and maximum
// values for the axis are mapped to the interval [-1,1], with the default
// axis value mapped to 0.
//
// Any additional scaling defined in the face's `avar` table is also
// applied, as described at https://docs.microsoft.com/en-us/typography/opentype/spec/avar.
//
// This method panics if `coords` has not the correct length, that is the number of axis inf 'fvar'.
func (f *Font) NormalizeVariations(coords []float32) []VarCoord {
// Axis normalization is a two-stage process. First we normalize
// based on the [min,def,max] values for the axis to be [-1,0,1].
// Then, if there's an `avar' table, we renormalize this range.
normalized := f.fvar.normalizeCoordinates(coords)
// now applying 'avar'
for i, av := range f.avar.AxisSegmentMaps {
normalized[i] = av.Map(normalized[i])
}
return normalized
}
+245
View File
@@ -0,0 +1,245 @@
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause
package fontscan
import (
"encoding/xml"
"fmt"
"os"
"path/filepath"
"strings"
)
// fcVars captures the environment configuration that determines how fontconfig resolves configuration
// files. It can be populated from the environment by [fcVarsFromEnv], but is decoupled from the environment
// for ease of testing.
type fcVars struct {
// xdgDataHome is the location of the user's data files, extracted from $XDG_DATA_HOME.
xdgDataHome string
// xdgDataHome is the location of the user's config files, extracted from $XDG_CONFIG_HOME.
xdgConfigHome string
// userHome is the home directory of the current user, resolved from $HOME.
userHome string
// configFile is the name of the configuration file, extracted from $FONTCONFIG_FILE.
configFile string
// paths is the list of configuration paths, extracted from $FONTCONFIG_PATH
paths []string
// sysroot is the root directory of the fontconfig system logically. It is prepended
// to all other paths, and is usually empty.
sysroot string
}
func fcVarsFromEnv() fcVars {
home := os.Getenv("HOME")
return fcVars{
xdgDataHome: getEnvWithDefault("XDG_DATA_HOME", filepath.Join(home, ".local", "share")),
xdgConfigHome: getEnvWithDefault("XDG_CONFIG_HOME", filepath.Join(home, ".config")),
configFile: getEnvWithDefault("FONTCONFIG_FILE", "fonts.conf"),
paths: filepath.SplitList(getEnvWithDefault("$FONTCONFIG_PATH", "/etc/fonts")),
sysroot: os.Getenv("FONTCONFIG_SYSROOT"),
userHome: home,
}
}
// resolveRoot returns the path of the root fontconfig file according to the fcVars.
func (f fcVars) resolveRoot(logger Logger) string {
return f.resolvePath(logger, f.configFile)
}
// resolvePath applies fontconfig's heuristics for finding a path referenced within its config.
func (f fcVars) resolvePath(logger Logger, path string) string {
hasSysroot := len(f.sysroot) > 0
if filepath.IsAbs(path) {
if hasSysroot && !strings.HasPrefix(path, f.sysroot) {
path = filepath.Join(f.sysroot, path)
}
return path
}
if strings.HasPrefix(path, "~") {
path = filepath.Join(f.userHome, strings.TrimPrefix(path, "~"))
if hasSysroot {
path = filepath.Join(f.sysroot, path)
}
return path
}
for _, p := range f.paths {
candidate := filepath.Join(p, path)
if hasSysroot {
candidate = filepath.Join(f.sysroot, candidate)
}
if _, err := os.Stat(candidate); err != nil {
continue
}
return candidate
}
logger.Printf("fontconfig referenced path %q, but it could not be resolved to a real path", path)
return ""
}
func getEnvWithDefault(envVar string, defaultVal string) string {
val, ok := os.LookupEnv(envVar)
if !ok {
return defaultVal
}
return val
}
const (
_ = iota
fcDir
fcInclude
)
// fcDirective is either a <dir> or a <include> element,
// as indicated by [kind]
type fcDirective struct {
dir struct {
Dir string `xml:",chardata"`
Prefix string `xml:"prefix,attr"`
}
include struct {
Include string `xml:",chardata"`
IgnoreMissing string `xml:"ignore_missing,attr"`
Prefix string `xml:"prefix,attr"`
}
kind uint8
}
func (directive *fcDirective) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
switch start.Name.Local {
case "dir":
directive.kind = fcDir
return d.DecodeElement(&directive.dir, &start)
case "include":
directive.kind = fcInclude
return d.DecodeElement(&directive.include, &start)
default:
// ignore the element
return d.Skip()
}
}
// parseFcFile opens and process a FontConfig config file,
// returning the font directories to scan and the (optionnal)
// supplementary config files (or directories) to include.
// The file parameter is expected to already be resolved by
// resolvePath().
func (fc fcVars) parseFcFile(logger Logger, file, currentWorkingDir string) (fontDirs, includes []string, _ error) {
f, err := os.Open(file)
if err != nil {
return nil, nil, fmt.Errorf("opening fontconfig config file: %s", err)
}
defer f.Close()
var config struct {
Fontconfig []fcDirective `xml:",any"`
}
err = xml.NewDecoder(f).Decode(&config)
if err != nil {
return nil, nil, fmt.Errorf("parsing fontconfig config file: %s", err)
}
// post-process : handle "prefix" attr and use absolute path
for _, item := range config.Fontconfig {
switch item.kind {
case fcDir:
dir := item.dir.Dir
switch item.dir.Prefix {
case "default", "cwd":
dir = filepath.Join(currentWorkingDir, dir)
case "relative":
dir = filepath.Join(filepath.Dir(file), dir)
case "xdg":
dir = filepath.Join(fc.xdgDataHome, dir)
}
fontDirs = append(fontDirs, dir)
case fcInclude:
include := item.include.Include
if item.include.Prefix == "xdg" {
include = filepath.Join(fc.xdgConfigHome, include)
}
include = fc.resolvePath(logger, include)
if len(include) > 0 {
includes = append(includes, include)
}
}
}
return
}
// parseFcDir processes all the files in [dir] matching the [09]*.conf pattern
// seen is updated with the processed fontconfig files. The dir parameter is
// expected to already be resolved by resolvePath.
func (fc fcVars) parseFcDir(logger Logger, dir, currentWorkingDir string, seen map[string]bool) (fontDirs, includes []string, _ error) {
entries, err := readDir(dir)
if err != nil {
return nil, nil, fmt.Errorf("reading fontconfig config directory: %s", err)
}
for _, entry := range entries {
if entry.IsDir() {
continue
}
if name := entry.Name(); strings.HasSuffix(name, ".conf") {
c := name[0]
if '0' <= c && c <= '9' {
file := filepath.Join(dir, name)
seen[file] = true
fds, incs, err := fc.parseFcFile(logger, file, currentWorkingDir)
if err != nil {
return nil, nil, err
}
fontDirs = append(fontDirs, fds...)
includes = append(includes, incs...)
}
}
}
return
}
// parseFcConfig recursively parses the fontconfig config file at [rootConfig]
// and its includes, returning the font directories to scan
func (fc fcVars) parseFcConfig(logger Logger) ([]string, error) {
root := fc.resolveRoot(logger)
seen := map[string]bool{root: true}
cwd, err := os.Getwd()
if err != nil {
return nil, fmt.Errorf("processing fontconfig config file: %s", err)
}
// includes is a queue
dirs, includes, err := fc.parseFcFile(logger, root, cwd)
if err != nil {
return nil, err
}
for i := 0; i < len(includes); i++ {
include := includes[i]
if seen[include] {
continue
}
seen[include] = true
fi, err := os.Stat(include)
if err != nil { // gracefully ignore broken includes
logger.Printf("missing fontconfig include %s: skipping", include)
continue
}
var newDirs, newIncludes []string
if fi.IsDir() {
newDirs, newIncludes, err = fc.parseFcDir(logger, include, cwd, seen)
} else {
newDirs, newIncludes, err = fc.parseFcFile(logger, include, cwd)
}
if err != nil {
return nil, err
}
dirs = append(dirs, newDirs...)
includes = append(includes, newIncludes...)
}
return dirs, nil
}
+614
View File
@@ -0,0 +1,614 @@
package fontscan
import (
"fmt"
"log"
"path/filepath"
"sync"
"github.com/go-text/typesetting/font"
ot "github.com/go-text/typesetting/font/opentype"
"github.com/go-text/typesetting/language"
)
type cacheEntry struct {
Location
Family string
font.Aspect
}
// Logger is a type that can log warnings.
type Logger interface {
Printf(format string, args ...interface{})
}
// The family substitution algorithm is copied from fontconfig
// and the match algorithm is inspired from Rust font-kit library
// SystemFonts loads the system fonts, using an index stored in [cacheDir].
// See [FontMap.UseSystemFonts] for more details.
//
// If [logger] is nil, log.Default() is used.
func SystemFonts(logger Logger, cacheDir string) ([]Footprint, error) {
if logger == nil {
logger = log.New(log.Writer(), "fontscan", log.Flags())
}
// safe for concurrent use; subsequent calls are no-ops
err := initSystemFonts(logger, cacheDir)
if err != nil {
return nil, err
}
// systemFonts is read-only, so may be used concurrently
return systemFonts.flatten(), nil
}
// FontMap provides a mechanism to select a [font.Face] from a font description.
// It supports system and user-provided fonts, and implements the CSS font substitutions
// rules.
//
// Note that [FontMap] is NOT safe for concurrent use, but several font maps may coexist
// in an application.
//
// [FontMap] is mainly designed to work with an index built by scanning the system fonts :
// see [UseSystemFonts] for more details.
type FontMap struct {
logger Logger
// caches of already loaded faceCache : the two maps are updated conjointly
firstFace *font.Face
faceCache map[Location]*font.Face
metaCache map[*font.Font]cacheEntry
// the database to query, either loaded from an index
// or populated with the [UseSystemFonts], [AddFont], and/or [AddFace] method.
database fontSet
scriptMap map[language.Script][]int
lru runeLRU
// built holds whether the candidates are populated.
built bool
// the candidates for the current query, which influences ResolveFace output
candidates candidates
// internal buffers used in [buildCandidates]
footprintsBuffer scoredFootprints
cribleBuffer familyCrible
query Query // current query
script language.Script // current script
}
// NewFontMap return a new font map, which should be filled with the `UseSystemFonts`
// or `AddFont` methods. The provided logger will be used to record non-fatal errors
// encountered during font loading. If logger is nil, log.Default() is used.
func NewFontMap(logger Logger) *FontMap {
if logger == nil {
logger = log.New(log.Writer(), "fontscan", log.Flags())
}
fm := &FontMap{
logger: logger,
faceCache: make(map[Location]*font.Face),
metaCache: make(map[*font.Font]cacheEntry),
cribleBuffer: make(familyCrible, 150),
scriptMap: make(map[language.Script][]int),
}
fm.lru.maxSize = 4096
return fm
}
// SetRuneCacheSize configures the size of the cache powering [FontMap.ResolveFace].
// Applications displaying large quantities of text should tune this value to be greater
// than the number of unique glyphs they expect to display at one time in order to achieve
// optimal performance when segmenting text by face rune coverage.
func (fm *FontMap) SetRuneCacheSize(size int) {
fm.lru.maxSize = size
}
// UseSystemFonts loads the system fonts and adds them to the font map.
//
// The first call of this method trigger a rather long scan.
// A per-application on-disk cache is used to speed up subsequent initialisations.
// Callers can provide an appropriate directory path within which this cache may be
// stored. If the empty string is provided, the FontMap will attempt to infer a correct,
// platform-dependent cache path.
//
// NOTE: On Android, callers *must* provide a writable path manually, as it cannot
// be inferred without access to the Java runtime environment of the application.
//
// Multiple font maps may call this method concurrently, without duplicating
// the work of finding the system fonts.
func (fm *FontMap) UseSystemFonts(cacheDir string) error {
// safe for concurrent use; subsequent calls are no-ops
err := initSystemFonts(fm.logger, cacheDir)
if err != nil {
return err
}
// systemFonts is read-only, so may be used concurrently
fm.appendFootprints(systemFonts.flatten()...)
fm.built = false
fm.lru.Clear()
return nil
}
// appendFootprints adds the provided footprints to the database and maps their script
// coverage.
func (fm *FontMap) appendFootprints(footprints ...Footprint) {
startIdx := len(fm.database)
fm.database = append(fm.database, footprints...)
// Insert entries into scriptMap for each footprint's covered scripts.
for i, fp := range footprints {
dbIdx := startIdx + i
for _, script := range fp.Scripts {
fm.scriptMap[script] = append(fm.scriptMap[script], dbIdx)
}
}
}
// systemFonts is a global index of the system fonts.
// initSystemFontsOnce protects the initial assignment,
// and `systemFonts` use is then read-only
var (
systemFonts systemFontsIndex
initSystemFontsOnce sync.Once
)
func cacheDir(userProvided string) (string, error) {
if userProvided != "" {
return userProvided, nil
}
// load an existing index
return platformCacheDir()
}
// initSystemFonts scan the system fonts and update `SystemFonts`.
// If the returned error is nil, `SystemFonts` is guaranteed to contain
// at least one valid font.Face.
// It is protected by sync.Once, and is then safe to use by multiple goroutines.
func initSystemFonts(logger Logger, userCacheDir string) error {
var err error
initSystemFontsOnce.Do(func() {
const cacheFilePattern = "font_index_v%d.cache"
// load an existing index
var dir string
dir, err = cacheDir(userCacheDir)
if err != nil {
return
}
cachePath := filepath.Join(dir, fmt.Sprintf(cacheFilePattern, cacheFormatVersion))
systemFonts, err = refreshSystemFontsIndex(logger, cachePath)
})
return err
}
func refreshSystemFontsIndex(logger Logger, cachePath string) (systemFontsIndex, error) {
fontDirectories, err := DefaultFontDirectories(logger)
if err != nil {
return nil, fmt.Errorf("searching font directories: %s", err)
}
logger.Printf("using system font dirs %q", fontDirectories)
currentIndex, _ := deserializeIndexFile(cachePath)
// if an error occured (the cache file does not exists or is invalid), we start from scratch
updatedIndex, err := scanFontFootprints(logger, currentIndex, fontDirectories...)
if err != nil {
return nil, fmt.Errorf("scanning system fonts: %s", err)
}
// since ResolveFace must always return a valid face, we make sure
// at least one font exists and is valid.
// Otherwise, the font map is useless; this is an extreme case anyway.
err = updatedIndex.assertValid()
if err != nil {
return nil, fmt.Errorf("loading system fonts: %s", err)
}
// write back the index in the cache file
err = updatedIndex.serializeToFile(cachePath)
if err != nil {
return nil, fmt.Errorf("updating cache: %s", err)
}
return updatedIndex, nil
}
// [AddFont] loads the faces contained in [fontFile] and add them to
// the font map.
// [fileID] is used as the [Location.File] entry returned by [FontLocation].
//
// If `familyName` is not empty, it is used as the family name for `fontFile`
// instead of the one found in the font file.
//
// An error is returned if the font resource is not supported.
//
// The order of calls to [AddFont] and [AddFace] determines relative priority
// of manually loaded fonts. See [ResolveFace] for details about when this matters.
func (fm *FontMap) AddFont(fontFile font.Resource, fileID, familyName string) error {
loaders, err := ot.NewLoaders(fontFile)
if err != nil {
return fmt.Errorf("unsupported font resource: %s", err)
}
// eagerly load the faces
faces, err := font.ParseTTC(fontFile)
if err != nil {
return fmt.Errorf("unsupported font resource: %s", err)
}
// by construction of fonts.Loader and fonts.FontDescriptor,
// fontDescriptors and face have the same length
if len(faces) != len(loaders) {
panic("internal error: inconsistent font descriptors and loader")
}
var addedFonts []Footprint
for i, fontDesc := range loaders {
fp, _, err := newFootprintFromLoader(fontDesc, true, scanBuffer{})
// the font won't be usable, just ignore it
if err != nil {
continue
}
fp.Location.File = fileID
fp.Location.Index = uint16(i)
// TODO: for now, we do not handle variable fonts
if familyName != "" {
// give priority to the user provided family
fp.Family = font.NormalizeFamily(familyName)
}
addedFonts = append(addedFonts, fp)
fm.cache(fp, faces[i])
}
if len(addedFonts) == 0 {
return fmt.Errorf("empty font resource %s", fileID)
}
fm.appendFootprints(addedFonts...)
fm.built = false
fm.lru.Clear()
return nil
}
// [AddFace] inserts an already-loaded font.Face into the FontMap. The caller
// is responsible for ensuring that [md] is accurate for the face.
//
// The order of calls to [AddFont] and [AddFace] determines relative priority
// of manually loaded fonts. See [ResolveFace] for details about when this matters.
func (fm *FontMap) AddFace(face *font.Face, location Location, md font.Description) {
fp := newFootprintFromFont(face.Font, location, md)
fm.cache(fp, face)
fm.appendFootprints(fp)
fm.built = false
fm.lru.Clear()
}
func (fm *FontMap) cache(fp Footprint, face *font.Face) {
if fm.firstFace == nil {
fm.firstFace = face
}
fm.faceCache[fp.Location] = face
fm.metaCache[face.Font] = cacheEntry{fp.Location, fp.Family, fp.Aspect}
}
// FontLocation returns the origin of the provided font. If the font was not
// previously returned from this FontMap by a call to ResolveFace, the zero
// value will be returned instead.
func (fm *FontMap) FontLocation(ft *font.Font) Location {
return fm.metaCache[ft].Location
}
// FontMetadata returns a description of the provided font. If the font was not
// previously returned from this FontMap by a call to ResolveFace, the zero
// value will be returned instead.
//
// Note that, for fonts added with [AddFace], it is the user provided description
// that is returned, not the one returned by [Font.Describe]
func (fm *FontMap) FontMetadata(ft *font.Font) (family string, aspect font.Aspect) {
item := fm.metaCache[ft]
return item.Family, item.Aspect
}
// FindSystemFont looks for a system font with the given [family],
// returning the first match, or false is no one is found.
//
// User added fonts are ignored, and the [FontMap] must have been
// initialized with [UseSystemFonts] or this method will always return false.
//
// Family names are compared through [font.Normalize].
func (fm *FontMap) FindSystemFont(family string) (Location, bool) {
family = font.NormalizeFamily(family)
for _, footprint := range fm.database {
if footprint.isUserProvided {
continue
}
if footprint.Family == family {
return footprint.Location, true
}
}
return Location{}, false
}
// FindSystemFonts is the same as FindSystemFont, but returns all matched fonts.
func (fm *FontMap) FindSystemFonts(family string) []Location {
var locations []Location
family = font.NormalizeFamily(family)
for _, footprint := range fm.database {
if footprint.isUserProvided {
continue
}
if footprint.Family == family {
locations = append(locations, footprint.Location)
}
}
return locations
}
// SetQuery set the families and aspect required, influencing subsequent
// [ResolveFace] calls. See also [SetScript].
func (fm *FontMap) SetQuery(query Query) {
if len(query.Families) == 0 {
query.Families = []string{""}
}
fm.query = query
fm.built = false
}
// SetScript set the script to which the (next) runes passed to [ResolveFace]
// belongs, influencing the choice of fallback fonts.
func (fm *FontMap) SetScript(s language.Script) {
fm.script = s
fm.built = false
}
// candidates is a cache storing the indices into FontMap.database of footprints matching a Query
// families
type candidates struct {
// footprints with exact match :
// for each queried family, at most one footprint is selected
withoutFallback []int
// footprints matching the expanded query (where subsitutions have been applied)
withFallback []int
manual []int // manually inserted faces to be tried if the other candidates fail.
}
// reset slices, setting the capacity of withoutFallback to nbFamilies
func (cd *candidates) resetWithSize(nbFamilies int) {
if cap(cd.withoutFallback) < nbFamilies { // reallocate
cd.withoutFallback = make([]int, nbFamilies)
}
cd.withoutFallback = cd.withoutFallback[:0]
cd.withFallback = cd.withFallback[:0]
cd.manual = cd.manual[:0]
}
func (fm *FontMap) buildCandidates() {
if fm.built {
return
}
fm.candidates.resetWithSize(len(fm.query.Families))
// first pass for an exact match
{
for _, family := range fm.query.Families {
candidates := fm.database.selectByFamilyExact(family, fm.cribleBuffer, &fm.footprintsBuffer)
if len(candidates) == 0 {
continue
}
// select the correct aspects
candidates = fm.database.retainsBestMatches(candidates, fm.query.Aspect)
// with no system fallback, the CSS spec says
// that only one font among the candidates must be tried
fm.candidates.withoutFallback = append(fm.candidates.withoutFallback, candidates[0])
}
}
// second pass with substitutions
{
candidates := fm.database.selectByFamilyWithSubs(fm.query.Families, fm.script, fm.cribleBuffer, &fm.footprintsBuffer)
// select the correct aspects
candidates = fm.database.retainsBestMatches(candidates, fm.query.Aspect)
// candidates is owned by fm.footprintsBuffer: copy its content
S := fm.candidates.withFallback
if L := len(candidates); cap(S) < L {
S = make([]int, L)
} else {
S = S[:L]
}
copy(S, candidates)
fm.candidates.withFallback = S
}
// third pass with user provided fonts
{
fm.candidates.manual = fm.database.filterUserProvided(fm.candidates.manual)
fm.candidates.manual = fm.database.retainsBestMatches(fm.candidates.manual, fm.query.Aspect)
}
fm.built = true
}
// returns nil if not candidates supports the rune `r`
func (fm *FontMap) resolveForRune(candidates []int, r rune) *font.Face {
for _, footprintIndex := range candidates {
// check the coverage
if fp := fm.database[footprintIndex]; fp.Runes.Contains(r) {
// try to use the font
face, err := fm.loadFont(fp)
if err != nil { // very unlikely; try another family
fm.logger.Printf("failed loading face: %v", err)
continue
}
return face
}
}
return nil
}
// returns nil if no candidates support the language `lang`
func (fm *FontMap) resolveForLang(candidates []int, lang LangID) *font.Face {
for _, footprintIndex := range candidates {
// check the coverage
if fp := fm.database[footprintIndex]; fp.Langs.Contains(lang) {
// try to use the font
face, err := fm.loadFont(fp)
if err != nil { // very unlikely; try another family
fm.logger.Printf("failed loading face: %v", err)
continue
}
return face
}
}
return nil
}
// ResolveFace select a font based on the current query (set by [FontMap.SetQuery] and [FontMap.SetScript]),
// and supporting the given rune, applying CSS font selection rules.
//
// Fonts are tried with the following steps :
//
// 1 - Only fonts matching exacly one of the [Query.Families] are considered; the list
// is prunned to keep the best match with [Query.Aspect]
// 2 - Fallback fonts are considered, that is fonts with similar families and fonts
// supporting the current script; the list is also prunned according to [Query.Aspect]
// 3 - Fonts added manually by [AddFont] and [AddFace] (prunned according to [Query.Aspect]),
// will be searched, in the order in which they were added.
// 4 - All fonts matching the current script (set by [FontMap.SetScript]) are tried,
// ignoring [Query.Aspect]
//
// If no fonts match after these steps, an arbitrary face will be returned.
// This face will be nil only if the underlying font database is empty,
// or if the file system is broken; otherwise the returned [font.Face] is always valid.
func (fm *FontMap) ResolveFace(r rune) (face *font.Face) {
key := fm.lru.KeyFor(fm.query, fm.script, r)
face, ok := fm.lru.Get(key, fm.query)
if ok {
return face
}
defer func() {
fm.lru.Put(key, fm.query, face)
}()
// Build the candidates if we missed the cache. If they're already built this is a
// no-op.
fm.buildCandidates()
// we first look up for an exact family match, without substitutions
if face := fm.resolveForRune(fm.candidates.withoutFallback, r); face != nil {
return face
}
// if no family has matched so far, try again with system fallback,
// including fonts with matching script and user provided ones
if face := fm.resolveForRune(fm.candidates.withFallback, r); face != nil {
return face
}
// try manually loaded faces even if the typeface doesn't match, looking for matching aspects
// and rune coverage.
// Note that, when [SetScript] has been called, this step is actually not needed,
// since the fonts supporting the given script are already added in [withFallback] fonts
if face := fm.resolveForRune(fm.candidates.manual, r); face != nil {
return face
}
fm.logger.Printf("No font matched for aspect %v, script %s, and rune %U (%c) -> searching by script coverage only", fm.query.Aspect, fm.script, r, r)
scriptCandidates := fm.scriptMap[fm.script]
if face := fm.resolveForRune(scriptCandidates, r); face != nil {
return face
}
fm.logger.Printf("No font matched for script %s and rune %U (%c) -> returning arbitrary face", fm.script, r, r)
// return an arbitrary face
if fm.firstFace == nil && len(fm.database) > 0 {
for _, fp := range fm.database {
face, err := fm.loadFont(fp)
if err != nil {
// very unlikely; warn and keep going
fm.logger.Printf("failed loading face: %v", err)
continue
}
return face
}
}
return fm.firstFace
// refreshSystemFontsIndex makes sure at least one face is valid
// and AddFont also check for valid font files, meaning that
// a valid FontMap should always contain a valid face,
// and we should never return a nil face.
}
// ResolveForLang returns the first face supporting the given language
// (for the actual query), or nil if no one is found.
//
// The matching logic is similar to the one used by [ResolveFace].
func (fm *FontMap) ResolveFaceForLang(lang LangID) *font.Face {
// no-op if already built
fm.buildCandidates()
// we first look up for an exact family match, without substitutions
if face := fm.resolveForLang(fm.candidates.withoutFallback, lang); face != nil {
return face
}
// if no family has matched so far, try again with system fallback
if face := fm.resolveForLang(fm.candidates.withFallback, lang); face != nil {
return face
}
// try manually loaded faces even if the typeface doesn't match, looking for matching aspects
// and rune coverage.
if face := fm.resolveForLang(fm.candidates.manual, lang); face != nil {
return face
}
return nil
}
func (fm *FontMap) loadFont(fp Footprint) (*font.Face, error) {
if face, hasCached := fm.faceCache[fp.Location]; hasCached {
return face, nil
}
// since user provided fonts are added to `faceCache`
// we may now assume the font is stored on the file system
face, err := fp.loadFromDisk()
if err != nil {
return nil, err
}
// add the face to the cache
fm.cache(fp, face)
return face, nil
}
@@ -0,0 +1,10 @@
package fontscan
import "fmt"
func platformCacheDir() (string, error) {
// There is no stable way to infer the proper place to store the cache
// with access to the Java runtime for the application. Rather than
// clutter our API with that, require the caller to provide a path.
return "", fmt.Errorf("user must provide cache directory on android")
}
@@ -0,0 +1,16 @@
//go:build !android && !tinygo
package fontscan
import (
"fmt"
"os"
)
func platformCacheDir() (string, error) {
configDir, err := os.UserCacheDir()
if err != nil {
return "", fmt.Errorf("resolving index cache path: %s", err)
}
return configDir, nil
}
@@ -0,0 +1,18 @@
//go:build tinygo
package fontscan
import (
"fmt"
"os"
"path/filepath"
)
func platformCacheDir() (string, error) {
// if no path is provided we cannot get cache dir with tinygo, so just make one up.
homeDir, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("resolving index cache path: %s", err)
}
return filepath.Join(homeDir, ".cache"), nil
}
+146
View File
@@ -0,0 +1,146 @@
package fontscan
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/go-text/typesetting/font"
ot "github.com/go-text/typesetting/font/opentype"
"github.com/go-text/typesetting/font/opentype/tables"
)
// Location identifies where a font.Face is stored.
type Location = font.FontID
// Footprint is a condensed summary of the main information
// about a font, serving as a lightweight surrogate
// for the original font file.
type Footprint struct {
// Location stores the adress of the font resource.
Location Location
// Family is the general nature of the font, like
// "Arial"
// Note that, for performance reason, we store the
// normalized version of the family name.
Family string
// Runes is the set of runes supported by the font.
Runes RuneSet
// Scripts is the set of scripts deduced from [Runes]
Scripts ScriptSet
// Langs is the set of languages deduced from [Runes]
Langs LangSet
// Aspect precises the visual characteristics
// of the font among a family, like "Bold Italic"
Aspect font.Aspect
// isUserProvided is set to true for fonts add manually to
// a FontMap
// User fonts will always be tried if no other fonts match,
// and will have priority among font with same family name.
//
// This field is not serialized in the index, since it is always false
// for system fonts.
isUserProvided bool
}
func newFootprintFromFont(f *font.Font, location Location, md font.Description) (out Footprint) {
out.Runes, out.Scripts, _ = newCoveragesFromCmap(f.Cmap, nil)
out.Langs = newLangsetFromCoverage(out.Runes)
out.Family = font.NormalizeFamily(md.Family)
out.Aspect = md.Aspect
out.Location = location
out.isUserProvided = true
return out
}
func newFootprintFromLoader(ld *ot.Loader, isUserProvided bool, buffer scanBuffer) (out Footprint, _ scanBuffer, err error) {
raw := buffer.tableBuffer
// since raw is shared, special car must be taken in the parsing order
raw, _ = ld.RawTableTo(ot.MustNewTag("OS/2"), raw)
fp := tables.FPNone
if os2, _, err := tables.ParseOs2(raw); err != nil {
fp = os2.FontPage()
}
// we can use the buffer since ProcessCmap do not keep any reference on
// the input slice
raw, err = ld.RawTableTo(ot.MustNewTag("cmap"), raw)
if err != nil {
return Footprint{}, buffer, err
}
tb, _, err := tables.ParseCmap(raw)
if err != nil {
return Footprint{}, buffer, err
}
cmap, _, err := font.ProcessCmap(tb, fp)
if err != nil {
return Footprint{}, buffer, err
}
out.Runes, out.Scripts, buffer.cmapBuffer = newCoveragesFromCmap(cmap, buffer.cmapBuffer) // ... and build the corresponding rune set
out.Langs = newLangsetFromCoverage(out.Runes)
desc, raw := font.Describe(ld, raw)
out.Family = font.NormalizeFamily(desc.Family)
out.Aspect = desc.Aspect
out.isUserProvided = isUserProvided
buffer.tableBuffer = raw
return out, buffer, nil
}
// returns true for .ttf and .ttc font files
func (fp *Footprint) isTruetypeHint() bool {
switch strings.ToLower(filepath.Ext(fp.Location.File)) {
case ".ttf", ".ttc":
return true
default:
return false
}
}
// isMonoHint returns true if "mono" is included in the family name
// this is not very precise but much more efficient than using [font.Font.IsMonospace]
func (fp *Footprint) isMonoHint() bool {
return strings.Contains(fp.Family, "mono")
}
// loadFromDisk assume the footprint location refers to the file system
func (fp *Footprint) loadFromDisk() (*font.Face, error) {
location := fp.Location
file, err := os.Open(location.File)
if err != nil {
return nil, err
}
defer file.Close()
loaders, err := ot.NewLoaders(file)
if err != nil {
return nil, err
}
if index := int(location.Index); len(loaders) <= index {
// this should only happen if the font file as changed
// since the last scan (very unlikely)
return nil, fmt.Errorf("invalid font index in collection: %d >= %d", index, len(loaders))
}
ft, err := font.NewFont(loaders[location.Index])
if err != nil {
return nil, fmt.Errorf("reading font at %s: %s", location.File, err)
}
return font.NewFace(ft), nil
}

Some files were not shown because too many files have changed in this diff Show More