mirror of
https://github.com/bloeys/nterm.git
synced 2025-12-29 14:38:19 +00:00
260 lines
6.5 KiB
Go
Executable File
260 lines
6.5 KiB
Go
Executable File
package glyphs
|
|
|
|
import (
|
|
"errors"
|
|
"image"
|
|
"image/draw"
|
|
"image/png"
|
|
"math"
|
|
"os"
|
|
"unicode"
|
|
|
|
"github.com/bloeys/gglm/gglm"
|
|
"github.com/bloeys/nterm/assert"
|
|
"github.com/golang/freetype/truetype"
|
|
"golang.org/x/image/font"
|
|
"golang.org/x/image/math/fixed"
|
|
)
|
|
|
|
type FontAtlas struct {
|
|
Font *truetype.Font
|
|
Img *image.RGBA
|
|
Glyphs map[rune]FontAtlasGlyph
|
|
|
|
//Advance is global to the atlas because we only support monospaced fonts
|
|
Advance int
|
|
LineHeight int
|
|
SizeUV gglm.Vec2
|
|
}
|
|
|
|
type FontAtlasGlyph struct {
|
|
U float32
|
|
V float32
|
|
|
|
Ascent float32
|
|
Descent float32
|
|
BearingX float32
|
|
}
|
|
|
|
//NewFontAtlasFromFile reads a TTF or TTC file and produces a font texture atlas containing
|
|
//all its characters using the specified options. The atlas uses equally sized tiles
|
|
//such that all characters use an equal horizontal/vertical on the atlas.
|
|
//If the character is smaller than the tile then the rest of the tile is empty.
|
|
//
|
|
//Only monospaced fonts are supported
|
|
func NewFontAtlasFromFile(fontFile string, fontOptions *truetype.Options) (*FontAtlas, error) {
|
|
|
|
fBytes, err := os.ReadFile(fontFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
f, err := truetype.Parse(fBytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
face := truetype.NewFace(f, fontOptions)
|
|
return NewFontAtlasFromFont(f, face, uint(fontOptions.Size))
|
|
}
|
|
|
|
//NewFontAtlasFromFile uses the passed font to produce a font texture atlas containing
|
|
//all its characters using the specified options. The atlas uses equally sized tiles
|
|
//such that all characters use an equal horizontal/vertical on the atlas.
|
|
//If the character is smaller than the tile then the rest of the tile is empty.
|
|
//
|
|
//Only monospaced fonts are supported.
|
|
func NewFontAtlasFromFont(f *truetype.Font, face font.Face, pointSize uint) (*FontAtlas, error) {
|
|
|
|
const maxAtlasSize = 8192
|
|
|
|
glyphs := getGlyphsFromRuneRanges(getGlyphRangesFromFont(f))
|
|
assert.T(len(glyphs) > 0, "no glyphs")
|
|
|
|
//Find advance and line height
|
|
const charPaddingX = 2
|
|
const charPaddingY = 2
|
|
charAdvFixed, _ := face.GlyphAdvance('L')
|
|
charAdv := charAdvFixed.Ceil() + charPaddingX
|
|
|
|
//Find largest vertical character.
|
|
//We don't use face.Metrics().Height because its not reliable
|
|
lineHeightFixed := fixed.Int26_6(0)
|
|
for _, g := range glyphs {
|
|
|
|
gBounds, _, _ := face.GlyphBounds(g)
|
|
ascent := absFixedI26_6(gBounds.Min.Y)
|
|
descent := absFixedI26_6(gBounds.Max.Y)
|
|
|
|
charHeight := ascent + descent
|
|
if charHeight > lineHeightFixed {
|
|
lineHeightFixed = charHeight
|
|
}
|
|
}
|
|
lineHeightFixed = fixed.I(lineHeightFixed.Ceil())
|
|
lineHeight := lineHeightFixed.Ceil()
|
|
|
|
//Calculate needed atlas size
|
|
atlasSizeX := 128
|
|
atlasSizeY := 128
|
|
|
|
maxLinesInAtlas := atlasSizeY/lineHeight - 2
|
|
charsPerLine := atlasSizeX/charAdv - 1
|
|
linesNeeded := int(math.Ceil(float64(len(glyphs))/float64(charsPerLine))) + 1
|
|
|
|
for linesNeeded > maxLinesInAtlas {
|
|
|
|
atlasSizeX *= 2
|
|
atlasSizeY *= 2
|
|
|
|
maxLinesInAtlas = atlasSizeY/lineHeight - 2
|
|
|
|
charsPerLine = atlasSizeX/charAdv - 1
|
|
linesNeeded = int(math.Ceil(float64(len(glyphs))/float64(charsPerLine))) + 1
|
|
}
|
|
|
|
if atlasSizeX > maxAtlasSize {
|
|
return nil, errors.New("atlas size went beyond the maximum of 8192*8192")
|
|
}
|
|
|
|
//Create atlas
|
|
atlasSizeXF32 := float32(atlasSizeX)
|
|
atlasSizeYF32 := float32(atlasSizeY)
|
|
atlas := &FontAtlas{
|
|
Font: f,
|
|
Img: image.NewRGBA(image.Rect(0, 0, atlasSizeX, atlasSizeY)),
|
|
Glyphs: make(map[rune]FontAtlasGlyph, len(glyphs)),
|
|
|
|
Advance: charAdv - charPaddingX,
|
|
LineHeight: lineHeight,
|
|
SizeUV: *gglm.NewVec2(float32(charAdv-charPaddingX)/atlasSizeXF32, float32(lineHeight)/atlasSizeYF32),
|
|
}
|
|
|
|
//Clear background to black
|
|
draw.Draw(atlas.Img, atlas.Img.Bounds(), image.Black, image.Point{}, draw.Src)
|
|
drawer := &font.Drawer{
|
|
Dst: atlas.Img,
|
|
Src: image.White,
|
|
Face: face,
|
|
}
|
|
|
|
//Put glyphs on atlas
|
|
charPaddingXFixed := fixed.I(charPaddingX)
|
|
charPaddingYFixed := fixed.I(charPaddingY)
|
|
|
|
charsOnLine := 0
|
|
drawer.Dot = fixed.P(atlas.Advance+charPaddingX, lineHeight)
|
|
for _, g := range glyphs {
|
|
|
|
gBounds, _, _ := face.GlyphBounds(g)
|
|
ascent := absFixedI26_6(gBounds.Min.Y)
|
|
descent := absFixedI26_6(gBounds.Max.Y)
|
|
bearingX := absFixedI26_6(gBounds.Min.X)
|
|
|
|
atlas.Glyphs[g] = FontAtlasGlyph{
|
|
U: float32((drawer.Dot.X).Floor()) / atlasSizeXF32,
|
|
V: (atlasSizeYF32 - float32((drawer.Dot.Y).Ceil())) / atlasSizeYF32,
|
|
|
|
Ascent: float32(ascent.Ceil()),
|
|
Descent: float32(descent.Ceil()),
|
|
BearingX: float32(bearingX.Ceil()),
|
|
}
|
|
|
|
//Get glyph to draw but undo any applied descent so that the glyph is drawn sitting on the line exactly.
|
|
//Bearing will be applied correctly but descent will be the responsibility of the positioning code
|
|
imgRect, mask, maskp, gAdvanceFixed, _ := face.Glyph(drawer.Dot, g)
|
|
if imgRect.Max.Y > drawer.Dot.Y.Ceil() {
|
|
diff := imgRect.Max.Y - drawer.Dot.Y.Ceil()
|
|
imgRect.Min.Y -= diff
|
|
imgRect.Max.Y -= diff
|
|
}
|
|
|
|
//Draw glyph and advance dot
|
|
draw.DrawMask(drawer.Dst, imgRect, drawer.Src, image.Point{}, mask, maskp, draw.Over)
|
|
drawer.Dot.X += fixed.I(gAdvanceFixed.Ceil()) + charPaddingXFixed
|
|
|
|
charsOnLine++
|
|
if charsOnLine == charsPerLine {
|
|
|
|
charsOnLine = 0
|
|
drawer.Dot.X = fixed.I(atlas.Advance) + charPaddingXFixed
|
|
drawer.Dot.Y += lineHeightFixed + charPaddingYFixed
|
|
}
|
|
}
|
|
|
|
return atlas, nil
|
|
}
|
|
|
|
func SaveImgToPNG(img image.Image, file string) error {
|
|
|
|
outFile, err := os.Create(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer outFile.Close()
|
|
|
|
err = png.Encode(outFile, img)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
//getGlyphRangesFromFont returns a list of ranges, each range is: [i][0]<=range<[i][1]
|
|
func getGlyphRangesFromFont(f *truetype.Font) (ret [][2]rune) {
|
|
|
|
isRuneInPrivateUseArea := func(r rune) bool {
|
|
return 0xe000 <= r && r <= 0xf8ff ||
|
|
0xf0000 <= r && r <= 0xffffd ||
|
|
0x100000 <= r && r <= 0x10fffd
|
|
}
|
|
|
|
rr := [2]rune{-1, -1}
|
|
for r := rune(0); r <= unicode.MaxRune; r++ {
|
|
if isRuneInPrivateUseArea(r) {
|
|
continue
|
|
}
|
|
if f.Index(r) == 0 {
|
|
continue
|
|
}
|
|
if rr[1] == r {
|
|
rr[1] = r + 1
|
|
continue
|
|
}
|
|
if rr[0] != -1 {
|
|
ret = append(ret, rr)
|
|
}
|
|
rr = [2]rune{r, r + 1}
|
|
}
|
|
if rr[0] != -1 {
|
|
ret = append(ret, rr)
|
|
}
|
|
return ret
|
|
}
|
|
|
|
//getGlyphsFromRuneRanges takes ranges of runes and produces an array of all the runes in these ranges
|
|
func getGlyphsFromRuneRanges(ranges [][2]rune) []rune {
|
|
|
|
out := make([]rune, 0)
|
|
for _, rr := range ranges {
|
|
|
|
temp := make([]rune, 0, rr[1]-rr[0])
|
|
for r := rr[0]; r < rr[1]; r++ {
|
|
temp = append(temp, r)
|
|
}
|
|
|
|
out = append(out, temp...)
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
func absFixedI26_6(x fixed.Int26_6) fixed.Int26_6 {
|
|
if x < 0 {
|
|
return -x
|
|
}
|
|
|
|
return x
|
|
}
|