// 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.Shading != 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.appendObject(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...) editor.fontSourcesCache = p.renderer.fontSourcesCache editor.validatedPaths = p.editor.pathValidationCache() 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 local.objects.len() == 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 scene, err := renderer.CompilePageContext(p.ctx, page) if canceled := p.ctx.Err(); canceled != nil { return nil, box, canceled } if err != nil { return nil, box, err } if scene == nil { return nil, box, fmt.Errorf("PDF local compositing: invalid compiled scene") } return p.groupSceneBounds(scene) } // groupSceneBounds 计算已编译场景在页面内的像素对齐区域 // 入参: scene 中立绘制场景 // 返回: *RasterPage 非空场景, Box 页面区域, error 度量错误 func (p *pdfImporter) groupSceneBounds(scene *RasterPage) (*RasterPage, Box, error) { geometry, err := p.renderer.Geometry() if err != nil { return nil, Box{}, err } box, err := scene.BoundsContext(p.ctx, geometry) if err != nil { return nil, Box{}, err } 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 } return scene, box, nil } // renderGroupScene 在指定区域渲染已编译场景,不修改缓存的命令与裁剪 // 入参: source 原始场景, box 页面裁取区域 // 返回: image.Image 透明图像, error 后端渲染错误 func (p *pdfImporter) renderGroupScene(source *RasterPage, box Box) (image.Image, error) { return p.renderGroupSceneInto(source, box, nil) } // renderGroupSceneInto 在指定区域绘制场景,按所选后端能力复用独占目标 // 入参: source 原始场景, box 页面裁取区域, target 可选目标缓冲 // 返回: image.Image 透明图像, error 后端渲染错误 func (p *pdfImporter) renderGroupSceneInto(source *RasterPage, box Box, target *image.RGBA) (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) } if target != nil { if buffered, ok := backend.(RasterBufferBackend); ok { if err := buffered.RenderInto(&scene, target); err != nil { return nil, err } return target, nil } } 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.appendObject(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.Shading != 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.WalkContext(p.ctx, 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 }