mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
1 parent
b3c4bbc844
commit
961bc52038
43 files changed
+2283
-2594
No files matched your search
@@ -20,6 +20,7 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"math"
|
"math"
|
||||||
|
|
||||||
|
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
|
||||||
"github.com/xiaoqidun/pdfgo"
|
"github.com/xiaoqidun/pdfgo"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -65,3 +66,52 @@ func ClipPathKey(ctx context.Context, path pdfgo.Path, matrix pdfgo.Matrix) ([32
|
|||||||
hash.Sum(key[:0])
|
hash.Sum(key[:0])
|
||||||
return key, nil
|
return key, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ClipContainsBoundary 判断单一矩形裁剪是否完整覆盖对象边界,不近似曲线或斜边
|
||||||
|
// 入参: path PDF裁剪路径, matrix 页面变换, boundary OFD对象边界
|
||||||
|
// 返回: bool 是否可由对象边界代替裁剪
|
||||||
|
func ClipContainsBoundary(path pdfgo.Path, matrix pdfgo.Matrix, boundary basegeom.Box) bool {
|
||||||
|
if len(path.Text) != 0 || len(path.Segments) < 4 || len(path.Segments) > 6 || !finite(boundary.X) || !finite(boundary.Y) || !finite(boundary.W) || !finite(boundary.H) || boundary.W <= 0 || boundary.H <= 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var points [4]pdfgo.Point
|
||||||
|
for index := range 4 {
|
||||||
|
segment := path.Segments[index]
|
||||||
|
operator := "L"
|
||||||
|
if index == 0 {
|
||||||
|
operator = "M"
|
||||||
|
}
|
||||||
|
if segment.Operator != operator || len(segment.Points) != 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
points[index] = matrix.Apply(segment.Points[0])
|
||||||
|
if !finite(points[index].X) || !finite(points[index].Y) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end := 4
|
||||||
|
if end < len(path.Segments) && path.Segments[end].Operator == "L" {
|
||||||
|
segment := path.Segments[end]
|
||||||
|
if len(segment.Points) != 1 || matrix.Apply(segment.Points[0]) != points[0] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
end++
|
||||||
|
}
|
||||||
|
if end < len(path.Segments) {
|
||||||
|
segment := path.Segments[end]
|
||||||
|
if segment.Operator != "C" || len(segment.Points) != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
end++
|
||||||
|
}
|
||||||
|
if end != len(path.Segments) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
a, b, c, d := points[0], points[1], points[2], points[3]
|
||||||
|
if !(a.Y == b.Y && b.X == c.X && c.Y == d.Y && d.X == a.X || a.X == b.X && b.Y == c.Y && c.X == d.X && d.Y == a.Y) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
left, right := math.Min(a.X, c.X), math.Max(a.X, c.X)
|
||||||
|
top, bottom := math.Min(a.Y, c.Y), math.Max(a.Y, c.Y)
|
||||||
|
return left < right && top < bottom && boundary.X >= left && boundary.Y >= top && boundary.X+boundary.W <= right && boundary.Y+boundary.H <= bottom
|
||||||
|
}
|
||||||
@@ -15,6 +15,8 @@
|
|||||||
package pdf
|
package pdf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/xiaoqidun/pdfgo"
|
"github.com/xiaoqidun/pdfgo"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -35,3 +37,113 @@ func OutputColor(space *pdfgo.ColorSpace, values [4]float64) ([3]float64, error)
|
|||||||
func NormalBlend(mode pdfgo.Name) bool {
|
func NormalBlend(mode pdfgo.Name) bool {
|
||||||
return mode == "" || mode == "Normal" || mode == "Compatible"
|
return mode == "" || mode == "Normal" || mode == "Compatible"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NativePaint 使用库层色料定义求值设备四色,不以备用色代替原生浓度
|
||||||
|
// 入参: paint 源画刷, space 设备四色空间, intent 渲染意图
|
||||||
|
// 返回: *pdfgo.Paint 原生画刷或nil, [4]bool 显式通道, error 色料定义错误
|
||||||
|
func NativePaint(paint pdfgo.Paint, space *pdfgo.ColorSpace, intent pdfgo.Name) (*pdfgo.Paint, [4]bool, error) {
|
||||||
|
if paint.Colorant == nil {
|
||||||
|
return nil, [4]bool{}, nil
|
||||||
|
}
|
||||||
|
result, err := paint.Colorant.Space.ResolveInGroup(paint.Colorant.Tints, &pdfgo.ColorantDevice{Space: space}, space, intent, false)
|
||||||
|
if err != nil || len(result.Alternates) != 0 {
|
||||||
|
return nil, [4]bool{}, err
|
||||||
|
}
|
||||||
|
return result.Process, result.ProcessMask, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompositeColor 将原始画刷变换到混合空间,渐变在源空间求值
|
||||||
|
// 入参: paint 画刷, point 页面坐标, space 混合空间, intent 渲染意图, conversion 设备转换函数
|
||||||
|
// 返回: [4]float64 混合分量, bool 是否着色, error 颜色错误
|
||||||
|
func CompositeColor(paint pdfgo.Paint, point pdfgo.Point, space *pdfgo.ColorSpace, intent pdfgo.Name, conversion pdfgo.ColorConversion) ([4]float64, bool, error) {
|
||||||
|
if paint.Shading != nil {
|
||||||
|
return [4]float64{}, false, &pdfgo.UnsupportedError{Feature: "local shading pattern compositing"}
|
||||||
|
}
|
||||||
|
if paint.Tiling != nil {
|
||||||
|
return [4]float64{}, false, &pdfgo.UnsupportedError{Feature: "local tiling pattern compositing"}
|
||||||
|
}
|
||||||
|
var values [4]float64
|
||||||
|
var source *pdfgo.ColorSpace
|
||||||
|
if paint.Function != nil {
|
||||||
|
g := paint.Function
|
||||||
|
if g.Bounds != nil {
|
||||||
|
inverse, ok := g.PatternMatrix.Inverse()
|
||||||
|
if !ok {
|
||||||
|
return values, false, fmt.Errorf("singular function shading bounds transform")
|
||||||
|
}
|
||||||
|
local := inverse.Apply(point)
|
||||||
|
if local.X < g.Bounds.XMin || local.X > g.Bounds.XMax || local.Y < g.Bounds.YMin || local.Y > g.Bounds.YMax {
|
||||||
|
return values, false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inverse, ok := g.Matrix.Inverse()
|
||||||
|
if !ok {
|
||||||
|
return values, false, fmt.Errorf("singular function shading transform")
|
||||||
|
}
|
||||||
|
local := inverse.Apply(point)
|
||||||
|
source, intent = g.Space, g.Intent
|
||||||
|
if local.X < g.Domain.XMin || local.X > g.Domain.XMax || local.Y < g.Domain.YMin || local.Y > g.Domain.YMax {
|
||||||
|
if g.Background == nil {
|
||||||
|
return values, false, nil
|
||||||
|
}
|
||||||
|
values = *g.Background
|
||||||
|
} else {
|
||||||
|
var err error
|
||||||
|
values, err = g.ValuesAt(local)
|
||||||
|
if err != nil {
|
||||||
|
return values, false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if paint.Axial != nil || paint.Radial != nil {
|
||||||
|
var background *[4]float64
|
||||||
|
var bounds *pdfgo.Rectangle
|
||||||
|
var matrix pdfgo.Matrix
|
||||||
|
if g := paint.Axial; g != nil {
|
||||||
|
source, intent = g.Space, g.Intent
|
||||||
|
background, bounds, matrix = g.Background, g.Bounds, g.Matrix
|
||||||
|
} else {
|
||||||
|
g := paint.Radial
|
||||||
|
source, intent = g.Space, g.Intent
|
||||||
|
background, bounds, matrix = g.Background, g.Bounds, g.Matrix
|
||||||
|
}
|
||||||
|
if bounds != nil {
|
||||||
|
local := point
|
||||||
|
if matrix != (pdfgo.Matrix{}) {
|
||||||
|
inverse, ok := matrix.Inverse()
|
||||||
|
if !ok {
|
||||||
|
return values, false, fmt.Errorf("singular shading transform")
|
||||||
|
}
|
||||||
|
local = inverse.Apply(point)
|
||||||
|
}
|
||||||
|
if local.X < bounds.XMin || local.X > bounds.XMax || local.Y < bounds.YMin || local.Y > bounds.YMax {
|
||||||
|
return values, false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t, visible := ShadingPosition(paint, point)
|
||||||
|
var err error
|
||||||
|
if !visible {
|
||||||
|
if background == nil {
|
||||||
|
return values, false, nil
|
||||||
|
}
|
||||||
|
values = *background
|
||||||
|
} else if paint.Axial != nil {
|
||||||
|
values, err = paint.Axial.ValuesAt(t)
|
||||||
|
} else {
|
||||||
|
values, err = paint.Radial.ValuesAt(t)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return values, false, err
|
||||||
|
}
|
||||||
|
} else if paint.Space != nil {
|
||||||
|
values, source = paint.Values, paint.Space
|
||||||
|
} else if paint.CMYK != nil {
|
||||||
|
values, source = *paint.CMYK, &pdfgo.ColorSpace{Model: "DeviceCMYK"}
|
||||||
|
} else if paint.SourceSpace == "DeviceGray" {
|
||||||
|
values[0], source = paint.RGB[0], &pdfgo.ColorSpace{Model: "DeviceGray"}
|
||||||
|
} else {
|
||||||
|
copy(values[:], paint.RGB[:])
|
||||||
|
source = &pdfgo.ColorSpace{Model: "DeviceRGB"}
|
||||||
|
}
|
||||||
|
values, err := space.ConvertWith(values[:source.Components()], source, intent, conversion)
|
||||||
|
return values, true, err
|
||||||
|
}
|
||||||
@@ -93,3 +93,17 @@ func (s *NativeImage) NRGBA64At(x, y int) color.NRGBA64 {
|
|||||||
rgb, _ := OutputColor(s.Space, values)
|
rgb, _ := OutputColor(s.Space, values)
|
||||||
return color.NRGBA64{R: uint16(math.Round(rgb[0] * 65535)), G: uint16(math.Round(rgb[1] * 65535)), B: uint16(math.Round(rgb[2] * 65535)), A: uint16(math.Round(alpha * 65535))}
|
return color.NRGBA64{R: uint16(math.Round(rgb[0] * 65535)), G: uint16(math.Round(rgb[1] * 65535)), B: uint16(math.Round(rgb[2] * 65535)), A: uint16(math.Round(alpha * 65535))}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ImageProcessColorants 识别可在四色设备上直接绘制的专色色料
|
||||||
|
// 入参: image PDF图像
|
||||||
|
// 返回: [4]bool 参与绘制的四色通道, bool 是否全部属于印刷原色, error 色空间错误
|
||||||
|
func ImageProcessColorants(image *pdfgo.Image) ([4]bool, bool, error) {
|
||||||
|
source, err := image.ColorantSpace()
|
||||||
|
if err != nil {
|
||||||
|
return [4]bool{}, false, err
|
||||||
|
}
|
||||||
|
space := &pdfgo.ColorSpace{Model: "DeviceCMYK"}
|
||||||
|
process := source.PrepareProcess(&pdfgo.ColorantDevice{Space: space}, space, false)
|
||||||
|
mask := process.Mask()
|
||||||
|
return mask, mask != ([4]bool{}), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
// 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 pdf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/xiaoqidun/pdfgo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReferenceCache 保存单次会话拥有的引用文档及页面,零值可用,须串行使用且使用后不应复制
|
||||||
|
// Close只释放缓存自行打开的阅读器,不关闭来源文档或调用方提供的阅读器
|
||||||
|
type ReferenceCache struct {
|
||||||
|
readers map[[32]byte]*pdfgo.Reader
|
||||||
|
streams map[*pdfgo.Stream]*pdfgo.Reader
|
||||||
|
pages map[referencePageKey]*pdfgo.Page
|
||||||
|
}
|
||||||
|
|
||||||
|
// referencePageKey 区分目标文档、索引和文本标签,避免重复遍历页面树
|
||||||
|
type referencePageKey struct {
|
||||||
|
reader *pdfgo.Reader
|
||||||
|
index int64
|
||||||
|
label string
|
||||||
|
labeled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve 解析引用目标,目标不可用时按PDF标准保留代理内容
|
||||||
|
// 入参: ctx 取消上下文, source 来源文档, reference 引用目标, fileResolver 外部文件读取, referenceResolver 调用方页面解析, warning 诊断回调
|
||||||
|
// 返回: *pdfgo.Page 目标页面,不可用时为空, error 取消或调用方解析错误
|
||||||
|
func (c *ReferenceCache) Resolve(ctx context.Context, source *pdfgo.Reader, reference pdfgo.ReferenceXObject, fileResolver pdfgo.FileResolver, referenceResolver pdfgo.ReferenceResolver, warning func(pdfgo.Diagnostic)) (*pdfgo.Page, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if referenceResolver != nil {
|
||||||
|
return referenceResolver(ctx, source, reference)
|
||||||
|
}
|
||||||
|
reader := c.streams[reference.File.Embedded]
|
||||||
|
if reader == nil {
|
||||||
|
if reference.File.Embedded == nil && fileResolver == nil {
|
||||||
|
return referenceProxy(ctx, fmt.Errorf("external reference file unavailable"), warning)
|
||||||
|
}
|
||||||
|
data, err := source.ReadFileData(ctx, reference.File, fileResolver)
|
||||||
|
if err != nil {
|
||||||
|
return referenceProxy(ctx, err, warning)
|
||||||
|
}
|
||||||
|
key := sha256.Sum256(data)
|
||||||
|
reader = c.readers[key]
|
||||||
|
if reader == nil {
|
||||||
|
reader, err = pdfgo.NewReaderWithOptions(bytes.NewReader(data), int64(len(data)), pdfgo.ReaderOptions{Warning: warning})
|
||||||
|
if err != nil {
|
||||||
|
return referenceProxy(ctx, err, warning)
|
||||||
|
}
|
||||||
|
if c.readers == nil {
|
||||||
|
c.readers = make(map[[32]byte]*pdfgo.Reader)
|
||||||
|
}
|
||||||
|
c.readers[key] = reader
|
||||||
|
}
|
||||||
|
if reference.File.Embedded != nil {
|
||||||
|
if c.streams == nil {
|
||||||
|
c.streams = make(map[*pdfgo.Stream]*pdfgo.Reader)
|
||||||
|
}
|
||||||
|
c.streams[reference.File.Embedded] = reader
|
||||||
|
}
|
||||||
|
}
|
||||||
|
key := referencePageKey{reader: reader, index: reference.PageIndex}
|
||||||
|
if reference.PageLabel != nil {
|
||||||
|
key.index, key.label, key.labeled = 0, *reference.PageLabel, true
|
||||||
|
}
|
||||||
|
if page := c.pages[key]; page != nil {
|
||||||
|
return page, ctx.Err()
|
||||||
|
}
|
||||||
|
page, err := reader.ResolveReferencePage(ctx, reference)
|
||||||
|
if err != nil {
|
||||||
|
return referenceProxy(ctx, err, warning)
|
||||||
|
}
|
||||||
|
if c.pages == nil {
|
||||||
|
c.pages = make(map[referencePageKey]*pdfgo.Page)
|
||||||
|
}
|
||||||
|
c.pages[key] = page
|
||||||
|
return page, ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// referenceProxy 记录目标不可用原因,不将合法的代理绘制视为转换失败
|
||||||
|
// 入参: ctx 取消上下文, cause 目标不可用原因, warning 诊断回调
|
||||||
|
// 返回: *pdfgo.Page 空目标, error 取消错误
|
||||||
|
func referenceProxy(ctx context.Context, cause error, warning func(pdfgo.Diagnostic)) (*pdfgo.Page, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if errors.Is(cause, context.Canceled) || errors.Is(cause, context.DeadlineExceeded) {
|
||||||
|
return nil, cause
|
||||||
|
}
|
||||||
|
if warning != nil {
|
||||||
|
warning(pdfgo.Diagnostic{Message: "reference XObject proxy retained: " + cause.Error()})
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close 释放本次会话打开的引用文档并清空缓存,可重复调用,之后可重新解析
|
||||||
|
func (c *ReferenceCache) Close() {
|
||||||
|
for _, reader := range c.readers {
|
||||||
|
reader.Close()
|
||||||
|
}
|
||||||
|
c.readers, c.streams, c.pages = nil, nil, nil
|
||||||
|
}
|
||||||
@@ -48,11 +48,11 @@ func ShadingColorantSpace(paint pdfgo.Paint) *pdfgo.ColorantSpace {
|
|||||||
// 返回: *ShadingColorants 独立采样状态,不可原生映射时为nil
|
// 返回: *ShadingColorants 独立采样状态,不可原生映射时为nil
|
||||||
func PrepareShadingColorants(paint pdfgo.Paint, space *pdfgo.ColorSpace, softMask bool) *ShadingColorants {
|
func PrepareShadingColorants(paint pdfgo.Paint, space *pdfgo.ColorSpace, softMask bool) *ShadingColorants {
|
||||||
colorants := ShadingColorantSpace(paint)
|
colorants := ShadingColorantSpace(paint)
|
||||||
Process := colorants.PrepareProcess(&pdfgo.ColorantDevice{Space: space}, space, softMask)
|
process := colorants.PrepareProcess(&pdfgo.ColorantDevice{Space: space}, space, softMask)
|
||||||
if Process == nil || Process.Mask() == ([4]bool{}) {
|
if process == nil || process.Mask() == ([4]bool{}) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
s := &ShadingColorants{paint: paint, Process: Process, tints: make([]float64, len(colorants.Names))}
|
s := &ShadingColorants{paint: paint, Process: process, tints: make([]float64, len(colorants.Names))}
|
||||||
if paint.Mesh != nil {
|
if paint.Mesh != nil {
|
||||||
s.mesh = paint.Mesh.NewSampler()
|
s.mesh = paint.Mesh.NewSampler()
|
||||||
}
|
}
|
||||||
@@ -94,3 +94,20 @@ func (s *ShadingColorants) MeshAt(patch int, u, v float64) ([4]float64, error) {
|
|||||||
}
|
}
|
||||||
return s.Process.Convert(s.tints)
|
return s.Process.Convert(s.tints)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ShadingPosition 计算渐变参数,未延伸区域及不存在非负圆半径的位置不着色
|
||||||
|
// 入参: paint 渐变画刷, point 页面坐标
|
||||||
|
// 返回: float64 归一化参数, bool 是否着色
|
||||||
|
func ShadingPosition(paint pdfgo.Paint, point pdfgo.Point) (float64, bool) {
|
||||||
|
var geometry pdfgo.GradientPosition
|
||||||
|
var err error
|
||||||
|
if g := paint.Axial; g != nil {
|
||||||
|
geometry, err = g.PreparePosition()
|
||||||
|
} else {
|
||||||
|
geometry, err = paint.Radial.PreparePosition()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return geometry.PositionAt(point)
|
||||||
|
}
|
||||||
+10
-575
@@ -18,9 +18,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
|
||||||
"reflect"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
|
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
|
||||||
"github.com/xiaoqidun/pdfgo"
|
"github.com/xiaoqidun/pdfgo"
|
||||||
@@ -64,12 +61,6 @@ type PDFImportReport struct {
|
|||||||
Warnings []pdfgo.Diagnostic
|
Warnings []pdfgo.Diagnostic
|
||||||
}
|
}
|
||||||
|
|
||||||
// pdfPointBounds 逐点汇总边界,不保留文字或路径的临时坐标数组
|
|
||||||
type pdfPointBounds struct {
|
|
||||||
minX, minY, maxX, maxY float64
|
|
||||||
found bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// pdfImporter 保存单次转换的对象与资源状态
|
// pdfImporter 保存单次转换的对象与资源状态
|
||||||
type pdfImporter struct {
|
type pdfImporter struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
@@ -119,9 +110,7 @@ type pdfImporter struct {
|
|||||||
annotationData []byte
|
annotationData []byte
|
||||||
resolveFile pdfgo.FileResolver
|
resolveFile pdfgo.FileResolver
|
||||||
resolveReference pdfgo.ReferenceResolver
|
resolveReference pdfgo.ReferenceResolver
|
||||||
referenceReaders map[[32]byte]*pdfgo.Reader
|
references *codecpdf.ReferenceCache
|
||||||
referenceStreams map[*pdfgo.Stream]*pdfgo.Reader
|
|
||||||
referencePages map[pdfReferencePageKey]*pdfgo.Page
|
|
||||||
halftoneWarnings map[*pdfgo.Halftone]bool
|
halftoneWarnings map[*pdfgo.Halftone]bool
|
||||||
halftones map[string]*pdfgo.Halftone
|
halftones map[string]*pdfgo.Halftone
|
||||||
transferBackdrop bool
|
transferBackdrop bool
|
||||||
@@ -181,10 +170,8 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
|
|||||||
importer.resolveReference = options.ResolveReference
|
importer.resolveReference = options.ResolveReference
|
||||||
importer.halftones = options.Halftones
|
importer.halftones = options.Halftones
|
||||||
importer.compression = options.Compression
|
importer.compression = options.Compression
|
||||||
importer.referenceReaders = make(map[[32]byte]*pdfgo.Reader)
|
importer.references = &codecpdf.ReferenceCache{}
|
||||||
importer.referenceStreams = make(map[*pdfgo.Stream]*pdfgo.Reader)
|
defer importer.references.Close()
|
||||||
importer.referencePages = make(map[pdfReferencePageKey]*pdfgo.Page)
|
|
||||||
defer importer.closeReferenceReaders()
|
|
||||||
importer.halftoneWarnings = make(map[*pdfgo.Halftone]bool)
|
importer.halftoneWarnings = make(map[*pdfgo.Halftone]bool)
|
||||||
if security := reader.Encryption(); security != nil && !security.Owner && security.Permissions&0xf3c != 0xf3c {
|
if security := reader.Encryption(); security != nil && !security.Owner && security.Permissions&0xf3c != 0xf3c {
|
||||||
if options.Strict {
|
if options.Strict {
|
||||||
@@ -325,564 +312,12 @@ func ConvertPDF(ctx context.Context, source io.ReaderAt, size int64, output io.W
|
|||||||
return report, err
|
return report, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// commitObjects 将当前页面待转换对象写入编辑文档
|
// referencePage 衔接PDF访问器与当前导入会话的引用文档缓存
|
||||||
// 返回: error 对象写入错误
|
// 入参: ctx 取消上下文, source 来源文档, reference 引用目标
|
||||||
func (p *pdfImporter) commitObjects() error {
|
// 返回: *pdfgo.Page 目标页面,不可用时为空, error 取消或调用方解析错误
|
||||||
if p.objects.len() == 0 {
|
func (p *pdfImporter) referencePage(ctx context.Context, source *pdfgo.Reader, reference pdfgo.ReferenceXObject) (*pdfgo.Page, error) {
|
||||||
return nil
|
if p.references == nil {
|
||||||
|
p.references = &codecpdf.ReferenceCache{}
|
||||||
}
|
}
|
||||||
objects, err := p.objects.data(p.ctx)
|
return p.references.Resolve(ctx, source, reference, p.resolveFile, p.resolveReference, p.warning)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
objects, err = p.sharedClipObjects(objects)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := p.objectValidation().appendOwnedObjects(p.ctx, p.page, objects); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p.objects = nil
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// pdfNumbers 按OFD数值格式序列化坐标
|
|
||||||
// 入参: values 数值列表
|
|
||||||
// 返回: string 空格分隔的数值
|
|
||||||
func pdfNumbers(values ...float64) string {
|
|
||||||
var buffer [128]byte
|
|
||||||
data := buffer[:0]
|
|
||||||
for n, v := range values {
|
|
||||||
if n != 0 {
|
|
||||||
data = append(data, ' ')
|
|
||||||
}
|
|
||||||
data = strconv.AppendFloat(data, v, 'g', -1, 64)
|
|
||||||
}
|
|
||||||
return string(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// pdfDashPattern 换算PDF虚线长度,将单值数组展开为等长线段与间隔
|
|
||||||
// 入参: dash PDF虚线数组, scale 坐标缩放比例
|
|
||||||
// 返回: string OFD虚线数组
|
|
||||||
func pdfDashPattern(dash []float64, scale float64) string {
|
|
||||||
if len(dash) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
values := make([]float64, max(2, len(dash)))
|
|
||||||
for i, value := range dash {
|
|
||||||
values[i] = value * scale
|
|
||||||
}
|
|
||||||
if len(dash) == 1 {
|
|
||||||
values[1] = values[0]
|
|
||||||
}
|
|
||||||
return pdfNumbers(values...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// pdfBoundary 序列化对象边界
|
|
||||||
// 入参: b 对象边界
|
|
||||||
// 返回: string OFD边界数据
|
|
||||||
func pdfBoundary(b Box) string { return pdfNumbers(b.X, b.Y, b.W, b.H) }
|
|
||||||
|
|
||||||
// pdfBounds 计算点集合的包围框
|
|
||||||
// 入参: points 坐标点集合
|
|
||||||
// 返回: Box 包围框
|
|
||||||
func pdfBounds(points []pdfgo.Point) Box {
|
|
||||||
var bounds pdfPointBounds
|
|
||||||
for _, point := range points {
|
|
||||||
bounds.add(point)
|
|
||||||
}
|
|
||||||
return bounds.box()
|
|
||||||
}
|
|
||||||
|
|
||||||
// add 按原顺序累计一个控制点的最小和最大坐标
|
|
||||||
// 入参: point 坐标点
|
|
||||||
func (b *pdfPointBounds) add(point pdfgo.Point) {
|
|
||||||
if !b.found {
|
|
||||||
b.minX, b.minY, b.maxX, b.maxY = point.X, point.Y, point.X, point.Y
|
|
||||||
b.found = true
|
|
||||||
}
|
|
||||||
b.minX, b.minY = math.Min(b.minX, point.X), math.Min(b.minY, point.Y)
|
|
||||||
b.maxX, b.maxY = math.Max(b.maxX, point.X), math.Max(b.maxY, point.Y)
|
|
||||||
}
|
|
||||||
|
|
||||||
// box 取得原精度边界,空集合仍返回零值
|
|
||||||
// 返回: Box 坐标范围
|
|
||||||
func (b *pdfPointBounds) box() Box {
|
|
||||||
if !b.found {
|
|
||||||
return Box{}
|
|
||||||
}
|
|
||||||
return Box{X: b.minX, Y: b.minY, W: math.Max(b.maxX-b.minX, 1e-6), H: math.Max(b.maxY-b.minY, 1e-6)}
|
|
||||||
}
|
|
||||||
|
|
||||||
// pathBoundsContext 逐段计算变换后的控制点边界,不创建临时点集合
|
|
||||||
// 入参: ctx 取消上下文, path PDF路径
|
|
||||||
// 返回: Box 控制点边界,空路径返回零值, error 取消错误
|
|
||||||
func (p *pdfImporter) pathBoundsContext(ctx context.Context, path pdfgo.Path) (Box, error) {
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return Box{}, err
|
|
||||||
}
|
|
||||||
minX, minY, maxX, maxY := math.Inf(1), math.Inf(1), math.Inf(-1), math.Inf(-1)
|
|
||||||
found := false
|
|
||||||
for _, segment := range path.Segments {
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return Box{}, err
|
|
||||||
}
|
|
||||||
for index, point := range segment.Points {
|
|
||||||
if index&255 == 0 {
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return Box{}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
point = p.matrix.Apply(point)
|
|
||||||
minX, minY = math.Min(minX, point.X), math.Min(minY, point.Y)
|
|
||||||
maxX, maxY = math.Max(maxX, point.X), math.Max(maxY, point.Y)
|
|
||||||
found = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return Box{}, err
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
return Box{}, nil
|
|
||||||
}
|
|
||||||
return Box{X: minX, Y: minY, W: math.Max(maxX-minX, 1e-6), H: math.Max(maxY-minY, 1e-6)}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// color 保留设备色彩分量并复用文档颜色空间
|
|
||||||
// 入参: paint PDF颜色与不透明度
|
|
||||||
// 返回: *FillColor OFD颜色
|
|
||||||
func (p *pdfImporter) color(paint pdfgo.Paint) *FillColor {
|
|
||||||
alpha := int(math.Round(paint.Alpha * 255))
|
|
||||||
if paint.CMYK != nil && (paint.Space == nil || !paint.Space.Calibrated()) {
|
|
||||||
if p.cmykSpace == "" {
|
|
||||||
p.cmykSpace = p.editor.nextID()
|
|
||||||
p.editor.resources = append(p.editor.resources, editorResource{space: &ColorSpace{ID: p.cmykSpace, Type: "CMYK", BitsPerComponent: 16}})
|
|
||||||
}
|
|
||||||
v := paint.CMYK
|
|
||||||
return &FillColor{ColorSpace: p.cmykSpace, Value: pdfNumbers(math.Round(v[0]*65535), math.Round(v[1]*65535), math.Round(v[2]*65535), math.Round(v[3]*65535)), Alpha: &alpha}
|
|
||||||
}
|
|
||||||
return &FillColor{Value: pdfNumbers(math.Round(paint.RGB[0]*255), math.Round(paint.RGB[1]*255), math.Round(paint.RGB[2]*255)), Alpha: &alpha}
|
|
||||||
}
|
|
||||||
|
|
||||||
// pathDataContext 转换路径并在分段处理时检查取消
|
|
||||||
// 入参: ctx 取消上下文, path PDF路径, origin 对象边界
|
|
||||||
// 返回: string OFD路径数据, error 取消错误
|
|
||||||
func (p *pdfImporter) pathDataContext(ctx context.Context, path pdfgo.Path, origin Box) (string, error) {
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
data := make([]byte, 0, len(path.Segments)*48)
|
|
||||||
for index, segment := range path.Segments {
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if index != 0 {
|
|
||||||
data = append(data, ' ')
|
|
||||||
}
|
|
||||||
data = append(data, segment.Operator...)
|
|
||||||
for _, point := range segment.Points {
|
|
||||||
point = p.matrix.Apply(point)
|
|
||||||
data = append(data, ' ')
|
|
||||||
data = strconv.AppendFloat(data, point.X-origin.X, 'g', -1, 64)
|
|
||||||
data = append(data, ' ')
|
|
||||||
data = strconv.AppendFloat(data, point.Y-origin.Y, 'g', -1, 64)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return string(data), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// clips 省略覆盖对象边界的矩形裁剪,复用其余几何并保留位移及路径交集
|
|
||||||
// 入参: paths 裁剪路径, origin 对象边界
|
|
||||||
// 返回: *Clips OFD裁剪区域,无裁剪时为空, error 未解析的裁剪字形
|
|
||||||
func (p *pdfImporter) clips(paths []pdfgo.Path, origin Box) (*Clips, error) {
|
|
||||||
if len(paths) == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
var clips *Clips
|
|
||||||
var areas []ClipArea
|
|
||||||
translation := ""
|
|
||||||
for _, path := range paths {
|
|
||||||
if err := p.ctx.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if pdfClipContainsBoundary(path, p.matrix, origin) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if clips == nil {
|
|
||||||
clips = &Clips{Clip: make([]Clip, 0, len(paths))}
|
|
||||||
areas = make([]ClipArea, len(paths))
|
|
||||||
if origin.X != 0 || origin.Y != 0 {
|
|
||||||
translation = pdfNumbers(1, 0, 0, 1, -origin.X, -origin.Y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
index := len(clips.Clip)
|
|
||||||
area := &areas[index]
|
|
||||||
area.CTM = translation
|
|
||||||
if len(path.Segments) != 0 {
|
|
||||||
encoded, err := p.clipPath(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
area.Path = encoded.path[:1:1]
|
|
||||||
}
|
|
||||||
for _, mark := range path.Text {
|
|
||||||
objects, ok := p.clipTexts[mark]
|
|
||||||
if !ok {
|
|
||||||
if err := p.flushPath(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
count := p.objects.len()
|
|
||||||
textCount := p.report.TextObjects
|
|
||||||
if err := p.text(pdfgo.TextMark{Font: mark.Font, Glyphs: mark.Glyphs, Positions: mark.Positions, Matrix: mark.Matrix, Size: mark.Size, HorizontalScale: mark.HorizontalScale, Mode: 7, Clip: mark, Style: pdfgo.Style{Fill: pdfgo.Paint{Alpha: 1}}}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
p.objects.truncate(count)
|
|
||||||
p.report.TextObjects = textCount
|
|
||||||
objects = p.clipTexts[mark]
|
|
||||||
}
|
|
||||||
if len(path.Text) == 1 {
|
|
||||||
area.Text = objects[:len(objects):len(objects)]
|
|
||||||
} else {
|
|
||||||
area.Text = append(area.Text, objects...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(area.Path)+len(area.Text) == 0 {
|
|
||||||
fill, stroke := true, false
|
|
||||||
area.Path = []PathObject{{Boundary: "0 0 1 1", AbbreviatedData: "M 0 0 C", Fill: &fill, Stroke: &stroke}}
|
|
||||||
}
|
|
||||||
clips.Clip = append(clips.Clip, Clip{Area: areas[index : index+1 : index+1]})
|
|
||||||
}
|
|
||||||
return clips, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// path 合并相同状态下的不透明描边,保留独立子路径与绘制顺序
|
|
||||||
// 入参: mark PDF路径绘制信息
|
|
||||||
// 返回: error 错误信息
|
|
||||||
func (p *pdfImporter) path(mark pdfgo.PathMark) error {
|
|
||||||
if mark.Style.BlendMode != "" && mark.Style.BlendMode != "Normal" && mark.Style.BlendMode != "Compatible" {
|
|
||||||
return &pdfgo.UnsupportedError{Feature: "path blend mode " + string(mark.Style.BlendMode)}
|
|
||||||
}
|
|
||||||
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 && (p.pendingPath.StrokeMatrix != mark.StrokeMatrix || !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 {
|
|
||||||
if p.rasterMaskAllowed(err) {
|
|
||||||
mask := mark.Style.SoftMask
|
|
||||||
mark.Style.SoftMask = nil
|
|
||||||
return p.rasterGroup(pdfgo.GroupMark{Alpha: 1, SoftMask: mask}, func(v pdfgo.Visitor) error { return v.Path(mark) })
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if mark.Fill && mark.Style.FillOverprint && pdfOverprintNeedsSeparation(mark.Style.Fill) || mark.Stroke && mark.Style.StrokeOverprint && pdfOverprintNeedsSeparation(mark.Style.Stroke) {
|
|
||||||
return &pdfgo.UnsupportedError{Feature: "color separation overprint"}
|
|
||||||
}
|
|
||||||
if p.warning != nil && (mark.Fill && p.gradientError(mark.Style.Fill) != nil || mark.Stroke && p.gradientError(mark.Style.Stroke) != nil) {
|
|
||||||
if err := p.ctx.Err(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return p.gradientPath(mark)
|
|
||||||
}
|
|
||||||
box, err := p.pathBoundsContext(p.ctx, mark.Path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if box == (Box{}) {
|
|
||||||
return fmt.Errorf("empty PDF painted path")
|
|
||||||
}
|
|
||||||
scale := math.Hypot(p.matrix[0], p.matrix[1])
|
|
||||||
drawing, origin, ctm := *p, box, ""
|
|
||||||
marginScale := scale
|
|
||||||
if mark.StrokeMatrix != (pdfgo.Matrix{}) {
|
|
||||||
inverse, ok := mark.StrokeMatrix.Inverse()
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid PDF stroke matrix")
|
|
||||||
}
|
|
||||||
drawing.matrix, origin, scale = inverse, Box{}, 1
|
|
||||||
m := p.matrix.Mul(mark.StrokeMatrix)
|
|
||||||
marginScale = math.Max(math.Hypot(m[0], m[2]), math.Hypot(m[1], m[3]))
|
|
||||||
}
|
|
||||||
if mark.Stroke {
|
|
||||||
margin := mark.Style.LineWidth * marginScale / 2 * math.Max(1, mark.Style.MiterLimit)
|
|
||||||
box = Box{X: box.X - margin, Y: box.Y - margin, W: box.W + 2*margin, H: box.H + 2*margin}
|
|
||||||
}
|
|
||||||
if mark.StrokeMatrix == (pdfgo.Matrix{}) {
|
|
||||||
origin = box
|
|
||||||
} else {
|
|
||||||
m := p.matrix.Mul(mark.StrokeMatrix)
|
|
||||||
m[4], m[5] = m[4]-box.X, m[5]-box.Y
|
|
||||||
ctm = pdfNumbers(m[:]...)
|
|
||||||
}
|
|
||||||
paint := func(value pdfgo.Paint) (*FillColor, error) {
|
|
||||||
if value.Tiling != nil || pdfGradientPattern(value) {
|
|
||||||
return p.paintColor(value, box)
|
|
||||||
}
|
|
||||||
return drawing.paintColor(value, origin)
|
|
||||||
}
|
|
||||||
fillColor, strokePaint := p.color(mark.Style.Fill), p.color(mark.Style.Stroke)
|
|
||||||
if mark.Fill {
|
|
||||||
fillColor, err = paint(mark.Style.Fill)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if mark.Stroke {
|
|
||||||
strokePaint, err = paint(mark.Style.Stroke)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
strokeColor := StrokeColor(*strokePaint)
|
|
||||||
clips, err := p.clips(mark.Style.Clips, box)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
data, err := drawing.pathDataContext(p.ctx, mark.Path, origin)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
object := PathObject{Boundary: pdfBoundary(box), CTM: ctm, AbbreviatedData: data, Fill: &mark.Fill, Stroke: &mark.Stroke, FillColor: fillColor, 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: clips}
|
|
||||||
object.LineWidthSet = object.LineWidth == 0
|
|
||||||
if mark.Path.EvenOdd {
|
|
||||||
object.Rule = "Even-Odd"
|
|
||||||
}
|
|
||||||
if len(mark.Style.Dash) > 0 {
|
|
||||||
object.DashPattern = pdfDashPattern(mark.Style.Dash, scale)
|
|
||||||
phase := mark.Style.DashPhase * scale
|
|
||||||
object.DashOffset = &phase
|
|
||||||
}
|
|
||||||
p.appendObject(GraphicObject{Type: "PathObject", PathObject: object})
|
|
||||||
p.report.PathObjects++
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// paintColor 将页面渐变转换为对象局部坐标
|
|
||||||
// 入参: paint PDF画刷, box 对象边界
|
|
||||||
// 返回: *FillColor OFD颜色或渐变, error 图案转换错误
|
|
||||||
func (p *pdfImporter) paintColor(paint pdfgo.Paint, box Box) (*FillColor, error) {
|
|
||||||
if err := p.gradientError(paint); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if pdfGradientPattern(paint) {
|
|
||||||
return p.gradientPattern(paint, box)
|
|
||||||
}
|
|
||||||
if paint.Mesh != nil {
|
|
||||||
return p.meshColor(paint, box)
|
|
||||||
}
|
|
||||||
color := p.color(paint)
|
|
||||||
if paint.Tiling != nil {
|
|
||||||
pattern, err := p.tilingPattern(paint.Tiling, paint)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
color.Pattern = pattern
|
|
||||||
return color, nil
|
|
||||||
}
|
|
||||||
if paint.Radial != nil {
|
|
||||||
gradient := paint.Radial
|
|
||||||
matrix := p.matrix
|
|
||||||
if gradient.Matrix != (pdfgo.Matrix{}) {
|
|
||||||
matrix = matrix.Mul(gradient.Matrix)
|
|
||||||
}
|
|
||||||
start, end := matrix.Apply(gradient.Start), matrix.Apply(gradient.End)
|
|
||||||
xx, yy, xy := matrix[0]*matrix[0]+matrix[2]*matrix[2], matrix[1]*matrix[1]+matrix[3]*matrix[3], matrix[0]*matrix[1]+matrix[2]*matrix[3]
|
|
||||||
major := (xx + yy + math.Hypot(xx-yy, 2*xy)) / 2
|
|
||||||
determinant := matrix[0]*matrix[3] - matrix[1]*matrix[2]
|
|
||||||
if major <= 0 || determinant == 0 {
|
|
||||||
return nil, fmt.Errorf("invalid PDF radial gradient matrix")
|
|
||||||
}
|
|
||||||
scale := math.Sqrt(major)
|
|
||||||
eccentricity := math.Sqrt(math.Max(0, 1-determinant*determinant/(major*major)))
|
|
||||||
angle := math.Atan2(2*xy, xx-yy) * 90 / math.Pi
|
|
||||||
extend := 0
|
|
||||||
if gradient.Extend[0] {
|
|
||||||
extend |= 1
|
|
||||||
}
|
|
||||||
if gradient.Extend[1] {
|
|
||||||
extend |= 2
|
|
||||||
}
|
|
||||||
shading := &RadialShd{StartPoint: pdfNumbers(start.X-box.X, start.Y-box.Y), EndPoint: pdfNumbers(end.X-box.X, end.Y-box.Y), StartRadius: gradient.StartRadius * scale, EndRadius: gradient.EndRadius * scale, Eccentricity: eccentricity, Angle: angle, Extend: strconv.Itoa(extend)}
|
|
||||||
var err error
|
|
||||||
shading.Segment, err = p.gradientSegments(gradient.Stops, gradient.Space, gradient.Intent)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
color.Value = ""
|
|
||||||
color.RadialShd = shading
|
|
||||||
return color, nil
|
|
||||||
}
|
|
||||||
if paint.Axial == nil {
|
|
||||||
return color, nil
|
|
||||||
}
|
|
||||||
gradient := paint.Axial
|
|
||||||
start := p.matrix.Apply(gradient.Start)
|
|
||||||
inverse, ok := p.matrix.Inverse()
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("invalid PDF gradient matrix")
|
|
||||||
}
|
|
||||||
dx, dy := gradient.End.X-gradient.Start.X, gradient.End.Y-gradient.Start.Y
|
|
||||||
gx, gy := inverse[0]*dx+inverse[1]*dy, inverse[2]*dx+inverse[3]*dy
|
|
||||||
length := gx*gx + gy*gy
|
|
||||||
if length == 0 {
|
|
||||||
return nil, fmt.Errorf("invalid PDF axial gradient axis")
|
|
||||||
}
|
|
||||||
factor := (dx*dx + dy*dy) / length
|
|
||||||
end := pdfgo.Point{X: start.X + gx*factor, Y: start.Y + gy*factor}
|
|
||||||
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)}
|
|
||||||
var err error
|
|
||||||
shading.Segment, err = p.gradientSegments(gradient.Stops, gradient.Space, gradient.Intent)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
color.Value = ""
|
|
||||||
color.AxialShd = shading
|
|
||||||
return color, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildTilingPattern 保留PDF图案单元与页面坐标之间的仿射关系
|
|
||||||
// 入参: source 平铺图案, base 无色图案基色
|
|
||||||
// 返回: *Pattern OFD图案, error 转换错误
|
|
||||||
func (p *pdfImporter) buildTilingPattern(source *pdfgo.TilingPattern, base pdfgo.Paint) (*Pattern, error) {
|
|
||||||
const unit = 25.4 / 72
|
|
||||||
box := source.BBox
|
|
||||||
width, height := box.XMax-box.XMin, box.YMax-box.YMin
|
|
||||||
xstep, ystep := math.Abs(source.XStep), math.Abs(source.YStep)
|
|
||||||
if width <= 0 || height <= 0 || !finite(xstep) || !finite(ystep) || xstep == 0 || ystep == 0 {
|
|
||||||
return nil, fmt.Errorf("invalid PDF tiling pattern dimensions")
|
|
||||||
}
|
|
||||||
localToPDF := pdfgo.Matrix{1 / unit, 0, 0, -1 / unit, box.XMin, box.YMax}
|
|
||||||
m := p.matrix.Mul(source.Matrix).Mul(localToPDF)
|
|
||||||
pattern := &Pattern{Width: math.Min(width, xstep) * unit, Height: math.Min(height, ystep) * unit, XStep: xstep * unit, YStep: ystep * unit, RelativeTo: "Page", CTM: pdfNumbers(m[:]...)}
|
|
||||||
cell := *p
|
|
||||||
cell.matrix = pdfgo.Matrix{unit, 0, 0, -unit, -box.XMin * unit, box.YMax * unit}
|
|
||||||
cell.pageWidth, cell.pageHeight = width*unit, height*unit
|
|
||||||
cell.rasterDPI *= m.MaxScale()
|
|
||||||
if !finite(cell.rasterDPI) || cell.rasterDPI < 0 {
|
|
||||||
return nil, fmt.Errorf("invalid PDF tiling pattern raster scale")
|
|
||||||
}
|
|
||||||
cell.objects, cell.pendingPath = nil, nil
|
|
||||||
if err := source.Walk(p.ctx, base, cell.visitor()); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := cell.flushPath(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
objects, err := cell.objects.data(p.ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
pattern.CellContent.Objects = objects
|
|
||||||
if len(objects) != 0 && (width > xstep || height > ystep) {
|
|
||||||
columns, rows := math.Ceil(width/xstep), math.Ceil(height/ystep)
|
|
||||||
if !finite(columns*rows) || columns*rows >= float64(uint(^uint(0)>>1)+1) {
|
|
||||||
return nil, fmt.Errorf("PDF tiling pattern instance count overflow")
|
|
||||||
}
|
|
||||||
bounds := make([]Box, len(objects))
|
|
||||||
for index, object := range objects {
|
|
||||||
boundary := ""
|
|
||||||
if object.Type == "PathObject" && object.PathObject.Stroke != nil && !*object.PathObject.Stroke {
|
|
||||||
boundary = object.PathObject.Boundary
|
|
||||||
} else if object.Type == "ImageObject" && object.ImageObject.Border == nil {
|
|
||||||
boundary = object.ImageObject.Boundary
|
|
||||||
}
|
|
||||||
if boundary != "" {
|
|
||||||
var err error
|
|
||||||
bounds[index], err = ParseBox(boundary)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
left, right, top, bottom := pdfPatternInstanceRange(columns, rows, bounds, pattern.Width, pattern.Height, xstep*unit, ystep*unit)
|
|
||||||
var expanded pdfObjectBuffer
|
|
||||||
for row := top; row <= bottom; row++ {
|
|
||||||
for column := left; column <= right; column++ {
|
|
||||||
if err := p.ctx.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
dx, dy := float64(column)*xstep*unit, float64(row)*ystep*unit
|
|
||||||
for index, object := range objects {
|
|
||||||
if index%pdfObjectChunkLimit == 0 {
|
|
||||||
if err := p.ctx.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b := bounds[index]
|
|
||||||
if b.W > 0 && b.H > 0 && (b.X+dx+b.W < 0 || b.Y+dy+b.H < 0 || b.X+dx > pattern.Width || b.Y+dy > pattern.Height) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
translated, err := transformEditorObject(object, dx, dy, 1)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
expanded.append(translated)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pattern.CellContent.Objects, err = expanded.data(p.ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pattern, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// pdfOpaqueBlack 判断不受底层分色影响的全黑设备色
|
|
||||||
// 入参: paint PDF颜色与不透明度
|
|
||||||
// 返回: bool 是否为不透明全黑CMYK颜色
|
|
||||||
func pdfOpaqueBlack(paint pdfgo.Paint) bool {
|
|
||||||
return paint.Alpha == 1 && paint.CMYK != nil && paint.CMYK[3] == 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// pdfOverprintNeedsSeparation 判断彩色CMYK套印是否依赖分色输出
|
|
||||||
// 入参: paint PDF颜色与不透明度
|
|
||||||
// 返回: bool 是否需要保留分色语义
|
|
||||||
func pdfOverprintNeedsSeparation(paint pdfgo.Paint) bool {
|
|
||||||
_, process := paint.Process.CMYKMask()
|
|
||||||
return (paint.CMYK != nil || process) && !pdfOpaqueBlack(paint)
|
|
||||||
}
|
}
|
||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
|
"maps"
|
||||||
"math"
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -43,6 +44,12 @@ type PDFThreeDRenderOptions struct {
|
|||||||
Time float64
|
Time float64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pdfThreeDMarkupState 复用同页批注的宿主解析及模型校验,不保留模型解码缓冲
|
||||||
|
type pdfThreeDMarkupState struct {
|
||||||
|
sources map[pdfgo.Reference]pdfgo.ThreeD
|
||||||
|
sums map[*pdfgo.Stream][16]byte
|
||||||
|
}
|
||||||
|
|
||||||
// threeDAnnotation 按非交互呈现规则处理激活状态,保留原始模型与附件动作
|
// threeDAnnotation 按非交互呈现规则处理激活状态,保留原始模型与附件动作
|
||||||
// 入参: ctx 取消上下文, page PDF页面, annotation 三维注解
|
// 入参: ctx 取消上下文, page PDF页面, annotation 三维注解
|
||||||
// 返回: error 外观、资源或取消错误
|
// 返回: error 外观、资源或取消错误
|
||||||
@@ -191,3 +198,57 @@ func RenderPDFThreeD(ctx context.Context, model *U3DModel, source pdfgo.ThreeD,
|
|||||||
ResolveTexture: options.ResolveTexture, Nodes: options.Nodes, Time: options.Time,
|
ResolveTexture: options.ResolveTexture, Nodes: options.Nodes, Time: options.Time,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// threeDMarkupAnnotation 应用默认三维视图的批注可见性,其他视图批注保留为隐藏注解
|
||||||
|
// 入参: ctx 取消上下文, annotation 批注, markup 关联, state 同页缓存, strict 严格检查开关
|
||||||
|
// 返回: pdfgo.Annotation 静态批注, error 关联、校验或取消错误
|
||||||
|
func (p *pdfImporter) threeDMarkupAnnotation(ctx context.Context, annotation pdfgo.Annotation, markup *pdfgo.ThreeDMarkup, state *pdfThreeDMarkupState, strict bool) (pdfgo.Annotation, error) {
|
||||||
|
if markup == nil {
|
||||||
|
return annotation, nil
|
||||||
|
}
|
||||||
|
if strict {
|
||||||
|
return annotation, &pdfgo.UnsupportedError{Feature: "3D markup view switching conversion"}
|
||||||
|
}
|
||||||
|
source, ok := state.sources[markup.Annotation.Reference]
|
||||||
|
if !ok || markup.Annotation.Reference == (pdfgo.Reference{}) {
|
||||||
|
var err error
|
||||||
|
source, err = p.reader.ReadThreeDContext(ctx, markup.Annotation)
|
||||||
|
if err != nil {
|
||||||
|
return annotation, err
|
||||||
|
}
|
||||||
|
if markup.Annotation.Reference != (pdfgo.Reference{}) {
|
||||||
|
if state.sources == nil {
|
||||||
|
state.sources = make(map[pdfgo.Reference]pdfgo.ThreeD)
|
||||||
|
}
|
||||||
|
state.sources[markup.Annotation.Reference] = source
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if markup.Checksum != nil {
|
||||||
|
sum, ok := state.sums[source.Stream]
|
||||||
|
if !ok {
|
||||||
|
var err error
|
||||||
|
sum, err = pdfgo.ThreeDArtworkChecksum(ctx, source.Stream)
|
||||||
|
if err != nil {
|
||||||
|
return annotation, err
|
||||||
|
}
|
||||||
|
if state.sums == nil {
|
||||||
|
state.sums = make(map[*pdfgo.Stream][16]byte)
|
||||||
|
}
|
||||||
|
state.sums[source.Stream] = sum
|
||||||
|
}
|
||||||
|
if sum != *markup.Checksum {
|
||||||
|
p.warning(pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF 3D markup artwork checksum differs; view association may be stale"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visible := source.ActivationPolicy.Activation != "XA" && markup.MatchesView(source.SelectedView)
|
||||||
|
if !visible {
|
||||||
|
flags, err := p.reader.ReadAnnotationFlags(annotation)
|
||||||
|
if err != nil {
|
||||||
|
return annotation, err
|
||||||
|
}
|
||||||
|
annotation.Dictionary = maps.Clone(annotation.Dictionary)
|
||||||
|
annotation.Dictionary["F"] = pdfgo.Integer(flags | 2)
|
||||||
|
}
|
||||||
|
p.warning(pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF 3D markup default view visibility applied; view switching not transferred"})
|
||||||
|
return annotation, ctx.Err()
|
||||||
|
}
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
// 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 (
|
|
||||||
"context"
|
|
||||||
"maps"
|
|
||||||
|
|
||||||
"github.com/xiaoqidun/pdfgo"
|
|
||||||
)
|
|
||||||
|
|
||||||
// pdfThreeDMarkupState 复用同页批注的宿主解析及模型校验,不保留模型解码缓冲
|
|
||||||
type pdfThreeDMarkupState struct {
|
|
||||||
sources map[pdfgo.Reference]pdfgo.ThreeD
|
|
||||||
sums map[*pdfgo.Stream][16]byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// threeDMarkupAnnotation 应用默认三维视图的批注可见性,其他视图批注保留为隐藏注解
|
|
||||||
// 入参: ctx 取消上下文, annotation 批注, markup 关联, state 同页缓存, strict 严格检查开关
|
|
||||||
// 返回: pdfgo.Annotation 静态批注, error 关联、校验或取消错误
|
|
||||||
func (p *pdfImporter) threeDMarkupAnnotation(ctx context.Context, annotation pdfgo.Annotation, markup *pdfgo.ThreeDMarkup, state *pdfThreeDMarkupState, strict bool) (pdfgo.Annotation, error) {
|
|
||||||
if markup == nil {
|
|
||||||
return annotation, nil
|
|
||||||
}
|
|
||||||
if strict {
|
|
||||||
return annotation, &pdfgo.UnsupportedError{Feature: "3D markup view switching conversion"}
|
|
||||||
}
|
|
||||||
source, ok := state.sources[markup.Annotation.Reference]
|
|
||||||
if !ok || markup.Annotation.Reference == (pdfgo.Reference{}) {
|
|
||||||
var err error
|
|
||||||
source, err = p.reader.ReadThreeDContext(ctx, markup.Annotation)
|
|
||||||
if err != nil {
|
|
||||||
return annotation, err
|
|
||||||
}
|
|
||||||
if markup.Annotation.Reference != (pdfgo.Reference{}) {
|
|
||||||
if state.sources == nil {
|
|
||||||
state.sources = make(map[pdfgo.Reference]pdfgo.ThreeD)
|
|
||||||
}
|
|
||||||
state.sources[markup.Annotation.Reference] = source
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if markup.Checksum != nil {
|
|
||||||
sum, ok := state.sums[source.Stream]
|
|
||||||
if !ok {
|
|
||||||
var err error
|
|
||||||
sum, err = pdfgo.ThreeDArtworkChecksum(ctx, source.Stream)
|
|
||||||
if err != nil {
|
|
||||||
return annotation, err
|
|
||||||
}
|
|
||||||
if state.sums == nil {
|
|
||||||
state.sums = make(map[*pdfgo.Stream][16]byte)
|
|
||||||
}
|
|
||||||
state.sums[source.Stream] = sum
|
|
||||||
}
|
|
||||||
if sum != *markup.Checksum {
|
|
||||||
p.warning(pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF 3D markup artwork checksum differs; view association may be stale"})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
visible := source.ActivationPolicy.Activation != "XA" && markup.MatchesView(source.SelectedView)
|
|
||||||
if !visible {
|
|
||||||
flags, err := p.reader.ReadAnnotationFlags(annotation)
|
|
||||||
if err != nil {
|
|
||||||
return annotation, err
|
|
||||||
}
|
|
||||||
annotation.Dictionary = maps.Clone(annotation.Dictionary)
|
|
||||||
annotation.Dictionary["F"] = pdfgo.Integer(flags | 2)
|
|
||||||
}
|
|
||||||
p.warning(pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF 3D markup default view visibility applied; view switching not transferred"})
|
|
||||||
return annotation, ctx.Err()
|
|
||||||
}
|
|
||||||
@@ -145,3 +145,65 @@ func (p *pdfImporter) unmappedTrigger(ctx context.Context, owner string, trigger
|
|||||||
p.warning(pdfgo.Diagnostic{Message: "PDF " + feature + " not transferred to OFD"})
|
p.warning(pdfgo.Diagnostic{Message: "PDF " + feature + " not transferred to OFD"})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// annotationTriggerActions 按事件转换注解动作,页面打开动作独立于注解外观
|
||||||
|
// 入参: ctx 取消上下文, annotation PDF注解, event OFD事件
|
||||||
|
// 返回: []Action 动作序列, error 事件、动作或取消错误
|
||||||
|
func (p *pdfImporter) annotationTriggerActions(ctx context.Context, annotation pdfgo.Annotation, event string) ([]Action, error) {
|
||||||
|
if annotation.Subtype == "Widget" && event == "CLICK" {
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
triggers, err := p.reader.ReadAnnotationTriggers(ctx, annotation)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(triggers) == 0 {
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
flags, err := p.reader.ReadAnnotationFlags(annotation)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var converted []Action
|
||||||
|
for _, trigger := range triggers {
|
||||||
|
if trigger.Event == "PC" || trigger.Event == "PV" || trigger.Event == "PI" {
|
||||||
|
if event == "PO" {
|
||||||
|
if err := p.unmappedTrigger(ctx, "annotation", trigger); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (trigger.Event == "PO") != (event == "PO") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if flags&64 != 0 && (trigger.Event == "E" || trigger.Event == "X" || trigger.Event == "D" || trigger.Event == "U") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch trigger.Event {
|
||||||
|
case "U", "PO":
|
||||||
|
default:
|
||||||
|
return nil, &pdfgo.UnsupportedError{Feature: "annotation trigger " + string(trigger.Event)}
|
||||||
|
}
|
||||||
|
currentPage := p.page
|
||||||
|
if event == "PO" {
|
||||||
|
currentPage = p.pageActionPage
|
||||||
|
}
|
||||||
|
var source pdfgo.Annotation
|
||||||
|
if event == "CLICK" {
|
||||||
|
source = annotation
|
||||||
|
}
|
||||||
|
actions, err := p.eventActions(ctx, trigger.Action, event, source, ¤tPage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
converted = append(converted, actions...)
|
||||||
|
if event == "PO" {
|
||||||
|
p.pageActionPage = currentPage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if event == "PO" && len(converted) != 0 && (p.page < 0 || p.page >= len(p.editor.pages)) {
|
||||||
|
return nil, fmt.Errorf("invalid annotation trigger page")
|
||||||
|
}
|
||||||
|
return converted, ctx.Err()
|
||||||
|
}
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
// 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 (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/xiaoqidun/pdfgo"
|
|
||||||
)
|
|
||||||
|
|
||||||
// annotationTriggerActions 按事件转换注解动作,页面打开动作独立于注解外观
|
|
||||||
// 入参: ctx 取消上下文, annotation PDF注解, event OFD事件
|
|
||||||
// 返回: []Action 动作序列, error 事件、动作或取消错误
|
|
||||||
func (p *pdfImporter) annotationTriggerActions(ctx context.Context, annotation pdfgo.Annotation, event string) ([]Action, error) {
|
|
||||||
if annotation.Subtype == "Widget" && event == "CLICK" {
|
|
||||||
return nil, ctx.Err()
|
|
||||||
}
|
|
||||||
triggers, err := p.reader.ReadAnnotationTriggers(ctx, annotation)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(triggers) == 0 {
|
|
||||||
return nil, ctx.Err()
|
|
||||||
}
|
|
||||||
flags, err := p.reader.ReadAnnotationFlags(annotation)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var converted []Action
|
|
||||||
for _, trigger := range triggers {
|
|
||||||
if trigger.Event == "PC" || trigger.Event == "PV" || trigger.Event == "PI" {
|
|
||||||
if event == "PO" {
|
|
||||||
if err := p.unmappedTrigger(ctx, "annotation", trigger); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (trigger.Event == "PO") != (event == "PO") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if flags&64 != 0 && (trigger.Event == "E" || trigger.Event == "X" || trigger.Event == "D" || trigger.Event == "U") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
switch trigger.Event {
|
|
||||||
case "U", "PO":
|
|
||||||
default:
|
|
||||||
return nil, &pdfgo.UnsupportedError{Feature: "annotation trigger " + string(trigger.Event)}
|
|
||||||
}
|
|
||||||
currentPage := p.page
|
|
||||||
if event == "PO" {
|
|
||||||
currentPage = p.pageActionPage
|
|
||||||
}
|
|
||||||
var source pdfgo.Annotation
|
|
||||||
if event == "CLICK" {
|
|
||||||
source = annotation
|
|
||||||
}
|
|
||||||
actions, err := p.eventActions(ctx, trigger.Action, event, source, ¤tPage)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
converted = append(converted, actions...)
|
|
||||||
if event == "PO" {
|
|
||||||
p.pageActionPage = currentPage
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if event == "PO" && len(converted) != 0 && (p.page < 0 || p.page >= len(p.editor.pages)) {
|
|
||||||
return nil, fmt.Errorf("invalid annotation trigger page")
|
|
||||||
}
|
|
||||||
return converted, ctx.Err()
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
// 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 (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/xiaoqidun/pdfgo"
|
|
||||||
)
|
|
||||||
|
|
||||||
// documentAttachments 导入文档名称树中的附件,与注解及动作引用共用附件资源
|
|
||||||
// 入参: ctx 取消上下文
|
|
||||||
// 返回: error 名称树、附件读取或写入错误
|
|
||||||
func (p *pdfImporter) documentAttachments(ctx context.Context) error {
|
|
||||||
return p.reader.WalkEmbeddedFiles(ctx, func(_ string, file pdfgo.FileSpecification) error {
|
|
||||||
if file.Embedded == nil {
|
|
||||||
if p.warning == nil {
|
|
||||||
return fmt.Errorf("PDF embedded file has no embedded data")
|
|
||||||
}
|
|
||||||
p.warning(pdfgo.Diagnostic{Message: "PDF embedded file without embedded data ignored"})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
data, err := file.Embedded.DecodeContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
var unsupported *pdfgo.UnsupportedError
|
|
||||||
if p.warning != nil && errors.As(err, &unsupported) {
|
|
||||||
p.warning(pdfgo.Diagnostic{Message: "PDF document attachment not transferred to OFD: " + err.Error()})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = p.attachmentFile(file.Name, data)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
+24
-1
@@ -14,7 +14,9 @@
|
|||||||
|
|
||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import "context"
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
// pdfObjectChunkLimit 限制导入暂存片段的容量,不限制页面对象数量
|
// pdfObjectChunkLimit 限制导入暂存片段的容量,不限制页面对象数量
|
||||||
const pdfObjectChunkLimit = 256
|
const pdfObjectChunkLimit = 256
|
||||||
@@ -126,3 +128,24 @@ func (b *pdfObjectBuffer) data(ctx context.Context) ([]GraphicObject, error) {
|
|||||||
b.parts = b.parts[:1]
|
b.parts = b.parts[:1]
|
||||||
return objects, nil
|
return objects, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// commitObjects 将当前页面待转换对象写入编辑文档
|
||||||
|
// 返回: error 对象写入错误
|
||||||
|
func (p *pdfImporter) commitObjects() error {
|
||||||
|
if p.objects.len() == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
objects, err := p.objects.data(p.ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
objects, err = p.sharedClipObjects(objects)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := p.objectValidation().appendOwnedObjects(p.ctx, p.page, objects); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.objects = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+184
-50
@@ -15,7 +15,7 @@
|
|||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
"reflect"
|
||||||
|
|
||||||
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
|
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
|
||||||
"github.com/xiaoqidun/pdfgo"
|
"github.com/xiaoqidun/pdfgo"
|
||||||
@@ -28,55 +28,6 @@ type pdfClipPath struct {
|
|||||||
path *[1]PathObject
|
path *[1]PathObject
|
||||||
}
|
}
|
||||||
|
|
||||||
// pdfClipContainsBoundary 判断单一矩形裁剪是否完整覆盖对象边界,不近似曲线或斜边
|
|
||||||
// 入参: path PDF裁剪路径, matrix 页面变换, boundary OFD对象边界
|
|
||||||
// 返回: bool 是否可由对象边界代替裁剪
|
|
||||||
func pdfClipContainsBoundary(path pdfgo.Path, matrix pdfgo.Matrix, boundary Box) bool {
|
|
||||||
if len(path.Text) != 0 || len(path.Segments) < 4 || len(path.Segments) > 6 || !finite(boundary.X) || !finite(boundary.Y) || !finite(boundary.W) || !finite(boundary.H) || boundary.W <= 0 || boundary.H <= 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
var points [4]pdfgo.Point
|
|
||||||
for index := range 4 {
|
|
||||||
segment := path.Segments[index]
|
|
||||||
operator := "L"
|
|
||||||
if index == 0 {
|
|
||||||
operator = "M"
|
|
||||||
}
|
|
||||||
if segment.Operator != operator || len(segment.Points) != 1 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
points[index] = matrix.Apply(segment.Points[0])
|
|
||||||
if !finite(points[index].X) || !finite(points[index].Y) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
end := 4
|
|
||||||
if end < len(path.Segments) && path.Segments[end].Operator == "L" {
|
|
||||||
segment := path.Segments[end]
|
|
||||||
if len(segment.Points) != 1 || matrix.Apply(segment.Points[0]) != points[0] {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
end++
|
|
||||||
}
|
|
||||||
if end < len(path.Segments) {
|
|
||||||
segment := path.Segments[end]
|
|
||||||
if segment.Operator != "C" || len(segment.Points) != 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
end++
|
|
||||||
}
|
|
||||||
if end != len(path.Segments) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
a, b, c, d := points[0], points[1], points[2], points[3]
|
|
||||||
if !(a.Y == b.Y && b.X == c.X && c.Y == d.Y && d.X == a.X || a.X == b.X && b.Y == c.Y && c.X == d.X && d.Y == a.Y) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
left, right := math.Min(a.X, c.X), math.Max(a.X, c.X)
|
|
||||||
top, bottom := math.Min(a.Y, c.Y), math.Max(a.Y, c.Y)
|
|
||||||
return left < right && top < bottom && boundary.X >= left && boundary.Y >= top && boundary.X+boundary.W <= right && boundary.Y+boundary.H <= bottom
|
|
||||||
}
|
|
||||||
|
|
||||||
// clipPath 复用页面内重复路径编码,按估算字节预算淘汰且不缓存错误
|
// clipPath 复用页面内重复路径编码,按估算字节预算淘汰且不缓存错误
|
||||||
// 入参: path 裁剪路径
|
// 入参: path 裁剪路径
|
||||||
// 返回: pdfClipPath 变换后的边界及编码, error 取消错误
|
// 返回: pdfClipPath 变换后的边界及编码, error 取消错误
|
||||||
@@ -109,3 +60,186 @@ func (p *pdfImporter) clipPath(path pdfgo.Path) (pdfClipPath, error) {
|
|||||||
p.clipPaths.put(key, result, len(data)+len(boundary)+512)
|
p.clipPaths.put(key, result, len(data)+len(boundary)+512)
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sharedClipObjects 将连续重复的大裁剪区提取为复合对象,保留页面坐标和绘制顺序
|
||||||
|
// 入参: objects 本库生成且尚未分配标识的对象
|
||||||
|
// 返回: []GraphicObject 提取后的对象, error 资源创建或取消错误
|
||||||
|
func (p *pdfImporter) sharedClipObjects(objects []GraphicObject) ([]GraphicObject, error) {
|
||||||
|
var output []GraphicObject
|
||||||
|
for start := 0; start < len(objects); {
|
||||||
|
if err := p.ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
clips := pdfPageClip(objects[start])
|
||||||
|
end := start + 1
|
||||||
|
if clips != nil {
|
||||||
|
for end < len(objects) && pdfMatchesPageClip(objects[end], clips) {
|
||||||
|
if err := p.ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
end++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if end-start < 2 {
|
||||||
|
if output != nil {
|
||||||
|
output = append(output, objects[start])
|
||||||
|
}
|
||||||
|
start = end
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if output == nil {
|
||||||
|
output = append(make([]GraphicObject, 0, len(objects)), objects[:start]...)
|
||||||
|
}
|
||||||
|
members := objects[start:end:end]
|
||||||
|
for index := range members {
|
||||||
|
*editorObjectClips(&members[index]) = nil
|
||||||
|
}
|
||||||
|
id, err := p.objectValidation().addOwnedVector(p.ctx, members, p.pageWidth, p.pageHeight)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
output = append(output, GraphicObject{Type: "CompositeObject", CompositeGraphicUnit: CompositeGraphicUnit{
|
||||||
|
Boundary: pdfBoundary(Box{W: p.pageWidth, H: p.pageHeight}), ResourceID: id, Clips: clips,
|
||||||
|
}})
|
||||||
|
start = end
|
||||||
|
}
|
||||||
|
if output == nil {
|
||||||
|
return objects, nil
|
||||||
|
}
|
||||||
|
return output, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfPageClip 将导入裁剪区还原到页面坐标,仅提取包含大量重复路径数据的裁剪
|
||||||
|
// 入参: object 导入对象
|
||||||
|
// 返回: *Clips 页面坐标裁剪,不满足提取条件时为空
|
||||||
|
func pdfPageClip(object GraphicObject) *Clips {
|
||||||
|
if object.Type != "TextObject" && object.Type != "PathObject" && object.Type != "ImageObject" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
clips := *editorObjectClips(&object)
|
||||||
|
if clips == nil || clips.usesObjectMatrix() || len(clips.Clip) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
boundary, _ := editorGeometry(object)
|
||||||
|
box, err := ParseBox(boundary)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
translation := ""
|
||||||
|
if box.X != 0 || box.Y != 0 {
|
||||||
|
translation = pdfNumbers(1, 0, 0, 1, -box.X, -box.Y)
|
||||||
|
}
|
||||||
|
bytes := 0
|
||||||
|
result := &Clips{Clip: make([]Clip, len(clips.Clip))}
|
||||||
|
for index, clip := range clips.Clip {
|
||||||
|
if len(clip.Area) != 1 || clip.Area[0].CTM != translation {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
area := clip.Area[0]
|
||||||
|
area.CTM = ""
|
||||||
|
result.Clip[index].Area = []ClipArea{area}
|
||||||
|
for _, path := range area.Path {
|
||||||
|
bytes += len(path.AbbreviatedData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if bytes < 2048 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfMatchesPageClip 比较页面坐标裁剪,不将相同外接矩形视为相同路径
|
||||||
|
// 入参: object 导入对象, pageClip 已还原的裁剪区
|
||||||
|
// 返回: bool 是否可共享裁剪区
|
||||||
|
func pdfMatchesPageClip(object GraphicObject, pageClip *Clips) bool {
|
||||||
|
if object.Type != "TextObject" && object.Type != "PathObject" && object.Type != "ImageObject" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
clips := *editorObjectClips(&object)
|
||||||
|
if clips == nil || clips.usesObjectMatrix() || len(clips.Clip) != len(pageClip.Clip) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
boundary, _ := editorGeometry(object)
|
||||||
|
box, err := ParseBox(boundary)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
translation := ""
|
||||||
|
if box.X != 0 || box.Y != 0 {
|
||||||
|
translation = pdfNumbers(1, 0, 0, 1, -box.X, -box.Y)
|
||||||
|
}
|
||||||
|
for index, clip := range clips.Clip {
|
||||||
|
if len(clip.Area) != 1 || clip.Area[0].CTM != translation {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
area, expected := clip.Area[0], pageClip.Clip[index].Area[0]
|
||||||
|
if area.DrawParam != expected.DrawParam || !reflect.DeepEqual(area.Path, expected.Path) || !reflect.DeepEqual(area.Text, expected.Text) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// clips 省略覆盖对象边界的矩形裁剪,复用其余几何并保留位移及路径交集
|
||||||
|
// 入参: paths 裁剪路径, origin 对象边界
|
||||||
|
// 返回: *Clips OFD裁剪区域,无裁剪时为空, error 未解析的裁剪字形
|
||||||
|
func (p *pdfImporter) clips(paths []pdfgo.Path, origin Box) (*Clips, error) {
|
||||||
|
if len(paths) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
var clips *Clips
|
||||||
|
var areas []ClipArea
|
||||||
|
translation := ""
|
||||||
|
for _, path := range paths {
|
||||||
|
if err := p.ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if codecpdf.ClipContainsBoundary(path, p.matrix, origin) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if clips == nil {
|
||||||
|
clips = &Clips{Clip: make([]Clip, 0, len(paths))}
|
||||||
|
areas = make([]ClipArea, len(paths))
|
||||||
|
if origin.X != 0 || origin.Y != 0 {
|
||||||
|
translation = pdfNumbers(1, 0, 0, 1, -origin.X, -origin.Y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
index := len(clips.Clip)
|
||||||
|
area := &areas[index]
|
||||||
|
area.CTM = translation
|
||||||
|
if len(path.Segments) != 0 {
|
||||||
|
encoded, err := p.clipPath(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
area.Path = encoded.path[:1:1]
|
||||||
|
}
|
||||||
|
for _, mark := range path.Text {
|
||||||
|
objects, ok := p.clipTexts[mark]
|
||||||
|
if !ok {
|
||||||
|
if err := p.flushPath(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
count := p.objects.len()
|
||||||
|
textCount := p.report.TextObjects
|
||||||
|
if err := p.text(pdfgo.TextMark{Font: mark.Font, Glyphs: mark.Glyphs, Positions: mark.Positions, Matrix: mark.Matrix, Size: mark.Size, HorizontalScale: mark.HorizontalScale, Mode: 7, Clip: mark, Style: pdfgo.Style{Fill: pdfgo.Paint{Alpha: 1}}}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
p.objects.truncate(count)
|
||||||
|
p.report.TextObjects = textCount
|
||||||
|
objects = p.clipTexts[mark]
|
||||||
|
}
|
||||||
|
if len(path.Text) == 1 {
|
||||||
|
area.Text = objects[:len(objects):len(objects)]
|
||||||
|
} else {
|
||||||
|
area.Text = append(area.Text, objects...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(area.Path)+len(area.Text) == 0 {
|
||||||
|
fill, stroke := true, false
|
||||||
|
area.Path = []PathObject{{Boundary: "0 0 1 1", AbbreviatedData: "M 0 0 C", Fill: &fill, Stroke: &stroke}}
|
||||||
|
}
|
||||||
|
clips.Clip = append(clips.Clip, Clip{Area: areas[index : index+1 : index+1]})
|
||||||
|
}
|
||||||
|
return clips, nil
|
||||||
|
}
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
// 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 (
|
|
||||||
"reflect"
|
|
||||||
)
|
|
||||||
|
|
||||||
// sharedClipObjects 将连续重复的大裁剪区提取为复合对象,保留页面坐标和绘制顺序
|
|
||||||
// 入参: objects 本库生成且尚未分配标识的对象
|
|
||||||
// 返回: []GraphicObject 提取后的对象, error 资源创建或取消错误
|
|
||||||
func (p *pdfImporter) sharedClipObjects(objects []GraphicObject) ([]GraphicObject, error) {
|
|
||||||
var output []GraphicObject
|
|
||||||
for start := 0; start < len(objects); {
|
|
||||||
if err := p.ctx.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
clips := pdfPageClip(objects[start])
|
|
||||||
end := start + 1
|
|
||||||
if clips != nil {
|
|
||||||
for end < len(objects) && pdfMatchesPageClip(objects[end], clips) {
|
|
||||||
if err := p.ctx.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
end++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if end-start < 2 {
|
|
||||||
if output != nil {
|
|
||||||
output = append(output, objects[start])
|
|
||||||
}
|
|
||||||
start = end
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if output == nil {
|
|
||||||
output = append(make([]GraphicObject, 0, len(objects)), objects[:start]...)
|
|
||||||
}
|
|
||||||
members := objects[start:end:end]
|
|
||||||
for index := range members {
|
|
||||||
*editorObjectClips(&members[index]) = nil
|
|
||||||
}
|
|
||||||
id, err := p.objectValidation().addOwnedVector(p.ctx, members, p.pageWidth, p.pageHeight)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
output = append(output, GraphicObject{Type: "CompositeObject", CompositeGraphicUnit: CompositeGraphicUnit{
|
|
||||||
Boundary: pdfBoundary(Box{W: p.pageWidth, H: p.pageHeight}), ResourceID: id, Clips: clips,
|
|
||||||
}})
|
|
||||||
start = end
|
|
||||||
}
|
|
||||||
if output == nil {
|
|
||||||
return objects, nil
|
|
||||||
}
|
|
||||||
return output, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// pdfPageClip 将导入裁剪区还原到页面坐标,仅提取包含大量重复路径数据的裁剪
|
|
||||||
// 入参: object 导入对象
|
|
||||||
// 返回: *Clips 页面坐标裁剪,不满足提取条件时为空
|
|
||||||
func pdfPageClip(object GraphicObject) *Clips {
|
|
||||||
if object.Type != "TextObject" && object.Type != "PathObject" && object.Type != "ImageObject" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
clips := *editorObjectClips(&object)
|
|
||||||
if clips == nil || clips.usesObjectMatrix() || len(clips.Clip) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
boundary, _ := editorGeometry(object)
|
|
||||||
box, err := ParseBox(boundary)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
translation := ""
|
|
||||||
if box.X != 0 || box.Y != 0 {
|
|
||||||
translation = pdfNumbers(1, 0, 0, 1, -box.X, -box.Y)
|
|
||||||
}
|
|
||||||
bytes := 0
|
|
||||||
result := &Clips{Clip: make([]Clip, len(clips.Clip))}
|
|
||||||
for index, clip := range clips.Clip {
|
|
||||||
if len(clip.Area) != 1 || clip.Area[0].CTM != translation {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
area := clip.Area[0]
|
|
||||||
area.CTM = ""
|
|
||||||
result.Clip[index].Area = []ClipArea{area}
|
|
||||||
for _, path := range area.Path {
|
|
||||||
bytes += len(path.AbbreviatedData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if bytes < 2048 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// pdfMatchesPageClip 比较页面坐标裁剪,不将相同外接矩形视为相同路径
|
|
||||||
// 入参: object 导入对象, pageClip 已还原的裁剪区
|
|
||||||
// 返回: bool 是否可共享裁剪区
|
|
||||||
func pdfMatchesPageClip(object GraphicObject, pageClip *Clips) bool {
|
|
||||||
if object.Type != "TextObject" && object.Type != "PathObject" && object.Type != "ImageObject" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
clips := *editorObjectClips(&object)
|
|
||||||
if clips == nil || clips.usesObjectMatrix() || len(clips.Clip) != len(pageClip.Clip) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
boundary, _ := editorGeometry(object)
|
|
||||||
box, err := ParseBox(boundary)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
translation := ""
|
|
||||||
if box.X != 0 || box.Y != 0 {
|
|
||||||
translation = pdfNumbers(1, 0, 0, 1, -box.X, -box.Y)
|
|
||||||
}
|
|
||||||
for index, clip := range clips.Clip {
|
|
||||||
if len(clip.Area) != 1 || clip.Area[0].CTM != translation {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
area, expected := clip.Area[0], pageClip.Clip[index].Area[0]
|
|
||||||
if area.DrawParam != expected.DrawParam || !reflect.DeepEqual(area.Path, expected.Path) || !reflect.DeepEqual(area.Text, expected.Text) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
+6
-130
@@ -145,7 +145,7 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
|
|||||||
if !c.softMask && space.Model == "DeviceCMYK" && !space.Calibrated() {
|
if !c.softMask && space.Model == "DeviceCMYK" && !space.Calibrated() {
|
||||||
if node.image != nil && !node.image.Image.ImageMask {
|
if node.image != nil && !node.image.Image.ImageMask {
|
||||||
if overprint {
|
if overprint {
|
||||||
processMask, processOverprint, err = pdfImageProcessColorants(node.image.Image)
|
processMask, processOverprint, err = codecpdf.ImageProcessColorants(node.image.Image)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -155,7 +155,7 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
|
|||||||
processMask, processOverprint = paint.Process.CMYKMask()
|
processMask, processOverprint = paint.Process.CMYKMask()
|
||||||
}
|
}
|
||||||
if paint.Colorant != nil {
|
if paint.Colorant != nil {
|
||||||
nativePaint, processMask, err = pdfCompositeNativePaint(paint, space, style.RenderingIntent)
|
nativePaint, processMask, err = codecpdf.NativePaint(paint, space, style.RenderingIntent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -311,7 +311,7 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
|
|||||||
if nativePaint != nil {
|
if nativePaint != nil {
|
||||||
uniformValues = nativePaint.Values
|
uniformValues = nativePaint.Values
|
||||||
} else {
|
} else {
|
||||||
uniformValues, _, err = pdfCompositeColor(paint, pdfgo.Point{}, space, style.RenderingIntent, style.ColorConversion)
|
uniformValues, _, err = codecpdf.CompositeColor(paint, pdfgo.Point{}, space, style.RenderingIntent, style.ColorConversion)
|
||||||
}
|
}
|
||||||
uniformReady = true
|
uniformReady = true
|
||||||
}
|
}
|
||||||
@@ -344,7 +344,7 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
|
|||||||
if sampler != nil {
|
if sampler != nil {
|
||||||
values, visible, err = sampler.color(point)
|
values, visible, err = sampler.color(point)
|
||||||
} else {
|
} else {
|
||||||
values, visible, err = pdfCompositeColor(paint, point, space, style.RenderingIntent, style.ColorConversion)
|
values, visible, err = codecpdf.CompositeColor(paint, point, space, style.RenderingIntent, style.ColorConversion)
|
||||||
}
|
}
|
||||||
if !visible {
|
if !visible {
|
||||||
shape = 0
|
shape = 0
|
||||||
@@ -409,20 +409,6 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// pdfCompositeNativePaint 使用库层色料定义求值设备四色,不以备用色代替原生浓度
|
|
||||||
// 入参: paint 源画刷, space 设备四色空间, intent 渲染意图
|
|
||||||
// 返回: *pdfgo.Paint 原生画刷或nil, [4]bool 显式通道, error 色料定义错误
|
|
||||||
func pdfCompositeNativePaint(paint pdfgo.Paint, space *pdfgo.ColorSpace, intent pdfgo.Name) (*pdfgo.Paint, [4]bool, error) {
|
|
||||||
if paint.Colorant == nil {
|
|
||||||
return nil, [4]bool{}, nil
|
|
||||||
}
|
|
||||||
result, err := paint.Colorant.Space.ResolveInGroup(paint.Colorant.Tints, &pdfgo.ColorantDevice{Space: space}, space, intent, false)
|
|
||||||
if err != nil || len(result.Alternates) != 0 {
|
|
||||||
return nil, [4]bool{}, err
|
|
||||||
}
|
|
||||||
return result.Process, result.ProcessMask, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// pdfCompositeInterpolate 按形状覆盖混合预乘颜色及累计透明度
|
// pdfCompositeInterpolate 按形状覆盖混合预乘颜色及累计透明度
|
||||||
// 入参: target 原像素与输出, source 替换像素, shape 覆盖率
|
// 入参: target 原像素与输出, source 替换像素, shape 覆盖率
|
||||||
// 返回: error 像素或覆盖率错误
|
// 返回: error 像素或覆盖率错误
|
||||||
@@ -860,7 +846,7 @@ func (c *pdfCompositor) imageColor(mark *pdfgo.ImageMark, point pdfgo.Point, spa
|
|||||||
if mark.Style.Fill.Tiling != nil || mark.Style.Fill.Shading != nil {
|
if mark.Style.Fill.Tiling != nil || mark.Style.Fill.Shading != nil {
|
||||||
return [4]float64{}, alpha * (1 - values[0]), nil
|
return [4]float64{}, alpha * (1 - values[0]), nil
|
||||||
}
|
}
|
||||||
color, visible, err := pdfCompositeColor(mark.Style.Fill, mark.Matrix.Apply(point), space, mark.Style.RenderingIntent, mark.Style.ColorConversion)
|
color, visible, err := codecpdf.CompositeColor(mark.Style.Fill, mark.Matrix.Apply(point), space, mark.Style.RenderingIntent, mark.Style.ColorConversion)
|
||||||
if !visible {
|
if !visible {
|
||||||
return color, 0, err
|
return color, 0, err
|
||||||
}
|
}
|
||||||
@@ -873,116 +859,6 @@ func (c *pdfCompositor) imageColor(mark *pdfgo.ImageMark, point pdfgo.Point, spa
|
|||||||
return values, alpha, err
|
return values, alpha, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// pdfImageProcessColorants 识别可在四色设备上直接绘制的专色色料
|
|
||||||
// 入参: image PDF图像
|
|
||||||
// 返回: [4]bool 参与绘制的四色通道, bool 是否全部属于印刷原色, error 色空间错误
|
|
||||||
func pdfImageProcessColorants(image *pdfgo.Image) ([4]bool, bool, error) {
|
|
||||||
source, err := image.ColorantSpace()
|
|
||||||
if err != nil {
|
|
||||||
return [4]bool{}, false, err
|
|
||||||
}
|
|
||||||
space := &pdfgo.ColorSpace{Model: "DeviceCMYK"}
|
|
||||||
process := source.PrepareProcess(&pdfgo.ColorantDevice{Space: space}, space, false)
|
|
||||||
mask := process.Mask()
|
|
||||||
return mask, mask != ([4]bool{}), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// pdfCompositeColor 将原始画刷变换到混合空间,渐变在源空间求值
|
|
||||||
// 入参: paint 画刷, point 页面坐标, space 混合空间, intent 渲染意图, conversion 设备转换函数
|
|
||||||
// 返回: [4]float64 混合分量, bool 是否着色, error 颜色错误
|
|
||||||
func pdfCompositeColor(paint pdfgo.Paint, point pdfgo.Point, space *pdfgo.ColorSpace, intent pdfgo.Name, conversion pdfgo.ColorConversion) ([4]float64, bool, error) {
|
|
||||||
if paint.Shading != nil {
|
|
||||||
return [4]float64{}, false, &pdfgo.UnsupportedError{Feature: "local shading pattern compositing"}
|
|
||||||
}
|
|
||||||
if paint.Tiling != nil {
|
|
||||||
return [4]float64{}, false, &pdfgo.UnsupportedError{Feature: "local tiling pattern compositing"}
|
|
||||||
}
|
|
||||||
var values [4]float64
|
|
||||||
var source *pdfgo.ColorSpace
|
|
||||||
if paint.Function != nil {
|
|
||||||
g := paint.Function
|
|
||||||
if g.Bounds != nil {
|
|
||||||
inverse, ok := g.PatternMatrix.Inverse()
|
|
||||||
if !ok {
|
|
||||||
return values, false, fmt.Errorf("singular function shading bounds transform")
|
|
||||||
}
|
|
||||||
local := inverse.Apply(point)
|
|
||||||
if local.X < g.Bounds.XMin || local.X > g.Bounds.XMax || local.Y < g.Bounds.YMin || local.Y > g.Bounds.YMax {
|
|
||||||
return values, false, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
inverse, ok := g.Matrix.Inverse()
|
|
||||||
if !ok {
|
|
||||||
return values, false, fmt.Errorf("singular function shading transform")
|
|
||||||
}
|
|
||||||
local := inverse.Apply(point)
|
|
||||||
source, intent = g.Space, g.Intent
|
|
||||||
if local.X < g.Domain.XMin || local.X > g.Domain.XMax || local.Y < g.Domain.YMin || local.Y > g.Domain.YMax {
|
|
||||||
if g.Background == nil {
|
|
||||||
return values, false, nil
|
|
||||||
}
|
|
||||||
values = *g.Background
|
|
||||||
} else {
|
|
||||||
var err error
|
|
||||||
values, err = g.ValuesAt(local)
|
|
||||||
if err != nil {
|
|
||||||
return values, false, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if paint.Axial != nil || paint.Radial != nil {
|
|
||||||
var background *[4]float64
|
|
||||||
var bounds *pdfgo.Rectangle
|
|
||||||
var matrix pdfgo.Matrix
|
|
||||||
if g := paint.Axial; g != nil {
|
|
||||||
source, intent = g.Space, g.Intent
|
|
||||||
background, bounds, matrix = g.Background, g.Bounds, g.Matrix
|
|
||||||
} else {
|
|
||||||
g := paint.Radial
|
|
||||||
source, intent = g.Space, g.Intent
|
|
||||||
background, bounds, matrix = g.Background, g.Bounds, g.Matrix
|
|
||||||
}
|
|
||||||
if bounds != nil {
|
|
||||||
local := point
|
|
||||||
if matrix != (pdfgo.Matrix{}) {
|
|
||||||
inverse, ok := matrix.Inverse()
|
|
||||||
if !ok {
|
|
||||||
return values, false, fmt.Errorf("singular shading transform")
|
|
||||||
}
|
|
||||||
local = inverse.Apply(point)
|
|
||||||
}
|
|
||||||
if local.X < bounds.XMin || local.X > bounds.XMax || local.Y < bounds.YMin || local.Y > bounds.YMax {
|
|
||||||
return values, false, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
t, visible := pdfShadingPosition(paint, point)
|
|
||||||
var err error
|
|
||||||
if !visible {
|
|
||||||
if background == nil {
|
|
||||||
return values, false, nil
|
|
||||||
}
|
|
||||||
values = *background
|
|
||||||
} else if paint.Axial != nil {
|
|
||||||
values, err = paint.Axial.ValuesAt(t)
|
|
||||||
} else {
|
|
||||||
values, err = paint.Radial.ValuesAt(t)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return values, false, err
|
|
||||||
}
|
|
||||||
} else if paint.Space != nil {
|
|
||||||
values, source = paint.Values, paint.Space
|
|
||||||
} else if paint.CMYK != nil {
|
|
||||||
values, source = *paint.CMYK, &pdfgo.ColorSpace{Model: "DeviceCMYK"}
|
|
||||||
} else if paint.SourceSpace == "DeviceGray" {
|
|
||||||
values[0], source = paint.RGB[0], &pdfgo.ColorSpace{Model: "DeviceGray"}
|
|
||||||
} else {
|
|
||||||
copy(values[:], paint.RGB[:])
|
|
||||||
source = &pdfgo.ColorSpace{Model: "DeviceRGB"}
|
|
||||||
}
|
|
||||||
values, err := space.ConvertWith(values[:source.Components()], source, intent, conversion)
|
|
||||||
return values, true, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// color 复用精确参数对应的颜色,区域、背景及其他画刷保持原求值路径
|
// color 复用精确参数对应的颜色,区域、背景及其他画刷保持原求值路径
|
||||||
// 入参: point 页面坐标
|
// 入参: point 页面坐标
|
||||||
// 返回: [4]float64 混合分量, bool 是否着色, error 颜色错误
|
// 返回: [4]float64 混合分量, bool 是否着色, error 颜色错误
|
||||||
@@ -990,7 +866,7 @@ func (s *pdfCompositeColorSampler) color(point pdfgo.Point) ([4]float64, bool, e
|
|||||||
g := s.paint.Axial
|
g := s.paint.Axial
|
||||||
r := s.paint.Radial
|
r := s.paint.Radial
|
||||||
if g == nil && r == nil || g != nil && (g.Bounds != nil || g.Background != nil) || r != nil && (r.Bounds != nil || r.Background != nil) || s.paint.Function != nil || s.paint.Tiling != nil {
|
if g == nil && r == nil || g != nil && (g.Bounds != nil || g.Background != nil) || r != nil && (r.Bounds != nil || r.Background != nil) || s.paint.Function != nil || s.paint.Tiling != nil {
|
||||||
return pdfCompositeColor(s.paint, point, s.space, s.intent, s.conversion)
|
return codecpdf.CompositeColor(s.paint, point, s.space, s.intent, s.conversion)
|
||||||
}
|
}
|
||||||
if !s.prepared {
|
if !s.prepared {
|
||||||
var err error
|
var err error
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ func (p *pdfImporter) directCompositeNode(node pdfCompositeNode, space *pdfgo.Co
|
|||||||
}
|
}
|
||||||
if space.Model == "DeviceCMYK" && style.ColorConversion != (pdfgo.ColorConversion{}) {
|
if space.Model == "DeviceCMYK" && style.ColorConversion != (pdfgo.ColorConversion{}) {
|
||||||
if node.image != nil && !node.image.Image.ImageMask {
|
if node.image != nil && !node.image.Image.ImageMask {
|
||||||
_, native, err := pdfImageProcessColorants(node.image.Image)
|
_, native, err := codecpdf.ImageProcessColorants(node.image.Image)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@@ -348,7 +348,7 @@ func (p *pdfImporter) processOverprint(nodes []pdfCompositeNode) (bool, error) {
|
|||||||
}
|
}
|
||||||
style, fill, stroke := node.style()
|
style, fill, stroke := node.style()
|
||||||
if node.image != nil && style.FillOverprint {
|
if node.image != nil && style.FillOverprint {
|
||||||
if _, found, err := pdfImageProcessColorants(node.image.Image); err != nil || found {
|
if _, found, err := codecpdf.ImageProcessColorants(node.image.Image); err != nil || found {
|
||||||
return found, err
|
return found, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -371,7 +371,7 @@ func (p *pdfImporter) processOverprint(nodes []pdfCompositeNode) (bool, error) {
|
|||||||
if native := codecpdf.PrepareShadingColorants(paint, &pdfgo.ColorSpace{Model: "DeviceCMYK"}, false); native != nil {
|
if native := codecpdf.PrepareShadingColorants(paint, &pdfgo.ColorSpace{Model: "DeviceCMYK"}, false); native != nil {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
native, _, err := pdfCompositeNativePaint(paint, &pdfgo.ColorSpace{Model: "DeviceCMYK"}, style.RenderingIntent)
|
native, _, err := codecpdf.NativePaint(paint, &pdfgo.ColorSpace{Model: "DeviceCMYK"}, style.RenderingIntent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,12 +21,20 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"image/color"
|
"image/color"
|
||||||
"math"
|
"math"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
|
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
|
||||||
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
|
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
|
||||||
"github.com/xiaoqidun/pdfgo"
|
"github.com/xiaoqidun/pdfgo"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// pdfPointBounds 逐点汇总边界,不保留文字或路径的临时坐标数组
|
||||||
|
type pdfPointBounds struct {
|
||||||
|
minX, minY, maxX, maxY float64
|
||||||
|
found bool
|
||||||
|
}
|
||||||
|
|
||||||
// pdfGeometryPath 转换PDF路径并衔接OFD几何类型
|
// pdfGeometryPath 转换PDF路径并衔接OFD几何类型
|
||||||
// 入参: ctx 取消上下文, source PDF路径, matrix 坐标变换
|
// 入参: ctx 取消上下文, source PDF路径, matrix 坐标变换
|
||||||
// 返回: GeometryPath 独立几何路径, error 指令、坐标或取消错误
|
// 返回: GeometryPath 独立几何路径, error 指令、坐标或取消错误
|
||||||
@@ -203,3 +211,278 @@ func (p *pdfImporter) coverageClip(paths []pdfgo.Path, geometry GeometryBackend)
|
|||||||
}
|
}
|
||||||
return clip, nil
|
return clip, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pdfGlyphPath 将OFD字形几何交给PDF路径适配器转换
|
||||||
|
// 入参: outline 字形轮廓, matrix 字形到页面的变换
|
||||||
|
// 返回: pdfgo.Path 页面路径, error 非字体轮廓指令
|
||||||
|
func pdfGlyphPath(outline GeometryPath, matrix pdfgo.Matrix) (pdfgo.Path, error) {
|
||||||
|
return codecpdf.GlyphPath(basegeom.Path(outline), matrix)
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfNumbers 按OFD数值格式序列化坐标
|
||||||
|
// 入参: values 数值列表
|
||||||
|
// 返回: string 空格分隔的数值
|
||||||
|
func pdfNumbers(values ...float64) string {
|
||||||
|
var buffer [128]byte
|
||||||
|
data := buffer[:0]
|
||||||
|
for n, v := range values {
|
||||||
|
if n != 0 {
|
||||||
|
data = append(data, ' ')
|
||||||
|
}
|
||||||
|
data = strconv.AppendFloat(data, v, 'g', -1, 64)
|
||||||
|
}
|
||||||
|
return string(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfDashPattern 换算PDF虚线长度,将单值数组展开为等长线段与间隔
|
||||||
|
// 入参: dash PDF虚线数组, scale 坐标缩放比例
|
||||||
|
// 返回: string OFD虚线数组
|
||||||
|
func pdfDashPattern(dash []float64, scale float64) string {
|
||||||
|
if len(dash) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
values := make([]float64, max(2, len(dash)))
|
||||||
|
for i, value := range dash {
|
||||||
|
values[i] = value * scale
|
||||||
|
}
|
||||||
|
if len(dash) == 1 {
|
||||||
|
values[1] = values[0]
|
||||||
|
}
|
||||||
|
return pdfNumbers(values...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfBoundary 序列化对象边界
|
||||||
|
// 入参: b 对象边界
|
||||||
|
// 返回: string OFD边界数据
|
||||||
|
func pdfBoundary(b Box) string { return pdfNumbers(b.X, b.Y, b.W, b.H) }
|
||||||
|
|
||||||
|
// pdfBounds 计算点集合的包围框
|
||||||
|
// 入参: points 坐标点集合
|
||||||
|
// 返回: Box 包围框
|
||||||
|
func pdfBounds(points []pdfgo.Point) Box {
|
||||||
|
var bounds pdfPointBounds
|
||||||
|
for _, point := range points {
|
||||||
|
bounds.add(point)
|
||||||
|
}
|
||||||
|
return bounds.box()
|
||||||
|
}
|
||||||
|
|
||||||
|
// add 按原顺序累计一个控制点的最小和最大坐标
|
||||||
|
// 入参: point 坐标点
|
||||||
|
func (b *pdfPointBounds) add(point pdfgo.Point) {
|
||||||
|
if !b.found {
|
||||||
|
b.minX, b.minY, b.maxX, b.maxY = point.X, point.Y, point.X, point.Y
|
||||||
|
b.found = true
|
||||||
|
}
|
||||||
|
b.minX, b.minY = math.Min(b.minX, point.X), math.Min(b.minY, point.Y)
|
||||||
|
b.maxX, b.maxY = math.Max(b.maxX, point.X), math.Max(b.maxY, point.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// box 取得原精度边界,空集合仍返回零值
|
||||||
|
// 返回: Box 坐标范围
|
||||||
|
func (b *pdfPointBounds) box() Box {
|
||||||
|
if !b.found {
|
||||||
|
return Box{}
|
||||||
|
}
|
||||||
|
return Box{X: b.minX, Y: b.minY, W: math.Max(b.maxX-b.minX, 1e-6), H: math.Max(b.maxY-b.minY, 1e-6)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pathBoundsContext 逐段计算变换后的控制点边界,不创建临时点集合
|
||||||
|
// 入参: ctx 取消上下文, path PDF路径
|
||||||
|
// 返回: Box 控制点边界,空路径返回零值, error 取消错误
|
||||||
|
func (p *pdfImporter) pathBoundsContext(ctx context.Context, path pdfgo.Path) (Box, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return Box{}, err
|
||||||
|
}
|
||||||
|
minX, minY, maxX, maxY := math.Inf(1), math.Inf(1), math.Inf(-1), math.Inf(-1)
|
||||||
|
found := false
|
||||||
|
for _, segment := range path.Segments {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return Box{}, err
|
||||||
|
}
|
||||||
|
for index, point := range segment.Points {
|
||||||
|
if index&255 == 0 {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return Box{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
point = p.matrix.Apply(point)
|
||||||
|
minX, minY = math.Min(minX, point.X), math.Min(minY, point.Y)
|
||||||
|
maxX, maxY = math.Max(maxX, point.X), math.Max(maxY, point.Y)
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return Box{}, err
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return Box{}, nil
|
||||||
|
}
|
||||||
|
return Box{X: minX, Y: minY, W: math.Max(maxX-minX, 1e-6), H: math.Max(maxY-minY, 1e-6)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// pathDataContext 转换路径并在分段处理时检查取消
|
||||||
|
// 入参: ctx 取消上下文, path PDF路径, origin 对象边界
|
||||||
|
// 返回: string OFD路径数据, error 取消错误
|
||||||
|
func (p *pdfImporter) pathDataContext(ctx context.Context, path pdfgo.Path, origin Box) (string, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
data := make([]byte, 0, len(path.Segments)*48)
|
||||||
|
for index, segment := range path.Segments {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if index != 0 {
|
||||||
|
data = append(data, ' ')
|
||||||
|
}
|
||||||
|
data = append(data, segment.Operator...)
|
||||||
|
for _, point := range segment.Points {
|
||||||
|
point = p.matrix.Apply(point)
|
||||||
|
data = append(data, ' ')
|
||||||
|
data = strconv.AppendFloat(data, point.X-origin.X, 'g', -1, 64)
|
||||||
|
data = append(data, ' ')
|
||||||
|
data = strconv.AppendFloat(data, point.Y-origin.Y, 'g', -1, 64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// path 合并相同状态下的不透明描边,保留独立子路径与绘制顺序
|
||||||
|
// 入参: mark PDF路径绘制信息
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (p *pdfImporter) path(mark pdfgo.PathMark) error {
|
||||||
|
if mark.Style.BlendMode != "" && mark.Style.BlendMode != "Normal" && mark.Style.BlendMode != "Compatible" {
|
||||||
|
return &pdfgo.UnsupportedError{Feature: "path blend mode " + string(mark.Style.BlendMode)}
|
||||||
|
}
|
||||||
|
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 && (p.pendingPath.StrokeMatrix != mark.StrokeMatrix || !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 {
|
||||||
|
if p.rasterMaskAllowed(err) {
|
||||||
|
mask := mark.Style.SoftMask
|
||||||
|
mark.Style.SoftMask = nil
|
||||||
|
return p.rasterGroup(pdfgo.GroupMark{Alpha: 1, SoftMask: mask}, func(v pdfgo.Visitor) error { return v.Path(mark) })
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if mark.Fill && mark.Style.FillOverprint && pdfOverprintNeedsSeparation(mark.Style.Fill) || mark.Stroke && mark.Style.StrokeOverprint && pdfOverprintNeedsSeparation(mark.Style.Stroke) {
|
||||||
|
return &pdfgo.UnsupportedError{Feature: "color separation overprint"}
|
||||||
|
}
|
||||||
|
if p.warning != nil && (mark.Fill && p.gradientError(mark.Style.Fill) != nil || mark.Stroke && p.gradientError(mark.Style.Stroke) != nil) {
|
||||||
|
if err := p.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return p.gradientPath(mark)
|
||||||
|
}
|
||||||
|
box, err := p.pathBoundsContext(p.ctx, mark.Path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if box == (Box{}) {
|
||||||
|
return fmt.Errorf("empty PDF painted path")
|
||||||
|
}
|
||||||
|
scale := math.Hypot(p.matrix[0], p.matrix[1])
|
||||||
|
drawing, origin, ctm := *p, box, ""
|
||||||
|
marginScale := scale
|
||||||
|
if mark.StrokeMatrix != (pdfgo.Matrix{}) {
|
||||||
|
inverse, ok := mark.StrokeMatrix.Inverse()
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid PDF stroke matrix")
|
||||||
|
}
|
||||||
|
drawing.matrix, origin, scale = inverse, Box{}, 1
|
||||||
|
m := p.matrix.Mul(mark.StrokeMatrix)
|
||||||
|
marginScale = math.Max(math.Hypot(m[0], m[2]), math.Hypot(m[1], m[3]))
|
||||||
|
}
|
||||||
|
if mark.Stroke {
|
||||||
|
margin := mark.Style.LineWidth * marginScale / 2 * math.Max(1, mark.Style.MiterLimit)
|
||||||
|
box = Box{X: box.X - margin, Y: box.Y - margin, W: box.W + 2*margin, H: box.H + 2*margin}
|
||||||
|
}
|
||||||
|
if mark.StrokeMatrix == (pdfgo.Matrix{}) {
|
||||||
|
origin = box
|
||||||
|
} else {
|
||||||
|
m := p.matrix.Mul(mark.StrokeMatrix)
|
||||||
|
m[4], m[5] = m[4]-box.X, m[5]-box.Y
|
||||||
|
ctm = pdfNumbers(m[:]...)
|
||||||
|
}
|
||||||
|
paint := func(value pdfgo.Paint) (*FillColor, error) {
|
||||||
|
if value.Tiling != nil || pdfGradientPattern(value) {
|
||||||
|
return p.paintColor(value, box)
|
||||||
|
}
|
||||||
|
return drawing.paintColor(value, origin)
|
||||||
|
}
|
||||||
|
fillColor, strokePaint := p.color(mark.Style.Fill), p.color(mark.Style.Stroke)
|
||||||
|
if mark.Fill {
|
||||||
|
fillColor, err = paint(mark.Style.Fill)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mark.Stroke {
|
||||||
|
strokePaint, err = paint(mark.Style.Stroke)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
strokeColor := StrokeColor(*strokePaint)
|
||||||
|
clips, err := p.clips(mark.Style.Clips, box)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data, err := drawing.pathDataContext(p.ctx, mark.Path, origin)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
object := PathObject{Boundary: pdfBoundary(box), CTM: ctm, AbbreviatedData: data, Fill: &mark.Fill, Stroke: &mark.Stroke, FillColor: fillColor, 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: clips}
|
||||||
|
object.LineWidthSet = object.LineWidth == 0
|
||||||
|
if mark.Path.EvenOdd {
|
||||||
|
object.Rule = "Even-Odd"
|
||||||
|
}
|
||||||
|
if len(mark.Style.Dash) > 0 {
|
||||||
|
object.DashPattern = pdfDashPattern(mark.Style.Dash, scale)
|
||||||
|
phase := mark.Style.DashPhase * scale
|
||||||
|
object.DashOffset = &phase
|
||||||
|
}
|
||||||
|
p.appendObject(GraphicObject{Type: "PathObject", PathObject: object})
|
||||||
|
p.report.PathObjects++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
|
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
|
||||||
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
|
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
|
||||||
"github.com/xiaoqidun/pdfgo"
|
"github.com/xiaoqidun/pdfgo"
|
||||||
)
|
)
|
||||||
@@ -743,3 +744,56 @@ func (p *pdfImporter) maskStyle(style pdfgo.Style) (pdfgo.Style, error) {
|
|||||||
style.SoftMask = nil
|
style.SoftMask = nil
|
||||||
return style, 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
|
||||||
|
}
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package ofdgo
|
|
||||||
|
|
||||||
import "github.com/xiaoqidun/pdfgo"
|
|
||||||
|
|
||||||
// halftone 检查连续色调转换,避免将打印网屏误用于屏幕显示
|
|
||||||
// 入参: style 图形状态
|
|
||||||
// 返回: error 严格模式无法保留设备网屏
|
|
||||||
func (p *pdfImporter) halftone(style pdfgo.Style) error {
|
|
||||||
h := style.Halftone
|
|
||||||
message := "PDF continuous-tone appearance retained; device halftone not transferred to OFD"
|
|
||||||
if h == nil || h.Type == 0 {
|
|
||||||
if style.HalftoneOrigin == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
message = "PDF continuous-tone appearance retained; device halftone origin not transferred to OFD"
|
|
||||||
}
|
|
||||||
if p.warning == nil {
|
|
||||||
return &pdfgo.UnsupportedError{Feature: "device halftone conversion"}
|
|
||||||
}
|
|
||||||
if p.halftoneWarnings == nil {
|
|
||||||
p.halftoneWarnings = make(map[*pdfgo.Halftone]bool)
|
|
||||||
}
|
|
||||||
if !p.halftoneWarnings[h] {
|
|
||||||
p.halftoneWarnings[h] = true
|
|
||||||
p.warning(pdfgo.Diagnostic{Message: message})
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
// 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/color"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
|
||||||
|
"github.com/xiaoqidun/pdfgo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// pdfImageKey 区分图像流、渲染意图、原生输出空间、模板填充色及采样尺寸
|
||||||
|
type pdfImageKey struct {
|
||||||
|
stream *pdfgo.Stream
|
||||||
|
intent pdfgo.Name
|
||||||
|
process pdfgo.Name
|
||||||
|
tint color.NRGBA64
|
||||||
|
size image.Point
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfImageResource 保存图像的有效颜色空间及对应资源,避免跨资源重映射复用
|
||||||
|
type pdfImageResource struct {
|
||||||
|
space pdfgo.Object
|
||||||
|
id string
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfImageContentKey 区分编码内容、渲染意图、原生输出空间、模板填充色及有损采样尺寸
|
||||||
|
type pdfImageContentKey struct {
|
||||||
|
data [32]byte
|
||||||
|
intent pdfgo.Name
|
||||||
|
process pdfgo.Name
|
||||||
|
tint color.NRGBA64
|
||||||
|
size image.Point
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfImageContent 保存不依赖间接资源的图像描述及已注册编号
|
||||||
|
type pdfImageContent struct {
|
||||||
|
source *pdfgo.Image
|
||||||
|
id string
|
||||||
|
}
|
||||||
|
|
||||||
|
// image 转换图像样本,不将整页或其他对象栅格化
|
||||||
|
// 有损编码复用已解码像素,候选容量超限时沿用原编码路径
|
||||||
|
// 入参: mark PDF图像绘制信息
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
|
||||||
|
if err := p.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if mark.Style.BlendMode != "" && mark.Style.BlendMode != "Normal" && mark.Style.BlendMode != "Compatible" {
|
||||||
|
return &pdfgo.UnsupportedError{Feature: "image blend mode " + string(mark.Style.BlendMode)}
|
||||||
|
}
|
||||||
|
if err := p.flushPath(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if mark.Image.ImageMask && (mark.Style.Fill.Axial != nil || mark.Style.Fill.Radial != nil || mark.Style.Fill.Function != nil || mark.Style.Fill.Mesh != nil || mark.Style.Fill.Shading != nil) {
|
||||||
|
if p.warning != nil {
|
||||||
|
return p.compositeRegion(nil, pdfCompositeNode{image: &mark}, &pdfgo.ColorSpace{Model: "DeviceRGB"}, false)
|
||||||
|
}
|
||||||
|
return &pdfgo.UnsupportedError{Feature: "gradient stencil image"}
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
mark.Style, err = p.maskStyle(mark.Style)
|
||||||
|
if err != nil {
|
||||||
|
if p.rasterMaskAllowed(err) {
|
||||||
|
mask := mark.Style.SoftMask
|
||||||
|
mark.Style.SoftMask = nil
|
||||||
|
return p.rasterGroup(pdfgo.GroupMark{Alpha: 1, SoftMask: mask}, func(v pdfgo.Visitor) error { return v.Image(mark) })
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if mark.Style.FillOverprint {
|
||||||
|
return &pdfgo.UnsupportedError{Feature: "image color separation overprint"}
|
||||||
|
}
|
||||||
|
source := mark.Image
|
||||||
|
key := pdfImageKey{stream: source.Stream, intent: source.Intent}
|
||||||
|
key.process, err = p.imageProcessModel(source)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !source.ImageMask && p.compression.Mode == CompressionLossy {
|
||||||
|
m := p.matrix.Mul(mark.Matrix)
|
||||||
|
key.size = compressionImageSize(math.Hypot(m[0], m[1]), math.Hypot(m[2], m[3]), p.compression.ImageDPI())
|
||||||
|
if key.size.X >= source.Width || key.size.Y >= source.Height {
|
||||||
|
key.size = image.Point{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if source.ImageMask {
|
||||||
|
fill := mark.Style.Fill.RGB
|
||||||
|
if values := mark.Style.Fill.CMYK; values != nil {
|
||||||
|
space := &pdfgo.ColorSpace{Model: "DeviceCMYK"}
|
||||||
|
fill, err = space.RGB(values[:], mark.Style.RenderingIntent)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
key.tint = color.NRGBA64{
|
||||||
|
R: uint16(math.Round(fill[0] * 65535)),
|
||||||
|
G: uint16(math.Round(fill[1] * 65535)),
|
||||||
|
B: uint16(math.Round(fill[2] * 65535)),
|
||||||
|
A: 65535,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
id := ""
|
||||||
|
for _, resource := range p.imageIDs[key] {
|
||||||
|
if reflect.DeepEqual(resource.space, source.ColorSpace) {
|
||||||
|
id = resource.id
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
streamCached := id != ""
|
||||||
|
var contentKey pdfImageContentKey
|
||||||
|
cacheable := false
|
||||||
|
if id == "" {
|
||||||
|
contentKey, cacheable, err = p.imageContentKey(source, key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if cacheable {
|
||||||
|
if p.imageContents == nil {
|
||||||
|
p.imageContents = &renderCache[pdfImageContentKey, pdfImageContent]{limit: 8 << 20}
|
||||||
|
}
|
||||||
|
if cached, found := p.imageContents.get(contentKey); found && cached.matches(source) {
|
||||||
|
id = cached.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if id == "" {
|
||||||
|
var jbig2Original, jpegOriginal []byte
|
||||||
|
var decoded image.Image
|
||||||
|
if key.process != "" {
|
||||||
|
decoded, err = p.imageProcess(source, key.process)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if key.size != (image.Point{}) {
|
||||||
|
decoded, err = pdfgo.ResizeImage(p.ctx, decoded, key.size)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
jbig2Original, err = source.JBIG2FileContext(p.ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
jpegOriginal, err = source.JPEGFileContext(p.ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(jbig2Original) == 0 && len(jpegOriginal) == 0 && (source.ImageMask || key.size != (image.Point{})) {
|
||||||
|
decoded, err = source.DecodeImageSizeContext(p.ctx, key.size)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if source.ImageMask {
|
||||||
|
decoded = &codecpdf.StencilImage{Source: decoded, Fill: key.tint}
|
||||||
|
}
|
||||||
|
data := jbig2Original
|
||||||
|
if len(data) == 0 {
|
||||||
|
data = jpegOriginal
|
||||||
|
}
|
||||||
|
optimized := false
|
||||||
|
if len(data) == 0 && decoded != nil && !source.ImageMask && p.compression.Mode == CompressionLossy {
|
||||||
|
data, err = pdfgo.EncodeImageResource(p.ctx, decoded, p.compression)
|
||||||
|
if err != nil && !errors.Is(err, io.ErrShortBuffer) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
optimized = err == nil
|
||||||
|
}
|
||||||
|
if len(data) == 0 {
|
||||||
|
var encoded bytes.Buffer
|
||||||
|
if decoded != nil {
|
||||||
|
err = pdfgo.EncodePNG(p.ctx, &encoded, decoded)
|
||||||
|
} else {
|
||||||
|
err = source.EncodePNGContext(p.ctx, &encoded)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data = encoded.Bytes()
|
||||||
|
}
|
||||||
|
if !source.ImageMask && p.compression.Mode == CompressionLossy {
|
||||||
|
if !optimized {
|
||||||
|
data, err = pdfgo.OptimizeImageResource(p.ctx, data, p.compression, key.size)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
decoded = nil
|
||||||
|
}
|
||||||
|
id, err = p.editor.addDecodedImageContext(p.ctx, data, decoded)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if cacheable {
|
||||||
|
p.imageContents.put(contentKey, pdfImageContent{source: source, id: id}, len(source.Stream.Data)+65536)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !streamCached {
|
||||||
|
if p.imageIDs == nil {
|
||||||
|
p.imageIDs = map[pdfImageKey][]pdfImageResource{}
|
||||||
|
}
|
||||||
|
p.imageIDs[key] = append(p.imageIDs[key], pdfImageResource{space: source.ColorSpace, id: id})
|
||||||
|
}
|
||||||
|
return p.appendImage(mark, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// appendImage 定位已注册的图像资源,保留透明度与裁剪
|
||||||
|
// 入参: mark 图像绘制信息, id 资源编号
|
||||||
|
// 返回: error 裁剪错误
|
||||||
|
func (p *pdfImporter) appendImage(mark pdfgo.ImageMark, id string) error {
|
||||||
|
m := p.matrix.Mul(mark.Matrix).Mul(pdfgo.Matrix{1, 0, 0, -1, 0, 1})
|
||||||
|
box := pdfBounds([]pdfgo.Point{m.Apply(pdfgo.Point{}), m.Apply(pdfgo.Point{X: 1}), m.Apply(pdfgo.Point{Y: 1}), m.Apply(pdfgo.Point{X: 1, Y: 1})})
|
||||||
|
alpha := int(math.Round(mark.Style.Fill.Alpha * 255))
|
||||||
|
clips, err := p.clips(mark.Style.Clips, box)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
object := ImageObject{Boundary: pdfBoundary(box), ResourceID: id, CTM: pdfNumbers(m[0], m[1], m[2], m[3], m[4]-box.X, m[5]-box.Y), Alpha: &alpha, Clips: clips}
|
||||||
|
p.appendObject(GraphicObject{Type: "ImageObject", ImageObject: object})
|
||||||
|
p.report.ImageObjects++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// imageContentKey 对无间接依赖的小图像生成内容键,超大图像仍沿用流缓存
|
||||||
|
// 入参: source 图像描述, key 流缓存键
|
||||||
|
// 返回: pdfImageContentKey 内容键, bool 是否可复用, error 取消错误
|
||||||
|
func (p *pdfImporter) imageContentKey(source *pdfgo.Image, key pdfImageKey) (pdfImageContentKey, bool, error) {
|
||||||
|
digest, reusable, err := codecpdf.ImageDigest(p.ctx, source)
|
||||||
|
if !reusable {
|
||||||
|
return pdfImageContentKey{}, reusable, err
|
||||||
|
}
|
||||||
|
return pdfImageContentKey{data: digest, intent: key.intent, process: key.process, tint: key.tint, size: key.size}, true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// matches 核对完整编码与有效图像属性,不以摘要相同代替内容相同
|
||||||
|
// 入参: source 待复用图像
|
||||||
|
// 返回: bool 是否可使用同一图像资源
|
||||||
|
func (c pdfImageContent) matches(source *pdfgo.Image) bool {
|
||||||
|
return codecpdf.SameImage(c.source, source)
|
||||||
|
}
|
||||||
|
|
||||||
|
// imageProcessModel 选择与页面原生过程色一致的图像输出,已等价的备用色沿用现有编码路径
|
||||||
|
// 入参: source 源图像
|
||||||
|
// 返回: pdfgo.Name 原生输出空间,无需重映射时为空, error 色料解析错误
|
||||||
|
func (p *pdfImporter) imageProcessModel(source *pdfgo.Image) (pdfgo.Name, error) {
|
||||||
|
space := p.compositeSpace
|
||||||
|
if source.ImageMask || !space.Device() {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
colorants, err := source.ColorantSpace()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
process := colorants.PrepareProcess(&pdfgo.ColorantDevice{Space: space}, space, false)
|
||||||
|
if process == nil || process.Mask() == ([4]bool{}) || process.AlternateEquivalent() {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return space.Model, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// imageProcess 复用源分量并保留原始分辨率,颜色变换随编码逐像素执行
|
||||||
|
// 入参: source 源图像, model 原生设备空间
|
||||||
|
// 返回: image.Image 只读图像, error 解码或分量映射错误
|
||||||
|
func (p *pdfImporter) imageProcess(source *pdfgo.Image, model pdfgo.Name) (image.Image, error) {
|
||||||
|
cache := p.compositingCache()
|
||||||
|
space := &pdfgo.ColorSpace{Model: model}
|
||||||
|
process, err := cache.imageProcess(p, source, space, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if process == nil {
|
||||||
|
return nil, fmt.Errorf("invalid native PDF image process space %s", model)
|
||||||
|
}
|
||||||
|
return &codecpdf.NativeImage{Source: process, Space: space}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// imageProcess 复用同组原生采样,仅在需要备用颜色时保留完整分量
|
||||||
|
// 入参: importer 导入器, source 源图像, space 组混合空间, softMask 是否用于软蒙版
|
||||||
|
// 返回: *pdfgo.ImageProcess 原生采样,需要备用色时为nil, error 解码或布局错误
|
||||||
|
func (c *pdfCompositeCache) imageProcess(importer *pdfImporter, source *pdfgo.Image, space *pdfgo.ColorSpace, softMask bool) (*pdfgo.ImageProcess, error) {
|
||||||
|
key := pdfImageProcessKey{image: source, space: space, softMask: softMask}
|
||||||
|
if process, found := c.imageProcesses[key]; found {
|
||||||
|
return process, nil
|
||||||
|
}
|
||||||
|
device := &pdfgo.ColorantDevice{Space: space}
|
||||||
|
var process *pdfgo.ImageProcess
|
||||||
|
var err error
|
||||||
|
if components := c.images[source]; components != nil {
|
||||||
|
process, err = components.PrepareProcess(device, space, softMask)
|
||||||
|
} else {
|
||||||
|
process, err = source.DecodeProcessContext(importer.ctx, device, space, softMask)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if c.imageProcesses == nil {
|
||||||
|
c.imageProcesses = make(map[pdfImageProcessKey]*pdfgo.ImageProcess)
|
||||||
|
}
|
||||||
|
c.imageProcesses[key] = process
|
||||||
|
return process, nil
|
||||||
|
}
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
// 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 (
|
|
||||||
"image"
|
|
||||||
"image/color"
|
|
||||||
|
|
||||||
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
|
|
||||||
"github.com/xiaoqidun/pdfgo"
|
|
||||||
)
|
|
||||||
|
|
||||||
// pdfImageContentKey 区分编码内容、渲染意图、原生输出空间、模板填充色及有损采样尺寸
|
|
||||||
type pdfImageContentKey struct {
|
|
||||||
data [32]byte
|
|
||||||
intent pdfgo.Name
|
|
||||||
process pdfgo.Name
|
|
||||||
tint color.NRGBA64
|
|
||||||
size image.Point
|
|
||||||
}
|
|
||||||
|
|
||||||
// pdfImageContent 保存不依赖间接资源的图像描述及已注册编号
|
|
||||||
type pdfImageContent struct {
|
|
||||||
source *pdfgo.Image
|
|
||||||
id string
|
|
||||||
}
|
|
||||||
|
|
||||||
// imageContentKey 对无间接依赖的小图像生成内容键,超大图像仍沿用流缓存
|
|
||||||
// 入参: source 图像描述, key 流缓存键
|
|
||||||
// 返回: pdfImageContentKey 内容键, bool 是否可复用, error 取消错误
|
|
||||||
func (p *pdfImporter) imageContentKey(source *pdfgo.Image, key pdfImageKey) (pdfImageContentKey, bool, error) {
|
|
||||||
digest, reusable, err := codecpdf.ImageDigest(p.ctx, source)
|
|
||||||
if !reusable {
|
|
||||||
return pdfImageContentKey{}, reusable, err
|
|
||||||
}
|
|
||||||
return pdfImageContentKey{data: digest, intent: key.intent, process: key.process, tint: key.tint, size: key.size}, true, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// matches 核对完整编码与有效图像属性,不以摘要相同代替内容相同
|
|
||||||
// 入参: source 待复用图像
|
|
||||||
// 返回: bool 是否可使用同一图像资源
|
|
||||||
func (c pdfImageContent) matches(source *pdfgo.Image) bool {
|
|
||||||
return codecpdf.SameImage(c.source, source)
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
// 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"
|
|
||||||
"image"
|
|
||||||
|
|
||||||
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
|
|
||||||
"github.com/xiaoqidun/pdfgo"
|
|
||||||
)
|
|
||||||
|
|
||||||
// imageProcessModel 选择与页面原生过程色一致的图像输出,已等价的备用色沿用现有编码路径
|
|
||||||
// 入参: source 源图像
|
|
||||||
// 返回: pdfgo.Name 原生输出空间,无需重映射时为空, error 色料解析错误
|
|
||||||
func (p *pdfImporter) imageProcessModel(source *pdfgo.Image) (pdfgo.Name, error) {
|
|
||||||
space := p.compositeSpace
|
|
||||||
if source.ImageMask || !space.Device() {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
colorants, err := source.ColorantSpace()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
process := colorants.PrepareProcess(&pdfgo.ColorantDevice{Space: space}, space, false)
|
|
||||||
if process == nil || process.Mask() == ([4]bool{}) || process.AlternateEquivalent() {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
return space.Model, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// imageProcess 复用源分量并保留原始分辨率,颜色变换随编码逐像素执行
|
|
||||||
// 入参: source 源图像, model 原生设备空间
|
|
||||||
// 返回: image.Image 只读图像, error 解码或分量映射错误
|
|
||||||
func (p *pdfImporter) imageProcess(source *pdfgo.Image, model pdfgo.Name) (image.Image, error) {
|
|
||||||
cache := p.compositingCache()
|
|
||||||
space := &pdfgo.ColorSpace{Model: model}
|
|
||||||
process, err := cache.imageProcess(p, source, space, false)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if process == nil {
|
|
||||||
return nil, fmt.Errorf("invalid native PDF image process space %s", model)
|
|
||||||
}
|
|
||||||
return &codecpdf.NativeImage{Source: process, Space: space}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// imageProcess 复用同组原生采样,仅在需要备用颜色时保留完整分量
|
|
||||||
// 入参: importer 导入器, source 源图像, space 组混合空间, softMask 是否用于软蒙版
|
|
||||||
// 返回: *pdfgo.ImageProcess 原生采样,需要备用色时为nil, error 解码或布局错误
|
|
||||||
func (c *pdfCompositeCache) imageProcess(importer *pdfImporter, source *pdfgo.Image, space *pdfgo.ColorSpace, softMask bool) (*pdfgo.ImageProcess, error) {
|
|
||||||
key := pdfImageProcessKey{image: source, space: space, softMask: softMask}
|
|
||||||
if process, found := c.imageProcesses[key]; found {
|
|
||||||
return process, nil
|
|
||||||
}
|
|
||||||
device := &pdfgo.ColorantDevice{Space: space}
|
|
||||||
var process *pdfgo.ImageProcess
|
|
||||||
var err error
|
|
||||||
if components := c.images[source]; components != nil {
|
|
||||||
process, err = components.PrepareProcess(device, space, softMask)
|
|
||||||
} else {
|
|
||||||
process, err = source.DecodeProcessContext(importer.ctx, device, space, softMask)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if c.imageProcesses == nil {
|
|
||||||
c.imageProcesses = make(map[pdfImageProcessKey]*pdfgo.ImageProcess)
|
|
||||||
}
|
|
||||||
c.imageProcesses[key] = process
|
|
||||||
return process, nil
|
|
||||||
}
|
|
||||||
@@ -16,6 +16,7 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -36,6 +37,32 @@ type pdfEmbeddedFile struct {
|
|||||||
data []byte
|
data []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// documentAttachments 导入文档名称树中的附件,与注解及动作引用共用附件资源
|
||||||
|
// 入参: ctx 取消上下文
|
||||||
|
// 返回: error 名称树、附件读取或写入错误
|
||||||
|
func (p *pdfImporter) documentAttachments(ctx context.Context) error {
|
||||||
|
return p.reader.WalkEmbeddedFiles(ctx, func(_ string, file pdfgo.FileSpecification) error {
|
||||||
|
if file.Embedded == nil {
|
||||||
|
if p.warning == nil {
|
||||||
|
return fmt.Errorf("PDF embedded file has no embedded data")
|
||||||
|
}
|
||||||
|
p.warning(pdfgo.Diagnostic{Message: "PDF embedded file without embedded data ignored"})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
data, err := file.Embedded.DecodeContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
var unsupported *pdfgo.UnsupportedError
|
||||||
|
if p.warning != nil && errors.As(err, &unsupported) {
|
||||||
|
p.warning(pdfgo.Diagnostic{Message: "PDF document attachment not transferred to OFD: " + err.Error()})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = p.attachmentFile(file.Name, data)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// launchLinkAction 保留文件打开目标,不执行应用程序或猜测平台启动参数
|
// launchLinkAction 保留文件打开目标,不执行应用程序或猜测平台启动参数
|
||||||
// 入参: object 启动动作, strict 是否禁止启动语义损失
|
// 入参: object 启动动作, strict 是否禁止启动语义损失
|
||||||
// 返回: *Action 附件动作, error 解析、取消或无法表达的语义
|
// 返回: *Action 附件动作, error 解析、取消或无法表达的语义
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
// 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 (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"image"
|
|
||||||
"math"
|
|
||||||
|
|
||||||
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
|
|
||||||
"github.com/xiaoqidun/pdfgo"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
@@ -15,8 +15,13 @@
|
|||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
|
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
|
||||||
"github.com/xiaoqidun/pdfgo"
|
"github.com/xiaoqidun/pdfgo"
|
||||||
@@ -68,12 +73,10 @@ func (p *pdfImporter) destinationBounds(page *pdfgo.Page) (Box, error) {
|
|||||||
matrix: matrix, pageBox: page.CropBox, pageWidth: width, pageHeight: height,
|
matrix: matrix, pageBox: page.CropBox, pageWidth: width, pageHeight: height,
|
||||||
rasterDPI: p.rasterDPI, fontIDs: make(map[*pdfgo.Font]string),
|
rasterDPI: p.rasterDPI, fontIDs: make(map[*pdfgo.Font]string),
|
||||||
resolveFile: p.resolveFile, resolveReference: p.resolveReference,
|
resolveFile: p.resolveFile, resolveReference: p.resolveReference,
|
||||||
referenceReaders: make(map[[32]byte]*pdfgo.Reader),
|
references: &codecpdf.ReferenceCache{},
|
||||||
referenceStreams: make(map[*pdfgo.Stream]*pdfgo.Reader),
|
halftones: p.halftones, halftoneWarnings: make(map[*pdfgo.Halftone]bool),
|
||||||
referencePages: make(map[pdfReferencePageKey]*pdfgo.Page),
|
|
||||||
halftones: p.halftones, halftoneWarnings: make(map[*pdfgo.Halftone]bool),
|
|
||||||
}
|
}
|
||||||
defer local.closeReferenceReaders()
|
defer local.references.Close()
|
||||||
if err := local.compositePage(nil, func(visitor pdfgo.Visitor) error {
|
if err := local.compositePage(nil, func(visitor pdfgo.Visitor) error {
|
||||||
return p.reader.WalkPage(p.ctx, page, visitor)
|
return p.reader.WalkPage(p.ctx, page, visitor)
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
@@ -123,3 +126,102 @@ func (p *pdfImporter) destinationBounds(page *pdfgo.Page) (Box, error) {
|
|||||||
p.destinationBoxes[page.Reference] = box
|
p.destinationBoxes[page.Reference] = box
|
||||||
return box, nil
|
return box, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// outlines 按先序批量转换目录,保留层级、展开状态和动作顺序
|
||||||
|
// 入参: ctx 取消上下文
|
||||||
|
// 返回: error 严格模式转换错误或取消错误
|
||||||
|
func (p *pdfImporter) outlines(ctx context.Context) error {
|
||||||
|
count := 0
|
||||||
|
data, err := encodeOFDXMLContext(ctx, func(x *ofdXML) {
|
||||||
|
x.root("Outlines", nil)
|
||||||
|
depth := 0
|
||||||
|
err := p.reader.WalkOutlines(ctx, func(level int, item pdfgo.OutlineItem) error {
|
||||||
|
actions, err := p.outlineActions(ctx, item)
|
||||||
|
if err != nil {
|
||||||
|
if p.warning == nil || ctx.Err() != nil || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) || errors.Is(err, errPDFMediaRead) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.warning(pdfgo.Diagnostic{Message: fmt.Sprintf("PDF outline %q actions not transferred to OFD: %v", item.Title, err)})
|
||||||
|
actions = nil
|
||||||
|
}
|
||||||
|
for depth > level {
|
||||||
|
x.end("OutlineElem")
|
||||||
|
depth--
|
||||||
|
}
|
||||||
|
attrs := ofdAttrs{{Name: xml.Name{Local: "Title"}, Value: item.Title}}
|
||||||
|
attrs.add("Expanded", strconv.FormatBool(item.Expanded))
|
||||||
|
x.start("OutlineElem", attrs)
|
||||||
|
x.actions(actions)
|
||||||
|
depth++
|
||||||
|
count++
|
||||||
|
return x.err
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
x.err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for depth > 0 {
|
||||||
|
x.end("OutlineElem")
|
||||||
|
depth--
|
||||||
|
}
|
||||||
|
x.end("Outlines")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
if p.warning == nil || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) || errors.Is(err, errPDFMediaRead) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.warning(pdfgo.Diagnostic{Message: fmt.Sprintf("PDF outlines not transferred to OFD: %v", err)})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if count > 0 {
|
||||||
|
p.editor.outlines = bytes.TrimPrefix(data, []byte(xml.Header))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// outlineActions 转换目录目标及动作链,不为无目标节点补充跳转
|
||||||
|
// 入参: ctx 取消上下文, item PDF目录节点
|
||||||
|
// 返回: []Action OFD动作序列, error 动作或取消错误
|
||||||
|
func (p *pdfImporter) outlineActions(ctx context.Context, item pdfgo.OutlineItem) ([]Action, error) {
|
||||||
|
object, err := p.reader.Resolve(item.Action)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dest, err := p.reader.Resolve(item.Destination)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if dest != nil {
|
||||||
|
if object != nil {
|
||||||
|
return nil, fmt.Errorf("PDF outline has both destination and action")
|
||||||
|
}
|
||||||
|
object = pdfgo.Dictionary{"S": pdfgo.Name("GoTo"), "D": dest}
|
||||||
|
}
|
||||||
|
currentPage := -1
|
||||||
|
var actions []Action
|
||||||
|
err = p.reader.WalkActions(ctx, object, func(info pdfgo.ActionInfo) error {
|
||||||
|
converted, err := p.linkActions(pdfgo.Annotation{Dictionary: pdfgo.Dictionary{"A": info.Dictionary}}, p.warning == nil, ¤tPage)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, errPDFMediaRead) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var unsupported *pdfgo.UnsupportedError
|
||||||
|
if p.warning != nil && (errors.As(err, &unsupported) || errors.Is(err, pdfgo.ErrInvalidDestination) || errors.Is(err, pdfgo.ErrInvalidAction)) {
|
||||||
|
p.warning(pdfgo.Diagnostic{Message: fmt.Sprintf("PDF outline action not transferred to OFD: %v", err)})
|
||||||
|
currentPage = -1
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if converted != nil {
|
||||||
|
actions = append(actions, converted...)
|
||||||
|
} else {
|
||||||
|
currentPage = -1
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return actions, err
|
||||||
|
}
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
// 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"
|
|
||||||
"context"
|
|
||||||
"encoding/xml"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/xiaoqidun/pdfgo"
|
|
||||||
)
|
|
||||||
|
|
||||||
// outlines 按先序批量转换目录,保留层级、展开状态和动作顺序
|
|
||||||
// 入参: ctx 取消上下文
|
|
||||||
// 返回: error 严格模式转换错误或取消错误
|
|
||||||
func (p *pdfImporter) outlines(ctx context.Context) error {
|
|
||||||
count := 0
|
|
||||||
data, err := encodeOFDXMLContext(ctx, func(x *ofdXML) {
|
|
||||||
x.root("Outlines", nil)
|
|
||||||
depth := 0
|
|
||||||
err := p.reader.WalkOutlines(ctx, func(level int, item pdfgo.OutlineItem) error {
|
|
||||||
actions, err := p.outlineActions(ctx, item)
|
|
||||||
if err != nil {
|
|
||||||
if p.warning == nil || ctx.Err() != nil || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) || errors.Is(err, errPDFMediaRead) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p.warning(pdfgo.Diagnostic{Message: fmt.Sprintf("PDF outline %q actions not transferred to OFD: %v", item.Title, err)})
|
|
||||||
actions = nil
|
|
||||||
}
|
|
||||||
for depth > level {
|
|
||||||
x.end("OutlineElem")
|
|
||||||
depth--
|
|
||||||
}
|
|
||||||
attrs := ofdAttrs{{Name: xml.Name{Local: "Title"}, Value: item.Title}}
|
|
||||||
attrs.add("Expanded", strconv.FormatBool(item.Expanded))
|
|
||||||
x.start("OutlineElem", attrs)
|
|
||||||
x.actions(actions)
|
|
||||||
depth++
|
|
||||||
count++
|
|
||||||
return x.err
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
x.err = err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for depth > 0 {
|
|
||||||
x.end("OutlineElem")
|
|
||||||
depth--
|
|
||||||
}
|
|
||||||
x.end("Outlines")
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
if ctx.Err() != nil {
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
if p.warning == nil || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) || errors.Is(err, errPDFMediaRead) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p.warning(pdfgo.Diagnostic{Message: fmt.Sprintf("PDF outlines not transferred to OFD: %v", err)})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if count > 0 {
|
|
||||||
p.editor.outlines = bytes.TrimPrefix(data, []byte(xml.Header))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// outlineActions 转换目录目标及动作链,不为无目标节点补充跳转
|
|
||||||
// 入参: ctx 取消上下文, item PDF目录节点
|
|
||||||
// 返回: []Action OFD动作序列, error 动作或取消错误
|
|
||||||
func (p *pdfImporter) outlineActions(ctx context.Context, item pdfgo.OutlineItem) ([]Action, error) {
|
|
||||||
object, err := p.reader.Resolve(item.Action)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
dest, err := p.reader.Resolve(item.Destination)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if dest != nil {
|
|
||||||
if object != nil {
|
|
||||||
return nil, fmt.Errorf("PDF outline has both destination and action")
|
|
||||||
}
|
|
||||||
object = pdfgo.Dictionary{"S": pdfgo.Name("GoTo"), "D": dest}
|
|
||||||
}
|
|
||||||
currentPage := -1
|
|
||||||
var actions []Action
|
|
||||||
err = p.reader.WalkActions(ctx, object, func(info pdfgo.ActionInfo) error {
|
|
||||||
converted, err := p.linkActions(pdfgo.Annotation{Dictionary: pdfgo.Dictionary{"A": info.Dictionary}}, p.warning == nil, ¤tPage)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, errPDFMediaRead) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var unsupported *pdfgo.UnsupportedError
|
|
||||||
if p.warning != nil && (errors.As(err, &unsupported) || errors.Is(err, pdfgo.ErrInvalidDestination) || errors.Is(err, pdfgo.ErrInvalidAction)) {
|
|
||||||
p.warning(pdfgo.Diagnostic{Message: fmt.Sprintf("PDF outline action not transferred to OFD: %v", err)})
|
|
||||||
currentPage = -1
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if converted != nil {
|
|
||||||
actions = append(actions, converted...)
|
|
||||||
} else {
|
|
||||||
currentPage = -1
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
return actions, err
|
|
||||||
}
|
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
@@ -127,3 +128,91 @@ func pdfPatternCellCost(value reflect.Value, budget, depth int) int {
|
|||||||
}
|
}
|
||||||
return cost
|
return cost
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildTilingPattern 保留PDF图案单元与页面坐标之间的仿射关系
|
||||||
|
// 入参: source 平铺图案, base 无色图案基色
|
||||||
|
// 返回: *Pattern OFD图案, error 转换错误
|
||||||
|
func (p *pdfImporter) buildTilingPattern(source *pdfgo.TilingPattern, base pdfgo.Paint) (*Pattern, error) {
|
||||||
|
const unit = 25.4 / 72
|
||||||
|
box := source.BBox
|
||||||
|
width, height := box.XMax-box.XMin, box.YMax-box.YMin
|
||||||
|
xstep, ystep := math.Abs(source.XStep), math.Abs(source.YStep)
|
||||||
|
if width <= 0 || height <= 0 || !finite(xstep) || !finite(ystep) || xstep == 0 || ystep == 0 {
|
||||||
|
return nil, fmt.Errorf("invalid PDF tiling pattern dimensions")
|
||||||
|
}
|
||||||
|
localToPDF := pdfgo.Matrix{1 / unit, 0, 0, -1 / unit, box.XMin, box.YMax}
|
||||||
|
m := p.matrix.Mul(source.Matrix).Mul(localToPDF)
|
||||||
|
pattern := &Pattern{Width: math.Min(width, xstep) * unit, Height: math.Min(height, ystep) * unit, XStep: xstep * unit, YStep: ystep * unit, RelativeTo: "Page", CTM: pdfNumbers(m[:]...)}
|
||||||
|
cell := *p
|
||||||
|
cell.matrix = pdfgo.Matrix{unit, 0, 0, -unit, -box.XMin * unit, box.YMax * unit}
|
||||||
|
cell.pageWidth, cell.pageHeight = width*unit, height*unit
|
||||||
|
cell.rasterDPI *= m.MaxScale()
|
||||||
|
if !finite(cell.rasterDPI) || cell.rasterDPI < 0 {
|
||||||
|
return nil, fmt.Errorf("invalid PDF tiling pattern raster scale")
|
||||||
|
}
|
||||||
|
cell.objects, cell.pendingPath = nil, nil
|
||||||
|
if err := source.Walk(p.ctx, base, cell.visitor()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := cell.flushPath(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
objects, err := cell.objects.data(p.ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pattern.CellContent.Objects = objects
|
||||||
|
if len(objects) != 0 && (width > xstep || height > ystep) {
|
||||||
|
columns, rows := math.Ceil(width/xstep), math.Ceil(height/ystep)
|
||||||
|
if !finite(columns*rows) || columns*rows >= float64(uint(^uint(0)>>1)+1) {
|
||||||
|
return nil, fmt.Errorf("PDF tiling pattern instance count overflow")
|
||||||
|
}
|
||||||
|
bounds := make([]Box, len(objects))
|
||||||
|
for index, object := range objects {
|
||||||
|
boundary := ""
|
||||||
|
if object.Type == "PathObject" && object.PathObject.Stroke != nil && !*object.PathObject.Stroke {
|
||||||
|
boundary = object.PathObject.Boundary
|
||||||
|
} else if object.Type == "ImageObject" && object.ImageObject.Border == nil {
|
||||||
|
boundary = object.ImageObject.Boundary
|
||||||
|
}
|
||||||
|
if boundary != "" {
|
||||||
|
var err error
|
||||||
|
bounds[index], err = ParseBox(boundary)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
left, right, top, bottom := pdfPatternInstanceRange(columns, rows, bounds, pattern.Width, pattern.Height, xstep*unit, ystep*unit)
|
||||||
|
var expanded pdfObjectBuffer
|
||||||
|
for row := top; row <= bottom; row++ {
|
||||||
|
for column := left; column <= right; column++ {
|
||||||
|
if err := p.ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dx, dy := float64(column)*xstep*unit, float64(row)*ystep*unit
|
||||||
|
for index, object := range objects {
|
||||||
|
if index%pdfObjectChunkLimit == 0 {
|
||||||
|
if err := p.ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b := bounds[index]
|
||||||
|
if b.W > 0 && b.H > 0 && (b.X+dx+b.W < 0 || b.Y+dy+b.H < 0 || b.X+dx > pattern.Width || b.Y+dy > pattern.Height) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
translated, err := transformEditorObject(object, dx, dy, 1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
expanded.append(translated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pattern.CellContent.Objects, err = expanded.data(p.ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pattern, nil
|
||||||
|
}
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
// 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"
|
|
||||||
"context"
|
|
||||||
"crypto/sha256"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/xiaoqidun/pdfgo"
|
|
||||||
)
|
|
||||||
|
|
||||||
// pdfReferencePageKey 区分目标文档、索引和文本标签,避免重复遍历页面树
|
|
||||||
type pdfReferencePageKey struct {
|
|
||||||
reader *pdfgo.Reader
|
|
||||||
index int64
|
|
||||||
label string
|
|
||||||
labeled bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// referencePage 解析引用目标,目标不可用时按PDF标准保留代理内容
|
|
||||||
// 入参: ctx 取消上下文, source 来源文档, reference 引用目标
|
|
||||||
// 返回: *pdfgo.Page 目标页面,不可用时为空, error 取消或调用方解析错误
|
|
||||||
func (p *pdfImporter) referencePage(ctx context.Context, source *pdfgo.Reader, reference pdfgo.ReferenceXObject) (*pdfgo.Page, error) {
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if p.resolveReference != nil {
|
|
||||||
return p.resolveReference(ctx, source, reference)
|
|
||||||
}
|
|
||||||
reader := p.referenceStreams[reference.File.Embedded]
|
|
||||||
if reader == nil {
|
|
||||||
if reference.File.Embedded == nil && p.resolveFile == nil {
|
|
||||||
return p.referenceProxy(ctx, fmt.Errorf("external reference file unavailable"))
|
|
||||||
}
|
|
||||||
data, err := source.ReadFileData(ctx, reference.File, p.resolveFile)
|
|
||||||
if err != nil {
|
|
||||||
return p.referenceProxy(ctx, err)
|
|
||||||
}
|
|
||||||
key := sha256.Sum256(data)
|
|
||||||
reader = p.referenceReaders[key]
|
|
||||||
if reader == nil {
|
|
||||||
reader, err = pdfgo.NewReaderWithOptions(bytes.NewReader(data), int64(len(data)), pdfgo.ReaderOptions{Warning: p.warning})
|
|
||||||
if err != nil {
|
|
||||||
return p.referenceProxy(ctx, err)
|
|
||||||
}
|
|
||||||
p.referenceReaders[key] = reader
|
|
||||||
}
|
|
||||||
if reference.File.Embedded != nil {
|
|
||||||
p.referenceStreams[reference.File.Embedded] = reader
|
|
||||||
}
|
|
||||||
}
|
|
||||||
key := pdfReferencePageKey{reader: reader, index: reference.PageIndex}
|
|
||||||
if reference.PageLabel != nil {
|
|
||||||
key.index, key.label, key.labeled = 0, *reference.PageLabel, true
|
|
||||||
}
|
|
||||||
if page := p.referencePages[key]; page != nil {
|
|
||||||
return page, ctx.Err()
|
|
||||||
}
|
|
||||||
page, err := reader.ResolveReferencePage(ctx, reference)
|
|
||||||
if err != nil {
|
|
||||||
return p.referenceProxy(ctx, err)
|
|
||||||
}
|
|
||||||
p.referencePages[key] = page
|
|
||||||
return page, ctx.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
// referenceProxy 记录目标不可用原因,不将合法的代理绘制视为转换失败
|
|
||||||
// 入参: ctx 取消上下文, cause 目标不可用原因
|
|
||||||
// 返回: *pdfgo.Page 空目标, error 取消错误
|
|
||||||
func (p *pdfImporter) referenceProxy(ctx context.Context, cause error) (*pdfgo.Page, error) {
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if errors.Is(cause, context.Canceled) || errors.Is(cause, context.DeadlineExceeded) {
|
|
||||||
return nil, cause
|
|
||||||
}
|
|
||||||
if p.warning != nil {
|
|
||||||
p.warning(pdfgo.Diagnostic{Message: "reference XObject proxy retained: " + cause.Error()})
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// closeReferenceReaders 释放本次转换打开的引用文档,不关闭调用方提供的阅读器
|
|
||||||
func (p *pdfImporter) closeReferenceReaders() {
|
|
||||||
for _, reader := range p.referenceReaders {
|
|
||||||
reader.Close()
|
|
||||||
}
|
|
||||||
p.referenceReaders, p.referenceStreams = nil, nil
|
|
||||||
p.referencePages = nil
|
|
||||||
}
|
|
||||||
@@ -20,12 +20,32 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
"mime"
|
"mime"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
|
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
|
||||||
"github.com/xiaoqidun/pdfgo"
|
"github.com/xiaoqidun/pdfgo"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// pdfRenditionImport 保存可转换事件中的屏幕关联,首次遇到呈现动作时建立
|
||||||
|
type pdfRenditionImport struct {
|
||||||
|
actions map[pdfgo.Reference][]pdfgo.Dictionary
|
||||||
|
media map[pdfgo.Reference][]pdfRenditionResource
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfRenditionResource 保存屏幕候选媒体及其独立控制标识
|
||||||
|
type pdfRenditionResource struct {
|
||||||
|
ID string
|
||||||
|
Kind string
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfRenditionAssignment 区分同一屏幕的呈现定义,重复触发不重复登记
|
||||||
|
type pdfRenditionAssignment struct {
|
||||||
|
Screen pdfgo.Reference
|
||||||
|
Rendition uintptr
|
||||||
|
}
|
||||||
|
|
||||||
// screenAnnotation 转换屏幕注解的音视频呈现,不忽略必须遵守的播放器约束
|
// screenAnnotation 转换屏幕注解的音视频呈现,不忽略必须遵守的播放器约束
|
||||||
// 入参: ctx 取消上下文, page PDF页面, annotation 屏幕注解, strict 严格检查开关
|
// 入参: ctx 取消上下文, page PDF页面, annotation 屏幕注解, strict 严格检查开关
|
||||||
// 返回: error 动作、媒体或外观错误
|
// 返回: error 动作、媒体或外观错误
|
||||||
@@ -259,3 +279,175 @@ func (p *pdfImporter) nativeRendition(rendition *pdfgo.Rendition) (Action, strin
|
|||||||
result.Movie = &Movie{Operator: "Play"}
|
result.Movie = &Movie{Operator: "Play"}
|
||||||
return result, "Video", true, nil
|
return result, "Video", true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// renditionControls 生成屏幕关联的控制序列,切换时不停止即将重新播放的资源
|
||||||
|
// 入参: screen 屏幕引用, operation 停止、暂停或恢复操作, playing 即将播放的资源,控制动作为空, strict 是否禁止语义损失
|
||||||
|
// 返回: []Action 控制序列,nil表示无法转换, error 资源或严格模式错误
|
||||||
|
func (p *pdfImporter) renditionControls(screen pdfgo.Reference, operation int64, playing string, strict bool) ([]Action, error) {
|
||||||
|
if p.renditions == nil {
|
||||||
|
p.renditions = &pdfRenditionImport{actions: make(map[pdfgo.Reference][]pdfgo.Dictionary), media: make(map[pdfgo.Reference][]pdfRenditionResource)}
|
||||||
|
p.renditions.err = p.collectRenditions()
|
||||||
|
}
|
||||||
|
if err := p.ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if p.renditions.err != nil {
|
||||||
|
return nil, p.renditionActionLoss("screen associations cannot be determined: "+p.renditions.err.Error(), strict)
|
||||||
|
}
|
||||||
|
resources, err := p.renditionResources(screen, strict)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, resource := range resources {
|
||||||
|
if resource.Kind != "Video" && (playing == "" || len(resources) > 1) {
|
||||||
|
return nil, p.renditionActionLoss("audio rendition control cannot be represented by OFD Sound", strict)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
operator := map[int64]string{1: "Stop", 2: "Pause", 3: "Resume"}[operation]
|
||||||
|
actions := make([]Action, 0, len(resources))
|
||||||
|
for _, resource := range resources {
|
||||||
|
if resource.ID != playing {
|
||||||
|
actions = append(actions, Action{Event: "CLICK", Movie: &Movie{ResourceID: resource.ID, Operator: operator}})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return actions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// renditionResources 解析一个屏幕的可用媒体,文件数据仅在该屏幕参与转换时读取
|
||||||
|
// 入参: screen 屏幕引用, strict 是否禁止忽略无效动作
|
||||||
|
// 返回: []pdfRenditionResource 候选媒体, error 解析、资源或取消错误
|
||||||
|
func (p *pdfImporter) renditionResources(screen pdfgo.Reference, strict bool) ([]pdfRenditionResource, error) {
|
||||||
|
if resources, exists := p.renditions.media[screen]; exists {
|
||||||
|
return resources, nil
|
||||||
|
}
|
||||||
|
resources := make([]pdfRenditionResource, 0)
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
for _, object := range p.renditions.actions[screen] {
|
||||||
|
playback, err := p.reader.ReadRenditionPlayback(p.ctx, object)
|
||||||
|
if err != nil {
|
||||||
|
if !strict && errors.Is(err, pdfgo.ErrInvalidAction) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, exists := p.pages[playback.Page]; !exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
selected, err := playback.Rendition.Select(p.ctx, func(candidate *pdfgo.Rendition) (bool, error) {
|
||||||
|
_, _, viable, err := p.nativeRendition(candidate)
|
||||||
|
return viable, err
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if selected == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_, kind, _, err := p.nativeRendition(selected)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
id, err := p.renditionResource(selected, kind, playback)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if id != "" && !seen[id] {
|
||||||
|
seen[id] = true
|
||||||
|
resources = append(resources, pdfRenditionResource{ID: id, Kind: kind})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.renditions.media[screen] = resources
|
||||||
|
return resources, p.ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// collectRenditions 索引可转换事件中的媒体关联,不解码媒体或执行脚本
|
||||||
|
// 返回: error 动作结构、引用或取消错误
|
||||||
|
func (p *pdfImporter) collectRenditions() error {
|
||||||
|
seen := make(map[pdfRenditionAssignment]bool)
|
||||||
|
collect := func(object pdfgo.Object) error {
|
||||||
|
return p.reader.WalkActions(p.ctx, object, func(info pdfgo.ActionInfo) error {
|
||||||
|
if info.Type != "Rendition" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
value, err := p.reader.Resolve(info.Dictionary["OP"])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if value != pdfgo.Integer(0) && value != pdfgo.Integer(4) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
screen, ok := info.Dictionary["AN"].(pdfgo.Reference)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
value, err = p.reader.Resolve(info.Dictionary["R"])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r, ok := value.(pdfgo.Dictionary)
|
||||||
|
if !ok || r == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
key := pdfRenditionAssignment{Screen: screen, Rendition: reflect.ValueOf(r).Pointer()}
|
||||||
|
if !seen[key] {
|
||||||
|
seen[key] = true
|
||||||
|
p.renditions.actions[screen] = append(p.renditions.actions[screen], info.Dictionary)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
opened, err := p.reader.ReadOpenAction(p.ctx)
|
||||||
|
if err != nil && !errors.Is(err, pdfgo.ErrInvalidDestination) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if opened.Action != nil {
|
||||||
|
if err := collect(opened.Action); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := p.reader.WalkOutlines(p.ctx, func(_ int, item pdfgo.OutlineItem) error {
|
||||||
|
return collect(item.Action)
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, page := range p.pageOrder {
|
||||||
|
triggers, err := p.reader.ReadPageTriggers(p.ctx, page)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, trigger := range triggers {
|
||||||
|
if trigger.Event == "O" {
|
||||||
|
if err := collect(trigger.Action); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
annotations, err := page.AnnotationsContext(p.ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, annotation := range annotations {
|
||||||
|
flags, err := p.reader.ReadAnnotationFlags(annotation)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if flags&64 == 0 && (annotation.Subtype == "Screen" || annotation.Subtype == "Link") {
|
||||||
|
if err := collect(annotation.Dictionary["A"]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
triggers, err := p.reader.ReadAnnotationTriggers(p.ctx, annotation)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, trigger := range triggers {
|
||||||
|
if trigger.Event == "PO" || trigger.Event == "U" && flags&64 == 0 && annotation.Subtype != "Widget" {
|
||||||
|
if err := collect(trigger.Action); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return p.ctx.Err()
|
||||||
|
}
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
// 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 (
|
|
||||||
"errors"
|
|
||||||
"reflect"
|
|
||||||
|
|
||||||
"github.com/xiaoqidun/pdfgo"
|
|
||||||
)
|
|
||||||
|
|
||||||
// pdfRenditionImport 保存可转换事件中的屏幕关联,首次遇到呈现动作时建立
|
|
||||||
type pdfRenditionImport struct {
|
|
||||||
actions map[pdfgo.Reference][]pdfgo.Dictionary
|
|
||||||
media map[pdfgo.Reference][]pdfRenditionResource
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
// pdfRenditionResource 保存屏幕候选媒体及其独立控制标识
|
|
||||||
type pdfRenditionResource struct {
|
|
||||||
ID string
|
|
||||||
Kind string
|
|
||||||
}
|
|
||||||
|
|
||||||
// pdfRenditionAssignment 区分同一屏幕的呈现定义,重复触发不重复登记
|
|
||||||
type pdfRenditionAssignment struct {
|
|
||||||
Screen pdfgo.Reference
|
|
||||||
Rendition uintptr
|
|
||||||
}
|
|
||||||
|
|
||||||
// renditionControls 生成屏幕关联的控制序列,切换时不停止即将重新播放的资源
|
|
||||||
// 入参: screen 屏幕引用, operation 停止、暂停或恢复操作, playing 即将播放的资源,控制动作为空, strict 是否禁止语义损失
|
|
||||||
// 返回: []Action 控制序列,nil表示无法转换, error 资源或严格模式错误
|
|
||||||
func (p *pdfImporter) renditionControls(screen pdfgo.Reference, operation int64, playing string, strict bool) ([]Action, error) {
|
|
||||||
if p.renditions == nil {
|
|
||||||
p.renditions = &pdfRenditionImport{actions: make(map[pdfgo.Reference][]pdfgo.Dictionary), media: make(map[pdfgo.Reference][]pdfRenditionResource)}
|
|
||||||
p.renditions.err = p.collectRenditions()
|
|
||||||
}
|
|
||||||
if err := p.ctx.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if p.renditions.err != nil {
|
|
||||||
return nil, p.renditionActionLoss("screen associations cannot be determined: "+p.renditions.err.Error(), strict)
|
|
||||||
}
|
|
||||||
resources, err := p.renditionResources(screen, strict)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for _, resource := range resources {
|
|
||||||
if resource.Kind != "Video" && (playing == "" || len(resources) > 1) {
|
|
||||||
return nil, p.renditionActionLoss("audio rendition control cannot be represented by OFD Sound", strict)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
operator := map[int64]string{1: "Stop", 2: "Pause", 3: "Resume"}[operation]
|
|
||||||
actions := make([]Action, 0, len(resources))
|
|
||||||
for _, resource := range resources {
|
|
||||||
if resource.ID != playing {
|
|
||||||
actions = append(actions, Action{Event: "CLICK", Movie: &Movie{ResourceID: resource.ID, Operator: operator}})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return actions, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// renditionResources 解析一个屏幕的可用媒体,文件数据仅在该屏幕参与转换时读取
|
|
||||||
// 入参: screen 屏幕引用, strict 是否禁止忽略无效动作
|
|
||||||
// 返回: []pdfRenditionResource 候选媒体, error 解析、资源或取消错误
|
|
||||||
func (p *pdfImporter) renditionResources(screen pdfgo.Reference, strict bool) ([]pdfRenditionResource, error) {
|
|
||||||
if resources, exists := p.renditions.media[screen]; exists {
|
|
||||||
return resources, nil
|
|
||||||
}
|
|
||||||
resources := make([]pdfRenditionResource, 0)
|
|
||||||
seen := make(map[string]bool)
|
|
||||||
for _, object := range p.renditions.actions[screen] {
|
|
||||||
playback, err := p.reader.ReadRenditionPlayback(p.ctx, object)
|
|
||||||
if err != nil {
|
|
||||||
if !strict && errors.Is(err, pdfgo.ErrInvalidAction) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if _, exists := p.pages[playback.Page]; !exists {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
selected, err := playback.Rendition.Select(p.ctx, func(candidate *pdfgo.Rendition) (bool, error) {
|
|
||||||
_, _, viable, err := p.nativeRendition(candidate)
|
|
||||||
return viable, err
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if selected == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
_, kind, _, err := p.nativeRendition(selected)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
id, err := p.renditionResource(selected, kind, playback)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if id != "" && !seen[id] {
|
|
||||||
seen[id] = true
|
|
||||||
resources = append(resources, pdfRenditionResource{ID: id, Kind: kind})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p.renditions.media[screen] = resources
|
|
||||||
return resources, p.ctx.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
// collectRenditions 索引可转换事件中的媒体关联,不解码媒体或执行脚本
|
|
||||||
// 返回: error 动作结构、引用或取消错误
|
|
||||||
func (p *pdfImporter) collectRenditions() error {
|
|
||||||
seen := make(map[pdfRenditionAssignment]bool)
|
|
||||||
collect := func(object pdfgo.Object) error {
|
|
||||||
return p.reader.WalkActions(p.ctx, object, func(info pdfgo.ActionInfo) error {
|
|
||||||
if info.Type != "Rendition" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
value, err := p.reader.Resolve(info.Dictionary["OP"])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if value != pdfgo.Integer(0) && value != pdfgo.Integer(4) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
screen, ok := info.Dictionary["AN"].(pdfgo.Reference)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
value, err = p.reader.Resolve(info.Dictionary["R"])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
r, ok := value.(pdfgo.Dictionary)
|
|
||||||
if !ok || r == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
key := pdfRenditionAssignment{Screen: screen, Rendition: reflect.ValueOf(r).Pointer()}
|
|
||||||
if !seen[key] {
|
|
||||||
seen[key] = true
|
|
||||||
p.renditions.actions[screen] = append(p.renditions.actions[screen], info.Dictionary)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
opened, err := p.reader.ReadOpenAction(p.ctx)
|
|
||||||
if err != nil && !errors.Is(err, pdfgo.ErrInvalidDestination) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if opened.Action != nil {
|
|
||||||
if err := collect(opened.Action); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := p.reader.WalkOutlines(p.ctx, func(_ int, item pdfgo.OutlineItem) error {
|
|
||||||
return collect(item.Action)
|
|
||||||
}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, page := range p.pageOrder {
|
|
||||||
triggers, err := p.reader.ReadPageTriggers(p.ctx, page)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, trigger := range triggers {
|
|
||||||
if trigger.Event == "O" {
|
|
||||||
if err := collect(trigger.Action); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
annotations, err := page.AnnotationsContext(p.ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, annotation := range annotations {
|
|
||||||
flags, err := p.reader.ReadAnnotationFlags(annotation)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if flags&64 == 0 && (annotation.Subtype == "Screen" || annotation.Subtype == "Link") {
|
|
||||||
if err := collect(annotation.Dictionary["A"]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
triggers, err := p.reader.ReadAnnotationTriggers(p.ctx, annotation)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, trigger := range triggers {
|
|
||||||
if trigger.Event == "PO" || trigger.Event == "U" && flags&64 == 0 && annotation.Subtype != "Widget" {
|
|
||||||
if err := collect(trigger.Action); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return p.ctx.Err()
|
|
||||||
}
|
|
||||||
+102
-12
@@ -18,6 +18,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"math"
|
"math"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
|
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
|
||||||
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
|
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
|
||||||
@@ -725,19 +726,108 @@ func pdfShadingClippedNode(node pdfCompositeNode, box pdfgo.Rectangle, matrix pd
|
|||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
// pdfShadingPosition 计算渐变参数,未延伸区域及不存在非负圆半径的位置不着色
|
// color 保留设备色彩分量并复用文档颜色空间
|
||||||
// 入参: paint 渐变画刷, point 页面坐标
|
// 入参: paint PDF颜色与不透明度
|
||||||
// 返回: float64 归一化参数, bool 是否着色
|
// 返回: *FillColor OFD颜色
|
||||||
func pdfShadingPosition(paint pdfgo.Paint, point pdfgo.Point) (float64, bool) {
|
func (p *pdfImporter) color(paint pdfgo.Paint) *FillColor {
|
||||||
var geometry pdfgo.GradientPosition
|
alpha := int(math.Round(paint.Alpha * 255))
|
||||||
var err error
|
if paint.CMYK != nil && (paint.Space == nil || !paint.Space.Calibrated()) {
|
||||||
if g := paint.Axial; g != nil {
|
if p.cmykSpace == "" {
|
||||||
geometry, err = g.PreparePosition()
|
p.cmykSpace = p.editor.nextID()
|
||||||
} else {
|
p.editor.resources = append(p.editor.resources, editorResource{space: &ColorSpace{ID: p.cmykSpace, Type: "CMYK", BitsPerComponent: 16}})
|
||||||
geometry, err = paint.Radial.PreparePosition()
|
}
|
||||||
|
v := paint.CMYK
|
||||||
|
return &FillColor{ColorSpace: p.cmykSpace, Value: pdfNumbers(math.Round(v[0]*65535), math.Round(v[1]*65535), math.Round(v[2]*65535), math.Round(v[3]*65535)), Alpha: &alpha}
|
||||||
}
|
}
|
||||||
|
return &FillColor{Value: pdfNumbers(math.Round(paint.RGB[0]*255), math.Round(paint.RGB[1]*255), math.Round(paint.RGB[2]*255)), Alpha: &alpha}
|
||||||
|
}
|
||||||
|
|
||||||
|
// paintColor 将页面渐变转换为对象局部坐标
|
||||||
|
// 入参: paint PDF画刷, box 对象边界
|
||||||
|
// 返回: *FillColor OFD颜色或渐变, error 图案转换错误
|
||||||
|
func (p *pdfImporter) paintColor(paint pdfgo.Paint, box Box) (*FillColor, error) {
|
||||||
|
if err := p.gradientError(paint); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if pdfGradientPattern(paint) {
|
||||||
|
return p.gradientPattern(paint, box)
|
||||||
|
}
|
||||||
|
if paint.Mesh != nil {
|
||||||
|
return p.meshColor(paint, box)
|
||||||
|
}
|
||||||
|
color := p.color(paint)
|
||||||
|
if paint.Tiling != nil {
|
||||||
|
pattern, err := p.tilingPattern(paint.Tiling, paint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
color.Pattern = pattern
|
||||||
|
return color, nil
|
||||||
|
}
|
||||||
|
if paint.Radial != nil {
|
||||||
|
gradient := paint.Radial
|
||||||
|
matrix := p.matrix
|
||||||
|
if gradient.Matrix != (pdfgo.Matrix{}) {
|
||||||
|
matrix = matrix.Mul(gradient.Matrix)
|
||||||
|
}
|
||||||
|
start, end := matrix.Apply(gradient.Start), matrix.Apply(gradient.End)
|
||||||
|
xx, yy, xy := matrix[0]*matrix[0]+matrix[2]*matrix[2], matrix[1]*matrix[1]+matrix[3]*matrix[3], matrix[0]*matrix[1]+matrix[2]*matrix[3]
|
||||||
|
major := (xx + yy + math.Hypot(xx-yy, 2*xy)) / 2
|
||||||
|
determinant := matrix[0]*matrix[3] - matrix[1]*matrix[2]
|
||||||
|
if major <= 0 || determinant == 0 {
|
||||||
|
return nil, fmt.Errorf("invalid PDF radial gradient matrix")
|
||||||
|
}
|
||||||
|
scale := math.Sqrt(major)
|
||||||
|
eccentricity := math.Sqrt(math.Max(0, 1-determinant*determinant/(major*major)))
|
||||||
|
angle := math.Atan2(2*xy, xx-yy) * 90 / math.Pi
|
||||||
|
extend := 0
|
||||||
|
if gradient.Extend[0] {
|
||||||
|
extend |= 1
|
||||||
|
}
|
||||||
|
if gradient.Extend[1] {
|
||||||
|
extend |= 2
|
||||||
|
}
|
||||||
|
shading := &RadialShd{StartPoint: pdfNumbers(start.X-box.X, start.Y-box.Y), EndPoint: pdfNumbers(end.X-box.X, end.Y-box.Y), StartRadius: gradient.StartRadius * scale, EndRadius: gradient.EndRadius * scale, Eccentricity: eccentricity, Angle: angle, Extend: strconv.Itoa(extend)}
|
||||||
|
var err error
|
||||||
|
shading.Segment, err = p.gradientSegments(gradient.Stops, gradient.Space, gradient.Intent)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
color.Value = ""
|
||||||
|
color.RadialShd = shading
|
||||||
|
return color, nil
|
||||||
|
}
|
||||||
|
if paint.Axial == nil {
|
||||||
|
return color, nil
|
||||||
|
}
|
||||||
|
gradient := paint.Axial
|
||||||
|
start := p.matrix.Apply(gradient.Start)
|
||||||
|
inverse, ok := p.matrix.Inverse()
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid PDF gradient matrix")
|
||||||
|
}
|
||||||
|
dx, dy := gradient.End.X-gradient.Start.X, gradient.End.Y-gradient.Start.Y
|
||||||
|
gx, gy := inverse[0]*dx+inverse[1]*dy, inverse[2]*dx+inverse[3]*dy
|
||||||
|
length := gx*gx + gy*gy
|
||||||
|
if length == 0 {
|
||||||
|
return nil, fmt.Errorf("invalid PDF axial gradient axis")
|
||||||
|
}
|
||||||
|
factor := (dx*dx + dy*dy) / length
|
||||||
|
end := pdfgo.Point{X: start.X + gx*factor, Y: start.Y + gy*factor}
|
||||||
|
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)}
|
||||||
|
var err error
|
||||||
|
shading.Segment, err = p.gradientSegments(gradient.Stops, gradient.Space, gradient.Intent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, false
|
return nil, err
|
||||||
}
|
}
|
||||||
return geometry.PositionAt(point)
|
color.Value = ""
|
||||||
|
color.AxialShd = shading
|
||||||
|
return color, nil
|
||||||
}
|
}
|
||||||
@@ -15,223 +15,15 @@
|
|||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
|
||||||
"image/color"
|
|
||||||
"io"
|
|
||||||
"math"
|
"math"
|
||||||
"reflect"
|
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
|
|
||||||
"github.com/xiaoqidun/pdfgo"
|
"github.com/xiaoqidun/pdfgo"
|
||||||
)
|
)
|
||||||
|
|
||||||
// pdfImageKey 区分图像流、渲染意图、原生输出空间、模板填充色及采样尺寸
|
|
||||||
type pdfImageKey struct {
|
|
||||||
stream *pdfgo.Stream
|
|
||||||
intent pdfgo.Name
|
|
||||||
process pdfgo.Name
|
|
||||||
tint color.NRGBA64
|
|
||||||
size image.Point
|
|
||||||
}
|
|
||||||
|
|
||||||
// pdfImageResource 保存图像的有效颜色空间及对应资源,避免跨资源重映射复用
|
|
||||||
type pdfImageResource struct {
|
|
||||||
space pdfgo.Object
|
|
||||||
id string
|
|
||||||
}
|
|
||||||
|
|
||||||
// image 转换图像样本,不将整页或其他对象栅格化
|
|
||||||
// 有损编码复用已解码像素,候选容量超限时沿用原编码路径
|
|
||||||
// 入参: mark PDF图像绘制信息
|
|
||||||
// 返回: error 错误信息
|
|
||||||
func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
|
|
||||||
if err := p.ctx.Err(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if mark.Style.BlendMode != "" && mark.Style.BlendMode != "Normal" && mark.Style.BlendMode != "Compatible" {
|
|
||||||
return &pdfgo.UnsupportedError{Feature: "image blend mode " + string(mark.Style.BlendMode)}
|
|
||||||
}
|
|
||||||
if err := p.flushPath(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if mark.Image.ImageMask && (mark.Style.Fill.Axial != nil || mark.Style.Fill.Radial != nil || mark.Style.Fill.Function != nil || mark.Style.Fill.Mesh != nil || mark.Style.Fill.Shading != nil) {
|
|
||||||
if p.warning != nil {
|
|
||||||
return p.compositeRegion(nil, pdfCompositeNode{image: &mark}, &pdfgo.ColorSpace{Model: "DeviceRGB"}, false)
|
|
||||||
}
|
|
||||||
return &pdfgo.UnsupportedError{Feature: "gradient stencil image"}
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
mark.Style, err = p.maskStyle(mark.Style)
|
|
||||||
if err != nil {
|
|
||||||
if p.rasterMaskAllowed(err) {
|
|
||||||
mask := mark.Style.SoftMask
|
|
||||||
mark.Style.SoftMask = nil
|
|
||||||
return p.rasterGroup(pdfgo.GroupMark{Alpha: 1, SoftMask: mask}, func(v pdfgo.Visitor) error { return v.Image(mark) })
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if mark.Style.FillOverprint {
|
|
||||||
return &pdfgo.UnsupportedError{Feature: "image color separation overprint"}
|
|
||||||
}
|
|
||||||
source := mark.Image
|
|
||||||
key := pdfImageKey{stream: source.Stream, intent: source.Intent}
|
|
||||||
key.process, err = p.imageProcessModel(source)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !source.ImageMask && p.compression.Mode == CompressionLossy {
|
|
||||||
m := p.matrix.Mul(mark.Matrix)
|
|
||||||
key.size = compressionImageSize(math.Hypot(m[0], m[1]), math.Hypot(m[2], m[3]), p.compression.ImageDPI())
|
|
||||||
if key.size.X >= source.Width || key.size.Y >= source.Height {
|
|
||||||
key.size = image.Point{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if source.ImageMask {
|
|
||||||
fill := mark.Style.Fill.RGB
|
|
||||||
if values := mark.Style.Fill.CMYK; values != nil {
|
|
||||||
space := &pdfgo.ColorSpace{Model: "DeviceCMYK"}
|
|
||||||
fill, err = space.RGB(values[:], mark.Style.RenderingIntent)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
key.tint = color.NRGBA64{
|
|
||||||
R: uint16(math.Round(fill[0] * 65535)),
|
|
||||||
G: uint16(math.Round(fill[1] * 65535)),
|
|
||||||
B: uint16(math.Round(fill[2] * 65535)),
|
|
||||||
A: 65535,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
id := ""
|
|
||||||
for _, resource := range p.imageIDs[key] {
|
|
||||||
if reflect.DeepEqual(resource.space, source.ColorSpace) {
|
|
||||||
id = resource.id
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
streamCached := id != ""
|
|
||||||
var contentKey pdfImageContentKey
|
|
||||||
cacheable := false
|
|
||||||
if id == "" {
|
|
||||||
contentKey, cacheable, err = p.imageContentKey(source, key)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if cacheable {
|
|
||||||
if p.imageContents == nil {
|
|
||||||
p.imageContents = &renderCache[pdfImageContentKey, pdfImageContent]{limit: 8 << 20}
|
|
||||||
}
|
|
||||||
if cached, found := p.imageContents.get(contentKey); found && cached.matches(source) {
|
|
||||||
id = cached.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if id == "" {
|
|
||||||
var jbig2Original, jpegOriginal []byte
|
|
||||||
var decoded image.Image
|
|
||||||
if key.process != "" {
|
|
||||||
decoded, err = p.imageProcess(source, key.process)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if key.size != (image.Point{}) {
|
|
||||||
decoded, err = pdfgo.ResizeImage(p.ctx, decoded, key.size)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
jbig2Original, err = source.JBIG2FileContext(p.ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
jpegOriginal, err = source.JPEGFileContext(p.ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(jbig2Original) == 0 && len(jpegOriginal) == 0 && (source.ImageMask || key.size != (image.Point{})) {
|
|
||||||
decoded, err = source.DecodeImageSizeContext(p.ctx, key.size)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if source.ImageMask {
|
|
||||||
decoded = &codecpdf.StencilImage{Source: decoded, Fill: key.tint}
|
|
||||||
}
|
|
||||||
data := jbig2Original
|
|
||||||
if len(data) == 0 {
|
|
||||||
data = jpegOriginal
|
|
||||||
}
|
|
||||||
optimized := false
|
|
||||||
if len(data) == 0 && decoded != nil && !source.ImageMask && p.compression.Mode == CompressionLossy {
|
|
||||||
data, err = pdfgo.EncodeImageResource(p.ctx, decoded, p.compression)
|
|
||||||
if err != nil && !errors.Is(err, io.ErrShortBuffer) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
optimized = err == nil
|
|
||||||
}
|
|
||||||
if len(data) == 0 {
|
|
||||||
var encoded bytes.Buffer
|
|
||||||
if decoded != nil {
|
|
||||||
err = pdfgo.EncodePNG(p.ctx, &encoded, decoded)
|
|
||||||
} else {
|
|
||||||
err = source.EncodePNGContext(p.ctx, &encoded)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
data = encoded.Bytes()
|
|
||||||
}
|
|
||||||
if !source.ImageMask && p.compression.Mode == CompressionLossy {
|
|
||||||
if !optimized {
|
|
||||||
data, err = pdfgo.OptimizeImageResource(p.ctx, data, p.compression, key.size)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
decoded = nil
|
|
||||||
}
|
|
||||||
id, err = p.editor.addDecodedImageContext(p.ctx, data, decoded)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if cacheable {
|
|
||||||
p.imageContents.put(contentKey, pdfImageContent{source: source, id: id}, len(source.Stream.Data)+65536)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !streamCached {
|
|
||||||
if p.imageIDs == nil {
|
|
||||||
p.imageIDs = map[pdfImageKey][]pdfImageResource{}
|
|
||||||
}
|
|
||||||
p.imageIDs[key] = append(p.imageIDs[key], pdfImageResource{space: source.ColorSpace, id: id})
|
|
||||||
}
|
|
||||||
return p.appendImage(mark, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// appendImage 定位已注册的图像资源,保留透明度与裁剪
|
|
||||||
// 入参: mark 图像绘制信息, id 资源编号
|
|
||||||
// 返回: error 裁剪错误
|
|
||||||
func (p *pdfImporter) appendImage(mark pdfgo.ImageMark, id string) error {
|
|
||||||
m := p.matrix.Mul(mark.Matrix).Mul(pdfgo.Matrix{1, 0, 0, -1, 0, 1})
|
|
||||||
box := pdfBounds([]pdfgo.Point{m.Apply(pdfgo.Point{}), m.Apply(pdfgo.Point{X: 1}), m.Apply(pdfgo.Point{Y: 1}), m.Apply(pdfgo.Point{X: 1, Y: 1})})
|
|
||||||
alpha := int(math.Round(mark.Style.Fill.Alpha * 255))
|
|
||||||
clips, err := p.clips(mark.Style.Clips, box)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
object := ImageObject{Boundary: pdfBoundary(box), ResourceID: id, CTM: pdfNumbers(m[0], m[1], m[2], m[3], m[4]-box.X, m[5]-box.Y), Alpha: &alpha, Clips: clips}
|
|
||||||
p.appendObject(GraphicObject{Type: "ImageObject", ImageObject: object})
|
|
||||||
p.report.ImageObjects++
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// text 保留字形及逐字基线,外部字体优先复用同名内嵌资源
|
// text 保留字形及逐字基线,外部字体优先复用同名内嵌资源
|
||||||
// 入参: mark PDF文字绘制信息
|
// 入参: mark PDF文字绘制信息
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
@@ -632,3 +424,90 @@ func (p *pdfImporter) degenerateText(mark pdfgo.TextMark) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// disjointText 检查实际字形轮廓是否互不相交,允许无重叠挖空文字直接保留
|
||||||
|
// 入参: node 隐式文字组
|
||||||
|
// 返回: bool 是否互不相交, error 轮廓编译错误
|
||||||
|
func (p *pdfImporter) disjointText(node pdfCompositeNode) (bool, error) {
|
||||||
|
cache := p.compositingCache()
|
||||||
|
if result, found := cache.textDisjoint[node.group]; found {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
geometry, err := p.renderer.Geometry()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
var inverse pdfgo.Matrix
|
||||||
|
var boxes []Box
|
||||||
|
for _, child := range node.children {
|
||||||
|
mark := child.text
|
||||||
|
if mark == nil {
|
||||||
|
mark = child.glyph
|
||||||
|
}
|
||||||
|
if mark == nil || len(mark.Glyphs) != 1 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if inverse == (pdfgo.Matrix{}) {
|
||||||
|
var ok bool
|
||||||
|
inverse, ok = p.matrix.Mul(mark.Matrix).Inverse()
|
||||||
|
if !ok {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scene, _, err := p.compileGroup(func(local *pdfImporter) error {
|
||||||
|
if child.group != nil {
|
||||||
|
return child.coverage(local, false)
|
||||||
|
}
|
||||||
|
_, fill, stroke := child.style()
|
||||||
|
if fill {
|
||||||
|
if err := child.coverage(local, false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if stroke {
|
||||||
|
return child.coverage(local, true)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if scene == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var box Box
|
||||||
|
for _, command := range scene.Commands {
|
||||||
|
path, err := rasterCommandGeometry(geometry, command)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
path, err = geometry.Transform(path, MatrixFromValues([6]float64(inverse)))
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
bounds, err := geometry.Bounds(path)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
box = unionTextBox(box, bounds)
|
||||||
|
}
|
||||||
|
if box.W <= 0 || box.H <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, previous := range boxes {
|
||||||
|
if box.X < previous.X+previous.W && previous.X < box.X+box.W && box.Y < previous.Y+previous.H && previous.Y < box.Y+box.H {
|
||||||
|
if cache.textDisjoint == nil {
|
||||||
|
cache.textDisjoint = make(map[*pdfgo.GroupMark]bool)
|
||||||
|
}
|
||||||
|
cache.textDisjoint[node.group] = false
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
boxes = append(boxes, box)
|
||||||
|
}
|
||||||
|
if cache.textDisjoint == nil {
|
||||||
|
cache.textDisjoint = make(map[*pdfgo.GroupMark]bool)
|
||||||
|
}
|
||||||
|
cache.textDisjoint[node.group] = true
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package ofdgo
|
|
||||||
|
|
||||||
import "github.com/xiaoqidun/pdfgo"
|
|
||||||
|
|
||||||
// disjointText 检查实际字形轮廓是否互不相交,允许无重叠挖空文字直接保留
|
|
||||||
// 入参: node 隐式文字组
|
|
||||||
// 返回: bool 是否互不相交, error 轮廓编译错误
|
|
||||||
func (p *pdfImporter) disjointText(node pdfCompositeNode) (bool, error) {
|
|
||||||
cache := p.compositingCache()
|
|
||||||
if result, found := cache.textDisjoint[node.group]; found {
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
geometry, err := p.renderer.Geometry()
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
var inverse pdfgo.Matrix
|
|
||||||
var boxes []Box
|
|
||||||
for _, child := range node.children {
|
|
||||||
mark := child.text
|
|
||||||
if mark == nil {
|
|
||||||
mark = child.glyph
|
|
||||||
}
|
|
||||||
if mark == nil || len(mark.Glyphs) != 1 {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
if inverse == (pdfgo.Matrix{}) {
|
|
||||||
var ok bool
|
|
||||||
inverse, ok = p.matrix.Mul(mark.Matrix).Inverse()
|
|
||||||
if !ok {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
scene, _, err := p.compileGroup(func(local *pdfImporter) error {
|
|
||||||
if child.group != nil {
|
|
||||||
return child.coverage(local, false)
|
|
||||||
}
|
|
||||||
_, fill, stroke := child.style()
|
|
||||||
if fill {
|
|
||||||
if err := child.coverage(local, false); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if stroke {
|
|
||||||
return child.coverage(local, true)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if scene == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var box Box
|
|
||||||
for _, command := range scene.Commands {
|
|
||||||
path, err := rasterCommandGeometry(geometry, command)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
path, err = geometry.Transform(path, MatrixFromValues([6]float64(inverse)))
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
bounds, err := geometry.Bounds(path)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
box = unionTextBox(box, bounds)
|
|
||||||
}
|
|
||||||
if box.W <= 0 || box.H <= 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, previous := range boxes {
|
|
||||||
if box.X < previous.X+previous.W && previous.X < box.X+box.W && box.Y < previous.Y+previous.H && previous.Y < box.Y+box.H {
|
|
||||||
if cache.textDisjoint == nil {
|
|
||||||
cache.textDisjoint = make(map[*pdfgo.GroupMark]bool)
|
|
||||||
}
|
|
||||||
cache.textDisjoint[node.group] = false
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
boxes = append(boxes, box)
|
|
||||||
}
|
|
||||||
if cache.textDisjoint == nil {
|
|
||||||
cache.textDisjoint = make(map[*pdfgo.GroupMark]bool)
|
|
||||||
}
|
|
||||||
cache.textDisjoint[node.group] = true
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
// 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 (
|
|
||||||
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
|
|
||||||
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
|
|
||||||
"github.com/xiaoqidun/pdfgo"
|
|
||||||
)
|
|
||||||
|
|
||||||
// pdfGlyphPath 将OFD字形几何交给PDF路径适配器转换
|
|
||||||
// 入参: outline 字形轮廓, matrix 字形到页面的变换
|
|
||||||
// 返回: pdfgo.Path 页面路径, error 非字体轮廓指令
|
|
||||||
func pdfGlyphPath(outline GeometryPath, matrix pdfgo.Matrix) (pdfgo.Path, error) {
|
|
||||||
return codecpdf.GlyphPath(basegeom.Path(outline), matrix)
|
|
||||||
}
|
|
||||||
@@ -217,3 +217,43 @@ func (p *pdfImporter) transferOpaque(node pdfCompositeNode) (bool, error) {
|
|||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// halftone 检查连续色调转换,避免将打印网屏误用于屏幕显示
|
||||||
|
// 入参: style 图形状态
|
||||||
|
// 返回: error 严格模式无法保留设备网屏
|
||||||
|
func (p *pdfImporter) halftone(style pdfgo.Style) error {
|
||||||
|
h := style.Halftone
|
||||||
|
message := "PDF continuous-tone appearance retained; device halftone not transferred to OFD"
|
||||||
|
if h == nil || h.Type == 0 {
|
||||||
|
if style.HalftoneOrigin == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
message = "PDF continuous-tone appearance retained; device halftone origin not transferred to OFD"
|
||||||
|
}
|
||||||
|
if p.warning == nil {
|
||||||
|
return &pdfgo.UnsupportedError{Feature: "device halftone conversion"}
|
||||||
|
}
|
||||||
|
if p.halftoneWarnings == nil {
|
||||||
|
p.halftoneWarnings = make(map[*pdfgo.Halftone]bool)
|
||||||
|
}
|
||||||
|
if !p.halftoneWarnings[h] {
|
||||||
|
p.halftoneWarnings[h] = true
|
||||||
|
p.warning(pdfgo.Diagnostic{Message: message})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfOpaqueBlack 判断不受底层分色影响的全黑设备色
|
||||||
|
// 入参: paint PDF颜色与不透明度
|
||||||
|
// 返回: bool 是否为不透明全黑CMYK颜色
|
||||||
|
func pdfOpaqueBlack(paint pdfgo.Paint) bool {
|
||||||
|
return paint.Alpha == 1 && paint.CMYK != nil && paint.CMYK[3] == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfOverprintNeedsSeparation 判断彩色CMYK套印是否依赖分色输出
|
||||||
|
// 入参: paint PDF颜色与不透明度
|
||||||
|
// 返回: bool 是否需要保留分色语义
|
||||||
|
func pdfOverprintNeedsSeparation(paint pdfgo.Paint) bool {
|
||||||
|
_, process := paint.Process.CMYKMask()
|
||||||
|
return (paint.CMYK != nil || process) && !pdfOpaqueBlack(paint)
|
||||||
|
}
|
||||||
@@ -17,6 +17,8 @@ package ofdgo
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"maps"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"github.com/tdewolff/canvas"
|
"github.com/tdewolff/canvas"
|
||||||
@@ -106,6 +108,38 @@ func (n *pdfNavigation) addActions(renderer *Renderer, page int, source actionSo
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// addLifecycleActions 按事件归属收集打开动作,不受点击区域限制
|
||||||
|
// 入参: ctx 取消上下文, source 动作来源, page 页面索引,负值表示文档, bookmarks 书签, pageIndex 页面索引表, pages 页面数据
|
||||||
|
// 返回: error 目标或取消错误
|
||||||
|
func (n *pdfNavigation) addLifecycleActions(ctx context.Context, source actionSource, page int, bookmarks map[string]Dest, pageIndex map[string]int, pages []RenderDocumentPage) error {
|
||||||
|
event := "PO"
|
||||||
|
if page < 0 {
|
||||||
|
event = "DO"
|
||||||
|
}
|
||||||
|
for _, action := range source.Actions {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if action.Event != event || source.Graphic {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
value, err := n.actionTarget(action, page, source, bookmarks, pageIndex, pages)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if value == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if action.Event == "DO" {
|
||||||
|
n.Open = append(n.Open, *value)
|
||||||
|
} else {
|
||||||
|
n.PageOpen[page] = append(n.PageOpen[page], *value)
|
||||||
|
}
|
||||||
|
n.nativeWrite = true
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// partitionPDFActions 将新增动作区域与已有区域分离,使每个区域具有唯一动作序列
|
// partitionPDFActions 将新增动作区域与已有区域分离,使每个区域具有唯一动作序列
|
||||||
// 入参: ctx 取消上下文, renderer 渲染器, areas 已有分区, path 新动作区域, action 新动作
|
// 入参: ctx 取消上下文, renderer 渲染器, areas 已有分区, path 新动作区域, action 新动作
|
||||||
// 返回: []pdfActionArea 新分区, error 几何、复杂度或取消错误
|
// 返回: []pdfActionArea 新分区, error 几何、复杂度或取消错误
|
||||||
@@ -219,3 +253,33 @@ func pdfNavigationValues(actions []pdfNavigationAction, pages []pdfgo.Reference)
|
|||||||
}
|
}
|
||||||
return values
|
return values
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// writeAttachments 在页面绘制后读取附件,保留文件字节,不自动添加个人信息和时间
|
||||||
|
// 入参: ctx 取消上下文, options PDF写出配置
|
||||||
|
// 返回: error 附件读取或取消错误
|
||||||
|
func (n *pdfNavigation) writeAttachments(ctx context.Context, options *pdfgo.RewriteOptions) error {
|
||||||
|
if len(n.attachments) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
options.Attachments = make(map[string]*pdfgo.EmbeddedFile, len(n.attachments))
|
||||||
|
for _, id := range slices.Sorted(maps.Keys(n.attachments)) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
attachment := n.attachments[id]
|
||||||
|
input, err := n.source.openFile(n.source.ResPath(attachment.FileLoc))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read attachment %s: %w", id, err)
|
||||||
|
}
|
||||||
|
data, err := io.ReadAll(imageInput{ReadCloser: input, context: ctx})
|
||||||
|
closeErr := input.Close()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read attachment %s: %w", id, err)
|
||||||
|
}
|
||||||
|
if closeErr != nil {
|
||||||
|
return closeErr
|
||||||
|
}
|
||||||
|
options.Attachments[id] = &pdfgo.EmbeddedFile{Name: attachment.FileName(), Data: data}
|
||||||
|
}
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
// 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 (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"maps"
|
|
||||||
"slices"
|
|
||||||
|
|
||||||
"github.com/xiaoqidun/pdfgo"
|
|
||||||
)
|
|
||||||
|
|
||||||
// writeAttachments 在页面绘制后读取附件,保留文件字节,不自动添加个人信息和时间
|
|
||||||
// 入参: ctx 取消上下文, options PDF写出配置
|
|
||||||
// 返回: error 附件读取或取消错误
|
|
||||||
func (n *pdfNavigation) writeAttachments(ctx context.Context, options *pdfgo.RewriteOptions) error {
|
|
||||||
if len(n.attachments) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
options.Attachments = make(map[string]*pdfgo.EmbeddedFile, len(n.attachments))
|
|
||||||
for _, id := range slices.Sorted(maps.Keys(n.attachments)) {
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
attachment := n.attachments[id]
|
|
||||||
input, err := n.source.openFile(n.source.ResPath(attachment.FileLoc))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("read attachment %s: %w", id, err)
|
|
||||||
}
|
|
||||||
data, err := io.ReadAll(imageInput{ReadCloser: input, context: ctx})
|
|
||||||
closeErr := input.Close()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("read attachment %s: %w", id, err)
|
|
||||||
}
|
|
||||||
if closeErr != nil {
|
|
||||||
return closeErr
|
|
||||||
}
|
|
||||||
options.Attachments[id] = &pdfgo.EmbeddedFile{Name: attachment.FileName(), Data: data}
|
|
||||||
}
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
|
"io"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/tdewolff/canvas"
|
"github.com/tdewolff/canvas"
|
||||||
@@ -150,6 +151,34 @@ func (r *pdfRenderer) glyphPath(path *canvas.Path, matrix canvas.Matrix) *canvas
|
|||||||
return converted
|
return converted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// writePDF 传递后端资源记录及OFD导航修正,写出最终PDF
|
||||||
|
// 入参: ctx 取消上下文, data 后端PDF数据, writer 输出流, optimization 优化配置
|
||||||
|
// 返回: error 资源、压缩或写入错误
|
||||||
|
func (r *pdfRenderer) writePDF(ctx context.Context, data []byte, writer io.Writer, optimization pdfgo.OptimizeOptions) error {
|
||||||
|
if r.imageError != nil {
|
||||||
|
return r.imageError
|
||||||
|
}
|
||||||
|
options := codecpdf.WriteOptions{
|
||||||
|
Images: r.images,
|
||||||
|
Text: r.text,
|
||||||
|
ExactImages: r.exactImages,
|
||||||
|
ExactText: r.exactText,
|
||||||
|
Optimization: optimization,
|
||||||
|
}
|
||||||
|
if r.navigation != nil && r.navigation.nativeWrite {
|
||||||
|
options.Links = make(map[int][]*pdfgo.LinkRegion, len(r.navigation.Link))
|
||||||
|
for page, links := range r.navigation.Link {
|
||||||
|
regions := make([]*pdfgo.LinkRegion, len(links))
|
||||||
|
for i, link := range links {
|
||||||
|
regions[i] = link.Region
|
||||||
|
}
|
||||||
|
options.Links[page] = regions
|
||||||
|
}
|
||||||
|
options.PrepareRewrite = r.navigation.prepareRewrite
|
||||||
|
}
|
||||||
|
return codecpdf.Write(ctx, data, writer, options)
|
||||||
|
}
|
||||||
|
|
||||||
// newPDFNavigation 建立文档导航与输出页索引,不读取页面图元
|
// newPDFNavigation 建立文档导航与输出页索引,不读取页面图元
|
||||||
// 入参: renderer 渲染器, doc 文档结构, pages 输出页面描述
|
// 入参: renderer 渲染器, doc 文档结构, pages 输出页面描述
|
||||||
// 返回: *pdfNavigation 导航信息, error 资源或目标错误
|
// 返回: *pdfNavigation 导航信息, error 资源或目标错误
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
// 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 "context"
|
|
||||||
|
|
||||||
// addLifecycleActions 按事件归属收集打开动作,不受点击区域限制
|
|
||||||
// 入参: ctx 取消上下文, source 动作来源, page 页面索引,负值表示文档, bookmarks 书签, pageIndex 页面索引表, pages 页面数据
|
|
||||||
// 返回: error 目标或取消错误
|
|
||||||
func (n *pdfNavigation) addLifecycleActions(ctx context.Context, source actionSource, page int, bookmarks map[string]Dest, pageIndex map[string]int, pages []RenderDocumentPage) error {
|
|
||||||
event := "PO"
|
|
||||||
if page < 0 {
|
|
||||||
event = "DO"
|
|
||||||
}
|
|
||||||
for _, action := range source.Actions {
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if action.Event != event || source.Graphic {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
value, err := n.actionTarget(action, page, source, bookmarks, pageIndex, pages)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if value == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if action.Event == "DO" {
|
|
||||||
n.Open = append(n.Open, *value)
|
|
||||||
} else {
|
|
||||||
n.PageOpen[page] = append(n.PageOpen[page], *value)
|
|
||||||
}
|
|
||||||
n.nativeWrite = true
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -16,14 +16,37 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"maps"
|
"maps"
|
||||||
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/xiaoqidun/pdfgo"
|
"github.com/xiaoqidun/pdfgo"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// pdfVideoResource 共享视频内容及各播放区域,控制动作按资源关联
|
||||||
|
type pdfVideoResource struct {
|
||||||
|
Clip *pdfgo.MediaClip
|
||||||
|
Screens []*pdfVideoScreen
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfVideoScreen 保存播放区域的页面归属及呈现参数
|
||||||
|
type pdfVideoScreen struct {
|
||||||
|
Page int
|
||||||
|
Screen *pdfgo.ScreenAnnotation
|
||||||
|
Rendition *pdfgo.Rendition
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfMovieAction 保存视频控制及可选播放区域,待全部来源收集后关联
|
||||||
|
type pdfMovieAction struct {
|
||||||
|
Resource *pdfVideoResource
|
||||||
|
Target *pdfVideoScreen
|
||||||
|
Operation int
|
||||||
|
}
|
||||||
|
|
||||||
// mediaFile 复用同一包内文件的输出说明,不合并播放资源及窗口
|
// mediaFile 复用同一包内文件的输出说明,不合并播放资源及窗口
|
||||||
// 入参: media 音视频资源
|
// 入参: media 音视频资源
|
||||||
// 返回: *pdfgo.FileSpecification 待读取的共享文件
|
// 返回: *pdfgo.FileSpecification 待读取的共享文件
|
||||||
@@ -67,3 +90,198 @@ func (n *pdfNavigation) loadMediaFiles(ctx context.Context, files map[string]*pd
|
|||||||
}
|
}
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// soundAction 保留音量、循环及等待语义,PDF同步播放同时限制交互
|
||||||
|
// 入参: action OFD音频动作
|
||||||
|
// 返回: *pdfNavigationAction PDF音频动作, error 资源或参数错误
|
||||||
|
func (n *pdfNavigation) soundAction(action Sound) (*pdfNavigationAction, error) {
|
||||||
|
volume := 100
|
||||||
|
if action.Volume != nil {
|
||||||
|
volume = *action.Volume
|
||||||
|
}
|
||||||
|
if volume < 0 || volume > 100 {
|
||||||
|
return nil, fmt.Errorf("invalid sound volume")
|
||||||
|
}
|
||||||
|
file := n.audio[action.ResourceID]
|
||||||
|
if file == nil {
|
||||||
|
media, err := n.source.Media(action.ResourceID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if media.Type != "Audio" {
|
||||||
|
return nil, fmt.Errorf("sound resource is not audio: %s", action.ResourceID)
|
||||||
|
}
|
||||||
|
if n.audio == nil {
|
||||||
|
n.audio = make(map[string]*pdfgo.FileSpecification)
|
||||||
|
}
|
||||||
|
file = n.mediaFile(media)
|
||||||
|
n.audio[action.ResourceID] = file
|
||||||
|
}
|
||||||
|
return &pdfNavigationAction{Sound: &pdfgo.SoundAction{
|
||||||
|
Sound: pdfgo.Sound{File: file}, Volume: float64(volume) / 100,
|
||||||
|
Repeat: action.Repeat, Synchronous: action.Synchronous && !action.Repeat,
|
||||||
|
}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadAudio 在绘制后读取自描述音频文件,原始内容不作采样转换
|
||||||
|
// 入参: ctx 取消上下文
|
||||||
|
// 返回: error 资源读取或取消错误
|
||||||
|
func (n *pdfNavigation) loadAudio(ctx context.Context) error {
|
||||||
|
return n.loadMediaFiles(ctx, n.audio)
|
||||||
|
}
|
||||||
|
|
||||||
|
// movieAction 按OFD第12章选择内嵌区域或浮窗,控制动作不另建播放器
|
||||||
|
// 入参: action 视频动作, page 来源页索引,负值表示文档, source 动作来源, pages 输出页面
|
||||||
|
// 返回: *pdfNavigationAction 待关联视频动作, error 资源或参数错误
|
||||||
|
func (n *pdfNavigation) movieAction(action Movie, page int, source actionSource, pages []RenderDocumentPage) (*pdfNavigationAction, error) {
|
||||||
|
operation := 0
|
||||||
|
switch action.Operator {
|
||||||
|
case "", "Play":
|
||||||
|
case "Stop":
|
||||||
|
operation = 1
|
||||||
|
case "Pause":
|
||||||
|
operation = 2
|
||||||
|
case "Resume":
|
||||||
|
operation = 3
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid movie operator: %s", action.Operator)
|
||||||
|
}
|
||||||
|
resource := n.video[action.ResourceID]
|
||||||
|
if resource == nil {
|
||||||
|
media, err := n.source.Media(action.ResourceID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if media.Type != "Video" {
|
||||||
|
return nil, fmt.Errorf("movie resource is not video: %s", action.ResourceID)
|
||||||
|
}
|
||||||
|
resource = &pdfVideoResource{Clip: &pdfgo.MediaClip{
|
||||||
|
Subtype: "MCD", ContentType: pdfVideoContentType(media),
|
||||||
|
File: n.mediaFile(media),
|
||||||
|
}}
|
||||||
|
if n.video == nil {
|
||||||
|
n.video = make(map[string]*pdfVideoResource)
|
||||||
|
}
|
||||||
|
n.video[action.ResourceID] = resource
|
||||||
|
}
|
||||||
|
value := &pdfMovieAction{Resource: resource, Operation: operation}
|
||||||
|
if operation != 0 {
|
||||||
|
return &pdfNavigationAction{Movie: value}, nil
|
||||||
|
}
|
||||||
|
if len(pages) == 0 || page >= len(pages) {
|
||||||
|
return nil, fmt.Errorf("missing movie playback page")
|
||||||
|
}
|
||||||
|
var rect pdfgo.Rectangle
|
||||||
|
if source.Graphic {
|
||||||
|
if page < 0 || source.Box.W <= 0 || source.Box.H <= 0 {
|
||||||
|
return nil, fmt.Errorf("invalid movie playback region")
|
||||||
|
}
|
||||||
|
const scale = 72.0 / 25.4
|
||||||
|
box := source.Box
|
||||||
|
rect = pdfgo.Rectangle{XMin: box.X * scale, YMin: (pages[page].Box.H - box.Y - box.H) * scale, XMax: (box.X + box.W) * scale, YMax: (pages[page].Box.H - box.Y) * scale}
|
||||||
|
} else {
|
||||||
|
page = 0
|
||||||
|
}
|
||||||
|
for _, screen := range resource.Screens {
|
||||||
|
if screen.Page == page && screen.Screen.Rect == rect {
|
||||||
|
value.Target = screen
|
||||||
|
return &pdfNavigationAction{Movie: value}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
screen := &pdfVideoScreen{Page: page, Screen: &pdfgo.ScreenAnnotation{Rect: rect, Hidden: true}, Rendition: &pdfgo.Rendition{Subtype: "MR", Clip: resource.Clip}}
|
||||||
|
if !source.Graphic {
|
||||||
|
screen.Rendition.Screen = pdfgo.Dictionary{"MH": pdfgo.Dictionary{"W": pdfgo.Integer(0)}, "BE": pdfgo.Dictionary{"F": pdfgo.Dictionary{"D": pdfgo.Array{pdfgo.Integer(640), pdfgo.Integer(360)}, "R": pdfgo.Integer(1)}}}
|
||||||
|
}
|
||||||
|
resource.Screens = append(resource.Screens, screen)
|
||||||
|
value.Target = screen
|
||||||
|
return &pdfNavigationAction{Movie: value}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// values 将资源控制关联到全部播放区域,重新播放前停止同一资源的其他实例
|
||||||
|
// 返回: []pdfgo.NavigationAction 有序呈现动作
|
||||||
|
func (a *pdfMovieAction) values() []pdfgo.NavigationAction {
|
||||||
|
values := make([]pdfgo.NavigationAction, 0, len(a.Resource.Screens))
|
||||||
|
for _, screen := range a.Resource.Screens {
|
||||||
|
if screen == a.Target {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
operation := a.Operation
|
||||||
|
if operation == 0 {
|
||||||
|
operation = 1
|
||||||
|
}
|
||||||
|
values = append(values, pdfgo.NavigationAction{Rendition: &pdfgo.RenditionAction{Target: screen.Screen, Operation: operation}})
|
||||||
|
}
|
||||||
|
if a.Target != nil {
|
||||||
|
values = append(values, pdfgo.NavigationAction{Rendition: &pdfgo.RenditionAction{Target: a.Target.Screen, Rendition: a.Target.Rendition}})
|
||||||
|
}
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeVideo 在绘制后读取被播放的视频,原样嵌入并关联输出页面
|
||||||
|
// 入参: ctx 取消上下文, pages 输出页引用, options 替换配置
|
||||||
|
// 返回: error 资源读取或取消错误
|
||||||
|
func (n *pdfNavigation) writeVideo(ctx context.Context, pages []pdfgo.Reference, options *pdfgo.RewriteOptions) error {
|
||||||
|
files := make(map[string]*pdfgo.FileSpecification)
|
||||||
|
for _, id := range slices.Sorted(maps.Keys(n.video)) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
resource := n.video[id]
|
||||||
|
for _, screen := range resource.Screens {
|
||||||
|
if screen.Page < 0 || screen.Page >= len(pages) {
|
||||||
|
return fmt.Errorf("missing movie playback page")
|
||||||
|
}
|
||||||
|
if options.ScreenAnnotations == nil {
|
||||||
|
options.ScreenAnnotations = make(map[pdfgo.Reference][]*pdfgo.ScreenAnnotation)
|
||||||
|
}
|
||||||
|
ref := pages[screen.Page]
|
||||||
|
options.ScreenAnnotations[ref] = append(options.ScreenAnnotations[ref], screen.Screen)
|
||||||
|
files[id] = resource.Clip.File
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := n.loadMediaFiles(ctx, files); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, id := range slices.Sorted(maps.Keys(files)) {
|
||||||
|
clip := n.video[id].Clip
|
||||||
|
if clip.ContentType == "" {
|
||||||
|
kind := http.DetectContentType(clip.File.Embedded.Data)
|
||||||
|
if !strings.HasPrefix(kind, "video/") && kind != "application/ogg" {
|
||||||
|
return fmt.Errorf("cannot identify video content type: %s", id)
|
||||||
|
}
|
||||||
|
clip.ContentType = kind
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfVideoContentType 转换明确声明的视频格式,未知格式留待检查文件头
|
||||||
|
// 入参: media 视频资源
|
||||||
|
// 返回: string 媒体类型,无法确定时为空
|
||||||
|
func pdfVideoContentType(media MultiMedia) string {
|
||||||
|
format := strings.ToLower(strings.TrimSpace(media.Format))
|
||||||
|
if format == "" {
|
||||||
|
format = strings.TrimPrefix(strings.ToLower(path.Ext(media.MediaFile)), ".")
|
||||||
|
}
|
||||||
|
switch format {
|
||||||
|
case "mp4", "m4v":
|
||||||
|
return "video/mp4"
|
||||||
|
case "mpeg", "mpg":
|
||||||
|
return "video/mpeg"
|
||||||
|
case "mov", "qt":
|
||||||
|
return "video/quicktime"
|
||||||
|
case "webm":
|
||||||
|
return "video/webm"
|
||||||
|
case "ogv", "ogg":
|
||||||
|
return "video/ogg"
|
||||||
|
case "avi":
|
||||||
|
return "video/x-msvideo"
|
||||||
|
case "wmv":
|
||||||
|
return "video/x-ms-wmv"
|
||||||
|
case "3gp":
|
||||||
|
return "video/3gpp"
|
||||||
|
case "3g2":
|
||||||
|
return "video/3gpp2"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
// 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 (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"maps"
|
|
||||||
"net/http"
|
|
||||||
"path"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/xiaoqidun/pdfgo"
|
|
||||||
)
|
|
||||||
|
|
||||||
// pdfVideoResource 共享视频内容及各播放区域,控制动作按资源关联
|
|
||||||
type pdfVideoResource struct {
|
|
||||||
Clip *pdfgo.MediaClip
|
|
||||||
Screens []*pdfVideoScreen
|
|
||||||
}
|
|
||||||
|
|
||||||
// pdfVideoScreen 保存播放区域的页面归属及呈现参数
|
|
||||||
type pdfVideoScreen struct {
|
|
||||||
Page int
|
|
||||||
Screen *pdfgo.ScreenAnnotation
|
|
||||||
Rendition *pdfgo.Rendition
|
|
||||||
}
|
|
||||||
|
|
||||||
// pdfMovieAction 保存视频控制及可选播放区域,待全部来源收集后关联
|
|
||||||
type pdfMovieAction struct {
|
|
||||||
Resource *pdfVideoResource
|
|
||||||
Target *pdfVideoScreen
|
|
||||||
Operation int
|
|
||||||
}
|
|
||||||
|
|
||||||
// movieAction 按OFD第12章选择内嵌区域或浮窗,控制动作不另建播放器
|
|
||||||
// 入参: action 视频动作, page 来源页索引,负值表示文档, source 动作来源, pages 输出页面
|
|
||||||
// 返回: *pdfNavigationAction 待关联视频动作, error 资源或参数错误
|
|
||||||
func (n *pdfNavigation) movieAction(action Movie, page int, source actionSource, pages []RenderDocumentPage) (*pdfNavigationAction, error) {
|
|
||||||
operation := 0
|
|
||||||
switch action.Operator {
|
|
||||||
case "", "Play":
|
|
||||||
case "Stop":
|
|
||||||
operation = 1
|
|
||||||
case "Pause":
|
|
||||||
operation = 2
|
|
||||||
case "Resume":
|
|
||||||
operation = 3
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("invalid movie operator: %s", action.Operator)
|
|
||||||
}
|
|
||||||
resource := n.video[action.ResourceID]
|
|
||||||
if resource == nil {
|
|
||||||
media, err := n.source.Media(action.ResourceID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if media.Type != "Video" {
|
|
||||||
return nil, fmt.Errorf("movie resource is not video: %s", action.ResourceID)
|
|
||||||
}
|
|
||||||
resource = &pdfVideoResource{Clip: &pdfgo.MediaClip{
|
|
||||||
Subtype: "MCD", ContentType: pdfVideoContentType(media),
|
|
||||||
File: n.mediaFile(media),
|
|
||||||
}}
|
|
||||||
if n.video == nil {
|
|
||||||
n.video = make(map[string]*pdfVideoResource)
|
|
||||||
}
|
|
||||||
n.video[action.ResourceID] = resource
|
|
||||||
}
|
|
||||||
value := &pdfMovieAction{Resource: resource, Operation: operation}
|
|
||||||
if operation != 0 {
|
|
||||||
return &pdfNavigationAction{Movie: value}, nil
|
|
||||||
}
|
|
||||||
if len(pages) == 0 || page >= len(pages) {
|
|
||||||
return nil, fmt.Errorf("missing movie playback page")
|
|
||||||
}
|
|
||||||
var rect pdfgo.Rectangle
|
|
||||||
if source.Graphic {
|
|
||||||
if page < 0 || source.Box.W <= 0 || source.Box.H <= 0 {
|
|
||||||
return nil, fmt.Errorf("invalid movie playback region")
|
|
||||||
}
|
|
||||||
const scale = 72.0 / 25.4
|
|
||||||
box := source.Box
|
|
||||||
rect = pdfgo.Rectangle{XMin: box.X * scale, YMin: (pages[page].Box.H - box.Y - box.H) * scale, XMax: (box.X + box.W) * scale, YMax: (pages[page].Box.H - box.Y) * scale}
|
|
||||||
} else {
|
|
||||||
page = 0
|
|
||||||
}
|
|
||||||
for _, screen := range resource.Screens {
|
|
||||||
if screen.Page == page && screen.Screen.Rect == rect {
|
|
||||||
value.Target = screen
|
|
||||||
return &pdfNavigationAction{Movie: value}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
screen := &pdfVideoScreen{Page: page, Screen: &pdfgo.ScreenAnnotation{Rect: rect, Hidden: true}, Rendition: &pdfgo.Rendition{Subtype: "MR", Clip: resource.Clip}}
|
|
||||||
if !source.Graphic {
|
|
||||||
screen.Rendition.Screen = pdfgo.Dictionary{"MH": pdfgo.Dictionary{"W": pdfgo.Integer(0)}, "BE": pdfgo.Dictionary{"F": pdfgo.Dictionary{"D": pdfgo.Array{pdfgo.Integer(640), pdfgo.Integer(360)}, "R": pdfgo.Integer(1)}}}
|
|
||||||
}
|
|
||||||
resource.Screens = append(resource.Screens, screen)
|
|
||||||
value.Target = screen
|
|
||||||
return &pdfNavigationAction{Movie: value}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// values 将资源控制关联到全部播放区域,重新播放前停止同一资源的其他实例
|
|
||||||
// 返回: []pdfgo.NavigationAction 有序呈现动作
|
|
||||||
func (a *pdfMovieAction) values() []pdfgo.NavigationAction {
|
|
||||||
values := make([]pdfgo.NavigationAction, 0, len(a.Resource.Screens))
|
|
||||||
for _, screen := range a.Resource.Screens {
|
|
||||||
if screen == a.Target {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
operation := a.Operation
|
|
||||||
if operation == 0 {
|
|
||||||
operation = 1
|
|
||||||
}
|
|
||||||
values = append(values, pdfgo.NavigationAction{Rendition: &pdfgo.RenditionAction{Target: screen.Screen, Operation: operation}})
|
|
||||||
}
|
|
||||||
if a.Target != nil {
|
|
||||||
values = append(values, pdfgo.NavigationAction{Rendition: &pdfgo.RenditionAction{Target: a.Target.Screen, Rendition: a.Target.Rendition}})
|
|
||||||
}
|
|
||||||
return values
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeVideo 在绘制后读取被播放的视频,原样嵌入并关联输出页面
|
|
||||||
// 入参: ctx 取消上下文, pages 输出页引用, options 替换配置
|
|
||||||
// 返回: error 资源读取或取消错误
|
|
||||||
func (n *pdfNavigation) writeVideo(ctx context.Context, pages []pdfgo.Reference, options *pdfgo.RewriteOptions) error {
|
|
||||||
files := make(map[string]*pdfgo.FileSpecification)
|
|
||||||
for _, id := range slices.Sorted(maps.Keys(n.video)) {
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
resource := n.video[id]
|
|
||||||
for _, screen := range resource.Screens {
|
|
||||||
if screen.Page < 0 || screen.Page >= len(pages) {
|
|
||||||
return fmt.Errorf("missing movie playback page")
|
|
||||||
}
|
|
||||||
if options.ScreenAnnotations == nil {
|
|
||||||
options.ScreenAnnotations = make(map[pdfgo.Reference][]*pdfgo.ScreenAnnotation)
|
|
||||||
}
|
|
||||||
ref := pages[screen.Page]
|
|
||||||
options.ScreenAnnotations[ref] = append(options.ScreenAnnotations[ref], screen.Screen)
|
|
||||||
files[id] = resource.Clip.File
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := n.loadMediaFiles(ctx, files); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, id := range slices.Sorted(maps.Keys(files)) {
|
|
||||||
clip := n.video[id].Clip
|
|
||||||
if clip.ContentType == "" {
|
|
||||||
kind := http.DetectContentType(clip.File.Embedded.Data)
|
|
||||||
if !strings.HasPrefix(kind, "video/") && kind != "application/ogg" {
|
|
||||||
return fmt.Errorf("cannot identify video content type: %s", id)
|
|
||||||
}
|
|
||||||
clip.ContentType = kind
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
// pdfVideoContentType 转换明确声明的视频格式,未知格式留待检查文件头
|
|
||||||
// 入参: media 视频资源
|
|
||||||
// 返回: string 媒体类型,无法确定时为空
|
|
||||||
func pdfVideoContentType(media MultiMedia) string {
|
|
||||||
format := strings.ToLower(strings.TrimSpace(media.Format))
|
|
||||||
if format == "" {
|
|
||||||
format = strings.TrimPrefix(strings.ToLower(path.Ext(media.MediaFile)), ".")
|
|
||||||
}
|
|
||||||
switch format {
|
|
||||||
case "mp4", "m4v":
|
|
||||||
return "video/mp4"
|
|
||||||
case "mpeg", "mpg":
|
|
||||||
return "video/mpeg"
|
|
||||||
case "mov", "qt":
|
|
||||||
return "video/quicktime"
|
|
||||||
case "webm":
|
|
||||||
return "video/webm"
|
|
||||||
case "ogv", "ogg":
|
|
||||||
return "video/ogg"
|
|
||||||
case "avi":
|
|
||||||
return "video/x-msvideo"
|
|
||||||
case "wmv":
|
|
||||||
return "video/x-ms-wmv"
|
|
||||||
case "3gp":
|
|
||||||
return "video/3gpp"
|
|
||||||
case "3g2":
|
|
||||||
return "video/3gpp2"
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
// 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 (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/xiaoqidun/pdfgo"
|
|
||||||
)
|
|
||||||
|
|
||||||
// soundAction 保留音量、循环及等待语义,PDF同步播放同时限制交互
|
|
||||||
// 入参: action OFD音频动作
|
|
||||||
// 返回: *pdfNavigationAction PDF音频动作, error 资源或参数错误
|
|
||||||
func (n *pdfNavigation) soundAction(action Sound) (*pdfNavigationAction, error) {
|
|
||||||
volume := 100
|
|
||||||
if action.Volume != nil {
|
|
||||||
volume = *action.Volume
|
|
||||||
}
|
|
||||||
if volume < 0 || volume > 100 {
|
|
||||||
return nil, fmt.Errorf("invalid sound volume")
|
|
||||||
}
|
|
||||||
file := n.audio[action.ResourceID]
|
|
||||||
if file == nil {
|
|
||||||
media, err := n.source.Media(action.ResourceID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if media.Type != "Audio" {
|
|
||||||
return nil, fmt.Errorf("sound resource is not audio: %s", action.ResourceID)
|
|
||||||
}
|
|
||||||
if n.audio == nil {
|
|
||||||
n.audio = make(map[string]*pdfgo.FileSpecification)
|
|
||||||
}
|
|
||||||
file = n.mediaFile(media)
|
|
||||||
n.audio[action.ResourceID] = file
|
|
||||||
}
|
|
||||||
return &pdfNavigationAction{Sound: &pdfgo.SoundAction{
|
|
||||||
Sound: pdfgo.Sound{File: file}, Volume: float64(volume) / 100,
|
|
||||||
Repeat: action.Repeat, Synchronous: action.Synchronous && !action.Repeat,
|
|
||||||
}}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// loadAudio 在绘制后读取自描述音频文件,原始内容不作采样转换
|
|
||||||
// 入参: ctx 取消上下文
|
|
||||||
// 返回: error 资源读取或取消错误
|
|
||||||
func (n *pdfNavigation) loadAudio(ctx context.Context) error {
|
|
||||||
return n.loadMediaFiles(ctx, n.audio)
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
// 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 (
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
|
|
||||||
"github.com/xiaoqidun/pdfgo"
|
|
||||||
)
|
|
||||||
|
|
||||||
// writePDF 传递后端资源记录及OFD导航修正,写出最终PDF
|
|
||||||
// 入参: ctx 取消上下文, data 后端PDF数据, writer 输出流, optimization 优化配置
|
|
||||||
// 返回: error 资源、压缩或写入错误
|
|
||||||
func (r *pdfRenderer) writePDF(ctx context.Context, data []byte, writer io.Writer, optimization pdfgo.OptimizeOptions) error {
|
|
||||||
if r.imageError != nil {
|
|
||||||
return r.imageError
|
|
||||||
}
|
|
||||||
options := codecpdf.WriteOptions{
|
|
||||||
Images: r.images,
|
|
||||||
Text: r.text,
|
|
||||||
ExactImages: r.exactImages,
|
|
||||||
ExactText: r.exactText,
|
|
||||||
Optimization: optimization,
|
|
||||||
}
|
|
||||||
if r.navigation != nil && r.navigation.nativeWrite {
|
|
||||||
options.Links = make(map[int][]*pdfgo.LinkRegion, len(r.navigation.Link))
|
|
||||||
for page, links := range r.navigation.Link {
|
|
||||||
regions := make([]*pdfgo.LinkRegion, len(links))
|
|
||||||
for i, link := range links {
|
|
||||||
regions[i] = link.Region
|
|
||||||
}
|
|
||||||
options.Links[page] = regions
|
|
||||||
}
|
|
||||||
options.PrepareRewrite = r.navigation.prepareRewrite
|
|
||||||
}
|
|
||||||
return codecpdf.Write(ctx, data, writer, options)
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user