mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
1 parent
bec13d523f
commit
b3c4bbc844
45 files changed
+1885
-1453
No files matched your search
@@ -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 (
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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))}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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])
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
@@ -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
|
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in new issue
Block a user