feat(文件拆分): 拆分PDF格式适配模块
build.yaml / build (push) Successful in 51s

This commit is contained in:
xiaoqidun committed 2026-10-14 09:02:24 +08:00
1 parent bec13d523f
commit b3c4bbc844
45 files changed
+1885 -1453

No files matched your search

-1
View File
@@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
// Package threed 提供U3D模型解码、场景求值及软件绘制能力
package threed package threed
import ( import (
+67
View File
@@ -0,0 +1,67 @@
// 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 (
"context"
"crypto/sha256"
"encoding/binary"
"math"
"github.com/xiaoqidun/pdfgo"
)
// ClipPathKey 按路径完整内容与变换生成键,不依赖可变切片的地址
// 入参: ctx 取消上下文, path 裁剪路径, matrix 页面变换
// 返回: [32]byte 内容摘要, error 取消错误
func ClipPathKey(ctx context.Context, path pdfgo.Path, matrix pdfgo.Matrix) ([32]byte, error) {
hash := sha256.New()
var value [16]byte
for _, number := range matrix {
binary.LittleEndian.PutUint64(value[:8], math.Float64bits(number))
hash.Write(value[:8])
}
binary.LittleEndian.PutUint64(value[:8], uint64(len(path.Segments)))
value[8] = 0
if path.EvenOdd {
value[8] = 1
}
hash.Write(value[:9])
for _, segment := range path.Segments {
if err := ctx.Err(); err != nil {
return [32]byte{}, err
}
binary.LittleEndian.PutUint64(value[:8], uint64(len(segment.Operator)))
binary.LittleEndian.PutUint64(value[8:], uint64(len(segment.Points)))
hash.Write(value[:])
hash.Write([]byte(segment.Operator))
for index, point := range segment.Points {
if index&255 == 0 {
if err := ctx.Err(); err != nil {
return [32]byte{}, err
}
}
binary.LittleEndian.PutUint64(value[:8], math.Float64bits(point.X))
binary.LittleEndian.PutUint64(value[8:], math.Float64bits(point.Y))
hash.Write(value[:])
}
}
if err := ctx.Err(); err != nil {
return [32]byte{}, err
}
var key [32]byte
hash.Sum(key[:0])
return key, nil
}
+37
View File
@@ -0,0 +1,37 @@
// 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 (
"github.com/xiaoqidun/pdfgo"
)
// OutputColor 将合成结果映射到OFD输出,与保留的设备四色对象使用相同显示换算
// 入参: space 合成空间, values 非预乘分量
// 返回: [3]float64 显示颜色, error 校准颜色转换错误
func OutputColor(space *pdfgo.ColorSpace, values [4]float64) ([3]float64, error) {
if space.Model == "DeviceCMYK" && !space.Calibrated() {
black := 1 - values[3]
return [3]float64{(1 - values[0]) * black, (1 - values[1]) * black, (1 - values[2]) * black}, nil
}
return space.RGB(values[:space.Components()], "RelativeColorimetric")
}
// NormalBlend 判断混合模式是否为普通覆盖
// 入参: mode 混合模式
// 返回: bool 是否普通覆盖
func NormalBlend(mode pdfgo.Name) bool {
return mode == "" || mode == "Normal" || mode == "Compatible"
}
+195
View File
@@ -0,0 +1,195 @@
// 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"
"fmt"
"image"
"io"
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
"github.com/xiaoqidun/pdfgo"
)
// TextPage 保存后端文字对象数量及待替换区段
type TextPage struct {
Count int
Replacements []pdfgo.TextReplacement
}
// WriteOptions 保存PDF最终写出所需的像素、文字和导航修正
// Images逐页记录绘制图像,nil图像表示不替换;Links保留全部链接位置,包括矩形区域的nil项
// PrepareRewrite在页面引用确定后填充导航动作,不得提前写出PDF
type WriteOptions struct {
Images [][]image.Image
Text []TextPage
Links map[int][]*pdfgo.LinkRegion
ExactImages bool
ExactText bool
Optimization pdfgo.OptimizeOptions
PrepareRewrite func(context.Context, *pdfgo.Reader, []pdfgo.Reference, *pdfgo.RewriteOptions) error
}
// Write 合并图像、文字、导航修正及压缩,直接写出最终PDF
// 入参: ctx 取消上下文, data 后端PDF数据, writer 输出流, settings 写出配置
// 返回: error 资源、压缩或写入错误
func Write(ctx context.Context, data []byte, writer io.Writer, settings WriteOptions) error {
nativeWrite := settings.PrepareRewrite != nil
if !settings.ExactImages && !settings.ExactText && !nativeWrite && settings.Optimization.Compression.Mode == pdfgo.CompressionUnchanged {
if err := ctx.Err(); err != nil {
return err
}
n, err := writer.Write(data)
if err == nil && n != len(data) {
err = io.ErrShortWrite
}
return err
}
reader, err := pdfgo.NewReader(bytes.NewReader(data), int64(len(data)))
if err != nil {
return err
}
defer reader.Close()
if !settings.ExactImages && !settings.ExactText && !nativeWrite {
_, err := reader.OptimizeTo(ctx, writer, settings.Optimization)
return err
}
images := make(map[pdfgo.Reference]image.Image)
links := make(map[pdfgo.AnnotationLocation]pdfgo.LinkRegion)
texts := make(map[pdfgo.Reference][]pdfgo.TextReplacement)
references := make([]pdfgo.Reference, len(settings.Images))
pages := 0
err = reader.WalkPages(ctx, func(index int, page *pdfgo.Page) error {
if index >= len(settings.Images) {
return fmt.Errorf("unexpected PDF output page")
}
pages++
references[index] = page.Reference
if nativeWrite {
annotations, err := page.AnnotationsContext(ctx)
if err != nil {
return err
}
expected := settings.Links[index]
if len(annotations) != len(expected) {
return fmt.Errorf("PDF output link count differs")
}
for i, link := range expected {
if annotations[i].Subtype != pdfgo.Name("Link") {
return fmt.Errorf("unexpected PDF output annotation")
}
if link != nil {
links[pdfgo.AnnotationLocation{Page: page.Reference, Index: i}] = *link
}
}
}
if !settings.ExactImages && !settings.ExactText {
return nil
}
value, err := reader.Resolve(page.Resources["XObject"])
if err != nil {
return err
}
resources, _ := value.(pdfgo.Dictionary)
var content bytes.Buffer
if _, err := page.WriteContent(ctx, &content); err != nil {
return err
}
position := 0
textCount := 0
if err := pdfgo.WalkOperations(ctx, content.Bytes(), func(operation pdfgo.Operation) error {
if operation.Operator == "BT" {
textCount++
}
if !settings.ExactImages {
return nil
}
if operation.Operator != "Do" {
return nil
}
if len(operation.Operands) != 1 || position >= len(settings.Images[index]) {
return fmt.Errorf("unexpected PDF output image")
}
name, ok := operation.Operands[0].(pdfgo.Name)
if !ok {
return fmt.Errorf("invalid PDF output image name")
}
ref, ok := resources[name].(pdfgo.Reference)
if !ok {
return fmt.Errorf("invalid PDF output image reference")
}
original := settings.Images[index][position]
position++
if original == nil {
return nil
}
image, err := reader.ReadImage(ref)
if err != nil {
return err
}
if image.Width != original.Bounds().Dx() || image.Height != original.Bounds().Dy() {
return fmt.Errorf("PDF output image dimensions differ")
}
if previous := images[ref]; previous != nil && previous != original {
return fmt.Errorf("PDF output image reference conflicts")
}
images[ref] = original
return nil
}); err != nil {
return err
}
if settings.ExactImages && position != len(settings.Images[index]) {
return fmt.Errorf("PDF output image missing")
}
if settings.ExactText {
if index >= len(settings.Text) || textCount != settings.Text[index].Count {
return fmt.Errorf("PDF output text count differs")
}
if len(settings.Text[index].Replacements) != 0 {
texts[page.Reference] = settings.Text[index].Replacements
}
}
return nil
})
if err != nil {
return err
}
if pages != len(settings.Images) {
return fmt.Errorf("PDF output page missing")
}
options := pdfgo.RewriteOptions{Images: images, ImageOptions: pdfgo.ImageWriteOptions{PreblendBinary: true}, LinkRegions: links, TextReplacements: texts, Optimization: settings.Optimization}
if nativeWrite {
if err := settings.PrepareRewrite(ctx, reader, references, &options); err != nil {
return err
}
}
for ref, original := range images {
if err := ctx.Err(); err != nil {
return err
}
pixels, err := baseimage.PixelDataContext(ctx, original)
if err != nil {
return err
}
if pixels == nil || pixels.Bounds() != original.Bounds() {
return fmt.Errorf("PDF output image pixel bounds differ")
}
images[ref] = pixels
}
_, err = reader.RewriteTo(ctx, writer, options)
return err
}
+338
View File
@@ -0,0 +1,338 @@
// 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"
"crypto/sha256"
"encoding/binary"
"fmt"
"unicode/utf16"
basefont "github.com/xiaoqidun/ofdgo/internal/base/font"
"github.com/xiaoqidun/pdfgo"
)
// FontRepair 保存字体封装修复范围,不混用字形度量与位置索引
type FontRepair struct {
MetricsLimit uint16
Locations bool
}
// EmbeddedFontName 去除标准六位大写子集前缀,保留字体名称和样式后缀
// 入参: name PDF或内嵌字体名称
// 返回: string 原字体名称
func EmbeddedFontName(name string) string {
if len(name) <= 7 || name[6] != '+' {
return name
}
for i := range 6 {
if name[i] < 'A' || name[i] > 'Z' {
return name
}
}
return name[7:]
}
// FontProgram 为PDF子集字体补齐封装表,保留字形轮廓和编号
// 无效字符表按PDF已解码字形重建,不改变字符对应的字形编号
// 入参: source PDF字体, type1 已解析的Type1程序,nil时按需解析
// 返回: []byte 封装后的字体数据, FontRepair 字体修复范围, error 错误信息
func FontProgram(source *pdfgo.Font, type1 *basefont.Type1Program) ([]byte, FontRepair, error) {
var repair FontRepair
program := source.Program
if source.ProgramType == "FontFile" {
if type1 == nil {
parsed, err := basefont.ParseType1Program(program)
if err != nil {
return nil, repair, err
}
type1 = &parsed
}
var err error
program, err = type1.ToCFF(FontIdentity(source))
if err != nil {
return nil, repair, err
}
}
bareCFF := len(program) >= 4 && program[0] == 1 && program[1] == 0 && program[2] >= 4 && program[3] >= 1 && program[3] <= 4
if source.ProgramType == "Type1C" || source.ProgramType == "CIDFontType0C" || bareCFF {
var err error
program, _, err = basefont.WrapCFFToOTF(program)
if err != nil {
return nil, repair, err
}
}
tables, err := basefont.FileTables(program, 0)
if err != nil {
return nil, repair, err
}
if len(tables["head"]) < 54 || len(tables["maxp"]) < 6 || len(tables["hhea"]) < 36 || len(tables["hmtx"]) == 0 {
return nil, repair, fmt.Errorf("embedded PDF font lacks required metrics")
}
count := binary.BigEndian.Uint16(tables["maxp"][4:6])
if count == 0 {
return nil, repair, fmt.Errorf("embedded PDF font has no glyphs")
}
repair.Locations, err = basefont.NormalizeTrueTypeLocations(tables)
if err != nil {
return nil, repair, err
}
changed := repair.Locations
if head := tables["head"]; len(head) == 56 && binary.BigEndian.Uint32(head[:4]) == 0x00010000 && head[54] == 0 && head[55] == 0 {
tables["head"] = head[:54]
changed = true
}
if cff := tables["CFF "]; len(cff) != 0 {
sanitized, err := basefont.SanitizeCFF(cff)
if err != nil {
return nil, repair, err
}
normalized, err := basefont.NormalizeCFFCharstringsAt(sanitized, binary.BigEndian.Uint16(tables["head"][18:20]))
if err != nil {
return nil, repair, err
}
if !bytes.Equal(normalized, cff) {
tables["CFF "] = normalized
changed = true
}
}
metrics := int(binary.BigEndian.Uint16(tables["hhea"][34:36]))
if metrics == 0 || metrics > int(count) {
return nil, repair, fmt.Errorf("invalid embedded PDF font metric count")
}
metricLength := 4*metrics + 2*(int(count)-metrics)
if len(tables["hmtx"]) < metricLength {
if len(tables["hmtx"]) < 4*metrics || len(tables["hmtx"])%2 != 0 {
return nil, repair, fmt.Errorf("incomplete embedded PDF font metrics: have %d, need %d", len(tables["hmtx"]), metricLength)
}
limit := metrics + (len(tables["hmtx"])-4*metrics)/2
missing, err := missingLeftBearings(tables, limit, int(count))
if err != nil {
return nil, repair, err
}
tables["hmtx"] = append(bytes.Clone(tables["hmtx"]), missing...)
repair.MetricsLimit = uint16(limit)
changed = true
}
if len(tables["hmtx"]) > metricLength {
tables["hmtx"] = tables["hmtx"][:metricLength]
changed = true
}
invalidCmap := false
if source.Subtype == "TrueType" || source.Subtype == "Type0" {
mapping := basefont.ParseCmapMappings(tables["cmap"])
for char, glyph := range mapping {
if glyph >= count {
delete(mapping, char)
invalidCmap = true
}
}
if invalidCmap && source.Subtype == "TrueType" {
tables["cmap"] = basefont.BuildCmapTable(count, mapping)
changed = true
}
}
if len(tables["cmap"]) == 0 || source.Subtype == "Type0" && invalidCmap || (source.Subtype == "Type0" || source.Subtype == "TrueType") && CmapNeedsRebuild(tables["cmap"]) {
mapping, err := source.GlyphMap()
if err != nil {
return nil, repair, err
}
for _, glyph := range mapping {
if glyph >= count {
return nil, repair, fmt.Errorf("PDF font glyph %d exceeds glyph count %d", glyph, count)
}
}
basefont.AddPackedGlyphMapping(mapping, count)
tables["cmap"] = basefont.BuildCmapTable(count, mapping)
changed = true
}
if len(basefont.NamesFromTable(tables["name"])) == 0 {
fontName := FontIdentity(source)
name := utf16.Encode([]rune(fontName))
if len(name) > 32767 {
return nil, repair, fmt.Errorf("PDF font name exceeds name table capacity")
}
data := make([]byte, 42+len(name)*2)
binary.BigEndian.PutUint16(data[2:], 3)
binary.BigEndian.PutUint16(data[4:], 42)
for n, id := range []uint16{1, 4, 6} {
record := data[6+n*12:]
binary.BigEndian.PutUint16(record, 3)
binary.BigEndian.PutUint16(record[2:], 1)
binary.BigEndian.PutUint16(record[4:], 0x409)
binary.BigEndian.PutUint16(record[6:], id)
binary.BigEndian.PutUint16(record[8:], uint16(len(name)*2))
}
for n, v := range name {
binary.BigEndian.PutUint16(data[42+n*2:], v)
}
tables["name"] = data
changed = true
}
if len(tables["post"]) == 0 {
tables["post"] = basefont.BuildPostTable()
changed = true
}
if len(tables["OS/2"]) == 0 {
hhea := tables["hhea"]
tables["OS/2"] = basefont.BuildOS2TableWithMetrics(int16(binary.BigEndian.Uint16(hhea[4:6])), int16(binary.BigEndian.Uint16(hhea[6:8])))
changed = true
}
for _, tag := range []string{"cvt ", "fpgm", "prep"} {
if data, ok := tables[tag]; ok && len(data) == 0 {
delete(tables, tag)
changed = true
}
}
if !changed && !sfntMissingPadding(program) {
return program, repair, nil
}
result, err := basefont.SerializeOTF(tables)
return result, repair, err
}
// FontIdentity 返回字体名称或由原始程序生成的稳定封装标识
// 入参: source PDF字体
// 返回: string 字体标识
func FontIdentity(source *pdfgo.Font) string {
if source.Name != "" {
return source.Name
}
checksum := sha256.Sum256(source.Program)
return fmt.Sprintf("PDF-%x", checksum[:28])
}
// CmapNeedsRebuild 检查字体封装字符表是否存在结构错误或交叠区段
// 入参: data cmap表数据
// 返回: bool 是否需要按PDF显式字形编号重建
func CmapNeedsRebuild(data []byte) bool {
if len(data) < 4 || binary.BigEndian.Uint16(data) != 0 {
return true
}
count := int(binary.BigEndian.Uint16(data[2:]))
if count == 0 || count > (len(data)-4)/8 {
return true
}
for index := range count {
offset := uint64(binary.BigEndian.Uint32(data[8+8*index:]))
if offset < uint64(4+8*count) || offset > uint64(len(data)) || uint64(len(data))-offset < 2 {
return true
}
sub := data[int(offset):]
if binary.BigEndian.Uint16(sub) == 6 {
if len(sub) < 10 {
return true
}
length := int(binary.BigEndian.Uint16(sub[2:]))
first, glyphs := int(binary.BigEndian.Uint16(sub[6:])), int(binary.BigEndian.Uint16(sub[8:]))
if length%2 != 0 || length > len(sub) || 10+2*glyphs > length || first+glyphs > 65536 {
return true
}
continue
}
if binary.BigEndian.Uint16(sub) != 4 {
continue
}
if len(sub) < 16 {
return true
}
length := int(binary.BigEndian.Uint16(sub[2:]))
segmentBytes := int(binary.BigEndian.Uint16(sub[6:]))
segments := segmentBytes / 2
if segmentBytes == 0 || segmentBytes%2 != 0 || length%2 != 0 || length > len(sub) || 16+8*segments > length {
return true
}
glyphStart := 16 + 8*segments
previous := -1
for i := 0; i < segments; i++ {
end := int(binary.BigEndian.Uint16(sub[14+2*i:]))
start := int(binary.BigEndian.Uint16(sub[16+2*segments+2*i:]))
if end < start || start <= previous || i == segments-1 && (start != 65535 || end != 65535) {
return true
}
previous = end
position := 16 + 6*segments + 2*i
offset := int(binary.BigEndian.Uint16(sub[position:]))
if offset != 0 && (offset%2 != 0 || position+offset < glyphStart || position+offset+2*(end-start+1) > length) {
return true
}
}
}
return false
}
// sfntMissingPadding 判断字体表目录是否引用了未写入的末尾对齐字节
// 入参: program OpenType字体数据
// 返回: bool 是否需要重新封装
func sfntMissingPadding(program []byte) bool {
if len(program) < 12 {
return true
}
count := int(binary.BigEndian.Uint16(program[4:6]))
if count > (len(program)-12)/16 {
return true
}
for index := range count {
record := program[12+16*index:]
offset := int(binary.BigEndian.Uint32(record[8:12]))
length := int(binary.BigEndian.Uint32(record[12:16]))
if offset > len(program) || length > len(program)-offset || (4-length%4)%4 > len(program)-offset-length {
return true
}
}
return false
}
// missingLeftBearings 从轮廓边界恢复缺失的尾部左侧边距
// 入参: tables 字体表, start 首个缺失字形, count 字形总数
// 返回: []byte 补齐的hmtx数据, error 无法读取轮廓
func missingLeftBearings(tables map[string][]byte, start, count int) ([]byte, error) {
head, loca, glyf := tables["head"], tables["loca"], tables["glyf"]
if len(head) < 54 || len(glyf) == 0 {
return nil, fmt.Errorf("embedded PDF font lacks glyph outlines for missing metrics")
}
format := int(int16(binary.BigEndian.Uint16(head[50:52])))
entrySize := 2
if format == 1 {
entrySize = 4
} else if format != 0 {
return nil, fmt.Errorf("invalid embedded PDF font location format")
}
if len(loca) < (count+1)*entrySize {
return nil, fmt.Errorf("incomplete embedded PDF font glyph locations")
}
location := func(index int) int {
if format == 0 {
return int(binary.BigEndian.Uint16(loca[index*2:])) * 2
}
return int(binary.BigEndian.Uint32(loca[index*4:]))
}
result := make([]byte, 2*(count-start))
for index := start; index < count; index++ {
from, to := location(index), location(index+1)
if from > to || to > len(glyf) {
return nil, fmt.Errorf("invalid embedded PDF font glyph location")
}
if from == to {
continue
}
if to-from < 10 {
return nil, fmt.Errorf("incomplete embedded PDF font glyph outline")
}
copy(result[(index-start)*2:], glyf[from+2:from+4])
}
return result, nil
}
+101
View File
@@ -0,0 +1,101 @@
// 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 (
"context"
"fmt"
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
"github.com/xiaoqidun/pdfgo"
)
// GeometryPath 直接转换PDF路径坐标,不经文本编码或曲线展平
// 入参: ctx 取消上下文, source PDF路径, matrix 坐标变换
// 返回: basegeom.Path 独立几何路径, error 指令、坐标或取消错误
func GeometryPath(ctx context.Context, source pdfgo.Path, matrix pdfgo.Matrix) (basegeom.Path, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
path := make(basegeom.Path, len(source.Segments))
for i, segment := range source.Segments {
if err := ctx.Err(); err != nil {
return nil, err
}
var count int
switch segment.Operator {
case "M":
path[i].Verb, count = basegeom.Move, 1
case "L":
path[i].Verb, count = basegeom.Line, 1
case "B":
path[i].Verb, count = basegeom.Cubic, 3
case "C":
path[i].Verb = basegeom.ClosePath
default:
return nil, fmt.Errorf("invalid PDF path operator %q", segment.Operator)
}
if len(segment.Points) != count {
return nil, fmt.Errorf("invalid PDF path operands for %s", segment.Operator)
}
var points [3]basegeom.Point
for j, point := range segment.Points {
point = matrix.Apply(point)
points[j] = basegeom.Point{X: point.X, Y: point.Y}
}
if count != 0 {
path[i].End = points[count-1]
}
if count == 3 {
path[i].Control1, path[i].Control2 = points[0], points[1]
}
}
if err := path.Validate(); err != nil {
return nil, err
}
return path, ctx.Err()
}
// GlyphPath 将字形轮廓变换到PDF页面坐标,二次曲线精确转换为三次曲线
// 入参: outline 字形轮廓, matrix 字形到页面的变换
// 返回: pdfgo.Path 页面路径, error 非字体轮廓指令
func GlyphPath(outline basegeom.Path, matrix pdfgo.Matrix) (pdfgo.Path, error) {
path := pdfgo.Path{Segments: make([]pdfgo.Segment, 0, len(outline))}
var current, start basegeom.Point
point := func(p basegeom.Point) pdfgo.Point { return matrix.Apply(pdfgo.Point{X: p.X, Y: p.Y}) }
for _, segment := range outline {
switch segment.Verb {
case basegeom.Move:
start = segment.End
path.Segments = append(path.Segments, pdfgo.Segment{Operator: "M", Points: []pdfgo.Point{point(segment.End)}})
case basegeom.Line:
path.Segments = append(path.Segments, pdfgo.Segment{Operator: "L", Points: []pdfgo.Point{point(segment.End)}})
case basegeom.Quad:
c1 := basegeom.Point{X: current.X + (segment.Control1.X-current.X)*2/3, Y: current.Y + (segment.Control1.Y-current.Y)*2/3}
c2 := basegeom.Point{X: segment.End.X + (segment.Control1.X-segment.End.X)*2/3, Y: segment.End.Y + (segment.Control1.Y-segment.End.Y)*2/3}
path.Segments = append(path.Segments, pdfgo.Segment{Operator: "B", Points: []pdfgo.Point{point(c1), point(c2), point(segment.End)}})
case basegeom.Cubic:
path.Segments = append(path.Segments, pdfgo.Segment{Operator: "B", Points: []pdfgo.Point{point(segment.Control1), point(segment.Control2), point(segment.End)}})
case basegeom.ClosePath:
path.Segments = append(path.Segments, pdfgo.Segment{Operator: "C"})
current = start
continue
default:
return pdfgo.Path{}, fmt.Errorf("unsupported PDF glyph geometry verb %d", segment.Verb)
}
current = segment.End
}
return path, nil
}
+95
View File
@@ -0,0 +1,95 @@
// 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 (
"image"
"image/color"
"math"
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
"github.com/xiaoqidun/pdfgo"
)
// StencilImage 按当前填充色读取模板图像,不分配整幅彩色样本
type StencilImage struct {
Source image.Image
Fill color.NRGBA64
}
// NativeImage 按源像素输出原生过程色,不分配完整显示颜色缓冲
type NativeImage struct {
Source *pdfgo.ImageProcess
Space *pdfgo.ColorSpace
}
// ColorModel 保留填充色和模板的有效采样精度
// 返回: color.Model 非预乘颜色模型
func (s *StencilImage) ColorModel() color.Model {
model := s.Source.ColorModel()
if s.Fill.R%257 == 0 && s.Fill.G%257 == 0 && s.Fill.B%257 == 0 && (model == color.GrayModel || model == color.NRGBAModel || model == color.RGBAModel) {
return color.NRGBAModel
}
return color.NRGBA64Model
}
// Bounds 返回模板图像的采样边界
// 返回: image.Rectangle 图像边界
func (s *StencilImage) Bounds() image.Rectangle { return s.Source.Bounds() }
// At 以模板灰度计算填充覆盖率,不重复变换颜色
// 入参: x 横向坐标, y 纵向坐标
// 返回: color.Color 非预乘颜色
func (s *StencilImage) At(x, y int) color.Color {
return s.NRGBA64At(x, y)
}
// NRGBA64At 直接读取模板覆盖率,保留填充色的有效精度
// 入参: x 横向坐标, y 纵向坐标
// 返回: color.NRGBA64 非预乘颜色
func (s *StencilImage) NRGBA64At(x, y int) color.NRGBA64 {
if !image.Pt(x, y).In(s.Bounds()) {
return color.NRGBA64{}
}
gray := baseimage.NRGBA64At(s.Source, x, y)
pixel := s.Fill
pixel.A = uint16(65535 - gray.R)
return pixel
}
// ColorModel 保留16位原生分量与源透明度
// 返回: color.Model 非预乘颜色模型
func (s *NativeImage) ColorModel() color.Model { return color.NRGBA64Model }
// Bounds 返回源图像的完整采样边界
// 返回: image.Rectangle 原始像素边界
func (s *NativeImage) Bounds() image.Rectangle { return s.Source.Bounds() }
// At 获取按OFD输出映射的原生颜色
// 入参: x 横坐标, y 纵坐标
// 返回: color.Color 非预乘颜色
func (s *NativeImage) At(x, y int) color.Color { return s.NRGBA64At(x, y) }
// NRGBA64At 按原始精度转换设备过程色,不进行页面采样或备用变换
// 入参: x 横坐标, y 纵坐标
// 返回: color.NRGBA64 非预乘颜色
func (s *NativeImage) NRGBA64At(x, y int) color.NRGBA64 {
if !image.Pt(x, y).In(s.Bounds()) {
return color.NRGBA64{}
}
values, alpha := s.Source.ValuesAt(x, y)
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))}
}
+106
View File
@@ -0,0 +1,106 @@
// 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"
"reflect"
"github.com/xiaoqidun/pdfgo"
)
// imageValueBudget 限制图像描述的检查深度和保留字节数
type imageValueBudget struct {
nodes int
bytes int
}
// ImageDigest 对无间接依赖的小图像生成编码摘要,超预算时不参与内容缓存
// 入参: ctx 取消上下文, source 图像描述
// 返回: [32]byte 编码摘要, bool 是否可复用, error 取消错误
func ImageDigest(ctx context.Context, source *pdfgo.Image) ([32]byte, bool, error) {
if source == nil || source.Stream == nil || len(source.Stream.Data) > 1<<20 {
return [32]byte{}, false, nil
}
budget := imageValueBudget{nodes: 256, bytes: 65536}
for _, value := range []pdfgo.Object{source.Stream.Dictionary, source.ColorSpace, source.Decode, source.Mask, source.SoftMask} {
if !imageDirectValue(value, &budget) {
return [32]byte{}, false, ctx.Err()
}
}
hash := sha256.New()
data := source.Stream.Data
for len(data) != 0 {
if err := ctx.Err(); err != nil {
return [32]byte{}, false, err
}
n := min(len(data), 65536)
hash.Write(data[:n])
data = data[n:]
}
var result [32]byte
hash.Sum(result[:0])
return result, true, ctx.Err()
}
// SameImage 核对完整编码与有效图像属性,不以摘要相同代替内容相同
// 入参: left 已缓存图像, right 待复用图像,二者须包含有效编码流
// 返回: bool 是否可使用同一图像资源
func SameImage(left, right *pdfgo.Image) bool {
return left.Width == right.Width && left.Height == right.Height && left.BitsPerComponent == right.BitsPerComponent &&
left.ImageMask == right.ImageMask && left.Interpolate == right.Interpolate &&
bytes.Equal(left.Stream.Data, right.Stream.Data) && reflect.DeepEqual(left.Stream.Dictionary, right.Stream.Dictionary) &&
reflect.DeepEqual(left.ColorSpace, right.ColorSpace) && reflect.DeepEqual(left.Decode, right.Decode) &&
reflect.DeepEqual(left.Mask, right.Mask) && reflect.DeepEqual(left.SoftMask, right.SoftMask)
}
// imageDirectValue 检查图像描述是否完全独立,拒绝间接引用和嵌套流
// 入参: value 描述值, budget 描述检查预算
// 返回: bool 是否不依赖阅读器资源
func imageDirectValue(value pdfgo.Object, budget *imageValueBudget) bool {
if budget.nodes <= 0 || budget.bytes < 64 {
return false
}
budget.nodes--
budget.bytes -= 64
switch v := value.(type) {
case nil, pdfgo.Boolean, pdfgo.Integer, pdfgo.Real:
return true
case pdfgo.Name:
budget.bytes -= len(v)
return budget.bytes >= 0
case pdfgo.String:
budget.bytes -= len(v)
return budget.bytes >= 0
case pdfgo.Array:
for _, child := range v {
if !imageDirectValue(child, budget) {
return false
}
}
return true
case pdfgo.Dictionary:
for key, child := range v {
budget.bytes -= len(key)
if !imageDirectValue(child, budget) {
return false
}
}
return true
}
return false
}
@@ -12,42 +12,42 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package pdf
import ( import (
"context" "context"
"fmt" "fmt"
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
"math" "math"
"slices" "slices"
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
// PDF链接区域细分上限与曲线逼近误差,误差单位为毫米 // PDF链接区域细分上限与曲线逼近误差,误差单位为毫米
const ( const (
pdfLinkEdgeLimit = 4096 LinkEdgeLimit = 4096
pdfLinkQuadLimit = 65536 LinkQuadLimit = 65536
pdfLinkTolerance = 0.01 linkTolerance = 0.01
) )
// pdfLinkEdge 保存按纵坐标排序的边及原始绕行方向 // linkEdge 保存按纵坐标排序的边及原始绕行方向
type pdfLinkEdge struct { type linkEdge struct {
a, b pdfgo.Point a, b pdfgo.Point
winding int winding int
} }
// pdfLinkCross 保存水平扫描线与区域边的交点 // linkCross 保存水平扫描线与区域边的交点
type pdfLinkCross struct { type linkCross struct {
x float64 x float64
edge int edge int
} }
// pdfActionRegion 将非零绕行区域分解为PDF链接四边形,保留空洞和相交轮廓 // ActionRegion 将非零绕行区域分解为PDF链接四边形,保留空洞和相交轮廓
// 入参: ctx 取消上下文, outline 页面SVG路径, pageH 页面高度 // 入参: ctx 取消上下文, outline 页面SVG路径, pageH 页面高度
// 返回: *pdfgo.LinkRegion 链接区域,nil表示沿用矩形, error 解析或复杂度错误 // 返回: *pdfgo.LinkRegion 链接区域,nil表示沿用矩形, error 解析或复杂度错误
func pdfActionRegion(ctx context.Context, outline string, pageH float64) (*pdfgo.LinkRegion, error) { func ActionRegion(ctx context.Context, outline string, pageH float64) (*pdfgo.LinkRegion, error) {
if outline == "" { if outline == "" {
return nil, nil return nil, nil
} }
@@ -58,20 +58,20 @@ func pdfActionRegion(ctx context.Context, outline string, pageH float64) (*pdfgo
if err != nil { if err != nil {
return nil, err return nil, err
} }
if path.Len() > pdfLinkEdgeLimit { if path.Len() > LinkEdgeLimit {
return nil, fmt.Errorf("PDF link region exceeds edge limit") return nil, fmt.Errorf("PDF link region exceeds edge limit")
} }
if _, ok := basegeom.RectangleBounds(basegeom.Path(*geometryFromCanvasPath(path))); ok { if _, ok := basegeom.RectangleBounds(linkGeometry(path)); ok {
return nil, nil return nil, nil
} }
path, err = flattenPDFLink(ctx, path) path, err = FlattenLink(ctx, path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if quads := convexPDFLink(path, pageH); len(quads) != 0 { if quads := convexLink(path, pageH); len(quads) != 0 {
return pdfLinkRegionBounds(quads), nil return linkRegionBounds(quads), nil
} }
var edges []pdfLinkEdge var edges []linkEdge
var levels []float64 var levels []float64
var start, previous pdfgo.Point var start, previous pdfgo.Point
open := false open := false
@@ -79,7 +79,7 @@ func pdfActionRegion(ctx context.Context, outline string, pageH float64) (*pdfgo
if a.Y == b.Y { if a.Y == b.Y {
return return
} }
edge := pdfLinkEdge{a: a, b: b, winding: 1} edge := linkEdge{a: a, b: b, winding: 1}
if a.Y > b.Y { if a.Y > b.Y {
edge.a, edge.b, edge.winding = b, a, -1 edge.a, edge.b, edge.winding = b, a, -1
} }
@@ -129,7 +129,7 @@ func pdfActionRegion(ctx context.Context, outline string, pageH float64) (*pdfgo
return nil, fmt.Errorf("invalid PDF link intersection") return nil, fmt.Errorf("invalid PDF link intersection")
} }
levels = append(levels, y) levels = append(levels, y)
if len(levels) > pdfLinkQuadLimit { if len(levels) > LinkQuadLimit {
return nil, fmt.Errorf("PDF link region exceeds intersection limit") return nil, fmt.Errorf("PDF link region exceeds intersection limit")
} }
} }
@@ -138,7 +138,7 @@ func pdfActionRegion(ctx context.Context, outline string, pageH float64) (*pdfgo
slices.Sort(levels) slices.Sort(levels)
levels = slices.Compact(levels) levels = slices.Compact(levels)
region := &pdfgo.LinkRegion{} region := &pdfgo.LinkRegion{}
crossings := make([]pdfLinkCross, 0, len(edges)) crossings := make([]linkCross, 0, len(edges))
for i := 1; i < len(levels); i++ { for i := 1; i < len(levels); i++ {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return nil, err return nil, err
@@ -151,10 +151,10 @@ func pdfActionRegion(ctx context.Context, outline string, pageH float64) (*pdfgo
crossings = crossings[:0] crossings = crossings[:0]
for j, edge := range edges { for j, edge := range edges {
if edge.a.Y < mid && mid < edge.b.Y { if edge.a.Y < mid && mid < edge.b.Y {
crossings = append(crossings, pdfLinkCross{edge.x(mid), j}) crossings = append(crossings, linkCross{edge.x(mid), j})
} }
} }
slices.SortFunc(crossings, func(a, b pdfLinkCross) int { slices.SortFunc(crossings, func(a, b linkCross) int {
if a.x < b.x { if a.x < b.x {
return -1 return -1
} }
@@ -173,8 +173,8 @@ func pdfActionRegion(ctx context.Context, outline string, pageH float64) (*pdfgo
if before != 0 && winding == 0 { if before != 0 && winding == 0 {
a, b := edges[left], edges[cross.edge] a, b := edges[left], edges[cross.edge]
quad := [4]pdfgo.Point{{X: a.x(low), Y: low}, {X: b.x(low), Y: low}, {X: b.x(high), Y: high}, {X: a.x(high), Y: high}} quad := [4]pdfgo.Point{{X: a.x(low), Y: low}, {X: b.x(low), Y: low}, {X: b.x(high), Y: high}, {X: a.x(high), Y: high}}
region.Quads = appendPDFLinkQuad(region.Quads, quad) region.Quads = appendLinkQuad(region.Quads, quad)
if len(region.Quads) > pdfLinkQuadLimit { if len(region.Quads) > LinkQuadLimit {
return nil, fmt.Errorf("PDF link region exceeds quadrilateral limit") return nil, fmt.Errorf("PDF link region exceeds quadrilateral limit")
} }
} }
@@ -183,13 +183,13 @@ func pdfActionRegion(ctx context.Context, outline string, pageH float64) (*pdfgo
if len(region.Quads) == 0 { if len(region.Quads) == 0 {
return region, nil return region, nil
} }
return pdfLinkRegionBounds(region.Quads), nil return linkRegionBounds(region.Quads), nil
} }
// convexPDFLink 直接保留单个凸四边形,三角形只分成三个四边形 // convexLink 直接保留单个凸四边形,三角形只分成三个四边形
// 入参: path 已展开路径, pageH 页面高度 // 入参: path 已展开路径, pageH 页面高度
// 返回: [][4]pdfgo.Point 逆时针四边形,不适用时为空 // 返回: [][4]pdfgo.Point 逆时针四边形,不适用时为空
func convexPDFLink(path *canvas.Path, pageH float64) [][4]pdfgo.Point { func convexLink(path *canvas.Path, pageH float64) [][4]pdfgo.Point {
if path.HasSubpaths() || path.Len() > 6 { if path.HasSubpaths() || path.Len() > 6 {
return nil return nil
} }
@@ -208,7 +208,7 @@ func convexPDFLink(path *canvas.Path, pageH float64) [][4]pdfgo.Point {
direction := 0.0 direction := 0.0
for i, a := range vertices { for i, a := range vertices {
b, c := vertices[(i+1)%len(vertices)], vertices[(i+2)%len(vertices)] b, c := vertices[(i+1)%len(vertices)], vertices[(i+2)%len(vertices)]
cross := pdfLinkTurn(a, b, c) cross := linkTurn(a, b, c)
if !finite(cross) || cross == 0 || direction != 0 && (direction < 0) != (cross < 0) { if !finite(cross) || cross == 0 || direction != 0 && (direction < 0) != (cross < 0) {
return nil return nil
} }
@@ -220,13 +220,13 @@ func convexPDFLink(path *canvas.Path, pageH float64) [][4]pdfgo.Point {
if len(vertices) == 4 { if len(vertices) == 4 {
return [][4]pdfgo.Point{storage} return [][4]pdfgo.Point{storage}
} }
return appendPDFLinkTriangle(nil, [3]pdfgo.Point{vertices[0], vertices[1], vertices[2]}) return appendLinkTriangle(nil, [3]pdfgo.Point{vertices[0], vertices[1], vertices[2]})
} }
// pdfLinkRegionBounds 计算四边形共同的注解矩形,保证全部顶点落在矩形内 // linkRegionBounds 计算四边形共同的注解矩形,保证全部顶点落在矩形内
// 入参: quads 链接四边形 // 入参: quads 链接四边形
// 返回: *pdfgo.LinkRegion 链接区域 // 返回: *pdfgo.LinkRegion 链接区域
func pdfLinkRegionBounds(quads [][4]pdfgo.Point) *pdfgo.LinkRegion { func linkRegionBounds(quads [][4]pdfgo.Point) *pdfgo.LinkRegion {
region := &pdfgo.LinkRegion{Quads: quads, Rect: pdfgo.Rectangle{XMin: math.Inf(1), YMin: math.Inf(1), XMax: math.Inf(-1), YMax: math.Inf(-1)}} region := &pdfgo.LinkRegion{Quads: quads, Rect: pdfgo.Rectangle{XMin: math.Inf(1), YMin: math.Inf(1), XMax: math.Inf(-1), YMax: math.Inf(-1)}}
for _, quad := range quads { for _, quad := range quads {
for _, p := range quad { for _, p := range quad {
@@ -237,16 +237,16 @@ func pdfLinkRegionBounds(quads [][4]pdfgo.Point) *pdfgo.LinkRegion {
return region return region
} }
// flattenPDFLink 在取消和数量限制内展开动作曲线,复用标准椭圆弧转换 // FlattenLink 在取消和数量限制内展开动作曲线,复用标准椭圆弧转换
// 入参: ctx 取消上下文, path 页面SVG路径 // 入参: ctx 取消上下文, path 页面SVG路径
// 返回: *canvas.Path 闭合折线路径, error 精度或复杂度错误 // 返回: *canvas.Path 闭合折线路径, error 精度或复杂度错误
func flattenPDFLink(ctx context.Context, path *canvas.Path) (*canvas.Path, error) { func FlattenLink(ctx context.Context, path *canvas.Path) (*canvas.Path, error) {
geometry := geometryFromCanvasPath(path) geometry := linkGeometry(path)
result := &canvas.Path{} result := &canvas.Path{}
var current, start Point var current, start basegeom.Point
count := 0 count := 0
var flatten func([]Point, int) error var flatten func([]basegeom.Point, int) error
flatten = func(points []Point, depth int) error { flatten = func(points []basegeom.Point, depth int) error {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return err return err
} }
@@ -264,9 +264,9 @@ func flattenPDFLink(ctx context.Context, path *canvas.Path) (*canvas.Path, error
if !finite(length) || !finite(distance) { if !finite(length) || !finite(distance) {
return fmt.Errorf("invalid PDF link curve") return fmt.Errorf("invalid PDF link curve")
} }
if distance <= pdfLinkTolerance/2 { if distance <= linkTolerance/2 {
count++ count++
if count > pdfLinkEdgeLimit { if count > LinkEdgeLimit {
return fmt.Errorf("PDF link region exceeds edge limit") return fmt.Errorf("PDF link region exceeds edge limit")
} }
result.LineTo(last.X, -last.Y) result.LineTo(last.X, -last.Y)
@@ -275,7 +275,7 @@ func flattenPDFLink(ctx context.Context, path *canvas.Path) (*canvas.Path, error
if depth == 32 { if depth == 32 {
return fmt.Errorf("PDF link curve tolerance cannot be met") return fmt.Errorf("PDF link curve tolerance cannot be met")
} }
var work, left, right [4]Point var work, left, right [4]basegeom.Point
copy(work[:], points) copy(work[:], points)
for n := len(points); n > 0; n-- { for n := len(points); n > 0; n-- {
left[len(points)-n], right[n-1] = work[0], work[n-1] left[len(points)-n], right[n-1] = work[0], work[n-1]
@@ -288,36 +288,36 @@ func flattenPDFLink(ctx context.Context, path *canvas.Path) (*canvas.Path, error
} }
return flatten(right[:len(points)], depth+1) return flatten(right[:len(points)], depth+1)
} }
for _, segment := range *geometry { for _, segment := range geometry {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return nil, err return nil, err
} }
switch segment.Verb { switch segment.Verb {
case GeometryMove: case basegeom.Move:
result.MoveTo(segment.End.X, -segment.End.Y) result.MoveTo(segment.End.X, -segment.End.Y)
current, start = segment.End, segment.End current, start = segment.End, segment.End
case GeometryClose: case basegeom.ClosePath:
result.Close() result.Close()
current = start current = start
default: default:
curves := basegeom.Path{segment} curves := basegeom.Path{segment}
if segment.Verb == GeometryArc { if segment.Verb == basegeom.Arc {
var err error var err error
curves, err = basegeom.ArcCurves(current, segment, pdfLinkTolerance/2) curves, err = basegeom.ArcCurves(current, segment, linkTolerance/2)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(curves) > pdfLinkEdgeLimit-count { if len(curves) > LinkEdgeLimit-count {
return nil, fmt.Errorf("PDF link region exceeds edge limit") return nil, fmt.Errorf("PDF link region exceeds edge limit")
} }
} }
for _, curve := range curves { for _, curve := range curves {
var storage [4]Point var storage [4]basegeom.Point
points := append(storage[:0], current) points := append(storage[:0], current)
if curve.Verb == GeometryQuad || curve.Verb == GeometryCubic { if curve.Verb == basegeom.Quad || curve.Verb == basegeom.Cubic {
points = append(points, curve.Control1) points = append(points, curve.Control1)
} }
if curve.Verb == GeometryCubic { if curve.Verb == basegeom.Cubic {
points = append(points, curve.Control2) points = append(points, curve.Control2)
} }
points = append(points, curve.End) points = append(points, curve.End)
@@ -334,15 +334,15 @@ func flattenPDFLink(ctx context.Context, path *canvas.Path) (*canvas.Path, error
// x 计算非水平边在指定纵坐标处的横坐标 // x 计算非水平边在指定纵坐标处的横坐标
// 入参: y 扫描线纵坐标 // 入参: y 扫描线纵坐标
// 返回: float64 交点横坐标 // 返回: float64 交点横坐标
func (e pdfLinkEdge) x(y float64) float64 { func (e linkEdge) x(y float64) float64 {
t := (y - e.a.Y) / (e.b.Y - e.a.Y) t := (y - e.a.Y) / (e.b.Y - e.a.Y)
return e.a.X*(1-t) + e.b.X*t return e.a.X*(1-t) + e.b.X*t
} }
// appendPDFLinkQuad 收集梯形,尖端退化为三角形时细分为三个非退化四边形 // appendLinkQuad 收集梯形,尖端退化为三角形时细分为三个非退化四边形
// 入参: quads 已有四边形, quad 扫描带梯形 // 入参: quads 已有四边形, quad 扫描带梯形
// 返回: [][4]pdfgo.Point 四边形集合 // 返回: [][4]pdfgo.Point 四边形集合
func appendPDFLinkQuad(quads [][4]pdfgo.Point, quad [4]pdfgo.Point) [][4]pdfgo.Point { func appendLinkQuad(quads [][4]pdfgo.Point, quad [4]pdfgo.Point) [][4]pdfgo.Point {
epsilon := 1e-12 epsilon := 1e-12
for _, point := range quad { for _, point := range quad {
epsilon = max(epsilon, math.Abs(point.X)*1e-12, math.Abs(point.Y)*1e-12) epsilon = max(epsilon, math.Abs(point.X)*1e-12, math.Abs(point.Y)*1e-12)
@@ -365,27 +365,27 @@ func appendPDFLinkQuad(quads [][4]pdfgo.Point, quad [4]pdfgo.Point) [][4]pdfgo.P
} else if quad[2] == quad[3] { } else if quad[2] == quad[3] {
triangle = [3]pdfgo.Point{quad[0], quad[1], quad[2]} triangle = [3]pdfgo.Point{quad[0], quad[1], quad[2]}
} else { } else {
return appendValidPDFLinkQuad(quads, quad) return appendValidLinkQuad(quads, quad)
} }
return appendPDFLinkTriangle(quads, triangle) return appendLinkTriangle(quads, triangle)
} }
// appendPDFLinkTriangle 连接三角形重心和边中点,生成覆盖相同区域的凸四边形 // appendLinkTriangle 连接三角形重心和边中点,生成覆盖相同区域的凸四边形
// 入参: quads 已有四边形, triangle 逆时针三角形 // 入参: quads 已有四边形, triangle 逆时针三角形
// 返回: [][4]pdfgo.Point 四边形集合 // 返回: [][4]pdfgo.Point 四边形集合
func appendPDFLinkTriangle(quads [][4]pdfgo.Point, triangle [3]pdfgo.Point) [][4]pdfgo.Point { func appendLinkTriangle(quads [][4]pdfgo.Point, triangle [3]pdfgo.Point) [][4]pdfgo.Point {
center := pdfgo.Point{X: (triangle[0].X + triangle[1].X + triangle[2].X) / 3, Y: (triangle[0].Y + triangle[1].Y + triangle[2].Y) / 3} center := pdfgo.Point{X: (triangle[0].X + triangle[1].X + triangle[2].X) / 3, Y: (triangle[0].Y + triangle[1].Y + triangle[2].Y) / 3}
for i, a := range triangle { for i, a := range triangle {
b, c := triangle[(i+1)%3], triangle[(i+2)%3] b, c := triangle[(i+1)%3], triangle[(i+2)%3]
quads = appendValidPDFLinkQuad(quads, [4]pdfgo.Point{a, {X: (a.X + b.X) / 2, Y: (a.Y + b.Y) / 2}, center, {X: (a.X + c.X) / 2, Y: (a.Y + c.Y) / 2}}) quads = appendValidLinkQuad(quads, [4]pdfgo.Point{a, {X: (a.X + b.X) / 2, Y: (a.Y + b.Y) / 2}, center, {X: (a.X + c.X) / 2, Y: (a.Y + c.Y) / 2}})
} }
return quads return quads
} }
// pdfLinkTurn 计算相邻边的转向,消除近共线乘积相减产生的舍入噪声 // linkTurn 计算相邻边的转向,消除近共线乘积相减产生的舍入噪声
// 入参: a 前顶点, b 当前顶点, c 后顶点 // 入参: a 前顶点, b 当前顶点, c 后顶点
// 返回: float64 有向面积的两倍,数值退化时为零 // 返回: float64 有向面积的两倍,数值退化时为零
func pdfLinkTurn(a, b, c pdfgo.Point) float64 { func linkTurn(a, b, c pdfgo.Point) float64 {
u, v := (b.X-a.X)*(c.Y-b.Y), (b.Y-a.Y)*(c.X-b.X) u, v := (b.X-a.X)*(c.Y-b.Y), (b.Y-a.Y)*(c.X-b.X)
cross := u - v cross := u - v
if math.Abs(cross) <= (math.Abs(u)+math.Abs(v))*1e-12 { if math.Abs(cross) <= (math.Abs(u)+math.Abs(v))*1e-12 {
@@ -394,15 +394,51 @@ func pdfLinkTurn(a, b, c pdfgo.Point) float64 {
return cross return cross
} }
// appendValidPDFLinkQuad 仅保留有限且非退化的逆时针凸四边形 // appendValidLinkQuad 仅保留有限且非退化的逆时针凸四边形
// 入参: quads 已有四边形, quad 候选四边形 // 入参: quads 已有四边形, quad 候选四边形
// 返回: [][4]pdfgo.Point 四边形集合 // 返回: [][4]pdfgo.Point 四边形集合
func appendValidPDFLinkQuad(quads [][4]pdfgo.Point, quad [4]pdfgo.Point) [][4]pdfgo.Point { func appendValidLinkQuad(quads [][4]pdfgo.Point, quad [4]pdfgo.Point) [][4]pdfgo.Point {
for i, a := range quad { for i, a := range quad {
cross := pdfLinkTurn(a, quad[(i+1)%4], quad[(i+2)%4]) cross := linkTurn(a, quad[(i+1)%4], quad[(i+2)%4])
if !finite(cross) || cross <= 0 { if !finite(cross) || cross <= 0 {
return quads return quads
} }
} }
return append(quads, quad) return append(quads, quad)
} }
// linkGeometry 转换画布坐标为基础几何路径,保留曲线和椭圆弧参数
// 入参: path 画布路径
// 返回: basegeom.Path 左上角坐标路径
func linkGeometry(path *canvas.Path) basegeom.Path {
if path == nil {
return nil
}
result := make(basegeom.Path, 0, path.Len())
scanner := path.Scanner()
for scanner.Scan() {
end := scanner.End()
segment := basegeom.Segment{End: basegeom.Point{X: end.X, Y: -end.Y}}
switch scanner.Cmd() {
case canvas.MoveToCmd:
segment.Verb = basegeom.Move
case canvas.LineToCmd:
segment.Verb = basegeom.Line
case canvas.QuadToCmd:
control := scanner.CP1()
segment.Verb, segment.Control1 = basegeom.Quad, basegeom.Point{X: control.X, Y: -control.Y}
case canvas.CubeToCmd:
c1, c2 := scanner.CP1(), scanner.CP2()
segment.Verb = basegeom.Cubic
segment.Control1, segment.Control2 = basegeom.Point{X: c1.X, Y: -c1.Y}, basegeom.Point{X: c2.X, Y: -c2.Y}
case canvas.ArcToCmd:
segment.Verb = basegeom.Arc
segment.RadiusX, segment.RadiusY, segment.Rotation, segment.Large, segment.Sweep = scanner.Arc()
segment.Rotation, segment.Sweep = -segment.Rotation, !segment.Sweep
case canvas.CloseCmd:
segment.Verb = basegeom.ClosePath
}
result = append(result, segment)
}
return result
}
+162
View File
@@ -0,0 +1,162 @@
// 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"
"encoding/binary"
"fmt"
"math"
"net/url"
"path"
"strings"
"github.com/xiaoqidun/pdfgo"
)
// MediaFormat 获取媒体文件扩展名,URL参数和片段不参与格式判断
// 入参: file PDF文件说明
// 返回: string 媒体格式,未声明扩展名时为空
func MediaFormat(file pdfgo.FileSpecification) string {
name := strings.ReplaceAll(file.Name, "\\", "/")
if file.FileSystem == "URL" {
if location, err := url.Parse(name); err == nil {
name = location.Path
}
}
return strings.TrimPrefix(path.Ext(name), ".")
}
// SoundWAV 封装PDF采样音频,保持采样率、声道及数值,超过RIFF容量时使用RF64
// 入参: ctx 取消上下文, sound 音频流及参数
// 返回: []byte WAVE文件, error 不可表示的参数或损坏的采样数据
func SoundWAV(ctx context.Context, sound pdfgo.Sound) ([]byte, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if sound.File != nil || sound.Compression != "" {
return nil, &pdfgo.UnsupportedError{Feature: "external or compressed sound samples"}
}
if sound.Stream == nil || sound.Channels <= 0 || math.IsNaN(sound.Rate) || math.IsInf(sound.Rate, 0) || sound.Rate <= 0 {
return nil, fmt.Errorf("invalid sound sample parameters")
}
if sound.Rate != math.Trunc(sound.Rate) || sound.Rate > math.MaxUint32 || sound.Channels > math.MaxUint16 || sound.Bits != 8 && sound.Bits != 16 && sound.Bits != 24 && sound.Bits != 32 {
return nil, &pdfgo.UnsupportedError{Feature: "sound parameters in WAVE"}
}
format := uint16(1)
if sound.Encoding == "muLaw" {
format = 7
} else if sound.Encoding == "ALaw" {
format = 6
}
if format != 1 && sound.Bits != 8 {
return nil, fmt.Errorf("invalid companded sound depth")
}
width := sound.Bits / 8
align := uint64(sound.Channels) * uint64(width)
rate := uint64(sound.Rate)
if align > math.MaxUint16 || rate*align > math.MaxUint32 {
return nil, &pdfgo.UnsupportedError{Feature: "sound block alignment in WAVE"}
}
data, err := sound.Stream.DecodeContext(ctx)
if err != nil {
return nil, err
}
if uint64(len(data))%align != 0 {
return nil, fmt.Errorf("incomplete sound sample frame")
}
if format == 1 {
for offset := 0; offset < len(data); offset += width {
if offset%(1<<16) == 0 {
if err := ctx.Err(); err != nil {
return nil, err
}
}
if sound.Encoding == "Raw" && width > 1 || sound.Encoding == "Signed" && width == 1 {
data[offset] ^= 0x80
}
for left, right := offset, offset+width-1; left < right; left, right = left+1, right-1 {
data[left], data[right] = data[right], data[left]
}
}
}
formatSize := uint32(16)
extra := uint64(0)
if format == 1 && (sound.Bits > 16 || sound.Channels > 2) {
format = 0xfffe
formatSize = 40
extra = 36
} else if format != 1 {
formatSize = 18
extra = 14
}
size := uint64(36) + extra + uint64(len(data)) + uint64(len(data)%2)
large := size > math.MaxUint32
var out bytes.Buffer
put16 := func(v uint16) { _ = binary.Write(&out, binary.LittleEndian, v) }
put32 := func(v uint32) { _ = binary.Write(&out, binary.LittleEndian, v) }
put64 := func(v uint64) { _ = binary.Write(&out, binary.LittleEndian, v) }
if large {
out.WriteString("RF64")
put32(math.MaxUint32)
out.WriteString("WAVEds64")
put32(28)
put64(size + 36)
put64(uint64(len(data)))
put64(uint64(len(data)) / align)
put32(0)
} else {
out.WriteString("RIFF")
put32(uint32(size))
out.WriteString("WAVE")
}
out.WriteString("fmt ")
put32(formatSize)
put16(format)
put16(uint16(sound.Channels))
put32(uint32(rate))
put32(uint32(rate * align))
put16(uint16(align))
put16(uint16(sound.Bits))
if format != 1 {
if format == 0xfffe {
put16(22)
put16(uint16(sound.Bits))
put32(0)
out.Write([]byte{1, 0, 0, 0, 0, 0, 0x10, 0, 0x80, 0, 0, 0xaa, 0, 0x38, 0x9b, 0x71})
} else {
put16(0)
}
out.WriteString("fact")
put32(4)
if large {
put32(math.MaxUint32)
} else {
put32(uint32(uint64(len(data)) / align))
}
}
out.WriteString("data")
if large {
put32(math.MaxUint32)
} else {
put32(uint32(len(data)))
}
out.Write(data)
if len(data)%2 != 0 {
out.WriteByte(0)
}
return out.Bytes(), ctx.Err()
}
+26
View File
@@ -0,0 +1,26 @@
// 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 (
"math"
)
// finite 判断数值是否有限
// 入参: value 待检查数值
// 返回: bool 是否为有限数值
func finite(value float64) bool {
return !math.IsNaN(value) && !math.IsInf(value, 0)
}
+40
View File
@@ -0,0 +1,40 @@
// 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 (
"github.com/xiaoqidun/pdfgo"
)
// PageMatrix 将裁剪框、用户单位及页面旋转转换为OFD毫米坐标
// 入参: page PDF页面
// 返回: pdfgo.Matrix 坐标变换, float64 页面宽度, float64 页面高度
func PageMatrix(page *pdfgo.Page) (pdfgo.Matrix, float64, float64) {
b := page.CropBox
scale := 25.4 / 72 * page.UserUnit
width, height := (b.XMax-b.XMin)*scale, (b.YMax-b.YMin)*scale
m := pdfgo.Matrix{scale, 0, 0, -scale, -b.XMin * scale, b.YMax * scale}
switch page.Rotation {
case 90:
m = pdfgo.Matrix{0, 1, -1, 0, height, 0}.Mul(m)
width, height = height, width
case 180:
m = pdfgo.Matrix{-1, 0, 0, -1, width, height}.Mul(m)
case 270:
m = pdfgo.Matrix{0, -1, 1, 0, 0, width}.Mul(m)
width, height = height, width
}
return m, width, height
}
+158
View File
@@ -0,0 +1,158 @@
// 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 (
"fmt"
"github.com/xiaoqidun/pdfgo"
)
// RenditionFloatingWindow 校验浮窗必需的像素尺寸,呈现偏好不写成视频固有尺寸
// 入参: reader PDF阅读器, screen 媒体屏幕参数
// 返回: bool 是否具备合法尺寸, error 引用错误
func RenditionFloatingWindow(reader *pdfgo.Reader, screen pdfgo.Dictionary) (bool, error) {
value, err := reader.Resolve(screen["BE"])
if err != nil {
return false, err
}
preferences, _ := value.(pdfgo.Dictionary)
value, err = reader.Resolve(preferences["F"])
if err != nil {
return false, err
}
window, ok := value.(pdfgo.Dictionary)
if !ok {
return false, nil
}
value, err = reader.Resolve(window["D"])
if err != nil {
return false, err
}
size, ok := value.(pdfgo.Array)
if !ok || len(size) != 2 {
return false, nil
}
for _, part := range size {
value, err = reader.Resolve(part)
if err != nil {
return false, err
}
number, ok := value.(pdfgo.Integer)
if !ok || number < 0 {
return false, nil
}
}
return true, nil
}
// RenditionCriteria 检查媒体环境条件,未知尽力处理字段不影响候选可用性
// 入参: reader PDF阅读器, rendition 媒体呈现或选择器
// 返回: bool 是否可用, error 条件字典或引用错误
func RenditionCriteria(reader *pdfgo.Reader, rendition *pdfgo.Rendition) (bool, error) {
for _, constraints := range []struct {
dict pdfgo.Dictionary
required bool
}{{rendition.MustHonor, true}, {rendition.BestEffort, false}} {
for key, object := range constraints.dict {
if key != "C" {
if constraints.required {
return false, nil
}
continue
}
value, err := reader.Resolve(object)
if err != nil {
return false, err
}
criteria, ok := value.(pdfgo.Dictionary)
if !ok {
return false, fmt.Errorf("invalid media criteria")
}
for name, object := range criteria {
value, err := reader.Resolve(object)
if err != nil {
return false, err
}
if name == "Type" && value == pdfgo.Name("MediaCriteria") {
continue
}
switch name {
case "Type", "A", "C", "O", "S", "R", "D", "Z", "V", "P", "L":
return false, nil
default:
if constraints.required {
return false, nil
}
}
}
}
}
return true, nil
}
// RenditionParameters 合并可等价表达的播放参数,不支持的偏好不阻断播放
// 入参: reader PDF阅读器, dict 参数字典, supported 字段及可表达值的检查
// 返回: pdfgo.Dictionary 合并后的值, bool 是否可用, error 字典或引用错误
func RenditionParameters(reader *pdfgo.Reader, dict pdfgo.Dictionary, supported map[pdfgo.Name]func(pdfgo.Object) bool) (pdfgo.Dictionary, bool, error) {
values := make(pdfgo.Dictionary)
for key := range dict {
if key != "Type" && key != "MH" && key != "BE" {
return nil, false, nil
}
}
value, err := reader.Resolve(dict["MH"])
if err != nil {
return nil, false, err
}
mandatory, _ := value.(pdfgo.Dictionary)
for _, key := range []pdfgo.Name{"BE", "MH"} {
value, err := reader.Resolve(dict[key])
if err != nil {
return nil, false, err
}
if value == nil {
continue
}
parameters, ok := value.(pdfgo.Dictionary)
if !ok {
return nil, false, fmt.Errorf("invalid media playback constraints")
}
for name, object := range parameters {
if _, overridden := mandatory[name]; key == "BE" && overridden {
continue
}
check := supported[name]
if check == nil {
if key == "MH" {
return nil, false, nil
}
continue
}
resolved, err := reader.Resolve(object)
if err != nil {
return nil, false, err
}
if !check(resolved) {
if key == "MH" {
return nil, false, nil
}
continue
}
values[name] = resolved
}
}
return values, true, nil
}
@@ -12,22 +12,24 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package pdf
import "github.com/xiaoqidun/pdfgo" import (
"github.com/xiaoqidun/pdfgo"
)
// pdfShadingColorants 复用原始浓度缓冲及原生通道映射,不跨并发采样共享 // ShadingColorants 复用原始浓度缓冲及原生通道映射,不跨并发采样共享
type pdfShadingColorants struct { type ShadingColorants struct {
paint pdfgo.Paint paint pdfgo.Paint
process *pdfgo.ColorantProcess Process *pdfgo.ColorantProcess
tints []float64 tints []float64
mesh *pdfgo.MeshSampler mesh *pdfgo.MeshSampler
} }
// pdfShadingColorantSpace 返回渐变的源色料定义,不使用备用颜色推断通道 // ShadingColorantSpace 返回渐变的源色料定义,不使用备用颜色推断通道
// 入参: paint 渐变画刷 // 入参: paint 渐变画刷
// 返回: *pdfgo.ColorantSpace 原始色料定义,普通渐变为nil // 返回: *pdfgo.ColorantSpace 原始色料定义,普通渐变为nil
func pdfShadingColorantSpace(paint pdfgo.Paint) *pdfgo.ColorantSpace { func ShadingColorantSpace(paint pdfgo.Paint) *pdfgo.ColorantSpace {
switch { switch {
case paint.Axial != nil: case paint.Axial != nil:
return paint.Axial.ColorantSpace() return paint.Axial.ColorantSpace()
@@ -41,26 +43,26 @@ func pdfShadingColorantSpace(paint pdfgo.Paint) *pdfgo.ColorantSpace {
return nil return nil
} }
// pdfPrepareShadingColorants 为当前组建立原生过程采样,备用色保留原路径 // PrepareShadingColorants 为当前组建立原生过程采样,备用色保留原路径
// 入参: paint 渐变画刷, space 组空间, softMask 是否用于软蒙版 // 入参: paint 渐变画刷, space 组空间, softMask 是否用于软蒙版
// 返回: *pdfShadingColorants 独立采样状态,不可原生映射时为nil // 返回: *ShadingColorants 独立采样状态,不可原生映射时为nil
func pdfPrepareShadingColorants(paint pdfgo.Paint, space *pdfgo.ColorSpace, softMask bool) *pdfShadingColorants { func PrepareShadingColorants(paint pdfgo.Paint, space *pdfgo.ColorSpace, softMask bool) *ShadingColorants {
colorants := pdfShadingColorantSpace(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 := &pdfShadingColorants{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()
} }
return s return s
} }
// at 在归一化位置求值轴向或径向原始浓度 // At 在归一化位置求值轴向或径向原始浓度
// 入参: position 归一化渐变参数 // 入参: position 归一化渐变参数
// 返回: [4]float64 原生过程分量, error 求值错误 // 返回: [4]float64 原生过程分量, error 求值错误
func (s *pdfShadingColorants) at(position float64) ([4]float64, error) { func (s *ShadingColorants) At(position float64) ([4]float64, error) {
var err error var err error
if s.paint.Axial != nil { if s.paint.Axial != nil {
err = s.paint.Axial.ColorantValuesAt(position, s.tints) err = s.paint.Axial.ColorantValuesAt(position, s.tints)
@@ -70,25 +72,25 @@ func (s *pdfShadingColorants) at(position float64) ([4]float64, error) {
if err != nil { if err != nil {
return [4]float64{}, err return [4]float64{}, err
} }
return s.process.Convert(s.tints) return s.Process.Convert(s.tints)
} }
// pointAt 在函数定义域内求值原始浓度 // PointAt 在函数定义域内求值原始浓度
// 入参: point 函数坐标 // 入参: point 函数坐标
// 返回: [4]float64 原生过程分量, error 求值错误 // 返回: [4]float64 原生过程分量, error 求值错误
func (s *pdfShadingColorants) pointAt(point pdfgo.Point) ([4]float64, error) { func (s *ShadingColorants) PointAt(point pdfgo.Point) ([4]float64, error) {
if err := s.paint.Function.ColorantValuesAt(point, s.tints); err != nil { if err := s.paint.Function.ColorantValuesAt(point, s.tints); err != nil {
return [4]float64{}, err return [4]float64{}, err
} }
return s.process.Convert(s.tints) return s.Process.Convert(s.tints)
} }
// meshAt 插值网格原始浓度并复用函数结果 // MeshAt 插值网格原始浓度并复用函数结果
// 入参: patch 网格序号, u 横向参数, v 纵向参数 // 入参: patch 网格序号, u 横向参数, v 纵向参数
// 返回: [4]float64 原生过程分量, error 求值错误 // 返回: [4]float64 原生过程分量, error 求值错误
func (s *pdfShadingColorants) meshAt(patch int, u, v float64) ([4]float64, error) { func (s *ShadingColorants) MeshAt(patch int, u, v float64) ([4]float64, error) {
if err := s.mesh.ColorantValuesAt(patch, u, v, s.tints); err != nil { if err := s.mesh.ColorantValuesAt(patch, u, v, s.tints); err != nil {
return [4]float64{}, err return [4]float64{}, err
} }
return s.process.Convert(s.tints) return s.Process.Convert(s.tints)
} }
+270
View File
@@ -0,0 +1,270 @@
// 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 (
"context"
"errors"
"fmt"
"image"
"math"
basethreed "github.com/xiaoqidun/ofdgo/internal/base/threed"
"github.com/xiaoqidun/pdfgo"
)
// ThreeDRenderOptions 控制PDF三维视口的静态绘制,不改变注解激活状态
// 缓存与实例上限为0时采用basethreed.U3DRenderOptions的默认值
// ResolveTexture按原始URL提供外部纹理,空值不访问外部资源
// Nodes按名称替换PDF视图中的节点状态,可指定动作轨道与采样时间,不改变原始注解
// Time以秒指定模型动画的采样时刻,零值为初始时刻
type ThreeDRenderOptions struct {
Width, Height int
MaxMemoryBytes int64
MaxInstances int
ResolveTexture basethreed.U3DTextureResolver
Nodes []basethreed.U3DNodeState
Time float64
}
// RenderThreeD 按PDF相机、投影、背景与灯光绘制指定时刻的3DB区域,不执行脚本
// 返回图像不含3DB以外的注解背景,未支持的视图效果返回明确错误
// 每次从传入场景建立独立节点状态,不继承此前绘制的运行时状态
// 入参: ctx 取消上下文, model 解码后的U3D模型, source PDF三维参数, options 输出与资源限制
// 返回: *image.RGBA 不透明图像, error 参数、能力、预算或取消错误
func RenderThreeD(ctx context.Context, model *basethreed.U3DModel, source pdfgo.ThreeD, options ThreeDRenderOptions) (*image.RGBA, error) {
if ctx == nil {
return nil, fmt.Errorf("invalid PDF 3D render context")
}
if err := ctx.Err(); err != nil {
return nil, err
}
if source.Format != "U3D" {
return nil, &pdfgo.UnsupportedError{Feature: "3D model format " + string(source.Format)}
}
if len(source.Measurements) != 0 {
return nil, &pdfgo.UnsupportedError{Feature: "3D view measurement rendering"}
}
presentation := source.Presentation
if presentation.Overlay != nil {
return nil, &pdfgo.UnsupportedError{Feature: "3D view overlay"}
}
if source.Background.RGB == nil {
return nil, &pdfgo.UnsupportedError{Feature: "3D background color space"}
}
box := source.ViewBox
width, height := box.XMax-box.XMin, box.YMax-box.YMin
if !finite(width) || !finite(height) || width <= 0 || height <= 0 {
return nil, fmt.Errorf("invalid PDF 3D viewport")
}
projection := pdfgo.ThreeDProjection{Subtype: "P", Clipping: "ANF", FieldOfView: 90, PerspectiveBinding: "W"}
if source.Projection != nil {
projection = *source.Projection
}
render := basethreed.U3DRenderOptions{Width: options.Width, Height: options.Height, MaxMemoryBytes: options.MaxMemoryBytes, MaxInstances: options.MaxInstances, Background: source.Background.RGB, ResolveTexture: options.ResolveTexture, Time: options.Time}
if err := threeDRenderMode(source, &render); err != nil {
return nil, err
}
if err := threeDNodeStates(ctx, presentation.Nodes, &render); err != nil {
return nil, err
}
if err := threeDSections(ctx, presentation.CrossSections, &render); err != nil {
return nil, err
}
r, err := basethreed.NewRenderer(ctx, model, render, options.Nodes)
if err != nil {
return nil, err
}
camera, view, err := threeDCamera(ctx, model, r, source.Camera)
if err != nil {
return nil, err
}
render.View = view
camera.ScaleX, camera.ScaleY, camera.Near = 1, 1, 1
render.Camera = &camera
passes, err := r.SelectView(render)
if err != nil {
return nil, err
}
if err := r.PrepareSections(render.Sections); err != nil {
return nil, err
}
scale, err := projection.ScaleFactor(source.ViewBox, source.AnnotationBox)
if err != nil {
return nil, err
}
camera.Perspective = projection.Subtype == "P"
camera.ScaleX = scale * float64(options.Width) / width
camera.ScaleY = scale * float64(options.Height) / height
switch projection.Clipping {
case "XNF":
camera.Near = 0
if projection.Near != nil {
camera.Near = *projection.Near
}
if !finite(camera.Near) || camera.Near < 0 || camera.Perspective && camera.Near == 0 {
return nil, fmt.Errorf("invalid PDF 3D near distance")
}
if projection.Far != nil {
camera.ClipFar = true
camera.Far = *projection.Far
if !finite(camera.Far) {
return nil, fmt.Errorf("invalid PDF 3D far distance")
}
if camera.Far < camera.Near {
passes, camera.Far = nil, 0
camera.ClipFar = false
}
}
case "ANF":
minimum, maximum, found, err := r.DepthRange()
if err != nil {
return nil, err
}
if !found {
passes = nil
}
camera.Near = minimum
if camera.Perspective {
if maximum <= 0 {
passes = nil
}
camera.Near = max(1e-300, maximum*1e-9)
if minimum > 0 {
camera.Near = max(1e-300, minimum/2)
}
}
camera.Far = math.Nextafter(maximum, math.Inf(1))
if !finite(camera.Far) || camera.Far <= camera.Near {
camera.Far = 0
}
default:
return nil, fmt.Errorf("invalid PDF 3D clipping style")
}
if _, err := r.SelectView(render); err != nil {
return nil, err
}
if source.Lighting.Subtype != "Artwork" && source.Lighting.Subtype != "" {
if err := r.Reserve(uint64(len(source.Lighting.Lights)), 56); err != nil {
return nil, err
}
render.Lighting = &basethreed.U3DLighting{DiffuseOffset: source.Lighting.Ambient, Lights: make([]basethreed.U3DDirectionalLight, len(source.Lighting.Lights))}
for i, light := range source.Lighting.Lights {
if i%256 == 0 {
if err := ctx.Err(); err != nil {
return nil, err
}
}
direction := light.Direction
if light.CameraAttached {
direction = [3]float64{0, 0, 1}
}
render.Lighting.Lights[i] = basethreed.U3DDirectionalLight{Color: light.Color, Direction: direction, CameraRelative: light.CameraAttached}
}
}
return r.Render(render, passes)
}
// threeDCamera 解析PDF相机来源并保留模型视图的绘制轮次
// 入参: ctx 取消上下文, model 三维模型, renderer 场景实例, source 相机来源
// 返回: basethreed.U3DCamera 相机变换, string 视图节点, error 路径或变换错误
func threeDCamera(ctx context.Context, model *basethreed.U3DModel, renderer *basethreed.Renderer, source pdfgo.ThreeDCamera) (basethreed.U3DCamera, string, error) {
var camera basethreed.U3DCamera
selected := -1
for i, node := range model.Nodes {
if i%256 == 0 {
if err := ctx.Err(); err != nil {
return camera, "", err
}
}
if node.Kind == "View" {
selected = i
break
}
}
if source.Source == "M" {
m := source.Matrix
camera.ToWorld = [16]float64{m[0], m[1], m[2], 0, m[3], m[4], m[5], 0, m[6], m[7], m[8], 0, m[9], m[10], m[11], 1}
} else {
if source.Source != "" && source.Source != "U3D" || source.Source == "U3D" && len(source.Path) == 0 {
return camera, "", fmt.Errorf("invalid PDF 3D camera source")
}
if source.Source == "U3D" {
selected = -1
for i, node := range model.Nodes {
if i%256 == 0 {
if err := ctx.Err(); err != nil {
return camera, "", err
}
}
if node.Name == source.Path[len(source.Path)-1] {
selected = i
break
}
}
}
if selected < 0 || model.Nodes[selected].Kind != "View" {
return camera, "", fmt.Errorf("missing PDF 3D camera node")
}
var err error
camera.ToWorld, err = renderer.ViewCamera(selected, source.Path, source.Source == "U3D")
if errors.Is(err, basethreed.ErrAmbiguousCamera) {
return camera, "", &pdfgo.UnsupportedError{Feature: "ambiguous 3D camera instance"}
}
if errors.Is(err, basethreed.ErrCameraNotFound) {
return camera, "", fmt.Errorf("missing PDF 3D camera path")
}
if err != nil {
return camera, "", err
}
}
view := ""
if selected >= 0 {
view = model.Nodes[selected].Name
}
return camera, view, nil
}
// threeDNodeStates 将PDF节点参数转为场景覆盖,转换缓存计入绘制预算
// 入参: ctx 取消上下文, nodes 节点参数, options 绘制选项
// 返回: error 预算或取消错误
func threeDNodeStates(ctx context.Context, nodes []pdfgo.ThreeDNode, options *basethreed.U3DRenderOptions) error {
if len(nodes) == 0 {
return nil
}
limit := options.MaxMemoryBytes
if limit == 0 {
limit = basethreed.DefaultBytes
}
if limit <= 0 || uint64(len(nodes)) >= uint64(limit)/168 {
return fmt.Errorf("PDF 3D node memory exceeds limit")
}
options.MaxMemoryBytes = limit - int64(len(nodes))*168
options.Nodes = make([]basethreed.U3DNodeState, len(nodes))
for i, node := range nodes {
if i%256 == 0 {
if err := ctx.Err(); err != nil {
return err
}
}
state := basethreed.U3DNodeState{Name: node.Name, Opacity: node.Opacity, Visible: node.Visible}
if node.Matrix != nil {
m := node.Matrix
state.Matrix = &[16]float64{m[0], m[1], m[2], 0, m[3], m[4], m[5], 0, m[6], m[7], m[8], 0, m[9], m[10], m[11], 1}
}
options.Nodes[i] = state
}
return nil
}
@@ -12,14 +12,17 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package pdf
import "github.com/xiaoqidun/pdfgo" import (
basethreed "github.com/xiaoqidun/ofdgo/internal/base/threed"
"github.com/xiaoqidun/pdfgo"
)
// pdfThreeDRenderMode 将PDF标准绘制模式映射到通用几何绘制参数 // threeDRenderMode 将PDF标准绘制模式映射到通用几何绘制参数
// 入参: source 三维参数, options 绘制选项 // 入参: source 三维参数, options 绘制选项
// 返回: error 未支持的模式错误 // 返回: error 未支持的模式错误
func pdfThreeDRenderMode(source pdfgo.ThreeD, options *U3DRenderOptions) error { func threeDRenderMode(source pdfgo.ThreeD, options *basethreed.U3DRenderOptions) error {
mode := source.Presentation.RenderMode mode := source.Presentation.RenderMode
if mode == nil { if mode == nil {
return nil return nil
@@ -29,35 +32,35 @@ func pdfThreeDRenderMode(source pdfgo.ThreeD, options *U3DRenderOptions) error {
case "Transparent": case "Transparent":
options.Opacity = &mode.Opacity options.Opacity = &mode.Opacity
case "SolidWireframe", "TransparentWireframe": case "SolidWireframe", "TransparentWireframe":
options.Mode, options.AuxiliaryColor = U3DRenderSolidWireframe, &mode.AuxiliaryColor options.Mode, options.AuxiliaryColor = basethreed.U3DRenderSolidWireframe, &mode.AuxiliaryColor
if mode.Subtype == "TransparentWireframe" { if mode.Subtype == "TransparentWireframe" {
options.Opacity = &mode.Opacity options.Opacity = &mode.Opacity
} }
case "Wireframe": case "Wireframe":
options.Mode, options.AuxiliaryColor = U3DRenderWireframe, &mode.AuxiliaryColor options.Mode, options.AuxiliaryColor = basethreed.U3DRenderWireframe, &mode.AuxiliaryColor
case "ShadedWireframe": case "ShadedWireframe":
options.Mode = U3DRenderShadedWireframe options.Mode = basethreed.U3DRenderShadedWireframe
case "HiddenWireframe": case "HiddenWireframe":
options.Mode, options.AuxiliaryColor = U3DRenderHiddenWireframe, &mode.AuxiliaryColor options.Mode, options.AuxiliaryColor = basethreed.U3DRenderHiddenWireframe, &mode.AuxiliaryColor
case "Vertices": case "Vertices":
options.Mode, options.AuxiliaryColor = U3DRenderVertices, &mode.AuxiliaryColor options.Mode, options.AuxiliaryColor = basethreed.U3DRenderVertices, &mode.AuxiliaryColor
case "ShadedVertices": case "ShadedVertices":
options.Mode = U3DRenderShadedVertices options.Mode = basethreed.U3DRenderShadedVertices
case "BoundingBox": case "BoundingBox":
options.Mode, options.AuxiliaryColor = U3DRenderBoundingBox, &mode.AuxiliaryColor options.Mode, options.AuxiliaryColor = basethreed.U3DRenderBoundingBox, &mode.AuxiliaryColor
case "TransparentBoundingBox", "TransparentBoundingBoxOutline": case "TransparentBoundingBox", "TransparentBoundingBoxOutline":
options.Mode = U3DRenderBoundingBoxFaces options.Mode = basethreed.U3DRenderBoundingBoxFaces
options.FaceColor, options.Opacity = mode.FaceColor, &mode.Opacity options.FaceColor, options.Opacity = mode.FaceColor, &mode.Opacity
if mode.Subtype == "TransparentBoundingBoxOutline" { if mode.Subtype == "TransparentBoundingBoxOutline" {
options.Mode, options.AuxiliaryColor = U3DRenderBoundingBoxOutline, &mode.AuxiliaryColor options.Mode, options.AuxiliaryColor = basethreed.U3DRenderBoundingBoxOutline, &mode.AuxiliaryColor
} }
case "Illustration": case "Illustration":
options.Mode = U3DRenderIllustration options.Mode = basethreed.U3DRenderIllustration
options.AuxiliaryColor, options.FaceColor, options.CreaseAngle = &mode.AuxiliaryColor, mode.FaceColor, &mode.CreaseAngle options.AuxiliaryColor, options.FaceColor, options.CreaseAngle = &mode.AuxiliaryColor, mode.FaceColor, &mode.CreaseAngle
case "SolidOutline", "ShadedIllustration": case "SolidOutline", "ShadedIllustration":
options.Mode = U3DRenderSolidOutline options.Mode = basethreed.U3DRenderSolidOutline
if mode.Subtype == "ShadedIllustration" { if mode.Subtype == "ShadedIllustration" {
options.Mode = U3DRenderShadedIllustration options.Mode = basethreed.U3DRenderShadedIllustration
} }
options.AuxiliaryColor, options.CreaseAngle = &mode.AuxiliaryColor, &mode.CreaseAngle options.AuxiliaryColor, options.CreaseAngle = &mode.AuxiliaryColor, &mode.CreaseAngle
default: default:
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ofdgo package pdf
import ( import (
"context" "context"
@@ -23,10 +23,10 @@ import (
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
// pdfThreeDSections 按XYZ顺序构造剖切方向,中心保持为世界坐标固定点 // threeDSections 按XYZ顺序构造剖切方向,中心保持为世界坐标固定点
// 入参: ctx 取消上下文, sections PDF剖切面, options 绘制选项 // 入参: ctx 取消上下文, sections PDF剖切面, options 绘制选项
// 返回: error 方向、预算或取消错误 // 返回: error 方向、预算或取消错误
func pdfThreeDSections(ctx context.Context, sections []pdfgo.ThreeDCrossSection, options *U3DRenderOptions) error { func threeDSections(ctx context.Context, sections []pdfgo.ThreeDCrossSection, options *basethreed.U3DRenderOptions) error {
if len(sections) == 0 { if len(sections) == 0 {
return nil return nil
} }
@@ -38,7 +38,7 @@ func pdfThreeDSections(ctx context.Context, sections []pdfgo.ThreeDCrossSection,
return fmt.Errorf("PDF 3D section memory exceeds limit") return fmt.Errorf("PDF 3D section memory exceeds limit")
} }
options.MaxMemoryBytes = limit - int64(len(sections))*136 options.MaxMemoryBytes = limit - int64(len(sections))*136
options.Sections = make([]U3DCrossSection, len(sections)) options.Sections = make([]basethreed.U3DCrossSection, len(sections))
for i, source := range sections { for i, source := range sections {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return err return err
@@ -46,7 +46,7 @@ func pdfThreeDSections(ctx context.Context, sections []pdfgo.ThreeDCrossSection,
if source.Axis < 0 || source.Axis > 2 { if source.Axis < 0 || source.Axis > 2 {
return fmt.Errorf("invalid PDF 3D section axis") return fmt.Errorf("invalid PDF 3D section axis")
} }
section := U3DCrossSection{Center: source.Center, Color: source.Color, Opacity: source.Opacity} section := basethreed.U3DCrossSection{Center: source.Center, Color: source.Color, Opacity: source.Opacity}
section.AxisU[(source.Axis+1)%3], section.AxisV[(source.Axis+2)%3] = 1, 1 section.AxisU[(source.Axis+1)%3], section.AxisV[(source.Axis+2)%3] = 1, 1
for axis, angle := range source.Rotation { for axis, angle := range source.Rotation {
if axis == source.Axis { if axis == source.Axis {
+3 -23
View File
@@ -22,6 +22,7 @@ import (
"reflect" "reflect"
"strconv" "strconv"
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
@@ -204,7 +205,7 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
} }
} }
err = reader.WalkPages(ctx, func(index int, page *pdfgo.Page) error { err = reader.WalkPages(ctx, func(index int, page *pdfgo.Page) error {
_, width, height := pdfPageMatrix(page) _, width, height := codecpdf.PageMatrix(page)
if _, err := editor.AddPage(width, height); err != nil { if _, err := editor.AddPage(width, height); err != nil {
return err return err
} }
@@ -239,7 +240,7 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
} }
} }
err = reader.WalkPages(ctx, func(index int, page *pdfgo.Page) error { err = reader.WalkPages(ctx, func(index int, page *pdfgo.Page) error {
matrix, width, height := pdfPageMatrix(page) matrix, width, height := codecpdf.PageMatrix(page)
importer.matrix = matrix importer.matrix = matrix
importer.page = index importer.page = index
importer.rasterWarned = false importer.rasterWarned = false
@@ -345,27 +346,6 @@ func (p *pdfImporter) commitObjects() error {
return nil return nil
} }
// pdfPageMatrix 将裁剪框、用户单位及页面旋转转换为OFD毫米坐标
// 入参: page PDF页面
// 返回: pdfgo.Matrix 坐标变换, float64 页面宽度, float64 页面高度
func pdfPageMatrix(page *pdfgo.Page) (pdfgo.Matrix, float64, float64) {
b := page.CropBox
scale := 25.4 / 72 * page.UserUnit
width, height := (b.XMax-b.XMin)*scale, (b.YMax-b.YMin)*scale
m := pdfgo.Matrix{scale, 0, 0, -scale, -b.XMin * scale, b.YMax * scale}
switch page.Rotation {
case 90:
m = pdfgo.Matrix{0, 1, -1, 0, height, 0}.Mul(m)
width, height = height, width
case 180:
m = pdfgo.Matrix{-1, 0, 0, -1, width, height}.Mul(m)
case 270:
m = pdfgo.Matrix{0, -1, 1, 0, 0, width}.Mul(m)
width, height = height, width
}
return m, width, height
}
// pdfNumbers 按OFD数值格式序列化坐标 // pdfNumbers 按OFD数值格式序列化坐标
// 入参: values 数值列表 // 入参: values 数值列表
// 返回: string 空格分隔的数值 // 返回: string 空格分隔的数值
+6 -223
View File
@@ -22,7 +22,7 @@ import (
"math" "math"
"strings" "strings"
basethreed "github.com/xiaoqidun/ofdgo/internal/base/threed" codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
@@ -185,226 +185,9 @@ func (p *pdfImporter) threeDPainter(ctx context.Context, page *pdfgo.Page, sourc
// 入参: ctx 取消上下文, model 解码后的U3D模型, source PDF三维参数, options 输出与资源限制 // 入参: ctx 取消上下文, model 解码后的U3D模型, source PDF三维参数, options 输出与资源限制
// 返回: *image.RGBA 不透明图像, error 参数、能力、预算或取消错误 // 返回: *image.RGBA 不透明图像, error 参数、能力、预算或取消错误
func RenderPDFThreeD(ctx context.Context, model *U3DModel, source pdfgo.ThreeD, options PDFThreeDRenderOptions) (*image.RGBA, error) { func RenderPDFThreeD(ctx context.Context, model *U3DModel, source pdfgo.ThreeD, options PDFThreeDRenderOptions) (*image.RGBA, error) {
if ctx == nil { return codecpdf.RenderThreeD(ctx, model, source, codecpdf.ThreeDRenderOptions{
return nil, fmt.Errorf("invalid PDF 3D render context") Width: options.Width, Height: options.Height,
} MaxMemoryBytes: options.MaxMemoryBytes, MaxInstances: options.MaxInstances,
if err := ctx.Err(); err != nil { ResolveTexture: options.ResolveTexture, Nodes: options.Nodes, Time: options.Time,
return nil, err })
}
if source.Format != "U3D" {
return nil, &pdfgo.UnsupportedError{Feature: "3D model format " + string(source.Format)}
}
if len(source.Measurements) != 0 {
return nil, &pdfgo.UnsupportedError{Feature: "3D view measurement rendering"}
}
presentation := source.Presentation
if presentation.Overlay != nil {
return nil, &pdfgo.UnsupportedError{Feature: "3D view overlay"}
}
if source.Background.RGB == nil {
return nil, &pdfgo.UnsupportedError{Feature: "3D background color space"}
}
box := source.ViewBox
width, height := box.XMax-box.XMin, box.YMax-box.YMin
if !finite(width) || !finite(height) || width <= 0 || height <= 0 {
return nil, fmt.Errorf("invalid PDF 3D viewport")
}
projection := pdfgo.ThreeDProjection{Subtype: "P", Clipping: "ANF", FieldOfView: 90, PerspectiveBinding: "W"}
if source.Projection != nil {
projection = *source.Projection
}
render := U3DRenderOptions{Width: options.Width, Height: options.Height, MaxMemoryBytes: options.MaxMemoryBytes, MaxInstances: options.MaxInstances, Background: source.Background.RGB, ResolveTexture: options.ResolveTexture, Time: options.Time}
if err := pdfThreeDRenderMode(source, &render); err != nil {
return nil, err
}
if err := pdfThreeDNodeStates(ctx, presentation.Nodes, &render); err != nil {
return nil, err
}
if err := pdfThreeDSections(ctx, presentation.CrossSections, &render); err != nil {
return nil, err
}
r, err := basethreed.NewRenderer(ctx, model, render, options.Nodes)
if err != nil {
return nil, err
}
camera, view, err := pdfThreeDCamera(ctx, model, r, source.Camera)
if err != nil {
return nil, err
}
render.View = view
camera.ScaleX, camera.ScaleY, camera.Near = 1, 1, 1
render.Camera = &camera
passes, err := r.SelectView(render)
if err != nil {
return nil, err
}
if err := r.PrepareSections(render.Sections); err != nil {
return nil, err
}
scale, err := projection.ScaleFactor(source.ViewBox, source.AnnotationBox)
if err != nil {
return nil, err
}
camera.Perspective = projection.Subtype == "P"
camera.ScaleX = scale * float64(options.Width) / width
camera.ScaleY = scale * float64(options.Height) / height
switch projection.Clipping {
case "XNF":
camera.Near = 0
if projection.Near != nil {
camera.Near = *projection.Near
}
if !finite(camera.Near) || camera.Near < 0 || camera.Perspective && camera.Near == 0 {
return nil, fmt.Errorf("invalid PDF 3D near distance")
}
if projection.Far != nil {
camera.ClipFar = true
camera.Far = *projection.Far
if !finite(camera.Far) {
return nil, fmt.Errorf("invalid PDF 3D far distance")
}
if camera.Far < camera.Near {
passes, camera.Far = nil, 0
camera.ClipFar = false
}
}
case "ANF":
minimum, maximum, found, err := r.DepthRange()
if err != nil {
return nil, err
}
if !found {
passes = nil
}
camera.Near = minimum
if camera.Perspective {
if maximum <= 0 {
passes = nil
}
camera.Near = max(1e-300, maximum*1e-9)
if minimum > 0 {
camera.Near = max(1e-300, minimum/2)
}
}
camera.Far = math.Nextafter(maximum, math.Inf(1))
if !finite(camera.Far) || camera.Far <= camera.Near {
camera.Far = 0
}
default:
return nil, fmt.Errorf("invalid PDF 3D clipping style")
}
if _, err := r.SelectView(render); err != nil {
return nil, err
}
if source.Lighting.Subtype != "Artwork" && source.Lighting.Subtype != "" {
if err := r.Reserve(uint64(len(source.Lighting.Lights)), 56); err != nil {
return nil, err
}
render.Lighting = &U3DLighting{DiffuseOffset: source.Lighting.Ambient, Lights: make([]U3DDirectionalLight, len(source.Lighting.Lights))}
for i, light := range source.Lighting.Lights {
if i%256 == 0 {
if err := ctx.Err(); err != nil {
return nil, err
}
}
direction := light.Direction
if light.CameraAttached {
direction = [3]float64{0, 0, 1}
}
render.Lighting.Lights[i] = U3DDirectionalLight{Color: light.Color, Direction: direction, CameraRelative: light.CameraAttached}
}
}
return r.Render(render, passes)
}
// pdfThreeDCamera 解析PDF相机来源并保留模型视图的绘制轮次
// 入参: ctx 取消上下文, model 三维模型, renderer 场景实例, source 相机来源
// 返回: U3DCamera 相机变换, string 视图节点, error 路径或变换错误
func pdfThreeDCamera(ctx context.Context, model *U3DModel, renderer *basethreed.Renderer, source pdfgo.ThreeDCamera) (U3DCamera, string, error) {
var camera U3DCamera
selected := -1
for i, node := range model.Nodes {
if i%256 == 0 {
if err := ctx.Err(); err != nil {
return camera, "", err
}
}
if node.Kind == "View" {
selected = i
break
}
}
if source.Source == "M" {
m := source.Matrix
camera.ToWorld = [16]float64{m[0], m[1], m[2], 0, m[3], m[4], m[5], 0, m[6], m[7], m[8], 0, m[9], m[10], m[11], 1}
} else {
if source.Source != "" && source.Source != "U3D" || source.Source == "U3D" && len(source.Path) == 0 {
return camera, "", fmt.Errorf("invalid PDF 3D camera source")
}
if source.Source == "U3D" {
selected = -1
for i, node := range model.Nodes {
if i%256 == 0 {
if err := ctx.Err(); err != nil {
return camera, "", err
}
}
if node.Name == source.Path[len(source.Path)-1] {
selected = i
break
}
}
}
if selected < 0 || model.Nodes[selected].Kind != "View" {
return camera, "", fmt.Errorf("missing PDF 3D camera node")
}
var err error
camera.ToWorld, err = renderer.ViewCamera(selected, source.Path, source.Source == "U3D")
if errors.Is(err, basethreed.ErrAmbiguousCamera) {
return camera, "", &pdfgo.UnsupportedError{Feature: "ambiguous 3D camera instance"}
}
if errors.Is(err, basethreed.ErrCameraNotFound) {
return camera, "", fmt.Errorf("missing PDF 3D camera path")
}
if err != nil {
return camera, "", err
}
}
view := ""
if selected >= 0 {
view = model.Nodes[selected].Name
}
return camera, view, nil
}
// pdfThreeDNodeStates 将PDF节点参数转为场景覆盖,转换缓存计入绘制预算
// 入参: ctx 取消上下文, nodes 节点参数, options 绘制选项
// 返回: error 预算或取消错误
func pdfThreeDNodeStates(ctx context.Context, nodes []pdfgo.ThreeDNode, options *U3DRenderOptions) error {
if len(nodes) == 0 {
return nil
}
limit := options.MaxMemoryBytes
if limit == 0 {
limit = basethreed.DefaultBytes
}
if limit <= 0 || uint64(len(nodes)) >= uint64(limit)/168 {
return fmt.Errorf("PDF 3D node memory exceeds limit")
}
options.MaxMemoryBytes = limit - int64(len(nodes))*168
options.Nodes = make([]U3DNodeState, len(nodes))
for i, node := range nodes {
if i%256 == 0 {
if err := ctx.Err(); err != nil {
return err
}
}
state := U3DNodeState{Name: node.Name, Opacity: node.Opacity, Visible: node.Visible}
if node.Matrix != nil {
m := node.Matrix
state.Matrix = &[16]float64{m[0], m[1], m[2], 0, m[3], m[4], m[5], 0, m[6], m[7], m[8], 0, m[9], m[10], m[11], 1}
}
options.Nodes[i] = state
}
return nil
} }
+2 -47
View File
@@ -15,11 +15,9 @@
package ofdgo package ofdgo
import ( import (
"context"
"crypto/sha256"
"encoding/binary"
"math" "math"
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
@@ -79,54 +77,11 @@ func pdfClipContainsBoundary(path pdfgo.Path, matrix pdfgo.Matrix, boundary Box)
return left < right && top < bottom && boundary.X >= left && boundary.Y >= top && boundary.X+boundary.W <= right && boundary.Y+boundary.H <= bottom return left < right && top < bottom && boundary.X >= left && boundary.Y >= top && boundary.X+boundary.W <= right && boundary.Y+boundary.H <= bottom
} }
// pdfClipPathKey 按路径完整内容与变换生成键,不依赖可变切片的地址
// 入参: ctx 取消上下文, path 裁剪路径, matrix 页面变换
// 返回: [32]byte 内容摘要, error 取消错误
func pdfClipPathKey(ctx context.Context, path pdfgo.Path, matrix pdfgo.Matrix) ([32]byte, error) {
hash := sha256.New()
var value [16]byte
for _, number := range matrix {
binary.LittleEndian.PutUint64(value[:8], math.Float64bits(number))
hash.Write(value[:8])
}
binary.LittleEndian.PutUint64(value[:8], uint64(len(path.Segments)))
value[8] = 0
if path.EvenOdd {
value[8] = 1
}
hash.Write(value[:9])
for _, segment := range path.Segments {
if err := ctx.Err(); err != nil {
return [32]byte{}, err
}
binary.LittleEndian.PutUint64(value[:8], uint64(len(segment.Operator)))
binary.LittleEndian.PutUint64(value[8:], uint64(len(segment.Points)))
hash.Write(value[:])
hash.Write([]byte(segment.Operator))
for index, point := range segment.Points {
if index&255 == 0 {
if err := ctx.Err(); err != nil {
return [32]byte{}, err
}
}
binary.LittleEndian.PutUint64(value[:8], math.Float64bits(point.X))
binary.LittleEndian.PutUint64(value[8:], math.Float64bits(point.Y))
hash.Write(value[:])
}
}
if err := ctx.Err(); err != nil {
return [32]byte{}, err
}
var key [32]byte
hash.Sum(key[:0])
return key, nil
}
// clipPath 复用页面内重复路径编码,按估算字节预算淘汰且不缓存错误 // clipPath 复用页面内重复路径编码,按估算字节预算淘汰且不缓存错误
// 入参: path 裁剪路径 // 入参: path 裁剪路径
// 返回: pdfClipPath 变换后的边界及编码, error 取消错误 // 返回: pdfClipPath 变换后的边界及编码, error 取消错误
func (p *pdfImporter) clipPath(path pdfgo.Path) (pdfClipPath, error) { func (p *pdfImporter) clipPath(path pdfgo.Path) (pdfClipPath, error) {
key, err := pdfClipPathKey(p.ctx, path, p.matrix) key, err := codecpdf.ClipPathKey(p.ctx, path, p.matrix)
if err != nil { if err != nil {
return pdfClipPath{}, err return pdfClipPath{}, err
} }
+11 -28
View File
@@ -22,6 +22,7 @@ import (
"runtime" "runtime"
basefont "github.com/xiaoqidun/ofdgo/internal/base/font" basefont "github.com/xiaoqidun/ofdgo/internal/base/font"
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
@@ -93,7 +94,7 @@ type pdfCompositeColorSampler struct {
values map[float64][4]float64 values map[float64][4]float64
position pdfgo.GradientPosition position pdfgo.GradientPosition
converter pdfgo.ColorConverter converter pdfgo.ColorConverter
colorants *pdfShadingColorants colorants *codecpdf.ShadingColorants
prepared bool prepared bool
converted bool converted bool
} }
@@ -417,7 +418,7 @@ func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error, widg
if externalNames == nil { if externalNames == nil {
externalNames = make(map[string]bool) externalNames = make(map[string]bool)
} }
name := pdfEmbeddedFontName(mark.Font.Name) name := codecpdf.EmbeddedFontName(mark.Font.Name)
if !externalNames[basefont.NormalizeName(name)] { if !externalNames[basefont.NormalizeName(name)] {
for _, candidate := range fontExactCandidateNames(name) { for _, candidate := range fontExactCandidateNames(name) {
externalNames[basefont.NormalizeName(candidate)] = true externalNames[basefont.NormalizeName(candidate)] = true
@@ -441,7 +442,7 @@ func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error, widg
if err := p.halftone(mark.Style); err != nil { if err := p.halftone(mark.Style); err != nil {
return err return err
} }
if len(mark.Glyphs) > 1 && (mark.Object != nil && !mark.Object.Knockout || mark.Style.AlphaIsShape || mark.Style.Fill.Alpha != 1 || mark.Style.Stroke.Alpha != 1 || mark.Style.SoftMask != nil || !pdfNormalBlend(mark.Style.BlendMode) || mark.Mode%4 == 1 || mark.Mode%4 == 2) { if len(mark.Glyphs) > 1 && (mark.Object != nil && !mark.Object.Knockout || mark.Style.AlphaIsShape || mark.Style.Fill.Alpha != 1 || mark.Style.Stroke.Alpha != 1 || mark.Style.SoftMask != nil || !codecpdf.NormalBlend(mark.Style.BlendMode) || mark.Mode%4 == 1 || mark.Mode%4 == 2) {
for i := range mark.Glyphs { for i := range mark.Glyphs {
glyph := mark glyph := mark
glyph.Glyphs, glyph.Positions, glyph.Clip = mark.Glyphs[i:i+1], mark.Positions[i:i+1], nil glyph.Glyphs, glyph.Positions, glyph.Clip = mark.Glyphs[i:i+1], mark.Positions[i:i+1], nil
@@ -514,7 +515,7 @@ func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error, widg
return nil, err return nil, err
} }
for font := range embeddedFonts { for font := range embeddedFonts {
if !externalNames[basefont.NormalizeName(pdfEmbeddedFontName(font.Name))] { if !externalNames[basefont.NormalizeName(codecpdf.EmbeddedFontName(font.Name))] {
continue continue
} }
if err := p.ctx.Err(); err != nil { if err := p.ctx.Err(); err != nil {
@@ -640,7 +641,7 @@ func (p *pdfImporter) opaque(n pdfCompositeNode, space *pdfgo.ColorSpace) (bool,
} }
if n.group != nil { if n.group != nil {
g := n.group g := n.group
if g.Knockout && n.textObject == nil || g.Alpha != 1 || g.SoftMask != nil || !pdfNormalBlend(g.BlendMode) || g.ColorSpace != nil && !g.ColorSpace.Equal(space) { if g.Knockout && n.textObject == nil || g.Alpha != 1 || g.SoftMask != nil || !codecpdf.NormalBlend(g.BlendMode) || g.ColorSpace != nil && !g.ColorSpace.Equal(space) {
return false, nil return false, nil
} }
for _, child := range n.children { for _, child := range n.children {
@@ -654,7 +655,7 @@ func (p *pdfImporter) opaque(n pdfCompositeNode, space *pdfgo.ColorSpace) (bool,
if n.image != nil && style.FillOverprint { if n.image != nil && style.FillOverprint {
return false, nil return false, nil
} }
if style.SoftMask != nil || !pdfNormalBlend(style.BlendMode) || fill && style.Fill.Alpha != 1 || stroke && style.Stroke.Alpha != 1 { if style.SoftMask != nil || !codecpdf.NormalBlend(style.BlendMode) || fill && style.Fill.Alpha != 1 || stroke && style.Stroke.Alpha != 1 {
return false, nil return false, nil
} }
if fill && style.FillOverprint && pdfOverprintNeedsSeparation(style.Fill) || stroke && style.StrokeOverprint && pdfOverprintNeedsSeparation(style.Stroke) { if fill && style.FillOverprint && pdfOverprintNeedsSeparation(style.Fill) || stroke && style.StrokeOverprint && pdfOverprintNeedsSeparation(style.Stroke) {
@@ -675,7 +676,7 @@ func (p *pdfImporter) opaque(n pdfCompositeNode, space *pdfgo.ColorSpace) (bool,
func (n pdfCompositeNode) direct() bool { func (n pdfCompositeNode) direct() bool {
if g := n.group; g != nil { if g := n.group; g != nil {
single := n.textObject != nil && len(n.children) == 1 && (n.children[0].glyph != nil || n.children[0].text != nil && len(n.children[0].text.Glyphs) == 1) single := n.textObject != nil && len(n.children) == 1 && (n.children[0].glyph != nil || n.children[0].text != nil && len(n.children[0].text.Glyphs) == 1)
if g.Knockout && !single || !pdfNormalBlend(g.BlendMode) || g.ColorSpace != nil && !g.ColorSpace.SRGBEquivalent() { if g.Knockout && !single || !codecpdf.NormalBlend(g.BlendMode) || g.ColorSpace != nil && !g.ColorSpace.SRGBEquivalent() {
return false return false
} }
for _, child := range n.children { for _, child := range n.children {
@@ -701,7 +702,7 @@ func (n pdfCompositeNode) direct() bool {
if n.image != nil && s.FillOverprint { if n.image != nil && s.FillOverprint {
return false return false
} }
return pdfNormalBlend(s.BlendMode) && !(fill && s.FillOverprint && pdfOverprintNeedsSeparation(s.Fill) || stroke && s.StrokeOverprint && pdfOverprintNeedsSeparation(s.Stroke)) return codecpdf.NormalBlend(s.BlendMode) && !(fill && s.FillOverprint && pdfOverprintNeedsSeparation(s.Fill) || stroke && s.StrokeOverprint && pdfOverprintNeedsSeparation(s.Stroke))
} }
// style 获取图元实际使用的图形状态 // style 获取图元实际使用的图形状态
@@ -932,12 +933,12 @@ func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompo
} }
values, err = c.transfers[i].Apply(values[:], device, "") values, err = c.transfers[i].Apply(values[:], device, "")
if err == nil { if err == nil {
rgb, err = pdfCompositeOutputColor(space, values) rgb, err = codecpdf.OutputColor(space, values)
} }
} else if c.transfers != nil { } else if c.transfers != nil {
rgb, err = space.RGB(pixel.values[:space.Components()], "RelativeColorimetric") rgb, err = space.RGB(pixel.values[:space.Components()], "RelativeColorimetric")
} else { } else {
rgb, err = pdfCompositeOutputColor(space, pixel.values) rgb, err = codecpdf.OutputColor(space, pixel.values)
} }
if err != nil { if err != nil {
return err return err
@@ -965,21 +966,3 @@ func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompo
} }
return p.appendRasterGroup(output, box, 1) return p.appendRasterGroup(output, box, 1)
} }
// pdfCompositeOutputColor 将合成结果映射到OFD输出,与保留的设备四色对象使用相同显示换算
// 入参: space 合成空间, values 非预乘分量
// 返回: [3]float64 显示颜色, error 校准颜色转换错误
func pdfCompositeOutputColor(space *pdfgo.ColorSpace, values [4]float64) ([3]float64, error) {
if space.Model == "DeviceCMYK" && !space.Calibrated() {
black := 1 - values[3]
return [3]float64{(1 - values[0]) * black, (1 - values[1]) * black, (1 - values[2]) * black}, nil
}
return space.RGB(values[:space.Components()], "RelativeColorimetric")
}
// pdfNormalBlend 判断混合模式是否为普通覆盖
// 入参: mode 混合模式
// 返回: bool 是否普通覆盖
func pdfNormalBlend(mode pdfgo.Name) bool {
return mode == "" || mode == "Normal" || mode == "Compatible"
}
+7 -6
View File
@@ -21,6 +21,7 @@ import (
"math" "math"
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
@@ -162,11 +163,11 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
} }
} }
} }
var colorants *pdfShadingColorants var colorants *codecpdf.ShadingColorants
if node.image == nil || node.image.Image.ImageMask { if node.image == nil || node.image.Image.ImageMask {
colorants = pdfPrepareShadingColorants(paint, space, c.softMask) colorants = codecpdf.PrepareShadingColorants(paint, space, c.softMask)
if colorants != nil && overprint { if colorants != nil && overprint {
processMask, processOverprint = colorants.process.Mask(), true processMask, processOverprint = colorants.Process.Mask(), true
} }
} }
step := 25.4 / c.importer.rasterDPI step := 25.4 / c.importer.rasterDPI
@@ -449,7 +450,7 @@ func (c *pdfCompositor) drawGroup(node pdfCompositeNode, pixels []pdfCompositePi
} }
region := c.pixelBounds(b) region := c.pixelBounds(b)
blocked := c.transferBlocked blocked := c.transferBlocked
c.transferBlocked = blocked || g.Alpha != 1 || g.SoftMask != nil || !pdfNormalBlend(g.BlendMode) c.transferBlocked = blocked || g.Alpha != 1 || g.SoftMask != nil || !codecpdf.NormalBlend(g.BlendMode)
defer func() { c.transferBlocked = blocked }() defer func() { c.transferBlocked = blocked }()
if c.transfers != nil && node.textObject != nil && !c.transferBlocked { if c.transfers != nil && node.textObject != nil && !c.transferBlocked {
for _, child := range node.children { for _, child := range node.children {
@@ -469,7 +470,7 @@ func (c *pdfCompositor) drawGroup(node pdfCompositeNode, pixels []pdfCompositePi
} }
sameSpace := space.Equal(parent) sameSpace := space.Equal(parent)
sharedInitial := !g.Isolated && !g.Knockout && sameSpace sharedInitial := !g.Isolated && !g.Knockout && sameSpace
if sharedInitial && g.Alpha == 1 && g.SoftMask == nil && pdfNormalBlend(g.BlendMode) { if sharedInitial && g.Alpha == 1 && g.SoftMask == nil && codecpdf.NormalBlend(g.BlendMode) {
return c.draw(node.children, pixels, space) return c.draw(node.children, pixels, space)
} }
initial := pixels initial := pixels
@@ -1015,7 +1016,7 @@ func (s *pdfCompositeColorSampler) color(point pdfgo.Point) ([4]float64, bool, e
var intent pdfgo.Name var intent pdfgo.Name
var err error var err error
if s.colorants != nil { if s.colorants != nil {
values, err = s.colorants.at(position) values, err = s.colorants.At(position)
} else if g != nil { } else if g != nil {
values, err = g.ValuesAt(position) values, err = g.ValuesAt(position)
source, intent = g.Space, g.Intent source, intent = g.Space, g.Intent
+5 -4
View File
@@ -18,6 +18,7 @@ import (
"fmt" "fmt"
"math" "math"
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
@@ -237,8 +238,8 @@ func (p *pdfImporter) directCompositeNode(node pdfCompositeNode, space *pdfgo.Co
if i == 0 && !fill || i == 1 && !stroke { if i == 0 && !fill || i == 1 && !stroke {
continue continue
} }
if native := pdfPrepareShadingColorants(paint, space, false); native != nil { if native := codecpdf.PrepareShadingColorants(paint, space, false); native != nil {
if !native.process.AlternateEquivalent() || i == 0 && style.FillOverprint || i == 1 && style.StrokeOverprint { if !native.Process.AlternateEquivalent() || i == 0 && style.FillOverprint || i == 1 && style.StrokeOverprint {
return false, nil return false, nil
} }
} }
@@ -287,7 +288,7 @@ func (p *pdfImporter) directCompositeNode(node pdfCompositeNode, space *pdfgo.Co
} }
} }
directGroup := false directGroup := false
if group := node.group; group != nil && !group.Knockout && group.Alpha == 1 && group.SoftMask == nil && pdfNormalBlend(group.BlendMode) && (group.ColorSpace == nil || group.ColorSpace.SRGBEquivalent()) { if group := node.group; group != nil && !group.Knockout && group.Alpha == 1 && group.SoftMask == nil && codecpdf.NormalBlend(group.BlendMode) && (group.ColorSpace == nil || group.ColorSpace.SRGBEquivalent()) {
directGroup = true directGroup = true
} }
opaque, err := p.opaque(node, space) opaque, err := p.opaque(node, space)
@@ -367,7 +368,7 @@ func (p *pdfImporter) processOverprint(nodes []pdfCompositeNode) (bool, error) {
} }
for i, paint := range []pdfgo.Paint{style.Fill, style.Stroke} { for i, paint := range []pdfgo.Paint{style.Fill, style.Stroke} {
if i == 0 && fill && style.FillOverprint || i == 1 && stroke && style.StrokeOverprint { if i == 0 && fill && style.FillOverprint || i == 1 && stroke && style.StrokeOverprint {
if native := pdfPrepareShadingColorants(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 := pdfCompositeNativePaint(paint, &pdfgo.ColorSpace{Model: "DeviceCMYK"}, style.RenderingIntent)
+2 -1
View File
@@ -18,6 +18,7 @@ import (
"fmt" "fmt"
"math" "math"
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
@@ -52,7 +53,7 @@ func (p *pdfImporter) destinationBounds(page *pdfgo.Page) (Box, error) {
p.page = index p.page = index
defer func() { p.page = previous }() defer func() { p.page = previous }()
} }
matrix, width, height := pdfPageMatrix(page) matrix, width, height := codecpdf.PageMatrix(page)
editor := NewEditor() editor := NewEditor()
editor.SetRenderBackends(p.editor.Backends()) editor.SetRenderBackends(p.editor.Backends())
editor.SetFontDirs(p.editor.fontDirs...) editor.SetFontDirs(p.editor.fontDirs...)
+8 -322
View File
@@ -15,15 +15,13 @@
package ofdgo package ofdgo
import ( import (
"bytes"
"context" "context"
"crypto/sha256" "crypto/sha256"
"encoding/binary"
"fmt" "fmt"
"unicode/utf16"
"unicode/utf8" "unicode/utf8"
basefont "github.com/xiaoqidun/ofdgo/internal/base/font" basefont "github.com/xiaoqidun/ofdgo/internal/base/font"
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
@@ -36,12 +34,6 @@ type pdfImportedFont struct {
repairLimit uint16 repairLimit uint16
} }
// pdfFontRepair 保存字体封装修复范围,不混用字形度量与位置索引
type pdfFontRepair struct {
metricsLimit uint16
locations bool
}
// shapePDFText 使用导入字体整形注释文字,将毫米坐标约定转换为PDF文字空间方向 // shapePDFText 使用导入字体整形注释文字,将毫米坐标约定转换为PDF文字空间方向
// 入参: ctx 取消上下文, source 源字体, text 单行原文, rightToLeft 段落方向 // 入参: ctx 取消上下文, source 源字体, text 单行原文, rightToLeft 段落方向
// 返回: []pdfgo.ShapedGlyph 千分之一字号的定位字形, error 字体、整形或取消错误 // 返回: []pdfgo.ShapedGlyph 千分之一字号的定位字形, error 字体、整形或取消错误
@@ -86,13 +78,13 @@ func (p *pdfImporter) shapePDFText(ctx context.Context, source *pdfgo.Font, text
// 入参: source 外部字体, glyphs 当前文字字形 // 入参: source 外部字体, glyphs 当前文字字形
// 返回: *pdfImportedFont 可复用字体,无可靠匹配时为空 // 返回: *pdfImportedFont 可复用字体,无可靠匹配时为空
func (p *pdfImporter) embeddedTextFont(source *pdfgo.Font, glyphs []pdfgo.Glyph) *pdfImportedFont { func (p *pdfImporter) embeddedTextFont(source *pdfgo.Font, glyphs []pdfgo.Glyph) *pdfImportedFont {
name := pdfEmbeddedFontName(source.Name) name := codecpdf.EmbeddedFontName(source.Name)
if name == "" || len(glyphs) == 0 { if name == "" || len(glyphs) == 0 {
return nil return nil
} }
var matched *pdfImportedFont var matched *pdfImportedFont
for _, imported := range p.fonts { for _, imported := range p.fonts {
if pdfEmbeddedFontName(imported.resource.Font.FontName) != name { if codecpdf.EmbeddedFontName(imported.resource.Font.FontName) != name {
continue continue
} }
if matched != nil && matched.checksum != imported.checksum { if matched != nil && matched.checksum != imported.checksum {
@@ -116,21 +108,6 @@ func (p *pdfImporter) embeddedTextFont(source *pdfgo.Font, glyphs []pdfgo.Glyph)
return matched return matched
} }
// pdfEmbeddedFontName 去除标准六位大写子集前缀,保留字体名称和样式后缀
// 入参: name PDF或内嵌字体名称
// 返回: string 原字体名称
func pdfEmbeddedFontName(name string) string {
if len(name) <= 7 || name[6] != '+' {
return name
}
for i := range 6 {
if name[i] < 'A' || name[i] > 'Z' {
return name
}
}
return name[7:]
}
// importedFont 按原PDF字体复用包装和解析结果,失败结果不进入缓存 // importedFont 按原PDF字体复用包装和解析结果,失败结果不进入缓存
// 入参: source 源字体 // 入参: source 源字体
// 返回: *pdfImportedFont 只读导入字体, error 字体程序或后端错误 // 返回: *pdfImportedFont 只读导入字体, error 字体程序或后端错误
@@ -148,11 +125,11 @@ func (p *pdfImporter) importedFont(source *pdfgo.Font) (*pdfImportedFont, error)
type1 = &parsed type1 = &parsed
result.type1Glyphs = parsed.GlyphIDs() result.type1Glyphs = parsed.GlyphIDs()
} }
program, repair, err := pdfFontProgram(source, type1) program, repair, err := codecpdf.FontProgram(source, type1)
if err != nil { if err != nil {
return nil, fmt.Errorf("PDF font %s program: %w", source.Name, err) return nil, fmt.Errorf("PDF font %s program: %w", source.Name, err)
} }
if repair.locations && p.warning == nil { if repair.Locations && p.warning == nil {
return nil, fmt.Errorf("PDF font %s program: %w", source.Name, basefont.ErrTTInvalidLocations) return nil, fmt.Errorf("PDF font %s program: %w", source.Name, basefont.ErrTTInvalidLocations)
} }
if p.editor.backends.FontResources == nil { if p.editor.backends.FontResources == nil {
@@ -172,8 +149,8 @@ func (p *pdfImporter) importedFont(source *pdfgo.Font) (*pdfImportedFont, error)
if _, ok := result.metrics.(FontOutlines); !ok { if _, ok := result.metrics.(FontOutlines); !ok {
return nil, fmt.Errorf("PDF font backend does not provide glyph outlines") return nil, fmt.Errorf("PDF font backend does not provide glyph outlines")
} }
result.checksum, result.repairLimit = sha256.Sum256(result.resource.Data), repair.metricsLimit result.checksum, result.repairLimit = sha256.Sum256(result.resource.Data), repair.MetricsLimit
if repair.locations { if repair.Locations {
p.warning(pdfgo.Diagnostic{Message: fmt.Sprintf("PDF font %s has invalid glyph locations; index rebuilt with original glyph IDs", source.Name)}) p.warning(pdfgo.Diagnostic{Message: fmt.Sprintf("PDF font %s has invalid glyph locations; index rebuilt with original glyph IDs", source.Name)})
} }
if p.fonts == nil { if p.fonts == nil {
@@ -187,7 +164,7 @@ func (p *pdfImporter) importedFont(source *pdfgo.Font) (*pdfImportedFont, error)
// 入参: source 未嵌入的PDF字体 // 入参: source 未嵌入的PDF字体
// 返回: *pdfImportedFont 可用的符号字体,无匹配时为空, error 字体或取消错误 // 返回: *pdfImportedFont 可用的符号字体,无匹配时为空, error 字体或取消错误
func (p *pdfImporter) symbolFont(source *pdfgo.Font) (*pdfImportedFont, error) { func (p *pdfImporter) symbolFont(source *pdfgo.Font) (*pdfImportedFont, error) {
name := pdfEmbeddedFontName(source.Name) name := codecpdf.EmbeddedFontName(source.Name)
if source.Subtype != "Type1" || name != "Symbol" && name != "ZapfDingbats" { if source.Subtype != "Type1" || name != "Symbol" && name != "ZapfDingbats" {
return nil, nil return nil, nil
} }
@@ -231,294 +208,3 @@ func (p *pdfImporter) symbolFont(source *pdfgo.Font) (*pdfImportedFont, error) {
p.symbolFonts[source] = result p.symbolFonts[source] = result
return result, nil return result, nil
} }
// pdfFontProgram 为PDF子集字体补齐封装表,保留字形轮廓和编号
// 无效字符表按PDF已解码字形重建,不改变字符对应的字形编号
// 入参: source PDF字体, type1 已解析的Type1程序,nil时按需解析
// 返回: []byte 封装后的字体数据, pdfFontRepair 字体修复范围, error 错误信息
func pdfFontProgram(source *pdfgo.Font, type1 *basefont.Type1Program) ([]byte, pdfFontRepair, error) {
var repair pdfFontRepair
program := source.Program
if source.ProgramType == "FontFile" {
if type1 == nil {
parsed, err := basefont.ParseType1Program(program)
if err != nil {
return nil, repair, err
}
type1 = &parsed
}
var err error
program, err = type1.ToCFF(pdfFontIdentity(source))
if err != nil {
return nil, repair, err
}
}
bareCFF := len(program) >= 4 && program[0] == 1 && program[1] == 0 && program[2] >= 4 && program[3] >= 1 && program[3] <= 4
if source.ProgramType == "Type1C" || source.ProgramType == "CIDFontType0C" || bareCFF {
var err error
program, _, err = basefont.WrapCFFToOTF(program)
if err != nil {
return nil, repair, err
}
}
tables, err := basefont.FileTables(program, 0)
if err != nil {
return nil, repair, err
}
if len(tables["head"]) < 54 || len(tables["maxp"]) < 6 || len(tables["hhea"]) < 36 || len(tables["hmtx"]) == 0 {
return nil, repair, fmt.Errorf("embedded PDF font lacks required metrics")
}
count := binary.BigEndian.Uint16(tables["maxp"][4:6])
if count == 0 {
return nil, repair, fmt.Errorf("embedded PDF font has no glyphs")
}
repair.locations, err = basefont.NormalizeTrueTypeLocations(tables)
if err != nil {
return nil, repair, err
}
changed := repair.locations
if head := tables["head"]; len(head) == 56 && binary.BigEndian.Uint32(head[:4]) == 0x00010000 && head[54] == 0 && head[55] == 0 {
tables["head"] = head[:54]
changed = true
}
if cff := tables["CFF "]; len(cff) != 0 {
sanitized, err := basefont.SanitizeCFF(cff)
if err != nil {
return nil, repair, err
}
normalized, err := basefont.NormalizeCFFCharstringsAt(sanitized, binary.BigEndian.Uint16(tables["head"][18:20]))
if err != nil {
return nil, repair, err
}
if !bytes.Equal(normalized, cff) {
tables["CFF "] = normalized
changed = true
}
}
metrics := int(binary.BigEndian.Uint16(tables["hhea"][34:36]))
if metrics == 0 || metrics > int(count) {
return nil, repair, fmt.Errorf("invalid embedded PDF font metric count")
}
metricLength := 4*metrics + 2*(int(count)-metrics)
if len(tables["hmtx"]) < metricLength {
if len(tables["hmtx"]) < 4*metrics || len(tables["hmtx"])%2 != 0 {
return nil, repair, fmt.Errorf("incomplete embedded PDF font metrics: have %d, need %d", len(tables["hmtx"]), metricLength)
}
limit := metrics + (len(tables["hmtx"])-4*metrics)/2
missing, err := pdfMissingLeftBearings(tables, limit, int(count))
if err != nil {
return nil, repair, err
}
tables["hmtx"] = append(bytes.Clone(tables["hmtx"]), missing...)
repair.metricsLimit = uint16(limit)
changed = true
}
if len(tables["hmtx"]) > metricLength {
tables["hmtx"] = tables["hmtx"][:metricLength]
changed = true
}
invalidCmap := false
if source.Subtype == "TrueType" || source.Subtype == "Type0" {
mapping := basefont.ParseCmapMappings(tables["cmap"])
for char, glyph := range mapping {
if glyph >= count {
delete(mapping, char)
invalidCmap = true
}
}
if invalidCmap && source.Subtype == "TrueType" {
tables["cmap"] = basefont.BuildCmapTable(count, mapping)
changed = true
}
}
if len(tables["cmap"]) == 0 || source.Subtype == "Type0" && invalidCmap || (source.Subtype == "Type0" || source.Subtype == "TrueType") && pdfCmapNeedsRebuild(tables["cmap"]) {
mapping, err := source.GlyphMap()
if err != nil {
return nil, repair, err
}
for _, glyph := range mapping {
if glyph >= count {
return nil, repair, fmt.Errorf("PDF font glyph %d exceeds glyph count %d", glyph, count)
}
}
basefont.AddPackedGlyphMapping(mapping, count)
tables["cmap"] = basefont.BuildCmapTable(count, mapping)
changed = true
}
if len(basefont.NamesFromTable(tables["name"])) == 0 {
fontName := pdfFontIdentity(source)
name := utf16.Encode([]rune(fontName))
if len(name) > 32767 {
return nil, repair, fmt.Errorf("PDF font name exceeds name table capacity")
}
data := make([]byte, 42+len(name)*2)
binary.BigEndian.PutUint16(data[2:], 3)
binary.BigEndian.PutUint16(data[4:], 42)
for n, id := range []uint16{1, 4, 6} {
record := data[6+n*12:]
binary.BigEndian.PutUint16(record, 3)
binary.BigEndian.PutUint16(record[2:], 1)
binary.BigEndian.PutUint16(record[4:], 0x409)
binary.BigEndian.PutUint16(record[6:], id)
binary.BigEndian.PutUint16(record[8:], uint16(len(name)*2))
}
for n, v := range name {
binary.BigEndian.PutUint16(data[42+n*2:], v)
}
tables["name"] = data
changed = true
}
if len(tables["post"]) == 0 {
tables["post"] = basefont.BuildPostTable()
changed = true
}
if len(tables["OS/2"]) == 0 {
hhea := tables["hhea"]
tables["OS/2"] = basefont.BuildOS2TableWithMetrics(int16(binary.BigEndian.Uint16(hhea[4:6])), int16(binary.BigEndian.Uint16(hhea[6:8])))
changed = true
}
for _, tag := range []string{"cvt ", "fpgm", "prep"} {
if data, ok := tables[tag]; ok && len(data) == 0 {
delete(tables, tag)
changed = true
}
}
if !changed && !pdfSFNTMissingPadding(program) {
return program, repair, nil
}
result, err := basefont.SerializeOTF(tables)
return result, repair, err
}
// pdfFontIdentity 返回字体名称或由原始程序生成的稳定封装标识
// 入参: source PDF字体
// 返回: string 字体标识
func pdfFontIdentity(source *pdfgo.Font) string {
if source.Name != "" {
return source.Name
}
checksum := sha256.Sum256(source.Program)
return fmt.Sprintf("PDF-%x", checksum[:28])
}
// pdfCmapNeedsRebuild 检查字体封装字符表是否存在结构错误或交叠区段
// 入参: data cmap表数据
// 返回: bool 是否需要按PDF显式字形编号重建
func pdfCmapNeedsRebuild(data []byte) bool {
if len(data) < 4 || binary.BigEndian.Uint16(data) != 0 {
return true
}
count := int(binary.BigEndian.Uint16(data[2:]))
if count == 0 || count > (len(data)-4)/8 {
return true
}
for index := range count {
offset := uint64(binary.BigEndian.Uint32(data[8+8*index:]))
if offset < uint64(4+8*count) || offset > uint64(len(data)) || uint64(len(data))-offset < 2 {
return true
}
sub := data[int(offset):]
if binary.BigEndian.Uint16(sub) == 6 {
if len(sub) < 10 {
return true
}
length := int(binary.BigEndian.Uint16(sub[2:]))
first, glyphs := int(binary.BigEndian.Uint16(sub[6:])), int(binary.BigEndian.Uint16(sub[8:]))
if length%2 != 0 || length > len(sub) || 10+2*glyphs > length || first+glyphs > 65536 {
return true
}
continue
}
if binary.BigEndian.Uint16(sub) != 4 {
continue
}
if len(sub) < 16 {
return true
}
length := int(binary.BigEndian.Uint16(sub[2:]))
segmentBytes := int(binary.BigEndian.Uint16(sub[6:]))
segments := segmentBytes / 2
if segmentBytes == 0 || segmentBytes%2 != 0 || length%2 != 0 || length > len(sub) || 16+8*segments > length {
return true
}
glyphStart := 16 + 8*segments
previous := -1
for i := 0; i < segments; i++ {
end := int(binary.BigEndian.Uint16(sub[14+2*i:]))
start := int(binary.BigEndian.Uint16(sub[16+2*segments+2*i:]))
if end < start || start <= previous || i == segments-1 && (start != 65535 || end != 65535) {
return true
}
previous = end
position := 16 + 6*segments + 2*i
offset := int(binary.BigEndian.Uint16(sub[position:]))
if offset != 0 && (offset%2 != 0 || position+offset < glyphStart || position+offset+2*(end-start+1) > length) {
return true
}
}
}
return false
}
// pdfSFNTMissingPadding 判断字体表目录是否引用了未写入的末尾对齐字节
// 入参: program OpenType字体数据
// 返回: bool 是否需要重新封装
func pdfSFNTMissingPadding(program []byte) bool {
if len(program) < 12 {
return true
}
count := int(binary.BigEndian.Uint16(program[4:6]))
if count > (len(program)-12)/16 {
return true
}
for index := range count {
record := program[12+16*index:]
offset := int(binary.BigEndian.Uint32(record[8:12]))
length := int(binary.BigEndian.Uint32(record[12:16]))
if offset > len(program) || length > len(program)-offset || (4-length%4)%4 > len(program)-offset-length {
return true
}
}
return false
}
// pdfMissingLeftBearings 从轮廓边界恢复缺失的尾部左侧边距
// 入参: tables 字体表, start 首个缺失字形, count 字形总数
// 返回: []byte 补齐的hmtx数据, error 无法读取轮廓
func pdfMissingLeftBearings(tables map[string][]byte, start, count int) ([]byte, error) {
head, loca, glyf := tables["head"], tables["loca"], tables["glyf"]
if len(head) < 54 || len(glyf) == 0 {
return nil, fmt.Errorf("embedded PDF font lacks glyph outlines for missing metrics")
}
format := int(int16(binary.BigEndian.Uint16(head[50:52])))
entrySize := 2
if format == 1 {
entrySize = 4
} else if format != 0 {
return nil, fmt.Errorf("invalid embedded PDF font location format")
}
if len(loca) < (count+1)*entrySize {
return nil, fmt.Errorf("incomplete embedded PDF font glyph locations")
}
location := func(index int) int {
if format == 0 {
return int(binary.BigEndian.Uint16(loca[index*2:])) * 2
}
return int(binary.BigEndian.Uint32(loca[index*4:]))
}
result := make([]byte, 2*(count-start))
for index := start; index < count; index++ {
from, to := location(index), location(index+1)
if from > to || to > len(glyf) {
return nil, fmt.Errorf("invalid embedded PDF font glyph location")
}
if from == to {
continue
}
if to-from < 10 {
return nil, fmt.Errorf("incomplete embedded PDF font glyph outline")
}
copy(result[(index-start)*2:], glyf[from+2:from+4])
}
return result, nil
}
+6 -43
View File
@@ -19,57 +19,20 @@ import (
"crypto/sha256" "crypto/sha256"
"encoding/binary" "encoding/binary"
"fmt" "fmt"
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
"image/color" "image/color"
"math" "math"
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
// pdfGeometryPath 直接转换PDF路径坐标,不经文本编码或曲线展平 // pdfGeometryPath 转换PDF路径并衔接OFD几何类型
// 入参: ctx 取消上下文, source PDF路径, matrix 坐标变换 // 入参: ctx 取消上下文, source PDF路径, matrix 坐标变换
// 返回: GeometryPath 独立几何路径, error 指令、坐标或取消错误 // 返回: GeometryPath 独立几何路径, error 指令、坐标或取消错误
func pdfGeometryPath(ctx context.Context, source pdfgo.Path, matrix pdfgo.Matrix) (GeometryPath, error) { func pdfGeometryPath(ctx context.Context, source pdfgo.Path, matrix pdfgo.Matrix) (GeometryPath, error) {
if err := ctx.Err(); err != nil { path, err := codecpdf.GeometryPath(ctx, source, matrix)
return nil, err return GeometryPath(path), err
}
path := make(GeometryPath, len(source.Segments))
for i, segment := range source.Segments {
if err := ctx.Err(); err != nil {
return nil, err
}
var count int
switch segment.Operator {
case "M":
path[i].Verb, count = GeometryMove, 1
case "L":
path[i].Verb, count = GeometryLine, 1
case "B":
path[i].Verb, count = GeometryCubic, 3
case "C":
path[i].Verb = GeometryClose
default:
return nil, fmt.Errorf("invalid PDF path operator %q", segment.Operator)
}
if len(segment.Points) != count {
return nil, fmt.Errorf("invalid PDF path operands for %s", segment.Operator)
}
var points [3]Point
for j, point := range segment.Points {
point = matrix.Apply(point)
points[j] = Point{X: point.X, Y: point.Y}
}
if count != 0 {
path[i].End = points[count-1]
}
if count == 3 {
path[i].Control1, path[i].Control2 = points[0], points[1]
}
}
if err := path.validate(); err != nil {
return nil, err
}
return path, ctx.Err()
} }
// compileCoverage 直接编译无文字裁剪的填充覆盖,其他语义交给配置的页面编译器 // compileCoverage 直接编译无文字裁剪的填充覆盖,其他语义交给配置的页面编译器
@@ -200,7 +163,7 @@ func (p *pdfImporter) coverageClip(paths []pdfgo.Path, geometry GeometryBackend)
binary.LittleEndian.PutUint64(size[8:], math.Float64bits(p.pageHeight)) binary.LittleEndian.PutUint64(size[8:], math.Float64bits(p.pageHeight))
hash.Write(size[:]) hash.Write(size[:])
for _, path := range paths { for _, path := range paths {
key, err := pdfClipPathKey(p.ctx, path, p.matrix) key, err := codecpdf.ClipPathKey(p.ctx, path, p.matrix)
if err != nil { if err != nil {
return nil, err return nil, err
} }
+3 -2
View File
@@ -22,6 +22,7 @@ import (
"math" "math"
"reflect" "reflect"
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
@@ -634,7 +635,7 @@ func (p *pdfImporter) maskClip(mask *pdfgo.SoftMask) (pdfgo.Path, error) {
return &pdfgo.UnsupportedError{Feature: "image mask graphic"} return &pdfgo.UnsupportedError{Feature: "image mask graphic"}
} }
visitor.Group = func(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error { visitor.Group = func(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error {
if mark.Alpha != 1 || mark.SoftMask != nil || !pdfNormalBlend(mark.BlendMode) || mark.ColorSpace != nil && !mark.ColorSpace.Equal(mask.ColorSpace) { if mark.Alpha != 1 || mark.SoftMask != nil || !codecpdf.NormalBlend(mark.BlendMode) || mark.ColorSpace != nil && !mark.ColorSpace.Equal(mask.ColorSpace) {
return &pdfgo.UnsupportedError{Feature: "transparent mask group"} return &pdfgo.UnsupportedError{Feature: "transparent mask group"}
} }
return walk(visitor) return walk(visitor)
@@ -644,7 +645,7 @@ func (p *pdfImporter) maskClip(mask *pdfgo.SoftMask) (pdfgo.Path, error) {
return err return err
} }
paint := mark.Style.Fill paint := mark.Style.Fill
if !mark.Fill || mark.Stroke || paint.Alpha != 1 || paint.CMYK != nil || paint.Axial != nil || paint.Radial != nil || paint.Function != nil || paint.Mesh != nil || paint.Tiling != nil || paint.Shading != nil || paint.Space != nil && paint.Space.Calibrated() || paint.RGB[0] != paint.RGB[1] || paint.RGB[1] != paint.RGB[2] || mark.Style.SoftMask != nil || !pdfNormalBlend(mark.Style.BlendMode) { if !mark.Fill || mark.Stroke || paint.Alpha != 1 || paint.CMYK != nil || paint.Axial != nil || paint.Radial != nil || paint.Function != nil || paint.Mesh != nil || paint.Tiling != nil || paint.Shading != nil || paint.Space != nil && paint.Space.Calibrated() || paint.RGB[0] != paint.RGB[1] || paint.RGB[1] != paint.RGB[2] || mark.Style.SoftMask != nil || !codecpdf.NormalBlend(mark.Style.BlendMode) {
return &pdfgo.UnsupportedError{Feature: "nonbinary mask graphic"} return &pdfgo.UnsupportedError{Feature: "nonbinary mask graphic"}
} }
visible, err := opacity(paint.RGB[0]) visible, err := opacity(paint.RGB[0])
+6 -73
View File
@@ -15,12 +15,10 @@
package ofdgo package ofdgo
import ( import (
"bytes"
"crypto/sha256"
"image" "image"
"image/color" "image/color"
"reflect"
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
@@ -39,85 +37,20 @@ type pdfImageContent struct {
id string id string
} }
// pdfImageValueBudget 限制图像描述的检查深度和保留字节数
type pdfImageValueBudget struct {
nodes int
bytes int
}
// imageContentKey 对无间接依赖的小图像生成内容键,超大图像仍沿用流缓存 // imageContentKey 对无间接依赖的小图像生成内容键,超大图像仍沿用流缓存
// 入参: source 图像描述, key 流缓存键 // 入参: source 图像描述, key 流缓存键
// 返回: pdfImageContentKey 内容键, bool 是否可复用, error 取消错误 // 返回: pdfImageContentKey 内容键, bool 是否可复用, error 取消错误
func (p *pdfImporter) imageContentKey(source *pdfgo.Image, key pdfImageKey) (pdfImageContentKey, bool, error) { func (p *pdfImporter) imageContentKey(source *pdfgo.Image, key pdfImageKey) (pdfImageContentKey, bool, error) {
if source == nil || source.Stream == nil || len(source.Stream.Data) > 1<<20 { digest, reusable, err := codecpdf.ImageDigest(p.ctx, source)
return pdfImageContentKey{}, false, nil if !reusable {
return pdfImageContentKey{}, reusable, err
} }
budget := pdfImageValueBudget{nodes: 256, bytes: 65536} return pdfImageContentKey{data: digest, intent: key.intent, process: key.process, tint: key.tint, size: key.size}, true, err
for _, value := range []pdfgo.Object{source.Stream.Dictionary, source.ColorSpace, source.Decode, source.Mask, source.SoftMask} {
if !pdfImageDirectValue(value, &budget) {
return pdfImageContentKey{}, false, p.ctx.Err()
}
}
hash := sha256.New()
data := source.Stream.Data
for len(data) != 0 {
if err := p.ctx.Err(); err != nil {
return pdfImageContentKey{}, false, err
}
n := min(len(data), 65536)
hash.Write(data[:n])
data = data[n:]
}
result := pdfImageContentKey{intent: key.intent, process: key.process, tint: key.tint, size: key.size}
hash.Sum(result.data[:0])
return result, true, p.ctx.Err()
}
// pdfImageDirectValue 检查图像描述是否完全独立,拒绝间接引用和嵌套流
// 入参: value 描述值, budget 描述检查预算
// 返回: bool 是否不依赖阅读器资源
func pdfImageDirectValue(value pdfgo.Object, budget *pdfImageValueBudget) bool {
if budget.nodes <= 0 || budget.bytes < 64 {
return false
}
budget.nodes--
budget.bytes -= 64
switch v := value.(type) {
case nil, pdfgo.Boolean, pdfgo.Integer, pdfgo.Real:
return true
case pdfgo.Name:
budget.bytes -= len(v)
return budget.bytes >= 0
case pdfgo.String:
budget.bytes -= len(v)
return budget.bytes >= 0
case pdfgo.Array:
for _, child := range v {
if !pdfImageDirectValue(child, budget) {
return false
}
}
return true
case pdfgo.Dictionary:
for key, child := range v {
budget.bytes -= len(key)
if !pdfImageDirectValue(child, budget) {
return false
}
}
return true
}
return false
} }
// matches 核对完整编码与有效图像属性,不以摘要相同代替内容相同 // matches 核对完整编码与有效图像属性,不以摘要相同代替内容相同
// 入参: source 待复用图像 // 入参: source 待复用图像
// 返回: bool 是否可使用同一图像资源 // 返回: bool 是否可使用同一图像资源
func (c pdfImageContent) matches(source *pdfgo.Image) bool { func (c pdfImageContent) matches(source *pdfgo.Image) bool {
s := c.source return codecpdf.SameImage(c.source, source)
return s.Width == source.Width && s.Height == source.Height && s.BitsPerComponent == source.BitsPerComponent &&
s.ImageMask == source.ImageMask && s.Interpolate == source.Interpolate &&
bytes.Equal(s.Stream.Data, source.Stream.Data) && reflect.DeepEqual(s.Stream.Dictionary, source.Stream.Dictionary) &&
reflect.DeepEqual(s.ColorSpace, source.ColorSpace) && reflect.DeepEqual(s.Decode, source.Decode) &&
reflect.DeepEqual(s.Mask, source.Mask) && reflect.DeepEqual(s.SoftMask, source.SoftMask)
} }
+2 -34
View File
@@ -17,18 +17,11 @@ package ofdgo
import ( import (
"fmt" "fmt"
"image" "image"
"image/color"
"math"
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
// pdfNativeImage 按源像素输出原生过程色,不分配完整显示颜色缓冲
type pdfNativeImage struct {
source *pdfgo.ImageProcess
space *pdfgo.ColorSpace
}
// imageProcessModel 选择与页面原生过程色一致的图像输出,已等价的备用色沿用现有编码路径 // imageProcessModel 选择与页面原生过程色一致的图像输出,已等价的备用色沿用现有编码路径
// 入参: source 源图像 // 入参: source 源图像
// 返回: pdfgo.Name 原生输出空间,无需重映射时为空, error 色料解析错误 // 返回: pdfgo.Name 原生输出空间,无需重映射时为空, error 色料解析错误
@@ -61,7 +54,7 @@ func (p *pdfImporter) imageProcess(source *pdfgo.Image, model pdfgo.Name) (image
if process == nil { if process == nil {
return nil, fmt.Errorf("invalid native PDF image process space %s", model) return nil, fmt.Errorf("invalid native PDF image process space %s", model)
} }
return &pdfNativeImage{source: process, space: space}, nil return &codecpdf.NativeImage{Source: process, Space: space}, nil
} }
// imageProcess 复用同组原生采样,仅在需要备用颜色时保留完整分量 // imageProcess 复用同组原生采样,仅在需要备用颜色时保留完整分量
@@ -89,28 +82,3 @@ func (c *pdfCompositeCache) imageProcess(importer *pdfImporter, source *pdfgo.Im
c.imageProcesses[key] = process c.imageProcesses[key] = process
return process, nil return process, nil
} }
// ColorModel 保留16位原生分量与源透明度
// 返回: color.Model 非预乘颜色模型
func (s *pdfNativeImage) ColorModel() color.Model { return color.NRGBA64Model }
// Bounds 返回源图像的完整采样边界
// 返回: image.Rectangle 原始像素边界
func (s *pdfNativeImage) Bounds() image.Rectangle { return s.source.Bounds() }
// At 获取按OFD输出映射的原生颜色
// 入参: x 横坐标, y 纵坐标
// 返回: color.Color 非预乘颜色
func (s *pdfNativeImage) At(x, y int) color.Color { return s.NRGBA64At(x, y) }
// NRGBA64At 按原始精度转换设备过程色,不进行页面采样或备用变换
// 入参: x 横坐标, y 纵坐标
// 返回: color.NRGBA64 非预乘颜色
func (s *pdfNativeImage) NRGBA64At(x, y int) color.NRGBA64 {
if !image.Pt(x, y).In(s.Bounds()) {
return color.NRGBA64{}
}
values, alpha := s.source.ValuesAt(x, y)
rgb, _ := pdfCompositeOutputColor(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))}
}
+2 -1
View File
@@ -20,6 +20,7 @@ import (
"fmt" "fmt"
"maps" "maps"
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
@@ -497,7 +498,7 @@ func (p *pdfImporter) linkAction(annotation pdfgo.Annotation, strict bool, curre
} }
return nil, fmt.Errorf("PDF destination page missing") return nil, fmt.Errorf("PDF destination page missing")
} }
matrix, _, _ := pdfPageMatrix(targetPage) matrix, _, _ := codecpdf.PageMatrix(targetPage)
if destination.Mode == "FitBH" || destination.Mode == "FitBV" { if destination.Mode == "FitBH" || destination.Mode == "FitBV" {
return nil, &pdfgo.UnsupportedError{Feature: "destination mode " + string(destination.Mode)} return nil, &pdfgo.UnsupportedError{Feature: "destination mode " + string(destination.Mode)}
} }
+4 -140
View File
@@ -15,17 +15,15 @@
package ofdgo package ofdgo
import ( import (
"bytes"
"context" "context"
"encoding/binary"
"errors" "errors"
"fmt" "fmt"
"math" "math"
"net/url"
"path" "path"
"reflect" "reflect"
"strings" "strings"
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
@@ -134,9 +132,9 @@ func (p *pdfImporter) soundResource(ctx context.Context, sound pdfgo.Sound) (str
if err != nil || !available { if err != nil || !available {
return "", err return "", err
} }
format = pdfMediaFormat(*sound.File) format = codecpdf.MediaFormat(*sound.File)
} else { } else {
data, err = pdfSoundWAV(ctx, sound) data, err = codecpdf.SoundWAV(ctx, sound)
} }
if err != nil { if err != nil {
return "", err return "", err
@@ -220,7 +218,7 @@ func (p *pdfImporter) interactiveAnnotation(ctx context.Context, page *pdfgo.Pag
if config.Subtype == "Sound" { if config.Subtype == "Sound" {
kind = "Audio" kind = "Audio"
} }
id, err := p.editor.AddMedia(kind, pdfMediaFormat(file), data) id, err := p.editor.AddMedia(kind, codecpdf.MediaFormat(file), data)
if err != nil { if err != nil {
return err return err
} }
@@ -315,137 +313,3 @@ func (p *pdfImporter) mediaData(ctx context.Context, file pdfgo.FileSpecificatio
} }
return data, true, nil return data, true, nil
} }
// pdfMediaFormat 获取媒体文件扩展名,URL参数和片段不参与格式判断
// 入参: file PDF文件说明
// 返回: string 媒体格式,未声明扩展名时为空
func pdfMediaFormat(file pdfgo.FileSpecification) string {
name := strings.ReplaceAll(file.Name, "\\", "/")
if file.FileSystem == "URL" {
if location, err := url.Parse(name); err == nil {
name = location.Path
}
}
return strings.TrimPrefix(path.Ext(name), ".")
}
// pdfSoundWAV 封装PDF采样音频,保持采样率、声道及数值,超过RIFF容量时使用RF64
// 入参: ctx 取消上下文, sound 音频流及参数
// 返回: []byte WAVE文件, error 不可表示的参数或损坏的采样数据
func pdfSoundWAV(ctx context.Context, sound pdfgo.Sound) ([]byte, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if sound.File != nil || sound.Compression != "" {
return nil, &pdfgo.UnsupportedError{Feature: "external or compressed sound samples"}
}
if sound.Stream == nil || sound.Channels <= 0 || math.IsNaN(sound.Rate) || math.IsInf(sound.Rate, 0) || sound.Rate <= 0 {
return nil, fmt.Errorf("invalid sound sample parameters")
}
if sound.Rate != math.Trunc(sound.Rate) || sound.Rate > math.MaxUint32 || sound.Channels > math.MaxUint16 || sound.Bits != 8 && sound.Bits != 16 && sound.Bits != 24 && sound.Bits != 32 {
return nil, &pdfgo.UnsupportedError{Feature: "sound parameters in WAVE"}
}
format := uint16(1)
if sound.Encoding == "muLaw" {
format = 7
} else if sound.Encoding == "ALaw" {
format = 6
}
if format != 1 && sound.Bits != 8 {
return nil, fmt.Errorf("invalid companded sound depth")
}
width := sound.Bits / 8
align := uint64(sound.Channels) * uint64(width)
rate := uint64(sound.Rate)
if align > math.MaxUint16 || rate*align > math.MaxUint32 {
return nil, &pdfgo.UnsupportedError{Feature: "sound block alignment in WAVE"}
}
data, err := sound.Stream.DecodeContext(ctx)
if err != nil {
return nil, err
}
if uint64(len(data))%align != 0 {
return nil, fmt.Errorf("incomplete sound sample frame")
}
if format == 1 {
for offset := 0; offset < len(data); offset += width {
if offset%(1<<16) == 0 {
if err := ctx.Err(); err != nil {
return nil, err
}
}
if sound.Encoding == "Raw" && width > 1 || sound.Encoding == "Signed" && width == 1 {
data[offset] ^= 0x80
}
for left, right := offset, offset+width-1; left < right; left, right = left+1, right-1 {
data[left], data[right] = data[right], data[left]
}
}
}
formatSize := uint32(16)
extra := uint64(0)
if format == 1 && (sound.Bits > 16 || sound.Channels > 2) {
format = 0xfffe
formatSize = 40
extra = 36
} else if format != 1 {
formatSize = 18
extra = 14
}
size := uint64(36) + extra + uint64(len(data)) + uint64(len(data)%2)
large := size > math.MaxUint32
var out bytes.Buffer
put16 := func(v uint16) { _ = binary.Write(&out, binary.LittleEndian, v) }
put32 := func(v uint32) { _ = binary.Write(&out, binary.LittleEndian, v) }
put64 := func(v uint64) { _ = binary.Write(&out, binary.LittleEndian, v) }
if large {
out.WriteString("RF64")
put32(math.MaxUint32)
out.WriteString("WAVEds64")
put32(28)
put64(size + 36)
put64(uint64(len(data)))
put64(uint64(len(data)) / align)
put32(0)
} else {
out.WriteString("RIFF")
put32(uint32(size))
out.WriteString("WAVE")
}
out.WriteString("fmt ")
put32(formatSize)
put16(format)
put16(uint16(sound.Channels))
put32(uint32(rate))
put32(uint32(rate * align))
put16(uint16(align))
put16(uint16(sound.Bits))
if format != 1 {
if format == 0xfffe {
put16(22)
put16(uint16(sound.Bits))
put32(0)
out.Write([]byte{1, 0, 0, 0, 0, 0, 0x10, 0, 0x80, 0, 0, 0xaa, 0, 0x38, 0x9b, 0x71})
} else {
put16(0)
}
out.WriteString("fact")
put32(4)
if large {
put32(math.MaxUint32)
} else {
put32(uint32(uint64(len(data)) / align))
}
}
out.WriteString("data")
if large {
put32(math.MaxUint32)
} else {
put32(uint32(len(data)))
}
out.Write(data)
if len(data)%2 != 0 {
out.WriteByte(0)
}
return out.Bytes(), ctx.Err()
}
+3 -2
View File
@@ -21,6 +21,7 @@ import (
"slices" "slices"
"sync" "sync"
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
@@ -380,7 +381,7 @@ func (c *pdfCompositor) mesh(mesh *pdfgo.MeshGradient, space *pdfgo.ColorSpace,
} }
sampleRows := func(start, end int) error { sampleRows := func(start, end int) error {
colors := make(map[[4]float64][4]float64) colors := make(map[[4]float64][4]float64)
colorants := pdfPrepareShadingColorants(pdfgo.Paint{Mesh: mesh}, space, c.softMask) colorants := codecpdf.PrepareShadingColorants(pdfgo.Paint{Mesh: mesh}, space, c.softMask)
var sampler *pdfgo.MeshSampler var sampler *pdfgo.MeshSampler
if colorants == nil { if colorants == nil {
sampler = mesh.NewSampler() sampler = mesh.NewSampler()
@@ -404,7 +405,7 @@ func (c *pdfCompositor) mesh(mesh *pdfgo.MeshGradient, space *pdfgo.ColorSpace,
if sample.patch != 0 { if sample.patch != 0 {
var err error var err error
if colorants != nil { if colorants != nil {
values, err = colorants.meshAt(sample.patch-1, sample.u, sample.v) values, err = colorants.MeshAt(sample.patch-1, sample.u, sample.v)
} else { } else {
values, err = sampler.ValuesAt(sample.patch-1, sample.u, sample.v) values, err = sampler.ValuesAt(sample.patch-1, sample.u, sample.v)
} }
+2 -1
View File
@@ -19,6 +19,7 @@ import (
"errors" "errors"
"fmt" "fmt"
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
@@ -26,7 +27,7 @@ import (
// 入参: ctx 取消上下文, file 文件说明, attachment 是否作为附件保存 // 入参: ctx 取消上下文, file 文件说明, attachment 是否作为附件保存
// 返回: string 媒体或附件标识,缺少外部资源时为空, error 读取、注册或取消错误 // 返回: string 媒体或附件标识,缺少外部资源时为空, error 读取、注册或取消错误
func (p *pdfImporter) movieResource(ctx context.Context, file pdfgo.FileSpecification, attachment bool) (string, error) { func (p *pdfImporter) movieResource(ctx context.Context, file pdfgo.FileSpecification, attachment bool) (string, error) {
return p.mediaResource(ctx, file, "Video", pdfMediaFormat(file), attachment) return p.mediaResource(ctx, file, "Video", codecpdf.MediaFormat(file), attachment)
} }
// movieAnnotation 保留视频外观及可表示的播放动作,无法等价转换时按策略处理 // movieAnnotation 保留视频外观及可表示的播放动作,无法等价转换时按策略处理
+2 -42
View File
@@ -27,7 +27,7 @@ import (
"strconv" "strconv"
"unicode/utf8" "unicode/utf8"
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
@@ -46,46 +46,6 @@ type pdfImageResource struct {
id string id string
} }
// pdfStencilImage 按当前填充色读取模板图像,不分配整幅彩色样本
type pdfStencilImage struct {
source image.Image
fill color.NRGBA64
}
// ColorModel 保留填充色和模板的有效采样精度
// 返回: color.Model 非预乘颜色模型
func (s *pdfStencilImage) ColorModel() color.Model {
model := s.source.ColorModel()
if s.fill.R%257 == 0 && s.fill.G%257 == 0 && s.fill.B%257 == 0 && (model == color.GrayModel || model == color.NRGBAModel || model == color.RGBAModel) {
return color.NRGBAModel
}
return color.NRGBA64Model
}
// Bounds 返回模板图像的采样边界
// 返回: image.Rectangle 图像边界
func (s *pdfStencilImage) Bounds() image.Rectangle { return s.source.Bounds() }
// At 以模板灰度计算填充覆盖率,不重复变换颜色
// 入参: x 横向坐标, y 纵向坐标
// 返回: color.Color 非预乘颜色
func (s *pdfStencilImage) At(x, y int) color.Color {
return s.NRGBA64At(x, y)
}
// NRGBA64At 直接读取模板覆盖率,保留填充色的有效精度
// 入参: x 横向坐标, y 纵向坐标
// 返回: color.NRGBA64 非预乘颜色
func (s *pdfStencilImage) NRGBA64At(x, y int) color.NRGBA64 {
if !image.Pt(x, y).In(s.Bounds()) {
return color.NRGBA64{}
}
gray := baseimage.NRGBA64At(s.source, x, y)
pixel := s.fill
pixel.A = uint16(65535 - gray.R)
return pixel
}
// image 转换图像样本,不将整页或其他对象栅格化 // image 转换图像样本,不将整页或其他对象栅格化
// 有损编码复用已解码像素,候选容量超限时沿用原编码路径 // 有损编码复用已解码像素,候选容量超限时沿用原编码路径
// 入参: mark PDF图像绘制信息 // 入参: mark PDF图像绘制信息
@@ -203,7 +163,7 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
} }
} }
if source.ImageMask { if source.ImageMask {
decoded = &pdfStencilImage{source: decoded, fill: key.tint} decoded = &codecpdf.StencilImage{Source: decoded, Fill: key.tint}
} }
data := jbig2Original data := jbig2Original
if len(data) == 0 { if len(data) == 0 {
+7 -143
View File
@@ -22,6 +22,7 @@ import (
"mime" "mime"
"strings" "strings"
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
@@ -106,7 +107,7 @@ func (p *pdfImporter) renditionLinkActions(object pdfgo.Object, source pdfgo.Ann
return nil, err return nil, err
} }
if kind == "Video" { if kind == "Video" {
parameters, _, err := p.renditionParameters(selected.Screen, map[pdfgo.Name]func(pdfgo.Object) bool{ parameters, _, err := codecpdf.RenditionParameters(p.reader, selected.Screen, map[pdfgo.Name]func(pdfgo.Object) bool{
"W": func(value pdfgo.Object) bool { return value == pdfgo.Integer(0) || value == pdfgo.Integer(3) }, "W": func(value pdfgo.Object) bool { return value == pdfgo.Integer(0) || value == pdfgo.Integer(3) },
}) })
if err != nil { if err != nil {
@@ -155,7 +156,7 @@ func (p *pdfImporter) renditionLinkActions(object pdfgo.Object, source pdfgo.Ann
// 入参: selected 已选择的原生呈现, kind 媒体类型, playback 播放目标 // 入参: selected 已选择的原生呈现, kind 媒体类型, playback 播放目标
// 返回: string 资源标识,不可用时为空, error 文件或资源错误 // 返回: string 资源标识,不可用时为空, error 文件或资源错误
func (p *pdfImporter) renditionResource(selected *pdfgo.Rendition, kind string, playback pdfgo.RenditionPlayback) (string, error) { func (p *pdfImporter) renditionResource(selected *pdfgo.Rendition, kind string, playback pdfgo.RenditionPlayback) (string, error) {
format := pdfMediaFormat(*selected.Clip.File) format := codecpdf.MediaFormat(*selected.Clip.File)
if format == "" { if format == "" {
mediaType, _, _ := mime.ParseMediaType(selected.Clip.ContentType) mediaType, _, _ := mime.ParseMediaType(selected.Clip.ContentType)
format = strings.TrimPrefix(strings.SplitN(mediaType, "/", 2)[1], "x-") format = strings.TrimPrefix(strings.SplitN(mediaType, "/", 2)[1], "x-")
@@ -183,7 +184,7 @@ func (p *pdfImporter) renditionActionLoss(reason string, strict bool) error {
// 返回: Action 播放动作, string 媒体类别, bool 是否可用, error 参数错误 // 返回: Action 播放动作, string 媒体类别, bool 是否可用, error 参数错误
func (p *pdfImporter) nativeRendition(rendition *pdfgo.Rendition) (Action, string, bool, error) { func (p *pdfImporter) nativeRendition(rendition *pdfgo.Rendition) (Action, string, bool, error) {
result := Action{Event: "CLICK"} result := Action{Event: "CLICK"}
viable, err := p.renditionCriteria(rendition) viable, err := codecpdf.RenditionCriteria(p.reader, rendition)
if err != nil || !viable { if err != nil || !viable {
return result, "", false, err return result, "", false, err
} }
@@ -217,7 +218,7 @@ func (p *pdfImporter) nativeRendition(rendition *pdfgo.Rendition) (Action, strin
return result, "", false, nil return result, "", false, nil
} }
audio := strings.HasPrefix(mediaType, "audio/") audio := strings.HasPrefix(mediaType, "audio/")
play, viable, err := p.renditionParameters(rendition.Play, map[pdfgo.Name]func(pdfgo.Object) bool{ play, viable, err := codecpdf.RenditionParameters(p.reader, rendition.Play, map[pdfgo.Name]func(pdfgo.Object) bool{
"V": func(value pdfgo.Object) bool { "V": func(value pdfgo.Object) bool {
number, ok := value.(pdfgo.Integer) number, ok := value.(pdfgo.Integer)
return ok && number >= 0 && number <= 100 && (audio || number == 100) return ok && number >= 0 && number <= 100 && (audio || number == 100)
@@ -231,14 +232,14 @@ func (p *pdfImporter) nativeRendition(rendition *pdfgo.Rendition) (Action, strin
if err != nil || !viable { if err != nil || !viable {
return result, "", false, err return result, "", false, err
} }
screen, viable, err := p.renditionParameters(rendition.Screen, map[pdfgo.Name]func(pdfgo.Object) bool{ screen, viable, err := codecpdf.RenditionParameters(p.reader, rendition.Screen, map[pdfgo.Name]func(pdfgo.Object) bool{
"W": func(value pdfgo.Object) bool { return !audio && value == pdfgo.Integer(0) || value == pdfgo.Integer(3) }, "W": func(value pdfgo.Object) bool { return !audio && value == pdfgo.Integer(0) || value == pdfgo.Integer(3) },
}) })
if err != nil || !viable { if err != nil || !viable {
return result, "", false, err return result, "", false, err
} }
if screen["W"] == pdfgo.Integer(0) { if screen["W"] == pdfgo.Integer(0) {
valid, err := p.renditionFloatingWindow(rendition.Screen) valid, err := codecpdf.RenditionFloatingWindow(p.reader, rendition.Screen)
if err != nil || !valid { if err != nil || !valid {
return result, "", false, err return result, "", false, err
} }
@@ -258,140 +259,3 @@ 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
} }
// renditionFloatingWindow 校验浮窗必需的像素尺寸,呈现偏好不写成视频固有尺寸
// 入参: screen 媒体屏幕参数
// 返回: bool 是否具备合法尺寸, error 引用错误
func (p *pdfImporter) renditionFloatingWindow(screen pdfgo.Dictionary) (bool, error) {
value, err := p.reader.Resolve(screen["BE"])
if err != nil {
return false, err
}
preferences, _ := value.(pdfgo.Dictionary)
value, err = p.reader.Resolve(preferences["F"])
if err != nil {
return false, err
}
window, ok := value.(pdfgo.Dictionary)
if !ok {
return false, nil
}
value, err = p.reader.Resolve(window["D"])
if err != nil {
return false, err
}
size, ok := value.(pdfgo.Array)
if !ok || len(size) != 2 {
return false, nil
}
for _, part := range size {
value, err = p.reader.Resolve(part)
if err != nil {
return false, err
}
number, ok := value.(pdfgo.Integer)
if !ok || number < 0 {
return false, nil
}
}
return true, nil
}
// renditionCriteria 检查媒体环境条件,未知尽力处理字段不影响候选可用性
// 入参: rendition 媒体呈现或选择器
// 返回: bool 是否可用, error 条件字典或引用错误
func (p *pdfImporter) renditionCriteria(rendition *pdfgo.Rendition) (bool, error) {
for _, constraints := range []struct {
dict pdfgo.Dictionary
required bool
}{{rendition.MustHonor, true}, {rendition.BestEffort, false}} {
for key, object := range constraints.dict {
if key != "C" {
if constraints.required {
return false, nil
}
continue
}
value, err := p.reader.Resolve(object)
if err != nil {
return false, err
}
criteria, ok := value.(pdfgo.Dictionary)
if !ok {
return false, fmt.Errorf("invalid media criteria")
}
for name, object := range criteria {
value, err := p.reader.Resolve(object)
if err != nil {
return false, err
}
if name == "Type" && value == pdfgo.Name("MediaCriteria") {
continue
}
switch name {
case "Type", "A", "C", "O", "S", "R", "D", "Z", "V", "P", "L":
return false, nil
default:
if constraints.required {
return false, nil
}
}
}
}
}
return true, nil
}
// renditionParameters 合并可等价表达的播放参数,不支持的偏好不阻断播放
// 入参: dict 参数字典, supported 字段及可表达值的检查
// 返回: pdfgo.Dictionary 合并后的值, bool 是否可用, error 字典或引用错误
func (p *pdfImporter) renditionParameters(dict pdfgo.Dictionary, supported map[pdfgo.Name]func(pdfgo.Object) bool) (pdfgo.Dictionary, bool, error) {
values := make(pdfgo.Dictionary)
for key := range dict {
if key != "Type" && key != "MH" && key != "BE" {
return nil, false, nil
}
}
value, err := p.reader.Resolve(dict["MH"])
if err != nil {
return nil, false, err
}
mandatory, _ := value.(pdfgo.Dictionary)
for _, key := range []pdfgo.Name{"BE", "MH"} {
value, err := p.reader.Resolve(dict[key])
if err != nil {
return nil, false, err
}
if value == nil {
continue
}
parameters, ok := value.(pdfgo.Dictionary)
if !ok {
return nil, false, fmt.Errorf("invalid media playback constraints")
}
for name, object := range parameters {
if _, overridden := mandatory[name]; key == "BE" && overridden {
continue
}
check := supported[name]
if check == nil {
if key == "MH" {
return nil, false, nil
}
continue
}
resolved, err := p.reader.Resolve(object)
if err != nil {
return nil, false, err
}
if !check(resolved) {
if key == "MH" {
return nil, false, nil
}
continue
}
values[name] = resolved
}
}
return values, true, nil
}
+5 -4
View File
@@ -16,10 +16,11 @@ package ofdgo
import ( import (
"fmt" "fmt"
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
"image" "image"
"math" "math"
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
@@ -593,7 +594,7 @@ func (c *pdfCompositor) gradient(paint pdfgo.Paint, space *pdfgo.ColorSpace, nod
if err != nil { if err != nil {
return nil, err return nil, err
} }
colorants := pdfPrepareShadingColorants(paint, space, c.softMask) colorants := codecpdf.PrepareShadingColorants(paint, space, c.softMask)
step := 25.4 / c.importer.rasterDPI step := 25.4 / c.importer.rasterDPI
pixels := make([]pdfShadingPixel, c.width*c.height) pixels := make([]pdfShadingPixel, c.width*c.height)
for y := range c.height { for y := range c.height {
@@ -634,7 +635,7 @@ func (c *pdfCompositor) gradient(paint pdfgo.Paint, space *pdfgo.ColorSpace, nod
visible = domainClipped || local.X >= g.Domain.XMin && local.X <= g.Domain.XMax && local.Y >= g.Domain.YMin && local.Y <= g.Domain.YMax visible = domainClipped || local.X >= g.Domain.XMin && local.X <= g.Domain.XMax && local.Y >= g.Domain.YMin && local.Y <= g.Domain.YMax
if visible { if visible {
if colorants != nil { if colorants != nil {
values, err = colorants.pointAt(local) values, err = colorants.PointAt(local)
} else { } else {
values, err = g.ValuesAt(local) values, err = g.ValuesAt(local)
if err == nil { if err == nil {
@@ -647,7 +648,7 @@ func (c *pdfCompositor) gradient(paint pdfgo.Paint, space *pdfgo.ColorSpace, nod
parameter, visible = position.PositionAt(point) parameter, visible = position.PositionAt(point)
if visible { if visible {
if colorants != nil { if colorants != nil {
values, err = colorants.at(parameter) values, err = colorants.At(parameter)
} else if paint.Axial != nil { } else if paint.Axial != nil {
values, err = paint.Axial.ValuesAt(parameter) values, err = paint.Axial.ValuesAt(parameter)
} else { } else {
+4 -29
View File
@@ -15,39 +15,14 @@
package ofdgo package ofdgo
import ( import (
"fmt" basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
// pdfGlyphPath 将字形轮廓变换到PDF页面坐标,二次曲线精确转换为三次曲线 // pdfGlyphPath 将OFD字形几何交给PDF路径适配器转换
// 入参: outline 字形轮廓, matrix 字形到页面的变换 // 入参: outline 字形轮廓, matrix 字形到页面的变换
// 返回: pdfgo.Path 页面路径, error 非字体轮廓指令 // 返回: pdfgo.Path 页面路径, error 非字体轮廓指令
func pdfGlyphPath(outline GeometryPath, matrix pdfgo.Matrix) (pdfgo.Path, error) { func pdfGlyphPath(outline GeometryPath, matrix pdfgo.Matrix) (pdfgo.Path, error) {
path := pdfgo.Path{Segments: make([]pdfgo.Segment, 0, len(outline))} return codecpdf.GlyphPath(basegeom.Path(outline), matrix)
var current, start Point
point := func(p Point) pdfgo.Point { return matrix.Apply(pdfgo.Point{X: p.X, Y: p.Y}) }
for _, segment := range outline {
switch segment.Verb {
case GeometryMove:
start = segment.End
path.Segments = append(path.Segments, pdfgo.Segment{Operator: "M", Points: []pdfgo.Point{point(segment.End)}})
case GeometryLine:
path.Segments = append(path.Segments, pdfgo.Segment{Operator: "L", Points: []pdfgo.Point{point(segment.End)}})
case GeometryQuad:
c1 := Point{X: current.X + (segment.Control1.X-current.X)*2/3, Y: current.Y + (segment.Control1.Y-current.Y)*2/3}
c2 := Point{X: segment.End.X + (segment.Control1.X-segment.End.X)*2/3, Y: segment.End.Y + (segment.Control1.Y-segment.End.Y)*2/3}
path.Segments = append(path.Segments, pdfgo.Segment{Operator: "B", Points: []pdfgo.Point{point(c1), point(c2), point(segment.End)}})
case GeometryCubic:
path.Segments = append(path.Segments, pdfgo.Segment{Operator: "B", Points: []pdfgo.Point{point(segment.Control1), point(segment.Control2), point(segment.End)}})
case GeometryClose:
path.Segments = append(path.Segments, pdfgo.Segment{Operator: "C"})
current = start
continue
default:
return pdfgo.Path{}, fmt.Errorf("unsupported PDF glyph geometry verb %d", segment.Verb)
}
current = segment.End
}
return path, nil
} }
+2 -1
View File
@@ -20,6 +20,7 @@ import (
"strings" "strings"
"unicode/utf8" "unicode/utf8"
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
@@ -273,7 +274,7 @@ func pdfMergeReplacement(nodes []pdfCompositeNode, replacement *pdfgo.Replacemen
first := nodes[0].text first := nodes[0].text
style := first.Style style := first.Style
paint := style.Fill paint := style.Fill
if first.Font == nil || len(first.Font.Program) == 0 || first.Mode != 0 || first.Size <= 0 || first.HorizontalScale <= 0 || first.Clip != nil || style.SoftMask != nil || style.Transfer != nil || style.AlphaIsShape || style.FillOverprint || paint.Alpha != 1 || !pdfNormalBlend(style.BlendMode) || paint.Tiling != nil || paint.Axial != nil || paint.Radial != nil || paint.Mesh != nil || paint.Function != nil { if first.Font == nil || len(first.Font.Program) == 0 || first.Mode != 0 || first.Size <= 0 || first.HorizontalScale <= 0 || first.Clip != nil || style.SoftMask != nil || style.Transfer != nil || style.AlphaIsShape || style.FillOverprint || paint.Alpha != 1 || !codecpdf.NormalBlend(style.BlendMode) || paint.Tiling != nil || paint.Axial != nil || paint.Radial != nil || paint.Mesh != nil || paint.Function != nil {
return nil, false return nil, false
} }
inverse, ok := first.Matrix.Inverse() inverse, ok := first.Matrix.Inverse()
+9 -6
View File
@@ -14,7 +14,10 @@
package ofdgo package ofdgo
import "github.com/xiaoqidun/pdfgo" import (
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
"github.com/xiaoqidun/pdfgo"
)
// 图案标记保留设备四色通道及非零对象形状 // 图案标记保留设备四色通道及非零对象形状
const ( const (
@@ -99,7 +102,7 @@ func (c *pdfCompositor) selectTransfer(node pdfCompositeNode, outline bool) (*pd
if outline { if outline {
paint = style.Stroke paint = style.Stroke
} }
if c.transferBlocked || paint.Alpha != 1 || style.SoftMask != nil || !pdfNormalBlend(style.BlendMode) { if c.transferBlocked || paint.Alpha != 1 || style.SoftMask != nil || !codecpdf.NormalBlend(style.BlendMode) {
return nil, nil return nil, nil
} }
transfer, err := c.importer.resolveTransfer(style, c.transferModel) transfer, err := c.importer.resolveTransfer(style, c.transferModel)
@@ -108,7 +111,7 @@ func (c *pdfCompositor) selectTransfer(node pdfCompositeNode, outline bool) (*pd
} }
if pattern := paint.Shading; pattern != nil && (node.image == nil || node.image.Image.ImageMask) { if pattern := paint.Shading; pattern != nil && (node.image == nil || node.image.Image.ImageMask) {
inner := pattern.Style inner := pattern.Style
if inner.Fill.Alpha != 1 || inner.SoftMask != nil || !pdfNormalBlend(inner.BlendMode) { if inner.Fill.Alpha != 1 || inner.SoftMask != nil || !codecpdf.NormalBlend(inner.BlendMode) {
return nil, nil return nil, nil
} }
} }
@@ -171,7 +174,7 @@ func (p *pdfCompositePattern) opaqueTransfer() (bool, error) {
// 返回: bool 是否不透明, error 图案或图片遮罩引用错误 // 返回: bool 是否不透明, error 图案或图片遮罩引用错误
func (p *pdfImporter) transferOpaque(node pdfCompositeNode) (bool, error) { func (p *pdfImporter) transferOpaque(node pdfCompositeNode) (bool, error) {
if group := node.group; group != nil { if group := node.group; group != nil {
if group.Alpha != 1 || group.SoftMask != nil || !pdfNormalBlend(group.BlendMode) { if group.Alpha != 1 || group.SoftMask != nil || !codecpdf.NormalBlend(group.BlendMode) {
return false, nil return false, nil
} }
for _, child := range node.children { for _, child := range node.children {
@@ -182,7 +185,7 @@ func (p *pdfImporter) transferOpaque(node pdfCompositeNode) (bool, error) {
return true, nil return true, nil
} }
style, fill, stroke := node.style() style, fill, stroke := node.style()
if style.SoftMask != nil || !pdfNormalBlend(style.BlendMode) || fill && style.Fill.Alpha != 1 || stroke && style.Stroke.Alpha != 1 { if style.SoftMask != nil || !codecpdf.NormalBlend(style.BlendMode) || fill && style.Fill.Alpha != 1 || stroke && style.Stroke.Alpha != 1 {
return false, nil return false, nil
} }
if node.image != nil { if node.image != nil {
@@ -197,7 +200,7 @@ func (p *pdfImporter) transferOpaque(node pdfCompositeNode) (bool, error) {
for i, paint := range [2]pdfgo.Paint{style.Fill, style.Stroke} { for i, paint := range [2]pdfgo.Paint{style.Fill, style.Stroke} {
if pattern := paint.Shading; pattern != nil && (i == 0 && fill || i == 1 && stroke) { if pattern := paint.Shading; pattern != nil && (i == 0 && fill || i == 1 && stroke) {
inner := pattern.Style inner := pattern.Style
if inner.Fill.Alpha != 1 || inner.SoftMask != nil || !pdfNormalBlend(inner.BlendMode) { if inner.Fill.Alpha != 1 || inner.SoftMask != nil || !codecpdf.NormalBlend(inner.BlendMode) {
return false, nil return false, nil
} }
} }
+3 -2
View File
@@ -26,6 +26,7 @@ import (
"github.com/tdewolff/canvas/renderers/pdf" "github.com/tdewolff/canvas/renderers/pdf"
"github.com/tdewolff/canvas/renderers/ps" "github.com/tdewolff/canvas/renderers/ps"
"github.com/tdewolff/canvas/renderers/svg" "github.com/tdewolff/canvas/renderers/svg"
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
@@ -136,7 +137,7 @@ func (CanvasBackend) RenderPDF(r *Renderer, pages []RenderDocumentPage, writer i
} }
start := buf.Len() start := buf.Len()
p := pdf.New(buf, pages[0].Box.W, pages[0].Box.H, nil) p := pdf.New(buf, pages[0].Box.W, pages[0].Box.H, nil)
renderer := &pdfRenderer{PDF: p, glyphPaths: renderCache[*canvas.Path, *canvas.Path]{limit: 16 << 20}, images: make([][]image.Image, 1), text: make([]pdfPageText, 1), navigation: navigation} renderer := &pdfRenderer{PDF: p, glyphPaths: renderCache[*canvas.Path, *canvas.Path]{limit: 16 << 20}, images: make([][]image.Image, 1), text: make([]codecpdf.TextPage, 1), navigation: navigation}
defer func() { defer func() {
if renderer.imageError != nil { if renderer.imageError != nil {
r.canvasState().images = renderCache[*EncodedImage, image.Image]{limit: imageCacheLimit} r.canvasState().images = renderCache[*EncodedImage, image.Image]{limit: imageCacheLimit}
@@ -160,7 +161,7 @@ func (CanvasBackend) RenderPDF(r *Renderer, pages []RenderDocumentPage, writer i
if i > 0 { if i > 0 {
p.NewPage(page.Box.W, page.Box.H) p.NewPage(page.Box.W, page.Box.H)
renderer.images = append(renderer.images, nil) renderer.images = append(renderer.images, nil)
renderer.text = append(renderer.text, pdfPageText{}) renderer.text = append(renderer.text, codecpdf.TextPage{})
} }
navigation.apply(p, i) navigation.apply(p, i)
if err := r.renderCanvasPageToContext(canvas.NewContext(renderer), content, !r.TransparentBackground); err != nil { if err := r.renderCanvasPageToContext(canvas.NewContext(renderer), content, !r.TransparentBackground); err != nil {
+10 -9
View File
@@ -17,10 +17,11 @@ package ofdgo
import ( import (
"context" "context"
"fmt" "fmt"
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
"slices" "slices"
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
@@ -63,10 +64,10 @@ func (n *pdfNavigation) addActions(renderer *Renderer, page int, source actionSo
if err != nil { if err != nil {
return err return err
} }
if parsed.Len() > pdfLinkEdgeLimit { if parsed.Len() > codecpdf.LinkEdgeLimit {
return fmt.Errorf("PDF link region exceeds edge limit") return fmt.Errorf("PDF link region exceeds edge limit")
} }
parsed, err = flattenPDFLink(ctx, parsed) parsed, err = codecpdf.FlattenLink(ctx, parsed)
if err != nil { if err != nil {
return err return err
} }
@@ -92,7 +93,7 @@ func (n *pdfNavigation) addActions(renderer *Renderer, page int, source actionSo
if err != nil { if err != nil {
return err return err
} }
region, err := pdfActionRegion(ctx, outline, pages[page].Box.H) region, err := codecpdf.ActionRegion(ctx, outline, pages[page].Box.H)
if err != nil { if err != nil {
return err return err
} }
@@ -158,20 +159,20 @@ func partitionPDFActions(ctx context.Context, renderer *Renderer, areas []pdfAct
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(result) > pdfLinkEdgeLimit { if len(result) > codecpdf.LinkEdgeLimit {
return nil, fmt.Errorf("PDF action partition exceeds region limit") return nil, fmt.Errorf("PDF action partition exceeds region limit")
} }
} }
if len(remaining) != 0 { if len(remaining) != 0 {
result = append(result, pdfActionArea{Path: remaining, Actions: []pdfNavigationAction{action}}) result = append(result, pdfActionArea{Path: remaining, Actions: []pdfNavigationAction{action}})
} }
if len(result) > pdfLinkEdgeLimit { if len(result) > codecpdf.LinkEdgeLimit {
return nil, fmt.Errorf("PDF action partition exceeds region limit") return nil, fmt.Errorf("PDF action partition exceeds region limit")
} }
edges := 0 edges := 0
for _, area := range result { for _, area := range result {
edges += len(area.Path) edges += len(area.Path)
if edges > pdfLinkQuadLimit { if edges > codecpdf.LinkQuadLimit {
return nil, fmt.Errorf("PDF action partition exceeds total edge limit") return nil, fmt.Errorf("PDF action partition exceeds total edge limit")
} }
} }
@@ -185,14 +186,14 @@ func combinePDFActionRegion(ctx context.Context, geometry GeometryBackend, left,
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return nil, err return nil, err
} }
if len(left) > pdfLinkEdgeLimit || len(right) > pdfLinkEdgeLimit { if len(left) > codecpdf.LinkEdgeLimit || len(right) > codecpdf.LinkEdgeLimit {
return nil, fmt.Errorf("PDF action partition exceeds edge limit") return nil, fmt.Errorf("PDF action partition exceeds edge limit")
} }
result, err := geometry.Combine(left, right, operation) result, err := geometry.Combine(left, right, operation)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(result) > pdfLinkEdgeLimit { if len(result) > codecpdf.LinkEdgeLimit {
return nil, fmt.Errorf("PDF action partition exceeds edge limit") return nil, fmt.Errorf("PDF action partition exceeds edge limit")
} }
return result, ctx.Err() return result, ctx.Err()
+2 -7
View File
@@ -24,6 +24,7 @@ import (
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
cimage "github.com/tdewolff/canvas/image" cimage "github.com/tdewolff/canvas/image"
"github.com/tdewolff/canvas/renderers/pdf" "github.com/tdewolff/canvas/renderers/pdf"
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
@@ -36,16 +37,10 @@ type pdfRenderer struct {
exactImages bool exactImages bool
imageError error imageError error
navigation *pdfNavigation navigation *pdfNavigation
text []pdfPageText text []codecpdf.TextPage
exactText bool exactText bool
} }
// pdfPageText 记录后端文字对象数量及OFD原文所属区段
type pdfPageText struct {
count int
replacements []pdfgo.TextReplacement
}
// pdfNavigation PDF导航信息 // pdfNavigation PDF导航信息
type pdfNavigation struct { type pdfNavigation struct {
Link map[int][]pdfLink Link map[int][]pdfLink
+5 -4
View File
@@ -18,6 +18,7 @@ import (
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
canvastext "github.com/tdewolff/canvas/text" canvastext "github.com/tdewolff/canvas/text"
basefont "github.com/xiaoqidun/ofdgo/internal/base/font" basefont "github.com/xiaoqidun/ofdgo/internal/base/font"
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
@@ -25,12 +26,12 @@ import (
// 入参: text 画布文字, matrix 文字变换 // 入参: text 画布文字, matrix 文字变换
func (r *pdfRenderer) RenderText(text *canvas.Text, matrix canvas.Matrix) { func (r *pdfRenderer) RenderText(text *canvas.Text, matrix canvas.Matrix) {
if len(r.text) == 0 { if len(r.text) == 0 {
r.text = append(r.text, pdfPageText{}) r.text = append(r.text, codecpdf.TextPage{})
} }
page := &r.text[len(r.text)-1] page := &r.text[len(r.text)-1]
text.WalkSpans(func(_, _ float64, span canvas.TextSpan) { text.WalkSpans(func(_, _ float64, span canvas.TextSpan) {
if span.IsText() { if span.IsText() {
page.count++ page.Count++
} }
}) })
r.PDF.RenderText(text, matrix) r.PDF.RenderText(text, matrix)
@@ -40,8 +41,8 @@ func (r *pdfRenderer) RenderText(text *canvas.Text, matrix canvas.Matrix) {
// 入参: first 起始文字对象序号, text 原文 // 入参: first 起始文字对象序号, text 原文
func (r *pdfRenderer) preserveText(first int, text string) { func (r *pdfRenderer) preserveText(first int, text string) {
page := &r.text[len(r.text)-1] page := &r.text[len(r.text)-1]
if page.count > first { if page.Count > first {
page.replacements = append(page.replacements, pdfgo.TextReplacement{First: first, Count: page.count - first, Text: text}) page.Replacements = append(page.Replacements, pdfgo.TextReplacement{First: first, Count: page.Count - first, Text: text})
r.exactText = true r.exactText = true
} }
} }
+19 -147
View File
@@ -15,165 +15,37 @@
package ofdgo package ofdgo
import ( import (
"bytes"
"context" "context"
"fmt"
"image"
"io" "io"
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
// writePDF 合并图像、文字、导航修正及压缩,直接写出最终PDF // writePDF 传递后端资源记录及OFD导航修正,写出最终PDF
// 入参: ctx 取消上下文, data 后端PDF数据, writer 输出流, optimization 优化配置 // 入参: ctx 取消上下文, data 后端PDF数据, writer 输出流, optimization 优化配置
// 返回: error 资源、压缩或写入错误 // 返回: error 资源、压缩或写入错误
func (r *pdfRenderer) writePDF(ctx context.Context, data []byte, writer io.Writer, optimization pdfgo.OptimizeOptions) error { func (r *pdfRenderer) writePDF(ctx context.Context, data []byte, writer io.Writer, optimization pdfgo.OptimizeOptions) error {
if r.imageError != nil { if r.imageError != nil {
return r.imageError return r.imageError
} }
nativeWrite := r.navigation != nil && r.navigation.nativeWrite options := codecpdf.WriteOptions{
if !r.exactImages && !r.exactText && !nativeWrite && optimization.Compression.Mode == CompressionUnchanged { Images: r.images,
if err := ctx.Err(); err != nil { Text: r.text,
return err ExactImages: r.exactImages,
} ExactText: r.exactText,
n, err := writer.Write(data) Optimization: optimization,
if err == nil && n != len(data) {
err = io.ErrShortWrite
}
return err
} }
reader, err := pdfgo.NewReader(bytes.NewReader(data), int64(len(data))) if r.navigation != nil && r.navigation.nativeWrite {
if err != nil { options.Links = make(map[int][]*pdfgo.LinkRegion, len(r.navigation.Link))
return err 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
} }
defer reader.Close() return codecpdf.Write(ctx, data, writer, options)
if !r.exactImages && !r.exactText && !nativeWrite {
_, err := reader.OptimizeTo(ctx, writer, optimization)
return err
}
images := make(map[pdfgo.Reference]image.Image)
links := make(map[pdfgo.AnnotationLocation]pdfgo.LinkRegion)
texts := make(map[pdfgo.Reference][]pdfgo.TextReplacement)
references := make([]pdfgo.Reference, len(r.images))
pages := 0
err = reader.WalkPages(ctx, func(index int, page *pdfgo.Page) error {
if index >= len(r.images) {
return fmt.Errorf("unexpected PDF output page")
}
pages++
references[index] = page.Reference
if nativeWrite {
annotations, err := page.AnnotationsContext(ctx)
if err != nil {
return err
}
expected := r.navigation.Link[index]
if len(annotations) != len(expected) {
return fmt.Errorf("PDF output link count differs")
}
for i, link := range expected {
if annotations[i].Subtype != pdfgo.Name("Link") {
return fmt.Errorf("unexpected PDF output annotation")
}
if link.Region != nil {
links[pdfgo.AnnotationLocation{Page: page.Reference, Index: i}] = *link.Region
}
}
}
if !r.exactImages && !r.exactText {
return nil
}
value, err := reader.Resolve(page.Resources["XObject"])
if err != nil {
return err
}
resources, _ := value.(pdfgo.Dictionary)
var content bytes.Buffer
if _, err := page.WriteContent(ctx, &content); err != nil {
return err
}
position := 0
textCount := 0
if err := pdfgo.WalkOperations(ctx, content.Bytes(), func(operation pdfgo.Operation) error {
if operation.Operator == "BT" {
textCount++
}
if !r.exactImages {
return nil
}
if operation.Operator != "Do" {
return nil
}
if len(operation.Operands) != 1 || position >= len(r.images[index]) {
return fmt.Errorf("unexpected PDF output image")
}
name, ok := operation.Operands[0].(pdfgo.Name)
if !ok {
return fmt.Errorf("invalid PDF output image name")
}
ref, ok := resources[name].(pdfgo.Reference)
if !ok {
return fmt.Errorf("invalid PDF output image reference")
}
original := r.images[index][position]
position++
if original == nil {
return nil
}
image, err := reader.ReadImage(ref)
if err != nil {
return err
}
if image.Width != original.Bounds().Dx() || image.Height != original.Bounds().Dy() {
return fmt.Errorf("PDF output image dimensions differ")
}
if previous := images[ref]; previous != nil && previous != original {
return fmt.Errorf("PDF output image reference conflicts")
}
images[ref] = original
return nil
}); err != nil {
return err
}
if r.exactImages && position != len(r.images[index]) {
return fmt.Errorf("PDF output image missing")
}
if r.exactText {
if index >= len(r.text) || textCount != r.text[index].count {
return fmt.Errorf("PDF output text count differs")
}
if len(r.text[index].replacements) != 0 {
texts[page.Reference] = r.text[index].replacements
}
}
return nil
})
if err != nil {
return err
}
if pages != len(r.images) {
return fmt.Errorf("PDF output page missing")
}
options := pdfgo.RewriteOptions{Images: images, ImageOptions: pdfgo.ImageWriteOptions{PreblendBinary: true}, LinkRegions: links, TextReplacements: texts, Optimization: optimization}
if nativeWrite {
if err := r.navigation.prepareRewrite(ctx, reader, references, &options); err != nil {
return err
}
}
for ref, original := range images {
if err := ctx.Err(); err != nil {
return err
}
pixels, err := baseimage.PixelDataContext(ctx, original)
if err != nil {
return err
}
if pixels == nil || pixels.Bounds() != original.Bounds() {
return fmt.Errorf("PDF output image pixel bounds differ")
}
images[ref] = pixels
}
_, err = reader.RewriteTo(ctx, writer, options)
return err
} }
+3 -2
View File
@@ -18,6 +18,7 @@ import (
"strings" "strings"
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
) )
// ptPerMM 为毫米转换为排版点的比例 // ptPerMM 为毫米转换为排版点的比例
@@ -212,9 +213,9 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
} }
if pdfText { if pdfText {
if len(pdfOutput.text) == 0 { if len(pdfOutput.text) == 0 {
pdfOutput.text = append(pdfOutput.text, pdfPageText{}) pdfOutput.text = append(pdfOutput.text, codecpdf.TextPage{})
} }
defer pdfOutput.preserveText(pdfOutput.text[len(pdfOutput.text)-1].count, obj.Text()) defer pdfOutput.preserveText(pdfOutput.text[len(pdfOutput.text)-1].Count, obj.Text())
} }
codePos := 0 codePos := 0
textPos := 0 textPos := 0