feat(渲染能力): 完善三维渲染、字体及版本保存
build.yaml / build (push) Successful in 2m39s

This commit is contained in:
xiaoqidun committed 2026-10-11 10:20:14 +08:00
1 parent 13dbacad15
commit f71000befd
45 files changed
+4365 -404

No files matched your search

+2 -2
View File
@@ -7,9 +7,9 @@ require (
github.com/emmansun/gmsm v0.45.0 github.com/emmansun/gmsm v0.45.0
github.com/go-text/typesetting v0.3.5 github.com/go-text/typesetting v0.3.5
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
github.com/xiaoqidun/j2kgo v0.0.0-20261009063910-4865a21ef893 github.com/xiaoqidun/j2kgo v0.0.0-20261011021706-cedb3037ff6e
github.com/xiaoqidun/jbig2 v1.1.4 github.com/xiaoqidun/jbig2 v1.1.4
github.com/xiaoqidun/pdfgo v0.0.0-20261010065159-b32618b0bdd7 github.com/xiaoqidun/pdfgo v0.0.0-20261011021850-35fe3f96d36f
golang.org/x/crypto v0.58.0 golang.org/x/crypto v0.58.0
golang.org/x/image v0.47.0 golang.org/x/image v0.47.0
golang.org/x/text v0.43.0 golang.org/x/text v0.43.0
+4 -4
View File
@@ -58,12 +58,12 @@ github.com/tdewolff/parse/v2 v2.8.16 h1:bLk5svUOQRkW/Y2SJ+DeENSIkZBcTIkq+Atyv5D8
github.com/tdewolff/parse/v2 v2.8.16/go.mod h1:XdsoSFThlVIRIajAuqz1evNY7bagZS8LBOPA3aVopwQ= github.com/tdewolff/parse/v2 v2.8.16/go.mod h1:XdsoSFThlVIRIajAuqz1evNY7bagZS8LBOPA3aVopwQ=
github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk= 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/j2kgo v0.0.0-20261009063910-4865a21ef893 h1:jmQlBO7R+bcimOMs9jjyDWR1FqiPNGtZqA/V1WiYfPA= github.com/xiaoqidun/j2kgo v0.0.0-20261011021706-cedb3037ff6e h1:OuRcvG5g5AieBN3fNKo130Iu3f+6/ihiWSe8//QT1qg=
github.com/xiaoqidun/j2kgo v0.0.0-20261009063910-4865a21ef893/go.mod h1:DCwAfuDzCDYqUiAlZ7BDwigTjaXw7o9L1FtEma/D1aA= github.com/xiaoqidun/j2kgo v0.0.0-20261011021706-cedb3037ff6e/go.mod h1:DCwAfuDzCDYqUiAlZ7BDwigTjaXw7o9L1FtEma/D1aA=
github.com/xiaoqidun/jbig2 v1.1.4 h1:LF6J1si4JY+TszTh9+m4KAg9/ba0JjfXoGtQTQC2S8c= github.com/xiaoqidun/jbig2 v1.1.4 h1:LF6J1si4JY+TszTh9+m4KAg9/ba0JjfXoGtQTQC2S8c=
github.com/xiaoqidun/jbig2 v1.1.4/go.mod h1:9jromqVwMqlptUDEt6Yip8vqYA5I1YjhG7FgMl/Z2dA= github.com/xiaoqidun/jbig2 v1.1.4/go.mod h1:9jromqVwMqlptUDEt6Yip8vqYA5I1YjhG7FgMl/Z2dA=
github.com/xiaoqidun/pdfgo v0.0.0-20261010065159-b32618b0bdd7 h1:VFrEkWzhLLgoMZ4FNH/uN/Mdfo/TSGKzIPjqCL4m3J4= github.com/xiaoqidun/pdfgo v0.0.0-20261011021850-35fe3f96d36f h1:m3yM1CzrHWm+LW84WWcA2Kr1Ux8K/Znrgd0XVxGEtIw=
github.com/xiaoqidun/pdfgo v0.0.0-20261010065159-b32618b0bdd7/go.mod h1:fXz1emtF1YCpcIv+fglDZstjkHxjS2l3G4DVMgUzVnk= github.com/xiaoqidun/pdfgo v0.0.0-20261011021850-35fe3f96d36f/go.mod h1:hkZn8gHOI94K0fE2ynbXaE8J5T0/jCvoTS/yroKgb+E=
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=
+37 -6
View File
@@ -151,6 +151,30 @@ func (u *editorFontUsage) merge(other *editorFontUsage, unsafe bool) {
} }
} }
// editorObjectFontScan 判断对象是否需要扫描嵌套文字或复合资源
// 入参: object 图形对象
// 返回: bool 是否需要完整引用扫描
func editorObjectFontScan(object GraphicObject) bool {
var clips *Clips
var fill, stroke *FillColor
switch object.Type {
case "CompositeObject", "CompositeGraphicUnit":
return true
case "TextObject", "Text":
clips = object.TextObject.Clips
fill, stroke = object.TextObject.FillColor, (*FillColor)(object.TextObject.StrokeColor)
case "PathObject", "Path":
clips = object.PathObject.Clips
fill, stroke = object.PathObject.FillColor, (*FillColor)(object.PathObject.StrokeColor)
case "ImageObject":
clips = object.ImageObject.Clips
if object.ImageObject.Border != nil {
stroke = (*FillColor)(object.ImageObject.Border.BorderColor)
}
}
return clips != nil || fill != nil && fill.Pattern != nil || stroke != nil && stroke.Pattern != nil
}
// subsetFonts 收集新增字体实际使用的字形,仅裁剪保存结果,保留完整编辑字体 // subsetFonts 收集新增字体实际使用的字形,仅裁剪保存结果,保留完整编辑字体
// 入参: progress 保存进度回调 // 入参: progress 保存进度回调
// 返回: map[string][]byte 包内字体子集, error 错误信息 // 返回: map[string][]byte 包内字体子集, error 错误信息
@@ -181,11 +205,17 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
return nil, progress.report("fonts", 0, 0) return nil, progress.report("fonts", 0, 0)
} }
refs := editorResourceRefs{ids: make(map[string]bool), files: make(map[string]bool)} refs := editorResourceRefs{ids: make(map[string]bool), files: make(map[string]bool)}
composite := false nested := false
for _, resource := range e.resources {
if resource.draw != nil {
nested = true
break
}
}
if e.source != nil { if e.source != nil {
for _, name := range e.annotationFiles() { for _, name := range e.annotationFiles() {
if data, changed := e.source.reader.files[name]; changed { if data, changed := e.source.reader.files[name]; changed {
composite = true nested = true
if _, safe := refs.scan(bytes.NewReader(data), name); !safe { if _, safe := refs.scan(bytes.NewReader(data), name); !safe {
return nil, nil return nil, nil
} }
@@ -199,8 +229,8 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
} }
for _, layer := range page.Content.Layer { for _, layer := range page.Content.Layer {
for _, object := range layer.Objects { for _, object := range layer.Objects {
if object.Type == "CompositeObject" || object.Type == "CompositeGraphicUnit" { if editorObjectFontScan(object) {
composite = true nested = true
origin := e.objectOrigin(editorObjectID(object)) origin := e.objectOrigin(editorObjectID(object))
var data []byte var data []byte
var err error var err error
@@ -218,6 +248,7 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
if _, safe := refs.scan(bytes.NewReader(data), "Page.xml"); !safe { if _, safe := refs.scan(bytes.NewReader(data), "Page.xml"); !safe {
return nil, nil return nil, nil
} }
continue
} }
if object.Type != "TextObject" { if object.Type != "TextObject" {
continue continue
@@ -246,10 +277,10 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
} }
} }
} }
if composite { if nested {
_, _, _, files := e.usedResources() _, _, _, files := e.usedResources()
for i, resource := range e.resources { for i, resource := range e.resources {
if resource.composite == "" || !slices.Contains(files, resource.name) { if resource.definition() == "" || !slices.Contains(files, resource.name) {
continue continue
} }
if err := progress.report("fonts", len(e.pages)+i, total); err != nil { if err := progress.report("fonts", len(e.pages)+i, total); err != nil {
+23 -1
View File
@@ -16,6 +16,7 @@ package ofdgo
import ( import (
"bytes" "bytes"
"context"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"io" "io"
@@ -42,10 +43,26 @@ type editorXMLPatch struct {
// 入参: data XML原文 // 入参: data XML原文
// 返回: *editorXML 根节点, error 错误信息 // 返回: *editorXML 根节点, error 错误信息
func parseEditorXML(data []byte) (*editorXML, error) { func parseEditorXML(data []byte) (*editorXML, error) {
d := xml.NewDecoder(bytes.NewReader(data)) return parseEditorXMLContext(nil, data)
}
// parseEditorXMLContext 解析XML节点位置并响应取消
// 入参: ctx 取消上下文,可为nil, data XML原文
// 返回: *editorXML 根节点, error 解析或取消错误
func parseEditorXMLContext(ctx context.Context, data []byte) (*editorXML, error) {
var input io.Reader = bytes.NewReader(data)
if ctx != nil {
input = imageInput{ReadCloser: io.NopCloser(input), context: ctx}
}
d := xml.NewDecoder(input)
var root *editorXML var root *editorXML
var stack []*editorXML var stack []*editorXML
for { for {
if ctx != nil {
if err := ctx.Err(); err != nil {
return nil, err
}
}
start := int(d.InputOffset()) start := int(d.InputOffset())
token, err := d.Token() token, err := d.Token()
if err == io.EOF { if err == io.EOF {
@@ -131,7 +148,12 @@ func editorPatchXML(data []byte, patches []editorXMLPatch) []byte {
} }
return a.end - b.end return a.end - b.end
}) })
size := len(data)
for _, patch := range patches {
size += len(patch.data) - (patch.end - patch.start)
}
var result bytes.Buffer var result bytes.Buffer
result.Grow(size)
position := 0 position := 0
for _, patch := range patches { for _, patch := range patches {
result.Write(data[position:patch.start]) result.Write(data[position:patch.start])
+10 -1
View File
@@ -86,8 +86,17 @@ type FontShaperOptions interface {
ShapeTextWithOptions(value string, size float64, options TextShapeOptions) ([]ShapedGlyph, error) ShapeTextWithOptions(value string, size float64, options TextShapeOptions) ([]ShapedGlyph, error)
} }
// FontShaperRun 保留完整单行原文的上下文,仅塑形指定区间,不执行字体回退
// start和end为原文的符文索引,区间左闭右开,返回Cluster仍使用原文索引
// 调用方负责方向和脚本分段,options.Bidi须为false;坐标相对当前区间的基线起点
// value为已校验的原文符文序列,调用期间不得修改,返回结果不引用该切片
type FontShaperRun interface {
FontShaperOptions
ShapeTextRun(value []rune, start, end int, size float64, options TextShapeOptions) ([]ShapedGlyph, error)
}
// ShapedGlyph 保存塑形后的字形、原文簇和基线定位 // ShapedGlyph 保存塑形后的字形、原文簇和基线定位
// Bidi模式下VisualOrder为簇从左到右的零起始顺序,同簇字形共用顺序 // VisualOrder为簇从左到右的零起始顺序,同簇字形共用顺序
type ShapedGlyph struct { type ShapedGlyph struct {
Glyph uint16 Glyph uint16
Cluster int Cluster int
+3 -109
View File
@@ -18,7 +18,6 @@ import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"maps"
"math" "math"
"github.com/go-text/typesetting/font/cff" "github.com/go-text/typesetting/font/cff"
@@ -172,10 +171,11 @@ func staticCIDFont(table map[string][]byte, chars [][]byte) ([]byte, error) {
} }
names := encodeCFFIndex([][]byte{[]byte(name)}) names := encodeCFFIndex([][]byte{[]byte(name)})
strings := encodeCFFIndex([][]byte{[]byte("Adobe"), []byte("Identity")}) strings := encodeCFFIndex([][]byte{[]byte("Adobe"), []byte("Identity")})
charset := []byte{0} cids := make([]int, len(chars))
for gid := 1; gid < len(chars); gid++ { for gid := 1; gid < len(chars); gid++ {
charset = binary.BigEndian.AppendUint16(charset, uint16(gid)) cids[gid] = gid
} }
charset := encodeCFFSubsetCharset(cids)
selects := []byte{3, 0, 1, 0, 0, 0} selects := []byte{3, 0, 1, 0, 0, 0}
selects = binary.BigEndian.AppendUint16(selects, uint16(len(chars))) selects = binary.BigEndian.AppendUint16(selects, uint16(len(chars)))
fonts := encodeCFFIndex([][]byte{encodeCFFDict(cffDict{18: {0, 0}})}) fonts := encodeCFFIndex([][]byte{encodeCFFDict(cffDict{18: {0, 0}})})
@@ -203,109 +203,3 @@ func staticCIDFont(table map[string][]byte, chars [][]byte) ([]byte, error) {
} }
return nil, fmt.Errorf("CFF offsets did not converge") return nil, fmt.Errorf("CFF offsets did not converge")
} }
// staticCFFPrograms 读取无子程序和私有字典的CID字形,其他CFF保留原有保存策略
// 入参: sfnt 已解析字体
// 返回: [][]byte 独立字形程序,不符合条件时为空
func staticCFFPrograms(sfnt *sfntFont) [][]byte {
data := sfnt.Tables["CFF "]
if len(data) < 4 {
return nil
}
_, end, err := readType2Index(data, int(data[2]))
if err != nil {
return nil
}
tops, end, err := readType2Index(data, end)
if err != nil || len(tops) != 1 {
return nil
}
_, end, err = readType2Index(data, end)
if err != nil {
return nil
}
globals, _, err := readType2Index(data, end)
if err != nil || len(globals) != 0 {
return nil
}
dict, err := readCFFDict(tops[0])
if err != nil || len(dict[1230]) != 3 {
return nil
}
matrix, err := readCFFMatrix(dict, [6]float64{.001, 0, 0, .001, 0, 0})
if err != nil || matrix[0] != matrix[3] || matrix[1] != 0 || matrix[2] != 0 || matrix[4] != 0 || matrix[5] != 0 || math.Abs(matrix[0]*float64(sfnt.UnitsPerEm())-1) > 1e-6 {
return nil
}
offset, err := cffDictOffset(data, dict, 1236)
if err != nil {
return nil
}
fds, _, err := readType2Index(data, offset)
if err != nil || len(fds) != 1 {
return nil
}
fd, err := readCFFDict(fds[0])
if err != nil || len(fd) != 1 || len(fd[18]) != 2 || fd[18][0] != 0 {
return nil
}
offset, err = cffDictOffset(data, dict, 17)
if err != nil {
return nil
}
chars, _, err := readType2Index(data, offset)
if err != nil || len(chars) != int(sfnt.NumGlyphs()) {
return nil
}
return chars
}
// subsetStaticCFFFont 裁剪静态CID字形并保留原编号、字符映射和度量
// 入参: sfnt 已解析字体, glyphs 使用的字形
// 返回: []byte 字体子集, error 裁剪错误
func subsetStaticCFFFont(sfnt *sfntFont, glyphs []uint16) ([]byte, error) {
chars := staticCFFPrograms(sfnt)
if chars == nil {
return nil, fmt.Errorf("CFF font cannot be subset safely")
}
keep := make(map[uint16]bool, len(glyphs)+1)
keep[0] = true
for _, gid := range glyphs {
if int(gid) >= len(chars) {
return nil, fmt.Errorf("CFF glyph index out of range")
}
keep[gid] = true
}
empty := make(map[uint16][]byte)
for gid := range chars {
if !keep[uint16(gid)] {
width := sfnt.GlyphAdvance(uint16(gid))
program := empty[width]
if program == nil {
var err error
program, err = cffOutlineProgram(nil, width)
if err != nil {
return nil, err
}
empty[width] = program
}
chars[gid] = program
}
}
table := maps.Clone(sfnt.Tables)
var err error
table["CFF "], err = staticCIDFont(table, chars)
if err != nil {
return nil, err
}
mapping := make(map[rune]uint16)
for gid := range keep {
for _, char := range sfnt.glyphCharacters(gid) {
mapping[char] = gid
}
}
table["cmap"] = buildCmapTable(sfnt.NumGlyphs(), mapping)
for _, tag := range []string{"DSIG", "GSUB", "GPOS", "GDEF"} {
delete(table, tag)
}
return serializeOTF(table)
}
+239
View File
@@ -0,0 +1,239 @@
// 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"
"fmt"
"math"
)
// cidSubsetFont 保存CID字体裁剪所需的字典、映射和执行资源
type cidSubsetFont struct {
prefix []byte
strings [][]byte
top cffDict
dicts []cffDict
private []cffDict
fonts []*type2Font
selects []int
charset []byte
}
// readCIDSubsetFont 读取CID字体的原始字典及逐字形归属,不合并字体字典
// 入参: data CFF数据
// 返回: *cidSubsetFont 裁剪资源, error 结构错误或不支持的字典
func readCIDSubsetFont(data []byte) (*cidSubsetFont, error) {
if !isBareCFFData(data) {
return nil, fmt.Errorf("invalid CFF header")
}
names, end, err := readType2Index(data, int(data[2]))
if err != nil || len(names) != 1 {
return nil, fmt.Errorf("invalid CFF name index")
}
tops, end, err := readType2Index(data, end)
if err != nil || len(tops) != 1 {
return nil, fmt.Errorf("invalid CFF top index")
}
strings, end, err := readType2Index(data, end)
if err != nil {
return nil, err
}
globals, _, err := readType2Index(data, end)
if err != nil {
return nil, err
}
top, err := readCFFDict(tops[0])
if err != nil {
return nil, err
}
ros := top[1230]
if len(ros) != 3 {
return nil, fmt.Errorf("invalid CFF ROS")
}
for index, value := range ros {
limit := float64(math.MaxInt32)
if index < 2 {
limit = float64(min(64999, 390+len(strings)))
}
if !finite(value) || value < 0 || value > limit || value != math.Trunc(value) {
return nil, fmt.Errorf("invalid CFF ROS operand")
}
}
for op := range top {
switch op {
case 0, 1, 2, 3, 4, 5, 13, 14, 15, 17, 1200, 1201, 1202, 1203, 1204, 1205, 1206, 1207, 1208, 1230, 1231, 1232, 1233, 1234, 1236, 1237:
default:
return nil, fmt.Errorf("unsupported CID CFF top operator %d", op)
}
}
if value, ok := top[1206]; ok && (len(value) != 1 || value[0] != 2) {
return nil, fmt.Errorf("unsupported CFF charstring type")
}
if _, err := readCFFMatrix(top, [6]float64{.001, 0, 0, .001, 0, 0}); err != nil {
return nil, err
}
charOffset, err := cffDictOffset(data, top, 17)
if err != nil {
return nil, err
}
chars, _, err := readType2Index(data, charOffset)
if err != nil || len(chars) == 0 {
return nil, fmt.Errorf("invalid CFF charstrings")
}
charsetOffset, err := cffDictOffset(data, top, 15)
if err != nil || charsetOffset <= 2 {
return nil, fmt.Errorf("invalid CID CFF charset offset")
}
cids, err := parseCFFCharset(data, charsetOffset, len(chars))
if err != nil {
return nil, err
}
fdOffset, err := cffDictOffset(data, top, 1236)
if err != nil {
return nil, err
}
fds, _, err := readType2Index(data, fdOffset)
if err != nil || len(fds) == 0 || len(fds) > 256 {
return nil, fmt.Errorf("invalid CFF font dictionary count")
}
selectOffset, err := cffDictOffset(data, top, 1237)
if err != nil {
return nil, err
}
selects, err := readType2FDSelect(data, selectOffset, len(chars), len(fds))
if err != nil {
return nil, err
}
if data[selectOffset] == 3 && binary.BigEndian.Uint16(data[selectOffset+3:]) != 0 {
return nil, fmt.Errorf("CFF FDSelect must include glyph zero")
}
font := &cidSubsetFont{
prefix: append(bytes.Clone(data[:int(data[2])]), encodeCFFIndex(names)...),
strings: strings, top: top, selects: selects, charset: encodeCFFSubsetCharset(cids),
dicts: make([]cffDict, len(fds)), private: make([]cffDict, len(fds)), fonts: make([]*type2Font, len(fds)),
}
for index, fd := range fds {
dict, err := readCFFDict(fd)
if err != nil {
return nil, err
}
for op := range dict {
switch op {
case 18, 1207, 1238:
default:
return nil, fmt.Errorf("unsupported CFF font dictionary operator %d", op)
}
}
if _, err := readCFFMatrix(dict, [6]float64{1, 0, 0, 1, 0, 0}); err != nil {
return nil, err
}
if len(dict[18]) != 2 {
return nil, fmt.Errorf("missing CFF private dictionary")
}
resource := &type2Font{data: data, chars: chars, globals: globals, seed: 1, cid: true}
private, err := readType2Private(data, dict[18], resource)
if err != nil {
return nil, err
}
if _, err := encodeCFFSubsetPrivate(private); err != nil {
return nil, err
}
font.dicts[index], font.private[index], font.fonts[index] = dict, private, resource
}
return font, nil
}
// write 重建CID字体偏移,保留CID映射、字体矩阵及各字典的私有参数
// 入参: chars 裁剪后的字形程序
// 返回: []byte CFF数据, error 偏移或字典错误
func (f *cidSubsetFont) write(chars [][]byte) ([]byte, error) {
suffix := append(encodeCFFIndex(f.strings), encodeCFFIndex(f.fonts[0].globals)...)
selects := encodeCFFSubsetFDSelect(f.selects)
programs := encodeCFFIndex(chars)
private := make([][]byte, len(f.fonts))
locals := make([][]byte, len(f.fonts))
fds := make([][]byte, len(f.fonts))
for index, font := range f.fonts {
var err error
private[index], err = encodeCFFSubsetPrivate(f.private[index])
if err != nil {
return nil, err
}
if _, ok := f.private[index][19]; ok {
locals[index] = encodeCFFIndex(font.locals)
}
fds[index] = encodeCFFDict(f.dicts[index])
}
for range 16 {
top := encodeCFFIndex([][]byte{encodeCFFDict(f.top)})
array := encodeCFFIndex(fds)
offset := len(f.prefix) + len(top) + len(suffix)
f.top[15] = []float64{float64(offset)}
offset += len(f.charset)
f.top[1237] = []float64{float64(offset)}
offset += len(selects)
f.top[17] = []float64{float64(offset)}
offset += len(programs)
f.top[1236] = []float64{float64(offset)}
offset += len(array)
for index := range f.fonts {
f.dicts[index][18] = []float64{float64(len(private[index])), float64(offset)}
fds[index] = encodeCFFDict(f.dicts[index])
offset += len(private[index]) + len(locals[index])
}
updatedTop := encodeCFFIndex([][]byte{encodeCFFDict(f.top)})
updatedArray := encodeCFFIndex(fds)
if len(updatedTop) != len(top) || len(updatedArray) != len(array) {
continue
}
result := make([]byte, 0, offset)
for _, part := range [][]byte{f.prefix, updatedTop, suffix, f.charset, selects, programs, updatedArray} {
result = append(result, part...)
}
for index := range f.fonts {
result = append(result, private[index]...)
result = append(result, locals[index]...)
}
return result, nil
}
return nil, fmt.Errorf("CID CFF offsets did not converge")
}
// encodeCFFSubsetFDSelect 选择较短的逐字形或分段格式保存字体字典归属
// 入参: selects 字形所属字典
// 返回: []byte FDSelect数据
func encodeCFFSubsetFDSelect(selects []int) []byte {
ranges := []byte{3, 0, 0}
count := 0
for gid, fd := range selects {
if gid == 0 || fd != selects[gid-1] {
ranges = binary.BigEndian.AppendUint16(ranges, uint16(gid))
ranges = append(ranges, byte(fd))
count++
}
}
binary.BigEndian.PutUint16(ranges[1:], uint16(count))
ranges = binary.BigEndian.AppendUint16(ranges, uint16(len(selects)))
if len(ranges) < len(selects)+1 {
return ranges
}
result := make([]byte, len(selects)+1)
for gid, fd := range selects {
result[gid+1] = byte(fd)
}
return result
}
+339
View File
@@ -0,0 +1,339 @@
// 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"
"fmt"
"slices"
)
// compactCFFGlyphs 重排已裁剪字体的字形,同步字符集、编码和字体字典归属
// 入参: data 已裁剪的CFF数据, keep 保留的原字形编号
// 返回: []byte 紧凑CFF数据, []uint16 新编号对应的原编号, error 结构或编码错误
func compactCFFGlyphs(data []byte, keep map[uint16]bool) ([]byte, []uint16, error) {
ids := []uint16{0}
for gid, used := range keep {
if used && gid != 0 {
ids = append(ids, gid)
}
}
slices.Sort(ids)
font, err := readType2Font(data)
if err == nil {
sids, _, _, _, ok := getCFFCharsetInfo(data, len(font.chars))
if !ok {
return nil, nil, fmt.Errorf("invalid CFF charset")
}
_, end, _ := readType2Index(data, int(data[2]))
tops, _, _ := readType2Index(data, end)
dict, _ := readCFFDict(tops[0])
encoding, err := cffSubsetEncoding(data, dict)
if err != nil {
return nil, nil, err
}
if encoding != nil {
codes, err := readCFFSubsetEncoding(encoding, sids)
if err != nil {
return nil, nil, err
}
ordered := []uint16{0}
seen := map[uint16]bool{0: true}
for _, gid := range codes {
if keep[gid] && !seen[gid] {
ordered = append(ordered, gid)
seen[gid] = true
}
}
for _, gid := range ids {
if !seen[gid] {
ordered = append(ordered, gid)
}
}
ids = ordered
}
chars := make([][]byte, len(ids))
for index, gid := range ids {
if int(gid) >= len(font.chars) {
return nil, nil, fmt.Errorf("CFF glyph index out of range")
}
chars[index] = font.chars[gid]
}
result, err := rebuildNamedCFF(data, font, chars, ids)
return result, ids, err
}
cid, err := readCIDSubsetFont(data)
if err != nil {
return nil, nil, err
}
cids, _, _, _, ok := getCFFCharsetInfo(data, len(cid.selects))
if !ok {
return nil, nil, fmt.Errorf("invalid CID CFF charset")
}
chars := make([][]byte, len(ids))
selected := make([]int, len(ids))
selects := make([]int, len(ids))
dicts, private, fonts := []cffDict{}, []cffDict{}, []*type2Font{}
remap := make(map[int]int)
for index, gid := range ids {
if int(gid) >= len(cid.selects) {
return nil, nil, fmt.Errorf("CFF glyph index out of range")
}
fd := cid.selects[gid]
mapped, exists := remap[fd]
if !exists {
mapped = len(fonts)
remap[fd] = mapped
dicts = append(dicts, cid.dicts[fd])
private = append(private, cid.private[fd])
fonts = append(fonts, cid.fonts[fd])
}
chars[index], selected[index], selects[index] = cid.fonts[fd].chars[gid], cids[gid], mapped
}
cid.dicts, cid.private, cid.fonts = dicts, private, fonts
cid.charset, cid.selects = encodeCFFSubsetCharset(selected), selects
result, err := cid.write(chars)
return result, ids, err
}
// compactCFFTables 重建按字形编号索引的度量和字符映射,不保留旧编号的布局表
// 入参: font 原字体, cff 紧凑CFF数据, ids 新编号对应的原编号
// 返回: []byte 独立OpenType字体, error 封装错误
func compactCFFTables(font *sfntFont, cff []byte, ids []uint16) ([]byte, error) {
tables := make(map[string][]byte)
for _, tag := range []string{"head", "hhea", "maxp", "OS/2", "post", "name", "vhea", "gasp"} {
if data := font.Tables[tag]; data != nil {
tables[tag] = bytes.Clone(data)
}
}
var hmtx, vmtx []byte
mapping := make(map[rune]uint16)
for index, gid := range ids {
hmtx = binary.BigEndian.AppendUint16(hmtx, font.GlyphAdvance(gid))
hmtx = binary.BigEndian.AppendUint16(hmtx, uint16(font.sideBearing(gid, false)))
if font.vertical != 0 {
vmtx = binary.BigEndian.AppendUint16(vmtx, font.GlyphVerticalAdvance(gid))
vmtx = binary.BigEndian.AppendUint16(vmtx, uint16(font.sideBearing(gid, true)))
}
for _, char := range font.glyphCharacters(gid) {
mapping[char] = uint16(index)
}
}
tables["CFF "], tables["hmtx"] = cff, hmtx
tables["cmap"] = buildCmapTable(uint16(len(ids)), mapping)
binary.BigEndian.PutUint16(tables["hhea"][34:], uint16(len(ids)))
binary.BigEndian.PutUint16(tables["maxp"][4:], uint16(len(ids)))
if len(tables["post"]) >= 32 {
tables["post"] = tables["post"][:32]
binary.BigEndian.PutUint32(tables["post"], 0x30000)
}
if font.vertical != 0 {
tables["vmtx"] = vmtx
binary.BigEndian.PutUint16(tables["vhea"][34:], uint16(len(ids)))
}
if data := font.Tables["VORG"]; data != nil {
var err error
tables["VORG"], err = compactCFFVerticalOrigins(data, font.NumGlyphs(), ids)
if err != nil {
return nil, err
}
}
return serializeOTF(tables)
}
// compactCFFVerticalOrigins 重排CFF字体的逐字形纵向原点,保留默认原点
// 入参: data VORG表, count 原字形数, ids 新编号对应的原编号
// 返回: []byte 重建的VORG表, error 表结构或记录顺序错误
func compactCFFVerticalOrigins(data []byte, count uint16, ids []uint16) ([]byte, error) {
if len(data) < 8 || binary.BigEndian.Uint32(data) != 0x10000 {
return nil, fmt.Errorf("invalid CFF vertical origin header")
}
length := int(binary.BigEndian.Uint16(data[6:]))
if length > (len(data)-8)/4 {
return nil, fmt.Errorf("truncated CFF vertical origins")
}
origins := make(map[uint16]uint16, length)
previous := -1
for index := range length {
pos := 8 + index*4
gid := binary.BigEndian.Uint16(data[pos:])
if gid >= count || int(gid) <= previous {
return nil, fmt.Errorf("invalid CFF vertical origin glyph")
}
origins[gid] = binary.BigEndian.Uint16(data[pos+2:])
previous = int(gid)
}
result := bytes.Clone(data[:8])
for index, gid := range ids {
if origin, ok := origins[gid]; ok {
result = binary.BigEndian.AppendUint16(result, uint16(index))
result = binary.BigEndian.AppendUint16(result, origin)
}
}
binary.BigEndian.PutUint16(result[6:], uint16((len(result)-8)/4))
return result, nil
}
// encodeCFFSubsetCharset 选择最短的标准字符集格式,保持SID或CID及字形顺序
// 入参: ids 按字形编号排列的SID或CID,包含.notdef
// 返回: []byte 字符集数据
func encodeCFFSubsetCharset(ids []int) []byte {
sizes := [3]int{1 + 2*(len(ids)-1), 1, 1}
for first := 1; first < len(ids); {
end := first + 1
for end < len(ids) && ids[end] == ids[end-1]+1 {
end++
}
sizes[1] += 3 * ((end - first + 255) / 256)
sizes[2] += 4
first = end
}
format := 0
for index, size := range sizes {
if size < sizes[format] {
format = index
}
}
result := make([]byte, 1, sizes[format])
result[0] = byte(format)
for first := 1; first < len(ids); {
end := first + 1
if format != 0 {
limit := len(ids)
if format == 1 {
limit = min(limit, first+256)
}
for end < limit && ids[end] == ids[end-1]+1 {
end++
}
}
result = binary.BigEndian.AppendUint16(result, uint16(ids[first]))
if format == 1 {
result = append(result, byte(end-first-1))
} else if format == 2 {
result = binary.BigEndian.AppendUint16(result, uint16(end-first-1))
}
first = end
}
return result
}
// readCFFSubsetEncoding 解析自定义编码及补充映射,校验重复编码和字形归属
// 入参: data 编码数据, sids 原字符集
// 返回: [256]uint16 编码对应的字形编号, error 编码结构错误
func readCFFSubsetEncoding(data []byte, sids []int) ([256]uint16, error) {
var codes [256]uint16
if len(data) < 2 {
return codes, fmt.Errorf("truncated CFF encoding")
}
pos, gid := 2, 1
for index := 0; index < int(data[1]); index++ {
if pos >= len(data) {
return codes, fmt.Errorf("truncated CFF encoding")
}
first, count := int(data[pos]), 1
pos++
switch data[0] & 0x7f {
case 0:
case 1:
if pos >= len(data) {
return codes, fmt.Errorf("truncated CFF encoding range")
}
count += int(data[pos])
pos++
default:
return codes, fmt.Errorf("invalid CFF encoding format")
}
if first+count > len(codes) || gid+count > len(sids) {
return codes, fmt.Errorf("CFF encoding range out of bounds")
}
for code := first; code < first+count; code++ {
if codes[code] != 0 {
return codes, fmt.Errorf("duplicate CFF encoding")
}
codes[code] = uint16(gid)
gid++
}
}
if data[0]&0x80 != 0 {
if pos >= len(data) {
return codes, fmt.Errorf("truncated CFF encoding supplements")
}
count := int(data[pos])
pos++
if count > (len(data)-pos)/3 {
return codes, fmt.Errorf("truncated CFF encoding supplements")
}
for range count {
code, sid := data[pos], int(binary.BigEndian.Uint16(data[pos+1:]))
pos += 3
mapped := slices.Index(sids[:gid], sid)
if codes[code] != 0 || mapped <= 0 {
return codes, fmt.Errorf("invalid CFF supplemental glyph")
}
codes[code] = uint16(mapped)
}
}
if data[0]&0x7f > 1 || pos != len(data) {
return codes, fmt.Errorf("invalid CFF encoding length or format")
}
return codes, nil
}
// encodeCFFSubsetEncoding 按新字形顺序重建主编码及同字形的补充编码
// 入参: codes 原编码映射, sids 原字符集, ids 新编号对应的原编号
// 返回: []byte 编码数据, error 字形顺序不满足编码约束
func encodeCFFSubsetEncoding(codes [256]uint16, sids []int, ids []uint16) ([]byte, error) {
var base, supplements []byte
encodings := make(map[uint16][]byte)
for code, gid := range codes {
if gid != 0 {
encodings[gid] = append(encodings[gid], byte(code))
}
}
unencoded := false
for _, gid := range ids[1:] {
values := encodings[gid]
if len(values) == 0 {
unencoded = true
} else if unencoded {
return nil, fmt.Errorf("CFF encoded glyph follows unencoded glyph")
} else {
base = append(base, values[0])
for _, code := range values[1:] {
supplements = append(supplements, code)
supplements = binary.BigEndian.AppendUint16(supplements, uint16(sids[gid]))
}
}
}
var result []byte
if len(base) == 256 {
for index, code := range base {
if int(code) != index {
return nil, fmt.Errorf("CFF encoding exceeds format zero capacity")
}
}
result = []byte{1, 1, 0, 255}
} else {
result = append([]byte{0, byte(len(base))}, base...)
}
if len(supplements) != 0 {
result[0] |= 0x80
result = append(result, byte(len(supplements)/3))
result = append(result, supplements...)
}
return result, nil
}
+115 -39
View File
@@ -16,15 +16,14 @@ package ofdgo
import ( import (
"bytes" "bytes"
"encoding/binary"
"fmt" "fmt"
"maps" "maps"
) )
// subsetNamedCFFFont 裁剪非CID的CFF字体,保留字形编号、组合组件和度量 // subsetCFFFont 裁剪CFF字体,保留组合组件和度量,按需重排字形
// 入参: sfnt 已解析字体, glyphs 使用的字形 // 入参: sfnt 已解析字体, glyphs 使用的字形, preserve 是否保留原字形编号
// 返回: []byte 字体子集, error 裁剪错误 // 返回: []byte 字体子集, error 裁剪错误
func subsetNamedCFFFont(sfnt *sfntFont, glyphs []uint16) ([]byte, error) { func subsetCFFFont(sfnt *sfntFont, glyphs []uint16, preserve bool) ([]byte, error) {
keep := map[uint16]bool{0: true} keep := map[uint16]bool{0: true}
for _, gid := range glyphs { for _, gid := range glyphs {
if gid >= sfnt.NumGlyphs() { if gid >= sfnt.NumGlyphs() {
@@ -36,6 +35,11 @@ func subsetNamedCFFFont(sfnt *sfntFont, glyphs []uint16) ([]byte, error) {
if err != nil { if err != nil {
return bytes.Clone(sfnt.Write()), nil return bytes.Clone(sfnt.Write()), nil
} }
if !preserve {
if compact, ids, err := compactCFFGlyphs(data, keep); err == nil {
return compactCFFTables(sfnt, compact, ids)
}
}
tables := maps.Clone(sfnt.Tables) tables := maps.Clone(sfnt.Tables)
tables["CFF "] = data tables["CFF "] = data
mapping := make(map[rune]uint16) mapping := make(map[rune]uint16)
@@ -52,60 +56,106 @@ func subsetNamedCFFFont(sfnt *sfntFont, glyphs []uint16) ([]byte, error) {
} }
// subsetCFFCharstrings 清空未引用的Type2轮廓及子程序,保留原字宽、提示和编号 // subsetCFFCharstrings 清空未引用的Type2轮廓及子程序,保留原字宽、提示和编号
// 入参: data 非CID的CFF数据, keep 保留的字形,调用后包含组合组件 // 入参: data CFF数据, keep 保留的字形,调用后包含组合组件
// 返回: []byte 重建的CFF数据, error 结构或依赖错误 // 返回: []byte 重建的CFF数据, error 结构或依赖错误
func subsetCFFCharstrings(data []byte, keep map[uint16]bool) ([]byte, error) { func subsetCFFCharstrings(data []byte, keep map[uint16]bool) ([]byte, error) {
font, err := readType2Font(data) font, err := readType2Font(data)
if err != nil {
cid, err := readCIDSubsetFont(data)
if err != nil { if err != nil {
return nil, err return nil, err
} }
chars, err := subsetType2Programs(cid.fonts, cid.selects, keep)
if err != nil {
return nil, err
}
return cid.write(chars)
}
chars, err := subsetType2Programs([]*type2Font{font}, nil, keep)
if err != nil {
return nil, err
}
return rebuildNamedCFF(data, font, chars, nil)
}
// subsetType2Programs 按字形所属字典收集子程序依赖并清空未使用的轮廓
// 入参: fonts 执行资源, selects 字形所属字典,单字典时可为空, keep 保留的字形
// 返回: [][]byte 字形程序, error 执行或字宽错误
func subsetType2Programs(fonts []*type2Font, selects []int, keep map[uint16]bool) ([][]byte, error) {
keep[0] = true keep[0] = true
locals := make([]bool, len(font.locals)) programs := fonts[0].chars
globals := make([]bool, len(font.globals)) locals := make([][]bool, len(fonts))
empty := make([]map[float64][]byte, len(fonts))
for index, font := range fonts {
locals[index] = make([]bool, len(font.locals))
empty[index] = make(map[float64][]byte)
}
globals := make([]bool, len(fonts[0].globals))
var stack [48]float64 var stack [48]float64
for gid := range keep { for gid := range keep {
if int(gid) >= len(font.chars) { if int(gid) >= len(programs) {
return nil, fmt.Errorf("CFF glyph index out of range") return nil, fmt.Errorf("CFF glyph index out of range")
} }
fd := 0
if selects != nil {
fd = selects[gid]
}
font := fonts[fd]
steps := 0 steps := 0
state := type2State{font: font, args: stack[:0], width: font.def, seed: font.seed + uint64(gid), steps: &steps, dependencies: keep, localUsage: locals, globalUsage: globals} state := type2State{font: font, args: stack[:0], width: font.def, seed: font.seed + uint64(gid), steps: &steps, dependencies: keep, localUsage: locals[fd], globalUsage: globals}
if _, err := state.run(font.chars[gid], 0); err != nil { if _, err := state.run(font.chars[gid], 0); err != nil {
return nil, err return nil, err
} }
} }
chars := make([][]byte, len(font.chars)) chars := make([][]byte, len(programs))
empty := make(map[float64][]byte) for gid, program := range programs {
for gid, program := range font.chars {
if keep[uint16(gid)] { if keep[uint16(gid)] {
chars[gid] = program chars[gid] = program
continue continue
} }
fd := 0
if selects != nil {
fd = selects[gid]
}
font := fonts[fd]
steps := 0 steps := 0
state := type2State{font: font, args: stack[:0], width: font.def, widthOnly: true, seed: font.seed + uint64(gid), steps: &steps, dependencies: keep} state := type2State{font: font, args: stack[:0], width: font.def, widthOnly: true, seed: font.seed + uint64(gid), steps: &steps, dependencies: keep}
if _, err := state.run(program, 0); err != nil { if _, err := state.run(program, 0); err != nil {
return nil, err return nil, err
} }
if encoded, ok := empty[state.width]; ok { if encoded, ok := empty[fd][state.width]; ok {
chars[gid] = encoded chars[gid] = encoded
continue continue
} }
var encoded bytes.Buffer var encoded bytes.Buffer
if state.width != font.def { if state.width != font.def {
value := state.width - font.nominal value := state.width - font.nominal
if err := encodeType2Number(&encoded, value); err != nil { divisor := 1
if value < -32768 || value >= 32768 {
divisor = 2
}
for range divisor {
if err := encodeType2Number(&encoded, value/float64(divisor)); err != nil {
return nil, err return nil, err
} }
if parseNumberType2(encoded.Bytes(), 0) != value { }
if parseNumberType2(encoded.Bytes(), 0)*float64(divisor) != value {
return nil, fmt.Errorf("CFF width cannot be represented exactly") return nil, fmt.Errorf("CFF width cannot be represented exactly")
} }
if divisor == 2 {
encoded.Write([]byte{12, 10})
}
} }
encoded.WriteByte(14) encoded.WriteByte(14)
chars[gid] = encoded.Bytes() chars[gid] = encoded.Bytes()
empty[state.width] = chars[gid] empty[fd][state.width] = chars[gid]
} }
font.locals = subsetCFFSubroutines(font.locals, locals) globalPrograms := subsetCFFSubroutines(fonts[0].globals, globals)
font.globals = subsetCFFSubroutines(font.globals, globals) for index, font := range fonts {
return rebuildNamedCFF(data, font, chars) font.locals = subsetCFFSubroutines(font.locals, locals[index])
font.globals = globalPrograms
}
return chars, nil
} }
// subsetCFFSubroutines 将未引用的子程序替换为空返回,保持调用编号及偏移基数 // subsetCFFSubroutines 将未引用的子程序替换为空返回,保持调用编号及偏移基数
@@ -125,9 +175,9 @@ func subsetCFFSubroutines(programs [][]byte, used []bool) [][]byte {
} }
// rebuildNamedCFF 重建非CID的CFF偏移,保留字符集、编码和私有提示参数 // rebuildNamedCFF 重建非CID的CFF偏移,保留字符集、编码和私有提示参数
// 入参: data 原始CFF数据, font 执行资源, chars 替换后的字形程序 // 入参: data 原始CFF数据, font 执行资源, chars 替换后的字形程序, ids 字形重排顺序,nil时保留原编号
// 返回: []byte 重建的CFF数据, error 不支持的结构 // 返回: []byte 重建的CFF数据, error 不支持的结构
func rebuildNamedCFF(data []byte, font *type2Font, chars [][]byte) ([]byte, error) { func rebuildNamedCFF(data []byte, font *type2Font, chars [][]byte, ids []uint16) ([]byte, error) {
names, end, err := readType2Index(data, int(data[2])) names, end, err := readType2Index(data, int(data[2]))
if err != nil { if err != nil {
return nil, err return nil, err
@@ -151,35 +201,39 @@ func rebuildNamedCFF(data []byte, font *type2Font, chars [][]byte) ([]byte, erro
return nil, fmt.Errorf("unsupported CFF top operator %d", op) return nil, fmt.Errorf("unsupported CFF top operator %d", op)
} }
} }
sids, _, _, _, ok := getCFFCharsetInfo(data, len(chars)) sids, _, _, _, ok := getCFFCharsetInfo(data, len(font.chars))
if !ok { if !ok {
return nil, fmt.Errorf("invalid CFF charset") return nil, fmt.Errorf("invalid CFF charset")
} }
charset := []byte{0}
for _, sid := range sids[1:] {
charset = binary.BigEndian.AppendUint16(charset, uint16(sid))
}
encoding, err := cffSubsetEncoding(data, dict) encoding, err := cffSubsetEncoding(data, dict)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if ids != nil {
if encoding != nil {
codes, err := readCFFSubsetEncoding(encoding, sids)
if err != nil {
return nil, err
}
encoding, err = encodeCFFSubsetEncoding(codes, sids, ids)
if err != nil {
return nil, err
}
}
selected := make([]int, len(ids))
for index, gid := range ids {
selected[index] = sids[gid]
}
sids = selected
}
charset := encodeCFFSubsetCharset(sids)
private, err := readType2Private(data, dict[18], &type2Font{}) private, err := readType2Private(data, dict[18], &type2Font{})
if err != nil { if err != nil {
return nil, err return nil, err
} }
for op := range private { privateData, err := encodeCFFSubsetPrivate(private)
switch op { if err != nil {
case 6, 7, 8, 9, 10, 11, 19, 20, 21, 1209, 1210, 1211, 1212, 1213, 1214, 1217, 1218, 1219: return nil, err
default:
return nil, fmt.Errorf("unsupported CFF private operator %d", op)
}
}
privateData := encodeCFFDict(private)
if _, ok := private[19]; ok {
for private[19][0] != float64(len(privateData)) {
private[19] = []float64{float64(len(privateData))}
privateData = encodeCFFDict(private)
}
} }
prefix := bytes.Clone(data[:int(data[2])]) prefix := bytes.Clone(data[:int(data[2])])
prefix = append(prefix, encodeCFFIndex(names)...) prefix = append(prefix, encodeCFFIndex(names)...)
@@ -212,6 +266,28 @@ func rebuildNamedCFF(data []byte, font *type2Font, chars [][]byte) ([]byte, erro
return nil, fmt.Errorf("CFF dictionary offsets did not converge") return nil, fmt.Errorf("CFF dictionary offsets did not converge")
} }
// encodeCFFSubsetPrivate 保留私有字典参数,将局部子程序安排在字典之后
// 入参: private 私有字典
// 返回: []byte 字典数据, error 不支持的参数或偏移错误
func encodeCFFSubsetPrivate(private cffDict) ([]byte, error) {
for op := range private {
switch op {
case 6, 7, 8, 9, 10, 11, 19, 20, 21, 1209, 1210, 1211, 1212, 1213, 1214, 1217, 1218, 1219:
default:
return nil, fmt.Errorf("unsupported CFF private operator %d", op)
}
}
for range 16 {
data := encodeCFFDict(private)
value, ok := private[19]
if !ok || len(value) == 1 && value[0] == float64(len(data)) {
return data, nil
}
private[19] = []float64{float64(len(data))}
}
return nil, fmt.Errorf("CFF private offset did not converge")
}
// cffSubsetEncoding 读取自定义CFF编码及补充映射,预定义编码无需复制 // cffSubsetEncoding 读取自定义CFF编码及补充映射,预定义编码无需复制
// 入参: data CFF数据, dict 顶层字典 // 入参: data CFF数据, dict 顶层字典
// 返回: []byte 编码数据, error 编码格式错误 // 返回: []byte 编码数据, error 编码格式错误
+15 -10
View File
@@ -102,7 +102,11 @@ func sfntSubsettable(sfnt *sfntFont) bool {
return false return false
} }
font, err := readType2Font(sfnt.Tables["CFF "]) font, err := readType2Font(sfnt.Tables["CFF "])
return err == nil && len(font.chars) == int(sfnt.NumGlyphs()) || staticCFFPrograms(sfnt) != nil if err == nil {
return len(font.chars) == int(sfnt.NumGlyphs())
}
cid, err := readCIDSubsetFont(sfnt.Tables["CFF "])
return err == nil && len(cid.fonts[0].chars) == int(sfnt.NumGlyphs())
} }
return sfnt.IsTrueType && (sfnt.Tables["fvar"] == nil || variableSFNTSubsettable(sfnt)) return sfnt.IsTrueType && (sfnt.Tables["fvar"] == nil || variableSFNTSubsettable(sfnt))
} }
@@ -166,13 +170,14 @@ func subsetSFNTFont(data []byte, glyphs []uint16, mapped bool) ([]byte, error) {
return bytes.Clone(data), nil return bytes.Clone(data), nil
} }
if sfnt.IsCFF { if sfnt.IsCFF {
if font, err := readType2Font(sfnt.Tables["CFF "]); err == nil { count, err := parseCFFAndCountGlyphs(sfnt.Tables["CFF "])
if len(font.chars) != int(sfnt.NumGlyphs()) { if err != nil {
return nil, err
}
if count != int(sfnt.NumGlyphs()) {
return nil, fmt.Errorf("CFF glyph count does not match maxp") return nil, fmt.Errorf("CFF glyph count does not match maxp")
} }
return subsetNamedCFFFont(sfnt, glyphs) return subsetCFFFont(sfnt, glyphs, mapped)
}
return subsetStaticCFFFont(sfnt, glyphs)
} }
if sfnt.Tables["fvar"] != nil { if sfnt.Tables["fvar"] != nil {
return subsetVariableSFNTFont(sfnt, glyphs) return subsetVariableSFNTFont(sfnt, glyphs)
@@ -203,7 +208,7 @@ func subsetSFNTFont(data []byte, glyphs []uint16, mapped bool) ([]byte, error) {
return subsetCompactSFNTFont(sfnt, glyphs) return subsetCompactSFNTFont(sfnt, glyphs)
} }
// subsetSourceSFNTFont 裁剪静态TrueType或非CID的CFF原有字体,保留字形编号、组合依赖、度量和提示指令 // subsetSourceSFNTFont 裁剪静态TrueType或CFF原有字体,保留字形编号、组合依赖、度量和提示指令
// 集合、字形替换、可变、彩色和未知表保持原样,不对不完整的引用或异常字体猜测修复 // 集合、字形替换、可变、彩色和未知表保持原样,不对不完整的引用或异常字体猜测修复
// 入参: data 字体数据, usage 全包用字记录 // 入参: data 字体数据, usage 全包用字记录
// 返回: []byte 更小的字体子集,无确定收益时为空 // 返回: []byte 更小的字体子集,无确定收益时为空
@@ -217,7 +222,7 @@ func subsetSourceSFNTFont(data []byte, usage *editorFontUsage) []byte {
} }
for tag := range sfnt.Tables { for tag := range sfnt.Tables {
switch tag { switch tag {
case "cmap", "head", "hhea", "hmtx", "maxp", "OS/2", "post", "name", "glyf", "loca", "CFF ", "cvt ", "fpgm", "prep", "gasp", "kern", "vhea", "vmtx", "hdmx", "LTSH", "VDMX", "GDEF", "GPOS", "DSIG", "FFTM": case "cmap", "head", "hhea", "hmtx", "maxp", "OS/2", "post", "name", "glyf", "loca", "CFF ", "cvt ", "fpgm", "prep", "gasp", "kern", "vhea", "vmtx", "VORG", "hdmx", "LTSH", "VDMX", "GDEF", "GPOS", "DSIG", "FFTM":
default: default:
return nil return nil
} }
@@ -250,8 +255,8 @@ func subsetSourceSFNTFont(data []byte, usage *editorFontUsage) []byte {
} }
} }
if sfnt.IsCFF { if sfnt.IsCFF {
font, err := readType2Font(sfnt.Tables["CFF "]) count, err := parseCFFAndCountGlyphs(sfnt.Tables["CFF "])
if err != nil || len(font.chars) != int(sfnt.NumGlyphs()) { if err != nil || count != int(sfnt.NumGlyphs()) {
return nil return nil
} }
cff, err := subsetCFFCharstrings(sfnt.Tables["CFF "], glyphs) cff, err := subsetCFFCharstrings(sfnt.Tables["CFF "], glyphs)
+63 -7
View File
@@ -35,6 +35,9 @@ import (
xlanguage "golang.org/x/text/language" xlanguage "golang.org/x/text/language"
) )
// fontShapingBufferLimit 限制字体实例长期保留的字形缓冲容量
const fontShapingBufferLimit = 4096
// fontShaper 使用字体度量和原始数据提供可配置塑形,不依赖绘图后端 // fontShaper 使用字体度量和原始数据提供可配置塑形,不依赖绘图后端
type fontShaper struct { type fontShaper struct {
metrics FontMetrics metrics FontMetrics
@@ -61,8 +64,32 @@ func (f *fontShaper) ShapeTextWithOptions(value string, size float64, options Te
if !rasterPositive(size) || !utf8.ValidString(value) { if !rasterPositive(size) || !utf8.ValidString(value) {
return nil, fmt.Errorf("invalid shaping text or size") return nil, fmt.Errorf("invalid shaping text or size")
} }
for _, char := range value { runes := []rune(value)
if unicode.IsControl(char) || char == '\u2028' || char == '\u2029' { return f.shapeTextRun(runes, 0, len(runes), size, options)
}
// ShapeTextRun 利用原文上下文塑形指定区间,保留原文簇索引
// 入参: value 已校验的原文符文, start 起始符文索引, end 结束符文索引, size 毫米字号, options 已分段的塑形选项
// 返回: []ShapedGlyph 原文簇索引及区间内定位字形, error 区间、字符或选项错误
func (f *fontShaper) ShapeTextRun(value []rune, start, end int, size float64, options TextShapeOptions) ([]ShapedGlyph, error) {
if options.Bidi {
return nil, fmt.Errorf("bidirectional segmentation is not valid for an individual shaping run")
}
return f.shapeTextRun(value, start, end, size, options)
}
// shapeTextRun 共用完整行与局部区间的选项校验、字体初始化及字形塑形
// 入参: runes 原文符文, start 起始符文索引, end 结束符文索引, size 毫米字号, options 塑形选项
// 返回: []ShapedGlyph 定位字形, error 字体、区间或选项错误
func (f *fontShaper) shapeTextRun(runes []rune, start, end int, size float64, options TextShapeOptions) ([]ShapedGlyph, error) {
if !rasterPositive(size) {
return nil, fmt.Errorf("invalid shaping size")
}
if start < 0 || end < start || end > len(runes) {
return nil, fmt.Errorf("invalid shaping text range")
}
for _, char := range runes[start:end] {
if !utf8.ValidRune(char) || unicode.IsControl(char) || char == '\u2028' || char == '\u2029' {
return nil, fmt.Errorf("shaping requires a single text line") return nil, fmt.Errorf("shaping requires a single text line")
} }
} }
@@ -113,6 +140,9 @@ func (f *fontShaper) ShapeTextWithOptions(value string, size float64, options Te
features = append(features, feature) features = append(features, feature)
} }
} }
if start == end {
return nil, nil
}
f.shapeMu.Lock() f.shapeMu.Lock()
defer f.shapeMu.Unlock() defer f.shapeMu.Unlock()
if f.shapeFont == nil { if f.shapeFont == nil {
@@ -125,24 +155,32 @@ func (f *fontShaper) ShapeTextWithOptions(value string, size float64, options Te
f.shapeBuffer = harfbuzz.NewBuffer() f.shapeBuffer = harfbuzz.NewBuffer()
} }
if options.Bidi { if options.Bidi {
return f.shapeBidiText([]rune(value), size, props, features) return f.shapeBidiText(runes, size, props, features)
} }
buffer := f.shapeBuffer buffer := f.shapeBuffer
buffer.Clear() buffer.Clear()
defer f.releaseShapeBuffer()
buffer.Props = props buffer.Props = props
runes := []rune(value) buffer.AddRunes(runes, start, end-start)
buffer.AddRunes(runes, 0, len(runes))
buffer.GuessSegmentProperties() buffer.GuessSegmentProperties()
buffer.Shape(f.shapeFont, features) buffer.Shape(f.shapeFont, features)
unit := size / float64(f.metrics.UnitsPerEm()) unit := size / float64(f.metrics.UnitsPerEm())
result := make([]ShapedGlyph, len(buffer.Info)) result := make([]ShapedGlyph, len(buffer.Info))
var x, y float64 var x, y float64
previous, order := -1, -1
for i, info := range buffer.Info { for i, info := range buffer.Info {
if info.Glyph >= typefont.GID(f.metrics.NumGlyphs()) { if info.Glyph >= typefont.GID(f.metrics.NumGlyphs()) {
return nil, fmt.Errorf("invalid shaped glyph index %d", info.Glyph) return nil, fmt.Errorf("invalid shaped glyph index %d", info.Glyph)
} }
if info.Cluster < start || info.Cluster >= end {
return nil, fmt.Errorf("invalid shaped cluster index %d", info.Cluster)
}
if info.Cluster != previous {
previous = info.Cluster
order++
}
position := buffer.Pos[i] position := buffer.Pos[i]
result[i] = ShapedGlyph{Glyph: uint16(info.Glyph), Cluster: info.Cluster, result[i] = ShapedGlyph{Glyph: uint16(info.Glyph), Cluster: info.Cluster, VisualOrder: order,
X: (x + float64(position.XOffset)) * unit, Y: -(y + float64(position.YOffset)) * unit, X: (x + float64(position.XOffset)) * unit, Y: -(y + float64(position.YOffset)) * unit,
Advance: float64(position.XAdvance) * unit} Advance: float64(position.XAdvance) * unit}
if !finite(result[i].X) || !finite(result[i].Y) || !finite(result[i].Advance) { if !finite(result[i].X) || !finite(result[i].Y) || !finite(result[i].Advance) {
@@ -155,6 +193,17 @@ func (f *fontShaper) ShapeTextWithOptions(value string, size float64, options Te
return result, nil return result, nil
} }
// releaseShapeBuffer 释放原文上下文和超限缓冲,普通文本继续复用已有容量
func (f *fontShaper) releaseShapeBuffer() {
buffer := f.shapeBuffer
if cap(buffer.Info) > fontShapingBufferLimit || cap(buffer.Pos) > fontShapingBufferLimit {
f.shapeBuffer = harfbuzz.NewBuffer()
return
}
buffer.Clear()
buffer.AddRunes(nil, 0, 0)
}
// ResolveFace 返回指定字体 // ResolveFace 返回指定字体
// 入参: char 字符 // 入参: char 字符
// 返回: *typefont.Face 字体 // 返回: *typefont.Face 字体
@@ -211,12 +260,19 @@ func (f *fontShaper) shapeBidiText(runes []rune, size float64, props harfbuzz.Se
return nil, fmt.Errorf("invalid shaped glyph index %d", glyph.GlyphID) return nil, fmt.Errorf("invalid shaped glyph index %d", glyph.GlyphID)
} }
cluster := glyph.TextIndex() cluster := glyph.TextIndex()
if cluster < 0 || cluster >= len(runes) {
return nil, fmt.Errorf("invalid shaped cluster index %d", cluster)
}
order, exists := visual[cluster] order, exists := visual[cluster]
if !exists { if !exists {
order = len(visual) order = len(visual)
visual[cluster] = order visual[cluster] = order
} }
result = append(result, ShapedGlyph{Glyph: uint16(glyph.GlyphID), Cluster: cluster, VisualOrder: order, X: (x + float64(glyph.XOffset)) * unit, Y: -float64(glyph.YOffset) * unit, Advance: float64(glyph.Advance) * unit}) shaped := ShapedGlyph{Glyph: uint16(glyph.GlyphID), Cluster: cluster, VisualOrder: order, X: (x + float64(glyph.XOffset)) * unit, Y: -float64(glyph.YOffset) * unit, Advance: float64(glyph.Advance) * unit}
if !finite(shaped.X) || !finite(shaped.Y) || !finite(shaped.Advance) {
return nil, fmt.Errorf("shaped position exceeds finite range")
}
result = append(result, shaped)
x += float64(glyph.Advance) x += float64(glyph.Advance)
} }
} }
+4
View File
@@ -30,6 +30,7 @@ type type2Font struct {
names map[string]int names map[string]int
nominal, def float64 nominal, def float64
seed uint64 seed uint64
cid bool
} }
// type2State 按Type2规范保存跨子程序共享的执行状态 // type2State 按Type2规范保存跨子程序共享的执行状态
@@ -489,6 +490,9 @@ func (s *type2State) run(data []byte, depth int) (bool, error) {
// 入参: code 标准字符码, x 横向偏移, y 纵向偏移 // 入参: code 标准字符码, x 横向偏移, y 纵向偏移
// 返回: error 组件或指令错误 // 返回: error 组件或指令错误
func (s *type2State) appendComponent(code, x, y float64) error { func (s *type2State) appendComponent(code, x, y float64) error {
if s.font.cid {
return fmt.Errorf("CID CFF cannot use StandardEncoding components")
}
if code < 0 || code > 255 || code != math.Trunc(code) || type1StandardNames[int(code)] == "" { if code < 0 || code > 255 || code != math.Trunc(code) || type1StandardNames[int(code)] == "" {
return fmt.Errorf("invalid Type2 component code") return fmt.Errorf("invalid Type2 component code")
} }
+8 -7
View File
@@ -34,7 +34,7 @@ var ErrPDFPassword = pdfgo.ErrPassword
// RendererOptions复用渲染器字体来源和后端配置,Backends优先于其中的后端设置 // RendererOptions复用渲染器字体来源和后端配置,Backends优先于其中的后端设置
// RasterDPI指定局部合成与三维绘制精度,0沿用渲染器DPI,默认300dpi,Strict禁用局部合成 // RasterDPI指定局部合成与三维绘制精度,0沿用渲染器DPI,默认300dpi,Strict禁用局部合成
// OnProgress按open、pages、convert及write.*阶段报告进度,total为0表示总量未知 // OnProgress按open、pages、convert及write.*阶段报告进度,total为0表示总量未知
// ResolveFile读取PDF引用的外部媒体、附件和页面,默认不访问网络或本地路径 // ResolveFile读取PDF引用的外部媒体、附件、页面及三维纹理,默认不访问网络或本地路径
// ResolveReference可提供已解密的引用页面,目标阅读器由调用方维护 // ResolveReference可提供已解密的引用页面,目标阅读器由调用方维护
// Halftones提供只读设备命名网屏,所属阅读器由调用方维护,连续色调输出不网屏化 // Halftones提供只读设备命名网屏,所属阅读器由调用方维护,连续色调输出不网屏化
// Compression配置转换输出,有损模式按图片实际尺寸降采样,原始及无损模式保持完整采样 // Compression配置转换输出,有损模式按图片实际尺寸降采样,原始及无损模式保持完整采样
@@ -156,7 +156,12 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
return nil, PDFImportReport{}, err return nil, PDFImportReport{}, err
} }
} }
readOptions := pdfgo.ReaderOptions{Password: options.Password, PasswordUTF8: options.PasswordUTF8} editor := NewEditor()
editor.SetRenderBackends(renderer.Backends())
editor.fontDirs, editor.fontFS = renderer.FontSources()
editor.fontSourcesCache = renderer.fontSourcesCache
importer := pdfImporter{ctx: ctx, editor: editor, renderer: renderer, rasterDPI: renderer.DPI, fontIDs: map[*pdfgo.Font]string{}, fonts: map[*pdfgo.Font]*pdfImportedFont{}, pages: map[pdfgo.Reference]*pdfgo.Page{}, pageIDs: map[pdfgo.Reference]string{}}
readOptions := pdfgo.ReaderOptions{Password: options.Password, PasswordUTF8: options.PasswordUTF8, ShapeText: importer.shapePDFText}
var diagnostics []pdfgo.Diagnostic var diagnostics []pdfgo.Diagnostic
var readWarning func(pdfgo.Diagnostic) var readWarning func(pdfgo.Diagnostic)
if !options.Strict { if !options.Strict {
@@ -168,11 +173,7 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
return nil, PDFImportReport{}, err return nil, PDFImportReport{}, err
} }
defer reader.Close() defer reader.Close()
editor := NewEditor() importer.reader = reader
editor.SetRenderBackends(renderer.Backends())
editor.fontDirs, editor.fontFS = renderer.FontSources()
editor.fontSourcesCache = renderer.fontSourcesCache
importer := pdfImporter{ctx: ctx, reader: reader, editor: editor, renderer: renderer, rasterDPI: renderer.DPI, fontIDs: map[*pdfgo.Font]string{}, fonts: map[*pdfgo.Font]*pdfImportedFont{}, pages: map[pdfgo.Reference]*pdfgo.Page{}, pageIDs: map[pdfgo.Reference]string{}}
importer.report.Warnings = diagnostics importer.report.Warnings = diagnostics
importer.pageIndexes = make(map[pdfgo.Reference]int) importer.pageIndexes = make(map[pdfgo.Reference]int)
importer.resolveFile = options.ResolveFile importer.resolveFile = options.ResolveFile
+16 -4
View File
@@ -30,10 +30,16 @@ const pdfModelAttachmentLimit = 1024
// PDFThreeDRenderOptions 控制PDF三维视口的静态绘制,不改变注解激活状态 // PDFThreeDRenderOptions 控制PDF三维视口的静态绘制,不改变注解激活状态
// 缓存与实例上限为0时采用U3DRenderOptions的默认值 // 缓存与实例上限为0时采用U3DRenderOptions的默认值
// ResolveTexture按原始URL提供外部纹理,空值不访问外部资源
// Nodes按名称替换PDF视图中的节点状态,可指定动作轨道与采样时间,不改变原始注解
// Time以秒指定模型动画的采样时刻,零值为初始时刻
type PDFThreeDRenderOptions struct { type PDFThreeDRenderOptions struct {
Width, Height int Width, Height int
MaxMemoryBytes int64 MaxMemoryBytes int64
MaxInstances int MaxInstances int
ResolveTexture U3DTextureResolver
Nodes []U3DNodeState
Time float64
} }
// threeDAnnotation 按非交互呈现规则处理激活状态,保留原始模型与附件动作 // threeDAnnotation 按非交互呈现规则处理激活状态,保留原始模型与附件动作
@@ -147,7 +153,13 @@ func (p *pdfImporter) threeDPainter(ctx context.Context, page *pdfgo.Page, sourc
if !finite(w) || !finite(h) || w <= 0 || h <= 0 || w > 1<<30 || h > 1<<30 { if !finite(w) || !finite(h) || w <= 0 || h <= 0 || w > 1<<30 || h > 1<<30 {
return nil, fmt.Errorf("invalid PDF 3D raster dimensions") return nil, fmt.Errorf("invalid PDF 3D raster dimensions")
} }
img, err := RenderPDFThreeD(ctx, model, source, PDFThreeDRenderOptions{Width: int(w), Height: int(h)}) options := PDFThreeDRenderOptions{Width: int(w), Height: int(h)}
if p.resolveFile != nil {
options.ResolveTexture = func(ctx context.Context, address string) ([]byte, error) {
return p.resolveFile(ctx, pdfgo.FileSpecification{Name: address, FileSystem: "URL"})
}
}
img, err := RenderPDFThreeD(ctx, model, source, options)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -166,7 +178,7 @@ func (p *pdfImporter) threeDPainter(ctx context.Context, page *pdfgo.Page, sourc
}, nil }, nil
} }
// RenderPDFThreeD 按PDF相机、投影、背景与灯光绘制3DB区域,不执行脚本或动画 // RenderPDFThreeD 按PDF相机、投影、背景与灯光绘制指定时刻的3DB区域,不执行脚本
// 返回图像不含3DB以外的注解背景,未支持的视图效果返回明确错误 // 返回图像不含3DB以外的注解背景,未支持的视图效果返回明确错误
// 每次从传入场景建立独立节点状态,不继承此前绘制的运行时状态 // 每次从传入场景建立独立节点状态,不继承此前绘制的运行时状态
// 入参: ctx 取消上下文, model 解码后的U3D模型, source PDF三维参数, options 输出与资源限制 // 入参: ctx 取消上下文, model 解码后的U3D模型, source PDF三维参数, options 输出与资源限制
@@ -200,7 +212,7 @@ func RenderPDFThreeD(ctx context.Context, model *U3DModel, source pdfgo.ThreeD,
if source.Projection != nil { if source.Projection != nil {
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, ResolveTexture: options.ResolveTexture, Time: options.Time}
if err := pdfThreeDRenderMode(source, &render); err != nil { if err := pdfThreeDRenderMode(source, &render); err != nil {
return nil, err return nil, err
} }
@@ -210,7 +222,7 @@ func RenderPDFThreeD(ctx context.Context, model *U3DModel, source pdfgo.ThreeD,
if err := pdfThreeDSections(ctx, presentation.CrossSections, &render); err != nil { if err := pdfThreeDSections(ctx, presentation.CrossSections, &render); err != nil {
return nil, err return nil, err
} }
r, err := newU3DRender(ctx, model, render) r, err := newU3DRender(ctx, model, render, options.Nodes)
if err != nil { if err != nil {
return nil, err return nil, err
} }
+45 -26
View File
@@ -16,11 +16,10 @@ package ofdgo
import ( import (
"bytes" "bytes"
"context"
"crypto/sha256" "crypto/sha256"
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"maps"
"slices"
"unicode/utf16" "unicode/utf16"
"unicode/utf8" "unicode/utf8"
@@ -42,6 +41,46 @@ type pdfFontRepair struct {
locations bool locations bool
} }
// shapePDFText 使用导入字体整形注释文字,将毫米坐标约定转换为PDF文字空间方向
// 入参: ctx 取消上下文, source 源字体, text 单行原文, rightToLeft 段落方向
// 返回: []pdfgo.ShapedGlyph 千分之一字号的定位字形, error 字体、整形或取消错误
func (p *pdfImporter) shapePDFText(ctx context.Context, source *pdfgo.Font, text string, rightToLeft bool) ([]pdfgo.ShapedGlyph, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if len(source.Program) == 0 {
return nil, &pdfgo.UnsupportedError{Feature: "annotation shaping without embedded font"}
}
font, err := p.importedFont(source)
if err != nil {
return nil, err
}
shaper, ok := font.metrics.(FontShaperOptions)
if !ok {
return nil, &pdfgo.UnsupportedError{Feature: "annotation text shaping backend"}
}
options := TextShapeOptions{Bidi: true}
if rightToLeft {
options.Direction = "rtl"
}
glyphs, err := shaper.ShapeTextWithOptions(text, 1000, options)
if err != nil {
return nil, err
}
result := make([]pdfgo.ShapedGlyph, len(glyphs))
units := float64(font.metrics.UnitsPerEm())
if units <= 0 {
return nil, fmt.Errorf("invalid annotation font units")
}
for i, glyph := range glyphs {
if err := ctx.Err(); err != nil {
return nil, err
}
result[i] = pdfgo.ShapedGlyph{ID: glyph.Glyph, Cluster: glyph.Cluster, VisualOrder: glyph.VisualOrder, X: glyph.X, Y: -glyph.Y, Advance: glyph.Advance, Width: float64(font.metrics.GlyphAdvance(glyph.Glyph)) * 1000 / units}
}
return result, ctx.Err()
}
// embeddedTextFont 匹配同名内嵌字体,仅复用唯一且覆盖全部字符的字形程序 // embeddedTextFont 匹配同名内嵌字体,仅复用唯一且覆盖全部字符的字形程序
// 入参: source 外部字体, glyphs 当前文字字形 // 入参: source 外部字体, glyphs 当前文字字形
// 返回: *pdfImportedFont 可复用字体,无可靠匹配时为空 // 返回: *pdfImportedFont 可复用字体,无可靠匹配时为空
@@ -292,33 +331,13 @@ func pdfFontProgram(source *pdfgo.Font, type1 *type1Program) ([]byte, pdfFontRep
} }
} }
if len(tables["cmap"]) == 0 || source.Subtype == "Type0" && invalidCmap || (source.Subtype == "Type0" || source.Subtype == "TrueType") && pdfCmapNeedsRebuild(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, err := source.GlyphMap()
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))
if err != nil { if err != nil {
return nil, repair, err return nil, repair, err
} }
if len(glyphs) != 1 || !glyphs[0].HasID { for _, glyph := range mapping {
return nil, repair, &pdfgo.UnsupportedError{Feature: "font without explicit glyph mapping"} if glyph >= count {
} return nil, repair, fmt.Errorf("PDF font glyph %d exceeds glyph count %d", glyph, count)
glyph := glyphs[0]
if 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 {
char, _ := utf8.DecodeRuneInString(glyph.Text)
if _, exists := mapping[char]; !exists {
mapping[char] = glyph.ID
}
} }
} }
addPackedGlyphMapping(mapping, count) addPackedGlyphMapping(mapping, count)
+37 -8
View File
@@ -27,9 +27,11 @@ import (
type pdfTextReplacement struct { type pdfTextReplacement struct {
text *pdfgo.ReplacementText text *pdfgo.ReplacementText
count int count int
matched int
offset int64 offset int64
blocked bool blocked bool
applied bool applied bool
identity bool
} }
// pdfTextReplacements 按绘制顺序收集替换区段,避免拆分或重复写入原文 // pdfTextReplacements 按绘制顺序收集替换区段,避免拆分或重复写入原文
@@ -104,21 +106,26 @@ func (r *pdfTextReplacements) markedContent(mark pdfgo.MarkedContentMark) error
return nil return nil
} }
// apply 将完整且样式一致的区段合并为标准OFD文字对象 // apply 合并可等价表示的替换区段,原文相同但样式不同的区段保留原图元
// 入参: nodes 原始图元 // 入参: nodes 原始图元
// 返回: []pdfCompositeNode 转换图元, error 取消或语义转换错误 // 返回: []pdfCompositeNode 转换图元, error 取消或语义转换错误
func (r *pdfTextReplacements) apply(nodes []pdfCompositeNode) ([]pdfCompositeNode, error) { func (r *pdfTextReplacements) apply(nodes []pdfCompositeNode) ([]pdfCompositeNode, error) {
if len(r.order) == 0 { if len(r.order) == 0 {
return nodes, nil return nodes, nil
} }
r.count(nodes, false) if err := r.count(nodes, false); err != nil {
return nil, err
}
for _, span := range r.order {
span.identity = !span.blocked && span.count > 0 && span.matched == len(span.text.Text)
}
var err error var err error
nodes, err = r.merge(nodes) nodes, err = r.merge(nodes)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, span := range r.order { for _, span := range r.order {
if span.applied || !span.blocked && span.count == 0 && span.text.Text == "" { if span.applied || span.identity || !span.blocked && span.count == 0 && span.text.Text == "" {
continue continue
} }
if r.importer.warning == nil { if r.importer.warning == nil {
@@ -130,14 +137,33 @@ func (r *pdfTextReplacements) apply(nodes []pdfCompositeNode) ([]pdfCompositeNod
return nodes, nil return nodes, nil
} }
// count 统计完整区段中的文字图元,排除需要组级合成的内容 // count 统计区段文字并逐字核对原文,排除需要组级合成的内容
// 入参: nodes 图元, blocked 上层组是否阻止直接转换 // 入参: nodes 图元, blocked 上层组是否阻止直接转换
func (r *pdfTextReplacements) count(nodes []pdfCompositeNode, blocked bool) { // 返回: error 取消错误
func (r *pdfTextReplacements) count(nodes []pdfCompositeNode, blocked bool) error {
for _, node := range nodes { for _, node := range nodes {
if err := r.importer.ctx.Err(); err != nil {
return err
}
if node.text != nil { if node.text != nil {
if span := r.span(node.text.Replacement); span != nil { if span := r.span(node.text.Replacement); span != nil {
span.count++ span.count++
span.blocked = span.blocked || blocked span.blocked = span.blocked || blocked
for i, glyph := range node.text.Glyphs {
if span.matched < 0 {
break
}
if i%256 == 0 {
if err := r.importer.ctx.Err(); err != nil {
return err
}
}
if glyph.Text == "" || !strings.HasPrefix(span.text.Text[span.matched:], glyph.Text) {
span.matched = -1
break
}
span.matched += len(glyph.Text)
}
} }
} }
childBlocked := blocked childBlocked := blocked
@@ -145,8 +171,11 @@ func (r *pdfTextReplacements) count(nodes []pdfCompositeNode, blocked bool) {
opaque, err := node.opaque(group.ColorSpace) opaque, err := node.opaque(group.ColorSpace)
childBlocked = childBlocked || !opaque || err != nil || group.ColorSpace != nil && !group.ColorSpace.SRGBEquivalent() childBlocked = childBlocked || !opaque || err != nil || group.ColorSpace != nil && !group.ColorSpace.SRGBEquivalent()
} }
r.count(node.children, childBlocked) if err := r.count(node.children, childBlocked); err != nil {
return err
} }
}
return nil
} }
// merge 合并同层连续图元,保留原始绘制顺序与字形坐标 // merge 合并同层连续图元,保留原始绘制顺序与字形坐标
@@ -171,7 +200,7 @@ func (r *pdfTextReplacements) merge(nodes []pdfCompositeNode) ([]pdfCompositeNod
first = node.children[0].text first = node.children[0].text
} }
if first != nil { if first != nil {
if span := r.span(first.Replacement); span != nil && !span.applied && !span.blocked && span.text.Text != "" { if span := r.span(first.Replacement); span != nil && !span.applied && !span.blocked && span.text.Text != "" && !(span.identity && span.count == 1) {
var marks []pdfCompositeNode var marks []pdfCompositeNode
for end = i; end < len(nodes); end++ { for end = i; end < len(nodes); end++ {
current := nodes[end] current := nodes[end]
@@ -221,7 +250,7 @@ func (r *pdfTextReplacements) merge(nodes []pdfCompositeNode) ([]pdfCompositeNod
return result, nil return result, nil
} }
// clear 清除未转换的区段引用,防止局部绘制重复使用整段原文 // clear 清除无需替换及未转换的区段引用,防止局部绘制重复使用整段原文
// 入参: nodes 图元 // 入参: nodes 图元
func (r *pdfTextReplacements) clear(nodes []pdfCompositeNode) { func (r *pdfTextReplacements) clear(nodes []pdfCompositeNode) {
for _, node := range nodes { for _, node := range nodes {
+26
View File
@@ -17,6 +17,7 @@ package ofdgo
import ( import (
"archive/zip" "archive/zip"
"bytes" "bytes"
"context"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"io" "io"
@@ -546,6 +547,31 @@ func (r *Reader) readFileView(name string) ([]byte, error) {
return data, err return data, err
} }
// readFileViewContext 读取包内文件并响应取消,已加载数据直接借用
// 入参: ctx 取消上下文, name 文件名
// 返回: []byte 只读文件内容, error 读取或取消错误
func (r *Reader) readFileViewContext(ctx context.Context, name string) ([]byte, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if data, ok := r.files[cleanPackagePath(name)]; ok {
return data, nil
}
input, err := r.openFile(name)
if err != nil {
return nil, err
}
defer input.Close()
data, err := io.ReadAll(imageInput{ReadCloser: input, context: ctx})
if err != nil {
return nil, err
}
if err := ctx.Err(); err != nil {
return nil, err
}
return data, nil
}
// readFileData 统一解析包内路径并区分缓存借用与独立读取 // readFileData 统一解析包内路径并区分缓存借用与独立读取
// 入参: name 文件名 // 入参: name 文件名
// 返回: []byte 文件内容, bool 是否借用缓存, error 错误信息 // 返回: []byte 文件内容, bool 是否借用缓存, error 错误信息
+78 -9
View File
@@ -31,7 +31,7 @@ type U3DOptions struct {
MaxDecodedBytes int64 MaxDecodedBytes int64
} }
// U3DModel 保存独立于PDF的静态U3D场景,不执行动作或读取外部文件 // U3DModel 保存独立于PDF的U3D场景及动作数据,不执行动作或读取外部文件
type U3DModel struct { type U3DModel struct {
Major, Minor int16 Major, Minor int16
DefinedUnits bool DefinedUnits bool
@@ -42,6 +42,8 @@ type U3DModel struct {
Shaders []U3DShader Shaders []U3DShader
Lights []U3DLight Lights []U3DLight
Views []U3DViewResource Views []U3DViewResource
Textures []U3DTexture
Motions []U3DMotion
Metadata []U3DMetadata Metadata []U3DMetadata
} }
@@ -61,6 +63,8 @@ type U3DNode struct {
LineShaders [][]string LineShaders [][]string
PointShaders [][]string PointShaders [][]string
View *U3DView View *U3DView
BoneWeights []U3DBoneWeights
Animations []U3DAnimation
} }
// U3DParent 保存父节点名称及局部到父节点的变换,空名称引用默认节点 // U3DParent 保存父节点名称及局部到父节点的变换,空名称引用默认节点
@@ -77,12 +81,13 @@ type U3DMaterial struct {
Reflectivity, Opacity float32 Reflectivity, Opacity float32
} }
// U3DShader 保存无纹理着色器参数,不提前应用混合、透明测试或光照 // U3DShader 保存材质与八个纹理通道,Textures中的空项表示未启用
type U3DShader struct { type U3DShader struct {
Name, Material string Name, Material string
Attributes, AlphaFunction, Blend uint32 Attributes, AlphaFunction, Blend uint32
RenderPass uint32 RenderPass uint32
AlphaReference float32 AlphaReference float32
Textures [8]*U3DTextureLayer
} }
// U3DLight 保存环境、方向、点或聚光资源,Kind依次为0至3 // U3DLight 保存环境、方向、点或聚光资源,Kind依次为0至3
@@ -110,10 +115,12 @@ type u3dDecoder struct {
meshOrder []string meshOrder []string
nodes map[string]int nodes map[string]int
palettes map[uint32]map[string]bool palettes map[uint32]map[string]bool
textures map[string]int
} }
// DecodeU3D 按ECMA-363解码静态基础网格、线集、点集、组、材质、着色器和光源 // DecodeU3D 按ECMA-363解码静态几何、骨架、场景节点及材质、纹理图像等资源
// 暂不支持渐进网格、纹理和动画,遇到未支持内容返回错误而非部分场景 // 保留动作轨道和动画修饰器,不在解码时求值;绘制能力由RenderU3D另行校验
// 渐进网格尚未支持
// 点集球面预测暂限不超过两个非对跖方向,不推测多方向合并次序 // 点集球面预测暂限不超过两个非对跖方向,不推测多方向合并次序
// 入参: ctx 取消上下文, data U3D数据, options 解码限制 // 入参: ctx 取消上下文, data U3D数据, options 解码限制
// 返回: *U3DModel 独立场景数据, error 格式、能力、预算或取消错误 // 返回: *U3DModel 独立场景数据, error 格式、能力、预算或取消错误
@@ -177,7 +184,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 || block.kind == 0xffffff3e || block.kind == 0xffffff3f continuation := block.kind == 0xffffff15 || block.kind == 0xffffff3b || block.kind == 0xffffff3c || block.kind == 0xffffff3e || block.kind == 0xffffff3f || block.kind == 0xffffff5c
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)
} }
@@ -209,9 +216,15 @@ func DecodeU3D(ctx context.Context, data []byte, options U3DOptions) (*U3DModel,
} }
d.model.Meshes = append(d.model.Meshes, mesh.mesh) d.model.Meshes = append(d.model.Meshes, mesh.mesh)
} }
if err := d.checkTextures(); err != nil {
return nil, err
}
if err := d.checkParents(); err != nil { if err := d.checkParents(); err != nil {
return nil, err return nil, err
} }
if err := d.checkBoneWeights(); err != nil {
return nil, err
}
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return nil, err return nil, err
} }
@@ -307,6 +320,12 @@ func (d *u3dDecoder) chain(block u3dBlock) error {
} }
r := u3dValues{data: block.data} r := u3dValues{data: block.data}
name, kind, flags := r.text(), r.u32(), r.u32() name, kind, flags := r.text(), r.u32(), r.u32()
if r.err != nil {
return r.err
}
if name == "" {
return fmt.Errorf("cannot redefine U3D default resource")
}
if kind > 2 || flags & ^uint32(3) != 0 { if kind > 2 || flags & ^uint32(3) != 0 {
return fmt.Errorf("invalid U3D modifier chain") return fmt.Errorf("invalid U3D modifier chain")
} }
@@ -367,6 +386,12 @@ func (d *u3dDecoder) block(block u3dBlock, owner string, chain int, index uint32
} }
r := u3dValues{data: block.data} r := u3dValues{data: block.data}
name := r.text() name := r.text()
if r.err != nil {
return "", r.err
}
if name == "" {
return "", fmt.Errorf("cannot redefine U3D default resource")
}
var err error var err error
switch block.kind { switch block.kind {
case 0xffffff21, 0xffffff22, 0xffffff23, 0xffffff24: case 0xffffff21, 0xffffff22, 0xffffff23, 0xffffff24:
@@ -394,16 +419,38 @@ func (d *u3dDecoder) block(block u3dBlock, owner string, chain int, index uint32
return name, fmt.Errorf("invalid U3D primitive continuation location") return name, fmt.Errorf("invalid U3D primitive continuation location")
} }
err = d.primitiveContinuation(&r, name, block.kind == 0xffffff3e) err = d.primitiveContinuation(&r, name, block.kind == 0xffffff3e)
case 0xffffff43, 0xffffff44:
if (chain != 0 && chain != 1) || index == 0 || owner != name || r.u32() != index {
return name, fmt.Errorf("invalid U3D animation or bone weight modifier chain")
}
if _, exists := d.nodes[name]; chain == 0 && !exists || chain == 1 && d.meshes[name] == nil {
return name, fmt.Errorf("missing U3D animation or bone weight modifier target")
}
if block.kind == 0xffffff43 {
err = d.animation(&r, name, chain, index)
} else {
err = d.boneWeights(&r, name, chain, index)
}
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")
} }
err = d.shading(&r, name) err = d.shading(&r, name)
case 0xffffff51, 0xffffff52, 0xffffff53, 0xffffff54: case 0xffffff51, 0xffffff52, 0xffffff53, 0xffffff54, 0xffffff56:
if chain >= 0 { if chain >= 0 {
return name, fmt.Errorf("invalid U3D palette resource location") return name, fmt.Errorf("invalid U3D palette resource location")
} }
err = d.resource(&r, block.kind, name) err = d.resource(&r, block.kind, name)
case 0xffffff55:
if chain >= 0 && (chain != 2 || index != 0) {
return name, fmt.Errorf("invalid U3D texture chain")
}
err = d.textureDeclaration(&r, name)
case 0xffffff5c:
if chain >= 0 {
return name, fmt.Errorf("invalid U3D texture continuation location")
}
err = d.textureContinuation(&r, name)
default: default:
return name, fmt.Errorf("unsupported U3D block %#x", block.kind) return name, fmt.Errorf("unsupported U3D block %#x", block.kind)
} }
@@ -513,7 +560,7 @@ func (d *u3dDecoder) shading(r *u3dValues, name string) error {
return r.err return r.err
} }
// resource 读取独立材质、着色器、光源和视图资源 // resource 读取独立材质、着色器、光源、视图和动作资源
// 入参: r 字段读取器, kind 资源类型, name 资源名 // 入参: r 字段读取器, kind 资源类型, name 资源名
// 返回: error 结构或能力错误 // 返回: error 结构或能力错误
func (d *u3dDecoder) resource(r *u3dValues, kind uint32, name string) error { func (d *u3dDecoder) resource(r *u3dValues, kind uint32, name string) error {
@@ -527,6 +574,8 @@ func (d *u3dDecoder) resource(r *u3dValues, kind uint32, name string) error {
} }
palette[name] = true palette[name] = true
switch kind { switch kind {
case 0xffffff56:
return d.motion(r, name)
case 0xffffff52: case 0xffffff52:
return d.viewResource(r, name) return d.viewResource(r, name)
case 0xffffff54: case 0xffffff54:
@@ -548,8 +597,28 @@ func (d *u3dDecoder) resource(r *u3dValues, kind uint32, name string) error {
if shader.Attributes & ^uint32(7) != 0 || shader.AlphaFunction < 0x610 || shader.AlphaFunction > 0x617 || shader.Blend < 0x604 || shader.Blend > 0x607 || channels & ^uint32(255) != 0 || alpha & ^channels != 0 { if shader.Attributes & ^uint32(7) != 0 || shader.AlphaFunction < 0x610 || shader.AlphaFunction > 0x617 || shader.Blend < 0x604 || shader.Blend > 0x607 || channels & ^uint32(255) != 0 || alpha & ^channels != 0 {
return fmt.Errorf("invalid U3D shader parameters") return fmt.Errorf("invalid U3D shader parameters")
} }
if channels != 0 { for channel := range shader.Textures {
return fmt.Errorf("unsupported U3D textured shader") if channels&(1<<channel) == 0 {
continue
}
if err := d.reserve(1, 192); err != nil {
return err
}
layer := &U3DTextureLayer{Texture: r.text(), Intensity: r.f32(), Blend: r.u8(), BlendSource: r.u8(), BlendConstant: r.f32(), Mode: r.u8(), Alpha: alpha&(1<<channel) != 0}
for i := range layer.Transform {
layer.Transform[i] = r.f32()
}
for i := range layer.WrapTransform {
layer.WrapTransform[i] = r.f32()
}
layer.Repeat = r.u8()
if r.err != nil {
return r.err
}
if layer.Blend > 3 || layer.BlendSource > 1 || layer.Mode > 4 || layer.Repeat > 3 {
return fmt.Errorf("invalid U3D texture layer")
}
shader.Textures[channel] = layer
} }
d.model.Shaders = append(d.model.Shaders, shader) d.model.Shaders = append(d.model.Shaders, shader)
case 0xffffff51: case 0xffffff51:
+176
View File
@@ -0,0 +1,176 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"context"
"fmt"
"io"
)
// U3DAnimation 保存ECMA-363第9.7.3节的动画修饰器,Index为链内位置
// Attributes的1、2、4、8分别表示启动、锁定根骨骼、单轨播放和动作间平滑过渡
// TimeScale为整体时间倍率,BlendTime以毫秒计;Motions为空时使用无轨道的默认动作
type U3DAnimation struct {
Index, Attributes uint32
TimeScale, BlendTime float32
Motions []U3DAnimationMotion
}
// U3DAnimationMotion 保存动画中的动作引用,空名称引用默认动作
// Attributes的1、2分别表示循环和同步结束,TimeOffset以毫秒计,TimeScale为动作时间倍率
type U3DAnimationMotion struct {
Name string
Attributes uint32
TimeOffset, TimeScale float32
}
// animation 读取动画参数及有序动作引用,不在解码时调度或求值
// 入参: r 字段读取器, name 所属链名, chain 链类型, index 链内位置
// 返回: error 结构、参数、预算或取消错误
func (d *u3dDecoder) animation(r *u3dValues, name string, chain int, index uint32) error {
animation := U3DAnimation{Index: index, Attributes: r.u32(), TimeScale: r.f32()}
count := r.u32()
if r.err != nil {
return r.err
}
if len(r.data)-r.pos < 4 || uint64(count) > uint64(len(r.data)-r.pos-4)/14 {
return io.ErrUnexpectedEOF
}
if err := d.reserve(uint64(count), 32); err != nil {
return err
}
if count != 0 {
animation.Motions = make([]U3DAnimationMotion, int(count))
}
for i := range animation.Motions {
if i%256 == 0 {
if err := d.ctx.Err(); err != nil {
return err
}
}
motion := &animation.Motions[i]
motion.Name, motion.Attributes = r.text(), r.u32()
motion.TimeOffset, motion.TimeScale = r.f32(), r.f32()
if r.err != nil {
return r.err
}
}
animation.BlendTime = r.f32()
if err := r.done(); err != nil {
return err
}
if err := animation.validate(d.ctx); err != nil {
return err
}
if chain == 1 {
mesh := &d.meshes[name].mesh
mesh.Animations = append(mesh.Animations, animation)
} else {
node := &d.model.Nodes[d.nodes[name]]
node.Animations = append(node.Animations, animation)
}
return nil
}
// validate 校验动画属性及有限数值,保留零倍率与反向播放倍率
// 入参: ctx 取消上下文
// 返回: error 参数或取消错误
func (animation U3DAnimation) validate(ctx context.Context) error {
if err := ctx.Err(); err != nil {
return err
}
if animation.Index == 0 || animation.Attributes & ^uint32(15) != 0 || !finite(float64(animation.TimeScale)) || !finite(float64(animation.BlendTime)) || animation.BlendTime < 0 {
return fmt.Errorf("invalid U3D animation parameters")
}
for i, motion := range animation.Motions {
if i%256 == 0 {
if err := ctx.Err(); err != nil {
return err
}
}
if motion.Attributes & ^uint32(3) != 0 || !finite(float64(motion.TimeOffset)) || !finite(float64(motion.TimeScale)) {
return fmt.Errorf("invalid U3D animation motion parameters")
}
}
return nil
}
// checkAnimations 校验全部动作引用,仅对接入场景的修饰器检查求值能力
// 返回: error 引用、能力或取消错误
func (r *u3dRender) checkAnimations() error {
check := func(animations []U3DAnimation, evaluate bool) error {
for _, animation := range animations {
if err := animation.validate(r.ctx); err != nil {
return err
}
for i, motion := range animation.Motions {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
if motion.Name == "" {
continue
}
index, exists := r.motionIndex[motion.Name]
if !exists {
return fmt.Errorf("missing U3D animation motion %q", motion.Name)
}
if evaluate && len(animation.Motions) > 1 && len(r.model.Motions[index].Tracks) != 0 {
return fmt.Errorf("unsupported U3D animation evaluation for motion %q", motion.Name)
}
}
}
return nil
}
var active []bool
if r.connected != nil {
if err := r.reserve(uint64(len(r.model.Meshes)), 1); err != nil {
return err
}
defer func() { r.remaining += uint64(len(active)) }()
active = make([]bool, len(r.model.Meshes))
for i, node := range r.model.Nodes {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
if r.connected[i] && node.Kind == "Model" {
if mesh, exists := r.meshes[node.Resource]; exists {
active[mesh] = true
}
}
}
}
for i := range r.model.Meshes {
if err := r.ctx.Err(); err != nil {
return err
}
if err := check(r.model.Meshes[i].Animations, active == nil || active[i]); err != nil {
return err
}
}
for i := range r.model.Nodes {
if err := r.ctx.Err(); err != nil {
return err
}
if err := check(r.model.Nodes[i].Animations, r.connected == nil || r.connected[i]); err != nil {
return err
}
}
return r.ctx.Err()
}
+21
View File
@@ -46,6 +46,27 @@ type u3dHistogram struct {
total uint32 total uint32
} }
// finish 校验压缩块的冲刷字段及结束位置
// 返回: error 截断、冲刷或多余数据错误
func (r *u3dBits) finish() error {
if r.err != nil {
return r.err
}
if r.pos > uint64(len(r.data))*8 {
return io.ErrUnexpectedEOF
}
if r.u32() != 0 {
return fmt.Errorf("invalid U3D compression flush")
}
if r.err != nil {
return r.err
}
if (r.pos-16+7)/8 != uint64(len(r.data)) {
return fmt.Errorf("unexpected U3D compressed block data")
}
return nil
}
// take 读取定长字段,失败后不继续推进位置 // take 读取定长字段,失败后不继续推进位置
// 入参: size 字节数 // 入参: size 字节数
// 返回: []byte 字段数据,失败时为空 // 返回: []byte 字段数据,失败时为空
+18 -4
View File
@@ -63,10 +63,13 @@ func (r *u3dRender) collectLights() error {
continue continue
} }
j, ok := r.lightIndex[node.Resource] j, ok := r.lightIndex[node.Resource]
if !ok { if !ok && node.Resource != "" {
continue continue
} }
light := r.model.Lights[j] light := U3DLight{Attributes: 1, Color: [3]float32{.75, .75, .75}}
if ok {
light = r.model.Lights[j]
}
if light.Attributes&1 == 0 { if light.Attributes&1 == 0 {
continue continue
} }
@@ -152,11 +155,21 @@ func (r *u3dRender) overrideLighting(lighting *U3DLighting) error {
return nil return nil
} }
// style 解析着色器与材质,未启用的材质项使用固定默认值 // shaderPass 返回着色器的绘制轮次位图,默认着色器仅参与第一轮
// 入参: name 着色器名
// 返回: uint32 绘制轮次位图
func (r *u3dRender) shaderPass(name string) uint32 {
if index, exists := r.shaders[name]; exists {
return r.model.Shaders[index].RenderPass
}
return 1
}
// style 解析着色器与材质,缺失资源采用ECMA-363第8.4节的默认资源
// 入参: name 着色器名 // 入参: name 着色器名
// 返回: u3dRenderStyle 有效绘制参数, error 参数错误 // 返回: u3dRenderStyle 有效绘制参数, error 参数错误
func (r *u3dRender) style(name string) (u3dRenderStyle, error) { func (r *u3dRender) style(name string) (u3dRenderStyle, error) {
style := u3dRenderStyle{shader: U3DShader{Blend: 0x606, AlphaFunction: 0x617, RenderPass: math.MaxUint32}, ambient: [3]float64{.2, .2, .2}, diffuse: [3]float64{.8, .8, .8}, opacity: 1} style := u3dRenderStyle{shader: U3DShader{Attributes: 1, Blend: 0x606, AlphaFunction: 0x617, RenderPass: 1}, ambient: [3]float64{.75, .75, .75}, opacity: 1}
if index, ok := r.shaders[name]; ok { if index, ok := r.shaders[name]; ok {
style.shader = r.model.Shaders[index] style.shader = r.model.Shaders[index]
} }
@@ -166,6 +179,7 @@ func (r *u3dRender) style(name string) (u3dRenderStyle, error) {
} }
if index, ok := r.materials[shader.Material]; ok { if index, ok := r.materials[shader.Material]; ok {
material := r.model.Materials[index] material := r.model.Materials[index]
style.ambient, style.diffuse = [3]float64{.2, .2, .2}, [3]float64{.8, .8, .8}
if material.Attributes > 63 { if material.Attributes > 63 {
return style, fmt.Errorf("invalid U3D render material") return style, fmt.Errorf("invalid U3D render material")
} }
+34 -9
View File
@@ -68,15 +68,13 @@ func (r *u3dRender) drawPrimitives(instance u3dInstance, node *U3DNode, mesh *U3
if err := r.ctx.Err(); err != nil { if err := r.ctx.Err(); err != nil {
return err return err
} }
style := u3dRenderStyle{shader: U3DShader{Blend: 0x606, RenderPass: math.MaxUint32}} style := u3dRenderStyle{shader: U3DShader{Blend: 0x606, RenderPass: r.shaderPass(name)}}
var err error var err error
if shaded { if shaded {
style, err = r.style(name) 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
@@ -84,15 +82,22 @@ func (r *u3dRender) drawPrimitives(instance u3dInstance, node *U3DNode, mesh *U3
if instance.opacity >= 0 || r.opacity >= 0 { if instance.opacity >= 0 || r.opacity >= 0 {
style.shader.Blend = 0x606 style.shader.Blend = 0x606
} }
textures, err := r.shaderTextures(style.shader)
if err != nil {
return err
}
for i, corner := range corners { for i, corner := range corners {
var normal u3dVector
if shaded && !mesh.ExcludeNormals {
normal = normals[corner.Normal]
}
if err := textures.coordinates(&vertices[i], mesh, shading, corner, normal); err != nil {
return err
}
vertices[i].color = [4]float64{r.auxiliaryColor[0], r.auxiliaryColor[1], r.auxiliaryColor[2], 1} vertices[i].color = [4]float64{r.auxiliaryColor[0], r.auxiliaryColor[1], r.auxiliaryColor[2], 1}
if !shaded { if !shaded {
continue continue
} }
var normal u3dVector
if !mesh.ExcludeNormals {
normal = normals[corner.Normal]
}
var diffuse, specular *[4]float32 var diffuse, specular *[4]float32
if shading.Attributes&1 != 0 { if shading.Attributes&1 != 0 {
diffuse = &mesh.Diffuse[corner.Diffuse] diffuse = &mesh.Diffuse[corner.Diffuse]
@@ -106,8 +111,9 @@ func (r *u3dRender) drawPrimitives(instance u3dInstance, node *U3DNode, mesh *U3
} }
if instance.opacity >= 0 { if instance.opacity >= 0 {
vertices[i].color[3] = instance.opacity vertices[i].color[3] = instance.opacity
vertices[i].opacityOverride = true
} }
if r.opacity >= 0 { if r.opacity >= 0 && textures == nil {
vertices[i].color[3] *= r.opacity vertices[i].color[3] *= r.opacity
} }
} }
@@ -226,7 +232,7 @@ func u3dClipFraction(a, b float64) float64 {
return a / (a + b) return a / (a + b)
} }
// u3dInterpolate 在相机空间插值坐标与未预乘颜色 // u3dInterpolate 在相机空间插值位置、颜色与纹理输入
// 入参: a 起点, b 终点, t 比例 // 入参: a 起点, b 终点, t 比例
// 返回: u3dVertex 插值顶点 // 返回: u3dVertex 插值顶点
func u3dInterpolate(a, b u3dVertex, t float64) u3dVertex { func u3dInterpolate(a, b u3dVertex, t float64) u3dVertex {
@@ -237,6 +243,18 @@ func u3dInterpolate(a, b u3dVertex, t float64) u3dVertex {
for i := range result.color { for i := range result.color {
result.color[i] = a.color[i]*(1-t) + b.color[i]*t result.color[i] = a.color[i]*(1-t) + b.color[i]*t
} }
result.textureMask = a.textureMask | b.textureMask
result.opacityOverride = a.opacityOverride
if result.textureMask != 0 {
for layer := range result.texture {
if result.textureMask&(1<<layer) == 0 {
continue
}
for c := range result.texture[layer] {
result.texture[layer][c] = a.texture[layer][c]*(1-t) + b.texture[layer][c]*t
}
}
}
return result return result
} }
@@ -344,6 +362,10 @@ func (r *u3dRender) rasterSectionLine(a, b u3dVertex, rgb [3]float64, normal, or
// 入参: a 已裁剪起点, b 已裁剪终点, normal 面法线, origin 面上点, shader 着色器, pass 绘制轮次 // 入参: a 已裁剪起点, b 已裁剪终点, normal 面法线, origin 面上点, shader 着色器, pass 绘制轮次
// 返回: error 数值、资源或取消错误 // 返回: error 数值、资源或取消错误
func (r *u3dRender) rasterLine(a, b u3dVertex, normal, origin u3dVector, shader U3DShader, pass U3DViewPass) error { func (r *u3dRender) rasterLine(a, b u3dVertex, normal, origin u3dVector, shader U3DShader, pass U3DViewPass) error {
textures, err := r.shaderTextures(shader)
if err != nil {
return err
}
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}}
@@ -413,6 +435,9 @@ func (r *u3dRender) rasterLine(a, b u3dVertex, normal, origin u3dVector, shader
depth -= 8 * math.Abs(depth-math.Nextafter(depth, math.Inf(-1))) depth -= 8 * math.Abs(depth-math.Nextafter(depth, math.Inf(-1)))
if depth <= r.frame[pixel].depth { if depth <= r.frame[pixel].depth {
vertex := u3dInterpolate(a, b, weight) vertex := u3dInterpolate(a, b, weight)
if err := r.textureColor(textures, &vertex.color, &vertex.texture, vertex.point, vertex.opacityOverride); err != nil {
return err
}
visible, err := u3dFragmentColor(&vertex.color, vertex.point, &shader, &pass) visible, err := u3dFragmentColor(&vertex.color, vertex.point, &shader, &pass)
if err != nil { if err != nil {
return err return err
+6 -14
View File
@@ -16,7 +16,6 @@ package ofdgo
import ( import (
"fmt" "fmt"
"io"
) )
// U3D线集增量编码的算术编码上下文 // U3D线集增量编码的算术编码上下文
@@ -94,9 +93,11 @@ func (d *u3dDecoder) primitiveDeclaration(r *u3dValues, name string, points bool
return fmt.Errorf("invalid U3D reserved primitive parameter") return fmt.Errorf("invalid U3D reserved primitive parameter")
} }
} }
if r.u32() != 0 { bones, err := d.skeleton(r)
return fmt.Errorf("unsupported U3D skeletal primitive set") if err != nil {
return err
} }
decl.mesh.Bones = bones
if err := r.done(); err != nil { if err := r.done(); err != nil {
return err return err
} }
@@ -444,17 +445,8 @@ func (d *u3dDecoder) primitiveContinuation(r *u3dValues, name string, points boo
} }
} }
if b := reader.bits; b != nil { if b := reader.bits; b != nil {
if b.pos > uint64(len(b.data))*8 { if err := b.finish(); err != nil {
return io.ErrUnexpectedEOF return err
}
if b.u32() != 0 {
return fmt.Errorf("invalid U3D primitive compression flush")
}
if b.err != nil {
return b.err
}
if (b.pos-16+7)/8 != uint64(len(b.data)) {
return fmt.Errorf("unexpected U3D primitive compressed data")
} }
r.pos = len(r.data) r.pos = len(r.data)
} }
+141
View File
@@ -0,0 +1,141 @@
// 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"
)
// u3dTextureInverse 计算包裹变换的逆矩阵,保留非仿射齐次分量
// 入参: matrix 按列排列的包裹变换
// 返回: [16]float64 逆矩阵, error 奇异矩阵或非有限结果
func u3dTextureInverse(matrix [16]float32) ([16]float64, error) {
var rows [4][8]float64
for row := range 4 {
for col := range 4 {
rows[row][col] = float64(matrix[col*4+row])
if !finite(rows[row][col]) {
return [16]float64{}, fmt.Errorf("invalid U3D texture wrap transform")
}
}
rows[row][row+4] = 1
}
for col := range 4 {
pivot := col
for row := col + 1; row < 4; row++ {
if math.Abs(rows[row][col]) > math.Abs(rows[pivot][col]) {
pivot = row
}
}
if rows[pivot][col] == 0 {
return [16]float64{}, fmt.Errorf("singular U3D texture wrap transform")
}
rows[col], rows[pivot] = rows[pivot], rows[col]
divisor := rows[col][col]
for i := range 8 {
rows[col][i] /= divisor
}
for row := range 4 {
if row == col {
continue
}
factor := rows[row][col]
for i := range 8 {
rows[row][i] -= factor * rows[col][i]
}
}
}
var result [16]float64
for row := range 4 {
for col := range 4 {
value := rows[row][col+4]
if !finite(value) {
return [16]float64{}, fmt.Errorf("invalid U3D inverse texture wrap transform")
}
result[col*4+row] = value
}
}
return result, nil
}
// generated 将插值后的位置或法线映射到纹理,再应用纹理变换
// 经线未定义时取0,球心纬度取1/2,反射方向奇点取图像中心
// 入参: channel 通道索引, input 模型位置或相机法线, position 相机空间位置
// 返回: [3]float64 齐次纹理坐标, error 非有限值或无效投影
func (s *u3dTextureSet) generated(channel int, input [3]float64, position u3dVector) ([3]float64, error) {
layer := s.layers[channel]
for _, value := range input {
if !finite(value) {
return [3]float64{}, fmt.Errorf("invalid U3D texture mapping input")
}
}
uv := [4]float64{0, 0, 0, 1}
if layer.Mode == 4 {
incident := u3dVector{0, 0, 1}
if s.perspective {
incident = position.unit()
}
normal := u3dVector(input).unit()
factor := 2 * normal.dot(incident)
reflected := incident.sub(u3dVector{factor * normal[0], factor * normal[1], factor * normal[2]})
denominator := 2 * math.Hypot(math.Hypot(reflected[0], reflected[1]), 1-reflected[2])
uv[0], uv[1] = .5, .5
if denominator != 0 {
uv[0] += reflected[0] / denominator
uv[1] -= reflected[1] / denominator
}
} else {
var point [4]float64
for row := range 4 {
point[row] = s.wrap[channel][12+row]
for col, value := range input {
point[row] += s.wrap[channel][col*4+row] * value
}
}
for i := range 3 {
point[i] /= point[3]
if !finite(point[i]) {
return [3]float64{}, fmt.Errorf("invalid U3D texture wrap projection")
}
}
switch layer.Mode {
case 1:
uv[0], uv[1] = point[0], point[1]
case 2, 3:
if point[0] != 0 || point[1] != 0 {
uv[0] = math.Atan2(point[1], point[0]) / (2 * math.Pi)
if uv[0] < 0 {
uv[0]++
}
}
uv[1] = point[2]
if layer.Mode == 3 {
direction := (u3dVector{point[0], point[1], point[2]}).unit()
uv[1] = math.Acos(min(1, max(-1, direction[2]))) / math.Pi
}
}
}
var result [3]float64
for component, row := range [3]int{0, 1, 3} {
for col, value := range uv {
result[component] += float64(layer.Transform[col*4+row]) * value
}
if !finite(result[component]) {
return [3]float64{}, fmt.Errorf("invalid U3D generated texture coordinate")
}
}
return result, nil
}
+21 -17
View File
@@ -29,6 +29,9 @@ type U3DMesh struct {
Faces []U3DFace Faces []U3DFace
Lines []U3DLine Lines []U3DLine
Points []U3DPoint Points []U3DPoint
Bones []U3DBone
BoneWeights []U3DBoneWeights
Animations []U3DAnimation
Quality [3]uint32 Quality [3]uint32
InverseQuantization [5]float32 InverseQuantization [5]float32
NormalCrease, NormalUpdate, NormalTolerance float32 NormalCrease, NormalUpdate, NormalTolerance float32
@@ -76,7 +79,7 @@ type u3dMeshDeclaration struct {
// primitive 访问三角面、线段或点的着色编号和角点,不复制属性数组 // primitive 访问三角面、线段或点的着色编号和角点,不复制属性数组
// 入参: index 图元下标,依次为三角面、线段和点 // 入参: index 图元下标,依次为三角面、线段和点
// 返回: uint32 着色编号, []U3DCorner 只读角点 // 返回: uint32 着色编号, []U3DCorner 原数组中的角点切片
func (m *U3DMesh) primitive(index int) (uint32, []U3DCorner) { func (m *U3DMesh) primitive(index int) (uint32, []U3DCorner) {
if index < len(m.Faces) { if index < len(m.Faces) {
return m.Faces[index].Shading, m.Faces[index].Corners[:] return m.Faces[index].Shading, m.Faces[index].Corners[:]
@@ -116,16 +119,20 @@ func (d *u3dDecoder) meshDeclaration(r *u3dValues, name string) error {
decl.mesh.InverseQuantization[index] = r.f32() decl.mesh.InverseQuantization[index] = r.f32()
} }
decl.mesh.NormalCrease, decl.mesh.NormalUpdate, decl.mesh.NormalTolerance = r.f32(), r.f32(), r.f32() decl.mesh.NormalCrease, decl.mesh.NormalUpdate, decl.mesh.NormalTolerance = r.f32(), r.f32(), r.f32()
bones := r.u32()
if r.err != nil { if r.err != nil {
return r.err return r.err
} }
if minimum > maximum || maximum != decl.counts[1] { if minimum > maximum || maximum != decl.counts[1] {
return fmt.Errorf("invalid U3D mesh resolution") return fmt.Errorf("invalid U3D mesh resolution")
} }
if minimum != maximum || bones != 0 { if minimum != maximum {
return fmt.Errorf("unsupported U3D progressive or skeletal mesh") return fmt.Errorf("unsupported U3D progressive mesh")
} }
bones, err := d.skeleton(r)
if err != nil {
return err
}
decl.mesh.Bones = bones
if minimum == 0 { if minimum == 0 {
for _, count := range decl.counts[:6] { for _, count := range decl.counts[:6] {
if count != 0 { if count != 0 {
@@ -247,10 +254,16 @@ func (d *u3dDecoder) baseMesh(r *u3dValues, name string) error {
var histogram u3dHistogram var histogram u3dHistogram
var compressed *u3dBits var compressed *u3dBits
if !d.plain && counts[0] != 0 { if !d.plain && counts[0] != 0 {
if err := d.reserve(1, 1<<20); err != nil { compressed = newU3DBits(r.data[r.pos:])
var charged uint64
compressed.reserve = func(count, size uint64) error {
if err := d.reserve(count, size); err != nil {
return err return err
} }
compressed = newU3DBits(r.data[r.pos:]) charged += count * size
return nil
}
defer func() { d.remaining += charged }()
} }
readIndex := func(count uint32) (uint32, error) { readIndex := func(count uint32) (uint32, error) {
var value uint32 var value uint32
@@ -321,17 +334,8 @@ func (d *u3dDecoder) baseMesh(r *u3dValues, name string) error {
} }
} }
if compressed != nil { if compressed != nil {
if compressed.pos > uint64(len(compressed.data))*8 { if err := compressed.finish(); err != nil {
return io.ErrUnexpectedEOF return err
}
if compressed.u32() != 0 {
return fmt.Errorf("invalid U3D compression flush")
}
if compressed.err != nil {
return compressed.err
}
if (compressed.pos-16+7)/8 != uint64(len(compressed.data)) {
return fmt.Errorf("unexpected U3D compressed block data: decoded %d bits, stored %d bytes", compressed.pos-16, len(compressed.data))
} }
r.pos = len(r.data) r.pos = len(r.data)
} }
+34 -5
View File
@@ -40,12 +40,20 @@ const (
// u3dPrimitiveKey 区分共享点线的颜色及混合状态,避免透明边点重复叠加 // u3dPrimitiveKey 区分共享点线的颜色及混合状态,避免透明边点重复叠加
type u3dPrimitiveKey struct { type u3dPrimitiveKey struct {
a, b u3dVertex a, b [7]float64
blend uint32 blend uint32
function uint32 function uint32
reference float32 reference float32
} }
// u3dTexturedPrimitiveKey 额外区分纹理坐标及纹理层,不扩大普通边点缓存
type u3dTexturedPrimitiveKey struct {
base u3dPrimitiveKey
coordinates [2][8][3]float64
layers [8]*U3DTextureLayer
opacityOverride bool
}
// uniquePrimitive 在当前网格内去重着色边点,缓存按条目计入绘制预算 // uniquePrimitive 在当前网格内去重着色边点,缓存按条目计入绘制预算
// 入参: a 起点或顶点, b 终点或相同顶点, shader 着色器 // 入参: a 起点或顶点, b 终点或相同顶点, shader 着色器
// 返回: bool 是否首次绘制, error 预算或取消错误 // 返回: bool 是否首次绘制, error 预算或取消错误
@@ -59,14 +67,29 @@ func (r *u3dRender) uniquePrimitive(a, b u3dVertex, shader U3DShader) (bool, err
if left[i] != right[i] { if left[i] != right[i] {
if left[i] > right[i] { if left[i] > right[i] {
a, b = b, a a, b = b, a
left, right = right, left
} }
break break
} }
} }
key := u3dPrimitiveKey{a: a, b: b, blend: shader.Blend} key := u3dPrimitiveKey{a: left, b: right, blend: shader.Blend}
if shader.Attributes&2 != 0 { if shader.Attributes&2 != 0 {
key.function, key.reference = shader.AlphaFunction, shader.AlphaReference key.function, key.reference = shader.AlphaFunction, shader.AlphaReference
} }
if shader.Textures != ([8]*U3DTextureLayer{}) {
textured := u3dTexturedPrimitiveKey{base: key, coordinates: [2][8][3]float64{a.texture, b.texture}, layers: shader.Textures, opacityOverride: a.opacityOverride}
if _, ok := r.texturedPrimitives[textured]; ok {
return false, nil
}
if err := r.reserve(1, 1024); err != nil {
return false, err
}
if r.texturedPrimitives == nil {
r.texturedPrimitives = make(map[u3dTexturedPrimitiveKey]struct{})
}
r.texturedPrimitives[textured] = struct{}{}
return true, nil
}
if _, ok := r.primitives[key]; ok { if _, ok := r.primitives[key]; ok {
return false, nil return false, nil
} }
@@ -221,6 +244,13 @@ func (r *u3dRender) modePoint(vertex u3dVertex, shader U3DShader, pass U3DViewPa
if vertex.point[2] > r.frame[pixel].depth { if vertex.point[2] > r.frame[pixel].depth {
return nil return nil
} }
textures, err := r.shaderTextures(shader)
if err != nil {
return err
}
if err := r.textureColor(textures, &vertex.color, &vertex.texture, vertex.point, vertex.opacityOverride); err != nil {
return err
}
visible, err := u3dFragmentColor(&vertex.color, vertex.point, &shader, &pass) visible, err := u3dFragmentColor(&vertex.color, vertex.point, &shader, &pass)
if err != nil || !visible { if err != nil || !visible {
return err return err
@@ -251,11 +281,10 @@ func (r *u3dRender) drawBounds(instance u3dInstance, node *U3DNode, mesh *U3DMes
} }
} }
if uint64(shading) >= uint64(len(shaders)) { if uint64(shading) >= uint64(len(shaders)) {
visible = true visible = visible || passIndex == 0
} else if !visible { } else if !visible {
for _, name := range shaders[shading] { for _, name := range shaders[shading] {
index, ok := r.shaders[name] if r.shaderPass(name)&(1<<passIndex) != 0 {
if !ok || r.model.Shaders[index].RenderPass&(1<<passIndex) != 0 {
visible = true visible = true
break break
} }
+188
View File
@@ -0,0 +1,188 @@
// 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"
)
// U3DMotion 保存骨骼或节点动作轨道,解码不执行动画
type U3DMotion struct {
Name string
TimeInverseQuantization, RotationInverseQuantization float32
Tracks []U3DMotionTrack
}
// U3DMotionTrack 保存命名轨道及其关键帧,保持文件中的时间与帧次序
type U3DMotionTrack struct {
Name string
DisplacementInverseQuantization, ScaleInverseQuantization float32
Frames []U3DKeyFrame
}
// U3DKeyFrame 保存关键帧时间及相对父项的变换,Rotation按W、X、Y、Z排列
type U3DKeyFrame struct {
Time float32
Displacement, Scale [3]float32
Rotation [4]float32
}
// motion 读取ECMA-363第9.8.6节的动作资源,首尾帧保留未量化数值
// 入参: values 字段读取器, name 资源名
// 返回: error 结构、数值、预算或取消错误
func (d *u3dDecoder) motion(values *u3dValues, name string) error {
r := u3dResourceReader{values: values, decoder: d}
defer r.release()
count := r.u32()
motion := U3DMotion{Name: name, TimeInverseQuantization: r.f32(), RotationInverseQuantization: r.f32()}
if err := r.err(); err != nil {
return err
}
if motion.TimeInverseQuantization < 0 || motion.RotationInverseQuantization < 0 {
return fmt.Errorf("invalid U3D motion quantization")
}
if err := d.reserve(uint64(count), 48); err != nil {
return err
}
motion.Tracks = make([]U3DMotionTrack, int(count))
for index := range motion.Tracks {
if err := d.ctx.Err(); err != nil {
return err
}
track := &motion.Tracks[index]
track.Name = r.text()
frames := r.u32()
track.DisplacementInverseQuantization, track.ScaleInverseQuantization = r.f32(), r.f32()
if err := r.err(); err != nil {
return err
}
if track.DisplacementInverseQuantization < 0 || track.ScaleInverseQuantization < 0 {
return fmt.Errorf("invalid U3D motion track quantization")
}
if err := d.reserve(uint64(frames), 44); err != nil {
return err
}
track.Frames = make([]U3DKeyFrame, int(frames))
for i := range track.Frames {
if i%256 == 0 {
if err := d.ctx.Err(); err != nil {
return err
}
}
frame := &track.Frames[i]
if i == 0 || i == len(track.Frames)-1 {
frame.Time = r.f32()
for component := range frame.Displacement {
frame.Displacement[component] = r.f32()
}
for component := range frame.Rotation {
frame.Rotation[component] = r.f32()
}
for component := range frame.Scale {
frame.Scale[component] = r.f32()
}
} else if err := r.motionFrame(frame, track.Frames[i-1], &motion, track); err != nil {
return err
}
if err := r.err(); err != nil {
return err
}
}
}
if err := r.done(); err != nil {
return err
}
d.model.Motions = append(d.model.Motions, motion)
return nil
}
// motionFrame 按前一帧预测时间、位移和缩放,并右乘旋转差分四元数
// 入参: frame 目标帧, previous 前一帧, motion 动作量化参数, track 轨道量化参数
// 返回: error 符号、数值或读取错误
func (r *u3dResourceReader) motionFrame(frame *U3DKeyFrame, previous U3DKeyFrame, motion *U3DMotion, track *U3DMotionTrack) error {
time, err := r.motionDifference([]float32{previous.Time}, motion.TimeInverseQuantization, 0)
if err != nil {
return err
}
frame.Time = time[0]
position, err := r.motionDifference(previous.Displacement[:], track.DisplacementInverseQuantization, 2)
if err != nil {
return err
}
copy(frame.Displacement[:], position[:])
signs := r.value(4, true)
if signs > 15 {
return fmt.Errorf("invalid U3D motion rotation signs")
}
var difference [4]float64
var squared float64
for component := 1; component < 4; component++ {
value := float64(r.value(5, false)) * float64(motion.RotationInverseQuantization)
if signs&(1<<component) != 0 {
value = -value
}
difference[component] = value
squared += value * value
}
if err := r.err(); err != nil {
return err
}
if squared > 1 {
return fmt.Errorf("invalid U3D motion rotation difference")
}
difference[0] = math.Sqrt(1 - squared)
if signs&1 != 0 {
difference[0] = -difference[0]
}
p := previous.Rotation
w, x, y, z := float64(p[0]), float64(p[1]), float64(p[2]), float64(p[3])
a, b, c, e := difference[0], difference[1], difference[2], difference[3]
frame.Rotation = [4]float32{
float32(w*a - x*b - y*c - z*e),
float32(w*b + x*a + y*e - z*c),
float32(w*c - x*e + y*a + z*b),
float32(w*e + x*c - y*b + z*a),
}
for _, value := range frame.Rotation {
if !finite(float64(value)) {
return fmt.Errorf("invalid U3D reconstructed rotation")
}
}
scale, err := r.motionDifference(previous.Scale[:], track.ScaleInverseQuantization, 6)
copy(frame.Scale[:], scale[:])
return err
}
// motionDifference 用共享幅值上下文恢复一至三个差分分量
// 入参: previous 前一帧分量, scale 逆量化系数, context 符号上下文,下一项为幅值上下文
// 返回: [3]float32 重建分量, error 符号、数值或读取错误
func (r *u3dResourceReader) motionDifference(previous []float32, scale float32, context int) (values [3]float32, err error) {
signs := r.value(context, true)
if signs >= 1<<len(previous) {
return values, fmt.Errorf("invalid U3D motion difference signs")
}
for index, prior := range previous {
difference := float64(r.value(context+1, false)) * float64(scale)
if signs&(1<<index) != 0 {
difference = -difference
}
values[index] = float32(float64(prior) + difference)
if !finite(float64(values[index])) {
return values, fmt.Errorf("invalid U3D reconstructed motion value")
}
}
return values, r.err()
}
+1 -2
View File
@@ -47,8 +47,7 @@ func (r *u3dRender) outlineFace(outline *u3dOutline, world [3]u3dVector, vertice
return err return err
} }
} }
index, ok := r.shaders[name] if r.shaderPass(name)&(1<<passIndex) != 0 {
if !ok || r.model.Shaders[index].RenderPass&(1<<passIndex) != 0 {
active = true active = true
break break
} }
+329
View File
@@ -0,0 +1,329 @@
// 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 (
"cmp"
"fmt"
"math"
"slices"
)
// u3dAnimationState 保存一次绘制中动画求值的节点变换、骨骼姿态及对应权重
type u3dAnimationState struct {
matrix u3dMatrix
bones []U3DBoneState
weights *U3DBoneWeights
}
// u3dPlaybackKey 标识一条轨道在指定动作时间的采样,不包含节点绑定
type u3dPlaybackKey struct {
track *U3DMotionTrack
time float64
}
// u3dPlaybackCache 在一次动画求值中共享轨道时间范围和采样结果
type u3dPlaybackCache struct {
render *u3dRender
bounds map[*U3DMotionTrack][2]float64
samples map[u3dPlaybackKey]U3DKeyFrame
bytes uint64
}
// limits 校验轨道时间并缓存范围,避免共享动作按节点数量重复扫描全部帧
// 入参: track 动作轨道
// 返回: [2]float64 首末时间, error 空轨道、数值、预算或取消错误
func (c *u3dPlaybackCache) limits(track *U3DMotionTrack) ([2]float64, error) {
if err := c.render.ctx.Err(); err != nil {
return [2]float64{}, err
}
if bounds, exists := c.bounds[track]; exists {
return bounds, nil
}
if len(track.Frames) == 0 {
return [2]float64{}, fmt.Errorf("empty U3D animation track")
}
bounds := [2]float64{math.Inf(1), math.Inf(-1)}
for i, frame := range track.Frames {
if i%256 == 0 {
if err := c.render.ctx.Err(); err != nil {
return bounds, err
}
}
if !finite(float64(frame.Time)) {
return bounds, fmt.Errorf("invalid U3D animation key frame time")
}
bounds[0], bounds[1] = min(bounds[0], float64(frame.Time)), max(bounds[1], float64(frame.Time))
}
if c.bounds == nil {
if err := c.render.reserve(1, 1024); err != nil {
return bounds, err
}
c.bytes += 1024
c.bounds = make(map[*U3DMotionTrack][2]float64)
}
if err := c.render.reserve(1, 96); err != nil {
return bounds, err
}
c.bytes += 96
c.bounds[track] = bounds
return bounds, nil
}
// sample 复用相同轨道和时间的姿态,缓存只在本次求值期间有效
// 入参: track 动作轨道, time 已换算的动作时间
// 返回: U3DKeyFrame 采样姿态, error 轨道、预算或取消错误
func (c *u3dPlaybackCache) sample(track *U3DMotionTrack, time float64) (U3DKeyFrame, error) {
if err := c.render.ctx.Err(); err != nil {
return U3DKeyFrame{}, err
}
key := u3dPlaybackKey{track, time}
if frame, exists := c.samples[key]; exists {
return frame, nil
}
frame, err := track.Sample(c.render.ctx, time, false)
if err != nil {
return frame, err
}
if c.samples == nil {
if err := c.render.reserve(1, 1024); err != nil {
return frame, err
}
c.bytes += 1024
c.samples = make(map[u3dPlaybackKey]U3DKeyFrame)
}
if err := c.render.reserve(1, 128); err != nil {
return frame, err
}
c.bytes += 128
c.samples[key] = frame
return frame, nil
}
// prepareAnimations 按资源链、节点链顺序求值,不保存上一帧状态或推进实际时钟
// 入参: time 从初始时刻起算的秒数
// 返回: error 动作、骨架、修饰顺序、预算或取消错误
func (r *u3dRender) prepareAnimations(time float64) error {
cache := u3dPlaybackCache{render: r}
defer func() { r.remaining += cache.bytes }()
for i := range r.model.Nodes {
if err := r.ctx.Err(); err != nil {
return err
}
if r.connected != nil && !r.connected[i] {
continue
}
node := &r.model.Nodes[i]
mesh := r.nodeMesh(node)
groups := [2][]U3DAnimation{nil, node.Animations}
if node.Kind == "Model" && mesh != nil {
groups[0] = mesh.Animations
}
state := u3dAnimationState{matrix: u3dIdentity()}
used := false
for chain, group := range groups {
ordered := group
if len(group) > 1 {
if err := r.reserve(uint64(len(group)), 48); err != nil {
return err
}
ordered = slices.Clone(group)
slices.SortFunc(ordered, func(a, b U3DAnimation) int { return cmp.Compare(a.Index, b.Index) })
}
for index, animation := range ordered {
if err := r.ctx.Err(); err != nil {
return err
}
if index != 0 && animation.Index == ordered[index-1].Index {
return fmt.Errorf("duplicate U3D animation chain index")
}
if len(animation.Motions) == 0 || animation.Motions[0].Name == "" {
continue
}
motion := &r.model.Motions[r.motionIndex[animation.Motions[0].Name]]
if len(motion.Tracks) == 0 {
continue
}
used = true
if err := r.sampleAnimation(&state, node, mesh, &animation, motion, chain, time, &cache); err != nil {
return err
}
}
if len(group) > 1 {
r.remaining += uint64(len(group)) * 48
}
}
if used {
if r.animated == nil {
if err := r.reserve(1, 1024); err != nil {
return err
}
r.animated = make(map[string]u3dAnimationState)
}
if err := r.reserve(1, 384); err != nil {
return err
}
r.animated[node.Name] = state
}
}
return r.ctx.Err()
}
// animationTime 将毫秒偏移和两级时间倍率换算为动作时间,已绑定轨道共用循环区间
// 入参: animation 动画修饰器, tracks 动作轨道, bindings 骨骼绑定,空值表示全部轨道, time 从初始时刻起算的秒数, cache 本次采样缓存
// 返回: float64 动作采样时间, error 时间数值或取消错误
func (r *u3dRender) animationTime(animation *U3DAnimation, tracks []U3DMotionTrack, bindings map[string]int, time float64, cache *u3dPlaybackCache) (float64, error) {
reference := animation.Motions[0]
if animation.Attributes&1 == 0 {
time = 0
}
time = time*float64(animation.TimeScale)*float64(reference.TimeScale) - float64(reference.TimeOffset)/1000
if !finite(time) {
return 0, fmt.Errorf("invalid U3D animation time")
}
minimum, maximum := math.Inf(1), math.Inf(-1)
for i, track := range tracks {
if err := r.ctx.Err(); err != nil {
return 0, err
}
if bindings != nil {
if index, exists := bindings[track.Name]; !exists || index != i {
continue
}
}
bounds, err := cache.limits(&tracks[i])
if err != nil {
return 0, err
}
minimum, maximum = min(minimum, bounds[0]), max(maximum, bounds[1])
}
if math.IsInf(minimum, 1) {
return 0, fmt.Errorf("empty U3D animation tracks")
}
if reference.Attributes&1 != 0 && maximum > minimum {
time = math.Mod(time-minimum, maximum-minimum)
if time < 0 {
time += maximum - minimum
}
time += minimum
}
return max(minimum, min(time, maximum)), r.ctx.Err()
}
// sampleAnimation 求值单个动作的节点或骨骼姿态,锁定根骨骼时保留其初始变换
// 入参: state 本节点累计状态, node 节点, mesh 模型几何, animation 修饰器, motion 动作, chain 链类别, time 时间, cache 本次采样缓存
// 返回: error 轨道、修饰顺序、预算或取消错误
func (r *u3dRender) sampleAnimation(state *u3dAnimationState, node *U3DNode, mesh *U3DMesh, animation *U3DAnimation, motion *U3DMotion, chain int, time float64, cache *u3dPlaybackCache) error {
if animation.Attributes&4 != 0 {
if chain == 0 {
return fmt.Errorf("unsupported U3D resource single-track animation")
}
time, err := r.animationTime(animation, motion.Tracks[:1], nil, time, cache)
if err != nil {
return err
}
frame, err := cache.sample(&motion.Tracks[0], time)
if err != nil {
return err
}
matrix, err := frame.Matrix()
if err != nil {
return err
}
if animation.Attributes&2 == 0 {
state.matrix = state.matrix.multiply(u3dMatrix(matrix))
if !state.matrix.valid() {
return fmt.Errorf("invalid U3D animated node transform")
}
}
return nil
}
if node.Kind != "Model" || mesh == nil || len(mesh.Bones) == 0 {
return fmt.Errorf("missing U3D animation skeleton")
}
if state.weights != nil {
return fmt.Errorf("unsupported U3D successive skeletal animations")
}
if err := r.reserve(uint64(len(mesh.Bones)), 96); err != nil {
return err
}
defer func() { r.remaining += uint64(len(mesh.Bones)) * 96 }()
tracks := make(map[string]int, len(mesh.Bones))
for i, bone := range mesh.Bones {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
if _, exists := tracks[bone.Name]; exists {
return fmt.Errorf("duplicate U3D bone %q", bone.Name)
}
tracks[bone.Name] = -1
}
bound := 0
for i, track := range motion.Tracks {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
if index, exists := tracks[track.Name]; exists {
if index >= 0 {
return fmt.Errorf("ambiguous U3D animation track %q", track.Name)
}
tracks[track.Name] = i
bound++
}
}
if bound == 0 {
return nil
}
time, err := r.animationTime(animation, motion.Tracks, tracks, time, cache)
if err != nil {
return err
}
if err := r.reserve(uint64(len(mesh.Bones)), 64); err != nil {
return err
}
state.bones = make([]U3DBoneState, 0, len(mesh.Bones))
for i, bone := range mesh.Bones {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
if i == 0 && animation.Attributes&2 != 0 {
continue
}
if index := tracks[bone.Name]; index >= 0 {
frame, err := cache.sample(&motion.Tracks[index], time)
if err != nil {
return err
}
state.bones = append(state.bones, U3DBoneState{Name: bone.Name, Frame: frame})
}
}
if len(state.bones) == 0 {
r.remaining += uint64(cap(state.bones)) * 64
state.bones = nil
return nil
}
resourceEnd, nodeEnd := uint64(animation.Index), uint64(0)
if chain != 0 {
resourceEnd, nodeEnd = math.MaxUint64, uint64(animation.Index)
}
state.weights, err = r.skinWeightSet(mesh, node.BoneWeights, resourceEnd, nodeEnd)
return err
}
+91 -33
View File
@@ -36,9 +36,12 @@ type U3DCamera struct {
// Camera为空时使用View指定的节点或首个视图节点,Instance指定其父路径实例 // Camera为空时使用View指定的节点或首个视图节点,Instance指定其父路径实例
// Lighting为空时沿用模型灯光,非空时替换全部模型灯光 // Lighting为空时沿用模型灯光,非空时替换全部模型灯光
// Opacity为空时沿用着色器混合,非空时叠加面片、着色边点及包围盒轮廓的透明度 // Opacity为空时沿用着色器混合,非空时叠加面片、着色边点及包围盒轮廓的透明度
// 纹理合成后先应用节点透明度,再应用Opacity
// Mode控制绘制方式,AuxiliaryColor用于固定颜色的边点,FaceColor用于包围盒面及插图面,空值分别使用黑色和背景色 // Mode控制绘制方式,AuxiliaryColor用于固定颜色的边点,FaceColor用于包围盒面及插图面,空值分别使用黑色和背景色
// CreaseAngle为轮廓夹角,单位为度,空值使用45;着色插图以漫反射色的1/4补充自发光 // CreaseAngle为轮廓夹角,单位为度,空值使用45;着色插图以漫反射色的1/4补充自发光
// Nodes按名称覆盖节点,重复名称使用最后一项,每次绘制均从原场景开始 // Nodes按名称覆盖节点,重复名称使用最后一项,每次绘制均从原场景开始
// Time以秒指定动画采样时刻,零值为初始时刻,不随实际时钟推进
// ResolveTexture提供外部纹理,空值不访问外部资源;回调自身的内存与超时由调用方控制
// Background为空时使用白色;缓存与实例上限为0时分别使用256MiB和65536,不含输入及运行时开销 // Background为空时使用白色;缓存与实例上限为0时分别使用256MiB和65536,不含输入及运行时开销
type U3DRenderOptions struct { type U3DRenderOptions struct {
Width, Height int Width, Height int
@@ -53,7 +56,9 @@ type U3DRenderOptions struct {
FaceColor *[3]float64 FaceColor *[3]float64
CreaseAngle *float64 CreaseAngle *float64
Nodes []U3DNodeState Nodes []U3DNodeState
Time float64
Sections []U3DCrossSection Sections []U3DCrossSection
ResolveTexture U3DTextureResolver
MaxMemoryBytes int64 MaxMemoryBytes int64
MaxInstances int MaxInstances int
} }
@@ -87,8 +92,15 @@ type u3dRender struct {
toCamera u3dMatrix toCamera u3dMatrix
scene []u3dInstance scene []u3dInstance
byNode [][]int byNode [][]int
connected []bool
members []bool members []bool
meshes, shaders, materials, lightIndex map[string]int meshes, shaders, materials, lightIndex map[string]int
textureIndex map[string]int
viewIndex, motionIndex map[string]int
textures map[string]*image.NRGBA64
textureSets map[[8]*U3DTextureLayer]*u3dTextureSet
resolveTexture U3DTextureResolver
view *U3DView
lights []u3dRenderLight lights []u3dRenderLight
lighting *U3DLighting lighting *U3DLighting
opacity float64 opacity float64
@@ -97,7 +109,10 @@ type u3dRender struct {
auxiliaryColor, faceColor [3]float64 auxiliaryColor, faceColor [3]float64
depthOnly bool depthOnly bool
primitives map[u3dPrimitiveKey]struct{} primitives map[u3dPrimitiveKey]struct{}
texturedPrimitives map[u3dTexturedPrimitiveKey]struct{}
states map[string]U3DNodeState states map[string]U3DNodeState
posed map[string]*U3DMesh
animated map[string]u3dAnimationState
sections []u3dSection sections []u3dSection
sectionScratch [2][]u3dVertex sectionScratch [2][]u3dVertex
chunks [][]u3dFragment chunks [][]u3dFragment
@@ -106,8 +121,16 @@ type u3dRender struct {
order uint64 order uint64
} }
// RenderU3D 绘制静态无纹理网格、线集、点集及包围盒,返回不透明图像,不执行脚本或动画 // RenderU3D 绘制几何及视图纹理层,返回指定时刻的不透明图像,不执行脚本
// 采用逐顶点光照及逐像素深度混合,缺失资源回退为空网格、灰色材质或无光源 // 可通过节点覆盖指定动作轨道与采样时间,逐次生成独立姿态,不修改原模型
// 显式骨骼姿态按权重变换位置和法线,暂不生成骨骼链接或推导缺失权重
// 动画修饰器支持单动作的节点和骨骼求值,尚未实现的动作衔接返回明确错误
// 几何纹理支持模型坐标与程序化映射,参考形状约定见U3DTextureLayer
// 纹理按需解码并复用,不自动访问外部图像
// 采用逐顶点光照及逐像素深度混合,空名称引用标准默认资源
// 缺失几何、材质和着色器采用默认资源,缺失的非空光源引用不产生光照
// 缺失纹理返回错误,空纹理名称使用标准默认图像
// 仅绘制默认根的后代,非空父节点引用缺失时不展开该父路径
// 入参: ctx 取消上下文, model 场景, options 视图、输出与资源限制 // 入参: ctx 取消上下文, model 场景, options 视图、输出与资源限制
// 返回: *image.RGBA 图像, error 参数、能力、预算或取消错误 // 返回: *image.RGBA 图像, error 参数、能力、预算或取消错误
func RenderU3D(ctx context.Context, model *U3DModel, options U3DRenderOptions) (*image.RGBA, error) { func RenderU3D(ctx context.Context, model *U3DModel, options U3DRenderOptions) (*image.RGBA, error) {
@@ -123,17 +146,17 @@ func RenderU3D(ctx context.Context, model *U3DModel, options U3DRenderOptions) (
} }
// newU3DRender 建立有预算限制的场景实例与资源索引 // newU3DRender 建立有预算限制的场景实例与资源索引
// 入参: ctx 取消上下文, model 场景, options 输出与资源限制 // 入参: ctx 取消上下文, model 场景, options 输出与资源限制, states 后续节点覆盖分组
// 返回: *u3dRender 绘制状态, error 参数、场景、预算或取消错误 // 返回: *u3dRender 绘制状态, error 参数、场景、预算或取消错误
func newU3DRender(ctx context.Context, model *U3DModel, options U3DRenderOptions) (*u3dRender, error) { func newU3DRender(ctx context.Context, model *U3DModel, options U3DRenderOptions, states ...[]U3DNodeState) (*u3dRender, error) {
if ctx == nil || model == nil || options.Width <= 0 || options.Height <= 0 || options.MaxMemoryBytes < 0 || options.MaxInstances < 0 || options.Instance < 0 { if ctx == nil || model == nil || options.Width <= 0 || options.Height <= 0 || options.MaxMemoryBytes < 0 || options.MaxInstances < 0 || options.Instance < 0 || !finite(options.Time) {
return nil, fmt.Errorf("invalid U3D render options") return nil, fmt.Errorf("invalid U3D render options")
} }
limit := options.MaxMemoryBytes limit := options.MaxMemoryBytes
if limit == 0 { if limit == 0 {
limit = u3dDefaultBytes limit = u3dDefaultBytes
} }
r := &u3dRender{ctx: ctx, model: model, remaining: uint64(limit), opacity: -1, mode: options.Mode, creaseAngle: 45} r := &u3dRender{ctx: ctx, model: model, remaining: uint64(limit), opacity: -1, mode: options.Mode, creaseAngle: 45, resolveTexture: options.ResolveTexture}
if options.Mode > U3DRenderShadedIllustration { if options.Mode > U3DRenderShadedIllustration {
return nil, fmt.Errorf("unsupported U3D render mode") return nil, fmt.Errorf("unsupported U3D render mode")
} }
@@ -157,23 +180,37 @@ func newU3DRender(ctx context.Context, model *U3DModel, options U3DRenderOptions
if err := r.reserve(pixels, 60); err != nil { if err := r.reserve(pixels, 60); err != nil {
return nil, err return nil, err
} }
if err := r.resources(); err != nil {
return nil, err
}
order, index, err := r.sceneGraph()
if err != nil {
return nil, err
}
if err := r.checkAnimations(); err != nil {
return nil, err
}
maxInstances := options.MaxInstances maxInstances := options.MaxInstances
if maxInstances == 0 { if maxInstances == 0 {
maxInstances = 65536 maxInstances = 65536
} }
if err := r.nodeStates(options.Nodes); err != nil { groups := append([][]U3DNodeState{options.Nodes}, states...)
if err := r.nodeStates(groups...); err != nil {
return nil, err return nil, err
} }
if err := r.instances(maxInstances); err != nil { if err := r.prepareAnimations(options.Time); err != nil {
return nil, err
}
if err := r.instances(order, index, maxInstances); err != nil {
return nil, err
}
if err := r.poseNodes(); err != nil {
return nil, err return nil, err
} }
if err := r.reserve(uint64(len(r.scene)), 1); err != nil { if err := r.reserve(uint64(len(r.scene)), 1); err != nil {
return nil, err return nil, err
} }
r.members = make([]bool, len(r.scene)) r.members = make([]bool, len(r.scene))
if err := r.resources(); err != nil {
return nil, err
}
return r, nil return r, nil
} }
@@ -223,11 +260,21 @@ func (r *u3dRender) render(options U3DRenderOptions, passes []U3DViewPass) (*ima
} }
r.frame[i].color = background r.frame[i].color = background
} }
if r.view != nil {
if err := r.viewLayers(r.view.Backdrops); err != nil {
return nil, err
}
}
for passIndex, pass := range passes { for passIndex, pass := range passes {
if err := r.renderPass(pass, passIndex, passIndex == len(passes)-1); err != nil { if err := r.renderPass(pass, passIndex, passIndex == len(passes)-1); err != nil {
return nil, err return nil, err
} }
} }
if r.view != nil {
if err := r.viewLayers(r.view.Overlays); err != nil {
return nil, err
}
}
for i, pixel := range r.frame { for i, pixel := range r.frame {
if i%4096 == 0 { if i%4096 == 0 {
if err := r.ctx.Err(); err != nil { if err := r.ctx.Err(); err != nil {
@@ -263,6 +310,7 @@ func (r *u3dRender) reserve(count, size uint64) error {
// 入参: options 绘制选项 // 入参: options 绘制选项
// 返回: []U3DViewPass 绘制轮次, error 视图或投影错误 // 返回: []U3DViewPass 绘制轮次, error 视图或投影错误
func (r *u3dRender) selectView(options U3DRenderOptions) ([]U3DViewPass, error) { func (r *u3dRender) selectView(options U3DRenderOptions) ([]U3DViewPass, error) {
r.view = nil
r.viewport = [4]float64{0, 0, float64(options.Width), float64(options.Height)} r.viewport = [4]float64{0, 0, float64(options.Width), float64(options.Height)}
selected := -1 selected := -1
for i, node := range r.model.Nodes { for i, node := range r.model.Nodes {
@@ -286,23 +334,12 @@ func (r *u3dRender) selectView(options U3DRenderOptions) ([]U3DViewPass, error)
return nil, fmt.Errorf("invalid U3D view node") return nil, fmt.Errorf("invalid U3D view node")
} }
view := node.View view := node.View
if len(view.Backdrops) != 0 || len(view.Overlays) != 0 { if view.Attributes & ^uint32(7) != 0 {
return nil, fmt.Errorf("unsupported U3D textured view layers") return nil, fmt.Errorf("invalid U3D view attributes")
}
found := false
for i, resource := range r.model.Views {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return nil, err
}
}
if resource.Name == node.Resource {
if found {
return nil, fmt.Errorf("duplicate U3D view resource")
}
passes = resource.Passes
found = true
} }
r.view = view
if i, exists := r.viewIndex[node.Resource]; exists {
passes = r.model.Views[i].Passes
} }
if options.Camera == nil { if options.Camera == nil {
if options.Instance >= len(r.byNode[selected]) { if options.Instance >= len(r.byNode[selected]) {
@@ -374,10 +411,10 @@ func (r *u3dRender) selectView(options U3DRenderOptions) ([]U3DViewPass, error)
return passes, nil return passes, nil
} }
// resources 建立只读资源索引,不按名称反复扫描场景 // resources 建立只读资源索引,空名称保留给标准默认资源
// 返回: error 重名或预算错误 // 返回: error 名称、预算或取消错误
func (r *u3dRender) resources() error { func (r *u3dRender) resources() error {
count := uint64(len(r.model.Meshes)) + uint64(len(r.model.Shaders)) + uint64(len(r.model.Materials)) + uint64(len(r.model.Lights)) count := uint64(len(r.model.Meshes)) + uint64(len(r.model.Shaders)) + uint64(len(r.model.Materials)) + uint64(len(r.model.Lights)) + uint64(len(r.model.Textures)) + uint64(len(r.model.Views)) + uint64(len(r.model.Motions))
if err := r.reserve(count, 96); err != nil { if err := r.reserve(count, 96); err != nil {
return err return err
} }
@@ -385,12 +422,18 @@ func (r *u3dRender) resources() error {
r.shaders = make(map[string]int, len(r.model.Shaders)) r.shaders = make(map[string]int, len(r.model.Shaders))
r.materials = make(map[string]int, len(r.model.Materials)) r.materials = make(map[string]int, len(r.model.Materials))
r.lightIndex = make(map[string]int, len(r.model.Lights)) r.lightIndex = make(map[string]int, len(r.model.Lights))
r.textureIndex = make(map[string]int, len(r.model.Textures))
r.viewIndex = make(map[string]int, len(r.model.Views))
r.motionIndex = make(map[string]int, len(r.model.Motions))
add := func(index map[string]int, name string, i int) error { add := func(index map[string]int, name string, i int) error {
if i%256 == 0 { if i%256 == 0 {
if err := r.ctx.Err(); err != nil { if err := r.ctx.Err(); err != nil {
return err return err
} }
} }
if name == "" {
return fmt.Errorf("cannot redefine U3D default resource")
}
if _, ok := index[name]; ok { if _, ok := index[name]; ok {
return fmt.Errorf("duplicate U3D render resource %q", name) return fmt.Errorf("duplicate U3D render resource %q", name)
} }
@@ -417,6 +460,21 @@ func (r *u3dRender) resources() error {
return err return err
} }
} }
for i, v := range r.model.Textures {
if err := add(r.textureIndex, v.Name, i); err != nil {
return err
}
}
for i, v := range r.model.Views {
if err := add(r.viewIndex, v.Name, i); err != nil {
return err
}
}
for i, v := range r.model.Motions {
if err := add(r.motionIndex, v.Name, i); err != nil {
return err
}
}
return nil return nil
} }
@@ -494,11 +552,11 @@ func (r *u3dRender) drawScene(pass U3DViewPass, passIndex int) error {
if node.Visibility > 3 { if node.Visibility > 3 {
return fmt.Errorf("invalid U3D model visibility") return fmt.Errorf("invalid U3D model visibility")
} }
mesh, ok := r.meshes[node.Resource] mesh := r.nodeMesh(node)
if !ok { if mesh == nil {
continue continue
} }
if err := r.drawMesh(instance, node, &r.model.Meshes[mesh], pass, passIndex); err != nil { if err := r.drawMesh(instance, node, mesh, pass, passIndex); err != nil {
return err return err
} }
} }
+141
View File
@@ -0,0 +1,141 @@
// 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"
"unicode/utf8"
)
// u3dResourceReader 读取骨骼权重与动作资源中交错的普通字段和压缩字段
type u3dResourceReader struct {
values *u3dValues
decoder *u3dDecoder
bits *u3dBits
hist [8]u3dHistogram
charged uint64
}
// value 读取指定上下文的压缩字段,首次使用时初始化算术区间
// 入参: context 压缩上下文, small 是否为8位字段
// 返回: uint32 字段值
func (r *u3dResourceReader) value(context int, small bool) uint32 {
if r.err() != nil {
return 0
}
if r.decoder.plain {
if small {
return uint32(r.values.u8())
}
return r.values.u32()
}
if r.bits == nil {
r.bits = newU3DBits(r.values.data[r.values.pos:])
r.bits.reserve = func(count, size uint64) error {
if err := r.decoder.reserve(count, size); err != nil {
return err
}
r.charged += count * size
return nil
}
}
return r.bits.dynamicValue(&r.hist[context], small)
}
// u32 读取普通32位字段,压缩开始后沿用当前算术区间
// 返回: uint32 字段值
func (r *u3dResourceReader) u32() uint32 {
if r.bits != nil {
return r.bits.u32()
}
return r.values.u32()
}
// f32 读取有限单精度数值
// 返回: float32 字段值
func (r *u3dResourceReader) f32() float32 {
value := math.Float32frombits(r.u32())
if !finite(float64(value)) && r.err() == nil {
r.values.err = fmt.Errorf("invalid U3D floating-point value")
}
return value
}
// text 读取UTF-8名称,压缩开始后逐字节解码而不重置区间
// 返回: string 名称
func (r *u3dResourceReader) text() string {
if r.err() != nil {
return ""
}
if r.bits == nil {
return r.values.text()
}
length := int(r.bits.byteValue()) | int(r.bits.byteValue())<<8
if r.err() != nil {
return ""
}
if err := r.decoder.reserve(uint64(length), 2); err != nil {
r.values.err = err
return ""
}
data := make([]byte, length)
for index := range data {
if index%1024 == 0 {
if err := r.decoder.ctx.Err(); err != nil {
r.values.err = err
return ""
}
}
data[index] = r.bits.byteValue()
}
if r.err() == nil && !utf8.Valid(data) {
r.values.err = fmt.Errorf("invalid U3D UTF-8 string")
}
return string(data)
}
// err 返回字段或算术解码错误
// 返回: error 读取错误
func (r *u3dResourceReader) err() error {
if r.values.err != nil {
return r.values.err
}
if r.bits != nil {
return r.bits.err
}
return nil
}
// done 校验末尾冲刷字段和块边界
// 返回: error 读取或多余数据错误
func (r *u3dResourceReader) done() error {
if err := r.err(); err != nil {
return err
}
if r.bits != nil {
if err := r.bits.finish(); err != nil {
return err
}
r.values.pos = len(r.values.data)
}
return r.values.done()
}
// release 归还当前块压缩上下文占用的临时预算
func (r *u3dResourceReader) release() {
r.decoder.remaining += r.charged
r.charged = 0
}
+215
View File
@@ -0,0 +1,215 @@
// 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"
"math"
)
// U3DTrackState 指定动作资源、轨道和采样时间,Time与关键帧使用相同单位
// Loop将时间循环到首末帧之间,未启用时停留在端点,不自动解释动画修饰器的时间参数
type U3DTrackState struct {
Motion, Track string
Time float64
Loop bool
}
// Sample 按时间采样轨道,不修改原始帧或按帧数量分配临时数组
// 位移和缩放采用线性插值,旋转归一化后采用最短弧球面插值
// 允许帧乱序,同一时刻取文件中最后一帧;循环区间不包含末端,单帧轨道始终返回该帧
// 入参: ctx 取消上下文, time 与关键帧同单位的时间, loop 是否循环
// 返回: U3DKeyFrame 采样姿态, error 空轨道、数值或取消错误
func (track U3DMotionTrack) Sample(ctx context.Context, time float64, loop bool) (U3DKeyFrame, error) {
if ctx == nil || !finite(time) || len(track.Frames) == 0 {
return U3DKeyFrame{}, fmt.Errorf("invalid U3D motion sample")
}
first, last := 0, 0
for index, frame := range track.Frames {
if index%256 == 0 {
if err := ctx.Err(); err != nil {
return U3DKeyFrame{}, err
}
}
if !finite(float64(frame.Time)) {
return U3DKeyFrame{}, fmt.Errorf("invalid U3D key frame time")
}
if frame.Time <= track.Frames[first].Time {
first = index
}
if frame.Time >= track.Frames[last].Time {
last = index
}
}
minimum, maximum := float64(track.Frames[first].Time), float64(track.Frames[last].Time)
if loop && maximum > minimum {
time = math.Mod(time-minimum, maximum-minimum)
if time < 0 {
time += maximum - minimum
}
time += minimum
} else {
time = max(minimum, min(time, maximum))
}
lower, upper := first, last
for index, frame := range track.Frames {
if index%256 == 0 {
if err := ctx.Err(); err != nil {
return U3DKeyFrame{}, err
}
}
value := float64(frame.Time)
if value <= time && frame.Time >= track.Frames[lower].Time {
lower = index
}
if value > time && frame.Time <= track.Frames[upper].Time {
upper = index
}
}
a, b := track.Frames[lower], track.Frames[upper]
if float64(a.Time) == time {
b = a
}
qa, err := a.quaternion()
if err != nil {
return U3DKeyFrame{}, err
}
qb, err := b.quaternion()
if err != nil {
return U3DKeyFrame{}, err
}
fraction := float64(0)
if b.Time != a.Time {
fraction = (time - float64(a.Time)) / (float64(b.Time) - float64(a.Time))
}
result := U3DKeyFrame{Time: float32(time)}
for i := range result.Displacement {
result.Displacement[i] = float32(float64(a.Displacement[i])*(1-fraction) + float64(b.Displacement[i])*fraction)
result.Scale[i] = float32(float64(a.Scale[i])*(1-fraction) + float64(b.Scale[i])*fraction)
}
var dot float64
for i := range qa {
dot += qa[i] * qb[i]
}
if dot < 0 {
dot = -dot
for i := range qb {
qb[i] = -qb[i]
}
}
left, right := 1-fraction, fraction
if dot < .9995 {
angle := math.Acos(min(1, dot))
divisor := math.Sin(angle)
left, right = math.Sin(left*angle)/divisor, math.Sin(right*angle)/divisor
}
var rotation [4]float64
var length float64
for i := range rotation {
rotation[i] = left*qa[i] + right*qb[i]
length += rotation[i] * rotation[i]
}
length = math.Sqrt(length)
for i := range result.Rotation {
result.Rotation[i] = float32(rotation[i] / length)
}
if err := ctx.Err(); err != nil {
return U3DKeyFrame{}, err
}
return result, nil
}
// quaternion 校验姿态分量并归一化W、X、Y、Z旋转四元数
// 返回: [4]float64 单位四元数, error 非有限或零旋转错误
func (frame U3DKeyFrame) quaternion() ([4]float64, error) {
var rotation [4]float64
for _, values := range [][3]float32{frame.Displacement, frame.Scale} {
for _, value := range values {
if !finite(float64(value)) {
return rotation, fmt.Errorf("invalid U3D key frame transform")
}
}
}
var length float64
for i, value := range frame.Rotation {
rotation[i] = float64(value)
length += rotation[i] * rotation[i]
}
if !finite(length) || length == 0 {
return rotation, fmt.Errorf("invalid U3D key frame rotation")
}
length = math.Sqrt(length)
for i := range rotation {
rotation[i] /= length
}
return rotation, nil
}
// Matrix 将关键帧位移、旋转和缩放组合成相对父项的列式仿射矩阵,不使用Time
// 返回: [16]float64 先缩放、再旋转、后平移的变换, error 姿态数值错误
func (frame U3DKeyFrame) Matrix() ([16]float64, error) {
q, err := frame.quaternion()
if err != nil {
return [16]float64{}, err
}
w, x, y, z := q[0], q[1], q[2], q[3]
matrix := [16]float64{
1 - 2*(y*y+z*z), 2 * (x*y + w*z), 2 * (x*z - w*y), 0,
2 * (x*y - w*z), 1 - 2*(x*x+z*z), 2 * (y*z + w*x), 0,
2 * (x*z + w*y), 2 * (y*z - w*x), 1 - 2*(x*x+y*y), 0,
float64(frame.Displacement[0]), float64(frame.Displacement[1]), float64(frame.Displacement[2]), 1,
}
for axis, scale := range frame.Scale {
for component := range 3 {
matrix[axis*4+component] *= float64(scale)
}
}
return matrix, nil
}
// trackMatrix 按资源和轨道名称采样节点变换,不推测未命名轨道的绑定关系
// 入参: state 动作轨道状态
// 返回: [16]float64 节点变换, error 引用、数值或取消错误
func (r *u3dRender) trackMatrix(state U3DTrackState) ([16]float64, error) {
var track *U3DMotionTrack
if err := r.ctx.Err(); err != nil {
return [16]float64{}, err
}
if index, exists := r.motionIndex[state.Motion]; exists {
motion := &r.model.Motions[index]
for i := range motion.Tracks {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return [16]float64{}, err
}
}
if motion.Tracks[i].Name == state.Track {
if track != nil {
return [16]float64{}, fmt.Errorf("ambiguous U3D motion track %q", state.Track)
}
track = &motion.Tracks[i]
}
}
}
if track == nil {
return [16]float64{}, fmt.Errorf("missing U3D motion track %q in %q", state.Track, state.Motion)
}
frame, err := track.Sample(r.ctx, state.Time, state.Loop)
if err != nil {
return [16]float64{}, err
}
return frame.Matrix()
}
+70 -32
View File
@@ -174,42 +174,42 @@ func u3dTriangleNormal(points [3]u3dVector) u3dVector {
return normal.unit() return normal.unit()
} }
// instances 按拓扑顺序构建全部父路径实例,限制共享层级的指数展开 // sceneGraph 校验父子关系并确定拓扑顺序,只有默认根的后代接入场景
// 入参: limit 最大实例数 // 返回: []int 拓扑顺序, map[string]int 节点索引, error 结构、预算或取消错误
// 返回: error 结构、预算、实例上限或取消错误 func (r *u3dRender) sceneGraph() ([]int, map[string]int, error) {
func (r *u3dRender) instances(limit int) error {
nodes := r.model.Nodes nodes := r.model.Nodes
if err := r.reserve(uint64(len(nodes)), 160); err != nil { if err := r.reserve(uint64(len(nodes)), 161); err != nil {
return err return nil, nil, err
} }
index := make(map[string]int, len(nodes)) index := make(map[string]int, len(nodes))
for i, node := range nodes { for i, node := range nodes {
if i%256 == 0 { if i%256 == 0 {
if err := r.ctx.Err(); err != nil { if err := r.ctx.Err(); err != nil {
return err return nil, nil, err
} }
} }
if _, exists := index[node.Name]; exists || node.Name == "" { if _, exists := index[node.Name]; exists || node.Name == "" {
return fmt.Errorf("invalid U3D render node name") return nil, nil, fmt.Errorf("invalid U3D render node name")
} }
switch node.Kind { switch node.Kind {
case "Group", "Model", "Light", "View": case "Group", "Model", "Light", "View":
default: default:
return fmt.Errorf("unsupported U3D render node kind") return nil, nil, fmt.Errorf("unsupported U3D render node kind")
} }
index[node.Name] = i index[node.Name] = i
} }
counts := make([]int, len(nodes)) counts := make([]int, len(nodes))
children := make([][]int, len(nodes)) children := make([][]int, len(nodes))
r.byNode = make([][]int, len(nodes)) r.byNode = make([][]int, len(nodes))
r.connected = make([]bool, len(nodes))
for i, node := range nodes { for i, node := range nodes {
if err := r.reserve(uint64(len(node.Parents)), 16); err != nil { if err := r.reserve(uint64(len(node.Parents)), 16); err != nil {
return err return nil, nil, err
} }
for k, parent := range node.Parents { for k, parent := range node.Parents {
if k%256 == 0 { if k%256 == 0 {
if err := r.ctx.Err(); err != nil { if err := r.ctx.Err(); err != nil {
return err return nil, nil, err
} }
} }
if j, ok := index[parent.Name]; ok { if j, ok := index[parent.Name]; ok {
@@ -222,7 +222,7 @@ func (r *u3dRender) instances(limit int) error {
for i, n := range counts { for i, n := range counts {
if i%256 == 0 { if i%256 == 0 {
if err := r.ctx.Err(); err != nil { if err := r.ctx.Err(); err != nil {
return err return nil, nil, err
} }
} }
if n == 0 { if n == 0 {
@@ -231,9 +231,54 @@ func (r *u3dRender) instances(limit int) error {
} }
for cursor := 0; cursor < len(queue); cursor++ { for cursor := 0; cursor < len(queue); cursor++ {
if err := r.ctx.Err(); err != nil { if err := r.ctx.Err(); err != nil {
return err return nil, nil, err
} }
i := queue[cursor] i := queue[cursor]
for j, parent := range nodes[i].Parents {
if j%256 == 0 {
if err := r.ctx.Err(); err != nil {
return nil, nil, err
}
}
if parent.Name == "" {
r.connected[i] = true
break
}
if p, exists := index[parent.Name]; exists && r.connected[p] {
r.connected[i] = true
break
}
}
for childIndex, child := range children[i] {
if childIndex%256 == 0 {
if err := r.ctx.Err(); err != nil {
return nil, nil, err
}
}
counts[child]--
if counts[child] == 0 {
queue = append(queue, child)
}
}
}
if len(queue) != len(nodes) {
return nil, nil, fmt.Errorf("cyclic U3D node parents")
}
return queue, index, nil
}
// instances 按拓扑顺序展开接入场景的父路径,不将未解析的父节点视为默认根
// 入参: order 拓扑顺序, index 节点索引, limit 最大实例数
// 返回: error 变换、预算、实例上限或取消错误
func (r *u3dRender) instances(order []int, index map[string]int, limit int) error {
nodes := r.model.Nodes
for _, i := range order {
if err := r.ctx.Err(); err != nil {
return err
}
if !r.connected[i] {
continue
}
state := r.states[nodes[i].Name] state := r.states[nodes[i].Name]
for parentIndex, parent := range nodes[i].Parents { for parentIndex, parent := range nodes[i].Parents {
if parentIndex%256 == 0 { if parentIndex%256 == 0 {
@@ -241,20 +286,27 @@ func (r *u3dRender) instances(limit int) error {
return err return err
} }
} }
parents := []int{-1}
if parent.Name != "" {
j, exists := index[parent.Name]
if !exists || !r.connected[j] {
continue
}
parents = r.byNode[j]
}
var local u3dMatrix var local u3dMatrix
for k, v := range parent.Transform { for k, v := range parent.Transform {
local[k] = float64(v) local[k] = float64(v)
} }
if animation, exists := r.animated[nodes[i].Name]; exists {
local = local.multiply(animation.matrix)
}
if state.Matrix != nil { if state.Matrix != nil {
local = *state.Matrix local = *state.Matrix
} }
if !local.valid() { if !local.valid() {
return fmt.Errorf("unsupported U3D non-affine node transform") return fmt.Errorf("unsupported U3D non-affine node transform")
} }
parents := []int{-1}
if j, ok := index[parent.Name]; ok {
parents = r.byNode[j]
}
for _, p := range parents { for _, p := range parents {
if len(r.scene) >= limit { if len(r.scene) >= limit {
return fmt.Errorf("U3D instance count exceeds limit") return fmt.Errorf("U3D instance count exceeds limit")
@@ -284,20 +336,6 @@ func (r *u3dRender) instances(limit int) error {
r.scene = append(r.scene, u3dInstance{node: i, parent: p, world: world, opacity: opacity, visibility: visibility}) r.scene = append(r.scene, u3dInstance{node: i, parent: p, world: world, opacity: opacity, visibility: visibility})
} }
} }
for childIndex, child := range children[i] {
if childIndex%256 == 0 {
if err := r.ctx.Err(); err != nil {
return err
} }
} return r.ctx.Err()
counts[child]--
if counts[child] == 0 {
queue = append(queue, child)
}
}
}
if len(queue) != len(nodes) {
return fmt.Errorf("cyclic U3D node parents")
}
return nil
} }
+4 -5
View File
@@ -44,10 +44,10 @@ func (r *u3dRender) prepareSections(sections []U3DCrossSection) error {
if len(sections) == 0 { if len(sections) == 0 {
return nil return nil
} }
if err := r.reserve(uint64(len(sections)), 352); err != nil { if err := r.reserve(uint64(len(sections)), 1152); err != nil {
return err return err
} }
if err := r.reserve(uint64(len(sections))+3, 112); err != nil { if err := r.reserve(uint64(len(sections))+3, 512); err != nil {
return err return err
} }
r.sections = make([]u3dSection, len(sections)) r.sections = make([]u3dSection, len(sections))
@@ -140,11 +140,10 @@ func (r *u3dRender) sceneDiagonal() (float64, error) {
return 0, err return 0, err
} }
node := r.model.Nodes[instance.node] node := r.model.Nodes[instance.node]
index, exists := r.meshes[node.Resource] mesh := r.nodeMesh(&node)
if node.Kind != "Model" || !exists { if node.Kind != "Model" || mesh == nil {
continue continue
} }
mesh := &r.model.Meshes[index]
for i := range len(mesh.Faces) + len(mesh.Lines) + len(mesh.Points) { for i := range len(mesh.Faces) + len(mesh.Lines) + len(mesh.Points) {
_, corners := mesh.primitive(i) _, corners := mesh.primitive(i)
if i%256 == 0 { if i%256 == 0 {
+303
View File
@@ -0,0 +1,303 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"fmt"
"image"
"math"
)
// U3DTextureLayer 保存单个着色器纹理通道,矩阵按列排列
// Blend依次以0至3表示相乘、相加、替换及线性混合,BlendSource为1时使用BlendConstant
// Mode依次以0至4表示模型坐标、平面、圆柱、球面及反射映射
// Alpha控制是否使用纹理透明度,Repeat低两位分别启用横向及纵向平铺
// 平面映射直接取参考位置的XY;圆柱以Z为纵坐标,圆柱与球面的经线从正X轴朝正Y轴递增
// 球面纬度从正Z极到负Z极映射为0至1;反射映射使用相机空间球面环境图,图像纵坐标向下
type U3DTextureLayer struct {
Texture string
Intensity, BlendConstant float32
Blend, BlendSource uint8
Mode, Repeat uint8
Alpha bool
Transform, WrapTransform [16]float32
}
// u3dTextureSet 保存一次绘制内共用的有序纹理层
type u3dTextureSet struct {
layers [8]*U3DTextureLayer
images [8]*image.NRGBA64
wrap [8]*[16]float64
normal u3dMatrix
mask uint8
reflection, perspective bool
}
// shaderTextures 校验并缓存启用的纹理,未使用的着色器不会触发图像解码
// 入参: shader 着色器
// 返回: *u3dTextureSet 纹理组合,空值表示无纹理, error 参数、能力、预算或取消错误
func (r *u3dRender) shaderTextures(shader U3DShader) (*u3dTextureSet, error) {
if shader.Textures == ([8]*U3DTextureLayer{}) {
return nil, nil
}
if err := r.ctx.Err(); err != nil {
return nil, err
}
if cached := r.textureSets[shader.Textures]; cached != nil {
return cached, nil
}
if err := r.reserve(1, 512); err != nil {
return nil, err
}
set := &u3dTextureSet{layers: shader.Textures}
for i, layer := range set.layers {
if layer == nil {
continue
}
if layer.Blend > 3 || layer.BlendSource > 1 || layer.Mode > 4 || layer.Repeat > 3 || !finite(float64(layer.Intensity)) || !finite(float64(layer.BlendConstant)) {
return nil, fmt.Errorf("invalid U3D texture layer")
}
for _, matrix := range [2][16]float32{layer.Transform, layer.WrapTransform} {
for _, value := range matrix {
if !finite(float64(value)) {
return nil, fmt.Errorf("invalid U3D texture transform")
}
}
}
if layer.Mode >= 1 && layer.Mode <= 3 {
if err := r.reserve(1, 256); err != nil {
return nil, err
}
inverse, err := u3dTextureInverse(layer.WrapTransform)
if err != nil {
return nil, err
}
set.wrap[i] = &inverse
}
if layer.Mode == 4 && !set.reflection {
var ok bool
set.normal, ok = r.toCamera.inverse()
if !ok {
return nil, fmt.Errorf("invalid U3D reflection camera")
}
set.reflection, set.perspective = true, r.camera.Perspective
}
img, err := r.texture(layer.Texture)
if err != nil {
return nil, err
}
set.images[i] = img
set.mask |= 1 << i
}
if r.textureSets == nil {
r.textureSets = make(map[[8]*U3DTextureLayer]*u3dTextureSet)
}
r.textureSets[shader.Textures] = set
return set, nil
}
// coordinates 保存模型纹理坐标或程序化映射的插值输入,避免经线两侧直接插值
// 入参: vertex 目标顶点, mesh 几何资源, shading 属性布局, corner 属性索引, normal 世界法线
// 返回: error 坐标、索引或维度错误
func (s *u3dTextureSet) coordinates(vertex *u3dVertex, mesh *U3DMesh, shading U3DShading, corner U3DCorner, normal u3dVector) error {
if vertex.textureMask != 0 {
clear(vertex.texture[:])
}
vertex.textureMask = 0
if s == nil {
return nil
}
vertex.textureMask = s.mask
for i, layer := range s.layers {
if layer == nil {
continue
}
if layer.Mode != 0 {
if layer.Mode == 4 {
vertex.texture[i] = s.normal.normal(normal)
if vertex.texture[i] == ([3]float64{}) {
return fmt.Errorf("invalid U3D reflection normal")
}
} else {
if uint64(corner.Position) >= uint64(len(mesh.Positions)) {
return fmt.Errorf("invalid U3D texture position index")
}
for c, value := range mesh.Positions[corner.Position] {
if !finite(float64(value)) {
return fmt.Errorf("invalid U3D texture position")
}
vertex.texture[i][c] = float64(value)
}
}
continue
}
if i >= len(shading.TextureDimensions) || uint64(corner.Texture[i]) >= uint64(len(mesh.TextureCoordinates)) {
return fmt.Errorf("invalid U3D texture coordinate index")
}
dimension := shading.TextureDimensions[i]
if dimension < 1 || dimension > 4 {
return fmt.Errorf("invalid U3D texture coordinate dimensions")
}
value := [4]float64{0, 0, 0, 1}
for component := range dimension {
v := float64(mesh.TextureCoordinates[corner.Texture[i]][component])
if !finite(v) {
return fmt.Errorf("invalid U3D texture coordinate")
}
value[component] = v
}
for component, row := range [3]int{0, 1, 3} {
total := 0.0
for col, v := range value {
total += float64(layer.Transform[col*4+row]) * v
}
if !finite(total) {
return fmt.Errorf("invalid U3D transformed texture coordinate")
}
vertex.texture[i][component] = total
}
}
return nil
}
// textureColor 合成纹理后恢复节点指定的透明度,再叠加视图透明度
// 入参: textures 纹理组合, color 顶点插值颜色, coordinates 纹理输入, position 相机空间位置, override 是否指定节点透明度
// 返回: error 纹理映射或混合错误
func (r *u3dRender) textureColor(textures *u3dTextureSet, color *[4]float64, coordinates *[8][3]float64, position u3dVector, override bool) error {
if textures == nil {
return nil
}
opacity := color[3]
if err := textures.color(color, coordinates, position); err != nil {
return err
}
if override {
color[3] = opacity
}
if r.opacity >= 0 {
color[3] *= r.opacity
}
return nil
}
// color 按通道顺序合成纹理,在透明度覆盖、透明测试和雾效之前执行
// 入参: color 未预乘底色, coordinates 插值后的纹理输入, position 相机空间位置
// 返回: error 无效投影坐标或混合结果
func (s *u3dTextureSet) color(color *[4]float64, coordinates *[8][3]float64, position u3dVector) error {
if s == nil {
return nil
}
for i, layer := range s.layers {
if layer == nil {
continue
}
uv := coordinates[i]
if layer.Mode != 0 {
var err error
uv, err = s.generated(i, uv, position)
if err != nil {
return err
}
}
u, v := uv[0]/uv[2], uv[1]/uv[2]
if !finite(u) || !finite(v) {
return fmt.Errorf("invalid U3D projected texture coordinate")
}
sample := u3dShaderSample(s.images[i], u, v, layer.Repeat, layer.Alpha)
for c := range 3 {
sample[c] *= float64(layer.Intensity)
}
weight := sample[3]
if layer.BlendSource == 1 {
weight = min(1, max(0, float64(layer.BlendConstant)))
}
for c := range color {
switch layer.Blend {
case 0:
color[c] *= sample[c]
case 1:
color[c] += sample[c]
case 2:
color[c] = sample[c]
case 3:
color[c] = sample[c]*weight + color[c]*(1-weight)
}
if !finite(color[c]) {
return fmt.Errorf("invalid U3D texture blend result")
}
color[c] = min(1, max(0, color[c]))
}
}
return nil
}
// u3dShaderSample 按归一化坐标采样纹理,平铺时跨边界插值,未平铺时钳制边缘
// 全透明采样保留原始RGB,供相加、固定权重混合及透明度覆盖使用
// 入参: texture 纹理, u 横坐标, v 纵坐标, repeat 平铺位图, alpha 是否使用透明度
// 返回: [4]float64 未预乘颜色及透明度
func u3dShaderSample(texture *image.NRGBA64, u, v float64, repeat uint8, alpha bool) [4]float64 {
if repeat&1 != 0 {
u -= math.Floor(u)
} else {
u = min(1, max(0, u))
}
if repeat&2 != 0 {
v -= math.Floor(v)
} else {
v = min(1, max(0, v))
}
w, h := texture.Rect.Dx(), texture.Rect.Dy()
x, y := u*float64(w)-.5, v*float64(h)-.5
x0, y0 := int(math.Floor(x)), int(math.Floor(y))
fx, fy := x-float64(x0), y-float64(y0)
wx, wy := [2]float64{1 - fx, fx}, [2]float64{1 - fy, fy}
var result [4]float64
var transparent [3]float64
for row := range 2 {
py := min(h-1, max(0, y0+row))
if repeat&2 != 0 {
py = (y0 + row + h) % h
}
for col := range 2 {
px := min(w-1, max(0, x0+col))
if repeat&1 != 0 {
px = (x0 + col + w) % w
}
c := texture.NRGBA64At(px, py)
a := 1.0
if alpha {
a = float64(c.A) / 65535
}
weight := wx[col] * wy[row]
if a == 0 {
transparent[0] += float64(c.R) / 65535 * weight
transparent[1] += float64(c.G) / 65535 * weight
transparent[2] += float64(c.B) / 65535 * weight
}
result[0] += float64(c.R) / 65535 * a * weight
result[1] += float64(c.G) / 65535 * a * weight
result[2] += float64(c.B) / 65535 * a * weight
result[3] += a * weight
}
}
if result[3] != 0 {
for c := range 3 {
result[c] /= result[3]
}
} else {
copy(result[:3], transparent[:])
}
return result
}
+103
View File
@@ -0,0 +1,103 @@
// 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"
)
// U3DBone 保存相对父骨骼的初始位姿,Orientation按W、X、Y、Z排列
// Attributes标志决定链接、关节字段是否存在,以及各轴旋转约束是否启用和限幅
// RotationConstraints按X、Y、Z轴排列,每项依次为上限、下限;未指定骨骼姿态时不变形
type U3DBone struct {
Name, Parent string
Attributes uint32
Length float32
Displacement [3]float32
Orientation [4]float32
LinkCount uint32
LinkLength float32
StartJoint, EndJoint U3DBoneJoint
RotationConstraints [3][2]float32
}
// U3DBoneJoint 保存骨骼局部坐标系内关节椭圆的中心偏移和两轴尺寸
type U3DBoneJoint struct {
Center, Scale [2]float32
}
// skeleton 按ECMA-363第9.6.1.1.6节读取骨架,三角网格、点集和线集共用
// 入参: r 字段读取器
// 返回: []U3DBone 按父项在前排列的骨骼, error 结构、预算或取消错误
func (d *u3dDecoder) skeleton(r *u3dValues) ([]U3DBone, error) {
count := r.u32()
if r.err != nil {
return nil, r.err
}
if count == 0 {
return nil, nil
}
if uint64(count) > uint64(len(r.data)-r.pos)/64 {
return nil, io.ErrUnexpectedEOF
}
if err := d.reserve(uint64(count), 256); err != nil {
return nil, err
}
bones := make([]U3DBone, int(count))
names := make(map[string]bool, int(count))
for index := range bones {
if index%256 == 0 {
if err := d.ctx.Err(); err != nil {
return nil, err
}
}
bone := &bones[index]
bone.Name, bone.Parent = r.text(), r.text()
bone.Attributes, bone.Length = r.u32(), r.f32()
for component := range bone.Displacement {
bone.Displacement[component] = r.f32()
}
for component := range bone.Orientation {
bone.Orientation[component] = r.f32()
}
if bone.Attributes&1 != 0 {
bone.LinkCount, bone.LinkLength = r.u32(), r.f32()
}
if bone.Attributes&2 != 0 {
for _, joint := range []*U3DBoneJoint{&bone.StartJoint, &bone.EndJoint} {
joint.Center = [2]float32{r.f32(), r.f32()}
joint.Scale = [2]float32{r.f32(), r.f32()}
}
}
for axis := range bone.RotationConstraints {
bone.RotationConstraints[axis] = [2]float32{r.f32(), r.f32()}
}
if r.err != nil {
return nil, r.err
}
if bone.Attributes & ^uint32(255) != 0 {
return nil, fmt.Errorf("invalid U3D bone attributes")
}
if names[bone.Name] {
return nil, fmt.Errorf("duplicate U3D bone %q", bone.Name)
}
if index == 0 && bone.Parent != "" || index != 0 && !names[bone.Parent] {
return nil, fmt.Errorf("invalid U3D bone parent %q", bone.Parent)
}
names[bone.Name] = true
}
return bones, nil
}
+505
View File
@@ -0,0 +1,505 @@
// 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"
"slices"
)
// U3DBoneState 按骨骼名称指定相对父骨骼末端的姿态,根骨骼相对模型原点
// Frame可由动作轨道采样得到,Time不参与变形;单位缩放需显式设为[3]float32{1, 1, 1}
// 未覆盖项沿用文件或动画求值后的局部姿态,重复名称使用最后一项,不施加逆运动学约束
type U3DBoneState struct {
Name string
Frame U3DKeyFrame
}
// u3dSkinBone 保存初始姿态到当前姿态的变换及其逆矩阵
type u3dSkinBone struct {
matrix, inverse u3dMatrix
invertible bool
}
// u3dSkinPose 保存同一几何最近求值的姿态和权重,仅用于本次绘制
type u3dSkinPose struct {
weights *U3DBoneWeights
states []U3DBoneState
mesh *U3DMesh
}
// nodeMesh 返回节点本次绘制使用的几何,未变形节点直接引用原资源
// 入参: node 模型节点
// 返回: *U3DMesh 几何,资源缺失时为空
func (r *u3dRender) nodeMesh(node *U3DNode) *U3DMesh {
if mesh := r.posed[node.Name]; mesh != nil {
return mesh
}
if index, exists := r.meshes[node.Resource]; exists {
return &r.model.Meshes[index]
}
return nil
}
// poseNodes 应用动画和显式骨骼姿态,同源同姿态的节点及父路径共用变形几何
// 返回: error 骨架、权重、预算或取消错误
func (r *u3dRender) poseNodes() error {
if len(r.states) == 0 && len(r.animated) == 0 {
return r.ctx.Err()
}
var cache map[*U3DMesh]u3dSkinPose
var cacheBytes uint64
defer func() { r.remaining += cacheBytes }()
for i := range r.model.Nodes {
if err := r.ctx.Err(); err != nil {
return err
}
if r.connected != nil && !r.connected[i] {
continue
}
node := &r.model.Nodes[i]
states := r.states[node.Name].Bones
animation := r.animated[node.Name]
if len(states) == 0 && len(animation.bones) == 0 {
continue
}
mesh := r.nodeMesh(node)
if node.Kind != "Model" || mesh == nil {
return fmt.Errorf("missing U3D bone state geometry for %q", node.Name)
}
if r.posed == nil {
if err := r.reserve(1, 1024); err != nil {
return err
}
r.posed = make(map[string]*U3DMesh)
}
if err := r.reserve(1, 128); err != nil {
return err
}
var temporary uint64
if len(animation.bones) != 0 {
if len(states) == 0 {
states = animation.bones
} else {
count := uint64(len(animation.bones)) + uint64(len(states))
if err := r.reserve(count, 64); err != nil {
return err
}
temporary = count * 64
combined := make([]U3DBoneState, len(animation.bones), int(count))
copy(combined, animation.bones)
states = append(combined, states...)
}
}
weights := animation.weights
if weights == nil {
var err error
weights, err = r.skinWeightSource(mesh, node.BoneWeights, math.MaxUint64, math.MaxUint64)
if err != nil {
r.remaining += temporary
return err
}
}
previous, exists := cache[mesh]
shared := exists && previous.weights == weights && len(previous.states) == len(states)
if shared {
for j := range states {
if j%256 == 0 {
if err := r.ctx.Err(); err != nil {
r.remaining += temporary
return err
}
}
if states[j] != previous.states[j] {
shared = false
break
}
}
}
if shared {
r.posed[node.Name] = previous.mesh
r.remaining += temporary
continue
}
source, sets := mesh, node.BoneWeights
if animation.weights != nil {
geometry := *mesh
geometry.BoneWeights = []U3DBoneWeights{*animation.weights}
mesh, sets = &geometry, nil
}
posed, err := r.skinMesh(mesh, sets, states)
if err != nil {
r.remaining += temporary
return err
}
r.posed[node.Name] = posed
if len(r.model.Nodes) > 1 {
if cache == nil {
if err := r.reserve(1, 1024); err != nil {
return err
}
cacheBytes += 1024
cache = make(map[*U3DMesh]u3dSkinPose)
}
if !exists {
if err := r.reserve(1, 128); err != nil {
return err
}
cacheBytes += 128
}
if err := r.reserve(uint64(len(states)), 64); err != nil {
return err
}
cacheBytes += uint64(len(states)) * 64
saved := make([]U3DBoneState, len(states))
copy(saved, states)
cache[source] = u3dSkinPose{weights: weights, states: saved, mesh: posed}
released := uint64(len(previous.states)) * 64
r.remaining += released
cacheBytes -= released
}
r.remaining += temporary
}
return r.ctx.Err()
}
// boneTransforms 连接骨骼局部姿态,以初始姿态的逆变换将作者坐标映射到当前姿态
// 入参: bones 按父项在前排列的骨架, states 显式姿态
// 返回: []u3dSkinBone 变形矩阵, error 骨架、姿态或取消错误
func (r *u3dRender) boneTransforms(bones []U3DBone, states []U3DBoneState) ([]u3dSkinBone, error) {
index := make(map[string]int, len(bones))
for i, bone := range bones {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return nil, err
}
}
if _, exists := index[bone.Name]; exists {
return nil, fmt.Errorf("duplicate U3D bone %q", bone.Name)
}
index[bone.Name] = i
}
overrides := make(map[int]U3DKeyFrame, min(len(states), len(bones)))
for i, state := range states {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return nil, err
}
}
bone, exists := index[state.Name]
if !exists {
return nil, fmt.Errorf("missing U3D bone %q", state.Name)
}
overrides[bone] = state.Frame
}
bind, pose := make([]u3dMatrix, len(bones)), make([]u3dMatrix, len(bones))
result := make([]u3dSkinBone, len(bones))
for i, bone := range bones {
if err := r.ctx.Err(); err != nil {
return nil, err
}
if !finite(float64(bone.Length)) || bone.Length < 0 || bone.Attributes & ^uint32(255) != 0 {
return nil, fmt.Errorf("invalid U3D bone parameters")
}
if bone.Attributes&1 != 0 && bone.LinkCount != 0 {
return nil, fmt.Errorf("unsupported U3D bone link deformation")
}
initial := U3DKeyFrame{Displacement: bone.Displacement, Rotation: bone.Orientation, Scale: [3]float32{1, 1, 1}}
base, err := initial.Matrix()
if err != nil {
return nil, err
}
current := base
if frame, exists := overrides[i]; exists {
current, err = frame.Matrix()
if err != nil {
return nil, err
}
}
bind[i], pose[i] = u3dMatrix(base), u3dMatrix(current)
if i == 0 {
if bone.Parent != "" {
return nil, fmt.Errorf("invalid U3D root bone parent")
}
} else {
parent, exists := index[bone.Parent]
if !exists || parent >= i {
return nil, fmt.Errorf("invalid U3D bone parent %q", bone.Parent)
}
end := u3dIdentity()
end[12] = float64(bones[parent].Length)
bind[i] = bind[parent].multiply(end).multiply(bind[i])
pose[i] = pose[parent].multiply(end).multiply(pose[i])
}
inverse, ok := bind[i].inverse()
if !ok || !pose[i].valid() {
return nil, fmt.Errorf("invalid U3D bone transform")
}
result[i].matrix = pose[i].multiply(inverse)
if !result[i].matrix.valid() {
return nil, fmt.Errorf("invalid U3D bone deformation")
}
result[i].inverse, result[i].invertible = result[i].matrix.inverse()
}
return result, nil
}
// skinWeights 选取与几何类型相符的末项权重,节点链覆盖资源链
// 入参: mesh 原始几何, sets 节点权重
// 返回: *U3DBoneWeights 有效权重, error 类型、引用或取消错误
func (r *u3dRender) skinWeights(mesh *U3DMesh, sets []U3DBoneWeights) (*U3DBoneWeights, error) {
return r.skinWeightSet(mesh, sets, math.MaxUint64, math.MaxUint64)
}
// skinWeightSource 按修饰链截止位置选取权重,不使用动画后面的权重输出
// 入参: mesh 原始几何, sets 节点权重, resourceEnd、nodeEnd 两条链中首个不参与的索引
// 返回: *U3DBoneWeights 权重来源,内容尚未校验, error 类型、引用或取消错误
func (r *u3dRender) skinWeightSource(mesh *U3DMesh, sets []U3DBoneWeights, resourceEnd, nodeEnd uint64) (*U3DBoneWeights, error) {
kind := uint32(0)
for i, count := range []int{len(mesh.Faces), len(mesh.Lines), len(mesh.Points)} {
if count != 0 {
if kind != 0 {
return nil, fmt.Errorf("unsupported U3D mixed geometry deformation")
}
kind = 1 << i
}
}
var selected *U3DBoneWeights
for chain, group := range [][]U3DBoneWeights{mesh.BoneWeights, sets} {
end := resourceEnd
if chain != 0 {
end = nodeEnd
}
var last *U3DBoneWeights
for i := range group {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return nil, err
}
}
if uint64(group[i].Index) < end && group[i].Attributes == kind && (last == nil || group[i].Index > last.Index) {
last = &group[i]
}
}
if last != nil {
selected = last
}
}
if selected == nil {
return nil, fmt.Errorf("unsupported U3D deformation without bone weights")
}
return selected, r.ctx.Err()
}
// skinWeightSet 选取并校验指定修饰链位置之前的权重
// 入参: mesh 原始几何, sets 节点权重, resourceEnd、nodeEnd 两条链中首个不参与的索引
// 返回: *U3DBoneWeights 有效权重, error 类型、引用、数值或取消错误
func (r *u3dRender) skinWeightSet(mesh *U3DMesh, sets []U3DBoneWeights, resourceEnd, nodeEnd uint64) (*U3DBoneWeights, error) {
selected, err := r.skinWeightSource(mesh, sets, resourceEnd, nodeEnd)
if err != nil {
return nil, err
}
if selected.Index == 0 || len(selected.Offsets) == 0 || len(selected.Offsets)-1 > len(mesh.Positions) || selected.Offsets[0] != 0 || uint64(selected.Offsets[len(selected.Offsets)-1]) != uint64(len(selected.Weights)) {
return nil, fmt.Errorf("invalid U3D bone weight offsets")
}
for i := 1; i < len(selected.Offsets); i++ {
if err := r.ctx.Err(); err != nil {
return nil, err
}
start, end := selected.Offsets[i-1], selected.Offsets[i]
if end < start || uint64(end) > uint64(len(selected.Weights)) {
return nil, fmt.Errorf("invalid U3D bone weight range")
}
var total float64
for j, weight := range selected.Weights[start:end] {
if j%256 == 0 {
if err := r.ctx.Err(); err != nil {
return nil, err
}
}
if uint64(weight.Bone) >= uint64(len(mesh.Bones)) || !finite(float64(weight.Weight)) || weight.Weight < 0 {
return nil, fmt.Errorf("invalid U3D bone weight")
}
total += float64(weight.Weight)
}
if start != end && math.Abs(total-1) > 1e-6 {
return nil, fmt.Errorf("invalid U3D bone weight sum")
}
}
return selected, nil
}
// influence 返回指定位置的骨骼权重,未提供权重的位置保持不变
// 入参: position 作者位置索引
// 返回: []U3DBoneWeight 影响列表
func (weights *U3DBoneWeights) influence(position uint32) []U3DBoneWeight {
if uint64(position)+1 >= uint64(len(weights.Offsets)) {
return nil
}
return weights.Weights[weights.Offsets[position]:weights.Offsets[position+1]]
}
// skinVector 对位置或法线应用加权骨骼变换,法线在累加后统一归一化
// 入参: value 原向量, weights 位置权重, bones 骨骼变换, normal 是否处理法线
// 返回: [3]float32 变形结果, error 数值、奇异变换或取消错误
func (r *u3dRender) skinVector(value [3]float32, weights []U3DBoneWeight, bones []u3dSkinBone, normal bool) ([3]float32, error) {
input := u3dVector{float64(value[0]), float64(value[1]), float64(value[2])}
for _, component := range input {
if !finite(component) {
return [3]float32{}, fmt.Errorf("invalid U3D skin vector")
}
}
if len(weights) == 0 {
return value, nil
}
var result u3dVector
for i, weight := range weights {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return [3]float32{}, err
}
}
if weight.Weight == 0 {
continue
}
bone := &bones[weight.Bone]
v := bone.matrix.point(input)
if normal {
if !bone.invertible {
return [3]float32{}, fmt.Errorf("unsupported U3D singular bone normal transform")
}
m := bone.inverse
v = u3dVector{m[0]*input[0] + m[1]*input[1] + m[2]*input[2], m[4]*input[0] + m[5]*input[1] + m[6]*input[2], m[8]*input[0] + m[9]*input[1] + m[10]*input[2]}
}
for axis := range result {
result[axis] += v[axis] * float64(weight.Weight)
}
}
for _, component := range result {
if !finite(component) {
return [3]float32{}, fmt.Errorf("invalid U3D skin result")
}
}
if normal {
result = result.unit()
}
for axis := range value {
value[axis] = float32(result[axis])
if !finite(float64(value[axis])) {
return [3]float32{}, fmt.Errorf("U3D skin result exceeds range")
}
}
return value, nil
}
// skinMesh 复制受变形影响的属性,按位置与法线的组合保留独立法线索引
// 入参: mesh 原始几何, sets 节点权重, states 显式骨骼姿态
// 返回: *U3DMesh 独立姿态几何, error 结构、能力、预算或取消错误
func (r *u3dRender) skinMesh(mesh *U3DMesh, sets []U3DBoneWeights, states []U3DBoneState) (*U3DMesh, error) {
weights, err := r.skinWeights(mesh, sets)
if err != nil {
return nil, err
}
if err := r.reserve(uint64(len(mesh.Bones)), 768); err != nil {
return nil, err
}
if err := r.reserve(uint64(len(states)), 128); err != nil {
r.remaining += uint64(len(mesh.Bones)) * 768
return nil, err
}
temporary := uint64(len(mesh.Bones))*768 + uint64(len(states))*128
defer func() { r.remaining += temporary }()
bones, err := r.boneTransforms(mesh.Bones, states)
if err != nil {
return nil, err
}
if err := r.reserve(1, 768); err != nil {
return nil, err
}
if err := r.reserve(uint64(len(mesh.Positions)), 12); err != nil {
return nil, err
}
result := *mesh
result.Positions = make([][3]float32, len(mesh.Positions))
for i, position := range mesh.Positions {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return nil, err
}
}
result.Positions[i], err = r.skinVector(position, weights.influence(uint32(i)), bones, false)
if err != nil {
return nil, err
}
}
if mesh.ExcludeNormals {
return &result, r.ctx.Err()
}
bytes := uint64(len(mesh.Faces))*148 + uint64(len(mesh.Lines))*100 + uint64(len(mesh.Points))*52
if err := r.reserve(bytes, 1); err != nil {
return nil, err
}
result.Faces, result.Lines, result.Points = slices.Clone(mesh.Faces), slices.Clone(mesh.Lines), slices.Clone(mesh.Points)
result.Normals = nil
if err := r.reserve(1, 1024); err != nil {
return nil, err
}
index := make(map[[2]uint32]uint32)
defer func() { r.remaining += 1024 + uint64(len(index))*96 }()
for i := range len(result.Faces) + len(result.Lines) + len(result.Points) {
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return nil, err
}
}
_, corners := result.primitive(i)
for j := range corners {
corner := &corners[j]
if uint64(corner.Position) >= uint64(len(mesh.Positions)) || uint64(corner.Normal) >= uint64(len(mesh.Normals)) {
return nil, fmt.Errorf("invalid U3D skin corner index")
}
key := [2]uint32{corner.Position, corner.Normal}
normal, exists := index[key]
if !exists {
if err := r.reserve(1, 96); err != nil {
return nil, err
}
if len(result.Normals) == cap(result.Normals) {
capacity := max(16, uint64(cap(result.Normals))*2)
if capacity > math.MaxUint32 {
return nil, fmt.Errorf("U3D skin normal count exceeds limit")
}
if err := r.reserve(capacity, 12); err != nil {
return nil, err
}
grown := make([][3]float32, len(result.Normals), int(capacity))
copy(grown, result.Normals)
r.remaining += uint64(cap(result.Normals)) * 12
result.Normals = grown
}
value, err := r.skinVector(mesh.Normals[corner.Normal], weights.influence(corner.Position), bones, true)
if err != nil {
return nil, err
}
normal = uint32(len(result.Normals))
index[key] = normal
result.Normals = append(result.Normals, value)
}
corner.Normal = normal
}
}
return &result, r.ctx.Err()
}
+52 -7
View File
@@ -18,25 +18,38 @@ import "fmt"
// U3DNodeState 按名称覆盖节点,未指定透明度或可见性时沿父路径继承 // U3DNodeState 按名称覆盖节点,未指定透明度或可见性时沿父路径继承
// Opacity范围为0到1,采用标准透明混合;Matrix为相对父节点的列式仿射矩阵 // Opacity范围为0到1,采用标准透明混合;Matrix为相对父节点的列式仿射矩阵
// 未匹配的名称不影响场景,矩阵为空时保留每条父路径的原始变换 // 显式Opacity覆盖材质和纹理合成的透明度,之后仍应用视图附加透明度
// 未匹配的名称不影响场景,Matrix和Motion均为空时保留每条父路径的原始变换
// Motion以指定轨道替换节点变换,与Matrix互斥,不驱动骨架;显式变换优先于动画修饰器
// Bones覆盖动画求值后的指定骨骼姿态,未指定项沿用当前局部变换,不修改共享几何
type U3DNodeState struct { type U3DNodeState struct {
Name string Name string
Opacity *float64 Opacity *float64
Visible *bool Visible *bool
Matrix *[16]float64 Matrix *[16]float64
Motion *U3DTrackState
Bones []U3DBoneState
} }
// nodeStates 建立有预算限制的覆盖索引,重复名称整体替换而不合并字段 // nodeStates 建立有预算限制的覆盖索引,重复名称整体替换而不合并字段
// 入参: states 节点覆盖 // 入参: groups 按优先级递增排列的节点覆盖分组
// 返回: error 数值、预算或取消错误 // 返回: error 数值、预算或取消错误
func (r *u3dRender) nodeStates(states []U3DNodeState) error { func (r *u3dRender) nodeStates(groups ...[]U3DNodeState) error {
if len(states) == 0 { var count uint64
for _, states := range groups {
if uint64(len(states)) > ^uint64(0)-count {
return fmt.Errorf("U3D node state count exceeds limit")
}
count += uint64(len(states))
}
if count == 0 {
return nil return nil
} }
if err := r.reserve(uint64(len(states)), 128); err != nil { if err := r.reserve(count, 160); err != nil {
return err return err
} }
r.states = make(map[string]U3DNodeState, len(states)) r.states = make(map[string]U3DNodeState, int(count))
for _, states := range groups {
for i, state := range states { for i, state := range states {
if i%256 == 0 { if i%256 == 0 {
if err := r.ctx.Err(); err != nil { if err := r.ctx.Err(); err != nil {
@@ -45,7 +58,14 @@ func (r *u3dRender) nodeStates(states []U3DNodeState) error {
} }
r.states[state.Name] = state r.states[state.Name] = state
} }
for _, state := range r.states { }
var samples map[U3DTrackState]*[16]float64
defer func() {
if samples != nil {
r.remaining += 1024 + uint64(len(samples))*192
}
}()
for name, state := range r.states {
if err := r.ctx.Err(); err != nil { if err := r.ctx.Err(); err != nil {
return err return err
} }
@@ -55,6 +75,31 @@ func (r *u3dRender) nodeStates(states []U3DNodeState) error {
if state.Matrix != nil && !u3dMatrix(*state.Matrix).valid() { if state.Matrix != nil && !u3dMatrix(*state.Matrix).valid() {
return fmt.Errorf("invalid U3D node matrix") return fmt.Errorf("invalid U3D node matrix")
} }
if state.Motion != nil {
if state.Matrix != nil || !finite(state.Motion.Time) {
return fmt.Errorf("invalid U3D node motion state")
}
if samples == nil {
if err := r.reserve(1, 1024); err != nil {
return err
}
samples = make(map[U3DTrackState]*[16]float64)
}
matrix := samples[*state.Motion]
if matrix == nil {
if err := r.reserve(1, 320); err != nil {
return err
}
value, err := r.trackMatrix(*state.Motion)
if err != nil {
return err
}
matrix = &value
samples[*state.Motion] = matrix
}
state.Matrix = matrix
r.states[name] = state
}
} }
return nil return nil
} }
+179
View File
@@ -0,0 +1,179 @@
// 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"
// U3DTexture 保存纹理尺寸、图像类型及各通道的图像数据
type U3DTexture struct {
Name string
Width, Height uint32
ImageType uint8
Images []U3DTextureImage
}
// U3DTextureImage 保存压缩格式、通道位图及完整压缩数据,外部图像仅保留候选URL
// Compression依次以1至4表示JPEG-24、PNG、JPEG-8和TIFF,ByteCount仅用于内置图像
type U3DTextureImage struct {
Compression uint8
Channels uint8
External bool
ByteCount uint32
Data []byte
URLs []string
}
// u3dTextureChannels 将亮度通道展开为RGB覆盖位,保留透明度位
// 入参: channels 标准通道位图
// 返回: uint8 实际覆盖通道
func u3dTextureChannels(channels uint8) uint8 {
if channels&16 != 0 {
channels = channels&^16 | 14
}
return channels
}
// textureDeclaration 按ECMA-363第9.8.5.1节读取纹理及通道图像声明
// 入参: r 字段读取器, name 纹理名称
// 返回: error 格式、预算或取消错误
func (d *u3dDecoder) textureDeclaration(r *u3dValues, name string) error {
if _, exists := d.textures[name]; exists {
return fmt.Errorf("duplicate U3D texture %q", name)
}
texture := U3DTexture{Name: name, Height: r.u32(), Width: r.u32(), ImageType: r.u8()}
if texture.Width == 0 || texture.Height == 0 {
return fmt.Errorf("invalid U3D texture dimensions")
}
switch texture.ImageType {
case 1, 14, 15, 16, 17:
default:
return fmt.Errorf("invalid U3D texture image type")
}
count := r.u32()
if r.err != nil {
return r.err
}
if count == 0 || count > 4 || uint64(count) > uint64(len(r.data)-r.pos)/8 {
return fmt.Errorf("invalid U3D continuation image count")
}
if err := d.reserve(uint64(count), 64); err != nil {
return err
}
texture.Images = make([]U3DTextureImage, int(count))
var covered uint8
for n := range texture.Images {
if err := d.ctx.Err(); err != nil {
return err
}
image := &texture.Images[n]
image.Compression, image.Channels = r.u8(), r.u8()
attributes := uint16(r.u8()) | uint16(r.u8())<<8
if r.err != nil {
return r.err
}
channels := u3dTextureChannels(image.Channels)
if image.Compression < 1 || image.Compression > 4 || image.Channels == 0 || image.Channels&^uint8(31) != 0 || attributes > 1 || covered&channels != 0 || channels&^u3dTextureChannels(texture.ImageType) != 0 {
return fmt.Errorf("invalid U3D continuation image format")
}
covered |= channels
image.External = attributes == 1
if !image.External {
image.ByteCount = r.u32()
continue
}
count := r.u32()
if r.err != nil {
return r.err
}
if uint64(count) > uint64(len(r.data)-r.pos)/2 {
return fmt.Errorf("invalid U3D texture URL count")
}
if err := d.reserve(uint64(count), 16); err != nil {
return err
}
image.URLs = make([]string, int(count))
for i := range image.URLs {
if err := d.ctx.Err(); err != nil {
return err
}
image.URLs[i] = r.text()
if r.err != nil {
return r.err
}
}
}
if err := r.done(); err != nil {
return err
}
if covered != u3dTextureChannels(texture.ImageType) {
return fmt.Errorf("incomplete U3D texture channels")
}
if d.textures == nil {
d.textures = make(map[string]int)
}
d.textures[name] = len(d.model.Textures)
d.model.Textures = append(d.model.Textures, texture)
return nil
}
// textureContinuation 按声明长度合并纹理分片,不与输入共享存储
// 入参: r 字段读取器, name 纹理名称
// 返回: error 格式、预算或取消错误
func (d *u3dDecoder) textureContinuation(r *u3dValues, name string) error {
index := r.u32()
if r.err != nil {
return r.err
}
texture, exists := d.textures[name]
if !exists || uint64(index) >= uint64(len(d.model.Textures[texture].Images)) {
return fmt.Errorf("invalid U3D texture continuation reference")
}
image := &d.model.Textures[texture].Images[index]
data := r.take(len(r.data) - r.pos)
if image.External || uint64(len(image.Data))+uint64(len(data)) > uint64(image.ByteCount) {
return fmt.Errorf("invalid U3D texture continuation length")
}
if image.Data == nil {
if err := d.reserve(uint64(image.ByteCount), 1); err != nil {
return err
}
image.Data = make([]byte, 0, int(image.ByteCount))
}
for len(data) != 0 {
if err := d.ctx.Err(); err != nil {
return err
}
count := min(65536, len(data))
image.Data = append(image.Data, data[:count]...)
data = data[count:]
}
return d.ctx.Err()
}
// checkTextures 确认各通道的内置图像完整,不加载外部引用
// 返回: error 缺失分片或取消错误
func (d *u3dDecoder) checkTextures() error {
for _, texture := range d.model.Textures {
for _, image := range texture.Images {
if err := d.ctx.Err(); err != nil {
return err
}
if !image.External && uint64(len(image.Data)) != uint64(image.ByteCount) {
return fmt.Errorf("incomplete U3D texture %q", texture.Name)
}
}
}
return nil
}
+281
View File
@@ -0,0 +1,281 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"bytes"
"context"
"errors"
"fmt"
"image"
"image/color"
"image/jpeg"
"image/png"
"io"
"golang.org/x/image/tiff"
)
// U3DTextureResolver 由调用方按原始URL读取外部纹理,地址解析、访问策略和读取限额由调用方控制
// 返回的数据在本次绘制结束前不得修改;失败时尝试下一候选地址,取消及超时错误立即返回
// 入参: ctx 取消上下文, url 绝对或相对地址
// 返回: []byte 压缩图像数据, error 读取错误
type U3DTextureResolver func(ctx context.Context, url string) ([]byte, error)
// u3dImageReader 在图像解码器读取压缩数据时检查取消状态
type u3dImageReader struct {
ctx context.Context
data *bytes.Reader
}
// Read 读取顺序图像数据
// 入参: p 接收缓冲
// 返回: int 已读字节数, error 读取或取消错误
func (r *u3dImageReader) Read(p []byte) (int, error) {
if err := r.ctx.Err(); err != nil {
return 0, err
}
return r.data.Read(p)
}
// ReadAt 读取指定偏移的图像数据
// 入参: p 接收缓冲, offset 字节偏移
// 返回: int 已读字节数, error 读取或取消错误
func (r *u3dImageReader) ReadAt(p []byte, offset int64) (int, error) {
if err := r.ctx.Err(); err != nil {
return 0, err
}
return r.data.ReadAt(p, offset)
}
// texture 按需合成纹理并缓存,空名称使用ECMA-363第8.4.1节规定的默认纹理
// 入参: name 纹理名称
// 返回: *image.NRGBA64 非预乘纹理, error 资源、解码、预算或取消错误
func (r *u3dRender) texture(name string) (*image.NRGBA64, error) {
if err := r.ctx.Err(); err != nil {
return nil, err
}
if cached := r.textures[name]; cached != nil {
return cached, nil
}
if err := r.reserve(1, 256); err != nil {
return nil, err
}
var pixels *image.NRGBA64
if name == "" {
if err := r.reserve(64, 8); err != nil {
return nil, err
}
pixels = image.NewNRGBA64(image.Rect(0, 0, 8, 8))
for y := range 8 {
for x := range 8 {
c := color.NRGBA64{65535, 65535, 65535, 65535}
if x/4 != y/4 {
c.G, c.B = 26214, 13107
}
pixels.SetNRGBA64(x, y, c)
}
}
} else {
index, exists := r.textureIndex[name]
if !exists {
return nil, fmt.Errorf("missing U3D texture %q", name)
}
var err error
pixels, err = r.decodeTexture(&r.model.Textures[index])
if err != nil {
return nil, fmt.Errorf("U3D texture %q: %w", name, err)
}
}
if r.textures == nil {
r.textures = make(map[string]*image.NRGBA64)
}
r.textures[name] = pixels
return pixels, nil
}
// decodeTexture 合成各续图像指定的通道,保留十六位颜色与非预乘透明度
// 入参: texture 纹理资源
// 返回: *image.NRGBA64 合成图像, error 格式、预算或取消错误
func (r *u3dRender) decodeTexture(texture *U3DTexture) (*image.NRGBA64, error) {
switch texture.ImageType {
case 1, 14, 15, 16, 17:
default:
return nil, fmt.Errorf("invalid image type")
}
if texture.Width == 0 || texture.Height == 0 || len(texture.Images) == 0 || len(texture.Images) > 4 {
return nil, fmt.Errorf("invalid image dimensions or channel count")
}
var covered uint8
for _, source := range texture.Images {
if source.Compression < 1 || source.Compression > 4 {
return nil, fmt.Errorf("invalid image compression")
}
channels := u3dTextureChannels(source.Channels)
if source.Channels == 0 || source.Channels&^uint8(31) != 0 || channels&covered != 0 || channels&^u3dTextureChannels(texture.ImageType) != 0 {
return nil, fmt.Errorf("invalid image channels")
}
covered |= channels
if source.External && (r.resolveTexture == nil || len(source.URLs) == 0) {
return nil, fmt.Errorf("external texture image requires resolution")
}
if !source.External && uint64(len(source.Data)) != uint64(source.ByteCount) {
return nil, fmt.Errorf("invalid image data length")
}
}
if covered != u3dTextureChannels(texture.ImageType) {
return nil, fmt.Errorf("incomplete image channels")
}
if err := r.reserve(uint64(texture.Width)*uint64(texture.Height), 8); err != nil {
return nil, err
}
pixels := image.NewNRGBA64(image.Rect(0, 0, int(texture.Width), int(texture.Height)))
for y := range pixels.Rect.Dy() {
if err := r.ctx.Err(); err != nil {
return nil, err
}
for x := range pixels.Rect.Dx() {
if x%4096 == 0 {
if err := r.ctx.Err(); err != nil {
return nil, err
}
}
pixels.SetNRGBA64(x, y, color.NRGBA64{65535, 65535, 65535, 65535})
}
}
for _, source := range texture.Images {
if err := r.textureSource(pixels, source); err != nil {
return nil, err
}
}
return pixels, r.ctx.Err()
}
// textureSource 读取内置通道或按顺序尝试外部地址,不修改模型中的图像声明
// 入参: pixels 目标纹理, source 通道图像
// 返回: error 读取、解码、预算或取消错误
func (r *u3dRender) textureSource(pixels *image.NRGBA64, source U3DTextureImage) error {
if !source.External {
return r.textureChannels(pixels, source)
}
if r.resolveTexture == nil || len(source.URLs) == 0 {
return fmt.Errorf("external texture image requires resolution")
}
var last error
for _, address := range source.URLs {
if err := r.ctx.Err(); err != nil {
return err
}
data, err := r.resolveTexture(r.ctx, address)
if r.ctx.Err() != nil {
return r.ctx.Err()
}
if err == nil {
source.Data = data
err = r.textureChannels(pixels, source)
source.Data = nil
}
if canceled := r.ctx.Err(); canceled != nil {
return canceled
}
if err == nil || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return err
}
last = fmt.Errorf("external texture image %q: %w", address, err)
}
return last
}
// textureChannels 解码并合成一幅续图像,临时预算包括像素、分块及压缩数据开销
// 入参: pixels 目标纹理, source 通道图像
// 返回: error 格式、预算或取消错误
func (r *u3dRender) textureChannels(pixels *image.NRGBA64, source U3DTextureImage) error {
var config func(io.Reader) (image.Config, error)
var decode func(io.Reader) (image.Image, error)
switch source.Compression {
case 1, 3:
config, decode = jpeg.DecodeConfig, jpeg.Decode
case 2:
config, decode = png.DecodeConfig, png.Decode
case 4:
config, decode = tiff.DecodeConfig, tiff.Decode
default:
return fmt.Errorf("invalid image compression")
}
budget := r.remaining
defer func() { r.remaining = budget }()
if err := r.reserve(uint64(len(source.Data)), 4); err != nil {
return err
}
if err := r.reserve(1, 1<<20); err != nil {
return err
}
reader := &u3dImageReader{ctx: r.ctx, data: bytes.NewReader(source.Data)}
cfg, err := config(reader)
if err != nil {
return err
}
if cfg.Width != pixels.Rect.Dx() || cfg.Height != pixels.Rect.Dy() {
return fmt.Errorf("image dimensions differ from texture declaration")
}
if source.Compression == 3 && cfg.ColorModel != color.GrayModel || source.Compression == 1 && cfg.ColorModel != color.YCbCrModel {
return fmt.Errorf("invalid JPEG texture color model")
}
w, h := uint64(cfg.Width)+16, uint64(cfg.Height)+16
if source.Compression == 4 {
w, h = max(w, 1024), max(h, 1024)
}
if err := r.reserve(w*h, 32); err != nil {
return err
}
reader.data.Reset(source.Data)
img, err := decode(reader)
if err != nil {
return err
}
if img.Bounds() != pixels.Bounds() {
return fmt.Errorf("invalid decoded image dimensions")
}
channels := u3dTextureChannels(source.Channels)
grayAlpha := source.Channels == 1 && (cfg.ColorModel == color.GrayModel || cfg.ColorModel == color.Gray16Model)
for y := range cfg.Height {
for x := range cfg.Width {
if x%4096 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
value := imageNRGBA64At(img, x, y)
current := pixels.NRGBA64At(x, y)
if channels&8 != 0 {
current.R = value.R
}
if channels&4 != 0 {
current.G = value.G
}
if channels&2 != 0 {
current.B = value.B
}
if channels&1 != 0 {
current.A = value.A
if grayAlpha {
current.A = value.R
}
}
pixels.SetNRGBA64(x, y, current)
}
}
return r.ctx.Err()
}
+46 -24
View File
@@ -20,10 +20,13 @@ import (
"slices" "slices"
) )
// u3dVertex 保存裁剪前后的相机坐标及未预乘颜色 // u3dVertex 保存裁剪前后的相机坐标、未预乘颜色及纹理插值输入
type u3dVertex struct { type u3dVertex struct {
point u3dVector point u3dVector
color [4]float64 color [4]float64
texture [8][3]float64
textureMask uint8
opacityOverride bool
} }
// drawMesh 按实例复用顶点变换,着色列表按指定顺序绘制 // drawMesh 按实例复用顶点变换,着色列表按指定顺序绘制
@@ -51,8 +54,9 @@ func (r *u3dRender) drawMesh(instance u3dInstance, node *U3DNode, mesh *U3DMesh,
} }
r.primitives = make(map[u3dPrimitiveKey]struct{}) r.primitives = make(map[u3dPrimitiveKey]struct{})
defer func() { defer func() {
r.remaining += 1024 + uint64(len(r.primitives))*512 r.remaining += 1024 + uint64(len(r.primitives))*512 + uint64(len(r.texturedPrimitives))*1024
r.primitives = nil r.primitives = nil
r.texturedPrimitives = nil
}() }()
} }
normalCount := 0 normalCount := 0
@@ -161,15 +165,13 @@ func (r *u3dRender) drawMesh(instance u3dInstance, node *U3DNode, mesh *U3DMesh,
return err return err
} }
} }
style := u3dRenderStyle{shader: U3DShader{Blend: 0x606, RenderPass: math.MaxUint32}} style := u3dRenderStyle{shader: U3DShader{Blend: 0x606, RenderPass: r.shaderPass(name)}}
var err error var err error
if shaded { if shaded {
style, err = r.style(name) 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
@@ -177,7 +179,23 @@ func (r *u3dRender) drawMesh(instance u3dInstance, node *U3DNode, mesh *U3DMesh,
if instance.opacity >= 0 || r.opacity >= 0 { if instance.opacity >= 0 || r.opacity >= 0 {
style.shader.Blend = 0x606 style.shader.Blend = 0x606
} }
textures, err := r.shaderTextures(style.shader)
if err != nil {
return err
}
for i, corner := range face.Corners { for i, corner := range face.Corners {
normal := faceNormal
if shaded && !mesh.ExcludeNormals && invertible {
normal = normals[corner.Normal]
}
if !front {
for j := range normal {
normal[j] = -normal[j]
}
}
if err := textures.coordinates(&vertices[i], mesh, shading, corner, normal); err != nil {
return err
}
if !shaded { if !shaded {
vertices[i].color = [4]float64{r.auxiliaryColor[0], r.auxiliaryColor[1], r.auxiliaryColor[2], 1} vertices[i].color = [4]float64{r.auxiliaryColor[0], r.auxiliaryColor[1], r.auxiliaryColor[2], 1}
if r.mode == U3DRenderIllustration { if r.mode == U3DRenderIllustration {
@@ -192,15 +210,6 @@ func (r *u3dRender) drawMesh(instance u3dInstance, node *U3DNode, mesh *U3DMesh,
} }
continue continue
} }
normal := faceNormal
if !mesh.ExcludeNormals && invertible {
normal = normals[corner.Normal]
}
if !front {
for j := range normal {
normal[j] = -normal[j]
}
}
var diffuse, specular *[4]float32 var diffuse, specular *[4]float32
if shading.Attributes&1 != 0 { if shading.Attributes&1 != 0 {
diffuse = &mesh.Diffuse[corner.Diffuse] diffuse = &mesh.Diffuse[corner.Diffuse]
@@ -214,8 +223,9 @@ 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
vertices[i].opacityOverride = true
} }
if r.opacity >= 0 { if r.opacity >= 0 && textures == nil {
vertices[i].color[3] *= r.opacity vertices[i].color[3] *= r.opacity
} }
} }
@@ -235,7 +245,7 @@ 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 { if shader.Textures == ([8]*U3DTextureLayer{}) && shader.Blend == 0x606 && triangle[0].color[3] == 0 && triangle[1].color[3] == 0 && triangle[2].color[3] == 0 {
return r.ctx.Err() return r.ctx.Err()
} }
var storage [2][12]u3dVertex var storage [2][12]u3dVertex
@@ -259,13 +269,7 @@ func (r *u3dRender) clipTriangle(triangle [3]u3dVertex, shader U3DShader, pass U
} }
if (d >= 0) != (previousDistance >= 0) { if (d >= 0) != (previousDistance >= 0) {
t := u3dClipFraction(previousDistance, d) t := u3dClipFraction(previousDistance, d)
var intersection u3dVertex intersection := u3dInterpolate(previous, vertex, t)
for i := range intersection.point {
intersection.point[i] = previous.point[i]*(1-t) + vertex.point[i]*t
}
for i := range intersection.color {
intersection.color[i] = previous.color[i]*(1-t) + vertex.color[i]*t
}
if output == len(storage[next]) { if output == len(storage[next]) {
return fmt.Errorf("invalid U3D clipped polygon") return fmt.Errorf("invalid U3D clipped polygon")
} }
@@ -298,6 +302,10 @@ func (r *u3dRender) clipTriangle(triangle [3]u3dVertex, shader U3DShader, pass U
// 入参: triangle 已裁剪三角面, shader 着色器, pass 绘制轮次 // 入参: triangle 已裁剪三角面, shader 着色器, pass 绘制轮次
// 返回: error 数值、预算或取消错误 // 返回: error 数值、预算或取消错误
func (r *u3dRender) rasterTriangle(triangle [3]u3dVertex, shader U3DShader, pass U3DViewPass) error { func (r *u3dRender) rasterTriangle(triangle [3]u3dVertex, shader U3DShader, pass U3DViewPass) error {
textures, err := r.shaderTextures(shader)
if err != nil {
return err
}
var x, y, reciprocal [3]float64 var x, y, reciprocal [3]float64
for i, v := range triangle { for i, v := range triangle {
reciprocal[i] = 1 reciprocal[i] = 1
@@ -385,13 +393,27 @@ func (r *u3dRender) rasterTriangle(triangle [3]u3dVertex, shader U3DShader, pass
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])
} }
position := u3dVector{} position := u3dVector{}
if pass.Attributes&1 != 0 { if pass.Attributes&1 != 0 || textures != nil && textures.reflection {
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
} }
} }
if textures != nil {
var uv [8][3]float64
for layer := range textures.layers {
if textures.mask&(1<<layer) == 0 {
continue
}
for c := range uv[layer] {
uv[layer][c] = triangle[0].texture[layer][c] + w1*(triangle[1].texture[layer][c]-triangle[0].texture[layer][c]) + w2*(triangle[2].texture[layer][c]-triangle[0].texture[layer][c])
}
}
if err := r.textureColor(textures, &color, &uv, position, triangle[0].opacityOverride); err != nil {
return err
}
}
visible, err := u3dFragmentColor(&color, position, &shader, &pass) visible, err := u3dFragmentColor(&color, position, &shader, &pass)
if err != nil { if err != nil {
return err return err
+103
View File
@@ -0,0 +1,103 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"fmt"
"image"
"math"
)
// viewLayers 按ECMA-363第9.5.4节依次绘制背景层或覆盖层
// 入参: layers 按后至前排列的纹理层
// 返回: error 参数、资源、预算或取消错误
func (r *u3dRender) viewLayers(layers []U3DViewLayer) error {
for _, layer := range layers {
if err := r.ctx.Err(); err != nil {
return err
}
for _, value := range [...]float32{layer.Blend, layer.Rotation, layer.Location[0], layer.Location[1], layer.Scale[0], layer.Scale[1]} {
if !finite(float64(value)) {
return fmt.Errorf("invalid U3D view layer values")
}
}
texture, err := r.texture(layer.Texture)
if err != nil {
return err
}
blend := min(1, max(0, float64(layer.Blend)))
if blend == 0 || layer.Scale[0] == 0 || layer.Scale[1] == 0 {
continue
}
sin, cos := math.Sincos(float64(layer.Rotation))
x, y := float64(layer.Location[0]), float64(layer.Location[1])
if r.view.Attributes&1 != 0 {
x *= float64(r.output.Rect.Dx())
y *= float64(r.output.Rect.Dy())
}
sx, sy := float64(layer.Scale[0]), float64(layer.Scale[1])
rx, ry := float64(layer.Registration[0]), float64(layer.Registration[1])
m := RasterMatrix{cos * sx, -sin * sx, sin * sy, cos * sy, x - cos*sx*rx - sin*sy*ry, y + sin*sx*rx - cos*sy*ry}
bounds := rasterImageBounds(texture.Bounds(), m, r.output.Bounds())
for py := bounds.Min.Y; py < bounds.Max.Y; py++ {
for px := bounds.Min.X; px < bounds.Max.X; px++ {
if px%4096 == 0 || px == bounds.Min.X {
if err := r.ctx.Err(); err != nil {
return err
}
}
cx, cy := float64(px)+.5, float64(py)+.5
if cx < r.viewport[0] || cy < r.viewport[1] || cx >= r.viewport[0]+r.viewport[2] || cy >= r.viewport[1]+r.viewport[3] {
continue
}
dx, dy := cx-x, cy-y
u, v := (cos*dx-sin*dy)/sx+rx, (sin*dx+cos*dy)/sy+ry
if u < 0 || v < 0 || u >= float64(texture.Rect.Dx()) || v >= float64(texture.Rect.Dy()) {
continue
}
value := u3dTextureSample(texture, u, v)
pixel := &r.frame[py*r.output.Rect.Dx()+px]
alpha := value[3] * blend
for i := range pixel.color {
pixel.color[i] = value[i]*blend + pixel.color[i]*(1-alpha)
}
}
}
}
return r.ctx.Err()
}
// u3dTextureSample 在像素中心进行双线性采样,使用预乘颜色避免透明边缘色晕
// 入参: texture 纹理, x 横向像素坐标, y 纵向像素坐标
// 返回: [4]float64 预乘颜色及透明度
func u3dTextureSample(texture *image.NRGBA64, x, y float64) [4]float64 {
x, y = x-.5, y-.5
x0, y0 := int(math.Floor(x)), int(math.Floor(y))
fx, fy := x-float64(x0), y-float64(y0)
wx, wy := [2]float64{1 - fx, fx}, [2]float64{1 - fy, fy}
var result [4]float64
for row := range 2 {
for col := range 2 {
weight := wx[col] * wy[row]
c := texture.NRGBA64At(min(max(0, x0+col), texture.Rect.Max.X-1), min(max(0, y0+row), texture.Rect.Max.Y-1))
alpha := float64(c.A) / 65535
result[0] += float64(c.R) / 65535 * alpha * weight
result[1] += float64(c.G) / 65535 * alpha * weight
result[2] += float64(c.B) / 65535 * alpha * weight
result[3] += alpha * weight
}
}
return result
}
+180
View File
@@ -0,0 +1,180 @@
// 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"
)
// U3DBoneWeight 保存某骨骼对一个位置的影响系数
type U3DBoneWeight struct {
Bone uint32
Weight float32
}
// U3DBoneWeights 保存修饰链中的权重,Index为链内位置,Attributes的1、2、4分别表示网格、线集、点集
// 第i个位置的权重为Weights[Offsets[i]:Offsets[i+1]],空区间表示没有骨骼影响
type U3DBoneWeights struct {
Index, Attributes uint32
InverseQuantization float32
Offsets []uint32
Weights []U3DBoneWeight
}
// boneWeights 读取ECMA-363第9.7.4节权重修饰器,末项权重由其余各项之和确定
// 入参: values 字段读取器, name 所属链名, chain 链类型, index 链内位置
// 返回: error 结构、数值、预算或取消错误
func (d *u3dDecoder) boneWeights(values *u3dValues, name string, chain int, index uint32) error {
r := u3dResourceReader{values: values, decoder: d}
defer r.release()
weights := U3DBoneWeights{Index: index, Attributes: r.u32(), InverseQuantization: r.f32()}
positions := r.u32()
if err := r.err(); err != nil {
return err
}
if weights.Attributes != 1 && weights.Attributes != 2 && weights.Attributes != 4 || weights.InverseQuantization < 0 {
return fmt.Errorf("invalid U3D bone weight parameters")
}
if err := d.reserve(uint64(positions)+1, 4); err != nil {
return err
}
weights.Offsets = make([]uint32, int(positions)+1)
for position := range positions {
if err := d.ctx.Err(); err != nil {
return err
}
count := r.value(0, false)
if err := r.err(); err != nil {
return err
}
start := len(weights.Weights)
needed := uint64(start) + uint64(count)
if needed > math.MaxUint32 {
return fmt.Errorf("U3D bone weight count exceeds limit")
}
if needed > uint64(cap(weights.Weights)) {
capacity := max(needed, uint64(cap(weights.Weights))*2)
if err := d.reserve(capacity, 8); err != nil {
return err
}
grown := make([]U3DBoneWeight, int(needed), int(capacity))
copy(grown, weights.Weights)
d.remaining += uint64(cap(weights.Weights)) * 8
weights.Weights = grown
} else {
weights.Weights = weights.Weights[:int(needed)]
}
list := weights.Weights[start:]
for i := range list {
if i%256 == 0 {
if err := d.ctx.Err(); err != nil {
return err
}
}
list[i].Bone = r.value(1, false)
if err := r.err(); err != nil {
return err
}
}
var total float32
for i := 0; i+1 < len(list); i++ {
if i%256 == 0 {
if err := d.ctx.Err(); err != nil {
return err
}
}
list[i].Weight = float32(float64(r.value(2, false)) * float64(weights.InverseQuantization))
total += list[i].Weight
if err := r.err(); err != nil {
return err
}
if !finite(float64(total)) || total > 1 {
return fmt.Errorf("invalid U3D bone weight sum")
}
}
if len(list) != 0 {
list[len(list)-1].Weight = 1 - total
}
weights.Offsets[position+1] = uint32(needed)
}
if err := r.done(); err != nil {
return err
}
if chain == 1 {
d.meshes[name].mesh.BoneWeights = append(d.meshes[name].mesh.BoneWeights, weights)
} else {
node := &d.model.Nodes[d.nodes[name]]
node.BoneWeights = append(node.BoneWeights, weights)
}
return nil
}
// checkBoneWeights 在几何和节点声明齐备后校验权重引用,允许节点引用后续资源
// 返回: error 位置、骨骼引用或取消错误
func (d *u3dDecoder) checkBoneWeights() error {
for _, name := range d.meshOrder {
mesh := d.meshes[name]
if err := d.checkMeshBoneWeights(mesh, mesh.mesh.BoneWeights); err != nil {
return err
}
}
for _, node := range d.model.Nodes {
if err := d.checkMeshBoneWeights(d.meshes[node.Resource], node.BoneWeights); err != nil {
return err
}
}
return nil
}
// checkMeshBoneWeights 校验适用于指定几何类型的权重,不把未匹配的类型强加到几何上
// 入参: mesh 几何声明,可为空, sets 权重修饰器
// 返回: error 位置、骨骼引用或取消错误
func (d *u3dDecoder) checkMeshBoneWeights(mesh *u3dMeshDeclaration, sets []U3DBoneWeights) error {
for _, weights := range sets {
if err := d.ctx.Err(); err != nil {
return err
}
if mesh == nil {
if len(weights.Weights) != 0 {
return fmt.Errorf("missing U3D bone weight geometry")
}
continue
}
kind := uint32(1)
if mesh.points {
kind = 4
} else if mesh.line != nil {
kind = 2
}
if weights.Attributes != kind {
continue
}
if uint64(len(weights.Offsets)-1) > uint64(len(mesh.mesh.Positions)) {
return fmt.Errorf("invalid U3D bone weight position count")
}
for index, weight := range weights.Weights {
if index%1024 == 0 {
if err := d.ctx.Err(); err != nil {
return err
}
}
if uint64(weight.Bone) >= uint64(len(mesh.mesh.Bones)) {
return fmt.Errorf("invalid U3D bone weight index")
}
}
}
return nil
}
+24 -10
View File
@@ -84,7 +84,7 @@ func (r *Reader) versionFileDependencies(ctx context.Context, entry string, part
// versionEditGraph 沿标准文件引用建立版本依赖图,二进制资源只记录路径 // versionEditGraph 沿标准文件引用建立版本依赖图,二进制资源只记录路径
// 入参: ctx 取消上下文, entry 文档入口, parts 待修改条目, generated 本次自产页面及引用 // 入参: ctx 取消上下文, entry 文档入口, parts 待修改条目, generated 本次自产页面及引用
// 返回: *versionEditGraph 依赖图, error 读取或结构错误 // 返回: *versionEditGraph 依赖图, error 读取、结构或取消错误
func (r *Reader) versionEditGraph(ctx context.Context, entry string, parts map[string][]byte, generated map[string]editorGeneratedReferences) (*versionEditGraph, error) { func (r *Reader) versionEditGraph(ctx context.Context, entry string, parts map[string][]byte, generated map[string]editorGeneratedReferences) (*versionEditGraph, error) {
graph := &versionEditGraph{files: make(map[string]*versionEditFile), used: make(map[string]bool)} graph := &versionEditGraph{files: make(map[string]*versionEditFile), used: make(map[string]bool)}
queue := []string{entry} queue := []string{entry}
@@ -104,12 +104,12 @@ func (r *Reader) versionEditGraph(ctx context.Context, entry string, parts map[s
} }
if !ok { if !ok {
var err error var err error
data, err = r.readFile(name) data, err = r.readFileViewContext(ctx, name)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
root, err := parseEditorXML(data) root, err := parseEditorXMLContext(ctx, data)
if err != nil { if err != nil {
return nil, fmt.Errorf("version file %s: %w", name, err) return nil, fmt.Errorf("version file %s: %w", name, err)
} }
@@ -119,17 +119,20 @@ func (r *Reader) versionEditGraph(ctx context.Context, entry string, parts map[s
if root.name.Local == "Res" { if root.name.Local == "Res" {
base = root.attr("BaseLoc") base = root.attr("BaseLoc")
} }
var visit func(*editorXML) var visit func(*editorXML) error
visit = func(node *editorXML) { visit = func(node *editorXML) error {
if err := ctx.Err(); err != nil {
return err
}
if node != root && !node.matchesOFD(node.name.Local) { if node != root && !node.matchesOFD(node.name.Local) {
return return nil
} }
parent := "" parent := ""
if node.parent != nil { if node.parent != nil {
parent = node.parent.name.Local parent = node.parent.name.Local
} }
if parent == "Extension" && node.name.Local == "Data" { if parent == "Extension" && node.name.Local == "Data" {
return return nil
} }
add := func(attribute, value string, kind int) { add := func(attribute, value string, kind int) {
if strings.TrimSpace(value) == "" { if strings.TrimSpace(value) == "" {
@@ -154,10 +157,18 @@ func (r *Reader) versionEditGraph(ctx context.Context, entry string, parts map[s
} }
} }
for _, child := range node.children { for _, child := range node.children {
visit(child) if err := visit(child); err != nil {
return err
} }
} }
visit(root) return nil
}
if err := visit(root); err != nil {
return nil, err
}
}
if err := ctx.Err(); err != nil {
return nil, err
} }
return graph, nil return graph, nil
} }
@@ -408,7 +419,7 @@ func (r *Reader) versionOutputChanges(ctx context.Context, changes map[string][]
// rewrite 更新已隔离文件的引用,保留其余XML及相对路径基准 // rewrite 更新已隔离文件的引用,保留其余XML及相对路径基准
// 入参: paths 原路径到新路径的映射 // 入参: paths 原路径到新路径的映射
// 返回: []byte 更新后的XML, error 属性编码错误 // 返回: []byte 更新后的XML,未改动时借用原文, error 属性编码错误
func (file *versionEditFile) rewrite(paths map[string]string) ([]byte, error) { func (file *versionEditFile) rewrite(paths map[string]string) ([]byte, error) {
var patches []editorXMLPatch var patches []editorXMLPatch
headers := make(map[*editorXML][]versionEditLink) headers := make(map[*editorXML][]versionEditLink)
@@ -444,6 +455,9 @@ func (file *versionEditFile) rewrite(paths map[string]string) ([]byte, error) {
patches = append(patches, editorXMLPatch{node.start, node.open, data[:root.open]}) patches = append(patches, editorXMLPatch{node.start, node.open, data[:root.open]})
} }
} }
if len(patches) == 0 {
return file.data, nil
}
return editorPatchXML(file.data, patches), nil return editorPatchXML(file.data, patches), nil
} }
+35 -9
View File
@@ -441,8 +441,8 @@ func (e *Editor) writePartsWithPages(write func(string, []byte, bool) error, pro
return nil return nil
} }
// usedResources 筛选当前引用的新增资源及需要跨页复用的原资源文件,不修改资源池 // usedResources 沿图层、对象及资源依赖筛选新增资源和共享文件,不修改资源池
// 返回: []editorResource 新增字体, []editorResource 新增图片, []ColorSpace 新增颜色空间, []string 原资源文件 // 返回: []editorResource 新增字体, []editorResource 新增图片, []ColorSpace 新增颜色空间, []string 独立资源文件
func (e *Editor) usedResources() (fonts, images []editorResource, spaces []ColorSpace, sourceFiles []string) { func (e *Editor) usedResources() (fonts, images []editorResource, spaces []ColorSpace, sourceFiles []string) {
used := make(map[string]bool) used := make(map[string]bool)
promoted := make(map[string]bool) promoted := make(map[string]bool)
@@ -487,10 +487,13 @@ func (e *Editor) usedResources() (fonts, images []editorResource, spaces []Color
for _, page := range e.pages { for _, page := range e.pages {
collectActionReferences(page.Actions, used) collectActionReferences(page.Actions, used)
var original map[string]GraphicObject var original map[string]GraphicObject
var originalLayers map[string]string
if e.source != nil { if e.source != nil {
original = make(map[string]GraphicObject) original = make(map[string]GraphicObject)
originalLayers = make(map[string]string)
if source := e.source.pages[page.ID]; source != nil && source.original != nil { if source := e.source.pages[page.ID]; source != nil && source.original != nil {
for _, layer := range source.original.Content.Layer { for _, layer := range source.original.Content.Layer {
originalLayers[layer.ID] = layer.DrawParam
for _, object := range layer.Objects { for _, object := range layer.Objects {
original[editorObjectID(object)] = object original[editorObjectID(object)] = object
} }
@@ -498,6 +501,12 @@ func (e *Editor) usedResources() (fonts, images []editorResource, spaces []Color
} }
} }
for _, layer := range page.Content.Layer { for _, layer := range page.Content.Layer {
if layer.DrawParam != "" {
used[layer.DrawParam] = true
if e.source != nil && originalLayers[layer.ID] != layer.DrawParam {
promoted[layer.DrawParam] = true
}
}
for _, object := range layer.Objects { for _, object := range layer.Objects {
before, exists := original[editorObjectID(object)] before, exists := original[editorObjectID(object)]
collectObjectReferences(object, used) collectObjectReferences(object, used)
@@ -514,16 +523,33 @@ func (e *Editor) usedResources() (fonts, images []editorResource, spaces []Color
} }
normalizeResourceReferences(used) normalizeResourceReferences(used)
normalizeResourceReferences(promoted) normalizeResourceReferences(promoted)
for i := len(e.resources) - 1; i >= 0; i-- { definitions := make(map[string]int)
resource := e.resources[i] var pending []string
if resource.definition() != "" && used[resource.definition()] { for index, resource := range e.resources {
for _, id := range resource.references { if id := resource.definition(); id != "" {
used[id] = true definitions[id] = index
if e.source != nil { if used[id] {
promoted[id] = true pending = append(pending, id)
} }
} }
} }
for len(pending) != 0 {
id := pending[len(pending)-1]
pending = pending[:len(pending)-1]
index, ok := definitions[id]
if !ok {
continue
}
delete(definitions, id)
for _, reference := range e.resources[index].references {
used[reference] = true
if e.source != nil {
promoted[reference] = true
}
if _, ok := definitions[reference]; ok {
pending = append(pending, reference)
}
}
} }
for _, resource := range e.resources { for _, resource := range e.resources {
if resource.definition() != "" && used[resource.definition()] { if resource.definition() != "" && used[resource.definition()] {