1// Package glyph rasterizes single glyphs of a monospace font into fixed-size
2// cell bitmaps (one byte of coverage per pixel).
3package glyph
4
5import (
6 "fmt"
7 "image"
8 "math"
9 "os"
10 "os/exec"
11 "strings"
12
13 "golang.org/x/image/font"
14 "golang.org/x/image/font/opentype"
15 "golang.org/x/image/math/fixed"
16)
17
18// Style is a set of text attributes. Bold and Italic pick a font file;
19// Underline and Strike are drawn into the glyph bitmap.
20type Style uint8
21
22const (
23 Bold Style = 1 << iota
24 Italic
25 Underline
26 Strike
27)
28
29// fontBits is the part of a Style that selects one of the four font files.
30const fontBits = Bold | Italic
31
32// Paths holds the font file of each weight and slant, indexed by Bold|Italic.
33// An empty entry falls back to the nearest available face.
34type Paths [4]string
35
36// Face rasterizes runes of one font family at one pixel size. All glyphs are
37// clipped to a CellW x CellH box. It is used from one goroutine.
38type Face struct {
39 faces [4]font.Face
40 // CellW and CellH are the cell size in physical pixels.
41 CellW, CellH int
42 ascent fixed.Int26_6
43 ascentPx int
44 thick int // line thickness for underline and strike, in pixels
45}
46
47// fcPatterns are the fontconfig suffixes for Paths' four entries.
48var fcPatterns = [4]string{"", ":bold", ":italic", ":bold:italic"}
49
50// FindFonts asks fontconfig for the four files of a family. Styles the family
51// lacks resolve to whatever fontconfig picks, usually the regular file.
52func FindFonts(family string) (Paths, error) {
53 var p Paths
54 for i, suffix := range fcPatterns {
55 out, err := exec.Command("fc-match", "-f", "%{file}", family+suffix).Output()
56 if err != nil {
57 return p, fmt.Errorf("fontconfig lookup of %q failed: %w", family+suffix, err)
58 }
59 p[i] = strings.TrimSpace(string(out))
60 if p[i] == "" {
61 return p, fmt.Errorf("fontconfig found no file for %q", family+suffix)
62 }
63 }
64 return p, nil
65}
66
67// FamilyOf reports the family fontconfig resolved a request to, so callers can
68// warn when it is not the family that was asked for.
69func FamilyOf(request string) string {
70 out, err := exec.Command("fc-match", "-f", "%{family[0]}", request).Output()
71 if err != nil {
72 return ""
73 }
74 return strings.TrimSpace(string(out))
75}
76
77func loadFace(path string, px float64) (font.Face, error) {
78 data, err := os.ReadFile(path)
79 if err != nil {
80 return nil, fmt.Errorf("glyph: %w", err)
81 }
82 f, err := opentype.Parse(data)
83 if err != nil {
84 return nil, fmt.Errorf("glyph: parse %s: %w", path, err)
85 }
86 return opentype.NewFace(f, &opentype.FaceOptions{Size: px, DPI: 72, Hinting: font.HintingFull})
87}
88
89// Load opens the font files at px pixels. The cell size comes from the regular
90// face: the advance of "M", and ascent plus descent.
91func Load(paths Paths, px float64) (*Face, error) {
92 if px < 4 || px > 512 {
93 return nil, fmt.Errorf("glyph: unsupported font size %.1f px", px)
94 }
95 if paths[0] == "" {
96 return nil, fmt.Errorf("glyph: no regular font file")
97 }
98 f := &Face{}
99 byPath := map[string]font.Face{}
100 for i, path := range paths {
101 if path == "" {
102 continue // filled from a neighbour below
103 }
104 if face, ok := byPath[path]; ok {
105 f.faces[i] = face
106 continue
107 }
108 face, err := loadFace(path, px)
109 if err != nil {
110 f.Close()
111 return nil, err
112 }
113 byPath[path], f.faces[i] = face, face
114 }
115 // A missing bold-italic uses bold, then italic; any other gap uses regular.
116 for i := range f.faces {
117 if f.faces[i] != nil {
118 continue
119 }
120 switch {
121 case Style(i) == Bold|Italic && f.faces[Bold] != nil:
122 f.faces[i] = f.faces[Bold]
123 case Style(i) == Bold|Italic && f.faces[Italic] != nil:
124 f.faces[i] = f.faces[Italic]
125 default:
126 f.faces[i] = f.faces[0]
127 }
128 }
129 m := f.faces[0].Metrics()
130 adv, ok := f.faces[0].GlyphAdvance('M')
131 if !ok || adv <= 0 {
132 f.Close()
133 return nil, fmt.Errorf("glyph: font has no usable advance for 'M'")
134 }
135 f.CellW = int(math.Round(float64(adv) / 64))
136 f.CellH = int(math.Ceil(float64(m.Ascent+m.Descent) / 64))
137 f.ascent = m.Ascent
138 f.ascentPx = int(math.Round(float64(m.Ascent) / 64))
139 f.thick = max(1, int(math.Round(px/14)))
140 if f.CellW < 1 || f.CellH < 1 {
141 f.Close()
142 return nil, fmt.Errorf("glyph: degenerate cell %dx%d", f.CellW, f.CellH)
143 }
144 return f, nil
145}
146
147// Close releases the font faces.
148func (f *Face) Close() {
149 if f == nil {
150 return
151 }
152 closed := map[font.Face]bool{}
153 for i, face := range f.faces {
154 if face != nil && !closed[face] {
155 closed[face] = true
156 face.Close()
157 }
158 f.faces[i] = nil
159 }
160}
161
162// Rasterize draws r into dst, which must hold CellW*CellH bytes (stride
163// CellW). Pixels outside the cell are clipped; a missing glyph leaves dst
164// empty. Underline and Strike add a full-width line in the foreground.
165func (f *Face) Rasterize(r rune, style Style, dst []byte) {
166 clear(dst[:f.CellW*f.CellH])
167 face := f.faces[style&fontBits]
168 if dr, mask, maskp, _, ok := face.Glyph(fixed.Point26_6{X: 0, Y: f.ascent}, r); ok {
169 alpha, isAlpha := mask.(*image.Alpha)
170 for y := dr.Min.Y; y < dr.Max.Y; y++ {
171 if y < 0 || y >= f.CellH {
172 continue
173 }
174 for x := dr.Min.X; x < dr.Max.X; x++ {
175 if x < 0 || x >= f.CellW {
176 continue
177 }
178 mx, my := maskp.X+x-dr.Min.X, maskp.Y+y-dr.Min.Y
179 if isAlpha {
180 dst[y*f.CellW+x] = alpha.Pix[alpha.PixOffset(mx, my)]
181 continue
182 }
183 _, _, _, a := mask.At(mx, my).RGBA()
184 dst[y*f.CellW+x] = uint8(a >> 8)
185 }
186 }
187 }
188 if style&Underline != 0 {
189 // Below the baseline, a third of the way into the descent.
190 descent := f.CellH - f.ascentPx
191 f.line(dst, f.ascentPx+max(1, descent/3))
192 }
193 if style&Strike != 0 {
194 // Through the middle of the lowercase letters.
195 f.line(dst, f.ascentPx-max(1, f.ascentPx*3/10)-f.thick/2)
196 }
197}
198
199// line fills f.thick rows starting at y, clipped to the cell.
200func (f *Face) line(dst []byte, y int) {
201 for row := y; row < y+f.thick; row++ {
202 if row < 0 || row >= f.CellH {
203 continue
204 }
205 for x := 0; x < f.CellW; x++ {
206 dst[row*f.CellW+x] = 255
207 }
208 }
209}