feat(渲染编辑): 完善渲染编辑验签并优化性能
build.yaml / build (push) Successful in 45s

This commit is contained in:
xiaoqidun committed 2026-10-02 13:08:34 +08:00
1 parent 9c5ce1d66f
commit ac198f3351
56 files changed
+3029 -825

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-20261001010247-f9839c149d47 github.com/xiaoqidun/pdfgo v0.0.0-20261002050201-1eed5d9912bf
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-20261001010247-f9839c149d47 h1:K81kklR5kcVJzV7EWUNCpQJHxw1v2vLI7eegfOs0xbI= github.com/xiaoqidun/pdfgo v0.0.0-20261002050201-1eed5d9912bf h1:sST236XvU4MWCZjaH3d0hg4SsDJl6SNGL3qQdfgHP8w=
github.com/xiaoqidun/pdfgo v0.0.0-20261001010247-f9839c149d47/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ= github.com/xiaoqidun/pdfgo v0.0.0-20261002050201-1eed5d9912bf/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=
+61 -20
View File
@@ -70,17 +70,19 @@ type Editor struct {
// editorResource 文档内嵌资源 // editorResource 文档内嵌资源
type editorResource struct { type editorResource struct {
name string name string
data []byte data []byte
font *Font font *Font
image *MultiMedia image *MultiMedia
space *ColorSpace space *ColorSpace
composite string composite string
references []string vectorKey [32]byte
subset *editorFontSubset vectorStates map[string]editorCompositeState
states map[string]editorCompositeState references []string
draw *DrawParam subset *editorFontSubset
drawSource string states map[string]editorCompositeState
draw *DrawParam
drawSource string
} }
// editorResourceKey 资源内容和字体集合索引 // editorResourceKey 资源内容和字体集合索引
@@ -495,8 +497,8 @@ func (e *Editor) addImage(data []byte, config image.Config, format string, decod
return id, nil return id, nil
} }
// AddObject 按添加顺序放入正文图层,支持文字、路径和图片,自动分配对象ID // AddObject 按添加顺序放入正文图层,支持文字、路径、图片和矢量资源实例,自动分配对象ID
// 支持已注册资源、颜色、渐变、裁剪和动作;复合图元仅支持当前文档原对象的保真复制 // 支持已注册资源、颜色、渐变、裁剪和动作;原对象复制保留原文结构
// 入参: page 页面索引, object 对象内容,添加后不再引用调用方的可变数据 // 入参: page 页面索引, object 对象内容,添加后不再引用调用方的可变数据
// 返回: string 对象标识, error 错误信息 // 返回: string 对象标识, error 错误信息
func (e *Editor) AddObject(page int, object GraphicObject) (string, error) { func (e *Editor) AddObject(page int, object GraphicObject) (string, error) {
@@ -533,6 +535,8 @@ func (e *Editor) UpdateObject(page int, id string, object GraphicObject) error {
object.PathObject.ID = id object.PathObject.ID = id
case "ImageObject": case "ImageObject":
object.ImageObject.ID = id object.ImageObject.ID = id
case "CompositeObject":
object.CompositeGraphicUnit.ID = id
default: default:
return fmt.Errorf("unsupported object type %q", object.Type) return fmt.Errorf("unsupported object type %q", object.Type)
} }
@@ -781,6 +785,18 @@ 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)
if err != nil {
return GraphicObject{}, err
}
return cloneEditorObject(object)
}
// prepareObject 校验对象及嵌套颜色,不重复复制底纹单元
// 入参: id 对象标识, object 对象内容
// 返回: GraphicObject 对象内容, error 错误信息
func (v *editorValidation) prepareObject(id string, object GraphicObject) (GraphicObject, error) {
e := v.Editor
var boundary, ctm, drawParam, join string var boundary, ctm, drawParam, join string
var alpha *int var alpha *int
var clips *Clips var clips *Clips
@@ -828,13 +844,24 @@ func (e *Editor) prepareObject(id string, object GraphicObject) (GraphicObject,
} }
} }
if obj.Border != nil { if obj.Border != nil {
if err := e.validateImageBorder(obj.Border); err != nil { if err := v.validateImageBorder(obj.Border); err != nil {
return GraphicObject{}, err return GraphicObject{}, err
} }
} }
obj.ID = id obj.ID = id
boundary, ctm = obj.Boundary, obj.CTM boundary, ctm = obj.Boundary, obj.CTM
alpha, clips, actions = obj.Alpha, obj.Clips, obj.Actions alpha, clips, actions = obj.Alpha, obj.Clips, obj.Actions
case "CompositeObject":
obj := &object.CompositeGraphicUnit
if obj.ResourceID == "" || len(obj.Objects)+len(obj.TextObject)+len(obj.PathObject)+len(obj.ImageObject)+len(obj.CompositeGraphicUnit) != 0 || obj.extentSet || obj.Width != 0 || obj.Height != 0 || obj.Thumbnail != "" || obj.Substitution != "" {
return GraphicObject{}, fmt.Errorf("composite requires a vector resource without inline content")
}
if err := v.validateVector(obj.ResourceID); err != nil {
return GraphicObject{}, err
}
obj.ID = id
boundary, ctm, drawParam = obj.Boundary, obj.CTM, obj.DrawParam
alpha, clips, actions = obj.Alpha, obj.Clips, obj.Actions
default: default:
return GraphicObject{}, fmt.Errorf("unsupported object type %q", object.Type) return GraphicObject{}, fmt.Errorf("unsupported object type %q", object.Type)
} }
@@ -850,34 +877,48 @@ func (e *Editor) prepareObject(id string, object GraphicObject) (GraphicObject,
object.ImageObject.CTM = fmt.Sprintf("%s 0 0 %s 0 0", ofdNumber(box.W), ofdNumber(box.H)) object.ImageObject.CTM = fmt.Sprintf("%s 0 0 %s 0 0", ofdNumber(box.W), ofdNumber(box.H))
} }
if drawParam != "" { if drawParam != "" {
if _, err := e.editorDrawParam(drawParam, make(map[string]bool)); err != nil { defaults, err := e.editorDrawParam(drawParam, make(map[string]bool))
if err != nil {
return GraphicObject{}, err return GraphicObject{}, err
} }
if fill == nil {
fill = defaults.FillColor
}
if stroke == nil {
stroke = (*FillColor)(defaults.StrokeColor)
}
} }
if err := validateObjectActions(actions); err != nil { if err := validateObjectActions(actions); err != nil {
return GraphicObject{}, err return GraphicObject{}, err
} }
if err := e.validateObjectClips(clips); err != nil { if err := v.validateObjectClips(clips); err != nil {
return GraphicObject{}, err return GraphicObject{}, err
} }
if alpha != nil && (*alpha < 0 || *alpha > 255) { if alpha != nil && (*alpha < 0 || *alpha > 255) {
return GraphicObject{}, fmt.Errorf("alpha must be between 0 and 255") return GraphicObject{}, fmt.Errorf("alpha must be between 0 and 255")
} }
for _, color := range []*FillColor{fill, stroke} { for _, color := range []*FillColor{fill, stroke} {
if err := e.editorColor(color); err != nil { if err := v.editorColor(color); err != nil {
return GraphicObject{}, &EditError{Code: EditUnsupportedColor, Err: err} return GraphicObject{}, &EditError{Code: EditUnsupportedColor, Err: err}
} }
} }
if join != "" && join != "Miter" && join != "Round" && join != "Bevel" { if join != "" && join != "Miter" && join != "Round" && join != "Bevel" {
return GraphicObject{}, fmt.Errorf("invalid line join %q", join) return GraphicObject{}, fmt.Errorf("invalid line join %q", join)
} }
return cloneEditorObject(object) return object, nil
} }
// validateObjectClips 校验对象的路径与文字裁剪,拒绝嵌套裁剪和无效资源 // validateObjectClips 校验对象的路径与文字裁剪,拒绝嵌套裁剪和无效资源
// 入参: clips 裁剪集合,nil表示无裁剪 // 入参: clips 裁剪集合,nil表示无裁剪
// 返回: error 错误信息 // 返回: error 错误信息
func (e *Editor) validateObjectClips(clips *Clips) error { func (e *Editor) validateObjectClips(clips *Clips) error {
return (&editorValidation{Editor: e}).validateObjectClips(clips)
}
// validateObjectClips 沿当前颜色引用路径校验裁剪对象
// 入参: clips 裁剪集合
// 返回: error 错误信息
func (v *editorValidation) validateObjectClips(clips *Clips) error {
if clips == nil { if clips == nil {
return nil return nil
} }
@@ -901,7 +942,7 @@ func (e *Editor) validateObjectClips(clips *Clips) error {
if path.ID != "" || path.Clips != nil { if path.ID != "" || path.Clips != nil {
return fmt.Errorf("clip paths must not have object IDs or nested clips") return fmt.Errorf("clip paths must not have object IDs or nested clips")
} }
if _, err := e.prepareObject("", GraphicObject{Type: "PathObject", PathObject: path}); err != nil { if _, err := v.prepareObject("", GraphicObject{Type: "PathObject", PathObject: path}); err != nil {
return err return err
} }
} }
@@ -909,7 +950,7 @@ func (e *Editor) validateObjectClips(clips *Clips) error {
if text.ID != "" || text.Clips != nil || text.Actions != nil { if text.ID != "" || text.Clips != nil || text.Actions != nil {
return fmt.Errorf("clip text must not have object IDs, actions or nested clips") return fmt.Errorf("clip text must not have object IDs, actions or nested clips")
} }
if _, err := e.prepareObject("", GraphicObject{Type: "TextObject", TextObject: text}); err != nil { if _, err := v.prepareObject("", GraphicObject{Type: "TextObject", TextObject: text}); err != nil {
return err return err
} }
} }
+99 -88
View File
@@ -32,94 +32,9 @@ import (
func (e *Editor) AddAnnotation(index int, annotation Annotation) (string, error) { func (e *Editor) AddAnnotation(index int, annotation Annotation) (string, error) {
var id string var id string
err := e.Transaction(func(next *Editor) error { err := e.Transaction(func(next *Editor) error {
if index < 0 || index >= len(next.pages) { var data []byte
return fmt.Errorf("page index %d out of range", index) var err error
} id, data, err = next.prepareAnnotation(index, annotation)
if !slices.Contains([]string{"Link", "Path", "Highlight", "Stamp", "Watermark"}, annotation.Type) {
return fmt.Errorf("invalid annotation type %q", annotation.Type)
}
if _, err := creationBox(annotation.Appearance.Boundary); err != nil {
return err
}
if err := next.prepareSourceIDs(); err != nil {
return err
}
id = next.nextID()
objects := annotation.Appearance.Objects
if len(objects) == 0 {
for _, object := range annotation.Appearance.TextObject {
objects = append(objects, GraphicObject{Type: "TextObject", TextObject: object})
}
for _, object := range annotation.Appearance.PathObject {
objects = append(objects, GraphicObject{Type: "PathObject", PathObject: object})
}
for _, object := range annotation.Appearance.ImageObject {
objects = append(objects, GraphicObject{Type: "ImageObject", ImageObject: object})
}
if len(annotation.Appearance.CompositeGraphicUnit) != 0 {
return fmt.Errorf("use composite copy to insert existing complex content")
}
}
var content []byte
for _, object := range objects {
prepared, err := next.prepareObject(next.nextID(), cloneEditorData(object))
if err != nil {
return err
}
data, err := editorObjectXML(prepared)
if err != nil {
return err
}
content = append(content, bytes.TrimPrefix(data, []byte(xml.Header))...)
}
appearance, err := editorXMLContainer("Appearance", ofdAttrs{{Name: xml.Name{Local: "Boundary"}, Value: annotation.Appearance.Boundary}}, content)
if err != nil {
return err
}
date := annotation.LastModDate
if date == "" {
date = time.Now().Format("2006-01-02")
} else if _, err := time.Parse("2006-01-02", date); err != nil {
return fmt.Errorf("invalid annotation date %q: %w", date, err)
}
attrs := ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: id}, {Name: xml.Name{Local: "Type"}, Value: annotation.Type},
{Name: xml.Name{Local: "Creator"}, Value: annotation.Creator}, {Name: xml.Name{Local: "LastModDate"}, Value: date}}
attrs.add("Subtype", annotation.Subtype)
attrs.flag("Visible", annotation.Visible)
if annotation.NoZoom {
attrs.add("NoZoom", "true")
}
if annotation.NoRotate {
attrs.add("NoRotate", "true")
}
content = nil
if annotation.Remark != "" {
content = editorXMLText("Remark", annotation.Remark)
}
if annotation.Parameters != nil && len(annotation.Parameters.Parameter) != 0 {
var parameters []byte
for _, parameter := range annotation.Parameters.Parameter {
if parameter.Name == "" {
return fmt.Errorf("empty annotation parameter name")
}
var value bytes.Buffer
if err := xml.EscapeText(&value, []byte(parameter.Value)); err != nil {
return err
}
data, err := editorXMLContainer("Parameter", ofdAttrs{{Name: xml.Name{Local: "Name"}, Value: parameter.Name}}, value.Bytes())
if err != nil {
return err
}
parameters = append(parameters, bytes.TrimPrefix(data, []byte(xml.Header))...)
}
data, err := editorXMLContainer("Parameters", nil, parameters)
if err != nil {
return err
}
content = append(content, bytes.TrimPrefix(data, []byte(xml.Header))...)
}
content = append(content, bytes.TrimPrefix(appearance, []byte(xml.Header))...)
data, err := editorXMLContainer("Annot", attrs, content)
if err != nil { if err != nil {
return err return err
} }
@@ -131,6 +46,102 @@ func (e *Editor) AddAnnotation(index int, annotation Annotation) (string, error)
return id, nil return id, nil
} }
// prepareAnnotation 校验注解并分配外观标识,提交由调用方事务或独立导入文档负责
// 入参: index 页面索引, annotation 注解内容
// 返回: string 注解标识, []byte 标准XML, error 校验错误
func (e *Editor) prepareAnnotation(index int, annotation Annotation) (string, []byte, error) {
if index < 0 || index >= len(e.pages) {
return "", nil, fmt.Errorf("page index %d out of range", index)
}
if !slices.Contains([]string{"Link", "Path", "Highlight", "Stamp", "Watermark"}, annotation.Type) {
return "", nil, fmt.Errorf("invalid annotation type %q", annotation.Type)
}
if _, err := creationBox(annotation.Appearance.Boundary); err != nil {
return "", nil, err
}
if err := e.prepareSourceIDs(); err != nil {
return "", nil, err
}
id := e.nextID()
objects := annotation.Appearance.Objects
if len(objects) == 0 {
objects = nil
for _, object := range annotation.Appearance.TextObject {
objects = append(objects, GraphicObject{Type: "TextObject", TextObject: object})
}
for _, object := range annotation.Appearance.PathObject {
objects = append(objects, GraphicObject{Type: "PathObject", PathObject: object})
}
for _, object := range annotation.Appearance.ImageObject {
objects = append(objects, GraphicObject{Type: "ImageObject", ImageObject: object})
}
if len(annotation.Appearance.CompositeGraphicUnit) != 0 {
return "", nil, fmt.Errorf("use composite copy to insert existing complex content")
}
}
var content []byte
for _, object := range objects {
prepared, err := e.prepareObject(e.nextID(), cloneEditorData(object))
if err != nil {
return "", nil, err
}
data, err := editorObjectXML(prepared)
if err != nil {
return "", nil, err
}
content = append(content, bytes.TrimPrefix(data, []byte(xml.Header))...)
}
appearance, err := editorXMLContainer("Appearance", ofdAttrs{{Name: xml.Name{Local: "Boundary"}, Value: annotation.Appearance.Boundary}}, content)
if err != nil {
return "", nil, err
}
date := annotation.LastModDate
if date == "" {
date = time.Now().Format("2006-01-02")
} else if _, err := time.Parse("2006-01-02", date); err != nil {
return "", nil, fmt.Errorf("invalid annotation date %q: %w", date, err)
}
attrs := ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: id}, {Name: xml.Name{Local: "Type"}, Value: annotation.Type},
{Name: xml.Name{Local: "Creator"}, Value: annotation.Creator}, {Name: xml.Name{Local: "LastModDate"}, Value: date}}
attrs.add("Subtype", annotation.Subtype)
attrs.flag("Visible", annotation.Visible)
if annotation.NoZoom {
attrs.add("NoZoom", "true")
}
if annotation.NoRotate {
attrs.add("NoRotate", "true")
}
content = nil
if annotation.Remark != "" {
content = editorXMLText("Remark", annotation.Remark)
}
if annotation.Parameters != nil && len(annotation.Parameters.Parameter) != 0 {
var parameters []byte
for _, parameter := range annotation.Parameters.Parameter {
if parameter.Name == "" {
return "", nil, fmt.Errorf("empty annotation parameter name")
}
var value bytes.Buffer
if err := xml.EscapeText(&value, []byte(parameter.Value)); err != nil {
return "", nil, err
}
data, err := editorXMLContainer("Parameter", ofdAttrs{{Name: xml.Name{Local: "Name"}, Value: parameter.Name}}, value.Bytes())
if err != nil {
return "", nil, err
}
parameters = append(parameters, bytes.TrimPrefix(data, []byte(xml.Header))...)
}
data, err := editorXMLContainer("Parameters", nil, parameters)
if err != nil {
return "", nil, err
}
content = append(content, bytes.TrimPrefix(data, []byte(xml.Header))...)
}
content = append(content, bytes.TrimPrefix(appearance, []byte(xml.Header))...)
data, err := editorXMLContainer("Annot", attrs, content)
return id, data, err
}
// Annotation 获取注解的独立内容快照,修改原注解应使用局部编辑方法以保留扩展字段 // Annotation 获取注解的独立内容快照,修改原注解应使用局部编辑方法以保留扩展字段
// 入参: index 页面索引, id 注解标识 // 入参: index 页面索引, id 注解标识
// 返回: Annotation 注解内容, error 错误信息 // 返回: Annotation 注解内容, error 错误信息
+45 -8
View File
@@ -32,6 +32,14 @@ type PatternStyle struct {
CTM *string CTM *string
} }
// editorValidation 保存同次对象校验的底纹和矢量引用路径,允许共享但拒绝循环
type editorValidation struct {
*Editor
patterns map[*Pattern]bool
composites map[string]bool
vectors map[string]*editorCompositeNode
}
// GradientStops 解析当前文档的渐变分段,保留顺序并合并透明度 // GradientStops 解析当前文档的渐变分段,保留顺序并合并透明度
// 入参: segments 渐变分段, alpha 外层透明度 // 入参: segments 渐变分段, alpha 外层透明度
// 返回: []ColorStop 后端无关的色标, error 颜色解析错误 // 返回: []ColorStop 后端无关的色标, error 颜色解析错误
@@ -308,7 +316,8 @@ func collectPaintReferences(paint *FillColor, used map[string]bool) {
used[back.ColorSpace] = true used[back.ColorSpace] = true
} }
if paint.Pattern != nil { if paint.Pattern != nil {
for _, object := range paint.Pattern.CellContent.Objects { used[paint.Pattern.CellContent.Thumbnail] = true
for object := range paint.Pattern.CellContent.objects() {
collectObjectReferences(object, used) collectObjectReferences(object, used)
} }
} }
@@ -401,18 +410,40 @@ func (e *Editor) editorMeshColor(value *FillColor) error {
return err return err
} }
// editorColor 校验当前文档的纯色和渐变 // editorColor 校验当前文档的纯色、渐变和底纹
// 入参: value 颜色 // 入参: value 颜色
// 返回: error 错误信息 // 返回: error 错误信息
func (e *Editor) editorColor(value *FillColor) error { func (e *Editor) editorColor(value *FillColor) error {
return (&editorValidation{Editor: e}).editorColor(value)
}
// editorColor 沿当前引用路径校验底纹单元及绘制颜色
// 入参: value 颜色
// 返回: error 错误信息
func (v *editorValidation) editorColor(value *FillColor) error {
e := v.Editor
if value != nil && value.Pattern != nil {
pattern := value.Pattern
if v.patterns[pattern] {
return fmt.Errorf("cyclic pattern paint")
}
if v.patterns == nil {
v.patterns = make(map[*Pattern]bool)
}
v.patterns[pattern] = true
defer delete(v.patterns, pattern)
}
if value != nil && (value.GouraudShd != nil || value.LaGouraudShd != nil) { if value != nil && (value.GouraudShd != nil || value.LaGouraudShd != nil) {
return e.editorMeshColor(value) return e.editorMeshColor(value)
} }
if value != nil && value.Pattern != nil { if value != nil && value.Pattern != nil {
pattern := value.Pattern pattern := value.Pattern
if value.unsupported || value.AxialShd != nil || value.RadialShd != nil || !finite(pattern.Width) || !finite(pattern.Height) || pattern.Width <= 0 || pattern.Height <= 0 || !finite(pattern.XStep) || !finite(pattern.YStep) || pattern.XStep < pattern.Width || pattern.YStep < pattern.Height { if value.unsupported || value.AxialShd != nil || value.RadialShd != nil || !finite(pattern.Width) || !finite(pattern.Height) || pattern.Width <= 0 || pattern.Height <= 0 || !finite(pattern.XStep) || !finite(pattern.YStep) {
return fmt.Errorf("invalid pattern paint") return fmt.Errorf("invalid pattern paint")
} }
if !slices.Contains([]string{"", "Normal", "Row", "Column", "RowAndColumn"}, pattern.ReflectMethod) {
return fmt.Errorf("invalid pattern reflection")
}
if pattern.RelativeTo != "" && pattern.RelativeTo != "Page" && pattern.RelativeTo != "Object" { if pattern.RelativeTo != "" && pattern.RelativeTo != "Page" && pattern.RelativeTo != "Object" {
return fmt.Errorf("invalid pattern reference") return fmt.Errorf("invalid pattern reference")
} }
@@ -423,14 +454,20 @@ func (e *Editor) editorColor(value *FillColor) error {
} }
base := *value base := *value
base.Pattern = nil base.Pattern = nil
if _, err := e.Color(&base); err != nil { if base.Value == "" && base.Index == nil {
if base.Alpha != nil && (*base.Alpha < 0 || *base.Alpha > 255) {
return fmt.Errorf("color alpha must be between 0 and 255")
}
} else if _, err := e.Color(&base); err != nil {
return err return err
} }
for _, object := range pattern.CellContent.Objects { if pattern.CellContent.Thumbnail != "" {
if object.Type == "PathObject" && (object.PathObject.FillColor != nil && object.PathObject.FillColor.Pattern != nil || object.PathObject.StrokeColor != nil && object.PathObject.StrokeColor.Pattern != nil) || object.Type == "TextObject" && (object.TextObject.FillColor != nil && object.TextObject.FillColor.Pattern != nil || object.TextObject.StrokeColor != nil && object.TextObject.StrokeColor.Pattern != nil) { if _, err := e.editorImage(pattern.CellContent.Thumbnail); err != nil {
return fmt.Errorf("nested pattern paint is not supported") return err
} }
if _, err := e.prepareObject("", object); err != nil { }
for object := range pattern.CellContent.objects() {
if _, err := v.prepareObject("", object); err != nil {
return err return err
} }
} }
+64 -2
View File
@@ -474,12 +474,23 @@ func (e *Editor) compositeMembers(n *editorCompositeNode, reader *Reader, render
clips, clipMatrix = &copy, boundary clips, clipMatrix = &copy, boundary
} }
clip := n.clip clip := n.clip
var err error
if c.Boundary != "" {
clip, err = renderer.geometryExtentClip(box.W, box.H, boundary, clip)
if err != nil {
return nil, err
}
}
clip, err = renderer.compositeExtentClip(c, matrix, clip)
if err != nil {
return nil, err
}
if clips != nil { if clips != nil {
geometry, err := renderer.Geometry() geometry, err := renderer.Geometry()
if err != nil { if err != nil {
return nil, err return nil, err
} }
clip, err = geometry.Clip(renderer, clips, clipMatrix, n.clip) clip, err = geometry.Clip(renderer, clips, clipMatrix, clip)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1043,6 +1054,11 @@ func editorResourceReferences(data []byte) ([]string, error) {
var collect func(*editorXML) var collect func(*editorXML)
collect = func(node *editorXML) { collect = func(node *editorXML) {
if node.name.Space == "" || node.name.Space == ofdNamespace || node.name.Space == "http://www.ofdspec.org" { if node.name.Space == "" || node.name.Space == ofdNamespace || node.name.Space == "http://www.ofdspec.org" {
if node.name.Local == "Thumbnail" || node.name.Local == "Substitution" {
if id := editorResourceID(editorImportText(data, node)); id != "" {
refs[id] = true
}
}
for _, attr := range node.attrs { for _, attr := range node.attrs {
if attr.Name.Space == "" && editorObjectReference(attr.Name.Local) { if attr.Name.Space == "" && editorObjectReference(attr.Name.Local) {
refs[attr.Value] = true refs[attr.Value] = true
@@ -1062,7 +1078,53 @@ func editorResourceReferences(data []byte) ([]string, error) {
func collectCompositeReferences(composite CompositeGraphicUnit, used map[string]bool) { func collectCompositeReferences(composite CompositeGraphicUnit, used map[string]bool) {
used[composite.ResourceID] = true used[composite.ResourceID] = true
used[composite.DrawParam] = true used[composite.DrawParam] = true
for _, object := range composite.Objects { used[composite.Thumbnail] = true
used[composite.Substitution] = true
for object := range composite.objects() {
collectObjectReferences(object, used) collectObjectReferences(object, used)
} }
} }
// validateVector 校验共享矢量资源及嵌套绘制引用,不修改资源内容
// 入参: id 矢量资源标识
// 返回: error 尺寸、内容或引用错误
func (v *editorValidation) validateVector(id string) error {
if v.composites[id] {
return fmt.Errorf("cyclic composite resource %q", id)
}
if v.composites == nil {
v.composites = make(map[string]bool)
v.vectors = make(map[string]*editorCompositeNode)
}
v.composites[id] = true
defer delete(v.composites, id)
node := v.vectors[id]
if node == nil {
var err error
node, err = v.compositeDefinition(id)
if err != nil {
return err
}
v.vectors[id] = node
}
unit := node.object.CompositeGraphicUnit
if node.node.attr("Width") == "" || node.node.attr("Height") == "" || !finite(unit.Width) || !finite(unit.Height) || unit.Width < 0 || unit.Height < 0 {
return fmt.Errorf("invalid vector dimensions")
}
if node.node.child("Content") == nil {
return fmt.Errorf("vector content is missing")
}
for _, image := range []string{unit.Thumbnail, unit.Substitution} {
if image != "" {
if _, err := v.editorImage(image); err != nil {
return err
}
}
}
for object := range unit.objects() {
if _, err := v.prepareObject(editorObjectID(object), object); err != nil {
return err
}
}
return nil
}
+9 -2
View File
@@ -231,7 +231,14 @@ func (e *Editor) pasteCompositeSelection(selection *CompositeSelection, paste fu
if source.object.Type != "PathObject" || editReason(styleErr) != EditUnsupportedColor { if source.object.Type != "PathObject" || editReason(styleErr) != EditUnsupportedColor {
return styleErr return styleErr
} }
copy, err = e.wrapCompositePath(copy) copy.setStates(source.states)
copy.defaults = source.defaults
if !source.boundaryInCTM {
if err := copy.convertCoordinates(source.parent, true); err != nil {
return err
}
}
copy, err = e.wrapCompositePath(renderer, copy)
if err != nil { if err != nil {
return err return err
} }
@@ -260,7 +267,7 @@ func (e *Editor) pasteCompositeSelection(selection *CompositeSelection, paste fu
if err := copy.update(object); err != nil { if err := copy.update(object); err != nil {
return err return err
} }
if !source.boundaryInCTM { if !source.boundaryInCTM && styleErr == nil {
if err := copy.convertCoordinates(source.parent, true); err != nil { if err := copy.convertCoordinates(source.parent, true); err != nil {
return err return err
} }
+87 -10
View File
@@ -16,13 +16,15 @@ package ofdgo
import ( import (
"bytes" "bytes"
"crypto/sha256"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"maps" "maps"
"slices" "slices"
"strconv"
) )
// GroupCompositeObjects 组合内部同一容器的连续成员,保留标识、绘制顺序及共享资源 // GroupCompositeObjects 组合内部同一容器的连续成员,保留绘制顺序及共享资源
// 入参: page 页面索引, path 父路径, indexes 至少两个成员序号 // 入参: page 页面索引, path 父路径, indexes 至少两个成员序号
// 返回: int 新组合序号, error 错误信息 // 返回: int 新组合序号, error 错误信息
func (e *Editor) GroupCompositeObjects(page int, path ObjectPath, indexes []int) (int, error) { func (e *Editor) GroupCompositeObjects(page int, path ObjectPath, indexes []int) (int, error) {
@@ -30,7 +32,7 @@ func (e *Editor) GroupCompositeObjects(page int, path ObjectPath, indexes []int)
return 0, fmt.Errorf("grouping requires at least two objects") return 0, fmt.Errorf("grouping requires at least two objects")
} }
var result int var result int
err := e.editCompositeObjects(page, path, indexes, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error { err := e.editCompositeObjects(page, path, indexes, func(renderer *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
for _, member := range members { for _, member := range members {
if !member.Capabilities.Copy || !member.Capabilities.Order { if !member.Capabilities.Copy || !member.Capabilities.Order {
return fmt.Errorf("composite member cannot be grouped") return fmt.Errorf("composite member cannot be grouped")
@@ -39,25 +41,32 @@ func (e *Editor) GroupCompositeObjects(page int, path ObjectPath, indexes []int)
slices.SortFunc(nodes, func(a, b *editorCompositeNode) int { return a.index - b.index }) slices.SortFunc(nodes, func(a, b *editorCompositeNode) int { return a.index - b.index })
siblings := nodes[0].siblings() siblings := nodes[0].siblings()
first := slices.Index(siblings, nodes[0]) first := slices.Index(siblings, nodes[0])
var content []byte copies := make([]*editorCompositeNode, len(nodes))
for i, node := range nodes { for i, node := range nodes {
if first+i >= len(siblings) || siblings[first+i] != node { if first+i >= len(siblings) || siblings[first+i] != node {
return fmt.Errorf("grouping requires consecutive members in the same container") return fmt.Errorf("grouping requires consecutive members in the same container")
} }
content = append(content, bytes.TrimPrefix(node.data, []byte(xml.Header))...) copy, err := newEditorCompositeNode(node.data)
if err != nil {
return err
}
copy.setStates(node.states)
copy.defaults = node.defaults
if !node.boundaryInCTM {
if err := copy.convertCoordinates(node.parent, true); err != nil {
return err
}
}
copies[i] = copy
} }
content, err := editorXMLContainer("Content", nil, content) group, err := e.vectorInstance(renderer, copies)
if err != nil {
return err
}
content, err = editorXMLContainer("CompositeObject", ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: e.nextID()}, {Name: xml.Name{Local: "Boundary"}, Value: "0 0 1 1"}}, bytes.TrimPrefix(content, []byte(xml.Header)))
if err != nil { if err != nil {
return err return err
} }
for i, node := range nodes { for i, node := range nodes {
var replacement []byte var replacement []byte
if i == 0 { if i == 0 {
replacement = bytes.TrimPrefix(content, []byte(xml.Header)) replacement = bytes.TrimPrefix(group.data, []byte(xml.Header))
} }
node.owner.patches = append(node.owner.patches, editorXMLPatch{node.span.start, node.span.end, replacement}) node.owner.patches = append(node.owner.patches, editorXMLPatch{node.span.start, node.span.end, replacement})
} }
@@ -67,6 +76,74 @@ func (e *Editor) GroupCompositeObjects(page int, path ObjectPath, indexes []int)
return result, err return result, err
} }
// vectorInstance 将同一坐标系的内容封装为标准矢量资源,独立分配内部标识
// 入参: renderer 资源度量器, nodes 局部内容节点
// 返回: *editorCompositeNode 资源实例, error 尺寸、内容或资源错误
func (e *Editor) vectorInstance(renderer *Renderer, nodes []*editorCompositeNode) (*editorCompositeNode, error) {
if err := e.prepareSourceIDs(); err != nil {
return nil, err
}
var extent Box
for _, node := range nodes {
boundary, _ := editorGeometry(node.object)
box, err := creationBox(boundary)
if err != nil {
return nil, err
}
visible, err := renderer.MeasureObject(node.object, MeasureOptions{Defaults: node.defaults})
if err != nil {
return nil, err
}
extent = unionTextBox(extent, unionTextBox(box, visible.Bounds))
}
if !finite(extent.X) || !finite(extent.Y) || !finite(extent.W) || !finite(extent.H) || extent.W <= 0 || extent.H <= 0 {
return nil, fmt.Errorf("invalid vector dimensions")
}
var content []byte
states := make(map[string]editorCompositeState)
for _, node := range nodes {
object := transformEditorMatrix(cloneEditorData(node.object), TranslationMatrix(-extent.X, -extent.Y))
data, err := editorXMLObject(node.data, node.node, node.object, object)
if err != nil {
return nil, err
}
content = append(content, bytes.TrimPrefix(data, []byte(xml.Header))...)
maps.Copy(states, node.states)
}
content, err := editorXMLContainer("Content", nil, content)
if err != nil {
return nil, err
}
hash := sha256.New()
hash.Write([]byte(ofdNumber(extent.W) + " " + ofdNumber(extent.H)))
hash.Write(content)
key := [32]byte(hash.Sum(nil))
var resource string
for _, candidate := range e.resources {
if candidate.vectorKey == key && maps.Equal(candidate.vectorStates, states) {
resource = candidate.composite
break
}
}
if resource == "" {
data, err := editorXMLContainer("CompositeGraphicUnit", ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: strconv.Itoa(e.maxID + 1)}, {Name: xml.Name{Local: "Width"}, Value: ofdNumber(extent.W)}, {Name: xml.Name{Local: "Height"}, Value: ofdNumber(extent.H)}}, bytes.TrimPrefix(content, []byte(xml.Header)))
if err != nil {
return nil, err
}
resource, err = e.addCompositeResource(data, states)
if err != nil {
return nil, err
}
added := &e.resources[len(e.resources)-1]
added.vectorKey, added.vectorStates = key, maps.Clone(states)
}
data, err := editorXMLContainer("CompositeObject", ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: e.nextID()}, {Name: xml.Name{Local: "Boundary"}, Value: editorBoxString(extent)}, {Name: xml.Name{Local: "ResourceID"}, Value: resource}}, nil)
if err != nil {
return nil, err
}
return newEditorCompositeNode(data)
}
// UngroupCompositeObject 展开内部组合,隔离引用资源并保留父容器的样式和裁剪 // UngroupCompositeObject 展开内部组合,隔离引用资源并保留父容器的样式和裁剪
// 入参: page 页面索引, path 父路径, index 组合序号 // 入参: page 页面索引, path 父路径, index 组合序号
// 返回: []int 展开后的成员序号, error 错误信息 // 返回: []int 展开后的成员序号, error 错误信息
+6 -13
View File
@@ -93,11 +93,12 @@ func (e *Editor) CopyObjectsToComposite(page int, path ObjectPath, objects []Gra
return err return err
} }
if node.object.Type == "PathObject" { if node.object.Type == "PathObject" {
node.defaults = renderer.drawParamDefaults(e.copiedLayerStyle(source), nil)
style, styleErr := e.resolveEditorStyle(node.object, e.copiedLayerStyle(source)) style, styleErr := e.resolveEditorStyle(node.object, e.copiedLayerStyle(source))
_, fillErr := e.Color(style.PathObject.FillColor) _, fillErr := e.Color(style.PathObject.FillColor)
_, strokeErr := e.Color((*FillColor)(style.PathObject.StrokeColor)) _, strokeErr := e.Color((*FillColor)(style.PathObject.StrokeColor))
if styleErr != nil && editReason(styleErr) == EditUnsupportedColor || fillErr != nil || strokeErr != nil { if styleErr != nil && editReason(styleErr) == EditUnsupportedColor || fillErr != nil || strokeErr != nil {
node, err = e.wrapCompositePath(node) node, err = e.wrapCompositePath(renderer, node)
if err != nil { if err != nil {
return err return err
} }
@@ -166,19 +167,11 @@ func (e *Editor) CopyObjectsToComposite(page int, path ObjectPath, objects []Gra
return result, nil return result, nil
} }
// wrapCompositePath 通过独立容器变换复杂路径,保持渐变坐标与原始颜色节点 // wrapCompositePath 通过标准矢量实例变换复杂路径,保持渐变坐标与原始颜色节点
// 入参: node 复杂路径 // 入参: renderer 资源度量器, node 复杂路径
// 返回: *editorCompositeNode 包装节点, error 错误信息 // 返回: *editorCompositeNode 包装节点, error 错误信息
func (e *Editor) wrapCompositePath(node *editorCompositeNode) (*editorCompositeNode, error) { func (e *Editor) wrapCompositePath(renderer *Renderer, node *editorCompositeNode) (*editorCompositeNode, error) {
content, err := editorXMLContainer("Content", nil, bytes.TrimPrefix(node.data, []byte(xml.Header))) return e.vectorInstance(renderer, []*editorCompositeNode{node})
if err != nil {
return nil, err
}
data, err := editorXMLContainer("CompositeObject", ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: e.nextID()}, {Name: xml.Name{Local: "Boundary"}, Value: "0 0 1 1"}}, bytes.TrimPrefix(content, []byte(xml.Header)))
if err != nil {
return nil, err
}
return newEditorCompositeNode(data)
} }
// isolateCompositeStyle 固定对象的绘制参数,避免目标容器改变颜色、虚线和线帽 // isolateCompositeStyle 固定对象的绘制参数,避免目标容器改变颜色、虚线和线帽
+1 -1
View File
@@ -180,7 +180,7 @@ func editorXMLRemapIDs(data []byte, root *editorXML, ids map[string]string) ([]b
} }
patches = append(patches, editorXMLPatch{node.start, node.open, value}) patches = append(patches, editorXMLPatch{node.start, node.open, value})
} }
if node.name.Local == "Font" || node.name.Local == "Substitution" { if node.name.Local == "Font" || node.name.Local == "Substitution" || node.name.Local == "Thumbnail" {
if value := ids[editorResourceID(editorImportText(data, node))]; value != "" { if value := ids[editorResourceID(editorImportText(data, node))]; value != "" {
patches = append(patches, editorXMLContent(data, node, []byte(value))) patches = append(patches, editorXMLContent(data, node, []byte(value)))
} }
+9 -3
View File
@@ -49,8 +49,14 @@ func (e *Editor) AddFont(file FontFile, index int) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
data := parsed.Data return e.addFontResource(parsed, sha256.Sum256(parsed.Data))
key := editorResourceKey{checksum: sha256.Sum256(data)} }
// addFontResource 注册已解析的只读字体,各文档独立分配标识和包内路径
// 入参: parsed 字体资源, checksum 字体数据摘要
// 返回: string 字体标识, error 标识准备错误
func (e *Editor) addFontResource(parsed *FontResource, checksum [32]byte) (string, error) {
key := editorResourceKey{checksum: checksum}
if id, ok := e.resourceID[key]; ok { if id, ok := e.resourceID[key]; ok {
return id, nil return id, nil
} }
@@ -62,7 +68,7 @@ func (e *Editor) AddFont(file FontFile, index int) (string, error) {
definition.ID = id definition.ID = id
resource := editorResource{ resource := editorResource{
name: e.packageName("Res/Fonts/Font_" + id + parsed.Extension), name: e.packageName("Res/Fonts/Font_" + id + parsed.Extension),
data: data, data: parsed.Data,
font: &definition, font: &definition,
} }
resource.font.FontFile = "/" + resource.name resource.font.FontFile = "/" + resource.name
+73 -11
View File
@@ -26,7 +26,7 @@ import (
// GroupObjects 将同一容器内连续的对象组合,保留标识、绘制顺序及原始内容 // GroupObjects 将同一容器内连续的对象组合,保留标识、绘制顺序及原始内容
// 入参: page 页面索引, ids 至少两个对象标识 // 入参: page 页面索引, ids 至少两个对象标识
// 返回: string 组合标识, error 错误信息 // 返回: string 组合标识, error 错误信息
func (e *Editor) GroupObjects(page int, ids []string) (string, error) { func (e *Editor) GroupObjects(page int, ids []string) (id string, err error) {
_, indexes, err := e.selectedObjects(page, ids) _, indexes, err := e.selectedObjects(page, ids)
if err != nil { if err != nil {
return "", err return "", err
@@ -48,11 +48,32 @@ func (e *Editor) GroupObjects(page int, ids []string) (string, error) {
return "", fmt.Errorf("grouping requires consecutive objects in the same container") return "", fmt.Errorf("grouping requires consecutive objects in the same container")
} }
} }
reader, err := e.Reader()
if err != nil {
return "", err
}
defer reader.Close()
renderer := e.newRenderer(reader)
var extent Box
for _, index := range indexes {
object := layer.Objects[index.index]
boundary, _ := editorGeometry(object)
box, err := creationBox(boundary)
if err != nil {
return "", err
}
visible, err := renderer.ObjectBounds(object, layer.DrawParam)
if err != nil {
return "", err
}
extent = unionTextBox(extent, unionTextBox(box, visible))
}
var content []byte var content []byte
var source *editorSourcePage var source *editorSourcePage
states := make(map[string]editorCompositeState) states := make(map[string]editorCompositeState)
for _, index := range indexes { for _, index := range indexes {
object := layer.Objects[index.index] before := layer.Objects[index.index]
object := transformEditorMatrix(cloneEditorData(before), TranslationMatrix(-extent.X, -extent.Y))
var data []byte var data []byte
if origin := e.objectOrigin(editorObjectID(object)); origin != nil { if origin := e.objectOrigin(editorObjectID(object)); origin != nil {
source = origin.page source = origin.page
@@ -79,12 +100,36 @@ func (e *Editor) GroupObjects(page int, ids []string) (string, error) {
if err := e.prepareSourceIDs(); err != nil { if err := e.prepareSourceIDs(); err != nil {
return "", err return "", err
} }
id := strconv.Itoa(e.maxID + 1) maxID, resourceCount := e.maxID, len(e.resources)
defer func() {
if err != nil {
e.maxID, e.resources = maxID, e.resources[:resourceCount]
}
}()
id, resourceID := e.nextID(), e.nextID()
content, err = editorXMLContainer("Content", nil, content) content, err = editorXMLContainer("Content", nil, content)
if err != nil { if err != nil {
return "", err return "", err
} }
data, err := editorXMLContainer("CompositeObject", ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: id}, {Name: xml.Name{Local: "Boundary"}, Value: "0 0 1 1"}}, bytes.TrimPrefix(content, []byte(xml.Header))) data, err := editorXMLContainer("CompositeGraphicUnit", ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: resourceID}, {Name: xml.Name{Local: "Width"}, Value: ofdNumber(extent.W)}, {Name: xml.Name{Local: "Height"}, Value: ofdNumber(extent.H)}}, bytes.TrimPrefix(content, []byte(xml.Header)))
if err != nil {
return "", err
}
data, err = editorXMLContainer("CompositeGraphicUnits", nil, bytes.TrimPrefix(data, []byte(xml.Header)))
if err != nil {
return "", err
}
data, err = editorXMLContainer("Res", ofdAttrs{{Name: xml.Name{Local: "BaseLoc"}, Value: "."}}, bytes.TrimPrefix(data, []byte(xml.Header)))
if err != nil {
return "", err
}
resource, err := e.compositeResource(resourceID, data)
if err != nil {
return "", err
}
resource.states = states
e.resources = append(e.resources, resource)
data, err = editorXMLContainer("CompositeObject", ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: id}, {Name: xml.Name{Local: "Boundary"}, Value: editorBoxString(extent)}, {Name: xml.Name{Local: "ResourceID"}, Value: resourceID}}, nil)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -98,7 +143,6 @@ func (e *Editor) GroupObjects(page int, ids []string) (string, error) {
group.node.parent = parent group.node.parent = parent
group.object.CompositeGraphicUnit.states = states group.object.CompositeGraphicUnit.states = states
origin := &editorObjectOrigin{page: source, data: data, node: group.node, object: group.object} origin := &editorObjectOrigin{page: source, data: data, node: group.node, object: group.object}
e.maxID++
e.replaceGroup(page, indexes, []GraphicObject{group.object}, map[string]*editorObjectOrigin{id: origin}) e.replaceGroup(page, indexes, []GraphicObject{group.object}, map[string]*editorObjectOrigin{id: origin})
return id, nil return id, nil
} }
@@ -148,16 +192,34 @@ func editorUngroupable(node *editorXML) bool {
if !editorXMLAttributes(node, "ID Boundary CTM DrawParam Alpha Visible ResourceID Width Height") { if !editorXMLAttributes(node, "ID Boundary CTM DrawParam Alpha Visible ResourceID Width Height") {
return false return false
} }
for _, child := range node.children { var contentSupported func(*editorXML) bool
switch child.name.Local { contentSupported = func(content *editorXML) bool {
case "Content": attributes := ""
if !editorXMLAttributes(child, "") { if content.name.Local == "PageBlock" {
attributes = "ID"
}
if !editorXMLAttributes(content, attributes) {
return false
}
for _, member := range content.children {
if member.name.Space != "" && member.name.Space != ofdNamespace && member.name.Space != "http://www.ofdspec.org" {
return false return false
} }
for _, member := range child.children { if member.name.Local == "PageBlock" {
if member.name.Space != "" && member.name.Space != ofdNamespace && member.name.Space != "http://www.ofdspec.org" || !slices.Contains([]string{"TextObject", "PathObject", "ImageObject", "CompositeObject", "CompositeGraphicUnit"}, member.name.Local) { if !contentSupported(member) {
return false return false
} }
} else if !slices.Contains([]string{"TextObject", "PathObject", "ImageObject", "CompositeObject", "CompositeGraphicUnit"}, member.name.Local) {
return false
}
}
return true
}
for _, child := range node.children {
switch child.name.Local {
case "Content", "PageBlock":
if !contentSupported(child) {
return false
} }
case "Clips": case "Clips":
if !editorXMLSupported(child) { if !editorXMLSupported(child) {
+1 -1
View File
@@ -1077,7 +1077,7 @@ func (m *editorPageImport) encode(entry editorImportEntry, node *editorXML) ([]b
candidate := path.Join(m.directory, "Annotations", "Page_"+m.id(node.parent.attr("PageID"))+".xml") candidate := path.Join(m.directory, "Annotations", "Page_"+m.id(node.parent.attr("PageID"))+".xml")
value, err = m.copyXML(loc, candidate) value, err = m.copyXML(loc, candidate)
} }
case "Font", "Substitution": case "Font", "Substitution", "Thumbnail":
value, err = m.reference(strings.TrimSpace(value)) value, err = m.reference(strings.TrimSpace(value))
} }
if err != nil { if err != nil {
+8 -1
View File
@@ -702,6 +702,13 @@ func editorStrokeScale(ctm string) float64 {
// 入参: border 边框 // 入参: border 边框
// 返回: error 错误信息 // 返回: error 错误信息
func (e *Editor) validateImageBorder(border *ImageBorder) error { func (e *Editor) validateImageBorder(border *ImageBorder) error {
return (&editorValidation{Editor: e}).validateImageBorder(border)
}
// validateImageBorder 沿当前颜色引用路径校验图片边框
// 入参: border 边框
// 返回: error 错误信息
func (v *editorValidation) validateImageBorder(border *ImageBorder) error {
if border == nil { if border == nil {
return nil return nil
} }
@@ -717,7 +724,7 @@ func (e *Editor) validateImageBorder(border *ImageBorder) error {
if err := validateEditorStroke(stroke); err != nil { if err := validateEditorStroke(stroke); err != nil {
return err return err
} }
return e.editorColor((*FillColor)(border.BorderColor)) return v.editorColor((*FillColor)(border.BorderColor))
} }
// validateEditorStroke 校验路径描边尺寸、端点和虚线 // validateEditorStroke 校验路径描边尺寸、端点和虚线
+26 -2
View File
@@ -421,8 +421,8 @@ func (e *Editor) transformBorderedImage(object GraphicObject, matrix Matrix, ori
return transformEditorMatrix(object, matrix), nil return transformEditorMatrix(object, matrix), nil
} }
// transformEditorMatrix 在页面坐标中变换基本对象,保留局部绘制数据和裁剪 // transformEditorMatrix 在所在坐标系中变换对象,保留局部绘制数据和裁剪
// 入参: object 对象, matrix 页面变换 // 入参: object 对象, matrix 所在坐标系中的变换
// 返回: GraphicObject 变换后的独立对象 // 返回: GraphicObject 变换后的独立对象
func transformEditorMatrix(object GraphicObject, matrix Matrix) GraphicObject { func transformEditorMatrix(object GraphicObject, matrix Matrix) GraphicObject {
var boundary, ctm *string var boundary, ctm *string
@@ -447,6 +447,30 @@ func transformEditorMatrix(object GraphicObject, matrix Matrix) GraphicObject {
if clips := editorObjectClips(&object); *clips != nil && (*clips).TransFlag != nil && !*(*clips).TransFlag { if clips := editorObjectClips(&object); *clips != nil && (*clips).TransFlag != nil && !*(*clips).TransFlag {
*clips = transformObjectClips(*clips, local) *clips = transformObjectClips(*clips, local)
} }
if !axisAlignedMatrix(matrix) && before.W > 0 && before.H > 0 {
clips := editorObjectClips(&object)
if *clips == nil {
*clips = &Clips{}
} else {
copy := **clips
copy.Clip = slices.Clone(copy.Clip)
*clips = &copy
}
clipMatrix := local
if (*clips).TransFlag == nil || *(*clips).TransFlag {
if inverse, ok := NewMatrix(*ctm).Invert(); ok {
clipMatrix = inverse.Multiply(local)
} else {
*clips = transformObjectClips(*clips, NewMatrix(*ctm))
flag := false
(*clips).TransFlag = &flag
}
}
fill, stroke := true, false
path := PathObject{Boundary: editorBoxString(Box{W: before.W, H: before.H}), Fill: &fill, Stroke: &stroke,
AbbreviatedData: "M 0 0 L " + ofdNumber(before.W) + " 0 L " + ofdNumber(before.W) + " " + ofdNumber(before.H) + " L 0 " + ofdNumber(before.H) + " C"}
(*clips).Clip = append((*clips).Clip, Clip{Area: []ClipArea{{CTM: clipMatrix.String(), Path: []PathObject{path}}}})
}
return object return object
} }
+17 -9
View File
@@ -81,10 +81,16 @@ func (e *Editor) AddWatermarks(ctx context.Context, indexes []int, object Graphi
if _, err := creationNumbers(*ctm, 6); err != nil { if _, err := creationNumbers(*ctm, 6); err != nil {
return nil, err return nil, err
} }
} else if object.Type == "ImageObject" {
*ctm = (Matrix{a: box.W, d: box.H}).String()
} }
*ctm = rotation.Multiply(NewMatrix(*ctm)).String() object = transformEditorMatrix(object, rotation)
if object.Type == "TextObject" {
boundary = &object.TextObject.Boundary
} else {
boundary = &object.ImageObject.Boundary
}
extent, _ := ParseBox(*boundary)
extent.X, extent.Y = 0, 0
*boundary = editorBoxString(extent)
preview := e.transactionSnapshot() preview := e.transactionSnapshot()
preview.SetHistoryLimit(0) preview.SetHistoryLimit(0)
id, err := preview.AddObject(indexes[0], object) id, err := preview.AddObject(indexes[0], object)
@@ -169,13 +175,15 @@ func layoutWatermark(ctx context.Context, base GraphicObject, bounds, box Box, o
} }
if scale != 1 { if scale != 1 {
matrix := Matrix{a: scale, d: scale} matrix := Matrix{a: scale, d: scale}
if base.Type == "TextObject" { base = transformEditorMatrix(base, matrix)
base.TextObject.CTM = matrix.Multiply(NewMatrix(base.TextObject.CTM)).String()
} else {
base.ImageObject.CTM = matrix.Multiply(NewMatrix(base.ImageObject.CTM)).String()
}
bounds = matrix.TransformBox(bounds) bounds = matrix.TransformBox(bounds)
} }
var extent Box
if base.Type == "TextObject" {
extent, _ = ParseBox(base.TextObject.Boundary)
} else {
extent, _ = ParseBox(base.ImageObject.Boundary)
}
columns, rows := 1.0, 1.0 columns, rows := 1.0, 1.0
if options.Tile { if options.Tile {
angle := math.Mod(options.Angle, 360) * math.Pi / 180 angle := math.Mod(options.Angle, 360) * math.Pi / 180
@@ -199,7 +207,7 @@ func layoutWatermark(ctx context.Context, base GraphicObject, bounds, box Box, o
x := (float64(column)+0.5)*area.W/columns - bounds.X - bounds.W/2 x := (float64(column)+0.5)*area.W/columns - bounds.X - bounds.W/2
y := (float64(row)+0.5)*area.H/rows - bounds.Y - bounds.H/2 y := (float64(row)+0.5)*area.H/rows - bounds.Y - bounds.H/2
object := base object := base
boundary := editorBoxString(Box{X: x, Y: y, W: box.W, H: box.H}) boundary := editorBoxString(Box{X: x, Y: y, W: extent.W, H: extent.H})
if object.Type == "TextObject" { if object.Type == "TextObject" {
object.TextObject.Boundary = boundary object.TextObject.Boundary = boundary
} else { } else {
+1 -1
View File
@@ -205,7 +205,7 @@ func (r *Renderer) resetFontBackendCache() {
r.fontSourcesCache.fallback = ResolvedFont{} r.fontSourcesCache.fallback = ResolvedFont{}
r.fontSourcesCache.fallbackRead = false r.fontSourcesCache.fallbackRead = false
} }
r.fontMetrics = renderCache[[32]byte, FontMetrics]{limit: 32 << 20} r.fontMetrics = &renderCache[[32]byte, FontMetrics]{limit: 32 << 20}
r.glyphOutlines = renderCache[glyphOutlineKey, GeometryPath]{limit: 16 << 20} r.glyphOutlines = renderCache[glyphOutlineKey, GeometryPath]{limit: 16 << 20}
for _, state := range r.backendStates { for _, state := range r.backendStates {
if fonts, ok := state.(interface{ resetFonts() }); ok { if fonts, ok := state.(interface{ resetFonts() }); ok {
+8 -6
View File
@@ -177,13 +177,15 @@ func ttProject(x, y int32, vector [2]float64) int32 {
// 入参: point 点, distance 投影距离, touch 是否标记触及状态 // 入参: point 点, distance 投影距离, touch 是否标记触及状态
func (v *ttInterpreter) move(point *ttPoint, distance int32, touch bool) { func (v *ttInterpreter) move(point *ttPoint, distance int32, touch bool) {
g := &v.graphics g := &v.graphics
dot := g.freedom[0]*g.projection[0] + g.freedom[1]*g.projection[1] if distance != 0 {
if math.Abs(dot) < 1.0/16384 { dot := g.freedom[0]*g.projection[0] + g.freedom[1]*g.projection[1]
v.err = fmt.Errorf("perpendicular TrueType movement vectors") if math.Abs(dot) < 1.0/16384 {
return v.err = fmt.Errorf("perpendicular TrueType movement vectors")
return
}
point.x += int32(math.Round(float64(distance) * g.freedom[0] / dot))
point.y += int32(math.Round(float64(distance) * g.freedom[1] / dot))
} }
point.x += int32(math.Round(float64(distance) * g.freedom[0] / dot))
point.y += int32(math.Round(float64(distance) * g.freedom[1] / dot))
if touch { if touch {
point.tx = point.tx || g.freedom[0] != 0 point.tx = point.tx || g.freedom[0] != 0
point.ty = point.ty || g.freedom[1] != 0 point.ty = point.ty || g.freedom[1] != 0
+33 -28
View File
@@ -66,6 +66,12 @@ type fontUsagePage struct {
usage map[string]int usage map[string]int
} }
// fontUsageReferences 保存字体统计中的当前引用路径,不合并共享内容的用量
type fontUsageReferences struct {
composites map[string]bool
patterns map[*Pattern]bool
}
// Fonts 获取OFD声明的字体列表 // Fonts 获取OFD声明的字体列表
// 返回: []Font 字体列表, error 错误信息 // 返回: []Font 字体列表, error 错误信息
func (r *Reader) Fonts() ([]Font, error) { func (r *Reader) Fonts() ([]Font, error) {
@@ -492,8 +498,8 @@ func (r *Renderer) countLayerFonts(layer Layer, usage map[string]int) {
} }
// countObjectFonts 统计图元字体使用次数 // countObjectFonts 统计图元字体使用次数
// 入参: obj 图元对象, usage 字体使用次数, defaults 默认绘制参数, visited 已访问资源 // 入参: obj 图元对象, usage 字体使用次数, defaults 默认绘制参数, visited 当前引用路径
func (r *Renderer) countObjectFonts(obj GraphicObject, usage map[string]int, defaults *DrawParam, visited map[string]bool) { func (r *Renderer) countObjectFonts(obj GraphicObject, usage map[string]int, defaults *DrawParam, visited *fontUsageReferences) {
var fill *FillColor var fill *FillColor
var stroke *StrokeColor var stroke *StrokeColor
var shouldFill, shouldStroke bool var shouldFill, shouldStroke bool
@@ -539,50 +545,49 @@ func (r *Renderer) countObjectFonts(obj GraphicObject, usage map[string]int, def
} }
// countCompositeFonts 统计复合图元字体使用次数 // countCompositeFonts 统计复合图元字体使用次数
// 入参: cgu 复合图元, usage 字体使用次数, defaults 默认绘制参数, visited 已访问资源 // 入参: cgu 复合图元, usage 字体使用次数, defaults 默认绘制参数, visited 当前引用路径
func (r *Renderer) countCompositeFonts(cgu CompositeGraphicUnit, usage map[string]int, defaults *DrawParam, visited map[string]bool) { func (r *Renderer) countCompositeFonts(cgu CompositeGraphicUnit, usage map[string]int, defaults *DrawParam, visited *fontUsageReferences) {
r.countClipFonts(cgu.Clips, usage) r.countClipFonts(cgu.Clips, usage)
defaults = r.drawParamDefaults(cgu.DrawParam, defaults) defaults = r.drawParamDefaults(cgu.DrawParam, defaults)
if cgu.ResourceID != "" { if cgu.ResourceID != "" {
if visited == nil { if visited == nil {
visited = make(map[string]bool) visited = &fontUsageReferences{}
} }
if visited[cgu.ResourceID] { if visited.composites[cgu.ResourceID] {
return return
} }
visited[cgu.ResourceID] = true if visited.composites == nil {
visited.composites = make(map[string]bool)
}
visited.composites[cgu.ResourceID] = true
if ref := r.CompositeGraphicUnits[cgu.ResourceID]; ref != nil { if ref := r.CompositeGraphicUnits[cgu.ResourceID]; ref != nil {
r.countCompositeFonts(*ref, usage, defaults, visited) r.countCompositeFonts(*ref, usage, defaults, visited)
} }
defer delete(visited, cgu.ResourceID) defer delete(visited.composites, cgu.ResourceID)
} }
if len(cgu.Objects) > 0 { for object := range cgu.objects() {
for _, obj := range cgu.Objects { r.countObjectFonts(object, usage, defaults, visited)
r.countObjectFonts(obj, usage, defaults, visited)
}
return
}
for _, text := range cgu.TextObject {
r.countObjectFonts(GraphicObject{Type: "TextObject", TextObject: text}, usage, defaults, visited)
}
for _, path := range cgu.PathObject {
r.countObjectFonts(GraphicObject{Type: "PathObject", PathObject: path}, usage, defaults, visited)
}
for _, image := range cgu.ImageObject {
r.countObjectFonts(GraphicObject{Type: "ImageObject", ImageObject: image}, usage, defaults, visited)
}
for _, sub := range cgu.CompositeGraphicUnit {
r.countCompositeFonts(sub, usage, defaults, visited)
} }
} }
// countPatternFonts 统计底纹单元字体使用次数 // countPatternFonts 统计底纹单元字体使用次数
// 入参: fill 填充或描边颜色, usage 字体使用次数, visited 已访问资源 // 入参: fill 填充或描边颜色, usage 字体使用次数, visited 当前引用路径
func (r *Renderer) countPatternFonts(fill *FillColor, usage map[string]int, visited map[string]bool) { func (r *Renderer) countPatternFonts(fill *FillColor, usage map[string]int, visited *fontUsageReferences) {
if fill == nil || fill.Pattern == nil { if fill == nil || fill.Pattern == nil {
return return
} }
for _, obj := range fill.Pattern.CellContent.Objects { if visited == nil {
visited = &fontUsageReferences{}
}
if visited.patterns[fill.Pattern] {
return
}
if visited.patterns == nil {
visited.patterns = make(map[*Pattern]bool)
}
visited.patterns[fill.Pattern] = true
defer delete(visited.patterns, fill.Pattern)
for obj := range fill.Pattern.CellContent.objects() {
r.countObjectFonts(obj, usage, nil, visited) r.countObjectFonts(obj, usage, nil, visited)
} }
} }
+64 -1
View File
@@ -145,11 +145,74 @@ func geometryRectangleBounds(path GeometryPath) (Box, bool) {
return Box{}, false return Box{}, false
} }
a, b, c, d := path[0].End, path[1].End, path[2].End, path[3].End a, b, c, d := path[0].End, path[1].End, path[2].End, path[3].End
for _, point := range [...]Point{a, b, c, d} {
if !finite(point.X) || !finite(point.Y) {
return Box{}, false
}
}
if a.X == c.X || a.Y == c.Y { if a.X == c.X || a.Y == c.Y {
return Box{}, false return Box{}, false
} }
if !(a.X == b.X && b.Y == c.Y && c.X == d.X && d.Y == a.Y || a.Y == b.Y && b.X == c.X && c.Y == d.Y && d.X == a.X) { if !(a.X == b.X && b.Y == c.Y && c.X == d.X && d.Y == a.Y || a.Y == b.Y && b.X == c.X && c.Y == d.Y && d.X == a.X) {
return Box{}, false return Box{}, false
} }
return Box{X: min(a.X, c.X), Y: min(a.Y, c.Y), W: math.Abs(c.X - a.X), H: math.Abs(c.Y - a.Y)}, true width, height := math.Abs(c.X-a.X), math.Abs(c.Y-a.Y)
if !finite(width) || !finite(height) {
return Box{}, false
}
return Box{X: min(a.X, c.X), Y: min(a.Y, c.Y), W: width, H: height}, true
}
// clipGeometry 将非零填充路径与页面裁剪相交
// 入参: geometry 几何后端, path 页面路径, clip 页面裁剪
// 返回: GeometryPath 裁剪路径, error 几何错误
func clipGeometry(geometry GeometryBackend, path GeometryPath, clip *GeometryPath) (GeometryPath, error) {
if clip == nil {
return path, nil
}
if len(*clip) == 0 {
return nil, nil
}
if rect, ok := geometryRectangleBounds(*clip); ok {
if bounds, ok := geometryRectangleBounds(path); ok {
left, top := max(rect.X, bounds.X), max(rect.Y, bounds.Y)
right, bottom := min(rect.X+rect.W, bounds.X+bounds.W), min(rect.Y+rect.H, bounds.Y+bounds.H)
if right <= left || bottom <= top {
return nil, nil
}
return geometryRectangle(Box{X: left, Y: top, W: right - left, H: bottom - top}), nil
}
if err := path.validate(); err != nil {
return nil, err
}
contained := true
for _, segment := range path {
if segment.Verb == GeometryArc {
contained = false
break
}
points := [3]Point{segment.End, segment.Control1, segment.Control2}
count := 1
if segment.Verb == GeometryQuad {
count = 2
} else if segment.Verb == GeometryCubic {
count = 3
} else if segment.Verb == GeometryClose {
continue
}
for _, point := range points[:count] {
if point.X < rect.X || point.Y < rect.Y || point.X > rect.X+rect.W || point.Y > rect.Y+rect.H {
contained = false
break
}
}
if !contained {
break
}
}
if contained {
return path, nil
}
}
return geometry.Combine(path, *clip, GeometryIntersect)
} }
+85
View File
@@ -0,0 +1,85 @@
// 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 "math"
// geometryConvexContains 判断单个非退化凸直线区域是否包含另一闭合直线区域
// 入参: outer 外部区域, inner 内部区域
// 返回: bool 是否完全包含,曲线及多轮廓返回false
func geometryConvexContains(outer, inner GeometryPath) bool {
a, b := geometryPolygonPoints(outer), geometryPolygonPoints(inner)
if len(a) < 3 || len(b) < 3 {
return false
}
nondegenerate := false
for i, start := range a {
end := a[(i+1)%len(a)]
dx, dy := end.X-start.X, end.Y-start.Y
length := math.Hypot(dx, dy)
if !finite(length) {
return false
}
if length == 0 {
continue
}
dx, dy = dx/length, dy/length
distance := func(point Point) float64 {
x, y := point.X-start.X, point.Y-start.Y
value := math.FMA(dx, y, -dy*x)
bound := (math.Abs(dx*y) + math.Abs(dy*x) + math.Abs(dx)*(math.Abs(point.Y)+math.Abs(start.Y)) + math.Abs(dy)*(math.Abs(point.X)+math.Abs(start.X))) * 0x1p-49
if finite(bound) && math.Abs(value) <= bound {
return 0
}
return value
}
sign := 0.0
for _, point := range a {
value := distance(point)
if !finite(value) || sign*value < 0 {
return false
}
if value != 0 {
sign, nondegenerate = math.Copysign(1, value), true
}
}
for _, point := range b {
value := distance(point)
if !finite(value) || sign*value < 0 {
return false
}
}
}
return nondegenerate
}
// geometryPolygonPoints 读取单个显式闭合直线轮廓的顶点,不修改原路径
// 入参: path 直线轮廓
// 返回: []Point 独立顶点,其他路径返回nil
func geometryPolygonPoints(path GeometryPath) []Point {
if len(path) < 4 || path[0].Verb != GeometryMove || path[len(path)-1].Verb != GeometryClose {
return nil
}
for i, segment := range path[:len(path)-1] {
if i > 0 && segment.Verb != GeometryLine || !finite(segment.End.X) || !finite(segment.End.Y) {
return nil
}
}
points := make([]Point, len(path)-1)
for i := range points {
points[i] = path[i].End
}
return points
}
+7 -2
View File
@@ -216,9 +216,10 @@ func (line geometryPolyline) dash(pattern []float64, total, offset float64) ([]g
remaining := pattern[index] - phase remaining := pattern[index] - phase
var result []geometryPolyline var result []geometryPolyline
var run []Point var run []Point
var direction Point
flush := func() { flush := func() {
if len(run) != 0 { if len(run) != 0 {
result = append(result, geometryPolyline{points: run}) result = append(result, geometryPolyline{points: run, direction: direction})
run = nil run = nil
} }
} }
@@ -255,8 +256,12 @@ func (line geometryPolyline) dash(pattern []float64, total, offset float64) ([]g
if index%2 == 0 { if index%2 == 0 {
if len(run) == 0 { if len(run) == 0 {
run = append(run, geometryLerp(a, b, at/length)) run = append(run, geometryLerp(a, b, at/length))
direction = Point{(b.X - a.X) / length, (b.Y - a.Y) / length}
}
end := geometryLerp(a, b, (at+step)/length)
if end != run[len(run)-1] {
run = append(run, end)
} }
run = append(run, geometryLerp(a, b, (at+step)/length))
} }
at += step at += step
remaining -= step remaining -= step
+6 -5
View File
@@ -129,28 +129,29 @@ func (p GeometryPath) Transform(matrix Matrix) (GeometryPath, error) {
func (p GeometryPath) validate() error { func (p GeometryPath) validate() error {
started := false started := false
for _, s := range p { for _, s := range p {
points := []Point{s.End} points := [3]Point{s.End, s.Control1, s.Control2}
count := 1
switch s.Verb { switch s.Verb {
case GeometryMove: case GeometryMove:
started = true started = true
case GeometryLine: case GeometryLine:
case GeometryQuad: case GeometryQuad:
points = append(points, s.Control1) count = 2
case GeometryCubic: case GeometryCubic:
points = append(points, s.Control1, s.Control2) count = 3
case GeometryArc: case GeometryArc:
if !finite(s.RadiusX) || !finite(s.RadiusY) || !finite(s.Rotation) || s.RadiusX < 0 || s.RadiusY < 0 { if !finite(s.RadiusX) || !finite(s.RadiusY) || !finite(s.Rotation) || s.RadiusX < 0 || s.RadiusY < 0 {
return fmt.Errorf("invalid arc parameters") return fmt.Errorf("invalid arc parameters")
} }
case GeometryClose: case GeometryClose:
points = nil count = 0
default: default:
return fmt.Errorf("unsupported geometry command %d", s.Verb) return fmt.Errorf("unsupported geometry command %d", s.Verb)
} }
if !started { if !started {
return fmt.Errorf("geometry path must start with move") return fmt.Errorf("geometry path must start with move")
} }
for _, point := range points { for _, point := range points[:count] {
if !finite(point.X) || !finite(point.Y) { if !finite(point.X) || !finite(point.Y) {
return fmt.Errorf("non-finite geometry coordinate") return fmt.Errorf("non-finite geometry coordinate")
} }
+74 -1
View File
@@ -16,7 +16,10 @@ package ofdgo
import ( import (
"encoding/xml" "encoding/xml"
"fmt"
"iter"
"strconv" "strconv"
"strings"
) )
// graphicObjectTarget 图形对象集合 // graphicObjectTarget 图形对象集合
@@ -144,6 +147,19 @@ func (c *CompositeGraphicUnit) UnmarshalXML(d *xml.Decoder, start xml.StartEleme
c.Boundary = attrValue(start, "Boundary") c.Boundary = attrValue(start, "Boundary")
c.CTM = attrValue(start, "CTM") c.CTM = attrValue(start, "CTM")
c.DrawParam = attrValue(start, "DrawParam") c.DrawParam = attrValue(start, "DrawParam")
for _, dimension := range []struct {
name string
value *float64
}{{"Width", &c.Width}, {"Height", &c.Height}} {
if value := attrValue(start, dimension.name); value != "" {
c.extentSet = true
var err error
*dimension.value, err = strconv.ParseFloat(strings.TrimSpace(value), 64)
if err != nil {
return fmt.Errorf("invalid vector %s: %w", dimension.name, err)
}
}
}
if value := attrValue(start, "Alpha"); value != "" { if value := attrValue(start, "Alpha"); value != "" {
if alpha, err := strconv.Atoi(value); err == nil { if alpha, err := strconv.Atoi(value); err == nil {
c.Alpha = &alpha c.Alpha = &alpha
@@ -170,10 +186,57 @@ func (a *Appearance) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error
// 入参: d XML解码器, start 起始节点 // 入参: d XML解码器, start 起始节点
// 返回: error 错误信息 // 返回: error 错误信息
func (p *PatternContent) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { func (p *PatternContent) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
*p = PatternContent{} *p = PatternContent{Thumbnail: attrValue(start, "Thumbnail")}
return decodeObjectContainer(d, start, p.decodeObject) return decodeObjectContainer(d, start, p.decodeObject)
} }
// objects 按绘制顺序访问底纹单元,Objects优先于分类数组
// 返回: iter.Seq[GraphicObject] 图形对象序列
func (p PatternContent) objects() iter.Seq[GraphicObject] {
return func(yield func(GraphicObject) bool) {
if len(p.Objects) != 0 {
for _, object := range p.Objects {
if !yield(object) {
return
}
}
return
}
for _, object := range p.TextObject {
if !yield(GraphicObject{Type: "TextObject", TextObject: object}) {
return
}
}
for _, object := range p.PathObject {
if !yield(GraphicObject{Type: "PathObject", PathObject: object}) {
return
}
}
for _, object := range p.ImageObject {
if !yield(GraphicObject{Type: "ImageObject", ImageObject: object}) {
return
}
}
for _, object := range p.CompositeGraphicUnit {
if !yield(GraphicObject{Type: "CompositeObject", CompositeGraphicUnit: object}) {
return
}
}
}
}
// objects 按绘制顺序访问矢量内容,Objects优先于分类数组
// 返回: iter.Seq[GraphicObject] 图形对象序列
func (c CompositeGraphicUnit) objects() iter.Seq[GraphicObject] {
return (PatternContent{Objects: c.Objects, TextObject: c.TextObject, PathObject: c.PathObject, ImageObject: c.ImageObject, CompositeGraphicUnit: c.CompositeGraphicUnit}).objects()
}
// empty 检查底纹单元是否没有绘制内容,缩略图不参与绘制
// 返回: bool 是否为空
func (p PatternContent) empty() bool {
return len(p.Objects) == 0 && len(p.TextObject) == 0 && len(p.PathObject) == 0 && len(p.ImageObject) == 0 && len(p.CompositeGraphicUnit) == 0
}
// UnmarshalXML 解析渐变分段并保留位置的缺省状态 // UnmarshalXML 解析渐变分段并保留位置的缺省状态
// 入参: d XML解码器, start 起始节点 // 入参: d XML解码器, start 起始节点
// 返回: error 错误信息 // 返回: error 错误信息
@@ -279,6 +342,16 @@ func (c *CompositeGraphicUnit) decodeObject(d *xml.Decoder, start xml.StartEleme
return err return err
} }
switch start.Name.Local { switch start.Name.Local {
case "Thumbnail", "Substitution":
var reference string
if err := d.DecodeElement(&reference, &start); err != nil {
return err
}
if start.Name.Local == "Thumbnail" {
c.Thumbnail = strings.TrimSpace(reference)
} else {
c.Substitution = strings.TrimSpace(reference)
}
case "Clips": case "Clips":
var clips Clips var clips Clips
if err := d.DecodeElement(&clips, &start); err != nil { if err := d.DecodeElement(&clips, &start); err != nil {
+6 -4
View File
@@ -109,8 +109,8 @@ type TextObject struct {
TextCode []TextCode `xml:"TextCode"` TextCode []TextCode `xml:"TextCode"`
Clips *Clips `xml:"Clips"` Clips *Clips `xml:"Clips"`
Actions []Action `xml:"Actions>Action"` Actions []Action `xml:"Actions>Action"`
LineWidthSet bool `xml:"-"` LineWidthSet bool `xml:"-"`
layout *textLayout layout *textLayout
} }
// FillColor 填充颜色 // FillColor 填充颜色
@@ -140,7 +140,9 @@ type Pattern struct {
} }
// PatternContent 图案单元内容 // PatternContent 图案单元内容
// Objects非空时保留绘制顺序,否则依次使用文字、路径、图片和复合图元数组
type PatternContent struct { type PatternContent struct {
Thumbnail string `xml:"Thumbnail,attr"`
Objects []GraphicObject `xml:"-"` Objects []GraphicObject `xml:"-"`
TextObject []TextObject `xml:"TextObject"` TextObject []TextObject `xml:"TextObject"`
PathObject []PathObject `xml:"PathObject"` PathObject []PathObject `xml:"PathObject"`
@@ -189,8 +191,8 @@ type PathObject struct {
AbbreviatedData string `xml:"AbbreviatedData"` AbbreviatedData string `xml:"AbbreviatedData"`
Clips *Clips `xml:"Clips"` Clips *Clips `xml:"Clips"`
Actions []Action `xml:"Actions>Action"` Actions []Action `xml:"Actions>Action"`
LineWidthSet bool `xml:"-"` LineWidthSet bool `xml:"-"`
dashPatternSet bool dashPatternSet bool
} }
// StrokeColor 勾边颜色 // StrokeColor 勾边颜色
+16 -5
View File
@@ -68,10 +68,8 @@ type pdfImporter struct {
matrix pdfgo.Matrix matrix pdfgo.Matrix
page int page int
fontIDs map[*pdfgo.Font]string fontIDs map[*pdfgo.Font]string
fontMetrics map[string]FontMetrics fonts map[*pdfgo.Font]*pdfImportedFont
fontRepairLimit map[*pdfgo.Font]uint16
fontWarnings map[string]bool fontWarnings map[string]bool
type1Glyphs map[*pdfgo.Font]map[string]uint16
clipTexts map[*pdfgo.TextClip]TextObject clipTexts map[*pdfgo.TextClip]TextObject
cmykSpace string cmykSpace string
objects []GraphicObject objects []GraphicObject
@@ -90,8 +88,10 @@ type pdfImporter struct {
compositeCache *pdfCompositeCache compositeCache *pdfCompositeCache
compositeNodes []pdfCompositeNode compositeNodes []pdfCompositeNode
compositeSpace *pdfgo.ColorSpace compositeSpace *pdfgo.ColorSpace
annotationData []byte
resolveFile pdfgo.FileResolver resolveFile pdfgo.FileResolver
halftoneWarnings map[*pdfgo.Halftone]bool halftoneWarnings map[*pdfgo.Halftone]bool
transferBackdrop bool
} }
// ImportPDF 将PDF内容转换为独立OFD编辑文档,失败时不返回部分结果 // ImportPDF 将PDF内容转换为独立OFD编辑文档,失败时不返回部分结果
@@ -132,7 +132,7 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
editor := NewEditor() editor := NewEditor()
editor.SetRenderBackends(renderer.Backends()) editor.SetRenderBackends(renderer.Backends())
editor.fontDirs, editor.fontFS = renderer.FontSources() editor.fontDirs, editor.fontFS = renderer.FontSources()
importer := pdfImporter{ctx: ctx, reader: reader, editor: editor, renderer: renderer, rasterDPI: renderer.DPI, 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, renderer: renderer, rasterDPI: renderer.DPI, fontIDs: map[*pdfgo.Font]string{}, fonts: map[*pdfgo.Font]*pdfImportedFont{}, pages: map[pdfgo.Reference]*pdfgo.Page{}, pageIDs: map[pdfgo.Reference]string{}}
importer.report.Warnings = diagnostics importer.report.Warnings = diagnostics
importer.pageIndexes = make(map[pdfgo.Reference]int) importer.pageIndexes = make(map[pdfgo.Reference]int)
importer.resolveFile = options.ResolveFile importer.resolveFile = options.ResolveFile
@@ -188,6 +188,7 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
importer.compositeNodes = nil importer.compositeNodes = nil
importer.compositeSpace = nil importer.compositeSpace = nil
importer.clipTexts = make(map[*pdfgo.TextClip]TextObject) importer.clipTexts = make(map[*pdfgo.TextClip]TextObject)
importer.annotationData = nil
if err := importer.compositePage(nil, func(visitor pdfgo.Visitor) error { return reader.WalkPage(ctx, page, visitor) }); err != nil { if err := importer.compositePage(nil, func(visitor pdfgo.Visitor) error { return reader.WalkPage(ctx, page, visitor) }); err != nil {
return fmt.Errorf("import PDF page %d: %w", index+1, err) return fmt.Errorf("import PDF page %d: %w", index+1, err)
} }
@@ -197,6 +198,12 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
if err := importer.annotations(ctx, page, options.Strict); err != nil { if err := importer.annotations(ctx, page, options.Strict); err != nil {
return fmt.Errorf("import PDF page %d: %w", index+1, err) return fmt.Errorf("import PDF page %d: %w", index+1, err)
} }
if len(importer.annotationData) != 0 {
if err := editor.appendAnnotations(index, importer.annotationData); err != nil {
return fmt.Errorf("import PDF page %d: %w", index+1, err)
}
importer.annotationData = nil
}
if err := importer.commitObjects(); err != nil { if err := importer.commitObjects(); err != nil {
return fmt.Errorf("import PDF page %d: %w", index+1, err) return fmt.Errorf("import PDF page %d: %w", index+1, err)
} }
@@ -545,6 +552,9 @@ func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error {
// 入参: paint PDF画刷, box 对象边界 // 入参: paint PDF画刷, box 对象边界
// 返回: *FillColor OFD颜色或渐变, error 图案转换错误 // 返回: *FillColor OFD颜色或渐变, error 图案转换错误
func (p *pdfImporter) paintColor(paint pdfgo.Paint, box Box) (*FillColor, error) { func (p *pdfImporter) paintColor(paint pdfgo.Paint, box Box) (*FillColor, error) {
if err := pdfGradientError(paint); err != nil {
return nil, err
}
if paint.Mesh != nil { if paint.Mesh != nil {
return p.meshColor(paint, box) return p.meshColor(paint, box)
} }
@@ -704,5 +714,6 @@ func pdfOpaqueBlack(paint pdfgo.Paint) bool {
// 入参: paint PDF颜色与不透明度 // 入参: paint PDF颜色与不透明度
// 返回: bool 是否需要保留分色语义 // 返回: bool 是否需要保留分色语义
func pdfOverprintNeedsSeparation(paint pdfgo.Paint) bool { func pdfOverprintNeedsSeparation(paint pdfgo.Paint) bool {
return paint.CMYK != nil && !pdfOpaqueBlack(paint) _, process := paint.Process.CMYKMask()
return (paint.CMYK != nil || process) && !pdfOpaqueBlack(paint)
} }
+11 -3
View File
@@ -15,7 +15,9 @@
package ofdgo package ofdgo
import ( import (
"bytes"
"context" "context"
"encoding/xml"
"fmt" "fmt"
"maps" "maps"
"strconv" "strconv"
@@ -42,7 +44,7 @@ func (p *pdfImporter) appearanceAnnotation(ctx context.Context, page *pdfgo.Page
if err != nil { if err != nil {
return err return err
} }
if annotation.Subtype == "FreeText" { if annotation.Subtype == "FreeText" || annotation.Subtype == "Line" {
annotation.Dictionary = maps.Clone(annotation.Dictionary) annotation.Dictionary = maps.Clone(annotation.Dictionary)
annotation.Dictionary["Contents"] = pdfgo.String("\xef\xbb\xbf" + remark) annotation.Dictionary["Contents"] = pdfgo.String("\xef\xbb\xbf" + remark)
} }
@@ -101,8 +103,12 @@ func (p *pdfImporter) appearanceAnnotation(ctx context.Context, page *pdfgo.Page
} }
if flags&(2|32) == 0 { if flags&(2|32) == 0 {
p.compositeNodes = stamp.compositeNodes p.compositeNodes = stamp.compositeNodes
p.transferBackdrop = stamp.transferBackdrop
} }
converted := Annotation{Type: pdfAnnotationType(annotation.Subtype), Subtype: string(annotation.Subtype), Remark: remark, Appearance: Appearance{Boundary: pdfBoundary(box), Objects: stamp.objects}} converted := Annotation{Type: pdfAnnotationType(annotation.Subtype), Subtype: string(annotation.Subtype), Remark: remark, Appearance: Appearance{Boundary: pdfBoundary(box), Objects: stamp.objects}}
if annotation.Subtype == "Redact" {
converted.Parameters = &AnnotationParameters{Parameter: []AnnotationParameter{{Name: "PDF.Redact.Pending", Value: "true"}}}
}
if annotation.Subtype == "Text" { if annotation.Subtype == "Text" {
value, err := p.reader.Resolve(annotation.Dictionary["Open"]) value, err := p.reader.Resolve(annotation.Dictionary["Open"])
if err != nil { if err != nil {
@@ -159,9 +165,11 @@ func (p *pdfImporter) appearanceAnnotation(ctx context.Context, page *pdfgo.Page
} }
converted.Parameters.Parameter = append(converted.Parameters.Parameter, AnnotationParameter{Name: "PDF.M", Value: modified}) converted.Parameters.Parameter = append(converted.Parameters.Parameter, AnnotationParameter{Name: "PDF.M", Value: modified})
} }
if _, err := p.editor.AddAnnotation(p.page, converted); err != nil { _, data, err := p.editor.prepareAnnotation(p.page, converted)
if err != nil {
return fmt.Errorf("PDF annotation: %w", err) return fmt.Errorf("PDF annotation: %w", err)
} }
p.annotationData = append(p.annotationData, bytes.TrimPrefix(data, []byte(xml.Header))...)
p.report.TextObjects += stamp.report.TextObjects p.report.TextObjects += stamp.report.TextObjects
p.report.PathObjects += stamp.report.PathObjects p.report.PathObjects += stamp.report.PathObjects
p.report.ImageObjects += stamp.report.ImageObjects p.report.ImageObjects += stamp.report.ImageObjects
@@ -183,7 +191,7 @@ func pdfAnnotationType(subtype pdfgo.Name) string {
return "Watermark" return "Watermark"
case "Highlight", "Underline", "Squiggly", "StrikeOut": case "Highlight", "Underline", "Squiggly", "StrikeOut":
return "Highlight" return "Highlight"
case "Text", "FreeText", "Line", "Square", "Circle", "Polygon", "PolyLine", "Caret", "Ink", "FileAttachment", "Sound", "Movie", "Screen", "Link", "Popup", "Widget", "PrinterMark", "TrapNet", "3D", "RichMedia": case "Text", "FreeText", "Line", "Square", "Circle", "Polygon", "PolyLine", "Caret", "Ink", "FileAttachment", "Sound", "Movie", "Screen", "Link", "Popup", "Widget", "PrinterMark", "TrapNet", "3D", "RichMedia", "Redact":
return "Path" return "Path"
} }
return "" return ""
+233 -48
View File
@@ -26,11 +26,13 @@ import (
// pdfCompositeNode 保留局部合成所需的原始图元和透明组层级 // pdfCompositeNode 保留局部合成所需的原始图元和透明组层级
type pdfCompositeNode struct { type pdfCompositeNode struct {
path *pdfgo.PathMark path *pdfgo.PathMark
text *pdfgo.TextMark text *pdfgo.TextMark
image *pdfgo.ImageMark image *pdfgo.ImageMark
group *pdfgo.GroupMark group *pdfgo.GroupMark
children []pdfCompositeNode children []pdfCompositeNode
textObject *pdfgo.TextObject
glyph *pdfgo.TextMark
} }
// pdfCompositePixel 保存非预乘源空间分量及累计透明度 // pdfCompositePixel 保存非预乘源空间分量及累计透明度
@@ -43,13 +45,16 @@ type pdfCompositePixel struct {
// pdfCompositor 在指定局部区域中按源颜色空间合成PDF图元 // pdfCompositor 在指定局部区域中按源颜色空间合成PDF图元
type pdfCompositor struct { type pdfCompositor struct {
importer *pdfImporter importer *pdfImporter
box Box box Box
width, height int width, height int
inverse pdfgo.Matrix inverse pdfgo.Matrix
cache *pdfCompositeCache cache *pdfCompositeCache
masks map[*pdfgo.SoftMask][]float64 masks map[*pdfgo.SoftMask][]float64
meshes map[pdfMeshKey][]pdfMeshPixel meshes map[pdfMeshKey][]pdfShadingPixel
gradients map[pdfGradientKey][]pdfShadingPixel
transfers []*pdfgo.TransferFunction
transferBlocked bool
} }
// pdfCompositeKey 区分图元的填充与描边几何 // pdfCompositeKey 区分图元的填充与描边几何
@@ -68,11 +73,16 @@ type pdfCompositeGeometry struct {
// pdfCompositeCache 在单页内复用几何、图像分量及蒙版内容 // pdfCompositeCache 在单页内复用几何、图像分量及蒙版内容
type pdfCompositeCache struct { type pdfCompositeCache struct {
geometry map[pdfCompositeKey]pdfCompositeGeometry geometry map[pdfCompositeKey]pdfCompositeGeometry
images map[*pdfgo.Image]*pdfgo.ImageComponents images map[*pdfgo.Image]*pdfgo.ImageComponents
masks map[*pdfgo.SoftMask][]pdfCompositeNode imageMatrix map[*pdfgo.ImageMark]pdfgo.Matrix
meshes map[*pdfgo.MeshGradient][]pdfMeshTriangle masks map[*pdfgo.SoftMask][]pdfCompositeNode
patterns map[pdfgo.Paint]*pdfCompositePattern meshes map[*pdfgo.MeshGradient][]pdfMeshTriangle
patterns map[pdfgo.Paint]*pdfCompositePattern
shadings map[pdfGradientKey]pdfCompositeGeometry
transfers map[pdfTransferKey]*pdfgo.TransferFunction
textDisjoint map[*pdfgo.GroupMark]bool
groups map[*pdfgo.GroupMark]Box
} }
// compositingCache 按需创建单页合成缓存,页面切换时由导入器释放 // compositingCache 按需创建单页合成缓存,页面切换时由导入器释放
@@ -95,6 +105,21 @@ func (c *pdfCompositor) coverage(node pdfCompositeNode, stroke bool) (image.Imag
return nil, nil return nil, nil
} }
} }
geometry, err := c.geometry(node, stroke)
if err != nil {
return nil, err
}
b := geometry.box
if geometry.scene == nil || b.X >= c.box.X+c.box.W || b.Y >= c.box.Y+c.box.H || b.X+b.W <= c.box.X || b.Y+b.H <= c.box.Y {
return nil, nil
}
return c.importer.renderGroupScene(geometry.scene, c.box)
}
// geometry 缓存图元或组的实际覆盖场景与像素对齐边界
// 入参: node 图元或组, stroke 是否描边
// 返回: pdfCompositeGeometry 覆盖场景, error 编译错误
func (c *pdfCompositor) geometry(node pdfCompositeNode, stroke bool) (pdfCompositeGeometry, error) {
key := pdfCompositeKey{path: node.path, text: node.text, image: node.image, stroke: stroke} key := pdfCompositeKey{path: node.path, text: node.text, image: node.image, stroke: stroke}
geometry, ok := c.cache.geometry[key] geometry, ok := c.cache.geometry[key]
if !ok { if !ok {
@@ -102,15 +127,82 @@ func (c *pdfCompositor) coverage(node pdfCompositeNode, stroke bool) (image.Imag
var err error var err error
geometry.scene, geometry.box, err = c.importer.compileGroup(func(local *pdfImporter) error { return node.coverage(local, stroke) }) geometry.scene, geometry.box, err = c.importer.compileGroup(func(local *pdfImporter) error { return node.coverage(local, stroke) })
if err != nil { if err != nil {
return nil, err return geometry, err
} }
c.cache.geometry[key] = geometry c.cache.geometry[key] = geometry
} }
b := geometry.box return geometry, nil
if geometry.scene == nil || b.X >= c.box.X+c.box.W || b.Y >= c.box.Y+c.box.H || b.X+b.W <= c.box.X || b.Y+b.H <= c.box.Y { }
return nil, nil
// groupBounds 缓存组内实际覆盖范围,外部字体沿用描述符保守边界
// 入参: node 透明组
// 返回: Box 组覆盖范围, error 几何编译错误
func (c *pdfCompositor) groupBounds(node pdfCompositeNode) (Box, error) {
if box, found := c.cache.groups[node.group]; found {
return box, nil
} }
return c.importer.renderGroupScene(geometry.scene, c.box) var box Box
for _, child := range node.children {
if err := c.importer.ctx.Err(); err != nil {
return Box{}, err
}
if child.group != nil {
bounds, err := c.groupBounds(child)
if err != nil {
return Box{}, err
}
box = unionTextBox(box, bounds)
continue
}
bounds, err := c.markBounds(child)
if err != nil {
return Box{}, err
}
box = unionTextBox(box, bounds)
}
if c.cache.groups == nil {
c.cache.groups = make(map[*pdfgo.GroupMark]Box)
}
c.cache.groups[node.group] = box
return box, nil
}
// markBounds 合并图元的填充与描边范围,外部字体不在排除阶段加载
// 入参: node 基本图元
// 返回: Box 图元覆盖范围, error 几何编译错误
func (c *pdfCompositor) markBounds(node pdfCompositeNode) (Box, error) {
_, fill, stroke := node.style()
var box Box
for _, outline := range []bool{false, true} {
if outline && !stroke || !outline && !fill {
continue
}
if node.text != nil && len(node.text.Font.Program) == 0 && node.text.Font.BoundingBox != nil {
box = unionTextBox(box, c.importer.compositeTextBounds(*node.text, outline))
continue
}
geometry, err := c.geometry(node, outline)
if err != nil {
return Box{}, err
}
box = unionTextBox(box, geometry.box)
}
return box, nil
}
// pixelBounds 将页面范围映射到当前块,保留一像素余量供浮点边界与抗锯齿使用
// 入参: box 页面范围
// 返回: image.Rectangle 当前块内的像素范围
func (c *pdfCompositor) pixelBounds(box Box) image.Rectangle {
if box.W <= 0 || box.H <= 0 {
return image.Rectangle{}
}
step := 25.4 / c.importer.rasterDPI
x0 := int(math.Max(0, math.Min(float64(c.width), math.Floor((box.X-c.box.X)/step)-1)))
y0 := int(math.Max(0, math.Min(float64(c.height), math.Floor((box.Y-c.box.Y)/step)-1)))
x1 := int(math.Max(0, math.Min(float64(c.width), math.Ceil((box.X+box.W-c.box.X)/step)+1)))
y1 := int(math.Max(0, math.Min(float64(c.height), math.Ceil((box.Y+box.H-c.box.Y)/step)+1)))
return image.Rect(x0, y0, x1, y1)
} }
// compositeTextBounds 按字体描述符计算保守边界,仅用于排除不相交的外部字体 // compositeTextBounds 按字体描述符计算保守边界,仅用于排除不相交的外部字体
@@ -158,35 +250,59 @@ func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error) ([]p
if err := p.halftone(mark.Style); err != nil { if err := p.halftone(mark.Style); err != nil {
return err return err
} }
if err := p.resolveTransfer(&mark.Style); err != nil {
return err
}
nodes = append(nodes, pdfCompositeNode{path: &mark}) nodes = append(nodes, pdfCompositeNode{path: &mark})
return nil return nil
} }
v.Text = func(mark pdfgo.TextMark) error { v.Text = func(mark pdfgo.TextMark) error {
if err := p.halftone(mark.Style); err != nil { var text []pdfCompositeNode
return err
}
if mark.Font.Subtype != "Type3" { if mark.Font.Subtype != "Type3" {
nodes = append(nodes, pdfCompositeNode{text: &mark}) if err := p.halftone(mark.Style); err != nil {
return nil
}
if mark.Clip != nil {
return &pdfgo.UnsupportedError{Feature: "Type3 text clipping"}
}
glyphs := active[mark.Font]
if glyphs == nil {
glyphs = make(map[string]bool)
active[mark.Font] = glyphs
}
for index, glyph := range mark.Glyphs {
if glyphs[glyph.Name] {
return fmt.Errorf("recursive PDF Type3 glyph %q", glyph.Name)
}
glyphs[glyph.Name] = true
err := p.reader.WalkType3Glyph(p.ctx, mark, index, v)
delete(glyphs, glyph.Name)
if err != nil {
return err return err
} }
if err := p.resolveTransfer(&mark.Style); err != nil {
return err
}
if len(mark.Glyphs) > 1 && (mark.Object != nil && !mark.Object.Knockout || mark.Style.AlphaIsShape || mark.Style.Fill.Alpha != 1 || mark.Style.Stroke.Alpha != 1 || mark.Style.SoftMask != nil || !pdfNormalBlend(mark.Style.BlendMode) || mark.Mode%4 == 1 || mark.Mode%4 == 2) {
for i := range mark.Glyphs {
glyph := mark
glyph.Glyphs, glyph.Positions, glyph.Clip = mark.Glyphs[i:i+1], mark.Positions[i:i+1], nil
text = append(text, pdfCompositeNode{text: &glyph})
}
} else {
text = append(text, pdfCompositeNode{text: &mark})
}
} else {
glyphs := active[mark.Font]
if glyphs == nil {
glyphs = make(map[string]bool)
active[mark.Font] = glyphs
}
for index, glyph := range mark.Glyphs {
if glyphs[glyph.Name] {
return fmt.Errorf("recursive PDF Type3 glyph %q", glyph.Name)
}
glyphs[glyph.Name] = true
children, err := collect(func(visitor pdfgo.Visitor) error { return p.reader.WalkType3Glyph(p.ctx, mark, index, visitor) })
delete(glyphs, glyph.Name)
if err != nil {
return err
}
part := mark
part.Glyphs, part.Positions = mark.Glyphs[index:index+1], mark.Positions[index:index+1]
text = append(text, pdfCompositeNode{group: &pdfgo.GroupMark{Alpha: 1}, children: children, glyph: &part})
}
}
if mark.Object != nil && mark.Object.Knockout {
if len(nodes) == 0 || nodes[len(nodes)-1].textObject != mark.Object {
nodes = append(nodes, pdfCompositeNode{group: &pdfgo.GroupMark{Alpha: 1, Knockout: true}, textObject: mark.Object})
}
node := &nodes[len(nodes)-1]
node.children = append(node.children, text...)
} else {
nodes = append(nodes, text...)
} }
return nil return nil
} }
@@ -194,6 +310,9 @@ func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error) ([]p
if err := p.halftone(mark.Style); err != nil { if err := p.halftone(mark.Style); err != nil {
return err return err
} }
if err := p.resolveTransfer(&mark.Style); err != nil {
return err
}
nodes = append(nodes, pdfCompositeNode{image: &mark}) nodes = append(nodes, pdfCompositeNode{image: &mark})
return nil return nil
} }
@@ -236,7 +355,24 @@ func (p *pdfImporter) compositePage(space *pdfgo.ColorSpace, walk func(pdfgo.Vis
} }
} }
p.compositeNodes = nil p.compositeNodes = nil
p.transferBackdrop = false
p.compositeSpace = space p.compositeSpace = space
if pdfCompositeHasTransfer(nodes) {
if p.warning == nil {
return &pdfgo.UnsupportedError{Feature: "device transfer function rasterization"}
}
if overprint, err := p.processOverprint(nodes); err != nil {
return err
} else if overprint {
return &pdfgo.UnsupportedError{Feature: "per-colorant transfer function overprinting"}
}
if err := p.compositeRegion(nil, pdfCompositeNode{group: &pdfgo.GroupMark{Alpha: 1, ColorSpace: space}, children: nodes}, space, true); err != nil {
return err
}
p.compositeNodes = nodes
p.transferBackdrop = true
return nil
}
return p.compositeObjects(nodes) return p.compositeObjects(nodes)
} }
@@ -250,6 +386,17 @@ func (p *pdfImporter) compositeObjects(nodes []pdfCompositeNode) error {
if err != nil { if err != nil {
return fmt.Errorf("inspect graphic %d: %w", i+1, err) return fmt.Errorf("inspect graphic %d: %w", i+1, err)
} }
transfer := pdfCompositeHasTransfer([]pdfCompositeNode{node})
if p.transferBackdrop || transfer {
direct = false
for _, source := range [][]pdfCompositeNode{p.compositeNodes, {node}} {
if overprint, err := p.processOverprint(source); err != nil {
return err
} else if overprint {
return &pdfgo.UnsupportedError{Feature: "per-colorant transfer function overprinting"}
}
}
}
if direct { if direct {
if err := node.emit(p.visitor()); err != nil { if err := node.emit(p.visitor()); err != nil {
return fmt.Errorf("convert graphic %d: %w", i+1, err) return fmt.Errorf("convert graphic %d: %w", i+1, err)
@@ -266,6 +413,7 @@ func (p *pdfImporter) compositeObjects(nodes []pdfCompositeNode) error {
} }
} }
p.compositeNodes = append(p.compositeNodes, node) p.compositeNodes = append(p.compositeNodes, node)
p.transferBackdrop = p.transferBackdrop || transfer
} }
return p.flushPath() return p.flushPath()
} }
@@ -276,7 +424,7 @@ func (p *pdfImporter) compositeObjects(nodes []pdfCompositeNode) error {
func (n pdfCompositeNode) opaque(space *pdfgo.ColorSpace) bool { func (n pdfCompositeNode) opaque(space *pdfgo.ColorSpace) bool {
if n.group != nil { if n.group != nil {
g := n.group g := n.group
if g.Knockout || g.Alpha != 1 || g.SoftMask != nil || !pdfNormalBlend(g.BlendMode) || g.ColorSpace != nil && !g.ColorSpace.Equal(space) { if g.Knockout && n.textObject == nil || g.Alpha != 1 || g.SoftMask != nil || !pdfNormalBlend(g.BlendMode) || g.ColorSpace != nil && !g.ColorSpace.Equal(space) {
return false return false
} }
for _, child := range n.children { for _, child := range n.children {
@@ -309,7 +457,8 @@ func (n pdfCompositeNode) opaque(space *pdfgo.ColorSpace) bool {
// 返回: bool 是否可直接转换 // 返回: bool 是否可直接转换
func (n pdfCompositeNode) direct() bool { func (n pdfCompositeNode) direct() bool {
if g := n.group; g != nil { if g := n.group; g != nil {
if g.Knockout || !pdfNormalBlend(g.BlendMode) || g.ColorSpace != nil && !g.ColorSpace.SRGBEquivalent() { single := n.textObject != nil && len(n.children) == 1 && (n.children[0].glyph != nil || n.children[0].text != nil && len(n.children[0].text.Glyphs) == 1)
if g.Knockout && !single || !pdfNormalBlend(g.BlendMode) || g.ColorSpace != nil && !g.ColorSpace.SRGBEquivalent() {
return false return false
} }
for _, child := range n.children { for _, child := range n.children {
@@ -320,6 +469,9 @@ func (n pdfCompositeNode) direct() bool {
return true return true
} }
s, fill, stroke := n.style() s, fill, stroke := n.style()
if fill && stroke && (s.Fill.Alpha != 1 || s.Stroke.Alpha != 1 || s.SoftMask != nil) {
return false
}
if n.image != nil && s.FillOverprint { if n.image != nil && s.FillOverprint {
return false return false
} }
@@ -352,6 +504,14 @@ func (n pdfCompositeNode) emit(visitor pdfgo.Visitor) error {
if n.image != nil { if n.image != nil {
return visitor.Image(*n.image) return visitor.Image(*n.image)
} }
if n.textObject != nil || n.glyph != nil {
for _, child := range n.children {
if err := child.emit(visitor); err != nil {
return err
}
}
return nil
}
return visitor.Group(*n.group, func(v pdfgo.Visitor) error { return visitor.Group(*n.group, func(v pdfgo.Visitor) error {
for _, child := range n.children { for _, child := range n.children {
if err := child.emit(v); err != nil { if err := child.emit(v); err != nil {
@@ -391,6 +551,7 @@ func (n pdfCompositeNode) coverage(p *pdfImporter, stroke bool) error {
style, _, _ := n.style() style, _, _ := n.style()
style.Fill, style.Stroke = pdfgo.Paint{RGB: [3]float64{1, 1, 1}, Alpha: 1}, pdfgo.Paint{RGB: [3]float64{1, 1, 1}, Alpha: 1} style.Fill, style.Stroke = pdfgo.Paint{RGB: [3]float64{1, 1, 1}, Alpha: 1}, pdfgo.Paint{RGB: [3]float64{1, 1, 1}, Alpha: 1}
style.SoftMask, style.BlendMode = nil, "Normal" style.SoftMask, style.BlendMode = nil, "Normal"
style.Transfer = nil
style.FillOverprint, style.StrokeOverprint = false, false style.FillOverprint, style.StrokeOverprint = false, false
if n.path != nil { if n.path != nil {
mark := *n.path mark := *n.path
@@ -452,11 +613,21 @@ func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompo
cache := p.compositingCache() cache := p.compositingCache()
step := 25.4 / p.rasterDPI step := 25.4 / p.rasterDPI
buffer := make([]pdfCompositePixel, min(256, w)*min(256, h)) buffer := make([]pdfCompositePixel, min(256, w)*min(256, h))
var transfers []*pdfgo.TransferFunction
var previousTransfers []*pdfgo.TransferFunction
if pdfCompositeHasTransfer(backdrop) || pdfCompositeHasTransfer([]pdfCompositeNode{node}) {
transfers = make([]*pdfgo.TransferFunction, len(buffer))
previousTransfers = make([]*pdfgo.TransferFunction, len(buffer))
}
for y := 0; y < h; y += 256 { for y := 0; y < h; y += 256 {
for x := 0; x < w; x += 256 { for x := 0; x < w; x += 256 {
runtime.Gosched() runtime.Gosched()
width, height := min(256, w-x), min(256, h-y) width, height := min(256, w-x), min(256, h-y)
c := pdfCompositor{importer: p, box: Box{X: box.X + float64(x)*step, Y: box.Y + float64(y)*step, W: float64(width) * step, H: float64(height) * step}, width: width, height: height, inverse: inverse, cache: cache, masks: map[*pdfgo.SoftMask][]float64{}} c := pdfCompositor{importer: p, box: Box{X: box.X + float64(x)*step, Y: box.Y + float64(y)*step, W: float64(width) * step, H: float64(height) * step}, width: width, height: height, inverse: inverse, cache: cache, masks: map[*pdfgo.SoftMask][]float64{}}
if transfers != nil {
c.transfers = transfers[:width*height]
clear(c.transfers)
}
pixels := buffer[:width*height] pixels := buffer[:width*height]
clear(pixels) clear(pixels)
if err := c.draw(backdrop, pixels, space); err != nil { if err := c.draw(backdrop, pixels, space); err != nil {
@@ -465,14 +636,21 @@ func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompo
for i := range pixels { for i := range pixels {
pixels[i].effect = 0 pixels[i].effect = 0
} }
copy(previousTransfers, c.transfers)
if err := c.draw([]pdfCompositeNode{node}, pixels, space); err != nil { if err := c.draw([]pdfCompositeNode{node}, pixels, space); err != nil {
return err return err
} }
for i, pixel := range pixels { for i, pixel := range pixels {
if pixel.effect == 0 { if pixel.effect == 0 && (c.transfers == nil || c.transfers[i] == previousTransfers[i]) {
continue continue
} }
rgb, err := pdfCompositeOutputColor(space, pixel.values) var rgb [3]float64
var err error
if c.transfers != nil {
rgb, err = space.RGB(pixel.values[:space.Components()], "RelativeColorimetric")
} else {
rgb, err = pdfCompositeOutputColor(space, pixel.values)
}
if err != nil { if err != nil {
return err return err
} }
@@ -483,6 +661,13 @@ func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompo
} }
alpha = 1 alpha = 1
} }
if c.transfers != nil && c.transfers[i] != nil {
values, err := c.transfers[i].Apply(rgb[:], "DeviceRGB", "")
if err != nil {
return err
}
copy(rgb[:], values[:3])
}
output.SetNRGBA(x+i%width, y+i/width, color.NRGBA{R: uint8(math.Round(rgb[0] * 255)), G: uint8(math.Round(rgb[1] * 255)), B: uint8(math.Round(rgb[2] * 255)), A: uint8(math.Round(alpha * 255))}) output.SetNRGBA(x+i%width, y+i/width, color.NRGBA{R: uint8(math.Round(rgb[0] * 255)), G: uint8(math.Round(rgb[1] * 255)), B: uint8(math.Round(rgb[2] * 255)), A: uint8(math.Round(alpha * 255))})
} }
} }
+220 -46
View File
@@ -46,6 +46,10 @@ func (c *pdfCompositor) draw(nodes []pdfCompositeNode, pixels []pdfCompositePixe
// 入参: node 图元, pixels 背景与输出像素, space 混合空间 // 入参: node 图元, pixels 背景与输出像素, space 混合空间
// 返回: error 几何或颜色错误 // 返回: error 几何或颜色错误
func (c *pdfCompositor) drawMark(node pdfCompositeNode, pixels []pdfCompositePixel, space *pdfgo.ColorSpace) error { func (c *pdfCompositor) drawMark(node pdfCompositeNode, pixels []pdfCompositePixel, space *pdfgo.ColorSpace) error {
box, err := c.markBounds(node)
if err != nil || c.pixelBounds(box).Empty() {
return err
}
style, fill, stroke := node.style() style, fill, stroke := node.style()
mask, err := c.mask(style.SoftMask, space) mask, err := c.mask(style.SoftMask, space)
if err != nil { if err != nil {
@@ -84,7 +88,7 @@ func (c *pdfCompositor) drawMark(node pdfCompositeNode, pixels []pdfCompositePix
} }
} }
if grouped { if grouped {
return pdfCompositeGroup(pixels, initial, result, space, space, style.Fill.Alpha, mask, style.BlendMode) return pdfCompositeGroup(pixels, initial, result, space, space, style.Fill.Alpha, mask, style.BlendMode, style.RenderingIntent, style.ColorConversion)
} }
return nil return nil
} }
@@ -98,17 +102,36 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
return err return err
} }
style, _, _ := node.style() style, _, _ := node.style()
var processMask [4]bool var transfer *pdfgo.TransferFunction
processOverprint := false imageShape := false
if node.image != nil && overprint && space.Model == "DeviceCMYK" && !space.Calibrated() { if c.transfers != nil {
processMask, processOverprint, err = pdfImageProcessColorants(node.image.Image) transfer, err = c.selectTransfer(node, outline)
if err != nil { if err != nil {
return err return err
} }
if node.image != nil && (node.image.Image.ImageMask || node.image.Image.Mask != nil) {
soft, err := node.image.Image.HasSoftMask()
if err != nil {
return err
}
imageShape = !soft
}
}
var processMask [4]bool
processOverprint := false
if overprint && space.Model == "DeviceCMYK" && !space.Calibrated() {
if node.image != nil {
processMask, processOverprint, err = pdfImageProcessColorants(node.image.Image)
if err != nil {
return err
}
} else {
processMask, processOverprint = paint.Process.CMYKMask()
}
} }
compatible := overprint && style.OverprintMode == 1 && node.image == nil && space.Model == "DeviceCMYK" && !space.Calibrated() && (paint.CMYK != nil || paint.Space != nil && paint.Space.Model == "DeviceCMYK" && !paint.Space.Calibrated())
step := 25.4 / c.importer.rasterDPI step := 25.4 / c.importer.rasterDPI
uniform := node.image == nil && paint.Axial == nil && paint.Radial == nil && paint.Tiling == nil && paint.Mesh == nil uniform := node.image == nil && paint.Axial == nil && paint.Radial == nil && paint.Function == nil && paint.Tiling == nil && paint.Mesh == nil
compatible := overprint && !processOverprint && style.OverprintMode == 1 && uniform && paint.SourceSpace == "DeviceCMYK" && space.Model == "DeviceCMYK" && !space.Calibrated()
var pattern []pdfCompositePixel var pattern []pdfCompositePixel
if paint.Tiling != nil && (node.image == nil || node.image.Image.ImageMask) { if paint.Tiling != nil && (node.image == nil || node.image.Image.ImageMask) {
backdrop := pixels backdrop := pixels
@@ -120,26 +143,37 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
return err return err
} }
} }
var mesh []pdfMeshPixel var shading []pdfShadingPixel
if paint.Mesh != nil { if paint.Mesh != nil && (node.image == nil || node.image.Image.ImageMask) {
mesh, err = c.mesh(paint.Mesh, space) shading, err = c.mesh(paint.Mesh, space, style.ColorConversion)
if err != nil {
return err
}
} else if (paint.Axial != nil || paint.Radial != nil || paint.Function != nil) && (node.image == nil || node.image.Image.ImageMask) {
shading, err = c.gradient(paint, space, &node, outline)
if err != nil { if err != nil {
return err return err
} }
} }
var uniformValues [4]float64 var uniformValues [4]float64
uniformReady := false uniformReady := false
for y := 0; y < c.height; y++ { geometry, err := c.geometry(node, outline)
if err != nil {
return err
}
region := c.pixelBounds(geometry.box)
for y := region.Min.Y; y < region.Max.Y; y++ {
if err := c.importer.ctx.Err(); err != nil { if err := c.importer.ctx.Err(); err != nil {
return err return err
} }
for x := 0; x < c.width; x++ { for x := region.Min.X; x < region.Max.X; x++ {
_, _, _, a := coverage.At(x, y).RGBA() _, _, _, a := coverage.At(x, y).RGBA()
if a == 0 { if a == 0 {
continue continue
} }
index := y*c.width + x index := y*c.width + x
shape, opacity := float64(a)/65535, paint.Alpha shape, opacity := float64(a)/65535, paint.Alpha
selectionShape := 1.0
if mask != nil { if mask != nil {
opacity *= mask[index] opacity *= mask[index]
} }
@@ -154,15 +188,31 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
if node.image != nil { if node.image != nil {
point := c.inverse.Apply(pdfgo.Point{X: c.box.X + (float64(x)+.5)*step, Y: c.box.Y + (float64(y)+.5)*step}) point := c.inverse.Apply(pdfgo.Point{X: c.box.X + (float64(x)+.5)*step, Y: c.box.Y + (float64(y)+.5)*step})
var alpha float64 var alpha float64
_, alpha, err = c.imageColor(*node.image, point, space) _, alpha, err = c.imageColor(node.image, point, space)
if imageShape {
selectionShape = alpha
}
opacity *= alpha
}
} else if shading != nil {
values = shading[index].values
if paint.Mesh == nil {
shape = shading[index].shape
} else {
shape *= shading[index].shape
}
if node.image != nil && paint.Mesh != nil {
point := c.inverse.Apply(pdfgo.Point{X: c.box.X + (float64(x)+.5)*step, Y: c.box.Y + (float64(y)+.5)*step})
var alpha float64
_, alpha, err = c.imageColor(node.image, point, space)
if imageShape {
selectionShape = alpha
}
opacity *= alpha opacity *= alpha
} }
} else if mesh != nil {
values = mesh[index].values
shape *= mesh[index].shape
} else if uniform { } else if uniform {
if !uniformReady { if !uniformReady {
uniformValues, _, err = pdfCompositeColor(paint, pdfgo.Point{}, space, style.RenderingIntent) uniformValues, _, err = pdfCompositeColor(paint, pdfgo.Point{}, space, style.RenderingIntent, style.ColorConversion)
uniformReady = true uniformReady = true
} }
values = uniformValues values = uniformValues
@@ -170,11 +220,14 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
point := c.inverse.Apply(pdfgo.Point{X: c.box.X + (float64(x)+.5)*step, Y: c.box.Y + (float64(y)+.5)*step}) point := c.inverse.Apply(pdfgo.Point{X: c.box.X + (float64(x)+.5)*step, Y: c.box.Y + (float64(y)+.5)*step})
if node.image != nil { if node.image != nil {
var alpha float64 var alpha float64
values, alpha, err = c.imageColor(*node.image, point, space) values, alpha, err = c.imageColor(node.image, point, space)
if imageShape {
selectionShape = alpha
}
opacity *= alpha opacity *= alpha
} else { } else {
var visible bool var visible bool
values, visible, err = pdfCompositeColor(paint, point, space, style.RenderingIntent) values, visible, err = pdfCompositeColor(paint, point, space, style.RenderingIntent, style.ColorConversion)
if !visible { if !visible {
shape = 0 shape = 0
} }
@@ -212,6 +265,9 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
} }
} }
pixels[index].shape += shape * (1 - pixels[index].shape) pixels[index].shape += shape * (1 - pixels[index].shape)
if c.transfers != nil && shape != 0 && selectionShape != 0 {
c.transfers[index] = transfer
}
} }
} }
return nil return nil
@@ -235,10 +291,35 @@ func pdfCompositeInterpolate(target *pdfCompositePixel, source pdfCompositePixel
// 返回: error 颜色或合成错误 // 返回: error 颜色或合成错误
func (c *pdfCompositor) drawGroup(node pdfCompositeNode, pixels []pdfCompositePixel, parent *pdfgo.ColorSpace) error { func (c *pdfCompositor) drawGroup(node pdfCompositeNode, pixels []pdfCompositePixel, parent *pdfgo.ColorSpace) error {
g := node.group g := node.group
b, err := c.groupBounds(node)
if err != nil {
return err
}
if b.W <= 0 || b.H <= 0 || b.X >= c.box.X+c.box.W || b.Y >= c.box.Y+c.box.H || b.X+b.W <= c.box.X || b.Y+b.H <= c.box.Y {
return nil
}
blocked := c.transferBlocked
c.transferBlocked = blocked || g.Alpha != 1 || g.SoftMask != nil || !pdfNormalBlend(g.BlendMode)
defer func() { c.transferBlocked = blocked }()
if c.transfers != nil && node.textObject != nil && !c.transferBlocked {
for _, child := range node.children {
opaque, err := c.importer.transferOpaque(child)
if err != nil {
return err
}
if !opaque {
c.transferBlocked = true
break
}
}
}
space := g.ColorSpace space := g.ColorSpace
if space == nil { if space == nil {
space = parent space = parent
} }
if !g.Isolated && !g.Knockout && g.Alpha == 1 && g.SoftMask == nil && pdfNormalBlend(g.BlendMode) && space.Equal(parent) {
return c.draw(node.children, pixels, space)
}
initial := make([]pdfCompositePixel, len(pixels)) initial := make([]pdfCompositePixel, len(pixels))
if !g.Isolated { if !g.Isolated {
sameSpace := space.Equal(parent) sameSpace := space.Equal(parent)
@@ -277,7 +358,7 @@ func (c *pdfCompositor) drawGroup(node pdfCompositeNode, pixels []pdfCompositePi
} }
} }
} }
return pdfCompositeGroup(pixels, initial, result, space, parent, g.Alpha, mask, g.BlendMode) return pdfCompositeGroup(pixels, initial, result, space, parent, g.Alpha, mask, g.BlendMode, g.RenderingIntent, g.ColorConversion)
} }
// drawKnockout 将每个对象与组初始背景合成,再按对象形状替换先前对象 // drawKnockout 将每个对象与组初始背景合成,再按对象形状替换先前对象
@@ -286,12 +367,32 @@ func (c *pdfCompositor) drawGroup(node pdfCompositeNode, pixels []pdfCompositePi
func (c *pdfCompositor) drawKnockout(nodes []pdfCompositeNode, initial, pixels []pdfCompositePixel, space *pdfgo.ColorSpace) error { func (c *pdfCompositor) drawKnockout(nodes []pdfCompositeNode, initial, pixels []pdfCompositePixel, space *pdfgo.ColorSpace) error {
candidate := make([]pdfCompositePixel, len(initial)) candidate := make([]pdfCompositePixel, len(initial))
for _, node := range nodes { for _, node := range nodes {
copy(candidate, initial) var box Box
var err error
if node.group != nil {
box, err = c.groupBounds(node)
} else {
box, err = c.markBounds(node)
}
if err != nil {
return err
}
region := c.pixelBounds(box)
if region.Empty() {
continue
}
for y := region.Min.Y; y < region.Max.Y; y++ {
start, end := y*c.width+region.Min.X, y*c.width+region.Max.X
copy(candidate[start:end], initial[start:end])
}
if err := c.draw([]pdfCompositeNode{node}, candidate, space); err != nil { if err := c.draw([]pdfCompositeNode{node}, candidate, space); err != nil {
return err return err
} }
for i, source := range candidate { for y := region.Min.Y; y < region.Max.Y; y++ {
pdfCompositeKnockout(&pixels[i], initial[i], source) for x := region.Min.X; x < region.Max.X; x++ {
i := y*c.width + x
pdfCompositeKnockout(&pixels[i], initial[i], candidate[i])
}
} }
} }
return nil return nil
@@ -316,9 +417,9 @@ func pdfCompositeKnockout(target *pdfCompositePixel, initial, source pdfComposit
} }
// pdfCompositeGroup 移除组初始背景贡献后,将组结果合成到父空间 // pdfCompositeGroup 移除组初始背景贡献后,将组结果合成到父空间
// 入参: pixels 父组输出, initial 初始背景, result 组结果, space 组空间, parent 父空间, opacity 组不透明度, mask 蒙版, mode 混合模式 // 入参: pixels 父组输出, initial 初始背景, result 组结果, space 组空间, parent 父空间, opacity 组不透明度, mask 蒙版, mode 混合模式, intent 渲染意图, conversion 设备转换函数
// 返回: error 颜色或混合错误 // 返回: error 颜色或混合错误
func pdfCompositeGroup(pixels, initial, result []pdfCompositePixel, space, parent *pdfgo.ColorSpace, opacity float64, mask []float64, mode pdfgo.Name) error { func pdfCompositeGroup(pixels, initial, result []pdfCompositePixel, space, parent *pdfgo.ColorSpace, opacity float64, mask []float64, mode, intent pdfgo.Name, conversion pdfgo.ColorConversion) error {
sameSpace := space.Equal(parent) sameSpace := space.Equal(parent)
components := space.Components() components := space.Components()
for i, pixel := range result { for i, pixel := range result {
@@ -332,7 +433,7 @@ func pdfCompositeGroup(pixels, initial, result []pdfCompositePixel, space, paren
} }
if !sameSpace { if !sameSpace {
var err error var err error
values, err = parent.Convert(values[:components], space, "RelativeColorimetric") values, err = parent.ConvertWith(values[:components], space, intent, conversion)
if err != nil { if err != nil {
return err return err
} }
@@ -378,7 +479,9 @@ func (c *pdfCompositor) mask(mask *pdfgo.SoftMask, inherited *pdfgo.ColorSpace)
pixels[i].alpha = 1 pixels[i].alpha = 1
} }
} }
if err := c.draw(nodes, pixels, space); err != nil { local := *c
local.transfers = nil
if err := local.draw(nodes, pixels, space); err != nil {
return nil, err return nil, err
} }
result := make([]float64, len(pixels)) result := make([]float64, len(pixels))
@@ -402,7 +505,7 @@ func (c *pdfCompositor) mask(mask *pdfgo.SoftMask, inherited *pdfgo.ColorSpace)
// imageColor 采样映射后的原始图像分量,插值在源空间内进行 // imageColor 采样映射后的原始图像分量,插值在源空间内进行
// 入参: mark 图像, point 页面坐标, space 混合空间 // 入参: mark 图像, point 页面坐标, space 混合空间
// 返回: [4]float64 源颜色, float64 图像透明度, error 解码或变换错误 // 返回: [4]float64 源颜色, float64 图像透明度, error 解码或变换错误
func (c *pdfCompositor) imageColor(mark pdfgo.ImageMark, point pdfgo.Point, space *pdfgo.ColorSpace) ([4]float64, float64, error) { func (c *pdfCompositor) imageColor(mark *pdfgo.ImageMark, point pdfgo.Point, space *pdfgo.ColorSpace) ([4]float64, float64, error) {
source := c.cache.images[mark.Image] source := c.cache.images[mark.Image]
if source == nil { if source == nil {
var err error var err error
@@ -412,18 +515,27 @@ func (c *pdfCompositor) imageColor(mark pdfgo.ImageMark, point pdfgo.Point, spac
} }
c.cache.images[mark.Image] = source c.cache.images[mark.Image] = source
} }
inverse, ok := mark.Matrix.Inverse() inverse, found := c.cache.imageMatrix[mark]
if !ok { if !found {
return [4]float64{}, 0, fmt.Errorf("invalid PDF image matrix") var ok bool
inverse, ok = mark.Matrix.Inverse()
if !ok {
return [4]float64{}, 0, fmt.Errorf("invalid PDF image matrix")
}
if c.cache.imageMatrix == nil {
c.cache.imageMatrix = make(map[*pdfgo.ImageMark]pdfgo.Matrix)
}
c.cache.imageMatrix[mark] = inverse
} }
point = inverse.Apply(point) point = inverse.Apply(point)
if point.X < 0 || point.X > 1 || point.Y < 0 || point.Y > 1 { if point.X < 0 || point.X > 1 || point.Y < 0 || point.Y > 1 {
return [4]float64{}, 0, nil return [4]float64{}, 0, nil
} }
x, y := point.X*float64(source.Rect.Dx())-.5, (1-point.Y)*float64(source.Rect.Dy())-.5 x, y := point.X*float64(source.Rect.Dx())-.5, (1-point.Y)*float64(source.Rect.Dy())-.5
_, nativeProcess := source.Process.CMYKMask()
process := mark.Style.FillOverprint && space.Model == "DeviceCMYK" && !space.Calibrated() && len(source.Colorants) > 0 process := mark.Style.FillOverprint && space.Model == "DeviceCMYK" && !space.Calibrated() && len(source.Colorants) > 0
var indices [4]int var indices [4]int
if process { if process && !nativeProcess {
for i, name := range source.Colorants { for i, name := range source.Colorants {
index := -1 index := -1
for j, known := range []pdfgo.Name{"Cyan", "Magenta", "Yellow", "Black"} { for j, known := range []pdfgo.Name{"Cyan", "Magenta", "Yellow", "Black"} {
@@ -441,7 +553,7 @@ func (c *pdfCompositor) imageColor(mark pdfgo.ImageMark, point pdfgo.Point, spac
sample := func(x, y int) ([4]float64, float64) { sample := func(x, y int) ([4]float64, float64) {
x, y = max(0, min(source.Rect.Dx()-1, x)), max(0, min(source.Rect.Dy()-1, y)) x, y = max(0, min(source.Rect.Dx()-1, x)), max(0, min(source.Rect.Dy()-1, y))
values, alpha := source.ValuesAt(x, y) values, alpha := source.ValuesAt(x, y)
if process { if process && !nativeProcess {
values = [4]float64{} values = [4]float64{}
for j, tint := range source.TintsAt(x, y) { for j, tint := range source.TintsAt(x, y) {
values[indices[j]] = float64(tint) / 65535 values[indices[j]] = float64(tint) / 65535
@@ -483,16 +595,16 @@ func (c *pdfCompositor) imageColor(mark pdfgo.ImageMark, point pdfgo.Point, spac
if mark.Style.Fill.Tiling != nil { if mark.Style.Fill.Tiling != nil {
return [4]float64{}, alpha * (1 - values[0]), nil return [4]float64{}, alpha * (1 - values[0]), nil
} }
color, visible, err := pdfCompositeColor(mark.Style.Fill, mark.Matrix.Apply(point), space, mark.Style.RenderingIntent) color, visible, err := pdfCompositeColor(mark.Style.Fill, mark.Matrix.Apply(point), space, mark.Style.RenderingIntent, mark.Style.ColorConversion)
if !visible { if !visible {
return color, 0, err return color, 0, err
} }
return color, alpha * (1 - values[0]), err return color, alpha * (1 - values[0]), err
} }
if process { if process || space.Equal(source.Space) {
return values, alpha, nil return values, alpha, nil
} }
values, err := space.Convert(values[:source.Space.Components()], source.Space, mark.Style.RenderingIntent) values, err := space.ConvertWith(values[:source.Space.Components()], source.Space, mark.Style.RenderingIntent, mark.Style.ColorConversion)
return values, alpha, err return values, alpha, err
} }
@@ -501,6 +613,13 @@ func (c *pdfCompositor) imageColor(mark pdfgo.ImageMark, point pdfgo.Point, spac
// 返回: [4]bool 参与绘制的四色通道, bool 是否全部属于印刷原色, error 色空间错误 // 返回: [4]bool 参与绘制的四色通道, bool 是否全部属于印刷原色, error 色空间错误
func pdfImageProcessColorants(image *pdfgo.Image) ([4]bool, bool, error) { func pdfImageProcessColorants(image *pdfgo.Image) ([4]bool, bool, error) {
var mask [4]bool var mask [4]bool
process, err := image.ProcessColorants()
if err != nil {
return mask, false, err
}
if mask, native := process.CMYKMask(); native {
return mask, true, nil
}
names, err := image.Colorants() names, err := image.Colorants()
if err != nil { if err != nil {
return mask, false, err return mask, false, err
@@ -525,27 +644,80 @@ func pdfImageProcessColorants(image *pdfgo.Image) ([4]bool, bool, error) {
} }
// pdfCompositeColor 将原始画刷变换到混合空间,渐变在源空间求值 // pdfCompositeColor 将原始画刷变换到混合空间,渐变在源空间求值
// 入参: paint 画刷, point 页面坐标, space 混合空间, intent 渲染意图 // 入参: paint 画刷, point 页面坐标, space 混合空间, intent 渲染意图, conversion 设备转换函数
// 返回: [4]float64 混合分量, bool 是否着色, error 颜色错误 // 返回: [4]float64 混合分量, bool 是否着色, error 颜色错误
func pdfCompositeColor(paint pdfgo.Paint, point pdfgo.Point, space *pdfgo.ColorSpace, intent pdfgo.Name) ([4]float64, bool, error) { func pdfCompositeColor(paint pdfgo.Paint, point pdfgo.Point, space *pdfgo.ColorSpace, intent pdfgo.Name, conversion pdfgo.ColorConversion) ([4]float64, bool, error) {
if paint.Tiling != nil { if paint.Tiling != nil {
return [4]float64{}, false, &pdfgo.UnsupportedError{Feature: "local tiling pattern compositing"} return [4]float64{}, false, &pdfgo.UnsupportedError{Feature: "local tiling pattern compositing"}
} }
var values [4]float64 var values [4]float64
var source *pdfgo.ColorSpace var source *pdfgo.ColorSpace
if paint.Axial != nil || paint.Radial != nil { if paint.Function != nil {
t, visible := pdfShadingPosition(paint, point) g := paint.Function
if !visible { if g.Bounds != nil {
return values, false, nil inverse, ok := g.PatternMatrix.Inverse()
if !ok {
return values, false, fmt.Errorf("singular function shading bounds transform")
}
local := inverse.Apply(point)
if local.X < g.Bounds.XMin || local.X > g.Bounds.XMax || local.Y < g.Bounds.YMin || local.Y > g.Bounds.YMax {
return values, false, nil
}
} }
var err error inverse, ok := g.Matrix.Inverse()
if !ok {
return values, false, fmt.Errorf("singular function shading transform")
}
local := inverse.Apply(point)
source, intent = g.Space, g.Intent
if local.X < g.Domain.XMin || local.X > g.Domain.XMax || local.Y < g.Domain.YMin || local.Y > g.Domain.YMax {
if g.Background == nil {
return values, false, nil
}
values = *g.Background
} else {
var err error
values, err = g.ValuesAt(local)
if err != nil {
return values, false, err
}
}
} else if paint.Axial != nil || paint.Radial != nil {
var background *[4]float64
var bounds *pdfgo.Rectangle
var matrix pdfgo.Matrix
if g := paint.Axial; g != nil { if g := paint.Axial; g != nil {
values, err = g.ValuesAt(t)
source, intent = g.Space, g.Intent source, intent = g.Space, g.Intent
background, bounds, matrix = g.Background, g.Bounds, g.Matrix
} else { } else {
g := paint.Radial g := paint.Radial
values, err = g.ValuesAt(t)
source, intent = g.Space, g.Intent source, intent = g.Space, g.Intent
background, bounds, matrix = g.Background, g.Bounds, g.Matrix
}
if bounds != nil {
local := point
if matrix != (pdfgo.Matrix{}) {
inverse, ok := matrix.Inverse()
if !ok {
return values, false, fmt.Errorf("singular shading transform")
}
local = inverse.Apply(point)
}
if local.X < bounds.XMin || local.X > bounds.XMax || local.Y < bounds.YMin || local.Y > bounds.YMax {
return values, false, nil
}
}
t, visible := pdfShadingPosition(paint, point)
var err error
if !visible {
if background == nil {
return values, false, nil
}
values = *background
} else if paint.Axial != nil {
values, err = paint.Axial.ValuesAt(t)
} else {
values, err = paint.Radial.ValuesAt(t)
} }
if err != nil { if err != nil {
return values, false, err return values, false, err
@@ -554,11 +726,13 @@ func pdfCompositeColor(paint pdfgo.Paint, point pdfgo.Point, space *pdfgo.ColorS
values, source = paint.Values, paint.Space values, source = paint.Values, paint.Space
} else if paint.CMYK != nil { } else if paint.CMYK != nil {
values, source = *paint.CMYK, &pdfgo.ColorSpace{Model: "DeviceCMYK"} values, source = *paint.CMYK, &pdfgo.ColorSpace{Model: "DeviceCMYK"}
} else if paint.SourceSpace == "DeviceGray" {
values[0], source = paint.RGB[0], &pdfgo.ColorSpace{Model: "DeviceGray"}
} else { } else {
copy(values[:], paint.RGB[:]) copy(values[:], paint.RGB[:])
source = &pdfgo.ColorSpace{Model: "DeviceRGB"} source = &pdfgo.ColorSpace{Model: "DeviceRGB"}
} }
values, err := space.Convert(values[:source.Components()], source, intent) values, err := space.ConvertWith(values[:source.Components()], source, intent, conversion)
return values, true, err return values, true, err
} }
+112 -6
View File
@@ -23,9 +23,11 @@ import (
// pdfCompositePattern 保存图案单元的原始图元和独立几何缓存 // pdfCompositePattern 保存图案单元的原始图元和独立几何缓存
type pdfCompositePattern struct { type pdfCompositePattern struct {
nodes []pdfCompositeNode nodes []pdfCompositeNode
local pdfImporter local pdfImporter
box Box box Box
transferChecked bool
transferOpaque bool
} }
// tiling 将图案作为非隔离组绘制,保留背景贡献及单元间隙 // tiling 将图案作为非隔离组绘制,保留背景贡献及单元间隙
@@ -59,6 +61,9 @@ func (c *pdfCompositor) tiling(paint pdfgo.Paint, backdrop []pdfCompositePixel,
for i, pixel := range backdrop { for i, pixel := range backdrop {
result[i] = pdfCompositePixel{values: pixel.values, alpha: pixel.alpha} result[i] = pdfCompositePixel{values: pixel.values, alpha: pixel.alpha}
} }
candidate := make([]pdfCompositePixel, len(backdrop))
disjoint := source.BBox.XMax-source.BBox.XMin <= xstep && source.BBox.YMax-source.BBox.YMin <= ystep
pixelStep := 25.4 / c.importer.rasterDPI
localInverse, _ := pattern.local.matrix.Inverse() localInverse, _ := pattern.local.matrix.Inverse()
for row := int(top); row <= int(bottom); row++ { for row := int(top); row <= int(bottom); row++ {
for column := int(left); column <= int(right); column++ { for column := int(left); column <= int(right); column++ {
@@ -67,11 +72,36 @@ func (c *pdfCompositor) tiling(paint pdfgo.Paint, backdrop []pdfCompositePixel,
} }
x, y := float64(column)*xstep, float64(row)*ystep x, y := float64(column)*xstep, float64(row)*ystep
dx, dy := matrix[0]*x+matrix[2]*y+pattern.box.X, matrix[1]*x+matrix[3]*y+pattern.box.Y dx, dy := matrix[0]*x+matrix[2]*y+pattern.box.X, matrix[1]*x+matrix[3]*y+pattern.box.Y
cell := pdfCompositor{importer: &pattern.local, box: Box{X: c.box.X - dx, Y: c.box.Y - dy, W: c.box.W, H: c.box.H}, left := int(math.Max(0, math.Min(float64(c.width), math.Floor((dx-c.box.X)/pixelStep)-1)))
width: c.width, height: c.height, inverse: localInverse, cache: pattern.local.compositingCache(), masks: map[*pdfgo.SoftMask][]float64{}} right := int(math.Max(0, math.Min(float64(c.width), math.Ceil((dx+pattern.box.W-c.box.X)/pixelStep)+1)))
if err := cell.draw(pattern.nodes, result, space); err != nil { top := int(math.Max(0, math.Min(float64(c.height), math.Floor((dy-c.box.Y)/pixelStep)-1)))
bottom := int(math.Max(0, math.Min(float64(c.height), math.Ceil((dy+pattern.box.H-c.box.Y)/pixelStep)+1)))
width, height := right-left, bottom-top
if width <= 0 || height <= 0 {
continue
}
pixels := candidate[:width*height]
for y := range height {
for x := range width {
index := (top+y)*c.width + left + x
initial := result[index]
if disjoint {
initial = backdrop[index]
}
pixels[y*width+x] = pdfCompositePixel{values: initial.values, alpha: initial.alpha}
}
}
cell := pdfCompositor{importer: &pattern.local, box: Box{X: c.box.X + float64(left)*pixelStep - dx, Y: c.box.Y + float64(top)*pixelStep - dy, W: float64(width) * pixelStep, H: float64(height) * pixelStep},
width: width, height: height, inverse: localInverse, cache: pattern.local.compositingCache(), masks: map[*pdfgo.SoftMask][]float64{}}
if err := cell.draw(pattern.nodes, pixels, space); err != nil {
return nil, err return nil, err
} }
for y := range height {
for x := range width {
index := (top+y)*c.width + left + x
pdfCompositePatternCell(&result[index], backdrop[index], pixels[y*width+x], disjoint)
}
}
} }
} }
for i := range result { for i := range result {
@@ -86,6 +116,29 @@ func (c *pdfCompositor) tiling(paint pdfgo.Paint, backdrop []pdfCompositePixel,
return result, nil return result, nil
} }
// pdfCompositePatternCell 合并互不重叠单元的覆盖贡献,避免相邻抗锯齿边缘重复混合
// 入参: target 图案累计结果, initial 原始背景, source 单元结果, disjoint 单元边界是否互不重叠
func pdfCompositePatternCell(target *pdfCompositePixel, initial, source pdfCompositePixel, disjoint bool) {
if source.shape == 0 {
return
}
if disjoint {
alpha := math.Max(0, math.Min(1, target.alpha+source.alpha-initial.alpha))
if alpha != 0 {
for i := range target.values {
target.values[i] = (target.values[i]*target.alpha + source.values[i]*source.alpha - initial.values[i]*initial.alpha) / alpha
}
}
target.alpha = alpha
target.effect = math.Min(1, target.effect+source.effect)
target.shape = math.Min(1, target.shape+source.shape)
} else {
target.values, target.alpha = source.values, source.alpha
target.effect += source.effect * (1 - target.effect)
target.shape += source.shape * (1 - target.shape)
}
}
// compositePattern 复用图案单元解析及几何上下文,供检查和局部合成共同使用 // compositePattern 复用图案单元解析及几何上下文,供检查和局部合成共同使用
// 入参: paint 图案画刷 // 入参: paint 图案画刷
// 返回: *pdfCompositePattern 单元内容, error 解析或边界错误 // 返回: *pdfCompositePattern 单元内容, error 解析或边界错误
@@ -127,7 +180,54 @@ func (p *pdfImporter) compositePattern(paint pdfgo.Paint) (*pdfCompositePattern,
// 入参: node 图元, space 混合空间 // 入参: node 图元, space 混合空间
// 返回: bool 是否可直接转换, error 图案解析错误 // 返回: bool 是否可直接转换, error 图案解析错误
func (p *pdfImporter) directCompositeNode(node pdfCompositeNode, space *pdfgo.ColorSpace) (bool, error) { func (p *pdfImporter) directCompositeNode(node pdfCompositeNode, space *pdfgo.ColorSpace) (bool, error) {
if node.group == nil && space.Model == "DeviceCMYK" && !space.Calibrated() {
style, fill, stroke := node.style()
if style.ColorConversion != (pdfgo.ColorConversion{}) {
if node.image != nil && !node.image.Image.ImageMask {
source := p.compositingCache().images[node.image.Image]
if source == nil {
var err error
source, err = node.image.Image.DecodeComponents()
if err != nil {
return false, err
}
p.compositingCache().images[node.image.Image] = source
}
if source.Space.Model == "DeviceRGB" && source.Space.Device() {
return false, nil
}
} else {
for i, paint := range [2]pdfgo.Paint{style.Fill, style.Stroke} {
if i == 0 && !fill || i == 1 && !stroke || paint.None || paint.Tiling != nil {
continue
}
source := paint.Space
switch {
case paint.Axial != nil:
source = paint.Axial.Space
case paint.Radial != nil:
source = paint.Radial.Space
case paint.Function != nil:
source = paint.Function.Space
case paint.Mesh != nil:
source = paint.Mesh.Space
}
if source != nil && source.Model == "DeviceRGB" && source.Device() || source == nil && paint.CMYK == nil && paint.SourceSpace != "DeviceGray" {
return false, nil
}
}
}
}
}
if !node.opaque(space) && !(space.SRGBEquivalent() && node.direct()) { if !node.opaque(space) && !(space.SRGBEquivalent() && node.direct()) {
if node.textObject != nil && space.SRGBEquivalent() {
for _, child := range node.children {
if direct, err := p.directCompositeNode(child, space); err != nil || !direct {
return direct, err
}
}
return p.disjointText(node)
}
return false, nil return false, nil
} }
if node.group != nil { if node.group != nil {
@@ -138,6 +238,9 @@ func (p *pdfImporter) directCompositeNode(node pdfCompositeNode, space *pdfgo.Co
} }
return true, nil return true, nil
} }
if node.image != nil && !node.image.Image.ImageMask {
return true, nil
}
style, fill, stroke := node.style() style, fill, stroke := node.style()
for i, paint := range []pdfgo.Paint{style.Fill, style.Stroke} { for i, paint := range []pdfgo.Paint{style.Fill, style.Stroke} {
if i == 0 && !fill || i == 1 && !stroke || paint.Tiling == nil { if i == 0 && !fill || i == 1 && !stroke || paint.Tiling == nil {
@@ -173,6 +276,9 @@ func (p *pdfImporter) processOverprint(nodes []pdfCompositeNode) (bool, error) {
return found, err return found, err
} }
} }
if node.image != nil && !node.image.Image.ImageMask {
continue
}
if fill && style.FillOverprint && pdfOverprintNeedsSeparation(style.Fill) || stroke && style.StrokeOverprint && pdfOverprintNeedsSeparation(style.Stroke) { if fill && style.FillOverprint && pdfOverprintNeedsSeparation(style.Fill) || stroke && style.StrokeOverprint && pdfOverprintNeedsSeparation(style.Stroke) {
return true, nil return true, nil
} }
+340
View File
@@ -0,0 +1,340 @@
// 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"
"crypto/sha256"
"encoding/binary"
"fmt"
"maps"
"slices"
"unicode/utf16"
"unicode/utf8"
"github.com/xiaoqidun/pdfgo"
)
// pdfImportedFont 保存单次导入共用的字体资源、度量及源字形映射,不保存文档标识
type pdfImportedFont struct {
resource *FontResource
checksum [32]byte
metrics FontMetrics
type1Glyphs map[string]uint16
repairLimit uint16
}
// importedFont 按原PDF字体复用包装和解析结果,失败结果不进入缓存
// 入参: source 源字体
// 返回: *pdfImportedFont 只读导入字体, error 字体程序或后端错误
func (p *pdfImporter) importedFont(source *pdfgo.Font) (*pdfImportedFont, error) {
if cached := p.fonts[source]; cached != nil {
return cached, nil
}
var type1 *type1Program
result := &pdfImportedFont{}
if source.ProgramType == "FontFile" {
parsed, err := parseType1Program(source.Program)
if err != nil {
return nil, fmt.Errorf("PDF font %s program: %w", source.Name, err)
}
type1 = &parsed
result.type1Glyphs = parsed.glyphIDs()
}
program, limit, err := pdfFontProgram(source, type1)
if err != nil {
return nil, fmt.Errorf("PDF font %s program: %w", source.Name, err)
}
if p.editor.backends.FontResources == nil {
return nil, fmt.Errorf("PDF font %s resource: font resources: %w", source.Name, ErrBackendUnavailable)
}
result.resource, err = p.editor.backends.FontResources.OpenFontResource(FontFile{Name: source.Name + ".ttf", Data: program}, 0)
if err != nil {
return nil, fmt.Errorf("PDF font %s resource: %w", source.Name, err)
}
if p.editor.backends.Fonts == nil {
return nil, fmt.Errorf("PDF font backend unavailable")
}
result.metrics, err = p.editor.backends.Fonts.OpenFont(result.resource.Data)
if err != nil {
return nil, fmt.Errorf("PDF font %s metrics: %w", source.Name, err)
}
if _, ok := result.metrics.(FontOutlines); !ok {
return nil, fmt.Errorf("PDF font backend does not provide glyph outlines")
}
result.checksum, result.repairLimit = sha256.Sum256(result.resource.Data), limit
if p.fonts == nil {
p.fonts = make(map[*pdfgo.Font]*pdfImportedFont)
}
p.fonts[source] = result
return result, nil
}
// pdfFontProgram 为PDF子集字体补齐封装表,保留字形轮廓和编号
// 复合字体的重复映射区段按PDF字符映射重建,不改变CID对应的字形
// 入参: source PDF字体, type1 已解析的Type1程序,nil时按需解析
// 返回: []byte 封装后的字体数据, uint16 缺失度量前的完整字形数, error 错误信息
func pdfFontProgram(source *pdfgo.Font, type1 *type1Program) ([]byte, uint16, error) {
program := source.Program
if source.ProgramType == "FontFile" {
if type1 == nil {
parsed, err := parseType1Program(program)
if err != nil {
return nil, 0, err
}
type1 = &parsed
}
var err error
program, err = type1.toCFF(pdfFontIdentity(source))
if err != nil {
return nil, 0, err
}
}
bareCFF := len(program) >= 4 && program[0] == 1 && program[1] == 0 && program[2] >= 4 && program[3] >= 1 && program[3] <= 4
if source.ProgramType == "Type1C" || source.ProgramType == "CIDFontType0C" || bareCFF {
var err error
program, _, err = wrapCFFToOTF(program)
if err != nil {
return nil, 0, err
}
}
tables, err := fontFileTables(program, 0)
if err != nil {
return nil, 0, err
}
if len(tables["head"]) < 54 || len(tables["maxp"]) < 6 || len(tables["hhea"]) < 36 || len(tables["hmtx"]) == 0 {
return nil, 0, fmt.Errorf("embedded PDF font lacks required metrics")
}
count := binary.BigEndian.Uint16(tables["maxp"][4:6])
if count == 0 {
return nil, 0, fmt.Errorf("embedded PDF font has no glyphs")
}
changed := false
var repairedLimit uint16
metrics := int(binary.BigEndian.Uint16(tables["hhea"][34:36]))
if metrics == 0 || metrics > int(count) {
return nil, 0, fmt.Errorf("invalid embedded PDF font metric count")
}
metricLength := 4*metrics + 2*(int(count)-metrics)
if len(tables["hmtx"]) < metricLength {
if len(tables["hmtx"]) < 4*metrics || len(tables["hmtx"])%2 != 0 {
return nil, 0, fmt.Errorf("incomplete embedded PDF font metrics: have %d, need %d", len(tables["hmtx"]), metricLength)
}
limit := metrics + (len(tables["hmtx"])-4*metrics)/2
missing, err := pdfMissingLeftBearings(tables, limit, int(count))
if err != nil {
return nil, 0, err
}
tables["hmtx"] = append(bytes.Clone(tables["hmtx"]), missing...)
repairedLimit = uint16(limit)
changed = true
}
if len(tables["hmtx"]) > metricLength {
tables["hmtx"] = tables["hmtx"][:metricLength]
changed = true
}
invalidCmap := false
if source.Subtype == "TrueType" || source.Subtype == "Type0" {
mapping := parseCmapMappings(tables["cmap"])
for char, glyph := range mapping {
if glyph >= count {
delete(mapping, char)
invalidCmap = true
}
}
if invalidCmap && source.Subtype == "TrueType" {
tables["cmap"] = buildCmapTable(count, mapping)
changed = true
}
}
if len(tables["cmap"]) == 0 || source.Subtype == "Type0" && (invalidCmap || pdfCmapOverlaps(tables["cmap"])) {
mapping := map[rune]uint16{}
for _, code := range slices.Sorted(maps.Keys(source.Unicode)) {
glyphs, err := source.Decode([]byte(code))
if err != nil {
return nil, 0, err
}
if len(glyphs) != 1 || !glyphs[0].HasID {
return nil, 0, &pdfgo.UnsupportedError{Feature: "font without explicit glyph mapping"}
}
glyph := glyphs[0]
if glyph.ID >= count {
return nil, 0, fmt.Errorf("PDF font glyph %d exceeds glyph count %d", glyph.ID, count)
}
if utf8.RuneCountInString(glyph.Text) == 1 {
char, _ := utf8.DecodeRuneInString(glyph.Text)
if _, exists := mapping[char]; !exists {
mapping[char] = glyph.ID
}
}
}
addPackedGlyphMapping(mapping, count)
tables["cmap"] = buildCmapTable(count, mapping)
changed = true
}
if len(fontNamesFromTable(tables["name"])) == 0 {
fontName := pdfFontIdentity(source)
name := utf16.Encode([]rune(fontName))
if len(name) > 32767 {
return nil, 0, fmt.Errorf("PDF font name exceeds name table capacity")
}
data := make([]byte, 42+len(name)*2)
binary.BigEndian.PutUint16(data[2:], 3)
binary.BigEndian.PutUint16(data[4:], 42)
for n, id := range []uint16{1, 4, 6} {
record := data[6+n*12:]
binary.BigEndian.PutUint16(record, 3)
binary.BigEndian.PutUint16(record[2:], 1)
binary.BigEndian.PutUint16(record[4:], 0x409)
binary.BigEndian.PutUint16(record[6:], id)
binary.BigEndian.PutUint16(record[8:], uint16(len(name)*2))
}
for n, v := range name {
binary.BigEndian.PutUint16(data[42+n*2:], v)
}
tables["name"] = data
changed = true
}
if len(tables["post"]) == 0 {
tables["post"] = buildPostTable()
changed = true
}
if len(tables["OS/2"]) == 0 {
hhea := tables["hhea"]
tables["OS/2"] = buildOS2TableWithMetrics(int16(binary.BigEndian.Uint16(hhea[4:6])), int16(binary.BigEndian.Uint16(hhea[6:8])))
changed = true
}
for _, tag := range []string{"cvt ", "fpgm", "prep"} {
if data, ok := tables[tag]; ok && len(data) == 0 {
delete(tables, tag)
changed = true
}
}
if !changed && !pdfSFNTMissingPadding(program) {
return program, repairedLimit, nil
}
result, err := serializeOTF(tables)
return result, repairedLimit, err
}
// pdfFontIdentity 返回字体名称或由原始程序生成的稳定封装标识
// 入参: source PDF字体
// 返回: string 字体标识
func pdfFontIdentity(source *pdfgo.Font) string {
if source.Name != "" {
return source.Name
}
checksum := sha256.Sum256(source.Program)
return fmt.Sprintf("PDF-%x", checksum[:28])
}
// pdfCmapOverlaps 检查格式4字符映射是否包含交叠区段
// 入参: data cmap表数据
// 返回: bool 是否存在交叠
func pdfCmapOverlaps(data []byte) bool {
if len(data) < 4 {
return false
}
count := int(binary.BigEndian.Uint16(data[2:]))
if count > (len(data)-4)/8 {
return false
}
for index := range count {
offset := uint64(binary.BigEndian.Uint32(data[8+8*index:]))
if offset > uint64(len(data)) || uint64(len(data))-offset < 16 {
continue
}
sub := data[int(offset):]
if binary.BigEndian.Uint16(sub) != 4 {
continue
}
length := int(binary.BigEndian.Uint16(sub[2:]))
segments := int(binary.BigEndian.Uint16(sub[6:])) / 2
if length > len(sub) || 16+8*segments > length {
continue
}
for i := 1; i < segments; i++ {
previous := binary.BigEndian.Uint16(sub[14+2*(i-1):])
start := binary.BigEndian.Uint16(sub[16+2*segments+2*i:])
if start <= previous {
return true
}
}
}
return false
}
// pdfSFNTMissingPadding 判断字体表目录是否引用了未写入的末尾对齐字节
// 入参: program OpenType字体数据
// 返回: bool 是否需要重新封装
func pdfSFNTMissingPadding(program []byte) bool {
if len(program) < 12 {
return true
}
count := int(binary.BigEndian.Uint16(program[4:6]))
if count > (len(program)-12)/16 {
return true
}
for index := range count {
record := program[12+16*index:]
offset := int(binary.BigEndian.Uint32(record[8:12]))
length := int(binary.BigEndian.Uint32(record[12:16]))
if offset > len(program) || length > len(program)-offset || (4-length%4)%4 > len(program)-offset-length {
return true
}
}
return false
}
// pdfMissingLeftBearings 从轮廓边界恢复缺失的尾部左侧边距
// 入参: tables 字体表, start 首个缺失字形, count 字形总数
// 返回: []byte 补齐的hmtx数据, error 无法读取轮廓
func pdfMissingLeftBearings(tables map[string][]byte, start, count int) ([]byte, error) {
head, loca, glyf := tables["head"], tables["loca"], tables["glyf"]
if len(head) < 54 || len(glyf) == 0 {
return nil, fmt.Errorf("embedded PDF font lacks glyph outlines for missing metrics")
}
format := int(int16(binary.BigEndian.Uint16(head[50:52])))
entrySize := 2
if format == 1 {
entrySize = 4
} else if format != 0 {
return nil, fmt.Errorf("invalid embedded PDF font location format")
}
if len(loca) < (count+1)*entrySize {
return nil, fmt.Errorf("incomplete embedded PDF font glyph locations")
}
location := func(index int) int {
if format == 0 {
return int(binary.BigEndian.Uint16(loca[index*2:])) * 2
}
return int(binary.BigEndian.Uint32(loca[index*4:]))
}
result := make([]byte, 2*(count-start))
for index := start; index < count; index++ {
from, to := location(index), location(index+1)
if from > to || to > len(glyf) {
return nil, fmt.Errorf("invalid embedded PDF font glyph location")
}
if from == to {
continue
}
if to-from < 10 {
return nil, fmt.Errorf("incomplete embedded PDF font glyph outline")
}
copy(result[(index-start)*2:], glyf[from+2:from+4])
}
return result, nil
}
+8 -4
View File
@@ -155,7 +155,7 @@ func (p *pdfImporter) groupPaths(mark pdfgo.GroupMark, walk func(pdfgo.Visitor)
if stroke { if stroke {
color, overprint = path.Style.Stroke, path.Style.StrokeOverprint color, overprint = path.Style.Stroke, path.Style.StrokeOverprint
} }
if color.Tiling != nil || color.Axial != nil || color.Radial != nil || color.Mesh != nil || overprint && pdfOverprintNeedsSeparation(color) { if color.Tiling != nil || color.Axial != nil || color.Radial != nil || color.Function != nil || color.Mesh != nil || overprint && pdfOverprintNeedsSeparation(color) {
return fmt.Errorf("%w: patterned or overprinted group content", errPDFGroupRaster) return fmt.Errorf("%w: patterned or overprinted group content", errPDFGroupRaster)
} }
if color.Alpha == 0 { if color.Alpha == 0 {
@@ -165,7 +165,7 @@ func (p *pdfImporter) groupPaths(mark pdfgo.GroupMark, walk func(pdfgo.Visitor)
if paint == nil { if paint == nil {
paint, clips = &color, path.Style.Clips paint, clips = &color, path.Style.Clips
} else if !reflect.DeepEqual(clips, path.Style.Clips) { } else if !reflect.DeepEqual(clips, path.Style.Clips) {
return &pdfgo.UnsupportedError{Feature: "independently clipped transparent group"} return fmt.Errorf("%w: independently clipped group content", errPDFGroupRaster)
} else if color.Alpha != 1 || !reflect.DeepEqual(*paint, color) { } else if color.Alpha != 1 || !reflect.DeepEqual(*paint, color) {
regions = append(regions, pdfPaintRegion{*paint, combined}) regions = append(regions, pdfPaintRegion{*paint, combined})
combined = nil combined = nil
@@ -335,7 +335,10 @@ func (p *pdfImporter) compileGroup(build func(*pdfImporter) error) (*RasterPage,
if _, err := editor.AddPage(p.pageWidth, p.pageHeight); err != nil { if _, err := editor.AddPage(p.pageWidth, p.pageHeight); err != nil {
return nil, box, err return nil, box, err
} }
local := pdfImporter{ctx: p.ctx, reader: p.reader, editor: editor, renderer: p.renderer, matrix: p.matrix, pageBox: p.pageBox, pageWidth: p.pageWidth, pageHeight: p.pageHeight, rasterDPI: p.rasterDPI, warning: p.warning, rasterWarned: true, fontIDs: map[*pdfgo.Font]string{}, fontMetrics: map[string]FontMetrics{}} if p.fonts == nil {
p.fonts = make(map[*pdfgo.Font]*pdfImportedFont)
}
local := pdfImporter{ctx: p.ctx, reader: p.reader, editor: editor, renderer: p.renderer, matrix: p.matrix, pageBox: p.pageBox, pageWidth: p.pageWidth, pageHeight: p.pageHeight, rasterDPI: p.rasterDPI, warning: p.warning, rasterWarned: true, fontIDs: map[*pdfgo.Font]string{}, fonts: p.fonts}
if err := build(&local); err != nil { if err := build(&local); err != nil {
return nil, box, err return nil, box, err
} }
@@ -387,6 +390,7 @@ func (p *pdfImporter) compileGroup(build func(*pdfImporter) error) (*RasterPage,
// 返回: image.Image 透明图像, error 后端渲染错误 // 返回: image.Image 透明图像, error 后端渲染错误
func (p *pdfImporter) renderGroupScene(source *RasterPage, box Box) (image.Image, error) { func (p *pdfImporter) renderGroupScene(source *RasterPage, box Box) (image.Image, error) {
scene := *source scene := *source
scene.DPI = p.rasterDPI
scene.Commands = append([]RasterCommand(nil), source.Commands...) scene.Commands = append([]RasterCommand(nil), source.Commands...)
scene.Width, scene.Height = box.W, box.H scene.Width, scene.Height = box.W, box.H
for i := range scene.Commands { for i := range scene.Commands {
@@ -567,7 +571,7 @@ func (p *pdfImporter) maskClip(mask *pdfgo.SoftMask) (pdfgo.Path, error) {
} }
visitor.Path = func(mark pdfgo.PathMark) error { visitor.Path = func(mark pdfgo.PathMark) error {
paint := mark.Style.Fill paint := mark.Style.Fill
if !mark.Fill || mark.Stroke || paint.Alpha != 1 || paint.CMYK != nil || paint.Axial != nil || paint.Radial != nil || paint.Mesh != nil || paint.Tiling != nil || paint.Space != nil && paint.Space.Calibrated() || paint.RGB[0] != paint.RGB[1] || paint.RGB[1] != paint.RGB[2] || mark.Style.SoftMask != nil || !pdfNormalBlend(mark.Style.BlendMode) { if !mark.Fill || mark.Stroke || paint.Alpha != 1 || paint.CMYK != nil || paint.Axial != nil || paint.Radial != nil || paint.Function != nil || paint.Mesh != nil || paint.Tiling != nil || paint.Space != nil && paint.Space.Calibrated() || paint.RGB[0] != paint.RGB[1] || paint.RGB[1] != paint.RGB[2] || mark.Style.SoftMask != nil || !pdfNormalBlend(mark.Style.BlendMode) {
return &pdfgo.UnsupportedError{Feature: "nonbinary mask graphic"} return &pdfgo.UnsupportedError{Feature: "nonbinary mask graphic"}
} }
visible, err := opacity(paint.RGB[0]) visible, err := opacity(paint.RGB[0])
+2 -9
View File
@@ -16,21 +16,14 @@ package ofdgo
import "github.com/xiaoqidun/pdfgo" import "github.com/xiaoqidun/pdfgo"
// halftone 检查连续色调转换及不可忽略的传递函数,避免将打印网屏误用于屏幕显示 // halftone 检查连续色调转换,避免将打印网屏误用于屏幕显示
// 入参: style 图形状态 // 入参: style 图形状态
// 返回: error 严格模式或非恒等传递函数无法表达 // 返回: error 严格模式无法保留设备网屏
func (p *pdfImporter) halftone(style pdfgo.Style) error { func (p *pdfImporter) halftone(style pdfgo.Style) error {
h := style.Halftone h := style.Halftone
if h == nil || h.Type == 0 { if h == nil || h.Type == 0 {
return nil return nil
} }
identity, err := h.IdentityTransfer(p.reader)
if err != nil {
return err
}
if !identity {
return &pdfgo.UnsupportedError{Feature: "halftone transfer function conversion"}
}
if p.warning == nil { if p.warning == nil {
return &pdfgo.UnsupportedError{Feature: "device halftone conversion"} return &pdfgo.UnsupportedError{Feature: "device halftone conversion"}
} }
+10
View File
@@ -205,6 +205,16 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict
} }
continue continue
} }
if annotation.Subtype == "Redact" {
if strict {
return &pdfgo.UnsupportedError{Feature: "pending PDF redaction conversion"}
}
if err := p.appearanceAnnotation(ctx, page, annotation); err != nil {
return err
}
p.report.Warnings = append(p.report.Warnings, pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF pending redaction appearance imported; content removal not applied; overlay data not transferred"})
continue
}
if annotation.Subtype != "Link" && pdfAnnotationType(annotation.Subtype) != "" { if annotation.Subtype != "Link" && pdfAnnotationType(annotation.Subtype) != "" {
if err := p.appearanceAnnotation(ctx, page, annotation); err != nil { if err := p.appearanceAnnotation(ctx, page, annotation); err != nil {
return err return err
+44 -23
View File
@@ -35,14 +35,9 @@ type pdfMeshTriangle struct {
// pdfMeshKey 区分网格及其输出混合空间 // pdfMeshKey 区分网格及其输出混合空间
type pdfMeshKey struct { type pdfMeshKey struct {
mesh *pdfgo.MeshGradient mesh *pdfgo.MeshGradient
space *pdfgo.ColorSpace space *pdfgo.ColorSpace
} conversion pdfgo.ColorConversion
// pdfMeshPixel 保存抗锯齿覆盖率与源空间合成分量
type pdfMeshPixel struct {
values [4]float64
shape float64
} }
// pdfMeshSample 保存采样点参数,patch为一基编号,零表示未覆盖 // pdfMeshSample 保存采样点参数,patch为一基编号,零表示未覆盖
@@ -77,7 +72,7 @@ func (p *pdfImporter) meshColor(paint pdfgo.Paint, box Box) (*FillColor, error)
} }
color := pdfgo.Paint{RGB: rgb, Alpha: 1} color := pdfgo.Paint{RGB: rgb, Alpha: 1}
if mesh.Space.Model == "DeviceCMYK" && !mesh.Space.Calibrated() { if mesh.Space.Model == "DeviceCMYK" && !mesh.Space.Calibrated() {
color.CMYK = &values color.CMYK = (*[4]float64)(values)
} }
converted := p.color(color) converted := p.color(color)
shading.Point = append(shading.Point, ShdPoint{X: point.X - box.X, Y: point.Y - box.Y, Color: ShdColor{Value: converted.Value, ColorSpace: converted.ColorSpace}}) shading.Point = append(shading.Point, ShdPoint{X: point.X - box.X, Y: point.Y - box.Y, Color: ShdColor{Value: converted.Value, ColorSpace: converted.ColorSpace}})
@@ -160,10 +155,10 @@ func (p *pdfImporter) meshTriangles(mesh *pdfgo.MeshGradient) ([]pdfMeshTriangle
} }
// mesh 对当前图块执行四倍超采样,曲面重叠按后补片及较大v、u取值 // mesh 对当前图块执行四倍超采样,曲面重叠按后补片及较大v、u取值
// 入参: mesh 原曲面, space 输出混合空间 // 入参: mesh 原曲面, space 输出混合空间, conversion 设备转换函数
// 返回: []pdfMeshPixel 颜色及覆盖, error 几何或颜色错误 // 返回: []pdfShadingPixel 颜色及覆盖, error 几何或颜色错误
func (c *pdfCompositor) mesh(mesh *pdfgo.MeshGradient, space *pdfgo.ColorSpace) ([]pdfMeshPixel, error) { func (c *pdfCompositor) mesh(mesh *pdfgo.MeshGradient, space *pdfgo.ColorSpace, conversion pdfgo.ColorConversion) ([]pdfShadingPixel, error) {
key := pdfMeshKey{mesh, space} key := pdfMeshKey{mesh, space, conversion}
if pixels, ok := c.meshes[key]; ok { if pixels, ok := c.meshes[key]; ok {
return pixels, nil return pixels, nil
} }
@@ -222,23 +217,49 @@ func (c *pdfCompositor) mesh(mesh *pdfgo.MeshGradient, space *pdfgo.ColorSpace)
} }
} }
} }
pixels := make([]pdfMeshPixel, c.width*c.height) pixels := make([]pdfShadingPixel, c.width*c.height)
var background [4]float64
if mesh.Background != nil {
var err error
background, err = space.ConvertWith(mesh.Background[:mesh.Space.Components()], mesh.Space, mesh.Intent, conversion)
if err != nil {
return nil, err
}
}
var inverse pdfgo.Matrix
if mesh.Bounds != nil {
var ok bool
inverse, ok = c.importer.matrix.Mul(mesh.Matrix).Inverse()
if !ok {
return nil, fmt.Errorf("singular mesh bounds transform")
}
}
for y := 0; y < h; y++ { for y := 0; y < h; y++ {
if err := c.importer.ctx.Err(); err != nil { if err := c.importer.ctx.Err(); err != nil {
return nil, err return nil, err
} }
for x := 0; x < w; x++ { for x := 0; x < w; x++ {
if box := mesh.Bounds; box != nil {
point := inverse.Apply(pdfgo.Point{X: c.box.X + (float64(x)+.5)/scale, Y: c.box.Y + (float64(y)+.5)/scale})
if point.X < box.XMin || point.X > box.XMax || point.Y < box.YMin || point.Y > box.YMax {
continue
}
}
sample := grid[y*w+x] sample := grid[y*w+x]
if sample.patch == 0 { if sample.patch == 0 && mesh.Background == nil {
continue continue
} }
values, err := mesh.ValuesAt(sample.patch-1, sample.u, sample.v) values := background
if err != nil { if sample.patch != 0 {
return nil, err var err error
} values, err = mesh.ValuesAt(sample.patch-1, sample.u, sample.v)
values, err = space.Convert(values[:mesh.Space.Components()], mesh.Space, mesh.Intent) if err != nil {
if err != nil { return nil, err
return nil, err }
values, err = space.ConvertWith(values[:mesh.Space.Components()], mesh.Space, mesh.Intent, conversion)
if err != nil {
return nil, err
}
} }
pixel := &pixels[(y/samples)*c.width+x/samples] pixel := &pixels[(y/samples)*c.width+x/samples]
for i, value := range values { for i, value := range values {
@@ -258,7 +279,7 @@ func (c *pdfCompositor) mesh(mesh *pdfgo.MeshGradient, space *pdfgo.ColorSpace)
pixel.shape /= samples * samples pixel.shape /= samples * samples
} }
if c.meshes == nil { if c.meshes == nil {
c.meshes = map[pdfMeshKey][]pdfMeshPixel{} c.meshes = map[pdfMeshKey][]pdfShadingPixel{}
} }
c.meshes[key] = pixels c.meshes[key] = pixels
return pixels, nil return pixels, nil
+78 -311
View File
@@ -16,17 +16,12 @@ package ofdgo
import ( import (
"bytes" "bytes"
"crypto/sha256"
"encoding/binary"
"fmt" "fmt"
"image" "image"
"image/color" "image/color"
"image/png" "image/png"
"maps"
"math" "math"
"slices"
"strconv" "strconv"
"unicode/utf16"
"unicode/utf8" "unicode/utf8"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
@@ -81,7 +76,10 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
if err := p.flushPath(); err != nil { if err := p.flushPath(); err != nil {
return err return err
} }
if mark.Image.ImageMask && (mark.Style.Fill.Axial != nil || mark.Style.Fill.Radial != nil) { if mark.Image.ImageMask && (mark.Style.Fill.Axial != nil || mark.Style.Fill.Radial != nil || mark.Style.Fill.Function != nil || mark.Style.Fill.Mesh != nil) {
if p.warning != nil {
return p.compositeRegion(nil, pdfCompositeNode{image: &mark}, &pdfgo.ColorSpace{Model: "DeviceRGB"}, false)
}
return &pdfgo.UnsupportedError{Feature: "gradient stencil image"} return &pdfgo.UnsupportedError{Feature: "gradient stencil image"}
} }
var err error var err error
@@ -180,13 +178,13 @@ func (p *pdfImporter) appendImage(mark pdfgo.ImageMark, id string) error {
// 入参: mark PDF文字绘制信息 // 入参: mark PDF文字绘制信息
// 返回: error 错误信息 // 返回: error 错误信息
func (p *pdfImporter) text(mark pdfgo.TextMark) error { func (p *pdfImporter) text(mark pdfgo.TextMark) error {
if mark.Font.Subtype == pdfgo.Name("Type3") && mark.Mode == 3 {
return nil
}
if mark.Style.BlendMode != "" && mark.Style.BlendMode != "Normal" && mark.Style.BlendMode != "Compatible" { if mark.Style.BlendMode != "" && mark.Style.BlendMode != "Normal" && mark.Style.BlendMode != "Compatible" {
return &pdfgo.UnsupportedError{Feature: "text blend mode " + string(mark.Style.BlendMode)} return &pdfgo.UnsupportedError{Feature: "text blend mode " + string(mark.Style.BlendMode)}
} }
if mark.Font.Subtype == pdfgo.Name("Type3") { if mark.Font.Subtype == pdfgo.Name("Type3") {
if mark.Clip != nil {
return &pdfgo.UnsupportedError{Feature: "Type3 text clipping"}
}
if err := p.flushPath(); err != nil { if err := p.flushPath(); err != nil {
return err return err
} }
@@ -226,36 +224,24 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
if embedded && font.ProgramType != "FontFile" && font.ProgramType != "FontFile2" && font.ProgramType != "OpenType" && font.ProgramType != "Type1C" && font.ProgramType != "CIDFontType0C" { if embedded && font.ProgramType != "FontFile" && font.ProgramType != "FontFile2" && font.ProgramType != "OpenType" && font.ProgramType != "Type1C" && font.ProgramType != "CIDFontType0C" {
return &pdfgo.UnsupportedError{Feature: "font program " + string(font.ProgramType)} return &pdfgo.UnsupportedError{Feature: "font program " + string(font.ProgramType)}
} }
var imported *pdfImportedFont
if embedded {
imported, err = p.importedFont(font)
if err != nil {
return err
}
}
id := p.fontIDs[font] id := p.fontIDs[font]
if id == "" { if id == "" {
var err error var err error
if embedded { if embedded {
var type1 *type1Program if imported.repairLimit != 0 {
if font.ProgramType == "FontFile" {
parsed, err := parseType1Program(font.Program)
if err != nil {
return fmt.Errorf("PDF font %s program: %w", font.Name, err)
}
type1 = &parsed
if p.type1Glyphs == nil {
p.type1Glyphs = make(map[*pdfgo.Font]map[string]uint16)
}
p.type1Glyphs[font] = parsed.glyphIDs()
}
var program []byte
var limit uint16
program, limit, err = pdfFontProgram(font, type1)
if err != nil {
return fmt.Errorf("PDF font %s program: %w", font.Name, err)
}
if limit != 0 {
if p.fontRepairLimit == nil {
p.fontRepairLimit = make(map[*pdfgo.Font]uint16)
}
p.fontRepairLimit[font] = limit
p.report.Warnings = append(p.report.Warnings, pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF embedded font lacks trailing side bearings; unused metrics reconstructed from glyph bounds"}) p.report.Warnings = append(p.report.Warnings, pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF embedded font lacks trailing side bearings; unused metrics reconstructed from glyph bounds"})
} }
id, err = p.editor.AddFont(FontFile{Name: font.Name + ".ttf", Data: program}, 0) id, err = p.editor.addFontResource(imported.resource, imported.checksum)
if err == nil {
p.editor.fontMetrics[id] = imported.metrics
}
} else { } else {
id, err = p.editor.AddExternalFont(font.Name) id, err = p.editor.AddExternalFont(font.Name)
} }
@@ -264,9 +250,9 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
} }
p.fontIDs[font] = id p.fontIDs[font] = id
} }
if limit := p.fontRepairLimit[font]; limit != 0 { if embedded && imported.repairLimit != 0 {
for _, glyph := range mark.Glyphs { for _, glyph := range mark.Glyphs {
if glyph.HasID && glyph.ID >= limit { if glyph.HasID && glyph.ID >= imported.repairLimit {
return &pdfgo.UnsupportedError{Feature: "embedded font glyph with missing side bearing"} return &pdfgo.UnsupportedError{Feature: "embedded font glyph with missing side bearing"}
} }
} }
@@ -274,33 +260,39 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
var metrics FontMetrics var metrics FontMetrics
var outline FontOutlines var outline FontOutlines
if embedded { if embedded {
metrics = p.fontMetrics[id] metrics = imported.metrics
if metrics == nil { outline = metrics.(FontOutlines)
if p.editor.backends.Fonts == nil { }
return fmt.Errorf("PDF font backend unavailable") fill, stroke, visible := paintMode == 0 || paintMode == 2, paintMode == 1 || paintMode == 2, paintMode != 3
if stroke && !embedded {
if resolved, err := p.editor.editorFont(id); err == nil {
if provider, ok := resolved.(FontOutlines); ok {
metrics, outline = resolved, provider
for _, glyph := range mark.Glyphs {
char, _ := utf8.DecodeRuneInString(glyph.Text)
if utf8.RuneCountInString(glyph.Text) != 1 || resolved.GlyphIndex(char) == 0 && char != 0 {
metrics, outline = nil, nil
break
}
}
} }
metrics, err = p.editor.backends.Fonts.OpenFont(p.editor.fonts[id].Data)
if err != nil {
return fmt.Errorf("PDF font %s metrics: %w", font.Name, err)
}
p.fontMetrics[id] = metrics
}
var ok bool
outline, ok = metrics.(FontOutlines)
if !ok {
return fmt.Errorf("PDF font backend does not provide glyph outlines")
} }
} }
outlineStroke := stroke && outline != nil
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 var points []pdfgo.Point
object := TextObject{Font: id, Size: mark.Size * unit, HScale: mark.HorizontalScale} object := TextObject{Font: id, Size: mark.Size * unit, HScale: mark.HorizontalScale}
position := 0 position := 0
for n, glyph := range mark.Glyphs { for n, glyph := range mark.Glyphs {
if err := p.ctx.Err(); err != nil {
return err
}
gid := glyph.ID gid := glyph.ID
if embedded && font.ProgramType == "FontFile" { if embedded && font.ProgramType == "FontFile" {
var found bool var found bool
gid, found = p.type1Glyphs[font][glyph.Name] gid, found = imported.type1Glyphs[glyph.Name]
if !found { if !found {
return fmt.Errorf("missing Type1 glyph %q in PDF font %s", glyph.Name, font.Name) return fmt.Errorf("missing Type1 glyph %q in PDF font %s", glyph.Name, font.Name)
} }
@@ -329,12 +321,18 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
if !embedded && glyph.Text == "" { if !embedded && glyph.Text == "" {
return &pdfgo.UnsupportedError{Feature: "unmapped external font character"} return &pdfgo.UnsupportedError{Feature: "unmapped external font character"}
} }
if outlineStroke && !embedded {
char, _ := utf8.DecodeRuneInString(glyph.Text)
gid = metrics.GlyphIndex(char)
}
origin := mark.Positions[n] origin := mark.Positions[n]
object.TextCode = append(object.TextCode, TextCode{X: pdfNumbers(origin.X * unit), Y: pdfNumbers(-origin.Y * unit), Value: glyph.Text}) object.TextCode = append(object.TextCode, TextCode{X: pdfNumbers(origin.X * unit), Y: pdfNumbers(-origin.Y * unit), Value: glyph.Text})
if embedded { if embedded {
count := utf8.RuneCountInString(glyph.Text) count := utf8.RuneCountInString(glyph.Text)
object.CGTransform = append(object.CGTransform, CGTransform{CodePosition: position, CodeCount: count, GlyphCount: 1, Glyphs: strconv.Itoa(int(gid))}) object.CGTransform = append(object.CGTransform, CGTransform{CodePosition: position, CodeCount: count, GlyphCount: 1, Glyphs: strconv.Itoa(int(gid))})
position += count position += count
}
if embedded || outlineStroke {
path, err := outline.GlyphOutline(gid, object.Size) path, err := outline.GlyphOutline(gid, object.Size)
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)
@@ -360,6 +358,14 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
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}))
} }
if outlineStroke {
matrix := mark.Matrix.Mul(pdfgo.Matrix{mark.HorizontalScale / unit, 0, 0, -1 / unit, origin.X, origin.Y})
glyphPath, err := pdfGlyphPath(path, matrix)
if err != nil {
return err
}
glyphPaths = append(glyphPaths, glyphPath)
}
} else { } else {
width := glyph.Width / 1000 * object.Size * mark.HorizontalScale width := glyph.Width / 1000 * object.Size * mark.HorizontalScale
for _, point := range []pdfgo.Point{{X: 0, Y: -object.Size}, {X: width, Y: -object.Size}, {X: 0, Y: object.Size / 4}, {X: width, Y: object.Size / 4}} { for _, point := range []pdfgo.Point{{X: 0, Y: -object.Size}, {X: width, Y: -object.Size}, {X: 0, Y: object.Size / 4}, {X: width, Y: object.Size / 4}} {
@@ -368,8 +374,7 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
} }
} }
box := pdfBounds(points) box := pdfBounds(points)
fill, stroke, visible := paintMode == 0 || paintMode == 2, paintMode == 1 || paintMode == 2, paintMode != 3 if stroke && !outlineStroke {
if stroke {
strokeMatrix := mark.StrokeMatrix strokeMatrix := mark.StrokeMatrix
if strokeMatrix == (pdfgo.Matrix{}) { if strokeMatrix == (pdfgo.Matrix{}) {
strokeMatrix = pdfgo.Identity() strokeMatrix = pdfgo.Identity()
@@ -394,7 +399,7 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
} }
} }
object.Boundary = pdfBoundary(box) object.Boundary = pdfBoundary(box)
if stroke { if stroke && !outlineStroke {
strokePaint, err := p.paintColor(mark.Style.Stroke, box) strokePaint, err := p.paintColor(mark.Style.Stroke, box)
if err != nil { if err != nil {
return err return err
@@ -410,7 +415,7 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
object.Stroke = &stroke object.Stroke = &stroke
object.Visible = &visible object.Visible = &visible
object.FillColor = p.color(mark.Style.Fill) object.FillColor = p.color(mark.Style.Fill)
if fill { if fill && !outlineStroke {
object.FillColor, err = p.paintColor(mark.Style.Fill, box) object.FillColor, err = p.paintColor(mark.Style.Fill, box)
if err != nil { if err != nil {
return err return err
@@ -432,264 +437,26 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
} }
p.clipTexts[mark.Clip] = clipObject p.clipTexts[mark.Clip] = clipObject
} }
if outlineStroke {
for _, path := range glyphPaths {
if len(path.Segments) == 0 {
continue
}
if err := p.ctx.Err(); err != nil {
return err
}
matrix := mark.StrokeMatrix
if matrix == (pdfgo.Matrix{}) {
matrix = pdfgo.Identity()
}
if err := p.appendPath(pdfgo.PathMark{Path: path, Style: mark.Style, Fill: fill, Stroke: true, StrokeMatrix: matrix}); err != nil {
return err
}
}
object.Visible = new(bool)
object.Fill, object.Stroke = new(bool), new(bool)
}
p.objects = append(p.objects, GraphicObject{Type: "TextObject", TextObject: object}) p.objects = append(p.objects, GraphicObject{Type: "TextObject", TextObject: object})
p.report.TextObjects++ p.report.TextObjects++
return nil return nil
} }
// pdfFontProgram 为PDF子集字体补齐封装表,保留字形轮廓和编号
// 复合字体的重复映射区段按PDF字符映射重建,不改变CID对应的字形
// 入参: source PDF字体, type1 已解析的Type1程序,nil时按需解析
// 返回: []byte 封装后的字体数据, uint16 缺失度量前的完整字形数, error 错误信息
func pdfFontProgram(source *pdfgo.Font, type1 *type1Program) ([]byte, uint16, error) {
program := source.Program
if source.ProgramType == "FontFile" {
if type1 == nil {
parsed, err := parseType1Program(program)
if err != nil {
return nil, 0, err
}
type1 = &parsed
}
var err error
program, err = type1.toCFF(pdfFontIdentity(source))
if err != nil {
return nil, 0, err
}
}
bareCFF := len(program) >= 4 && program[0] == 1 && program[1] == 0 && program[2] >= 4 && program[3] >= 1 && program[3] <= 4
if source.ProgramType == "Type1C" || source.ProgramType == "CIDFontType0C" || bareCFF {
var err error
program, _, err = wrapCFFToOTF(program)
if err != nil {
return nil, 0, err
}
}
tables, err := fontFileTables(program, 0)
if err != nil {
return nil, 0, err
}
if len(tables["head"]) < 54 || len(tables["maxp"]) < 6 || len(tables["hhea"]) < 36 || len(tables["hmtx"]) == 0 {
return nil, 0, fmt.Errorf("embedded PDF font lacks required metrics")
}
count := binary.BigEndian.Uint16(tables["maxp"][4:6])
if count == 0 {
return nil, 0, fmt.Errorf("embedded PDF font has no glyphs")
}
changed := false
var repairedLimit uint16
metrics := int(binary.BigEndian.Uint16(tables["hhea"][34:36]))
if metrics == 0 || metrics > int(count) {
return nil, 0, fmt.Errorf("invalid embedded PDF font metric count")
}
metricLength := 4*metrics + 2*(int(count)-metrics)
if len(tables["hmtx"]) < metricLength {
if len(tables["hmtx"]) < 4*metrics || len(tables["hmtx"])%2 != 0 {
return nil, 0, fmt.Errorf("incomplete embedded PDF font metrics: have %d, need %d", len(tables["hmtx"]), metricLength)
}
limit := metrics + (len(tables["hmtx"])-4*metrics)/2
missing, err := pdfMissingLeftBearings(tables, limit, int(count))
if err != nil {
return nil, 0, err
}
tables["hmtx"] = append(bytes.Clone(tables["hmtx"]), missing...)
repairedLimit = uint16(limit)
changed = true
}
if len(tables["hmtx"]) > metricLength {
tables["hmtx"] = tables["hmtx"][:metricLength]
changed = true
}
invalidCmap := false
if source.Subtype == "TrueType" || source.Subtype == "Type0" {
mapping := parseCmapMappings(tables["cmap"])
for char, glyph := range mapping {
if glyph >= count {
delete(mapping, char)
invalidCmap = true
}
}
if invalidCmap && source.Subtype == "TrueType" {
tables["cmap"] = buildCmapTable(count, mapping)
changed = true
}
}
if len(tables["cmap"]) == 0 || source.Subtype == "Type0" && (invalidCmap || pdfCmapOverlaps(tables["cmap"])) {
mapping := map[rune]uint16{}
for _, code := range slices.Sorted(maps.Keys(source.Unicode)) {
glyphs, err := source.Decode([]byte(code))
if err != nil {
return nil, 0, err
}
if len(glyphs) != 1 || !glyphs[0].HasID {
return nil, 0, &pdfgo.UnsupportedError{Feature: "font without explicit glyph mapping"}
}
glyph := glyphs[0]
if glyph.ID >= count {
return nil, 0, fmt.Errorf("PDF font glyph %d exceeds glyph count %d", glyph.ID, count)
}
if utf8.RuneCountInString(glyph.Text) == 1 {
char, _ := utf8.DecodeRuneInString(glyph.Text)
if _, exists := mapping[char]; !exists {
mapping[char] = glyph.ID
}
}
}
addPackedGlyphMapping(mapping, count)
tables["cmap"] = buildCmapTable(count, mapping)
changed = true
}
if len(fontNamesFromTable(tables["name"])) == 0 {
fontName := pdfFontIdentity(source)
name := utf16.Encode([]rune(fontName))
if len(name) > 32767 {
return nil, 0, fmt.Errorf("PDF font name exceeds name table capacity")
}
data := make([]byte, 42+len(name)*2)
binary.BigEndian.PutUint16(data[2:], 3)
binary.BigEndian.PutUint16(data[4:], 42)
for n, id := range []uint16{1, 4, 6} {
record := data[6+n*12:]
binary.BigEndian.PutUint16(record, 3)
binary.BigEndian.PutUint16(record[2:], 1)
binary.BigEndian.PutUint16(record[4:], 0x409)
binary.BigEndian.PutUint16(record[6:], id)
binary.BigEndian.PutUint16(record[8:], uint16(len(name)*2))
}
for n, v := range name {
binary.BigEndian.PutUint16(data[42+n*2:], v)
}
tables["name"] = data
changed = true
}
if len(tables["post"]) == 0 {
tables["post"] = buildPostTable()
changed = true
}
if len(tables["OS/2"]) == 0 {
hhea := tables["hhea"]
tables["OS/2"] = buildOS2TableWithMetrics(int16(binary.BigEndian.Uint16(hhea[4:6])), int16(binary.BigEndian.Uint16(hhea[6:8])))
changed = true
}
for _, tag := range []string{"cvt ", "fpgm", "prep"} {
if data, ok := tables[tag]; ok && len(data) == 0 {
delete(tables, tag)
changed = true
}
}
if !changed && !pdfSFNTMissingPadding(program) {
return program, repairedLimit, nil
}
result, err := serializeOTF(tables)
return result, repairedLimit, err
}
// pdfFontIdentity 返回字体名称或由原始程序生成的稳定封装标识
// 入参: source PDF字体
// 返回: string 字体标识
func pdfFontIdentity(source *pdfgo.Font) string {
if source.Name != "" {
return source.Name
}
checksum := sha256.Sum256(source.Program)
return fmt.Sprintf("PDF-%x", checksum[:28])
}
// pdfCmapOverlaps 检查格式4字符映射是否包含交叠区段
// 入参: data cmap表数据
// 返回: bool 是否存在交叠
func pdfCmapOverlaps(data []byte) bool {
if len(data) < 4 {
return false
}
count := int(binary.BigEndian.Uint16(data[2:]))
if count > (len(data)-4)/8 {
return false
}
for index := range count {
offset := uint64(binary.BigEndian.Uint32(data[8+8*index:]))
if offset > uint64(len(data)) || uint64(len(data))-offset < 16 {
continue
}
sub := data[int(offset):]
if binary.BigEndian.Uint16(sub) != 4 {
continue
}
length := int(binary.BigEndian.Uint16(sub[2:]))
segments := int(binary.BigEndian.Uint16(sub[6:])) / 2
if length > len(sub) || 16+8*segments > length {
continue
}
for i := 1; i < segments; i++ {
previous := binary.BigEndian.Uint16(sub[14+2*(i-1):])
start := binary.BigEndian.Uint16(sub[16+2*segments+2*i:])
if start <= previous {
return true
}
}
}
return false
}
// pdfSFNTMissingPadding 判断字体表目录是否引用了未写入的末尾对齐字节
// 入参: program OpenType字体数据
// 返回: bool 是否需要重新封装
func pdfSFNTMissingPadding(program []byte) bool {
if len(program) < 12 {
return true
}
count := int(binary.BigEndian.Uint16(program[4:6]))
if count > (len(program)-12)/16 {
return true
}
for index := range count {
record := program[12+16*index:]
offset := int(binary.BigEndian.Uint32(record[8:12]))
length := int(binary.BigEndian.Uint32(record[12:16]))
if offset > len(program) || length > len(program)-offset || (4-length%4)%4 > len(program)-offset-length {
return true
}
}
return false
}
// pdfMissingLeftBearings 从轮廓边界恢复缺失的尾部左侧边距
// 入参: tables 字体表, start 首个缺失字形, count 字形总数
// 返回: []byte 补齐的hmtx数据, error 无法读取轮廓
func pdfMissingLeftBearings(tables map[string][]byte, start, count int) ([]byte, error) {
head, loca, glyf := tables["head"], tables["loca"], tables["glyf"]
if len(head) < 54 || len(glyf) == 0 {
return nil, fmt.Errorf("embedded PDF font lacks glyph outlines for missing metrics")
}
format := int(int16(binary.BigEndian.Uint16(head[50:52])))
entrySize := 2
if format == 1 {
entrySize = 4
} else if format != 0 {
return nil, fmt.Errorf("invalid embedded PDF font location format")
}
if len(loca) < (count+1)*entrySize {
return nil, fmt.Errorf("incomplete embedded PDF font glyph locations")
}
location := func(index int) int {
if format == 0 {
return int(binary.BigEndian.Uint16(loca[index*2:])) * 2
}
return int(binary.BigEndian.Uint32(loca[index*4:]))
}
result := make([]byte, 2*(count-start))
for index := start; index < count; index++ {
from, to := location(index), location(index+1)
if from > to || to > len(glyf) {
return nil, fmt.Errorf("invalid embedded PDF font glyph location")
}
if from == to {
continue
}
if to-from < 10 {
return nil, fmt.Errorf("incomplete embedded PDF font glyph outline")
}
copy(result[(index-start)*2:], glyf[from+2:from+4])
}
return result, nil
}
+242 -1
View File
@@ -15,11 +15,29 @@
package ofdgo package ofdgo
import ( import (
"fmt"
"image"
"math" "math"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
// pdfShadingPixel 保存抗锯齿覆盖率与混合空间分量
type pdfShadingPixel struct {
values [4]float64
shape float64
}
// pdfGradientKey 区分渐变画刷及其输出混合空间
type pdfGradientKey struct {
axial *pdfgo.AxialGradient
radial *pdfgo.RadialGradient
function *pdfgo.FunctionGradient
space *pdfgo.ColorSpace
geometry pdfCompositeKey
conversion pdfgo.ColorConversion
}
// gradientSegments 保留线性源颜色的设备分量,校准颜色经源空间转换 // gradientSegments 保留线性源颜色的设备分量,校准颜色经源空间转换
// 入参: stops 源颜色分段, space 源颜色空间 // 入参: stops 源颜色分段, space 源颜色空间
// 返回: []ShdSegment OFD颜色分段, error 不可线性表达的变换 // 返回: []ShdSegment OFD颜色分段, error 不可线性表达的变换
@@ -63,9 +81,12 @@ func pdfGradientStopsError(stops []pdfgo.GradientStop, space *pdfgo.ColorSpace)
// 入参: paint 画刷 // 入参: paint 画刷
// 返回: error 不可等价表达的渐变 // 返回: error 不可等价表达的渐变
func pdfGradientError(paint pdfgo.Paint) error { func pdfGradientError(paint pdfgo.Paint) error {
if paint.Function != nil {
return &pdfgo.UnsupportedError{Feature: "function shading conversion"}
}
if paint.Mesh != nil { if paint.Mesh != nil {
mesh := paint.Mesh mesh := paint.Mesh
if len(mesh.Patches) != 0 || len(mesh.Triangles) == 0 || mesh.UsesFunction() { if len(mesh.Patches) != 0 || len(mesh.Triangles) == 0 || mesh.UsesFunction() || mesh.Background != nil || mesh.Bounds != nil {
return &pdfgo.UnsupportedError{Feature: "nonlinear mesh conversion"} return &pdfgo.UnsupportedError{Feature: "nonlinear mesh conversion"}
} }
if mesh.Space.Calibrated() && !mesh.Space.SRGBEquivalent() { if mesh.Space.Calibrated() && !mesh.Space.SRGBEquivalent() {
@@ -73,9 +94,15 @@ func pdfGradientError(paint pdfgo.Paint) error {
} }
} }
if paint.Axial != nil { if paint.Axial != nil {
if paint.Axial.Background != nil || paint.Axial.Bounds != nil {
return &pdfgo.UnsupportedError{Feature: "bounded axial pattern conversion"}
}
return pdfGradientStopsError(paint.Axial.Stops, paint.Axial.Space) return pdfGradientStopsError(paint.Axial.Stops, paint.Axial.Space)
} }
if paint.Radial != nil { if paint.Radial != nil {
if paint.Radial.Background != nil || paint.Radial.Bounds != nil {
return &pdfgo.UnsupportedError{Feature: "bounded radial pattern conversion"}
}
return pdfGradientStopsError(paint.Radial.Stops, paint.Radial.Space) return pdfGradientStopsError(paint.Radial.Stops, paint.Radial.Space)
} }
return nil return nil
@@ -96,6 +123,8 @@ func (p *pdfImporter) gradientPath(mark pdfgo.PathMark) error {
space = paint.Axial.Space space = paint.Axial.Space
case paint.Radial != nil: case paint.Radial != nil:
space = paint.Radial.Space space = paint.Radial.Space
case paint.Function != nil:
space = paint.Function.Space
case paint.Mesh != nil: case paint.Mesh != nil:
space = paint.Mesh.Space space = paint.Mesh.Space
} }
@@ -111,6 +140,218 @@ func (p *pdfImporter) gradientPath(mark pdfgo.PathMark) error {
return p.compositeRegion(nil, pdfCompositeNode{path: &mark}, space, false) return p.compositeRegion(nil, pdfCompositeNode{path: &mark}, space, false)
} }
// gradient 合成二维函数着色及带区域的渐变,保留边缘覆盖和对象透明度
// 入参: paint 渐变画刷, space 混合空间, node 几何图元,可为空, outline 是否描边
// 返回: []pdfShadingPixel 颜色及覆盖, error 求值或变换错误
func (c *pdfCompositor) gradient(paint pdfgo.Paint, space *pdfgo.ColorSpace, node *pdfCompositeNode, outline bool) ([]pdfShadingPixel, error) {
if paint.Axial != nil && paint.Axial.Background == nil && paint.Axial.Bounds == nil || paint.Radial != nil && paint.Radial.Background == nil && paint.Radial.Bounds == nil {
return nil, nil
}
key := pdfGradientKey{axial: paint.Axial, radial: paint.Radial, function: paint.Function, space: space}
var conversion pdfgo.ColorConversion
if node != nil {
key.geometry = pdfCompositeKey{path: node.path, text: node.text, image: node.image, stroke: outline}
style, _, _ := node.style()
conversion = style.ColorConversion
key.conversion = conversion
}
if pixels, ok := c.gradients[key]; ok {
return pixels, nil
}
var background *[4]float64
var bounds *pdfgo.Rectangle
var matrix pdfgo.Matrix
var source *pdfgo.ColorSpace
var intent pdfgo.Name
if paint.Axial != nil {
g := *paint.Axial
background, bounds, matrix, source, intent = g.Background, g.Bounds, g.Matrix, g.Space, g.Intent
g.Background, g.Bounds = nil, nil
paint.Axial = &g
} else if paint.Radial != nil {
g := *paint.Radial
background, bounds, matrix, source, intent = g.Background, g.Bounds, g.Matrix, g.Space, g.Intent
g.Background, g.Bounds = nil, nil
g.Matrix = pdfgo.Matrix{}
paint.Radial = &g
} else {
g := *paint.Function
background, bounds, matrix, source, intent = g.Background, g.Bounds, g.PatternMatrix, g.Space, g.Intent
g.Background, g.Bounds = nil, nil
paint.Function = &g
}
var backgroundValues [4]float64
if background != nil {
var err error
backgroundValues, err = space.ConvertWith(background[:source.Components()], source, intent, conversion)
if err != nil {
return nil, err
}
}
inverse := pdfgo.Identity()
if matrix != (pdfgo.Matrix{}) && (bounds != nil || paint.Radial != nil) {
var ok bool
inverse, ok = matrix.Inverse()
if !ok {
return nil, fmt.Errorf("singular shading transform")
}
}
var functionInverse pdfgo.Matrix
if paint.Function != nil {
var ok bool
functionInverse, ok = paint.Function.Matrix.Inverse()
if !ok {
return nil, fmt.Errorf("singular function shading transform")
}
}
const samples = 4
var coverage image.Image
domainClipped := node != nil && paint.Function != nil && background == nil
if node != nil {
local, importer := *c, *c.importer
importer.rasterDPI *= samples
local.importer = &importer
var err error
if domainClipped {
geometry, ok := c.cache.shadings[key]
if !ok {
g := paint.Function
n := pdfShadingClippedNode(*node, g.Domain, g.Matrix)
geometry.scene, geometry.box, err = c.importer.compileGroup(func(p *pdfImporter) error { return n.coverage(p, outline) })
if err != nil {
return nil, err
}
if c.cache.shadings == nil {
c.cache.shadings = map[pdfGradientKey]pdfCompositeGeometry{}
}
c.cache.shadings[key] = geometry
}
if geometry.scene != nil {
coverage, err = importer.renderGroupScene(geometry.scene, c.box)
}
} else {
coverage, err = local.coverage(*node, outline)
}
if err != nil {
return nil, err
}
if coverage == nil {
return make([]pdfShadingPixel, c.width*c.height), nil
}
}
step := 25.4 / c.importer.rasterDPI
pixels := make([]pdfShadingPixel, c.width*c.height)
for y := range c.height {
if err := c.importer.ctx.Err(); err != nil {
return nil, err
}
for x := range c.width {
pixel := &pixels[y*c.width+x]
for sy := range samples {
for sx := range samples {
weight := 1.0
if coverage != nil {
_, _, _, a := coverage.At(x*samples+sx, y*samples+sy).RGBA()
weight = float64(a) / 65535
if weight == 0 {
continue
}
}
pagePoint := c.inverse.Apply(pdfgo.Point{X: c.box.X + (float64(x)+(float64(sx)+.5)/samples)*step, Y: c.box.Y + (float64(y)+(float64(sy)+.5)/samples)*step})
point := pagePoint
local := point
if bounds != nil || paint.Radial != nil {
local = inverse.Apply(point)
}
if bounds != nil {
if local.X < bounds.XMin || local.X > bounds.XMax || local.Y < bounds.YMin || local.Y > bounds.YMax {
continue
}
}
if paint.Radial != nil {
point = local
}
var values [4]float64
var visible bool
var err error
if g := paint.Function; g != nil {
local = functionInverse.Apply(point)
visible = domainClipped || local.X >= g.Domain.XMin && local.X <= g.Domain.XMax && local.Y >= g.Domain.YMin && local.Y <= g.Domain.YMax
if visible {
values, err = g.ValuesAt(local)
if err == nil {
values, err = space.ConvertWith(values[:source.Components()], source, intent, conversion)
}
}
} else {
values, visible, err = pdfCompositeColor(paint, point, space, intent, conversion)
}
if err != nil {
return nil, err
}
if !visible {
if background == nil {
continue
}
values = backgroundValues
}
if node != nil && node.image != nil {
_, alpha, err := c.imageColor(node.image, pagePoint, space)
if err != nil {
return nil, err
}
weight *= alpha
}
for i, value := range values {
pixel.values[i] += value * weight
}
pixel.shape += weight
}
}
if pixel.shape != 0 {
for i := range pixel.values {
pixel.values[i] /= pixel.shape
}
pixel.shape /= samples * samples
}
}
}
if c.gradients == nil {
c.gradients = map[pdfGradientKey][]pdfShadingPixel{}
}
c.gradients[key] = pixels
return pixels, nil
}
// pdfShadingClippedNode 将函数定义域作为几何裁剪,避免重合边界的覆盖率重复相乘
// 入参: node 原图元, box 定义域矩形, matrix 定义域到页面的变换
// 返回: pdfCompositeNode 带独立裁剪的图元
func pdfShadingClippedNode(node pdfCompositeNode, box pdfgo.Rectangle, matrix pdfgo.Matrix) pdfCompositeNode {
clip := pdfgo.Path{Segments: []pdfgo.Segment{
{Operator: "M", Points: []pdfgo.Point{matrix.Apply(pdfgo.Point{X: box.XMin, Y: box.YMin})}},
{Operator: "L", Points: []pdfgo.Point{matrix.Apply(pdfgo.Point{X: box.XMax, Y: box.YMin})}},
{Operator: "L", Points: []pdfgo.Point{matrix.Apply(pdfgo.Point{X: box.XMax, Y: box.YMax})}},
{Operator: "L", Points: []pdfgo.Point{matrix.Apply(pdfgo.Point{X: box.XMin, Y: box.YMax})}},
{Operator: "C"},
}}
var style *pdfgo.Style
if node.path != nil {
mark := *node.path
node.path = &mark
style = &mark.Style
} else if node.text != nil {
mark := *node.text
node.text = &mark
style = &mark.Style
} else {
mark := *node.image
node.image = &mark
style = &mark.Style
}
style.Clips = append(append([]pdfgo.Path(nil), style.Clips...), clip)
return node
}
// pdfShadingPosition 计算渐变参数,未延伸区域及不存在非负圆半径的位置不着色 // pdfShadingPosition 计算渐变参数,未延伸区域及不存在非负圆半径的位置不着色
// 入参: paint 渐变画刷, point 页面坐标 // 入参: paint 渐变画刷, point 页面坐标
// 返回: float64 归一化参数, bool 是否着色 // 返回: float64 归一化参数, bool 是否着色
+115
View File
@@ -0,0 +1,115 @@
// 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 "github.com/xiaoqidun/pdfgo"
// disjointText 检查实际字形轮廓是否互不相交,允许无重叠挖空文字直接保留
// 入参: node 隐式文字组
// 返回: bool 是否互不相交, error 轮廓编译错误
func (p *pdfImporter) disjointText(node pdfCompositeNode) (bool, error) {
cache := p.compositingCache()
if result, found := cache.textDisjoint[node.group]; found {
return result, nil
}
geometry, err := p.renderer.Geometry()
if err != nil {
return false, err
}
var inverse pdfgo.Matrix
var boxes []Box
for _, child := range node.children {
mark := child.text
if mark == nil {
mark = child.glyph
}
if mark == nil || len(mark.Glyphs) != 1 {
return false, nil
}
if inverse == (pdfgo.Matrix{}) {
var ok bool
inverse, ok = p.matrix.Mul(mark.Matrix).Inverse()
if !ok {
return false, nil
}
}
scene, _, err := p.compileGroup(func(local *pdfImporter) error {
if child.group != nil {
return child.coverage(local, false)
}
_, fill, stroke := child.style()
if fill {
if err := child.coverage(local, false); err != nil {
return err
}
}
if stroke {
return child.coverage(local, true)
}
return nil
})
if err != nil {
return false, err
}
if scene == nil {
continue
}
var box Box
for _, command := range scene.Commands {
path := geometryFromRaster(command.Path)
if command.Image != nil {
b := command.Image.Bounds()
path = geometryRectangle(Box{X: float64(b.Min.X), Y: float64(b.Min.Y), W: float64(b.Dx()), H: float64(b.Dy())})
}
path, err = geometry.Transform(path, MatrixFromValues([6]float64(command.Transform)))
if err != nil {
return false, err
}
if command.Stroke != nil {
path, err = geometry.Stroke(path, *command.Stroke)
if err != nil {
return false, err
}
}
path, err = geometry.Transform(path, MatrixFromValues([6]float64(inverse)))
if err != nil {
return false, err
}
bounds, err := geometry.Bounds(path)
if err != nil {
return false, err
}
box = unionTextBox(box, bounds)
}
if box.W <= 0 || box.H <= 0 {
continue
}
for _, previous := range boxes {
if box.X < previous.X+previous.W && previous.X < box.X+box.W && box.Y < previous.Y+previous.H && previous.Y < box.Y+box.H {
if cache.textDisjoint == nil {
cache.textDisjoint = make(map[*pdfgo.GroupMark]bool)
}
cache.textDisjoint[node.group] = false
return false, nil
}
}
boxes = append(boxes, box)
}
if cache.textDisjoint == nil {
cache.textDisjoint = make(map[*pdfgo.GroupMark]bool)
}
cache.textDisjoint[node.group] = true
return true, nil
}
+53
View File
@@ -0,0 +1,53 @@
// 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"
"github.com/xiaoqidun/pdfgo"
)
// pdfGlyphPath 将字形轮廓变换到PDF页面坐标,二次曲线精确转换为三次曲线
// 入参: outline 字形轮廓, matrix 字形到页面的变换
// 返回: pdfgo.Path 页面路径, error 非字体轮廓指令
func pdfGlyphPath(outline GeometryPath, matrix pdfgo.Matrix) (pdfgo.Path, error) {
path := pdfgo.Path{Segments: make([]pdfgo.Segment, 0, len(outline))}
var current, start Point
point := func(p Point) pdfgo.Point { return matrix.Apply(pdfgo.Point{X: p.X, Y: p.Y}) }
for _, segment := range outline {
switch segment.Verb {
case GeometryMove:
start = segment.End
path.Segments = append(path.Segments, pdfgo.Segment{Operator: "M", Points: []pdfgo.Point{point(segment.End)}})
case GeometryLine:
path.Segments = append(path.Segments, pdfgo.Segment{Operator: "L", Points: []pdfgo.Point{point(segment.End)}})
case GeometryQuad:
c1 := Point{current.X + (segment.Control1.X-current.X)*2/3, current.Y + (segment.Control1.Y-current.Y)*2/3}
c2 := Point{segment.End.X + (segment.Control1.X-segment.End.X)*2/3, segment.End.Y + (segment.Control1.Y-segment.End.Y)*2/3}
path.Segments = append(path.Segments, pdfgo.Segment{Operator: "B", Points: []pdfgo.Point{point(c1), point(c2), point(segment.End)}})
case GeometryCubic:
path.Segments = append(path.Segments, pdfgo.Segment{Operator: "B", Points: []pdfgo.Point{point(segment.Control1), point(segment.Control2), point(segment.End)}})
case GeometryClose:
path.Segments = append(path.Segments, pdfgo.Segment{Operator: "C"})
current = start
continue
default:
return pdfgo.Path{}, fmt.Errorf("unsupported PDF glyph geometry verb %d", segment.Verb)
}
current = segment.End
}
return path, nil
}
+163
View File
@@ -0,0 +1,163 @@
// 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 "github.com/xiaoqidun/pdfgo"
// pdfTransferKey 区分图形状态与网屏覆盖函数
type pdfTransferKey struct {
transfer *pdfgo.TransferFunction
halftone *pdfgo.Halftone
}
// resolveTransfer 在页面内复用连续RGB设备的有效传递函数
// 入参: style 原始图形状态与有效输出状态
// 返回: error 网屏传递函数错误
func (p *pdfImporter) resolveTransfer(style *pdfgo.Style) error {
if style.Halftone == nil {
return nil
}
cache := p.compositingCache()
if cache.transfers == nil {
cache.transfers = make(map[pdfTransferKey]*pdfgo.TransferFunction)
}
key := pdfTransferKey{style.Transfer, style.Halftone}
transfer, found := cache.transfers[key]
if !found {
var err error
transfer, err = p.reader.DeviceTransfer(style.Transfer, style.Halftone, "DeviceRGB")
if err != nil {
return err
}
cache.transfers[key] = transfer
}
style.Transfer = transfer
return nil
}
// pdfCompositeHasTransfer 检查页面是否需要保留最终设备传递函数
// 入参: nodes 原始图元及透明组
// 返回: bool 是否存在非恒等函数
func pdfCompositeHasTransfer(nodes []pdfCompositeNode) bool {
for _, node := range nodes {
if node.group != nil {
if pdfCompositeHasTransfer(node.children) {
return true
}
} else {
style, _, _ := node.style()
if style.Transfer != nil {
return true
}
}
}
return false
}
// selectTransfer 按最上层基本图元及祖先的不透明状态选择最终设备函数
// 入参: node 基本图元, outline 是否描边
// 返回: *pdfgo.TransferFunction 当前函数或设备默认值, error 图案解析错误
func (c *pdfCompositor) selectTransfer(node pdfCompositeNode, outline bool) (*pdfgo.TransferFunction, error) {
style, _, _ := node.style()
paint := style.Fill
if outline {
paint = style.Stroke
}
if c.transferBlocked || paint.Alpha != 1 || style.SoftMask != nil || !pdfNormalBlend(style.BlendMode) {
return nil, nil
}
if style.Transfer == nil {
return nil, nil
}
if node.image != nil {
soft, err := node.image.Image.HasSoftMask()
if err != nil || soft {
return nil, err
}
}
if paint.Tiling != nil && (node.image == nil || node.image.Image.ImageMask) {
pattern, err := c.importer.compositePattern(paint)
if err != nil {
return nil, err
}
opaque, err := pattern.opaqueTransfer()
if err != nil || !opaque {
return nil, err
}
}
return style.Transfer, nil
}
// opaqueTransfer 复用图案不透明性检查,图案内部函数不提前作用于源颜色
// 返回: bool 是否允许使用外层设备函数, error 单元解析错误
func (p *pdfCompositePattern) opaqueTransfer() (bool, error) {
if !p.transferChecked {
for _, child := range p.nodes {
opaque, err := p.local.transferOpaque(child)
if err != nil {
return false, err
}
if !opaque {
p.transferChecked = true
return false, nil
}
}
p.transferChecked, p.transferOpaque = true, true
}
return p.transferOpaque, nil
}
// transferOpaque 检查全部图案内容的不透明条件,不限制混合颜色空间
// 入参: node 单元图元或透明组
// 返回: bool 是否不透明, error 图案或图片遮罩引用错误
func (p *pdfImporter) transferOpaque(node pdfCompositeNode) (bool, error) {
if group := node.group; group != nil {
if group.Alpha != 1 || group.SoftMask != nil || !pdfNormalBlend(group.BlendMode) {
return false, nil
}
for _, child := range node.children {
if opaque, err := p.transferOpaque(child); err != nil || !opaque {
return opaque, err
}
}
return true, nil
}
style, fill, stroke := node.style()
if style.SoftMask != nil || !pdfNormalBlend(style.BlendMode) || fill && style.Fill.Alpha != 1 || stroke && style.Stroke.Alpha != 1 {
return false, nil
}
if node.image != nil {
soft, err := node.image.Image.HasSoftMask()
if err != nil || soft {
return false, err
}
if !node.image.Image.ImageMask {
return true, nil
}
}
for i, paint := range [2]pdfgo.Paint{style.Fill, style.Stroke} {
if i == 0 && !fill || i == 1 && !stroke || paint.Tiling == nil {
continue
}
pattern, err := p.compositePattern(paint)
if err != nil {
return false, err
}
if opaque, err := pattern.opaqueTransfer(); err != nil || !opaque {
return opaque, err
}
}
return true, nil
}
+7
View File
@@ -30,6 +30,8 @@ type semanticCompiler struct {
measurement ObjectMeasurement measurement ObjectMeasurement
measure, contours, textOnly, pattern bool measure, contours, textOnly, pattern bool
patterns *renderCache[patternCellKey, []RasterCommand] patterns *renderCache[patternCellKey, []RasterCommand]
patternReferences map[*Pattern]bool
patternPage *Matrix
} }
// semanticStyle 保存已继承的路径颜色和描边参数 // semanticStyle 保存已继承的路径颜色和描边参数
@@ -466,11 +468,16 @@ func (c *semanticCompiler) fillPattern(clip GeometryPath, pattern *PatternPaint,
matrix := NewMatrix(pattern.CTM) matrix := NewMatrix(pattern.CTM)
if pattern.RelativeTo != "Page" { if pattern.RelativeTo != "Page" {
matrix = object.Multiply(matrix) matrix = object.Multiply(matrix)
} else if c.patternPage != nil {
matrix = c.patternPage.Multiply(matrix)
} }
inverse, ok := matrix.Invert() inverse, ok := matrix.Invert()
if !ok { if !ok {
return fmt.Errorf("invalid pattern transform") return fmt.Errorf("invalid pattern transform")
} }
if pattern.CellContent.empty() {
return nil
}
box, err := c.geometry.Bounds(clip) box, err := c.geometry.Bounds(clip)
if err != nil { if err != nil {
return err return err
+9
View File
@@ -129,6 +129,15 @@ func (c *semanticCompiler) addImage(img image.Image, matrix Matrix, clip *Geomet
if box.X <= visible.X && box.Y <= visible.Y && visible.X+visible.W <= box.X+box.W && visible.Y+visible.H <= box.Y+box.H { if box.X <= visible.X && box.Y <= visible.Y && visible.X+visible.W <= box.X+box.W && visible.Y+visible.H <= box.Y+box.H {
clip = nil clip = nil
} }
} else {
bounds := img.Bounds()
outline, err := geometryRectangle(Box{X: float64(bounds.Min.X), Y: float64(bounds.Min.Y), W: float64(bounds.Dx()), H: float64(bounds.Dy())}).Transform(matrix)
if err != nil {
return err
}
if geometryConvexContains(*clip, outline) {
clip = nil
}
} }
} }
if clip != nil { if clip != nil {
+12 -3
View File
@@ -16,6 +16,7 @@ package ofdgo
import ( import (
"crypto/sha256" "crypto/sha256"
"fmt"
"slices" "slices"
) )
@@ -101,6 +102,9 @@ func (c *semanticCompiler) sharedStamp(stamp Stamp, compile func(*semanticCompil
// 入参: pattern 底纹画刷, matrix 单元到页面矩阵 // 入参: pattern 底纹画刷, matrix 单元到页面矩阵
// 返回: []RasterCommand 只读单元指令, error 编译错误 // 返回: []RasterCommand 只读单元指令, error 编译错误
func (c *semanticCompiler) patternCell(pattern *PatternPaint, matrix Matrix) ([]RasterCommand, error) { func (c *semanticCompiler) patternCell(pattern *PatternPaint, matrix Matrix) ([]RasterCommand, error) {
if c.patternReferences[pattern.Pattern] {
return nil, fmt.Errorf("cyclic pattern paint")
}
x, y := 0.0, 0.0 x, y := 0.0, 0.0
if patternCellTranslatable(pattern) { if patternCellTranslatable(pattern) {
x, y = matrix.e, matrix.f x, y = matrix.e, matrix.f
@@ -119,9 +123,14 @@ func (c *semanticCompiler) patternCell(pattern *PatternPaint, matrix Matrix) ([]
if commands, ok := c.patterns.get(key); ok { if commands, ok := c.patterns.get(key); ok {
return translatePatternCommands(commands, x, y), nil return translatePatternCommands(commands, x, y), nil
} }
child := &semanticCompiler{renderer: c.renderer, geometry: c.geometry, page: &RasterPage{Width: c.page.Width, Height: c.page.Height, DPI: c.page.DPI}, pattern: true, patterns: c.patterns} if c.patternReferences == nil {
c.patternReferences = make(map[*Pattern]bool)
}
c.patternReferences[pattern.Pattern] = true
defer delete(c.patternReferences, pattern.Pattern)
child := &semanticCompiler{renderer: c.renderer, geometry: c.geometry, page: &RasterPage{Width: c.page.Width, Height: c.page.Height, DPI: c.page.DPI}, pattern: true, patterns: c.patterns, patternReferences: c.patternReferences, patternPage: &matrix}
defaults := &DrawParam{FillColor: &pattern.Color, StrokeColor: (*StrokeColor)(&pattern.Color)} defaults := &DrawParam{FillColor: &pattern.Color, StrokeColor: (*StrokeColor)(&pattern.Color)}
for _, object := range pattern.CellContent.Objects { for object := range pattern.CellContent.objects() {
object = mergeGraphicObjectAlpha(object, pattern.Alpha) object = mergeGraphicObjectAlpha(object, pattern.Alpha)
if err := c.renderer.WalkObject(&object, RenderState{Defaults: defaults, Parent: &matrix, BoundaryInCTM: true}, child); err != nil { if err := c.renderer.WalkObject(&object, RenderState{Defaults: defaults, Parent: &matrix, BoundaryInCTM: true}, child); err != nil {
return nil, err return nil, err
@@ -142,7 +151,7 @@ func patternCellTranslatable(pattern *PatternPaint) bool {
if pattern.Color.Pattern != nil { if pattern.Color.Pattern != nil {
return false return false
} }
for _, object := range pattern.CellContent.Objects { for object := range pattern.CellContent.objects() {
var fill, stroke *FillColor var fill, stroke *FillColor
switch object.Type { switch object.Type {
case "PathObject": case "PathObject":
+18 -13
View File
@@ -368,12 +368,27 @@ func renderObjectMatrix(boundary string, local Matrix, state RenderState) (Matri
return matrix, linear return matrix, linear
} }
// objectGeometryClip 解析对象裁剪并合并父裁剪 // objectGeometryClip 合并对象外接矩形、显式裁剪和父裁剪
// 入参: clips 对象裁剪, boundary 边界, local 局部矩阵, state 继承状态 // 入参: clips 对象裁剪, boundary 边界, local 局部矩阵, state 继承状态
// 返回: *GeometryPath 页面裁剪, error 几何错误 // 返回: *GeometryPath 页面裁剪, error 几何错误
func (r *Renderer) objectGeometryClip(clips *Clips, boundary string, local Matrix, state RenderState) (*GeometryPath, error) { func (r *Renderer) objectGeometryClip(clips *Clips, boundary string, local Matrix, state RenderState) (*GeometryPath, error) {
clip := state.Clip
if boundary != "" {
box, err := ParseBox(boundary)
if err != nil {
return nil, err
}
if box.W <= 0 || box.H <= 0 {
return new(GeometryPath), nil
}
matrix, _ := renderObjectMatrix(boundary, IdentityMatrix, state)
clip, err = r.geometryExtentClip(box.W, box.H, matrix, clip)
if err != nil {
return nil, err
}
}
if clips == nil { if clips == nil {
return state.Clip, nil return clip, nil
} }
matrix, _ := renderObjectMatrix(boundary, local, state) matrix, _ := renderObjectMatrix(boundary, local, state)
if clips.TransFlag != nil && !*clips.TransFlag { if clips.TransFlag != nil && !*clips.TransFlag {
@@ -390,17 +405,7 @@ func (r *Renderer) objectGeometryClip(clips *Clips, boundary string, local Matri
if err != nil { if err != nil {
return nil, err return nil, err
} }
return geometry.Clip(r, clips, matrix, state.Clip) return geometry.Clip(r, clips, matrix, clip)
}
// clipGeometry 将非零填充路径与页面裁剪相交
// 入参: geometry 几何后端, path 页面路径, clip 页面裁剪
// 返回: GeometryPath 裁剪路径, error 几何错误
func clipGeometry(geometry GeometryBackend, path GeometryPath, clip *GeometryPath) (GeometryPath, error) {
if clip == nil {
return path, nil
}
return geometry.Combine(path, *clip, GeometryIntersect)
} }
// addGeometrySpan 以公共几何语义收集文字选择区域,保留旋转和斜切 // addGeometrySpan 以公共几何语义收集文字选择区域,保留旋转和斜切
+51 -30
View File
@@ -198,6 +198,17 @@ func (r *Renderer) walkComposite(object CompositeGraphicUnit, state RenderState,
boundary = state.Parent.Multiply(boundary) boundary = state.Parent.Multiply(boundary)
} }
matrix := boundary.Multiply(NewMatrix(object.CTM)) matrix := boundary.Multiply(NewMatrix(object.CTM))
var err error
if object.Boundary != "" {
state.Clip, err = r.geometryExtentClip(box.W, box.H, boundary, state.Clip)
if err != nil {
return err
}
}
state.Clip, err = r.compositeExtentClip(object, matrix, state.Clip)
if err != nil {
return err
}
clips, clipMatrix := object.Clips, matrix clips, clipMatrix := object.Clips, matrix
if clips != nil && clips.TransFlag != nil && !*clips.TransFlag { if clips != nil && clips.TransFlag != nil && !*clips.TransFlag {
copy := *clips copy := *clips
@@ -236,38 +247,48 @@ func (r *Renderer) walkComposite(object CompositeGraphicUnit, state RenderState,
} }
} }
} }
if len(object.Objects) != 0 { for member := range object.objects() {
for _, member := range object.Objects { member = mergeGraphicObjectAlpha(member, object.Alpha)
member = mergeGraphicObjectAlpha(member, object.Alpha) if err := r.walkObject(&member, state, visitor, references); err != nil {
if err := r.walkObject(&member, state, visitor, references); err != nil {
return err
}
}
return nil
}
for _, member := range object.ImageObject {
member.Alpha = mergeAlpha(member.Alpha, object.Alpha)
if err := visitor.DrawObject(&GraphicObject{Type: "ImageObject", ImageObject: member}, state); err != nil {
return err
}
}
for _, member := range object.PathObject {
member.Alpha = mergeAlpha(member.Alpha, object.Alpha)
if err := visitor.DrawObject(&GraphicObject{Type: "PathObject", PathObject: member}, state); err != nil {
return err
}
}
for _, member := range object.TextObject {
member.Alpha = mergeAlpha(member.Alpha, object.Alpha)
if err := visitor.DrawObject(&GraphicObject{Type: "TextObject", TextObject: member}, state); err != nil {
return err
}
}
for _, member := range object.CompositeGraphicUnit {
member.Alpha = mergeAlpha(member.Alpha, object.Alpha)
if err := r.walkComposite(member, state, visitor, references); err != nil {
return err return err
} }
} }
return nil return nil
} }
// compositeExtentClip 按矢量资源尺寸裁剪内容,资源裁剪随实例变换
// 入参: object 矢量资源, matrix 资源到页面矩阵, parent 父裁剪
// 返回: *GeometryPath 页面裁剪, error 尺寸或几何错误
func (r *Renderer) compositeExtentClip(object CompositeGraphicUnit, matrix Matrix, parent *GeometryPath) (*GeometryPath, error) {
if !object.extentSet && object.Width == 0 && object.Height == 0 {
return parent, nil
}
return r.geometryExtentClip(object.Width, object.Height, matrix, parent)
}
// geometryExtentClip 按给定坐标系限制宽高并与父裁剪求交
// 入参: width 宽度, height 高度, matrix 到页面矩阵, parent 父裁剪
// 返回: *GeometryPath 页面裁剪, error 尺寸或几何错误
func (r *Renderer) geometryExtentClip(width, height float64, matrix Matrix, parent *GeometryPath) (*GeometryPath, error) {
if !finite(width) || !finite(height) || width < 0 || height < 0 {
return nil, fmt.Errorf("invalid clipping dimensions")
}
if width == 0 || height == 0 {
return new(GeometryPath), nil
}
geometry, err := r.Geometry()
if err != nil {
return nil, err
}
clip, err := geometry.Transform(geometryRectangle(Box{W: width, H: height}), matrix)
if err != nil {
return nil, err
}
if parent != nil {
clip, err = clipGeometry(geometry, clip, parent)
if err != nil {
return nil, err
}
}
return &clip, nil
}
+4 -1
View File
@@ -51,9 +51,11 @@ type Renderer struct {
preparedFonts map[string]*PreparedFont preparedFonts map[string]*PreparedFont
fontPreparations *renderCache[[32]byte, PreparedFont] fontPreparations *renderCache[[32]byte, PreparedFont]
fontSourcesCache *fontSourceCache fontSourcesCache *fontSourceCache
fontMetrics renderCache[[32]byte, FontMetrics] fontMetrics *renderCache[[32]byte, FontMetrics]
glyphOutlines renderCache[glyphOutlineKey, GeometryPath] glyphOutlines renderCache[glyphOutlineKey, GeometryPath]
renderError error renderError error
patternReferences map[*Pattern]bool
patternPage *Matrix
TransparentBackground bool TransparentBackground bool
} }
@@ -100,6 +102,7 @@ func (r *Renderer) childRenderer(reader *Reader) *Renderer {
renderer := NewRenderer(reader, opts...) renderer := NewRenderer(reader, opts...)
renderer.sharedBackendStates = r.sharedBackendStates renderer.sharedBackendStates = r.sharedBackendStates
renderer.fontPreparations = r.fontPreparations renderer.fontPreparations = r.fontPreparations
renderer.fontMetrics = r.fontMetrics
renderer.decodeImages = r.decodeImages renderer.decodeImages = r.decodeImages
renderer.TransparentBackground = r.TransparentBackground renderer.TransparentBackground = r.TransparentBackground
return renderer return renderer
+10 -5
View File
@@ -15,7 +15,6 @@
package ofdgo package ofdgo
import ( import (
"fmt"
"image" "image"
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
@@ -66,10 +65,10 @@ func (r *clipRenderer) add(path *canvas.Path) {
} }
// buildObjectClipPath 构建对象裁剪路径并应用父级变换 // buildObjectClipPath 构建对象裁剪路径并应用父级变换
// 入参: clips 裁剪对象, pageH 页面高度, bx 边界X坐标, by 边界Y坐标, objectCTM 对象CTM, parentCTM 父级CTM, boundaryInCTM 边界是否参与父级CTM // 入参: clips 裁剪对象, pageH 页面高度, boundary 外接矩形, objectCTM 对象CTM, parentCTM 父级CTM, boundaryInCTM 边界是否参与父级CTM
// 返回: *canvas.Path 路径对象 // 返回: *canvas.Path 路径对象
func (r *Renderer) buildObjectClipPath(clips *Clips, pageH float64, bx, by float64, objectCTM Matrix, parentCTM *Matrix, boundaryInCTM bool) *canvas.Path { func (r *Renderer) buildObjectClipPath(clips *Clips, pageH float64, boundary string, objectCTM Matrix, parentCTM *Matrix, boundaryInCTM bool) *canvas.Path {
path, err := r.objectGeometryClip(clips, fmt.Sprintf("%g %g 0 0", bx, by), objectCTM, RenderState{Parent: parentCTM, BoundaryInCTM: boundaryInCTM}) path, err := r.objectGeometryClip(clips, boundary, objectCTM, RenderState{Parent: parentCTM, BoundaryInCTM: boundaryInCTM})
if err != nil { if err != nil {
r.renderError = err r.renderError = err
return nil return nil
@@ -116,7 +115,7 @@ func (r *Renderer) buildClipPath(clips *Clips, pageH float64, bx, by float64, ob
m := areaCTM.Multiply(TranslationMatrix(box.X, box.Y)).Multiply(ctm) m := areaCTM.Multiply(TranslationMatrix(box.X, box.Y)).Multiply(ctm)
ctx := canvas.NewContext(renderer) ctx := canvas.NewContext(renderer)
ctx.SetView(canvas.Matrix{{m.a, -m.c, bx + m.e}, {-m.b, m.d, pageH - by - m.f}}) ctx.SetView(canvas.Matrix{{m.a, -m.c, bx + m.e}, {-m.b, m.d, pageH - by - m.f}})
renderer.clip = r.buildObjectClipPath(textObj.Clips, pageH, box.X, box.Y, ctm, &areaCTM, true) renderer.clip = r.buildObjectClipPath(textObj.Clips, pageH, textObj.Boundary, ctm, &areaCTM, true)
if renderer.clip != nil { if renderer.clip != nil {
renderer.clip = renderer.clip.Translate(bx, -by) renderer.clip = renderer.clip.Translate(bx, -by)
} }
@@ -167,6 +166,9 @@ func intersectClipPath(parent, current *canvas.Path) *canvas.Path {
if currentOK && currentRect.Contains(parent.FastBounds()) { if currentOK && currentRect.Contains(parent.FastBounds()) {
return parent return parent
} }
if result, ok := intersectConvexCanvasPaths(parent, current); ok {
return result
}
return parent.And(current) return parent.And(current)
} }
@@ -208,6 +210,9 @@ func applyClipPath(path, clip *canvas.Path) *canvas.Path {
return pathRect.And(rect).ToPath() return pathRect.And(rect).ToPath()
} }
} }
if result, ok := intersectConvexCanvasPaths(path, clip); ok {
return result
}
return path.And(clip) return path.And(clip)
} }
+152
View File
@@ -0,0 +1,152 @@
// 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 (
"math"
"github.com/tdewolff/canvas"
)
// intersectConvexCanvasPaths 逐边裁剪单个凸多边形,避免仿射矩形共边进入曲线扫线
// 入参: left 左侧路径, right 右侧路径
// 返回: *canvas.Path 交集路径, bool 是否适用凸多边形裁剪
func intersectConvexCanvasPaths(left, right *canvas.Path) (*canvas.Path, bool) {
a, ok := convexCanvasPolygon(left)
if !ok {
return nil, false
}
b, ok := convexCanvasPolygon(right)
if !ok {
return nil, false
}
for i, start := range b {
end := b[(i+1)%len(b)]
dx, dy := end.X-start.X, end.Y-start.Y
length := math.Hypot(dx, dy)
if length == 0 {
continue
}
dx, dy = dx/length, dy/length
distance := func(p canvas.Point) float64 { return convexCanvasDistance(start, p, dx, dy) }
sign := 1.0
for _, p := range b {
if d := distance(p); d != 0 {
sign = math.Copysign(1, d)
break
}
}
if len(a) == 0 {
break
}
var clipped []canvas.Point
appendPoint := func(p canvas.Point) {
if len(clipped) == 0 || p != clipped[len(clipped)-1] {
clipped = append(clipped, p)
}
}
previous := a[len(a)-1]
previousDistance := sign * distance(previous)
if !finite(previousDistance) {
return nil, false
}
for _, p := range a {
d := sign * distance(p)
if !finite(d) {
return nil, false
}
if (d >= 0) != (previousDistance >= 0) {
scale := math.Max(math.Abs(d), math.Abs(previousDistance))
fraction := (previousDistance / scale) / (previousDistance/scale - d/scale)
intersection := geometryLerp(Point{previous.X, previous.Y}, Point{p.X, p.Y}, fraction)
appendPoint(canvas.Point{X: intersection.X, Y: intersection.Y})
}
if d >= 0 {
appendPoint(p)
}
previous, previousDistance = p, d
}
if len(clipped) > 1 && clipped[0] == clipped[len(clipped)-1] {
clipped = clipped[:len(clipped)-1]
}
a = clipped
}
result := &canvas.Path{}
if len(a) >= 3 {
result.MoveTo(a[0].X, a[0].Y)
for _, p := range a[1:] {
result.LineTo(p.X, p.Y)
}
result.Close()
}
return result, true
}
// convexCanvasPolygon 检查单个闭合直线轮廓的所有顶点位于各边的同一侧
// 入参: path 路径
// 返回: []canvas.Point 独立顶点, bool 是否为非退化凸多边形
func convexCanvasPolygon(path *canvas.Path) ([]canvas.Point, bool) {
if path == nil || path.HasSubpaths() || !path.Closed() {
return nil, false
}
data := path.Data()
for i := 0; i < len(data); i += 4 {
if i+4 > len(data) || data[i] != canvas.MoveToCmd && data[i] != canvas.LineToCmd && data[i] != canvas.CloseCmd {
return nil, false
}
}
points := path.Coords()
if len(points) < 4 || points[0] != points[len(points)-1] {
return nil, false
}
points = points[:len(points)-1]
positive := false
for i, start := range points {
end := points[(i+1)%len(points)]
dx, dy := end.X-start.X, end.Y-start.Y
length := math.Hypot(dx, dy)
if !finite(length) {
return nil, false
}
if length == 0 {
continue
}
dx, dy = dx/length, dy/length
sign := 0.0
for _, p := range points {
d := convexCanvasDistance(start, p, dx, dy)
if !finite(d) || sign*d < 0 {
return nil, false
}
if d != 0 {
sign, positive = math.Copysign(1, d), true
}
}
}
return points, positive
}
// convexCanvasDistance 按浮点乘法误差界识别共线点,不使用固定坐标容差
// 入参: start 边起点, point 待测顶点, dx 横向单位分量, dy 纵向单位分量
// 返回: float64 顶点到边的有向距离
func convexCanvasDistance(start, point canvas.Point, dx, dy float64) float64 {
x, y := point.X-start.X, point.Y-start.Y
distance := math.FMA(dx, y, -dy*x)
errorBound := (math.Abs(dx*y) + math.Abs(dy*x)) * 0x1p-49
if finite(errorBound) && math.Abs(distance) <= errorBound {
return 0
}
return distance
}
+8 -1
View File
@@ -95,7 +95,7 @@ func (r *Renderer) renderImage(ctx *canvas.Context, obj ImageObject, pageH float
{-ctm.b / imgW, ctm.d / imgH, pageH - box.Y - ctm.d - ctm.f}, {-ctm.b / imgW, ctm.d / imgH, pageH - box.Y - ctm.d - ctm.f},
} }
} }
clipPath := intersectClipPath(parentClip, r.buildObjectClipPath(obj.Clips, pageH, box.X, box.Y, localCTM, parentCTM, boundaryInCTM)) clipPath := intersectClipPath(parentClip, r.buildObjectClipPath(obj.Clips, pageH, obj.Boundary, localCTM, parentCTM, boundaryInCTM))
imageClip := clipPath imageClip := clipPath
compiler, compiled := ctx.Renderer.(*canvasPageCompiler) compiler, compiled := ctx.Renderer.(*canvasPageCompiler)
if compiled && imageClip != nil && imageClipContains(img, imageClip, m) { if compiled && imageClip != nil && imageClipContains(img, imageClip, m) {
@@ -171,6 +171,9 @@ func imageClipContains(img image.Image, clipPath *canvas.Path, m canvas.Matrix)
if rect, ok := rectangularPath(clipPath); ok { if rect, ok := rectangularPath(clipPath); ok {
return rect.Contains(outline.FastBounds()) return rect.Contains(outline.FastBounds())
} }
if geometryConvexContains(*geometryFromCanvasPath(clipPath), *geometryFromCanvasPath(outline)) {
return true
}
return clipPath.Contains(outline) return clipPath.Contains(outline)
} }
@@ -190,6 +193,7 @@ func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canva
if imageClipContains(img, clipPath, m) { if imageClipContains(img, clipPath, m) {
return img return img
} }
original := img
source, err := imagePixelData(img) source, err := imagePixelData(img)
if err != nil { if err != nil {
r.renderError = err r.renderError = err
@@ -231,6 +235,9 @@ func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canva
draw.Draw(mask, mask.Bounds(), result, result.Bounds().Min, draw.Src) draw.Draw(mask, mask.Bounds(), result, result.Bounds().Min, draw.Src)
} }
if mask.Opaque() { if mask.Opaque() {
if scale <= 1 {
return original
}
return img return img
} }
source = img source = img
+1 -1
View File
@@ -158,7 +158,7 @@ func (r *Renderer) measureImage(ctx *canvas.Context, obj ImageObject, pageH floa
} }
} }
p := canvas.Rectangle(1, 1).Transform(canvas.Matrix{{m.a, m.c, m.e}, {-m.b, -m.d, pageH - m.f}}) p := canvas.Rectangle(1, 1).Transform(canvas.Matrix{{m.a, m.c, m.e}, {-m.b, -m.d, pageH - m.f}})
clip := intersectClipPath(parentClip, r.buildObjectClipPath(obj.Clips, pageH, box.X, box.Y, ctm, parentCTM, boundaryInCTM)) clip := intersectClipPath(parentClip, r.buildObjectClipPath(obj.Clips, pageH, obj.Boundary, ctm, parentCTM, boundaryInCTM))
ctx.Renderer.(*boundsRenderer).add(applyClipPath(p, clip)) ctx.Renderer.(*boundsRenderer).add(applyClipPath(p, clip))
if obj.Border != nil { if obj.Border != nil {
border := *obj.Border border := *obj.Border
+23 -3
View File
@@ -15,6 +15,7 @@
package ofdgo package ofdgo
import ( import (
"fmt"
"image/color" "image/color"
"math" "math"
"slices" "slices"
@@ -234,7 +235,7 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64
{-parentCTM.b, parentCTM.d, pageH*(1-parentCTM.d) - parentCTM.f}, {-parentCTM.b, parentCTM.d, pageH*(1-parentCTM.d) - parentCTM.f},
}) })
} }
clipPath := intersectClipPath(parentClip, r.buildObjectClipPath(obj.Clips, pageH, bx, by, localCTM, parentCTM, boundaryInCTM)) clipPath := intersectClipPath(parentClip, r.buildObjectClipPath(obj.Clips, pageH, obj.Boundary, localCTM, parentCTM, boundaryInCTM))
shouldFill := false shouldFill := false
if obj.Fill != nil { if obj.Fill != nil {
shouldFill = *obj.Fill shouldFill = *obj.Fill
@@ -331,9 +332,22 @@ func (r *Renderer) renderPattern(ctx *canvas.Context, pattern *PatternPaint, pag
r.pageText = nil r.pageText = nil
defer func() { r.pageText = text }() defer func() { r.pageText = text }()
} }
if clip == nil || clip.Empty() || len(pattern.CellContent.Objects) == 0 { if clip == nil || clip.Empty() || pattern.CellContent.empty() {
return return
} }
if r.patternReferences[pattern.Pattern] {
r.renderError = fmt.Errorf("cyclic pattern paint")
return
}
if r.patternReferences == nil {
r.patternReferences = make(map[*Pattern]bool)
}
r.patternReferences[pattern.Pattern] = true
parentPage := r.patternPage
defer func() {
delete(r.patternReferences, pattern.Pattern)
r.patternPage = parentPage
}()
xStep, yStep := pattern.XStep, pattern.YStep xStep, yStep := pattern.XStep, pattern.YStep
if xStep < pattern.Width { if xStep < pattern.Width {
xStep = pattern.Width xStep = pattern.Width
@@ -347,6 +361,8 @@ func (r *Renderer) renderPattern(ctx *canvas.Context, pattern *PatternPaint, pag
patternCTM := NewMatrix(pattern.CTM) patternCTM := NewMatrix(pattern.CTM)
if pattern.RelativeTo != "Page" { if pattern.RelativeTo != "Page" {
patternCTM = objectCTM.Multiply(patternCTM) patternCTM = objectCTM.Multiply(patternCTM)
} else if parentPage != nil {
patternCTM = parentPage.Multiply(patternCTM)
} }
invCTM, ok := patternCTM.Invert() invCTM, ok := patternCTM.Invert()
if !ok { if !ok {
@@ -387,9 +403,13 @@ func (r *Renderer) renderPattern(ctx *canvas.Context, pattern *PatternPaint, pag
if iy%2 != 0 && (pattern.ReflectMethod == "Row" || pattern.ReflectMethod == "RowAndColumn") { if iy%2 != 0 && (pattern.ReflectMethod == "Row" || pattern.ReflectMethod == "RowAndColumn") {
tileCTM = tileCTM.Multiply(Matrix{a: 1, d: -1, f: pattern.Height}) tileCTM = tileCTM.Multiply(Matrix{a: 1, d: -1, f: pattern.Height})
} }
for _, obj := range pattern.CellContent.Objects { r.patternPage = &tileCTM
for obj := range pattern.CellContent.objects() {
obj = mergeGraphicObjectAlpha(obj, pattern.Alpha) obj = mergeGraphicObjectAlpha(obj, pattern.Alpha)
r.renderObject(ctx, &obj, pageH, defaults, &tileCTM, true, clip) r.renderObject(ctx, &obj, pageH, defaults, &tileCTM, true, clip)
if r.renderError != nil {
return
}
} }
} }
} }
+21 -3
View File
@@ -49,7 +49,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
if boundaryInCTM && parentCTM != nil { if boundaryInCTM && parentCTM != nil {
objectCTM = parentCTM.Multiply(TranslationMatrix(bx, by)).Multiply(localCTM) objectCTM = parentCTM.Multiply(TranslationMatrix(bx, by)).Multiply(localCTM)
} }
clipPath := intersectClipPath(parentClip, r.buildObjectClipPath(obj.Clips, pageH, bx, by, localCTM, parentCTM, boundaryInCTM)) clipPath := intersectClipPath(parentClip, r.buildObjectClipPath(obj.Clips, pageH, obj.Boundary, localCTM, parentCTM, boundaryInCTM))
var dp *DrawParam var dp *DrawParam
if obj.DrawParam != "" { if obj.DrawParam != "" {
dp = r.getDrawParam(obj.DrawParam, nil) dp = r.getDrawParam(obj.DrawParam, nil)
@@ -325,7 +325,25 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
if fillPaint != nil || fillPattern != nil { if fillPaint != nil || fillPattern != nil {
ctx.SetFill(fillPaint) ctx.SetFill(fillPaint)
ctx.SetStrokeColor(canvas.Transparent) ctx.SetStrokeColor(canvas.Transparent)
if fillClip != nil || shadedFill || fillPattern != nil { glyphClip := fillClip
if fillClip != nil && !embeddedFont && face.FauxBold == 0 && !textCodePositioned(tc, xs, ys) && !shadedFill && fillPattern == nil && glyph.GlyphID < 0 && !hasUnderline {
if rect, ok := rectangularPath(fillClip); ok {
transform := canvas.Identity.Translate(canvasX, canvasY)
if useTextMatrix {
transform = transform.Mul(glyphMatrix)
}
if rect.Contains(glyphPath.Copy().Transform(transform.Scale(hScale, 1)).Bounds()) {
available := true
for _, char := range str {
available = available && face.Font.GlyphIndex(char) != 0
}
if available {
glyphClip, drawAsGlyphPath = nil, false
}
}
}
}
if glyphClip != nil || shadedFill || fillPattern != nil {
scaleX := hScale scaleX := hScale
if advanceLimit > 0 && glyphWidth*scaleX > advanceLimit { if advanceLimit > 0 && glyphWidth*scaleX > advanceLimit {
scaleX = advanceLimit / glyphWidth scaleX = advanceLimit / glyphWidth
@@ -335,7 +353,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
if useTextMatrix { if useTextMatrix {
textTransform = textTransform.Mul(glyphMatrix) textTransform = textTransform.Mul(glyphMatrix)
} }
glyphPath = applyClipPath(glyphPath.Copy().Transform(textTransform.Scale(scaleX, 1)), fillClip) glyphPath = applyClipPath(glyphPath.Copy().Transform(textTransform.Scale(scaleX, 1)), glyphClip)
if fillPattern != nil { if fillPattern != nil {
r.renderPattern(ctx, fillPattern, pageH, glyphPath, objectCTM) r.renderPattern(ctx, fillPattern, pageH, glyphPath, objectCTM)
} else { } else {
+24 -18
View File
@@ -78,24 +78,24 @@ type DrawParams struct {
// DrawParam 绘制参数 // DrawParam 绘制参数
type DrawParam struct { type DrawParam struct {
ID string `xml:"ID,attr"` ID string `xml:"ID,attr"`
Relative string `xml:"Relative,attr"` Relative string `xml:"Relative,attr"`
ResourceID string `xml:"ResourceID,attr"` ResourceID string `xml:"ResourceID,attr"`
BaseLoc string `xml:"BaseLoc,attr"` BaseLoc string `xml:"BaseLoc,attr"`
Link string `xml:"Link,attr"` Link string `xml:"Link,attr"`
LineWidth float64 `xml:"LineWidth,attr"` LineWidth float64 `xml:"LineWidth,attr"`
Join string `xml:"Join,attr"` Join string `xml:"Join,attr"`
Cap string `xml:"Cap,attr"` Cap string `xml:"Cap,attr"`
DashOffset *float64 `xml:"DashOffset,attr"` DashOffset *float64 `xml:"DashOffset,attr"`
DashPattern string `xml:"DashPattern,attr"` DashPattern string `xml:"DashPattern,attr"`
MiterLimit float64 `xml:"MiterLimit,attr"` MiterLimit float64 `xml:"MiterLimit,attr"`
Font string `xml:"Font,attr"` Font string `xml:"Font,attr"`
Size float64 `xml:"Size,attr"` Size float64 `xml:"Size,attr"`
Weight int `xml:"Weight,attr"` Weight int `xml:"Weight,attr"`
Italic bool `xml:"Italic,attr"` Italic bool `xml:"Italic,attr"`
FillColor *FillColor `xml:"FillColor"` FillColor *FillColor `xml:"FillColor"`
StrokeColor *StrokeColor `xml:"StrokeColor"` StrokeColor *StrokeColor `xml:"StrokeColor"`
LineWidthSet bool `xml:"-"` LineWidthSet bool `xml:"-"`
dashPatternSet bool dashPatternSet bool
} }
@@ -105,6 +105,7 @@ type CompositeGraphicUnits struct {
} }
// CompositeGraphicUnit 复合图元 // CompositeGraphicUnit 复合图元
// Width、Height、Thumbnail和Substitution用于矢量资源定义
type CompositeGraphicUnit struct { type CompositeGraphicUnit struct {
ID string `xml:"ID,attr"` ID string `xml:"ID,attr"`
BaseLoc string `xml:"BaseLoc,attr"` BaseLoc string `xml:"BaseLoc,attr"`
@@ -114,6 +115,10 @@ type CompositeGraphicUnit struct {
DrawParam string `xml:"DrawParam,attr"` DrawParam string `xml:"DrawParam,attr"`
Alpha *int `xml:"Alpha,attr"` Alpha *int `xml:"Alpha,attr"`
Visible *bool `xml:"Visible,attr"` Visible *bool `xml:"Visible,attr"`
Width float64 `xml:"Width,attr"`
Height float64 `xml:"Height,attr"`
Thumbnail string `xml:"Thumbnail"`
Substitution string `xml:"Substitution"`
Objects []GraphicObject `xml:"-"` Objects []GraphicObject `xml:"-"`
TextObject []TextObject `xml:"TextObject"` TextObject []TextObject `xml:"TextObject"`
PathObject []PathObject `xml:"PathObject"` PathObject []PathObject `xml:"PathObject"`
@@ -122,4 +127,5 @@ type CompositeGraphicUnit struct {
Clips *Clips `xml:"Clips"` Clips *Clips `xml:"Clips"`
Actions []Action `xml:"Actions>Action"` Actions []Action `xml:"Actions>Action"`
states map[string]editorCompositeState states map[string]editorCompositeState
extentSet bool
} }
+242
View File
@@ -0,0 +1,242 @@
// 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"
"encoding/asn1"
"fmt"
"math/big"
"sort"
"time"
"github.com/emmansun/gmsm/smx509"
)
// signatureCRL 已验证的直接CRL及目标条目
type signatureCRL struct {
list *smx509.RevocationList
baseNumber *big.Int
authority []byte
entry *smx509.RevocationListEntry
}
// verifySignatureCRLs 验证完整CRL及增量合并后的目标状态
// 入参: raws CRL字节, cert 证书, issuer 颁发者, at 验证时间, maxAge 最大年龄
// 返回: SignatureRevocationReport 吊销结果
func verifySignatureCRLs(raws [][]byte, cert, issuer *smx509.Certificate, at time.Time, maxAge time.Duration) SignatureRevocationReport {
result := SignatureRevocationReport{Certificate: signatureCertInfo(cert.Raw), Source: "CRL", Status: "unknown"}
lists := make([]signatureCRL, 0, len(raws))
for _, raw := range raws {
item, err := readSignatureCRL(raw, cert, issuer, at)
if err != nil {
result.Error = err.Error()
continue
}
lists = append(lists, item)
}
sort.Slice(lists, func(i, j int) bool {
if !lists[i].list.ThisUpdate.Equal(lists[j].list.ThisUpdate) {
return lists[i].list.ThisUpdate.Before(lists[j].list.ThisUpdate)
}
return lists[i].list.Number.Cmp(lists[j].list.Number) < 0
})
versions := make(map[struct {
number string
delta bool
}]*smx509.RevocationList, len(lists))
bases := make(map[string][]*signatureCRL)
for n := range lists {
item := &lists[n]
if n != 0 {
previous := lists[n-1].list
order := item.list.Number.Cmp(previous.Number)
if order < 0 || order == 0 && !item.list.ThisUpdate.Equal(previous.ThisUpdate) {
result.Error = "inconsistent CRL numbering"
return result
}
}
key := struct {
number string
delta bool
}{item.list.Number.String(), item.baseNumber != nil}
if previous := versions[key]; previous != nil && !bytes.Equal(item.list.RawTBSRevocationList, previous.RawTBSRevocationList) {
result.Error = "conflicting CRL version"
return result
}
versions[key] = item.list
if item.baseNumber == nil {
id := string(item.authority)
bases[id] = append(bases[id], item)
}
}
var selected *signatureCRL
var selectedEntry *smx509.RevocationListEntry
for n := range lists {
item := &lists[n]
if !signatureEvidenceFresh(item.list.ThisUpdate, item.list.NextUpdate, at, maxAge) {
result.Error = "stale or future CRL"
continue
}
entry := item.entry
if item.baseNumber != nil {
candidates := bases[string(item.authority)]
index := sort.Search(len(candidates), func(n int) bool {
return candidates[n].list.Number.Cmp(item.list.Number) >= 0
})
if index == 0 || candidates[index-1].list.Number.Cmp(item.baseNumber) < 0 {
result.Error = "matching complete CRL not found"
continue
}
if entry == nil {
entry = candidates[index-1].entry
}
}
if selected != nil && item.list.Number.Cmp(selected.list.Number) == 0 && !signatureCRLStatusEqual(entry, selectedEntry) {
result.Error = "conflicting complete and delta CRL status"
return result
}
selected, selectedEntry = item, entry
}
if selected == nil {
if result.Error == "" {
result.Error = "usable CRL not found"
}
return result
}
result.ThisUpdate, result.NextUpdate = selected.list.ThisUpdate, selected.list.NextUpdate
result.Checked, result.OK, result.Status, result.Error = true, true, "good", ""
if selectedEntry != nil && selectedEntry.ReasonCode != 8 {
result.OK, result.Status, result.RevokedAt = false, "revoked", selectedEntry.RevocationTime
}
return result
}
// readSignatureCRL 校验直接CRL签名、扩展及目标条目
// 入参: raw CRL字节, cert 证书, issuer 颁发者, at 验证时间
// 返回: signatureCRL 已验证列表, error 校验错误
func readSignatureCRL(raw []byte, cert, issuer *smx509.Certificate, at time.Time) (signatureCRL, error) {
crl, err := smx509.ParseRevocationList(raw)
if err != nil {
return signatureCRL{}, err
}
if len(crl.Raw) != len(raw) {
return signatureCRL{}, fmt.Errorf("trailing CRL data")
}
if !bytes.Equal(crl.RawIssuer, issuer.RawSubject) {
return signatureCRL{}, fmt.Errorf("CRL issuer mismatch")
}
if len(crl.AuthorityKeyId) != 0 && !bytes.Equal(crl.AuthorityKeyId, issuer.SubjectKeyId) {
return signatureCRL{}, fmt.Errorf("CRL authority key mismatch")
}
if err := crl.CheckSignatureFrom(issuer); err != nil {
return signatureCRL{}, err
}
if crl.ThisUpdate.IsZero() || crl.ThisUpdate.After(at) || !crl.NextUpdate.IsZero() && !crl.NextUpdate.After(crl.ThisUpdate) {
return signatureCRL{}, fmt.Errorf("invalid CRL time window")
}
item := signatureCRL{list: crl}
for n, ext := range crl.Extensions {
for _, previous := range crl.Extensions[:n] {
if ext.Id.Equal(previous.Id) {
return signatureCRL{}, fmt.Errorf("duplicate CRL extension")
}
}
switch ext.Id.String() {
case "2.5.29.35":
if ext.Critical {
return signatureCRL{}, fmt.Errorf("invalid CRL authority key extension")
}
item.authority = ext.Value
case "2.5.29.20":
number, err := signatureCRLNumber(ext.Value)
if err != nil || ext.Critical {
return signatureCRL{}, fmt.Errorf("invalid CRL number")
}
crl.Number = number
case "2.5.29.27":
number, err := signatureCRLNumber(ext.Value)
if err != nil || !ext.Critical {
return signatureCRL{}, fmt.Errorf("invalid delta CRL indicator")
}
item.baseNumber = number
case "2.5.29.28":
return signatureCRL{}, fmt.Errorf("scoped CRL not supported")
default:
if ext.Critical {
return signatureCRL{}, fmt.Errorf("unsupported critical CRL extension")
}
}
}
if crl.Number == nil || item.baseNumber != nil && item.baseNumber.Cmp(crl.Number) >= 0 {
return signatureCRL{}, fmt.Errorf("invalid CRL numbering")
}
for n := range crl.RevokedCertificateEntries {
entry := &crl.RevokedCertificateEntries[n]
for m, ext := range entry.Extensions {
for _, previous := range entry.Extensions[:m] {
if ext.Id.Equal(previous.Id) {
return signatureCRL{}, fmt.Errorf("duplicate CRL entry extension")
}
}
if ext.Critical || ext.Id.String() == "2.5.29.29" {
return signatureCRL{}, fmt.Errorf("unsupported CRL entry extension")
}
if ext.Id.String() == "2.5.29.21" {
var reason asn1.Enumerated
rest, err := asn1.Unmarshal(ext.Value, &reason)
if err != nil || len(rest) != 0 || reason < 0 || reason > 10 || reason == 7 {
return signatureCRL{}, fmt.Errorf("invalid CRL reason code")
}
}
}
if entry.ReasonCode == 8 && item.baseNumber == nil {
return signatureCRL{}, fmt.Errorf("removeFromCRL requires delta processing")
}
if entry.RevocationTime.After(crl.ThisUpdate) {
return signatureCRL{}, fmt.Errorf("CRL revocation time is in the future")
}
if entry.SerialNumber.Cmp(cert.SerialNumber) == 0 {
if item.entry != nil {
return signatureCRL{}, fmt.Errorf("duplicate CRL certificate entry")
}
item.entry = entry
}
}
return item, nil
}
// signatureCRLNumber 解析非负CRL编号
// 入参: raw ASN.1整数
// 返回: *big.Int 编号, error 编码错误
func signatureCRLNumber(raw []byte) (*big.Int, error) {
var number *big.Int
rest, err := asn1.Unmarshal(raw, &number)
if err != nil || len(rest) != 0 || number.Sign() < 0 {
return nil, fmt.Errorf("invalid CRL number encoding")
}
return number, nil
}
// signatureCRLStatusEqual 比较完整及增量列表的目标状态
// 入参: a 首个条目, b 第二个条目
// 返回: bool 状态是否一致
func signatureCRLStatusEqual(a, b *smx509.RevocationListEntry) bool {
aGood, bGood := a == nil || a.ReasonCode == 8, b == nil || b.ReasonCode == 8
if aGood || bGood {
return aGood == bGood
}
return a.ReasonCode == b.ReasonCode && a.RevocationTime.Equal(b.RevocationTime)
}
+3 -67
View File
@@ -42,6 +42,7 @@ type SignatureRevocationRequest struct {
// SignatureRevocationOptions 离线吊销检查选项 // SignatureRevocationOptions 离线吊销检查选项
// 检查签署者及制章者证书, 不代表整条证书链或TSA的吊销状态 // 检查签署者及制章者证书, 不代表整条证书链或TSA的吊销状态
// CRL支持直接完整列表及增量合并, 尚不支持间接或带作用域列表
// Fetch仅提供DER证据, 本库不主动联网, 缺少NextUpdate时必须显式设置MaxAge // Fetch仅提供DER证据, 本库不主动联网, 缺少NextUpdate时必须显式设置MaxAge
// VerifyTime缺省取验签指定时间或当前时间, 不采用签名自报时间 // VerifyTime缺省取验签指定时间或当前时间, 不采用签名自报时间
type SignatureRevocationOptions struct { type SignatureRevocationOptions struct {
@@ -120,8 +121,8 @@ func VerifySignatureRevocation(cert, issuer []byte, options SignatureRevocationO
evidence.OCSPResponses = append(cloneSignatureEvidence(evidence.OCSPResponses), fetched.OCSPResponses...) evidence.OCSPResponses = append(cloneSignatureEvidence(evidence.OCSPResponses), fetched.OCSPResponses...)
} }
var good *SignatureRevocationReport var good *SignatureRevocationReport
for _, raw := range evidence.CRLs { if len(evidence.CRLs) != 0 {
item := verifySignatureCRL(raw, cert, issuer, at, options.MaxAge) item := verifySignatureCRLs(evidence.CRLs, c, i, at, options.MaxAge)
if item.Checked && item.Status == "revoked" { if item.Checked && item.Status == "revoked" {
return item return item
} }
@@ -172,71 +173,6 @@ func signatureEvidenceFresh(thisUpdate, nextUpdate, at time.Time, maxAge time.Du
return nextUpdate.After(thisUpdate) && at.Before(nextUpdate) return nextUpdate.After(thisUpdate) && at.Before(nextUpdate)
} }
// verifySignatureCRL 验证直接完整CRL及目标序列号
// 入参: raw CRL字节, cert 证书, issuer 颁发者, at 验证时间, maxAge 最大年龄
// 返回: SignatureRevocationReport 吊销结果
func verifySignatureCRL(raw, cert, issuer []byte, at time.Time, maxAge time.Duration) (result SignatureRevocationReport) {
result = SignatureRevocationReport{Certificate: signatureCertInfo(cert), Source: "CRL", Status: "unknown"}
err := func() error {
crl, err := smx509.ParseRevocationList(raw)
if err != nil {
return err
}
parent, err := parseSignatureCertificate(issuer)
if err != nil {
return err
}
if !bytes.Equal(crl.RawIssuer, parent.RawSubject) {
return fmt.Errorf("CRL issuer mismatch")
}
if len(crl.AuthorityKeyId) != 0 && !bytes.Equal(crl.AuthorityKeyId, parent.SubjectKeyId) {
return fmt.Errorf("CRL authority key mismatch")
}
if err := crl.CheckSignatureFrom(parent); err != nil {
return err
}
for _, ext := range crl.Extensions {
if ext.Id.String() == "2.5.29.27" || ext.Id.String() == "2.5.29.28" {
return fmt.Errorf("delta or scoped CRL not supported")
}
if ext.Critical {
return fmt.Errorf("unsupported critical CRL extension")
}
}
result.ThisUpdate, result.NextUpdate = crl.ThisUpdate, crl.NextUpdate
if !signatureEvidenceFresh(crl.ThisUpdate, crl.NextUpdate, at, maxAge) {
return fmt.Errorf("stale or future CRL")
}
c, err := parseSignatureCertificate(cert)
if err != nil {
return err
}
for _, entry := range crl.RevokedCertificateEntries {
for _, ext := range entry.Extensions {
if ext.Critical || ext.Id.String() == "2.5.29.29" {
return fmt.Errorf("unsupported CRL entry extension")
}
}
if entry.SerialNumber.Cmp(c.SerialNumber) == 0 {
if entry.ReasonCode == 8 {
return fmt.Errorf("removeFromCRL requires delta processing")
}
if entry.RevocationTime.After(crl.ThisUpdate) {
return fmt.Errorf("CRL revocation time is in the future")
}
result.Checked, result.Status, result.RevokedAt = true, "revoked", entry.RevocationTime
return nil
}
}
result.Checked, result.OK, result.Status = true, true, "good"
return nil
}()
if err != nil {
result.Error = err.Error()
}
return result
}
// verifySignatureOCSP 验证OCSP签名、授权、颁发者绑定及状态 // verifySignatureOCSP 验证OCSP签名、授权、颁发者绑定及状态
// 入参: raw 响应DER, cert 证书, issuer 颁发者, at 验证时间, maxAge 最大年龄 // 入参: raw 响应DER, cert 证书, issuer 颁发者, at 验证时间, maxAge 最大年龄
// 返回: SignatureRevocationReport 吊销结果 // 返回: SignatureRevocationReport 吊销结果
+4 -2
View File
@@ -989,8 +989,10 @@ func (x *ofdXML) pattern(pattern *Pattern) {
attrs.add("RelativeTo", pattern.RelativeTo) attrs.add("RelativeTo", pattern.RelativeTo)
attrs.add("CTM", pattern.CTM) attrs.add("CTM", pattern.CTM)
x.start("Pattern", attrs) x.start("Pattern", attrs)
x.start("CellContent", nil) var content ofdAttrs
for _, object := range pattern.CellContent.Objects { content.add("Thumbnail", pattern.CellContent.Thumbnail)
x.start("CellContent", content)
for object := range pattern.CellContent.objects() {
x.object(object, false) x.object(object, false)
} }
x.end("CellContent") x.end("CellContent")