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])] } }