mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 15:06:46 +08:00
1 parent
7cdc1d2c07
commit
4fa65175bc
11 files changed
+1551
-29
No files matched your search
@@ -9,7 +9,7 @@ require (
|
|||||||
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
|
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
|
||||||
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
|
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
|
||||||
github.com/xiaoqidun/jbig2 v1.1.1
|
github.com/xiaoqidun/jbig2 v1.1.1
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20260926052151-12ff0e0004de
|
github.com/xiaoqidun/pdfgo v0.0.0-20260926072349-1d4e7a9c2616
|
||||||
golang.org/x/crypto v0.57.0
|
golang.org/x/crypto v0.57.0
|
||||||
golang.org/x/image v0.46.0
|
golang.org/x/image v0.46.0
|
||||||
golang.org/x/text v0.42.0
|
golang.org/x/text v0.42.0
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk=
|
|||||||
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||||
github.com/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI=
|
github.com/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI=
|
||||||
github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ=
|
github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ=
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20260926052151-12ff0e0004de h1:Aiyem3mCMV7+EAAGLYaI2XQGwEK1bZ1Bz+xTF5f0xiY=
|
github.com/xiaoqidun/pdfgo v0.0.0-20260926072349-1d4e7a9c2616 h1:16JKsw9CKBf+BBVeiiHg12bg7h1ocKSxyhUpw6eRIBk=
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20260926052151-12ff0e0004de/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ=
|
github.com/xiaoqidun/pdfgo v0.0.0-20260926072349-1d4e7a9c2616/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
|
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
|
||||||
|
|||||||
@@ -0,0 +1,381 @@
|
|||||||
|
// 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"
|
||||||
|
"math"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/tdewolff/font"
|
||||||
|
)
|
||||||
|
|
||||||
|
// sfntOutliner 缓存字体程序并提取独立字形轮廓
|
||||||
|
type sfntOutliner struct {
|
||||||
|
font *font.SFNT
|
||||||
|
once sync.Once
|
||||||
|
program *ttInterpreter
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ttGlyph 保存字形轮廓及四个度量虚拟点
|
||||||
|
type ttGlyph struct {
|
||||||
|
points []ttPoint
|
||||||
|
ends []uint16
|
||||||
|
metrics *[4]ttPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
// path 按TrueType指令和隐含点规则生成闭合轮廓,CFF保留原有解析
|
||||||
|
// 入参: path 路径接收器, glyph 字形编号, scale 字体单位缩放
|
||||||
|
// 返回: error 字形解析错误
|
||||||
|
func (f *sfntOutliner) path(path font.Pather, glyph uint16, scale float64) error {
|
||||||
|
if !f.font.IsTrueType {
|
||||||
|
return f.font.GlyphPath(path, glyph, 0, 0, 0, scale, font.NoHinting)
|
||||||
|
}
|
||||||
|
contour, err := f.glyph(glyph, make(map[uint16]bool))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
points := contour.points
|
||||||
|
first := 0
|
||||||
|
for _, endpoint := range contour.ends {
|
||||||
|
last := int(endpoint)
|
||||||
|
if last < first || last >= len(points)-4 {
|
||||||
|
return fmt.Errorf("invalid TrueType contour endpoints")
|
||||||
|
}
|
||||||
|
if last == first {
|
||||||
|
first = last + 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
point := func(i int) Point {
|
||||||
|
return Point{X: float64(points[i].x) * scale / 64, Y: float64(points[i].y) * scale / 64}
|
||||||
|
}
|
||||||
|
start, end := point(first), point(last)
|
||||||
|
begin, stop := first, last
|
||||||
|
if points[first].on {
|
||||||
|
begin++
|
||||||
|
} else if points[last].on {
|
||||||
|
start, stop = end, last-1
|
||||||
|
} else {
|
||||||
|
start = Point{X: (start.X + end.X) / 2, Y: (start.Y + end.Y) / 2}
|
||||||
|
}
|
||||||
|
path.MoveTo(start.X, start.Y)
|
||||||
|
var control Point
|
||||||
|
pending := false
|
||||||
|
for i := begin; i <= stop; i++ {
|
||||||
|
p := point(i)
|
||||||
|
if points[i].on {
|
||||||
|
if pending {
|
||||||
|
path.QuadTo(control.X, control.Y, p.X, p.Y)
|
||||||
|
} else {
|
||||||
|
path.LineTo(p.X, p.Y)
|
||||||
|
}
|
||||||
|
pending = false
|
||||||
|
} else {
|
||||||
|
if pending {
|
||||||
|
path.QuadTo(control.X, control.Y, (control.X+p.X)/2, (control.Y+p.Y)/2)
|
||||||
|
}
|
||||||
|
control, pending = p, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pending {
|
||||||
|
path.QuadTo(control.X, control.Y, start.X, start.Y)
|
||||||
|
}
|
||||||
|
path.Close()
|
||||||
|
first = last + 1
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// glyph 解码简单或复合字形并执行对应字节码,不改写字体资源
|
||||||
|
// 入参: id 字形编号, active 递归访问集合
|
||||||
|
// 返回: ttGlyph 字形轮廓, error 字形错误
|
||||||
|
func (f *sfntOutliner) glyph(id uint16, active map[uint16]bool) (ttGlyph, error) {
|
||||||
|
var result ttGlyph
|
||||||
|
if id >= f.font.NumGlyphs() || active[id] {
|
||||||
|
return result, fmt.Errorf("invalid TrueType component reference %d", id)
|
||||||
|
}
|
||||||
|
active[id] = true
|
||||||
|
defer delete(active, id)
|
||||||
|
data := f.font.Glyf.Get(id)
|
||||||
|
if len(data) == 0 {
|
||||||
|
data = make([]byte, 12)
|
||||||
|
}
|
||||||
|
if len(data) < 10 {
|
||||||
|
return result, fmt.Errorf("truncated TrueType glyph %d", id)
|
||||||
|
}
|
||||||
|
n := int(int16(binary.BigEndian.Uint16(data)))
|
||||||
|
var program []byte
|
||||||
|
var err error
|
||||||
|
if n >= 0 {
|
||||||
|
result, program, err = readTTSimple(data, n)
|
||||||
|
} else {
|
||||||
|
result, program, err = f.composite(data, active)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
left := int32(int16(binary.BigEndian.Uint16(data[2:]))) - int32(f.font.Hmtx.LeftSideBearing(id))
|
||||||
|
top := int32(int16(binary.BigEndian.Uint16(data[8:])))
|
||||||
|
if result.metrics != nil {
|
||||||
|
result.points = append(result.points, result.metrics[:]...)
|
||||||
|
} else {
|
||||||
|
result.points = append(result.points, ttPoint{x: left * 64, ox: left * 64}, ttPoint{x: (left + int32(f.font.GlyphAdvance(id))) * 64, ox: (left + int32(f.font.GlyphAdvance(id))) * 64}, ttPoint{y: top * 64, oy: top * 64}, ttPoint{y: (top - int32(f.font.GlyphVerticalAdvance(id))) * 64, oy: (top - int32(f.font.GlyphVerticalAdvance(id))) * 64})
|
||||||
|
}
|
||||||
|
if len(program) == 0 {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
outlineOnly, err := ttOutlineOnly(f.font.Tables, program)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
if outlineOnly {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
f.once.Do(func() { f.program, f.err = newTTInterpreter(f.font.Tables, f.font.UnitsPerEm()) })
|
||||||
|
if f.err != nil {
|
||||||
|
return result, f.err
|
||||||
|
}
|
||||||
|
if f.program.graphics.inhibit {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
vm := f.program.clone()
|
||||||
|
vm.zones[1], vm.ends = result.points, result.ends
|
||||||
|
if err := vm.run(program); err != nil {
|
||||||
|
return result, fmt.Errorf("TrueType glyph %d: %w", id, err)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// readTTSimple 读取简单字形的重复标志、差分坐标和字节码
|
||||||
|
// 入参: data 字形数据, count 轮廓数
|
||||||
|
// 返回: ttGlyph 字形轮廓, []byte 字节码, error 数据错误
|
||||||
|
func readTTSimple(data []byte, count int) (ttGlyph, []byte, error) {
|
||||||
|
var result ttGlyph
|
||||||
|
if len(data) < 12+2*count {
|
||||||
|
return result, nil, fmt.Errorf("truncated TrueType contour endpoints")
|
||||||
|
}
|
||||||
|
pos := 10
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
end := binary.BigEndian.Uint16(data[pos:])
|
||||||
|
pos += 2
|
||||||
|
if i > 0 && end <= result.ends[i-1] {
|
||||||
|
return result, nil, fmt.Errorf("unordered TrueType contour endpoints")
|
||||||
|
}
|
||||||
|
result.ends = append(result.ends, end)
|
||||||
|
}
|
||||||
|
length := int(binary.BigEndian.Uint16(data[pos:]))
|
||||||
|
pos += 2
|
||||||
|
if length > len(data)-pos {
|
||||||
|
return result, nil, fmt.Errorf("truncated TrueType glyph instructions")
|
||||||
|
}
|
||||||
|
program := data[pos : pos+length]
|
||||||
|
pos += length
|
||||||
|
points := 0
|
||||||
|
if count > 0 {
|
||||||
|
points = int(result.ends[count-1]) + 1
|
||||||
|
}
|
||||||
|
flags := make([]byte, 0, points)
|
||||||
|
for len(flags) < points {
|
||||||
|
if pos >= len(data) {
|
||||||
|
return result, nil, fmt.Errorf("truncated TrueType point flags")
|
||||||
|
}
|
||||||
|
flag := data[pos]
|
||||||
|
pos++
|
||||||
|
repeat := 1
|
||||||
|
if flag&8 != 0 {
|
||||||
|
if pos >= len(data) {
|
||||||
|
return result, nil, fmt.Errorf("truncated TrueType flag repeat")
|
||||||
|
}
|
||||||
|
repeat += int(data[pos])
|
||||||
|
pos++
|
||||||
|
}
|
||||||
|
if repeat > points-len(flags) {
|
||||||
|
return result, nil, fmt.Errorf("excessive TrueType flag repeat")
|
||||||
|
}
|
||||||
|
for i := 0; i < repeat; i++ {
|
||||||
|
flags = append(flags, flag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.points = make([]ttPoint, points)
|
||||||
|
for axis := 0; axis < 2; axis++ {
|
||||||
|
coordinate := int32(0)
|
||||||
|
short, same := byte(2<<axis), byte(16<<axis)
|
||||||
|
for i, flag := range flags {
|
||||||
|
if flag&short != 0 {
|
||||||
|
if pos >= len(data) {
|
||||||
|
return result, nil, fmt.Errorf("truncated TrueType short coordinate")
|
||||||
|
}
|
||||||
|
delta := int32(data[pos])
|
||||||
|
pos++
|
||||||
|
if flag&same == 0 {
|
||||||
|
delta = -delta
|
||||||
|
}
|
||||||
|
coordinate += delta
|
||||||
|
} else if flag&same == 0 {
|
||||||
|
if len(data)-pos < 2 {
|
||||||
|
return result, nil, fmt.Errorf("truncated TrueType coordinate")
|
||||||
|
}
|
||||||
|
coordinate += int32(int16(binary.BigEndian.Uint16(data[pos:])))
|
||||||
|
pos += 2
|
||||||
|
}
|
||||||
|
p := &result.points[i]
|
||||||
|
p.on = flag&1 != 0
|
||||||
|
if axis == 0 {
|
||||||
|
p.x, p.ox = coordinate*64, coordinate*64
|
||||||
|
} else {
|
||||||
|
p.y, p.oy = coordinate*64, coordinate*64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, program, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// composite 合成分量变换和点匹配,保留分量顺序及父级字节码
|
||||||
|
// 入参: data 复合字形数据, active 递归访问集合
|
||||||
|
// 返回: ttGlyph 字形轮廓, []byte 字节码, error 数据错误
|
||||||
|
func (f *sfntOutliner) composite(data []byte, active map[uint16]bool) (ttGlyph, []byte, error) {
|
||||||
|
var result ttGlyph
|
||||||
|
pos := 10
|
||||||
|
hasInstructions := false
|
||||||
|
for {
|
||||||
|
if len(data)-pos < 6 {
|
||||||
|
return result, nil, fmt.Errorf("truncated TrueType component")
|
||||||
|
}
|
||||||
|
flags, id := binary.BigEndian.Uint16(data[pos:]), binary.BigEndian.Uint16(data[pos+2:])
|
||||||
|
pos += 4
|
||||||
|
a, b := int32(data[pos]), int32(data[pos+1])
|
||||||
|
pos += 2
|
||||||
|
if flags&1 != 0 {
|
||||||
|
pos -= 2
|
||||||
|
if len(data)-pos < 4 {
|
||||||
|
return result, nil, fmt.Errorf("truncated TrueType component arguments")
|
||||||
|
}
|
||||||
|
a, b = int32(binary.BigEndian.Uint16(data[pos:])), int32(binary.BigEndian.Uint16(data[pos+2:]))
|
||||||
|
pos += 4
|
||||||
|
}
|
||||||
|
if flags&2 != 0 {
|
||||||
|
if flags&1 != 0 {
|
||||||
|
a, b = int32(int16(a)), int32(int16(b))
|
||||||
|
} else {
|
||||||
|
a, b = int32(int8(a)), int32(int8(b))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
matrix := [4]float64{1, 0, 0, 1}
|
||||||
|
indices := []int(nil)
|
||||||
|
switch flags & 0xc8 {
|
||||||
|
case 8:
|
||||||
|
indices = []int{0}
|
||||||
|
case 0x40:
|
||||||
|
indices = []int{0, 3}
|
||||||
|
case 0x80:
|
||||||
|
indices = []int{0, 1, 2, 3}
|
||||||
|
case 0:
|
||||||
|
default:
|
||||||
|
return result, nil, fmt.Errorf("conflicting TrueType component transforms")
|
||||||
|
}
|
||||||
|
for _, index := range indices {
|
||||||
|
if len(data)-pos < 2 {
|
||||||
|
return result, nil, fmt.Errorf("truncated TrueType component transform")
|
||||||
|
}
|
||||||
|
matrix[index] = float64(int16(binary.BigEndian.Uint16(data[pos:]))) / 16384
|
||||||
|
pos += 2
|
||||||
|
}
|
||||||
|
if flags&8 != 0 {
|
||||||
|
matrix[3] = matrix[0]
|
||||||
|
}
|
||||||
|
part, err := f.glyph(id, active)
|
||||||
|
if err != nil {
|
||||||
|
return result, nil, err
|
||||||
|
}
|
||||||
|
if flags&0x200 != 0 {
|
||||||
|
result.metrics = new([4]ttPoint)
|
||||||
|
copy(result.metrics[:], part.points[len(part.points)-4:])
|
||||||
|
}
|
||||||
|
part.points = part.points[:len(part.points)-4]
|
||||||
|
for i := range part.points {
|
||||||
|
p := &part.points[i]
|
||||||
|
x, y := float64(p.x), float64(p.y)
|
||||||
|
p.x = int32(math.Round(matrix[0]*x + matrix[2]*y))
|
||||||
|
p.y = int32(math.Round(matrix[1]*x + matrix[3]*y))
|
||||||
|
}
|
||||||
|
dx, dy := a*64, b*64
|
||||||
|
if flags&2 == 0 {
|
||||||
|
if a < 0 || b < 0 || int64(a) >= int64(len(result.points)) || int64(b) >= int64(len(part.points)) {
|
||||||
|
return result, nil, fmt.Errorf("invalid TrueType component point matching")
|
||||||
|
}
|
||||||
|
dx, dy = result.points[a].x-part.points[b].x, result.points[a].y-part.points[b].y
|
||||||
|
} else if flags&0x1800 == 0x800 {
|
||||||
|
x, y := float64(dx), float64(dy)
|
||||||
|
dx, dy = int32(math.Round(matrix[0]*x+matrix[2]*y)), int32(math.Round(matrix[1]*x+matrix[3]*y))
|
||||||
|
}
|
||||||
|
if flags&4 != 0 {
|
||||||
|
dx, dy = int32(math.Round(float64(dx)/64))*64, int32(math.Round(float64(dy)/64))*64
|
||||||
|
}
|
||||||
|
if len(result.points)+len(part.points) > 65536 {
|
||||||
|
return result, nil, fmt.Errorf("excessive TrueType component points")
|
||||||
|
}
|
||||||
|
for _, end := range part.ends {
|
||||||
|
result.ends = append(result.ends, uint16(len(result.points)+int(end)))
|
||||||
|
}
|
||||||
|
for i := range part.points {
|
||||||
|
p := &part.points[i]
|
||||||
|
p.x += dx
|
||||||
|
p.y += dy
|
||||||
|
p.ox, p.oy = p.x, p.y
|
||||||
|
p.tx, p.ty = false, false
|
||||||
|
}
|
||||||
|
result.points = append(result.points, part.points...)
|
||||||
|
hasInstructions = hasInstructions || flags&0x100 != 0
|
||||||
|
if flags&0x20 == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var program []byte
|
||||||
|
if hasInstructions {
|
||||||
|
if len(data)-pos < 2 {
|
||||||
|
return result, nil, fmt.Errorf("truncated TrueType composite instructions")
|
||||||
|
}
|
||||||
|
n := int(binary.BigEndian.Uint16(data[pos:]))
|
||||||
|
pos += 2
|
||||||
|
if n > len(data)-pos {
|
||||||
|
return result, nil, fmt.Errorf("truncated TrueType composite instructions")
|
||||||
|
}
|
||||||
|
program = data[pos : pos+n]
|
||||||
|
}
|
||||||
|
return result, program, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ttOutlineOnly 识别已剥离提示表但仍引用字体函数或控制值的子集字形,使用设计轮廓
|
||||||
|
// 入参: tables 字体表, code 字形程序
|
||||||
|
// 返回: bool 是否只提取轮廓, error 字节码错误
|
||||||
|
func ttOutlineOnly(tables map[string][]byte, code []byte) (bool, error) {
|
||||||
|
if len(tables["fpgm"])+len(tables["prep"])+len(tables["cvt "]) != 0 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
for pc := 0; pc < len(code); {
|
||||||
|
op := code[pc]
|
||||||
|
if op == 0x2a || op == 0x2b || op == 0x3e || op == 0x3f || op == 0x44 || op == 0x45 || op == 0x70 || op >= 0x73 && op <= 0x75 || op >= 0xe0 {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
pc, err = ttInstructionEnd(code, pc)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
+1003
File diff suppressed because it is too large.
Load diff
+55
-8
@@ -26,10 +26,12 @@ import (
|
|||||||
"github.com/xiaoqidun/pdfgo"
|
"github.com/xiaoqidun/pdfgo"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PDFImportOptions 指定PDF转换的运行时后端、进度通知与检查策略
|
// PDFImportOptions 指定PDF转换的密码、运行时后端、进度通知与检查策略
|
||||||
|
// Password按PDFDocEncoding字节传入,空值尝试默认空密码
|
||||||
// Strict禁止恢复缺失资源或丢失内容语义,默认在报告中记录警告
|
// Strict禁止恢复缺失资源或丢失内容语义,默认在报告中记录警告
|
||||||
// OnProgress按open、pages、convert及write.*阶段报告进度,total为0表示总量未知
|
// OnProgress按open、pages、convert及write.*阶段报告进度,total为0表示总量未知
|
||||||
type PDFImportOptions struct {
|
type PDFImportOptions struct {
|
||||||
|
Password []byte
|
||||||
Backends *RenderBackends
|
Backends *RenderBackends
|
||||||
Progress func(int) error
|
Progress func(int) error
|
||||||
OnProgress func(stage string, completed, total int) error
|
OnProgress func(stage string, completed, total int) error
|
||||||
@@ -84,7 +86,7 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
|
|||||||
return nil, PDFImportReport{}, err
|
return nil, PDFImportReport{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reader, err := pdfgo.NewReader(source, size)
|
reader, err := pdfgo.NewReaderWithPassword(source, size, options.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, PDFImportReport{}, err
|
return nil, PDFImportReport{}, err
|
||||||
}
|
}
|
||||||
@@ -93,6 +95,12 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
|
|||||||
editor.SetRenderBackends(*options.Backends)
|
editor.SetRenderBackends(*options.Backends)
|
||||||
}
|
}
|
||||||
importer := pdfImporter{ctx: ctx, reader: reader, editor: editor, fontIDs: map[*pdfgo.Font]string{}, fontMetrics: map[string]FontMetrics{}, pages: map[pdfgo.Reference]*pdfgo.Page{}, pageIDs: map[pdfgo.Reference]string{}}
|
importer := pdfImporter{ctx: ctx, reader: reader, editor: editor, fontIDs: map[*pdfgo.Font]string{}, fontMetrics: map[string]FontMetrics{}, pages: map[pdfgo.Reference]*pdfgo.Page{}, pageIDs: map[pdfgo.Reference]string{}}
|
||||||
|
if security := reader.Encryption(); security != nil && !security.Owner && security.Permissions&0xf3c != 0xf3c {
|
||||||
|
if options.Strict {
|
||||||
|
return nil, PDFImportReport{}, &pdfgo.UnsupportedError{Feature: "PDF access permission conversion"}
|
||||||
|
}
|
||||||
|
importer.report.Warnings = append(importer.report.Warnings, pdfgo.Diagnostic{Message: "PDF access permissions not transferred to OFD"})
|
||||||
|
}
|
||||||
if !options.Strict {
|
if !options.Strict {
|
||||||
importer.warning = func(warning pdfgo.Diagnostic) {
|
importer.warning = func(warning pdfgo.Diagnostic) {
|
||||||
warning.Page = importer.page + 1
|
warning.Page = importer.page + 1
|
||||||
@@ -358,7 +366,7 @@ func (p *pdfImporter) path(mark pdfgo.PathMark) error {
|
|||||||
}
|
}
|
||||||
return p.appendPath(mark)
|
return p.appendPath(mark)
|
||||||
}
|
}
|
||||||
if p.pendingPath != nil && !reflect.DeepEqual(p.pendingPath.Style, mark.Style) {
|
if p.pendingPath != nil && (p.pendingPath.StrokeMatrix != mark.StrokeMatrix || !reflect.DeepEqual(p.pendingPath.Style, mark.Style)) {
|
||||||
if err := p.flushPath(); err != nil {
|
if err := p.flushPath(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -408,15 +416,42 @@ func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error {
|
|||||||
}
|
}
|
||||||
box := pdfBounds(points)
|
box := pdfBounds(points)
|
||||||
scale := math.Hypot(p.matrix[0], p.matrix[1])
|
scale := math.Hypot(p.matrix[0], p.matrix[1])
|
||||||
|
drawing, origin, ctm := *p, box, ""
|
||||||
|
marginScale := scale
|
||||||
|
if mark.StrokeMatrix != (pdfgo.Matrix{}) {
|
||||||
|
inverse, ok := mark.StrokeMatrix.Inverse()
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid PDF stroke matrix")
|
||||||
|
}
|
||||||
|
if mark.Style.Fill.Radial != nil || mark.Style.Stroke.Radial != nil {
|
||||||
|
return &pdfgo.UnsupportedError{Feature: "radial gradient with anisotropic path stroke"}
|
||||||
|
}
|
||||||
|
drawing.matrix, origin, scale = inverse, Box{}, 1
|
||||||
|
m := p.matrix.Mul(mark.StrokeMatrix)
|
||||||
|
marginScale = math.Max(math.Hypot(m[0], m[2]), math.Hypot(m[1], m[3]))
|
||||||
|
}
|
||||||
if mark.Stroke {
|
if mark.Stroke {
|
||||||
margin := mark.Style.LineWidth * scale / 2 * math.Max(1, mark.Style.MiterLimit)
|
margin := mark.Style.LineWidth * marginScale / 2 * math.Max(1, mark.Style.MiterLimit)
|
||||||
box = Box{box.X - margin, box.Y - margin, box.W + 2*margin, box.H + 2*margin}
|
box = Box{box.X - margin, box.Y - margin, box.W + 2*margin, box.H + 2*margin}
|
||||||
}
|
}
|
||||||
fillColor, err := p.paintColor(mark.Style.Fill, box)
|
if mark.StrokeMatrix == (pdfgo.Matrix{}) {
|
||||||
|
origin = box
|
||||||
|
} else {
|
||||||
|
m := p.matrix.Mul(mark.StrokeMatrix)
|
||||||
|
m[4], m[5] = m[4]-box.X, m[5]-box.Y
|
||||||
|
ctm = pdfNumbers(m[:]...)
|
||||||
|
}
|
||||||
|
paint := func(value pdfgo.Paint) (*FillColor, error) {
|
||||||
|
if value.Tiling != nil {
|
||||||
|
return p.paintColor(value, box)
|
||||||
|
}
|
||||||
|
return drawing.paintColor(value, origin)
|
||||||
|
}
|
||||||
|
fillColor, err := paint(mark.Style.Fill)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
strokePaint, err := p.paintColor(mark.Style.Stroke, box)
|
strokePaint, err := paint(mark.Style.Stroke)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -425,7 +460,7 @@ func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
object := PathObject{Boundary: pdfBoundary(box), AbbreviatedData: p.pathData(mark.Path, box), Fill: &mark.Fill, Stroke: &mark.Stroke, FillColor: fillColor, StrokeColor: &strokeColor, LineWidth: mark.Style.LineWidth * scale, Cap: []string{"Butt", "Round", "Square"}[mark.Style.Cap], Join: []string{"Miter", "Round", "Bevel"}[mark.Style.Join], MiterLimit: mark.Style.MiterLimit, Clips: clips}
|
object := PathObject{Boundary: pdfBoundary(box), CTM: ctm, AbbreviatedData: drawing.pathData(mark.Path, origin), Fill: &mark.Fill, Stroke: &mark.Stroke, FillColor: fillColor, StrokeColor: &strokeColor, LineWidth: mark.Style.LineWidth * scale, Cap: []string{"Butt", "Round", "Square"}[mark.Style.Cap], Join: []string{"Miter", "Round", "Bevel"}[mark.Style.Join], MiterLimit: mark.Style.MiterLimit, Clips: clips}
|
||||||
object.LineWidthSet = object.LineWidth == 0
|
object.LineWidthSet = object.LineWidth == 0
|
||||||
if mark.Path.EvenOdd {
|
if mark.Path.EvenOdd {
|
||||||
object.Rule = "Even-Odd"
|
object.Rule = "Even-Odd"
|
||||||
@@ -480,7 +515,19 @@ func (p *pdfImporter) paintColor(paint pdfgo.Paint, box Box) (*FillColor, error)
|
|||||||
return color, nil
|
return color, nil
|
||||||
}
|
}
|
||||||
gradient := paint.Axial
|
gradient := paint.Axial
|
||||||
start, end := p.matrix.Apply(gradient.Start), p.matrix.Apply(gradient.End)
|
start := p.matrix.Apply(gradient.Start)
|
||||||
|
inverse, ok := p.matrix.Inverse()
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid PDF gradient matrix")
|
||||||
|
}
|
||||||
|
dx, dy := gradient.End.X-gradient.Start.X, gradient.End.Y-gradient.Start.Y
|
||||||
|
gx, gy := inverse[0]*dx+inverse[1]*dy, inverse[2]*dx+inverse[3]*dy
|
||||||
|
length := gx*gx + gy*gy
|
||||||
|
if length == 0 {
|
||||||
|
return nil, fmt.Errorf("invalid PDF axial gradient axis")
|
||||||
|
}
|
||||||
|
factor := (dx*dx + dy*dy) / length
|
||||||
|
end := pdfgo.Point{X: start.X + gx*factor, Y: start.Y + gy*factor}
|
||||||
extend := 0
|
extend := 0
|
||||||
if gradient.Extend[0] {
|
if gradient.Extend[0] {
|
||||||
extend |= 1
|
extend |= 1
|
||||||
|
|||||||
@@ -239,6 +239,40 @@ func (p *pdfImporter) darkenStampAppearance(page *pdfgo.Page, box Box, mark pdfg
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// formWidget 将表单当前外观转换为页面对象并报告交互语义差异
|
||||||
|
// 入参: ctx 取消上下文, page PDF页面, annotation 表单控件, strict 严格检查开关
|
||||||
|
// 返回: error 外观或转换错误
|
||||||
|
func (p *pdfImporter) formWidget(ctx context.Context, page *pdfgo.Page, annotation pdfgo.Annotation, strict bool) error {
|
||||||
|
if strict {
|
||||||
|
return &pdfgo.UnsupportedError{Feature: "interactive PDF form conversion"}
|
||||||
|
}
|
||||||
|
value, err := p.reader.Resolve(annotation.Dictionary["F"])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
flags, ok := value.(pdfgo.Integer)
|
||||||
|
if value != nil && !ok {
|
||||||
|
return fmt.Errorf("invalid PDF widget flags")
|
||||||
|
}
|
||||||
|
if flags&(1|2|32) != 0 {
|
||||||
|
p.report.Warnings = append(p.report.Warnings, pdfgo.Diagnostic{Page: p.page + 1, Message: "hidden PDF form field not transferred"})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if flags&(8|16) != 0 || annotation.Dictionary["OC"] != nil {
|
||||||
|
return &pdfgo.UnsupportedError{Feature: "widget viewing behavior"}
|
||||||
|
}
|
||||||
|
visitor := pdfgo.Visitor{Path: p.path, Text: p.text, Image: p.image, Warning: p.warning}
|
||||||
|
visitor.Group = func(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error { return p.group(mark, walk, visitor) }
|
||||||
|
if err := p.reader.WalkAnnotationAppearance(ctx, page, annotation, visitor); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := p.flushPath(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.report.Warnings = append(p.report.Warnings, pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF form field appearance imported; interactive behavior not transferred"})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// signatureWidget 转换可见签名外观,不将PDF签名误写成OFD签名
|
// signatureWidget 转换可见签名外观,不将PDF签名误写成OFD签名
|
||||||
// 入参: ctx 取消上下文, page PDF页面, annotation 签名控件
|
// 入参: ctx 取消上下文, page PDF页面, annotation 签名控件
|
||||||
// 返回: error 外观或转换错误
|
// 返回: error 外观或转换错误
|
||||||
|
|||||||
+26
-4
@@ -87,14 +87,36 @@ func (p *pdfImporter) group(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error
|
|||||||
outline := shape
|
outline := shape
|
||||||
evenOdd := path.Path.EvenOdd
|
evenOdd := path.Path.EvenOdd
|
||||||
if stroke {
|
if stroke {
|
||||||
dashes := make([]float64, len(path.Style.Dash))
|
strokeShape, strokeScale, tolerance := shape, scale, 0.001
|
||||||
for i, dash := range path.Style.Dash {
|
var strokeTransform pdfgo.Matrix
|
||||||
dashes[i] = dash * scale
|
if path.StrokeMatrix != (pdfgo.Matrix{}) {
|
||||||
|
inverse, ok := path.StrokeMatrix.Inverse()
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid PDF stroke matrix")
|
||||||
}
|
}
|
||||||
outline, err = geometry.Stroke(shape, StrokeOptions{Width: path.Style.LineWidth * scale, Cap: []string{"Butt", "Round", "Square"}[path.Style.Cap], Join: []string{"Miter", "Round", "Bevel"}[path.Style.Join], MiterLimit: path.Style.MiterLimit, DashOffset: path.Style.DashPhase * scale, Dashes: dashes, Tolerance: 0.001})
|
local := pdfImporter{matrix: inverse}
|
||||||
|
strokeShape, err = ParseGeometryPath(local.pathData(path.Path, Box{}))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
strokeScale = 1
|
||||||
|
strokeTransform = p.matrix.Mul(path.StrokeMatrix)
|
||||||
|
tolerance /= math.Max(math.Hypot(strokeTransform[0], strokeTransform[2]), math.Hypot(strokeTransform[1], strokeTransform[3]))
|
||||||
|
}
|
||||||
|
dashes := make([]float64, len(path.Style.Dash))
|
||||||
|
for i, dash := range path.Style.Dash {
|
||||||
|
dashes[i] = dash * strokeScale
|
||||||
|
}
|
||||||
|
outline, err = geometry.Stroke(strokeShape, StrokeOptions{Width: path.Style.LineWidth * strokeScale, Cap: []string{"Butt", "Round", "Square"}[path.Style.Cap], Join: []string{"Miter", "Round", "Bevel"}[path.Style.Join], MiterLimit: path.Style.MiterLimit, DashOffset: path.Style.DashPhase * strokeScale, Dashes: dashes, Tolerance: tolerance})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if strokeTransform != (pdfgo.Matrix{}) {
|
||||||
|
outline, err = geometry.Transform(outline, NewMatrix(pdfNumbers(strokeTransform[:]...)))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
evenOdd = false
|
evenOdd = false
|
||||||
}
|
}
|
||||||
outline, err = geometry.Normalize(outline, evenOdd)
|
outline, err = geometry.Normalize(outline, evenOdd)
|
||||||
|
|||||||
+12
-2
@@ -22,7 +22,7 @@ import (
|
|||||||
"github.com/xiaoqidun/pdfgo"
|
"github.com/xiaoqidun/pdfgo"
|
||||||
)
|
)
|
||||||
|
|
||||||
// annotations 将链接、印章和签名外观按页面顺序转换为OFD对象
|
// annotations 将链接、印章和表单外观按页面顺序转换为OFD对象
|
||||||
// 入参: ctx 取消上下文, page PDF页面, strict 严格检查开关
|
// 入参: ctx 取消上下文, page PDF页面, strict 严格检查开关
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict bool) error {
|
func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict bool) error {
|
||||||
@@ -33,7 +33,17 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict
|
|||||||
for _, annotation := range annotations {
|
for _, annotation := range annotations {
|
||||||
dict := annotation.Dictionary
|
dict := annotation.Dictionary
|
||||||
if annotation.Subtype == "Widget" {
|
if annotation.Subtype == "Widget" {
|
||||||
if err := p.signatureWidget(ctx, page, annotation); err != nil {
|
field, err := p.reader.ReadField(annotation)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
annotation.Dictionary = field
|
||||||
|
if field["FT"] == pdfgo.Name("Sig") && field["V"] != nil {
|
||||||
|
err = p.signatureWidget(ctx, page, annotation)
|
||||||
|
} else {
|
||||||
|
err = p.formWidget(ctx, page, annotation, strict)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func (CanvasBackend) OpenFont(data []byte) (FontMetrics, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
metrics := &canvasFontMetrics{SFNT: sfnt}
|
metrics := &canvasFontMetrics{SFNT: sfnt, outlines: &sfntOutliner{font: sfnt}}
|
||||||
metrics.fontShaper = &fontShaper{metrics: metrics}
|
metrics.fontShaper = &fontShaper{metrics: metrics}
|
||||||
return metrics, nil
|
return metrics, nil
|
||||||
}
|
}
|
||||||
@@ -45,6 +45,7 @@ func (CanvasBackend) OpenFont(data []byte) (FontMetrics, error) {
|
|||||||
type canvasFontMetrics struct {
|
type canvasFontMetrics struct {
|
||||||
*font.SFNT
|
*font.SFNT
|
||||||
*fontShaper
|
*fontShaper
|
||||||
|
outlines *sfntOutliner
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write 返回字体数据,不修改字体元信息
|
// Write 返回字体数据,不修改字体元信息
|
||||||
@@ -61,7 +62,7 @@ func (f canvasFontMetrics) GlyphOutline(glyph uint16, size float64) (GeometryPat
|
|||||||
return nil, fmt.Errorf("invalid glyph or size")
|
return nil, fmt.Errorf("invalid glyph or size")
|
||||||
}
|
}
|
||||||
path := &canvas.Path{}
|
path := &canvas.Path{}
|
||||||
if err := f.GlyphPath(path, glyph, 0, 0, 0, size/float64(f.UnitsPerEm()), font.NoHinting); err != nil {
|
if err := f.outlines.path(path, glyph, size/float64(f.UnitsPerEm())); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return *geometryFromCanvasPath(path), nil
|
return *geometryFromCanvasPath(path), nil
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ package ofdgo
|
|||||||
import (
|
import (
|
||||||
"github.com/tdewolff/canvas"
|
"github.com/tdewolff/canvas"
|
||||||
canvastext "github.com/tdewolff/canvas/text"
|
canvastext "github.com/tdewolff/canvas/text"
|
||||||
"github.com/tdewolff/font"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// textGlyphPathCacheKey 字形路径缓存键
|
// textGlyphPathCacheKey 字形路径缓存键
|
||||||
@@ -51,15 +50,29 @@ func textGlyphWidth(face *canvas.FontFace, glyph textGlyph) float64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// textGlyphPath 获取绘制字形路径
|
// textGlyphPath 获取绘制字形路径
|
||||||
// 入参: face 字体, glyph 绘制字形
|
// 入参: face 字体, glyph 绘制字形, outlines 轮廓解析器
|
||||||
// 返回: *canvas.Path 字形路径, float64 字形宽度
|
// 返回: *canvas.Path 字形路径, float64 字形宽度, error 字形解析错误
|
||||||
func textGlyphPath(face *canvas.FontFace, glyph textGlyph) (*canvas.Path, float64) {
|
func textGlyphPath(face *canvas.FontFace, glyph textGlyph, outlines *sfntOutliner) (*canvas.Path, float64, error) {
|
||||||
if glyph.GlyphID < 0 || glyph.GlyphID > 0xFFFF {
|
|
||||||
return face.ToPath(glyph.Text)
|
|
||||||
}
|
|
||||||
p := &canvas.Path{}
|
p := &canvas.Path{}
|
||||||
|
width := 0.0
|
||||||
|
if glyph.GlyphID < 0 || glyph.GlyphID > 0xFFFF {
|
||||||
|
x, y := face.XOffset, face.YOffset
|
||||||
|
for _, shaped := range face.Glyphs(glyph.Text) {
|
||||||
|
part := &canvas.Path{}
|
||||||
|
if err := outlines.path(part, shaped.ID, face.MmPerEm); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
p = p.Append(part.Translate(face.MmPerEm*float64(x+shaped.XOffset), face.MmPerEm*float64(y+shaped.YOffset)))
|
||||||
|
x, y = x+shaped.XAdvance, y+shaped.YAdvance
|
||||||
|
}
|
||||||
|
width = face.MmPerEm * float64(x)
|
||||||
|
} else {
|
||||||
glyphID := uint16(glyph.GlyphID)
|
glyphID := uint16(glyph.GlyphID)
|
||||||
_ = face.Font.GlyphPath(p, glyphID, face.PPEM(canvas.DefaultResolution), 0, 0, face.MmPerEm, font.NoHinting)
|
if err := outlines.path(p, glyphID, face.MmPerEm); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
width = face.MmPerEm * float64(face.Font.GlyphAdvance(glyphID))
|
||||||
|
}
|
||||||
if face.FauxBold != 0 {
|
if face.FauxBold != 0 {
|
||||||
d := face.FauxBold * face.Size
|
d := face.FauxBold * face.Size
|
||||||
if face.Font.IsTrueType {
|
if face.Font.IsTrueType {
|
||||||
@@ -73,7 +86,7 @@ func textGlyphPath(face *canvas.FontFace, glyph textGlyph) (*canvas.Path, float6
|
|||||||
if face.FauxItalic != 0 {
|
if face.FauxItalic != 0 {
|
||||||
p = p.Transform(canvas.Identity.Shear(face.FauxItalic, 0))
|
p = p.Transform(canvas.Identity.Shear(face.FauxItalic, 0))
|
||||||
}
|
}
|
||||||
return p, face.MmPerEm * float64(face.Font.GlyphAdvance(glyphID))
|
return p, width, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// cachedTextGlyphPath 获取缓存的字形路径
|
// cachedTextGlyphPath 获取缓存的字形路径
|
||||||
@@ -95,7 +108,16 @@ func (r *Renderer) cachedTextGlyphPath(face *canvas.FontFace, glyph textGlyph) (
|
|||||||
if cached, ok := r.canvasState().textGlyphPathCache[key]; ok {
|
if cached, ok := r.canvasState().textGlyphPathCache[key]; ok {
|
||||||
return cached.path, cached.width
|
return cached.path, cached.width
|
||||||
}
|
}
|
||||||
path, width := textGlyphPath(face, glyph)
|
outlines := r.canvasState().fontOutlines[face.Font]
|
||||||
|
if outlines == nil {
|
||||||
|
outlines = &sfntOutliner{font: face.Font.SFNT}
|
||||||
|
r.canvasState().fontOutlines[face.Font] = outlines
|
||||||
|
}
|
||||||
|
path, width, err := textGlyphPath(face, glyph, outlines)
|
||||||
|
if err != nil {
|
||||||
|
r.renderError = err
|
||||||
|
return &canvas.Path{}, 0
|
||||||
|
}
|
||||||
r.canvasState().textGlyphPathCache[key] = textGlyphPathCacheValue{path: path, width: width}
|
r.canvasState().textGlyphPathCache[key] = textGlyphPathCacheValue{path: path, width: width}
|
||||||
return path, width
|
return path, width
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ type canvasBackendState struct {
|
|||||||
fontMap map[string]*canvas.FontFamily
|
fontMap map[string]*canvas.FontFamily
|
||||||
svgFontCache map[*canvas.Font]SVGFont
|
svgFontCache map[*canvas.Font]SVGFont
|
||||||
textGlyphPathCache map[textGlyphPathCacheKey]textGlyphPathCacheValue
|
textGlyphPathCache map[textGlyphPathCacheKey]textGlyphPathCacheValue
|
||||||
|
fontOutlines map[*canvas.Font]*sfntOutliner
|
||||||
}
|
}
|
||||||
|
|
||||||
// canvasState 获取当前渲染器的Canvas私有缓存,按需创建且不向其他后端暴露
|
// canvasState 获取当前渲染器的Canvas私有缓存,按需创建且不向其他后端暴露
|
||||||
@@ -48,4 +49,5 @@ func (s *canvasBackendState) resetFonts() {
|
|||||||
s.fontMap = make(map[string]*canvas.FontFamily)
|
s.fontMap = make(map[string]*canvas.FontFamily)
|
||||||
s.svgFontCache = make(map[*canvas.Font]SVGFont)
|
s.svgFontCache = make(map[*canvas.Font]SVGFont)
|
||||||
s.textGlyphPathCache = make(map[textGlyphPathCacheKey]textGlyphPathCacheValue)
|
s.textGlyphPathCache = make(map[textGlyphPathCacheKey]textGlyphPathCacheValue)
|
||||||
|
s.fontOutlines = make(map[*canvas.Font]*sfntOutliner)
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user