/
fc.go
180 lines (173 loc) · 5.36 KB
/
fc.go
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
package resources
import (
"bufio"
"errors"
"os"
"os/exec"
"path"
"strings"
"sync"
"github.com/npillmayer/schuko/gconf"
"github.com/npillmayer/tyse/core"
"github.com/npillmayer/tyse/core/font"
xfont "golang.org/x/image/font"
)
func findFontConfigBinary() (path string, err error) {
path = gconf.GetString("fontconfig")
if path == "" {
trace().Infof("fontconfig not configured: key 'fontconfig' should point location of 'fc-list' binary")
err = errors.New("fontconfig not configured")
}
return
}
func cacheFontConfigList(update bool) (string, bool) {
appkey := gconf.GetString("app-key")
trace().Debugf("config[app-key] = %s", appkey)
uconfdir, err := os.UserConfigDir()
if appkey == "" || err != nil {
trace().Errorf("user config directory not set")
return "", false
}
fcListFilename := path.Join(uconfdir, appkey, "fontlist.txt")
if _, err := os.Stat(fcListFilename); err == nil {
// fontlist already exists
if !update {
return fcListFilename, true
}
} else { // create config sub-dir for this application
dir := path.Join(uconfdir, appkey)
if _, err = os.Stat(dir); os.IsNotExist(err) {
err = os.MkdirAll(dir, 0755)
if err != nil {
err = core.WrapError(err, core.EINVALID,
"user configuration path cannot be created: %s", dir)
core.UserError(err)
return "", false
}
}
}
fcpath, err := findFontConfigBinary()
if err != nil {
return "", false
}
if !path.IsAbs(fcpath) {
err = core.Error(core.EINVALID, "fontconfig binary fc-list must point to absolute path: %s", fcpath)
core.UserError(err)
return "", false
}
if fi, err := os.Stat(fcpath); err != nil || (fi.Mode().Perm()&0100) == 0 {
err = core.WrapError(err, core.EINVALID,
"fontconfig configuration points to an invalid binary: %s", fcpath)
core.UserError(err)
return "", false
}
fontlistFile, err := os.Create(fcListFilename)
if err == nil {
fccmd := exec.Command(fcpath)
fccmd.Stdout = fontlistFile
err = fccmd.Run()
}
if err != nil {
err = core.WrapError(err, core.EINVALID,
"fontconfig output file cannot be created: %s", fcListFilename)
core.UserError(err)
return "", false
}
return fcListFilename, true
}
func loadFontConfigList() ([]font.Descriptor, bool) {
fclist, ok := cacheFontConfigList(false)
if !ok {
return []font.Descriptor{}, false
}
fc, err := os.Open(fclist)
if err != nil {
err = core.WrapError(err, core.EINVALID,
"fontconfig font list cannot be opened: %s", fclist)
core.UserError(err)
return []font.Descriptor{}, false
}
defer fc.Close()
scanner := bufio.NewScanner(fc)
ttc := 0
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
fields := strings.Split(line, ":")
if len(fields) < 3 {
continue
}
fontpath := strings.TrimSpace(fields[0])
fontname := strings.TrimSpace(fields[1])
fontname = strings.TrimPrefix(fontname, ".")
fontvari := strings.ToLower(fields[2])
if strings.HasSuffix(fontpath, ".ttc") {
ttc++
continue
}
desc := font.Descriptor{
Family: fontname,
Path: fontpath,
}
if strings.Contains(fontvari, "regular") {
desc.Variants = []string{"regular"}
} else if strings.Contains(fontvari, "text") {
desc.Variants = []string{"regular"}
} else if strings.Contains(fontvari, "light") {
desc.Variants = []string{"light"}
} else if strings.Contains(fontvari, "italic") {
desc.Variants = []string{"italic"}
} else if strings.Contains(fontvari, "bold") {
desc.Variants = []string{"bold"}
} else if strings.Contains(fontvari, "black") {
desc.Variants = []string{"bold"}
}
fontConfigDescriptors = append(fontConfigDescriptors, desc)
}
if err = scanner.Err(); err != nil {
err = core.WrapError(err, core.EINVALID,
"encountered a problem during reading of fontconfig font list: %s", fclist)
core.UserError(err)
return fontConfigDescriptors, false
}
if ttc > 0 {
trace().Infof("skipping %d platform fonts: TTC not yet supported", ttc)
}
return fontConfigDescriptors, true
}
var loadFontConfigListTask sync.Once
var loadedFontConfigListOK bool
var fontConfigDescriptors []font.Descriptor
// findFontConfigFont searches for a locally installed font variant using the fontconfig
// system (https://www.freedesktop.org/wiki/Software/fontconfig/).
// fontconfig has to be configured in the global application configuration by
// setting the absolute path of the 'fc-list' binary.
//
// FindFontConfigFont will copy the output of fc-list to the user's config
// directory once. Subsequent calls will use the cached entries to search for
// a font, given a name pattern, a style and a weight.
//
// We call the binary instead of using the C library because of possible version
// issues. If fontconfig is not configured, FindFontConfigFont will silently return an
// empty font descriptor and an empty variant name.
//
func findFontConfigFont(pattern string, style xfont.Style, weight xfont.Weight) (
desc font.Descriptor, variant string) {
//
loadFontConfigListTask.Do(func() {
_, loadedFontConfigListOK = loadFontConfigList()
trace().Infof("loaded fontconfig list")
})
if !loadedFontConfigListOK {
return
}
var confidence font.MatchConfidence
desc, variant, confidence = font.ClosestMatch(fontConfigDescriptors, pattern, style, weight)
trace().Debugf("closest fontconfig match confidence for %s|%s= %d", desc.Family, variant, confidence)
if confidence > font.LowConfidence {
return
}
return font.Descriptor{}, ""
}