mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
This commit is contained in:
1 parent
444a84c629
commit
b30a44b7ce
25 files changed
+529
-63
No files matched your search
@@ -9,7 +9,7 @@ require (
|
||||
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
|
||||
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
|
||||
github.com/xiaoqidun/jbig2 v1.1.1
|
||||
github.com/xiaoqidun/pdfgo v0.0.0-20260924091402-7973f8a3807a
|
||||
github.com/xiaoqidun/pdfgo v0.0.0-20260924110344-e481bdea25e9
|
||||
golang.org/x/crypto v0.57.0
|
||||
golang.org/x/image v0.46.0
|
||||
golang.org/x/text v0.42.0
|
||||
|
||||
@@ -60,8 +60,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-20260924091402-7973f8a3807a h1:+mspTtduelAEpmCAw/Pc6p2azO1vkuF2wIJRDogZHU4=
|
||||
github.com/xiaoqidun/pdfgo v0.0.0-20260924091402-7973f8a3807a/go.mod h1:/D0XHgfZ7xNhix95swpNaThpuqoJjVR5wIadogtR5pI=
|
||||
github.com/xiaoqidun/pdfgo v0.0.0-20260924110344-e481bdea25e9 h1:MjDKg9lMG/5AmEr+SV7C2y4dzfhpfXDSYScfu2mZixQ=
|
||||
github.com/xiaoqidun/pdfgo v0.0.0-20260924110344-e481bdea25e9/go.mod h1:/D0XHgfZ7xNhix95swpNaThpuqoJjVR5wIadogtR5pI=
|
||||
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=
|
||||
|
||||
@@ -3445,12 +3445,13 @@ func setPathStyle(object *ofdgo.PathObject, args []js.Value) error {
|
||||
}
|
||||
if !args[4].IsNull() {
|
||||
width := args[4].Float()
|
||||
if width <= 0 || math.IsNaN(width) || math.IsInf(width, 0) {
|
||||
return fmt.Errorf("line width must be positive and finite")
|
||||
if width < 0 || math.IsNaN(width) || math.IsInf(width, 0) {
|
||||
return fmt.Errorf("line width must be nonnegative and finite")
|
||||
}
|
||||
if scale := editorPathScale(*object); width != object.LineWidth*scale {
|
||||
object.LineWidth = width / scale
|
||||
}
|
||||
object.LineWidthSet = width == 0
|
||||
}
|
||||
if !args[1].IsNull() {
|
||||
if object.FillColor == nil {
|
||||
|
||||
+2
-1
@@ -58,8 +58,9 @@ func (r *Renderer) drawParamDefaults(id string, defaults *DrawParam) *DrawParam
|
||||
// 入参: base 基础绘制参数, dp 覆盖绘制参数
|
||||
// 返回: *DrawParam 合并后的绘制参数
|
||||
func mergeDrawParam(base DrawParam, dp *DrawParam) *DrawParam {
|
||||
if dp.LineWidth > 0 {
|
||||
if dp.LineWidth > 0 || dp.LineWidthSet {
|
||||
base.LineWidth = dp.LineWidth
|
||||
base.LineWidthSet = dp.LineWidthSet
|
||||
}
|
||||
if dp.Join != "" {
|
||||
base.Join = dp.Join
|
||||
|
||||
+1
-1
@@ -886,7 +886,7 @@ func transformEditorObject(object GraphicObject, dx, dy, scale float64) (Graphic
|
||||
layout.options.FirstLineIndent *= scale
|
||||
obj.layout = &layout
|
||||
}
|
||||
if obj.LineWidth == 0 && obj.Stroke != nil && *obj.Stroke {
|
||||
if obj.LineWidth == 0 && !obj.LineWidthSet && obj.Stroke != nil && *obj.Stroke {
|
||||
obj.LineWidth = defaultPathLineWidth
|
||||
}
|
||||
obj.LineWidth *= scale
|
||||
|
||||
@@ -565,14 +565,7 @@ func (e *Editor) TransformObjects(page int, ids []string, dx, dy, scale float64)
|
||||
return GraphicObject{}, err
|
||||
}
|
||||
}
|
||||
after, err := e.transformObject(object, dx, dy, scale)
|
||||
if err != nil {
|
||||
return GraphicObject{}, err
|
||||
}
|
||||
if origin := e.objectOrigin(editorObjectID(object)); origin != nil && origin.node.attr("LineWidth") == "0" && object.Type == "TextObject" && object.TextObject.LineWidth == 0 {
|
||||
after.TextObject.LineWidth = 0
|
||||
}
|
||||
return after, nil
|
||||
return e.transformObject(object, dx, dy, scale)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -610,7 +610,7 @@ func (e *Editor) compositeMemberStyle(node *editorCompositeNode) (GraphicObject,
|
||||
base = &DrawParam{}
|
||||
}
|
||||
object, err := e.resolveEditorStyleDefaults(node.object, base)
|
||||
if err == nil && object.Type == "PathObject" && object.PathObject.LineWidth == 0 {
|
||||
if err == nil && object.Type == "PathObject" && object.PathObject.LineWidth == 0 && !object.PathObject.LineWidthSet {
|
||||
object.PathObject.LineWidth = defaultPathLineWidth
|
||||
}
|
||||
return object, err
|
||||
|
||||
@@ -263,6 +263,9 @@ func (e *Editor) addEditorDrawParam(draw DrawParam) (string, error) {
|
||||
attrs.add(pair[0], pair[1])
|
||||
}
|
||||
attrs.number("LineWidth", draw.LineWidth)
|
||||
if draw.LineWidthSet && draw.LineWidth == 0 {
|
||||
attrs.add("LineWidth", "0")
|
||||
}
|
||||
attrs.number("MiterLimit", draw.MiterLimit)
|
||||
if draw.Relative == "" || draw.dashPatternSet || draw.DashPattern != "" {
|
||||
attrs = append(attrs, xml.Attr{Name: xml.Name{Local: "DashPattern"}, Value: draw.DashPattern})
|
||||
|
||||
@@ -610,10 +610,12 @@ func editorPreservedObject(before, after GraphicObject) bool {
|
||||
a, b := &before.TextObject, after.TextObject
|
||||
a.ID, a.Boundary, a.CTM, a.Alpha = b.ID, b.Boundary, b.CTM, b.Alpha
|
||||
a.Fill, a.Stroke, a.FillColor, a.StrokeColor, a.LineWidth = b.Fill, b.Stroke, b.FillColor, b.StrokeColor, b.LineWidth
|
||||
a.LineWidthSet = b.LineWidthSet
|
||||
case "PathObject":
|
||||
a, b := &before.PathObject, after.PathObject
|
||||
a.ID, a.Boundary, a.CTM, a.Alpha = b.ID, b.Boundary, b.CTM, b.Alpha
|
||||
a.Fill, a.Stroke, a.FillColor, a.StrokeColor, a.LineWidth = b.Fill, b.Stroke, b.FillColor, b.StrokeColor, b.LineWidth
|
||||
a.LineWidthSet = b.LineWidthSet
|
||||
a.DashOffset, a.DashPattern, a.Cap, a.Join = b.DashOffset, b.DashPattern, b.Cap, b.Join
|
||||
a.dashPatternSet = b.dashPatternSet
|
||||
case "ImageObject":
|
||||
|
||||
@@ -285,11 +285,12 @@ func (e *Editor) resolveEditorStyleDefaults(object GraphicObject, base *DrawPara
|
||||
style := mergeDrawParam(*base, dp)
|
||||
if object.Type == "PathObject" {
|
||||
obj := &object.PathObject
|
||||
style = mergeDrawParam(*style, &DrawParam{LineWidth: obj.LineWidth, Join: obj.Join, Cap: obj.Cap,
|
||||
style = mergeDrawParam(*style, &DrawParam{LineWidth: obj.LineWidth, LineWidthSet: obj.LineWidthSet, Join: obj.Join, Cap: obj.Cap,
|
||||
MiterLimit: obj.MiterLimit, DashPattern: obj.DashPattern, dashPatternSet: obj.dashPatternSet, DashOffset: obj.DashOffset,
|
||||
FillColor: obj.FillColor, StrokeColor: obj.StrokeColor})
|
||||
obj.DrawParam = ""
|
||||
obj.LineWidth, obj.Join, obj.Cap = style.LineWidth, style.Join, style.Cap
|
||||
obj.LineWidthSet = style.LineWidthSet
|
||||
obj.MiterLimit, obj.DashPattern, obj.DashOffset = style.MiterLimit, style.DashPattern, style.DashOffset
|
||||
obj.dashPatternSet = style.dashPatternSet
|
||||
obj.FillColor, obj.StrokeColor = style.FillColor, style.StrokeColor
|
||||
@@ -298,12 +299,13 @@ func (e *Editor) resolveEditorStyleDefaults(object GraphicObject, base *DrawPara
|
||||
if style.Cap != "" && style.Cap != "Butt" || style.DashPattern != "" || style.DashOffset != nil && *style.DashOffset != 0 {
|
||||
return GraphicObject{}, &EditError{Code: EditUnsupportedStyle, Err: fmt.Errorf("text draw parameters require unsupported stroke styles")}
|
||||
}
|
||||
style = mergeDrawParam(*style, &DrawParam{LineWidth: obj.LineWidth, Join: obj.Join,
|
||||
style = mergeDrawParam(*style, &DrawParam{LineWidth: obj.LineWidth, LineWidthSet: obj.LineWidthSet, Join: obj.Join,
|
||||
MiterLimit: obj.MiterLimit, FillColor: obj.FillColor, StrokeColor: obj.StrokeColor})
|
||||
text := mergeDrawParam(*dp, &DrawParam{Font: obj.Font, Size: obj.Size, Weight: obj.Weight, Italic: obj.Italic})
|
||||
obj.DrawParam = ""
|
||||
obj.Font, obj.Size, obj.Weight, obj.Italic = text.Font, text.Size, text.Weight, text.Italic
|
||||
obj.LineWidth, obj.Join, obj.MiterLimit = style.LineWidth, style.Join, style.MiterLimit
|
||||
obj.LineWidthSet = style.LineWidthSet
|
||||
obj.FillColor, obj.StrokeColor = style.FillColor, style.StrokeColor
|
||||
}
|
||||
for _, color := range []*FillColor{style.FillColor, (*FillColor)(style.StrokeColor)} {
|
||||
@@ -396,10 +398,11 @@ func (e *Editor) styleObject(object GraphicObject, style ObjectStyle) (GraphicOb
|
||||
path.StrokeColor = (*StrokeColor)(editorStyleColor((*FillColor)(style.StrokeColor), (*FillColor)(path.StrokeColor)))
|
||||
}
|
||||
if style.LineWidth != nil {
|
||||
if !finite(*style.LineWidth) || *style.LineWidth <= 0 {
|
||||
return GraphicObject{}, fmt.Errorf("line width must be positive")
|
||||
if !finite(*style.LineWidth) || *style.LineWidth < 0 {
|
||||
return GraphicObject{}, fmt.Errorf("line width must be nonnegative")
|
||||
}
|
||||
path.LineWidth = *style.LineWidth
|
||||
path.LineWidthSet = path.LineWidth == 0
|
||||
}
|
||||
if style.DashPattern != nil {
|
||||
path.DashPattern = *style.DashPattern
|
||||
@@ -547,8 +550,9 @@ func copyEditorPathStyle(to, from PathObject, scale float64) PathObject {
|
||||
to.Fill, to.Stroke, to.Alpha = from.Fill, from.Stroke, from.Alpha
|
||||
to.FillColor, to.StrokeColor = from.FillColor, from.StrokeColor
|
||||
to.LineWidth, to.Cap, to.Join, to.MiterLimit = from.LineWidth, from.Cap, from.Join, from.MiterLimit
|
||||
to.LineWidthSet = from.LineWidthSet
|
||||
to.DashPattern, to.DashOffset, to.dashPatternSet = from.DashPattern, from.DashOffset, true
|
||||
if to.LineWidth == 0 {
|
||||
if to.LineWidth == 0 && !to.LineWidthSet {
|
||||
to.LineWidth = defaultPathLineWidth
|
||||
}
|
||||
if to.Cap == "" {
|
||||
|
||||
+12
-2
@@ -35,7 +35,7 @@ func (o GraphicObject) Actions() []Action {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalXML 解析路径并区分省略的虚线样式与显式实线
|
||||
// UnmarshalXML 解析路径并区分省略样式、显式实线与零线宽
|
||||
// 入参: d XML解码器, start 起始节点
|
||||
// 返回: error 错误信息
|
||||
func (p *PathObject) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
@@ -48,6 +48,11 @@ func (p *PathObject) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error
|
||||
return err
|
||||
}
|
||||
*p = PathObject(value.plain)
|
||||
for _, attr := range start.Attr {
|
||||
if attr.Name.Local == "LineWidth" {
|
||||
p.LineWidthSet = p.LineWidth == 0
|
||||
}
|
||||
}
|
||||
if value.DashPattern != nil {
|
||||
p.DashPattern = *value.DashPattern
|
||||
p.dashPatternSet = p.DashPattern == ""
|
||||
@@ -55,7 +60,7 @@ func (p *PathObject) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalXML 解析绘制参数并保留显式实线对基础参数的覆盖
|
||||
// UnmarshalXML 解析绘制参数并保留显式实线和零线宽对基础参数的覆盖
|
||||
// 入参: d XML解码器, start 起始节点
|
||||
// 返回: error 错误信息
|
||||
func (p *DrawParam) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
@@ -68,6 +73,11 @@ func (p *DrawParam) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
return err
|
||||
}
|
||||
*p = DrawParam(value.plain)
|
||||
for _, attr := range start.Attr {
|
||||
if attr.Name.Local == "LineWidth" {
|
||||
p.LineWidthSet = p.LineWidth == 0
|
||||
}
|
||||
}
|
||||
if value.DashPattern != nil {
|
||||
p.DashPattern = *value.DashPattern
|
||||
p.dashPatternSet = p.DashPattern == ""
|
||||
|
||||
+12
-10
@@ -109,7 +109,8 @@ type TextObject struct {
|
||||
TextCode []TextCode `xml:"TextCode"`
|
||||
Clips *Clips `xml:"Clips"`
|
||||
Actions []Action `xml:"Actions>Action"`
|
||||
layout *textLayout
|
||||
LineWidthSet bool `xml:"-"`
|
||||
layout *textLayout
|
||||
}
|
||||
|
||||
// FillColor 填充颜色
|
||||
@@ -166,15 +167,14 @@ type CGTransform struct {
|
||||
|
||||
// PathObject 路径对象
|
||||
type PathObject struct {
|
||||
ID string `xml:"ID,attr"`
|
||||
Boundary string `xml:"Boundary,attr"`
|
||||
DrawParam string `xml:"DrawParam,attr"`
|
||||
LineWidth float64 `xml:"LineWidth,attr"`
|
||||
Join string `xml:"Join,attr"`
|
||||
Cap string `xml:"Cap,attr"`
|
||||
DashOffset *float64 `xml:"DashOffset,attr"`
|
||||
DashPattern string `xml:"DashPattern,attr"`
|
||||
dashPatternSet bool
|
||||
ID string `xml:"ID,attr"`
|
||||
Boundary string `xml:"Boundary,attr"`
|
||||
DrawParam string `xml:"DrawParam,attr"`
|
||||
LineWidth float64 `xml:"LineWidth,attr"`
|
||||
Join string `xml:"Join,attr"`
|
||||
Cap string `xml:"Cap,attr"`
|
||||
DashOffset *float64 `xml:"DashOffset,attr"`
|
||||
DashPattern string `xml:"DashPattern,attr"`
|
||||
MiterLimit float64 `xml:"MiterLimit,attr"`
|
||||
CTM string `xml:"CTM,attr"`
|
||||
Alpha *int `xml:"Alpha,attr"`
|
||||
@@ -187,6 +187,8 @@ type PathObject struct {
|
||||
AbbreviatedData string `xml:"AbbreviatedData"`
|
||||
Clips *Clips `xml:"Clips"`
|
||||
Actions []Action `xml:"Actions>Action"`
|
||||
LineWidthSet bool `xml:"-"`
|
||||
dashPatternSet bool
|
||||
}
|
||||
|
||||
// StrokeColor 勾边颜色
|
||||
|
||||
+84
-3
@@ -19,6 +19,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -57,6 +58,9 @@ type pdfImporter struct {
|
||||
pages map[pdfgo.Reference]*pdfgo.Page
|
||||
pageIDs map[pdfgo.Reference]string
|
||||
imageIDs map[*pdfgo.Stream]string
|
||||
maskClips map[*pdfgo.SoftMask]pdfgo.Path
|
||||
pageBox pdfgo.Rectangle
|
||||
pendingPath *pdfgo.PathMark
|
||||
}
|
||||
|
||||
// ImportPDF 将PDF内容转换为独立OFD编辑文档,失败时不返回部分结果
|
||||
@@ -91,7 +95,12 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
|
||||
matrix, _, _ := pdfPageMatrix(page)
|
||||
importer.matrix = matrix
|
||||
importer.page = index
|
||||
importer.pageBox = page.CropBox
|
||||
importer.maskClips = make(map[*pdfgo.SoftMask]pdfgo.Path)
|
||||
visitor := pdfgo.Visitor{Path: importer.path, Text: importer.text, Image: importer.image}
|
||||
visitor.Group = func(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error {
|
||||
return importer.group(mark, walk, visitor)
|
||||
}
|
||||
if !options.Strict {
|
||||
visitor.Warning = func(warning pdfgo.Diagnostic) {
|
||||
warning.Page = index + 1
|
||||
@@ -101,6 +110,9 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
|
||||
if err := reader.WalkPage(ctx, page, visitor); err != nil {
|
||||
return fmt.Errorf("import PDF page %d: %w", index+1, err)
|
||||
}
|
||||
if err := importer.flushPath(); err != nil {
|
||||
return fmt.Errorf("import PDF page %d: %w", index+1, err)
|
||||
}
|
||||
if err := importer.links(page); err != nil {
|
||||
return fmt.Errorf("import PDF page %d: %w", index+1, err)
|
||||
}
|
||||
@@ -249,10 +261,52 @@ func (p *pdfImporter) clips(paths []pdfgo.Path, origin Box) *Clips {
|
||||
return clips
|
||||
}
|
||||
|
||||
// path 添加独立可编辑路径
|
||||
// path 合并相同状态下的不透明描边,保留独立子路径与绘制顺序
|
||||
// 入参: mark PDF路径绘制信息
|
||||
// 返回: error 错误信息
|
||||
func (p *pdfImporter) path(mark pdfgo.PathMark) error {
|
||||
if !mark.Stroke || mark.Fill || mark.Style.Stroke.Alpha != 1 {
|
||||
if err := p.flushPath(); err != nil {
|
||||
return err
|
||||
}
|
||||
return p.appendPath(mark)
|
||||
}
|
||||
if p.pendingPath != nil && !reflect.DeepEqual(p.pendingPath.Style, mark.Style) {
|
||||
if err := p.flushPath(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if p.pendingPath == nil {
|
||||
p.pendingPath = &mark
|
||||
} else {
|
||||
p.pendingPath.Path.Segments = append(p.pendingPath.Path.Segments, mark.Path.Segments...)
|
||||
}
|
||||
if len(p.pendingPath.Path.Segments) >= 512 {
|
||||
return p.flushPath()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// flushPath 写入连续描边批次,保持后续图元的叠放位置
|
||||
// 返回: error 转换错误
|
||||
func (p *pdfImporter) flushPath() error {
|
||||
if p.pendingPath == nil {
|
||||
return nil
|
||||
}
|
||||
mark := *p.pendingPath
|
||||
p.pendingPath = nil
|
||||
return p.appendPath(mark)
|
||||
}
|
||||
|
||||
// appendPath 添加独立可编辑路径
|
||||
// 入参: mark PDF路径绘制信息
|
||||
// 返回: error 错误信息
|
||||
func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error {
|
||||
var err error
|
||||
mark.Style, err = p.maskStyle(mark.Style)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if mark.Fill && mark.Style.FillOverprint && !pdfOpaqueBlack(mark.Style.Fill) || mark.Stroke && mark.Style.StrokeOverprint && !pdfOpaqueBlack(mark.Style.Stroke) {
|
||||
return &pdfgo.UnsupportedError{Feature: "color separation overprint"}
|
||||
}
|
||||
@@ -271,8 +325,9 @@ func (p *pdfImporter) path(mark pdfgo.PathMark) error {
|
||||
margin := mark.Style.LineWidth * scale / 2 * math.Max(1, mark.Style.MiterLimit)
|
||||
box = Box{box.X - margin, box.Y - margin, box.W + 2*margin, box.H + 2*margin}
|
||||
}
|
||||
strokeColor := StrokeColor(*p.color(mark.Style.Stroke))
|
||||
object := PathObject{Boundary: pdfBoundary(box), AbbreviatedData: p.pathData(mark.Path, box), Fill: &mark.Fill, Stroke: &mark.Stroke, FillColor: p.color(mark.Style.Fill), StrokeColor: &strokeColor, LineWidth: mark.Style.LineWidth * scale, Cap: []string{"Butt", "Round", "Square"}[mark.Style.Cap], Join: []string{"Miter", "Round", "Bevel"}[mark.Style.Join], MiterLimit: mark.Style.MiterLimit, Clips: p.clips(mark.Style.Clips, box)}
|
||||
strokeColor := StrokeColor(*p.pathColor(mark.Style.Stroke, box))
|
||||
object := PathObject{Boundary: pdfBoundary(box), AbbreviatedData: p.pathData(mark.Path, box), Fill: &mark.Fill, Stroke: &mark.Stroke, FillColor: p.pathColor(mark.Style.Fill, box), StrokeColor: &strokeColor, LineWidth: mark.Style.LineWidth * scale, Cap: []string{"Butt", "Round", "Square"}[mark.Style.Cap], Join: []string{"Miter", "Round", "Bevel"}[mark.Style.Join], MiterLimit: mark.Style.MiterLimit, Clips: p.clips(mark.Style.Clips, box)}
|
||||
object.LineWidthSet = object.LineWidth == 0
|
||||
if mark.Path.EvenOdd {
|
||||
object.Rule = "Even-Odd"
|
||||
}
|
||||
@@ -290,6 +345,32 @@ func (p *pdfImporter) path(mark pdfgo.PathMark) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// pathColor 将页面渐变轴转换为对象局部坐标
|
||||
// 入参: paint PDF画刷, box 对象边界
|
||||
// 返回: *FillColor OFD颜色或渐变
|
||||
func (p *pdfImporter) pathColor(paint pdfgo.Paint, box Box) *FillColor {
|
||||
color := p.color(paint)
|
||||
if paint.Axial == nil {
|
||||
return color
|
||||
}
|
||||
gradient := paint.Axial
|
||||
start, end := p.matrix.Apply(gradient.Start), p.matrix.Apply(gradient.End)
|
||||
extend := 0
|
||||
if gradient.Extend[0] {
|
||||
extend |= 1
|
||||
}
|
||||
if gradient.Extend[1] {
|
||||
extend |= 2
|
||||
}
|
||||
shading := &AxialShd{StartPoint: pdfNumbers(start.X-box.X, start.Y-box.Y), EndPoint: pdfNumbers(end.X-box.X, end.Y-box.Y), Extend: strconv.Itoa(extend)}
|
||||
for _, stop := range gradient.Stops {
|
||||
shading.Segment = append(shading.Segment, ShdSegment{Position: stop.Position, Color: ShdColor{Value: pdfNumbers(math.Round(stop.RGB[0]*255), math.Round(stop.RGB[1]*255), math.Round(stop.RGB[2]*255))}})
|
||||
}
|
||||
color.Value = ""
|
||||
color.AxialShd = shading
|
||||
return color
|
||||
}
|
||||
|
||||
// pdfOpaqueBlack 判断不受底层分色影响的全黑设备色
|
||||
// 入参: paint PDF颜色与不透明度
|
||||
// 返回: bool 是否为不透明全黑CMYK颜色
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
// 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"
|
||||
"math"
|
||||
"reflect"
|
||||
|
||||
"github.com/xiaoqidun/pdfgo"
|
||||
)
|
||||
|
||||
// group 将不透明图元的透明组转换为互不重叠的填充区域,保留矢量与组不透明度
|
||||
// 入参: mark 组边界状态, walk 组内容访问函数, visitor 页面访问器
|
||||
// 返回: error 不支持的组内容或转换错误
|
||||
func (p *pdfImporter) group(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error, visitor pdfgo.Visitor) error {
|
||||
if err := p.flushPath(); err != nil {
|
||||
return err
|
||||
}
|
||||
if mark.SoftMask != nil {
|
||||
clip, err := p.maskClip(mark.SoftMask)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
original := walk
|
||||
walk = func(v pdfgo.Visitor) error {
|
||||
return original(pdfClippedVisitor(v, clip))
|
||||
}
|
||||
}
|
||||
if mark.Alpha == 1 {
|
||||
if err := walk(visitor); err != nil {
|
||||
return err
|
||||
}
|
||||
return p.flushPath()
|
||||
}
|
||||
geometry, err := p.editor.Geometry()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
type paintRegion struct {
|
||||
paint pdfgo.Paint
|
||||
path GeometryPath
|
||||
}
|
||||
var regions []paintRegion
|
||||
var combined GeometryPath
|
||||
var paint *pdfgo.Paint
|
||||
var clips []pdfgo.Path
|
||||
scale := math.Hypot(p.matrix[0], p.matrix[1])
|
||||
collect := pdfgo.Visitor{Warning: visitor.Warning}
|
||||
collect.Path = func(path pdfgo.PathMark) error {
|
||||
shape, err := ParseGeometryPath(p.pathData(path.Path, Box{}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, stroke := range []bool{false, true} {
|
||||
if stroke && !path.Stroke || !stroke && !path.Fill {
|
||||
continue
|
||||
}
|
||||
color, overprint := path.Style.Fill, path.Style.FillOverprint
|
||||
if stroke {
|
||||
color, overprint = path.Style.Stroke, path.Style.StrokeOverprint
|
||||
}
|
||||
if color.Alpha != 1 || color.Axial != nil || overprint && !pdfOpaqueBlack(color) {
|
||||
return &pdfgo.UnsupportedError{Feature: "transparent or overprinted group content"}
|
||||
}
|
||||
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"}
|
||||
} else if !reflect.DeepEqual(*paint, color) {
|
||||
regions = append(regions, paintRegion{*paint, combined})
|
||||
combined = nil
|
||||
paint = &color
|
||||
}
|
||||
outline := shape
|
||||
evenOdd := path.Path.EvenOdd
|
||||
if stroke {
|
||||
dashes := make([]float64, len(path.Style.Dash))
|
||||
for i, dash := range path.Style.Dash {
|
||||
dashes[i] = dash * scale
|
||||
}
|
||||
outline, err = geometry.Stroke(shape, StrokeOptions{Width: path.Style.LineWidth * scale, Cap: []string{"Butt", "Round", "Square"}[path.Style.Cap], Join: []string{"Miter", "Round", "Bevel"}[path.Style.Join], MiterLimit: path.Style.MiterLimit, DashOffset: path.Style.DashPhase * scale, Dashes: dashes, Tolerance: 0.001})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
evenOdd = false
|
||||
}
|
||||
outline, err = geometry.Normalize(outline, evenOdd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
combined = append(combined, outline...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := walk(collect); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(combined) == 0 {
|
||||
return nil
|
||||
}
|
||||
regions = append(regions, paintRegion{*paint, combined})
|
||||
var covered GeometryPath
|
||||
for i := len(regions) - 1; i >= 0; i-- {
|
||||
region := regions[i]
|
||||
visible := region.path
|
||||
if len(covered) > 0 {
|
||||
visible, err = geometry.Combine(visible, covered, GeometrySubtract)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if i > 0 {
|
||||
covered, err = geometry.Combine(covered, region.path, GeometryUnion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(visible) == 0 {
|
||||
continue
|
||||
}
|
||||
box, err := geometry.Bounds(visible)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if box.W == 0 || box.H == 0 {
|
||||
continue
|
||||
}
|
||||
visible, err = geometry.Transform(visible, TranslationMatrix(-box.X, -box.Y))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := visible.OFD()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
region.paint.Alpha = mark.Alpha
|
||||
fill, stroke := true, false
|
||||
object := PathObject{Boundary: pdfBoundary(box), AbbreviatedData: data, Fill: &fill, Stroke: &stroke, FillColor: p.color(region.paint), Clips: p.clips(clips, box)}
|
||||
p.objects = append(p.objects, GraphicObject{Type: "PathObject", PathObject: object})
|
||||
p.report.PathObjects++
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// pdfClippedVisitor 为图元及嵌套组附加裁剪,保留未提供的访问能力
|
||||
// 入参: visitor 图元访问器, clip 页面坐标裁剪
|
||||
// 返回: pdfgo.Visitor 裁剪访问器
|
||||
func pdfClippedVisitor(visitor pdfgo.Visitor, clip pdfgo.Path) pdfgo.Visitor {
|
||||
result := visitor
|
||||
if visitor.Path != nil {
|
||||
result.Path = func(mark pdfgo.PathMark) error {
|
||||
mark.Style.Clips = append(append([]pdfgo.Path(nil), mark.Style.Clips...), clip)
|
||||
return visitor.Path(mark)
|
||||
}
|
||||
}
|
||||
if visitor.Text != nil {
|
||||
result.Text = func(mark pdfgo.TextMark) error {
|
||||
mark.Style.Clips = append(append([]pdfgo.Path(nil), mark.Style.Clips...), clip)
|
||||
return visitor.Text(mark)
|
||||
}
|
||||
}
|
||||
if visitor.Image != nil {
|
||||
result.Image = func(mark pdfgo.ImageMark) error {
|
||||
mark.Style.Clips = append(append([]pdfgo.Path(nil), mark.Style.Clips...), clip)
|
||||
return visitor.Image(mark)
|
||||
}
|
||||
}
|
||||
if visitor.Group != nil {
|
||||
result.Group = func(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error {
|
||||
return visitor.Group(mark, func(v pdfgo.Visitor) error { return walk(pdfClippedVisitor(v, clip)) })
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// maskClip 将二值亮度蒙版转换为标准矢量裁剪,不栅格化原图元
|
||||
// 入参: mask PDF蒙版
|
||||
// 返回: pdfgo.Path 页面坐标裁剪路径, error 不可精确转换或几何错误
|
||||
func (p *pdfImporter) maskClip(mask *pdfgo.SoftMask) (pdfgo.Path, error) {
|
||||
if clip, ok := p.maskClips[mask]; ok {
|
||||
return clip, nil
|
||||
}
|
||||
geometry, err := p.editor.Geometry()
|
||||
if err != nil {
|
||||
return pdfgo.Path{}, err
|
||||
}
|
||||
opacity := func(v float64) (bool, error) {
|
||||
v = math.Max(0, math.Min(1, mask.Transfer[0]+v*(mask.Transfer[1]-mask.Transfer[0])))
|
||||
if v != 0 && v != 1 {
|
||||
return false, &pdfgo.UnsupportedError{Feature: "nonbinary luminosity mask"}
|
||||
}
|
||||
return v == 1, nil
|
||||
}
|
||||
background, err := opacity(mask.Background)
|
||||
if err != nil {
|
||||
return pdfgo.Path{}, err
|
||||
}
|
||||
box := p.pageBox
|
||||
var region GeometryPath
|
||||
if background {
|
||||
region, err = ParseGeometryPath(pdfNumbersPathBox(box))
|
||||
if err != nil {
|
||||
return pdfgo.Path{}, err
|
||||
}
|
||||
region, err = geometry.Normalize(region, false)
|
||||
if err != nil {
|
||||
return pdfgo.Path{}, err
|
||||
}
|
||||
}
|
||||
identity := pdfImporter{matrix: pdfgo.Identity()}
|
||||
var visitor pdfgo.Visitor
|
||||
visitor.Group = func(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error {
|
||||
if mark.Alpha != 1 || mark.SoftMask != nil {
|
||||
return &pdfgo.UnsupportedError{Feature: "transparent mask group"}
|
||||
}
|
||||
return walk(visitor)
|
||||
}
|
||||
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.RGB[0] != paint.RGB[1] || paint.RGB[1] != paint.RGB[2] || mark.Style.SoftMask != nil {
|
||||
return &pdfgo.UnsupportedError{Feature: "nonbinary mask graphic"}
|
||||
}
|
||||
visible, err := opacity(paint.RGB[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
shape, err := ParseGeometryPath(identity.pathData(mark.Path, Box{}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
shape, err = geometry.Normalize(shape, mark.Path.EvenOdd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, clip := range mark.Style.Clips {
|
||||
c, err := ParseGeometryPath(identity.pathData(clip, Box{}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c, err = geometry.Normalize(c, clip.EvenOdd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
shape, err = geometry.Combine(shape, c, GeometryIntersect)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
operation := GeometrySubtract
|
||||
if visible {
|
||||
operation = GeometryUnion
|
||||
}
|
||||
region, err = geometry.Combine(region, shape, operation)
|
||||
return err
|
||||
}
|
||||
if err := mask.Walk(visitor); err != nil {
|
||||
return pdfgo.Path{}, err
|
||||
}
|
||||
path := pdfgo.Path{}
|
||||
for _, segment := range region {
|
||||
point := func(p Point) pdfgo.Point { return pdfgo.Point{X: p.X, Y: p.Y} }
|
||||
switch segment.Verb {
|
||||
case GeometryMove:
|
||||
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 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"})
|
||||
default:
|
||||
return pdfgo.Path{}, fmt.Errorf("unsupported mask geometry verb %d", segment.Verb)
|
||||
}
|
||||
}
|
||||
if p.maskClips == nil {
|
||||
p.maskClips = make(map[*pdfgo.SoftMask]pdfgo.Path)
|
||||
}
|
||||
p.maskClips[mask] = path
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// pdfNumbersPathBox 编码PDF坐标矩形的闭合路径
|
||||
// 入参: box 矩形边界
|
||||
// 返回: string 紧缩路径
|
||||
func pdfNumbersPathBox(box pdfgo.Rectangle) string {
|
||||
return "M " + pdfNumbers(box.XMin, box.YMin) + " L " + pdfNumbers(box.XMax, box.YMin) + " L " + pdfNumbers(box.XMax, box.YMax) + " L " + pdfNumbers(box.XMin, box.YMax) + " C"
|
||||
}
|
||||
|
||||
// maskStyle 将独立图元的二值蒙版附加到裁剪链
|
||||
// 入参: style 图形状态
|
||||
// 返回: pdfgo.Style 转换后的状态, error 蒙版转换错误
|
||||
func (p *pdfImporter) maskStyle(style pdfgo.Style) (pdfgo.Style, error) {
|
||||
if style.SoftMask == nil {
|
||||
return style, nil
|
||||
}
|
||||
clip, err := p.maskClip(style.SoftMask)
|
||||
if err != nil {
|
||||
return style, err
|
||||
}
|
||||
style.Clips = append(append([]pdfgo.Path(nil), style.Clips...), clip)
|
||||
style.SoftMask = nil
|
||||
return style, nil
|
||||
}
|
||||
@@ -33,6 +33,17 @@ import (
|
||||
// 入参: mark PDF图像绘制信息
|
||||
// 返回: error 错误信息
|
||||
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 {
|
||||
return &pdfgo.UnsupportedError{Feature: "gradient stencil image"}
|
||||
}
|
||||
var err error
|
||||
mark.Style, err = p.maskStyle(mark.Style)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if mark.Style.FillOverprint {
|
||||
return &pdfgo.UnsupportedError{Feature: "image color separation overprint"}
|
||||
}
|
||||
@@ -102,6 +113,17 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
|
||||
// 入参: mark PDF文字绘制信息
|
||||
// 返回: error 错误信息
|
||||
func (p *pdfImporter) text(mark pdfgo.TextMark) error {
|
||||
if err := p.flushPath(); err != nil {
|
||||
return err
|
||||
}
|
||||
if mark.Style.Fill.Axial != nil || mark.Style.Stroke.Axial != nil {
|
||||
return &pdfgo.UnsupportedError{Feature: "gradient text paint"}
|
||||
}
|
||||
var err error
|
||||
mark.Style, err = p.maskStyle(mark.Style)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if (mark.Mode == 0 || mark.Mode == 2) && mark.Style.FillOverprint && !pdfOpaqueBlack(mark.Style.Fill) || (mark.Mode == 1 || mark.Mode == 2) && mark.Style.StrokeOverprint && !pdfOpaqueBlack(mark.Style.Stroke) {
|
||||
return &pdfgo.UnsupportedError{Feature: "text color separation overprint"}
|
||||
}
|
||||
@@ -192,6 +214,7 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
|
||||
color := StrokeColor(*p.color(mark.Style.Stroke))
|
||||
object.StrokeColor = &color
|
||||
object.LineWidth = mark.Style.LineWidth * unit
|
||||
object.LineWidthSet = object.LineWidth == 0
|
||||
object.Join = []string{"Miter", "Round", "Bevel"}[mark.Style.Join]
|
||||
object.MiterLimit = mark.Style.MiterLimit
|
||||
if len(mark.Style.Dash) > 0 {
|
||||
@@ -234,6 +257,18 @@ func pdfFontProgram(source *pdfgo.Font) ([]byte, error) {
|
||||
return nil, fmt.Errorf("embedded PDF font has no glyphs")
|
||||
}
|
||||
changed := false
|
||||
metrics := int(binary.BigEndian.Uint16(tables["hhea"][34:36]))
|
||||
if metrics == 0 || metrics > int(count) {
|
||||
return nil, fmt.Errorf("invalid embedded PDF font metric count")
|
||||
}
|
||||
metricLength := 4*metrics + 2*(int(count)-metrics)
|
||||
if len(tables["hmtx"]) < metricLength {
|
||||
return nil, fmt.Errorf("incomplete embedded PDF font metrics")
|
||||
}
|
||||
if len(tables["hmtx"]) > metricLength {
|
||||
tables["hmtx"] = tables["hmtx"][:metricLength]
|
||||
changed = true
|
||||
}
|
||||
if len(tables["cmap"]) == 0 {
|
||||
mapping := map[rune]uint16{}
|
||||
for code := range source.Unicode {
|
||||
|
||||
@@ -260,7 +260,7 @@ func (c *semanticCompiler) objectStyle(object PathObject, defaults *DrawParam, l
|
||||
if defaults != nil {
|
||||
style.fill = defaults.FillColor
|
||||
style.stroke = (*FillColor)(defaults.StrokeColor)
|
||||
if defaults.LineWidth > 0 {
|
||||
if defaults.LineWidth > 0 || defaults.LineWidthSet {
|
||||
style.options.Width = defaults.LineWidth
|
||||
}
|
||||
if defaults.Cap != "" {
|
||||
@@ -283,7 +283,7 @@ func (c *semanticCompiler) objectStyle(object PathObject, defaults *DrawParam, l
|
||||
if object.StrokeColor != nil {
|
||||
style.stroke = (*FillColor)(object.StrokeColor)
|
||||
}
|
||||
if object.LineWidth > 0 {
|
||||
if object.LineWidth > 0 || object.LineWidthSet {
|
||||
style.options.Width = object.LineWidth
|
||||
}
|
||||
if object.Cap != "" {
|
||||
@@ -313,6 +313,9 @@ func (c *semanticCompiler) objectStyle(object PathObject, defaults *DrawParam, l
|
||||
style.options.Dashes[i] *= scale
|
||||
}
|
||||
}
|
||||
if style.options.Width == 0 {
|
||||
style.options.Width = 25.4 / c.renderer.DPI
|
||||
}
|
||||
return style
|
||||
}
|
||||
|
||||
@@ -389,7 +392,7 @@ func (c *semanticCompiler) textObject(object TextObject, state RenderState) erro
|
||||
return nil
|
||||
}
|
||||
_, linear := renderObjectMatrix(object.Boundary, NewMatrix(object.CTM), state)
|
||||
style := c.objectStyle(PathObject{DrawParam: object.DrawParam, FillColor: object.FillColor, StrokeColor: object.StrokeColor, Alpha: object.Alpha, LineWidth: object.LineWidth, Join: object.Join, MiterLimit: object.MiterLimit}, state.Defaults, linear)
|
||||
style := c.objectStyle(PathObject{DrawParam: object.DrawParam, FillColor: object.FillColor, StrokeColor: object.StrokeColor, Alpha: object.Alpha, LineWidth: object.LineWidth, LineWidthSet: object.LineWidthSet, Join: object.Join, MiterLimit: object.MiterLimit}, state.Defaults, linear)
|
||||
if style.fill == nil {
|
||||
style.fill = withFillAlpha(&FillColor{}, object.Alpha)
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ func (CanvasBackend) Combine(left, right GeometryPath, operation GeometryOperati
|
||||
var result *canvas.Path
|
||||
switch operation {
|
||||
case GeometryIntersect:
|
||||
result = a.And(b)
|
||||
result = applyClipPath(a, b)
|
||||
case GeometryUnion:
|
||||
result = a.Or(b)
|
||||
case GeometrySubtract:
|
||||
|
||||
@@ -108,7 +108,7 @@ func (s *pathStyle) applyLineJoin(join string, limit float64) {
|
||||
// applyDrawParam 应用绘制参数样式
|
||||
// 入参: r 渲染器, dp 绘制参数, bx 边界X坐标, by 边界Y坐标, pageH 页面高度, alpha 对象透明度
|
||||
func (s *pathStyle) applyDrawParam(r *Renderer, dp *DrawParam, bx, by, pageH float64, alpha *int) {
|
||||
if dp.LineWidth > 0 {
|
||||
if dp.LineWidth > 0 || dp.LineWidthSet {
|
||||
s.lineWidth = dp.LineWidth
|
||||
}
|
||||
if dp.FillColor != nil {
|
||||
@@ -132,7 +132,7 @@ func (s *pathStyle) applyDrawParam(r *Renderer, dp *DrawParam, bx, by, pageH flo
|
||||
// applyPathObject 应用路径对象样式
|
||||
// 入参: r 渲染器, obj 路径对象, bx 边界X坐标, by 边界Y坐标, pageH 页面高度
|
||||
func (s *pathStyle) applyPathObject(r *Renderer, obj PathObject, bx, by, pageH float64) {
|
||||
if obj.LineWidth > 0 {
|
||||
if obj.LineWidth > 0 || obj.LineWidthSet {
|
||||
s.lineWidth = obj.LineWidth
|
||||
}
|
||||
if obj.FillColor != nil {
|
||||
@@ -210,6 +210,9 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64
|
||||
}
|
||||
style.applyPathObject(r, obj, bx, by, pageH)
|
||||
style.scale(ctm)
|
||||
if style.lineWidth == 0 {
|
||||
style.lineWidth = 25.4 / r.DPI
|
||||
}
|
||||
objectCTM := TranslationMatrix(bx, by).Multiply(ctm)
|
||||
if boundaryInCTM && parentCTM != nil {
|
||||
objectCTM = parentCTM.Multiply(TranslationMatrix(bx, by)).Multiply(localCTM)
|
||||
|
||||
@@ -110,7 +110,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
|
||||
if obj.StrokeColor != nil {
|
||||
strokeStyle.applyStrokeColor(r, obj.StrokeColor, bx, by, pageH, obj.Alpha)
|
||||
}
|
||||
if obj.LineWidth > 0 {
|
||||
if obj.LineWidth > 0 || obj.LineWidthSet {
|
||||
strokeStyle.lineWidth = obj.LineWidth
|
||||
}
|
||||
strokeStyle.applyLineJoin(obj.Join, obj.MiterLimit)
|
||||
@@ -118,6 +118,9 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
|
||||
strokeStyle.strokePaint = colorWithAlpha(canvas.Black, obj.Alpha)
|
||||
}
|
||||
strokeStyle.scale(ctm)
|
||||
if strokeStyle.lineWidth == 0 {
|
||||
strokeStyle.lineWidth = 25.4 / r.DPI
|
||||
}
|
||||
transformShdPaint(strokeStyle.strokePaint, parentCTM, bx, by, pageH, boundaryInCTM)
|
||||
strokeStyle.strokePaint, shadingClip, strokeView = resolveShdPaint(ctx, strokeStyle.strokePaint)
|
||||
strokeClip = intersectClipPath(strokeClip, shadingClip)
|
||||
|
||||
+18
-17
@@ -78,24 +78,25 @@ type DrawParams struct {
|
||||
|
||||
// DrawParam 绘制参数
|
||||
type DrawParam struct {
|
||||
ID string `xml:"ID,attr"`
|
||||
Relative string `xml:"Relative,attr"`
|
||||
ResourceID string `xml:"ResourceID,attr"`
|
||||
BaseLoc string `xml:"BaseLoc,attr"`
|
||||
Link string `xml:"Link,attr"`
|
||||
LineWidth float64 `xml:"LineWidth,attr"`
|
||||
Join string `xml:"Join,attr"`
|
||||
Cap string `xml:"Cap,attr"`
|
||||
DashOffset *float64 `xml:"DashOffset,attr"`
|
||||
DashPattern string `xml:"DashPattern,attr"`
|
||||
ID string `xml:"ID,attr"`
|
||||
Relative string `xml:"Relative,attr"`
|
||||
ResourceID string `xml:"ResourceID,attr"`
|
||||
BaseLoc string `xml:"BaseLoc,attr"`
|
||||
Link string `xml:"Link,attr"`
|
||||
LineWidth float64 `xml:"LineWidth,attr"`
|
||||
Join string `xml:"Join,attr"`
|
||||
Cap string `xml:"Cap,attr"`
|
||||
DashOffset *float64 `xml:"DashOffset,attr"`
|
||||
DashPattern string `xml:"DashPattern,attr"`
|
||||
MiterLimit float64 `xml:"MiterLimit,attr"`
|
||||
Font string `xml:"Font,attr"`
|
||||
Size float64 `xml:"Size,attr"`
|
||||
Weight int `xml:"Weight,attr"`
|
||||
Italic bool `xml:"Italic,attr"`
|
||||
FillColor *FillColor `xml:"FillColor"`
|
||||
StrokeColor *StrokeColor `xml:"StrokeColor"`
|
||||
LineWidthSet bool `xml:"-"`
|
||||
dashPatternSet bool
|
||||
MiterLimit float64 `xml:"MiterLimit,attr"`
|
||||
Font string `xml:"Font,attr"`
|
||||
Size float64 `xml:"Size,attr"`
|
||||
Weight int `xml:"Weight,attr"`
|
||||
Italic bool `xml:"Italic,attr"`
|
||||
FillColor *FillColor `xml:"FillColor"`
|
||||
StrokeColor *StrokeColor `xml:"StrokeColor"`
|
||||
}
|
||||
|
||||
// CompositeGraphicUnits 复合图元集合
|
||||
|
||||
@@ -260,7 +260,6 @@ func (r *Reader) applySignatureCoverage(report *SignatureVerifyReport, listPath
|
||||
for name := range r.files {
|
||||
include(name)
|
||||
}
|
||||
// 可达文件始终必需, 不允许伪装为签名载体后排除
|
||||
if err := r.signatureCoverageReferences(o.DocRoot, required); err != nil {
|
||||
report.CoverageOK, report.CoverageError = false, err.Error()
|
||||
}
|
||||
|
||||
@@ -330,7 +330,6 @@ func parseSESSeal(raw asn1.RawValue) (*sesSeal, error) {
|
||||
if info.Version != 5 {
|
||||
return nil, fmt.Errorf("unexpected timestamp in ses seal version %d", info.Version)
|
||||
}
|
||||
// V5仅保留解析能力, 不将未验证的制章时间戳当作可信时间
|
||||
timestampRaw = append([]byte(nil), items[4].FullBytes...)
|
||||
}
|
||||
picType, picData, err := parseSESPicture(infoItems[3])
|
||||
|
||||
@@ -128,7 +128,6 @@ func layoutShapedText(obj *TextObject, value string, options TextLayout, metrics
|
||||
if options.Shaping.Bidi {
|
||||
shift = visualShifts[glyph.Cluster]
|
||||
} else if options.Shaping.Direction == "rtl" {
|
||||
// 逻辑簇后的间距位于视觉左侧,整行不计末尾字距
|
||||
shift = extra[len(chars)] - extra[glyph.Cluster] - options.LetterSpacing
|
||||
}
|
||||
gx, gy := x+glyph.X*hScale+shift, y+glyph.Y
|
||||
|
||||
@@ -68,5 +68,10 @@ func (obj *TextObject) UnmarshalXML(decoder *xml.Decoder, start xml.StartElement
|
||||
}
|
||||
}
|
||||
*obj = TextObject(value)
|
||||
for _, attr := range start.Attr {
|
||||
if attr.Name.Local == "LineWidth" {
|
||||
obj.LineWidthSet = obj.LineWidth == 0
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -668,6 +668,9 @@ func (x *ofdXML) object(object GraphicObject, root bool) {
|
||||
attrs.number("ReadDirection", float64(obj.ReadDirection))
|
||||
attrs.number("CharDirection", float64(obj.CharDirection))
|
||||
attrs.number("LineWidth", obj.LineWidth)
|
||||
if obj.LineWidthSet && obj.LineWidth == 0 {
|
||||
attrs.add("LineWidth", "0")
|
||||
}
|
||||
attrs.number("MiterLimit", obj.MiterLimit)
|
||||
attrs.add("Join", obj.Join)
|
||||
if obj.Italic {
|
||||
@@ -686,6 +689,9 @@ func (x *ofdXML) object(object GraphicObject, root bool) {
|
||||
attrs.add("Boundary", obj.Boundary)
|
||||
attrs.add("CTM", obj.CTM)
|
||||
attrs.number("LineWidth", obj.LineWidth)
|
||||
if obj.LineWidthSet && obj.LineWidth == 0 {
|
||||
attrs.add("LineWidth", "0")
|
||||
}
|
||||
attrs.number("MiterLimit", obj.MiterLimit)
|
||||
attrs.add("Join", obj.Join)
|
||||
attrs.add("Cap", obj.Cap)
|
||||
|
||||
Reference in new issue
Block a user