feat(注解字形): 优化注解字形与页面写出
build.yaml / build (push) Successful in 45s

This commit is contained in:
xiaoqidun committed 2026-10-04 13:35:47 +08:00
1 parent 5d599dc835
commit 290fc57c34
17 files changed
+220 -38

No files matched your search

+1 -1
View File
@@ -10,7 +10,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-20261004025424-ce051c731285 github.com/xiaoqidun/pdfgo v0.0.0-20261004052854-254c40ac6764
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
+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/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-20261004025424-ce051c731285 h1:KApg/AQoRuFj6UzFyNY5yOmb1di749tzYuOx9VqY0vU= github.com/xiaoqidun/pdfgo v0.0.0-20261004052854-254c40ac6764 h1:YKsnM25IZ2xqdcm6Gl30boDID7ctPxNa/QPxUoCjmKQ=
github.com/xiaoqidun/pdfgo v0.0.0-20261004025424-ce051c731285/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ= github.com/xiaoqidun/pdfgo v0.0.0-20261004052854-254c40ac6764/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=
+30 -12
View File
@@ -155,26 +155,16 @@ func (r *Reader) parseAnnotations(doc *Document) error {
if doc.Annotations == "" { if doc.Annotations == "" {
return nil return nil
} }
annPath := r.ResPath(doc.Annotations) annotations, err := r.annotationIndex(doc)
f, err := r.openFile(annPath)
if err != nil { if err != nil {
return err return err
} }
defer f.Close()
var annotations Annotations
if err := xml.NewDecoder(f).Decode(&annotations); err != nil {
return err
}
if r.Annots == nil { if r.Annots == nil {
r.Annots = make(map[string][]Annotation) r.Annots = make(map[string][]Annotation)
} }
r.annotationFiles = make(map[string][]string) annPath := r.ResPath(doc.Annotations)
for _, page := range annotations.Page { for _, page := range annotations.Page {
annotPath := resolveResourcePath(annPath, "", page.FileLoc) annotPath := resolveResourcePath(annPath, "", page.FileLoc)
if file, ok := r.packageFile(annotPath); ok {
annotPath = cleanPackagePath(file.Name)
}
r.annotationFiles[annotPath] = append(r.annotationFiles[annotPath], page.PageID)
af, err := r.openFile(annotPath) af, err := r.openFile(annotPath)
if err != nil { if err != nil {
continue continue
@@ -189,3 +179,31 @@ func (r *Reader) parseAnnotations(doc *Document) error {
} }
return nil return nil
} }
// annotationIndex 读取注解文件索引,不解析页面外观
// 入参: doc 文档结构
// 返回: Annotations 注解索引, error 错误信息
func (r *Reader) annotationIndex(doc *Document) (Annotations, error) {
if doc.Annotations == "" {
return Annotations{}, nil
}
annPath := r.ResPath(doc.Annotations)
f, err := r.openFile(annPath)
if err != nil {
return Annotations{}, err
}
defer f.Close()
var annotations Annotations
if err := xml.NewDecoder(f).Decode(&annotations); err != nil {
return Annotations{}, err
}
r.annotationFiles = make(map[string][]string)
for _, page := range annotations.Page {
annotPath := resolveResourcePath(annPath, "", page.FileLoc)
if file, ok := r.packageFile(annotPath); ok {
annotPath = cleanPackagePath(file.Name)
}
r.annotationFiles[annotPath] = append(r.annotationFiles[annotPath], page.PageID)
}
return annotations, nil
}
+29 -2
View File
@@ -35,6 +35,9 @@ import (
"github.com/xiaoqidun/jbig2" "github.com/xiaoqidun/jbig2"
) )
// editorPathValidationLimit 限制同次对象校验保留的路径数量
const editorPathValidationLimit = 1024
// Editor 编辑OFD文档,长度单位为毫米,页面索引从0开始,实例需串行使用 // Editor 编辑OFD文档,长度单位为毫米,页面索引从0开始,实例需串行使用
// Info可修改文档元数据,通过方法管理页面、对象和资源,WriteTo另存结果,不覆盖输入 // Info可修改文档元数据,通过方法管理页面、对象和资源,WriteTo另存结果,不覆盖输入
// OnWriteProgress可选,按fonts、pages、references、resources、compress、write阶段同步回报进度,返回错误则停止保存 // OnWriteProgress可选,按fonts、pages、references、resources、compress、write阶段同步回报进度,返回错误则停止保存
@@ -785,7 +788,14 @@ func (e *Editor) findObject(page int, id string) (*Layer, int, error) {
// 入参: id 对象标识, object 对象内容 // 入参: id 对象标识, object 对象内容
// 返回: GraphicObject 对象内容, error 错误信息 // 返回: GraphicObject 对象内容, error 错误信息
func (e *Editor) prepareObject(id string, object GraphicObject) (GraphicObject, error) { func (e *Editor) prepareObject(id string, object GraphicObject) (GraphicObject, error) {
object, err := (&editorValidation{Editor: e}).prepareObject(id, object) return (&editorValidation{Editor: e}).prepareObjectCopy(id, object)
}
// prepareObjectCopy 校验对象并隔离输入数据,同次校验共用几何缓存
// 入参: id 对象标识, object 对象内容
// 返回: GraphicObject 独立副本, error 错误信息
func (v *editorValidation) prepareObjectCopy(id string, object GraphicObject) (GraphicObject, error) {
object, err := v.prepareObject(id, object)
if err != nil { if err != nil {
return GraphicObject{}, err return GraphicObject{}, err
} }
@@ -819,7 +829,7 @@ func (v *editorValidation) prepareObject(id string, object GraphicObject) (Graph
fill, stroke = obj.FillColor, (*FillColor)(obj.StrokeColor) fill, stroke = obj.FillColor, (*FillColor)(obj.StrokeColor)
case "PathObject": case "PathObject":
obj := &object.PathObject obj := &object.PathObject
if err := creationPath(obj.AbbreviatedData); err != nil { if err := v.creationPath(obj.AbbreviatedData); err != nil {
return GraphicObject{}, err return GraphicObject{}, err
} }
if obj.Rule != "" && obj.Rule != "NonZero" && obj.Rule != "Even-Odd" { if obj.Rule != "" && obj.Rule != "NonZero" && obj.Rule != "Even-Odd" {
@@ -1185,6 +1195,23 @@ func creationPath(value string) error {
return err return err
} }
// creationPath 在同次校验中复用相同路径的成功结果,不缓存错误或依赖可变资源
// 入参: value 路径数据
// 返回: error 错误信息
func (v *editorValidation) creationPath(value string) error {
if v.paths[value] {
return nil
}
if err := creationPath(value); err != nil {
return err
}
if v.paths == nil || len(v.paths) >= editorPathValidationLimit {
v.paths = make(map[string]bool)
}
v.paths[value] = true
return nil
}
// creationNumbers 解析创建接口的有限十进制数值序列 // creationNumbers 解析创建接口的有限十进制数值序列
// 入参: value 数值序列, count 数值数量 // 入参: value 数值序列, count 数值数量
// 返回: []float64 数值, error 错误信息 // 返回: []float64 数值, error 错误信息
+3
View File
@@ -33,6 +33,9 @@ func (e *Editor) Annotations(index int) ([]AnnotationInfo, error) {
if e.source == nil { if e.source == nil {
return nil, nil return nil, nil
} }
if _, err := e.source.reader.Doc(); err != nil {
return nil, err
}
return e.source.reader.annotationInfos(index, e.pages[index].ID), nil return e.source.reader.annotationInfos(index, e.pages[index].ID), nil
} }
+2 -1
View File
@@ -395,10 +395,11 @@ func (e *Editor) commitAnnotationParts(base *editorSource, parts map[string][]by
if err := reader.initRoot(); err != nil { if err := reader.initRoot(); err != nil {
return err return err
} }
doc, err := reader.Doc() doc, err := reader.docStructure()
if err != nil { if err != nil {
return err return err
} }
_, _ = reader.annotationIndex(doc)
before, after := e.source, *base before, after := e.source, *base
after.reader, after.document = reader, doc after.reader, after.document = reader, doc
if before == nil { if before == nil {
+9 -3
View File
@@ -90,9 +90,10 @@ func (e *Editor) CopyObjects(page int, objects []GraphicObject, dx, dy float64)
origins := make([]*editorObjectOrigin, len(objects)) origins := make([]*editorObjectOrigin, len(objects))
result := make([]string, len(objects)) result := make([]string, len(objects))
maximum := e.maxID + len(objects) maximum := e.maxID + len(objects)
validation := &editorValidation{Editor: e}
for i, object := range objects { for i, object := range objects {
result[i] = strconv.Itoa(e.maxID + i + 1) result[i] = strconv.Itoa(e.maxID + i + 1)
object, err := e.prepareCopiedObject(result[i], object) object, err := validation.prepareCopiedObject(result[i], object)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -124,13 +125,18 @@ func (e *Editor) CopyObjects(page int, objects []GraphicObject, dx, dy float64)
if target < len(layers) { if target < len(layers) {
target = len(layers) - 1 target = len(layers) - 1
} }
for i, object := range prepared { for i := 0; i < len(prepared); {
style := e.copiedLayerStyle(objects[i]) style := e.copiedLayerStyle(objects[i])
if target == len(layers) || layers[target].DrawParam != style { if target == len(layers) || layers[target].DrawParam != style {
layers = append(layers, Layer{ID: e.nextID(), Type: "Body", DrawParam: style}) layers = append(layers, Layer{ID: e.nextID(), Type: "Body", DrawParam: style})
target = len(layers) - 1 target = len(layers) - 1
} }
layers[target].Objects = append(layers[target].Objects, object) end := i + 1
for end < len(prepared) && e.copiedLayerStyle(objects[end]) == style {
end++
}
layers[target].Objects = append(layers[target].Objects, prepared[i:end]...)
i = end
} }
e.replaceLayers(page, layers) e.replaceLayers(page, layers)
return result, nil return result, nil
+2 -1
View File
@@ -32,12 +32,13 @@ type PatternStyle struct {
CTM *string CTM *string
} }
// editorValidation 保存同次对象校验的底纹和矢量引用路径,允许共享但拒绝循环 // editorValidation 保存同次对象校验的引用路径及已验证几何,允许共享但拒绝循环
type editorValidation struct { type editorValidation struct {
*Editor *Editor
patterns map[*Pattern]bool patterns map[*Pattern]bool
composites map[string]bool composites map[string]bool
vectors map[string]*editorCompositeNode vectors map[string]*editorCompositeNode
paths map[string]bool
} }
// GradientStops 解析当前文档的渐变分段,保留顺序并合并透明度 // GradientStops 解析当前文档的渐变分段,保留顺序并合并透明度
+2 -1
View File
@@ -64,9 +64,10 @@ func (e *Editor) CopyObjectsToComposite(page int, path ObjectPath, objects []Gra
} }
} }
var content []byte var content []byte
validation := &editorValidation{Editor: e}
for _, source := range objects { for _, source := range objects {
id := e.nextID() id := e.nextID()
object, err := e.prepareCopiedObject(id, source) object, err := validation.prepareCopiedObject(id, source)
if err != nil { if err != nil {
return err return err
} }
+4 -3
View File
@@ -672,7 +672,8 @@ func editorPreservedObject(before, after GraphicObject) bool {
// prepareCopiedObject 保真复制原对象,不重新生成字形或丢弃未修改字段 // prepareCopiedObject 保真复制原对象,不重新生成字形或丢弃未修改字段
// 入参: id 新标识, object 对象快照 // 入参: id 新标识, object 对象快照
// 返回: GraphicObject 独立副本, error 错误信息 // 返回: GraphicObject 独立副本, error 错误信息
func (e *Editor) prepareCopiedObject(id string, object GraphicObject) (GraphicObject, error) { func (v *editorValidation) prepareCopiedObject(id string, object GraphicObject) (GraphicObject, error) {
e := v.Editor
origin := e.snapshotOrigin(object) origin := e.snapshotOrigin(object)
if origin == nil && object.origin != nil { if origin == nil && object.origin != nil {
references := make(map[string]bool) references := make(map[string]bool)
@@ -684,7 +685,7 @@ func (e *Editor) prepareCopiedObject(id string, object GraphicObject) (GraphicOb
} }
object.origin = nil object.origin = nil
if origin == nil { if origin == nil {
return e.prepareObject(id, object) return v.prepareObjectCopy(id, object)
} }
if !editorXMLTransformable(origin.node) || !editorXMLContainersSupported(origin.node) || !editorXMLCopyable(origin.node) || origin.reason != nil && (object.Type != "PathObject" || editReason(origin.reason) != EditUnsupportedColor) { if !editorXMLTransformable(origin.node) || !editorXMLContainersSupported(origin.node) || !editorXMLCopyable(origin.node) || origin.reason != nil && (object.Type != "PathObject" || editReason(origin.reason) != EditUnsupportedColor) {
return GraphicObject{}, fmt.Errorf("object cannot be copied without changing its original structure") return GraphicObject{}, fmt.Errorf("object cannot be copied without changing its original structure")
@@ -704,7 +705,7 @@ func (e *Editor) prepareCopiedObject(id string, object GraphicObject) (GraphicOb
if !editorXMLSupported(origin.node) { if !editorXMLSupported(origin.node) {
return GraphicObject{}, fmt.Errorf("copy would replace unsupported object content") return GraphicObject{}, fmt.Errorf("copy would replace unsupported object content")
} }
return e.prepareObject(id, object) return v.prepareObjectCopy(id, object)
} }
if err := validateEditorGeometry(object); err != nil { if err := validateEditorGeometry(object); err != nil {
return GraphicObject{}, err return GraphicObject{}, err
+35 -1
View File
@@ -391,6 +391,18 @@ func (e *Editor) sourcePageXML(index int, source *editorSourcePage) ([]byte, err
// 入参: layer 图层 // 入参: layer 图层
// 返回: []byte 图层XML, error 错误信息 // 返回: []byte 图层XML, error 错误信息
func (e *Editor) sourceLayerXML(layer Layer) ([]byte, error) { func (e *Editor) sourceLayerXML(layer Layer) ([]byte, error) {
if !slices.ContainsFunc(layer.Objects, func(object GraphicObject) bool { return e.objectOrigin(editorObjectID(object)) != nil }) {
data, err := encodeOFDXML(func(x *ofdXML) {
attrs := ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: layer.ID}, {Name: xml.Name{Local: "Type"}, Value: layer.Type}}
attrs.add("DrawParam", layer.DrawParam)
x.root("Layer", attrs)
for _, object := range layer.Objects {
x.object(object, false)
}
x.end("Layer")
})
return bytes.TrimPrefix(data, []byte(xml.Header)), err
}
var content []byte var content []byte
for _, object := range layer.Objects { for _, object := range layer.Objects {
var data []byte var data []byte
@@ -417,6 +429,17 @@ func (e *Editor) sourceLayerXML(layer Layer) ([]byte, error) {
// 入参: page 页面 // 入参: page 页面
// 返回: []byte 页面XML, error 错误信息 // 返回: []byte 页面XML, error 错误信息
func (e *Editor) sourceNewPageXML(page PageContent) ([]byte, error) { func (e *Editor) sourceNewPageXML(page PageContent) ([]byte, error) {
preserved := false
for _, layer := range page.Content.Layer {
if slices.ContainsFunc(layer.Objects, func(object GraphicObject) bool { return e.objectOrigin(editorObjectID(object)) != nil }) {
preserved = true
break
}
}
if !preserved {
data, err := encodeOFDXML(func(x *ofdXML) { x.page(page) })
return bytes.TrimPrefix(data, []byte(xml.Header)), err
}
area, err := editorXMLContainer("Area", nil, editorXMLText("PhysicalBox", page.Area.PhysicalBox)) area, err := editorXMLContainer("Area", nil, editorXMLText("PhysicalBox", page.Area.PhysicalBox))
if err != nil { if err != nil {
return nil, err return nil, err
@@ -433,7 +456,15 @@ func (e *Editor) sourceNewPageXML(page PageContent) ([]byte, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return editorXMLContainer("Page", nil, append(area, content...)) data := append(area, content...)
if len(page.Actions) != 0 {
actions, err := encodeOFDXML(func(x *ofdXML) { x.actions(page.Actions) })
if err != nil {
return nil, err
}
data = append(data, bytes.TrimPrefix(actions, []byte(xml.Header))...)
}
return editorXMLContainer("Page", nil, data)
} }
// writeSource 将未修改ZIP条目直接复制到新包,逐项写入改动,不持有原资源解压副本 // writeSource 将未修改ZIP条目直接复制到新包,逐项写入改动,不持有原资源解压副本
@@ -585,6 +616,9 @@ func (e *Editor) sourceReader(progress editorProgress) (*Reader, error) {
delete(reader.pageHeaderCache, name) delete(reader.pageHeaderCache, name)
} }
} }
if _, err := reader.Doc(); err != nil {
return nil, err
}
return reader, nil return reader, nil
} }
files := maps.Clone(e.source.reader.files) files := maps.Clone(e.source.reader.files)
+7
View File
@@ -45,6 +45,13 @@ type FontOutlineBatch interface {
GlyphOutlines(glyphs []uint16, size float64) ([]GeometryPath, error) GlyphOutlines(glyphs []uint16, size float64) ([]GeometryPath, error)
} }
// FontGlyphBounds 提供与字形轮廓一致的边界,字号及边界单位为毫米
// 不使用字体全局边界替代单个字形,空轮廓返回零边界
type FontGlyphBounds interface {
FontOutlines
GlyphBounds(glyph uint16, size float64) (Box, error)
}
// FontGlyphDiagnostics 提供已提取字形的恢复提示,不将提示作为轮廓解析失败 // FontGlyphDiagnostics 提供已提取字形的恢复提示,不将提示作为轮廓解析失败
// 字形不存在或不支持的指令仍由GlyphOutline返回错误 // 字形不存在或不支持的指令仍由GlyphOutline返回错误
type FontGlyphDiagnostics interface { type FontGlyphDiagnostics interface {
+10 -1
View File
@@ -27,18 +27,24 @@ import (
// 入参: 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 {
annotations, err := page.Annotations() annotations, err := page.AnnotationsContext(ctx)
if err != nil { if err != nil {
return err return err
} }
parents := make(map[pdfgo.Reference]int) parents := make(map[pdfgo.Reference]int)
for i, annotation := range annotations { for i, annotation := range annotations {
if err := ctx.Err(); err != nil {
return err
}
if annotation.Reference != (pdfgo.Reference{}) && annotation.Subtype != "Popup" { if annotation.Reference != (pdfgo.Reference{}) && annotation.Subtype != "Popup" {
parents[annotation.Reference] = i parents[annotation.Reference] = i
} }
} }
attached := make(map[int]bool) attached := make(map[int]bool)
for i, annotation := range annotations { for i, annotation := range annotations {
if err := ctx.Err(); err != nil {
return err
}
if annotation.Subtype != "Popup" { if annotation.Subtype != "Popup" {
continue continue
} }
@@ -61,6 +67,9 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict
} }
} }
for i, annotation := range annotations { for i, annotation := range annotations {
if err := ctx.Err(); err != nil {
return err
}
if attached[i] { if attached[i] {
continue continue
} }
+16 -7
View File
@@ -313,8 +313,11 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
var glyphPaths []pdfgo.Path var glyphPaths []pdfgo.Path
const unit = 25.4 / 72 const unit = 25.4 / 72
m := p.matrix.Mul(mark.Matrix).Mul(pdfgo.Matrix{1 / unit, 0, 0, -1 / unit, 0, 0}) m := p.matrix.Mul(mark.Matrix).Mul(pdfgo.Matrix{1 / unit, 0, 0, -1 / unit, 0, 0})
var points []pdfgo.Point points := make([]pdfgo.Point, 0, 4*len(mark.Glyphs))
object := TextObject{Font: id, Size: mark.Size * unit, HScale: mark.HorizontalScale} object := TextObject{Font: id, Size: mark.Size * unit, HScale: mark.HorizontalScale, TextCode: make([]TextCode, 0, len(mark.Glyphs))}
if embedded {
object.CGTransform = make([]CGTransform, 0, len(mark.Glyphs))
}
position := 0 position := 0
for n, glyph := range mark.Glyphs { for n, glyph := range mark.Glyphs {
if err := p.ctx.Err(); err != nil { if err := p.ctx.Err(); err != nil {
@@ -360,7 +363,17 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
position += count position += count
} }
if embedded || outlineStroke { if embedded || outlineStroke {
path, err := outline.GlyphOutline(gid, object.Size) var path GeometryPath
var bounds Box
var err error
if provider, ok := outline.(FontGlyphBounds); ok && !outlineStroke {
bounds, err = provider.GlyphBounds(gid, object.Size)
} else {
path, err = outline.GlyphOutline(gid, object.Size)
if err == nil {
bounds, err = path.Bounds()
}
}
if err != nil { if err != nil {
return fmt.Errorf("PDF font %s outline: %w", font.Name, err) return fmt.Errorf("PDF font %s outline: %w", font.Name, err)
} }
@@ -378,10 +391,6 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
} }
} }
} }
bounds, err := path.Bounds()
if err != nil {
return err
}
for _, point := range []pdfgo.Point{{X: bounds.X, Y: bounds.Y}, {X: bounds.X + bounds.W, Y: bounds.Y}, {X: bounds.X, Y: bounds.Y + bounds.H}, {X: bounds.X + bounds.W, Y: bounds.Y + bounds.H}} { for _, point := range []pdfgo.Point{{X: bounds.X, Y: bounds.Y}, {X: bounds.X + bounds.W, Y: bounds.Y}, {X: bounds.X, Y: bounds.Y + bounds.H}, {X: bounds.X + bounds.W, Y: bounds.Y + bounds.H}} {
points = append(points, m.Apply(pdfgo.Point{X: origin.X*unit + point.X*mark.HorizontalScale, Y: -origin.Y*unit + point.Y})) points = append(points, m.Apply(pdfgo.Point{X: origin.X*unit + point.X*mark.HorizontalScale, Y: -origin.Y*unit + point.Y}))
} }
+17 -2
View File
@@ -46,6 +46,7 @@ type Reader struct {
compositeGraphicUnitCache map[string]*CompositeGraphicUnit compositeGraphicUnitCache map[string]*CompositeGraphicUnit
pageHeaderCache map[string]PageContent pageHeaderCache map[string]PageContent
doc *Document doc *Document
relatedPending bool
documentIndex int documentIndex int
Stamps map[string][]Stamp Stamps map[string][]Stamp
Annots map[string][]Annotation Annots map[string][]Annotation
@@ -128,6 +129,21 @@ func (r *Reader) Document(index int) (*Reader, error) {
// Doc 获取当前文档结构 // Doc 获取当前文档结构
// 返回: *Document 文档结构, error 错误信息 // 返回: *Document 文档结构, error 错误信息
func (r *Reader) Doc() (*Document, error) { func (r *Reader) Doc() (*Document, error) {
doc, err := r.docStructure()
if err != nil {
return nil, err
}
if r.relatedPending {
r.relatedPending = false
_ = r.parseAnnotations(doc)
_ = r.parseSignatures(doc)
}
return doc, nil
}
// docStructure 读取文档及资源索引,关联注解和签名由Doc按需解析
// 返回: *Document 文档结构, error 错误信息
func (r *Reader) docStructure() (*Document, error) {
if r.doc != nil { if r.doc != nil {
return r.doc, nil return r.doc, nil
} }
@@ -155,8 +171,7 @@ func (r *Reader) Doc() (*Document, error) {
r.loadRes(res) r.loadRes(res)
} }
r.doc = &doc r.doc = &doc
_ = r.parseAnnotations(&doc) r.relatedPending = true
_ = r.parseSignatures(&doc)
return r.doc, nil return r.doc, nil
} }
+50 -1
View File
@@ -16,16 +16,33 @@ package ofdgo
import ( import (
"fmt" "fmt"
"sync"
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
"github.com/tdewolff/font" "github.com/tdewolff/font"
) )
// canvasGlyphBoundsLimit 限制单个字体保留的字形与字号边界数量
const canvasGlyphBoundsLimit = 4096
// canvasFontMetrics 保留默认字体度量,写出时不更新原始时间 // canvasFontMetrics 保留默认字体度量,写出时不更新原始时间
type canvasFontMetrics struct { type canvasFontMetrics struct {
*font.SFNT *font.SFNT
*fontShaper *fontShaper
outlines *sfntOutliner outlines *sfntOutliner
bounds *canvasFontBounds
}
// canvasFontBounds 缓存已成功提取的字形边界,不保留轮廓或失败结果
type canvasFontBounds struct {
mu sync.Mutex
values map[canvasGlyphSize]Box
}
// canvasGlyphSize 区分字形编号及精确毫米字号
type canvasGlyphSize struct {
glyph uint16
size float64
} }
// ResolveFont 按需加载字体资源,保留内嵌数据及外部字体的名称、样式和集合索引匹配 // ResolveFont 按需加载字体资源,保留内嵌数据及外部字体的名称、样式和集合索引匹配
@@ -43,7 +60,7 @@ func (CanvasBackend) OpenFont(data []byte) (FontMetrics, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
metrics := &canvasFontMetrics{SFNT: sfnt, outlines: &sfntOutliner{font: sfnt}} metrics := &canvasFontMetrics{SFNT: sfnt, outlines: &sfntOutliner{font: sfnt}, bounds: &canvasFontBounds{}}
metrics.fontShaper = &fontShaper{metrics: metrics} metrics.fontShaper = &fontShaper{metrics: metrics}
return metrics, nil return metrics, nil
} }
@@ -68,6 +85,38 @@ func (f canvasFontMetrics) GlyphOutline(glyph uint16, size float64) (GeometryPat
return *geometryFromCanvasPath(path), nil 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 返回字形指令栈下溢后保留设计轮廓的提示,原始字体不变 // GlyphWarning 返回字形指令栈下溢后保留设计轮廓的提示,原始字体不变
// 入参: glyph 字形编号 // 入参: glyph 字形编号
// 返回: error 恢复原因,无恢复时为空 // 返回: error 恢复原因,无恢复时为空
+1
View File
@@ -517,6 +517,7 @@ func (x *ofdXML) layer(layer Layer) {
var attrs ofdAttrs var attrs ofdAttrs
attrs.add("ID", layer.ID) attrs.add("ID", layer.ID)
attrs.add("Type", layer.Type) attrs.add("Type", layer.Type)
attrs.add("DrawParam", layer.DrawParam)
x.start("Layer", attrs) x.start("Layer", attrs)
for _, object := range layer.Objects { for _, object := range layer.Objects {
x.object(object, false) x.object(object, false)