Files
opencloud/services/thumbnails/pkg/preprocessor/fontloader.go
Jörn Friedrich Dreyer b07b5a1149 use plain pkg module
Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
2025-01-13 16:42:19 +01:00

193 lines
5.0 KiB
Go

package preprocessor
import (
"encoding/json"
"os"
"path/filepath"
"time"
"github.com/opencloud-eu/opencloud/pkg/sync"
"golang.org/x/image/font"
"golang.org/x/image/font/gofont/goregular"
"golang.org/x/image/font/opentype"
)
// FontMap maps a script with the target font to be used for that script
// It also uses a DefaultFont in case there isn't a matching script in the map
//
// For cases like Japanese where multiple scripts are used, we rely on the text
// analyzer to use the script which is unique to japanese (Hiragana or Katakana)
// even if it has to overwrite the "official" detected script (Han). This means
// that "Han" should be used just for chinese while "Hiragana" and "Katakana"
// should be used for japanese
type FontMap struct {
FontMap map[string]string `json:"fontMap"`
DefaultFont string `json:"defaultFont"`
}
// FontMapData contains the location of the loaded file (in FLoc) and the FontMap loaded
// from the file
type FontMapData struct {
FMap *FontMap
FLoc string
}
// LoadedFace contains the location of the font used, and the loaded face (font.Face)
// ready to be used
type LoadedFace struct {
FontFile string
Face font.Face
}
// FontLoader represents a FontLoader. Use the "NewFontLoader" to get a instance
type FontLoader struct {
faceCache sync.Cache
fontMapData *FontMapData
faceOpts *opentype.FaceOptions
}
// NewFontLoader creates a new FontLoader based on the fontMapFile. The FaceOptions will
// be the same for all the font loaded by this instance.
// Note that only the fonts described in the fontMapFile will be used.
//
// The fontMapFile has the following structure
//
// {
// "fontMap": {
// "Han": "packaged/myFont-CJK.otf",
// "Arabic": "packaged/myFont-Arab.otf",
// "Latin": "/fonts/regular/myFont.otf"
// }
// "defaultFont": "/fonts/regular/myFont.otf"
// }
//
// The fontMapFile contains paths to where the fonts are located in the FS.
// Absolute paths can be used as shown above. If a relative path is used,
// it will be relative to the fontMapFile location. This should make the
// packaging easier since all the fonts can be placed in the same directory
// where the fontMapFile is, or in inner directories.
func NewFontLoader(fontMapFile string, faceOpts *opentype.FaceOptions) (*FontLoader, error) {
fontMap := &FontMap{}
if fontMapFile != "" {
file, err := os.Open(fontMapFile)
if err != nil {
return nil, err
}
defer file.Close()
parser := json.NewDecoder(file)
if err = parser.Decode(fontMap); err != nil {
return nil, err
}
}
return &FontLoader{
faceCache: sync.NewCache(5),
fontMapData: &FontMapData{
FMap: fontMap,
FLoc: fontMapFile,
},
faceOpts: faceOpts,
}, nil
}
// LoadFaceForScript loads and returns the font face to be used for that script according to the
// FontMap set when the FontLoader was created. If the script doesn't have
// an associated font, a default font will be used. Note that the default font
// might not be able to handle properly the script
func (fl *FontLoader) LoadFaceForScript(script string) (*LoadedFace, error) {
var parsedFont *opentype.Font
var parsingError error
fontFile := fl.fontMapData.FMap.DefaultFont
if val, ok := fl.fontMapData.FMap.FontMap[script]; ok {
fontFile = val
}
if fontFile != "" && !filepath.IsAbs(fontFile) {
fontFile = filepath.Join(filepath.Dir(fl.fontMapData.FLoc), fontFile)
}
// if the face for the script isn't cached, load the font file and create a new face
cachedFace := fl.faceCache.Load(fontFile)
if cachedFace != nil {
return cachedFace.V.(*LoadedFace), nil
}
if fontFile == "" {
parsedFont, parsingError = opentype.Parse(goregular.TTF)
if parsingError != nil {
return nil, parsingError
}
} else {
// opentype.ParseReaderAt seems to require to keep the file opened
// so read the font file into memory
data, err := os.ReadFile(fontFile)
if err != nil {
return nil, err
}
parsedFont, parsingError = opentype.Parse(data)
if parsingError != nil {
return nil, parsingError
}
}
face, err := opentype.NewFace(parsedFont, fl.faceOpts)
if err != nil {
return nil, err
}
loadedFace := &LoadedFace{
FontFile: fontFile,
Face: face,
}
fl.faceCache.Store(fontFile, loadedFace, time.Now().Add(10*time.Minute))
return loadedFace, nil
}
// GetFaceOptSize returns face opt size
func (fl *FontLoader) GetFaceOptSize() float64 {
return fl.faceOpts.Size
}
// GetFaceOptDPI returns face opt DPI
func (fl *FontLoader) GetFaceOptDPI() float64 {
return fl.faceOpts.DPI
}
// GetScriptList returns script list
func (fl *FontLoader) GetScriptList() []string {
fontMap := fl.fontMapData.FMap.FontMap
arePresent := map[string]bool{
"Common": false,
"Inherited": false,
}
listSize := len(fontMap)
for key := range arePresent {
if _, inFontMap := fontMap[key]; inFontMap {
arePresent[key] = true
} else {
listSize++
}
}
keys := make([]string, listSize)
i := 0
for k := range fontMap {
keys[i] = k
i++
}
for script, isPresent := range arePresent {
if !isPresent {
keys[i] = script
i++
}
}
return keys
}