package font import ( "encoding/json" "fmt" "image" "image/color" "image/png" "io" "strings" ) // AtlasGlyph is one glyph's placement in the atlas. type AtlasGlyph struct { X int `json:"x"` Y int `json:"y"` W int `json:"w"` H int `json:"h"` Advance int `json:"advance"` // cursor movement after drawing: W + spacing } // Metrics is the JSON sidecar written next to the atlas PNG. type Metrics struct { Name string `json:"name"` Atlas string `json:"atlas"` Height int `json:"height"` LineHeight int `json:"lineHeight"` Baseline int `json:"baseline"` Spacing int `json:"spacing"` SpaceWidth int `json:"spaceWidth"` Glyphs map[string]AtlasGlyph `json:"glyphs"` } // BuildAtlas packs all glyphs into a grid atlas image (white pixels on // transparency, so game engines can tint) and returns the metrics. // atlasName is stored in the metrics so loaders can find the image. func (ft *Font) BuildAtlas(atlasName string) (*image.NRGBA, *Metrics) { n := len(ft.Order) cols := 1 for cols*cols < n { cols++ } rows := (n + cols - 1) / cols cellW := ft.MaxGlyphWidth() + 1 // 1 px padding against bleed cellH := ft.Height + 1 img := image.NewNRGBA(image.Rect(0, 0, cols*cellW, rows*cellH)) white := color.NRGBA{255, 255, 255, 255} m := &Metrics{ Name: ft.Name, Atlas: atlasName, Height: ft.Height, LineHeight: ft.LineHeight, Baseline: ft.Baseline, Spacing: ft.Spacing, SpaceWidth: ft.SpaceWidth, Glyphs: map[string]AtlasGlyph{}, } for i, r := range ft.Order { g := ft.Glyphs[r] x0 := (i % cols) * cellW y0 := (i / cols) * cellH drawGlyph(img, g, x0, y0, white) m.Glyphs[string(r)] = AtlasGlyph{X: x0, Y: y0, W: g.W, H: g.H, Advance: g.W + ft.Spacing} } return img, m } func drawGlyph(img *image.NRGBA, g *Glyph, x0, y0 int, c color.NRGBA) { for y, row := range g.Rows { for x, on := range row { if on { img.SetNRGBA(x0+x, y0+y, c) } } } } // RenderText draws text into a new image using the font. scale is an // integer upscale factor; unknown characters are skipped with a warning // returned. "\n" (literal backslash-n) and real newlines both break lines. func (ft *Font) RenderText(text string, scale int, fg color.NRGBA) (*image.NRGBA, []string) { if scale < 1 { scale = 1 } text = strings.ReplaceAll(text, `\n`, "\n") lines := strings.Split(text, "\n") var warnings []string width := 0 for _, line := range lines { if w := ft.lineWidth(line); w > width { width = w } } if width == 0 { width = 1 } height := ft.LineHeight*(len(lines)-1) + ft.Height small := image.NewNRGBA(image.Rect(0, 0, width, height)) for li, line := range lines { x := 0 y := li * ft.LineHeight for _, r := range line { if r == ' ' { x += ft.SpaceWidth + ft.Spacing continue } g, ok := ft.Glyphs[r] if !ok { warnings = append(warnings, fmt.Sprintf("no glyph for %q — skipped", string(r))) x += ft.SpaceWidth + ft.Spacing continue } drawGlyph(small, g, x, y, fg) x += g.W + ft.Spacing } } if scale == 1 { return small, warnings } b := small.Bounds() big := image.NewNRGBA(image.Rect(0, 0, b.Dx()*scale, b.Dy()*scale)) for y := 0; y < b.Dy(); y++ { for x := 0; x < b.Dx(); x++ { c := small.NRGBAAt(x, y) for dy := 0; dy < scale; dy++ { for dx := 0; dx < scale; dx++ { big.SetNRGBA(x*scale+dx, y*scale+dy, c) } } } } return big, warnings } func (ft *Font) lineWidth(line string) int { x := 0 for _, r := range line { if r == ' ' { x += ft.SpaceWidth + ft.Spacing continue } if g, ok := ft.Glyphs[r]; ok { x += g.W + ft.Spacing } else { x += ft.SpaceWidth + ft.Spacing } } if x > 0 { x -= ft.Spacing // no trailing spacing } return x } // WritePNG encodes an image as PNG. func WritePNG(w io.Writer, img image.Image) error { return png.Encode(w, img) } // WriteMetrics encodes metrics as indented JSON. func WriteMetrics(w io.Writer, m *Metrics) error { enc := json.NewEncoder(w) enc.SetIndent("", " ") return enc.Encode(m) }