feat(渲染压缩): 完善三维渲染并优化转换压缩
build.yaml / build (push) Successful in 45s

This commit is contained in:
xiaoqidun committed 2026-10-08 10:21:17 +08:00
1 parent c8a163a077
commit 0351dd54f5
31 files changed
+993 -311

No files matched your search

+1 -1
View File
@@ -10,7 +10,7 @@ require (
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
github.com/xiaoqidun/jbig2 v1.1.1
github.com/xiaoqidun/pdfgo v0.0.0-20261007170436-881165d037e5
github.com/xiaoqidun/pdfgo v0.0.0-20261008021024-e8f1cc0a7ad9
golang.org/x/crypto v0.57.0
golang.org/x/image v0.46.0
golang.org/x/text v0.42.0
+2 -2
View File
@@ -62,8 +62,8 @@ github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk=
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
github.com/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI=
github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ=
github.com/xiaoqidun/pdfgo v0.0.0-20261007170436-881165d037e5 h1:FZ24gdR14GDzZ0+pV0ZQUjxA71eSiEMAiHH2B5O8fPo=
github.com/xiaoqidun/pdfgo v0.0.0-20261007170436-881165d037e5/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ=
github.com/xiaoqidun/pdfgo v0.0.0-20261008021024-e8f1cc0a7ad9 h1:aHEEZYzIL7cCLW9IXMlBC+LyCxkYrge3Zw/GfioDT2I=
github.com/xiaoqidun/pdfgo v0.0.0-20261008021024-e8f1cc0a7ad9/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
+12 -2
View File
@@ -15,6 +15,7 @@
package ofdgo
import (
"bytes"
"crypto/sha256"
"image"
"runtime"
@@ -34,6 +35,12 @@ type outputImageJob struct {
err error
}
// outputImageSource 保存已读取资源的摘要和格式,不保留原始编码
type outputImageSource struct {
key outputImageKey
jpeg bool
}
// outputImageOptions 统一资源压缩及像素需求,受保护图片只进行无损编码
// 入参: key 资源路径
// 返回: CompressionOptions 实际策略, image.Point 像素需求
@@ -51,9 +58,9 @@ func (e *Editor) outputImageOptions(key string) (CompressionOptions, image.Point
}
// prepareOutputImages 有界并行优化独立图片,读取、缓存及进度仍在调用线程处理
// 入参: keys 资源路径, load 串行读取方法,空值表示跳过, results 本次写出的候选缓存
// 入参: keys 资源路径, load 串行读取方法,空值表示跳过, results 候选缓存, sources 原始资源摘要,可为空
// 返回: error 读取、进度或取消错误
func (e *Editor) prepareOutputImages(keys []string, load func(string) ([]byte, error), results map[outputImageKey][]byte) error {
func (e *Editor) prepareOutputImages(keys []string, load func(string) ([]byte, error), results map[outputImageKey][]byte, sources map[string]outputImageSource) error {
workers := min(2, runtime.GOMAXPROCS(0))
if workers < 2 || len(keys) < 2 {
return nil
@@ -84,6 +91,9 @@ func (e *Editor) prepareOutputImages(keys []string, load func(string) ([]byte, e
}
options, demand := e.outputImageOptions(key)
cacheKey := outputImageKey{hash: sha256.Sum256(data), options: options, size: demand}
if sources != nil {
sources[key] = outputImageSource{key: cacheKey, jpeg: bytes.HasPrefix(data, []byte{255, 216})}
}
if _, cached := results[cacheKey]; cached {
continue
}
+28 -17
View File
@@ -169,6 +169,7 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
formats := make(map[string]string)
seen := make(map[string]map[[32]byte]string)
imageResults := make(map[outputImageKey][]byte)
imageSources := make(map[string]outputImageSource)
ordered := slices.Sorted(maps.Keys(actual))
var imageKeys []string
for _, key := range ordered {
@@ -191,7 +192,7 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
}
return data, err
}
if err := e.prepareOutputImages(imageKeys, load, imageResults); err != nil {
if err := e.prepareOutputImages(imageKeys, load, imageResults, imageSources); err != nil {
return err
}
for _, key := range ordered {
@@ -205,24 +206,22 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
if err := editorProgress(e.OnWriteProgress).report("compress", e.output.completed, 0); err != nil {
return err
}
if data, ok := parts[actual[key]]; ok && len(data) > outputOptimizationBufferLimit {
continue
}
if reader != nil && parts[actual[key]] == nil {
if file, ok := reader.packageFile(actual[key]); ok && file.UncompressedSize64 > outputOptimizationBufferLimit {
source, prefetched := imageSources[key]
var data []byte
if !prefetched {
var err error
data, err = load(key)
if err != nil {
return err
}
if data == nil {
continue
}
}
data, err := read(actual[key])
if err != nil {
return err
}
if len(data) > outputOptimizationBufferLimit {
continue
source.key.hash = sha256.Sum256(data)
}
if role == "MediaFile" {
options, demand := e.outputImageOptions(key)
cacheKey := outputImageKey{hash: sha256.Sum256(data), options: options, size: demand}
cacheKey := outputImageKey{hash: source.key.hash, options: options, size: demand}
candidate, cached := imageResults[cacheKey]
var err error
if !cached {
@@ -231,6 +230,8 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
imageResults[cacheKey] = nil
if len(candidate) < len(data) {
imageResults[cacheKey] = candidate
} else {
candidate = nil
}
}
}
@@ -238,14 +239,24 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
if e.output.ctx.Err() != nil {
return e.output.ctx.Err()
}
} else if candidate != nil && len(candidate) < len(data) {
} else if candidate != nil {
data = candidate
parts[actual[key]] = data
source.key.hash = sha256.Sum256(data)
}
if bytes.HasPrefix(data, []byte{255, 216}) {
if data != nil {
source.jpeg = bytes.HasPrefix(data, []byte{255, 216})
}
if source.jpeg {
formats[key] = "JPEG"
ext := path.Ext(actual[key])
if !strings.EqualFold(ext, ".jpg") && !strings.EqualFold(ext, ".jpeg") {
if data == nil {
data, err = read(actual[key])
if err != nil {
return err
}
}
stem := strings.TrimSuffix(actual[key], ext)
name := stem + ".jpg"
for n := 1; reserved[strings.ToLower(name)]; n++ {
@@ -262,7 +273,7 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
if seen[role] == nil {
seen[role] = make(map[[32]byte]string)
}
hash := sha256.Sum256(data)
hash := source.key.hash
if first, ok := seen[role][hash]; ok {
aliases[key] = first
removed[key] = true
+4 -6
View File
@@ -78,11 +78,10 @@ func (p GeometryPath) Stroke(options StrokeOptions) (GeometryPath, error) {
}
}
for _, part := range parts {
outline, err := part.stroke(options)
result, err = part.stroke(result, options)
if err != nil {
return nil, err
}
result = append(result, outline...)
if len(result) > 1<<20 {
return nil, fmt.Errorf("stroke outline limit exceeded")
}
@@ -282,11 +281,10 @@ func (line geometryPolyline) dash(pattern []float64, total, offset float64) ([]g
return result, nil
}
// stroke 用同向矩形、连接楔形及圆盘组成非零填充区域
// 入参: options 描边样式
// stroke 追加同向矩形、连接楔形及圆盘组成的非零填充区域
// 入参: result 已有轮廓, options 描边样式
// 返回: GeometryPath 填充轮廓, error 不支持的退化方形线帽
func (line geometryPolyline) stroke(options StrokeOptions) (GeometryPath, error) {
var result GeometryPath
func (line geometryPolyline) stroke(result GeometryPath, options StrokeOptions) (GeometryPath, error) {
half := options.Width / 2
polygon := func(points ...Point) {
scale := 0.0
+1
View File
@@ -118,6 +118,7 @@ type pdfImporter struct {
halftoneWarnings map[*pdfgo.Halftone]bool
halftones map[string]*pdfgo.Halftone
transferBackdrop bool
transferModel pdfgo.Name
compression CompressionOptions
}
+1 -1
View File
@@ -402,7 +402,7 @@ func (r *u3dRender) depthRange() (float64, float64, bool, error) {
}
mesh := &r.model.Meshes[index]
matrix := r.toCamera.multiply(instance.world)
for j := range len(mesh.Faces) + len(mesh.Lines) {
for j := range len(mesh.Faces) + len(mesh.Lines) + len(mesh.Points) {
_, corners := mesh.primitive(j)
if j%256 == 0 {
if err := r.ctx.Err(); err != nil {
+61 -28
View File
@@ -56,8 +56,11 @@ type pdfCompositor struct {
meshes map[pdfMeshKey][]pdfShadingPixel
gradients map[pdfGradientKey][]pdfShadingPixel
transfers []*pdfgo.TransferFunction
channels []uint8
transferModel pdfgo.Name
transferBlocked bool
shadingSource bool
softMask bool
}
// pdfCompositeKey 区分图元的填充与描边几何
@@ -89,10 +92,18 @@ type pdfCompositeColorSampler struct {
values map[float64][4]float64
position pdfgo.GradientPosition
converter pdfgo.ColorConverter
colorants *pdfShadingColorants
prepared bool
converted bool
}
// pdfImageProcessKey 隔离图像的组空间及软蒙版采样,不复用错误的原生映射
type pdfImageProcessKey struct {
image *pdfgo.Image
space *pdfgo.ColorSpace
softMask bool
}
// pdfCompositeCache 在单页内复用几何、图像分量及蒙版内容
type pdfCompositeCache struct {
geometry *renderCache[pdfCompositeSceneKey, pdfCompositeGeometry]
@@ -100,6 +111,7 @@ type pdfCompositeCache struct {
bounds map[pdfCompositeKey]Box
images map[*pdfgo.Image]*pdfgo.ImageComponents
imageMatrix map[*pdfgo.ImageMark]pdfgo.Matrix
imageProcesses map[pdfImageProcessKey]*pdfgo.ImageProcess
masks map[*pdfgo.SoftMask][]pdfCompositeNode
meshes map[*pdfgo.MeshGradient][]pdfMeshTriangle
patterns map[pdfgo.Paint]*pdfCompositePattern
@@ -346,9 +358,6 @@ func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error) ([]p
if err := p.halftone(mark.Style); err != nil {
return err
}
if err := p.resolveTransfer(&mark.Style); err != nil {
return err
}
nodes = append(nodes, pdfCompositeNode{path: &mark})
return nil
}
@@ -386,9 +395,6 @@ func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error) ([]p
if err := p.halftone(mark.Style); err != nil {
return err
}
if err := p.resolveTransfer(&mark.Style); err != nil {
return err
}
if len(mark.Glyphs) > 1 && (mark.Object != nil && !mark.Object.Knockout || mark.Style.AlphaIsShape || mark.Style.Fill.Alpha != 1 || mark.Style.Stroke.Alpha != 1 || mark.Style.SoftMask != nil || !pdfNormalBlend(mark.Style.BlendMode) || mark.Mode%4 == 1 || mark.Mode%4 == 2) {
for i := range mark.Glyphs {
glyph := mark
@@ -426,9 +432,6 @@ func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error) ([]p
if err := p.halftone(mark.Style); err != nil {
return err
}
if err := p.resolveTransfer(&mark.Style); err != nil {
return err
}
nodes = append(nodes, pdfCompositeNode{image: &mark})
return nil
}
@@ -499,15 +502,19 @@ func (p *pdfImporter) compositePage(space *pdfgo.ColorSpace, walk func(pdfgo.Vis
p.compositeNodes = nil
p.transferBackdrop = false
p.compositeSpace = space
if pdfCompositeHasTransfer(nodes) {
device, err := p.transferDevice(space, nodes)
if err != nil {
return err
}
p.transferModel = device
hasTransfer, err := p.compositeHasTransfer(nodes, device)
if err != nil {
return err
}
if hasTransfer {
if p.warning == nil {
return &pdfgo.UnsupportedError{Feature: "device transfer function rasterization"}
}
if overprint, err := p.processOverprint(nodes); err != nil {
return err
} else if overprint {
return &pdfgo.UnsupportedError{Feature: "per-colorant transfer function overprinting"}
}
if err := p.compositeRegion(nil, pdfCompositeNode{group: &pdfgo.GroupMark{Alpha: 1, ColorSpace: space}, children: nodes}, space, true); err != nil {
return err
}
@@ -540,16 +547,16 @@ func (p *pdfImporter) compositeObjects(nodes []pdfCompositeNode) error {
if err != nil {
return fmt.Errorf("inspect graphic %d: %w", i+1, err)
}
transfer := pdfCompositeHasTransfer([]pdfCompositeNode{node})
device := p.transferModel
if device == "" {
device = "DeviceRGB"
}
transfer, err := p.compositeHasTransfer([]pdfCompositeNode{node}, device)
if err != nil {
return err
}
if p.transferBackdrop || transfer {
direct = false
for _, source := range [][]pdfCompositeNode{p.compositeNodes, {node}} {
if overprint, err := p.processOverprint(source); err != nil {
return err
} else if overprint {
return &pdfgo.UnsupportedError{Feature: "per-colorant transfer function overprinting"}
}
}
}
if direct {
if err := node.emit(p.visitor()); err != nil {
@@ -785,7 +792,19 @@ func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompo
scratch := &pdfCompositeScratch{}
var transfers []*pdfgo.TransferFunction
var previousTransfers []*pdfgo.TransferFunction
if pdfCompositeHasTransfer(backdrop) || pdfCompositeHasTransfer([]pdfCompositeNode{node}) {
device := p.transferModel
if device == "" {
device = "DeviceRGB"
}
hasTransfer := false
for _, nodes := range [][]pdfCompositeNode{backdrop, {node}} {
found, err := p.compositeHasTransfer(nodes, device)
if err != nil {
return err
}
hasTransfer = hasTransfer || found
}
if hasTransfer {
transfers = make([]*pdfgo.TransferFunction, len(buffer))
previousTransfers = make([]*pdfgo.TransferFunction, len(buffer))
}
@@ -799,6 +818,7 @@ func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompo
c := pdfCompositor{importer: p, box: Box{X: box.X + float64(x)*step, Y: box.Y + float64(y)*step, W: float64(width) * step, H: float64(height) * step}, width: width, height: height, inverse: inverse, cache: cache, scratch: scratch, masks: map[*pdfgo.SoftMask][]float64{}}
if transfers != nil {
c.transfers = transfers[:width*height]
c.transferModel = device
clear(c.transfers)
}
pixels := buffer[:width*height]
@@ -824,7 +844,18 @@ func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompo
}
var rgb [3]float64
var err error
if c.transfers != nil {
if c.transfers != nil && device == "DeviceCMYK" {
values := pixel.values
if flatten {
for j := range values {
values[j] *= pixel.alpha
}
}
values, err = c.transfers[i].Apply(values[:], device, "")
if err == nil {
rgb, err = pdfCompositeOutputColor(space, values)
}
} else if c.transfers != nil {
rgb, err = space.RGB(pixel.values[:space.Components()], "RelativeColorimetric")
} else {
rgb, err = pdfCompositeOutputColor(space, pixel.values)
@@ -834,12 +865,14 @@ func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompo
}
alpha := pixel.alpha
if flatten {
for j := range rgb {
rgb[j] = pixel.alpha*rgb[j] + 1 - pixel.alpha
if c.transfers == nil || device != "DeviceCMYK" {
for j := range rgb {
rgb[j] = pixel.alpha*rgb[j] + 1 - pixel.alpha
}
}
alpha = 1
}
if c.transfers != nil && c.transfers[i] != nil {
if c.transfers != nil && device != "DeviceCMYK" && c.transfers[i] != nil {
values, err := c.transfers[i].Apply(rgb[:], "DeviceRGB", "")
if err != nil {
return err
+219 -101
View File
@@ -65,12 +65,18 @@ func (c *pdfCompositor) drawMark(node pdfCompositeNode, pixels []pdfCompositePix
return err
}
var initial []pdfCompositePixel
var initialChannels []uint8
result := pixels
grouped := fill && stroke && (style.FillOverprint || style.StrokeOverprint) && style.Fill.Alpha == style.Stroke.Alpha
if fill && stroke {
initial = c.acquirePixels(len(pixels))
defer c.releasePixels(initial)
copy(initial, pixels)
if c.channels != nil && !grouped {
initialChannels = c.acquireChannels(len(c.channels))
defer c.releaseChannels(initialChannels)
copy(initialChannels, c.channels)
}
if grouped {
result = c.acquirePixels(len(pixels))
defer c.releasePixels(result)
@@ -96,7 +102,7 @@ func (c *pdfCompositor) drawMark(node pdfCompositeNode, pixels []pdfCompositePix
} else if outline {
knockout = initial
}
if err := c.drawPaint(node, outline, paint, mode, overprint, paintMask, knockout, result, space); err != nil {
if err := c.drawPaint(node, outline, paint, mode, overprint, paintMask, knockout, initialChannels, result, space); err != nil {
return err
}
}
@@ -107,9 +113,9 @@ func (c *pdfCompositor) drawMark(node pdfCompositeNode, pixels []pdfCompositePix
}
// drawPaint 结合几何覆盖与源颜色绘制一个填充或描边
// 入参: node 图元, outline 描边标志, paint 画刷, mode 混合模式, overprint 叠印标志, mask 蒙版, initial 挖空初始背景, pixels 输出, space 混合空间
// 入参: node 图元, outline 描边标志, paint 画刷, mode 混合模式, overprint 叠印标志, mask 蒙版, initial 挖空初始背景, initialChannels 初始通道, pixels 输出, space 混合空间
// 返回: error 几何或颜色错误
func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdfgo.Paint, mode pdfgo.Name, overprint bool, mask []float64, initial, pixels []pdfCompositePixel, space *pdfgo.ColorSpace) error {
func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdfgo.Paint, mode pdfgo.Name, overprint bool, mask []float64, initial []pdfCompositePixel, initialChannels []uint8, pixels []pdfCompositePixel, space *pdfgo.ColorSpace) error {
coverage, err := c.coverage(node, outline)
if err != nil || coverage == nil {
return err
@@ -123,40 +129,78 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
if err != nil {
return err
}
if node.image != nil && (node.image.Image.ImageMask || node.image.Image.Mask != nil) {
soft, err := node.image.Image.HasSoftMask()
if err != nil {
return err
}
imageShape = !soft
}
if node.image != nil && (node.image.Image.ImageMask || node.image.Image.Mask != nil) {
soft, err := node.image.Image.HasSoftMask()
if err != nil {
return err
}
imageShape = !soft
}
var processMask [4]bool
processOverprint := false
if overprint && space.Model == "DeviceCMYK" && !space.Calibrated() {
if node.image != nil {
processMask, processOverprint, err = pdfImageProcessColorants(node.image.Image)
if err != nil {
return err
var nativePaint *pdfgo.Paint
if !c.softMask && space.Model == "DeviceCMYK" && !space.Calibrated() {
if node.image != nil && !node.image.Image.ImageMask {
if overprint {
processMask, processOverprint, err = pdfImageProcessColorants(node.image.Image)
if err != nil {
return err
}
}
} else {
processMask, processOverprint = paint.Process.CMYKMask()
if overprint {
processMask, processOverprint = paint.Process.CMYKMask()
}
if paint.Colorant != nil {
nativePaint, processMask, err = pdfCompositeNativePaint(paint, space, style.RenderingIntent)
if err != nil {
return err
}
processOverprint = overprint && nativePaint != nil
}
}
}
var colorants *pdfShadingColorants
if node.image == nil || node.image.Image.ImageMask {
colorants = pdfPrepareShadingColorants(paint, space, c.softMask)
if colorants != nil && overprint {
processMask, processOverprint = colorants.process.Mask(), true
}
}
step := 25.4 / c.importer.rasterDPI
uniform := node.image == nil && paint.Axial == nil && paint.Radial == nil && paint.Function == nil && paint.Tiling == nil && paint.Mesh == nil && paint.Shading == nil
compatible := overprint && !processOverprint && style.OverprintMode == 1 && uniform && paint.SourceSpace == "DeviceCMYK" && space.Model == "DeviceCMYK" && !space.Calibrated()
solid := paint.Axial == nil && paint.Radial == nil && paint.Function == nil && paint.Tiling == nil && paint.Mesh == nil && paint.Shading == nil
uniform := node.image == nil && solid
compatible := overprint && !processOverprint && style.OverprintMode == 1 && solid && (node.image == nil || node.image.Image.ImageMask) && paint.SourceSpace == "DeviceCMYK" && space.Model == "DeviceCMYK" && !space.Calibrated()
sourceSpace := paint.SourceSpace
if node.image != nil && !node.image.Image.ImageMask && transfer != nil && c.transferModel == "DeviceCMYK" {
sourceSpace, err = node.image.Image.ColorSpaceFamily()
if err != nil {
return err
}
}
var marked *[4]bool
if processOverprint {
marked = &processMask
}
if transfer != nil && c.transferModel == "DeviceCMYK" && !processOverprint && sourceSpace == "DeviceGray" {
transfer = transfer.SelectChannels([4]bool{false, false, false, true})
}
var transferVariants [16]*pdfgo.TransferFunction
var transferSelected [16]bool
var pattern []pdfCompositePixel
var patternChannels []uint8
if (paint.Shading != nil || paint.Tiling != nil) && (node.image == nil || node.image.Image.ImageMask) && (c.channels != nil || c.transfers != nil) {
patternChannels = c.acquireChannels(len(pixels))
defer c.releaseChannels(patternChannels)
clear(patternChannels)
}
if paint.Shading != nil && (node.image == nil || node.image.Image.ImageMask) {
backdrop := pixels
if initial != nil {
backdrop = initial
}
pattern, err = c.shadingPattern(paint.Shading, node, outline, backdrop, space)
pattern, err = c.shadingPattern(paint.Shading, node, outline, backdrop, space, patternChannels)
if err != nil {
return err
}
@@ -167,7 +211,7 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
if initial != nil {
backdrop = initial
}
pattern, err = c.tiling(paint, backdrop, space)
pattern, err = c.tiling(paint, backdrop, space, patternChannels)
if err != nil {
return err
}
@@ -188,11 +232,11 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
var uniformValues [4]float64
uniformReady := false
var sampler *pdfCompositeColorSampler
if g := paint.Axial; node.image == nil && shading == nil && g != nil && g.Bounds == nil && g.Background == nil {
sampler = &pdfCompositeColorSampler{paint: paint, space: space, intent: style.RenderingIntent, conversion: style.ColorConversion}
if g := paint.Axial; (node.image == nil || node.image.Image.ImageMask) && shading == nil && g != nil && g.Bounds == nil && g.Background == nil {
sampler = &pdfCompositeColorSampler{paint: paint, space: space, intent: style.RenderingIntent, conversion: style.ColorConversion, colorants: colorants}
}
if g := paint.Radial; node.image == nil && shading == nil && g != nil && g.Bounds == nil && g.Background == nil {
sampler = &pdfCompositeColorSampler{paint: paint, space: space, intent: style.RenderingIntent, conversion: style.ColorConversion}
if g := paint.Radial; (node.image == nil || node.image.Image.ImageMask) && shading == nil && g != nil && g.Bounds == nil && g.Background == nil {
sampler = &pdfCompositeColorSampler{paint: paint, space: space, intent: style.RenderingIntent, conversion: style.ColorConversion, colorants: colorants}
}
bounds, err := c.geometryBounds(node, outline)
if err != nil {
@@ -217,6 +261,9 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
var values [4]float64
if pattern != nil {
sample := pattern[index]
if patternChannels != nil && patternChannels[index]&pdfTransferShape == 0 {
selectionShape = 0
}
values = sample.values
if paint.Shading != nil {
shape = sample.shape
@@ -231,9 +278,11 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
var alpha float64
_, alpha, err = c.imageColor(node.image, point, space)
if imageShape {
selectionShape = alpha
selectionShape *= alpha
shape *= alpha
} else {
opacity *= alpha
}
opacity *= alpha
}
} else if shading != nil {
values = shading[index].values
@@ -247,9 +296,9 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
var alpha float64
_, alpha, err = c.imageColor(node.image, point, space)
if imageShape {
selectionShape = alpha
selectionShape *= alpha
}
if c.shadingSource {
if c.shadingSource || imageShape {
shape *= alpha
} else {
opacity *= alpha
@@ -257,7 +306,11 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
}
} else if uniform {
if !uniformReady {
uniformValues, _, err = pdfCompositeColor(paint, pdfgo.Point{}, space, style.RenderingIntent, style.ColorConversion)
if nativePaint != nil {
uniformValues = nativePaint.Values
} else {
uniformValues, _, err = pdfCompositeColor(paint, pdfgo.Point{}, space, style.RenderingIntent, style.ColorConversion)
}
uniformReady = true
}
values = uniformValues
@@ -266,10 +319,20 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
if node.image != nil {
var alpha float64
values, alpha, err = c.imageColor(node.image, point, space)
if imageShape {
selectionShape = alpha
if nativePaint != nil {
values = nativePaint.Values
}
if c.shadingSource {
if err == nil && sampler != nil {
var visible bool
values, visible, err = sampler.color(point)
if !visible {
alpha = 0
}
}
if imageShape {
selectionShape *= alpha
}
if c.shadingSource || imageShape {
shape *= alpha
} else {
opacity *= alpha
@@ -306,14 +369,58 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
}
}
pixels[index].shape += shape * (1 - pixels[index].shape)
bits := pdfTransferChannels
if (c.channels != nil || transfer != nil && c.transferModel == "DeviceCMYK") && (compatible || marked != nil || patternChannels != nil) {
bits = 0
for j, selected := range pdfPaintChannels(values, compatible, marked) {
if selected {
bits |= 1 << j
}
}
if patternChannels != nil {
bits &= patternChannels[index]
}
}
if c.channels != nil && shape != 0 && selectionShape != 0 {
if initialChannels != nil && initial != nil {
c.channels[index] = initialChannels[index]
}
c.channels[index] |= bits | pdfTransferShape
}
if c.transfers != nil && shape != 0 && selectionShape != 0 {
c.transfers[index] = transfer
selected := transfer
if transfer != nil && c.transferModel == "DeviceCMYK" && bits != pdfTransferChannels {
if !transferSelected[bits] {
var channels [4]bool
for j := range channels {
channels[j] = bits&(1<<j) != 0
}
transferVariants[bits] = transfer.SelectChannels(channels)
transferSelected[bits] = true
}
selected = transferVariants[bits]
}
c.transfers[index] = selected
}
}
}
return nil
}
// pdfCompositeNativePaint 使用库层色料定义求值设备四色,不以备用色代替原生浓度
// 入参: paint 源画刷, space 设备四色空间, intent 渲染意图
// 返回: *pdfgo.Paint 原生画刷或nil, [4]bool 显式通道, error 色料定义错误
func pdfCompositeNativePaint(paint pdfgo.Paint, space *pdfgo.ColorSpace, intent pdfgo.Name) (*pdfgo.Paint, [4]bool, error) {
if paint.Colorant == nil {
return nil, [4]bool{}, nil
}
result, err := paint.Colorant.Space.ResolveInGroup(paint.Colorant.Tints, &pdfgo.ColorantDevice{Space: space}, space, intent, false)
if err != nil || len(result.Alternates) != 0 {
return nil, [4]bool{}, err
}
return result.Process, result.ProcessMask, nil
}
// pdfCompositeInterpolate 按形状覆盖混合预乘颜色及累计透明度
// 入参: target 原像素与输出, source 替换像素, shape 覆盖率
// 返回: error 像素或覆盖率错误
@@ -389,15 +496,26 @@ func (c *pdfCompositor) drawGroup(node pdfCompositeNode, pixels []pdfCompositePi
result := c.acquirePixels(len(initial))
defer c.releasePixels(result)
copy(result, initial)
local := *c
if c.channels != nil {
local.channels = c.acquireChannels(len(c.channels))
defer c.releaseChannels(local.channels)
if g.Isolated {
clear(local.channels)
} else {
copy(local.channels, c.channels)
}
}
if g.Knockout {
if err := c.drawKnockout(node.children, initial, result, space); err != nil {
if err := local.drawKnockout(node.children, initial, result, space); err != nil {
return err
}
} else {
if err := c.draw(node.children, result, space); err != nil {
if err := local.draw(node.children, result, space); err != nil {
return err
}
}
c.gradients, c.meshes = local.gradients, local.meshes
mask, err := c.mask(g.SoftMask, parent)
if err != nil {
return err
@@ -415,7 +533,26 @@ func (c *pdfCompositor) drawGroup(node pdfCompositeNode, pixels []pdfCompositePi
}
}
}
return pdfCompositeGroupRegion(c.importer.ctx, pixels, initial, result, c.width, region, space, parent, g.Alpha, mask, g.BlendMode, g.RenderingIntent, g.ColorConversion)
if err := pdfCompositeGroupRegion(c.importer.ctx, pixels, initial, result, c.width, region, space, parent, g.Alpha, mask, g.BlendMode, g.RenderingIntent, g.ColorConversion); err != nil {
return err
}
if c.channels != nil {
independent := g.Isolated || !space.Equal(parent)
for y := region.Min.Y; y < region.Max.Y; y++ {
if err := c.importer.ctx.Err(); err != nil {
return err
}
for i, end := y*c.width+region.Min.X, y*c.width+region.Max.X; i < end; i++ {
if result[i].shape != 0 && local.channels[i]&pdfTransferShape != 0 {
c.channels[i] = local.channels[i]
if independent {
c.channels[i] |= pdfTransferChannels
}
}
}
}
}
return nil
}
// drawKnockout 将每个对象与组初始背景合成,再按对象形状替换先前对象
@@ -428,6 +565,16 @@ func (c *pdfCompositor) drawKnockout(nodes []pdfCompositeNode, initial, pixels [
candidate := c.acquirePixels(len(initial))
defer c.releasePixels(candidate)
clear(candidate)
local := *c
var initialChannels []uint8
if c.channels != nil {
initialChannels = c.acquireChannels(len(c.channels))
defer c.releaseChannels(initialChannels)
copy(initialChannels, c.channels)
local.channels = c.acquireChannels(len(c.channels))
defer c.releaseChannels(local.channels)
clear(local.channels)
}
for _, node := range nodes {
var box Box
var err error
@@ -449,8 +596,11 @@ func (c *pdfCompositor) drawKnockout(nodes []pdfCompositeNode, initial, pixels [
}
start, end := y*c.width+region.Min.X, y*c.width+region.Max.X
copy(candidate[start:end], initial[start:end])
if initialChannels != nil {
copy(local.channels[start:end], initialChannels[start:end])
}
}
if err := c.draw([]pdfCompositeNode{node}, candidate, space); err != nil {
if err := local.draw([]pdfCompositeNode{node}, candidate, space); err != nil {
return err
}
for y := region.Min.Y; y < region.Max.Y; y++ {
@@ -459,9 +609,13 @@ func (c *pdfCompositor) drawKnockout(nodes []pdfCompositeNode, initial, pixels [
if err := pdfCompositeKnockout(&pixels[i], initial[i], candidate[i]); err != nil {
return err
}
if c.channels != nil && candidate[i].shape != 0 {
c.channels[i] = local.channels[i]
}
}
}
}
c.gradients, c.meshes = local.gradients, local.meshes
return nil
}
@@ -580,6 +734,8 @@ func (c *pdfCompositor) mask(mask *pdfgo.SoftMask, inherited *pdfgo.ColorSpace)
}
local := *c
local.transfers = nil
local.channels = nil
local.softMask = true
if err := local.draw(nodes, pixels, space); err != nil {
return nil, err
}
@@ -643,34 +799,25 @@ func (c *pdfCompositor) imageColor(mark *pdfgo.ImageMark, point pdfgo.Point, spa
c.cache.images[mark.Image] = source
}
x, y := point.X*float64(source.Rect.Dx())-.5, (1-point.Y)*float64(source.Rect.Dy())-.5
_, nativeProcess := source.Process.CMYKMask()
process := mark.Style.FillOverprint && space.Model == "DeviceCMYK" && !space.Calibrated() && len(source.Colorants) > 0
var indices [4]int
if process && !nativeProcess {
for i, name := range source.Colorants {
index := -1
for j, known := range []pdfgo.Name{"Cyan", "Magenta", "Yellow", "Black"} {
if name == known {
index = j
}
}
if index < 0 {
process = false
break
}
indices[i] = index
key := pdfImageProcessKey{image: mark.Image, space: space, softMask: c.softMask}
process, prepared := c.cache.imageProcesses[key]
if !prepared {
var err error
process, err = source.PrepareProcess(&pdfgo.ColorantDevice{Space: space}, space, c.softMask)
if err != nil {
return [4]float64{}, 0, err
}
if c.cache.imageProcesses == nil {
c.cache.imageProcesses = make(map[pdfImageProcessKey]*pdfgo.ImageProcess)
}
c.cache.imageProcesses[key] = process
}
sample := func(x, y int) ([4]float64, float64) {
x, y = max(0, min(source.Rect.Dx()-1, x)), max(0, min(source.Rect.Dy()-1, y))
values, alpha := source.ValuesAt(x, y)
if process && !nativeProcess {
values = [4]float64{}
for j, tint := range source.TintsAt(x, y) {
values[indices[j]] = float64(tint) / 65535
}
x, y = source.Rect.Min.X+max(0, min(source.Rect.Dx()-1, x)), source.Rect.Min.Y+max(0, min(source.Rect.Dy()-1, y))
if process != nil {
return process.ValuesAt(x, y)
}
return values, alpha
return source.ValuesAt(x, y)
}
values, alpha := sample(int(math.Floor(x+.5)), int(math.Floor(y+.5)))
if mark.Image.Interpolate {
@@ -712,7 +859,7 @@ func (c *pdfCompositor) imageColor(mark *pdfgo.ImageMark, point pdfgo.Point, spa
}
return color, alpha * (1 - values[0]), err
}
if process || space.Equal(source.Space) {
if process != nil || space.Equal(source.Space) {
return values, alpha, nil
}
values, err := space.ConvertWith(values[:source.Space.Components()], source.Space, mark.Style.RenderingIntent, mark.Style.ColorConversion)
@@ -723,35 +870,14 @@ func (c *pdfCompositor) imageColor(mark *pdfgo.ImageMark, point pdfgo.Point, spa
// 入参: image PDF图像
// 返回: [4]bool 参与绘制的四色通道, bool 是否全部属于印刷原色, error 色空间错误
func pdfImageProcessColorants(image *pdfgo.Image) ([4]bool, bool, error) {
var mask [4]bool
process, err := image.ProcessColorants()
source, err := image.ColorantSpace()
if err != nil {
return mask, false, err
return [4]bool{}, false, err
}
if mask, native := process.CMYKMask(); native {
return mask, true, nil
}
names, err := image.Colorants()
if err != nil {
return mask, false, err
}
if len(names) == 0 {
return mask, false, nil
}
for _, name := range names {
found := false
for j, known := range []pdfgo.Name{"Cyan", "Magenta", "Yellow", "Black"} {
if name == known {
mask[j] = true
found = true
break
}
}
if !found {
return [4]bool{}, false, nil
}
}
return mask, true, nil
space := &pdfgo.ColorSpace{Model: "DeviceCMYK"}
process := source.PrepareProcess(&pdfgo.ColorantDevice{Space: space}, space, false)
mask := process.Mask()
return mask, mask != ([4]bool{}), nil
}
// pdfCompositeColor 将原始画刷变换到混合空间,渐变在源空间求值
@@ -882,14 +1008,16 @@ func (s *pdfCompositeColorSampler) color(point pdfgo.Point) ([4]float64, bool, e
var source *pdfgo.ColorSpace
var intent pdfgo.Name
var err error
if g != nil {
if s.colorants != nil {
values, err = s.colorants.at(position)
} else if g != nil {
values, err = g.ValuesAt(position)
source, intent = g.Space, g.Intent
} else {
values, err = r.ValuesAt(position)
source, intent = r.Space, r.Intent
}
if err == nil {
if err == nil && s.colorants == nil {
if !s.converted {
s.converter, err = s.space.PrepareConversion(source, intent, s.conversion)
s.converted = err == nil
@@ -917,17 +1045,7 @@ func pdfCompositePaintOver(backdrop *pdfCompositePixel, source [4]float64, alpha
if alpha == 0 {
return nil
}
mask := [4]bool{true, true, true, true}
if marked != nil {
mask = *marked
}
if compatible {
for i := range source {
if source[i] == 0 {
mask[i] = false
}
}
}
mask := pdfPaintChannels(source, compatible, marked)
values, combined, err := space.CompositeOverprint(backdrop.values[:space.Components()], source[:space.Components()], backdrop.alpha, alpha, mode, mask[:space.Components()])
if err != nil {
return err
+59 -5
View File
@@ -31,9 +31,9 @@ type pdfCompositePattern struct {
}
// tiling 将图案作为非隔离组绘制,保留背景贡献及单元间隙
// 入参: paint 图案画刷, backdrop 初始背景, space 混合空间
// 入参: paint 图案画刷, backdrop 初始背景, space 混合空间, channels 可选源通道输出
// 返回: []pdfCompositePixel 去除背景贡献的图案像素, error 解析或绘制错误
func (c *pdfCompositor) tiling(paint pdfgo.Paint, backdrop []pdfCompositePixel, space *pdfgo.ColorSpace) ([]pdfCompositePixel, error) {
func (c *pdfCompositor) tiling(paint pdfgo.Paint, backdrop []pdfCompositePixel, space *pdfgo.ColorSpace, channels []uint8) ([]pdfCompositePixel, error) {
source := paint.Tiling
matrix := c.importer.matrix.Mul(source.Matrix)
inverse, ok := matrix.Inverse()
@@ -74,6 +74,11 @@ func (c *pdfCompositor) tiling(paint pdfgo.Paint, backdrop []pdfCompositePixel,
}
candidate := c.acquirePixels(len(backdrop))
defer c.releasePixels(candidate)
var candidateChannels []uint8
if channels != nil {
candidateChannels = c.acquireChannels(len(backdrop))
defer c.releaseChannels(candidateChannels)
}
disjoint := source.BBox.XMax-source.BBox.XMin <= xstep && source.BBox.YMax-source.BBox.YMin <= ystep
pixelStep := 25.4 / c.importer.rasterDPI
localInverse, _ := pattern.local.matrix.Inverse()
@@ -93,6 +98,10 @@ func (c *pdfCompositor) tiling(paint pdfgo.Paint, backdrop []pdfCompositePixel,
continue
}
pixels := candidate[:width*height]
var cellChannels []uint8
if channels != nil {
cellChannels = candidateChannels[:width*height]
}
for y := range height {
if err := c.importer.ctx.Err(); err != nil {
return nil, err
@@ -104,10 +113,16 @@ func (c *pdfCompositor) tiling(paint pdfgo.Paint, backdrop []pdfCompositePixel,
initial = backdrop[index]
}
pixels[y*width+x] = pdfCompositePixel{values: initial.values, alpha: initial.alpha}
if cellChannels != nil {
cellChannels[y*width+x] = 0
if !disjoint {
cellChannels[y*width+x] = channels[index]
}
}
}
}
cell := pdfCompositor{importer: &pattern.local, box: Box{X: c.box.X + float64(left)*pixelStep - dx, Y: c.box.Y + float64(top)*pixelStep - dy, W: float64(width) * pixelStep, H: float64(height) * pixelStep},
width: width, height: height, inverse: localInverse, cache: pattern.local.compositingCache(), scratch: c.scratch, masks: map[*pdfgo.SoftMask][]float64{}}
width: width, height: height, inverse: localInverse, cache: pattern.local.compositingCache(), scratch: c.scratch, masks: map[*pdfgo.SoftMask][]float64{}, softMask: c.softMask, channels: cellChannels}
err := cell.draw(pattern.nodes, pixels, space)
cell.releaseMasks()
if err != nil {
@@ -120,6 +135,13 @@ func (c *pdfCompositor) tiling(paint pdfgo.Paint, backdrop []pdfCompositePixel,
for x := range width {
index := (top+y)*c.width + left + x
pdfCompositePatternCell(&result[index], backdrop[index], pixels[y*width+x], disjoint)
if channels != nil && pixels[y*width+x].shape != 0 {
if disjoint {
channels[index] |= cellChannels[y*width+x]
} else {
channels[index] = cellChannels[y*width+x]
}
}
}
}
}
@@ -208,9 +230,21 @@ func (p *pdfImporter) compositePattern(paint pdfgo.Paint) (*pdfCompositePattern,
// 入参: node 图元, space 混合空间
// 返回: bool 是否可直接转换, error 图案解析错误
func (p *pdfImporter) directCompositeNode(node pdfCompositeNode, space *pdfgo.ColorSpace) (bool, error) {
if node.group == nil && space.Model == "DeviceCMYK" && !space.Calibrated() {
if node.group == nil && space.Device() {
style, fill, stroke := node.style()
if style.ColorConversion != (pdfgo.ColorConversion{}) {
if node.image == nil || node.image.Image.ImageMask {
for i, paint := range [2]pdfgo.Paint{style.Fill, style.Stroke} {
if i == 0 && !fill || i == 1 && !stroke {
continue
}
if native := pdfPrepareShadingColorants(paint, space, false); native != nil {
if !native.process.AlternateEquivalent() || i == 0 && style.FillOverprint || i == 1 && style.StrokeOverprint {
return false, nil
}
}
}
}
if space.Model == "DeviceCMYK" && style.ColorConversion != (pdfgo.ColorConversion{}) {
if node.image != nil && !node.image.Image.ImageMask {
source := p.compositingCache().images[node.image.Image]
if source == nil {
@@ -314,7 +348,27 @@ func (p *pdfImporter) processOverprint(nodes []pdfCompositeNode) (bool, error) {
if fill && style.FillOverprint && pdfOverprintNeedsSeparation(style.Fill) || stroke && style.StrokeOverprint && pdfOverprintNeedsSeparation(style.Stroke) {
return true, nil
}
if style.Transfer != nil || style.Halftone != nil {
if fill && style.FillOverprint && style.Fill.CMYK != nil || stroke && style.StrokeOverprint && style.Stroke.CMYK != nil {
transfer, err := p.resolveTransfer(style, "DeviceCMYK")
if err != nil || transfer != nil {
return transfer != nil, err
}
}
}
for i, paint := range []pdfgo.Paint{style.Fill, style.Stroke} {
if i == 0 && fill && style.FillOverprint || i == 1 && stroke && style.StrokeOverprint {
if native := pdfPrepareShadingColorants(paint, &pdfgo.ColorSpace{Model: "DeviceCMYK"}, false); native != nil {
return true, nil
}
native, _, err := pdfCompositeNativePaint(paint, &pdfgo.ColorSpace{Model: "DeviceCMYK"}, style.RenderingIntent)
if err != nil {
return false, err
}
if native != nil && (!pdfOpaqueBlack(*native) || !pdfOpaqueBlack(paint)) {
return true, nil
}
}
if paint.Shading != nil && (i == 0 && fill || i == 1 && stroke) {
nodes, err := p.compositeShadingPattern(paint.Shading)
if err != nil {
+22 -3
View File
@@ -24,9 +24,10 @@ type pdfCompositeBuffers[T any] [8][]T
// pdfCompositeScratch 复用像素及蒙版缓冲,不跨页持有活动内容
type pdfCompositeScratch struct {
pixels pdfCompositeBuffers[pdfCompositePixel]
masks pdfCompositeBuffers[float64]
rasters [8]*image.RGBA
pixels pdfCompositeBuffers[pdfCompositePixel]
masks pdfCompositeBuffers[float64]
channels pdfCompositeBuffers[uint8]
rasters [8]*image.RGBA
}
// acquireCoverage 借用当前块的独占画布,保留活动画布供嵌套合成读取
@@ -121,6 +122,24 @@ func (c *pdfCompositor) releasePixels(pixels []pdfCompositePixel) {
}
}
// acquireChannels 借用图案通道标记,仅在设备函数需要时分配
// 入参: count 当前块像素数
// 返回: []uint8 尚未初始化的独占通道缓冲
func (c *pdfCompositor) acquireChannels(count int) []uint8 {
if c.scratch == nil {
c.scratch = &pdfCompositeScratch{}
}
return c.scratch.channels.acquire(count)
}
// releaseChannels 归还不再参与嵌套合成的通道标记
// 入参: channels 当前调用借用的通道缓冲
func (c *pdfCompositor) releaseChannels(channels []uint8) {
if c.scratch != nil {
c.scratch.channels.release(channels)
}
}
// releaseMasks 在当前块绘制结束后归还蒙版结果,不复用其他块的坐标缓存
func (c *pdfCompositor) releaseMasks() {
if c.scratch != nil {
+13 -2
View File
@@ -56,19 +56,27 @@ func (p *pdfImporter) compositeShadingPattern(pattern *pdfgo.ShadingPattern) ([]
}
// shadingPattern 统一对象和图案的几何覆盖,按相交面积合成背景及内部着色
// 入参: pattern 着色图案, node 使用图元, outline 是否描边, backdrop 初始背景, space 父级混合空间
// 入参: pattern 着色图案, node 使用图元, outline 是否描边, backdrop 初始背景, space 父级混合空间, channels 可选源通道输出
// 返回: []pdfCompositePixel 源颜色、形状及不透明度, error 状态或合成错误
func (c *pdfCompositor) shadingPattern(pattern *pdfgo.ShadingPattern, node pdfCompositeNode, outline bool, backdrop []pdfCompositePixel, space *pdfgo.ColorSpace) ([]pdfCompositePixel, error) {
func (c *pdfCompositor) shadingPattern(pattern *pdfgo.ShadingPattern, node pdfCompositeNode, outline bool, backdrop []pdfCompositePixel, space *pdfgo.ColorSpace, channels []uint8) ([]pdfCompositePixel, error) {
nodes, err := c.importer.shadingSources(pattern, node, outline)
if err != nil {
return nil, err
}
local := *c
local.transfers, local.shadingSource = nil, true
local.channels = channels
result, err := local.shadingSourcePixels(nodes[len(nodes)-1], backdrop, space)
if err != nil || len(nodes) == 1 {
return result, err
}
var backgroundChannels []uint8
if channels != nil {
backgroundChannels = c.acquireChannels(len(channels))
defer c.releaseChannels(backgroundChannels)
clear(backgroundChannels)
}
local.channels = backgroundChannels
background, err := local.shadingSourcePixels(nodes[0], backdrop, space)
if err != nil {
c.releasePixels(result)
@@ -115,6 +123,9 @@ func (c *pdfCompositor) shadingPattern(pattern *pdfgo.ShadingPattern, node pdfCo
}
result[i].alpha, result[i].effect = alpha, alpha
result[i].shape += background[i].shape * remaining
if channels != nil && background[i].shape*remaining > 0 {
channels[i] |= backgroundChannels[i]
}
}
return result, nil
}
+2 -2
View File
@@ -74,8 +74,8 @@ func (c *pdfFormCache) reset() {
// 入参: nodes 普通表单连续图元
// 返回: bool 是否已保存为复合对象, error 检查或转换错误
func (p *pdfImporter) compositeForm(nodes []pdfCompositeNode) (bool, error) {
if pdfCompositeHasTransfer(nodes) {
return false, nil
if found, err := p.compositeHasTransfer(nodes, p.transferModel); err != nil || found {
return false, err
}
for _, node := range nodes {
if err := p.ctx.Err(); err != nil {
+7 -6
View File
@@ -24,12 +24,13 @@ import (
"github.com/xiaoqidun/pdfgo"
)
// pdfImageContentKey 区分编码内容、渲染意图、模板填充色及有损采样尺寸
// pdfImageContentKey 区分编码内容、渲染意图、原生输出空间、模板填充色及有损采样尺寸
type pdfImageContentKey struct {
data [32]byte
intent pdfgo.Name
tint color.NRGBA64
size image.Point
data [32]byte
intent pdfgo.Name
process pdfgo.Name
tint color.NRGBA64
size image.Point
}
// pdfImageContent 保存不依赖间接资源的图像描述及已注册编号
@@ -67,7 +68,7 @@ func (p *pdfImporter) imageContentKey(source *pdfgo.Image, key pdfImageKey) (pdf
hash.Write(data[:n])
data = data[n:]
}
result := pdfImageContentKey{intent: key.intent, tint: key.tint, size: key.size}
result := pdfImageContentKey{intent: key.intent, process: key.process, tint: key.tint, size: key.size}
hash.Sum(result.data[:0])
return result, true, p.ctx.Err()
}
+99
View File
@@ -0,0 +1,99 @@
// 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"
"image/color"
"math"
"github.com/xiaoqidun/pdfgo"
)
// pdfNativeImage 按源像素输出原生过程色,不分配完整显示颜色缓冲
type pdfNativeImage struct {
source *pdfgo.ImageProcess
space *pdfgo.ColorSpace
}
// imageProcessModel 选择与页面原生过程色一致的图像输出,已等价的备用色沿用现有编码路径
// 入参: source 源图像
// 返回: pdfgo.Name 原生输出空间,无需重映射时为空, error 色料解析错误
func (p *pdfImporter) imageProcessModel(source *pdfgo.Image) (pdfgo.Name, error) {
space := p.compositeSpace
if source.ImageMask || !space.Device() {
return "", nil
}
colorants, err := source.ColorantSpace()
if err != nil {
return "", err
}
process := colorants.PrepareProcess(&pdfgo.ColorantDevice{Space: space}, space, false)
if process == nil || process.Mask() == ([4]bool{}) || process.AlternateEquivalent() {
return "", nil
}
return space.Model, nil
}
// imageProcess 复用源分量并保留原始分辨率,颜色变换随编码逐像素执行
// 入参: source 源图像, model 原生设备空间
// 返回: image.Image 只读图像, error 解码或分量映射错误
func (p *pdfImporter) imageProcess(source *pdfgo.Image, model pdfgo.Name) (image.Image, error) {
cache := p.compositingCache()
components := cache.images[source]
if components == nil {
var err error
components, err = source.DecodeComponentsViewContext(p.ctx)
if err != nil {
return nil, err
}
cache.images[source] = components
}
space := &pdfgo.ColorSpace{Model: model}
process, err := components.PrepareProcess(&pdfgo.ColorantDevice{Space: space}, space, false)
if err != nil {
return nil, err
}
if process == nil {
return nil, fmt.Errorf("invalid native PDF image process space %s", model)
}
return &pdfNativeImage{source: process, space: space}, nil
}
// ColorModel 保留16位原生分量与源透明度
// 返回: color.Model 非预乘颜色模型
func (s *pdfNativeImage) ColorModel() color.Model { return color.NRGBA64Model }
// Bounds 返回源图像的完整采样边界
// 返回: image.Rectangle 原始像素边界
func (s *pdfNativeImage) Bounds() image.Rectangle { return s.source.Bounds() }
// At 获取按OFD输出映射的原生颜色
// 入参: x 横坐标, y 纵坐标
// 返回: color.Color 非预乘颜色
func (s *pdfNativeImage) At(x, y int) color.Color { return s.NRGBA64At(x, y) }
// NRGBA64At 按原始精度转换设备过程色,不进行页面采样或备用变换
// 入参: x 横坐标, y 纵坐标
// 返回: color.NRGBA64 非预乘颜色
func (s *pdfNativeImage) NRGBA64At(x, y int) color.NRGBA64 {
if !image.Pt(x, y).In(s.Bounds()) {
return color.NRGBA64{}
}
values, alpha := s.source.ValuesAt(x, y)
rgb, _ := pdfCompositeOutputColor(s.space, values)
return color.NRGBA64{R: uint16(math.Round(rgb[0] * 65535)), G: uint16(math.Round(rgb[1] * 65535)), B: uint16(math.Round(rgb[2] * 65535)), A: uint16(math.Round(alpha * 65535))}
}
+13 -4
View File
@@ -40,6 +40,7 @@ type pdfMeshKey struct {
mesh *pdfgo.MeshGradient
space *pdfgo.ColorSpace
conversion pdfgo.ColorConversion
softMask bool
}
// pdfMeshSample 保存采样点参数,patch为一基编号,零表示未覆盖
@@ -187,7 +188,7 @@ func (c *pdfCompositor) mesh(mesh *pdfgo.MeshGradient, space *pdfgo.ColorSpace,
if err := c.importer.ctx.Err(); err != nil {
return nil, err
}
key := pdfMeshKey{mesh, space, conversion}
key := pdfMeshKey{mesh, space, conversion, c.softMask}
if pixels, ok := c.meshes[key]; ok {
return pixels, nil
}
@@ -272,7 +273,11 @@ func (c *pdfCompositor) mesh(mesh *pdfgo.MeshGradient, space *pdfgo.ColorSpace,
}
sampleRows := func(start, end int) error {
colors := make(map[[4]float64][4]float64)
sampler := mesh.NewSampler()
colorants := pdfPrepareShadingColorants(pdfgo.Paint{Mesh: mesh}, space, c.softMask)
var sampler *pdfgo.MeshSampler
if colorants == nil {
sampler = mesh.NewSampler()
}
for y := start * samples; y < end*samples; y++ {
if err := c.importer.ctx.Err(); err != nil {
return err
@@ -291,13 +296,17 @@ func (c *pdfCompositor) mesh(mesh *pdfgo.MeshGradient, space *pdfgo.ColorSpace,
values := background
if sample.patch != 0 {
var err error
values, err = sampler.ValuesAt(sample.patch-1, sample.u, sample.v)
if colorants != nil {
values, err = colorants.meshAt(sample.patch-1, sample.u, sample.v)
} else {
values, err = sampler.ValuesAt(sample.patch-1, sample.u, sample.v)
}
if err != nil {
return err
}
if cached, ok := colors[values]; ok {
values = cached
} else {
} else if colorants == nil {
source := values
values, err = converter.Convert(values[:mesh.Space.Components()])
if err != nil {
+34 -15
View File
@@ -28,12 +28,13 @@ import (
"github.com/xiaoqidun/pdfgo"
)
// pdfImageKey 区分同一图像流的渲染意图及模板填充色
// pdfImageKey 区分图像流、渲染意图、原生输出空间、模板填充色及采样尺寸
type pdfImageKey struct {
stream *pdfgo.Stream
intent pdfgo.Name
tint color.NRGBA64
size image.Point
stream *pdfgo.Stream
intent pdfgo.Name
process pdfgo.Name
tint color.NRGBA64
size image.Point
}
// pdfImageResource 保存图像的有效颜色空间及对应资源,避免跨资源重映射复用
@@ -116,6 +117,10 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
}
source := mark.Image
key := pdfImageKey{stream: source.Stream, intent: source.Intent}
key.process, err = p.imageProcessModel(source)
if err != nil {
return err
}
if !source.ImageMask && p.compression.Mode == CompressionLossy {
m := p.matrix.Mul(mark.Matrix)
key.size = compressionImageSize(math.Hypot(m[0], m[1]), math.Hypot(m[2], m[3]), p.compression.ImageDPI())
@@ -164,20 +169,34 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
}
}
if id == "" {
jbig2Original, err := source.JBIG2FileContext(p.ctx)
if err != nil {
return err
}
jpegOriginal, err := source.JPEGFileContext(p.ctx)
if err != nil {
return err
}
var jbig2Original, jpegOriginal []byte
var decoded image.Image
if len(jbig2Original) == 0 && len(jpegOriginal) == 0 && (source.ImageMask || key.size != (image.Point{})) {
decoded, err = source.DecodeImageSizeContext(p.ctx, key.size)
if key.process != "" {
decoded, err = p.imageProcess(source, key.process)
if err != nil {
return err
}
if key.size != (image.Point{}) {
decoded, err = pdfgo.ResizeImage(p.ctx, decoded, key.size)
if err != nil {
return err
}
}
} else {
jbig2Original, err = source.JBIG2FileContext(p.ctx)
if err != nil {
return err
}
jpegOriginal, err = source.JPEGFileContext(p.ctx)
if err != nil {
return err
}
if len(jbig2Original) == 0 && len(jpegOriginal) == 0 && (source.ImageMask || key.size != (image.Point{})) {
decoded, err = source.DecodeImageSizeContext(p.ctx, key.size)
if err != nil {
return err
}
}
}
if source.ImageMask {
decoded = &pdfStencilImage{source: decoded, fill: key.tint}
+3 -2
View File
@@ -24,13 +24,14 @@ import (
// pdfPatternCellLimit 限制单页图案矢量快照的估算字节数
const pdfPatternCellLimit = 8 << 20
// pdfPatternCellKey 隔离图案来源、基色、坐标、精度与目标文档资源
// pdfPatternCellKey 隔离图案来源、基色、坐标、精度、混合空间与目标文档资源
type pdfPatternCellKey struct {
source *pdfgo.TilingPattern
base pdfgo.Paint
matrix pdfgo.Matrix
dpi float64
editor *Editor
space *pdfgo.ColorSpace
}
// pdfPatternInstanceRange 收缩已知路径及图片的实例范围,未知边界保留完整范围
@@ -63,7 +64,7 @@ func (p *pdfImporter) tilingPattern(source *pdfgo.TilingPattern, base pdfgo.Pain
if p.patternCells == nil {
p.patternCells = &renderCache[pdfPatternCellKey, *Pattern]{limit: pdfPatternCellLimit}
}
key := pdfPatternCellKey{source: source, matrix: p.matrix, dpi: p.rasterDPI, editor: p.editor}
key := pdfPatternCellKey{source: source, matrix: p.matrix, dpi: p.rasterDPI, editor: p.editor, space: p.compositeSpace}
if source.PaintType == 2 {
base.Tiling, base.Axial, base.Radial, base.Mesh, base.Function, base.Shading = nil, nil, nil, nil, nil, nil
base.Alpha = 1
+14 -6
View File
@@ -36,6 +36,7 @@ type pdfGradientKey struct {
space *pdfgo.ColorSpace
geometry pdfCompositeKey
conversion pdfgo.ColorConversion
softMask bool
}
// pdfICCKey 区分原始ICC空间及输出渲染意图
@@ -252,7 +253,7 @@ func (c *pdfCompositor) gradient(paint pdfgo.Paint, space *pdfgo.ColorSpace, nod
if paint.Axial != nil && paint.Axial.Background == nil && paint.Axial.Bounds == nil || paint.Radial != nil && paint.Radial.Background == nil && paint.Radial.Bounds == nil {
return nil, nil
}
key := pdfGradientKey{axial: paint.Axial, radial: paint.Radial, function: paint.Function, space: space}
key := pdfGradientKey{axial: paint.Axial, radial: paint.Radial, function: paint.Function, space: space, softMask: c.softMask}
var conversion pdfgo.ColorConversion
if node != nil {
key.geometry = pdfCompositeKey{path: node.path, text: node.text, image: node.image, stroke: outline}
@@ -360,6 +361,7 @@ func (c *pdfCompositor) gradient(paint pdfgo.Paint, space *pdfgo.ColorSpace, nod
if err != nil {
return nil, err
}
colorants := pdfPrepareShadingColorants(paint, space, c.softMask)
step := 25.4 / c.importer.rasterDPI
pixels := make([]pdfShadingPixel, c.width*c.height)
for y := range c.height {
@@ -399,21 +401,27 @@ func (c *pdfCompositor) gradient(paint pdfgo.Paint, space *pdfgo.ColorSpace, nod
local = functionInverse.Apply(point)
visible = domainClipped || local.X >= g.Domain.XMin && local.X <= g.Domain.XMax && local.Y >= g.Domain.YMin && local.Y <= g.Domain.YMax
if visible {
values, err = g.ValuesAt(local)
if err == nil {
values, err = converter.Convert(values[:source.Components()])
if colorants != nil {
values, err = colorants.pointAt(local)
} else {
values, err = g.ValuesAt(local)
if err == nil {
values, err = converter.Convert(values[:source.Components()])
}
}
}
} else {
var parameter float64
parameter, visible = position.PositionAt(point)
if visible {
if paint.Axial != nil {
if colorants != nil {
values, err = colorants.at(parameter)
} else if paint.Axial != nil {
values, err = paint.Axial.ValuesAt(parameter)
} else {
values, err = paint.Radial.ValuesAt(parameter)
}
if err == nil {
if err == nil && colorants == nil {
values, err = converter.Convert(values[:source.Components()])
}
}
+94
View File
@@ -0,0 +1,94 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import "github.com/xiaoqidun/pdfgo"
// pdfShadingColorants 复用原始浓度缓冲及原生通道映射,不跨并发采样共享
type pdfShadingColorants struct {
paint pdfgo.Paint
process *pdfgo.ColorantProcess
tints []float64
mesh *pdfgo.MeshSampler
}
// pdfShadingColorantSpace 返回渐变的源色料定义,不使用备用颜色推断通道
// 入参: paint 渐变画刷
// 返回: *pdfgo.ColorantSpace 原始色料定义,普通渐变为nil
func pdfShadingColorantSpace(paint pdfgo.Paint) *pdfgo.ColorantSpace {
switch {
case paint.Axial != nil:
return paint.Axial.ColorantSpace()
case paint.Radial != nil:
return paint.Radial.ColorantSpace()
case paint.Function != nil:
return paint.Function.ColorantSpace()
case paint.Mesh != nil:
return paint.Mesh.ColorantSpace()
}
return nil
}
// pdfPrepareShadingColorants 为当前组建立原生过程采样,备用色保留原路径
// 入参: paint 渐变画刷, space 组空间, softMask 是否用于软蒙版
// 返回: *pdfShadingColorants 独立采样状态,不可原生映射时为nil
func pdfPrepareShadingColorants(paint pdfgo.Paint, space *pdfgo.ColorSpace, softMask bool) *pdfShadingColorants {
colorants := pdfShadingColorantSpace(paint)
process := colorants.PrepareProcess(&pdfgo.ColorantDevice{Space: space}, space, softMask)
if process == nil || process.Mask() == ([4]bool{}) {
return nil
}
s := &pdfShadingColorants{paint: paint, process: process, tints: make([]float64, len(colorants.Names))}
if paint.Mesh != nil {
s.mesh = paint.Mesh.NewSampler()
}
return s
}
// at 在归一化位置求值轴向或径向原始浓度
// 入参: position 归一化渐变参数
// 返回: [4]float64 原生过程分量, error 求值错误
func (s *pdfShadingColorants) at(position float64) ([4]float64, error) {
var err error
if s.paint.Axial != nil {
err = s.paint.Axial.ColorantValuesAt(position, s.tints)
} else {
err = s.paint.Radial.ColorantValuesAt(position, s.tints)
}
if err != nil {
return [4]float64{}, err
}
return s.process.Convert(s.tints)
}
// pointAt 在函数定义域内求值原始浓度
// 入参: point 函数坐标
// 返回: [4]float64 原生过程分量, error 求值错误
func (s *pdfShadingColorants) pointAt(point pdfgo.Point) ([4]float64, error) {
if err := s.paint.Function.ColorantValuesAt(point, s.tints); err != nil {
return [4]float64{}, err
}
return s.process.Convert(s.tints)
}
// meshAt 插值网格原始浓度并复用函数结果
// 入参: patch 网格序号, u 横向参数, v 纵向参数
// 返回: [4]float64 原生过程分量, error 求值错误
func (s *pdfShadingColorants) meshAt(patch int, u, v float64) ([4]float64, error) {
if err := s.mesh.ColorantValuesAt(patch, u, v, s.tints); err != nil {
return [4]float64{}, err
}
return s.process.Convert(s.tints)
}
+64 -23
View File
@@ -16,54 +16,78 @@ package ofdgo
import "github.com/xiaoqidun/pdfgo"
// pdfTransferKey 区分图形状态与网屏覆盖函数
// 图案标记保留设备四色通道及非零对象形状
const (
pdfTransferChannels uint8 = 15
pdfTransferShape uint8 = 16
)
// pdfTransferKey 区分图形状态、网屏覆盖函数及输出设备
type pdfTransferKey struct {
transfer *pdfgo.TransferFunction
halftone *pdfgo.Halftone
model pdfgo.Name
}
// resolveTransfer 在页面内复用连续RGB设备的有效传递函数
// 入参: style 原始图形状态与有效输出状态
// 返回: error 网屏传递函数错误
func (p *pdfImporter) resolveTransfer(style *pdfgo.Style) error {
// resolveTransfer 在页面内复用输出设备的有效函数,不修改原始图形状态
// 入参: style 原始图形状态, model 原生设备颜色空间
// 返回: *pdfgo.TransferFunction 有效函数, error 网屏传递函数错误
func (p *pdfImporter) resolveTransfer(style pdfgo.Style, model pdfgo.Name) (*pdfgo.TransferFunction, error) {
if style.Halftone == nil && style.Transfer == nil {
return nil
return nil, nil
}
if model == "" {
model = "DeviceRGB"
}
cache := p.compositingCache()
if cache.transfers == nil {
cache.transfers = make(map[pdfTransferKey]*pdfgo.TransferFunction)
}
key := pdfTransferKey{style.Transfer, style.Halftone}
key := pdfTransferKey{style.Transfer, style.Halftone, model}
transfer, found := cache.transfers[key]
if !found {
var err error
transfer, err = p.reader.DeviceTransfer(style.Transfer, style.Halftone, "DeviceRGB")
transfer, err = p.reader.DeviceTransfer(style.Transfer, style.Halftone, model)
if err != nil {
return err
return nil, err
}
cache.transfers[key] = transfer
}
style.Transfer = transfer
return nil
return transfer, nil
}
// pdfCompositeHasTransfer 检查页面是否需要保留最终设备传递函数
// 入参: nodes 原始图元及透明组
// 返回: bool 是否存在非恒等函数
func pdfCompositeHasTransfer(nodes []pdfCompositeNode) bool {
// compositeHasTransfer 检查最终设备使用的通道,不将备用通道误判为有效函数
// 入参: nodes 原始图元及透明组, model 原生设备颜色空间
// 返回: bool 是否存在非恒等函数, error 网屏函数错误
func (p *pdfImporter) compositeHasTransfer(nodes []pdfCompositeNode, model pdfgo.Name) (bool, error) {
for _, node := range nodes {
if node.group != nil {
if pdfCompositeHasTransfer(node.children) {
return true
if found, err := p.compositeHasTransfer(node.children, model); err != nil || found {
return found, err
}
} else {
style, _, _ := node.style()
if style.Transfer != nil {
return true
transfer, err := p.resolveTransfer(style, model)
if err != nil || transfer != nil {
return transfer != nil, err
}
}
}
return false
return false, nil
}
// transferDevice 为四色叠印保留原生设备通道,其余页面使用连续RGB设备
// 入参: space 页面混合空间, nodes 参与合成的图元
// 返回: pdfgo.Name 原生设备空间, error 图案或图像解析错误
func (p *pdfImporter) transferDevice(space *pdfgo.ColorSpace, nodes []pdfCompositeNode) (pdfgo.Name, error) {
if space.Model == "DeviceCMYK" && !space.Calibrated() {
if found, err := p.processOverprint(nodes); err != nil {
return "", err
} else if found {
return "DeviceCMYK", nil
}
}
return "DeviceRGB", nil
}
// selectTransfer 按最上层基本图元及祖先的不透明状态选择最终设备函数
@@ -78,8 +102,9 @@ func (c *pdfCompositor) selectTransfer(node pdfCompositeNode, outline bool) (*pd
if c.transferBlocked || paint.Alpha != 1 || style.SoftMask != nil || !pdfNormalBlend(style.BlendMode) {
return nil, nil
}
if style.Transfer == nil {
return nil, nil
transfer, err := c.importer.resolveTransfer(style, c.transferModel)
if err != nil || transfer == nil {
return transfer, err
}
if pattern := paint.Shading; pattern != nil && (node.image == nil || node.image.Image.ImageMask) {
inner := pattern.Style
@@ -103,7 +128,23 @@ func (c *pdfCompositor) selectTransfer(node pdfCompositeNode, outline bool) (*pd
return nil, err
}
}
return style.Transfer, nil
return transfer, nil
}
// pdfPaintChannels 选择兼容叠印实际使用源颜色的通道
// 入参: values 源四色分量, compatible 是否保留零分量背景, marked 指定通道,nil表示全部
// 返回: [4]bool 各通道是否取源颜色
func pdfPaintChannels(values [4]float64, compatible bool, marked *[4]bool) [4]bool {
channels := [4]bool{true, true, true, true}
if marked != nil {
channels = *marked
}
if compatible {
for i := range channels {
channels[i] = channels[i] && values[i] != 0
}
}
return channels
}
// opaqueTransfer 复用图案不透明性检查,图案内部函数不提前作用于源颜色
+5 -5
View File
@@ -69,18 +69,18 @@ func (c *renderCache[K, V]) put(key K, value V, cost int) {
c.used += cost
}
// rasterCommandCost 估算指令快照及后端路径的缓存成本,不含借用图片
// rasterCommandCost 按保留容量估算中立指令成本,不含临时后端路径及借用图片
// 入参: command 绘制指令
// 返回: int 字节成本
func rasterCommandCost(command RasterCommand) int {
cost := 512 + (len(command.Path)+len(command.Clip))*208
cost := 512 + (cap(command.Path)+cap(command.Clip))*56
if command.Stroke != nil {
cost += 128 + len(command.Stroke.Dashes)*16
cost += 128 + cap(command.Stroke.Dashes)*8
}
if command.Paint.Gradient != nil {
cost += 256 + len(command.Paint.Gradient.Stops)*64
cost += 256 + cap(command.Paint.Gradient.Stops)*64
if mesh := command.Paint.Gradient.Mesh; mesh != nil {
cost += 32 + len(mesh.Triangles)*240
cost += 32 + cap(mesh.Triangles)*240
}
}
return cost
+15 -10
View File
@@ -59,6 +59,7 @@ type U3DNode struct {
Visibility uint32
Shaders [][]string
LineShaders [][]string
PointShaders [][]string
View *U3DView
}
@@ -111,8 +112,9 @@ type u3dDecoder struct {
palettes map[uint32]map[string]bool
}
// DecodeU3D 按ECMA-363解码静态基础网格、线集、组、材质、着色器和光源
// DecodeU3D 按ECMA-363解码静态基础网格、线集、点集、组、材质、着色器和光源
// 暂不支持渐进网格、纹理和动画,遇到未支持内容返回错误而非部分场景
// 点集球面预测暂限不超过两个非对跖方向,不推测多方向合并次序
// 入参: ctx 取消上下文, data U3D数据, options 解码限制
// 返回: *U3DModel 独立场景数据, error 格式、能力、预算或取消错误
func DecodeU3D(ctx context.Context, data []byte, options U3DOptions) (*U3DModel, error) {
@@ -175,7 +177,7 @@ func DecodeU3D(ctx context.Context, data []byte, options U3DOptions) (*U3DModel,
if err != nil {
return nil, fmt.Errorf("U3D block at %d: %w", offset, err)
}
continuation := block.kind == 0xffffff15 || block.kind == 0xffffff3b || block.kind == 0xffffff3c || block.kind == 0xffffff3f
continuation := block.kind == 0xffffff15 || block.kind == 0xffffff3b || block.kind == 0xffffff3c || block.kind == 0xffffff3e || block.kind == 0xffffff3f
if uint64(offset) < declarationEnd && (uint64(end) > declarationEnd || continuation) || uint64(offset) >= declarationEnd && !continuation {
return nil, fmt.Errorf("invalid U3D declaration boundary at %d", offset)
}
@@ -382,16 +384,16 @@ func (d *u3dDecoder) block(block u3dBlock, owner string, chain int, index uint32
return name, fmt.Errorf("invalid U3D mesh continuation location")
}
err = d.baseMesh(&r, name)
case 0xffffff37:
case 0xffffff36, 0xffffff37:
if chain >= 0 && (chain != 1 || index != 0) {
return name, fmt.Errorf("invalid U3D line set chain")
return name, fmt.Errorf("invalid U3D primitive chain")
}
err = d.lineDeclaration(&r, name)
case 0xffffff3f:
err = d.primitiveDeclaration(&r, name, block.kind == 0xffffff36)
case 0xffffff3e, 0xffffff3f:
if chain >= 0 {
return name, fmt.Errorf("invalid U3D line continuation location")
return name, fmt.Errorf("invalid U3D primitive continuation location")
}
err = d.lineContinuation(&r, name)
err = d.primitiveContinuation(&r, name, block.kind == 0xffffff3e)
case 0xffffff45:
if chain != 0 || index == 0 || owner != name || r.u32() != index {
return name, fmt.Errorf("invalid U3D shading modifier chain")
@@ -461,7 +463,7 @@ func (d *u3dDecoder) node(r *u3dValues, kind uint32, name string) error {
return r.err
}
// shading 分别读取网格和线集着色列表,保留列表内各着色器的先后次序
// shading 分别读取网格、线集和点集着色列表,保留列表内各着色器的先后次序
// 入参: r 字段读取器, name 目标节点名
// 返回: error 结构、能力或预算错误
func (d *u3dDecoder) shading(r *u3dValues, name string) error {
@@ -470,7 +472,7 @@ func (d *u3dDecoder) shading(r *u3dValues, name string) error {
return fmt.Errorf("missing U3D shading target")
}
flags := r.u32()
if flags & ^uint32(3) != 0 {
if flags & ^uint32(7) != 0 {
return fmt.Errorf("unsupported U3D shading modifier target")
}
count := r.u32()
@@ -505,6 +507,9 @@ func (d *u3dDecoder) shading(r *u3dValues, name string) error {
if flags&2 != 0 {
d.model.Nodes[index].LineShaders = lists
}
if flags&4 != 0 {
d.model.Nodes[index].PointShaders = lists
}
return r.err
}
+25 -13
View File
@@ -19,44 +19,50 @@ import (
"math"
)
// drawLines 绘制作者线集,端点独立着色且不参与面的背向剔除
// drawPrimitives 绘制作者点集及线集,独立着色且不参与面的背向剔除
// 入参: instance 场景实例, node 模型节点, mesh 属性数组, positions 世界位置, normals 世界法线, pass 绘制轮次, passIndex 轮次索引
// 返回: error 几何、资源或取消错误
func (r *u3dRender) drawLines(instance u3dInstance, node *U3DNode, mesh *U3DMesh, positions, normals []u3dVector, pass U3DViewPass, passIndex int) error {
func (r *u3dRender) drawPrimitives(instance u3dInstance, node *U3DNode, mesh *U3DMesh, positions, normals []u3dVector, pass U3DViewPass, passIndex int) error {
if r.depthOnly {
return nil
}
shaded := r.mode.shaded()
for index, line := range mesh.Lines {
for index := range len(mesh.Lines) + len(mesh.Points) {
shadingIndex, corners := mesh.primitive(len(mesh.Faces) + index)
point := index >= len(mesh.Lines)
if index%128 == 0 {
if err := r.ctx.Err(); err != nil {
return err
}
}
if uint64(line.Shading) >= uint64(len(mesh.Shadings)) {
return fmt.Errorf("invalid U3D render line shading index")
if uint64(shadingIndex) >= uint64(len(mesh.Shadings)) {
return fmt.Errorf("invalid U3D render primitive shading index")
}
shading := mesh.Shadings[line.Shading]
shading := mesh.Shadings[shadingIndex]
if shading.Attributes > 3 || len(shading.TextureDimensions) > 8 {
return fmt.Errorf("invalid U3D render line shading")
return fmt.Errorf("invalid U3D render primitive shading")
}
var world [2]u3dVector
var vertices [2]u3dVertex
for i, corner := range line.Corners {
for i, corner := range corners {
if uint64(corner.Position) >= uint64(len(positions)) || shaded && (!mesh.ExcludeNormals && uint64(corner.Normal) >= uint64(len(normals)) || shading.Attributes&1 != 0 && uint64(corner.Diffuse) >= uint64(len(mesh.Diffuse)) || shading.Attributes&2 != 0 && uint64(corner.Specular) >= uint64(len(mesh.Specular))) {
return fmt.Errorf("invalid U3D render line corner index")
return fmt.Errorf("invalid U3D render primitive corner index")
}
world[i] = positions[corner.Position]
vertices[i].point = r.toCamera.point(world[i])
for _, value := range vertices[i].point {
if !finite(value) {
return fmt.Errorf("invalid U3D line camera position")
return fmt.Errorf("invalid U3D primitive camera position")
}
}
}
shaderNames := []string{""}
if uint64(line.Shading) < uint64(len(node.LineShaders)) {
shaderNames = node.LineShaders[line.Shading]
shaders := node.LineShaders
if point {
shaders = node.PointShaders
}
if uint64(shadingIndex) < uint64(len(shaders)) {
shaderNames = shaders[shadingIndex]
}
for _, name := range shaderNames {
if err := r.ctx.Err(); err != nil {
@@ -78,7 +84,7 @@ func (r *u3dRender) drawLines(instance u3dInstance, node *U3DNode, mesh *U3DMesh
if instance.opacity >= 0 || r.opacity >= 0 {
style.shader.Blend = 0x606
}
for i, corner := range line.Corners {
for i, corner := range corners {
vertices[i].color = [4]float64{r.auxiliaryColor[0], r.auxiliaryColor[1], r.auxiliaryColor[2], 1}
if !shaded {
continue
@@ -105,6 +111,12 @@ func (r *u3dRender) drawLines(instance u3dInstance, node *U3DNode, mesh *U3DMesh
vertices[i].color[3] *= r.opacity
}
}
if point {
if err := r.modePoint(vertices[0], style.shader, pass); err != nil {
return err
}
continue
}
if r.mode == U3DRenderVertices || r.mode == U3DRenderShadedVertices {
for _, vertex := range vertices {
fresh, err := r.uniquePrimitive(vertex, vertex, style.shader)
+98 -47
View File
@@ -49,11 +49,13 @@ const (
u3dLineContextCount
)
// u3dLineState 按顶点累计线段端点属性,预测开销不随顶点度数增长
// u3dLineState 按顶点缓存点集及线集属性预测
type u3dLineState struct {
offsets [11]int
stride int
stats []float64
offsets [11]int
stride int
stats []float64
pointOffsets []uint32
pointPredicted []bool
}
// u3dLineReader 保存当前续块的独立压缩上下文
@@ -63,17 +65,17 @@ type u3dLineReader struct {
hist [u3dLineContextCount]u3dHistogram
}
// lineDeclaration 读取ECMA-363第9.6.3节的线集声明
// 入参: r 字段读取器, name 资源名
// primitiveDeclaration 读取ECMA-363第9.6.2和9.6.3节的点集及线集声明
// 入参: r 字段读取器, name 资源名, points 是否为点集
// 返回: error 结构、能力或预算错误
func (d *u3dDecoder) lineDeclaration(r *u3dValues, name string) error {
func (d *u3dDecoder) primitiveDeclaration(r *u3dValues, name string, points bool) error {
if _, exists := d.meshes[name]; exists {
return fmt.Errorf("duplicate U3D model resource %q", name)
}
if r.u32() != 0 || r.u32() != 0 {
return fmt.Errorf("invalid U3D line declaration")
return fmt.Errorf("invalid U3D primitive declaration")
}
decl := &u3dMeshDeclaration{mesh: U3DMesh{Name: name}, line: new(u3dLineState)}
decl := &u3dMeshDeclaration{mesh: U3DMesh{Name: name}, line: new(u3dLineState), points: points}
for i := range decl.counts {
decl.counts[i] = r.u32()
}
@@ -88,11 +90,11 @@ func (d *u3dDecoder) lineDeclaration(r *u3dValues, name string) error {
}
for range 3 {
if r.u32() != 0 {
return fmt.Errorf("invalid U3D reserved line parameter")
return fmt.Errorf("invalid U3D reserved primitive parameter")
}
}
if r.u32() != 0 {
return fmt.Errorf("unsupported U3D skeletal line set")
return fmt.Errorf("unsupported U3D skeletal primitive set")
}
if err := r.done(); err != nil {
return err
@@ -100,7 +102,7 @@ func (d *u3dDecoder) lineDeclaration(r *u3dValues, name string) error {
if decl.counts[1] == 0 {
for _, count := range decl.counts[:6] {
if count != 0 {
return fmt.Errorf("invalid U3D empty line set")
return fmt.Errorf("invalid U3D empty primitive set")
}
}
decl.decoded = true
@@ -123,7 +125,11 @@ func (d *u3dDecoder) lineDeclaration(r *u3dValues, name string) error {
}
}
c := decl.counts
bytes := uint64(c[0])*100 + (uint64(c[1])+uint64(c[2]))*12 + (uint64(c[3])+uint64(c[4])+uint64(c[5]))*16
primitiveBytes := uint64(100)
if points {
primitiveBytes = 52
}
bytes := uint64(c[0])*primitiveBytes + (uint64(c[1])+uint64(c[2]))*12 + (uint64(c[3])+uint64(c[4])+uint64(c[5]))*16
if err := d.reserve(bytes, 1); err != nil {
return err
}
@@ -131,16 +137,27 @@ func (d *u3dDecoder) lineDeclaration(r *u3dValues, name string) error {
return err
}
m := &decl.mesh
m.Lines = make([]U3DLine, 0, int(c[0]))
if points {
m.Points = make([]U3DPoint, 0, int(c[0]))
} else {
m.Lines = make([]U3DLine, 0, int(c[0]))
}
m.Positions, m.Normals = make([][3]float32, 0, int(c[1])), make([][3]float32, 0, int(c[2]))
m.Diffuse, m.Specular, m.TextureCoordinates = make([][4]float32, 0, int(c[3])), make([][4]float32, 0, int(c[4])), make([][4]float32, 0, int(c[5]))
state.stats = make([]float64, int(c[1])*state.stride)
if points {
if err := d.reserve(uint64(c[1])*5+4, 1); err != nil {
return err
}
state.pointOffsets = make([]uint32, 1, int(c[1])+1)
state.pointPredicted = make([]bool, int(c[1]))
}
d.meshes[name] = decl
d.meshOrder = append(d.meshOrder, name)
return nil
}
// average 取得指定顶点各属性的端点算术平均
// average 取得指定顶点各属性预测,点集法线使用缓存的球面均值
// 入参: position 顶点索引
// 返回: [11][4]float64 法线、漫反射、镜面及八层纹理预测
func (s *u3dLineState) average(position uint32) (values [11][4]float64) {
@@ -162,8 +179,14 @@ func (s *u3dLineState) average(position uint32) (values [11][4]float64) {
// accumulate 累计线段端点的独立属性,重复使用的属性按端点分别计数
// 入参: mesh 共享属性数组, line 已完成的线段
func (s *u3dLineState) accumulate(mesh *U3DMesh, line U3DLine) {
shading := mesh.Shadings[line.Shading]
for _, corner := range line.Corners {
s.accumulateCorners(mesh, line.Shading, line.Corners[:])
}
// accumulateCorners 累计点或线端的属性,用于后续位置预测
// 入参: mesh 属性数组, shadingIndex 着色索引, corners 新增角点
func (s *u3dLineState) accumulateCorners(mesh *U3DMesh, shadingIndex uint32, corners []U3DCorner) {
shading := mesh.Shadings[shadingIndex]
for _, corner := range corners {
var values [11][4]float32
var used [11]bool
normal := mesh.Normals[corner.Normal]
@@ -191,7 +214,7 @@ func (s *u3dLineState) accumulate(mesh *U3DMesh, line U3DLine) {
}
}
// value 按指定上下文和位宽读取线集字段
// value 按指定上下文和位宽读取点集或线集字段
// 入参: context 压缩上下文, small 是否为8位字段
// 返回: uint32 字段值
func (r *u3dLineReader) value(context int, small bool) uint32 {
@@ -209,7 +232,7 @@ func (r *u3dLineReader) value(context int, small bool) uint32 {
// 返回: uint32 索引, error 读取或范围错误
func (r *u3dLineReader) index(count uint32) (uint32, error) {
if count == 0 {
return 0, fmt.Errorf("invalid U3D line position range")
return 0, fmt.Errorf("invalid U3D primitive position range")
}
var value uint32
if r.bits != nil {
@@ -221,7 +244,7 @@ func (r *u3dLineReader) index(count uint32) (uint32, error) {
return 0, err
}
if value >= count {
return 0, fmt.Errorf("invalid U3D line position index")
return 0, fmt.Errorf("invalid U3D primitive position index")
}
return value, nil
}
@@ -241,7 +264,7 @@ func (r *u3dLineReader) err() error {
func (r *u3dLineReader) reconstruct(prediction [4]float64, scale float32, signContext, componentContext, dimensions int) (values [4]float32, err error) {
signs := r.value(signContext, true)
if signs >= 1<<dimensions {
return values, fmt.Errorf("invalid U3D line difference signs")
return values, fmt.Errorf("invalid U3D primitive difference signs")
}
for i := range dimensions {
difference := float64(r.value(componentContext+i, false)) * float64(scale)
@@ -250,7 +273,7 @@ func (r *u3dLineReader) reconstruct(prediction [4]float64, scale float32, signCo
}
values[i] = float32(prediction[i] + difference)
if !finite(float64(values[i])) {
return values, fmt.Errorf("invalid U3D reconstructed line attribute")
return values, fmt.Errorf("invalid U3D reconstructed primitive attribute")
}
}
return values, r.err()
@@ -268,7 +291,7 @@ func (r *u3dLineReader) attribute(pool *[][4]float32, limit uint32, prediction [
return uint32(len(*pool) - 1), nil
}
if flag != 0 || uint64(len(*pool)) >= uint64(limit) {
return 0, fmt.Errorf("invalid U3D line attribute pool")
return 0, fmt.Errorf("invalid U3D primitive attribute pool")
}
value, err := r.reconstruct(prediction, scale, sign, component, 4)
if err != nil {
@@ -279,13 +302,13 @@ func (r *u3dLineReader) attribute(pool *[][4]float32, limit uint32, prediction [
return index, nil
}
// lineContinuation 按位置分段恢复线集,预测状态跨续块保存
// 入参: r 字段读取器, name 资源名
// primitiveContinuation 按位置分段恢复点集及线集,预测状态跨续块保存
// 入参: r 字段读取器, name 资源名, points 是否为点集
// 返回: error 结构、数值、预算或取消错误
func (d *u3dDecoder) lineContinuation(r *u3dValues, name string) error {
func (d *u3dDecoder) primitiveContinuation(r *u3dValues, name string, points bool) error {
decl := d.meshes[name]
if decl == nil || decl.line == nil || decl.decoded || r.u32() != 0 {
return fmt.Errorf("invalid U3D line continuation reference")
if decl == nil || decl.line == nil || decl.points != points || decl.decoded || r.u32() != 0 {
return fmt.Errorf("invalid U3D primitive continuation reference")
}
start, end := r.u32(), r.u32()
mesh := &decl.mesh
@@ -293,7 +316,7 @@ func (d *u3dDecoder) lineContinuation(r *u3dValues, name string) error {
return r.err
}
if uint64(start) != uint64(len(mesh.Positions)) || end <= start || end > decl.counts[1] {
return fmt.Errorf("invalid U3D line resolution range")
return fmt.Errorf("invalid U3D primitive resolution range")
}
reader := &u3dLineReader{values: r}
var charged uint64
@@ -322,7 +345,6 @@ func (d *u3dDecoder) lineContinuation(r *u3dValues, name string) error {
prediction[i] = float64(value)
}
}
attributes := decl.line.average(split)
value, err := reader.reconstruct(prediction, mesh.InverseQuantization[0], u3dLinePositionSign, u3dLinePositionX, 3)
if err != nil {
return err
@@ -331,8 +353,21 @@ func (d *u3dDecoder) lineContinuation(r *u3dValues, name string) error {
normalStart := uint32(len(mesh.Normals))
normals := reader.value(u3dLineNormalCount, false)
if normals > decl.counts[2]-normalStart {
return fmt.Errorf("invalid U3D line normal count")
return fmt.Errorf("invalid U3D primitive normal count")
}
if points && position > 0 && normals > 0 && !decl.line.pointPredicted[split] {
first, last := decl.line.pointOffsets[split], decl.line.pointOffsets[split+1]
prediction, err := d.pointNormalPrediction(mesh, mesh.Points[first:last])
if err != nil {
return err
}
base := int(split)*decl.line.stride + decl.line.offsets[0]
for i, value := range prediction {
decl.line.stats[base+i] = value * float64(last-first)
}
decl.line.pointPredicted[split] = true
}
attributes := decl.line.average(split)
for i := uint32(0); i < normals; i++ {
if i%256 == 0 {
if err := d.ctx.Err(); err != nil {
@@ -346,8 +381,8 @@ func (d *u3dDecoder) lineContinuation(r *u3dValues, name string) error {
mesh.Normals = append(mesh.Normals, [3]float32{value[0], value[1], value[2]})
}
lines := reader.value(u3dLineCount, false)
if lines > decl.counts[0]-uint32(len(mesh.Lines)) {
return fmt.Errorf("invalid U3D line count")
if lines > decl.counts[0]-uint32(len(mesh.Lines)+len(mesh.Points)) {
return fmt.Errorf("invalid U3D primitive count")
}
for i := uint32(0); i < lines; i++ {
if i%256 == 0 {
@@ -357,19 +392,24 @@ func (d *u3dDecoder) lineContinuation(r *u3dValues, name string) error {
}
line := U3DLine{Shading: reader.value(u3dLineShading, false)}
if uint64(line.Shading) >= uint64(len(mesh.Shadings)) {
return fmt.Errorf("invalid U3D line shading index")
return fmt.Errorf("invalid U3D primitive shading index")
}
first, err := reader.index(position)
if err != nil {
return err
corners := line.Corners[:1]
corners[0].Position = position
if !points {
first, err := reader.index(position)
if err != nil {
return err
}
line.Corners[0].Position, line.Corners[1].Position = first, position
corners = line.Corners[:]
}
line.Corners[0].Position, line.Corners[1].Position = first, position
shading := mesh.Shadings[line.Shading]
for j := range line.Corners {
corner := &line.Corners[j]
for j := range corners {
corner := &corners[j]
normal := reader.value(u3dLineNormalIndex, false)
if normal >= normals {
return fmt.Errorf("invalid U3D line local normal index")
return fmt.Errorf("invalid U3D primitive local normal index")
}
corner.Normal = normalStart + normal
if shading.Attributes&1 != 0 {
@@ -394,8 +434,15 @@ func (d *u3dDecoder) lineContinuation(r *u3dValues, name string) error {
if err := reader.err(); err != nil {
return err
}
mesh.Lines = append(mesh.Lines, line)
decl.line.accumulate(mesh, line)
if points {
mesh.Points = append(mesh.Points, U3DPoint{Shading: line.Shading, Corners: [1]U3DCorner{corners[0]}})
} else {
mesh.Lines = append(mesh.Lines, line)
}
decl.line.accumulateCorners(mesh, line.Shading, corners)
}
if points {
decl.line.pointOffsets = append(decl.line.pointOffsets, uint32(len(mesh.Points)))
}
if err := reader.err(); err != nil {
return err
@@ -406,13 +453,13 @@ func (d *u3dDecoder) lineContinuation(r *u3dValues, name string) error {
return io.ErrUnexpectedEOF
}
if b.u32() != 0 {
return fmt.Errorf("invalid U3D line compression flush")
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 line compressed data")
return fmt.Errorf("unexpected U3D primitive compressed data")
}
r.pos = len(r.data)
}
@@ -420,14 +467,18 @@ func (d *u3dDecoder) lineContinuation(r *u3dValues, name string) error {
return err
}
if end == decl.counts[1] {
for i, count := range []int{len(mesh.Lines), len(mesh.Positions), len(mesh.Normals), len(mesh.Diffuse), len(mesh.Specular), len(mesh.TextureCoordinates)} {
for i, count := range []int{len(mesh.Lines) + len(mesh.Points), len(mesh.Positions), len(mesh.Normals), len(mesh.Diffuse), len(mesh.Specular), len(mesh.TextureCoordinates)} {
if uint64(count) != uint64(decl.counts[i]) {
return fmt.Errorf("U3D line set does not match declaration")
return fmt.Errorf("U3D primitive set does not match declaration")
}
}
decl.decoded = true
d.remaining += uint64(len(decl.line.stats)) * 8
decl.line.stats = nil
if points {
d.remaining += uint64(cap(decl.line.pointOffsets))*4 + uint64(len(decl.line.pointPredicted))
decl.line.pointOffsets, decl.line.pointPredicted = nil, nil
}
}
return nil
}
+19 -6
View File
@@ -19,7 +19,7 @@ import (
"io"
)
// U3DMesh 保存静态三角面和线段的共享属性数组,索引对应原始作者几何
// U3DMesh 保存静态三角面、线段和点的共享属性数组,索引对应原始作者几何
type U3DMesh struct {
Name string
ExcludeNormals bool
@@ -28,6 +28,7 @@ type U3DMesh struct {
Shadings []U3DShading
Faces []U3DFace
Lines []U3DLine
Points []U3DPoint
Quality [3]uint32
InverseQuantization [5]float32
NormalCrease, NormalUpdate, NormalTolerance float32
@@ -52,29 +53,41 @@ type U3DLine struct {
Corners [2]U3DCorner
}
// U3DPoint 保存点集着色描述及独立属性索引
type U3DPoint struct {
Shading uint32
Corners [1]U3DCorner
}
// U3DCorner 保存各属性索引,未启用的属性不可用于访问数组
type U3DCorner struct {
Position, Normal, Diffuse, Specular uint32
Texture [8]uint32
}
// u3dMeshDeclaration 保存基础网格或线集声明及续块接收状态
// u3dMeshDeclaration 保存几何声明及续块接收状态
type u3dMeshDeclaration struct {
mesh U3DMesh
counts [7]uint32
decoded bool
line *u3dLineState
points bool
}
// primitive 访问三角面或线段的着色编号和角点,不复制属性数组
// 入参: index 图元下标,三角面在前,线段在后
// primitive 访问三角面、线段或点的着色编号和角点,不复制属性数组
// 入参: index 图元下标,依次为三角面、线段和点
// 返回: uint32 着色编号, []U3DCorner 只读角点
func (m *U3DMesh) primitive(index int) (uint32, []U3DCorner) {
if index < len(m.Faces) {
return m.Faces[index].Shading, m.Faces[index].Corners[:]
}
line := &m.Lines[index-len(m.Faces)]
return line.Shading, line.Corners[:]
index -= len(m.Faces)
if index < len(m.Lines) {
line := &m.Lines[index]
return line.Shading, line.Corners[:]
}
point := &m.Points[index-len(m.Lines)]
return point.Shading, point.Corners[:]
}
// meshDeclaration 读取完整基础网格声明,不将渐进网格当作完整模型
+4 -1
View File
@@ -236,12 +236,15 @@ func (r *u3dRender) drawBounds(instance u3dInstance, node *U3DNode, mesh *U3DMes
minimum := u3dVector{math.Inf(1), math.Inf(1), math.Inf(1)}
maximum := u3dVector{math.Inf(-1), math.Inf(-1), math.Inf(-1)}
visible := false
for i := range len(mesh.Faces) + len(mesh.Lines) {
for i := range len(mesh.Faces) + len(mesh.Lines) + len(mesh.Points) {
shading, corners := mesh.primitive(i)
shaders := node.Shaders
if i >= len(mesh.Faces) {
shaders = node.LineShaders
}
if i >= len(mesh.Faces)+len(mesh.Lines) {
shaders = node.PointShaders
}
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
return err
+71
View File
@@ -0,0 +1,71 @@
// 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"
)
// pointNormalPrediction 按ECMA-363第9.6.2.2.4.4节计算点集球面法线预测
// 入参: mesh 属性数组, points 分裂位置的点
// 返回: u3dVector 预测方向, error 不支持的多方向预测或取消错误
func (d *u3dDecoder) pointNormalPrediction(mesh *U3DMesh, points []U3DPoint) (u3dVector, error) {
var directions [2]u3dVector
var counts [2]int
var previous [3]float32
var direction u3dVector
for i, point := range points {
if i%256 == 0 {
if err := d.ctx.Err(); err != nil {
return u3dVector{}, err
}
}
normal := mesh.Normals[point.Corners[0].Normal]
if i == 0 || normal != previous {
direction = (u3dVector{float64(normal[0]), float64(normal[1]), float64(normal[2])}).unit()
previous = normal
}
if counts[0] == 0 || direction == directions[0] {
directions[0] = direction
counts[0]++
} else if counts[1] == 0 || direction == directions[1] {
directions[1] = direction
counts[1]++
} else {
return u3dVector{}, fmt.Errorf("unsupported U3D point normal prediction with multiple directions")
}
}
if counts[1] == 0 {
return directions[0], nil
}
a, b := directions[0], directions[1]
if a == (u3dVector{}) || b == (u3dVector{}) {
return u3dVector{}, fmt.Errorf("unsupported U3D point normal prediction with zero direction")
}
dot := max(-1, min(1, a.dot(b)))
tangent := b.sub(u3dVector{a[0] * dot, a[1] * dot, a[2] * dot})
length := math.Sqrt(tangent.dot(tangent))
if length == 0 {
return u3dVector{}, fmt.Errorf("unsupported U3D antipodal point normal prediction")
}
angle := math.Atan2(length, dot) * (float64(counts[1]) / float64(len(points)))
sine, cosine := math.Sincos(angle)
var result u3dVector
for i := range result {
result[i] = a[i]*cosine + tangent[i]/length*sine
}
return result.unit(), nil
}
+1 -1
View File
@@ -106,7 +106,7 @@ type u3dRender struct {
order uint64
}
// RenderU3D 绘制静态无纹理网格、线集及其顶点或包围盒,返回不透明图像,不执行脚本或动画
// RenderU3D 绘制静态无纹理网格、线集、点集及包围盒,返回不透明图像,不执行脚本或动画
// 采用逐顶点光照及逐像素深度混合,缺失资源回退为空网格、灰色材质或无光源
// 入参: ctx 取消上下文, model 场景, options 视图、输出与资源限制
// 返回: *image.RGBA 图像, error 参数、能力、预算或取消错误
+1 -1
View File
@@ -145,7 +145,7 @@ func (r *u3dRender) sceneDiagonal() (float64, error) {
continue
}
mesh := &r.model.Meshes[index]
for i := range len(mesh.Faces) + len(mesh.Lines) {
for i := range len(mesh.Faces) + len(mesh.Lines) + len(mesh.Points) {
_, corners := mesh.primitive(i)
if i%256 == 0 {
if err := r.ctx.Err(); err != nil {
+1 -1
View File
@@ -95,7 +95,7 @@ func (r *u3dRender) drawMesh(instance u3dInstance, node *U3DNode, mesh *U3DMesh,
normals[i] = inverse.normal(u3dVector{float64(n[0]), float64(n[1]), float64(n[2])})
}
}
if err := r.drawLines(instance, node, mesh, positions, normals, pass, passIndex); err != nil {
if err := r.drawPrimitives(instance, node, mesh, positions, normals, pass, passIndex); err != nil {
return err
}
for index, face := range mesh.Faces {