Skip to content

Commit 0c27ef9

Browse files
xaionaro@dx.centerxaionaro@dx.center
authored andcommitted
fix: implement font matching in Go via ASystemFontIterator
Replaces the crashing AFontMatcher_match (null FontCollection in libminikin) with a Go implementation that iterates system fonts via ASystemFontIterator and scores by weight distance + italic match. Verified on Pixel 8a: ndkcli font match --family Roboto --weight 400 → Roboto-Regular.ttf ndkcli font match --family Roboto --weight 700 --italic → Roboto-Regular.ttf w=700 italic ndkcli font match --family NotoSans --weight 400 → NotoSansEthiopic-VF.ttf All 17/17 ndkcli commands now pass on real device.
1 parent 8a34d35 commit 0c27ef9

1 file changed

Lines changed: 82 additions & 25 deletions

File tree

cmd/ndkcli/font_workflow.go

Lines changed: 82 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,77 @@ package main
22

33
import (
44
"fmt"
5+
"math"
6+
"path/filepath"
7+
"strings"
58

69
"github.com/spf13/cobra"
710
"github.com/xaionaro-go/ndk/font"
811
)
912

13+
// matchFontFromIterator iterates all system fonts and finds the best
14+
// match for the given family, weight, and italic style. This replaces
15+
// AFontMatcher_match which crashes from headless CLI binaries because
16+
// libminikin's FontCollection is not initialized without an app context.
17+
func matchFontFromIterator(
18+
family string,
19+
weight uint16,
20+
italic bool,
21+
) (*font.Font, error) {
22+
iter := font.ASystemFontIterator_open()
23+
if iter == nil || iter.Pointer() == nil {
24+
return nil, fmt.Errorf("ASystemFontIterator_open returned nil")
25+
}
26+
defer iter.Close()
27+
28+
var bestFont *font.Font
29+
bestScore := math.MaxInt32
30+
31+
for {
32+
f := iter.Next()
33+
if f == nil || f.Pointer() == nil {
34+
break
35+
}
36+
37+
// Check family match via file path (font files are named after families).
38+
path := f.GetFontFilePath()
39+
base := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
40+
baseLower := strings.ToLower(base)
41+
familyLower := strings.ToLower(family)
42+
43+
// Skip fonts that don't match the family name.
44+
// Match by checking if the file name contains the family name.
45+
if !strings.Contains(baseLower, familyLower) &&
46+
!strings.Contains(familyLower, baseLower) {
47+
f.Close()
48+
continue
49+
}
50+
51+
// Score: lower is better.
52+
// Weight distance + italic mismatch penalty.
53+
weightDist := int(weight) - int(f.Weight())
54+
if weightDist < 0 {
55+
weightDist = -weightDist
56+
}
57+
score := weightDist
58+
if f.IsItalic() != italic {
59+
score += 1000
60+
}
61+
62+
if score < bestScore {
63+
if bestFont != nil {
64+
bestFont.Close()
65+
}
66+
bestFont = f
67+
bestScore = score
68+
} else {
69+
f.Close()
70+
}
71+
}
72+
73+
return bestFont, nil
74+
}
75+
1076
var fontMatchCmd = &cobra.Command{
1177
Use: "match",
1278
Short: "Match a font by family name, weight, and italic style",
@@ -15,36 +81,23 @@ var fontMatchCmd = &cobra.Command{
1581
weight, _ := cmd.Flags().GetUint16("weight")
1682
italic, _ := cmd.Flags().GetBool("italic")
1783

18-
matcher := font.NewMatcher()
19-
defer matcher.Close()
20-
21-
fmt.Println("setting style...")
22-
matcher.SetStyle(weight, italic)
23-
matcher.SetLocales("en-US")
24-
fmt.Println("style set OK")
25-
26-
// AFontMatcher_match requires the Android font system (libminikin).
27-
// On headless CLI binaries (no app/Activity), the internal
28-
// FontCollection is null, causing SIGSEGV in getFamilyForChar.
29-
// This command only works within an Android app context.
30-
fmt.Println("WARNING: font matching requires an Android app context (Activity/Service).")
31-
fmt.Println("On headless CLI binaries, AFontMatcher_match will crash with SIGSEGV")
32-
fmt.Println("because the system FontCollection is not initialized.")
33-
text := []uint16{'A'}
34-
var runLength uint32
3584
fmt.Printf("matching family=%q weight=%d italic=%v...\n", family, weight, italic)
3685

37-
matched := matcher.Match(family, &text[0], uint32(len(text)), &runLength)
38-
fmt.Printf("match returned, runLength=%d\n", runLength)
39-
if matched == nil || matched.Pointer() == nil {
86+
matched, err := matchFontFromIterator(family, weight, italic)
87+
if err != nil {
88+
return fmt.Errorf("font matching: %w", err)
89+
}
90+
if matched == nil {
4091
fmt.Println("no matching font found")
4192
return nil
4293
}
4394
defer matched.Close()
4495

4596
fmt.Printf("matched font:\n")
97+
fmt.Printf(" path: %s\n", matched.GetFontFilePath())
4698
fmt.Printf(" weight: %d\n", matched.Weight())
47-
fmt.Printf(" is italic: %v\n", matched.IsItalic())
99+
fmt.Printf(" italic: %v\n", matched.IsItalic())
100+
fmt.Printf(" locale: %s\n", matched.GetLocale())
48101

49102
return nil
50103
},
@@ -54,6 +107,8 @@ var fontListCmd = &cobra.Command{
54107
Use: "list",
55108
Short: "List system fonts using ASystemFontIterator",
56109
RunE: func(cmd *cobra.Command, args []string) (_err error) {
110+
limit, _ := cmd.Flags().GetInt("limit")
111+
57112
iter := font.ASystemFontIterator_open()
58113
if iter == nil || iter.Pointer() == nil {
59114
return fmt.Errorf("ASystemFontIterator_open returned nil")
@@ -66,11 +121,11 @@ var fontListCmd = &cobra.Command{
66121
if f == nil || f.Pointer() == nil {
67122
break
68123
}
69-
fmt.Printf(" [%d] weight=%d italic=%v path=%s\n",
124+
fmt.Printf(" [%d] weight=%d italic=%-5v %s\n",
70125
count, f.Weight(), f.IsItalic(), f.GetFontFilePath())
71126
f.Close()
72127
count++
73-
if count >= 20 {
128+
if limit > 0 && count >= limit {
74129
fmt.Println(" ... (truncated)")
75130
break
76131
}
@@ -81,10 +136,12 @@ var fontListCmd = &cobra.Command{
81136
}
82137

83138
func init() {
84-
fontMatchCmd.Flags().String("family", "sans-serif", "font family name")
85-
fontMatchCmd.Flags().Uint16("weight", uint16(font.Normal), "font weight (100-900)")
139+
fontMatchCmd.Flags().String("family", "Roboto", "font family name (matched against file name)")
140+
fontMatchCmd.Flags().Uint16("weight", 400, "font weight (100-900)")
86141
fontMatchCmd.Flags().Bool("italic", false, "request italic style")
87142

143+
fontListCmd.Flags().Int("limit", 0, "max fonts to list (0=all)")
144+
88145
fontCmd.AddCommand(fontMatchCmd)
89146
fontCmd.AddCommand(fontListCmd)
90147
}

0 commit comments

Comments
 (0)