mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 15:06:46 +08:00
222 lines
7.3 KiB
Go
222 lines
7.3 KiB
Go
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package font
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"strings"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
// Face 字体文件中的字体名称与零起始索引,不依赖文件名
|
|
// Family和Style为排版族,MenuFamily和MenuStyle为兼容四款式的菜单名称
|
|
// FamilyKey区分兼容族、厂商和版本,未知时为空;Bold和Italic表示字体固有样式
|
|
type Face struct {
|
|
Index int `json:"index"`
|
|
FullName string `json:"fullName"`
|
|
Family string `json:"family"`
|
|
Style string `json:"style"`
|
|
PostScriptName string `json:"postscriptName"`
|
|
Names []string `json:"names"`
|
|
MenuFamily string `json:"menuFamily"`
|
|
MenuStyle string `json:"menuStyle"`
|
|
FamilyKey string `json:"familyKey"`
|
|
Bold bool `json:"bold"`
|
|
Italic bool `json:"italic"`
|
|
}
|
|
|
|
// FaceInfo 从名称表读取字体信息
|
|
// 入参: data 名称表数据, index 零起始字体索引
|
|
// 返回: Face 字体信息
|
|
func FaceInfo(data []byte, index int) Face {
|
|
return faceInfoLanguage(data, index, 0xffff, 0xffff)
|
|
}
|
|
|
|
// faceInfoLanguage 优先读取指定平台和语言的显示名称,保留标识及全部别名
|
|
// 入参: data 名称表数据, index 字体索引, platform 平台标识, language 语言标识
|
|
// 返回: Face 字体信息
|
|
func faceInfoLanguage(data []byte, index int, platform, language uint16) Face {
|
|
records, values := fontNameValues(data)
|
|
name := func(ids ...uint16) string {
|
|
for _, id := range ids {
|
|
for _, record := range records {
|
|
if record.Name == id {
|
|
if value := strings.TrimSpace(values[[4]uint16{record.Platform, record.Encoding, record.Language, id}]); value != "" {
|
|
return value
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
localized := func(ids ...uint16) string {
|
|
for _, id := range ids {
|
|
for _, record := range records {
|
|
if record.Platform == platform && record.Language == language && record.Name == id {
|
|
if value := strings.TrimSpace(values[[4]uint16{record.Platform, record.Encoding, record.Language, id}]); value != "" {
|
|
return value
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return name(ids...)
|
|
}
|
|
key := ""
|
|
if family, version := name(1), name(5); family != "" && version != "" {
|
|
key = fmt.Sprintf("%x", sha256.Sum256([]byte(strings.Join([]string{family, version, name(8)}, "\x00"))))
|
|
}
|
|
family, style := localized(1), localized(2)
|
|
if family == "" {
|
|
family = localized(16, 4, 6)
|
|
}
|
|
if style == "" {
|
|
style = localized(17)
|
|
}
|
|
return Face{Index: index, FullName: localized(4, 6, 16, 1), Family: localized(16, 1), Style: localized(17, 2), PostScriptName: name(6), Names: fontNamesFromValues(records, values), MenuFamily: family, MenuStyle: style, FamilyKey: key}
|
|
}
|
|
|
|
// TableFaceInfo 读取名称及字体固有款式,名称键不随显示语言变化
|
|
// 入参: tables 字体表, index 集合索引, platform 平台标识, language 语言标识
|
|
// 返回: Face 字体信息
|
|
func TableFaceInfo(tables map[string][]byte, index int, platform, language uint16) Face {
|
|
face := faceInfoLanguage(tables["name"], index, platform, language)
|
|
key := face.FamilyKey
|
|
if head := tables["head"]; len(head) >= 46 {
|
|
key += string(head[4:8])
|
|
style := binary.BigEndian.Uint16(head[44:])
|
|
face.Bold, face.Italic = style&1 != 0, style&2 != 0
|
|
}
|
|
if os2 := tables["OS/2"]; len(os2) >= 64 {
|
|
key += string(os2[58:62])
|
|
style := binary.BigEndian.Uint16(os2[62:])
|
|
face.Bold, face.Italic = style&32 != 0, style&1 != 0 || style&512 != 0
|
|
}
|
|
if face.FamilyKey != "" {
|
|
face.FamilyKey = fmt.Sprintf("%x", sha256.Sum256([]byte(key)))
|
|
}
|
|
return face
|
|
}
|
|
|
|
// FileCount 校验字体或集合头并读取字体数量
|
|
// 入参: data OpenType数据
|
|
// 返回: int 字体数量, error 格式错误
|
|
func FileCount(data []byte) (int, error) {
|
|
if len(data) < 12 {
|
|
return 0, fmt.Errorf("invalid font header")
|
|
}
|
|
switch string(data[:4]) {
|
|
case "\x00\x01\x00\x00", "OTTO", "true":
|
|
return 1, nil
|
|
case "ttcf":
|
|
version := binary.BigEndian.Uint32(data[4:])
|
|
count := uint64(binary.BigEndian.Uint32(data[8:]))
|
|
if (version != 0x00010000 && version != 0x00020000) || count == 0 || count > uint64(len(data)-12)/4 {
|
|
return 0, fmt.Errorf("invalid font collection header")
|
|
}
|
|
return int(count), nil
|
|
default:
|
|
return 0, fmt.Errorf("invalid OpenType header")
|
|
}
|
|
}
|
|
|
|
// ReadFaces 读取已解封装字体或集合的名称信息
|
|
// 入参: data OpenType数据
|
|
// 返回: []Face 字体列表, error 格式或名称表错误
|
|
func ReadFaces(data []byte) ([]Face, error) {
|
|
return ReadLocalizedFaces(data, 0xffff, 0xffff)
|
|
}
|
|
|
|
// ReadLocalizedFaces 读取指定平台和语言的字体名称,缺失时沿用名称表顺序
|
|
// 入参: data OpenType数据, platform 名称表平台标识, language 名称表语言标识
|
|
// 返回: []Face 字体列表, error 格式或名称表错误
|
|
func ReadLocalizedFaces(data []byte, platform, language uint16) ([]Face, error) {
|
|
count, err := FileCount(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
faces := make([]Face, 0, count)
|
|
for index := 0; index < count; index++ {
|
|
tables, err := FileTables(data, index)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
face := TableFaceInfo(tables, index, platform, language)
|
|
if face.FullName == "" {
|
|
return nil, fmt.Errorf("font %d has no usable name", index)
|
|
}
|
|
faces = append(faces, face)
|
|
}
|
|
return faces, nil
|
|
}
|
|
|
|
// ExtractFace 提取已解封装字体中的集合项,规范字符映射并保留原始表
|
|
// 入参: data OpenType数据, index 零起始字体索引
|
|
// 返回: []byte 独立字体数据, error 格式或索引错误
|
|
func ExtractFace(data []byte, index int) ([]byte, error) {
|
|
var err error
|
|
if bytes.HasPrefix(data, []byte("ttcf")) {
|
|
data, err = ExtractCollectionFont(data, index)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
index = 0
|
|
}
|
|
tables, err := FileTables(data, index)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cmap, err := NormalizeCmap(tables["cmap"])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !bytes.Equal(cmap, tables["cmap"]) {
|
|
tables["cmap"] = cmap
|
|
return SerializeOTF(tables)
|
|
}
|
|
return bytes.Clone(data), nil
|
|
}
|
|
|
|
// MissingGlyphs 检查独立字体的字符覆盖,忽略段落换行并按首次出现顺序去重
|
|
// 入参: data 独立OpenType数据, text 待检查文字
|
|
// 返回: string 缺失字符, error 字体或文字格式错误
|
|
func MissingGlyphs(data []byte, text string) (string, error) {
|
|
if !utf8.ValidString(text) {
|
|
return "", fmt.Errorf("text must be UTF-8")
|
|
}
|
|
data, err := DefaultCFF2Font(data)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
sfnt, err := ParseSFNT(data)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
seen := make(map[rune]bool)
|
|
var missing strings.Builder
|
|
for _, char := range text {
|
|
if char == '\r' || char == '\n' || seen[char] {
|
|
continue
|
|
}
|
|
seen[char] = true
|
|
if sfnt.GlyphIndex(char) == 0 {
|
|
missing.WriteRune(char)
|
|
}
|
|
}
|
|
return missing.String(), nil
|
|
}
|