mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 14:16:55 +08:00
840 lines
29 KiB
Go
840 lines
29 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"
|
|
"math"
|
|
"reflect"
|
|
|
|
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
|
|
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
|
|
"github.com/xiaoqidun/pdfgo"
|
|
)
|
|
|
|
// errPDFGroupRaster 表示透明组需要按指定精度局部合成
|
|
var errPDFGroupRaster = errors.New("PDF transparency group requires local compositing")
|
|
|
|
// pdfPaintRegion 保存透明组内的单色填充区域
|
|
type pdfPaintRegion struct {
|
|
paint pdfgo.Paint
|
|
path GeometryPath
|
|
intent pdfgo.Name
|
|
origin pdfObjectOrigin
|
|
}
|
|
|
|
// 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 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 {
|
|
return walk(visitor)
|
|
}
|
|
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 {
|
|
masked, err := m.Image.HasMask()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if masked {
|
|
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 intent pdfgo.Name
|
|
var origin pdfObjectOrigin
|
|
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 := codecpdf.PathData(p.ctx, path.Path, p.matrix, 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 && codecpdf.OverprintNeedsSeparation(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
|
|
intent = path.Style.RenderingIntent
|
|
origin = pdfObjectOrigin{sequence: path.Sequence}
|
|
} 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) || intent != path.Style.RenderingIntent {
|
|
regions = append(regions, pdfPaintRegion{*paint, combined, intent, origin})
|
|
combined = nil
|
|
paint = &color
|
|
intent = path.Style.RenderingIntent
|
|
origin = pdfObjectOrigin{sequence: path.Sequence}
|
|
}
|
|
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 = codecpdf.PathData(p.ctx, path.Path, local.matrix, 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
|
|
}
|
|
if len(outline) != 0 {
|
|
if len(combined) == 0 {
|
|
origin = pdfObjectOrigin{sequence: path.Sequence}
|
|
} else {
|
|
origin = origin.merge(pdfObjectOrigin{sequence: path.Sequence})
|
|
}
|
|
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 {
|
|
regions = append(regions, pdfPaintRegion{*paint, combined, intent, origin})
|
|
}
|
|
if len(regions) == 0 {
|
|
return nil
|
|
}
|
|
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
|
|
}
|
|
color, err := p.color(region.paint, region.intent)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
object := PathObject{Boundary: pdfBoundary(box), AbbreviatedData: data, Fill: &fill, Stroke: &stroke, FillColor: color, Clips: objectClips}
|
|
p.appendOriginObject(GraphicObject{Type: "PathObject", PathObject: object}, region.origin)
|
|
p.report.PathObjects++
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// rasterGroup 在透明背景上合成局部图元,组透明度应用于合成结果,保留组外对象
|
|
// 入参: mark 组状态, walk 内容访问函数
|
|
// 返回: error 转换或渲染错误
|
|
func (p *pdfImporter) rasterGroup(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error {
|
|
var origin pdfObjectOrigin
|
|
img, box, err := p.renderGroup(func(local *pdfImporter) error {
|
|
if err := walk(local.visitor()); err != nil {
|
|
return err
|
|
}
|
|
var err error
|
|
origin, err = local.objects.combinedOrigin(local.ctx)
|
|
return err
|
|
}, 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, origin)
|
|
}
|
|
|
|
// 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.SetResources(p.resources)
|
|
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.symbolFonts == nil {
|
|
p.symbolFonts = make(map[*pdfgo.Font]*pdfImportedFont)
|
|
}
|
|
if p.fontPrograms == nil {
|
|
p.fontPrograms = make(map[pdfFontProgramKey]*pdfImportedFont)
|
|
}
|
|
if p.clipPaths == nil {
|
|
p.clipPaths = resourceCache[[32]byte, pdfClipPath](p.resources, 8<<20)
|
|
}
|
|
local := pdfImporter{resources: p.resources, 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, fontPrograms: p.fontPrograms, symbolFonts: p.symbolFonts, clipPaths: p.clipPaths, textOrigins: p.textOrigins}
|
|
if err := build(&local); err != nil {
|
|
return nil, box, err
|
|
}
|
|
if err := p.ctx.Err(); 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 := local.temporaryReader()
|
|
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)
|
|
}
|
|
|
|
// temporaryReader 为局部编译准备资源,带有保留信息时使用完整快照
|
|
// 返回: *Reader 独立阅读器, error 准备或取消错误
|
|
func (p *pdfImporter) temporaryReader() (*Reader, error) {
|
|
if err := p.ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
if p.editor.source != nil || len(p.editor.origins) != 0 {
|
|
return p.editor.readerContext(p.ctx, nil)
|
|
}
|
|
return p.editor.resourceReader()
|
|
}
|
|
|
|
// 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 组透明度, origin 来源区段
|
|
// 返回: error 保存错误
|
|
func (p *pdfImporter) appendRasterGroup(img image.Image, box Box, opacity float64, origin pdfObjectOrigin) error {
|
|
var data bytes.Buffer
|
|
if err := pdfgo.EncodePNG(p.ctx, &data, img); err != nil {
|
|
return err
|
|
}
|
|
if err := p.ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
id, err := p.editor.addDecodedImageContext(p.ctx, data.Bytes(), img)
|
|
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.appendOriginObject(GraphicObject{Type: "ImageObject", ImageObject: object}, origin)
|
|
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, backdrop.intent, backdrop.origin})
|
|
}
|
|
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
|
|
}
|
|
origin := source.origin
|
|
if source.paint.Alpha != 1 {
|
|
origin = backdrop.origin.merge(origin)
|
|
}
|
|
next = append(next, pdfPaintRegion{paint, overlap, "", origin})
|
|
}
|
|
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, source.intent, source.origin})
|
|
}
|
|
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 || !codecpdf.NormalBlend(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 || !codecpdf.NormalBlend(mark.Style.BlendMode) {
|
|
return &pdfgo.UnsupportedError{Feature: "nonbinary mask graphic"}
|
|
}
|
|
visible, err := opacity(paint.RGB[0])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
data, err := codecpdf.PathData(p.ctx, mark.Path, identity.matrix, 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 := codecpdf.PathData(p.ctx, clip, identity.matrix, 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
|
|
}
|
|
|
|
// rasterMaskAllowed 仅对不可矢量表达的蒙版启用显式局部合成,格式错误仍返回
|
|
// 入参: err 蒙版转换错误
|
|
// 返回: bool 是否允许局部合成
|
|
func (p *pdfImporter) rasterMaskAllowed(err error) bool {
|
|
var unsupported *pdfgo.UnsupportedError
|
|
return p.warning != nil && errors.As(err, &unsupported)
|
|
}
|
|
|
|
// applySoftMask 按原始颜色分量计算蒙版,逐块应用透明度及亮度传递
|
|
// 入参: source 源图像, box 页面区域, mask PDF软蒙版
|
|
// 返回: image.Image 合成图像, error 蒙版解释或渲染错误
|
|
func (p *pdfImporter) applySoftMask(source image.Image, box Box, mask *pdfgo.SoftMask) (image.Image, error) {
|
|
if err := p.ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
inverse, ok := p.matrix.Inverse()
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid PDF mask transform")
|
|
}
|
|
out := image.NewNRGBA(source.Bounds())
|
|
w, h := out.Rect.Dx(), out.Rect.Dy()
|
|
step := 25.4 / p.rasterDPI
|
|
cache := p.compositingCache()
|
|
scratch := &pdfCompositeScratch{}
|
|
space := &pdfgo.ColorSpace{Model: "DeviceRGB"}
|
|
for y := 0; y < h; y += pdfCompositeTileSize {
|
|
for x := 0; x < w; x += pdfCompositeTileSize {
|
|
if err := p.ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
width, height := min(pdfCompositeTileSize, w-x), min(pdfCompositeTileSize, 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, scratch: scratch, masks: map[*pdfgo.SoftMask][]float64{}}
|
|
alpha, err := c.mask(mask, space)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for i, opacity := range alpha {
|
|
if i%width == 0 {
|
|
if err := p.ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
px, py := out.Rect.Min.X+x+i%width, out.Rect.Min.Y+y+i/width
|
|
pixel := baseimage.NRGBAAt(source, px, py)
|
|
pixel.A = uint8(math.Round(float64(pixel.A) * opacity))
|
|
out.SetNRGBA(px, py, pixel)
|
|
}
|
|
c.releaseMasks()
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|