367 lines
8.4 KiB
Go
367 lines
8.4 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"image/draw"
|
|
"image/png"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"golang.org/x/image/font"
|
|
"golang.org/x/image/font/basicfont"
|
|
"golang.org/x/image/font/opentype"
|
|
"golang.org/x/image/font/sfnt"
|
|
"golang.org/x/image/math/fixed"
|
|
)
|
|
|
|
type glyphJSON struct {
|
|
C int `json:"c"`
|
|
X int `json:"x"`
|
|
Y int `json:"y"`
|
|
W int `json:"w"`
|
|
H int `json:"h"`
|
|
Advance float32 `json:"advance"`
|
|
OffX float32 `json:"offX"`
|
|
OffY float32 `json:"offY"`
|
|
}
|
|
|
|
type whiteJSON struct {
|
|
X int `json:"x"`
|
|
Y int `json:"y"`
|
|
W int `json:"w"`
|
|
H int `json:"h"`
|
|
}
|
|
|
|
type debugFontJSON struct {
|
|
LineHeight float32 `json:"lineHeight"`
|
|
Base float32 `json:"base"`
|
|
Space float32 `json:"space"`
|
|
Glyphs []glyphJSON `json:"glyphs"`
|
|
White whiteJSON `json:"white"`
|
|
}
|
|
|
|
type kernJSON struct {
|
|
A int32 `json:"a"`
|
|
B int32 `json:"b"`
|
|
K float32 `json:"k"`
|
|
}
|
|
|
|
type uiFontJSON struct {
|
|
Name string `json:"name"`
|
|
Size float32 `json:"size"`
|
|
Scale float32 `json:"scale"`
|
|
LineHeight float32 `json:"lineHeight"`
|
|
Ascent float32 `json:"ascent"`
|
|
Glyphs []glyphJSON `json:"glyphs"`
|
|
Kerning []kernJSON `json:"kerning"`
|
|
White whiteJSON `json:"white"`
|
|
}
|
|
|
|
type bakedGlyph struct {
|
|
r rune
|
|
img *image.Alpha
|
|
dr image.Rectangle
|
|
adv fixed.Int26_6
|
|
x, y int
|
|
}
|
|
|
|
func main() {
|
|
out := flag.String("out", "../../engine/res/fonts", "output directory")
|
|
ttf := flag.String("ttf", "", "OFL font (Inter or Noto Sans) to bake ui atlases from")
|
|
sizes := flag.String("sizes", "12,14,16,20", "ui font sizes in logical pixels")
|
|
scales := flag.String("scales", "1,2", "atlas scales")
|
|
name := flag.String("name", "ui", "ui atlas file prefix")
|
|
flag.Parse()
|
|
|
|
if err := os.MkdirAll(*out, 0o755); err != nil {
|
|
fail(err)
|
|
}
|
|
if err := bakeDebug(*out); err != nil {
|
|
fail(err)
|
|
}
|
|
if *ttf == "" {
|
|
fmt.Println("no -ttf given, ui atlases were not baked")
|
|
return
|
|
}
|
|
data, err := os.ReadFile(*ttf)
|
|
if err != nil {
|
|
fail(err)
|
|
}
|
|
fnt, err := opentype.Parse(data)
|
|
if err != nil {
|
|
fail(err)
|
|
}
|
|
charset := uiCharset()
|
|
for _, size := range parseInts(*sizes) {
|
|
for _, scale := range parseInts(*scales) {
|
|
if err := bakeUI(fnt, *out, *name, size, scale, charset); err != nil {
|
|
fail(err)
|
|
}
|
|
}
|
|
}
|
|
fmt.Println("remember to copy the font license into", filepath.Join(*out, "LICENSE"))
|
|
}
|
|
|
|
func parseInts(s string) []int {
|
|
var out []int
|
|
for _, part := range strings.Split(s, ",") {
|
|
part = strings.TrimSpace(part)
|
|
if part == "" {
|
|
continue
|
|
}
|
|
v, err := strconv.Atoi(part)
|
|
if err != nil || v <= 0 {
|
|
fail(fmt.Errorf("bad number %q", part))
|
|
}
|
|
out = append(out, v)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func uiCharset() []rune {
|
|
var out []rune
|
|
add := func(lo, hi rune) {
|
|
for r := lo; r <= hi; r++ {
|
|
out = append(out, r)
|
|
}
|
|
}
|
|
add(0x20, 0x7E)
|
|
add(0xA0, 0xFF)
|
|
add(0x400, 0x45F)
|
|
add(0x490, 0x491)
|
|
out = append(out,
|
|
0x2013, 0x2014, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2026,
|
|
0x2116, 0x20BD, 0x2190, 0x2191, 0x2192, 0x2193, 0x25B8, 0x25BE,
|
|
0x2713, 0x2715, 0x2318, 0x21E7, 0x2325,
|
|
)
|
|
return out
|
|
}
|
|
|
|
func bakeUI(fnt *opentype.Font, out, name string, size, scale int, charset []rune) error {
|
|
face, err := opentype.NewFace(fnt, &opentype.FaceOptions{
|
|
Size: float64(size * scale),
|
|
DPI: 72,
|
|
Hinting: font.HintingFull,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer face.Close()
|
|
m := face.Metrics()
|
|
ascent := m.Ascent.Ceil()
|
|
lineH := m.Height.Ceil()
|
|
if lineH <= 0 {
|
|
lineH = ascent + m.Descent.Ceil()
|
|
}
|
|
|
|
var buf sfnt.Buffer
|
|
var glyphs []*bakedGlyph
|
|
for _, r := range charset {
|
|
idx, err := fnt.GlyphIndex(&buf, r)
|
|
if err != nil || (idx == 0 && r != ' ') {
|
|
continue
|
|
}
|
|
dr, mask, mp, adv, ok := face.Glyph(fixed.P(0, 0), r)
|
|
if !ok {
|
|
continue
|
|
}
|
|
g := &bakedGlyph{r: r, dr: dr, adv: adv}
|
|
if !dr.Empty() {
|
|
g.img = image.NewAlpha(image.Rect(0, 0, dr.Dx(), dr.Dy()))
|
|
draw.Draw(g.img, g.img.Bounds(), mask, mp, draw.Src)
|
|
}
|
|
glyphs = append(glyphs, g)
|
|
}
|
|
|
|
w, h := packGlyphs(glyphs, 1)
|
|
img := image.NewNRGBA(image.Rect(0, 0, w, h))
|
|
for _, g := range glyphs {
|
|
if g.img == nil {
|
|
continue
|
|
}
|
|
b := g.img.Bounds()
|
|
for y := 0; y < b.Dy(); y++ {
|
|
for x := 0; x < b.Dx(); x++ {
|
|
a := g.img.AlphaAt(x, y).A
|
|
if a == 0 {
|
|
continue
|
|
}
|
|
img.SetNRGBA(g.x+x, g.y+y, color.NRGBA{R: 255, G: 255, B: 255, A: a})
|
|
}
|
|
}
|
|
}
|
|
wx, wy := w-3, h-3
|
|
for y := 0; y < 2; y++ {
|
|
for x := 0; x < 2; x++ {
|
|
img.SetNRGBA(wx+x, wy+y, color.NRGBA{R: 255, G: 255, B: 255, A: 255})
|
|
}
|
|
}
|
|
|
|
s := float32(scale)
|
|
meta := uiFontJSON{
|
|
Name: name + "-" + strconv.Itoa(size),
|
|
Size: float32(size),
|
|
Scale: s,
|
|
LineHeight: float32(lineH) / s,
|
|
Ascent: float32(ascent) / s,
|
|
White: whiteJSON{X: wx, Y: wy, W: 2, H: 2},
|
|
}
|
|
present := make([]rune, 0, len(glyphs))
|
|
for _, g := range glyphs {
|
|
gj := glyphJSON{C: int(g.r), Advance: float32(g.adv) / 64 / s}
|
|
if g.img != nil {
|
|
gj.X, gj.Y = g.x, g.y
|
|
gj.W, gj.H = g.dr.Dx(), g.dr.Dy()
|
|
gj.OffX = float32(g.dr.Min.X) / s
|
|
gj.OffY = float32(g.dr.Min.Y+ascent) / s
|
|
}
|
|
meta.Glyphs = append(meta.Glyphs, gj)
|
|
present = append(present, g.r)
|
|
}
|
|
for _, a := range present {
|
|
for _, b := range present {
|
|
if k := face.Kern(a, b); k != 0 {
|
|
meta.Kerning = append(meta.Kerning, kernJSON{A: int32(a), B: int32(b), K: float32(k) / 64 / s})
|
|
}
|
|
}
|
|
}
|
|
|
|
base := filepath.Join(out, name+"-"+strconv.Itoa(size)+"@"+strconv.Itoa(scale))
|
|
if err := writePNG(base+".png", img); err != nil {
|
|
return err
|
|
}
|
|
blob, err := json.Marshal(meta)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.WriteFile(base+".json", append(blob, '\n'), 0o644); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("wrote %s (%dx%d, %d glyphs, %d kerning pairs)\n", base, w, h, len(meta.Glyphs), len(meta.Kerning))
|
|
return nil
|
|
}
|
|
|
|
func packGlyphs(glyphs []*bakedGlyph, pad int) (int, int) {
|
|
order := make([]*bakedGlyph, 0, len(glyphs))
|
|
for _, g := range glyphs {
|
|
if g.img != nil {
|
|
order = append(order, g)
|
|
}
|
|
}
|
|
sort.Slice(order, func(i, j int) bool {
|
|
if order[i].dr.Dy() != order[j].dr.Dy() {
|
|
return order[i].dr.Dy() > order[j].dr.Dy()
|
|
}
|
|
return order[i].r < order[j].r
|
|
})
|
|
for width := 128; ; width *= 2 {
|
|
x, y, shelf := pad, pad, 0
|
|
ok := true
|
|
for _, g := range order {
|
|
gw, gh := g.dr.Dx()+pad, g.dr.Dy()+pad
|
|
if gw+pad > width {
|
|
ok = false
|
|
break
|
|
}
|
|
if x+gw > width {
|
|
x = pad
|
|
y += shelf
|
|
shelf = 0
|
|
}
|
|
g.x, g.y = x, y
|
|
x += gw
|
|
shelf = max(shelf, gh)
|
|
}
|
|
height := nextPow2(y + shelf + 4)
|
|
if ok && (height <= width || width >= 2048) {
|
|
return width, height
|
|
}
|
|
}
|
|
}
|
|
|
|
func bakeDebug(out string) error {
|
|
const cols, pad = 16, 1
|
|
face := basicfont.Face7x13
|
|
cellW := face.Width + pad*2
|
|
cellH := face.Height + pad*2
|
|
first, last := 32, 126
|
|
count := last - first + 1
|
|
rows := (count + cols - 1) / cols
|
|
imgW := nextPow2(cellW * cols)
|
|
imgH := nextPow2(cellH * rows)
|
|
|
|
img := image.NewNRGBA(image.Rect(0, 0, imgW, imgH))
|
|
meta := debugFontJSON{
|
|
LineHeight: float32(face.Height),
|
|
Base: float32(face.Ascent),
|
|
Space: 7,
|
|
Glyphs: make([]glyphJSON, 0, count),
|
|
}
|
|
d := &font.Drawer{Dst: img, Src: image.NewUniform(color.NRGBA{R: 255, G: 255, B: 255, A: 255}), Face: face}
|
|
for i := 0; i < count; i++ {
|
|
c := rune(first + i)
|
|
cx := (i % cols) * cellW
|
|
cy := (i / cols) * cellH
|
|
adv, ok := face.GlyphAdvance(c)
|
|
if !ok {
|
|
continue
|
|
}
|
|
d.Dot = fixed.P(cx+pad, cy+pad+face.Ascent)
|
|
d.DrawString(string(c))
|
|
meta.Glyphs = append(meta.Glyphs, glyphJSON{
|
|
C: int(c), X: cx + pad, Y: cy + pad, W: face.Width, H: face.Height,
|
|
Advance: float32(adv) / 64,
|
|
})
|
|
if c == ' ' {
|
|
meta.Space = float32(adv) / 64
|
|
}
|
|
}
|
|
wx, wy := imgW-4, imgH-4
|
|
for y := 0; y < 2; y++ {
|
|
for x := 0; x < 2; x++ {
|
|
img.Set(wx+x, wy+y, color.NRGBA{R: 255, G: 255, B: 255, A: 255})
|
|
}
|
|
}
|
|
meta.White = whiteJSON{X: wx, Y: wy, W: 2, H: 2}
|
|
if err := writePNG(filepath.Join(out, "debug.png"), img); err != nil {
|
|
return err
|
|
}
|
|
blob, err := json.MarshalIndent(meta, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(filepath.Join(out, "debug.json"), append(blob, '\n'), 0o644)
|
|
}
|
|
|
|
func writePNG(path string, img image.Image) error {
|
|
f, err := os.Create(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := png.Encode(f, img); err != nil {
|
|
f.Close()
|
|
return err
|
|
}
|
|
return f.Close()
|
|
}
|
|
|
|
func nextPow2(v int) int {
|
|
n := 1
|
|
for n < v {
|
|
n <<= 1
|
|
}
|
|
return n
|
|
}
|
|
|
|
func fail(err error) {
|
|
fmt.Fprintln(os.Stderr, "genfont:", err)
|
|
os.Exit(1)
|
|
}
|