Skip to content

Commit 801c7e8

Browse files
deadprogramconejoninja
authored andcommitted
cmd/fontpreview: add tool with generated font previews
Add a font preview generator that renders sample PNGs for each bundled font. Signed-off-by: deadprogram <ron@hybridgroup.com>
1 parent ae17b9d commit 801c7e8

13 files changed

Lines changed: 189 additions & 0 deletions

File tree

cmd/fontpreview/main.go

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
// fontpreview renders a sample of each tinyfont font to a PNG in the images/ directory.
2+
// Run from the repo root: go run ./cmd/fontpreview
3+
package main
4+
5+
import (
6+
"fmt"
7+
"image"
8+
"image/color"
9+
"image/png"
10+
"os"
11+
"path/filepath"
12+
13+
"tinygo.org/x/tinyfont"
14+
"tinygo.org/x/tinyfont/freemono"
15+
"tinygo.org/x/tinyfont/freesans"
16+
"tinygo.org/x/tinyfont/freeserif"
17+
"tinygo.org/x/tinyfont/gophers"
18+
"tinygo.org/x/tinyfont/notoemoji"
19+
"tinygo.org/x/tinyfont/notosans"
20+
"tinygo.org/x/tinyfont/proggy"
21+
"tinygo.org/x/tinyfont/shnm"
22+
)
23+
24+
// imgDisplay is an in-memory display backed by an image.RGBA.
25+
type imgDisplay struct {
26+
img *image.RGBA
27+
}
28+
29+
func (d *imgDisplay) SetPixel(x, y int16, c color.RGBA) {
30+
b := d.img.Bounds()
31+
if int(x) >= 0 && int(x) < b.Max.X && int(y) >= 0 && int(y) < b.Max.Y {
32+
d.img.SetRGBA(int(x), int(y), c)
33+
}
34+
}
35+
36+
func (d *imgDisplay) Size() (int16, int16) {
37+
b := d.img.Bounds()
38+
return int16(b.Max.X), int16(b.Max.Y)
39+
}
40+
41+
func (d *imgDisplay) Display() error { return nil }
42+
43+
// measureWidth returns the pixel width of text rendered in font.
44+
func measureWidth(font tinyfont.Fonter, text string) int {
45+
w := 0
46+
for _, r := range text {
47+
g := font.GetGlyph(r)
48+
w += int(g.Info().XAdvance)
49+
}
50+
return w
51+
}
52+
53+
// scaleImage returns a nearest-neighbour upscaled copy of src.
54+
func scaleImage(src *image.RGBA, scale int) *image.RGBA {
55+
b := src.Bounds()
56+
dst := image.NewRGBA(image.Rect(0, 0, b.Max.X*scale, b.Max.Y*scale))
57+
for y := 0; y < b.Max.Y; y++ {
58+
for x := 0; x < b.Max.X; x++ {
59+
c := src.RGBAAt(x, y)
60+
for dy := 0; dy < scale; dy++ {
61+
for dx := 0; dx < scale; dx++ {
62+
dst.SetRGBA(x*scale+dx, y*scale+dy, c)
63+
}
64+
}
65+
}
66+
}
67+
return dst
68+
}
69+
70+
// renderFontRows renders multiple rows of text in font and saves a PNG to outDir/name.png.
71+
// Small fonts are scaled up so they remain visible at a reasonable size.
72+
func renderFontRows(name string, font tinyfont.Fonter, rows []string, outDir string, extraBottomPadding int) error {
73+
const padding = 4
74+
const fgVal = 0x00
75+
const bg = 0xFF
76+
77+
yAdv := int(font.GetYAdvance())
78+
if yAdv == 0 {
79+
yAdv = 16
80+
}
81+
82+
// Calculate max width and total height.
83+
maxW := 0
84+
for _, row := range rows {
85+
w := measureWidth(font, row)
86+
if w > maxW {
87+
maxW = w
88+
}
89+
}
90+
91+
w := maxW + padding*2
92+
h := len(rows)*yAdv + padding*2 + extraBottomPadding
93+
94+
img := image.NewRGBA(image.Rect(0, 0, w, h))
95+
// White background.
96+
white := color.RGBA{bg, bg, bg, 0xFF}
97+
for y := 0; y < h; y++ {
98+
for x := 0; x < w; x++ {
99+
img.SetRGBA(x, y, white)
100+
}
101+
}
102+
103+
d := &imgDisplay{img}
104+
fgColor := color.RGBA{fgVal, fgVal, fgVal, 0xFF}
105+
for i, row := range rows {
106+
y := int16(padding + i*yAdv + yAdv)
107+
tinyfont.WriteLine(d, font, int16(padding), y, row, fgColor)
108+
}
109+
110+
// Scale up small fonts so the image is at least ~48px tall.
111+
scale := 1
112+
switch {
113+
case h <= 16:
114+
scale = 5
115+
case h <= 24:
116+
scale = 4
117+
case h <= 48:
118+
scale = 2
119+
}
120+
if scale > 1 {
121+
img = scaleImage(img, scale)
122+
}
123+
124+
path := filepath.Join(outDir, name+".png")
125+
f, err := os.Create(path)
126+
if err != nil {
127+
return err
128+
}
129+
defer f.Close()
130+
return png.Encode(f, img)
131+
}
132+
133+
func main() {
134+
const outDir = "images"
135+
136+
asciiRows := []string{
137+
"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
138+
"abcdefghijklmnopqrstuvwxyz",
139+
"0123456789",
140+
"!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~",
141+
}
142+
143+
// Gophers font only shows uppercase.
144+
gophersRows := []string{
145+
"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
146+
}
147+
148+
// A selection of emoji known to exist in the 12pt NotoEmoji font.
149+
emojiRows := []string{
150+
"☀♠♥♦⭐❤✅✨🎉🎊🔥💡",
151+
}
152+
153+
// Japanese hiragana characters for shnm font.
154+
japaneseRows := []string{
155+
"あいうえおかきくけこ",
156+
"さしすせそたちつてと",
157+
"なにぬねのはひふへほ",
158+
}
159+
160+
type fontEntry struct {
161+
name string
162+
font tinyfont.Fonter
163+
rows []string
164+
extraBottomPadding int
165+
}
166+
167+
fonts := []fontEntry{
168+
{"freemono", &freemono.Regular12pt7b, asciiRows, 8},
169+
{"freesans", &freesans.Regular12pt7b, asciiRows, 8},
170+
{"freeserif", &freeserif.Regular12pt7b, asciiRows, 8},
171+
{"gophers", &gophers.Regular32pt, gophersRows, 0},
172+
{"notoemoji", &notoemoji.NotoEmojiRegular12pt, emojiRows, 0},
173+
{"notosans", &notosans.Notosans12pt, asciiRows, 0},
174+
{"proggy", &proggy.TinySZ8pt7b, asciiRows, 0},
175+
{"shnm", &shnm.Shnmk12, japaneseRows, 0},
176+
{"org_01", &tinyfont.Org01, asciiRows, 4},
177+
{"picopixel", &tinyfont.Picopixel, asciiRows, 4},
178+
{"tiny3x3a2pt7b", &tinyfont.Tiny3x3a2pt7b, asciiRows, 0},
179+
{"tomthumb", &tinyfont.TomThumb, asciiRows, 4},
180+
}
181+
182+
for _, fe := range fonts {
183+
if err := renderFontRows(fe.name, fe.font, fe.rows, outDir, fe.extraBottomPadding); err != nil {
184+
fmt.Fprintf(os.Stderr, "error rendering %s: %v\n", fe.name, err)
185+
} else {
186+
fmt.Printf("rendered %s → %s/%s.png\n", fe.name, outDir, fe.name)
187+
}
188+
}
189+
}

images/freemono.png

2.95 KB
Loading

images/freesans.png

3.53 KB
Loading

images/freeserif.png

3.44 KB
Loading

images/gophers.png

3.04 KB
Loading

images/notoemoji.png

890 Bytes
Loading

images/notosans.png

1.52 KB
Loading

images/org_01.png

1.07 KB
Loading

images/picopixel.png

996 Bytes
Loading

images/proggy.png

1.47 KB
Loading

0 commit comments

Comments
 (0)