mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 17:27:08 +08:00
1 parent
051fc3b65e
commit
cf8dfd046b
21 files changed
+1071
-237
No files matched your search
@@ -2957,8 +2957,7 @@ function refreshEditorFonts() {
|
||||
|
||||
async function readTextFont(font) {
|
||||
if (font.embedded) return (await callWASM("ofdgoEditorFont", font.id.slice(9))).bytes;
|
||||
if (font.file) return (await callWASM("ofdgoFontFace", await fontManager.read(font.file), font.index)).bytes;
|
||||
return fontManager.read(font);
|
||||
return (await callWASM("ofdgoFontFace", await fontManager.read(font.file || font), font.index || 0)).bytes;
|
||||
}
|
||||
|
||||
function updateTextFonts(item, refresh = false) {
|
||||
|
||||
@@ -3,11 +3,11 @@ module github.com/xiaoqidun/ofdgo
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.2.6
|
||||
github.com/emmansun/gmsm v0.45.0
|
||||
github.com/go-fonts/latin-modern v0.3.3
|
||||
github.com/go-text/typesetting v0.3.5
|
||||
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
|
||||
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
|
||||
github.com/xiaoqidun/j2kgo v0.0.0-20261009063910-4865a21ef893
|
||||
github.com/xiaoqidun/jbig2 v1.1.4
|
||||
github.com/xiaoqidun/pdfgo v0.0.0-20261010065159-b32618b0bdd7
|
||||
@@ -23,12 +23,12 @@ require (
|
||||
github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc // indirect
|
||||
github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 // indirect
|
||||
github.com/ByteArena/poly2tri-go v0.0.0-20170716161910-d102ad91854f // indirect
|
||||
github.com/andybalholm/brotli v1.2.2 // indirect
|
||||
github.com/benoitkugler/textlayout v0.3.2 // indirect
|
||||
github.com/benoitkugler/textprocessing v0.0.6 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
|
||||
github.com/srwiley/scanx v0.0.0-20190309010443-e94503791388 // indirect
|
||||
github.com/tdewolff/font v0.0.0-20260822205238-d0d2f004b117 // indirect
|
||||
github.com/tdewolff/minify/v2 v2.24.17 // indirect
|
||||
github.com/tdewolff/parse/v2 v2.8.16 // indirect
|
||||
github.com/yuin/goldmark v1.8.5 // indirect
|
||||
|
||||
@@ -20,8 +20,8 @@ github.com/ByteArena/poly2tri-go v0.0.0-20170716161910-d102ad91854f h1:l7moT9o/v
|
||||
github.com/ByteArena/poly2tri-go v0.0.0-20170716161910-d102ad91854f/go.mod h1:vIOkSdX3NDCPwgu8FIuTat2zDF0FPXXQ0RYFRy+oQic=
|
||||
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw=
|
||||
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
|
||||
github.com/andybalholm/brotli v1.2.2 h1:HzTuoo2ErYQqf5qvcJInB8uvqSVxRttzkFexPWtnceM=
|
||||
github.com/andybalholm/brotli v1.2.2/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/andybalholm/brotli v1.2.6 h1:ftYnfj6usCp+UGV5kSJ3+chpMQgU+gJf/AxsUQ52REI=
|
||||
github.com/andybalholm/brotli v1.2.6/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/benoitkugler/pstokenizer v1.0.0/go.mod h1:l1G2Voirz0q/jj0TQfabNxVsa8HZXh/VMxFSRALWTiE=
|
||||
github.com/benoitkugler/textlayout v0.3.2 h1:Y/LTkwpQ9G0Fia9yhPmZA9IR5AnE8Cq30j3C+Gx5/IE=
|
||||
github.com/benoitkugler/textlayout v0.3.2/go.mod h1:o+1hFV+JSHBC9qNLIuwVoLedERU7sBPgEFcuSgfvi/w=
|
||||
@@ -50,8 +50,8 @@ github.com/srwiley/scanx v0.0.0-20190309010443-e94503791388 h1:ZdkidVdpLW13BQ9a+
|
||||
github.com/srwiley/scanx v0.0.0-20190309010443-e94503791388/go.mod h1:C/WY5lmWfMtPFYYBTd3Lzdn4FTLr+RxlIeiBNye+/os=
|
||||
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c h1:eXJXpQAo8l0AdVnsjJctG38GlBnHnwHfhLOTLYxZYNE=
|
||||
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c/go.mod h1:K9Ux3cwFtyAFRITZ46g7v+ORXgI3ENtQa5rL9yjNz5Y=
|
||||
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6 h1:phNTWRYLLTlPpUgxkd6FDmWLlrOzpEpQAqq1ryLXYS8=
|
||||
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6/go.mod h1:ipESUihcEdgRBhIwTRHmBTOMamccMQrp4iCxfa1aFws=
|
||||
github.com/tdewolff/font v0.0.0-20260822205238-d0d2f004b117 h1:5vSJjsbV9AH0U+Ex0uej8ROrpOBlOVBrw0mGlVef7oo=
|
||||
github.com/tdewolff/font v0.0.0-20260822205238-d0d2f004b117/go.mod h1:ipESUihcEdgRBhIwTRHmBTOMamccMQrp4iCxfa1aFws=
|
||||
github.com/tdewolff/minify/v2 v2.24.17 h1:6AbitfVyq0M7aW6i+XL7+49DeTQZwloOMs9O574arBg=
|
||||
github.com/tdewolff/minify/v2 v2.24.17/go.mod h1:kVqn9vxXUKtlHexSNrWbYePqioOT5mc4ou/KVSMpfCM=
|
||||
github.com/tdewolff/parse/v2 v2.8.16 h1:bLk5svUOQRkW/Y2SJ+DeENSIkZBcTIkq+Atyv5D8feI=
|
||||
|
||||
+3
-5
@@ -16,8 +16,6 @@ package ofdgo
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/tdewolff/font"
|
||||
)
|
||||
|
||||
// FixFontDataAggressive 激进修复字体数据
|
||||
@@ -52,10 +50,10 @@ func FixFontDataAggressive(data []byte, fixCmap, fixName bool) (bool, []byte, ma
|
||||
}
|
||||
|
||||
// fontSFNTData 保留字体原始表数据和时间,重新计算目录与校验和
|
||||
// 入参: sfnt 已解析的字体
|
||||
// 入参: tables 原始字体表
|
||||
// 返回: []byte 独立SFNT数据
|
||||
func fontSFNTData(sfnt *font.SFNT) []byte {
|
||||
data, _ := serializeOTF(sfnt.Tables)
|
||||
func fontSFNTData(tables map[string][]byte) []byte {
|
||||
data, _ := serializeOTF(tables)
|
||||
return data
|
||||
}
|
||||
|
||||
|
||||
+5
-5
@@ -23,7 +23,7 @@ import (
|
||||
|
||||
"github.com/go-text/typesetting/font/cff"
|
||||
"github.com/go-text/typesetting/font/opentype"
|
||||
"github.com/tdewolff/font"
|
||||
"github.com/go-text/typesetting/font/opentype/tables"
|
||||
)
|
||||
|
||||
// defaultCFF2Font 将CFF2默认实例转换为静态OpenType字体,保留字形编号和三次轮廓
|
||||
@@ -90,7 +90,7 @@ func defaultCFF2Font(data []byte) (result []byte, err error) {
|
||||
}
|
||||
chars := make([][]byte, count)
|
||||
for gid := range chars {
|
||||
segments, _, err := program.LoadGlyph(uint16(gid), nil)
|
||||
segments, _, err := program.LoadGlyph(tables.GlyphID(gid), nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("CFF2 glyph %d: %w", gid, err)
|
||||
}
|
||||
@@ -207,7 +207,7 @@ func staticCIDFont(table map[string][]byte, chars [][]byte) ([]byte, error) {
|
||||
// staticCFFPrograms 读取无子程序和私有字典的CID字形,其他CFF保留原有保存策略
|
||||
// 入参: sfnt 已解析字体
|
||||
// 返回: [][]byte 独立字形程序,不符合条件时为空
|
||||
func staticCFFPrograms(sfnt *font.SFNT) [][]byte {
|
||||
func staticCFFPrograms(sfnt *sfntFont) [][]byte {
|
||||
data := sfnt.Tables["CFF "]
|
||||
if len(data) < 4 {
|
||||
return nil
|
||||
@@ -262,7 +262,7 @@ func staticCFFPrograms(sfnt *font.SFNT) [][]byte {
|
||||
// subsetStaticCFFFont 裁剪静态CID字形并保留原编号、字符映射和度量
|
||||
// 入参: sfnt 已解析字体, glyphs 使用的字形
|
||||
// 返回: []byte 字体子集, error 裁剪错误
|
||||
func subsetStaticCFFFont(sfnt *font.SFNT, glyphs []uint16) ([]byte, error) {
|
||||
func subsetStaticCFFFont(sfnt *sfntFont, glyphs []uint16) ([]byte, error) {
|
||||
chars := staticCFFPrograms(sfnt)
|
||||
if chars == nil {
|
||||
return nil, fmt.Errorf("CFF font cannot be subset safely")
|
||||
@@ -299,7 +299,7 @@ func subsetStaticCFFFont(sfnt *font.SFNT, glyphs []uint16) ([]byte, error) {
|
||||
}
|
||||
mapping := make(map[rune]uint16)
|
||||
for gid := range keep {
|
||||
for _, char := range sfnt.Cmap.ToUnicode(gid) {
|
||||
for _, char := range sfnt.glyphCharacters(gid) {
|
||||
mapping[char] = gid
|
||||
}
|
||||
}
|
||||
|
||||
+3
-5
@@ -20,8 +20,6 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/tdewolff/font"
|
||||
)
|
||||
|
||||
// FontFace 字体文件中的字体名称与零起始索引,不依赖文件名
|
||||
@@ -37,7 +35,7 @@ type FontFace struct {
|
||||
// Faces 读取字体名称表,集合文件按原顺序列出各项,不解析字形轮廓
|
||||
// 返回: []FontFace 字体列表, error 格式或名称表错误
|
||||
func (f FontFile) Faces() ([]FontFace, error) {
|
||||
data, err := font.ToSFNT(f.Data)
|
||||
data, err := decodeFontContainer(f.Data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -64,7 +62,7 @@ func (f FontFile) Faces() ([]FontFace, error) {
|
||||
// 入参: index 零起始字体索引,非集合文件只能为0
|
||||
// 返回: []byte 独立字体数据, error 格式或索引错误
|
||||
func (f FontFile) Face(index int) ([]byte, error) {
|
||||
data, err := font.ToSFNT(f.Data)
|
||||
data, err := decodeFontContainer(f.Data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -105,7 +103,7 @@ func (f FontFile) MissingGlyphs(index int, text string) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
sfnt, err := font.ParseSFNT(data, 0)
|
||||
sfnt, err := parseSFNTFont(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
+51
-8
@@ -21,12 +21,16 @@ import (
|
||||
"math"
|
||||
"sync"
|
||||
|
||||
"github.com/tdewolff/font"
|
||||
"github.com/go-text/typesetting/font/cff"
|
||||
"github.com/go-text/typesetting/font/opentype"
|
||||
)
|
||||
|
||||
// sfntOutliner 缓存字体程序并提取独立字形轮廓
|
||||
type sfntOutliner struct {
|
||||
font *font.SFNT
|
||||
font *sfntFont
|
||||
cffOnce sync.Once
|
||||
cff *cff.CFF
|
||||
cffErr error
|
||||
once sync.Once
|
||||
program *ttInterpreter
|
||||
err error
|
||||
@@ -42,12 +46,51 @@ type ttGlyph struct {
|
||||
warning error
|
||||
}
|
||||
|
||||
// path 按TrueType指令和隐含点规则生成闭合轮廓,CFF保留原有解析
|
||||
// path 按字体设计坐标提取闭合轮廓,TrueType保留字形指令处理
|
||||
// 入参: path 路径接收器, glyph 字形编号, scale 字体单位缩放
|
||||
// 返回: error 字形解析错误
|
||||
func (f *sfntOutliner) path(path font.Pather, glyph uint16, scale float64) error {
|
||||
func (f *sfntOutliner) path(path glyphPather, glyph uint16, scale float64) error {
|
||||
if !f.font.IsTrueType {
|
||||
return f.font.GlyphPath(path, glyph, 0, 0, 0, scale, font.NoHinting)
|
||||
f.cffOnce.Do(func() {
|
||||
data, err := sanitizeCFF(f.font.Tables["CFF "])
|
||||
if err == nil {
|
||||
data, err = normalizeCFFCharstringsAt(data, f.font.UnitsPerEm())
|
||||
}
|
||||
if err != nil {
|
||||
f.cffErr = err
|
||||
return
|
||||
}
|
||||
f.cff, f.cffErr = cff.Parse(data)
|
||||
})
|
||||
if f.cffErr != nil {
|
||||
return f.cffErr
|
||||
}
|
||||
segments, _, err := f.cff.LoadGlyph(glyph)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
open := false
|
||||
for _, segment := range segments {
|
||||
p := segment.Args
|
||||
switch segment.Op {
|
||||
case opentype.SegmentOpMoveTo:
|
||||
if open {
|
||||
path.Close()
|
||||
}
|
||||
path.MoveTo(float64(p[0].X)*scale, float64(p[0].Y)*scale)
|
||||
open = true
|
||||
case opentype.SegmentOpLineTo:
|
||||
path.LineTo(float64(p[0].X)*scale, float64(p[0].Y)*scale)
|
||||
case opentype.SegmentOpCubeTo:
|
||||
path.CubeTo(float64(p[0].X)*scale, float64(p[0].Y)*scale, float64(p[1].X)*scale, float64(p[1].Y)*scale, float64(p[2].X)*scale, float64(p[2].Y)*scale)
|
||||
default:
|
||||
return fmt.Errorf("invalid CFF curve")
|
||||
}
|
||||
}
|
||||
if open {
|
||||
path.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
contour, err := f.glyph(glyph, make(map[uint16]bool))
|
||||
if err != nil {
|
||||
@@ -183,10 +226,10 @@ func (f *sfntOutliner) glyph(id uint16, active map[uint16]bool) (ttGlyph, error)
|
||||
// 入参: id 字形编号, data 完整字形头
|
||||
// 返回: [4]ttPoint 左右边距与上下原点
|
||||
func (f *sfntOutliner) phantomPoints(id uint16, data []byte) [4]ttPoint {
|
||||
left := int32(int16(binary.BigEndian.Uint16(data[2:]))) - int32(f.font.Hmtx.LeftSideBearing(id))
|
||||
left := int32(int16(binary.BigEndian.Uint16(data[2:]))) - int32(f.font.sideBearing(id, false))
|
||||
top := int32(int16(binary.BigEndian.Uint16(data[8:])))
|
||||
if f.font.Vmtx != nil {
|
||||
top += int32(f.font.Vmtx.TopSideBearing(id))
|
||||
if f.font.vertical != 0 {
|
||||
top += int32(f.font.sideBearing(id, true))
|
||||
}
|
||||
return [4]ttPoint{{x: left * 64, ox: left * 64}, {x: (left + int32(f.font.GlyphAdvance(id))) * 64, ox: (left + int32(f.font.GlyphAdvance(id))) * 64}, {y: top * 64, oy: top * 64}, {y: (top - int32(f.font.GlyphVerticalAdvance(id))) * 64, oy: (top - int32(f.font.GlyphVerticalAdvance(id))) * 64}}
|
||||
}
|
||||
|
||||
+1
-3
@@ -22,8 +22,6 @@ import (
|
||||
"path"
|
||||
"slices"
|
||||
"sort"
|
||||
|
||||
"github.com/tdewolff/font"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -246,7 +244,7 @@ func (r *Reader) embeddedFontFace(of Font) (*FontFace, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if data, err = font.ToSFNT(data); err == nil {
|
||||
if data, err = decodeFontContainer(data); err == nil {
|
||||
if count, err := fontFileCount(data); err == nil {
|
||||
faces = make([]*FontFace, count)
|
||||
for index := range faces {
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
// 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 ofdgo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// sfntGlyphBoundsLimit 限制单个字体保留的字形与字号边界数量
|
||||
const sfntGlyphBoundsLimit = 4096
|
||||
|
||||
// sfntFontMetrics 保留默认字体度量,写出时不更新原始时间
|
||||
type sfntFontMetrics struct {
|
||||
*sfntFont
|
||||
*fontShaper
|
||||
outlines *sfntOutliner
|
||||
bounds *sfntFontBounds
|
||||
}
|
||||
|
||||
// sfntFontBounds 缓存已成功提取的字形边界,不保留轮廓或失败结果
|
||||
type sfntFontBounds struct {
|
||||
mu sync.Mutex
|
||||
values map[sfntGlyphSize]Box
|
||||
}
|
||||
|
||||
// sfntGlyphSize 区分字形编号及精确毫米字号
|
||||
type sfntGlyphSize struct {
|
||||
glyph uint16
|
||||
size float64
|
||||
}
|
||||
|
||||
// glyphPather 接收字体设计坐标中的轮廓指令
|
||||
type glyphPather interface {
|
||||
MoveTo(float64, float64)
|
||||
LineTo(float64, float64)
|
||||
QuadTo(float64, float64, float64, float64)
|
||||
CubeTo(float64, float64, float64, float64, float64, float64)
|
||||
Close()
|
||||
}
|
||||
|
||||
// glyphGeometry 将字体轮廓转换为纵轴向下的几何路径
|
||||
type glyphGeometry struct{ path GeometryPath }
|
||||
|
||||
// MoveTo 开始子路径
|
||||
// 入参: x 横坐标, y 纵坐标
|
||||
func (p *glyphGeometry) MoveTo(x, y float64) {
|
||||
p.path = append(p.path, GeometrySegment{Verb: GeometryMove, End: Point{x, -y}})
|
||||
}
|
||||
|
||||
// LineTo 添加直线
|
||||
// 入参: x 横坐标, y 纵坐标
|
||||
func (p *glyphGeometry) LineTo(x, y float64) {
|
||||
p.path = append(p.path, GeometrySegment{Verb: GeometryLine, End: Point{x, -y}})
|
||||
}
|
||||
|
||||
// QuadTo 添加二次贝塞尔曲线
|
||||
// 入参: cx 控制点横坐标, cy 控制点纵坐标, x 终点横坐标, y 终点纵坐标
|
||||
func (p *glyphGeometry) QuadTo(cx, cy, x, y float64) {
|
||||
p.path = append(p.path, GeometrySegment{Verb: GeometryQuad, Control1: Point{cx, -cy}, End: Point{x, -y}})
|
||||
}
|
||||
|
||||
// CubeTo 添加三次贝塞尔曲线
|
||||
// 入参: ax 第一控制点横坐标, ay 第一控制点纵坐标, bx 第二控制点横坐标, by 第二控制点纵坐标, x 终点横坐标, y 终点纵坐标
|
||||
func (p *glyphGeometry) CubeTo(ax, ay, bx, by, x, y float64) {
|
||||
p.path = append(p.path, GeometrySegment{Verb: GeometryCubic, Control1: Point{ax, -ay}, Control2: Point{bx, -by}, End: Point{x, -y}})
|
||||
}
|
||||
|
||||
// Close 闭合当前子路径
|
||||
func (p *glyphGeometry) Close() { p.path = append(p.path, GeometrySegment{Verb: GeometryClose}) }
|
||||
|
||||
// ResolveFont 按需加载字体资源,保留内嵌数据及外部字体的名称、样式和集合索引匹配
|
||||
// 入参: r 渲染器, id 字体标识, exact 是否禁止无关回退
|
||||
// 返回: ResolvedFont 字体与来源, error 字体不可用错误
|
||||
func (b SFNTBackend) ResolveFont(r *Renderer, id string, exact bool) (ResolvedFont, error) {
|
||||
return r.resolveFontSource(b, id, exact)
|
||||
}
|
||||
|
||||
// OpenFont 解析独立SFNT数据,度量与原有编辑排版保持一致
|
||||
// 入参: data 字体数据
|
||||
// 返回: FontMetrics 字体度量, error 解析错误
|
||||
func (SFNTBackend) OpenFont(data []byte) (FontMetrics, error) {
|
||||
data, err := defaultCFF2Font(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sfnt, err := parseSFNTFont(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
metrics := &sfntFontMetrics{sfntFont: sfnt, outlines: &sfntOutliner{font: sfnt}, bounds: &sfntFontBounds{}}
|
||||
metrics.fontShaper = &fontShaper{metrics: metrics}
|
||||
return metrics, nil
|
||||
}
|
||||
|
||||
// Write 返回字体数据,不修改字体元信息
|
||||
// 返回: []byte 独立SFNT数据
|
||||
func (f sfntFontMetrics) Write() []byte {
|
||||
return f.sfntFont.Write()
|
||||
}
|
||||
|
||||
// GlyphOutline 返回基线原点、纵轴向下的字形轮廓
|
||||
// 入参: glyph 字形编号, size 字号,单位为毫米
|
||||
// 返回: GeometryPath 字形路径, error 字形解析错误
|
||||
func (f sfntFontMetrics) GlyphOutline(glyph uint16, size float64) (GeometryPath, error) {
|
||||
if !rasterPositive(size) || glyph >= f.NumGlyphs() {
|
||||
return nil, fmt.Errorf("invalid glyph or size")
|
||||
}
|
||||
path := &glyphGeometry{}
|
||||
if err := f.outlines.path(path, glyph, size/float64(f.UnitsPerEm())); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return path.path, nil
|
||||
}
|
||||
|
||||
// GlyphBounds 返回字形轮廓的精确边界,重复编号及字号复用有限缓存
|
||||
// 入参: glyph 字形编号, size 字号,单位为毫米
|
||||
// 返回: Box 字形边界, error 字形解析错误
|
||||
func (f sfntFontMetrics) GlyphBounds(glyph uint16, size float64) (Box, error) {
|
||||
key := sfntGlyphSize{glyph: glyph, size: size}
|
||||
if f.bounds != nil {
|
||||
f.bounds.mu.Lock()
|
||||
value, ok := f.bounds.values[key]
|
||||
f.bounds.mu.Unlock()
|
||||
if ok {
|
||||
return value, nil
|
||||
}
|
||||
}
|
||||
outline, err := f.GlyphOutline(glyph, size)
|
||||
if err != nil {
|
||||
return Box{}, err
|
||||
}
|
||||
value, err := outline.Bounds()
|
||||
if err != nil {
|
||||
return Box{}, err
|
||||
}
|
||||
if f.bounds != nil {
|
||||
f.bounds.mu.Lock()
|
||||
if f.bounds.values == nil || len(f.bounds.values) >= sfntGlyphBoundsLimit {
|
||||
f.bounds.values = make(map[sfntGlyphSize]Box)
|
||||
}
|
||||
f.bounds.values[key] = value
|
||||
f.bounds.mu.Unlock()
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// GlyphWarning 返回无效字形指令后保留设计轮廓的提示,原始字体不变
|
||||
// 入参: glyph 字形编号
|
||||
// 返回: error 恢复原因,无恢复时为空
|
||||
func (f sfntFontMetrics) GlyphWarning(glyph uint16) error {
|
||||
return f.outlines.glyphWarning(glyph)
|
||||
}
|
||||
|
||||
// GlyphOutlines 批量提取字形轮廓,重复编号只解析一次
|
||||
// 入参: glyphs 字形编号, size 毫米字号
|
||||
// 返回: []GeometryPath 同序只读轮廓, error 轮廓错误
|
||||
func (f sfntFontMetrics) GlyphOutlines(glyphs []uint16, size float64) ([]GeometryPath, error) {
|
||||
if !rasterPositive(size) {
|
||||
return nil, fmt.Errorf("invalid glyph size")
|
||||
}
|
||||
result := make([]GeometryPath, len(glyphs))
|
||||
paths := make(map[uint16]GeometryPath, len(glyphs))
|
||||
for i, glyph := range glyphs {
|
||||
path, ok := paths[glyph]
|
||||
if !ok {
|
||||
var err error
|
||||
path, err = f.GlyphOutline(glyph, size)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
paths[glyph] = path
|
||||
}
|
||||
result[i] = path
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
+126
-43
@@ -20,15 +20,13 @@ import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
|
||||
"github.com/tdewolff/font"
|
||||
)
|
||||
|
||||
// SFNTBackend 提供默认字体资源处理,不依赖Canvas绘图库
|
||||
// SFNTBackend 提供独立的OpenType字体度量、轮廓和资源处理
|
||||
type SFNTBackend struct{}
|
||||
|
||||
// sfntResourceMetrics 隔离资源解析器的度量实现
|
||||
type sfntResourceMetrics struct{ *font.SFNT }
|
||||
type sfntResourceMetrics struct{ *sfntFont }
|
||||
|
||||
// Name 返回资源后端标识
|
||||
// 返回: string 后端标识
|
||||
@@ -47,11 +45,11 @@ func (SFNTBackend) OpenFontResource(file FontFile, index int) (*FontResource, er
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sfnt, err := font.ParseSFNT(data, 0)
|
||||
sfnt, err := parseSFNTFont(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
name := sfntFontName(sfnt, font.NamePostScript, font.NameFull, font.NameFontFamily)
|
||||
name := sfntFontName(sfnt, 6, 4, 1)
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("font has no name")
|
||||
}
|
||||
@@ -61,7 +59,7 @@ func (SFNTBackend) OpenFontResource(file FontFile, index int) (*FontResource, er
|
||||
}
|
||||
return &FontResource{
|
||||
FontMetrics: sfntResourceMetrics{sfnt}, Data: data, Extension: extension,
|
||||
Font: Font{FontName: name, FamilyName: sfntFontName(sfnt, font.NamePreferredFamily, font.NameFontFamily), Charset: "unicode", Bold: sfnt.Head.MacStyle[0], Italic: sfnt.Head.MacStyle[1], FixedWidth: sfnt.Post.IsFixedPitch != 0},
|
||||
Font: Font{FontName: name, FamilyName: sfntFontName(sfnt, 16, 1), Charset: "unicode", Bold: binary.BigEndian.Uint16(sfnt.Tables["head"][44:])&1 != 0, Italic: binary.BigEndian.Uint16(sfnt.Tables["head"][44:])&2 != 0, FixedWidth: len(sfnt.Tables["post"]) >= 16 && binary.BigEndian.Uint32(sfnt.Tables["post"][12:]) != 0},
|
||||
CanSubset: sfntSubsettable(sfnt),
|
||||
}, nil
|
||||
}
|
||||
@@ -90,12 +88,12 @@ func (SFNTBackend) SubsetSourceFont(data []byte, usage FontUsage) ([]byte, error
|
||||
|
||||
// Write 返回未改动元信息的字体数据
|
||||
// 返回: []byte 字体数据
|
||||
func (f sfntResourceMetrics) Write() []byte { return fontSFNTData(f.SFNT) }
|
||||
func (f sfntResourceMetrics) Write() []byte { return f.sfntFont.Write() }
|
||||
|
||||
// sfntSubsettable 判断默认资源后端能否无损裁剪该字体
|
||||
// 入参: sfnt 已解析字体
|
||||
// 返回: bool 是否支持裁剪
|
||||
func sfntSubsettable(sfnt *font.SFNT) bool {
|
||||
func sfntSubsettable(sfnt *sfntFont) bool {
|
||||
if fontEmbeddingFlags(sfnt)&0x0100 != 0 || slices.ContainsFunc([]string{"COLR", "CBDT", "sbix", "SVG "}, func(tag string) bool { return sfnt.Tables[tag] != nil }) {
|
||||
return false
|
||||
}
|
||||
@@ -108,7 +106,7 @@ func sfntSubsettable(sfnt *font.SFNT) bool {
|
||||
// fontEmbeddingFlags 读取OpenType的嵌入标志,未提供OS/2表时返回零
|
||||
// 入参: sfnt 已解析字体
|
||||
// 返回: uint16 嵌入标志
|
||||
func fontEmbeddingFlags(sfnt *font.SFNT) uint16 {
|
||||
func fontEmbeddingFlags(sfnt *sfntFont) uint16 {
|
||||
if table := sfnt.Tables["OS/2"]; len(table) >= 10 {
|
||||
return binary.BigEndian.Uint16(table[8:10])
|
||||
}
|
||||
@@ -119,7 +117,7 @@ func fontEmbeddingFlags(sfnt *font.SFNT) uint16 {
|
||||
// 入参: data 独立OpenType字体
|
||||
// 返回: error 字体解析或嵌入限制
|
||||
func validateFontEmbedding(data []byte) error {
|
||||
sfnt, err := font.ParseSFNT(data, 0)
|
||||
sfnt, err := parseSFNTFont(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -133,10 +131,14 @@ func validateFontEmbedding(data []byte) error {
|
||||
// sfntFontName 获取字体中的名称
|
||||
// 入参: sfnt 字体, names 名称类型,按顺序查找
|
||||
// 返回: string 字体名称
|
||||
func sfntFontName(sfnt *font.SFNT, names ...font.NameID) string {
|
||||
func sfntFontName(sfnt *sfntFont, names ...uint16) string {
|
||||
records, values := fontNameValues(sfnt.Tables["name"])
|
||||
for _, name := range names {
|
||||
for _, record := range sfnt.Name.Get(name) {
|
||||
if value := record.String(); value != "" {
|
||||
for _, record := range records {
|
||||
if record.Name != name {
|
||||
continue
|
||||
}
|
||||
if value := values[[4]uint16{record.Platform, record.Encoding, record.Language, name}]; value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
@@ -152,7 +154,7 @@ func subsetSFNTFont(data []byte, glyphs []uint16, mapped bool) ([]byte, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sfnt, err := font.ParseSFNT(bytes.Clone(data), 0)
|
||||
sfnt, err := parseSFNTFont(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -169,7 +171,7 @@ func subsetSFNTFont(data []byte, glyphs []uint16, mapped bool) ([]byte, error) {
|
||||
usage := newEditorFontUsage()
|
||||
for _, id := range glyphs {
|
||||
usage.glyphs[id] = true
|
||||
for _, char := range sfnt.Cmap.ToUnicode(id) {
|
||||
for _, char := range sfnt.glyphCharacters(id) {
|
||||
usage.chars[char] = true
|
||||
}
|
||||
}
|
||||
@@ -188,15 +190,7 @@ func subsetSFNTFont(data []byte, glyphs []uint16, mapped bool) ([]byte, error) {
|
||||
}
|
||||
return fixed, nil
|
||||
}
|
||||
subset, err := sfnt.Subset(glyphs, font.SubsetOptions{Tables: []string{
|
||||
"cmap", "head", "hhea", "hmtx", "maxp", "OS/2", "post", "glyf", "loca", "cvt ", "fpgm", "prep", "gasp",
|
||||
}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
subset.Tables["name"] = sfnt.Tables["name"]
|
||||
clear(subset.Tables["head"][8:12])
|
||||
return serializeOTF(subset.Tables)
|
||||
return subsetCompactSFNTFont(sfnt, glyphs)
|
||||
}
|
||||
|
||||
// subsetSourceSFNTFont 裁剪静态TrueType原有字体,保留字形编号、复合依赖、度量及提示指令
|
||||
@@ -207,7 +201,7 @@ func subsetSourceSFNTFont(data []byte, usage *editorFontUsage) []byte {
|
||||
if usage.unsafe || len(usage.chars)+len(usage.glyphs) == 0 || bytes.HasPrefix(data, []byte("ttcf")) {
|
||||
return nil
|
||||
}
|
||||
sfnt, err := font.ParseSFNT(data, 0)
|
||||
sfnt, err := parseSFNTFont(data)
|
||||
if err != nil || !sfnt.IsTrueType || fontEmbeddingFlags(sfnt)&0x0100 != 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -218,20 +212,13 @@ func subsetSourceSFNTFont(data []byte, usage *editorFontUsage) []byte {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
var unicodeTables []uint16
|
||||
subtables := make(map[uint32]uint16)
|
||||
for i, record := range sfnt.Cmap.EncodingRecords {
|
||||
if record.Format == 14 {
|
||||
var unicodeTables []sfntCmap
|
||||
for _, record := range sfnt.cmaps {
|
||||
if record.format == 14 {
|
||||
return nil
|
||||
}
|
||||
offset := binary.BigEndian.Uint32(sfnt.Tables["cmap"][8+8*i:])
|
||||
index, ok := subtables[offset]
|
||||
if !ok {
|
||||
index = uint16(len(subtables))
|
||||
subtables[offset] = index
|
||||
}
|
||||
if record.PlatformID == 0 || record.PlatformID == 3 && (record.EncodingID == 1 || record.EncodingID == 10) {
|
||||
unicodeTables = append(unicodeTables, index)
|
||||
if record.platform == 0 || record.platform == 3 && (record.encoding == 1 || record.encoding == 10) {
|
||||
unicodeTables = append(unicodeTables, record)
|
||||
}
|
||||
}
|
||||
if len(unicodeTables) == 0 {
|
||||
@@ -242,8 +229,8 @@ func subsetSourceSFNTFont(data []byte, usage *editorFontUsage) []byte {
|
||||
mapping := make(map[rune]uint16, len(usage.chars))
|
||||
for char := range usage.chars {
|
||||
id := sfnt.GlyphIndex(char)
|
||||
for _, index := range unicodeTables {
|
||||
if alternate, ok := sfnt.Cmap.Subtables[index].Get(char); ok && alternate != id {
|
||||
for _, cmap := range unicodeTables {
|
||||
if alternate, ok := cmap.mapping[char]; ok && alternate != id {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -261,7 +248,7 @@ func subsetSourceSFNTFont(data []byte, usage *editorFontUsage) []byte {
|
||||
if id >= sfnt.NumGlyphs() {
|
||||
return nil
|
||||
}
|
||||
dependencies, err := sfnt.Glyf.Dependencies(id)
|
||||
dependencies, err := sfnt.glyphDependencies(id)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -271,13 +258,14 @@ func subsetSourceSFNTFont(data []byte, usage *editorFontUsage) []byte {
|
||||
}
|
||||
var glyf, loca []byte
|
||||
for id := 0; id <= int(sfnt.NumGlyphs()); id++ {
|
||||
if sfnt.Head.IndexToLocFormat == 0 {
|
||||
if binary.BigEndian.Uint16(sfnt.Tables["head"][50:]) == 0 {
|
||||
loca = binary.BigEndian.AppendUint16(loca, uint16(len(glyf)/2))
|
||||
} else {
|
||||
loca = binary.BigEndian.AppendUint32(loca, uint32(len(glyf)))
|
||||
}
|
||||
if id < int(sfnt.NumGlyphs()) && glyphs[uint16(id)] {
|
||||
glyf = append(glyf, sfnt.Glyf.Get(uint16(id))...)
|
||||
data, _ := trueTypeGlyphData(sfnt.Tables, uint16(id))
|
||||
glyf = append(glyf, data...)
|
||||
if len(glyf)%2 != 0 {
|
||||
glyf = append(glyf, 0)
|
||||
}
|
||||
@@ -295,3 +283,98 @@ func subsetSourceSFNTFont(data []byte, usage *editorFontUsage) []byte {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// subsetCompactSFNTFont 重排新建静态字体的字形,同步复合引用、字符映射和度量
|
||||
// 入参: sfnt 字体, glyphs 使用字形
|
||||
// 返回: []byte 紧凑字体子集, error 字形数据错误
|
||||
func subsetCompactSFNTFont(sfnt *sfntFont, glyphs []uint16) ([]byte, error) {
|
||||
ids := []uint16{0}
|
||||
remap := map[uint16]uint16{0: 0}
|
||||
for _, id := range glyphs {
|
||||
if id >= sfnt.NumGlyphs() {
|
||||
return nil, fmt.Errorf("font glyph index out of range")
|
||||
}
|
||||
if _, ok := remap[id]; !ok {
|
||||
remap[id] = uint16(len(ids))
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
for index := 0; index < len(ids); index++ {
|
||||
dependencies, err := sfnt.glyphDependencies(ids[index])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, id := range dependencies {
|
||||
if _, ok := remap[id]; !ok {
|
||||
remap[id] = uint16(len(ids))
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
table := make(map[string][]byte)
|
||||
for _, tag := range []string{"head", "hhea", "maxp", "OS/2", "post", "name", "cvt ", "fpgm", "prep", "gasp", "vhea"} {
|
||||
if data := sfnt.Tables[tag]; data != nil {
|
||||
table[tag] = bytes.Clone(data)
|
||||
}
|
||||
}
|
||||
var glyf, loca, hmtx, vmtx []byte
|
||||
mapping := make(map[rune]uint16)
|
||||
for _, id := range ids {
|
||||
loca = binary.BigEndian.AppendUint32(loca, uint32(len(glyf)))
|
||||
data, err := trueTypeGlyphData(sfnt.Tables, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data = bytes.Clone(data)
|
||||
if len(data) >= 10 && int16(binary.BigEndian.Uint16(data)) < 0 {
|
||||
for pos := 10; ; {
|
||||
flags := binary.BigEndian.Uint16(data[pos:])
|
||||
child := binary.BigEndian.Uint16(data[pos+2:])
|
||||
binary.BigEndian.PutUint16(data[pos+2:], remap[child])
|
||||
pos += 6
|
||||
if flags&1 != 0 {
|
||||
pos += 2
|
||||
}
|
||||
switch flags & 0xc8 {
|
||||
case 8:
|
||||
pos += 2
|
||||
case 0x40:
|
||||
pos += 4
|
||||
case 0x80:
|
||||
pos += 8
|
||||
}
|
||||
if flags&0x20 == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
glyf = append(glyf, data...)
|
||||
for len(glyf)%4 != 0 {
|
||||
glyf = append(glyf, 0)
|
||||
}
|
||||
hmtx = binary.BigEndian.AppendUint16(hmtx, sfnt.GlyphAdvance(id))
|
||||
hmtx = binary.BigEndian.AppendUint16(hmtx, uint16(sfnt.sideBearing(id, false)))
|
||||
if sfnt.vertical != 0 {
|
||||
vmtx = binary.BigEndian.AppendUint16(vmtx, sfnt.GlyphVerticalAdvance(id))
|
||||
vmtx = binary.BigEndian.AppendUint16(vmtx, uint16(sfnt.sideBearing(id, true)))
|
||||
}
|
||||
for _, char := range sfnt.glyphCharacters(id) {
|
||||
mapping[char] = remap[id]
|
||||
}
|
||||
}
|
||||
loca = binary.BigEndian.AppendUint32(loca, uint32(len(glyf)))
|
||||
table["glyf"], table["loca"], table["hmtx"] = glyf, loca, hmtx
|
||||
table["cmap"] = buildCmapTable(uint16(len(ids)), mapping)
|
||||
binary.BigEndian.PutUint16(table["head"][50:], 1)
|
||||
binary.BigEndian.PutUint16(table["hhea"][34:], uint16(len(ids)))
|
||||
binary.BigEndian.PutUint16(table["maxp"][4:], uint16(len(ids)))
|
||||
if len(table["post"]) >= 32 {
|
||||
table["post"] = table["post"][:32]
|
||||
binary.BigEndian.PutUint32(table["post"], 0x30000)
|
||||
}
|
||||
if sfnt.vertical != 0 {
|
||||
table["vmtx"] = vmtx
|
||||
binary.BigEndian.PutUint16(table["vhea"][34:], uint16(len(ids)))
|
||||
}
|
||||
return serializeOTF(table)
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
// 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 ofdgo
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/go-text/typesetting/font/opentype/tables"
|
||||
)
|
||||
|
||||
// sfntFont 保存独立字体表和基础度量,不包含绘图库对象
|
||||
type sfntFont struct {
|
||||
Tables map[string][]byte
|
||||
IsTrueType, IsCFF bool
|
||||
cmaps []sfntCmap
|
||||
units, count, horizontal, vertical uint16
|
||||
reverseOnce sync.Once
|
||||
reverse map[uint16][]rune
|
||||
}
|
||||
|
||||
// sfntCmap 保存编码记录及对应字符映射
|
||||
type sfntCmap struct {
|
||||
platform, encoding, format uint16
|
||||
mapping map[rune]uint16
|
||||
}
|
||||
|
||||
// parseSFNTFont 校验独立字体的必需表、度量范围及字符映射
|
||||
// 入参: data 独立SFNT数据
|
||||
// 返回: *sfntFont 字体, error 格式错误
|
||||
func parseSFNTFont(data []byte) (*sfntFont, error) {
|
||||
table, err := fontFileTables(data, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
head, maxp, hhea := table["head"], table["maxp"], table["hhea"]
|
||||
if len(head) != 54 || len(maxp) < 6 || len(hhea) < 36 {
|
||||
return nil, fmt.Errorf("missing or truncated font metrics")
|
||||
}
|
||||
f := &sfntFont{Tables: table, IsTrueType: table["glyf"] != nil, IsCFF: table["CFF "] != nil}
|
||||
f.units, f.count = binary.BigEndian.Uint16(head[18:]), binary.BigEndian.Uint16(maxp[4:])
|
||||
f.horizontal = binary.BigEndian.Uint16(hhea[34:])
|
||||
if f.units < 16 || f.units > 16384 || f.count == 0 || f.IsTrueType == f.IsCFF || !validSFNTMetrics(table["hmtx"], f.count, f.horizontal) {
|
||||
return nil, fmt.Errorf("invalid font outlines or horizontal metrics")
|
||||
}
|
||||
if vhea := table["vhea"]; vhea != nil {
|
||||
if len(vhea) < 36 {
|
||||
return nil, fmt.Errorf("truncated vertical metrics header")
|
||||
}
|
||||
f.vertical = binary.BigEndian.Uint16(vhea[34:])
|
||||
if !validSFNTMetrics(table["vmtx"], f.count, f.vertical) {
|
||||
return nil, fmt.Errorf("invalid vertical metrics")
|
||||
}
|
||||
}
|
||||
if f.IsTrueType {
|
||||
format := binary.BigEndian.Uint16(head[50:])
|
||||
if format > 1 || len(table["loca"]) < (int(f.count)+1)*(2+2*int(format)) {
|
||||
return nil, fmt.Errorf("invalid glyph location table")
|
||||
}
|
||||
}
|
||||
f.cmaps, err = readSFNTCmaps(table["cmap"])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, cmap := range f.cmaps {
|
||||
for _, glyph := range cmap.mapping {
|
||||
if glyph >= f.count {
|
||||
return nil, fmt.Errorf("cmap glyph index out of range")
|
||||
}
|
||||
}
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// validSFNTMetrics 校验完整度量记录和末尾边距数组的范围
|
||||
// 入参: data 度量表, count 字形数, metrics 完整度量记录数
|
||||
// 返回: bool 是否有效
|
||||
func validSFNTMetrics(data []byte, count, metrics uint16) bool {
|
||||
return metrics != 0 && metrics <= count && len(data) >= int(metrics)*4+int(count-metrics)*2
|
||||
}
|
||||
|
||||
// readSFNTCmaps 校验各编码子表并保留原顺序,共享子表只解析一次
|
||||
// 入参: data cmap表
|
||||
// 返回: []sfntCmap 编码记录, error 表结构错误
|
||||
func readSFNTCmaps(data []byte) ([]sfntCmap, error) {
|
||||
data, err := normalizeCmap(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, _, err := tables.ParseCmap(data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
count := int(binary.BigEndian.Uint16(data[2:]))
|
||||
result := make([]sfntCmap, 0, count)
|
||||
parsed := make(map[uint32]map[rune]uint16)
|
||||
for i := range count {
|
||||
record := data[4+8*i:]
|
||||
offset := binary.BigEndian.Uint32(record[4:])
|
||||
if uint64(offset)+2 > uint64(len(data)) {
|
||||
return nil, fmt.Errorf("invalid cmap offset")
|
||||
}
|
||||
sub := data[offset:]
|
||||
format := binary.BigEndian.Uint16(sub)
|
||||
mapping, found := parsed[offset]
|
||||
if !found {
|
||||
mapping = make(map[rune]uint16)
|
||||
var err error
|
||||
switch format {
|
||||
case 0:
|
||||
parseCmapFormat0(sub, mapping)
|
||||
case 4:
|
||||
parseCmapFormat4(sub, mapping)
|
||||
case 6:
|
||||
parseCmapFormat6(sub, mapping)
|
||||
case 12:
|
||||
parseCmapFormat12(sub, mapping)
|
||||
case 2:
|
||||
mapping, err = parseCmapFormat2(sub)
|
||||
case 8, 10, 13:
|
||||
mapping, err = parseCmapExtended(sub)
|
||||
case 14:
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported cmap format %d", format)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parsed[offset] = mapping
|
||||
}
|
||||
result = append(result, sfntCmap{binary.BigEndian.Uint16(record), binary.BigEndian.Uint16(record[2:]), format, mapping})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// UnitsPerEm 返回字体设计单位
|
||||
// 返回: uint16 每em单位数
|
||||
func (f *sfntFont) UnitsPerEm() uint16 { return f.units }
|
||||
|
||||
// NumGlyphs 返回字形总数
|
||||
// 返回: uint16 字形数
|
||||
func (f *sfntFont) NumGlyphs() uint16 { return f.count }
|
||||
|
||||
// GlyphIndex 按编码表顺序查找字符,不替换缺失字形
|
||||
// 入参: char 字符
|
||||
// 返回: uint16 字形编号
|
||||
func (f *sfntFont) GlyphIndex(char rune) uint16 {
|
||||
for _, cmap := range f.cmaps {
|
||||
if gid := cmap.mapping[char]; gid != 0 {
|
||||
return gid
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// glyphCharacters 返回指向指定字形的全部字符
|
||||
// 入参: gid 字形编号
|
||||
// 返回: []rune 排序后的字符
|
||||
func (f *sfntFont) glyphCharacters(gid uint16) []rune {
|
||||
f.reverseOnce.Do(func() {
|
||||
f.reverse = make(map[uint16][]rune)
|
||||
seen := make(map[rune]bool)
|
||||
for _, cmap := range f.cmaps {
|
||||
for char, glyph := range cmap.mapping {
|
||||
if glyph != 0 && !seen[char] {
|
||||
f.reverse[glyph] = append(f.reverse[glyph], char)
|
||||
seen[char] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, chars := range f.reverse {
|
||||
sort.Slice(chars, func(i, j int) bool { return chars[i] < chars[j] })
|
||||
}
|
||||
})
|
||||
return f.reverse[gid]
|
||||
}
|
||||
|
||||
// GlyphAdvance 返回水平字宽
|
||||
// 入参: gid 字形编号
|
||||
// 返回: uint16 字宽
|
||||
func (f *sfntFont) GlyphAdvance(gid uint16) uint16 {
|
||||
if gid >= f.count {
|
||||
return 0
|
||||
}
|
||||
return binary.BigEndian.Uint16(f.Tables["hmtx"][int(min(gid, f.horizontal-1))*4:])
|
||||
}
|
||||
|
||||
// GlyphVerticalAdvance 返回纵向字距,无垂直度量时使用em高度
|
||||
// 入参: gid 字形编号
|
||||
// 返回: uint16 纵向字距
|
||||
func (f *sfntFont) GlyphVerticalAdvance(gid uint16) uint16 {
|
||||
if f.vertical == 0 {
|
||||
return f.units
|
||||
}
|
||||
if gid >= f.count {
|
||||
return 0
|
||||
}
|
||||
return binary.BigEndian.Uint16(f.Tables["vmtx"][int(min(gid, f.vertical-1))*4:])
|
||||
}
|
||||
|
||||
// sideBearing 返回指定方向的字形边距
|
||||
// 入参: gid 字形编号, vertical 是否读取顶部边距
|
||||
// 返回: int16 边距
|
||||
func (f *sfntFont) sideBearing(gid uint16, vertical bool) int16 {
|
||||
data, count := f.Tables["hmtx"], f.horizontal
|
||||
if vertical {
|
||||
data, count = f.Tables["vmtx"], f.vertical
|
||||
}
|
||||
if count == 0 || gid >= f.count {
|
||||
return 0
|
||||
}
|
||||
offset := int(gid)*4 + 2
|
||||
if gid >= count {
|
||||
offset = int(count)*4 + int(gid-count)*2
|
||||
}
|
||||
return int16(binary.BigEndian.Uint16(data[offset:]))
|
||||
}
|
||||
|
||||
// VerticalMetrics 依据OS/2排版标志选择行度量,缺失时使用hhea
|
||||
// 返回: uint16 上升高度, uint16 下降高度, uint16 行间隙
|
||||
func (f *sfntFont) VerticalMetrics() (uint16, uint16, uint16) {
|
||||
hhea, os2 := f.Tables["hhea"], f.Tables["OS/2"]
|
||||
a, d, gap := int(int16(binary.BigEndian.Uint16(hhea[4:]))), int(int16(binary.BigEndian.Uint16(hhea[6:]))), int(int16(binary.BigEndian.Uint16(hhea[8:])))
|
||||
if len(os2) >= 78 {
|
||||
if binary.BigEndian.Uint16(os2[62:])&0x80 != 0 {
|
||||
ta, td := int(int16(binary.BigEndian.Uint16(os2[68:]))), int(int16(binary.BigEndian.Uint16(os2[70:])))
|
||||
if ta > 0 && td < 0 {
|
||||
a, d, gap = ta, td, int(int16(binary.BigEndian.Uint16(os2[72:])))
|
||||
}
|
||||
} else {
|
||||
wa, wd := int(binary.BigEndian.Uint16(os2[74:])), int(binary.BigEndian.Uint16(os2[76:]))
|
||||
if wa != 0 && wd != 0 {
|
||||
gap, a, d = a-d+gap-wa-wd, wa, -wd
|
||||
}
|
||||
}
|
||||
}
|
||||
return uint16(max(0, a)), uint16(max(0, -d)), uint16(min(65535, max(0, gap)))
|
||||
}
|
||||
|
||||
// Write 保留原始表数据并重新计算字体校验和
|
||||
// 返回: []byte 独立字体
|
||||
func (f *sfntFont) Write() []byte { data, _ := serializeOTF(f.Tables); return data }
|
||||
|
||||
// glyphDependencies 收集复合字形依赖,拒绝循环引用和截断记录
|
||||
// 入参: gid 字形编号
|
||||
// 返回: []uint16 依赖字形, error 字形错误
|
||||
func (f *sfntFont) glyphDependencies(gid uint16) ([]uint16, error) {
|
||||
seen, active := make(map[uint16]bool), make(map[uint16]bool)
|
||||
var result []uint16
|
||||
var visit func(uint16) error
|
||||
visit = func(id uint16) error {
|
||||
if id >= f.count || active[id] || len(active) > 64 {
|
||||
return fmt.Errorf("invalid composite glyph reference")
|
||||
}
|
||||
if seen[id] {
|
||||
return nil
|
||||
}
|
||||
seen[id], active[id] = true, true
|
||||
defer delete(active, id)
|
||||
data, err := trueTypeGlyphData(f.Tables, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(data) < 10 {
|
||||
return fmt.Errorf("truncated glyph header")
|
||||
}
|
||||
if int16(binary.BigEndian.Uint16(data)) >= 0 {
|
||||
return nil
|
||||
}
|
||||
for pos := 10; ; {
|
||||
if len(data)-pos < 4 {
|
||||
return fmt.Errorf("truncated composite glyph")
|
||||
}
|
||||
flags, child := binary.BigEndian.Uint16(data[pos:]), binary.BigEndian.Uint16(data[pos+2:])
|
||||
pos += 6
|
||||
if flags&1 != 0 {
|
||||
pos += 2
|
||||
}
|
||||
switch flags & 0xc8 {
|
||||
case 8:
|
||||
pos += 2
|
||||
case 0x40:
|
||||
pos += 4
|
||||
case 0x80:
|
||||
pos += 8
|
||||
case 0:
|
||||
default:
|
||||
return fmt.Errorf("invalid composite transform")
|
||||
}
|
||||
if pos > len(data) {
|
||||
return fmt.Errorf("truncated composite transform")
|
||||
}
|
||||
if !seen[child] {
|
||||
result = append(result, child)
|
||||
}
|
||||
if err := visit(child); err != nil {
|
||||
return err
|
||||
}
|
||||
if flags&0x20 == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
err := visit(gid)
|
||||
return result, err
|
||||
}
|
||||
+52
-22
@@ -26,8 +26,6 @@ import (
|
||||
"runtime"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/tdewolff/font"
|
||||
)
|
||||
|
||||
// 字体来源:文件、文件系统和系统字体
|
||||
@@ -37,11 +35,8 @@ const (
|
||||
fontSourceSystem
|
||||
)
|
||||
|
||||
// systemFontIndex 串行初始化系统字体索引,不注册绘图后端
|
||||
var systemFontIndex struct {
|
||||
sync.Mutex
|
||||
fonts *font.SystemFonts
|
||||
}
|
||||
// systemFontIndex 按需建立只读系统字体索引
|
||||
var systemFontIndex = sync.OnceValue(indexSystemFonts)
|
||||
|
||||
// fontSourceKind 字体来源类型
|
||||
type fontSourceKind uint8
|
||||
@@ -100,20 +95,11 @@ func (c *fontSourceCache) child() *fontSourceCache {
|
||||
func (r *Renderer) readFontSource(source fontSource, definition *Font) ([]byte, error) {
|
||||
key := fontSourceKey{kind: source.kind, index: source.index, face: source.face, name: source.name}
|
||||
if source.kind == fontSourceSystem {
|
||||
systemFontIndex.Lock()
|
||||
if systemFontIndex.fonts == nil {
|
||||
systemFontIndex.fonts, _ = font.FindSystemFonts(font.DefaultFontDirs())
|
||||
}
|
||||
weight := 400
|
||||
if definition.Bold {
|
||||
weight = 700
|
||||
}
|
||||
match, ok := systemFontIndex.fonts.Match(source.name, font.ParseStyleCSS(weight, definition.Italic))
|
||||
systemFontIndex.Unlock()
|
||||
if !ok {
|
||||
matches := fontFileMatches(systemFontIndex(), []string{source.name}, definition.Bold, definition.Italic)
|
||||
if len(matches) == 0 {
|
||||
return nil, fmt.Errorf("font %q not found", source.name)
|
||||
}
|
||||
key = fontSourceKey{kind: fontSourceFile, name: match.Filename}
|
||||
key = fontSourceKey{kind: fontSourceFile, name: matches[0].name, face: matches[0].face}
|
||||
}
|
||||
if cached, ok := r.fontSourcesCache.data[key]; ok {
|
||||
return cached.data, cached.err
|
||||
@@ -349,15 +335,59 @@ func (r *Renderer) matchFontFS(index int, names []string, bold, italic bool) []f
|
||||
// systemFontDirs 获取系统字体目录
|
||||
// 返回: []string 字体目录列表
|
||||
func systemFontDirs() []string {
|
||||
home, _ := os.UserHomeDir()
|
||||
switch runtime.GOOS {
|
||||
case "android":
|
||||
return []string{"/product/fonts", "/system/fonts"}
|
||||
case "darwin":
|
||||
return []string{`/Library/Fonts`}
|
||||
return []string{"/System/Library/Fonts", "/Library/Fonts", filepath.Join(home, "Library", "Fonts")}
|
||||
case "linux":
|
||||
return []string{`/usr/share/fonts`}
|
||||
dirs := []string{"/usr/share/fonts", "/usr/local/share/fonts", filepath.Join(home, ".fonts")}
|
||||
data := os.Getenv("XDG_DATA_HOME")
|
||||
if data == "" {
|
||||
data = filepath.Join(home, ".local", "share")
|
||||
}
|
||||
dirs = append(dirs, filepath.Join(data, "fonts"))
|
||||
for _, dir := range filepath.SplitList(os.Getenv("XDG_DATA_DIRS")) {
|
||||
dirs = append(dirs, filepath.Join(dir, "fonts"))
|
||||
}
|
||||
return dirs
|
||||
case "windows":
|
||||
return []string{`C:\Windows\Fonts`}
|
||||
windows := os.Getenv("WINDIR")
|
||||
if windows == "" {
|
||||
windows = `C:\Windows`
|
||||
}
|
||||
dirs := []string{filepath.Join(windows, "Fonts")}
|
||||
if local := os.Getenv("LOCALAPPDATA"); local != "" {
|
||||
dirs = append(dirs, filepath.Join(local, "Microsoft", "Windows", "Fonts"))
|
||||
}
|
||||
return dirs
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// indexSystemFonts 递归读取系统字体名称,保留集合索引
|
||||
// 返回: []fontFileCandidate 字体候选
|
||||
func indexSystemFonts() []fontFileCandidate {
|
||||
var files []string
|
||||
seen := make(map[string]bool)
|
||||
for _, dir := range systemFontDirs() {
|
||||
_ = filepath.WalkDir(dir, func(name string, entry fs.DirEntry, err error) error {
|
||||
if err == nil && !entry.IsDir() && isFontFileName(name) && !seen[name] {
|
||||
seen[name] = true
|
||||
files = append(files, name)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
candidates := fontFileCandidates(files, filepath.Base)
|
||||
for i, candidate := range candidates {
|
||||
file, err := os.Open(candidate.name)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
candidates = appendFontFileNames(candidates, i, fontFileNames(file))
|
||||
file.Close()
|
||||
}
|
||||
return candidates
|
||||
}
|
||||
@@ -17,14 +17,12 @@ package ofdgo
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
"github.com/tdewolff/font"
|
||||
)
|
||||
|
||||
// variableSFNTSubsettable 核对TrueType可变轴与字形变化表的基本结构
|
||||
// 入参: sfnt 已解析字体
|
||||
// 返回: bool 是否具备可裁剪的变化数据
|
||||
func variableSFNTSubsettable(sfnt *font.SFNT) bool {
|
||||
func variableSFNTSubsettable(sfnt *sfntFont) bool {
|
||||
fvar, gvar := sfnt.Tables["fvar"], sfnt.Tables["gvar"]
|
||||
if len(fvar) < 16 || len(gvar) < 20 || binary.BigEndian.Uint32(fvar) != 0x10000 || binary.BigEndian.Uint32(gvar) != 0x10000 {
|
||||
return false
|
||||
@@ -38,7 +36,7 @@ func variableSFNTSubsettable(sfnt *font.SFNT) bool {
|
||||
// subsetVariableSFNTFont 保留字形编号及可变轴,仅裁剪未使用的轮廓和字形变化数据
|
||||
// 入参: sfnt 已解析字体, glyphs 使用的字形
|
||||
// 返回: []byte 字体子集, error 字形或变化表错误
|
||||
func subsetVariableSFNTFont(sfnt *font.SFNT, glyphs []uint16) ([]byte, error) {
|
||||
func subsetVariableSFNTFont(sfnt *sfntFont, glyphs []uint16) ([]byte, error) {
|
||||
if !variableSFNTSubsettable(sfnt) {
|
||||
return nil, fmt.Errorf("invalid TrueType variation tables")
|
||||
}
|
||||
@@ -49,12 +47,12 @@ func subsetVariableSFNTFont(sfnt *font.SFNT, glyphs []uint16) ([]byte, error) {
|
||||
return nil, fmt.Errorf("variable font glyph index out of range")
|
||||
}
|
||||
usage.glyphs[gid] = true
|
||||
for _, char := range sfnt.Cmap.ToUnicode(gid) {
|
||||
for _, char := range sfnt.glyphCharacters(gid) {
|
||||
usage.chars[char] = true
|
||||
}
|
||||
}
|
||||
for gid := range usage.glyphs {
|
||||
dependencies, err := sfnt.Glyf.Dependencies(gid)
|
||||
dependencies, err := sfnt.glyphDependencies(gid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
// 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 ofdgo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/zlib"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
|
||||
"github.com/andybalholm/brotli"
|
||||
)
|
||||
|
||||
// fontContainerLimit 限制压缩字体展开后的表数据总量
|
||||
const fontContainerLimit = 256 << 20
|
||||
|
||||
// woff2Tags 保存WOFF2表目录的标准标签顺序
|
||||
var woff2Tags = [...]string{
|
||||
"cmap", "head", "hhea", "hmtx", "maxp", "name", "OS/2", "post",
|
||||
"cvt ", "fpgm", "glyf", "loca", "prep", "CFF ", "VORG", "EBDT",
|
||||
"EBLC", "gasp", "hdmx", "kern", "LTSH", "PCLT", "VDMX", "vhea",
|
||||
"vmtx", "BASE", "GDEF", "GPOS", "GSUB", "EBSC", "JSTF", "MATH",
|
||||
"CBDT", "CBLC", "COLR", "CPAL", "SVG ", "sbix", "acnt", "avar",
|
||||
"bdat", "bloc", "bsln", "cvar", "fdsc", "feat", "fmtx", "fvar",
|
||||
"gvar", "hsty", "just", "lcar", "mort", "morx", "opbd", "prop",
|
||||
"trak", "Zapf", "Silf", "Glat", "Gloc", "Feat", "Sill",
|
||||
}
|
||||
|
||||
// decodeWOFF2 展开未变换的WOFF2表,字体集合及字形变换交由绘图适配器处理
|
||||
// 入参: data WOFF2文件
|
||||
// 返回: []byte OpenType数据, bool 是否已处理, error 封装错误
|
||||
func decodeWOFF2(data []byte) ([]byte, bool, error) {
|
||||
if len(data) < 48 || string(data[:4]) != "wOF2" || uint64(binary.BigEndian.Uint32(data[8:])) != uint64(len(data)) {
|
||||
return nil, true, fmt.Errorf("invalid WOFF2 header")
|
||||
}
|
||||
if string(data[4:8]) == "ttcf" {
|
||||
return nil, false, nil
|
||||
}
|
||||
count := int(binary.BigEndian.Uint16(data[12:]))
|
||||
if count == 0 || count > (len(data)-48)/2 {
|
||||
return nil, true, fmt.Errorf("invalid WOFF2 table count")
|
||||
}
|
||||
tags := make([]string, 0, count)
|
||||
sizes := make([]uint32, 0, count)
|
||||
table := make(map[string][]byte, count)
|
||||
pos, total, transformed := 48, uint64(0), false
|
||||
for range count {
|
||||
if pos >= len(data) {
|
||||
return nil, true, fmt.Errorf("truncated WOFF2 directory")
|
||||
}
|
||||
flags := data[pos]
|
||||
pos++
|
||||
var tag string
|
||||
if index := flags & 63; index == 63 {
|
||||
if len(data)-pos < 4 {
|
||||
return nil, true, fmt.Errorf("truncated WOFF2 tag")
|
||||
}
|
||||
tag = string(data[pos : pos+4])
|
||||
pos += 4
|
||||
} else {
|
||||
tag = woff2Tags[index]
|
||||
}
|
||||
if _, exists := table[tag]; exists {
|
||||
return nil, true, fmt.Errorf("duplicate WOFF2 table %q", tag)
|
||||
}
|
||||
table[tag] = nil
|
||||
size, err := readWOFF2Base128(data, &pos)
|
||||
if err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
version := flags >> 6
|
||||
changed := version != 0
|
||||
if tag == "glyf" || tag == "loca" {
|
||||
if version != 0 && version != 3 {
|
||||
return nil, true, fmt.Errorf("invalid WOFF2 glyph transform")
|
||||
}
|
||||
changed = version == 0
|
||||
} else if version != 0 && (tag != "hmtx" || version != 1) {
|
||||
return nil, true, fmt.Errorf("invalid WOFF2 table transform %q", tag)
|
||||
}
|
||||
if changed {
|
||||
transformed = true
|
||||
size, err = readWOFF2Base128(data, &pos)
|
||||
if err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
if tag == "loca" && size != 0 {
|
||||
return nil, true, fmt.Errorf("invalid WOFF2 loca length")
|
||||
}
|
||||
}
|
||||
total += uint64(size)
|
||||
if total+uint64(12+19*count) > fontContainerLimit {
|
||||
return nil, true, fmt.Errorf("WOFF2 font exceeds size limit")
|
||||
}
|
||||
tags = append(tags, tag)
|
||||
sizes = append(sizes, size)
|
||||
}
|
||||
end := uint64(pos) + uint64(binary.BigEndian.Uint32(data[20:]))
|
||||
if end > uint64(len(data)) {
|
||||
return nil, true, fmt.Errorf("invalid WOFF2 compressed length")
|
||||
}
|
||||
if err := validateWOFFBlocks(data, end, 28); err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
if transformed {
|
||||
return nil, false, nil
|
||||
}
|
||||
reader := brotli.NewReader(bytes.NewReader(data[pos:end]))
|
||||
body, err := io.ReadAll(io.LimitReader(reader, int64(total)+1))
|
||||
if err != nil {
|
||||
return nil, true, fmt.Errorf("decode WOFF2 data: %w", err)
|
||||
}
|
||||
if uint64(len(body)) != total {
|
||||
return nil, true, fmt.Errorf("invalid WOFF2 expanded length")
|
||||
}
|
||||
for i, tag := range tags {
|
||||
size := int(sizes[i])
|
||||
table[tag], body = body[:size], body[size:]
|
||||
}
|
||||
decoded, err := serializeOTF(table)
|
||||
return decoded, true, err
|
||||
}
|
||||
|
||||
// readWOFF2Base128 读取WOFF2无符号变长整数,拒绝溢出和前导零
|
||||
// 入参: data 编码数据, pos 当前读取位置
|
||||
// 返回: uint32 整数值, error 编码错误
|
||||
func readWOFF2Base128(data []byte, pos *int) (uint32, error) {
|
||||
var value uint32
|
||||
for i := range 5 {
|
||||
if *pos >= len(data) {
|
||||
return 0, fmt.Errorf("truncated WOFF2 integer")
|
||||
}
|
||||
b := data[*pos]
|
||||
*pos++
|
||||
if i == 0 && b == 0x80 || value > 0x1ffffff {
|
||||
return 0, fmt.Errorf("invalid WOFF2 integer")
|
||||
}
|
||||
value = value<<7 | uint32(b&127)
|
||||
if b&128 == 0 {
|
||||
return value, nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("invalid WOFF2 integer")
|
||||
}
|
||||
|
||||
// decodeWOFF 展开WOFF表数据,不解析或更改字体轮廓
|
||||
// 入参: data WOFF文件
|
||||
// 返回: []byte OpenType数据, error 封装错误
|
||||
func decodeWOFF(data []byte) ([]byte, error) {
|
||||
if len(data) < 44 || uint64(binary.BigEndian.Uint32(data[8:])) != uint64(len(data)) || binary.BigEndian.Uint16(data[14:]) != 0 {
|
||||
return nil, fmt.Errorf("invalid WOFF header")
|
||||
}
|
||||
count := int(binary.BigEndian.Uint16(data[12:]))
|
||||
if count == 0 || count > (len(data)-44)/20 {
|
||||
return nil, fmt.Errorf("invalid WOFF table count")
|
||||
}
|
||||
table := make(map[string][]byte, count)
|
||||
ranges := make([][2]uint64, 0, count)
|
||||
total := uint64(12 + 16*count)
|
||||
for i := range count {
|
||||
record := data[44+i*20:]
|
||||
tag := string(record[:4])
|
||||
offset, packed, size := uint64(binary.BigEndian.Uint32(record[4:])), uint64(binary.BigEndian.Uint32(record[8:])), uint64(binary.BigEndian.Uint32(record[12:]))
|
||||
total += (size + 3) &^ 3
|
||||
if _, exists := table[tag]; exists || offset < uint64(44+20*count) || offset%4 != 0 || packed > size || offset+packed > uint64(len(data)) || total > fontContainerLimit {
|
||||
return nil, fmt.Errorf("invalid WOFF table %q", tag)
|
||||
}
|
||||
body := data[offset : offset+packed]
|
||||
if packed < size {
|
||||
reader, err := zlib.NewReader(bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body, err = io.ReadAll(io.LimitReader(reader, int64(size)+1))
|
||||
reader.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if uint64(len(body)) != size {
|
||||
return nil, fmt.Errorf("invalid WOFF expanded length")
|
||||
}
|
||||
}
|
||||
table[tag] = body
|
||||
ranges = append(ranges, [2]uint64{offset, offset + packed})
|
||||
}
|
||||
if total != uint64(binary.BigEndian.Uint32(data[16:])) {
|
||||
return nil, fmt.Errorf("invalid WOFF SFNT size")
|
||||
}
|
||||
sort.Slice(ranges, func(i, j int) bool { return ranges[i][0] < ranges[j][0] })
|
||||
end := uint64(44 + 20*count)
|
||||
for _, interval := range ranges {
|
||||
if interval[0] != (end+3)&^3 {
|
||||
return nil, fmt.Errorf("invalid WOFF table ranges")
|
||||
}
|
||||
end = interval[1]
|
||||
}
|
||||
if err := validateWOFFBlocks(data, end, 24); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return serializeOTF(table)
|
||||
}
|
||||
|
||||
// validateWOFFBlocks 校验附加数据块的边界及对齐,字体元数据不写入输出
|
||||
// 入参: data 字体文件, end 字体数据结束位置, fields 元数据偏移字段位置
|
||||
// 返回: error 附加块错误
|
||||
func validateWOFFBlocks(data []byte, end uint64, fields int) error {
|
||||
for _, pos := range []int{fields, fields + 12} {
|
||||
offset, length := uint64(binary.BigEndian.Uint32(data[pos:])), uint64(binary.BigEndian.Uint32(data[pos+4:]))
|
||||
if offset == 0 {
|
||||
if length != 0 || pos == fields && binary.BigEndian.Uint32(data[pos+8:]) != 0 {
|
||||
return fmt.Errorf("invalid WOFF optional block")
|
||||
}
|
||||
continue
|
||||
}
|
||||
if length == 0 || offset != (end+3)&^3 || offset+length > uint64(len(data)) {
|
||||
return fmt.Errorf("invalid WOFF block range")
|
||||
}
|
||||
for _, b := range data[end:offset] {
|
||||
if b != 0 {
|
||||
return fmt.Errorf("invalid WOFF padding")
|
||||
}
|
||||
}
|
||||
end = offset + length
|
||||
}
|
||||
if end > uint64(len(data)) || uint64(len(data)) > (end+3)&^3 {
|
||||
return fmt.Errorf("invalid WOFF trailing data")
|
||||
}
|
||||
for _, b := range data[end:] {
|
||||
if b != 0 {
|
||||
return fmt.Errorf("invalid WOFF padding")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -258,5 +258,19 @@ func init() {
|
||||
// 返回: RenderBackends 默认后端组合
|
||||
func defaultRenderBackends() RenderBackends {
|
||||
backend := CanvasBackend{}
|
||||
return RenderBackends{FontResources: SFNTBackend{}, Fonts: backend, Geometry: backend, Compiler: backend, Raster: backend, SVG: backend, PDF: backend, EPS: backend}
|
||||
return RenderBackends{FontResources: SFNTBackend{}, Fonts: SFNTBackend{}, Geometry: backend, Compiler: backend, Raster: backend, SVG: backend, PDF: backend, EPS: backend}
|
||||
}
|
||||
|
||||
// OpenFont 使用独立字体后端读取字体度量
|
||||
// 入参: data 字体数据
|
||||
// 返回: FontMetrics 字体度量, error 解析错误
|
||||
func (CanvasBackend) OpenFont(data []byte) (FontMetrics, error) {
|
||||
return (SFNTBackend{}).OpenFont(data)
|
||||
}
|
||||
|
||||
// ResolveFont 使用独立字体后端解析字体来源
|
||||
// 入参: r 渲染器, id 字体标识, exact 是否禁止无关回退
|
||||
// 返回: ResolvedFont 字体与来源, error 解析错误
|
||||
func (CanvasBackend) ResolveFont(r *Renderer, id string, exact bool) (ResolvedFont, error) {
|
||||
return (SFNTBackend{}).ResolveFont(r, id, exact)
|
||||
}
|
||||
+29
-123
@@ -19,135 +19,41 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
"github.com/tdewolff/font"
|
||||
)
|
||||
|
||||
// canvasGlyphBoundsLimit 限制单个字体保留的字形与字号边界数量
|
||||
const canvasGlyphBoundsLimit = 4096
|
||||
// canvasFontLoadMu 保护绘图后端加载无名称字体时使用的共享状态
|
||||
var canvasFontLoadMu sync.Mutex
|
||||
|
||||
// canvasFontMetrics 保留默认字体度量,写出时不更新原始时间
|
||||
type canvasFontMetrics struct {
|
||||
*font.SFNT
|
||||
*fontShaper
|
||||
outlines *sfntOutliner
|
||||
bounds *canvasFontBounds
|
||||
// loadCanvasFont 串行加载绘图字体,不改动原始字体数据
|
||||
// 入参: data 字体数据, index 集合索引, style 字体样式
|
||||
// 返回: *canvas.Font 绘图字体, error 加载错误
|
||||
func loadCanvasFont(data []byte, index int, style canvas.FontStyle) (*canvas.Font, error) {
|
||||
canvasFontLoadMu.Lock()
|
||||
defer canvasFontLoadMu.Unlock()
|
||||
return canvas.LoadFont(data, index, style)
|
||||
}
|
||||
|
||||
// canvasFontBounds 缓存已成功提取的字形边界,不保留轮廓或失败结果
|
||||
type canvasFontBounds struct {
|
||||
mu sync.Mutex
|
||||
values map[canvasGlyphSize]Box
|
||||
}
|
||||
|
||||
// canvasGlyphSize 区分字形编号及精确毫米字号
|
||||
type canvasGlyphSize struct {
|
||||
glyph uint16
|
||||
size float64
|
||||
}
|
||||
|
||||
// ResolveFont 按需加载字体资源,保留内嵌数据及外部字体的名称、样式和集合索引匹配
|
||||
// 入参: r 渲染器, id 字体标识, exact 是否禁止无关回退
|
||||
// 返回: ResolvedFont 字体与来源, error 字体不可用错误
|
||||
func (b CanvasBackend) ResolveFont(r *Renderer, id string, exact bool) (ResolvedFont, error) {
|
||||
return r.resolveFontSource(b, id, exact)
|
||||
}
|
||||
|
||||
// OpenFont 解析独立SFNT数据,度量与原有编辑排版保持一致
|
||||
// 入参: data 字体数据
|
||||
// 返回: FontMetrics 字体度量, error 解析错误
|
||||
func (CanvasBackend) OpenFont(data []byte) (FontMetrics, error) {
|
||||
data, err := defaultCFF2Font(data)
|
||||
// decodeFontContainer 展开字体封装,保留原始OpenType表及CFF2轮廓
|
||||
// 入参: data 字体文件
|
||||
// 返回: []byte OpenType数据, error 格式错误
|
||||
func decodeFontContainer(data []byte) ([]byte, error) {
|
||||
if len(data) < 12 {
|
||||
return nil, fmt.Errorf("invalid font header")
|
||||
}
|
||||
switch string(data[:4]) {
|
||||
case "\x00\x01\x00\x00", "OTTO", "true", "ttcf":
|
||||
return data, nil
|
||||
case "wOFF":
|
||||
return decodeWOFF(data)
|
||||
case "wOF2":
|
||||
decoded, handled, err := decodeWOFF2(data)
|
||||
if handled {
|
||||
return decoded, err
|
||||
}
|
||||
}
|
||||
loaded, err := loadCanvasFont(data, 0, canvas.FontRegular)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sfnt, err := font.ParseSFNT(data, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
metrics := &canvasFontMetrics{SFNT: sfnt, outlines: &sfntOutliner{font: sfnt}, bounds: &canvasFontBounds{}}
|
||||
metrics.fontShaper = &fontShaper{metrics: metrics}
|
||||
return metrics, nil
|
||||
}
|
||||
|
||||
// Write 返回字体数据,不修改字体元信息
|
||||
// 返回: []byte 独立SFNT数据
|
||||
func (f canvasFontMetrics) Write() []byte {
|
||||
return fontSFNTData(f.SFNT)
|
||||
}
|
||||
|
||||
// GlyphOutline 返回基线原点、纵轴向下的字形轮廓
|
||||
// 入参: glyph 字形编号, size 字号,单位为毫米
|
||||
// 返回: GeometryPath 字形路径, error 字形解析错误
|
||||
func (f canvasFontMetrics) GlyphOutline(glyph uint16, size float64) (GeometryPath, error) {
|
||||
if !rasterPositive(size) || glyph >= f.NumGlyphs() {
|
||||
return nil, fmt.Errorf("invalid glyph or size")
|
||||
}
|
||||
path := &canvas.Path{}
|
||||
if err := f.outlines.path(path, glyph, size/float64(f.UnitsPerEm())); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return *geometryFromCanvasPath(path), nil
|
||||
}
|
||||
|
||||
// GlyphBounds 返回字形轮廓的精确边界,重复编号及字号复用有限缓存
|
||||
// 入参: glyph 字形编号, size 字号,单位为毫米
|
||||
// 返回: Box 字形边界, error 字形解析错误
|
||||
func (f canvasFontMetrics) GlyphBounds(glyph uint16, size float64) (Box, error) {
|
||||
key := canvasGlyphSize{glyph: glyph, size: size}
|
||||
if f.bounds != nil {
|
||||
f.bounds.mu.Lock()
|
||||
value, ok := f.bounds.values[key]
|
||||
f.bounds.mu.Unlock()
|
||||
if ok {
|
||||
return value, nil
|
||||
}
|
||||
}
|
||||
outline, err := f.GlyphOutline(glyph, size)
|
||||
if err != nil {
|
||||
return Box{}, err
|
||||
}
|
||||
value, err := outline.Bounds()
|
||||
if err != nil {
|
||||
return Box{}, err
|
||||
}
|
||||
if f.bounds != nil {
|
||||
f.bounds.mu.Lock()
|
||||
if f.bounds.values == nil || len(f.bounds.values) >= canvasGlyphBoundsLimit {
|
||||
f.bounds.values = make(map[canvasGlyphSize]Box)
|
||||
}
|
||||
f.bounds.values[key] = value
|
||||
f.bounds.mu.Unlock()
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// GlyphWarning 返回无效字形指令后保留设计轮廓的提示,原始字体不变
|
||||
// 入参: glyph 字形编号
|
||||
// 返回: error 恢复原因,无恢复时为空
|
||||
func (f canvasFontMetrics) GlyphWarning(glyph uint16) error {
|
||||
return f.outlines.glyphWarning(glyph)
|
||||
}
|
||||
|
||||
// GlyphOutlines 批量提取字形轮廓,重复编号只解析一次
|
||||
// 入参: glyphs 字形编号, size 毫米字号
|
||||
// 返回: []GeometryPath 同序只读轮廓, error 轮廓错误
|
||||
func (f canvasFontMetrics) GlyphOutlines(glyphs []uint16, size float64) ([]GeometryPath, error) {
|
||||
if !rasterPositive(size) {
|
||||
return nil, fmt.Errorf("invalid glyph size")
|
||||
}
|
||||
result := make([]GeometryPath, len(glyphs))
|
||||
paths := make(map[uint16]GeometryPath, len(glyphs))
|
||||
for i, glyph := range glyphs {
|
||||
path, ok := paths[glyph]
|
||||
if !ok {
|
||||
var err error
|
||||
path, err = f.GlyphOutline(glyph, size)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
paths[glyph] = path
|
||||
}
|
||||
result[i] = path
|
||||
}
|
||||
return result, nil
|
||||
return serializeOTF(loaded.Tables)
|
||||
}
|
||||
@@ -51,7 +51,10 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily {
|
||||
return ff
|
||||
}
|
||||
ff := canvas.NewFontFamily(fmt.Sprintf("ofdgo-%x-%d", resolved.digest, fontStyle))
|
||||
if err := ff.LoadFont(fontData, 0, fontStyle); err != nil {
|
||||
canvasFontLoadMu.Lock()
|
||||
err = ff.LoadFont(fontData, 0, fontStyle)
|
||||
canvasFontLoadMu.Unlock()
|
||||
if err != nil {
|
||||
r.renderError = err
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -103,7 +103,12 @@ func (r *Renderer) cachedTextGlyphPath(face *canvas.FontFace, glyph textGlyph) (
|
||||
}
|
||||
outlines := r.canvasState().fontOutlines[face.Font]
|
||||
if outlines == nil {
|
||||
outlines = &sfntOutliner{font: face.Font.SFNT}
|
||||
metrics, err := parseSFNTFont(fontSFNTData(face.Font.Tables))
|
||||
if err != nil {
|
||||
r.renderError = err
|
||||
return &canvas.Path{}, 0
|
||||
}
|
||||
outlines = &sfntOutliner{font: metrics}
|
||||
r.canvasState().fontOutlines[face.Font] = outlines
|
||||
}
|
||||
path, width, err := textGlyphPath(face, glyph, outlines)
|
||||
|
||||
@@ -17,7 +17,6 @@ package ofdgo
|
||||
import (
|
||||
"github.com/tdewolff/canvas"
|
||||
canvastext "github.com/tdewolff/canvas/text"
|
||||
canvasfont "github.com/tdewolff/font"
|
||||
"github.com/xiaoqidun/pdfgo"
|
||||
)
|
||||
|
||||
@@ -52,12 +51,12 @@ func (r *pdfRenderer) preserveText(first int, text string) {
|
||||
func (r *pdfRenderer) glyphText(face *canvas.FontFace, glyph textGlyph) (*canvas.Text, error) {
|
||||
font := r.fonts[face.Font]
|
||||
if font == nil {
|
||||
sfnt, err := canvasfont.ParseSFNT(fontSFNTData(face.Font.SFNT), 0)
|
||||
loaded, err := loadCanvasFont(fontSFNTData(face.Font.Tables), 0, face.Font.Style())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
copy := *face.Font
|
||||
copy.SFNT = sfnt
|
||||
copy.SFNT = loaded.SFNT
|
||||
font = ©
|
||||
if r.fonts == nil {
|
||||
r.fonts = make(map[*canvas.Font]*canvas.Font)
|
||||
|
||||
@@ -146,7 +146,7 @@ func (s *svgResourceRenderer) RenderText(text *canvas.Text, m canvas.Matrix) {
|
||||
font := text.MostCommonFontFace().Font
|
||||
resource, ok := s.renderer.canvasState().svgFontCache[font]
|
||||
if !ok {
|
||||
data := fontSFNTData(font.SFNT)
|
||||
data := fontSFNTData(font.Tables)
|
||||
resource = SVGFont{
|
||||
Name: fmt.Sprintf("ofdgo-%x-%d", sha256.Sum256(data), font.Style()),
|
||||
Weight: font.Style().CSS(),
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
"github.com/tdewolff/font"
|
||||
)
|
||||
|
||||
// ptPerMM 为毫米转换为排版点的比例
|
||||
@@ -183,7 +182,8 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
|
||||
ctx.Pop()
|
||||
return
|
||||
}
|
||||
face := ff.Face(sizePt, fillPaint, fontStyle, canvas.FontNormal, font.NoHinting)
|
||||
face := ff.Face(sizePt, fillPaint, fontStyle, canvas.FontNormal)
|
||||
face.Hinting = 0
|
||||
recoverBaseline := len(obj.TextCode) > 0 && obj.TextCode[0].Y == ""
|
||||
var metrics canvas.FontMetrics
|
||||
if textRun != nil || recoverBaseline {
|
||||
|
||||
Reference in new issue
Block a user