370 lines
7.3 KiB
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])]
|
|
}
|
|
}
|