maptile/util/fontnik/fontnik.go

370 lines
7.3 KiB
Go

package fontnik
import (
"fmt"
"image"
"image/draw"
"math"
"os"
"path/filepath"
"strings"
"git.zhouxhere.com/zhouxhere/maptile/protobuf"
"github.com/pkg/errors"
"golang.org/x/image/font"
"golang.org/x/image/font/opentype"
"golang.org/x/image/font/sfnt"
"golang.org/x/image/math/fixed"
"google.golang.org/protobuf/proto"
)
const (
DefaultBlockSize = 256
DefaultFontSize = 24
DefaultMaxCode = 0x9FFFF
)
// var (
// BasicLatinBlock = [2]rune{0x0000, 0x007F}
// CJKBlock = [2]rune{0x4E00, 0x9FFF}
// EmojiBlock = [2]rune{0x1F600, 0x1F64F}
// )
type FontFace struct {
Name string
Font font.Face
yStart int
maxCode int
}
type Fontnik struct {
outPath string
FontFaces []*FontFace
}
func NewFontnik(fontPath, outPath string) (*Fontnik, error) {
basePath, err := os.Executable()
if err != nil {
return nil, errors.Errorf("could not get execute path: %v", err)
}
defaultOutPath := "style/font"
if outPath == "" {
outPath = defaultOutPath
}
if err := os.MkdirAll(filepath.Join(filepath.Dir(basePath), outPath), 0644); err != nil {
return nil, err
}
files, err := os.ReadDir(fontPath)
if err != nil {
panic(err)
}
fontFaces := []*FontFace{}
for _, file := range files {
if file.IsDir() {
continue
}
filePath := filepath.Join(fontPath, file.Name())
if !strings.HasSuffix(file.Name(), ".ttf") && !strings.HasSuffix(file.Name(), ".otf") {
// return nil, errors.Errorf("font file not support %s", file.Name())
continue
}
fontBytes, err := os.ReadFile(filePath)
if err != nil {
// return nil, fmt.Errorf("读取字体文件失败: %v", err)
continue
}
openFont, err := opentype.Parse(fontBytes)
if err != nil {
// return nil, fmt.Errorf("解析字体失败: %v", err)
continue
}
fontFamily, err := openFont.Name(&sfnt.Buffer{}, sfnt.NameIDFamily)
if err != nil {
return nil, err
}
fontSubFamily, err := openFont.Name(&sfnt.Buffer{}, sfnt.NameIDSubfamily)
if err != nil {
return nil, err
}
fontFamily = fmt.Sprintf("%s %s", fontFamily, fontSubFamily)
face, err := opentype.NewFace(openFont, &opentype.FaceOptions{
Size: DefaultFontSize,
DPI: 72,
Hinting: font.HintingFull,
})
if err != nil {
return nil, err
}
metrics := face.Metrics()
// fontDesignedHeight := metrics.Ascent.Floor() + metrics.Descent.Floor()
// fixed := int(math.Round(float64(metrics.Height.Floor()-fontDesignedHeight)/2)) + 1
fontFaces = append(fontFaces, &FontFace{
Name: fontFamily,
Font: face,
// yStart: metrics.Height.Floor() + metrics.Descent.Floor() + fixed,
yStart: metrics.Ascent.Floor(),
// maxCode: openFont.NumGlyphs(),
maxCode: DefaultMaxCode,
})
}
return &Fontnik{
outPath: filepath.Join(filepath.Dir(basePath), outPath),
FontFaces: fontFaces,
}, nil
}
func (f *Fontnik) ToPBF() {
if _, err := os.Stat(f.outPath); err != nil {
err = os.MkdirAll(f.outPath, 0644)
if err != nil {
panic(err)
}
}
for _, face := range f.FontFaces {
if _, err := os.Stat(filepath.Join(f.outPath, face.Name)); err != nil {
err = os.MkdirAll(filepath.Join(f.outPath, face.Name), 0644)
if err != nil {
panic(err)
}
}
fmt.Println("Font:", face.Name, "MaxCode:", face.maxCode)
for start := 0; start <= int(face.maxCode); start += DefaultBlockSize {
endCode := min(start+DefaultBlockSize-1, face.maxCode)
stack, err := face.ProcessRange(start, endCode)
if err != nil {
return
}
if err := f.SaveStack(stack); err != nil {
fmt.Println(err)
return
}
}
}
}
func (f *Fontnik) SaveStack(stack *protobuf.Fontstack) error {
data, err := proto.Marshal(&protobuf.Glyphs{Stacks: []*protobuf.Fontstack{stack}})
if err != nil {
return err
}
filename := fmt.Sprintf("%s/%s/%s.pbf",
f.outPath,
*stack.Name,
*stack.Range,
)
return os.WriteFile(filename, data, 0644)
}
func (ff *FontFace) ProcessRange(start, end int) (*protobuf.Fontstack, error) {
fontRange := fmt.Sprintf("%d-%d", start, end)
stack := &protobuf.Fontstack{
Name: &ff.Name,
Range: &fontRange,
Glyphs: []*protobuf.Glyph{},
}
for code := start; code <= end; code++ {
glyph := ff.RenderGlyph(rune(code))
if glyph != nil {
stack.Glyphs = append(stack.Glyphs, glyph)
}
}
return stack, nil
}
func (ff *FontFace) RenderGlyph(code rune) *protobuf.Glyph {
bounds, mask, maskp, advance, ok := ff.Font.Glyph(fixed.P(0, ff.yStart), code)
if !ok {
return nil
}
size := bounds.Size()
width := uint32(size.X)
height := uint32(size.Y)
if width == 0 || height == 0 {
return nil
}
buffer := int(3)
id := uint32(code)
top := -int32(bounds.Min.Y)
left := int32(bounds.Min.X)
a := uint32(advance.Floor())
g := &protobuf.Glyph{
Id: &id,
Width: &width,
Height: &height,
Left: &left,
Top: &top,
Advance: &a,
}
w := int(*g.Width) + buffer*2
h := int(*g.Height) + buffer*2
dst := image.NewRGBA(image.Rect(0, 0, w, h))
draw.DrawMask(dst, dst.Bounds(), &image.Uniform{image.Black}, image.Point{}, mask, maskp.Sub(image.Pt(buffer, buffer)), draw.Over)
g.Bitmap = CalcSDF(dst, 8, 0.25)
return g
}
const INF = 1e20
func CalcSDF(img image.Image, radius float64, cutoff float64) []uint8 {
size := img.Bounds().Size()
w, h := size.X, size.Y
gridOuter := make([]float64, w*h)
gridInner := make([]float64, w*h)
f := make([]float64, w*h)
d := make([]float64, w*h)
v := make([]float64, w*h)
z := make([]float64, w*h)
for y := range h {
for x := range w {
i := x + y*w
_, _, _, a := img.At(x, y).RGBA()
alpha := float64(a) / math.MaxUint16
outer := float64(0)
inner := INF
if alpha != 1 {
if alpha == 0 {
outer = INF
inner = 0
} else {
outer = math.Pow(math.Max(0, 0.5-alpha), 2)
inner = math.Pow(math.Max(0, alpha-0.5), 2)
}
}
gridOuter[i] = outer
gridInner[i] = inner
}
}
edt(gridOuter, w, h, f, d, v, z)
edt(gridInner, w, h, f, d, v, z)
alphas := make([]uint8, w*h)
for y := range h {
for x := range w {
i := x + y*w
d := gridOuter[i] - gridInner[i]
a := math.Max(0, math.Min(255, math.Round(255-255*(d/radius+cutoff))))
alphas[i] = uint8(a)
}
}
return alphas
}
// 2D Euclidean distance transform by Felzenszwalb & Huttenlocher https://cs.brown.edu/~pff/papers/dt-final.pdf
func edt(data []float64, width int, height int, f []float64, d []float64, v []float64, z []float64) {
for x := range width {
for y := range height {
f[y] = data[y*width+x]
}
edt1d(f, d, v, z, height)
for y := range height {
data[y*width+x] = d[y]
}
}
for y := range height {
for x := range width {
f[x] = data[y*width+x]
}
edt1d(f, d, v, z, width)
for x := range width {
data[y*width+x] = math.Sqrt(d[x])
}
}
}
// 1D squared distance transform
func edt1d(f []float64, d []float64, v []float64, z []float64, n int) {
v[0] = 0
z[0] = -INF
z[1] = +INF
for q, k := 1, 0; q < (n); q++ {
getS := func() float64 {
return ((f[q] + float64(q)*float64(q)) - (f[int(v[k])] + v[k]*v[k])) / (2*float64(q) - 2*v[k])
}
s := getS()
for {
if s <= float64(z[k]) {
k--
s = getS()
continue
}
break
}
k++
v[k] = float64(q)
z[k] = float64(s)
z[k+1] = +INF
}
for q, k := 0, 0; q < n; q++ {
for {
if z[k+1] < float64(q) {
k++
continue
}
break
}
d[q] = (float64(q)-v[k])*(float64(q)-v[k]) + f[int(v[k])]
}
}