feat(编辑能力): 修复字体编辑错配,优化来源标识
build.yaml / build (push) Successful in 37s

This commit is contained in:
xiaoqidun committed 2026-09-16 19:11:33 +08:00
1 parent 1914b5329a
commit f9646c70e9
8 files changed
+163 -68

No files matched your search

+14
View File
@@ -265,12 +265,26 @@ a:focus-visible {
}
.font-listbox [role="option"] {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
padding: 6px 8px;
line-height: 1.4;
overflow-wrap: anywhere;
cursor: pointer;
}
.font-listbox [role="option"] > span:first-child {
min-width: 0;
}
.font-option-source {
flex: none;
color: var(--text-muted);
font-size: 12px;
}
.font-listbox [role="option"]:hover,
.font-listbox [aria-selected="true"] {
background: var(--accent-soft);
+23 -1
View File
@@ -26,6 +26,11 @@ export class FontPicker {
setFonts(fonts, selected) {
this.close();
this.fonts = fonts;
this.names = new Map();
for (const font of fonts) {
const name = (font.fullName || font.name).normalize("NFKC").toLocaleLowerCase();
this.names.set(name, (this.names.get(name) || 0) + 1);
}
const key = font => font?.id || font?.postscriptName;
this.value = fonts.length ? String(Math.max(0, fonts.findIndex(font => key(font) === key(selected)))) : "";
this.restore();
@@ -39,10 +44,17 @@ export class FontPicker {
restore() {
const font = this.fonts[Number(this.value)];
this.input.value = font?.fullName || font?.name || "";
this.input.title = font ? [this.input.value, this.sourceLabel(font)].filter(Boolean).join(" · ") : "";
this.input.setAttribute("aria-expanded", String(this.open));
this.input.removeAttribute("aria-activedescendant");
}
sourceLabel(font) {
const name = (font.fullName || font.name).normalize("NFKC").toLocaleLowerCase();
if (this.names.get(name) < 2) return "";
return font.embedded ? "内嵌" : font.file || ["stored", "upload"].includes(font.source) ? "上传" : "系统";
}
show(query = "") {
if (this.input.disabled) return;
const normalize = value => value.normalize("NFKC").toLocaleLowerCase().replace(/\s+/g, " ").trim();
@@ -58,7 +70,17 @@ export class FontPicker {
option.dataset.index = String(index);
option.setAttribute("role", "option");
option.setAttribute("aria-disabled", String(Boolean(font.disabled)));
option.textContent = font.fullName || font.name;
const name = document.createElement("span");
name.textContent = font.fullName || font.name;
option.append(name);
const source = this.sourceLabel(font);
if (source) {
const label = document.createElement("span");
label.className = "font-option-source";
label.textContent = source;
option.append(label);
}
option.title = font.name || font.fullName;
this.list.append(option);
}
if (!this.matches.length) {
+3 -12
View File
@@ -758,27 +758,18 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
return objects, nil
}
// editorFont 读取创作对象的完整内嵌字体,供画布输入使用
// editorFont 读取对象实际使用的内嵌字体,供画布输入使用
// 入参: args 字体资源标识
// 返回: any 字体数据, error 错误信息
func editorFont(args []js.Value) (any, error) {
if currentEditor == nil {
return nil, fmt.Errorf("no document is being edited")
}
fonts, err := currentSession.Reader.Fonts()
data, err := currentSession.Reader.FontData(args[0].String())
if err != nil {
return nil, err
}
for _, font := range fonts {
if font.ID == args[0].String() {
data, err := currentSession.Reader.ResData(font.FontFile)
if err != nil {
return nil, err
}
return successResult(map[string]any{"bytes": bytesToJS(data)}), nil
}
}
return nil, fmt.Errorf("font resource %q not found", args[0].String())
return successResult(map[string]any{"bytes": bytesToJS(data)}), nil
}
// replaceImage 替换图片资源并更新预览
+9 -16
View File
@@ -410,23 +410,16 @@ func (e *Editor) editorFont(id string) (*font.SFNT, error) {
return sfnt, nil
}
if e.source != nil {
reader := e.source.reader
if f := reader.fontCache[id]; f != nil && f.FontFile != "" {
data, err := reader.ResData(f.FontFile)
if err != nil {
return nil, err
}
data, err = font.ToSFNT(data)
if err != nil {
return nil, err
}
sfnt, err := font.ParseSFNT(data, 0)
if err != nil {
return nil, err
}
e.fonts[id] = sfnt
return sfnt, nil
data, err := e.source.reader.FontData(id)
if err != nil {
return nil, err
}
sfnt, err := font.ParseSFNT(data, 0)
if err != nil {
return nil, err
}
e.fonts[id] = sfnt
return sfnt, nil
}
return nil, fmt.Errorf("embedded font %q not found", id)
}
+21 -6
View File
@@ -189,7 +189,7 @@ func (fsys *FontFS) matchStyle(names []string, bold, italic bool) []fontFileMatc
// 返回: []fontFileMatch 按优先级排列的字体匹配
func fontFileMatches(candidates []fontFileCandidate, names []string, bold, italic bool) []fontFileMatch {
matches := make([]fontFileMatch, 0, len(candidates))
seen := make(map[string]int, len(candidates))
seen := make(map[fontFileKey]int, len(candidates))
if len(names) == 0 {
for _, name := range fontDefaultSystemNames() {
matches = append(matches, fontFileMatches(candidates, []string{name}, bold, italic)...)
@@ -199,7 +199,7 @@ func fontFileMatches(candidates []fontFileCandidate, names []string, bold, itali
})
unique := matches[:0]
for _, match := range matches {
key := strings.ToLower(match.name)
key := fontFileKey{strings.ToLower(match.name), match.face}
if _, ok := seen[key]; !ok {
seen[key] = len(unique)
unique = append(unique, match)
@@ -278,6 +278,7 @@ func cleanFontName(name string) string {
// fontFileCandidate 字体文件候选
type fontFileCandidate struct {
name string
face int
base string
lowerBase string
normalized string
@@ -421,15 +422,22 @@ func (m fontPatternMatcher) styleSuffixNormalized(name string) string {
// fontFileMatch 字体文件匹配结果
type fontFileMatch struct {
name string
face int
priority int
rank int
styleRank int
sortName string
}
// fontFileKey 字体文件与集合索引的去重键
type fontFileKey struct {
name string
face int
}
// appendFontFileMatch 追加字体文件匹配结果
// 入参: matches 匹配结果, seen 已匹配文件, matcher 匹配器, file 字体文件候选, rank 匹配等级, bold 是否粗体, italic 是否斜体
func appendFontFileMatch(matches *[]fontFileMatch, seen map[string]int, matcher fontPatternMatcher, file fontFileCandidate, rank int, bold, italic bool) {
func appendFontFileMatch(matches *[]fontFileMatch, seen map[fontFileKey]int, matcher fontPatternMatcher, file fontFileCandidate, rank int, bold, italic bool) {
if rank == fontMatchNone {
return
}
@@ -439,18 +447,20 @@ func appendFontFileMatch(matches *[]fontFileMatch, seen map[string]int, matcher
}
next := fontFileMatch{
name: file.name,
face: file.face,
priority: matcher.priority,
rank: rank,
styleRank: fontFileStyleRank(suffix, bold, italic),
sortName: file.lowerBase,
}
if index, ok := seen[file.fold]; ok {
key := fontFileKey{file.fold, file.face}
if index, ok := seen[key]; ok {
if fontFileMatchLess(next, (*matches)[index]) {
(*matches)[index] = next
}
return
}
seen[file.fold] = len(*matches)
seen[key] = len(*matches)
*matches = append(*matches, next)
}
@@ -483,8 +493,13 @@ func fontFileMatchLess(left, right fontFileMatch) bool {
// 返回: []string 字体文件列表
func fontFileMatchNames(matches []fontFileMatch) []string {
names := make([]string, 0, len(matches))
seen := make(map[string]bool, len(matches))
for _, match := range matches {
names = append(names, match.name)
key := strings.ToLower(match.name)
if !seen[key] {
seen[key] = true
names = append(names, match.name)
}
}
return names
}
+25
View File
@@ -15,6 +15,7 @@
package ofdgo
import (
"bytes"
"encoding/xml"
"fmt"
"io"
@@ -75,6 +76,30 @@ func (r *Reader) Fonts() ([]Font, error) {
return fonts, nil
}
// FontData 读取内嵌字体,集合按声明名称和样式提取为独立字体,其他格式保留原始字节
// 入参: id 字体资源标识
// 返回: []byte 字体数据, error 资源缺失或集合解析错误
func (r *Reader) FontData(id string) ([]byte, error) {
if r.fontCache[id] == nil {
if _, err := r.Fonts(); err != nil {
return nil, err
}
}
f := r.fontCache[id]
if f == nil || f.FontFile == "" {
return nil, fmt.Errorf("embedded font %q not found", id)
}
data, err := r.ResData(f.FontFile)
if err != nil {
return nil, err
}
if bytes.HasPrefix(data, []byte("ttcf")) {
index := fontCollectionIndex(data, []string{f.FontName, f.FamilyName}, f.Bold, f.Italic)
return extractCollectionFont(data, index)
}
return data, nil
}
// FontInfos 获取OFD字体诊断信息
// 返回: []FontInfo 字体诊断列表, error 错误信息
func (r *Renderer) FontInfos() ([]FontInfo, error) {
+19 -12
View File
@@ -40,6 +40,7 @@ const (
type fontSource struct {
kind fontSourceKind
index int
face int
name string
exact bool
priority int
@@ -49,6 +50,7 @@ type fontSource struct {
type fontSourceKey struct {
kind fontSourceKind
index int
face int
name string
}
@@ -76,7 +78,7 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily {
fontStyle := canvasFontStyle(of)
ff := canvas.NewFontFamily(of.FontName)
if of.FontFile != "" {
if fontData, err := r.Reader.ResData(of.FontFile); err == nil {
if fontData, err := r.Reader.FontData(fontID); err == nil {
if cidMap := getCFFCIDRuneMap(fontData); len(cidMap) > 0 {
r.fontCIDMap[fontID] = cidMap
}
@@ -144,12 +146,12 @@ func (r *Renderer) fontSources(fontID string, font *Font, style canvas.FontStyle
seen := make(map[fontSourceKey]bool)
for _, dir := range r.fontDirs {
for _, match := range r.matchFontFiles(dir, names, bold, italic) {
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFile, name: match.name, exact: true, priority: min(match.priority, 0)})
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFile, name: match.name, face: match.face, exact: true, priority: min(match.priority, 0)})
}
}
for index := range r.fontFS {
for _, match := range r.matchFontFS(index, names, bold, italic) {
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: match.name, exact: true, priority: min(match.priority, 0)})
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: match.name, face: match.face, exact: true, priority: min(match.priority, 0)})
}
}
sortFontSources(sources)
@@ -157,7 +159,7 @@ func (r *Renderer) fontSources(fontID string, font *Font, style canvas.FontStyle
if canLoadSystemFonts() {
for _, dir := range systemFontDirs() {
for _, match := range r.matchFontFiles(dir, names, bold, italic) {
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFile, name: match.name, exact: true, priority: min(match.priority, 0)})
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFile, name: match.name, face: match.face, exact: true, priority: min(match.priority, 0)})
}
}
for _, name := range fontSystemNames(names...) {
@@ -174,7 +176,7 @@ func (r *Renderer) fontSources(fontID string, font *Font, style canvas.FontStyle
sortFontSources(sources[systemStart:])
for index := range r.fontFS {
for _, match := range r.matchFontFS(index, nil, bold, italic) {
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: match.name})
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: match.name, face: match.face})
}
}
for index, fsys := range r.fontFS {
@@ -202,7 +204,7 @@ func appendFontSource(sources []fontSource, seen map[fontSourceKey]bool, source
if source.name == "" {
return sources
}
key := fontSourceKey{kind: source.kind, index: source.index, name: source.name}
key := fontSourceKey{kind: source.kind, index: source.index, name: source.name, face: source.face}
if seen[key] {
return sources
}
@@ -219,7 +221,7 @@ func (r *Renderer) fontSourceMatch(fontID string, font *Font) (fontSource, bool)
}
style := canvasFontStyle(font)
for _, source := range r.fontSources(fontID, font, style) {
key := fontCacheKey{fontSourceKey: fontSourceKey{kind: source.kind, index: source.index, name: source.name}, style: style}
key := fontCacheKey{fontSourceKey: fontSourceKey{kind: source.kind, index: source.index, name: source.name, face: source.face}, style: style}
if cached, ok := r.fontCache[key]; ok {
if cached != nil {
return source, true
@@ -238,22 +240,27 @@ func (r *Renderer) fontSourceMatch(fontID string, font *Font) (fontSource, bool)
// 入参: family 字体族, source 字体来源, style Canvas字体样式
// 返回: *canvas.FontFamily 字体族
func (r *Renderer) loadFontSource(family *canvas.FontFamily, source fontSource, style canvas.FontStyle) *canvas.FontFamily {
key := fontCacheKey{fontSourceKey: fontSourceKey{kind: source.kind, index: source.index, name: source.name}, style: style}
key := fontCacheKey{fontSourceKey: fontSourceKey{kind: source.kind, index: source.index, name: source.name, face: source.face}, style: style}
if cached, ok := r.fontCache[key]; ok {
return cached
}
var err error
var data []byte
switch source.kind {
case fontSourceFile:
err = family.LoadFontFile(source.name, style)
data, err = os.ReadFile(source.name)
case fontSourceFS:
var data []byte
data, err = fs.ReadFile(r.fontFS[source.index], source.name)
case fontSourceSystem:
err = family.LoadSystemFont(source.name, style)
}
if err == nil && source.kind != fontSourceSystem {
if bytes.HasPrefix(data, []byte("ttcf")) {
data, err = extractCollectionFont(data, source.face)
}
if err == nil {
err = family.LoadFont(data, 0, style)
}
case fontSourceSystem:
err = family.LoadSystemFont(source.name, style)
}
if err != nil {
r.fontCache[key] = nil
+49 -21
View File
@@ -34,42 +34,70 @@ type fontNameRecord struct {
}
// appendFontFileNames 使用字体内部名称匹配,缺失时保留文件名
// 入参: candidates 字体候选, index 文件候选索引, names 字体内部名称
// 入参: candidates 字体候选, index 文件候选索引, faces 各字体内部名称
// 返回: []fontFileCandidate 字体候选列表
func appendFontFileNames(candidates []fontFileCandidate, index int, names []string) []fontFileCandidate {
func appendFontFileNames(candidates []fontFileCandidate, index int, faces [][]string) []fontFileCandidate {
file := candidates[index]
for i, name := range names {
file.normalized = fontNormalizeName(name)
if i == 0 {
candidates[index] = file
} else {
candidates = append(candidates, file)
replaced := false
for face, names := range faces {
file.face = face
for _, name := range names {
file.normalized = fontNormalizeName(name)
if !replaced {
candidates[index] = file
replaced = true
} else {
candidates = append(candidates, file)
}
}
}
return candidates
}
// fontFileNames 读取字体或集合首项的名称
// fontFileNames 按字体索引读取字体或集合中各项的名称
// 入参: file 字体数据
// 返回: []string 字体名称及带样式的字体族名称
func fontFileNames(file io.ReaderAt) []string {
// 返回: [][]string 各字体名称及带样式的字体族名称
func fontFileNames(file io.ReaderAt) [][]string {
var header [12]byte
if _, err := file.ReadAt(header[:], 0); err != nil {
return nil
}
var offset int64
if string(header[:4]) == "ttcf" {
if binary.BigEndian.Uint32(header[8:]) == 0 {
return nil
}
if string(header[:4]) != "ttcf" {
return [][]string{fontFaceNames(file, 0)}
}
version := binary.BigEndian.Uint32(header[4:])
if version != 0x00010000 && version != 0x00020000 {
return nil
}
var faces [][]string
for i, count := uint32(0), binary.BigEndian.Uint32(header[8:]); i < count; i++ {
var entry [4]byte
if _, err := file.ReadAt(entry[:], 12); err != nil {
return nil
}
offset = int64(binary.BigEndian.Uint32(entry[:]))
if _, err := file.ReadAt(header[:], offset); err != nil {
if _, err := file.ReadAt(entry[:], 12+int64(i)*4); err != nil {
return nil
}
faces = append(faces, fontFaceNames(file, int64(binary.BigEndian.Uint32(entry[:]))))
}
return faces
}
// fontCollectionIndex 按内部名称与样式选择集合项,未匹配时保留首项
// 入参: data 集合数据, names 字体名称, bold 是否粗体, italic 是否斜体
// 返回: int 零起始字体索引
func fontCollectionIndex(data []byte, names []string, bold, italic bool) int {
candidates := appendFontFileNames([]fontFileCandidate{{}}, 0, fontFileNames(bytes.NewReader(data)))
if matches := fontFileMatches(candidates, names, bold, italic); len(matches) > 0 {
return matches[0].face
}
return 0
}
// fontFaceNames 读取指定字体表目录中的名称
// 入参: file 字体数据, offset 字体表目录偏移
// 返回: []string 字体名称及带样式的字体族名称
func fontFaceNames(file io.ReaderAt, offset int64) []string {
var header [12]byte
if _, err := file.ReadAt(header[:], offset); err != nil {
return nil
}
switch string(header[:4]) {
case "\x00\x01\x00\x00", "OTTO", "true":