tdewolff / canvas

Cairo in Go: vector to raster, SVG, PDF, EPS, WASM, OpenGL, Gio, etc.
MIT License
1.46k stars 97 forks source link

getSFNTMetadata bug #264

Open jilieryuyi opened 6 months ago

jilieryuyi commented 6 months ago

There is a bug in getSFNTMetadata the font loading on Windows is incomplete only the first name is used, and the matching method is not sound

msya.ttf names like: image

tdewolff commented 6 months ago

Thanks for raising this issue. Could you be more explicit? I believe the current implementation only looks for English names, and perhaps you're looking for a name in another language (Chinese)? This is more work to implement than it looks like, but I'll verify what would be required to do that.

Regarding the "matching method is not sound". Why? What problem are you having apart from the above?

jilieryuyi commented 6 months ago

Thanks for raising this issue. Could you be more explicit? I believe the current implementation only looks for English names, and perhaps you're looking for a name in another language (Chinese)? This is more work to implement than it looks like, but I'll verify what would be required to do that.

Regarding the "matching method is not sound". Why? What problem are you having apart from the above?

What I want to express is that a font contains multiple names, and they should all support matching these names

I have made the following attempts,but it runs slowly, try to write a simple font.ParseFont api for metadata ?


func FindSystemFonts(dirs []string) (*SystemFonts, error) {
    // TODO: use concurrency
    fonts := &SystemFonts{
        Fonts: map[string]map[Style]FontMetadata{},
    }
    walkedDirs := map[string]bool{}
    walkDir := func(dir string) error {
        return fs.WalkDir(os.DirFS(dir), ".", func(path string, d fs.DirEntry, err error) error {
            path = filepath.Join(dir, path)
            if err != nil {
                return err
            } else if d.IsDir() {
                if walkedDirs[path] {
                    return filepath.SkipDir
                }
                walkedDirs[path] = true
                return nil
            } else if !d.Type().IsRegular() {
                return nil
            }

            b, err := os.ReadFile(path)
            if err != nil {
                return nil
            }
            SFNT, err := font.ParseFont(b, 0)
            if err != nil {
                return nil
            }

            var names []string
            var style string
            for id := 0; id < 25; id++ {
                for _, record := range SFNT.Name.Get(font.NameID(id)) {
                    if id == 1 || id == 4 || id == 6 {
                        names = append(names, record.String())
                    }
                    if id == int(NameFontSubfamily) || id == int(NamePreferredSubfamily) {
                        style = record.String()
                    }
                }
            }
            var metadata FontMetadata //, err := getMetadata(f)

            metadata.Filename = path
            metadata.Families = names
            metadata.Style = ParseStyle(style)

            fonts.Add(metadata)

            return nil
        })
    }

    var Err error
    for _, dir := range dirs {
        if info, err := os.Stat(dir); os.IsNotExist(err) {
            continue
        } else if !info.IsDir() {
            continue
        }

        if err := walkDir(dir); err != nil && Err == nil {
            Err = err
        }
    }
    if Err != nil {
        return nil, Err
    }
    fonts.Generics = DefaultGenericFonts()
    return fonts, nil
}

type FontMetadata struct {
    Filename string
    Families []string
    Style    Style
}

func (s *SystemFonts) Add(metadata FontMetadata) {
    for _, Family := range metadata.Families {
        if _, ok := s.Fonts[Family]; !ok {
            s.Fonts[Family] = map[Style]FontMetadata{}
        }
        s.Fonts[Family][metadata.Style] = metadata
    }
}

func (s *SystemFonts) Match(name string, style Style) (FontMetadata, bool) {
    // expand generic font names
    families := strings.Split(name, ",")
    for i := 0; i < len(families); i++ {
        families[i] = strings.TrimSpace(families[i])
        if names, ok := s.Generics[families[i]]; ok {
            families = append(families[:i], append(names, families[i+1:]...)...)
            i += len(names) - 1
        }
    }

    families = append(families, name+" "+style.String())
    // find the first font name that exists
    var metadatas []map[Style]FontMetadata

    for _, family := range families {
        metadata, _ := s.Fonts[family]
        if metadata != nil {
            metadatas = append(metadatas, metadata)
        }
    }
    if metadatas == nil {
        return FontMetadata{}, false
    }

    // exact style match
    for _, m := range metadatas {
        if metadata, ok := m[style]; ok {
            return metadata, true
        }
    }

    styles := []Style{}
    weight := style.Weight()
    if weight == Regular {
        styles = append(styles, Medium)
    } else if weight == Medium {
        styles = append(styles, Regular)
    }
    if weight == SemiBold || weight == Bold || weight == ExtraBold || weight == Black {
        for s := weight + 1; s <= Black; s++ {
            styles = append(styles, s)
        }
        for s := weight - 1; Thin <= s; s-- {
            styles = append(styles, s)
        }
    } else {
        for s := weight - 1; Thin <= s; s-- {
            styles = append(styles, s)
        }
        for s := weight + 1; s <= Black; s++ {
            styles = append(styles, s)
        }
    }

    for _, s := range styles {
        for _, m := range metadatas {
            if metadata, ok := m[style&Italic|s]; ok {
                return metadata, true
            }
        }
    }
    return FontMetadata{}, false
}
jilieryuyi commented 6 months ago

try to cache system fonts, run faster


// FindSystemFont finds the path to a font from the system's fonts.
func FindSystemFont(name string, style FontStyle) (string, bool) {
    systemFonts.Lock()
    if systemFonts.SystemFonts == nil {
        systemFonts.SystemFonts, _ = loadSystemFontsCache()
        if systemFonts.SystemFonts == nil {
            systemFonts.SystemFonts, _ = font.FindSystemFonts(font.DefaultFontDirs())
            saveSystemFontsCache()
        }
    }
    systemFonts.Unlock()

    font, ok := systemFonts.Match(name, font.ParseStyleCSS(style.CSS(), style.Italic()))
    return font.Filename, ok
}

func loadSystemFontsCache() (*font.SystemFonts, error) {
    cacheDir := os.TempDir()
    cacheFile := "system_fonts.json"

    c := filepath.Join(cacheDir, cacheFile)
    data, err := os.ReadFile(c)
    if err != nil {
        return nil, err
    }

    var fonts font.SystemFonts
    err = json.Unmarshal(data, &fonts)
    if err != nil {
        return nil, err
    }

    return &fonts, err
}

func saveSystemFontsCache() {
    js, err := json.Marshal(systemFonts.SystemFonts)
    if err != nil {
        return
    }

    cacheDir := os.TempDir()
    cacheFile := "system_fonts.json"

    c := filepath.Join(cacheDir, cacheFile)
    os.WriteFile(c, []byte(js), 0777)
}
tdewolff commented 6 months ago

It gets cached in https://github.com/tdewolff/canvas/blob/master/font.go#L192, but perhaps that should move to the font/ subpackage...