feat(渲染压缩): 完善字体三维动作导航及压缩
build.yaml / build (push) Successful in 46s

This commit is contained in:
xiaoqidun committed 2026-10-08 01:06:43 +08:00
1 parent be5901100e
commit c8a163a077
35 files changed
+3020 -348

No files matched your search

+1 -1
View File
@@ -10,7 +10,7 @@ require (
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6 github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
github.com/xiaoqidun/jbig2 v1.1.1 github.com/xiaoqidun/jbig2 v1.1.1
github.com/xiaoqidun/pdfgo v0.0.0-20261007022230-c74c18b6029f github.com/xiaoqidun/pdfgo v0.0.0-20261007170436-881165d037e5
golang.org/x/crypto v0.57.0 golang.org/x/crypto v0.57.0
golang.org/x/image v0.46.0 golang.org/x/image v0.46.0
golang.org/x/text v0.42.0 golang.org/x/text v0.42.0
+2 -2
View File
@@ -62,8 +62,8 @@ github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk=
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
github.com/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI= github.com/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI=
github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ= github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ=
github.com/xiaoqidun/pdfgo v0.0.0-20261007022230-c74c18b6029f h1:8nEO48NLav1IVGlljGlrT1Fe8EnnSYJutIUJQe0+TpA= github.com/xiaoqidun/pdfgo v0.0.0-20261007170436-881165d037e5 h1:FZ24gdR14GDzZ0+pV0ZQUjxA71eSiEMAiHH2B5O8fPo=
github.com/xiaoqidun/pdfgo v0.0.0-20261007022230-c74c18b6029f/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ= github.com/xiaoqidun/pdfgo v0.0.0-20261007170436-881165d037e5/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA= github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
+140
View File
@@ -0,0 +1,140 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"crypto/sha256"
"image"
"runtime"
"sync"
"github.com/xiaoqidun/pdfgo"
)
// outputImageBatchLimit 限制并行批次持有的原始编码,超限图片单独处理
const outputImageBatchLimit = 16 << 20
// outputImageJob 保存独立图片任务,不访问编辑文档或调用进度回调
type outputImageJob struct {
key outputImageKey
data []byte
candidate []byte
err error
}
// outputImageOptions 统一资源压缩及像素需求,受保护图片只进行无损编码
// 入参: key 资源路径
// 返回: CompressionOptions 实际策略, image.Point 像素需求
func (e *Editor) outputImageOptions(key string) (CompressionOptions, image.Point) {
options := e.output.options
if !e.output.images[key] {
options.Mode = CompressionLossless
}
demand := e.output.sizes[key]
if options.Mode != CompressionLossy {
options = CompressionOptions{Mode: options.Mode}
demand = image.Point{}
}
return options, demand
}
// prepareOutputImages 有界并行优化独立图片,读取、缓存及进度仍在调用线程处理
// 入参: keys 资源路径, load 串行读取方法,空值表示跳过, results 本次写出的候选缓存
// 返回: error 读取、进度或取消错误
func (e *Editor) prepareOutputImages(keys []string, load func(string) ([]byte, error), results map[outputImageKey][]byte) error {
workers := min(2, runtime.GOMAXPROCS(0))
if workers < 2 || len(keys) < 2 {
return nil
}
batch := make([]outputImageJob, 0, workers)
size := 0
run := func() error {
if err := e.runOutputImages(batch, results); err != nil {
return err
}
clear(batch)
batch, size = batch[:0], 0
return nil
}
for _, key := range keys {
if err := e.output.ctx.Err(); err != nil {
return err
}
if err := editorProgress(e.OnWriteProgress).report("compress", e.output.completed, 0); err != nil {
return err
}
data, err := load(key)
if err != nil {
return err
}
if len(data) == 0 {
continue
}
options, demand := e.outputImageOptions(key)
cacheKey := outputImageKey{hash: sha256.Sum256(data), options: options, size: demand}
if _, cached := results[cacheKey]; cached {
continue
}
duplicate := false
for _, job := range batch {
duplicate = duplicate || job.key == cacheKey
}
if duplicate {
continue
}
if len(batch) != 0 && len(data) > outputImageBatchLimit-size {
if err := run(); err != nil {
return err
}
}
batch = append(batch, outputImageJob{key: cacheKey, data: data})
size += len(data)
if len(batch) == workers || size >= outputImageBatchLimit {
if err := run(); err != nil {
return err
}
}
}
return run()
}
// runOutputImages 等待整批结束后记录更小候选,失败图片保留原始编码
// 入参: jobs 独占图片任务, results 本次写出的候选缓存
// 返回: error 取消错误
func (e *Editor) runOutputImages(jobs []outputImageJob, results map[outputImageKey][]byte) error {
ctx := e.output.ctx
optimize := func(job *outputImageJob) {
job.candidate, job.err = pdfgo.OptimizeImageResource(ctx, job.data, job.key.options, job.key.size)
}
if len(jobs) == 1 {
optimize(&jobs[0])
} else {
var group sync.WaitGroup
for i := range jobs {
group.Go(func() { optimize(&jobs[i]) })
}
group.Wait()
}
if err := ctx.Err(); err != nil {
return err
}
for _, job := range jobs {
results[job.key] = nil
if job.err == nil && len(job.candidate) < len(job.data) {
results[job.key] = job.candidate
}
}
return nil
}
+28 -11
View File
@@ -67,7 +67,7 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
if data, ok := parts[name]; ok { if data, ok := parts[name]; ok {
return data, nil return data, nil
} }
return reader.readFile(name) return reader.readFileView(name)
} }
refs := editorResourceRefs{ids: make(map[string]bool), files: make(map[string]bool), fonts: make(map[string]*editorFontUsage)} refs := editorResourceRefs{ids: make(map[string]bool), files: make(map[string]bool), fonts: make(map[string]*editorFontUsage)}
var resources []string var resources []string
@@ -169,7 +169,32 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
formats := make(map[string]string) formats := make(map[string]string)
seen := make(map[string]map[[32]byte]string) seen := make(map[string]map[[32]byte]string)
imageResults := make(map[outputImageKey][]byte) imageResults := make(map[outputImageKey][]byte)
for _, key := range slices.Sorted(maps.Keys(actual)) { ordered := slices.Sorted(maps.Keys(actual))
var imageKeys []string
for _, key := range ordered {
if !refs.files[key] && files[resourceFile{key, "MediaFile"}] && !files[resourceFile{key, "FontFile"}] {
imageKeys = append(imageKeys, key)
}
}
load := func(key string) ([]byte, error) {
if data, ok := parts[actual[key]]; ok && len(data) > outputOptimizationBufferLimit {
return nil, nil
}
if reader != nil && parts[actual[key]] == nil {
if file, ok := reader.packageFile(actual[key]); ok && file.UncompressedSize64 > outputOptimizationBufferLimit {
return nil, nil
}
}
data, err := read(actual[key])
if len(data) > outputOptimizationBufferLimit {
return nil, nil
}
return data, err
}
if err := e.prepareOutputImages(imageKeys, load, imageResults); err != nil {
return err
}
for _, key := range ordered {
if refs.files[key] || files[resourceFile{key, "FontFile"}] && files[resourceFile{key, "MediaFile"}] { if refs.files[key] || files[resourceFile{key, "FontFile"}] && files[resourceFile{key, "MediaFile"}] {
continue continue
} }
@@ -196,15 +221,7 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
continue continue
} }
if role == "MediaFile" { if role == "MediaFile" {
options := e.output.options options, demand := e.outputImageOptions(key)
if !e.output.images[key] {
options.Mode = CompressionLossless
}
demand := e.output.sizes[key]
if options.Mode != CompressionLossy {
options = CompressionOptions{Mode: options.Mode}
demand = image.Point{}
}
cacheKey := outputImageKey{hash: sha256.Sum256(data), options: options, size: demand} cacheKey := outputImageKey{hash: sha256.Sum256(data), options: options, size: demand}
candidate, cached := imageResults[cacheKey] candidate, cached := imageResults[cacheKey]
var err error var err error
+134
View File
@@ -0,0 +1,134 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"fmt"
"reflect"
"slices"
)
// DocumentActions 获取文档打开动作的独立副本,不执行动作
// 返回: []Action 动作列表
func (e *Editor) DocumentActions() []Action {
if e.source == nil {
return nil
}
return cloneEditorData(e.source.document.Actions)
}
// SetDocumentActions 替换文档打开动作,空列表清除动作并记录撤销
// 入参: actions 事件为DO的动作及已注册的资源引用
// 返回: error 结构、编码或提交错误
func (e *Editor) SetDocumentActions(actions []Action) error {
if err := validateLifecycleActions(actions, "DO"); err != nil {
return err
}
current := e.DocumentActions()
if reflect.DeepEqual(current, actions) || len(current) == 0 && len(actions) == 0 {
return nil
}
base := e.source
var err error
if base == nil {
base, err = e.importBase()
if err != nil {
return err
}
}
name := cleanPackagePath(base.reader.OFD.DocBody[base.reader.documentIndex].DocRoot)
data, err := base.reader.readFile(name)
if err != nil {
return err
}
root, err := parseEditorXML(data)
if err != nil {
return err
}
encoded, err := editorActionsXML(actions)
if err != nil {
return err
}
patch := editorXMLPatch{root.close, root.close, encoded}
if node := root.child("Actions"); node != nil {
patch.start, patch.end = node.start, node.end
} else {
for _, child := range root.children {
if packageOFDNode(child, child.name.Local) && slices.Contains([]string{"VPreferences", "Bookmarks", "Annotations", "CustomTags", "Attachments", "Extensions"}, child.name.Local) {
patch.start, patch.end = child.start, child.start
break
}
}
}
return e.commitAnnotationParts(base, map[string][]byte{name: editorPatchXML(data, []editorXMLPatch{patch})})
}
// SetPageActions 替换页面打开动作,输入独立保存,空列表清除动作
// 入参: index 页面索引, actions 事件为PO的动作及已注册的资源引用
// 返回: error 页码或动作错误
func (e *Editor) SetPageActions(index int, actions []Action) error {
if err := validateLifecycleActions(actions, "PO"); err != nil {
return err
}
page, err := e.page(index)
if err != nil {
return err
}
if reflect.DeepEqual(page.Actions, actions) || len(page.Actions) == 0 && len(actions) == 0 {
return nil
}
before, after := cloneEditorData(page.Actions), cloneEditorData(actions)
page.Actions = cloneEditorData(after)
if change := e.recordChange(); change != nil {
change.undo = func(e *Editor) { e.pages[index].Actions = cloneEditorData(before) }
change.redo = func(e *Editor) { e.pages[index].Actions = cloneEditorData(after) }
}
return nil
}
// validateLifecycleActions 校验文档或页面动作的事件类型和动作结构
// 入参: actions 动作列表, event 允许的事件
// 返回: error 事件或动作错误
func validateLifecycleActions(actions []Action, event string) error {
for _, action := range actions {
if action.Event != event {
return fmt.Errorf("expected %s action event", event)
}
}
return validateObjectActions(actions)
}
// editorActionsXML 复用动作编码并补齐独立片段的命名空间,空列表不输出节点
// 入参: actions 动作列表
// 返回: []byte 动作片段, error 编码错误
func editorActionsXML(actions []Action) ([]byte, error) {
if len(actions) == 0 {
return nil, nil
}
data, err := encodeOFDXML(func(x *ofdXML) {
x.root("Document", nil)
x.actions(actions)
x.end("Document")
})
if err != nil {
return nil, err
}
root, err := parseEditorXML(data)
if err != nil {
return nil, err
}
node := root.child("Actions")
return editorXMLStandalone(data[node.start:node.end], node)
}
+17 -2
View File
@@ -270,7 +270,7 @@ func (e *Editor) sourcePartsPrepared(progress editorProgress, generated map[stri
return parts, nil return parts, nil
} }
// sourcePageXML 仅改写已修改的对象、页面尺寸和新增图层,保留原页块层级 // sourcePageXML 仅改写已修改的对象、页面尺寸、动作和新增图层,保留原页块层级
// 入参: index 页面索引, source 原页面 // 入参: index 页面索引, source 原页面
// 返回: []byte 页面XML, error 错误信息 // 返回: []byte 页面XML, error 错误信息
func (e *Editor) sourcePageXML(index int, source *editorSourcePage) ([]byte, error) { func (e *Editor) sourcePageXML(index int, source *editorSourcePage) ([]byte, error) {
@@ -416,13 +416,28 @@ func (e *Editor) sourcePageXML(index int, source *editorSourcePage) ([]byte, err
value := []byte("<ofd:Content xmlns:ofd=\"" + ofdNamespace + "\">") value := []byte("<ofd:Content xmlns:ofd=\"" + ofdNamespace + "\">")
value = append(value, added...) value = append(value, added...)
value = append(value, []byte("</ofd:Content>")...) value = append(value, []byte("</ofd:Content>")...)
patches = append(patches, editorXMLPatch{source.root.close, source.root.close, value}) position := source.root.close
if actions := source.root.child("Actions"); actions != nil {
position = actions.start
}
patches = append(patches, editorXMLPatch{position, position, value})
} else if content.open == content.end { } else if content.open == content.end {
patches = append(patches, editorXMLContent(data, content, added)) patches = append(patches, editorXMLContent(data, content, added))
} else { } else {
patches = append(patches, editorXMLPatch{content.close, content.close, added}) patches = append(patches, editorXMLPatch{content.close, content.close, added})
} }
} }
if !reflect.DeepEqual(page.Actions, source.original.Actions) {
encoded, err := editorActionsXML(page.Actions)
if err != nil {
return nil, err
}
patch := editorXMLPatch{source.root.close, source.root.close, encoded}
if node := source.root.child("Actions"); node != nil {
patch.start, patch.end = node.start, node.end
}
patches = append(patches, patch)
}
return editorPatchXML(data, patches), nil return editorPatchXML(data, patches), nil
} }
+32 -35
View File
@@ -435,7 +435,7 @@ func addPackedGlyphMapping(mapping map[rune]uint16, numGlyphs uint16) {
} }
} }
// buildCmapTable 构建cmap表 (Format 4) // buildCmapTable 构建格式4或12字符映射,省略缺字映射
// 入参: numGlyphs 字形数量, mapping 字符映射 // 入参: numGlyphs 字形数量, mapping 字符映射
// 返回: []byte cmap表数据 // 返回: []byte cmap表数据
func buildCmapTable(numGlyphs uint16, mapping map[rune]uint16) []byte { func buildCmapTable(numGlyphs uint16, mapping map[rune]uint16) []byte {
@@ -444,15 +444,13 @@ func buildCmapTable(numGlyphs uint16, mapping map[rune]uint16) []byte {
} }
var segs []cmapSegment var segs []cmapSegment
if mapping == nil { if mapping == nil {
end := uint16(0xFFFF)
if numGlyphs > 0 { if numGlyphs > 0 {
end = numGlyphs - 1 segs = append(segs, cmapSegment{start: 0, end: numGlyphs - 1, delta: 0, offset: 0})
} }
segs = append(segs, cmapSegment{start: 0, end: end, delta: 0, offset: 0})
} else { } else {
var codes []int var codes []int
for r := range mapping { for r, gid := range mapping {
if r <= 0xFFFF { if r >= 0 && r <= 0xFFFF && gid != 0 {
codes = append(codes, int(r)) codes = append(codes, int(r))
} }
} }
@@ -494,13 +492,13 @@ func buildCmapTable(numGlyphs uint16, mapping map[rune]uint16) []byte {
for _, s := range segs { for _, s := range segs {
endCounts = append(endCounts, s.end) endCounts = append(endCounts, s.end)
startCounts = append(startCounts, s.start) startCounts = append(startCounts, s.start)
if mapping == nil { if s.start == 0xFFFF {
idDeltas = append(idDeltas, 0) idDeltas = append(idDeltas, 1)
idRangeOffsets = append(idRangeOffsets, 0) idRangeOffsets = append(idRangeOffsets, 0)
continue continue
} }
if s.start == 0xFFFF { if mapping == nil {
idDeltas = append(idDeltas, 1) idDeltas = append(idDeltas, 0)
idRangeOffsets = append(idRangeOffsets, 0) idRangeOffsets = append(idRangeOffsets, 0)
continue continue
} }
@@ -549,7 +547,10 @@ func shouldBuildCmapFormat12(mapping map[rune]uint16) bool {
return false return false
} }
var codes []int var codes []int
for r := range mapping { for r, gid := range mapping {
if r < 0 || gid == 0 {
continue
}
if r >= 0xFFFF { if r >= 0xFFFF {
return true return true
} }
@@ -588,8 +589,8 @@ func buildCmapTableFormat12(numGlyphs uint16, mapping map[rune]uint16) []byte {
codes = append(codes, i) codes = append(codes, i)
} }
} else { } else {
for r := range mapping { for r, gid := range mapping {
if r >= 0 { if r >= 0 && gid != 0 {
codes = append(codes, int(r)) codes = append(codes, int(r))
} }
} }
@@ -617,7 +618,7 @@ func buildCmapTableFormat12(numGlyphs uint16, mapping map[rune]uint16) []byte {
if mapping != nil { if mapping != nil {
nextGID = mapping[nextRune] nextGID = mapping[nextRune]
} }
if nextRune != prevRune+1 || nextGID != prevGID+1 { if nextRune != prevRune+1 || uint32(nextGID) != uint32(prevGID)+1 {
break break
} }
group.endChar = uint32(nextRune) group.endChar = uint32(nextRune)
@@ -627,26 +628,22 @@ func buildCmapTableFormat12(numGlyphs uint16, mapping map[rune]uint16) []byte {
} }
groups = append(groups, group) groups = append(groups, group)
} }
sub := new(bytes.Buffer) data := make([]byte, 36+12*len(groups))
binary.Write(sub, binary.BigEndian, uint16(12)) binary.BigEndian.PutUint16(data[2:], 2)
binary.Write(sub, binary.BigEndian, uint16(0)) binary.BigEndian.PutUint16(data[6:], 4)
binary.Write(sub, binary.BigEndian, uint32(16+12*len(groups))) binary.BigEndian.PutUint32(data[8:], 20)
binary.Write(sub, binary.BigEndian, uint32(0)) binary.BigEndian.PutUint16(data[12:], 3)
binary.Write(sub, binary.BigEndian, uint32(len(groups))) binary.BigEndian.PutUint16(data[14:], 10)
for _, group := range groups { binary.BigEndian.PutUint32(data[16:], 20)
binary.Write(sub, binary.BigEndian, group.startChar) sub := data[20:]
binary.Write(sub, binary.BigEndian, group.endChar) binary.BigEndian.PutUint16(sub, 12)
binary.Write(sub, binary.BigEndian, group.startGID) binary.BigEndian.PutUint32(sub[4:], uint32(len(sub)))
binary.BigEndian.PutUint32(sub[12:], uint32(len(groups)))
for index, group := range groups {
position := 16 + 12*index
binary.BigEndian.PutUint32(sub[position:], group.startChar)
binary.BigEndian.PutUint32(sub[position+4:], group.endChar)
binary.BigEndian.PutUint32(sub[position+8:], group.startGID)
} }
mainBuf := new(bytes.Buffer) return data
binary.Write(mainBuf, binary.BigEndian, uint16(0))
binary.Write(mainBuf, binary.BigEndian, uint16(2))
binary.Write(mainBuf, binary.BigEndian, uint16(0))
binary.Write(mainBuf, binary.BigEndian, uint16(4))
binary.Write(mainBuf, binary.BigEndian, uint32(20))
binary.Write(mainBuf, binary.BigEndian, uint16(3))
binary.Write(mainBuf, binary.BigEndian, uint16(10))
binary.Write(mainBuf, binary.BigEndian, uint32(20))
mainBuf.Write(sub.Bytes())
return mainBuf.Bytes()
} }
+73 -12
View File
@@ -125,11 +125,12 @@ func (f *sfntOutliner) glyph(id uint16, active map[uint16]bool) (ttGlyph, error)
return result, fmt.Errorf("truncated TrueType glyph %d", id) return result, fmt.Errorf("truncated TrueType glyph %d", id)
} }
n := int(int16(binary.BigEndian.Uint16(data))) n := int(int16(binary.BigEndian.Uint16(data)))
phantoms := f.phantomPoints(id, data)
var program []byte var program []byte
if n >= 0 { if n >= 0 {
result, program, err = readTTSimple(data, n) result, program, err = readTTSimple(data, n)
} else { } else {
result, program, err = f.composite(data, active) result, program, err = f.composite(id, data, phantoms, active)
} }
if err != nil { if err != nil {
return result, err return result, err
@@ -137,12 +138,10 @@ func (f *sfntOutliner) glyph(id uint16, active map[uint16]bool) (ttGlyph, error)
if result.warning != nil { if result.warning != nil {
f.recordGlyphWarning(id, result.warning) f.recordGlyphWarning(id, result.warning)
} }
left := int32(int16(binary.BigEndian.Uint16(data[2:]))) - int32(f.font.Hmtx.LeftSideBearing(id))
top := int32(int16(binary.BigEndian.Uint16(data[8:])))
if result.metrics != nil { if result.metrics != nil {
result.points = append(result.points, result.metrics[:]...) result.points = append(result.points, result.metrics[:]...)
} else { } else {
result.points = append(result.points, ttPoint{x: left * 64, ox: left * 64}, ttPoint{x: (left + int32(f.font.GlyphAdvance(id))) * 64, ox: (left + int32(f.font.GlyphAdvance(id))) * 64}, ttPoint{y: top * 64, oy: top * 64}, ttPoint{y: (top - int32(f.font.GlyphVerticalAdvance(id))) * 64, oy: (top - int32(f.font.GlyphVerticalAdvance(id))) * 64}) result.points = append(result.points, phantoms[:]...)
} }
if len(program) == 0 { if len(program) == 0 {
return result, nil return result, nil
@@ -156,6 +155,11 @@ func (f *sfntOutliner) glyph(id uint16, active map[uint16]bool) (ttGlyph, error)
} }
f.once.Do(func() { f.program, f.err = newTTInterpreter(f.font.Tables, f.font.UnitsPerEm()) }) f.once.Do(func() { f.program, f.err = newTTInterpreter(f.font.Tables, f.font.UnitsPerEm()) })
if f.err != nil { if f.err != nil {
if ttRecoverableHint(f.err) {
result.warning = f.err
f.recordGlyphWarning(id, f.err)
return result, nil
}
return result, f.err return result, f.err
} }
if f.program.graphics.inhibit { if f.program.graphics.inhibit {
@@ -164,7 +168,7 @@ func (f *sfntOutliner) glyph(id uint16, active map[uint16]bool) (ttGlyph, error)
vm := f.program.clone() vm := f.program.clone()
vm.zones[1], vm.ends = append([]ttPoint(nil), result.points...), result.ends vm.zones[1], vm.ends = append([]ttPoint(nil), result.points...), result.ends
if err := vm.run(program); err != nil { if err := vm.run(program); err != nil {
if errors.Is(err, errTTStackUnderflow) { if ttRecoverableHint(err) {
result.warning = err result.warning = err
f.recordGlyphWarning(id, err) f.recordGlyphWarning(id, err)
return result, nil return result, nil
@@ -175,6 +179,25 @@ func (f *sfntOutliner) glyph(id uint16, active map[uint16]bool) (ttGlyph, error)
return result, nil return result, nil
} }
// phantomPoints 按水平和垂直度量生成字形末尾的四个虚拟点
// 入参: id 字形编号, data 完整字形头
// 返回: [4]ttPoint 左右边距与上下原点
func (f *sfntOutliner) phantomPoints(id uint16, data []byte) [4]ttPoint {
left := int32(int16(binary.BigEndian.Uint16(data[2:]))) - int32(f.font.Hmtx.LeftSideBearing(id))
top := int32(int16(binary.BigEndian.Uint16(data[8:])))
if f.font.Vmtx != nil {
top += int32(f.font.Vmtx.TopSideBearing(id))
}
return [4]ttPoint{{x: left * 64, ox: left * 64}, {x: (left + int32(f.font.GlyphAdvance(id))) * 64, ox: (left + int32(f.font.GlyphAdvance(id))) * 64}, {y: top * 64, oy: top * 64}, {y: (top - int32(f.font.GlyphVerticalAdvance(id))) * 64, oy: (top - int32(f.font.GlyphVerticalAdvance(id))) * 64}}
}
// ttRecoverableHint 判断栈参数、指令引用或控制流错误是否允许保留执行前轮廓
// 入参: err 指令错误
// 返回: bool 是否可恢复
func ttRecoverableHint(err error) bool {
return errors.Is(err, errTTStackUnderflow) || errors.Is(err, errTTInvalidReference) || errors.Is(err, errTTInvalidControlFlow)
}
// trueTypeGlyphData 按loca索引读取字形,拒绝倒序或越界的数据范围 // trueTypeGlyphData 按loca索引读取字形,拒绝倒序或越界的数据范围
// 入参: tables 字体表, id 字形编号 // 入参: tables 字体表, id 字形编号
// 返回: []byte 字形数据, error 索引错误 // 返回: []byte 字形数据, error 索引错误
@@ -231,6 +254,9 @@ func (f *sfntOutliner) glyphWarning(id uint16) error {
// 返回: ttGlyph 字形轮廓, []byte 字节码, error 数据错误 // 返回: ttGlyph 字形轮廓, []byte 字节码, error 数据错误
func readTTSimple(data []byte, count int) (ttGlyph, []byte, error) { func readTTSimple(data []byte, count int) (ttGlyph, []byte, error) {
var result ttGlyph var result ttGlyph
if count == 0 && len(data) == 10 {
return result, nil, nil
}
if len(data) < 12+2*count { if len(data) < 12+2*count {
return result, nil, fmt.Errorf("truncated TrueType contour endpoints") return result, nil, fmt.Errorf("truncated TrueType contour endpoints")
} }
@@ -311,17 +337,23 @@ func readTTSimple(data []byte, count int) (ttGlyph, []byte, error) {
} }
// composite 合成分量变换和点匹配,保留分量顺序及父级字节码 // composite 合成分量变换和点匹配,保留分量顺序及父级字节码
// 入参: data 复合字形数据, active 递归访问集合 // 入参: glyph 父字形编号, data 复合字形数据, phantoms 父字形虚拟点, active 递归访问集合
// 返回: ttGlyph 字形轮廓, []byte 字节码, error 数据错误 // 返回: ttGlyph 字形轮廓, []byte 字节码, error 数据错误
func (f *sfntOutliner) composite(data []byte, active map[uint16]bool) (ttGlyph, []byte, error) { func (f *sfntOutliner) composite(glyph uint16, data []byte, phantoms [4]ttPoint, active map[uint16]bool) (ttGlyph, []byte, error) {
var result ttGlyph var result ttGlyph
pos := 10 pos := 10
total := -1
first := true
hasInstructions := false hasInstructions := false
for { for {
if len(data)-pos < 6 { if len(data)-pos < 6 {
return result, nil, fmt.Errorf("truncated TrueType component") return result, nil, fmt.Errorf("truncated TrueType component")
} }
flags, id := binary.BigEndian.Uint16(data[pos:]), binary.BigEndian.Uint16(data[pos+2:]) flags, id := binary.BigEndian.Uint16(data[pos:]), binary.BigEndian.Uint16(data[pos+2:])
if first && flags&2 == 0 {
return result, nil, fmt.Errorf("first TrueType component requires coordinate offsets")
}
first = false
pos += 4 pos += 4
a, b := int32(data[pos]), int32(data[pos+1]) a, b := int32(data[pos]), int32(data[pos+1])
pos += 2 pos += 2
@@ -371,11 +403,13 @@ func (f *sfntOutliner) composite(data []byte, active map[uint16]bool) (ttGlyph,
result.warning = fmt.Errorf("TrueType component %d: %w", id, part.warning) result.warning = fmt.Errorf("TrueType component %d: %w", id, part.warning)
} }
if flags&0x200 != 0 { if flags&0x200 != 0 {
if result.metrics == nil {
result.metrics = new([4]ttPoint) result.metrics = new([4]ttPoint)
}
copy(result.metrics[:], part.points[len(part.points)-4:]) copy(result.metrics[:], part.points[len(part.points)-4:])
} }
part.points = part.points[:len(part.points)-4] pointCount := len(part.points) - 4
for i := range part.points { for i := range pointCount {
p := &part.points[i] p := &part.points[i]
x, y := float64(p.x), float64(p.y) x, y := float64(p.x), float64(p.y)
p.x = int32(math.Round(matrix[0]*x + matrix[2]*y)) p.x = int32(math.Round(matrix[0]*x + matrix[2]*y))
@@ -383,17 +417,44 @@ func (f *sfntOutliner) composite(data []byte, active map[uint16]bool) (ttGlyph,
} }
dx, dy := a*64, b*64 dx, dy := a*64, b*64
if flags&2 == 0 { if flags&2 == 0 {
if a < 0 || b < 0 || int64(a) >= int64(len(result.points)) || int64(b) >= int64(len(part.points)) { if b < 0 || int64(b) >= int64(len(part.points)) {
return result, nil, fmt.Errorf("invalid TrueType component point matching") return result, nil, fmt.Errorf("invalid TrueType component point matching")
} }
dx, dy = result.points[a].x-part.points[b].x, result.points[a].y-part.points[b].y child := part.points[b]
if int64(b) >= int64(pointCount) {
x, y := float64(child.x), float64(child.y)
child.x = int32(math.Round(matrix[0]*x + matrix[2]*y))
child.y = int32(math.Round(matrix[1]*x + matrix[3]*y))
}
var parent ttPoint
if a >= 0 && int64(a) < int64(len(result.points)) {
parent = result.points[a]
} else {
if total < 0 {
var err error
total, err = trueTypePointCount(f.font.Tables, glyph, map[uint16]int{}, map[uint16]bool{})
if err != nil {
return result, nil, err
}
}
if a < int32(total) || a >= int32(total)+4 {
return result, nil, fmt.Errorf("invalid TrueType component point matching")
}
metrics := &phantoms
if result.metrics != nil {
metrics = result.metrics
}
parent = metrics[a-int32(total)]
}
dx, dy = parent.x-child.x, parent.y-child.y
} else if flags&0x1800 == 0x800 { } else if flags&0x1800 == 0x800 {
x, y := float64(dx), float64(dy) x, y := float64(dx), float64(dy)
dx, dy = int32(math.Round(matrix[0]*x+matrix[2]*y)), int32(math.Round(matrix[1]*x+matrix[3]*y)) dx, dy = int32(math.Round(matrix[0]*x+matrix[2]*y)), int32(math.Round(matrix[1]*x+matrix[3]*y))
} }
if flags&4 != 0 { if flags&6 == 6 {
dx, dy = int32(math.Round(float64(dx)/64))*64, int32(math.Round(float64(dy)/64))*64 dx, dy = int32(math.Round(float64(dx)/64))*64, int32(math.Round(float64(dy)/64))*64
} }
part.points = part.points[:pointCount]
if len(result.points)+len(part.points) > 65536 { if len(result.points)+len(part.points) > 65536 {
return result, nil, fmt.Errorf("excessive TrueType component points") return result, nil, fmt.Errorf("excessive TrueType component points")
} }
+15 -5
View File
@@ -25,6 +25,12 @@ import (
// errTTStackUnderflow 表示字形程序缺少必需的栈参数 // errTTStackUnderflow 表示字形程序缺少必需的栈参数
var errTTStackUnderflow = errors.New("TrueType instruction stack underflow") var errTTStackUnderflow = errors.New("TrueType instruction stack underflow")
// errTTInvalidReference 表示字形指令引用不存在的点或函数
var errTTInvalidReference = errors.New("invalid TrueType instruction reference")
// errTTInvalidControlFlow 表示条件分支未闭合或跳转超出字节码范围
var errTTInvalidControlFlow = errors.New("invalid TrueType control flow")
// ttPoint 保存TrueType当前坐标、原始坐标及点状态,坐标使用26.6定点数 // ttPoint 保存TrueType当前坐标、原始坐标及点状态,坐标使用26.6定点数
type ttPoint struct { type ttPoint struct {
x, y, ox, oy int32 x, y, ox, oy int32
@@ -139,7 +145,7 @@ func (v *ttInterpreter) push(values ...int32) {
// 返回: *ttPoint 点 // 返回: *ttPoint 点
func (v *ttInterpreter) point(zone, index int32) *ttPoint { func (v *ttInterpreter) point(zone, index int32) *ttPoint {
if zone < 0 || zone > 1 || index < 0 || int64(index) >= int64(len(v.zones[zone])) { if zone < 0 || zone > 1 || index < 0 || int64(index) >= int64(len(v.zones[zone])) {
v.err = fmt.Errorf("invalid TrueType point %d in zone %d", index, zone) v.err = fmt.Errorf("%w: point %d in zone %d", errTTInvalidReference, index, zone)
return &ttPoint{} return &ttPoint{}
} }
return &v.zones[zone][index] return &v.zones[zone][index]
@@ -247,6 +253,7 @@ func ttInstructionEnd(code []byte, position int) (int, error) {
// 入参: code 字节码, position 起始位置, alternate 是否停在ELSE // 入参: code 字节码, position 起始位置, alternate 是否停在ELSE
// 返回: int 后续指令位置, error 分支错误 // 返回: int 后续指令位置, error 分支错误
func skipTTConditional(code []byte, position int, alternate bool) (int, error) { func skipTTConditional(code []byte, position int, alternate bool) (int, error) {
start := position
depth := 0 depth := 0
for position < len(code) { for position < len(code) {
op := code[position] op := code[position]
@@ -268,7 +275,7 @@ func skipTTConditional(code []byte, position int, alternate bool) (int, error) {
return 0, err return 0, err
} }
} }
return 0, fmt.Errorf("unterminated TrueType conditional") return 0, fmt.Errorf("%w: unterminated conditional at %d in %d bytes", errTTInvalidControlFlow, start, len(code))
} }
// run 执行字体、预处理或字形程序 // run 执行字体、预处理或字形程序
@@ -443,10 +450,13 @@ func (v *ttInterpreter) execute(code []byte, depth int) error {
if op == 0x2a { if op == 0x2a {
count = v.pop() count = v.pop()
} }
body, ok := v.functions[id] if count < 0 || count > 1000000 {
if !ok || count < 0 || count > 1000000 {
return fmt.Errorf("invalid TrueType function call %d", id) return fmt.Errorf("invalid TrueType function call %d", id)
} }
body, ok := v.functions[id]
if !ok {
return fmt.Errorf("%w: function %d", errTTInvalidReference, id)
}
for n := int32(0); n < count; n++ { for n := int32(0); n < count; n++ {
if err = v.execute(body, depth+1); err != nil { if err = v.execute(body, depth+1); err != nil {
return fmt.Errorf("TrueType function %d: %w", id, err) return fmt.Errorf("TrueType function %d: %w", id, err)
@@ -749,7 +759,7 @@ func (v *ttInterpreter) execute(code []byte, depth int) error {
return fmt.Errorf("TrueType instruction 0x%02x at %d: %w", op, start, v.err) return fmt.Errorf("TrueType instruction 0x%02x at %d: %w", op, start, v.err)
} }
if pc < 0 || pc > len(code) { if pc < 0 || pc > len(code) {
return fmt.Errorf("invalid TrueType jump target") return fmt.Errorf("%w: jump target %d at %d in %d bytes", errTTInvalidControlFlow, pc, start, len(code))
} }
} }
return nil return nil
+299
View File
@@ -0,0 +1,299 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"slices"
)
// errTTInvalidLocations 表示字形索引不满足递增偏移要求
var errTTInvalidLocations = errors.New("invalid TrueType glyph locations")
// normalizeTrueTypeLocations 验证物理字形边界后按原编号重排,不改变空字形和分量引用
// 入参: tables 独占字体表目录,表内容只读
// 返回: bool 是否重建索引, error 无法可靠恢复的索引或轮廓错误
func normalizeTrueTypeLocations(tables map[string][]byte) (bool, error) {
if _, ok := tables["glyf"]; !ok {
return false, nil
}
head, maxp, loca, glyf := tables["head"], tables["maxp"], tables["loca"], tables["glyf"]
if len(head) < 54 || len(maxp) < 6 {
return false, fmt.Errorf("%w: missing header", errTTInvalidLocations)
}
count := int(binary.BigEndian.Uint16(maxp[4:]))
width := 2
switch binary.BigEndian.Uint16(head[50:]) {
case 0:
case 1:
width = 4
default:
return false, fmt.Errorf("%w: format", errTTInvalidLocations)
}
if count == 0 || len(loca) < (count+1)*width {
return false, fmt.Errorf("%w: truncated index", errTTInvalidLocations)
}
location := func(i int) uint64 {
if width == 2 {
return uint64(binary.BigEndian.Uint16(loca[i*2:])) * 2
}
return uint64(binary.BigEndian.Uint32(loca[i*4:]))
}
ordered, previous := true, uint64(0)
for i := range count + 1 {
offset := location(i)
if offset > uint64(len(glyf)) {
return false, fmt.Errorf("%w: offset %d outside table", errTTInvalidLocations, i)
}
ordered = ordered && previous <= offset
previous = offset
}
if ordered {
return false, nil
}
offsets := make([]int, count+1)
for i := range offsets {
offsets[i] = int(location(i))
}
physical := slices.Clone(offsets)
physical = append(physical, len(glyf))
slices.Sort(physical)
physical = slices.Compact(physical)
bodies := make(map[int][]byte, count)
components := make([][]uint16, count)
var total uint64
for id := range count {
start := offsets[id]
if start == offsets[id+1] {
continue
}
if _, duplicate := bodies[start]; duplicate {
return false, fmt.Errorf("%w: ambiguous shared offset %d", errTTInvalidLocations, start)
}
index, _ := slices.BinarySearch(physical, start)
if index+1 >= len(physical) {
return false, fmt.Errorf("%w: missing glyph %d body", errTTInvalidLocations, id)
}
data := glyf[start:physical[index+1]]
size, refs, err := trueTypeGlyphSize(data, count)
if err != nil {
return false, fmt.Errorf("%w: glyph %d: %w", errTTInvalidLocations, id, err)
}
if len(data)-size > 3 || len(bytes.Trim(data[size:], "\x00")) != 0 {
return false, fmt.Errorf("%w: ambiguous glyph %d padding", errTTInvalidLocations, id)
}
bodies[start], components[id] = data[:size], refs
total += (uint64(size) + 3) &^ 3
if total > uint64(^uint(0)>>1) || total > uint64(^uint32(0)) {
return false, fmt.Errorf("%w: rebuilt table exceeds capacity", errTTInvalidLocations)
}
}
state := make([]uint8, count)
var visit func(int) error
visit = func(id int) error {
if state[id] == 1 {
return fmt.Errorf("%w: cyclic component %d", errTTInvalidLocations, id)
}
if state[id] == 2 {
return nil
}
state[id] = 1
for _, ref := range components[id] {
if err := visit(int(ref)); err != nil {
return err
}
}
state[id] = 2
return nil
}
for id := range count {
if err := visit(id); err != nil {
return false, err
}
}
result, locations := make([]byte, 0, total), make([]byte, (count+1)*4)
for id := range count {
binary.BigEndian.PutUint32(locations[id*4:], uint32(len(result)))
if offsets[id] != offsets[id+1] {
result = append(result, bodies[offsets[id]]...)
result = append(result, make([]byte, (-len(result))&3)...)
}
}
binary.BigEndian.PutUint32(locations[count*4:], uint32(len(result)))
header := bytes.Clone(head)
binary.BigEndian.PutUint16(header[50:], 1)
tables["head"], tables["loca"], tables["glyf"] = header, locations, result
return true, nil
}
// trueTypeGlyphSize 检查单个字形的完整记录长度及复合引用,不执行指令或分配坐标
// 入参: data 字形及尾部对齐数据, count 字形总数
// 返回: int 记录长度, []uint16 分量编号, error 结构错误
func trueTypeGlyphSize(data []byte, count int) (int, []uint16, error) {
if len(data) < 10 {
return 0, nil, fmt.Errorf("truncated glyph header")
}
contours := int(int16(binary.BigEndian.Uint16(data)))
pos := 10
if contours >= 0 {
if contours == 0 && len(data) == 10 {
return 10, nil, nil
}
if contours > (len(data)-12)/2 || len(data) < 12 {
return 0, nil, fmt.Errorf("truncated contour endpoints")
}
points := 0
for range contours {
end := int(binary.BigEndian.Uint16(data[pos:])) + 1
if end <= points {
return 0, nil, fmt.Errorf("unordered contour endpoints")
}
points, pos = end, pos+2
}
instructions := int(binary.BigEndian.Uint16(data[pos:]))
pos += 2
if instructions > len(data)-pos {
return 0, nil, fmt.Errorf("truncated glyph instructions")
}
pos += instructions
coordinates := 0
for seen := 0; seen < points; {
if pos >= len(data) {
return 0, nil, fmt.Errorf("truncated glyph flags")
}
flag, repeat := data[pos], 1
pos++
if flag&0x80 != 0 {
return 0, nil, fmt.Errorf("reserved glyph flag")
}
if flag&8 != 0 {
if pos >= len(data) {
return 0, nil, fmt.Errorf("truncated glyph repeat")
}
repeat += int(data[pos])
pos++
}
if repeat > points-seen {
return 0, nil, fmt.Errorf("excessive glyph repeat")
}
for axis := range 2 {
if flag&byte(2<<axis) != 0 {
coordinates += repeat
} else if flag&byte(16<<axis) == 0 {
coordinates += 2 * repeat
}
}
seen += repeat
}
if coordinates > len(data)-pos {
return 0, nil, fmt.Errorf("truncated glyph coordinates")
}
return pos + coordinates, nil, nil
}
var refs []uint16
instructions := false
for {
if len(data)-pos < 6 {
return 0, nil, fmt.Errorf("truncated glyph component")
}
flags, id := binary.BigEndian.Uint16(data[pos:]), binary.BigEndian.Uint16(data[pos+2:])
if int(id) >= count || flags&0xe010 != 0 {
return 0, nil, fmt.Errorf("invalid glyph component")
}
refs = append(refs, id)
pos += 6
if flags&1 != 0 {
pos += 2
}
switch flags & 0xc8 {
case 0:
case 8:
pos += 2
case 0x40:
pos += 4
case 0x80:
pos += 8
default:
return 0, nil, fmt.Errorf("conflicting component transforms")
}
if pos > len(data) {
return 0, nil, fmt.Errorf("truncated component arguments")
}
instructions = instructions || flags&0x100 != 0
if flags&0x20 == 0 {
break
}
}
if instructions {
if len(data)-pos < 2 {
return 0, nil, fmt.Errorf("truncated component instructions")
}
length := int(binary.BigEndian.Uint16(data[pos:]))
pos += 2
if length > len(data)-pos {
return 0, nil, fmt.Errorf("truncated component instructions")
}
pos += length
}
return pos, refs, nil
}
// trueTypePointCount 按完整轮廓计算虚拟点编号,局部复用分量计数,不执行字形指令
// 入参: tables 字体表, id 字形编号, counts 本次计数缓存, active 递归访问集合
// 返回: int 不含虚拟点的轮廓点数, error 结构或循环引用错误
func trueTypePointCount(tables map[string][]byte, id uint16, counts map[uint16]int, active map[uint16]bool) (int, error) {
if count, ok := counts[id]; ok {
return count, nil
}
maxp := tables["maxp"]
if len(maxp) < 6 || id >= binary.BigEndian.Uint16(maxp[4:]) || active[id] {
return 0, fmt.Errorf("invalid TrueType component reference %d", id)
}
active[id] = true
defer delete(active, id)
data, err := trueTypeGlyphData(tables, id)
if err != nil {
return 0, err
}
if len(data) == 0 {
counts[id] = 0
return 0, nil
}
_, refs, err := trueTypeGlyphSize(data, int(binary.BigEndian.Uint16(maxp[4:])))
if err != nil {
return 0, err
}
contours := int(int16(binary.BigEndian.Uint16(data)))
count := 0
if contours > 0 {
count = int(binary.BigEndian.Uint16(data[10+2*(contours-1):])) + 1
} else {
for _, ref := range refs {
points, err := trueTypePointCount(tables, ref, counts, active)
if err != nil {
return 0, err
}
count += points
if count > 65536 {
return 0, fmt.Errorf("excessive TrueType component points")
}
}
}
counts[id] = count
return count, nil
}
+28 -9
View File
@@ -78,6 +78,7 @@ type pdfImporter struct {
report PDFImportReport report PDFImportReport
matrix pdfgo.Matrix matrix pdfgo.Matrix
page int page int
pageActionPage int
fontIDs map[*pdfgo.Font]string fontIDs map[*pdfgo.Font]string
fonts map[*pdfgo.Font]*pdfImportedFont fonts map[*pdfgo.Font]*pdfImportedFont
fontWarnings map[string]bool fontWarnings map[string]bool
@@ -91,6 +92,7 @@ type pdfImporter struct {
pages map[pdfgo.Reference]*pdfgo.Page pages map[pdfgo.Reference]*pdfgo.Page
pageIDs map[pdfgo.Reference]string pageIDs map[pdfgo.Reference]string
pageIndexes map[pdfgo.Reference]int pageIndexes map[pdfgo.Reference]int
destinationBoxes map[pdfgo.Reference]Box
imageIDs map[pdfImageKey][]pdfImageResource imageIDs map[pdfImageKey][]pdfImageResource
imageContents *renderCache[pdfImageContentKey, pdfImageContent] imageContents *renderCache[pdfImageContentKey, pdfImageContent]
attachmentIDs map[pdfAttachmentKey]string attachmentIDs map[pdfAttachmentKey]string
@@ -209,6 +211,13 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
if err != nil { if err != nil {
return nil, PDFImportReport{}, err return nil, PDFImportReport{}, err
} }
if len(editor.pages) != 0 {
importer.page = -1
if err := importer.documentOpenActions(ctx); err != nil {
return nil, PDFImportReport{}, fmt.Errorf("import PDF document actions: %w", err)
}
importer.page = 0
}
if options.OnProgress != nil { if options.OnProgress != nil {
if err := options.OnProgress("convert", 0, len(editor.pages)); err != nil { if err := options.OnProgress("convert", 0, len(editor.pages)); err != nil {
return nil, PDFImportReport{}, err return nil, PDFImportReport{}, err
@@ -230,6 +239,9 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
importer.clipTexts = make(map[*pdfgo.TextClip][]TextObject) importer.clipTexts = make(map[*pdfgo.TextClip][]TextObject)
importer.clipPaths = nil importer.clipPaths = nil
importer.annotationData = nil importer.annotationData = nil
if err := importer.pageOpenActions(ctx, page); err != nil {
return fmt.Errorf("import PDF page %d actions: %w", index+1, err)
}
if err := importer.compositePage(nil, func(visitor pdfgo.Visitor) error { return reader.WalkPage(ctx, page, visitor) }); err != nil { if err := importer.compositePage(nil, func(visitor pdfgo.Visitor) error { return reader.WalkPage(ctx, page, visitor) }); err != nil {
return fmt.Errorf("import PDF page %d: %w", index+1, err) return fmt.Errorf("import PDF page %d: %w", index+1, err)
} }
@@ -478,24 +490,31 @@ func (p *pdfImporter) pathDataContext(ctx context.Context, path pdfgo.Path, orig
return string(data), nil return string(data), nil
} }
// clips 复用只读裁剪几何,以区域CTM保留对象位移及路径交集 // clips 省略覆盖对象边界的矩形裁剪,复用其余几何并保留位移及路径交集
// 入参: paths 裁剪路径, origin 对象边界 // 入参: paths 裁剪路径, origin 对象边界
// 返回: *Clips OFD裁剪区域,无裁剪时为空, error 未解析的裁剪字形 // 返回: *Clips OFD裁剪区域,无裁剪时为空, error 未解析的裁剪字形
func (p *pdfImporter) clips(paths []pdfgo.Path, origin Box) (*Clips, error) { func (p *pdfImporter) clips(paths []pdfgo.Path, origin Box) (*Clips, error) {
if len(paths) == 0 { if len(paths) == 0 {
return nil, nil return nil, nil
} }
flag := false var clips *Clips
clips := &Clips{TransFlag: &flag, Clip: make([]Clip, len(paths))} var areas []ClipArea
areas := make([]ClipArea, len(paths))
translation := "" translation := ""
if origin.X != 0 || origin.Y != 0 { for _, path := range paths {
translation = pdfNumbers(1, 0, 0, 1, -origin.X, -origin.Y)
}
for index, path := range paths {
if err := p.ctx.Err(); err != nil { if err := p.ctx.Err(); err != nil {
return nil, err return nil, err
} }
if pdfClipContainsBoundary(path, p.matrix, origin) {
continue
}
if clips == nil {
clips = &Clips{TransFlag: new(false), Clip: make([]Clip, 0, len(paths))}
areas = make([]ClipArea, len(paths))
if origin.X != 0 || origin.Y != 0 {
translation = pdfNumbers(1, 0, 0, 1, -origin.X, -origin.Y)
}
}
index := len(clips.Clip)
area := &areas[index] area := &areas[index]
area.CTM = translation area.CTM = translation
if len(path.Segments) != 0 { if len(path.Segments) != 0 {
@@ -530,7 +549,7 @@ func (p *pdfImporter) clips(paths []pdfgo.Path, origin Box) (*Clips, error) {
fill, stroke := true, false fill, stroke := true, false
area.Path = []PathObject{{Boundary: "0 0 1 1", AbbreviatedData: "M 0 0 C", Fill: &fill, Stroke: &stroke}} area.Path = []PathObject{{Boundary: "0 0 1 1", AbbreviatedData: "M 0 0 C", Fill: &fill, Stroke: &stroke}}
} }
clips.Clip[index].Area = areas[index : index+1 : index+1] clips.Clip = append(clips.Clip, Clip{Area: areas[index : index+1 : index+1]})
} }
return clips, nil return clips, nil
} }
+30 -58
View File
@@ -40,7 +40,7 @@ type PDFThreeDRenderOptions struct {
// 入参: ctx 取消上下文, page PDF页面, annotation 三维注解 // 入参: ctx 取消上下文, page PDF页面, annotation 三维注解
// 返回: error 外观、资源或取消错误 // 返回: error 外观、资源或取消错误
func (p *pdfImporter) threeDAnnotation(ctx context.Context, page *pdfgo.Page, annotation pdfgo.Annotation) error { func (p *pdfImporter) threeDAnnotation(ctx context.Context, page *pdfgo.Page, annotation pdfgo.Annotation) error {
source, err := p.reader.ReadThreeD(annotation) source, err := p.reader.ReadThreeDContext(ctx, annotation)
if err != nil { if err != nil {
return p.interactiveAppearanceFallback(ctx, page, annotation, "metadata", err) return p.interactiveAppearanceFallback(ctx, page, annotation, "metadata", err)
} }
@@ -78,7 +78,7 @@ func (p *pdfImporter) threeDAnnotation(ctx context.Context, page *pdfgo.Page, an
return err return err
} }
} }
paint, err = p.threeDPainter(ctx, source, annotation, data) paint, err = p.threeDPainter(ctx, page, source, annotation, data)
if err != nil { if err != nil {
if ctx.Err() != nil { if ctx.Err() != nil {
return ctx.Err() return ctx.Err()
@@ -101,9 +101,9 @@ func (p *pdfImporter) threeDAnnotation(ctx context.Context, page *pdfgo.Page, an
} }
// threeDPainter 准备三维静态图像,复用库层压缩,失败时不提交注解对象 // threeDPainter 准备三维静态图像,复用库层压缩,失败时不提交注解对象
// 入参: ctx 取消上下文, source 三维参数, annotation 注解, data 原始模型 // 入参: ctx 取消上下文, page PDF页面, source 三维参数, annotation 注解, data 原始模型
// 返回: func(*pdfImporter) error 绘制回调, error 模型、绘制或资源错误 // 返回: func(*pdfImporter) error 绘制回调, error 模型、绘制或资源错误
func (p *pdfImporter) threeDPainter(ctx context.Context, source pdfgo.ThreeD, annotation pdfgo.Annotation, data []byte) (func(*pdfImporter) error, error) { func (p *pdfImporter) threeDPainter(ctx context.Context, page *pdfgo.Page, source pdfgo.ThreeD, annotation pdfgo.Annotation, data []byte) (func(*pdfImporter) error, error) {
box := source.ViewBox box := source.ViewBox
width, height := box.XMax-box.XMin, box.YMax-box.YMin width, height := box.XMax-box.XMin, box.YMax-box.YMin
x, y := annotation.Rect.XMin/2+annotation.Rect.XMax/2, annotation.Rect.YMin/2+annotation.Rect.YMax/2 x, y := annotation.Rect.XMin/2+annotation.Rect.XMax/2, annotation.Rect.YMin/2+annotation.Rect.YMax/2
@@ -123,6 +123,17 @@ func (p *pdfImporter) threeDPainter(ctx context.Context, source pdfgo.ThreeD, an
nodes = append(nodes, pdfCompositeNode{path: &pdfgo.PathMark{Path: path, Fill: true, Style: pdfgo.Style{Fill: pdfgo.Paint{RGB: *source.Background.RGB, Alpha: 1}}}}) nodes = append(nodes, pdfCompositeNode{path: &pdfgo.PathMark{Path: path, Fill: true, Style: pdfgo.Style{Fill: pdfgo.Paint{RGB: *source.Background.RGB, Alpha: 1}}}})
} }
if width > 0 && height > 0 { if width > 0 && height > 0 {
var overlay []pdfCompositeNode
if source.Presentation.Overlay != nil {
var err error
overlay, err = p.collectCompositeNodes(func(visitor pdfgo.Visitor) error {
return p.reader.WalkThreeDOverlay(ctx, page, source, visitor)
})
if err != nil {
return nil, err
}
source.Presentation.Overlay = nil
}
if source.Format != "U3D" { if source.Format != "U3D" {
return nil, &pdfgo.UnsupportedError{Feature: "3D model format " + string(source.Format)} return nil, &pdfgo.UnsupportedError{Feature: "3D model format " + string(source.Format)}
} }
@@ -145,6 +156,7 @@ func (p *pdfImporter) threeDPainter(ctx context.Context, source pdfgo.ThreeD, an
return nil, err return nil, err
} }
nodes = append(nodes, pdfCompositeNode{image: &pdfgo.ImageMark{Image: resource, Matrix: pdfgo.Matrix{width, 0, 0, height, x + box.XMin, y + box.YMin}, Style: pdfgo.Style{Fill: pdfgo.Paint{Alpha: 1}}}}) nodes = append(nodes, pdfCompositeNode{image: &pdfgo.ImageMark{Image: resource, Matrix: pdfgo.Matrix{width, 0, 0, height, x + box.XMin, y + box.YMin}, Style: pdfgo.Style{Fill: pdfgo.Paint{Alpha: 1}}}})
nodes = append(nodes, overlay...)
} }
return func(stamp *pdfImporter) error { return func(stamp *pdfImporter) error {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
@@ -169,9 +181,12 @@ func RenderPDFThreeD(ctx context.Context, model *U3DModel, source pdfgo.ThreeD,
if source.Format != "U3D" { if source.Format != "U3D" {
return nil, &pdfgo.UnsupportedError{Feature: "3D model format " + string(source.Format)} 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 presentation := source.Presentation
if presentation.RenderMode != "" && presentation.RenderMode != "Solid" || presentation.Overlay != nil { if presentation.Overlay != nil {
return nil, &pdfgo.UnsupportedError{Feature: "3D view render mode or overlay"} return nil, &pdfgo.UnsupportedError{Feature: "3D view overlay"}
} }
if source.Background.RGB == nil { if source.Background.RGB == nil {
return nil, &pdfgo.UnsupportedError{Feature: "3D background color space"} return nil, &pdfgo.UnsupportedError{Feature: "3D background color space"}
@@ -186,6 +201,9 @@ func RenderPDFThreeD(ctx context.Context, model *U3DModel, source pdfgo.ThreeD,
projection = *source.Projection projection = *source.Projection
} }
render := U3DRenderOptions{Width: options.Width, Height: options.Height, MaxMemoryBytes: options.MaxMemoryBytes, MaxInstances: options.MaxInstances, Background: source.Background.RGB} render := U3DRenderOptions{Width: options.Width, Height: options.Height, MaxMemoryBytes: options.MaxMemoryBytes, MaxInstances: options.MaxInstances, Background: source.Background.RGB}
if err := pdfThreeDRenderMode(source, &render); err != nil {
return nil, err
}
if err := pdfThreeDNodeStates(ctx, presentation.Nodes, &render); err != nil { if err := pdfThreeDNodeStates(ctx, presentation.Nodes, &render); err != nil {
return nil, err return nil, err
} }
@@ -210,35 +228,11 @@ func RenderPDFThreeD(ctx context.Context, model *U3DModel, source pdfgo.ThreeD,
if err := r.prepareSections(render.Sections); err != nil { if err := r.prepareSections(render.Sections); err != nil {
return nil, err return nil, err
} }
scale := 0.0 scale, err := projection.ScaleFactor(source.ViewBox, source.AnnotationBox)
switch projection.Subtype {
case "P":
if !finite(projection.FieldOfView) || projection.FieldOfView <= 0 || projection.FieldOfView >= 180 {
return nil, &pdfgo.UnsupportedError{Feature: "degenerate 3D perspective field of view"}
}
diameter := projection.PerspectiveDiameter
if projection.PerspectiveBinding != "" {
diameter, err = pdfThreeDBinding(projection.PerspectiveBinding, width, height)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} camera.Perspective = projection.Subtype == "P"
if !finite(diameter) || diameter <= 0 {
return nil, fmt.Errorf("invalid PDF 3D perspective diameter")
}
scale = diameter / (2 * math.Tan(projection.FieldOfView*math.Pi/360))
camera.Perspective = true
case "O":
width := source.AnnotationBox.XMax - source.AnnotationBox.XMin
height := source.AnnotationBox.YMax - source.AnnotationBox.YMin
binding, err := pdfThreeDBinding(projection.OrthographicBinding, width, height)
if err != nil {
return nil, err
}
scale = projection.OrthographicScale * binding
default:
return nil, fmt.Errorf("invalid PDF 3D projection subtype")
}
camera.ScaleX = scale * float64(options.Width) / width camera.ScaleX = scale * float64(options.Width) / width
camera.ScaleY = scale * float64(options.Height) / height camera.ScaleY = scale * float64(options.Height) / height
switch projection.Clipping { switch projection.Clipping {
@@ -310,29 +304,6 @@ func RenderPDFThreeD(ctx context.Context, model *U3DModel, source pdfgo.ThreeD,
return r.render(render, passes) return r.render(render, passes)
} }
// pdfThreeDBinding 计算目标区域的标准绑定倍率,不按模型包围盒重新适配
// 入参: binding 绑定方式, width 区域宽度, height 区域高度
// 返回: float64 倍率, error 方式或尺寸错误
func pdfThreeDBinding(binding pdfgo.Name, width, height float64) (float64, error) {
if binding == "Absolute" {
return 1, nil
}
if !finite(width) || !finite(height) || width <= 0 || height <= 0 {
return 0, fmt.Errorf("invalid PDF 3D binding dimensions")
}
switch binding {
case "W":
return width, nil
case "H":
return height, nil
case "Min":
return min(width, height), nil
case "Max":
return max(width, height), nil
}
return 0, fmt.Errorf("invalid PDF 3D binding")
}
// pdfCamera 解析PDF相机来源并保留模型视图的绘制轮次 // pdfCamera 解析PDF相机来源并保留模型视图的绘制轮次
// 入参: source 相机来源 // 入参: source 相机来源
// 返回: U3DCamera 相机变换, string 视图节点, error 路径或变换错误 // 返回: U3DCamera 相机变换, string 视图节点, error 路径或变换错误
@@ -414,7 +385,7 @@ func (r *u3dRender) pdfCamera(source pdfgo.ThreeDCamera) (U3DCamera, string, err
return camera, view, nil return camera, view, nil
} }
// depthRange 计算可见模型面的相机深度范围,不为自动裁剪复制顶点 // depthRange 计算可见模型几何的相机深度范围,不为自动裁剪复制顶点
// 返回: float64 最近深度, float64 最远深度, bool 是否含几何, error 几何或取消错误 // 返回: float64 最近深度, float64 最远深度, bool 是否含几何, error 几何或取消错误
func (r *u3dRender) depthRange() (float64, float64, bool, error) { func (r *u3dRender) depthRange() (float64, float64, bool, error) {
minimum, maximum := math.Inf(1), math.Inf(-1) minimum, maximum := math.Inf(1), math.Inf(-1)
@@ -431,13 +402,14 @@ func (r *u3dRender) depthRange() (float64, float64, bool, error) {
} }
mesh := &r.model.Meshes[index] mesh := &r.model.Meshes[index]
matrix := r.toCamera.multiply(instance.world) matrix := r.toCamera.multiply(instance.world)
for j, face := range mesh.Faces { for j := range len(mesh.Faces) + len(mesh.Lines) {
_, corners := mesh.primitive(j)
if j%256 == 0 { if j%256 == 0 {
if err := r.ctx.Err(); err != nil { if err := r.ctx.Err(); err != nil {
return 0, 0, false, err return 0, 0, false, err
} }
} }
for _, corner := range face.Corners { for _, corner := range corners {
if uint64(corner.Position) >= uint64(len(mesh.Positions)) { if uint64(corner.Position) >= uint64(len(mesh.Positions)) {
return 0, 0, false, fmt.Errorf("invalid U3D position index") return 0, 0, false, fmt.Errorf("invalid U3D position index")
} }
+82
View File
@@ -0,0 +1,82 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"context"
"maps"
"github.com/xiaoqidun/pdfgo"
)
// pdfThreeDMarkupState 复用同页批注的宿主解析及模型校验,不保留模型解码缓冲
type pdfThreeDMarkupState struct {
sources map[pdfgo.Reference]pdfgo.ThreeD
sums map[*pdfgo.Stream][16]byte
}
// threeDMarkupAnnotation 应用默认三维视图的批注可见性,其他视图批注保留为隐藏注解
// 入参: ctx 取消上下文, annotation 批注, markup 关联, state 同页缓存, strict 严格检查开关
// 返回: pdfgo.Annotation 静态批注, error 关联、校验或取消错误
func (p *pdfImporter) threeDMarkupAnnotation(ctx context.Context, annotation pdfgo.Annotation, markup *pdfgo.ThreeDMarkup, state *pdfThreeDMarkupState, strict bool) (pdfgo.Annotation, error) {
if markup == nil {
return annotation, nil
}
if strict {
return annotation, &pdfgo.UnsupportedError{Feature: "3D markup view switching conversion"}
}
source, ok := state.sources[markup.Annotation.Reference]
if !ok || markup.Annotation.Reference == (pdfgo.Reference{}) {
var err error
source, err = p.reader.ReadThreeDContext(ctx, markup.Annotation)
if err != nil {
return annotation, err
}
if markup.Annotation.Reference != (pdfgo.Reference{}) {
if state.sources == nil {
state.sources = make(map[pdfgo.Reference]pdfgo.ThreeD)
}
state.sources[markup.Annotation.Reference] = source
}
}
if markup.Checksum != nil {
sum, ok := state.sums[source.Stream]
if !ok {
var err error
sum, err = pdfgo.ThreeDArtworkChecksum(ctx, source.Stream)
if err != nil {
return annotation, err
}
if state.sums == nil {
state.sums = make(map[*pdfgo.Stream][16]byte)
}
state.sums[source.Stream] = sum
}
if sum != *markup.Checksum {
p.warning(pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF 3D markup artwork checksum differs; view association may be stale"})
}
}
visible := source.ActivationPolicy.Activation != "XA" && markup.MatchesView(source.SelectedView)
if !visible {
flags, err := p.reader.ReadAnnotationFlags(annotation)
if err != nil {
return annotation, err
}
annotation.Dictionary = maps.Clone(annotation.Dictionary)
annotation.Dictionary["F"] = pdfgo.Integer(flags | 2)
}
p.warning(pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF 3D markup default view visibility applied; view switching not transferred"})
return annotation, ctx.Err()
}
+74
View File
@@ -0,0 +1,74 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import "github.com/xiaoqidun/pdfgo"
// pdfThreeDRenderMode 将PDF标准绘制模式映射到通用几何绘制参数
// 入参: source 三维参数, options 绘制选项
// 返回: error 未支持的模式错误
func pdfThreeDRenderMode(source pdfgo.ThreeD, options *U3DRenderOptions) error {
mode := source.Presentation.RenderMode
if mode == nil {
return nil
}
switch mode.Subtype {
case "Solid":
case "Transparent":
options.Opacity = &mode.Opacity
case "SolidWireframe", "TransparentWireframe":
options.Mode, options.AuxiliaryColor = U3DRenderSolidWireframe, &mode.AuxiliaryColor
if mode.Subtype == "TransparentWireframe" {
options.Opacity = &mode.Opacity
}
case "Wireframe":
options.Mode, options.AuxiliaryColor = U3DRenderWireframe, &mode.AuxiliaryColor
case "ShadedWireframe":
options.Mode = U3DRenderShadedWireframe
case "HiddenWireframe":
options.Mode, options.AuxiliaryColor = U3DRenderHiddenWireframe, &mode.AuxiliaryColor
case "Vertices":
options.Mode, options.AuxiliaryColor = U3DRenderVertices, &mode.AuxiliaryColor
case "ShadedVertices":
options.Mode = U3DRenderShadedVertices
case "BoundingBox":
options.Mode, options.AuxiliaryColor = U3DRenderBoundingBox, &mode.AuxiliaryColor
case "TransparentBoundingBox", "TransparentBoundingBoxOutline":
options.Mode = U3DRenderBoundingBoxFaces
options.FaceColor, options.Opacity = mode.FaceColor, &mode.Opacity
if mode.Subtype == "TransparentBoundingBoxOutline" {
options.Mode, options.AuxiliaryColor = U3DRenderBoundingBoxOutline, &mode.AuxiliaryColor
}
case "Illustration":
options.Mode = U3DRenderIllustration
options.AuxiliaryColor, options.FaceColor, options.CreaseAngle = &mode.AuxiliaryColor, mode.FaceColor, &mode.CreaseAngle
case "SolidOutline", "ShadedIllustration":
options.Mode = U3DRenderSolidOutline
if mode.Subtype == "ShadedIllustration" {
options.Mode = U3DRenderShadedIllustration
}
options.AuxiliaryColor, options.CreaseAngle = &mode.AuxiliaryColor, &mode.CreaseAngle
default:
return &pdfgo.UnsupportedError{Feature: "3D render mode " + string(mode.Subtype)}
}
if mode.AnnotationBackground && (mode.Subtype == "Illustration" || mode.Subtype == "TransparentBoundingBox" || mode.Subtype == "TransparentBoundingBoxOutline") {
options.FaceColor = source.AnnotationBackground
if source.AnnotationBackground == nil {
zero := 0.0
options.Opacity = &zero
}
}
return nil
}
+143
View File
@@ -0,0 +1,143 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"context"
"errors"
"fmt"
"github.com/xiaoqidun/pdfgo"
)
// documentOpenActions 转换文档初始目标与打开动作,不执行脚本或打开资源
// 入参: ctx 取消上下文
// 返回: error 动作、编码或取消错误
func (p *pdfImporter) documentOpenActions(ctx context.Context) error {
opened, err := p.reader.ReadOpenAction(ctx)
if err != nil {
if p.warning == nil || !errors.Is(err, pdfgo.ErrInvalidDestination) {
return err
}
p.warning(pdfgo.Diagnostic{Message: fmt.Sprintf("invalid PDF initial destination ignored: %v", err)})
}
var object pdfgo.Object
if opened.Action != nil {
object = opened.Action
}
if dest := opened.Destination; dest != nil {
array := append(pdfgo.Array{dest.Page, dest.Mode}, dest.Parameters...)
object = pdfgo.Dictionary{"S": pdfgo.Name("GoTo"), "D": array}
}
currentPage := 0
actions, err := p.eventActions(ctx, object, "DO", &currentPage)
if err != nil {
return err
}
if err := p.editor.SetDocumentActions(actions); err != nil {
return err
}
triggers, err := p.reader.ReadDocumentTriggers(ctx)
if err != nil {
return err
}
for _, trigger := range triggers {
if _, err := p.reader.ReadJavaScriptAction(ctx, trigger.Action); err != nil {
return err
}
if err := p.unmappedTrigger(ctx, "document", trigger); err != nil {
return err
}
}
return ctx.Err()
}
// pageOpenActions 将页面打开动作放在注解打开动作之前,关闭事件不改为打开事件
// 入参: ctx 取消上下文, page PDF页面
// 返回: error 事件、动作或取消错误
func (p *pdfImporter) pageOpenActions(ctx context.Context, page *pdfgo.Page) error {
p.pageActionPage = p.page
triggers, err := p.reader.ReadPageTriggers(ctx, page)
if err != nil {
return err
}
for _, trigger := range triggers {
if trigger.Event != "O" {
if err := p.unmappedTrigger(ctx, "page", trigger); err != nil {
return err
}
continue
}
actions, err := p.eventActions(ctx, trigger.Action, "PO", &p.pageActionPage)
if err != nil {
return err
}
if err := p.editor.SetPageActions(p.page, actions); err != nil {
return err
}
}
return ctx.Err()
}
// eventActions 按原顺序转换动作链,保留事件类型与链内跳转页状态
// 入参: ctx 取消上下文, object 动作对象, event OFD事件, currentPage 当前页面
// 返回: []Action 转换动作, error 动作或取消错误
func (p *pdfImporter) eventActions(ctx context.Context, object pdfgo.Object, event string, currentPage *int) ([]Action, error) {
var actions []Action
err := p.reader.WalkActions(ctx, object, func(info pdfgo.ActionInfo) error {
annotation := pdfgo.Annotation{Dictionary: pdfgo.Dictionary{"A": info.Dictionary}}
action, err := p.linkAction(annotation, p.warning == nil, currentPage)
if err != nil {
var unsupported *pdfgo.UnsupportedError
if event != "CLICK" && p.warning != nil && errors.Is(err, pdfgo.ErrInvalidAction) {
p.warning(pdfgo.Diagnostic{Message: fmt.Sprintf("invalid PDF %s action ignored: %v", event, err)})
return nil
}
if event != "CLICK" && p.warning != nil && errors.Is(err, pdfgo.ErrInvalidDestination) {
p.warning(pdfgo.Diagnostic{Message: fmt.Sprintf("invalid PDF %s destination ignored: %v", event, err)})
return nil
}
if event != "CLICK" && p.warning != nil && errors.As(err, &unsupported) {
p.warning(pdfgo.Diagnostic{Message: fmt.Sprintf("PDF %s action not transferred to OFD: %s", event, unsupported.Feature)})
return nil
}
return err
}
if action != nil {
action.Event = event
actions = append(actions, *action)
}
return nil
})
if err != nil {
return nil, err
}
return actions, ctx.Err()
}
// unmappedTrigger 校验无法映射的事件链,按导入策略报告而不改变触发条件
// 入参: ctx 取消上下文, owner 事件归属, trigger PDF事件
// 返回: error 动作链、取消或严格模式错误
func (p *pdfImporter) unmappedTrigger(ctx context.Context, owner string, trigger pdfgo.ActionTrigger) error {
if err := p.reader.WalkActions(ctx, trigger.Action, func(pdfgo.ActionInfo) error { return nil }); err != nil {
return err
}
feature := owner + " trigger " + string(trigger.Event)
if p.warning == nil {
return &pdfgo.UnsupportedError{Feature: feature}
}
p.warning(pdfgo.Diagnostic{Message: "PDF " + feature + " not transferred to OFD"})
return nil
}
+42 -6
View File
@@ -17,6 +17,7 @@ package ofdgo
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/json"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"maps" "maps"
@@ -36,8 +37,12 @@ func (p *pdfImporter) appearanceAnnotation(ctx context.Context, page *pdfgo.Page
// 入参: ctx 取消上下文, page PDF页面, annotation PDF注解, paint 绘制回调,空值使用原外观, actions 外观区域的动作 // 入参: ctx 取消上下文, page PDF页面, annotation PDF注解, paint 绘制回调,空值使用原外观, actions 外观区域的动作
// 返回: error 外观或转换错误 // 返回: error 外观或转换错误
func (p *pdfImporter) paintedAnnotation(ctx context.Context, page *pdfgo.Page, annotation pdfgo.Annotation, paint func(*pdfImporter) error, actions ...Action) error { func (p *pdfImporter) paintedAnnotation(ctx context.Context, page *pdfgo.Page, annotation pdfgo.Annotation, paint func(*pdfImporter) error, actions ...Action) error {
for _, key := range []pdfgo.Name{"AA", "OC", "A"} { clicks, opened, err := p.annotationTriggerActions(ctx, annotation)
if annotation.Subtype == "Widget" && (key == "A" || key == "AA") { if err != nil {
return err
}
for _, key := range []pdfgo.Name{"OC", "A"} {
if annotation.Subtype == "Widget" && key == "A" {
continue continue
} }
if key == "A" && (annotation.Subtype == "Link" || annotation.Subtype == "Movie") { if key == "A" && (annotation.Subtype == "Link" || annotation.Subtype == "Movie") {
@@ -63,6 +68,9 @@ func (p *pdfImporter) paintedAnnotation(ctx context.Context, page *pdfgo.Page, a
if err != nil { if err != nil {
return err return err
} }
if flags&64 != 0 && annotation.Subtype != "Widget" {
actions, clicks = nil, nil
}
invisible := flags&1 != 0 && !annotation.IsStandard() invisible := flags&1 != 0 && !annotation.IsStandard()
if flags&256 != 0 { if flags&256 != 0 {
return &pdfgo.UnsupportedError{Feature: "annotation ToggleNoView flag"} return &pdfgo.UnsupportedError{Feature: "annotation ToggleNoView flag"}
@@ -73,6 +81,16 @@ func (p *pdfImporter) paintedAnnotation(ctx context.Context, page *pdfgo.Page, a
p.matrix.Apply(pdfgo.Point{X: annotation.Rect.XMax, Y: annotation.Rect.YMax}), p.matrix.Apply(pdfgo.Point{X: annotation.Rect.XMax, Y: annotation.Rect.YMax}),
p.matrix.Apply(pdfgo.Point{X: annotation.Rect.XMin, Y: annotation.Rect.YMax}), p.matrix.Apply(pdfgo.Point{X: annotation.Rect.XMin, Y: annotation.Rect.YMax}),
}) })
if len(clicks) != 0 && annotation.Subtype == "Link" {
region, err := p.linkRegion(annotation, box)
if err != nil {
return err
}
for i := range clicks {
clicks[i].Region = region
}
}
actions = append(actions, clicks...)
stamp := *p stamp := *p
stamp.matrix = (pdfgo.Matrix{1, 0, 0, 1, -box.X, -box.Y}).Mul(p.matrix) stamp.matrix = (pdfgo.Matrix{1, 0, 0, 1, -box.X, -box.Y}).Mul(p.matrix)
stamp.objects = nil stamp.objects = nil
@@ -120,6 +138,21 @@ func (p *pdfImporter) paintedAnnotation(ctx context.Context, page *pdfgo.Page, a
return err return err
} }
converted := Annotation{Type: pdfAnnotationType(annotation.Subtype), Subtype: string(annotation.Subtype), Remark: remark, Appearance: Appearance{Boundary: pdfBoundary(box), Objects: objects}} converted := Annotation{Type: pdfAnnotationType(annotation.Subtype), Subtype: string(annotation.Subtype), Remark: remark, Appearance: Appearance{Boundary: pdfBoundary(box), Objects: objects}}
if annotation.Subtype == "Projection" {
projection, err := p.reader.ReadProjectionAnnotation(annotation)
if err != nil {
return err
}
converted.Parameters = &AnnotationParameters{Parameter: []AnnotationParameter{{Name: "PDF.Projection.Runtime", Value: "unavailable"}}}
if projection.Measurement != nil {
data, err := json.Marshal(projection.Measurement)
if err != nil {
return err
}
converted.Parameters.Parameter = append(converted.Parameters.Parameter, AnnotationParameter{Name: "PDF.Projection.Measurement", Value: string(data)})
}
p.warning(pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF projection annotation metadata and static appearance retained; runtime rendering not transferred"})
}
if annotation.Subtype == "Redact" { if annotation.Subtype == "Redact" {
converted.Parameters = &AnnotationParameters{Parameter: []AnnotationParameter{{Name: "PDF.Redact.Pending", Value: "true"}}} converted.Parameters = &AnnotationParameters{Parameter: []AnnotationParameter{{Name: "PDF.Redact.Pending", Value: "true"}}}
} }
@@ -187,13 +220,16 @@ func (p *pdfImporter) paintedAnnotation(ctx context.Context, page *pdfgo.Page, a
return fmt.Errorf("PDF annotation: %w", err) return fmt.Errorf("PDF annotation: %w", err)
} }
p.annotationData = append(p.annotationData, bytes.TrimPrefix(data, []byte(xml.Header))...) p.annotationData = append(p.annotationData, bytes.TrimPrefix(data, []byte(xml.Header))...)
if len(opened) != 0 {
p.editor.pages[p.page].Actions = append(p.editor.pages[p.page].Actions, opened...)
}
if len(actions) != 0 && annotation.Subtype == "Link" || len(clicks) != 0 {
p.report.Links++
}
p.report.TextObjects += stamp.report.TextObjects p.report.TextObjects += stamp.report.TextObjects
p.report.PathObjects += stamp.report.PathObjects p.report.PathObjects += stamp.report.PathObjects
p.report.ImageObjects += stamp.report.ImageObjects p.report.ImageObjects += stamp.report.ImageObjects
p.report.Warnings = append(p.report.Warnings, stamp.report.Warnings...) p.report.Warnings = append(p.report.Warnings, stamp.report.Warnings...)
if flags&64 != 0 {
p.report.Warnings = append(p.report.Warnings, pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF annotation read-only flag not transferred"})
}
return nil return nil
} }
@@ -208,7 +244,7 @@ func pdfAnnotationType(subtype pdfgo.Name) string {
return "Watermark" return "Watermark"
case "Highlight", "Underline", "Squiggly", "StrikeOut": case "Highlight", "Underline", "Squiggly", "StrikeOut":
return "Highlight" return "Highlight"
case "Text", "FreeText", "Line", "Square", "Circle", "Polygon", "PolyLine", "Caret", "Ink", "FileAttachment", "Sound", "Movie", "Screen", "Link", "Popup", "Widget", "PrinterMark", "TrapNet", "3D", "RichMedia", "Redact": case "Text", "FreeText", "Line", "Square", "Circle", "Polygon", "PolyLine", "Caret", "Ink", "FileAttachment", "Sound", "Movie", "Screen", "Link", "Popup", "Widget", "PrinterMark", "TrapNet", "3D", "RichMedia", "Redact", "Projection":
return "Path" return "Path"
} }
if !(pdfgo.Annotation{Subtype: subtype}).IsStandard() { if !(pdfgo.Annotation{Subtype: subtype}).IsStandard() {
+75
View File
@@ -0,0 +1,75 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"context"
"fmt"
"github.com/xiaoqidun/pdfgo"
)
// annotationTriggerActions 将释放点击和页面打开事件转换为OFD,不改变其他事件的触发条件
// 入参: ctx 取消上下文, annotation PDF注解
// 返回: []Action 点击动作, []Action 页面打开动作, error 事件、动作或取消错误
func (p *pdfImporter) annotationTriggerActions(ctx context.Context, annotation pdfgo.Annotation) ([]Action, []Action, error) {
if annotation.Subtype == "Widget" {
return nil, nil, nil
}
triggers, err := p.reader.ReadAnnotationTriggers(ctx, annotation)
if err != nil {
return nil, nil, err
}
if len(triggers) == 0 {
return nil, nil, nil
}
flags, err := p.reader.ReadAnnotationFlags(annotation)
if err != nil {
return nil, nil, err
}
var clicks, opened []Action
for _, trigger := range triggers {
if flags&64 != 0 && (trigger.Event == "E" || trigger.Event == "X" || trigger.Event == "D" || trigger.Event == "U") {
continue
}
event := ""
switch trigger.Event {
case "U":
event = "CLICK"
case "PO":
event = "PO"
default:
return nil, nil, &pdfgo.UnsupportedError{Feature: "annotation trigger " + string(trigger.Event)}
}
currentPage := p.page
if event == "PO" {
currentPage = p.pageActionPage
}
actions, err := p.eventActions(ctx, trigger.Action, event, &currentPage)
if err != nil {
return nil, nil, err
}
if event == "CLICK" {
clicks = append(clicks, actions...)
} else {
p.pageActionPage = currentPage
opened = append(opened, actions...)
}
}
if len(opened) != 0 && (p.page < 0 || p.page >= len(p.editor.pages)) {
return nil, nil, fmt.Errorf("invalid annotation trigger page")
}
return clicks, opened, ctx.Err()
}
+49
View File
@@ -30,6 +30,55 @@ type pdfClipPath struct {
path *[1]PathObject path *[1]PathObject
} }
// pdfClipContainsBoundary 判断单一矩形裁剪是否完整覆盖对象边界,不近似曲线或斜边
// 入参: path PDF裁剪路径, matrix 页面变换, boundary OFD对象边界
// 返回: bool 是否可由对象边界代替裁剪
func pdfClipContainsBoundary(path pdfgo.Path, matrix pdfgo.Matrix, boundary Box) bool {
if len(path.Text) != 0 || len(path.Segments) < 4 || len(path.Segments) > 6 || !finite(boundary.X) || !finite(boundary.Y) || !finite(boundary.W) || !finite(boundary.H) || boundary.W <= 0 || boundary.H <= 0 {
return false
}
var points [4]pdfgo.Point
for index := range 4 {
segment := path.Segments[index]
operator := "L"
if index == 0 {
operator = "M"
}
if segment.Operator != operator || len(segment.Points) != 1 {
return false
}
points[index] = matrix.Apply(segment.Points[0])
if !finite(points[index].X) || !finite(points[index].Y) {
return false
}
}
end := 4
if end < len(path.Segments) && path.Segments[end].Operator == "L" {
segment := path.Segments[end]
if len(segment.Points) != 1 || matrix.Apply(segment.Points[0]) != points[0] {
return false
}
end++
}
if end < len(path.Segments) {
segment := path.Segments[end]
if segment.Operator != "C" || len(segment.Points) != 0 {
return false
}
end++
}
if end != len(path.Segments) {
return false
}
a, b, c, d := points[0], points[1], points[2], points[3]
if !(a.Y == b.Y && b.X == c.X && c.Y == d.Y && d.X == a.X || a.X == b.X && b.Y == c.Y && c.X == d.X && d.Y == a.Y) {
return false
}
left, right := math.Min(a.X, c.X), math.Max(a.X, c.X)
top, bottom := math.Min(a.Y, c.Y), math.Max(a.Y, c.Y)
return left < right && top < bottom && boundary.X >= left && boundary.Y >= top && boundary.X+boundary.W <= right && boundary.Y+boundary.H <= bottom
}
// pdfClipPathKey 按路径完整内容与变换生成键,不依赖可变切片的地址 // pdfClipPathKey 按路径完整内容与变换生成键,不依赖可变切片的地址
// 入参: ctx 取消上下文, path 裁剪路径, matrix 页面变换 // 入参: ctx 取消上下文, path 裁剪路径, matrix 页面变换
// 返回: [32]byte 内容摘要, error 取消错误 // 返回: [32]byte 内容摘要, error 取消错误
+31 -5
View File
@@ -17,6 +17,7 @@ package ofdgo
import ( import (
"context" "context"
"fmt" "fmt"
"image"
"math" "math"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
@@ -51,9 +52,13 @@ func (c *pdfCompositor) draw(nodes []pdfCompositeNode, pixels []pdfCompositePixe
// 返回: error 几何或颜色错误 // 返回: error 几何或颜色错误
func (c *pdfCompositor) drawMark(node pdfCompositeNode, pixels []pdfCompositePixel, space *pdfgo.ColorSpace) error { func (c *pdfCompositor) drawMark(node pdfCompositeNode, pixels []pdfCompositePixel, space *pdfgo.ColorSpace) error {
box, err := c.markBounds(node) box, err := c.markBounds(node)
if err != nil || c.pixelBounds(box).Empty() { if err != nil {
return err return err
} }
region := c.pixelBounds(box)
if region.Empty() {
return nil
}
style, fill, stroke := node.style() style, fill, stroke := node.style()
mask, err := c.mask(style.SoftMask, space) mask, err := c.mask(style.SoftMask, space)
if err != nil { if err != nil {
@@ -96,7 +101,7 @@ func (c *pdfCompositor) drawMark(node pdfCompositeNode, pixels []pdfCompositePix
} }
} }
if grouped { if grouped {
return pdfCompositeGroup(c.importer.ctx, pixels, initial, result, space, space, style.Fill.Alpha, mask, style.BlendMode, style.RenderingIntent, style.ColorConversion) return pdfCompositeGroupRegion(c.importer.ctx, pixels, initial, result, c.width, region, space, space, style.Fill.Alpha, mask, style.BlendMode, style.RenderingIntent, style.ColorConversion)
} }
return nil return nil
} }
@@ -333,6 +338,7 @@ func (c *pdfCompositor) drawGroup(node pdfCompositeNode, pixels []pdfCompositePi
if b.W <= 0 || b.H <= 0 || b.X >= c.box.X+c.box.W || b.Y >= c.box.Y+c.box.H || b.X+b.W <= c.box.X || b.Y+b.H <= c.box.Y { if b.W <= 0 || b.H <= 0 || b.X >= c.box.X+c.box.W || b.Y >= c.box.Y+c.box.H || b.X+b.W <= c.box.X || b.Y+b.H <= c.box.Y {
return nil return nil
} }
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 || !pdfNormalBlend(g.BlendMode)
defer func() { c.transferBlocked = blocked }() defer func() { c.transferBlocked = blocked }()
@@ -362,12 +368,13 @@ func (c *pdfCompositor) drawGroup(node pdfCompositeNode, pixels []pdfCompositePi
} else { } else {
sameSpace := space.Equal(parent) sameSpace := space.Equal(parent)
components := parent.Components() components := parent.Components()
for i, pixel := range pixels { for i := range pixels {
if i%pdfCompositeTileSize == 0 { if i%pdfCompositeTileSize == 0 {
if err := c.importer.ctx.Err(); err != nil { if err := c.importer.ctx.Err(); err != nil {
return err return err
} }
} }
pixel := &pixels[i]
values := pixel.values values := pixel.values
if !sameSpace { if !sameSpace {
var err error var err error
@@ -408,7 +415,7 @@ func (c *pdfCompositor) drawGroup(node pdfCompositeNode, pixels []pdfCompositePi
} }
} }
} }
return pdfCompositeGroup(c.importer.ctx, pixels, initial, result, space, parent, g.Alpha, mask, g.BlendMode, g.RenderingIntent, g.ColorConversion) return pdfCompositeGroupRegion(c.importer.ctx, pixels, initial, result, c.width, region, space, parent, g.Alpha, mask, g.BlendMode, g.RenderingIntent, g.ColorConversion)
} }
// drawKnockout 将每个对象与组初始背景合成,再按对象形状替换先前对象 // drawKnockout 将每个对象与组初始背景合成,再按对象形状替换先前对象
@@ -477,19 +484,36 @@ func pdfCompositeKnockout(target *pdfCompositePixel, initial, source pdfComposit
// 入参: ctx 取消上下文, pixels 父组输出, initial 初始背景, result 组结果, space 组空间, parent 父空间, opacity 组不透明度, mask 蒙版, mode 混合模式, intent 渲染意图, conversion 设备转换函数 // 入参: ctx 取消上下文, pixels 父组输出, initial 初始背景, result 组结果, space 组空间, parent 父空间, opacity 组不透明度, mask 蒙版, mode 混合模式, intent 渲染意图, conversion 设备转换函数
// 返回: error 颜色或混合错误 // 返回: error 颜色或混合错误
func pdfCompositeGroup(ctx context.Context, pixels, initial, result []pdfCompositePixel, space, parent *pdfgo.ColorSpace, opacity float64, mask []float64, mode, intent pdfgo.Name, conversion pdfgo.ColorConversion) error { func pdfCompositeGroup(ctx context.Context, pixels, initial, result []pdfCompositePixel, space, parent *pdfgo.ColorSpace, opacity float64, mask []float64, mode, intent pdfgo.Name, conversion pdfgo.ColorConversion) error {
return pdfCompositeGroupRegion(ctx, pixels, initial, result, max(1, len(pixels)), image.Rect(0, 0, len(pixels), 1), space, parent, opacity, mask, mode, intent, conversion)
}
// pdfCompositeGroupRegion 在组覆盖区域合成像素,保留原栅格及形状贡献
// 入参: ctx 取消上下文, pixels 父组输出, initial 初始背景, result 组结果, stride 行跨度, region 覆盖区域, space 组空间, parent 父空间, opacity 组不透明度, mask 蒙版, mode 混合模式, intent 渲染意图, conversion 设备转换函数
// 返回: error 缓冲、区域、颜色或混合错误
func pdfCompositeGroupRegion(ctx context.Context, pixels, initial, result []pdfCompositePixel, stride int, region image.Rectangle, space, parent *pdfgo.ColorSpace, opacity float64, mask []float64, mode, intent pdfgo.Name, conversion pdfgo.ColorConversion) error {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return err return err
} }
if len(pixels) != len(initial) || len(pixels) != len(result) || mask != nil && len(mask) != len(pixels) { if len(pixels) != len(initial) || len(pixels) != len(result) || mask != nil && len(mask) != len(pixels) {
return fmt.Errorf("invalid PDF transparency group buffers") return fmt.Errorf("invalid PDF transparency group buffers")
} }
if stride <= 0 || region.Min.X < 0 || region.Min.Y < 0 || region.Max.X > stride || !region.Empty() && region.Max.Y > len(pixels)/stride {
return fmt.Errorf("invalid PDF transparency group region")
}
group, destination := pdfgo.ColorantGroup{Space: space}, pdfgo.ColorantGroup{Space: parent} group, destination := pdfgo.ColorantGroup{Space: space}, pdfgo.ColorantGroup{Space: parent}
compositor, err := destination.PrepareGroup(&group, mode, intent, conversion) compositor, err := destination.PrepareGroup(&group, mode, intent, conversion)
if err != nil { if err != nil {
return err return err
} }
if region.Empty() {
return nil
}
components, parentComponents := space.Components(), parent.Components() components, parentComponents := space.Components(), parent.Components()
for i, pixel := range result { for y := region.Min.Y; y < region.Max.Y; y++ {
if err := ctx.Err(); err != nil {
return err
}
for i, end := y*stride+region.Min.X, y*stride+region.Max.X; i < end; i++ {
if i%pdfCompositeTileSize == 0 { if i%pdfCompositeTileSize == 0 {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return err return err
@@ -499,6 +523,7 @@ func pdfCompositeGroup(ctx context.Context, pixels, initial, result []pdfComposi
if mask != nil { if mask != nil {
alpha *= mask[i] alpha *= mask[i]
} }
pixel := &result[i]
target := pdfgo.ColorantPixel{Values: pixels[i].values[:parentComponents], Alpha: pixels[i].alpha, Shape: pixels[i].shape, Effect: pixels[i].effect} target := pdfgo.ColorantPixel{Values: pixels[i].values[:parentComponents], Alpha: pixels[i].alpha, Shape: pixels[i].shape, Effect: pixels[i].effect}
if err := compositor.Composite(&target, if err := compositor.Composite(&target,
pdfgo.ColorantPixel{Values: initial[i].values[:components], Alpha: initial[i].alpha, Shape: initial[i].shape, Effect: initial[i].effect}, pdfgo.ColorantPixel{Values: initial[i].values[:components], Alpha: initial[i].alpha, Shape: initial[i].shape, Effect: initial[i].effect},
@@ -509,6 +534,7 @@ func pdfCompositeGroup(ctx context.Context, pixels, initial, result []pdfComposi
} }
pixels[i].alpha, pixels[i].effect, pixels[i].shape = target.Alpha, target.Effect, target.Shape pixels[i].alpha, pixels[i].effect, pixels[i].shape = target.Alpha, target.Effect, target.Shape
} }
}
return nil return nil
} }
+124
View File
@@ -0,0 +1,124 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"fmt"
"math"
"github.com/xiaoqidun/pdfgo"
)
// pdfDestinationFontBackend 禁止内容度量忽略实际绘制所需的缺失字体
type pdfDestinationFontBackend struct {
FontBackend
}
// ResolveFont 沿用字体匹配,只在实际绘制缺少字体数据时中止度量
// 入参: renderer 度量渲染器, id 字体标识, exact 是否禁止无关回退
// 返回: ResolvedFont 字体资源, error 解析或缺失错误
func (b pdfDestinationFontBackend) ResolveFont(renderer *Renderer, id string, exact bool) (ResolvedFont, error) {
font, err := b.FontBackend.ResolveFont(renderer, id, exact)
if err == nil && len(font.Data) == 0 {
err = fmt.Errorf("PDF destination font %q unavailable", id)
}
return font, err
}
// destinationBounds 度量目标页内容并缓存页面内范围,不包含注解或像素取整
// 入参: page PDF目标页
// 返回: Box OFD内容范围, error 度量错误
func (p *pdfImporter) destinationBounds(page *pdfgo.Page) (Box, error) {
if err := p.ctx.Err(); err != nil {
return Box{}, err
}
if box, ok := p.destinationBoxes[page.Reference]; ok {
return box, nil
}
if index, ok := p.pageIndexes[page.Reference]; ok {
previous := p.page
p.page = index
defer func() { p.page = previous }()
}
matrix, width, height := pdfPageMatrix(page)
editor := NewEditor()
editor.SetRenderBackends(p.editor.Backends())
editor.SetFontDirs(p.editor.fontDirs...)
editor.SetFontFS(p.editor.fontFS...)
editor.fontSourcesCache = p.renderer.fontSourcesCache
editor.validatedPaths = p.editor.pathValidationCache()
if _, err := editor.AddPage(width, height); err != nil {
return Box{}, err
}
local := pdfImporter{
ctx: p.ctx, reader: p.reader, editor: editor, renderer: p.renderer,
matrix: matrix, pageBox: page.CropBox, pageWidth: width, pageHeight: height,
rasterDPI: p.rasterDPI, fontIDs: make(map[*pdfgo.Font]string),
resolveFile: p.resolveFile, resolveReference: p.resolveReference,
referenceReaders: make(map[[32]byte]*pdfgo.Reader),
referenceStreams: make(map[*pdfgo.Stream]*pdfgo.Reader),
referencePages: make(map[pdfReferencePageKey]*pdfgo.Page),
halftones: p.halftones, halftoneWarnings: make(map[*pdfgo.Halftone]bool),
}
defer local.closeReferenceReaders()
if err := local.compositePage(nil, func(visitor pdfgo.Visitor) error {
return p.reader.WalkPage(p.ctx, page, visitor)
}); err != nil {
return Box{}, fmt.Errorf("PDF destination content: %w", err)
}
if err := local.commitObjects(); err != nil {
return Box{}, err
}
reader, err := editor.resourceReader()
if err != nil {
return Box{}, err
}
defer reader.Close()
renderer := p.renderer.childRenderer(reader)
renderer.TransparentBackground = true
renderer.RenderAnnotations = false
if renderer.backends.Fonts != nil {
renderer.backends.Fonts = pdfDestinationFontBackend{renderer.backends.Fonts}
}
scene, err := renderer.CompilePage(&editor.pages[0])
if err != nil {
return Box{}, err
}
if scene == nil {
return Box{}, fmt.Errorf("PDF destination content scene unavailable")
}
geometry, err := renderer.Geometry()
if err != nil {
return Box{}, err
}
box, err := scene.BoundsContext(p.ctx, geometry)
if err != nil {
return Box{}, err
}
left, top := math.Max(0, box.X), math.Max(0, box.Y)
right, bottom := math.Min(width, box.X+box.W), math.Min(height, box.Y+box.H)
box = Box{X: left, Y: top, W: right - left, H: bottom - top}
if !finite(left) || !finite(top) || !finite(right) || !finite(bottom) || box.W <= 0 || box.H <= 0 {
return Box{}, fmt.Errorf("PDF destination content bounds empty or invalid")
}
if err := p.ctx.Err(); err != nil {
return Box{}, err
}
if p.destinationBoxes == nil {
p.destinationBoxes = make(map[pdfgo.Reference]Box)
}
p.destinationBoxes[page.Reference] = box
return box, nil
}
+92 -44
View File
@@ -36,6 +36,12 @@ type pdfImportedFont struct {
repairLimit uint16 repairLimit uint16
} }
// pdfFontRepair 保存字体封装修复范围,不混用字形度量与位置索引
type pdfFontRepair struct {
metricsLimit uint16
locations bool
}
// embeddedTextFont 匹配同名内嵌字体,仅复用唯一且覆盖全部字符的字形程序 // embeddedTextFont 匹配同名内嵌字体,仅复用唯一且覆盖全部字符的字形程序
// 入参: source 外部字体, glyphs 当前文字字形 // 入参: source 外部字体, glyphs 当前文字字形
// 返回: *pdfImportedFont 可复用字体,无可靠匹配时为空 // 返回: *pdfImportedFont 可复用字体,无可靠匹配时为空
@@ -102,10 +108,13 @@ func (p *pdfImporter) importedFont(source *pdfgo.Font) (*pdfImportedFont, error)
type1 = &parsed type1 = &parsed
result.type1Glyphs = parsed.glyphIDs() result.type1Glyphs = parsed.glyphIDs()
} }
program, limit, err := pdfFontProgram(source, type1) program, repair, err := pdfFontProgram(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 {
return nil, fmt.Errorf("PDF font %s program: %w", source.Name, errTTInvalidLocations)
}
if p.editor.backends.FontResources == nil { if p.editor.backends.FontResources == nil {
return nil, fmt.Errorf("PDF font %s resource: font resources: %w", source.Name, ErrBackendUnavailable) return nil, fmt.Errorf("PDF font %s resource: font resources: %w", source.Name, ErrBackendUnavailable)
} }
@@ -123,7 +132,10 @@ 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), limit result.checksum, result.repairLimit = sha256.Sum256(result.resource.Data), repair.metricsLimit
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)})
}
if p.fonts == nil { if p.fonts == nil {
p.fonts = make(map[*pdfgo.Font]*pdfImportedFont) p.fonts = make(map[*pdfgo.Font]*pdfImportedFont)
} }
@@ -132,23 +144,24 @@ func (p *pdfImporter) importedFont(source *pdfgo.Font) (*pdfImportedFont, error)
} }
// pdfFontProgram 为PDF子集字体补齐封装表,保留字形轮廓和编号 // pdfFontProgram 为PDF子集字体补齐封装表,保留字形轮廓和编号
// 复合字体的重复映射区段按PDF字符映射重建,不改变CID对应的字形 // 无效字符表按PDF已解码字形重建,不改变字符对应的字形编号
// 入参: source PDF字体, type1 已解析的Type1程序,nil时按需解析 // 入参: source PDF字体, type1 已解析的Type1程序,nil时按需解析
// 返回: []byte 封装后的字体数据, uint16 缺失度量前的完整字形数, error 错误信息 // 返回: []byte 封装后的字体数据, pdfFontRepair 字体修复范围, error 错误信息
func pdfFontProgram(source *pdfgo.Font, type1 *type1Program) ([]byte, uint16, error) { func pdfFontProgram(source *pdfgo.Font, type1 *type1Program) ([]byte, pdfFontRepair, error) {
var repair pdfFontRepair
program := source.Program program := source.Program
if source.ProgramType == "FontFile" { if source.ProgramType == "FontFile" {
if type1 == nil { if type1 == nil {
parsed, err := parseType1Program(program) parsed, err := parseType1Program(program)
if err != nil { if err != nil {
return nil, 0, err return nil, repair, err
} }
type1 = &parsed type1 = &parsed
} }
var err error var err error
program, err = type1.toCFF(pdfFontIdentity(source)) program, err = type1.toCFF(pdfFontIdentity(source))
if err != nil { if err != nil {
return nil, 0, err return nil, repair, err
} }
} }
bareCFF := len(program) >= 4 && program[0] == 1 && program[1] == 0 && program[2] >= 4 && program[3] >= 1 && program[3] <= 4 bareCFF := len(program) >= 4 && program[0] == 1 && program[1] == 0 && program[2] >= 4 && program[3] >= 1 && program[3] <= 4
@@ -156,21 +169,25 @@ func pdfFontProgram(source *pdfgo.Font, type1 *type1Program) ([]byte, uint16, er
var err error var err error
program, _, err = wrapCFFToOTF(program) program, _, err = wrapCFFToOTF(program)
if err != nil { if err != nil {
return nil, 0, err return nil, repair, err
} }
} }
tables, err := fontFileTables(program, 0) tables, err := fontFileTables(program, 0)
if err != nil { if err != nil {
return nil, 0, err return nil, repair, err
} }
if len(tables["head"]) < 54 || len(tables["maxp"]) < 6 || len(tables["hhea"]) < 36 || len(tables["hmtx"]) == 0 { if len(tables["head"]) < 54 || len(tables["maxp"]) < 6 || len(tables["hhea"]) < 36 || len(tables["hmtx"]) == 0 {
return nil, 0, fmt.Errorf("embedded PDF font lacks required metrics") return nil, repair, fmt.Errorf("embedded PDF font lacks required metrics")
} }
count := binary.BigEndian.Uint16(tables["maxp"][4:6]) count := binary.BigEndian.Uint16(tables["maxp"][4:6])
if count == 0 { if count == 0 {
return nil, 0, fmt.Errorf("embedded PDF font has no glyphs") return nil, repair, fmt.Errorf("embedded PDF font has no glyphs")
} }
changed := false repair.locations, err = 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 { if head := tables["head"]; len(head) == 56 && binary.BigEndian.Uint32(head[:4]) == 0x00010000 && head[54] == 0 && head[55] == 0 {
tables["head"] = head[:54] tables["head"] = head[:54]
changed = true changed = true
@@ -178,34 +195,33 @@ func pdfFontProgram(source *pdfgo.Font, type1 *type1Program) ([]byte, uint16, er
if cff := tables["CFF "]; len(cff) != 0 { if cff := tables["CFF "]; len(cff) != 0 {
sanitized, err := sanitizeCFF(cff) sanitized, err := sanitizeCFF(cff)
if err != nil { if err != nil {
return nil, 0, err return nil, repair, err
} }
normalized, err := normalizeCFFCharstringsAt(sanitized, binary.BigEndian.Uint16(tables["head"][18:20])) normalized, err := normalizeCFFCharstringsAt(sanitized, binary.BigEndian.Uint16(tables["head"][18:20]))
if err != nil { if err != nil {
return nil, 0, err return nil, repair, err
} }
if !bytes.Equal(normalized, cff) { if !bytes.Equal(normalized, cff) {
tables["CFF "] = normalized tables["CFF "] = normalized
changed = true changed = true
} }
} }
var repairedLimit uint16
metrics := int(binary.BigEndian.Uint16(tables["hhea"][34:36])) metrics := int(binary.BigEndian.Uint16(tables["hhea"][34:36]))
if metrics == 0 || metrics > int(count) { if metrics == 0 || metrics > int(count) {
return nil, 0, fmt.Errorf("invalid embedded PDF font metric count") return nil, repair, fmt.Errorf("invalid embedded PDF font metric count")
} }
metricLength := 4*metrics + 2*(int(count)-metrics) metricLength := 4*metrics + 2*(int(count)-metrics)
if len(tables["hmtx"]) < metricLength { if len(tables["hmtx"]) < metricLength {
if len(tables["hmtx"]) < 4*metrics || len(tables["hmtx"])%2 != 0 { if len(tables["hmtx"]) < 4*metrics || len(tables["hmtx"])%2 != 0 {
return nil, 0, fmt.Errorf("incomplete embedded PDF font metrics: have %d, need %d", len(tables["hmtx"]), metricLength) 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 limit := metrics + (len(tables["hmtx"])-4*metrics)/2
missing, err := pdfMissingLeftBearings(tables, limit, int(count)) missing, err := pdfMissingLeftBearings(tables, limit, int(count))
if err != nil { if err != nil {
return nil, 0, err return nil, repair, err
} }
tables["hmtx"] = append(bytes.Clone(tables["hmtx"]), missing...) tables["hmtx"] = append(bytes.Clone(tables["hmtx"]), missing...)
repairedLimit = uint16(limit) repair.metricsLimit = uint16(limit)
changed = true changed = true
} }
if len(tables["hmtx"]) > metricLength { if len(tables["hmtx"]) > metricLength {
@@ -226,19 +242,28 @@ func pdfFontProgram(source *pdfgo.Font, type1 *type1Program) ([]byte, uint16, er
changed = true changed = true
} }
} }
if len(tables["cmap"]) == 0 || source.Subtype == "Type0" && (invalidCmap || pdfCmapOverlaps(tables["cmap"])) { if len(tables["cmap"]) == 0 || source.Subtype == "Type0" && invalidCmap || (source.Subtype == "Type0" || source.Subtype == "TrueType") && pdfCmapNeedsRebuild(tables["cmap"]) {
mapping := map[rune]uint16{} mapping := map[rune]uint16{}
for _, code := range slices.Sorted(maps.Keys(source.Unicode)) { var codes []string
if source.Subtype == "TrueType" {
codes = make([]string, 256)
for code := range codes {
codes[code] = string([]byte{byte(code)})
}
} else {
codes = slices.Sorted(maps.Keys(source.Unicode))
}
for _, code := range codes {
glyphs, err := source.Decode([]byte(code)) glyphs, err := source.Decode([]byte(code))
if err != nil { if err != nil {
return nil, 0, err return nil, repair, err
} }
if len(glyphs) != 1 || !glyphs[0].HasID { if len(glyphs) != 1 || !glyphs[0].HasID {
return nil, 0, &pdfgo.UnsupportedError{Feature: "font without explicit glyph mapping"} return nil, repair, &pdfgo.UnsupportedError{Feature: "font without explicit glyph mapping"}
} }
glyph := glyphs[0] glyph := glyphs[0]
if glyph.ID >= count { if glyph.ID >= count {
return nil, 0, fmt.Errorf("PDF font glyph %d exceeds glyph count %d", glyph.ID, count) return nil, repair, fmt.Errorf("PDF font glyph %d exceeds glyph count %d", glyph.ID, count)
} }
if utf8.RuneCountInString(glyph.Text) == 1 { if utf8.RuneCountInString(glyph.Text) == 1 {
char, _ := utf8.DecodeRuneInString(glyph.Text) char, _ := utf8.DecodeRuneInString(glyph.Text)
@@ -255,7 +280,7 @@ func pdfFontProgram(source *pdfgo.Font, type1 *type1Program) ([]byte, uint16, er
fontName := pdfFontIdentity(source) fontName := pdfFontIdentity(source)
name := utf16.Encode([]rune(fontName)) name := utf16.Encode([]rune(fontName))
if len(name) > 32767 { if len(name) > 32767 {
return nil, 0, fmt.Errorf("PDF font name exceeds name table capacity") return nil, repair, fmt.Errorf("PDF font name exceeds name table capacity")
} }
data := make([]byte, 42+len(name)*2) data := make([]byte, 42+len(name)*2)
binary.BigEndian.PutUint16(data[2:], 3) binary.BigEndian.PutUint16(data[2:], 3)
@@ -290,10 +315,10 @@ func pdfFontProgram(source *pdfgo.Font, type1 *type1Program) ([]byte, uint16, er
} }
} }
if !changed && !pdfSFNTMissingPadding(program) { if !changed && !pdfSFNTMissingPadding(program) {
return program, repairedLimit, nil return program, repair, nil
} }
result, err := serializeOTF(tables) result, err := serializeOTF(tables)
return result, repairedLimit, err return result, repair, err
} }
// pdfFontIdentity 返回字体名称或由原始程序生成的稳定封装标识 // pdfFontIdentity 返回字体名称或由原始程序生成的稳定封装标识
@@ -307,35 +332,58 @@ func pdfFontIdentity(source *pdfgo.Font) string {
return fmt.Sprintf("PDF-%x", checksum[:28]) return fmt.Sprintf("PDF-%x", checksum[:28])
} }
// pdfCmapOverlaps 检查格式4字符映射是否包含交叠区段 // pdfCmapNeedsRebuild 检查字体封装字符表是否存在结构错误或交叠区段
// 入参: data cmap表数据 // 入参: data cmap表数据
// 返回: bool 是否存在交叠 // 返回: bool 是否需要按PDF显式字形编号重建
func pdfCmapOverlaps(data []byte) bool { func pdfCmapNeedsRebuild(data []byte) bool {
if len(data) < 4 { if len(data) < 4 || binary.BigEndian.Uint16(data) != 0 {
return false return true
} }
count := int(binary.BigEndian.Uint16(data[2:])) count := int(binary.BigEndian.Uint16(data[2:]))
if count > (len(data)-4)/8 { if count == 0 || count > (len(data)-4)/8 {
return false return true
} }
for index := range count { for index := range count {
offset := uint64(binary.BigEndian.Uint32(data[8+8*index:])) offset := uint64(binary.BigEndian.Uint32(data[8+8*index:]))
if offset > uint64(len(data)) || uint64(len(data))-offset < 16 { if offset < uint64(4+8*count) || offset > uint64(len(data)) || uint64(len(data))-offset < 2 {
continue return true
} }
sub := data[int(offset):] 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 { if binary.BigEndian.Uint16(sub) != 4 {
continue continue
} }
length := int(binary.BigEndian.Uint16(sub[2:])) if len(sub) < 16 {
segments := int(binary.BigEndian.Uint16(sub[6:])) / 2 return true
if length > len(sub) || 16+8*segments > length {
continue
} }
for i := 1; i < segments; i++ { length := int(binary.BigEndian.Uint16(sub[2:]))
previous := binary.BigEndian.Uint16(sub[14+2*(i-1):]) segmentBytes := int(binary.BigEndian.Uint16(sub[6:]))
start := binary.BigEndian.Uint16(sub[16+2*segments+2*i:]) segments := segmentBytes / 2
if start <= previous { 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 true
} }
} }
+46 -71
View File
@@ -31,6 +31,11 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict
if err != nil { if err != nil {
return err return err
} }
markups, err := p.reader.ReadThreeDMarkups(ctx, annotations)
if err != nil {
return err
}
var markupState pdfThreeDMarkupState
parents := make(map[pdfgo.Reference]int) parents := make(map[pdfgo.Reference]int)
for i, annotation := range annotations { for i, annotation := range annotations {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
@@ -77,6 +82,12 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict
if attached[i] { if attached[i] {
continue continue
} }
if len(markups) != 0 {
annotation, err = p.threeDMarkupAnnotation(ctx, annotation, markups[i], &markupState, strict)
if err != nil {
return err
}
}
dict := annotation.Dictionary dict := annotation.Dictionary
optional, err := p.reader.Resolve(dict["OC"]) optional, err := p.reader.Resolve(dict["OC"])
if err != nil { if err != nil {
@@ -223,6 +234,9 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict
p.report.Warnings = append(p.report.Warnings, pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF pending redaction appearance imported; content removal not applied; overlay data not transferred"}) p.report.Warnings = append(p.report.Warnings, pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF pending redaction appearance imported; content removal not applied; overlay data not transferred"})
continue continue
} }
if annotation.Subtype == "Projection" && strict {
return &pdfgo.UnsupportedError{Feature: "projection annotation runtime conversion"}
}
if annotation.Subtype != "Link" && pdfAnnotationType(annotation.Subtype) != "" { if annotation.Subtype != "Link" && pdfAnnotationType(annotation.Subtype) != "" {
if err := p.appearanceAnnotation(ctx, page, annotation); err != nil { if err := p.appearanceAnnotation(ctx, page, annotation); err != nil {
return err return err
@@ -232,12 +246,15 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict
if annotation.Subtype != "Link" { if annotation.Subtype != "Link" {
return &pdfgo.UnsupportedError{Feature: "annotation " + string(annotation.Subtype)} return &pdfgo.UnsupportedError{Feature: "annotation " + string(annotation.Subtype)}
} }
additional, err := p.reader.Resolve(dict["AA"]) flags, err := p.reader.ReadAnnotationFlags(annotation)
if err != nil { if err != nil {
return err return err
} }
if additional != nil { if flags&64 != 0 {
return &pdfgo.UnsupportedError{Feature: "link annotation field \"AA\""} if err := p.appearanceAnnotation(ctx, page, annotation); err != nil {
return err
}
continue
} }
primary, err := p.reader.Resolve(dict["A"]) primary, err := p.reader.Resolve(dict["A"])
if err != nil { if err != nil {
@@ -281,9 +298,6 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict
if err := p.appearanceAnnotation(ctx, page, annotation, actions...); err != nil { if err := p.appearanceAnnotation(ctx, page, annotation, actions...); err != nil {
return err return err
} }
if len(actions) != 0 {
p.report.Links++
}
} }
return nil return nil
} }
@@ -446,73 +460,34 @@ 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, _, _ := pdfPageMatrix(targetPage)
dest := &Dest{Type: string(destination.Mode), PageID: p.pageIDs[destination.Page]} if destination.Mode == "FitBH" || destination.Mode == "FitBV" {
if destination.Mode == "XYZ" {
values := [3]float64{}
retained := [3]bool{}
for n, v := range destination.Parameters {
switch v := v.(type) {
case pdfgo.Integer:
values[n] = float64(v)
case pdfgo.Real:
values[n] = float64(v)
case nil:
retained[n] = true
}
}
point := matrix.Apply(pdfgo.Point{X: values[0], Y: values[1]})
dest.Left, dest.Top, dest.Zoom = point.X, point.Y, values[2]
dest.OmitLeft, dest.OmitTop = retained[0], retained[1]
if targetPage.Rotation == 90 || targetPage.Rotation == 270 {
dest.OmitLeft, dest.OmitTop = retained[1], retained[0]
}
dest.OmitZoom = retained[2] || values[2] == 0
} else if destination.Mode == "FitH" || destination.Mode == "FitV" {
var coordinate float64
switch value := destination.Parameters[0].(type) {
case pdfgo.Integer:
coordinate = float64(value)
case pdfgo.Real:
coordinate = float64(value)
}
point := pdfgo.Point{X: coordinate}
if destination.Mode == "FitH" {
point = pdfgo.Point{Y: coordinate}
}
point = matrix.Apply(point)
if targetPage.Rotation == 90 || targetPage.Rotation == 270 {
if dest.Type == "FitH" {
dest.Type = "FitV"
} else {
dest.Type = "FitH"
}
}
if dest.Type == "FitH" {
dest.Top, dest.OmitTop = point.Y, destination.Parameters[0] == nil
} else {
dest.Left, dest.OmitLeft = point.X, destination.Parameters[0] == nil
}
} else if destination.Mode == "FitR" {
values := [4]float64{}
for n, v := range destination.Parameters {
switch v := v.(type) {
case pdfgo.Integer:
values[n] = float64(v)
case pdfgo.Real:
values[n] = float64(v)
default:
return nil, fmt.Errorf("invalid PDF FitR destination coordinate")
}
}
bounds := pdfBounds([]pdfgo.Point{
matrix.Apply(pdfgo.Point{X: values[0], Y: values[1]}),
matrix.Apply(pdfgo.Point{X: values[2], Y: values[3]}),
})
dest.Left, dest.Top = bounds.X, bounds.Y
dest.Right, dest.Bottom = bounds.X+bounds.W, bounds.Y+bounds.H
} else if destination.Mode != "Fit" {
return nil, &pdfgo.UnsupportedError{Feature: "destination mode " + string(destination.Mode)} return nil, &pdfgo.UnsupportedError{Feature: "destination mode " + string(destination.Mode)}
} }
target, err := destination.Transform(matrix)
if err != nil {
return nil, err
}
if destination.Mode == "FitB" {
box, err := p.destinationBounds(targetPage)
if err != nil {
if canceled := p.ctx.Err(); canceled != nil {
return nil, canceled
}
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return nil, err
}
if strict {
return nil, err
}
p.report.Warnings = append(p.report.Warnings, pdfgo.Diagnostic{Page: p.page + 1, Message: fmt.Sprintf("PDF FitB destination bounds unavailable: %v; link omitted", err)})
return nil, nil
}
target.Mode = "FitR"
target.Left, target.Top = box.X, box.Y
target.Right, target.Bottom = box.X+box.W, box.Y+box.H
target.KeepLeft, target.KeepTop = false, false
}
dest := &Dest{Type: string(target.Mode), PageID: p.pageIDs[destination.Page], Left: target.Left, Top: target.Top, Right: target.Right, Bottom: target.Bottom, Zoom: target.Zoom, OmitLeft: target.KeepLeft, OmitTop: target.KeepTop, OmitZoom: target.KeepZoom}
action.Goto = &Goto{Dest: dest} action.Goto = &Goto{Dest: dest}
*currentPage = p.pageIndexes[destination.Page] *currentPage = p.pageIndexes[destination.Page]
} }
+1 -1
View File
@@ -117,7 +117,7 @@ func (f canvasFontMetrics) GlyphBounds(glyph uint16, size float64) (Box, error)
return value, nil return value, nil
} }
// GlyphWarning 返回字形指令栈下溢后保留设计轮廓的提示,原始字体不变 // GlyphWarning 返回无效字形指令后保留设计轮廓的提示,原始字体不变
// 入参: glyph 字形编号 // 入参: glyph 字形编号
// 返回: error 恢复原因,无恢复时为空 // 返回: error 恢复原因,无恢复时为空
func (f canvasFontMetrics) GlyphWarning(glyph uint16) error { func (f canvasFontMetrics) GlyphWarning(glyph uint16) error {
+22 -5
View File
@@ -58,6 +58,7 @@ type U3DNode struct {
Parents []U3DParent Parents []U3DParent
Visibility uint32 Visibility uint32
Shaders [][]string Shaders [][]string
LineShaders [][]string
View *U3DView View *U3DView
} }
@@ -110,7 +111,7 @@ type u3dDecoder struct {
palettes map[uint32]map[string]bool palettes map[uint32]map[string]bool
} }
// DecodeU3D 按ECMA-363解码静态基础网格、组、材质、着色器和光源 // DecodeU3D 按ECMA-363解码静态基础网格、线集、组、材质、着色器和光源
// 暂不支持渐进网格、纹理和动画,遇到未支持内容返回错误而非部分场景 // 暂不支持渐进网格、纹理和动画,遇到未支持内容返回错误而非部分场景
// 入参: ctx 取消上下文, data U3D数据, options 解码限制 // 入参: ctx 取消上下文, data U3D数据, options 解码限制
// 返回: *U3DModel 独立场景数据, error 格式、能力、预算或取消错误 // 返回: *U3DModel 独立场景数据, error 格式、能力、预算或取消错误
@@ -174,7 +175,7 @@ func DecodeU3D(ctx context.Context, data []byte, options U3DOptions) (*U3DModel,
if err != nil { if err != nil {
return nil, fmt.Errorf("U3D block at %d: %w", offset, err) return nil, fmt.Errorf("U3D block at %d: %w", offset, err)
} }
continuation := block.kind == 0xffffff15 || block.kind == 0xffffff3b || block.kind == 0xffffff3c continuation := block.kind == 0xffffff15 || block.kind == 0xffffff3b || block.kind == 0xffffff3c || block.kind == 0xffffff3f
if uint64(offset) < declarationEnd && (uint64(end) > declarationEnd || continuation) || uint64(offset) >= declarationEnd && !continuation { if uint64(offset) < declarationEnd && (uint64(end) > declarationEnd || continuation) || uint64(offset) >= declarationEnd && !continuation {
return nil, fmt.Errorf("invalid U3D declaration boundary at %d", offset) return nil, fmt.Errorf("invalid U3D declaration boundary at %d", offset)
} }
@@ -381,6 +382,16 @@ func (d *u3dDecoder) block(block u3dBlock, owner string, chain int, index uint32
return name, fmt.Errorf("invalid U3D mesh continuation location") return name, fmt.Errorf("invalid U3D mesh continuation location")
} }
err = d.baseMesh(&r, name) err = d.baseMesh(&r, name)
case 0xffffff37:
if chain >= 0 && (chain != 1 || index != 0) {
return name, fmt.Errorf("invalid U3D line set chain")
}
err = d.lineDeclaration(&r, name)
case 0xffffff3f:
if chain >= 0 {
return name, fmt.Errorf("invalid U3D line continuation location")
}
err = d.lineContinuation(&r, name)
case 0xffffff45: case 0xffffff45:
if chain != 0 || index == 0 || owner != name || r.u32() != index { if chain != 0 || index == 0 || owner != name || r.u32() != index {
return name, fmt.Errorf("invalid U3D shading modifier chain") return name, fmt.Errorf("invalid U3D shading modifier chain")
@@ -450,7 +461,7 @@ func (d *u3dDecoder) node(r *u3dValues, kind uint32, name string) error {
return r.err return r.err
} }
// shading 读取网格着色列表,保留列表内各着色器的先后次序 // shading 分别读取网格和线集着色列表,保留列表内各着色器的先后次序
// 入参: r 字段读取器, name 目标节点名 // 入参: r 字段读取器, name 目标节点名
// 返回: error 结构、能力或预算错误 // 返回: error 结构、能力或预算错误
func (d *u3dDecoder) shading(r *u3dValues, name string) error { func (d *u3dDecoder) shading(r *u3dValues, name string) error {
@@ -458,8 +469,9 @@ func (d *u3dDecoder) shading(r *u3dValues, name string) error {
if !exists || d.model.Nodes[index].Kind != "Model" { if !exists || d.model.Nodes[index].Kind != "Model" {
return fmt.Errorf("missing U3D shading target") return fmt.Errorf("missing U3D shading target")
} }
if r.u32() != 1 { flags := r.u32()
return fmt.Errorf("unsupported U3D non-mesh shading modifier") if flags & ^uint32(3) != 0 {
return fmt.Errorf("unsupported U3D shading modifier target")
} }
count := r.u32() count := r.u32()
if uint64(count) > uint64(len(r.data)-r.pos)/4 { if uint64(count) > uint64(len(r.data)-r.pos)/4 {
@@ -487,7 +499,12 @@ func (d *u3dDecoder) shading(r *u3dValues, name string) error {
lists[index][j] = r.text() lists[index][j] = r.text()
} }
} }
if flags&1 != 0 {
d.model.Nodes[index].Shaders = lists d.model.Nodes[index].Shaders = lists
}
if flags&2 != 0 {
d.model.Nodes[index].LineShaders = lists
}
return r.err return r.err
} }
+47 -3
View File
@@ -36,6 +36,7 @@ type u3dBits struct {
pos uint64 pos uint64
low, high, code uint32 low, high, code uint32
err error err error
reserve func(uint64, uint64) error
} }
// u3dHistogram 保存一个动态压缩上下文的频次及前缀和 // u3dHistogram 保存一个动态压缩上下文的频次及前缀和
@@ -224,7 +225,23 @@ func (r *u3dBits) index(count uint32) uint32 {
// 入参: h 频次上下文 // 入参: h 频次上下文
// 返回: uint32 原始整数 // 返回: uint32 原始整数
func (r *u3dBits) dynamic(h *u3dHistogram) uint32 { func (r *u3dBits) dynamic(h *u3dHistogram) uint32 {
return r.dynamicValue(h, false)
}
// dynamicValue 按字段位宽处理动态上下文的逃逸字面量,分配前检查可选预算
// 入参: h 频次上下文, small 是否为8位字段
// 返回: uint32 解码值
func (r *u3dBits) dynamicValue(h *u3dHistogram, small bool) uint32 {
if r.err != nil {
return 0
}
if h.total == 0 { if h.total == 0 {
if r.reserve != nil {
if err := r.reserve(1, 32); err != nil {
r.err = err
return 0
}
}
h.counts = make([]uint32, 1) h.counts = make([]uint32, 1)
h.tree = make([]uint32, 2) h.tree = make([]uint32, 2)
h.add(0) h.add(0)
@@ -235,17 +252,44 @@ func (r *u3dBits) dynamic(h *u3dHistogram) uint32 {
return 0 return 0
} }
r.advance(start, h.counts[symbol], h.total) r.advance(start, h.counts[symbol], h.total)
h.add(symbol) r.histogramAdd(h, symbol)
if r.err != nil {
return 0
}
if symbol != 0 { if symbol != 0 {
if small && symbol > 256 {
r.err = fmt.Errorf("invalid U3D compressed byte")
}
return symbol - 1 return symbol - 1
} }
value := r.u32() var value uint32
if small {
value = uint32(r.byteValue())
} else {
value = r.u32()
}
if value < 0xffff && r.err == nil { if value < 0xffff && r.err == nil {
h.add(value + 1) r.histogramAdd(h, value+1)
} }
return value return value
} }
// histogramAdd 在频次数组扩展前计入解码预算
// 入参: h 频次上下文, symbol 待更新符号
func (r *u3dBits) histogramAdd(h *u3dHistogram, symbol uint32) {
if r.err != nil {
return
}
if r.reserve != nil && symbol <= 0xffff && int(symbol) >= len(h.counts) {
length := min(65536, max(int(symbol)+1, len(h.counts)*2))
if err := r.reserve(uint64(length-len(h.counts)), 32); err != nil {
r.err = err
return
}
}
h.add(symbol)
}
// find 以树状前缀和定位符号,避免按符号数量线性扫描 // find 以树状前缀和定位符号,避免按符号数量线性扫描
// 入参: position 累计频次位置 // 入参: position 累计频次位置
// 返回: uint32 符号, uint32 符号前缀频次 // 返回: uint32 符号, uint32 符号前缀频次
+40 -1
View File
@@ -230,12 +230,17 @@ func (r *u3dRender) shade(position, normal u3dVector, style u3dRenderStyle, diff
if style.shader.Attributes&1 == 0 { if style.shader.Attributes&1 == 0 {
return color, nil return color, nil
} }
copy(color[:3], style.emissive[:])
if r.mode == U3DRenderShadedIllustration {
for c := range 3 {
color[c] += max(0, style.diffuse[c]) / 4
}
}
if r.lighting != nil { if r.lighting != nil {
for c := range 3 { for c := range 3 {
style.diffuse[c] += r.lighting.DiffuseOffset[c] style.diffuse[c] += r.lighting.DiffuseOffset[c]
} }
} }
copy(color[:3], style.emissive[:])
eye := (u3dVector{r.camera.ToWorld[12], r.camera.ToWorld[13], r.camera.ToWorld[14]}).sub(position).unit() eye := (u3dVector{r.camera.ToWorld[12], r.camera.ToWorld[13], r.camera.ToWorld[14]}).sub(position).unit()
if !r.camera.Perspective { if !r.camera.Perspective {
eye = (u3dVector{-r.camera.ToWorld[8], -r.camera.ToWorld[9], -r.camera.ToWorld[10]}).unit() eye = (u3dVector{-r.camera.ToWorld[8], -r.camera.ToWorld[9], -r.camera.ToWorld[10]}).unit()
@@ -300,6 +305,40 @@ func (r *u3dRender) shade(position, normal u3dVector, style u3dRenderStyle, diff
return color, nil return color, nil
} }
// u3dFragmentColor 对点、线、面的插值颜色应用透明度测试和相机空间雾效
// 入参: color 待更新的未预乘颜色, position 相机位置, shader 着色器, pass 绘制轮次
// 返回: bool 是否绘制, error 非有限颜色错误
func u3dFragmentColor(color *[4]float64, position u3dVector, shader *U3DShader, pass *U3DViewPass) (bool, error) {
for _, value := range color {
if !finite(value) {
return false, fmt.Errorf("invalid U3D fragment color")
}
}
color[3] = min(1, max(0, color[3]))
if shader.Attributes&2 != 0 && !u3dAlphaTest(shader.AlphaFunction, color[3], float64(shader.AlphaReference)) {
return false, nil
}
if pass.Attributes&1 != 0 {
distance := math.Hypot(math.Hypot(position[0], position[1]), position[2])
factor := (float64(pass.FogFar) - distance) / (float64(pass.FogFar) - float64(pass.FogNear))
if pass.FogMode != 0 {
amount := distance * math.Log(100) / float64(pass.FogFar)
if pass.FogMode == 2 {
amount *= amount
}
factor = math.Exp(-amount)
}
factor = min(1, max(0, factor))
for c := range 3 {
color[c] = color[c]*factor + float64(pass.FogColor[c])*(1-factor)
}
}
for c := range 3 {
color[c] = min(1, max(0, color[c]))
}
return true, nil
}
// u3dAlphaTest 按ECMA-363枚举比较透明度,不套用图形接口的枚举顺序 // u3dAlphaTest 按ECMA-363枚举比较透明度,不套用图形接口的枚举顺序
// 入参: function 比较函数, value 透明度, reference 参考值 // 入参: function 比较函数, value 透明度, reference 参考值
// 返回: bool 是否通过 // 返回: bool 是否通过
+191 -5
View File
@@ -19,6 +19,168 @@ import (
"math" "math"
) )
// drawLines 绘制作者线集,端点独立着色且不参与面的背向剔除
// 入参: instance 场景实例, node 模型节点, mesh 属性数组, positions 世界位置, normals 世界法线, pass 绘制轮次, passIndex 轮次索引
// 返回: error 几何、资源或取消错误
func (r *u3dRender) drawLines(instance u3dInstance, node *U3DNode, mesh *U3DMesh, positions, normals []u3dVector, pass U3DViewPass, passIndex int) error {
if r.depthOnly {
return nil
}
shaded := r.mode.shaded()
for index, line := range mesh.Lines {
if index%128 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
if uint64(line.Shading) >= uint64(len(mesh.Shadings)) {
return fmt.Errorf("invalid U3D render line shading index")
}
shading := mesh.Shadings[line.Shading]
if shading.Attributes > 3 || len(shading.TextureDimensions) > 8 {
return fmt.Errorf("invalid U3D render line shading")
}
var world [2]u3dVector
var vertices [2]u3dVertex
for i, corner := range line.Corners {
if uint64(corner.Position) >= uint64(len(positions)) || shaded && (!mesh.ExcludeNormals && uint64(corner.Normal) >= uint64(len(normals)) || shading.Attributes&1 != 0 && uint64(corner.Diffuse) >= uint64(len(mesh.Diffuse)) || shading.Attributes&2 != 0 && uint64(corner.Specular) >= uint64(len(mesh.Specular))) {
return fmt.Errorf("invalid U3D render line corner index")
}
world[i] = positions[corner.Position]
vertices[i].point = r.toCamera.point(world[i])
for _, value := range vertices[i].point {
if !finite(value) {
return fmt.Errorf("invalid U3D line camera position")
}
}
}
shaderNames := []string{""}
if uint64(line.Shading) < uint64(len(node.LineShaders)) {
shaderNames = node.LineShaders[line.Shading]
}
for _, name := range shaderNames {
if err := r.ctx.Err(); err != nil {
return err
}
style := u3dRenderStyle{shader: U3DShader{Blend: 0x606, RenderPass: math.MaxUint32}}
var err error
if shaded {
style, err = r.style(name)
if err != nil {
return err
}
} else if index, ok := r.shaders[name]; ok {
style.shader.RenderPass = r.model.Shaders[index].RenderPass
}
if style.shader.RenderPass&(1<<passIndex) == 0 {
continue
}
if instance.opacity >= 0 || r.opacity >= 0 {
style.shader.Blend = 0x606
}
for i, corner := range line.Corners {
vertices[i].color = [4]float64{r.auxiliaryColor[0], r.auxiliaryColor[1], r.auxiliaryColor[2], 1}
if !shaded {
continue
}
var normal u3dVector
if !mesh.ExcludeNormals {
normal = normals[corner.Normal]
}
var diffuse, specular *[4]float32
if shading.Attributes&1 != 0 {
diffuse = &mesh.Diffuse[corner.Diffuse]
}
if shading.Attributes&2 != 0 {
specular = &mesh.Specular[corner.Specular]
}
vertices[i].color, err = r.shade(world[i], normal, style, diffuse, specular)
if err != nil {
return err
}
if instance.opacity >= 0 {
vertices[i].color[3] = instance.opacity
}
if r.opacity >= 0 {
vertices[i].color[3] *= r.opacity
}
}
if r.mode == U3DRenderVertices || r.mode == U3DRenderShadedVertices {
for _, vertex := range vertices {
fresh, err := r.uniquePrimitive(vertex, vertex, style.shader)
if err != nil {
return err
}
if fresh {
if err := r.modePoint(vertex, style.shader, pass); err != nil {
return err
}
}
}
} else if err := r.modeLine(vertices[0], vertices[1], u3dVector{}, u3dVector{}, style.shader, pass); err != nil {
return err
}
for index, section := range r.sections {
if index%256 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
if section.intersection != nil {
if err := r.lineIntersection(vertices, index); err != nil {
return err
}
}
}
}
}
return nil
}
// lineIntersection 绘制作者线段与剖切面的交点,不绘制共面线段
// 入参: line 相机空间线段, index 剖切面索引
// 返回: error 数值、资源或取消错误
func (r *u3dRender) lineIntersection(line [2]u3dVertex, index int) error {
section := r.sections[index]
a, b := u3dPlaneDistance(section.plane, line[0].point), u3dPlaneDistance(section.plane, line[1].point)
if !finite(a) || !finite(b) {
return fmt.Errorf("invalid U3D line intersection distance")
}
if a == 0 && b == 0 || a < 0 && b < 0 || a > 0 && b > 0 {
return nil
}
point := u3dInterpolate(line[0], line[1], u3dClipFraction(a, b))
for i, other := range r.sections {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
if i != index {
distance := u3dPlaneDistance(other.plane, point.point)
if !finite(distance) {
return fmt.Errorf("invalid U3D line intersection clip")
}
if distance < 0 {
return nil
}
}
}
for i, plane := range r.frustumPlanes() {
if i == 1 && r.camera.Far == 0 && !r.camera.ClipFar {
continue
}
distance := u3dPlaneDistance(plane, point.point)
if !finite(distance) {
return fmt.Errorf("invalid U3D projected intersection")
}
if distance < 0 {
return nil
}
}
return r.rasterSectionLine(point, point, *section.intersection, u3dVector{}, u3dVector{})
}
// frustumPlanes 返回保留正侧的六个相机视锥面 // frustumPlanes 返回保留正侧的六个相机视锥面
// 返回: [6][4]float64 近、远、左右及上下平面 // 返回: [6][4]float64 近、远、左右及上下平面
func (r *u3dRender) frustumPlanes() [6][4]float64 { func (r *u3dRender) frustumPlanes() [6][4]float64 {
@@ -161,6 +323,15 @@ func u3dClipLine(a, b *u3dVertex, plane [4]float64) (bool, error) {
// 入参: a 已裁剪起点, b 已裁剪终点, rgb 交线颜色, normal 原面法线, origin 原面上一点 // 入参: a 已裁剪起点, b 已裁剪终点, rgb 交线颜色, normal 原面法线, origin 原面上一点
// 返回: error 数值、资源或取消错误 // 返回: error 数值、资源或取消错误
func (r *u3dRender) rasterSectionLine(a, b u3dVertex, rgb [3]float64, normal, origin u3dVector) error { func (r *u3dRender) rasterSectionLine(a, b u3dVertex, rgb [3]float64, normal, origin u3dVector) error {
a.color = [4]float64{rgb[0], rgb[1], rgb[2], 1}
b.color = a.color
return r.rasterLine(a, b, normal, origin, U3DShader{Blend: 0x606}, U3DViewPass{})
}
// rasterLine 绘制单像素线段,颜色与深度按透视插值,面深度偏移防止自身遮挡
// 入参: a 已裁剪起点, b 已裁剪终点, normal 面法线, origin 面上点, shader 着色器, pass 绘制轮次
// 返回: error 数值、资源或取消错误
func (r *u3dRender) rasterLine(a, b u3dVertex, normal, origin u3dVector, shader U3DShader, pass U3DViewPass) error {
cx := r.viewport[0] + r.viewport[2]/2 + r.camera.ShiftX cx := r.viewport[0] + r.viewport[2]/2 + r.camera.ShiftX
cy := r.viewport[1] + r.viewport[3]/2 + r.camera.ShiftY cy := r.viewport[1] + r.viewport[3]/2 + r.camera.ShiftY
planes := [4][4]float64{{r.camera.ScaleX, 0, 0, cx}, {-r.camera.ScaleX, 0, 0, float64(r.output.Rect.Dx()) - cx}, {0, -r.camera.ScaleY, 0, cy}, {0, r.camera.ScaleY, 0, float64(r.output.Rect.Dy()) - cy}} planes := [4][4]float64{{r.camera.ScaleX, 0, 0, cx}, {-r.camera.ScaleX, 0, 0, float64(r.output.Rect.Dx()) - cx}, {0, -r.camera.ScaleY, 0, cy}, {0, r.camera.ScaleY, 0, float64(r.output.Rect.Dy()) - cy}}
@@ -193,8 +364,8 @@ func (r *u3dRender) rasterSectionLine(a, b u3dVertex, rgb [3]float64, normal, or
return fmt.Errorf("invalid U3D line span") return fmt.Errorf("invalid U3D line span")
} }
count := max(1, int(steps)) count := max(1, int(steps))
color := [4]float64{rgb[0], rgb[1], rgb[2], 1}
r.order++ r.order++
previousPixel := -1
for i := 0; i <= count; i++ { for i := 0; i <= count; i++ {
if i%1024 == 0 { if i%1024 == 0 {
if err := r.ctx.Err(); err != nil { if err := r.ctx.Err(); err != nil {
@@ -202,13 +373,21 @@ func (r *u3dRender) rasterSectionLine(a, b u3dVertex, rgb [3]float64, normal, or
} }
} }
t := float64(i) / float64(count) t := float64(i) / float64(count)
px, py := math.Floor(x[0]*(1-t)+x[1]*t), math.Floor(y[0]*(1-t)+y[1]*t) px := math.Floor(x[0] + float64(i)*(x[1]-x[0])/float64(count))
py := math.Floor(y[0] + float64(i)*(y[1]-y[0])/float64(count))
if px < 0 || py < 0 || px >= float64(r.output.Rect.Dx()) || py >= float64(r.output.Rect.Dy()) { if px < 0 || py < 0 || px >= float64(r.output.Rect.Dx()) || py >= float64(r.output.Rect.Dy()) {
continue continue
} }
depth := a.point[2]*(1-t) + b.point[2]*t pixel := int(py)*r.output.Rect.Dx() + int(px)
if pixel == previousPixel {
continue
}
previousPixel = pixel
depth := a.point[2] + (b.point[2]-a.point[2])*t
weight := t
if r.camera.Perspective { if r.camera.Perspective {
depth = 1 / (q[0]*(1-t) + q[1]*t) depth = 1 / (q[0]*(1-t) + q[1]*t)
weight = t * q[1] * depth
} }
ray := u3dVector{(px + .5 - cx) / r.camera.ScaleX, (cy - py - .5) / r.camera.ScaleY, 1} ray := u3dVector{(px + .5 - cx) / r.camera.ScaleX, (cy - py - .5) / r.camera.ScaleY, 1}
denominator := normal[2] denominator := normal[2]
@@ -220,9 +399,16 @@ func (r *u3dRender) rasterSectionLine(a, b u3dVertex, rgb [3]float64, normal, or
depth = sampled depth = sampled
} }
depth -= 8 * math.Abs(depth-math.Nextafter(depth, math.Inf(-1))) depth -= 8 * math.Abs(depth-math.Nextafter(depth, math.Inf(-1)))
pixel := int(py)*r.output.Rect.Dx() + int(px)
if depth <= r.frame[pixel].depth { if depth <= r.frame[pixel].depth {
if err := r.fragment(pixel, depth, color, 0x606); err != nil { vertex := u3dInterpolate(a, b, weight)
visible, err := u3dFragmentColor(&vertex.color, vertex.point, &shader, &pass)
if err != nil {
return err
}
if !visible {
continue
}
if err := r.fragment(pixel, depth, vertex.color, shader.Blend); err != nil {
return err return err
} }
} }
+433
View File
@@ -0,0 +1,433 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"fmt"
"io"
)
const (
u3dLinePositionSign = iota
u3dLinePositionX
u3dLinePositionY
u3dLinePositionZ
u3dLineNormalCount
u3dLineNormalSign
u3dLineNormalX
u3dLineNormalY
u3dLineNormalZ
u3dLineCount
u3dLineShading
u3dLineNormalIndex
u3dLineDiffuseDuplicate
u3dLineSpecularDuplicate
u3dLineTextureDuplicate
u3dLineDiffuseSign
u3dLineSpecularSign
u3dLineTextureSign
u3dLineColorR
u3dLineColorG
u3dLineColorB
u3dLineColorA
u3dLineTextureU
u3dLineTextureV
u3dLineTextureS
u3dLineTextureT
u3dLineContextCount
)
// u3dLineState 按顶点累计线段端点属性,预测开销不随顶点度数增长
type u3dLineState struct {
offsets [11]int
stride int
stats []float64
}
// u3dLineReader 保存当前续块的独立压缩上下文
type u3dLineReader struct {
values *u3dValues
bits *u3dBits
hist [u3dLineContextCount]u3dHistogram
}
// lineDeclaration 读取ECMA-363第9.6.3节的线集声明
// 入参: r 字段读取器, name 资源名
// 返回: error 结构、能力或预算错误
func (d *u3dDecoder) lineDeclaration(r *u3dValues, name string) error {
if _, exists := d.meshes[name]; exists {
return fmt.Errorf("duplicate U3D model resource %q", name)
}
if r.u32() != 0 || r.u32() != 0 {
return fmt.Errorf("invalid U3D line declaration")
}
decl := &u3dMeshDeclaration{mesh: U3DMesh{Name: name}, line: new(u3dLineState)}
for i := range decl.counts {
decl.counts[i] = r.u32()
}
if err := d.meshShadings(r, decl); err != nil {
return err
}
for i := range decl.mesh.Quality {
decl.mesh.Quality[i] = r.u32()
}
for i := range decl.mesh.InverseQuantization {
decl.mesh.InverseQuantization[i] = r.f32()
}
for range 3 {
if r.u32() != 0 {
return fmt.Errorf("invalid U3D reserved line parameter")
}
}
if r.u32() != 0 {
return fmt.Errorf("unsupported U3D skeletal line set")
}
if err := r.done(); err != nil {
return err
}
if decl.counts[1] == 0 {
for _, count := range decl.counts[:6] {
if count != 0 {
return fmt.Errorf("invalid U3D empty line set")
}
}
decl.decoded = true
}
state := decl.line
var used [11]bool
used[0] = true
for _, shading := range decl.mesh.Shadings {
used[1] = used[1] || shading.Attributes&1 != 0
used[2] = used[2] || shading.Attributes&2 != 0
for layer := range shading.TextureDimensions {
used[3+layer] = true
}
}
for i, present := range used {
state.offsets[i] = -1
if present {
state.offsets[i] = state.stride
state.stride += 5
}
}
c := decl.counts
bytes := uint64(c[0])*100 + (uint64(c[1])+uint64(c[2]))*12 + (uint64(c[3])+uint64(c[4])+uint64(c[5]))*16
if err := d.reserve(bytes, 1); err != nil {
return err
}
if err := d.reserve(uint64(c[1]), uint64(state.stride)*8); err != nil {
return err
}
m := &decl.mesh
m.Lines = make([]U3DLine, 0, int(c[0]))
m.Positions, m.Normals = make([][3]float32, 0, int(c[1])), make([][3]float32, 0, int(c[2]))
m.Diffuse, m.Specular, m.TextureCoordinates = make([][4]float32, 0, int(c[3])), make([][4]float32, 0, int(c[4])), make([][4]float32, 0, int(c[5]))
state.stats = make([]float64, int(c[1])*state.stride)
d.meshes[name] = decl
d.meshOrder = append(d.meshOrder, name)
return nil
}
// average 取得指定顶点各属性的端点算术平均
// 入参: position 顶点索引
// 返回: [11][4]float64 法线、漫反射、镜面及八层纹理预测
func (s *u3dLineState) average(position uint32) (values [11][4]float64) {
for slot, offset := range s.offsets {
if offset < 0 {
continue
}
base := int(position)*s.stride + offset
count := s.stats[base+4]
if count != 0 {
for i := range values[slot] {
values[slot][i] = s.stats[base+i] / count
}
}
}
return values
}
// accumulate 累计线段端点的独立属性,重复使用的属性按端点分别计数
// 入参: mesh 共享属性数组, line 已完成的线段
func (s *u3dLineState) accumulate(mesh *U3DMesh, line U3DLine) {
shading := mesh.Shadings[line.Shading]
for _, corner := range line.Corners {
var values [11][4]float32
var used [11]bool
normal := mesh.Normals[corner.Normal]
values[0] = [4]float32{normal[0], normal[1], normal[2], 0}
used[0] = true
if shading.Attributes&1 != 0 {
values[1], used[1] = mesh.Diffuse[corner.Diffuse], true
}
if shading.Attributes&2 != 0 {
values[2], used[2] = mesh.Specular[corner.Specular], true
}
for layer := range shading.TextureDimensions {
values[3+layer], used[3+layer] = mesh.TextureCoordinates[corner.Texture[layer]], true
}
for slot, present := range used {
if !present {
continue
}
base := int(corner.Position)*s.stride + s.offsets[slot]
for i, value := range values[slot] {
s.stats[base+i] += float64(value)
}
s.stats[base+4]++
}
}
}
// value 按指定上下文和位宽读取线集字段
// 入参: context 压缩上下文, small 是否为8位字段
// 返回: uint32 字段值
func (r *u3dLineReader) value(context int, small bool) uint32 {
if r.bits != nil {
return r.bits.dynamicValue(&r.hist[context], small)
}
if small {
return uint32(r.values.u8())
}
return r.values.u32()
}
// index 读取静态索引并校验声明范围
// 入参: count 可用索引数量
// 返回: uint32 索引, error 读取或范围错误
func (r *u3dLineReader) index(count uint32) (uint32, error) {
if count == 0 {
return 0, fmt.Errorf("invalid U3D line position range")
}
var value uint32
if r.bits != nil {
value = r.bits.index(count)
} else {
value = r.values.u32()
}
if err := r.err(); err != nil {
return 0, err
}
if value >= count {
return 0, fmt.Errorf("invalid U3D line position index")
}
return value, nil
}
// err 返回当前字段或算术解码错误
// 返回: error 读取错误
func (r *u3dLineReader) err() error {
if r.bits != nil {
return r.bits.err
}
return r.values.err
}
// reconstruct 按符号位和独立分量上下文执行逆量化
// 入参: prediction 预测值, scale 逆量化系数, signContext 符号上下文, componentContext 分量首上下文, dimensions 分量数
// 返回: [4]float32 重建属性, error 数值或读取错误
func (r *u3dLineReader) reconstruct(prediction [4]float64, scale float32, signContext, componentContext, dimensions int) (values [4]float32, err error) {
signs := r.value(signContext, true)
if signs >= 1<<dimensions {
return values, fmt.Errorf("invalid U3D line difference signs")
}
for i := range dimensions {
difference := float64(r.value(componentContext+i, false)) * float64(scale)
if signs&(1<<i) != 0 {
difference = -difference
}
values[i] = float32(prediction[i] + difference)
if !finite(float64(values[i])) {
return values, fmt.Errorf("invalid U3D reconstructed line attribute")
}
}
return values, r.err()
}
// attribute 读取颜色或纹理属性,重复标记引用所属属性池的末项
// 入参: pool 属性池, limit 声明总数, prediction 预测值, scale 逆量化系数, duplicate 重复上下文, sign 符号上下文, component 分量首上下文
// 返回: uint32 属性索引, error 读取或范围错误
func (r *u3dLineReader) attribute(pool *[][4]float32, limit uint32, prediction [4]float64, scale float32, duplicate, sign, component int) (uint32, error) {
flag := r.value(duplicate, true)
if err := r.err(); err != nil {
return 0, err
}
if flag == 2 && len(*pool) > 0 {
return uint32(len(*pool) - 1), nil
}
if flag != 0 || uint64(len(*pool)) >= uint64(limit) {
return 0, fmt.Errorf("invalid U3D line attribute pool")
}
value, err := r.reconstruct(prediction, scale, sign, component, 4)
if err != nil {
return 0, err
}
index := uint32(len(*pool))
*pool = append(*pool, value)
return index, nil
}
// lineContinuation 按位置分段恢复线集,预测状态跨续块保存
// 入参: r 字段读取器, name 资源名
// 返回: error 结构、数值、预算或取消错误
func (d *u3dDecoder) lineContinuation(r *u3dValues, name string) error {
decl := d.meshes[name]
if decl == nil || decl.line == nil || decl.decoded || r.u32() != 0 {
return fmt.Errorf("invalid U3D line continuation reference")
}
start, end := r.u32(), r.u32()
mesh := &decl.mesh
if r.err != nil {
return r.err
}
if uint64(start) != uint64(len(mesh.Positions)) || end <= start || end > decl.counts[1] {
return fmt.Errorf("invalid U3D line resolution range")
}
reader := &u3dLineReader{values: r}
var charged uint64
if !d.plain {
reader.bits = newU3DBits(r.data[r.pos:])
reader.bits.reserve = func(count, size uint64) error {
if err := d.reserve(count, size); err != nil {
return err
}
charged += count * size
return nil
}
defer func() { d.remaining += charged }()
}
for position := start; position < end; position++ {
if err := d.ctx.Err(); err != nil {
return err
}
split, err := reader.index(max(1, position))
if err != nil {
return err
}
var prediction [4]float64
if position > 0 {
for i, value := range mesh.Positions[split] {
prediction[i] = float64(value)
}
}
attributes := decl.line.average(split)
value, err := reader.reconstruct(prediction, mesh.InverseQuantization[0], u3dLinePositionSign, u3dLinePositionX, 3)
if err != nil {
return err
}
mesh.Positions = append(mesh.Positions, [3]float32{value[0], value[1], value[2]})
normalStart := uint32(len(mesh.Normals))
normals := reader.value(u3dLineNormalCount, false)
if normals > decl.counts[2]-normalStart {
return fmt.Errorf("invalid U3D line normal count")
}
for i := uint32(0); i < normals; i++ {
if i%256 == 0 {
if err := d.ctx.Err(); err != nil {
return err
}
}
value, err := reader.reconstruct(attributes[0], mesh.InverseQuantization[1], u3dLineNormalSign, u3dLineNormalX, 3)
if err != nil {
return err
}
mesh.Normals = append(mesh.Normals, [3]float32{value[0], value[1], value[2]})
}
lines := reader.value(u3dLineCount, false)
if lines > decl.counts[0]-uint32(len(mesh.Lines)) {
return fmt.Errorf("invalid U3D line count")
}
for i := uint32(0); i < lines; i++ {
if i%256 == 0 {
if err := d.ctx.Err(); err != nil {
return err
}
}
line := U3DLine{Shading: reader.value(u3dLineShading, false)}
if uint64(line.Shading) >= uint64(len(mesh.Shadings)) {
return fmt.Errorf("invalid U3D line shading index")
}
first, err := reader.index(position)
if err != nil {
return err
}
line.Corners[0].Position, line.Corners[1].Position = first, position
shading := mesh.Shadings[line.Shading]
for j := range line.Corners {
corner := &line.Corners[j]
normal := reader.value(u3dLineNormalIndex, false)
if normal >= normals {
return fmt.Errorf("invalid U3D line local normal index")
}
corner.Normal = normalStart + normal
if shading.Attributes&1 != 0 {
corner.Diffuse, err = reader.attribute(&mesh.Diffuse, decl.counts[3], attributes[1], mesh.InverseQuantization[3], u3dLineDiffuseDuplicate, u3dLineDiffuseSign, u3dLineColorR)
if err != nil {
return err
}
}
if shading.Attributes&2 != 0 {
corner.Specular, err = reader.attribute(&mesh.Specular, decl.counts[4], attributes[2], mesh.InverseQuantization[4], u3dLineSpecularDuplicate, u3dLineSpecularSign, u3dLineColorR)
if err != nil {
return err
}
}
for layer := range shading.TextureDimensions {
corner.Texture[layer], err = reader.attribute(&mesh.TextureCoordinates, decl.counts[5], attributes[3+layer], mesh.InverseQuantization[2], u3dLineTextureDuplicate, u3dLineTextureSign, u3dLineTextureU)
if err != nil {
return err
}
}
}
if err := reader.err(); err != nil {
return err
}
mesh.Lines = append(mesh.Lines, line)
decl.line.accumulate(mesh, line)
}
if err := reader.err(); err != nil {
return err
}
}
if b := reader.bits; b != nil {
if b.pos > uint64(len(b.data))*8 {
return io.ErrUnexpectedEOF
}
if b.u32() != 0 {
return fmt.Errorf("invalid U3D line compression flush")
}
if b.err != nil {
return b.err
}
if (b.pos-16+7)/8 != uint64(len(b.data)) {
return fmt.Errorf("unexpected U3D line compressed data")
}
r.pos = len(r.data)
}
if err := r.done(); err != nil {
return err
}
if end == decl.counts[1] {
for i, count := range []int{len(mesh.Lines), len(mesh.Positions), len(mesh.Normals), len(mesh.Diffuse), len(mesh.Specular), len(mesh.TextureCoordinates)} {
if uint64(count) != uint64(decl.counts[i]) {
return fmt.Errorf("U3D line set does not match declaration")
}
}
decl.decoded = true
d.remaining += uint64(len(decl.line.stats)) * 8
decl.line.stats = nil
}
return nil
}
+63 -31
View File
@@ -19,7 +19,7 @@ import (
"io" "io"
) )
// U3DMesh 保存完整基础网格的独立数组,索引对应原始作者网格 // U3DMesh 保存静态三角面和线段的共享属性数组,索引对应原始作者几何
type U3DMesh struct { type U3DMesh struct {
Name string Name string
ExcludeNormals bool ExcludeNormals bool
@@ -27,12 +27,13 @@ type U3DMesh struct {
Diffuse, Specular, TextureCoordinates [][4]float32 Diffuse, Specular, TextureCoordinates [][4]float32
Shadings []U3DShading Shadings []U3DShading
Faces []U3DFace Faces []U3DFace
Lines []U3DLine
Quality [3]uint32 Quality [3]uint32
InverseQuantization [5]float32 InverseQuantization [5]float32
NormalCrease, NormalUpdate, NormalTolerance float32 NormalCrease, NormalUpdate, NormalTolerance float32
} }
// U3DShading 描述各面使用的颜色及纹理坐标,纹理维数依层排列 // U3DShading 描述图元使用的颜色及纹理坐标,纹理维数依层排列
type U3DShading struct { type U3DShading struct {
Attributes uint32 Attributes uint32
TextureDimensions []uint32 TextureDimensions []uint32
@@ -45,17 +46,35 @@ type U3DFace struct {
Corners [3]U3DCorner Corners [3]U3DCorner
} }
// U3DLine 保存着色描述及线段两端的独立属性索引
type U3DLine struct {
Shading uint32
Corners [2]U3DCorner
}
// U3DCorner 保存各属性索引,未启用的属性不可用于访问数组 // U3DCorner 保存各属性索引,未启用的属性不可用于访问数组
type U3DCorner struct { type U3DCorner struct {
Position, Normal, Diffuse, Specular uint32 Position, Normal, Diffuse, Specular uint32
Texture [8]uint32 Texture [8]uint32
} }
// u3dMeshDeclaration 保存基础网格声明及续块接收状态 // u3dMeshDeclaration 保存基础网格或线集声明及续块接收状态
type u3dMeshDeclaration struct { type u3dMeshDeclaration struct {
mesh U3DMesh mesh U3DMesh
counts [7]uint32 counts [7]uint32
decoded bool decoded bool
line *u3dLineState
}
// primitive 访问三角面或线段的着色编号和角点,不复制属性数组
// 入参: index 图元下标,三角面在前,线段在后
// 返回: uint32 着色编号, []U3DCorner 只读角点
func (m *U3DMesh) primitive(index int) (uint32, []U3DCorner) {
if index < len(m.Faces) {
return m.Faces[index].Shading, m.Faces[index].Corners[:]
}
line := &m.Lines[index-len(m.Faces)]
return line.Shading, line.Corners[:]
} }
// meshDeclaration 读取完整基础网格声明,不将渐进网格当作完整模型 // meshDeclaration 读取完整基础网格声明,不将渐进网格当作完整模型
@@ -73,6 +92,46 @@ func (d *u3dDecoder) meshDeclaration(r *u3dValues, name string) error {
for index := range decl.counts { for index := range decl.counts {
decl.counts[index] = r.u32() decl.counts[index] = r.u32()
} }
if err := d.meshShadings(r, decl); err != nil {
return err
}
minimum, maximum := r.u32(), r.u32()
for index := range decl.mesh.Quality {
decl.mesh.Quality[index] = r.u32()
}
for index := range decl.mesh.InverseQuantization {
decl.mesh.InverseQuantization[index] = r.f32()
}
decl.mesh.NormalCrease, decl.mesh.NormalUpdate, decl.mesh.NormalTolerance = r.f32(), r.f32(), r.f32()
bones := r.u32()
if r.err != nil {
return r.err
}
if minimum > maximum || maximum != decl.counts[1] {
return fmt.Errorf("invalid U3D mesh resolution")
}
if minimum != maximum || bones != 0 {
return fmt.Errorf("unsupported U3D progressive or skeletal mesh")
}
if minimum == 0 {
for _, count := range decl.counts[:6] {
if count != 0 {
return fmt.Errorf("invalid U3D empty mesh")
}
}
}
d.meshes[name] = decl
d.meshOrder = append(d.meshOrder, name)
return nil
}
// meshShadings 读取三角面与线集共用的着色描述
// 入参: r 字段读取器, decl 资源声明
// 返回: error 结构或预算错误
func (d *u3dDecoder) meshShadings(r *u3dValues, decl *u3dMeshDeclaration) error {
if r.err != nil {
return r.err
}
count := decl.counts[6] count := decl.counts[6]
if uint64(count) > uint64(len(r.data)-r.pos)/12 { if uint64(count) > uint64(len(r.data)-r.pos)/12 {
return io.ErrUnexpectedEOF return io.ErrUnexpectedEOF
@@ -104,34 +163,7 @@ func (d *u3dDecoder) meshDeclaration(r *u3dValues, name string) error {
} }
shading.OriginalID = r.u32() shading.OriginalID = r.u32()
} }
minimum, maximum := r.u32(), r.u32()
for index := range decl.mesh.Quality {
decl.mesh.Quality[index] = r.u32()
}
for index := range decl.mesh.InverseQuantization {
decl.mesh.InverseQuantization[index] = r.f32()
}
decl.mesh.NormalCrease, decl.mesh.NormalUpdate, decl.mesh.NormalTolerance = r.f32(), r.f32(), r.f32()
bones := r.u32()
if r.err != nil {
return r.err return r.err
}
if minimum > maximum || maximum != decl.counts[1] {
return fmt.Errorf("invalid U3D mesh resolution")
}
if minimum != maximum || bones != 0 {
return fmt.Errorf("unsupported U3D progressive or skeletal mesh")
}
if minimum == 0 {
for _, count := range decl.counts[:6] {
if count != 0 {
return fmt.Errorf("invalid U3D empty mesh")
}
}
}
d.meshes[name] = decl
d.meshOrder = append(d.meshOrder, name)
return nil
} }
// baseMesh 读取基础顶点与压缩面索引,保留各角点的独立属性 // baseMesh 读取基础顶点与压缩面索引,保留各角点的独立属性
@@ -139,7 +171,7 @@ func (d *u3dDecoder) meshDeclaration(r *u3dValues, name string) error {
// 返回: error 结构、能力、预算或取消错误 // 返回: error 结构、能力、预算或取消错误
func (d *u3dDecoder) baseMesh(r *u3dValues, name string) error { func (d *u3dDecoder) baseMesh(r *u3dValues, name string) error {
decl := d.meshes[name] decl := d.meshes[name]
if decl == nil || decl.decoded || decl.counts[1] == 0 || r.u32() != 0 { if decl == nil || decl.line != nil || decl.decoded || decl.counts[1] == 0 || r.u32() != 0 {
return fmt.Errorf("invalid U3D base mesh reference") return fmt.Errorf("invalid U3D base mesh reference")
} }
var counts [6]uint32 var counts [6]uint32
+347
View File
@@ -0,0 +1,347 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"fmt"
"math"
)
// U3DRenderMode 定义静态几何的呈现方式,边线和顶点使用单像素覆盖
type U3DRenderMode uint8
const (
U3DRenderSolid U3DRenderMode = iota // 实体
U3DRenderSolidWireframe // 实体线框
U3DRenderWireframe // 线框
U3DRenderShadedWireframe // 着色线框
U3DRenderHiddenWireframe // 隐藏线框
U3DRenderVertices // 顶点
U3DRenderShadedVertices // 着色顶点
U3DRenderBoundingBox // 包围盒线框
U3DRenderBoundingBoxFaces // 包围盒面
U3DRenderBoundingBoxOutline // 包围盒面及线框
U3DRenderIllustration // 插图
U3DRenderSolidOutline // 实体轮廓
U3DRenderShadedIllustration // 着色插图
)
// u3dPrimitiveKey 区分共享点线的颜色及混合状态,避免透明边点重复叠加
type u3dPrimitiveKey struct {
a, b u3dVertex
blend uint32
function uint32
reference float32
}
// uniquePrimitive 在当前网格内去重着色边点,缓存按条目计入绘制预算
// 入参: a 起点或顶点, b 终点或相同顶点, shader 着色器
// 返回: bool 是否首次绘制, error 预算或取消错误
func (r *u3dRender) uniquePrimitive(a, b u3dVertex, shader U3DShader) (bool, error) {
if r.primitives == nil {
return true, nil
}
left := [7]float64{a.point[0], a.point[1], a.point[2], a.color[0], a.color[1], a.color[2], a.color[3]}
right := [7]float64{b.point[0], b.point[1], b.point[2], b.color[0], b.color[1], b.color[2], b.color[3]}
for i := range left {
if left[i] != right[i] {
if left[i] > right[i] {
a, b = b, a
}
break
}
}
key := u3dPrimitiveKey{a: a, b: b, blend: shader.Blend}
if shader.Attributes&2 != 0 {
key.function, key.reference = shader.AlphaFunction, shader.AlphaReference
}
if _, ok := r.primitives[key]; ok {
return false, nil
}
if err := r.reserve(1, 512); err != nil {
return false, err
}
r.primitives[key] = struct{}{}
return true, nil
}
// shaded 判断绘制模式是否使用材质、法线和灯光
// 返回: bool 是否着色
func (mode U3DRenderMode) shaded() bool {
return mode == U3DRenderSolid || mode == U3DRenderSolidWireframe || mode == U3DRenderShadedWireframe || mode == U3DRenderShadedVertices || mode == U3DRenderSolidOutline || mode == U3DRenderShadedIllustration
}
// silhouette 判断绘制模式是否仅描绘轮廓边
// 返回: bool 是否使用轮廓边
func (mode U3DRenderMode) silhouette() bool {
return mode >= U3DRenderIllustration && mode <= U3DRenderShadedIllustration
}
// modeTriangle 根据当前模式绘制原三角面、原始边或顶点,不为裁剪边界生成网格边
// 入参: triangle 相机空间顶点, shader 着色器, pass 绘制轮次
// 返回: error 数值、资源或取消错误
func (r *u3dRender) modeTriangle(triangle [3]u3dVertex, shader U3DShader, pass U3DViewPass) error {
if r.depthOnly || r.mode == U3DRenderSolid || r.mode.silhouette() {
return r.sectionTriangle(triangle, shader, pass)
}
if r.mode == U3DRenderSolidWireframe {
if err := r.sectionTriangle(triangle, shader, pass); err != nil {
return err
}
}
if r.mode != U3DRenderShadedWireframe && r.mode != U3DRenderShadedVertices {
for i := range triangle {
triangle[i].color = [4]float64{r.auxiliaryColor[0], r.auxiliaryColor[1], r.auxiliaryColor[2], 1}
}
shader = U3DShader{Blend: 0x606}
}
normal := u3dTriangleNormal([3]u3dVector{triangle[0].point, triangle[1].point, triangle[2].point})
for i, vertex := range triangle {
var err error
if r.mode == U3DRenderVertices || r.mode == U3DRenderShadedVertices {
fresh, uniqueErr := r.uniquePrimitive(vertex, vertex, shader)
if uniqueErr != nil {
return uniqueErr
}
if !fresh {
continue
}
err = r.modePoint(vertex, shader, pass)
} else {
fresh, uniqueErr := r.uniquePrimitive(vertex, triangle[(i+1)%3], shader)
if uniqueErr != nil {
return uniqueErr
}
if !fresh {
continue
}
err = r.modeLine(vertex, triangle[(i+1)%3], normal, triangle[0].point, shader, pass)
}
if err != nil {
return err
}
}
if r.mode != U3DRenderSolidWireframe {
for i, section := range r.sections {
if section.intersection != nil {
if err := r.sectionIntersection(triangle, i); err != nil {
return err
}
}
}
}
return nil
}
// modeLine 对原始网格边应用剖切与视锥裁剪,再按像素深度绘制
// 入参: a 起点, b 终点, normal 面法线, origin 面上点, shader 着色器, pass 绘制轮次
// 返回: error 数值、资源或取消错误
func (r *u3dRender) modeLine(a, b u3dVertex, normal, origin u3dVector, shader U3DShader, pass U3DViewPass) error {
for i, section := range r.sections {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
keep, err := u3dClipLine(&a, &b, section.plane)
if err != nil || !keep {
return err
}
}
for i, plane := range r.frustumPlanes() {
if i == 1 && r.camera.Far == 0 && !r.camera.ClipFar {
continue
}
keep, err := u3dClipLine(&a, &b, plane)
if err != nil || !keep {
return err
}
}
return r.rasterLine(a, b, normal, origin, shader, pass)
}
// modePoint 绘制原始顶点,不在裁剪边界补充顶点
// 入参: vertex 顶点, shader 着色器, pass 绘制轮次
// 返回: error 数值、资源或取消错误
func (r *u3dRender) modePoint(vertex u3dVertex, shader U3DShader, pass U3DViewPass) error {
if err := r.ctx.Err(); err != nil {
return err
}
for i, section := range r.sections {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
distance := u3dPlaneDistance(section.plane, vertex.point)
if !finite(distance) {
return fmt.Errorf("invalid U3D point clip distance")
}
if distance < 0 {
return nil
}
}
for i, plane := range r.frustumPlanes() {
if i == 1 && r.camera.Far == 0 && !r.camera.ClipFar {
continue
}
distance := u3dPlaneDistance(plane, vertex.point)
if !finite(distance) {
return fmt.Errorf("invalid U3D point clip distance")
}
if distance < 0 {
return nil
}
}
q := 1.0
if r.camera.Perspective {
q = 1 / vertex.point[2]
}
x := math.Floor(r.viewport[0] + r.viewport[2]/2 + r.camera.ShiftX + vertex.point[0]*r.camera.ScaleX*q)
y := math.Floor(r.viewport[1] + r.viewport[3]/2 + r.camera.ShiftY - vertex.point[1]*r.camera.ScaleY*q)
if !finite(x) || !finite(y) {
return fmt.Errorf("invalid U3D projected vertex")
}
if x < 0 || y < 0 || x >= float64(r.output.Rect.Dx()) || y >= float64(r.output.Rect.Dy()) {
return nil
}
pixel := int(y)*r.output.Rect.Dx() + int(x)
if vertex.point[2] > r.frame[pixel].depth {
return nil
}
visible, err := u3dFragmentColor(&vertex.color, vertex.point, &shader, &pass)
if err != nil || !visible {
return err
}
r.order++
return r.fragment(pixel, vertex.point[2], vertex.color, shader.Blend)
}
// drawBounds 在模型局部空间计算包围盒,再应用实例变换,不以世界坐标轴重建包围盒
// 入参: instance 场景实例, node 模型节点, mesh 网格, pass 绘制轮次, passIndex 轮次索引
// 返回: error 几何、数值、预算或取消错误
func (r *u3dRender) drawBounds(instance u3dInstance, node *U3DNode, mesh *U3DMesh, pass U3DViewPass, passIndex int) error {
minimum := u3dVector{math.Inf(1), math.Inf(1), math.Inf(1)}
maximum := u3dVector{math.Inf(-1), math.Inf(-1), math.Inf(-1)}
visible := false
for i := range len(mesh.Faces) + len(mesh.Lines) {
shading, corners := mesh.primitive(i)
shaders := node.Shaders
if i >= len(mesh.Faces) {
shaders = node.LineShaders
}
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
if uint64(shading) >= uint64(len(shaders)) {
visible = true
} else if !visible {
for _, name := range shaders[shading] {
index, ok := r.shaders[name]
if !ok || r.model.Shaders[index].RenderPass&(1<<passIndex) != 0 {
visible = true
break
}
}
}
for _, corner := range corners {
if uint64(corner.Position) >= uint64(len(mesh.Positions)) {
return fmt.Errorf("invalid U3D bounds position index")
}
for j, value := range mesh.Positions[corner.Position] {
v := float64(value)
if !finite(v) {
return fmt.Errorf("invalid U3D bounds position")
}
minimum[j], maximum[j] = min(minimum[j], v), max(maximum[j], v)
}
}
}
if !visible || math.IsInf(minimum[0], 1) {
return nil
}
opacity := 1.0
if instance.opacity >= 0 {
opacity = instance.opacity
}
if r.opacity >= 0 {
opacity *= r.opacity
}
if r.mode == U3DRenderBoundingBox {
opacity = 0
}
var vertices [8]u3dVertex
for i := range vertices {
point := minimum
for j := range 3 {
if i&(1<<j) != 0 {
point[j] = maximum[j]
}
}
vertices[i].point = r.toCamera.point(instance.world.point(point))
for _, v := range vertices[i].point {
if !finite(v) {
return fmt.Errorf("invalid U3D transformed bounds")
}
}
vertices[i].color = [4]float64{r.faceColor[0], r.faceColor[1], r.faceColor[2], opacity}
}
shader := U3DShader{Blend: 0x606}
var edges [12][2]u3dVector
edgeCount := 0
for index, face := range [6][4]int{{0, 2, 3, 1}, {4, 5, 7, 6}, {0, 1, 5, 4}, {2, 6, 7, 3}, {0, 4, 6, 2}, {1, 3, 7, 5}} {
if index%2 == 1 && minimum[2-index/2] == maximum[2-index/2] {
continue
}
for i := 1; i < 3; i++ {
triangle := [3]u3dVertex{vertices[face[0]], vertices[face[i]], vertices[face[i+1]]}
r.order++
if err := r.sectionTriangle(triangle, shader, pass); err != nil {
return err
}
}
if r.mode == U3DRenderBoundingBoxFaces {
continue
}
normal := u3dTriangleNormal([3]u3dVector{vertices[face[0]].point, vertices[face[1]].point, vertices[face[2]].point})
for i, index := range face {
a, b := vertices[index], vertices[face[(i+1)%4]]
alpha := 1.0
if r.mode == U3DRenderBoundingBoxOutline {
alpha = opacity
duplicate := a.point == b.point
for _, edge := range edges[:edgeCount] {
if edge == ([2]u3dVector{a.point, b.point}) || edge == ([2]u3dVector{b.point, a.point}) {
duplicate = true
break
}
}
if duplicate {
continue
}
edges[edgeCount] = [2]u3dVector{a.point, b.point}
edgeCount++
}
a.color = [4]float64{r.auxiliaryColor[0], r.auxiliaryColor[1], r.auxiliaryColor[2], alpha}
b.color = a.color
if err := r.modeLine(a, b, normal, a.point, shader, pass); err != nil {
return err
}
}
}
return nil
}
+151
View File
@@ -0,0 +1,151 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"fmt"
"math"
)
// u3dOutline 保存单个模型实例的共享边索引,按插入顺序绘制
type u3dOutline struct {
index map[[2]u3dVector]int
edges []u3dOutlineEdge
}
// u3dOutlineEdge 保存正背面关系及正面法线角度区间,避免多面共享边的成对扫描
type u3dOutlineEdge struct {
a, b u3dVector
reference, tangent u3dVector
minimumNormal, maximumNormal u3dVector
visibleNormal u3dVector
minimum, maximum float64
front, back int
visible, frontVisible bool
}
// outlineFace 收集当前绘制轮次中的原始网格边,正面法线在同一半圆内取角度极值
// 入参: outline 共享边索引, world 世界坐标, vertices 相机顶点, front 是否正面, visible 是否绘制面, shaders 着色列表, passIndex 轮次
// 返回: error 数值、预算或取消错误
func (r *u3dRender) outlineFace(outline *u3dOutline, world [3]u3dVector, vertices [3]u3dVertex, front, visible bool, shaders []string, passIndex int) error {
active := false
for i, name := range shaders {
if i%128 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
index, ok := r.shaders[name]
if !ok || r.model.Shaders[index].RenderPass&(1<<passIndex) != 0 {
active = true
break
}
}
if !active {
return nil
}
normal := u3dTriangleNormal(world)
if normal == (u3dVector{}) {
return nil
}
cameraNormal := u3dTriangleNormal([3]u3dVector{vertices[0].point, vertices[1].point, vertices[2].point})
for i := range 3 {
j := (i + 1) % 3
a, b := world[i], world[j]
pa, pb := vertices[i].point, vertices[j].point
for c := range 3 {
if a[c] != b[c] {
if a[c] > b[c] {
a, b, pa, pb = b, a, pb, pa
}
break
}
}
key := [2]u3dVector{a, b}
index, exists := outline.index[key]
if !exists {
if err := r.reserve(1, 1024); err != nil {
return err
}
index = len(outline.edges)
outline.edges = append(outline.edges, u3dOutlineEdge{a: pa, b: pb})
outline.index[key] = index
}
edge := &outline.edges[index]
if visible && !edge.visible {
edge.visibleNormal = cameraNormal
}
edge.visible = edge.visible || visible
if !front {
edge.back++
continue
}
edge.frontVisible = edge.frontVisible || visible
if edge.front == 0 {
edge.reference = normal
edge.tangent = b.sub(a).unit().cross(normal).unit()
edge.minimumNormal, edge.maximumNormal = cameraNormal, cameraNormal
} else {
angle := math.Atan2(normal.dot(edge.tangent), normal.dot(edge.reference))
if !finite(angle) {
return fmt.Errorf("invalid U3D silhouette angle")
}
if angle < edge.minimum {
edge.minimum, edge.minimumNormal = angle, cameraNormal
}
if angle > edge.maximum {
edge.maximum, edge.maximumNormal = angle, cameraNormal
}
}
edge.front++
}
return nil
}
// drawOutline 绘制外缘、正背面交界和达到夹角的共享边,沿用像素深度去除遮挡
// 入参: outline 共享边索引, pass 绘制轮次
// 返回: error 数值、资源或取消错误
func (r *u3dRender) drawOutline(outline *u3dOutline, pass U3DViewPass) error {
shader := U3DShader{Blend: 0x606}
color := [4]float64{r.auxiliaryColor[0], r.auxiliaryColor[1], r.auxiliaryColor[2], 1}
for i, edge := range outline.edges {
if i%128 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
if !edge.visible {
continue
}
angle := (edge.maximum - edge.minimum) * 180 / math.Pi
if edge.front+edge.back != 1 && !(edge.front > 0 && edge.back > 0) && !(edge.front > 1 && angle >= math.Nextafter(r.creaseAngle, math.Inf(-1))) {
continue
}
a, b := u3dVertex{point: edge.a, color: color}, u3dVertex{point: edge.b, color: color}
normals := [2]u3dVector{edge.visibleNormal, edge.visibleNormal}
if edge.frontVisible {
normals = [2]u3dVector{edge.minimumNormal, edge.maximumNormal}
}
for i, normal := range normals {
if i == 1 && normal == normals[0] {
continue
}
if err := r.modeLine(a, b, normal, edge.a, shader, pass); err != nil {
return err
}
}
}
return nil
}
+71 -8
View File
@@ -35,6 +35,9 @@ type U3DCamera struct {
// U3DRenderOptions 控制静态绘制,指定Camera时使用整个输出视口 // U3DRenderOptions 控制静态绘制,指定Camera时使用整个输出视口
// Camera为空时使用View指定的节点或首个视图节点,Instance指定其父路径实例 // Camera为空时使用View指定的节点或首个视图节点,Instance指定其父路径实例
// Lighting为空时沿用模型灯光,非空时替换全部模型灯光 // Lighting为空时沿用模型灯光,非空时替换全部模型灯光
// Opacity为空时沿用着色器混合,非空时叠加面片、着色边点及包围盒轮廓的透明度
// Mode控制绘制方式,AuxiliaryColor用于固定颜色的边点,FaceColor用于包围盒面及插图面,空值分别使用黑色和背景色
// CreaseAngle为轮廓夹角,单位为度,空值使用45;着色插图以漫反射色的1/4补充自发光
// Nodes按名称覆盖节点,重复名称使用最后一项,每次绘制均从原场景开始 // Nodes按名称覆盖节点,重复名称使用最后一项,每次绘制均从原场景开始
// Background为空时使用白色;缓存与实例上限为0时分别使用256MiB和65536,不含输入及运行时开销 // Background为空时使用白色;缓存与实例上限为0时分别使用256MiB和65536,不含输入及运行时开销
type U3DRenderOptions struct { type U3DRenderOptions struct {
@@ -44,6 +47,11 @@ type U3DRenderOptions struct {
Camera *U3DCamera Camera *U3DCamera
Background *[3]float64 Background *[3]float64
Lighting *U3DLighting Lighting *U3DLighting
Opacity *float64
Mode U3DRenderMode
AuxiliaryColor *[3]float64
FaceColor *[3]float64
CreaseAngle *float64
Nodes []U3DNodeState Nodes []U3DNodeState
Sections []U3DCrossSection Sections []U3DCrossSection
MaxMemoryBytes int64 MaxMemoryBytes int64
@@ -83,6 +91,12 @@ type u3dRender struct {
meshes, shaders, materials, lightIndex map[string]int meshes, shaders, materials, lightIndex map[string]int
lights []u3dRenderLight lights []u3dRenderLight
lighting *U3DLighting lighting *U3DLighting
opacity float64
mode U3DRenderMode
creaseAngle float64
auxiliaryColor, faceColor [3]float64
depthOnly bool
primitives map[u3dPrimitiveKey]struct{}
states map[string]U3DNodeState states map[string]U3DNodeState
sections []u3dSection sections []u3dSection
sectionScratch [2][]u3dVertex sectionScratch [2][]u3dVertex
@@ -92,7 +106,7 @@ type u3dRender struct {
order uint64 order uint64
} }
// RenderU3D 绘制静态无纹理三角面,返回不透明图像,不执行脚本或动画 // RenderU3D 绘制静态无纹理网格、线集及其顶点或包围盒,返回不透明图像,不执行脚本或动画
// 采用逐顶点光照及逐像素深度混合,缺失资源回退为空网格、灰色材质或无光源 // 采用逐顶点光照及逐像素深度混合,缺失资源回退为空网格、灰色材质或无光源
// 入参: ctx 取消上下文, model 场景, options 视图、输出与资源限制 // 入参: ctx 取消上下文, model 场景, options 视图、输出与资源限制
// 返回: *image.RGBA 图像, error 参数、能力、预算或取消错误 // 返回: *image.RGBA 图像, error 参数、能力、预算或取消错误
@@ -119,7 +133,23 @@ func newU3DRender(ctx context.Context, model *U3DModel, options U3DRenderOptions
if limit == 0 { if limit == 0 {
limit = u3dDefaultBytes limit = u3dDefaultBytes
} }
r := &u3dRender{ctx: ctx, model: model, remaining: uint64(limit)} r := &u3dRender{ctx: ctx, model: model, remaining: uint64(limit), opacity: -1, mode: options.Mode, creaseAngle: 45}
if options.Mode > U3DRenderShadedIllustration {
return nil, fmt.Errorf("unsupported U3D render mode")
}
if options.CreaseAngle != nil {
if !finite(*options.CreaseAngle) {
return nil, fmt.Errorf("invalid U3D crease angle")
}
r.creaseAngle = *options.CreaseAngle
}
if options.Opacity != nil {
value := *options.Opacity
if !finite(value) || value < 0 || value > 1 {
return nil, fmt.Errorf("invalid U3D render opacity")
}
r.opacity = value
}
if uint64(options.Width) > uint64(^uint(0)>>1)/uint64(options.Height) { if uint64(options.Width) > uint64(^uint(0)>>1)/uint64(options.Height) {
return nil, fmt.Errorf("invalid U3D render dimensions") return nil, fmt.Errorf("invalid U3D render dimensions")
} }
@@ -169,6 +199,20 @@ func (r *u3dRender) render(options U3DRenderOptions, passes []U3DViewPass) (*ima
} }
background[i] = min(1, max(0, v)) background[i] = min(1, max(0, v))
} }
r.faceColor = background
for _, item := range []struct {
source *[3]float64
target *[3]float64
}{{options.AuxiliaryColor, &r.auxiliaryColor}, {options.FaceColor, &r.faceColor}} {
if item.source != nil {
for i, value := range *item.source {
if !finite(value) || value < 0 || value > 1 {
return nil, fmt.Errorf("invalid U3D mode color")
}
item.target[i] = value
}
}
}
r.frame = make([]u3dPixel, options.Width*options.Height) r.frame = make([]u3dPixel, options.Width*options.Height)
r.output = image.NewRGBA(image.Rect(0, 0, options.Width, options.Height)) r.output = image.NewRGBA(image.Rect(0, 0, options.Width, options.Height))
for i := range r.frame { for i := range r.frame {
@@ -412,9 +456,33 @@ func (r *u3dRender) renderPass(pass U3DViewPass, passIndex int, last bool) error
r.frame[i].head = -1 r.frame[i].head = -1
} }
r.fragments = 0 r.fragments = 0
if r.mode.shaded() {
if err := r.collectLights(); err != nil { if err := r.collectLights(); err != nil {
return err return err
} }
}
if r.mode == U3DRenderHiddenWireframe {
r.depthOnly = true
if err := r.drawScene(pass, passIndex); err != nil {
return err
}
r.depthOnly = false
}
if err := r.drawScene(pass, passIndex); err != nil {
return err
}
if last {
if err := r.drawSectionPlanes(); err != nil {
return err
}
}
return r.resolveFragments()
}
// drawScene 绘制当前轮次中的可见模型,不重置深度及片元缓存
// 入参: pass 绘制轮次, passIndex 轮次索引
// 返回: error 场景、资源或取消错误
func (r *u3dRender) drawScene(pass U3DViewPass, passIndex int) error {
for i, instance := range r.scene { for i, instance := range r.scene {
if err := r.ctx.Err(); err != nil { if err := r.ctx.Err(); err != nil {
return err return err
@@ -434,10 +502,5 @@ func (r *u3dRender) renderPass(pass U3DViewPass, passIndex int, last bool) error
return err return err
} }
} }
if last { return nil
if err := r.drawSectionPlanes(); err != nil {
return err
}
}
return r.resolveFragments()
} }
+6 -2
View File
@@ -145,13 +145,14 @@ func (r *u3dRender) sceneDiagonal() (float64, error) {
continue continue
} }
mesh := &r.model.Meshes[index] mesh := &r.model.Meshes[index]
for i, face := range mesh.Faces { for i := range len(mesh.Faces) + len(mesh.Lines) {
_, corners := mesh.primitive(i)
if i%256 == 0 { if i%256 == 0 {
if err := r.ctx.Err(); err != nil { if err := r.ctx.Err(); err != nil {
return 0, err return 0, err
} }
} }
for _, corner := range face.Corners { for _, corner := range corners {
if uint64(corner.Position) >= uint64(len(mesh.Positions)) { if uint64(corner.Position) >= uint64(len(mesh.Positions)) {
return 0, fmt.Errorf("invalid U3D position index") return 0, fmt.Errorf("invalid U3D position index")
} }
@@ -235,6 +236,9 @@ func (r *u3dRender) sectionTriangle(triangle [3]u3dVertex, shader U3DShader, pas
return err return err
} }
} }
if r.depthOnly {
return nil
}
for i, section := range r.sections { for i, section := range r.sections {
if section.intersection != nil { if section.intersection != nil {
if err := r.sectionIntersection(triangle, i); err != nil { if err := r.sectionIntersection(triangle, i); err != nil {
+90 -31
View File
@@ -30,13 +30,42 @@ type u3dVertex struct {
// 入参: instance 场景实例, node 模型节点, mesh 网格, pass 绘制轮次, passIndex 轮次索引 // 入参: instance 场景实例, node 模型节点, mesh 网格, pass 绘制轮次, passIndex 轮次索引
// 返回: error 几何、资源或取消错误 // 返回: error 几何、资源或取消错误
func (r *u3dRender) drawMesh(instance u3dInstance, node *U3DNode, mesh *U3DMesh, pass U3DViewPass, passIndex int) error { func (r *u3dRender) drawMesh(instance u3dInstance, node *U3DNode, mesh *U3DMesh, pass U3DViewPass, passIndex int) error {
count := uint64(len(mesh.Positions)) + uint64(len(mesh.Normals)) if r.mode >= U3DRenderBoundingBox && r.mode <= U3DRenderBoundingBoxOutline {
return r.drawBounds(instance, node, mesh, pass, passIndex)
}
if r.opacity == 0 && len(r.sections) == 0 && r.mode == U3DRenderSolid {
return r.ctx.Err()
}
shaded := r.mode.shaded() && !r.depthOnly
var outline *u3dOutline
if r.mode.silhouette() {
if err := r.reserve(1, 1024); err != nil {
return err
}
outline = &u3dOutline{index: make(map[[2]u3dVector]int)}
defer func() { r.remaining += 1024 + uint64(len(outline.edges))*1024 }()
}
if r.mode == U3DRenderShadedWireframe || r.mode == U3DRenderShadedVertices {
if err := r.reserve(1, 1024); err != nil {
return err
}
r.primitives = make(map[u3dPrimitiveKey]struct{})
defer func() {
r.remaining += 1024 + uint64(len(r.primitives))*512
r.primitives = nil
}()
}
normalCount := 0
if shaded {
normalCount = len(mesh.Normals)
}
count := uint64(len(mesh.Positions)) + uint64(normalCount)
if err := r.reserve(count, 24); err != nil { if err := r.reserve(count, 24); err != nil {
return err return err
} }
defer func() { r.remaining += count * 24 }() defer func() { r.remaining += count * 24 }()
positions := make([]u3dVector, len(mesh.Positions)) positions := make([]u3dVector, len(mesh.Positions))
normals := make([]u3dVector, len(mesh.Normals)) normals := make([]u3dVector, normalCount)
inverse, invertible := instance.world.inverse() inverse, invertible := instance.world.inverse()
for i, p := range mesh.Positions { for i, p := range mesh.Positions {
if i%1024 == 0 { if i%1024 == 0 {
@@ -51,7 +80,7 @@ func (r *u3dRender) drawMesh(instance u3dInstance, node *U3DNode, mesh *U3DMesh,
} }
} }
} }
for i, n := range mesh.Normals { for i, n := range mesh.Normals[:normalCount] {
if i%1024 == 0 { if i%1024 == 0 {
if err := r.ctx.Err(); err != nil { if err := r.ctx.Err(); err != nil {
return err return err
@@ -66,6 +95,9 @@ func (r *u3dRender) drawMesh(instance u3dInstance, node *U3DNode, mesh *U3DMesh,
normals[i] = inverse.normal(u3dVector{float64(n[0]), float64(n[1]), float64(n[2])}) normals[i] = inverse.normal(u3dVector{float64(n[0]), float64(n[1]), float64(n[2])})
} }
} }
if err := r.drawLines(instance, node, mesh, positions, normals, pass, passIndex); err != nil {
return err
}
for index, face := range mesh.Faces { for index, face := range mesh.Faces {
if index%128 == 0 { if index%128 == 0 {
if err := r.ctx.Err(); err != nil { if err := r.ctx.Err(); err != nil {
@@ -82,7 +114,7 @@ func (r *u3dRender) drawMesh(instance u3dInstance, node *U3DNode, mesh *U3DMesh,
var world [3]u3dVector var world [3]u3dVector
var vertices [3]u3dVertex var vertices [3]u3dVertex
for i, corner := range face.Corners { for i, corner := range face.Corners {
if uint64(corner.Position) >= uint64(len(positions)) || !mesh.ExcludeNormals && uint64(corner.Normal) >= uint64(len(normals)) || shading.Attributes&1 != 0 && uint64(corner.Diffuse) >= uint64(len(mesh.Diffuse)) || shading.Attributes&2 != 0 && uint64(corner.Specular) >= uint64(len(mesh.Specular)) { if uint64(corner.Position) >= uint64(len(positions)) || shaded && (!mesh.ExcludeNormals && uint64(corner.Normal) >= uint64(len(normals)) || shading.Attributes&1 != 0 && uint64(corner.Diffuse) >= uint64(len(mesh.Diffuse)) || shading.Attributes&2 != 0 && uint64(corner.Specular) >= uint64(len(mesh.Specular))) {
return fmt.Errorf("invalid U3D render corner index") return fmt.Errorf("invalid U3D render corner index")
} }
world[i] = positions[corner.Position] world[i] = positions[corner.Position]
@@ -94,7 +126,7 @@ func (r *u3dRender) drawMesh(instance u3dInstance, node *U3DNode, mesh *U3DMesh,
} }
} }
var faceNormal u3dVector var faceNormal u3dVector
if mesh.ExcludeNormals || !invertible { if shaded && (mesh.ExcludeNormals || !invertible) {
faceNormal = u3dTriangleNormal(world) faceNormal = u3dTriangleNormal(world)
} }
p0, p1, p2 := vertices[0].point, vertices[1].point, vertices[2].point p0, p1, p2 := vertices[0].point, vertices[1].point, vertices[2].point
@@ -107,30 +139,59 @@ func (r *u3dRender) drawMesh(instance u3dInstance, node *U3DNode, mesh *U3DMesh,
} }
front := facing < 0 front := facing < 0
visibility := instance.modelVisibility(node) visibility := instance.modelVisibility(node)
if facing == 0 || front && visibility&1 == 0 || !front && visibility&2 == 0 { surfaceVisible := facing != 0 && (front && visibility&1 != 0 || !front && visibility&2 != 0)
if r.mode == U3DRenderHiddenWireframe && !front || (r.mode == U3DRenderSolid || r.mode == U3DRenderSolidWireframe || r.depthOnly) && (facing == 0 || front && visibility&1 == 0 || !front && visibility&2 == 0) {
continue continue
} }
shaderNames := []string{""} shaderNames := []string{""}
if uint64(face.Shading) < uint64(len(node.Shaders)) { if uint64(face.Shading) < uint64(len(node.Shaders)) {
shaderNames = node.Shaders[face.Shading] shaderNames = node.Shaders[face.Shading]
} }
if outline != nil {
if err := r.outlineFace(outline, world, vertices, front, surfaceVisible, shaderNames, passIndex); err != nil {
return err
}
if !surfaceVisible {
continue
}
}
for shaderIndex, name := range shaderNames { for shaderIndex, name := range shaderNames {
if shaderIndex%128 == 0 { if shaderIndex%128 == 0 {
if err := r.ctx.Err(); err != nil { if err := r.ctx.Err(); err != nil {
return err return err
} }
} }
style, err := r.style(name) style := u3dRenderStyle{shader: U3DShader{Blend: 0x606, RenderPass: math.MaxUint32}}
var err error
if shaded {
style, err = r.style(name)
if err != nil { if err != nil {
return err return err
} }
} else if index, ok := r.shaders[name]; ok {
style.shader.RenderPass = r.model.Shaders[index].RenderPass
}
if style.shader.RenderPass&(1<<passIndex) == 0 { if style.shader.RenderPass&(1<<passIndex) == 0 {
continue continue
} }
if instance.opacity >= 0 { if instance.opacity >= 0 || r.opacity >= 0 {
style.shader.Blend = 0x606 style.shader.Blend = 0x606
} }
for i, corner := range face.Corners { for i, corner := range face.Corners {
if !shaded {
vertices[i].color = [4]float64{r.auxiliaryColor[0], r.auxiliaryColor[1], r.auxiliaryColor[2], 1}
if r.mode == U3DRenderIllustration {
alpha := 1.0
if instance.opacity >= 0 {
alpha = instance.opacity
}
if r.opacity >= 0 {
alpha *= r.opacity
}
vertices[i].color = [4]float64{r.faceColor[0], r.faceColor[1], r.faceColor[2], alpha}
}
continue
}
normal := faceNormal normal := faceNormal
if !mesh.ExcludeNormals && invertible { if !mesh.ExcludeNormals && invertible {
normal = normals[corner.Normal] normal = normals[corner.Normal]
@@ -154,13 +215,19 @@ func (r *u3dRender) drawMesh(instance u3dInstance, node *U3DNode, mesh *U3DMesh,
if instance.opacity >= 0 { if instance.opacity >= 0 {
vertices[i].color[3] = instance.opacity vertices[i].color[3] = instance.opacity
} }
if r.opacity >= 0 {
vertices[i].color[3] *= r.opacity
}
} }
r.order++ r.order++
if err := r.sectionTriangle(vertices, style.shader, pass); err != nil { if err := r.modeTriangle(vertices, style.shader, pass); err != nil {
return err return err
} }
} }
} }
if outline != nil {
return r.drawOutline(outline, pass)
}
return nil return nil
} }
@@ -168,6 +235,9 @@ func (r *u3dRender) drawMesh(instance u3dInstance, node *U3DNode, mesh *U3DMesh,
// 入参: triangle 三角面, shader 着色器, pass 绘制轮次 // 入参: triangle 三角面, shader 着色器, pass 绘制轮次
// 返回: error 数值、资源或取消错误 // 返回: error 数值、资源或取消错误
func (r *u3dRender) clipTriangle(triangle [3]u3dVertex, shader U3DShader, pass U3DViewPass) error { func (r *u3dRender) clipTriangle(triangle [3]u3dVertex, shader U3DShader, pass U3DViewPass) error {
if shader.Blend == 0x606 && triangle[0].color[3] == 0 && triangle[1].color[3] == 0 && triangle[2].color[3] == 0 {
return r.ctx.Err()
}
var storage [2][12]u3dVertex var storage [2][12]u3dVertex
copy(storage[0][:], triangle[:]) copy(storage[0][:], triangle[:])
size, current := 3, 0 size, current := 3, 0
@@ -306,39 +376,28 @@ func (r *u3dRender) rasterTriangle(triangle [3]u3dVertex, shader U3DShader, pass
if depth > r.frame[pixel].depth { if depth > r.frame[pixel].depth {
continue continue
} }
if r.depthOnly {
r.frame[pixel].depth = depth
continue
}
var color [4]float64 var color [4]float64
for c := range color { for c := range color {
color[c] = triangle[0].color[c] + w1*(triangle[1].color[c]-triangle[0].color[c]) + w2*(triangle[2].color[c]-triangle[0].color[c]) color[c] = triangle[0].color[c] + w1*(triangle[1].color[c]-triangle[0].color[c]) + w2*(triangle[2].color[c]-triangle[0].color[c])
if !finite(color[c]) {
return fmt.Errorf("invalid U3D interpolated color")
}
}
color[3] = min(1, max(0, color[3]))
if shader.Attributes&2 != 0 && !u3dAlphaTest(shader.AlphaFunction, color[3], float64(shader.AlphaReference)) {
continue
} }
position := u3dVector{}
if pass.Attributes&1 != 0 { if pass.Attributes&1 != 0 {
position := u3dVector{(px - r.viewport[0] - r.viewport[2]/2 - r.camera.ShiftX) / r.camera.ScaleX, (r.viewport[1] + r.viewport[3]/2 + r.camera.ShiftY - py) / r.camera.ScaleY, depth} position = u3dVector{(px - r.viewport[0] - r.viewport[2]/2 - r.camera.ShiftX) / r.camera.ScaleX, (r.viewport[1] + r.viewport[3]/2 + r.camera.ShiftY - py) / r.camera.ScaleY, depth}
if r.camera.Perspective { if r.camera.Perspective {
position[0] *= depth position[0] *= depth
position[1] *= depth position[1] *= depth
} }
distance := math.Hypot(math.Hypot(position[0], position[1]), position[2])
factor := (float64(pass.FogFar) - distance) / (float64(pass.FogFar) - float64(pass.FogNear))
if pass.FogMode != 0 {
amount := distance * math.Log(100) / float64(pass.FogFar)
if pass.FogMode == 2 {
amount *= amount
} }
factor = math.Exp(-amount) visible, err := u3dFragmentColor(&color, position, &shader, &pass)
if err != nil {
return err
} }
factor = min(1, max(0, factor)) if !visible {
for c := range 3 { continue
color[c] = color[c]*factor + float64(pass.FogColor[c])*(1-factor)
}
}
for c := range 3 {
color[c] = min(1, max(0, color[c]))
} }
if err := r.fragment(pixel, depth, color, shader.Blend); err != nil { if err := r.fragment(pixel, depth, color, shader.Blend); err != nil {
return err return err
+1
View File
@@ -448,6 +448,7 @@ func (e *Editor) usedResources() (fonts, images []editorResource, spaces []Color
promoted := make(map[string]bool) promoted := make(map[string]bool)
files := make(map[string]bool) files := make(map[string]bool)
if e.source != nil { if e.source != nil {
collectActionReferences(e.source.document.Actions, used)
for _, name := range e.annotationFiles() { for _, name := range e.annotationFiles() {
data, changed := e.source.reader.files[name] data, changed := e.source.reader.files[name]
if !changed { if !changed {