feat(转换能力): 完善PDF转换与字形渲染
build.yaml / build (push) Successful in 43s

This commit is contained in:
xiaoqidun committed 2026-09-26 15:59:27 +08:00
1 parent 7cdc1d2c07
commit 4fa65175bc
11 files changed
+1551 -29

No files matched your search

+1 -1
View File
@@ -9,7 +9,7 @@ require (
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
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/image v0.46.0
golang.org/x/text v0.42.0
+2 -2
View File
@@ -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/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI=
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-20260926052151-12ff0e0004de/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ=
github.com/xiaoqidun/pdfgo v0.0.0-20260926072349-1d4e7a9c2616 h1:16JKsw9CKBf+BBVeiiHg12bg7h1ocKSxyhUpw6eRIBk=
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/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
+381
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
+55 -8
View File
@@ -26,10 +26,12 @@ import (
"github.com/xiaoqidun/pdfgo"
)
// PDFImportOptions 指定PDF转换的运行时后端、进度通知与检查策略
// PDFImportOptions 指定PDF转换的密码、运行时后端、进度通知与检查策略
// Password按PDFDocEncoding字节传入,空值尝试默认空密码
// Strict禁止恢复缺失资源或丢失内容语义,默认在报告中记录警告
// OnProgress按open、pages、convert及write.*阶段报告进度,total为0表示总量未知
type PDFImportOptions struct {
Password []byte
Backends *RenderBackends
Progress func(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
}
}
reader, err := pdfgo.NewReader(source, size)
reader, err := pdfgo.NewReaderWithPassword(source, size, options.Password)
if err != nil {
return nil, PDFImportReport{}, err
}
@@ -93,6 +95,12 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
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{}}
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 {
importer.warning = func(warning pdfgo.Diagnostic) {
warning.Page = importer.page + 1
@@ -358,7 +366,7 @@ func (p *pdfImporter) path(mark pdfgo.PathMark) error {
}
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 {
return err
}
@@ -408,15 +416,42 @@ func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error {
}
box := pdfBounds(points)
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 {
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}
}
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 {
return err
}
strokePaint, err := p.paintColor(mark.Style.Stroke, box)
strokePaint, err := paint(mark.Style.Stroke)
if err != nil {
return err
}
@@ -425,7 +460,7 @@ func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error {
if err != nil {
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
if mark.Path.EvenOdd {
object.Rule = "Even-Odd"
@@ -480,7 +515,19 @@ func (p *pdfImporter) paintColor(paint pdfgo.Paint, box Box) (*FillColor, error)
return color, nil
}
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
if gradient.Extend[0] {
extend |= 1
+34
View File
@@ -239,6 +239,40 @@ func (p *pdfImporter) darkenStampAppearance(page *pdfgo.Page, box Box, mark pdfg
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签名
// 入参: ctx 取消上下文, page PDF页面, annotation 签名控件
// 返回: error 外观或转换错误
+26 -4
View File
@@ -87,14 +87,36 @@ func (p *pdfImporter) group(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error
outline := shape
evenOdd := path.Path.EvenOdd
if stroke {
dashes := make([]float64, len(path.Style.Dash))
for i, dash := range path.Style.Dash {
dashes[i] = dash * scale
strokeShape, strokeScale, tolerance := shape, scale, 0.001
var strokeTransform pdfgo.Matrix
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 {
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
}
outline, err = geometry.Normalize(outline, evenOdd)
+12 -2
View File
@@ -22,7 +22,7 @@ import (
"github.com/xiaoqidun/pdfgo"
)
// annotations 将链接、印章和签名外观按页面顺序转换为OFD对象
// annotations 将链接、印章和表单外观按页面顺序转换为OFD对象
// 入参: ctx 取消上下文, page PDF页面, strict 严格检查开关
// 返回: 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 {
dict := annotation.Dictionary
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
}
continue
+3 -2
View File
@@ -36,7 +36,7 @@ func (CanvasBackend) OpenFont(data []byte) (FontMetrics, error) {
if err != nil {
return nil, err
}
metrics := &canvasFontMetrics{SFNT: sfnt}
metrics := &canvasFontMetrics{SFNT: sfnt, outlines: &sfntOutliner{font: sfnt}}
metrics.fontShaper = &fontShaper{metrics: metrics}
return metrics, nil
}
@@ -45,6 +45,7 @@ func (CanvasBackend) OpenFont(data []byte) (FontMetrics, error) {
type canvasFontMetrics struct {
*font.SFNT
*fontShaper
outlines *sfntOutliner
}
// Write 返回字体数据,不修改字体元信息
@@ -61,7 +62,7 @@ func (f canvasFontMetrics) GlyphOutline(glyph uint16, size float64) (GeometryPat
return nil, fmt.Errorf("invalid glyph or size")
}
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 *geometryFromCanvasPath(path), nil
+32 -10
View File
@@ -17,7 +17,6 @@ package ofdgo
import (
"github.com/tdewolff/canvas"
canvastext "github.com/tdewolff/canvas/text"
"github.com/tdewolff/font"
)
// textGlyphPathCacheKey 字形路径缓存键
@@ -51,15 +50,29 @@ func textGlyphWidth(face *canvas.FontFace, glyph textGlyph) float64 {
}
// textGlyphPath 获取绘制字形路径
// 入参: face 字体, glyph 绘制字形
// 返回: *canvas.Path 字形路径, float64 字形宽度
func textGlyphPath(face *canvas.FontFace, glyph textGlyph) (*canvas.Path, float64) {
if glyph.GlyphID < 0 || glyph.GlyphID > 0xFFFF {
return face.ToPath(glyph.Text)
}
// 入参: face 字体, glyph 绘制字形, outlines 轮廓解析器
// 返回: *canvas.Path 字形路径, float64 字形宽度, error 字形解析错误
func textGlyphPath(face *canvas.FontFace, glyph textGlyph, outlines *sfntOutliner) (*canvas.Path, float64, error) {
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)
_ = 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 {
d := face.FauxBold * face.Size
if face.Font.IsTrueType {
@@ -73,7 +86,7 @@ func textGlyphPath(face *canvas.FontFace, glyph textGlyph) (*canvas.Path, float6
if 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 获取缓存的字形路径
@@ -95,7 +108,16 @@ func (r *Renderer) cachedTextGlyphPath(face *canvas.FontFace, glyph textGlyph) (
if cached, ok := r.canvasState().textGlyphPathCache[key]; ok {
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}
return path, width
}
+2
View File
@@ -26,6 +26,7 @@ type canvasBackendState struct {
fontMap map[string]*canvas.FontFamily
svgFontCache map[*canvas.Font]SVGFont
textGlyphPathCache map[textGlyphPathCacheKey]textGlyphPathCacheValue
fontOutlines map[*canvas.Font]*sfntOutliner
}
// canvasState 获取当前渲染器的Canvas私有缓存,按需创建且不向其他后端暴露
@@ -48,4 +49,5 @@ func (s *canvasBackendState) resetFonts() {
s.fontMap = make(map[string]*canvas.FontFamily)
s.svgFontCache = make(map[*canvas.Font]SVGFont)
s.textGlyphPathCache = make(map[textGlyphPathCacheKey]textGlyphPathCacheValue)
s.fontOutlines = make(map[*canvas.Font]*sfntOutliner)
}