Files
ofdgo/ofdgo_pdf_group.go
T
2026-10-04 16:29:30 +08:00

713 lines
24 KiB
Go

// 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"
"errors"
"fmt"
"image"
"image/png"
"math"
"reflect"
"github.com/xiaoqidun/pdfgo"
)
// errPDFGroupRaster 表示透明组需要按指定精度局部合成
var errPDFGroupRaster = errors.New("PDF transparency group requires local compositing")
// pdfPaintRegion 保存透明组内的单色填充区域
type pdfPaintRegion struct {
paint pdfgo.Paint
path GeometryPath
}
// group 将单色图元的透明组转换为互不重叠的填充区域,保留矢量与组不透明度
// 入参: mark 组边界状态, walk 组内容访问函数, visitor 页面访问器
// 返回: error 不支持的组内容或转换错误
func (p *pdfImporter) group(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error, visitor pdfgo.Visitor) error {
if mark.Knockout {
return &pdfgo.UnsupportedError{Feature: "knockout group outside page compositing"}
}
if mark.BlendMode != "" && mark.BlendMode != "Normal" && mark.BlendMode != "Compatible" {
return &pdfgo.UnsupportedError{Feature: "group blend mode " + string(mark.BlendMode)}
}
if err := p.flushPath(); err != nil {
return err
}
if mark.ColorSpace != nil && !mark.ColorSpace.SRGBEquivalent() {
if mark.Alpha != 1 || mark.SoftMask != nil {
return &pdfgo.UnsupportedError{Feature: "non-sRGB group color space"}
}
if err := walk(pdfOpaqueVisitor(visitor)); err != nil {
return err
}
}
if mark.SoftMask != nil {
clip, err := p.maskClip(mark.SoftMask)
if err != nil {
if p.rasterMaskAllowed(err) {
return p.rasterGroup(mark, walk)
}
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()
}
err := p.groupPaths(mark, walk, visitor)
if errors.Is(err, errPDFGroupRaster) && p.warning != nil {
return p.rasterGroup(mark, walk)
}
return err
}
// pdfOpaqueVisitor 验证组内没有透明混合,允许独立的已校准颜色保留为矢量
// 入参: visitor 正式访问器,语义提示由正式遍历交付
// 返回: pdfgo.Visitor 不透明性检查器
func pdfOpaqueVisitor(visitor pdfgo.Visitor) pdfgo.Visitor {
check := func(s pdfgo.Style, fill, stroke bool) error {
if s.SoftMask != nil || fill && s.Fill.Alpha != 1 || stroke && s.Stroke.Alpha != 1 || s.BlendMode != "" && s.BlendMode != "Normal" && s.BlendMode != "Compatible" {
return &pdfgo.UnsupportedError{Feature: "non-sRGB transparent group content"}
}
return nil
}
v := pdfgo.Visitor{
Reference: visitor.Reference,
OptionalContent: visitor.OptionalContent,
Halftones: visitor.Halftones,
Path: func(m pdfgo.PathMark) error { return check(m.Style, m.Fill, m.Stroke) },
Text: func(m pdfgo.TextMark) error {
return check(m.Style, m.Mode%4 == 0 || m.Mode%4 == 2, m.Mode%4 == 1 || m.Mode%4 == 2)
},
Image: func(m pdfgo.ImageMark) error {
if m.Image.ImageMask || m.Image.Mask != nil || m.Image.SoftMask != nil || m.Image.Stream.Dictionary["SMaskInData"] != nil {
return &pdfgo.UnsupportedError{Feature: "non-sRGB masked group image"}
}
return check(m.Style, true, false)
},
}
if visitor.Warning != nil {
v.Warning = func(pdfgo.Diagnostic) {}
}
if visitor.MarkedContent != nil {
v.MarkedContent = func(pdfgo.MarkedContentMark) error { return nil }
}
v.Group = func(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error {
if mark.Alpha != 1 || mark.SoftMask != nil || mark.BlendMode != "" && mark.BlendMode != "Normal" && mark.BlendMode != "Compatible" {
return &pdfgo.UnsupportedError{Feature: "non-sRGB nested transparent group"}
}
return walk(v)
}
return v
}
// groupPaths 将正常混合的透明路径组分解为标准矢量区域
// 入参: mark 组状态, walk 内容访问函数, visitor 页面访问器
// 返回: error 无法矢量表达或几何错误
func (p *pdfImporter) groupPaths(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error, visitor pdfgo.Visitor) error {
geometry, err := p.editor.Geometry()
if err != nil {
return err
}
var regions []pdfPaintRegion
var combined GeometryPath
var paint *pdfgo.Paint
var clips []pdfgo.Path
opaque := true
scale := math.Hypot(p.matrix[0], p.matrix[1])
collect := pdfgo.Visitor{
Reference: visitor.Reference,
OptionalContent: visitor.OptionalContent,
Halftones: visitor.Halftones,
Warning: visitor.Warning,
Text: func(pdfgo.TextMark) error { return errPDFGroupRaster },
Image: func(pdfgo.ImageMark) error { return errPDFGroupRaster },
Group: func(pdfgo.GroupMark, func(pdfgo.Visitor) error) error { return errPDFGroupRaster },
}
collect.Path = func(path pdfgo.PathMark) error {
if err := p.ctx.Err(); err != nil {
return err
}
if path.Style.SoftMask != nil || path.Style.BlendMode != "" && path.Style.BlendMode != "Normal" && path.Style.BlendMode != "Compatible" {
return &pdfgo.UnsupportedError{Feature: "masked or blended transparent group content"}
}
data, err := p.pathDataContext(p.ctx, path.Path, Box{})
if err != nil {
return err
}
shape, err := ParseGeometryPath(data)
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.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 {
continue
}
opaque = opaque && color.Alpha == 1
if paint == nil {
paint, clips = &color, path.Style.Clips
} else if !reflect.DeepEqual(clips, path.Style.Clips) {
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
paint = &color
}
outline := shape
evenOdd := path.Path.EvenOdd
if stroke {
if path.Style.LineWidth == 0 {
return fmt.Errorf("%w: zero-width group stroke", errPDFGroupRaster)
}
strokeShape, strokeScale, tolerance := shape, scale, 0.001
var strokeTransform pdfgo.Matrix
if path.StrokeMatrix != (pdfgo.Matrix{}) {
inverse, ok := path.StrokeMatrix.Inverse()
if !ok {
return fmt.Errorf("invalid PDF stroke matrix")
}
local := pdfImporter{matrix: inverse}
data, err = local.pathDataContext(p.ctx, path.Path, Box{})
if err != nil {
return err
}
strokeShape, err = ParseGeometryPath(data)
if err != nil {
return err
}
strokeScale = 1
strokeTransform = p.matrix.Mul(path.StrokeMatrix)
tolerance /= math.Max(math.Hypot(strokeTransform[0], strokeTransform[2]), math.Hypot(strokeTransform[1], strokeTransform[3]))
}
dashes := make([]float64, len(path.Style.Dash))
for i, dash := range path.Style.Dash {
dashes[i] = dash * strokeScale
}
outline, err = geometry.Stroke(strokeShape, StrokeOptions{Width: path.Style.LineWidth * strokeScale, Cap: []string{"Butt", "Round", "Square"}[path.Style.Cap], Join: []string{"Miter", "Round", "Bevel"}[path.Style.Join], MiterLimit: path.Style.MiterLimit, DashOffset: path.Style.DashPhase * strokeScale, Dashes: dashes, Tolerance: tolerance})
if err != nil {
return err
}
if strokeTransform != (pdfgo.Matrix{}) {
outline, err = geometry.Transform(outline, NewMatrix(pdfNumbers(strokeTransform[:]...)))
if err != nil {
return err
}
}
evenOdd = false
}
outline, err = geometry.Normalize(outline, evenOdd)
if err != nil {
return err
}
if err := p.ctx.Err(); err != nil {
return err
}
combined = append(combined, outline...)
}
return nil
}
if err := walk(collect); err != nil {
return err
}
if err := p.ctx.Err(); err != nil {
return err
}
if len(combined) == 0 {
return nil
}
regions = append(regions, pdfPaintRegion{*paint, combined})
if !opaque {
regions, err = pdfCompositeRegions(geometry, regions)
if err != nil {
return err
}
}
var covered GeometryPath
for i := len(regions) - 1; i >= 0; i-- {
if err := p.ctx.Err(); err != nil {
return err
}
region := regions[i]
visible := region.path
if opaque && len(covered) > 0 {
visible, err = geometry.Combine(visible, covered, GeometrySubtract)
if err != nil {
return err
}
}
if opaque && 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
objectClips, err := p.clips(clips, box)
if err != nil {
return err
}
object := PathObject{Boundary: pdfBoundary(box), AbbreviatedData: data, Fill: &fill, Stroke: &stroke, FillColor: p.color(region.paint), Clips: objectClips}
p.objects = append(p.objects, GraphicObject{Type: "PathObject", PathObject: object})
p.report.PathObjects++
}
return nil
}
// rasterGroup 在透明背景上合成局部图元,组透明度应用于合成结果,保留组外对象
// 入参: mark 组状态, walk 内容访问函数
// 返回: error 转换或渲染错误
func (p *pdfImporter) rasterGroup(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error {
img, box, err := p.renderGroup(func(local *pdfImporter) error { return walk(local.visitor()) }, nil)
if err != nil || img == nil {
return err
}
if mark.SoftMask != nil {
img, err = p.applySoftMask(img, box, mark.SoftMask)
if err != nil {
return err
}
}
return p.appendRasterGroup(img, box, mark.Alpha)
}
// visitor 绑定当前导入器的图元与透明组访问函数
// 返回: pdfgo.Visitor 页面访问器
func (p *pdfImporter) visitor() pdfgo.Visitor {
v := pdfgo.Visitor{Path: p.path, Text: p.text, Image: p.image, Warning: p.warning, Reference: p.referencePage, Halftones: p.halftones}
v.Group = func(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error { return p.group(mark, walk, v) }
return v
}
// renderGroup 按统一像素网格渲染局部内容,保留裁剪及后端选择
// 入参: build 局部图元构建函数, viewport 指定裁取区域,nil时按图元边界计算
// 返回: image.Image 透明图像, Box 页面区域, error 构建或渲染错误
func (p *pdfImporter) renderGroup(build func(*pdfImporter) error, viewport *Box) (image.Image, Box, error) {
scene, box, err := p.compileGroup(build)
if err != nil || scene == nil && viewport == nil {
return nil, box, err
}
if viewport != nil {
box = *viewport
}
if scene == nil {
scene = &RasterPage{DPI: p.rasterDPI}
}
img, err := p.renderGroupScene(scene, box)
return img, box, err
}
// compileGroup 编译局部图元并计算页面内的像素对齐边界
// 入参: build 局部图元构建函数
// 返回: *RasterPage 中立绘制场景, Box 页面区域, error 编译错误
func (p *pdfImporter) compileGroup(build func(*pdfImporter) error) (*RasterPage, Box, error) {
var box Box
if err := p.ctx.Err(); err != nil {
return nil, box, err
}
editor := NewEditor()
editor.SetRenderBackends(p.editor.Backends())
editor.SetFontDirs(p.editor.fontDirs...)
editor.SetFontFS(p.editor.fontFS...)
if _, err := editor.AddPage(p.pageWidth, p.pageHeight); err != nil {
return nil, box, err
}
if p.fonts == nil {
p.fonts = make(map[*pdfgo.Font]*pdfImportedFont)
}
if p.clipPaths == nil {
p.clipPaths = &renderCache[[32]byte, pdfClipPath]{limit: 8 << 20}
}
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, clipPaths: p.clipPaths}
if err := build(&local); err != nil {
return nil, box, err
}
if err := p.ctx.Err(); err != nil {
return nil, box, err
}
if err := local.flushPath(); err != nil {
return nil, box, err
}
if len(local.objects) == 0 {
return nil, box, nil
}
if err := local.commitObjects(); err != nil {
return nil, box, err
}
reader, err := editor.resourceReader()
if err != nil {
return nil, box, err
}
defer reader.Close()
page := &editor.pages[0]
renderer := p.renderer.childRenderer(reader)
renderer.TransparentBackground = true
for _, layer := range page.Content.Layer {
for _, object := range layer.Objects {
if err := p.ctx.Err(); err != nil {
return nil, box, err
}
bounds, err := renderer.ObjectBounds(object, layer.DrawParam)
if err != nil {
return nil, box, err
}
box = unionTextBox(box, bounds)
}
}
if box.W <= 0 || box.H <= 0 {
return nil, box, nil
}
step := 25.4 / p.rasterDPI
x, y := math.Max(0, math.Floor(box.X/step)*step), math.Max(0, math.Floor(box.Y/step)*step)
xend, yend := math.Ceil(math.Min(p.pageWidth, box.X+box.W)/step)*step, math.Ceil(math.Min(p.pageHeight, box.Y+box.H)/step)*step
box = Box{X: x, Y: y, W: xend - x, H: yend - y}
if box.W <= 0 || box.H <= 0 {
return nil, box, nil
}
scene, err := renderer.CompilePage(page)
if canceled := p.ctx.Err(); canceled != nil {
return nil, box, canceled
}
return scene, box, err
}
// renderGroupScene 在指定区域渲染已编译场景,不修改缓存的命令与裁剪
// 入参: source 原始场景, box 页面裁取区域
// 返回: 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 {
command := &scene.Commands[i]
command.Transform[4] -= box.X
command.Transform[5] -= box.Y
if command.Clip != nil {
command.Clip = append([]RasterSegment{}, command.Clip...)
for j := range command.Clip {
segment := &command.Clip[j]
segment.End.X, segment.End.Y = segment.End.X-box.X, segment.End.Y-box.Y
segment.Control1.X, segment.Control1.Y = segment.Control1.X-box.X, segment.Control1.Y-box.Y
segment.Control2.X, segment.Control2.Y = segment.Control2.X-box.X, segment.Control2.Y-box.Y
}
}
}
backend := p.editor.Backends().Raster
if backend == nil {
return nil, fmt.Errorf("PDF local compositing: %w", ErrBackendUnavailable)
}
return backend.Render(&scene)
}
// appendRasterGroup 保存局部合成结果并记录输出精度
// 入参: img 透明图像, box 页面区域, opacity 组透明度
// 返回: error 保存错误
func (p *pdfImporter) appendRasterGroup(img image.Image, box Box, opacity float64) error {
var data bytes.Buffer
if err := png.Encode(&data, img); err != nil {
return err
}
if err := p.ctx.Err(); err != nil {
return err
}
id, err := p.editor.AddImage(data.Bytes())
if err != nil {
return err
}
alpha := int(math.Round(opacity * 255))
object := ImageObject{ResourceID: id, Boundary: pdfBoundary(box), CTM: pdfNumbers(box.W, 0, 0, box.H, 0, 0), Alpha: &alpha}
p.objects = append(p.objects, GraphicObject{Type: "ImageObject", ImageObject: object})
p.report.ImageObjects++
if !p.rasterWarned {
p.warning(pdfgo.Diagnostic{Message: fmt.Sprintf("PDF effect locally composited at %g DPI; other objects retained", p.rasterDPI)})
p.rasterWarned = true
}
return nil
}
// pdfCompositeRegions 按正常混合计算互不重叠的区域,组透明度由调用方统一应用
// 入参: geometry 几何后端, regions 按绘制顺序排列的区域
// 返回: []pdfPaintRegion 合成区域, error 几何错误
func pdfCompositeRegions(geometry GeometryBackend, regions []pdfPaintRegion) ([]pdfPaintRegion, error) {
var result []pdfPaintRegion
for _, source := range regions {
remaining := source.path
var next []pdfPaintRegion
for _, backdrop := range result {
outside, err := geometry.Combine(backdrop.path, source.path, GeometrySubtract)
if err != nil {
return nil, err
}
if len(outside) != 0 {
next = append(next, pdfPaintRegion{backdrop.paint, outside})
}
overlap, err := geometry.Combine(backdrop.path, source.path, GeometryIntersect)
if err != nil {
return nil, err
}
if len(overlap) != 0 {
paint := pdfgo.Paint{Alpha: source.paint.Alpha + backdrop.paint.Alpha*(1-source.paint.Alpha)}
for i := range paint.RGB {
paint.RGB[i] = (source.paint.RGB[i]*source.paint.Alpha + backdrop.paint.RGB[i]*backdrop.paint.Alpha*(1-source.paint.Alpha)) / paint.Alpha
}
next = append(next, pdfPaintRegion{paint, overlap})
}
remaining, err = geometry.Combine(remaining, backdrop.path, GeometrySubtract)
if err != nil {
return nil, err
}
}
if len(remaining) != 0 {
next = append(next, pdfPaintRegion{source.paint, remaining})
}
result = next
}
return result, 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 err := p.ctx.Err(); err != nil {
return pdfgo.Path{}, err
}
if mask.Subtype != "Luminosity" || mask.ColorSpace == nil || mask.ColorSpace.Model != "DeviceGray" || mask.ColorSpace.Calibrated() {
return pdfgo.Path{}, &pdfgo.UnsupportedError{Feature: "nonbinary luminosity mask"}
}
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, err := mask.Transfer(v)
if err != nil {
return false, err
}
if v != 0 && v != 1 {
return false, &pdfgo.UnsupportedError{Feature: "nonbinary luminosity mask"}
}
return v == 1, nil
}
luminosity, err := mask.ColorSpace.Luminosity(mask.Backdrop, "RelativeColorimetric")
if err != nil {
return pdfgo.Path{}, err
}
background, err := opacity(luminosity)
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.Text = func(pdfgo.TextMark) error {
return &pdfgo.UnsupportedError{Feature: "text mask graphic"}
}
visitor.Image = func(pdfgo.ImageMark) error {
return &pdfgo.UnsupportedError{Feature: "image mask graphic"}
}
visitor.Group = func(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error {
if mark.Alpha != 1 || mark.SoftMask != nil || !pdfNormalBlend(mark.BlendMode) || mark.ColorSpace != nil && !mark.ColorSpace.Equal(mask.ColorSpace) {
return &pdfgo.UnsupportedError{Feature: "transparent mask group"}
}
return walk(visitor)
}
visitor.Path = func(mark pdfgo.PathMark) error {
if err := p.ctx.Err(); err != nil {
return err
}
paint := mark.Style.Fill
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])
if err != nil {
return err
}
data, err := identity.pathDataContext(p.ctx, mark.Path, Box{})
if err != nil {
return err
}
shape, err := ParseGeometryPath(data)
if err != nil {
return err
}
shape, err = geometry.Normalize(shape, mark.Path.EvenOdd)
if err != nil {
return err
}
for _, clip := range mark.Style.Clips {
if len(clip.Text) != 0 {
return &pdfgo.UnsupportedError{Feature: "text-clipped binary mask"}
}
data, err := identity.pathDataContext(p.ctx, clip, Box{})
if err != nil {
return err
}
c, err := ParseGeometryPath(data)
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 {
if err := p.ctx.Err(); err != nil {
return pdfgo.Path{}, err
}
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
}