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