package card
import (
"errors"
"image"
"image/draw"
"math"
"os"
"os/exec"
"runtime"
"strconv"
"strings"
"sync"
"golang.org/x/image/font"
"golang.org/x/image/font/opentype"
"golang.org/x/image/font/sfnt"
"golang.org/x/image/math/fixed"
)
// Fonts is an ordered fallback chain; the first font with a glyph wins.
// Font files are read through ReaderAt, never loaded whole.
type Fonts struct {
chain []*sfnt.Font
latin int // index in chain of the font for Latin text, -1 for the chain's order
mu sync.Mutex
faces map[faceKey]*face
}
type faceKey struct {
size float64 // device px
thick bool
}
type fontSpec struct {
path string
name string // substring of the full name to pick from a collection; "" takes index
idx int
}
var macFonts = []fontSpec{
{path: "/System/Library/Fonts/ヒラギノ丸ゴ ProN W4.ttc", name: "ProN"},
{path: "/System/Library/Fonts/ヒラギノ角ゴシック W4.ttc"},
{path: "/System/Library/Fonts/Supplemental/Arial Unicode.ttf"},
}
// Rounded faces first: they suit the card. fc-match always answers, so each
// family is only taken when fontconfig returns that family.
var linuxFamilies = []string{
"M PLUS Rounded 1c", "Zen Maru Gothic", "Kosugi Maru",
"Noto Sans CJK JP", "Source Han Sans JP", "Source Han Sans", "IPAexGothic", "VL Gothic",
}
// LoadFonts finds Japanese-capable fonts. $CHIRAMI_FONT ("path" or
// "path:index") goes first when set.
func LoadFonts() (*Fonts, error) {
var specs []fontSpec
if env := os.Getenv("CHIRAMI_FONT"); env != "" {
specs = append(specs, parseSpec(env))
}
if runtime.GOOS == "darwin" {
specs = append(specs, macFonts...)
} else {
specs = append(specs, fontconfigSpecs()...)
}
fs := &Fonts{faces: map[faceKey]*face{}, latin: -1}
if os.Getenv("CHIRAMI_FONT") == "" {
if latin, japanese := shippedFonts(); latin != nil && japanese != nil {
fs.chain, fs.latin = []*sfnt.Font{latin, japanese}, 0
}
}
for _, s := range specs {
if f, err := openFont(s); err == nil && renders(f) {
fs.chain = append(fs.chain, f)
}
}
if len(fs.chain) == 0 {
return nil, errors.New("card: no Japanese font found (set CHIRAMI_FONT=/path/to/font.ttc)")
}
return fs, nil
}
func parseSpec(s string) fontSpec {
if i := strings.LastIndexByte(s, ':'); i > 0 {
if n, err := strconv.Atoi(s[i+1:]); err == nil {
return fontSpec{path: s[:i], idx: n}
}
}
return fontSpec{path: s}
}
func fontconfigSpecs() []fontSpec {
var out []fontSpec
seen := map[string]bool{}
for _, fam := range linuxFamilies {
b, err := exec.Command("fc-match", "-f", "%{family}\n%{file}\n%{index}", fam+":lang=ja").Output()
if err != nil {
return out
}
parts := strings.SplitN(string(b), "\n", 3)
if len(parts) < 3 || !strings.Contains(parts[0], fam) || seen[parts[1]+parts[2]] {
continue
}
seen[parts[1]+parts[2]] = true
idx, _ := strconv.Atoi(strings.TrimSpace(parts[2]))
idx &= 0xffff // the high bits name a variable font's instance
out = append(out, fontSpec{path: parts[1], idx: idx})
}
return out
}
func openFont(s fontSpec) (*sfnt.Font, error) {
f, err := os.Open(s.path)
if err != nil {
return nil, err
}
c, err := sfnt.ParseCollectionReaderAt(f)
if err != nil {
f.Close()
return nil, err
}
idx := s.idx
if s.name != "" {
var buf sfnt.Buffer
for i := 0; i < c.NumFonts(); i++ {
fnt, err := c.Font(i)
if err != nil {
continue
}
if n, _ := fnt.Name(&buf, sfnt.NameIDFull); strings.Contains(n, s.name) {
idx = i
break
}
}
}
if idx >= c.NumFonts() {
idx = 0
}
return c.Font(idx)
}
// renders reports whether f can draw a Japanese glyph: x/image cannot draw
// CFF2 outlines, which variable CJK fonts such as Noto Sans CJK VF use.
func renders(f *sfnt.Font) bool {
ff, err := opentype.NewFace(f, &opentype.FaceOptions{Size: 16, DPI: 72})
if err != nil {
return false
}
defer ff.Close()
_, _, _, _, ok := ff.Glyph(fixed.Point26_6{}, 'あ')
return ok
}
// thickGamma darkens anti-aliased edges of small text on low-density
// displays: light text on the dark card loses its strokes to half-covered
// pixels at 1x. Text from 24 device px up keeps its true coverage.
const thickGamma = 0.7
// face is one font at one device size, with its glyph masks cached.
type face struct {
fonts *Fonts
size float64
thick bool
faces []font.Face // one per chain font, created lazily
glyphs map[glyphKey]*glyph
ascent fixed.Int26_6
height fixed.Int26_6
}
// glyphs are rasterised at a quarter-pixel horizontal phase, so text keeps
// its spacing at 1x instead of every glyph snapping to a whole pixel
const phases = 4
type glyphKey struct {
r rune
phase int
}
type glyph struct {
mask *image.Alpha // nil for blank glyphs
off image.Point // mask origin relative to the dot
adv fixed.Int26_6
}
// face returns the fonts at a device size; thick darkens small text, for
// low-density displays.
func (fs *Fonts) face(sizePx float64, thick bool) *face {
fs.mu.Lock()
defer fs.mu.Unlock()
k := faceKey{sizePx, thick && sizePx < 24}
if f := fs.faces[k]; f != nil {
return f
}
f := &face{fonts: fs, size: sizePx, thick: k.thick, faces: make([]font.Face, len(fs.chain)), glyphs: map[glyphKey]*glyph{}}
// line metrics come from the Japanese font, which the layout is made for
main := 0
if fs.latin == 0 && len(fs.chain) > 1 {
main = 1
}
m := f.chainFace(main).Metrics()
f.ascent, f.height = m.Ascent, m.Ascent+m.Descent
fs.faces[k] = f
return f
}
func (f *face) chainFace(i int) font.Face {
if f.faces[i] == nil {
ff, err := opentype.NewFace(f.fonts.chain[i], &opentype.FaceOptions{Size: f.size, DPI: 72, Hinting: font.HintingNone})
if err != nil {
panic(err) // only fails for invalid options
}
f.faces[i] = ff
}
return f.faces[i]
}
func (f *face) glyph(r rune, phase int) *glyph {
k := glyphKey{r, phase}
if g := f.glyphs[k]; g != nil {
return g
}
g := &glyph{}
var buf sfnt.Buffer
for i, fnt := range f.fonts.chain {
if i == f.fonts.latin && r >= 0x2E80 {
continue // the Latin font draws Latin only, even if it has kana
}
if gi, err := fnt.GlyphIndex(&buf, r); err != nil || gi == 0 {
if i < len(f.fonts.chain)-1 {
continue
}
}
ff := f.chainFace(i)
dr, mask, mp, adv, ok := ff.Glyph(fixed.Point26_6{X: fixed.Int26_6(phase * 64 / phases)}, r)
if !ok {
continue
}
g.adv = adv
if !dr.Empty() {
// opentype reuses its mask buffer, so keep a copy.
a := image.NewAlpha(image.Rect(0, 0, dr.Dx(), dr.Dy()))
draw.Draw(a, a.Bounds(), mask, mp, draw.Src)
if f.thick {
for i, v := range a.Pix {
a.Pix[i] = uint8(255*math.Pow(float64(v)/255, thickGamma) + 0.5)
}
}
g.mask, g.off = a, dr.Min
}
break
}
if len(f.glyphs) > 4096 {
clear(f.glyphs)
}
f.glyphs[k] = g
return g
}
// width of s in device px (26.6).
func (f *face) width(s string) fixed.Int26_6 {
var w fixed.Int26_6
for _, r := range s {
w += f.glyph(r, 0).adv
}
return w
}
// draw s with its baseline at (x, y) device px and returns the advance.
func (f *face) draw(dst *image.RGBA, x, y fixed.Int26_6, s string, src image.Image) fixed.Int26_6 {
start := x
for _, r := range s {
g := f.glyph(r, int(x&63)*phases/64)
if g.mask != nil {
p := image.Pt(x.Floor(), y.Round()).Add(g.off)
draw.DrawMask(dst, g.mask.Bounds().Add(p), src, image.Point{}, g.mask, image.Point{}, draw.Over)
}
x += g.adv
}
return x - start
}