mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 17:27:08 +08:00
This commit is contained in:
1 parent
1914b5329a
commit
f9646c70e9
8 files changed
+163
-68
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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":
|
||||
|
||||
Reference in new issue
Block a user