mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
1 parent
9c5ce1d66f
commit
ac198f3351
56 files changed
+2876
-672
No files matched your search
@@ -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
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
+50
-9
@@ -76,6 +76,8 @@ type editorResource struct {
|
|||||||
image *MultiMedia
|
image *MultiMedia
|
||||||
space *ColorSpace
|
space *ColorSpace
|
||||||
composite string
|
composite string
|
||||||
|
vectorKey [32]byte
|
||||||
|
vectorStates map[string]editorCompositeState
|
||||||
references []string
|
references []string
|
||||||
subset *editorFontSubset
|
subset *editorFontSubset
|
||||||
states map[string]editorCompositeState
|
states map[string]editorCompositeState
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,21 +32,40 @@ 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 err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return next.appendAnnotations(index, data)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
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) {
|
if !slices.Contains([]string{"Link", "Path", "Highlight", "Stamp", "Watermark"}, annotation.Type) {
|
||||||
return fmt.Errorf("invalid annotation type %q", annotation.Type)
|
return "", nil, fmt.Errorf("invalid annotation type %q", annotation.Type)
|
||||||
}
|
}
|
||||||
if _, err := creationBox(annotation.Appearance.Boundary); err != nil {
|
if _, err := creationBox(annotation.Appearance.Boundary); err != nil {
|
||||||
return err
|
return "", nil, err
|
||||||
}
|
}
|
||||||
if err := next.prepareSourceIDs(); err != nil {
|
if err := e.prepareSourceIDs(); err != nil {
|
||||||
return err
|
return "", nil, err
|
||||||
}
|
}
|
||||||
id = next.nextID()
|
id := e.nextID()
|
||||||
objects := annotation.Appearance.Objects
|
objects := annotation.Appearance.Objects
|
||||||
if len(objects) == 0 {
|
if len(objects) == 0 {
|
||||||
|
objects = nil
|
||||||
for _, object := range annotation.Appearance.TextObject {
|
for _, object := range annotation.Appearance.TextObject {
|
||||||
objects = append(objects, GraphicObject{Type: "TextObject", TextObject: object})
|
objects = append(objects, GraphicObject{Type: "TextObject", TextObject: object})
|
||||||
}
|
}
|
||||||
@@ -57,30 +76,30 @@ func (e *Editor) AddAnnotation(index int, annotation Annotation) (string, error)
|
|||||||
objects = append(objects, GraphicObject{Type: "ImageObject", ImageObject: object})
|
objects = append(objects, GraphicObject{Type: "ImageObject", ImageObject: object})
|
||||||
}
|
}
|
||||||
if len(annotation.Appearance.CompositeGraphicUnit) != 0 {
|
if len(annotation.Appearance.CompositeGraphicUnit) != 0 {
|
||||||
return fmt.Errorf("use composite copy to insert existing complex content")
|
return "", nil, fmt.Errorf("use composite copy to insert existing complex content")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var content []byte
|
var content []byte
|
||||||
for _, object := range objects {
|
for _, object := range objects {
|
||||||
prepared, err := next.prepareObject(next.nextID(), cloneEditorData(object))
|
prepared, err := e.prepareObject(e.nextID(), cloneEditorData(object))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", nil, err
|
||||||
}
|
}
|
||||||
data, err := editorObjectXML(prepared)
|
data, err := editorObjectXML(prepared)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", nil, err
|
||||||
}
|
}
|
||||||
content = append(content, bytes.TrimPrefix(data, []byte(xml.Header))...)
|
content = append(content, bytes.TrimPrefix(data, []byte(xml.Header))...)
|
||||||
}
|
}
|
||||||
appearance, err := editorXMLContainer("Appearance", ofdAttrs{{Name: xml.Name{Local: "Boundary"}, Value: annotation.Appearance.Boundary}}, content)
|
appearance, err := editorXMLContainer("Appearance", ofdAttrs{{Name: xml.Name{Local: "Boundary"}, Value: annotation.Appearance.Boundary}}, content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", nil, err
|
||||||
}
|
}
|
||||||
date := annotation.LastModDate
|
date := annotation.LastModDate
|
||||||
if date == "" {
|
if date == "" {
|
||||||
date = time.Now().Format("2006-01-02")
|
date = time.Now().Format("2006-01-02")
|
||||||
} else if _, err := time.Parse("2006-01-02", date); err != nil {
|
} else if _, err := time.Parse("2006-01-02", date); err != nil {
|
||||||
return fmt.Errorf("invalid annotation date %q: %w", date, err)
|
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},
|
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}}
|
{Name: xml.Name{Local: "Creator"}, Value: annotation.Creator}, {Name: xml.Name{Local: "LastModDate"}, Value: date}}
|
||||||
@@ -100,35 +119,27 @@ func (e *Editor) AddAnnotation(index int, annotation Annotation) (string, error)
|
|||||||
var parameters []byte
|
var parameters []byte
|
||||||
for _, parameter := range annotation.Parameters.Parameter {
|
for _, parameter := range annotation.Parameters.Parameter {
|
||||||
if parameter.Name == "" {
|
if parameter.Name == "" {
|
||||||
return fmt.Errorf("empty annotation parameter name")
|
return "", nil, fmt.Errorf("empty annotation parameter name")
|
||||||
}
|
}
|
||||||
var value bytes.Buffer
|
var value bytes.Buffer
|
||||||
if err := xml.EscapeText(&value, []byte(parameter.Value)); err != nil {
|
if err := xml.EscapeText(&value, []byte(parameter.Value)); err != nil {
|
||||||
return err
|
return "", nil, err
|
||||||
}
|
}
|
||||||
data, err := editorXMLContainer("Parameter", ofdAttrs{{Name: xml.Name{Local: "Name"}, Value: parameter.Name}}, value.Bytes())
|
data, err := editorXMLContainer("Parameter", ofdAttrs{{Name: xml.Name{Local: "Name"}, Value: parameter.Name}}, value.Bytes())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", nil, err
|
||||||
}
|
}
|
||||||
parameters = append(parameters, bytes.TrimPrefix(data, []byte(xml.Header))...)
|
parameters = append(parameters, bytes.TrimPrefix(data, []byte(xml.Header))...)
|
||||||
}
|
}
|
||||||
data, err := editorXMLContainer("Parameters", nil, parameters)
|
data, err := editorXMLContainer("Parameters", nil, parameters)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", nil, err
|
||||||
}
|
}
|
||||||
content = append(content, bytes.TrimPrefix(data, []byte(xml.Header))...)
|
content = append(content, bytes.TrimPrefix(data, []byte(xml.Header))...)
|
||||||
}
|
}
|
||||||
content = append(content, bytes.TrimPrefix(appearance, []byte(xml.Header))...)
|
content = append(content, bytes.TrimPrefix(appearance, []byte(xml.Header))...)
|
||||||
data, err := editorXMLContainer("Annot", attrs, content)
|
data, err := editorXMLContainer("Annot", attrs, content)
|
||||||
if err != nil {
|
return id, data, err
|
||||||
return err
|
|
||||||
}
|
|
||||||
return next.appendAnnotations(index, data)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return id, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Annotation 获取注解的独立内容快照,修改原注解应使用局部编辑方法以保留扩展字段
|
// Annotation 获取注解的独立内容快照,修改原注解应使用局部编辑方法以保留扩展字段
|
||||||
|
|||||||
+45
-8
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -474,12 +474,23 @@ func (e *Editor) compositeMembers(n *editorCompositeNode, reader *Reader, render
|
|||||||
clips, clipMatrix = ©, boundary
|
clips, clipMatrix = ©, 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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
|
||||||
content, err := editorXMLContainer("Content", nil, content)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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)))
|
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
|
||||||
|
}
|
||||||
|
group, err := e.vectorInstance(renderer, copies)
|
||||||
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 错误信息
|
||||||
|
|||||||
@@ -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 固定对象的绘制参数,避免目标容器改变颜色、虚线和线帽
|
||||||
|
|||||||
@@ -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)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+74
-12
@@ -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,17 +192,35 @@ 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
|
||||||
}
|
}
|
||||||
|
var contentSupported func(*editorXML) bool
|
||||||
|
contentSupported = func(content *editorXML) bool {
|
||||||
|
attributes := ""
|
||||||
|
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
|
||||||
|
}
|
||||||
|
if member.name.Local == "PageBlock" {
|
||||||
|
if !contentSupported(member) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else if !slices.Contains([]string{"TextObject", "PathObject", "ImageObject", "CompositeObject", "CompositeGraphicUnit"}, member.name.Local) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
for _, child := range node.children {
|
for _, child := range node.children {
|
||||||
switch child.name.Local {
|
switch child.name.Local {
|
||||||
case "Content":
|
case "Content", "PageBlock":
|
||||||
if !editorXMLAttributes(child, "") {
|
if !contentSupported(child) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
for _, member := range child.children {
|
|
||||||
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) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "Clips":
|
case "Clips":
|
||||||
if !editorXMLSupported(child) {
|
if !editorXMLSupported(child) {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 校验路径描边尺寸、端点和虚线
|
||||||
|
|||||||
@@ -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 = ©
|
||||||
|
}
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -177,6 +177,7 @@ 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
|
||||||
|
if distance != 0 {
|
||||||
dot := g.freedom[0]*g.projection[0] + g.freedom[1]*g.projection[1]
|
dot := g.freedom[0]*g.projection[0] + g.freedom[1]*g.projection[1]
|
||||||
if math.Abs(dot) < 1.0/16384 {
|
if math.Abs(dot) < 1.0/16384 {
|
||||||
v.err = fmt.Errorf("perpendicular TrueType movement vectors")
|
v.err = fmt.Errorf("perpendicular TrueType movement vectors")
|
||||||
@@ -184,6 +185,7 @@ func (v *ttInterpreter) move(point *ttPoint, distance int32, touch bool) {
|
|||||||
}
|
}
|
||||||
point.x += int32(math.Round(float64(distance) * g.freedom[0] / dot))
|
point.x += int32(math.Round(float64(distance) * g.freedom[0] / dot))
|
||||||
point.y += int32(math.Round(float64(distance) * g.freedom[1] / 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
@@ -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
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
+16
-5
@@ -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
@@ -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 ""
|
||||||
|
|||||||
+201
-16
@@ -31,6 +31,8 @@ type pdfCompositeNode struct {
|
|||||||
image *pdfgo.ImageMark
|
image *pdfgo.ImageMark
|
||||||
group *pdfgo.GroupMark
|
group *pdfgo.GroupMark
|
||||||
children []pdfCompositeNode
|
children []pdfCompositeNode
|
||||||
|
textObject *pdfgo.TextObject
|
||||||
|
glyph *pdfgo.TextMark
|
||||||
}
|
}
|
||||||
|
|
||||||
// pdfCompositePixel 保存非预乘源空间分量及累计透明度
|
// pdfCompositePixel 保存非预乘源空间分量及累计透明度
|
||||||
@@ -49,7 +51,10 @@ type pdfCompositor struct {
|
|||||||
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 区分图元的填充与描边几何
|
||||||
@@ -70,9 +75,14 @@ type pdfCompositeGeometry struct {
|
|||||||
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
|
||||||
|
imageMatrix map[*pdfgo.ImageMark]pdfgo.Matrix
|
||||||
masks map[*pdfgo.SoftMask][]pdfCompositeNode
|
masks map[*pdfgo.SoftMask][]pdfCompositeNode
|
||||||
meshes map[*pdfgo.MeshGradient][]pdfMeshTriangle
|
meshes map[*pdfgo.MeshGradient][]pdfMeshTriangle
|
||||||
patterns map[pdfgo.Paint]*pdfCompositePattern
|
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
|
|
||||||
}
|
}
|
||||||
return c.importer.renderGroupScene(geometry.scene, c.box)
|
|
||||||
|
// groupBounds 缓存组内实际覆盖范围,外部字体沿用描述符保守边界
|
||||||
|
// 入参: node 透明组
|
||||||
|
// 返回: Box 组覆盖范围, error 几何编译错误
|
||||||
|
func (c *pdfCompositor) groupBounds(node pdfCompositeNode) (Box, error) {
|
||||||
|
if box, found := c.cache.groups[node.group]; found {
|
||||||
|
return box, nil
|
||||||
|
}
|
||||||
|
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,20 +250,31 @@ 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 {
|
||||||
|
var text []pdfCompositeNode
|
||||||
|
if mark.Font.Subtype != "Type3" {
|
||||||
if err := p.halftone(mark.Style); err != nil {
|
if err := p.halftone(mark.Style); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if mark.Font.Subtype != "Type3" {
|
if err := p.resolveTransfer(&mark.Style); err != nil {
|
||||||
nodes = append(nodes, pdfCompositeNode{text: &mark})
|
return err
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
if mark.Clip != nil {
|
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) {
|
||||||
return &pdfgo.UnsupportedError{Feature: "Type3 text clipping"}
|
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]
|
glyphs := active[mark.Font]
|
||||||
if glyphs == nil {
|
if glyphs == nil {
|
||||||
glyphs = make(map[string]bool)
|
glyphs = make(map[string]bool)
|
||||||
@@ -182,11 +285,24 @@ func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error) ([]p
|
|||||||
return fmt.Errorf("recursive PDF Type3 glyph %q", glyph.Name)
|
return fmt.Errorf("recursive PDF Type3 glyph %q", glyph.Name)
|
||||||
}
|
}
|
||||||
glyphs[glyph.Name] = true
|
glyphs[glyph.Name] = true
|
||||||
err := p.reader.WalkType3Glyph(p.ctx, mark, index, v)
|
children, err := collect(func(visitor pdfgo.Visitor) error { return p.reader.WalkType3Glyph(p.ctx, mark, index, visitor) })
|
||||||
delete(glyphs, glyph.Name)
|
delete(glyphs, glyph.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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))})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+215
-41
@@ -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 transfer *pdfgo.TransferFunction
|
||||||
|
imageShape := false
|
||||||
|
if c.transfers != nil {
|
||||||
|
transfer, err = c.selectTransfer(node, outline)
|
||||||
|
if err != nil {
|
||||||
|
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
|
var processMask [4]bool
|
||||||
processOverprint := false
|
processOverprint := false
|
||||||
if node.image != nil && overprint && space.Model == "DeviceCMYK" && !space.Calibrated() {
|
if overprint && space.Model == "DeviceCMYK" && !space.Calibrated() {
|
||||||
|
if node.image != nil {
|
||||||
processMask, processOverprint, err = pdfImageProcessColorants(node.image.Image)
|
processMask, processOverprint, err = pdfImageProcessColorants(node.image.Image)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 !found {
|
||||||
|
var ok bool
|
||||||
|
inverse, ok = mark.Matrix.Inverse()
|
||||||
if !ok {
|
if !ok {
|
||||||
return [4]float64{}, 0, fmt.Errorf("invalid PDF image matrix")
|
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 {
|
||||||
|
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
|
return values, false, nil
|
||||||
}
|
}
|
||||||
var err error
|
}
|
||||||
if g := paint.Axial; g != nil {
|
inverse, ok := g.Matrix.Inverse()
|
||||||
values, err = g.ValuesAt(t)
|
if !ok {
|
||||||
|
return values, false, fmt.Errorf("singular function shading transform")
|
||||||
|
}
|
||||||
|
local := inverse.Apply(point)
|
||||||
source, intent = g.Space, g.Intent
|
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 {
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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])
|
||||||
|
|||||||
@@ -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"}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+38
-17
@@ -37,12 +37,7 @@ type pdfMeshTriangle struct {
|
|||||||
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,24 +217,50 @@ 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++ {
|
||||||
sample := grid[y*w+x]
|
if box := mesh.Bounds; box != nil {
|
||||||
if sample.patch == 0 {
|
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
|
continue
|
||||||
}
|
}
|
||||||
values, err := mesh.ValuesAt(sample.patch-1, sample.u, sample.v)
|
}
|
||||||
|
sample := grid[y*w+x]
|
||||||
|
if sample.patch == 0 && mesh.Background == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
values := background
|
||||||
|
if sample.patch != 0 {
|
||||||
|
var err error
|
||||||
|
values, err = mesh.ValuesAt(sample.patch-1, sample.u, sample.v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
values, err = space.Convert(values[:mesh.Space.Components()], mesh.Space, mesh.Intent)
|
values, err = space.ConvertWith(values[:mesh.Space.Components()], mesh.Space, mesh.Intent, conversion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
||||||
pixel.values[i] += value
|
pixel.values[i] += value
|
||||||
@@ -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
@@ -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")
|
|
||||||
}
|
}
|
||||||
metrics, err = p.editor.backends.Fonts.OpenFont(p.editor.fonts[id].Data)
|
fill, stroke, visible := paintMode == 0 || paintMode == 2, paintMode == 1 || paintMode == 2, paintMode != 3
|
||||||
if err != nil {
|
if stroke && !embedded {
|
||||||
return fmt.Errorf("PDF font %s metrics: %w", font.Name, err)
|
if resolved, err := p.editor.editorFont(id); err == nil {
|
||||||
}
|
if provider, ok := resolved.(FontOutlines); ok {
|
||||||
p.fontMetrics[id] = metrics
|
metrics, outline = resolved, provider
|
||||||
}
|
for _, glyph := range mark.Glyphs {
|
||||||
var ok bool
|
char, _ := utf8.DecodeRuneInString(glyph.Text)
|
||||||
outline, ok = metrics.(FontOutlines)
|
if utf8.RuneCountInString(glyph.Text) != 1 || resolved.GlyphIndex(char) == 0 && char != 0 {
|
||||||
if !ok {
|
metrics, outline = nil, nil
|
||||||
return fmt.Errorf("PDF font backend does not provide glyph outlines")
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
@@ -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 是否着色
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 以公共几何语义收集文字选择区域,保留旋转和斜切
|
||||||
|
|||||||
+46
-25
@@ -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,8 +247,7 @@ 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 err
|
||||||
@@ -245,29 +255,40 @@ func (r *Renderer) walkComposite(object CompositeGraphicUnit, state RenderState,
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
for _, member := range object.ImageObject {
|
|
||||||
member.Alpha = mergeAlpha(member.Alpha, object.Alpha)
|
// compositeExtentClip 按矢量资源尺寸裁剪内容,资源裁剪随实例变换
|
||||||
if err := visitor.DrawObject(&GraphicObject{Type: "ImageObject", ImageObject: member}, state); err != nil {
|
// 入参: object 矢量资源, matrix 资源到页面矩阵, parent 父裁剪
|
||||||
return err
|
// 返回: *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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, member := range object.PathObject {
|
return &clip, nil
|
||||||
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 nil
|
|
||||||
}
|
}
|
||||||
+4
-1
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
@@ -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")
|
||||||
|
|||||||
Reference in new issue
Block a user