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/font v0.0.0-20260913163313-54f98bb59ee6
|
||||
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/image v0.46.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/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI=
|
||||
github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ=
|
||||
github.com/xiaoqidun/pdfgo v0.0.0-20261001010247-f9839c149d47 h1:K81kklR5kcVJzV7EWUNCpQJHxw1v2vLI7eegfOs0xbI=
|
||||
github.com/xiaoqidun/pdfgo v0.0.0-20261001010247-f9839c149d47/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ=
|
||||
github.com/xiaoqidun/pdfgo v0.0.0-20261002050201-1eed5d9912bf h1:sST236XvU4MWCZjaH3d0hg4SsDJl6SNGL3qQdfgHP8w=
|
||||
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/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
|
||||
|
||||
+50
-9
@@ -76,6 +76,8 @@ type editorResource struct {
|
||||
image *MultiMedia
|
||||
space *ColorSpace
|
||||
composite string
|
||||
vectorKey [32]byte
|
||||
vectorStates map[string]editorCompositeState
|
||||
references []string
|
||||
subset *editorFontSubset
|
||||
states map[string]editorCompositeState
|
||||
@@ -495,8 +497,8 @@ func (e *Editor) addImage(data []byte, config image.Config, format string, decod
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// AddObject 按添加顺序放入正文图层,支持文字、路径和图片,自动分配对象ID
|
||||
// 支持已注册资源、颜色、渐变、裁剪和动作;复合图元仅支持当前文档原对象的保真复制
|
||||
// AddObject 按添加顺序放入正文图层,支持文字、路径、图片和矢量资源实例,自动分配对象ID
|
||||
// 支持已注册资源、颜色、渐变、裁剪和动作;原对象复制保留原文结构
|
||||
// 入参: page 页面索引, object 对象内容,添加后不再引用调用方的可变数据
|
||||
// 返回: 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
|
||||
case "ImageObject":
|
||||
object.ImageObject.ID = id
|
||||
case "CompositeObject":
|
||||
object.CompositeGraphicUnit.ID = id
|
||||
default:
|
||||
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 对象内容
|
||||
// 返回: 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 alpha *int
|
||||
var clips *Clips
|
||||
@@ -828,13 +844,24 @@ func (e *Editor) prepareObject(id string, object GraphicObject) (GraphicObject,
|
||||
}
|
||||
}
|
||||
if obj.Border != nil {
|
||||
if err := e.validateImageBorder(obj.Border); err != nil {
|
||||
if err := v.validateImageBorder(obj.Border); err != nil {
|
||||
return GraphicObject{}, err
|
||||
}
|
||||
}
|
||||
obj.ID = id
|
||||
boundary, ctm = obj.Boundary, obj.CTM
|
||||
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:
|
||||
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))
|
||||
}
|
||||
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
|
||||
}
|
||||
if fill == nil {
|
||||
fill = defaults.FillColor
|
||||
}
|
||||
if stroke == nil {
|
||||
stroke = (*FillColor)(defaults.StrokeColor)
|
||||
}
|
||||
}
|
||||
if err := validateObjectActions(actions); err != nil {
|
||||
return GraphicObject{}, err
|
||||
}
|
||||
if err := e.validateObjectClips(clips); err != nil {
|
||||
if err := v.validateObjectClips(clips); err != nil {
|
||||
return GraphicObject{}, err
|
||||
}
|
||||
if alpha != nil && (*alpha < 0 || *alpha > 255) {
|
||||
return GraphicObject{}, fmt.Errorf("alpha must be between 0 and 255")
|
||||
}
|
||||
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}
|
||||
}
|
||||
}
|
||||
if join != "" && join != "Miter" && join != "Round" && join != "Bevel" {
|
||||
return GraphicObject{}, fmt.Errorf("invalid line join %q", join)
|
||||
}
|
||||
return cloneEditorObject(object)
|
||||
return object, nil
|
||||
}
|
||||
|
||||
// validateObjectClips 校验对象的路径与文字裁剪,拒绝嵌套裁剪和无效资源
|
||||
// 入参: clips 裁剪集合,nil表示无裁剪
|
||||
// 返回: 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 {
|
||||
return nil
|
||||
}
|
||||
@@ -901,7 +942,7 @@ func (e *Editor) validateObjectClips(clips *Clips) error {
|
||||
if path.ID != "" || path.Clips != nil {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -909,7 +950,7 @@ func (e *Editor) validateObjectClips(clips *Clips) error {
|
||||
if text.ID != "" || text.Clips != nil || text.Actions != nil {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,21 +32,40 @@ import (
|
||||
func (e *Editor) AddAnnotation(index int, annotation Annotation) (string, error) {
|
||||
var id string
|
||||
err := e.Transaction(func(next *Editor) error {
|
||||
if index < 0 || index >= len(next.pages) {
|
||||
return fmt.Errorf("page index %d out of range", index)
|
||||
var data []byte
|
||||
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) {
|
||||
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 {
|
||||
return err
|
||||
return "", nil, err
|
||||
}
|
||||
if err := next.prepareSourceIDs(); err != nil {
|
||||
return err
|
||||
if err := e.prepareSourceIDs(); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
id = next.nextID()
|
||||
id := e.nextID()
|
||||
objects := annotation.Appearance.Objects
|
||||
if len(objects) == 0 {
|
||||
objects = nil
|
||||
for _, object := range annotation.Appearance.TextObject {
|
||||
objects = append(objects, GraphicObject{Type: "TextObject", TextObject: object})
|
||||
}
|
||||
@@ -57,30 +76,30 @@ func (e *Editor) AddAnnotation(index int, annotation Annotation) (string, error)
|
||||
objects = append(objects, GraphicObject{Type: "ImageObject", ImageObject: object})
|
||||
}
|
||||
if len(annotation.Appearance.CompositeGraphicUnit) != 0 {
|
||||
return fmt.Errorf("use composite copy to insert existing complex content")
|
||||
return "", nil, fmt.Errorf("use composite copy to insert existing complex content")
|
||||
}
|
||||
}
|
||||
var content []byte
|
||||
for _, object := range objects {
|
||||
prepared, err := next.prepareObject(next.nextID(), cloneEditorData(object))
|
||||
prepared, err := e.prepareObject(e.nextID(), cloneEditorData(object))
|
||||
if err != nil {
|
||||
return err
|
||||
return "", nil, err
|
||||
}
|
||||
data, err := editorObjectXML(prepared)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", nil, err
|
||||
}
|
||||
content = append(content, bytes.TrimPrefix(data, []byte(xml.Header))...)
|
||||
}
|
||||
appearance, err := editorXMLContainer("Appearance", ofdAttrs{{Name: xml.Name{Local: "Boundary"}, Value: annotation.Appearance.Boundary}}, content)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", nil, err
|
||||
}
|
||||
date := annotation.LastModDate
|
||||
if date == "" {
|
||||
date = time.Now().Format("2006-01-02")
|
||||
} else if _, err := time.Parse("2006-01-02", date); err != nil {
|
||||
return 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},
|
||||
{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
|
||||
for _, parameter := range annotation.Parameters.Parameter {
|
||||
if parameter.Name == "" {
|
||||
return fmt.Errorf("empty annotation parameter name")
|
||||
return "", nil, fmt.Errorf("empty annotation parameter name")
|
||||
}
|
||||
var value bytes.Buffer
|
||||
if err := xml.EscapeText(&value, []byte(parameter.Value)); err != nil {
|
||||
return err
|
||||
return "", nil, err
|
||||
}
|
||||
data, err := editorXMLContainer("Parameter", ofdAttrs{{Name: xml.Name{Local: "Name"}, Value: parameter.Name}}, value.Bytes())
|
||||
if err != nil {
|
||||
return err
|
||||
return "", nil, err
|
||||
}
|
||||
parameters = append(parameters, bytes.TrimPrefix(data, []byte(xml.Header))...)
|
||||
}
|
||||
data, err := editorXMLContainer("Parameters", nil, parameters)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", nil, err
|
||||
}
|
||||
content = append(content, bytes.TrimPrefix(data, []byte(xml.Header))...)
|
||||
}
|
||||
content = append(content, bytes.TrimPrefix(appearance, []byte(xml.Header))...)
|
||||
data, err := editorXMLContainer("Annot", attrs, content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return next.appendAnnotations(index, data)
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return id, nil
|
||||
return id, data, err
|
||||
}
|
||||
|
||||
// Annotation 获取注解的独立内容快照,修改原注解应使用局部编辑方法以保留扩展字段
|
||||
|
||||
+45
-8
@@ -32,6 +32,14 @@ type PatternStyle struct {
|
||||
CTM *string
|
||||
}
|
||||
|
||||
// editorValidation 保存同次对象校验的底纹和矢量引用路径,允许共享但拒绝循环
|
||||
type editorValidation struct {
|
||||
*Editor
|
||||
patterns map[*Pattern]bool
|
||||
composites map[string]bool
|
||||
vectors map[string]*editorCompositeNode
|
||||
}
|
||||
|
||||
// GradientStops 解析当前文档的渐变分段,保留顺序并合并透明度
|
||||
// 入参: segments 渐变分段, alpha 外层透明度
|
||||
// 返回: []ColorStop 后端无关的色标, error 颜色解析错误
|
||||
@@ -308,7 +316,8 @@ func collectPaintReferences(paint *FillColor, used map[string]bool) {
|
||||
used[back.ColorSpace] = true
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -401,18 +410,40 @@ func (e *Editor) editorMeshColor(value *FillColor) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// editorColor 校验当前文档的纯色和渐变
|
||||
// editorColor 校验当前文档的纯色、渐变和底纹
|
||||
// 入参: value 颜色
|
||||
// 返回: 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) {
|
||||
return e.editorMeshColor(value)
|
||||
}
|
||||
if value != nil && value.Pattern != nil {
|
||||
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")
|
||||
}
|
||||
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" {
|
||||
return fmt.Errorf("invalid pattern reference")
|
||||
}
|
||||
@@ -423,14 +454,20 @@ func (e *Editor) editorColor(value *FillColor) error {
|
||||
}
|
||||
base := *value
|
||||
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
|
||||
}
|
||||
for _, object := range pattern.CellContent.Objects {
|
||||
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) {
|
||||
return fmt.Errorf("nested pattern paint is not supported")
|
||||
if pattern.CellContent.Thumbnail != "" {
|
||||
if _, err := e.editorImage(pattern.CellContent.Thumbnail); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := e.prepareObject("", object); err != nil {
|
||||
}
|
||||
for object := range pattern.CellContent.objects() {
|
||||
if _, err := v.prepareObject("", object); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,12 +474,23 @@ func (e *Editor) compositeMembers(n *editorCompositeNode, reader *Reader, render
|
||||
clips, clipMatrix = ©, boundary
|
||||
}
|
||||
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 {
|
||||
geometry, err := renderer.Geometry()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clip, err = geometry.Clip(renderer, clips, clipMatrix, n.clip)
|
||||
clip, err = geometry.Clip(renderer, clips, clipMatrix, clip)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1043,6 +1054,11 @@ func editorResourceReferences(data []byte) ([]string, error) {
|
||||
var collect func(*editorXML)
|
||||
collect = func(node *editorXML) {
|
||||
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 {
|
||||
if attr.Name.Space == "" && editorObjectReference(attr.Name.Local) {
|
||||
refs[attr.Value] = true
|
||||
@@ -1062,7 +1078,53 @@ func editorResourceReferences(data []byte) ([]string, error) {
|
||||
func collectCompositeReferences(composite CompositeGraphicUnit, used map[string]bool) {
|
||||
used[composite.ResourceID] = 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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -260,7 +267,7 @@ func (e *Editor) pasteCompositeSelection(selection *CompositeSelection, paste fu
|
||||
if err := copy.update(object); err != nil {
|
||||
return err
|
||||
}
|
||||
if !source.boundaryInCTM {
|
||||
if !source.boundaryInCTM && styleErr == nil {
|
||||
if err := copy.convertCoordinates(source.parent, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -16,13 +16,15 @@ package ofdgo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// GroupCompositeObjects 组合内部同一容器的连续成员,保留标识、绘制顺序及共享资源
|
||||
// GroupCompositeObjects 组合内部同一容器的连续成员,保留绘制顺序及共享资源
|
||||
// 入参: page 页面索引, path 父路径, indexes 至少两个成员序号
|
||||
// 返回: 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")
|
||||
}
|
||||
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 {
|
||||
if !member.Capabilities.Copy || !member.Capabilities.Order {
|
||||
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 })
|
||||
siblings := nodes[0].siblings()
|
||||
first := slices.Index(siblings, nodes[0])
|
||||
var content []byte
|
||||
copies := make([]*editorCompositeNode, len(nodes))
|
||||
for i, node := range nodes {
|
||||
if first+i >= len(siblings) || siblings[first+i] != node {
|
||||
return fmt.Errorf("grouping requires consecutive members in the same container")
|
||||
}
|
||||
content = append(content, bytes.TrimPrefix(node.data, []byte(xml.Header))...)
|
||||
}
|
||||
content, err := editorXMLContainer("Content", nil, content)
|
||||
copy, err := newEditorCompositeNode(node.data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content, err = editorXMLContainer("CompositeObject", ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: e.nextID()}, {Name: xml.Name{Local: "Boundary"}, Value: "0 0 1 1"}}, bytes.TrimPrefix(content, []byte(xml.Header)))
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
for i, node := range nodes {
|
||||
var replacement []byte
|
||||
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})
|
||||
}
|
||||
@@ -67,6 +76,74 @@ func (e *Editor) GroupCompositeObjects(page int, path ObjectPath, indexes []int)
|
||||
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 展开内部组合,隔离引用资源并保留父容器的样式和裁剪
|
||||
// 入参: page 页面索引, path 父路径, index 组合序号
|
||||
// 返回: []int 展开后的成员序号, error 错误信息
|
||||
|
||||
@@ -93,11 +93,12 @@ func (e *Editor) CopyObjectsToComposite(page int, path ObjectPath, objects []Gra
|
||||
return err
|
||||
}
|
||||
if node.object.Type == "PathObject" {
|
||||
node.defaults = renderer.drawParamDefaults(e.copiedLayerStyle(source), nil)
|
||||
style, styleErr := e.resolveEditorStyle(node.object, e.copiedLayerStyle(source))
|
||||
_, fillErr := e.Color(style.PathObject.FillColor)
|
||||
_, strokeErr := e.Color((*FillColor)(style.PathObject.StrokeColor))
|
||||
if styleErr != nil && editReason(styleErr) == EditUnsupportedColor || fillErr != nil || strokeErr != nil {
|
||||
node, err = e.wrapCompositePath(node)
|
||||
node, err = e.wrapCompositePath(renderer, node)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -166,19 +167,11 @@ func (e *Editor) CopyObjectsToComposite(page int, path ObjectPath, objects []Gra
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// wrapCompositePath 通过独立容器变换复杂路径,保持渐变坐标与原始颜色节点
|
||||
// 入参: node 复杂路径
|
||||
// wrapCompositePath 通过标准矢量实例变换复杂路径,保持渐变坐标与原始颜色节点
|
||||
// 入参: renderer 资源度量器, node 复杂路径
|
||||
// 返回: *editorCompositeNode 包装节点, error 错误信息
|
||||
func (e *Editor) wrapCompositePath(node *editorCompositeNode) (*editorCompositeNode, error) {
|
||||
content, err := editorXMLContainer("Content", nil, bytes.TrimPrefix(node.data, []byte(xml.Header)))
|
||||
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)
|
||||
func (e *Editor) wrapCompositePath(renderer *Renderer, node *editorCompositeNode) (*editorCompositeNode, error) {
|
||||
return e.vectorInstance(renderer, []*editorCompositeNode{node})
|
||||
}
|
||||
|
||||
// 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})
|
||||
}
|
||||
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 != "" {
|
||||
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 {
|
||||
return "", err
|
||||
}
|
||||
data := parsed.Data
|
||||
key := editorResourceKey{checksum: sha256.Sum256(data)}
|
||||
return e.addFontResource(parsed, sha256.Sum256(parsed.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 {
|
||||
return id, nil
|
||||
}
|
||||
@@ -62,7 +68,7 @@ func (e *Editor) AddFont(file FontFile, index int) (string, error) {
|
||||
definition.ID = id
|
||||
resource := editorResource{
|
||||
name: e.packageName("Res/Fonts/Font_" + id + parsed.Extension),
|
||||
data: data,
|
||||
data: parsed.Data,
|
||||
font: &definition,
|
||||
}
|
||||
resource.font.FontFile = "/" + resource.name
|
||||
|
||||
+74
-12
@@ -26,7 +26,7 @@ import (
|
||||
// GroupObjects 将同一容器内连续的对象组合,保留标识、绘制顺序及原始内容
|
||||
// 入参: page 页面索引, ids 至少两个对象标识
|
||||
// 返回: 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)
|
||||
if err != nil {
|
||||
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")
|
||||
}
|
||||
}
|
||||
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 source *editorSourcePage
|
||||
states := make(map[string]editorCompositeState)
|
||||
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
|
||||
if origin := e.objectOrigin(editorObjectID(object)); origin != nil {
|
||||
source = origin.page
|
||||
@@ -79,12 +100,36 @@ func (e *Editor) GroupObjects(page int, ids []string) (string, error) {
|
||||
if err := e.prepareSourceIDs(); err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
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 {
|
||||
return "", err
|
||||
}
|
||||
@@ -98,7 +143,6 @@ func (e *Editor) GroupObjects(page int, ids []string) (string, error) {
|
||||
group.node.parent = parent
|
||||
group.object.CompositeGraphicUnit.states = states
|
||||
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})
|
||||
return id, nil
|
||||
}
|
||||
@@ -148,17 +192,35 @@ func editorUngroupable(node *editorXML) bool {
|
||||
if !editorXMLAttributes(node, "ID Boundary CTM DrawParam Alpha Visible ResourceID Width Height") {
|
||||
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 {
|
||||
switch child.name.Local {
|
||||
case "Content":
|
||||
if !editorXMLAttributes(child, "") {
|
||||
case "Content", "PageBlock":
|
||||
if !contentSupported(child) {
|
||||
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":
|
||||
if !editorXMLSupported(child) {
|
||||
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")
|
||||
value, err = m.copyXML(loc, candidate)
|
||||
}
|
||||
case "Font", "Substitution":
|
||||
case "Font", "Substitution", "Thumbnail":
|
||||
value, err = m.reference(strings.TrimSpace(value))
|
||||
}
|
||||
if err != nil {
|
||||
|
||||
@@ -702,6 +702,13 @@ func editorStrokeScale(ctm string) float64 {
|
||||
// 入参: border 边框
|
||||
// 返回: 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 {
|
||||
return nil
|
||||
}
|
||||
@@ -717,7 +724,7 @@ func (e *Editor) validateImageBorder(border *ImageBorder) error {
|
||||
if err := validateEditorStroke(stroke); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.editorColor((*FillColor)(border.BorderColor))
|
||||
return v.editorColor((*FillColor)(border.BorderColor))
|
||||
}
|
||||
|
||||
// validateEditorStroke 校验路径描边尺寸、端点和虚线
|
||||
|
||||
@@ -421,8 +421,8 @@ func (e *Editor) transformBorderedImage(object GraphicObject, matrix Matrix, ori
|
||||
return transformEditorMatrix(object, matrix), nil
|
||||
}
|
||||
|
||||
// transformEditorMatrix 在页面坐标中变换基本对象,保留局部绘制数据和裁剪
|
||||
// 入参: object 对象, matrix 页面变换
|
||||
// transformEditorMatrix 在所在坐标系中变换对象,保留局部绘制数据和裁剪
|
||||
// 入参: object 对象, matrix 所在坐标系中的变换
|
||||
// 返回: GraphicObject 变换后的独立对象
|
||||
func transformEditorMatrix(object GraphicObject, matrix Matrix) GraphicObject {
|
||||
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 {
|
||||
*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
|
||||
}
|
||||
|
||||
|
||||
@@ -81,10 +81,16 @@ func (e *Editor) AddWatermarks(ctx context.Context, indexes []int, object Graphi
|
||||
if _, err := creationNumbers(*ctm, 6); err != nil {
|
||||
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.SetHistoryLimit(0)
|
||||
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 {
|
||||
matrix := Matrix{a: scale, d: scale}
|
||||
if base.Type == "TextObject" {
|
||||
base.TextObject.CTM = matrix.Multiply(NewMatrix(base.TextObject.CTM)).String()
|
||||
} else {
|
||||
base.ImageObject.CTM = matrix.Multiply(NewMatrix(base.ImageObject.CTM)).String()
|
||||
}
|
||||
base = transformEditorMatrix(base, matrix)
|
||||
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
|
||||
if options.Tile {
|
||||
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
|
||||
y := (float64(row)+0.5)*area.H/rows - bounds.Y - bounds.H/2
|
||||
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" {
|
||||
object.TextObject.Boundary = boundary
|
||||
} else {
|
||||
|
||||
@@ -205,7 +205,7 @@ func (r *Renderer) resetFontBackendCache() {
|
||||
r.fontSourcesCache.fallback = ResolvedFont{}
|
||||
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}
|
||||
for _, state := range r.backendStates {
|
||||
if fonts, ok := state.(interface{ resetFonts() }); ok {
|
||||
|
||||
@@ -177,6 +177,7 @@ func ttProject(x, y int32, vector [2]float64) int32 {
|
||||
// 入参: point 点, distance 投影距离, touch 是否标记触及状态
|
||||
func (v *ttInterpreter) move(point *ttPoint, distance int32, touch bool) {
|
||||
g := &v.graphics
|
||||
if distance != 0 {
|
||||
dot := g.freedom[0]*g.projection[0] + g.freedom[1]*g.projection[1]
|
||||
if math.Abs(dot) < 1.0/16384 {
|
||||
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.y += int32(math.Round(float64(distance) * g.freedom[1] / dot))
|
||||
}
|
||||
if touch {
|
||||
point.tx = point.tx || g.freedom[0] != 0
|
||||
point.ty = point.ty || g.freedom[1] != 0
|
||||
|
||||
+33
-28
@@ -66,6 +66,12 @@ type fontUsagePage struct {
|
||||
usage map[string]int
|
||||
}
|
||||
|
||||
// fontUsageReferences 保存字体统计中的当前引用路径,不合并共享内容的用量
|
||||
type fontUsageReferences struct {
|
||||
composites map[string]bool
|
||||
patterns map[*Pattern]bool
|
||||
}
|
||||
|
||||
// Fonts 获取OFD声明的字体列表
|
||||
// 返回: []Font 字体列表, error 错误信息
|
||||
func (r *Reader) Fonts() ([]Font, error) {
|
||||
@@ -492,8 +498,8 @@ func (r *Renderer) countLayerFonts(layer Layer, usage map[string]int) {
|
||||
}
|
||||
|
||||
// countObjectFonts 统计图元字体使用次数
|
||||
// 入参: obj 图元对象, usage 字体使用次数, defaults 默认绘制参数, visited 已访问资源
|
||||
func (r *Renderer) countObjectFonts(obj GraphicObject, usage map[string]int, defaults *DrawParam, visited map[string]bool) {
|
||||
// 入参: obj 图元对象, usage 字体使用次数, defaults 默认绘制参数, visited 当前引用路径
|
||||
func (r *Renderer) countObjectFonts(obj GraphicObject, usage map[string]int, defaults *DrawParam, visited *fontUsageReferences) {
|
||||
var fill *FillColor
|
||||
var stroke *StrokeColor
|
||||
var shouldFill, shouldStroke bool
|
||||
@@ -539,50 +545,49 @@ func (r *Renderer) countObjectFonts(obj GraphicObject, usage map[string]int, def
|
||||
}
|
||||
|
||||
// countCompositeFonts 统计复合图元字体使用次数
|
||||
// 入参: cgu 复合图元, usage 字体使用次数, defaults 默认绘制参数, visited 已访问资源
|
||||
func (r *Renderer) countCompositeFonts(cgu CompositeGraphicUnit, usage map[string]int, defaults *DrawParam, visited map[string]bool) {
|
||||
// 入参: cgu 复合图元, usage 字体使用次数, defaults 默认绘制参数, visited 当前引用路径
|
||||
func (r *Renderer) countCompositeFonts(cgu CompositeGraphicUnit, usage map[string]int, defaults *DrawParam, visited *fontUsageReferences) {
|
||||
r.countClipFonts(cgu.Clips, usage)
|
||||
defaults = r.drawParamDefaults(cgu.DrawParam, defaults)
|
||||
if cgu.ResourceID != "" {
|
||||
if visited == nil {
|
||||
visited = make(map[string]bool)
|
||||
visited = &fontUsageReferences{}
|
||||
}
|
||||
if visited[cgu.ResourceID] {
|
||||
if visited.composites[cgu.ResourceID] {
|
||||
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 {
|
||||
r.countCompositeFonts(*ref, usage, defaults, visited)
|
||||
}
|
||||
defer delete(visited, cgu.ResourceID)
|
||||
defer delete(visited.composites, cgu.ResourceID)
|
||||
}
|
||||
if len(cgu.Objects) > 0 {
|
||||
for _, obj := range cgu.Objects {
|
||||
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)
|
||||
for object := range cgu.objects() {
|
||||
r.countObjectFonts(object, usage, defaults, visited)
|
||||
}
|
||||
}
|
||||
|
||||
// countPatternFonts 统计底纹单元字体使用次数
|
||||
// 入参: fill 填充或描边颜色, usage 字体使用次数, visited 已访问资源
|
||||
func (r *Renderer) countPatternFonts(fill *FillColor, usage map[string]int, visited map[string]bool) {
|
||||
// 入参: fill 填充或描边颜色, usage 字体使用次数, visited 当前引用路径
|
||||
func (r *Renderer) countPatternFonts(fill *FillColor, usage map[string]int, visited *fontUsageReferences) {
|
||||
if fill == nil || fill.Pattern == nil {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
+64
-1
@@ -145,11 +145,74 @@ func geometryRectangleBounds(path GeometryPath) (Box, bool) {
|
||||
return Box{}, false
|
||||
}
|
||||
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 {
|
||||
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) {
|
||||
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
|
||||
var result []geometryPolyline
|
||||
var run []Point
|
||||
var direction Point
|
||||
flush := func() {
|
||||
if len(run) != 0 {
|
||||
result = append(result, geometryPolyline{points: run})
|
||||
result = append(result, geometryPolyline{points: run, direction: direction})
|
||||
run = nil
|
||||
}
|
||||
}
|
||||
@@ -255,8 +256,12 @@ func (line geometryPolyline) dash(pattern []float64, total, offset float64) ([]g
|
||||
if index%2 == 0 {
|
||||
if len(run) == 0 {
|
||||
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
|
||||
remaining -= step
|
||||
|
||||
@@ -129,28 +129,29 @@ func (p GeometryPath) Transform(matrix Matrix) (GeometryPath, error) {
|
||||
func (p GeometryPath) validate() error {
|
||||
started := false
|
||||
for _, s := range p {
|
||||
points := []Point{s.End}
|
||||
points := [3]Point{s.End, s.Control1, s.Control2}
|
||||
count := 1
|
||||
switch s.Verb {
|
||||
case GeometryMove:
|
||||
started = true
|
||||
case GeometryLine:
|
||||
case GeometryQuad:
|
||||
points = append(points, s.Control1)
|
||||
count = 2
|
||||
case GeometryCubic:
|
||||
points = append(points, s.Control1, s.Control2)
|
||||
count = 3
|
||||
case GeometryArc:
|
||||
if !finite(s.RadiusX) || !finite(s.RadiusY) || !finite(s.Rotation) || s.RadiusX < 0 || s.RadiusY < 0 {
|
||||
return fmt.Errorf("invalid arc parameters")
|
||||
}
|
||||
case GeometryClose:
|
||||
points = nil
|
||||
count = 0
|
||||
default:
|
||||
return fmt.Errorf("unsupported geometry command %d", s.Verb)
|
||||
}
|
||||
if !started {
|
||||
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) {
|
||||
return fmt.Errorf("non-finite geometry coordinate")
|
||||
}
|
||||
|
||||
+74
-1
@@ -16,7 +16,10 @@ package ofdgo
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"iter"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// graphicObjectTarget 图形对象集合
|
||||
@@ -144,6 +147,19 @@ func (c *CompositeGraphicUnit) UnmarshalXML(d *xml.Decoder, start xml.StartEleme
|
||||
c.Boundary = attrValue(start, "Boundary")
|
||||
c.CTM = attrValue(start, "CTM")
|
||||
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 alpha, err := strconv.Atoi(value); err == nil {
|
||||
c.Alpha = &alpha
|
||||
@@ -170,10 +186,57 @@ func (a *Appearance) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error
|
||||
// 入参: d XML解码器, start 起始节点
|
||||
// 返回: 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)
|
||||
}
|
||||
|
||||
// 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 解析渐变分段并保留位置的缺省状态
|
||||
// 入参: d XML解码器, start 起始节点
|
||||
// 返回: error 错误信息
|
||||
@@ -279,6 +342,16 @@ func (c *CompositeGraphicUnit) decodeObject(d *xml.Decoder, start xml.StartEleme
|
||||
return err
|
||||
}
|
||||
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":
|
||||
var clips Clips
|
||||
if err := d.DecodeElement(&clips, &start); err != nil {
|
||||
|
||||
@@ -140,7 +140,9 @@ type Pattern struct {
|
||||
}
|
||||
|
||||
// PatternContent 图案单元内容
|
||||
// Objects非空时保留绘制顺序,否则依次使用文字、路径、图片和复合图元数组
|
||||
type PatternContent struct {
|
||||
Thumbnail string `xml:"Thumbnail,attr"`
|
||||
Objects []GraphicObject `xml:"-"`
|
||||
TextObject []TextObject `xml:"TextObject"`
|
||||
PathObject []PathObject `xml:"PathObject"`
|
||||
|
||||
+16
-5
@@ -68,10 +68,8 @@ type pdfImporter struct {
|
||||
matrix pdfgo.Matrix
|
||||
page int
|
||||
fontIDs map[*pdfgo.Font]string
|
||||
fontMetrics map[string]FontMetrics
|
||||
fontRepairLimit map[*pdfgo.Font]uint16
|
||||
fonts map[*pdfgo.Font]*pdfImportedFont
|
||||
fontWarnings map[string]bool
|
||||
type1Glyphs map[*pdfgo.Font]map[string]uint16
|
||||
clipTexts map[*pdfgo.TextClip]TextObject
|
||||
cmykSpace string
|
||||
objects []GraphicObject
|
||||
@@ -90,8 +88,10 @@ type pdfImporter struct {
|
||||
compositeCache *pdfCompositeCache
|
||||
compositeNodes []pdfCompositeNode
|
||||
compositeSpace *pdfgo.ColorSpace
|
||||
annotationData []byte
|
||||
resolveFile pdfgo.FileResolver
|
||||
halftoneWarnings map[*pdfgo.Halftone]bool
|
||||
transferBackdrop bool
|
||||
}
|
||||
|
||||
// ImportPDF 将PDF内容转换为独立OFD编辑文档,失败时不返回部分结果
|
||||
@@ -132,7 +132,7 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
|
||||
editor := NewEditor()
|
||||
editor.SetRenderBackends(renderer.Backends())
|
||||
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.pageIndexes = make(map[pdfgo.Reference]int)
|
||||
importer.resolveFile = options.ResolveFile
|
||||
@@ -188,6 +188,7 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
|
||||
importer.compositeNodes = nil
|
||||
importer.compositeSpace = nil
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 对象边界
|
||||
// 返回: *FillColor OFD颜色或渐变, 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 {
|
||||
return p.meshColor(paint, box)
|
||||
}
|
||||
@@ -704,5 +714,6 @@ func pdfOpaqueBlack(paint pdfgo.Paint) bool {
|
||||
// 入参: paint PDF颜色与不透明度
|
||||
// 返回: 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
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"maps"
|
||||
"strconv"
|
||||
@@ -42,7 +44,7 @@ func (p *pdfImporter) appearanceAnnotation(ctx context.Context, page *pdfgo.Page
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if annotation.Subtype == "FreeText" {
|
||||
if annotation.Subtype == "FreeText" || annotation.Subtype == "Line" {
|
||||
annotation.Dictionary = maps.Clone(annotation.Dictionary)
|
||||
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 {
|
||||
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}}
|
||||
if annotation.Subtype == "Redact" {
|
||||
converted.Parameters = &AnnotationParameters{Parameter: []AnnotationParameter{{Name: "PDF.Redact.Pending", Value: "true"}}}
|
||||
}
|
||||
if annotation.Subtype == "Text" {
|
||||
value, err := p.reader.Resolve(annotation.Dictionary["Open"])
|
||||
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})
|
||||
}
|
||||
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)
|
||||
}
|
||||
p.annotationData = append(p.annotationData, bytes.TrimPrefix(data, []byte(xml.Header))...)
|
||||
p.report.TextObjects += stamp.report.TextObjects
|
||||
p.report.PathObjects += stamp.report.PathObjects
|
||||
p.report.ImageObjects += stamp.report.ImageObjects
|
||||
@@ -183,7 +191,7 @@ func pdfAnnotationType(subtype pdfgo.Name) string {
|
||||
return "Watermark"
|
||||
case "Highlight", "Underline", "Squiggly", "StrikeOut":
|
||||
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 ""
|
||||
|
||||
+201
-16
@@ -31,6 +31,8 @@ type pdfCompositeNode struct {
|
||||
image *pdfgo.ImageMark
|
||||
group *pdfgo.GroupMark
|
||||
children []pdfCompositeNode
|
||||
textObject *pdfgo.TextObject
|
||||
glyph *pdfgo.TextMark
|
||||
}
|
||||
|
||||
// pdfCompositePixel 保存非预乘源空间分量及累计透明度
|
||||
@@ -49,7 +51,10 @@ type pdfCompositor struct {
|
||||
inverse pdfgo.Matrix
|
||||
cache *pdfCompositeCache
|
||||
masks map[*pdfgo.SoftMask][]float64
|
||||
meshes map[pdfMeshKey][]pdfMeshPixel
|
||||
meshes map[pdfMeshKey][]pdfShadingPixel
|
||||
gradients map[pdfGradientKey][]pdfShadingPixel
|
||||
transfers []*pdfgo.TransferFunction
|
||||
transferBlocked bool
|
||||
}
|
||||
|
||||
// pdfCompositeKey 区分图元的填充与描边几何
|
||||
@@ -70,9 +75,14 @@ type pdfCompositeGeometry struct {
|
||||
type pdfCompositeCache struct {
|
||||
geometry map[pdfCompositeKey]pdfCompositeGeometry
|
||||
images map[*pdfgo.Image]*pdfgo.ImageComponents
|
||||
imageMatrix map[*pdfgo.ImageMark]pdfgo.Matrix
|
||||
masks map[*pdfgo.SoftMask][]pdfCompositeNode
|
||||
meshes map[*pdfgo.MeshGradient][]pdfMeshTriangle
|
||||
patterns map[pdfgo.Paint]*pdfCompositePattern
|
||||
shadings map[pdfGradientKey]pdfCompositeGeometry
|
||||
transfers map[pdfTransferKey]*pdfgo.TransferFunction
|
||||
textDisjoint map[*pdfgo.GroupMark]bool
|
||||
groups map[*pdfgo.GroupMark]Box
|
||||
}
|
||||
|
||||
// compositingCache 按需创建单页合成缓存,页面切换时由导入器释放
|
||||
@@ -95,6 +105,21 @@ func (c *pdfCompositor) coverage(node pdfCompositeNode, stroke bool) (image.Imag
|
||||
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}
|
||||
geometry, ok := c.cache.geometry[key]
|
||||
if !ok {
|
||||
@@ -102,15 +127,82 @@ func (c *pdfCompositor) coverage(node pdfCompositeNode, stroke bool) (image.Imag
|
||||
var err error
|
||||
geometry.scene, geometry.box, err = c.importer.compileGroup(func(local *pdfImporter) error { return node.coverage(local, stroke) })
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return geometry, err
|
||||
}
|
||||
c.cache.geometry[key] = geometry
|
||||
}
|
||||
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 geometry, 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 按字体描述符计算保守边界,仅用于排除不相交的外部字体
|
||||
@@ -158,20 +250,31 @@ func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error) ([]p
|
||||
if err := p.halftone(mark.Style); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := p.resolveTransfer(&mark.Style); err != nil {
|
||||
return err
|
||||
}
|
||||
nodes = append(nodes, pdfCompositeNode{path: &mark})
|
||||
return nil
|
||||
}
|
||||
v.Text = func(mark pdfgo.TextMark) error {
|
||||
var text []pdfCompositeNode
|
||||
if mark.Font.Subtype != "Type3" {
|
||||
if err := p.halftone(mark.Style); err != nil {
|
||||
return err
|
||||
}
|
||||
if mark.Font.Subtype != "Type3" {
|
||||
nodes = append(nodes, pdfCompositeNode{text: &mark})
|
||||
return nil
|
||||
if err := p.resolveTransfer(&mark.Style); err != nil {
|
||||
return err
|
||||
}
|
||||
if mark.Clip != nil {
|
||||
return &pdfgo.UnsupportedError{Feature: "Type3 text clipping"}
|
||||
if len(mark.Glyphs) > 1 && (mark.Object != nil && !mark.Object.Knockout || mark.Style.AlphaIsShape || mark.Style.Fill.Alpha != 1 || mark.Style.Stroke.Alpha != 1 || mark.Style.SoftMask != nil || !pdfNormalBlend(mark.Style.BlendMode) || mark.Mode%4 == 1 || mark.Mode%4 == 2) {
|
||||
for i := range mark.Glyphs {
|
||||
glyph := mark
|
||||
glyph.Glyphs, glyph.Positions, glyph.Clip = mark.Glyphs[i:i+1], mark.Positions[i:i+1], nil
|
||||
text = append(text, pdfCompositeNode{text: &glyph})
|
||||
}
|
||||
} else {
|
||||
text = append(text, pdfCompositeNode{text: &mark})
|
||||
}
|
||||
} else {
|
||||
glyphs := active[mark.Font]
|
||||
if glyphs == nil {
|
||||
glyphs = make(map[string]bool)
|
||||
@@ -182,11 +285,24 @@ func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error) ([]p
|
||||
return fmt.Errorf("recursive PDF Type3 glyph %q", glyph.Name)
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
part := mark
|
||||
part.Glyphs, part.Positions = mark.Glyphs[index:index+1], mark.Positions[index:index+1]
|
||||
text = append(text, pdfCompositeNode{group: &pdfgo.GroupMark{Alpha: 1}, children: children, glyph: &part})
|
||||
}
|
||||
}
|
||||
if mark.Object != nil && mark.Object.Knockout {
|
||||
if len(nodes) == 0 || nodes[len(nodes)-1].textObject != mark.Object {
|
||||
nodes = append(nodes, pdfCompositeNode{group: &pdfgo.GroupMark{Alpha: 1, Knockout: true}, textObject: mark.Object})
|
||||
}
|
||||
node := &nodes[len(nodes)-1]
|
||||
node.children = append(node.children, text...)
|
||||
} else {
|
||||
nodes = append(nodes, text...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -194,6 +310,9 @@ func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error) ([]p
|
||||
if err := p.halftone(mark.Style); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := p.resolveTransfer(&mark.Style); err != nil {
|
||||
return err
|
||||
}
|
||||
nodes = append(nodes, pdfCompositeNode{image: &mark})
|
||||
return nil
|
||||
}
|
||||
@@ -236,7 +355,24 @@ func (p *pdfImporter) compositePage(space *pdfgo.ColorSpace, walk func(pdfgo.Vis
|
||||
}
|
||||
}
|
||||
p.compositeNodes = nil
|
||||
p.transferBackdrop = false
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -250,6 +386,17 @@ func (p *pdfImporter) compositeObjects(nodes []pdfCompositeNode) error {
|
||||
if err != nil {
|
||||
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 err := node.emit(p.visitor()); err != nil {
|
||||
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.transferBackdrop = p.transferBackdrop || transfer
|
||||
}
|
||||
return p.flushPath()
|
||||
}
|
||||
@@ -276,7 +424,7 @@ func (p *pdfImporter) compositeObjects(nodes []pdfCompositeNode) error {
|
||||
func (n pdfCompositeNode) opaque(space *pdfgo.ColorSpace) bool {
|
||||
if n.group != nil {
|
||||
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
|
||||
}
|
||||
for _, child := range n.children {
|
||||
@@ -309,7 +457,8 @@ func (n pdfCompositeNode) opaque(space *pdfgo.ColorSpace) bool {
|
||||
// 返回: bool 是否可直接转换
|
||||
func (n pdfCompositeNode) direct() bool {
|
||||
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
|
||||
}
|
||||
for _, child := range n.children {
|
||||
@@ -320,6 +469,9 @@ func (n pdfCompositeNode) direct() bool {
|
||||
return true
|
||||
}
|
||||
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 {
|
||||
return false
|
||||
}
|
||||
@@ -352,6 +504,14 @@ func (n pdfCompositeNode) emit(visitor pdfgo.Visitor) error {
|
||||
if n.image != nil {
|
||||
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 {
|
||||
for _, child := range n.children {
|
||||
if err := child.emit(v); err != nil {
|
||||
@@ -391,6 +551,7 @@ func (n pdfCompositeNode) coverage(p *pdfImporter, stroke bool) error {
|
||||
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.SoftMask, style.BlendMode = nil, "Normal"
|
||||
style.Transfer = nil
|
||||
style.FillOverprint, style.StrokeOverprint = false, false
|
||||
if n.path != nil {
|
||||
mark := *n.path
|
||||
@@ -452,11 +613,21 @@ func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompo
|
||||
cache := p.compositingCache()
|
||||
step := 25.4 / p.rasterDPI
|
||||
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 x := 0; x < w; x += 256 {
|
||||
runtime.Gosched()
|
||||
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{}}
|
||||
if transfers != nil {
|
||||
c.transfers = transfers[:width*height]
|
||||
clear(c.transfers)
|
||||
}
|
||||
pixels := buffer[:width*height]
|
||||
clear(pixels)
|
||||
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 {
|
||||
pixels[i].effect = 0
|
||||
}
|
||||
copy(previousTransfers, c.transfers)
|
||||
if err := c.draw([]pdfCompositeNode{node}, pixels, space); err != nil {
|
||||
return err
|
||||
}
|
||||
for i, pixel := range pixels {
|
||||
if pixel.effect == 0 {
|
||||
if pixel.effect == 0 && (c.transfers == nil || c.transfers[i] == previousTransfers[i]) {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -483,6 +661,13 @@ func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompo
|
||||
}
|
||||
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))})
|
||||
}
|
||||
}
|
||||
|
||||
+215
-41
@@ -46,6 +46,10 @@ func (c *pdfCompositor) draw(nodes []pdfCompositeNode, pixels []pdfCompositePixe
|
||||
// 入参: node 图元, pixels 背景与输出像素, space 混合空间
|
||||
// 返回: 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()
|
||||
mask, err := c.mask(style.SoftMask, space)
|
||||
if err != nil {
|
||||
@@ -84,7 +88,7 @@ func (c *pdfCompositor) drawMark(node pdfCompositeNode, pixels []pdfCompositePix
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -98,17 +102,36 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
|
||||
return err
|
||||
}
|
||||
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
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
processMask, processOverprint = paint.Process.CMYKMask()
|
||||
}
|
||||
}
|
||||
compatible := overprint && style.OverprintMode == 1 && node.image == nil && space.Model == "DeviceCMYK" && !space.Calibrated() && (paint.CMYK != nil || paint.Space != nil && paint.Space.Model == "DeviceCMYK" && !paint.Space.Calibrated())
|
||||
step := 25.4 / c.importer.rasterDPI
|
||||
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
|
||||
if paint.Tiling != nil && (node.image == nil || node.image.Image.ImageMask) {
|
||||
backdrop := pixels
|
||||
@@ -120,26 +143,37 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
|
||||
return err
|
||||
}
|
||||
}
|
||||
var mesh []pdfMeshPixel
|
||||
if paint.Mesh != nil {
|
||||
mesh, err = c.mesh(paint.Mesh, space)
|
||||
var shading []pdfShadingPixel
|
||||
if paint.Mesh != nil && (node.image == nil || node.image.Image.ImageMask) {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
}
|
||||
var uniformValues [4]float64
|
||||
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 {
|
||||
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()
|
||||
if a == 0 {
|
||||
continue
|
||||
}
|
||||
index := y*c.width + x
|
||||
shape, opacity := float64(a)/65535, paint.Alpha
|
||||
selectionShape := 1.0
|
||||
if mask != nil {
|
||||
opacity *= mask[index]
|
||||
}
|
||||
@@ -154,15 +188,31 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
|
||||
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})
|
||||
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
|
||||
}
|
||||
} else if mesh != nil {
|
||||
values = mesh[index].values
|
||||
shape *= mesh[index].shape
|
||||
} else if uniform {
|
||||
if !uniformReady {
|
||||
uniformValues, _, err = pdfCompositeColor(paint, pdfgo.Point{}, space, style.RenderingIntent)
|
||||
uniformValues, _, err = pdfCompositeColor(paint, pdfgo.Point{}, space, style.RenderingIntent, style.ColorConversion)
|
||||
uniformReady = true
|
||||
}
|
||||
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})
|
||||
if node.image != nil {
|
||||
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
|
||||
} else {
|
||||
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 {
|
||||
shape = 0
|
||||
}
|
||||
@@ -212,6 +265,9 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
|
||||
}
|
||||
}
|
||||
pixels[index].shape += shape * (1 - pixels[index].shape)
|
||||
if c.transfers != nil && shape != 0 && selectionShape != 0 {
|
||||
c.transfers[index] = transfer
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -235,10 +291,35 @@ func pdfCompositeInterpolate(target *pdfCompositePixel, source pdfCompositePixel
|
||||
// 返回: error 颜色或合成错误
|
||||
func (c *pdfCompositor) drawGroup(node pdfCompositeNode, pixels []pdfCompositePixel, parent *pdfgo.ColorSpace) error {
|
||||
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
|
||||
if space == nil {
|
||||
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))
|
||||
if !g.Isolated {
|
||||
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 将每个对象与组初始背景合成,再按对象形状替换先前对象
|
||||
@@ -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 {
|
||||
candidate := make([]pdfCompositePixel, len(initial))
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
for i, source := range candidate {
|
||||
pdfCompositeKnockout(&pixels[i], initial[i], source)
|
||||
for y := region.Min.Y; y < region.Max.Y; y++ {
|
||||
for x := region.Min.X; x < region.Max.X; x++ {
|
||||
i := y*c.width + x
|
||||
pdfCompositeKnockout(&pixels[i], initial[i], candidate[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -316,9 +417,9 @@ func pdfCompositeKnockout(target *pdfCompositePixel, initial, source pdfComposit
|
||||
}
|
||||
|
||||
// pdfCompositeGroup 移除组初始背景贡献后,将组结果合成到父空间
|
||||
// 入参: pixels 父组输出, initial 初始背景, result 组结果, space 组空间, parent 父空间, opacity 组不透明度, mask 蒙版, mode 混合模式
|
||||
// 入参: pixels 父组输出, initial 初始背景, result 组结果, space 组空间, parent 父空间, opacity 组不透明度, mask 蒙版, mode 混合模式, intent 渲染意图, conversion 设备转换函数
|
||||
// 返回: 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)
|
||||
components := space.Components()
|
||||
for i, pixel := range result {
|
||||
@@ -332,7 +433,7 @@ func pdfCompositeGroup(pixels, initial, result []pdfCompositePixel, space, paren
|
||||
}
|
||||
if !sameSpace {
|
||||
var err error
|
||||
values, err = parent.Convert(values[:components], space, "RelativeColorimetric")
|
||||
values, err = parent.ConvertWith(values[:components], space, intent, conversion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -378,7 +479,9 @@ func (c *pdfCompositor) mask(mask *pdfgo.SoftMask, inherited *pdfgo.ColorSpace)
|
||||
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
|
||||
}
|
||||
result := make([]float64, len(pixels))
|
||||
@@ -402,7 +505,7 @@ func (c *pdfCompositor) mask(mask *pdfgo.SoftMask, inherited *pdfgo.ColorSpace)
|
||||
// imageColor 采样映射后的原始图像分量,插值在源空间内进行
|
||||
// 入参: mark 图像, point 页面坐标, space 混合空间
|
||||
// 返回: [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]
|
||||
if source == nil {
|
||||
var err error
|
||||
@@ -412,18 +515,27 @@ func (c *pdfCompositor) imageColor(mark pdfgo.ImageMark, point pdfgo.Point, spac
|
||||
}
|
||||
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 {
|
||||
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)
|
||||
if point.X < 0 || point.X > 1 || point.Y < 0 || point.Y > 1 {
|
||||
return [4]float64{}, 0, nil
|
||||
}
|
||||
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
|
||||
var indices [4]int
|
||||
if process {
|
||||
if process && !nativeProcess {
|
||||
for i, name := range source.Colorants {
|
||||
index := -1
|
||||
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) {
|
||||
x, y = max(0, min(source.Rect.Dx()-1, x)), max(0, min(source.Rect.Dy()-1, y))
|
||||
values, alpha := source.ValuesAt(x, y)
|
||||
if process {
|
||||
if process && !nativeProcess {
|
||||
values = [4]float64{}
|
||||
for j, tint := range source.TintsAt(x, y) {
|
||||
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 {
|
||||
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 {
|
||||
return color, 0, err
|
||||
}
|
||||
return color, alpha * (1 - values[0]), err
|
||||
}
|
||||
if process {
|
||||
if process || space.Equal(source.Space) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -501,6 +613,13 @@ func (c *pdfCompositor) imageColor(mark pdfgo.ImageMark, point pdfgo.Point, spac
|
||||
// 返回: [4]bool 参与绘制的四色通道, bool 是否全部属于印刷原色, error 色空间错误
|
||||
func pdfImageProcessColorants(image *pdfgo.Image) ([4]bool, bool, error) {
|
||||
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()
|
||||
if err != nil {
|
||||
return mask, false, err
|
||||
@@ -525,27 +644,80 @@ func pdfImageProcessColorants(image *pdfgo.Image) ([4]bool, bool, error) {
|
||||
}
|
||||
|
||||
// pdfCompositeColor 将原始画刷变换到混合空间,渐变在源空间求值
|
||||
// 入参: paint 画刷, point 页面坐标, space 混合空间, intent 渲染意图
|
||||
// 入参: paint 画刷, point 页面坐标, space 混合空间, intent 渲染意图, conversion 设备转换函数
|
||||
// 返回: [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 {
|
||||
return [4]float64{}, false, &pdfgo.UnsupportedError{Feature: "local tiling pattern compositing"}
|
||||
}
|
||||
var values [4]float64
|
||||
var source *pdfgo.ColorSpace
|
||||
if paint.Axial != nil || paint.Radial != nil {
|
||||
t, visible := pdfShadingPosition(paint, point)
|
||||
if !visible {
|
||||
if paint.Function != nil {
|
||||
g := paint.Function
|
||||
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
|
||||
}
|
||||
var err error
|
||||
if g := paint.Axial; g != nil {
|
||||
values, err = g.ValuesAt(t)
|
||||
}
|
||||
inverse, ok := g.Matrix.Inverse()
|
||||
if !ok {
|
||||
return values, false, fmt.Errorf("singular function shading transform")
|
||||
}
|
||||
local := inverse.Apply(point)
|
||||
source, intent = g.Space, g.Intent
|
||||
if local.X < g.Domain.XMin || local.X > g.Domain.XMax || local.Y < g.Domain.YMin || local.Y > g.Domain.YMax {
|
||||
if g.Background == nil {
|
||||
return values, false, nil
|
||||
}
|
||||
values = *g.Background
|
||||
} else {
|
||||
var err error
|
||||
values, err = g.ValuesAt(local)
|
||||
if err != nil {
|
||||
return values, false, err
|
||||
}
|
||||
}
|
||||
} else if paint.Axial != nil || paint.Radial != nil {
|
||||
var background *[4]float64
|
||||
var bounds *pdfgo.Rectangle
|
||||
var matrix pdfgo.Matrix
|
||||
if g := paint.Axial; g != nil {
|
||||
source, intent = g.Space, g.Intent
|
||||
background, bounds, matrix = g.Background, g.Bounds, g.Matrix
|
||||
} else {
|
||||
g := paint.Radial
|
||||
values, err = g.ValuesAt(t)
|
||||
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 {
|
||||
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
|
||||
} else if paint.CMYK != nil {
|
||||
values, source = *paint.CMYK, &pdfgo.ColorSpace{Model: "DeviceCMYK"}
|
||||
} else if paint.SourceSpace == "DeviceGray" {
|
||||
values[0], source = paint.RGB[0], &pdfgo.ColorSpace{Model: "DeviceGray"}
|
||||
} else {
|
||||
copy(values[:], paint.RGB[:])
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ type pdfCompositePattern struct {
|
||||
nodes []pdfCompositeNode
|
||||
local pdfImporter
|
||||
box Box
|
||||
transferChecked bool
|
||||
transferOpaque bool
|
||||
}
|
||||
|
||||
// tiling 将图案作为非隔离组绘制,保留背景贡献及单元间隙
|
||||
@@ -59,6 +61,9 @@ func (c *pdfCompositor) tiling(paint pdfgo.Paint, backdrop []pdfCompositePixel,
|
||||
for i, pixel := range backdrop {
|
||||
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()
|
||||
for row := int(top); row <= int(bottom); row++ {
|
||||
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
|
||||
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},
|
||||
width: c.width, height: c.height, inverse: localInverse, cache: pattern.local.compositingCache(), masks: map[*pdfgo.SoftMask][]float64{}}
|
||||
if err := cell.draw(pattern.nodes, result, space); err != nil {
|
||||
left := int(math.Max(0, math.Min(float64(c.width), math.Floor((dx-c.box.X)/pixelStep)-1)))
|
||||
right := int(math.Max(0, math.Min(float64(c.width), math.Ceil((dx+pattern.box.W-c.box.X)/pixelStep)+1)))
|
||||
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
|
||||
}
|
||||
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 {
|
||||
@@ -86,6 +116,29 @@ func (c *pdfCompositor) tiling(paint pdfgo.Paint, backdrop []pdfCompositePixel,
|
||||
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 复用图案单元解析及几何上下文,供检查和局部合成共同使用
|
||||
// 入参: paint 图案画刷
|
||||
// 返回: *pdfCompositePattern 单元内容, error 解析或边界错误
|
||||
@@ -127,7 +180,54 @@ func (p *pdfImporter) compositePattern(paint pdfgo.Paint) (*pdfCompositePattern,
|
||||
// 入参: node 图元, space 混合空间
|
||||
// 返回: 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.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
|
||||
}
|
||||
if node.group != nil {
|
||||
@@ -138,6 +238,9 @@ func (p *pdfImporter) directCompositeNode(node pdfCompositeNode, space *pdfgo.Co
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
if node.image != nil && !node.image.Image.ImageMask {
|
||||
return true, nil
|
||||
}
|
||||
style, fill, stroke := node.style()
|
||||
for i, paint := range []pdfgo.Paint{style.Fill, style.Stroke} {
|
||||
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
|
||||
}
|
||||
}
|
||||
if node.image != nil && !node.image.Image.ImageMask {
|
||||
continue
|
||||
}
|
||||
if fill && style.FillOverprint && pdfOverprintNeedsSeparation(style.Fill) || stroke && style.StrokeOverprint && pdfOverprintNeedsSeparation(style.Stroke) {
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
if color.Alpha == 0 {
|
||||
@@ -165,7 +165,7 @@ func (p *pdfImporter) groupPaths(mark pdfgo.GroupMark, walk func(pdfgo.Visitor)
|
||||
if paint == nil {
|
||||
paint, clips = &color, 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) {
|
||||
regions = append(regions, pdfPaintRegion{*paint, combined})
|
||||
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 {
|
||||
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 {
|
||||
return nil, box, err
|
||||
}
|
||||
@@ -387,6 +390,7 @@ func (p *pdfImporter) compileGroup(build func(*pdfImporter) error) (*RasterPage,
|
||||
// 返回: image.Image 透明图像, error 后端渲染错误
|
||||
func (p *pdfImporter) renderGroupScene(source *RasterPage, box Box) (image.Image, error) {
|
||||
scene := *source
|
||||
scene.DPI = p.rasterDPI
|
||||
scene.Commands = append([]RasterCommand(nil), source.Commands...)
|
||||
scene.Width, scene.Height = box.W, box.H
|
||||
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 {
|
||||
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"}
|
||||
}
|
||||
visible, err := opacity(paint.RGB[0])
|
||||
|
||||
@@ -16,21 +16,14 @@ package ofdgo
|
||||
|
||||
import "github.com/xiaoqidun/pdfgo"
|
||||
|
||||
// halftone 检查连续色调转换及不可忽略的传递函数,避免将打印网屏误用于屏幕显示
|
||||
// halftone 检查连续色调转换,避免将打印网屏误用于屏幕显示
|
||||
// 入参: style 图形状态
|
||||
// 返回: error 严格模式或非恒等传递函数无法表达
|
||||
// 返回: error 严格模式无法保留设备网屏
|
||||
func (p *pdfImporter) halftone(style pdfgo.Style) error {
|
||||
h := style.Halftone
|
||||
if h == nil || h.Type == 0 {
|
||||
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 {
|
||||
return &pdfgo.UnsupportedError{Feature: "device halftone conversion"}
|
||||
}
|
||||
|
||||
@@ -205,6 +205,16 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict
|
||||
}
|
||||
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 err := p.appearanceAnnotation(ctx, page, annotation); err != nil {
|
||||
return err
|
||||
|
||||
+38
-17
@@ -37,12 +37,7 @@ type pdfMeshTriangle struct {
|
||||
type pdfMeshKey struct {
|
||||
mesh *pdfgo.MeshGradient
|
||||
space *pdfgo.ColorSpace
|
||||
}
|
||||
|
||||
// pdfMeshPixel 保存抗锯齿覆盖率与源空间合成分量
|
||||
type pdfMeshPixel struct {
|
||||
values [4]float64
|
||||
shape float64
|
||||
conversion pdfgo.ColorConversion
|
||||
}
|
||||
|
||||
// pdfMeshSample 保存采样点参数,patch为一基编号,零表示未覆盖
|
||||
@@ -77,7 +72,7 @@ func (p *pdfImporter) meshColor(paint pdfgo.Paint, box Box) (*FillColor, error)
|
||||
}
|
||||
color := pdfgo.Paint{RGB: rgb, Alpha: 1}
|
||||
if mesh.Space.Model == "DeviceCMYK" && !mesh.Space.Calibrated() {
|
||||
color.CMYK = &values
|
||||
color.CMYK = (*[4]float64)(values)
|
||||
}
|
||||
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}})
|
||||
@@ -160,10 +155,10 @@ func (p *pdfImporter) meshTriangles(mesh *pdfgo.MeshGradient) ([]pdfMeshTriangle
|
||||
}
|
||||
|
||||
// mesh 对当前图块执行四倍超采样,曲面重叠按后补片及较大v、u取值
|
||||
// 入参: mesh 原曲面, space 输出混合空间
|
||||
// 返回: []pdfMeshPixel 颜色及覆盖, error 几何或颜色错误
|
||||
func (c *pdfCompositor) mesh(mesh *pdfgo.MeshGradient, space *pdfgo.ColorSpace) ([]pdfMeshPixel, error) {
|
||||
key := pdfMeshKey{mesh, space}
|
||||
// 入参: mesh 原曲面, space 输出混合空间, conversion 设备转换函数
|
||||
// 返回: []pdfShadingPixel 颜色及覆盖, error 几何或颜色错误
|
||||
func (c *pdfCompositor) mesh(mesh *pdfgo.MeshGradient, space *pdfgo.ColorSpace, conversion pdfgo.ColorConversion) ([]pdfShadingPixel, error) {
|
||||
key := pdfMeshKey{mesh, space, conversion}
|
||||
if pixels, ok := c.meshes[key]; ok {
|
||||
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++ {
|
||||
if err := c.importer.ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for x := 0; x < w; x++ {
|
||||
sample := grid[y*w+x]
|
||||
if sample.patch == 0 {
|
||||
if box := mesh.Bounds; box != nil {
|
||||
point := inverse.Apply(pdfgo.Point{X: c.box.X + (float64(x)+.5)/scale, Y: c.box.Y + (float64(y)+.5)/scale})
|
||||
if point.X < box.XMin || point.X > box.XMax || point.Y < box.YMin || point.Y > box.YMax {
|
||||
continue
|
||||
}
|
||||
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 {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
pixel := &pixels[(y/samples)*c.width+x/samples]
|
||||
for i, value := range values {
|
||||
pixel.values[i] += value
|
||||
@@ -258,7 +279,7 @@ func (c *pdfCompositor) mesh(mesh *pdfgo.MeshGradient, space *pdfgo.ColorSpace)
|
||||
pixel.shape /= samples * samples
|
||||
}
|
||||
if c.meshes == nil {
|
||||
c.meshes = map[pdfMeshKey][]pdfMeshPixel{}
|
||||
c.meshes = map[pdfMeshKey][]pdfShadingPixel{}
|
||||
}
|
||||
c.meshes[key] = pixels
|
||||
return pixels, nil
|
||||
|
||||
+78
-311
@@ -16,17 +16,12 @@ package ofdgo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"maps"
|
||||
"math"
|
||||
"slices"
|
||||
"strconv"
|
||||
"unicode/utf16"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/xiaoqidun/pdfgo"
|
||||
@@ -81,7 +76,10 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
|
||||
if err := p.flushPath(); err != nil {
|
||||
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"}
|
||||
}
|
||||
var err error
|
||||
@@ -180,13 +178,13 @@ func (p *pdfImporter) appendImage(mark pdfgo.ImageMark, id string) error {
|
||||
// 入参: mark PDF文字绘制信息
|
||||
// 返回: 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" {
|
||||
return &pdfgo.UnsupportedError{Feature: "text blend mode " + string(mark.Style.BlendMode)}
|
||||
}
|
||||
if mark.Font.Subtype == pdfgo.Name("Type3") {
|
||||
if mark.Clip != nil {
|
||||
return &pdfgo.UnsupportedError{Feature: "Type3 text clipping"}
|
||||
}
|
||||
if err := p.flushPath(); err != nil {
|
||||
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" {
|
||||
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]
|
||||
if id == "" {
|
||||
var err error
|
||||
if embedded {
|
||||
var type1 *type1Program
|
||||
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
|
||||
if imported.repairLimit != 0 {
|
||||
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 {
|
||||
id, err = p.editor.AddExternalFont(font.Name)
|
||||
}
|
||||
@@ -264,9 +250,9 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
|
||||
}
|
||||
p.fontIDs[font] = id
|
||||
}
|
||||
if limit := p.fontRepairLimit[font]; limit != 0 {
|
||||
if embedded && imported.repairLimit != 0 {
|
||||
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"}
|
||||
}
|
||||
}
|
||||
@@ -274,33 +260,39 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
|
||||
var metrics FontMetrics
|
||||
var outline FontOutlines
|
||||
if embedded {
|
||||
metrics = p.fontMetrics[id]
|
||||
if metrics == nil {
|
||||
if p.editor.backends.Fonts == nil {
|
||||
return fmt.Errorf("PDF font backend unavailable")
|
||||
metrics = imported.metrics
|
||||
outline = metrics.(FontOutlines)
|
||||
}
|
||||
metrics, err = p.editor.backends.Fonts.OpenFont(p.editor.fonts[id].Data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("PDF font %s metrics: %w", font.Name, err)
|
||||
}
|
||||
p.fontMetrics[id] = metrics
|
||||
}
|
||||
var ok bool
|
||||
outline, ok = metrics.(FontOutlines)
|
||||
if !ok {
|
||||
return fmt.Errorf("PDF font backend does not provide glyph outlines")
|
||||
fill, stroke, visible := paintMode == 0 || paintMode == 2, paintMode == 1 || paintMode == 2, paintMode != 3
|
||||
if stroke && !embedded {
|
||||
if resolved, err := p.editor.editorFont(id); err == nil {
|
||||
if provider, ok := resolved.(FontOutlines); ok {
|
||||
metrics, outline = resolved, provider
|
||||
for _, glyph := range mark.Glyphs {
|
||||
char, _ := utf8.DecodeRuneInString(glyph.Text)
|
||||
if utf8.RuneCountInString(glyph.Text) != 1 || resolved.GlyphIndex(char) == 0 && char != 0 {
|
||||
metrics, outline = nil, nil
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
outlineStroke := stroke && outline != nil
|
||||
var glyphPaths []pdfgo.Path
|
||||
const unit = 25.4 / 72
|
||||
m := p.matrix.Mul(mark.Matrix).Mul(pdfgo.Matrix{1 / unit, 0, 0, -1 / unit, 0, 0})
|
||||
var points []pdfgo.Point
|
||||
object := TextObject{Font: id, Size: mark.Size * unit, HScale: mark.HorizontalScale}
|
||||
position := 0
|
||||
for n, glyph := range mark.Glyphs {
|
||||
if err := p.ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
gid := glyph.ID
|
||||
if embedded && font.ProgramType == "FontFile" {
|
||||
var found bool
|
||||
gid, found = p.type1Glyphs[font][glyph.Name]
|
||||
gid, found = imported.type1Glyphs[glyph.Name]
|
||||
if !found {
|
||||
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 == "" {
|
||||
return &pdfgo.UnsupportedError{Feature: "unmapped external font character"}
|
||||
}
|
||||
if outlineStroke && !embedded {
|
||||
char, _ := utf8.DecodeRuneInString(glyph.Text)
|
||||
gid = metrics.GlyphIndex(char)
|
||||
}
|
||||
origin := mark.Positions[n]
|
||||
object.TextCode = append(object.TextCode, TextCode{X: pdfNumbers(origin.X * unit), Y: pdfNumbers(-origin.Y * unit), Value: glyph.Text})
|
||||
if embedded {
|
||||
count := utf8.RuneCountInString(glyph.Text)
|
||||
object.CGTransform = append(object.CGTransform, CGTransform{CodePosition: position, CodeCount: count, GlyphCount: 1, Glyphs: strconv.Itoa(int(gid))})
|
||||
position += count
|
||||
}
|
||||
if embedded || outlineStroke {
|
||||
path, err := outline.GlyphOutline(gid, object.Size)
|
||||
if err != nil {
|
||||
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}} {
|
||||
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 {
|
||||
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}} {
|
||||
@@ -368,8 +374,7 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
|
||||
}
|
||||
}
|
||||
box := pdfBounds(points)
|
||||
fill, stroke, visible := paintMode == 0 || paintMode == 2, paintMode == 1 || paintMode == 2, paintMode != 3
|
||||
if stroke {
|
||||
if stroke && !outlineStroke {
|
||||
strokeMatrix := mark.StrokeMatrix
|
||||
if strokeMatrix == (pdfgo.Matrix{}) {
|
||||
strokeMatrix = pdfgo.Identity()
|
||||
@@ -394,7 +399,7 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
|
||||
}
|
||||
}
|
||||
object.Boundary = pdfBoundary(box)
|
||||
if stroke {
|
||||
if stroke && !outlineStroke {
|
||||
strokePaint, err := p.paintColor(mark.Style.Stroke, box)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -410,7 +415,7 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
|
||||
object.Stroke = &stroke
|
||||
object.Visible = &visible
|
||||
object.FillColor = p.color(mark.Style.Fill)
|
||||
if fill {
|
||||
if fill && !outlineStroke {
|
||||
object.FillColor, err = p.paintColor(mark.Style.Fill, box)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -432,264 +437,26 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
|
||||
}
|
||||
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.report.TextObjects++
|
||||
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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"math"
|
||||
|
||||
"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 保留线性源颜色的设备分量,校准颜色经源空间转换
|
||||
// 入参: stops 源颜色分段, space 源颜色空间
|
||||
// 返回: []ShdSegment OFD颜色分段, error 不可线性表达的变换
|
||||
@@ -63,9 +81,12 @@ func pdfGradientStopsError(stops []pdfgo.GradientStop, space *pdfgo.ColorSpace)
|
||||
// 入参: paint 画刷
|
||||
// 返回: error 不可等价表达的渐变
|
||||
func pdfGradientError(paint pdfgo.Paint) error {
|
||||
if paint.Function != nil {
|
||||
return &pdfgo.UnsupportedError{Feature: "function shading conversion"}
|
||||
}
|
||||
if paint.Mesh != nil {
|
||||
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"}
|
||||
}
|
||||
if mesh.Space.Calibrated() && !mesh.Space.SRGBEquivalent() {
|
||||
@@ -73,9 +94,15 @@ func pdfGradientError(paint pdfgo.Paint) error {
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
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 nil
|
||||
@@ -96,6 +123,8 @@ func (p *pdfImporter) gradientPath(mark pdfgo.PathMark) error {
|
||||
space = paint.Axial.Space
|
||||
case paint.Radial != nil:
|
||||
space = paint.Radial.Space
|
||||
case paint.Function != nil:
|
||||
space = paint.Function.Space
|
||||
case paint.Mesh != nil:
|
||||
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)
|
||||
}
|
||||
|
||||
// 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 计算渐变参数,未延伸区域及不存在非负圆半径的位置不着色
|
||||
// 入参: paint 渐变画刷, point 页面坐标
|
||||
// 返回: 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
|
||||
measure, contours, textOnly, pattern bool
|
||||
patterns *renderCache[patternCellKey, []RasterCommand]
|
||||
patternReferences map[*Pattern]bool
|
||||
patternPage *Matrix
|
||||
}
|
||||
|
||||
// semanticStyle 保存已继承的路径颜色和描边参数
|
||||
@@ -466,11 +468,16 @@ func (c *semanticCompiler) fillPattern(clip GeometryPath, pattern *PatternPaint,
|
||||
matrix := NewMatrix(pattern.CTM)
|
||||
if pattern.RelativeTo != "Page" {
|
||||
matrix = object.Multiply(matrix)
|
||||
} else if c.patternPage != nil {
|
||||
matrix = c.patternPage.Multiply(matrix)
|
||||
}
|
||||
inverse, ok := matrix.Invert()
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid pattern transform")
|
||||
}
|
||||
if pattern.CellContent.empty() {
|
||||
return nil
|
||||
}
|
||||
box, err := c.geometry.Bounds(clip)
|
||||
if err != nil {
|
||||
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 {
|
||||
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 {
|
||||
|
||||
+12
-3
@@ -16,6 +16,7 @@ package ofdgo
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"slices"
|
||||
)
|
||||
|
||||
@@ -101,6 +102,9 @@ func (c *semanticCompiler) sharedStamp(stamp Stamp, compile func(*semanticCompil
|
||||
// 入参: pattern 底纹画刷, 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
|
||||
if patternCellTranslatable(pattern) {
|
||||
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 {
|
||||
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)}
|
||||
for _, object := range pattern.CellContent.Objects {
|
||||
for object := range pattern.CellContent.objects() {
|
||||
object = mergeGraphicObjectAlpha(object, pattern.Alpha)
|
||||
if err := c.renderer.WalkObject(&object, RenderState{Defaults: defaults, Parent: &matrix, BoundaryInCTM: true}, child); err != nil {
|
||||
return nil, err
|
||||
@@ -142,7 +151,7 @@ func patternCellTranslatable(pattern *PatternPaint) bool {
|
||||
if pattern.Color.Pattern != nil {
|
||||
return false
|
||||
}
|
||||
for _, object := range pattern.CellContent.Objects {
|
||||
for object := range pattern.CellContent.objects() {
|
||||
var fill, stroke *FillColor
|
||||
switch object.Type {
|
||||
case "PathObject":
|
||||
|
||||
+18
-13
@@ -368,12 +368,27 @@ func renderObjectMatrix(boundary string, local Matrix, state RenderState) (Matri
|
||||
return matrix, linear
|
||||
}
|
||||
|
||||
// objectGeometryClip 解析对象裁剪并合并父裁剪
|
||||
// objectGeometryClip 合并对象外接矩形、显式裁剪和父裁剪
|
||||
// 入参: clips 对象裁剪, boundary 边界, local 局部矩阵, state 继承状态
|
||||
// 返回: *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 {
|
||||
return state.Clip, nil
|
||||
return clip, nil
|
||||
}
|
||||
matrix, _ := renderObjectMatrix(boundary, local, state)
|
||||
if clips.TransFlag != nil && !*clips.TransFlag {
|
||||
@@ -390,17 +405,7 @@ func (r *Renderer) objectGeometryClip(clips *Clips, boundary string, local Matri
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return geometry.Clip(r, clips, matrix, state.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)
|
||||
return geometry.Clip(r, clips, matrix, clip)
|
||||
}
|
||||
|
||||
// addGeometrySpan 以公共几何语义收集文字选择区域,保留旋转和斜切
|
||||
|
||||
+46
-25
@@ -198,6 +198,17 @@ func (r *Renderer) walkComposite(object CompositeGraphicUnit, state RenderState,
|
||||
boundary = state.Parent.Multiply(boundary)
|
||||
}
|
||||
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
|
||||
if clips != nil && clips.TransFlag != nil && !*clips.TransFlag {
|
||||
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)
|
||||
if err := r.walkObject(&member, state, visitor, references); err != nil {
|
||||
return err
|
||||
@@ -245,29 +255,40 @@ func (r *Renderer) walkComposite(object CompositeGraphicUnit, state RenderState,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
for _, member := range object.ImageObject {
|
||||
member.Alpha = mergeAlpha(member.Alpha, object.Alpha)
|
||||
if err := visitor.DrawObject(&GraphicObject{Type: "ImageObject", ImageObject: member}, state); err != nil {
|
||||
return err
|
||||
|
||||
// compositeExtentClip 按矢量资源尺寸裁剪内容,资源裁剪随实例变换
|
||||
// 入参: object 矢量资源, matrix 资源到页面矩阵, parent 父裁剪
|
||||
// 返回: *GeometryPath 页面裁剪, error 尺寸或几何错误
|
||||
func (r *Renderer) compositeExtentClip(object CompositeGraphicUnit, matrix Matrix, parent *GeometryPath) (*GeometryPath, error) {
|
||||
if !object.extentSet && object.Width == 0 && object.Height == 0 {
|
||||
return parent, nil
|
||||
}
|
||||
return r.geometryExtentClip(object.Width, object.Height, matrix, parent)
|
||||
}
|
||||
|
||||
// geometryExtentClip 按给定坐标系限制宽高并与父裁剪求交
|
||||
// 入参: width 宽度, height 高度, matrix 到页面矩阵, parent 父裁剪
|
||||
// 返回: *GeometryPath 页面裁剪, error 尺寸或几何错误
|
||||
func (r *Renderer) geometryExtentClip(width, height float64, matrix Matrix, parent *GeometryPath) (*GeometryPath, error) {
|
||||
if !finite(width) || !finite(height) || width < 0 || height < 0 {
|
||||
return nil, fmt.Errorf("invalid clipping dimensions")
|
||||
}
|
||||
if width == 0 || height == 0 {
|
||||
return new(GeometryPath), nil
|
||||
}
|
||||
geometry, err := r.Geometry()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clip, err := geometry.Transform(geometryRectangle(Box{W: width, H: height}), matrix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if parent != nil {
|
||||
clip, err = clipGeometry(geometry, clip, parent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, member := range object.PathObject {
|
||||
member.Alpha = mergeAlpha(member.Alpha, object.Alpha)
|
||||
if err := visitor.DrawObject(&GraphicObject{Type: "PathObject", PathObject: member}, state); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, member := range object.TextObject {
|
||||
member.Alpha = mergeAlpha(member.Alpha, object.Alpha)
|
||||
if err := visitor.DrawObject(&GraphicObject{Type: "TextObject", TextObject: member}, state); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, member := range object.CompositeGraphicUnit {
|
||||
member.Alpha = mergeAlpha(member.Alpha, object.Alpha)
|
||||
if err := r.walkComposite(member, state, visitor, references); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return &clip, nil
|
||||
}
|
||||
+4
-1
@@ -51,9 +51,11 @@ type Renderer struct {
|
||||
preparedFonts map[string]*PreparedFont
|
||||
fontPreparations *renderCache[[32]byte, PreparedFont]
|
||||
fontSourcesCache *fontSourceCache
|
||||
fontMetrics renderCache[[32]byte, FontMetrics]
|
||||
fontMetrics *renderCache[[32]byte, FontMetrics]
|
||||
glyphOutlines renderCache[glyphOutlineKey, GeometryPath]
|
||||
renderError error
|
||||
patternReferences map[*Pattern]bool
|
||||
patternPage *Matrix
|
||||
TransparentBackground bool
|
||||
}
|
||||
|
||||
@@ -100,6 +102,7 @@ func (r *Renderer) childRenderer(reader *Reader) *Renderer {
|
||||
renderer := NewRenderer(reader, opts...)
|
||||
renderer.sharedBackendStates = r.sharedBackendStates
|
||||
renderer.fontPreparations = r.fontPreparations
|
||||
renderer.fontMetrics = r.fontMetrics
|
||||
renderer.decodeImages = r.decodeImages
|
||||
renderer.TransparentBackground = r.TransparentBackground
|
||||
return renderer
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
package ofdgo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
@@ -66,10 +65,10 @@ func (r *clipRenderer) add(path *canvas.Path) {
|
||||
}
|
||||
|
||||
// buildObjectClipPath 构建对象裁剪路径并应用父级变换
|
||||
// 入参: clips 裁剪对象, pageH 页面高度, bx 边界X坐标, by 边界Y坐标, objectCTM 对象CTM, parentCTM 父级CTM, boundaryInCTM 边界是否参与父级CTM
|
||||
// 入参: clips 裁剪对象, pageH 页面高度, boundary 外接矩形, objectCTM 对象CTM, parentCTM 父级CTM, boundaryInCTM 边界是否参与父级CTM
|
||||
// 返回: *canvas.Path 路径对象
|
||||
func (r *Renderer) buildObjectClipPath(clips *Clips, pageH float64, bx, by float64, 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})
|
||||
func (r *Renderer) buildObjectClipPath(clips *Clips, pageH float64, boundary string, objectCTM Matrix, parentCTM *Matrix, boundaryInCTM bool) *canvas.Path {
|
||||
path, err := r.objectGeometryClip(clips, boundary, objectCTM, RenderState{Parent: parentCTM, BoundaryInCTM: boundaryInCTM})
|
||||
if err != nil {
|
||||
r.renderError = err
|
||||
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)
|
||||
ctx := canvas.NewContext(renderer)
|
||||
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 {
|
||||
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()) {
|
||||
return parent
|
||||
}
|
||||
if result, ok := intersectConvexCanvasPaths(parent, current); ok {
|
||||
return result
|
||||
}
|
||||
return parent.And(current)
|
||||
}
|
||||
|
||||
@@ -208,6 +210,9 @@ func applyClipPath(path, clip *canvas.Path) *canvas.Path {
|
||||
return pathRect.And(rect).ToPath()
|
||||
}
|
||||
}
|
||||
if result, ok := intersectConvexCanvasPaths(path, clip); ok {
|
||||
return result
|
||||
}
|
||||
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},
|
||||
}
|
||||
}
|
||||
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
|
||||
compiler, compiled := ctx.Renderer.(*canvasPageCompiler)
|
||||
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 {
|
||||
return rect.Contains(outline.FastBounds())
|
||||
}
|
||||
if geometryConvexContains(*geometryFromCanvasPath(clipPath), *geometryFromCanvasPath(outline)) {
|
||||
return true
|
||||
}
|
||||
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) {
|
||||
return img
|
||||
}
|
||||
original := img
|
||||
source, err := imagePixelData(img)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
if mask.Opaque() {
|
||||
if scale <= 1 {
|
||||
return original
|
||||
}
|
||||
return 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}})
|
||||
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))
|
||||
if obj.Border != nil {
|
||||
border := *obj.Border
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package ofdgo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"math"
|
||||
"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},
|
||||
})
|
||||
}
|
||||
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
|
||||
if obj.Fill != nil {
|
||||
shouldFill = *obj.Fill
|
||||
@@ -331,9 +332,22 @@ func (r *Renderer) renderPattern(ctx *canvas.Context, pattern *PatternPaint, pag
|
||||
r.pageText = nil
|
||||
defer func() { r.pageText = text }()
|
||||
}
|
||||
if clip == nil || clip.Empty() || len(pattern.CellContent.Objects) == 0 {
|
||||
if clip == nil || clip.Empty() || pattern.CellContent.empty() {
|
||||
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
|
||||
if xStep < pattern.Width {
|
||||
xStep = pattern.Width
|
||||
@@ -347,6 +361,8 @@ func (r *Renderer) renderPattern(ctx *canvas.Context, pattern *PatternPaint, pag
|
||||
patternCTM := NewMatrix(pattern.CTM)
|
||||
if pattern.RelativeTo != "Page" {
|
||||
patternCTM = objectCTM.Multiply(patternCTM)
|
||||
} else if parentPage != nil {
|
||||
patternCTM = parentPage.Multiply(patternCTM)
|
||||
}
|
||||
invCTM, ok := patternCTM.Invert()
|
||||
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") {
|
||||
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)
|
||||
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 {
|
||||
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
|
||||
if obj.DrawParam != "" {
|
||||
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 {
|
||||
ctx.SetFill(fillPaint)
|
||||
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
|
||||
if advanceLimit > 0 && glyphWidth*scaleX > advanceLimit {
|
||||
scaleX = advanceLimit / glyphWidth
|
||||
@@ -335,7 +353,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
|
||||
if useTextMatrix {
|
||||
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 {
|
||||
r.renderPattern(ctx, fillPattern, pageH, glyphPath, objectCTM)
|
||||
} else {
|
||||
|
||||
@@ -105,6 +105,7 @@ type CompositeGraphicUnits struct {
|
||||
}
|
||||
|
||||
// CompositeGraphicUnit 复合图元
|
||||
// Width、Height、Thumbnail和Substitution用于矢量资源定义
|
||||
type CompositeGraphicUnit struct {
|
||||
ID string `xml:"ID,attr"`
|
||||
BaseLoc string `xml:"BaseLoc,attr"`
|
||||
@@ -114,6 +115,10 @@ type CompositeGraphicUnit struct {
|
||||
DrawParam string `xml:"DrawParam,attr"`
|
||||
Alpha *int `xml:"Alpha,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:"-"`
|
||||
TextObject []TextObject `xml:"TextObject"`
|
||||
PathObject []PathObject `xml:"PathObject"`
|
||||
@@ -122,4 +127,5 @@ type CompositeGraphicUnit struct {
|
||||
Clips *Clips `xml:"Clips"`
|
||||
Actions []Action `xml:"Actions>Action"`
|
||||
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 离线吊销检查选项
|
||||
// 检查签署者及制章者证书, 不代表整条证书链或TSA的吊销状态
|
||||
// CRL支持直接完整列表及增量合并, 尚不支持间接或带作用域列表
|
||||
// Fetch仅提供DER证据, 本库不主动联网, 缺少NextUpdate时必须显式设置MaxAge
|
||||
// VerifyTime缺省取验签指定时间或当前时间, 不采用签名自报时间
|
||||
type SignatureRevocationOptions struct {
|
||||
@@ -120,8 +121,8 @@ func VerifySignatureRevocation(cert, issuer []byte, options SignatureRevocationO
|
||||
evidence.OCSPResponses = append(cloneSignatureEvidence(evidence.OCSPResponses), fetched.OCSPResponses...)
|
||||
}
|
||||
var good *SignatureRevocationReport
|
||||
for _, raw := range evidence.CRLs {
|
||||
item := verifySignatureCRL(raw, cert, issuer, at, options.MaxAge)
|
||||
if len(evidence.CRLs) != 0 {
|
||||
item := verifySignatureCRLs(evidence.CRLs, c, i, at, options.MaxAge)
|
||||
if item.Checked && item.Status == "revoked" {
|
||||
return item
|
||||
}
|
||||
@@ -172,71 +173,6 @@ func signatureEvidenceFresh(thisUpdate, nextUpdate, at time.Time, maxAge time.Du
|
||||
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签名、授权、颁发者绑定及状态
|
||||
// 入参: raw 响应DER, cert 证书, issuer 颁发者, at 验证时间, maxAge 最大年龄
|
||||
// 返回: SignatureRevocationReport 吊销结果
|
||||
|
||||
+4
-2
@@ -989,8 +989,10 @@ func (x *ofdXML) pattern(pattern *Pattern) {
|
||||
attrs.add("RelativeTo", pattern.RelativeTo)
|
||||
attrs.add("CTM", pattern.CTM)
|
||||
x.start("Pattern", attrs)
|
||||
x.start("CellContent", nil)
|
||||
for _, object := range pattern.CellContent.Objects {
|
||||
var content ofdAttrs
|
||||
content.add("Thumbnail", pattern.CellContent.Thumbnail)
|
||||
x.start("CellContent", content)
|
||||
for object := range pattern.CellContent.objects() {
|
||||
x.object(object, false)
|
||||
}
|
||||
x.end("CellContent")
|
||||
|
||||
Reference in new issue
Block a user