diff --git a/go.mod b/go.mod index f554f82..9e58dfc 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 67c0438..ec838f9 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/ofdgo_compression_parallel.go b/ofdgo_compression_parallel.go index c0e40ea..329ec37 100644 --- a/ofdgo_compression_parallel.go +++ b/ofdgo_compression_parallel.go @@ -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 } diff --git a/ofdgo_compression_resources.go b/ofdgo_compression_resources.go index 57c2da2..9119fbd 100644 --- a/ofdgo_compression_resources.go +++ b/ofdgo_compression_resources.go @@ -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 diff --git a/ofdgo_geom_stroke.go b/ofdgo_geom_stroke.go index f574ca5..6b6b511 100644 --- a/ofdgo_geom_stroke.go +++ b/ofdgo_geom_stroke.go @@ -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 diff --git a/ofdgo_pdf.go b/ofdgo_pdf.go index 0136d43..ddd963e 100644 --- a/ofdgo_pdf.go +++ b/ofdgo_pdf.go @@ -118,6 +118,7 @@ type pdfImporter struct { halftoneWarnings map[*pdfgo.Halftone]bool halftones map[string]*pdfgo.Halftone transferBackdrop bool + transferModel pdfgo.Name compression CompressionOptions } diff --git a/ofdgo_pdf_3d.go b/ofdgo_pdf_3d.go index 64b035e..6971630 100644 --- a/ofdgo_pdf_3d.go +++ b/ofdgo_pdf_3d.go @@ -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 { diff --git a/ofdgo_pdf_composite.go b/ofdgo_pdf_composite.go index c622e3d..c2ef13e 100644 --- a/ofdgo_pdf_composite.go +++ b/ofdgo_pdf_composite.go @@ -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 diff --git a/ofdgo_pdf_composite_draw.go b/ofdgo_pdf_composite_draw.go index 572518b..cb817cf 100644 --- a/ofdgo_pdf_composite_draw.go +++ b/ofdgo_pdf_composite_draw.go @@ -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< 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 diff --git a/ofdgo_pdf_composite_pattern.go b/ofdgo_pdf_composite_pattern.go index fdcc9a4..231dc29 100644 --- a/ofdgo_pdf_composite_pattern.go +++ b/ofdgo_pdf_composite_pattern.go @@ -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 { diff --git a/ofdgo_pdf_composite_scratch.go b/ofdgo_pdf_composite_scratch.go index 7a4bb2f..755a5b3 100644 --- a/ofdgo_pdf_composite_scratch.go +++ b/ofdgo_pdf_composite_scratch.go @@ -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 { diff --git a/ofdgo_pdf_composite_shading.go b/ofdgo_pdf_composite_shading.go index e984d55..8f7ee0b 100644 --- a/ofdgo_pdf_composite_shading.go +++ b/ofdgo_pdf_composite_shading.go @@ -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 } diff --git a/ofdgo_pdf_form.go b/ofdgo_pdf_form.go index 394959e..f314693 100644 --- a/ofdgo_pdf_form.go +++ b/ofdgo_pdf_form.go @@ -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 { diff --git a/ofdgo_pdf_image_cache.go b/ofdgo_pdf_image_cache.go index 42e3dd4..fccb72a 100644 --- a/ofdgo_pdf_image_cache.go +++ b/ofdgo_pdf_image_cache.go @@ -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() } diff --git a/ofdgo_pdf_image_native.go b/ofdgo_pdf_image_native.go new file mode 100644 index 0000000..2cc73f3 --- /dev/null +++ b/ofdgo_pdf_image_native.go @@ -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))} +} diff --git a/ofdgo_pdf_mesh.go b/ofdgo_pdf_mesh.go index 9886be9..3eaaef6 100644 --- a/ofdgo_pdf_mesh.go +++ b/ofdgo_pdf_mesh.go @@ -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 { diff --git a/ofdgo_pdf_object.go b/ofdgo_pdf_object.go index c15cf36..f23b3a5 100644 --- a/ofdgo_pdf_object.go +++ b/ofdgo_pdf_object.go @@ -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} diff --git a/ofdgo_pdf_pattern.go b/ofdgo_pdf_pattern.go index 68275f4..2d0a986 100644 --- a/ofdgo_pdf_pattern.go +++ b/ofdgo_pdf_pattern.go @@ -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 diff --git a/ofdgo_pdf_shading.go b/ofdgo_pdf_shading.go index 48acfd1..90fa574 100644 --- a/ofdgo_pdf_shading.go +++ b/ofdgo_pdf_shading.go @@ -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()]) } } diff --git a/ofdgo_pdf_shading_colorants.go b/ofdgo_pdf_shading_colorants.go new file mode 100644 index 0000000..771a7ec --- /dev/null +++ b/ofdgo_pdf_shading_colorants.go @@ -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) +} diff --git a/ofdgo_pdf_transfer.go b/ofdgo_pdf_transfer.go index 4a6bd33..31d84dd 100644 --- a/ofdgo_pdf_transfer.go +++ b/ofdgo_pdf_transfer.go @@ -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 复用图案不透明性检查,图案内部函数不提前作用于源颜色 diff --git a/ofdgo_render_cache.go b/ofdgo_render_cache.go index 59f68aa..daf6d43 100644 --- a/ofdgo_render_cache.go +++ b/ofdgo_render_cache.go @@ -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 diff --git a/ofdgo_u3d.go b/ofdgo_u3d.go index 0c30c2a..d1a3a12 100644 --- a/ofdgo_u3d.go +++ b/ofdgo_u3d.go @@ -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 } diff --git a/ofdgo_u3d_line.go b/ofdgo_u3d_line.go index ca2213e..c090477 100644 --- a/ofdgo_u3d_line.go +++ b/ofdgo_u3d_line.go @@ -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) diff --git a/ofdgo_u3d_lineset.go b/ofdgo_u3d_lineset.go index db384ae..02678fb 100644 --- a/ofdgo_u3d_lineset.go +++ b/ofdgo_u3d_lineset.go @@ -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<= 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 } diff --git a/ofdgo_u3d_mesh.go b/ofdgo_u3d_mesh.go index fa9822e..13a8734 100644 --- a/ofdgo_u3d_mesh.go +++ b/ofdgo_u3d_mesh.go @@ -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 读取完整基础网格声明,不将渐进网格当作完整模型 diff --git a/ofdgo_u3d_mode.go b/ofdgo_u3d_mode.go index 03362a8..0c93361 100644 --- a/ofdgo_u3d_mode.go +++ b/ofdgo_u3d_mode.go @@ -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 diff --git a/ofdgo_u3d_pointset.go b/ofdgo_u3d_pointset.go new file mode 100644 index 0000000..47bd905 --- /dev/null +++ b/ofdgo_u3d_pointset.go @@ -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 +} diff --git a/ofdgo_u3d_render.go b/ofdgo_u3d_render.go index b7fcc62..32495f2 100644 --- a/ofdgo_u3d_render.go +++ b/ofdgo_u3d_render.go @@ -106,7 +106,7 @@ type u3dRender struct { order uint64 } -// RenderU3D 绘制静态无纹理网格、线集及其顶点或包围盒,返回不透明图像,不执行脚本或动画 +// RenderU3D 绘制静态无纹理网格、线集、点集及包围盒,返回不透明图像,不执行脚本或动画 // 采用逐顶点光照及逐像素深度混合,缺失资源回退为空网格、灰色材质或无光源 // 入参: ctx 取消上下文, model 场景, options 视图、输出与资源限制 // 返回: *image.RGBA 图像, error 参数、能力、预算或取消错误 diff --git a/ofdgo_u3d_section.go b/ofdgo_u3d_section.go index a26ff46..b4d4ea2 100644 --- a/ofdgo_u3d_section.go +++ b/ofdgo_u3d_section.go @@ -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 { diff --git a/ofdgo_u3d_triangle.go b/ofdgo_u3d_triangle.go index 36e3533..ebeae68 100644 --- a/ofdgo_u3d_triangle.go +++ b/ofdgo_u3d_triangle.go @@ -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 {