canvas icon indicating copy to clipboard operation
canvas copied to clipboard

getSFNTMetadata bug

Open jilieryuyi opened this issue 1 year ago • 4 comments

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

jilieryuyi avatar Jan 08 '24 08:01 jilieryuyi

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?

tdewolff avatar Jan 08 '24 14:01 tdewolff

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 avatar Jan 09 '24 00:01 jilieryuyi

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

jilieryuyi avatar Jan 09 '24 00:01 jilieryuyi

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

tdewolff avatar Jan 11 '24 19:01 tdewolff