From bfecdeb0d62628b8c1ad2db789dad0ec787d3842 Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Mon, 19 Oct 2026 10:25:11 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=BD=92=E5=B9=B6=E6=96=87=E4=BB=B6):=20?= =?UTF-8?q?=E5=BD=92=E5=B9=B6=E6=A0=B9=E5=8C=85=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ofdgo_compression_images.go | 177 +++++++++++++++++++++++++++ ofdgo_compression_raster.go | 197 ------------------------------- ofdgo_editor_draw.go | 149 ----------------------- ofdgo_editor_resource_style.go | 128 ++++++++++++++++++++ ofdgo_paint.go | 109 +++++++++++++++++ ofdgo_paint_model.go | 127 -------------------- ofdgo_render.go | 112 +++++++++++++++++- ofdgo_render_bounds.go | 125 -------------------- ofdgo_render_compile.go | 94 +++++++++++++++ ofdgo_render_shading.go | 114 ------------------ ofdgo_renderer_paint_canvas.go | 104 ---------------- ofdgo_renderer_shading_canvas.go | 83 +++++++++++++ 12 files changed, 702 insertions(+), 817 deletions(-) delete mode 100644 ofdgo_compression_raster.go delete mode 100644 ofdgo_editor_draw.go delete mode 100644 ofdgo_paint_model.go delete mode 100644 ofdgo_render_bounds.go delete mode 100644 ofdgo_render_shading.go delete mode 100644 ofdgo_renderer_paint_canvas.go diff --git a/ofdgo_compression_images.go b/ofdgo_compression_images.go index 5301c3a..33e8079 100644 --- a/ofdgo_compression_images.go +++ b/ofdgo_compression_images.go @@ -15,10 +15,17 @@ package ofdgo import ( + "bytes" "context" + "encoding/binary" + "errors" "fmt" "image" + "io" "math" + + baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" + "github.com/xiaoqidun/pdfgo" ) // compressionPatternImageLimit 限制底纹尺寸分析的变换组合数,超限时保留原图尺寸 @@ -397,3 +404,173 @@ func (v *compressionImages) patternImages(color *FillColor, object Matrix) error } return nil } + +// outputImageFormat 识别可由资源压缩生成的图片格式 +// 入参: data 图片编码 +// 返回: string 标准资源格式,未识别时为空 +func outputImageFormat(data []byte) string { + if baseimage.IsJPEG(data) { + return "JPEG" + } + if baseimage.IsPNG(data) { + return "PNG" + } + return "" +} + +// optimizeImageResource 优化可完整保留的BMP及TIFF,复用图像压缩和降采样策略 +// 原始模式及无法保留的色彩配置不转换,仅采用小于原始编码的结果 +// 入参: ctx 取消上下文, data 图片编码, options 压缩选项, size 像素需求 +// 返回: []byte 图片编码,可能引用输入, error 解码、编码或取消错误 +func optimizeImageResource(ctx context.Context, data []byte, options CompressionOptions, size image.Point) ([]byte, error) { + if err := options.Validate(); err != nil { + return nil, err + } + if err := ctx.Err(); err != nil { + return nil, err + } + if options.Mode != CompressionUnchanged && baseimage.IsTIFF(data) { + return optimizeTIFFResource(ctx, data, options, size) + } + if options.Mode == CompressionUnchanged || !bytes.HasPrefix(data, []byte("BM")) { + return pdfgo.OptimizeImageResource(ctx, data, options, size) + } + return optimizeBMPResource(ctx, data, options, size) +} + +// optimizeBMPResource 压缩可保留色彩与密度的位图,未知元数据及高精度位域保持原样 +// 入参: ctx 取消上下文, data BMP编码, options 压缩选项, size 像素需求 +// 返回: []byte 图片编码, error 解码、编码或取消错误 +func optimizeBMPResource(ctx context.Context, data []byte, options CompressionOptions, size image.Point) ([]byte, error) { + offset, width, height, depth, compression, err := baseimage.ParseBMPHeader(data) + if err != nil { + return data, nil + } + if height < 0 { + height = -height + } + if int64(width)*int64(height) > outputOptimizationBufferLimit/4 || compression != 0 && compression != 3 { + return data, nil + } + header := binary.LittleEndian.Uint32(data[14:18]) + if header != 40 && header != 108 && header != 124 { + return data, nil + } + if header >= 108 && binary.LittleEndian.Uint32(data[70:74]) != 0x73524742 { + return data, nil + } + if header == 124 && (binary.LittleEndian.Uint32(data[122:126]) != 0 || binary.LittleEndian.Uint32(data[126:130]) != 0 || binary.LittleEndian.Uint32(data[130:134]) != 0) { + return data, nil + } + stride := (int64(width)*int64(depth) + 31) / 32 * 4 + if int64(offset)+stride*int64(height) != int64(len(data)) { + return data, nil + } + density, ok := baseimage.BMPDensity(data) + if !ok { + return data, nil + } + if depth == 16 || depth == 32 { + masks, err := baseimage.BMPColorMasks(data, offset, depth, compression) + if err != nil { + return data, nil + } + if options.Mode == CompressionLossless { + for _, mask := range masks { + if mask.Max > 255 { + return data, nil + } + } + } + } + var pixels image.Image + if depth == 16 || compression == 3 { + pixels, err = baseimage.DecodeBMPContext(ctx, data) + } else { + pixels, _, err = image.Decode(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), ctx: ctx}) + } + if err != nil { + return nil, err + } + return optimizeRasterResource(ctx, data, pixels, options, size, density) +} + +// optimizeRasterResource 编码已解码的资源图片,同步降采样后的像素密度,仅采用更小结果 +// 先降采样再编码,绝对密度按最终尺寸量化,相对密度保留精确比例 +// 有密度时保留PNG,未知密度按最终像素选择编码格式 +// 入参: ctx 取消上下文, data 原编码, pixels 像素, options 压缩选项, size 像素需求, density 像素密度 +// 返回: []byte 图片编码, error 编码或取消错误 +func optimizeRasterResource(ctx context.Context, data []byte, pixels image.Image, options CompressionOptions, size image.Point, density baseimage.Density) ([]byte, error) { + hasDensity := density.X != 0 || density.Y != 0 + before := pixels.Bounds().Size() + if options.Mode == CompressionLossy { + var err error + pixels, err = pdfgo.ResizeImage(ctx, pixels, size) + if err != nil { + return nil, err + } + } + var ok bool + density, ok = density.Scale(before, pixels.Bounds().Size()) + if !ok { + return data, ctx.Err() + } + var candidate []byte + var err error + if hasDensity { + var encoded bytes.Buffer + err = pdfgo.EncodePNGWithOptions(ctx, &encoded, pixels, options) + candidate = encoded.Bytes() + } else { + candidate, err = pdfgo.EncodeImageResource(ctx, pixels, options) + } + if err != nil { + if errors.Is(err, io.ErrShortBuffer) { + return data, ctx.Err() + } + return nil, err + } + if hasDensity { + if len(candidate) > outputOptimizationBufferLimit-baseimage.PNGDensityChunkSize { + return data, ctx.Err() + } + candidate, ok = baseimage.WithPNGDensity(candidate, density) + if !ok { + return data, ctx.Err() + } + } + if err := ctx.Err(); err != nil { + return nil, err + } + if len(candidate) < len(data) { + return candidate, nil + } + return data, nil +} + +// optimizeTIFFResource 压缩单目录TIFF,保留方向、透明度、采样精度及像素密度 +// 主图或单个瓦片的预计缓冲超限时保留原编码 +// 入参: ctx 取消上下文, data TIFF编码, options 压缩选项, size 显示像素需求 +// 返回: []byte 图片编码,不可完整转换时保留原文, error 编码或取消错误 +func optimizeTIFFResource(ctx context.Context, data []byte, options CompressionOptions, size image.Point) ([]byte, error) { + layout, err := baseimage.ReadTIFFLayoutContext(ctx, bytes.NewReader(data)) + if err != nil { + return data, ctx.Err() + } + pixelSize := int64(layout.DecodedPixelBytes()) + if int64(layout.Width) > outputOptimizationBufferLimit/pixelSize/int64(layout.Height) { + return data, ctx.Err() + } + if layout.TileWidth != 0 && int64(layout.TileWidth) > outputOptimizationBufferLimit/pixelSize/int64(layout.TileHeight) { + return data, ctx.Err() + } + density, ok := baseimage.TIFFConversionDensity(data, layout) + if !ok { + return data, ctx.Err() + } + pixels, err := baseimage.DecodeTIFF(ctx, data, layout) + if err != nil { + return data, ctx.Err() + } + return optimizeRasterResource(ctx, data, pixels, options, size, density) +} diff --git a/ofdgo_compression_raster.go b/ofdgo_compression_raster.go deleted file mode 100644 index 194743d..0000000 --- a/ofdgo_compression_raster.go +++ /dev/null @@ -1,197 +0,0 @@ -// Copyright 2025-2026 肖其顿 (XIAO QI DUN) -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ofdgo - -import ( - "bytes" - "context" - "encoding/binary" - "errors" - "image" - "io" - - baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" - "github.com/xiaoqidun/pdfgo" -) - -// outputImageFormat 识别可由资源压缩生成的图片格式 -// 入参: data 图片编码 -// 返回: string 标准资源格式,未识别时为空 -func outputImageFormat(data []byte) string { - if baseimage.IsJPEG(data) { - return "JPEG" - } - if baseimage.IsPNG(data) { - return "PNG" - } - return "" -} - -// optimizeImageResource 优化可完整保留的BMP及TIFF,复用图像压缩和降采样策略 -// 原始模式及无法保留的色彩配置不转换,仅采用小于原始编码的结果 -// 入参: ctx 取消上下文, data 图片编码, options 压缩选项, size 像素需求 -// 返回: []byte 图片编码,可能引用输入, error 解码、编码或取消错误 -func optimizeImageResource(ctx context.Context, data []byte, options CompressionOptions, size image.Point) ([]byte, error) { - if err := options.Validate(); err != nil { - return nil, err - } - if err := ctx.Err(); err != nil { - return nil, err - } - if options.Mode != CompressionUnchanged && baseimage.IsTIFF(data) { - return optimizeTIFFResource(ctx, data, options, size) - } - if options.Mode == CompressionUnchanged || !bytes.HasPrefix(data, []byte("BM")) { - return pdfgo.OptimizeImageResource(ctx, data, options, size) - } - return optimizeBMPResource(ctx, data, options, size) -} - -// optimizeBMPResource 压缩可保留色彩与密度的位图,未知元数据及高精度位域保持原样 -// 入参: ctx 取消上下文, data BMP编码, options 压缩选项, size 像素需求 -// 返回: []byte 图片编码, error 解码、编码或取消错误 -func optimizeBMPResource(ctx context.Context, data []byte, options CompressionOptions, size image.Point) ([]byte, error) { - offset, width, height, depth, compression, err := baseimage.ParseBMPHeader(data) - if err != nil { - return data, nil - } - if height < 0 { - height = -height - } - if int64(width)*int64(height) > outputOptimizationBufferLimit/4 || compression != 0 && compression != 3 { - return data, nil - } - header := binary.LittleEndian.Uint32(data[14:18]) - if header != 40 && header != 108 && header != 124 { - return data, nil - } - if header >= 108 && binary.LittleEndian.Uint32(data[70:74]) != 0x73524742 { - return data, nil - } - if header == 124 && (binary.LittleEndian.Uint32(data[122:126]) != 0 || binary.LittleEndian.Uint32(data[126:130]) != 0 || binary.LittleEndian.Uint32(data[130:134]) != 0) { - return data, nil - } - stride := (int64(width)*int64(depth) + 31) / 32 * 4 - if int64(offset)+stride*int64(height) != int64(len(data)) { - return data, nil - } - density, ok := baseimage.BMPDensity(data) - if !ok { - return data, nil - } - if depth == 16 || depth == 32 { - masks, err := baseimage.BMPColorMasks(data, offset, depth, compression) - if err != nil { - return data, nil - } - if options.Mode == CompressionLossless { - for _, mask := range masks { - if mask.Max > 255 { - return data, nil - } - } - } - } - var pixels image.Image - if depth == 16 || compression == 3 { - pixels, err = baseimage.DecodeBMPContext(ctx, data) - } else { - pixels, _, err = image.Decode(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), ctx: ctx}) - } - if err != nil { - return nil, err - } - return optimizeRasterResource(ctx, data, pixels, options, size, density) -} - -// optimizeRasterResource 编码已解码的资源图片,同步降采样后的像素密度,仅采用更小结果 -// 先降采样再编码,绝对密度按最终尺寸量化,相对密度保留精确比例 -// 有密度时保留PNG,未知密度按最终像素选择编码格式 -// 入参: ctx 取消上下文, data 原编码, pixels 像素, options 压缩选项, size 像素需求, density 像素密度 -// 返回: []byte 图片编码, error 编码或取消错误 -func optimizeRasterResource(ctx context.Context, data []byte, pixels image.Image, options CompressionOptions, size image.Point, density baseimage.Density) ([]byte, error) { - hasDensity := density.X != 0 || density.Y != 0 - before := pixels.Bounds().Size() - if options.Mode == CompressionLossy { - var err error - pixels, err = pdfgo.ResizeImage(ctx, pixels, size) - if err != nil { - return nil, err - } - } - var ok bool - density, ok = density.Scale(before, pixels.Bounds().Size()) - if !ok { - return data, ctx.Err() - } - var candidate []byte - var err error - if hasDensity { - var encoded bytes.Buffer - err = pdfgo.EncodePNGWithOptions(ctx, &encoded, pixels, options) - candidate = encoded.Bytes() - } else { - candidate, err = pdfgo.EncodeImageResource(ctx, pixels, options) - } - if err != nil { - if errors.Is(err, io.ErrShortBuffer) { - return data, ctx.Err() - } - return nil, err - } - if hasDensity { - if len(candidate) > outputOptimizationBufferLimit-baseimage.PNGDensityChunkSize { - return data, ctx.Err() - } - candidate, ok = baseimage.WithPNGDensity(candidate, density) - if !ok { - return data, ctx.Err() - } - } - if err := ctx.Err(); err != nil { - return nil, err - } - if len(candidate) < len(data) { - return candidate, nil - } - return data, nil -} - -// optimizeTIFFResource 压缩单目录TIFF,保留方向、透明度、采样精度及像素密度 -// 主图或单个瓦片的预计缓冲超限时保留原编码 -// 入参: ctx 取消上下文, data TIFF编码, options 压缩选项, size 显示像素需求 -// 返回: []byte 图片编码,不可完整转换时保留原文, error 编码或取消错误 -func optimizeTIFFResource(ctx context.Context, data []byte, options CompressionOptions, size image.Point) ([]byte, error) { - layout, err := baseimage.ReadTIFFLayoutContext(ctx, bytes.NewReader(data)) - if err != nil { - return data, ctx.Err() - } - pixelSize := int64(layout.DecodedPixelBytes()) - if int64(layout.Width) > outputOptimizationBufferLimit/pixelSize/int64(layout.Height) { - return data, ctx.Err() - } - if layout.TileWidth != 0 && int64(layout.TileWidth) > outputOptimizationBufferLimit/pixelSize/int64(layout.TileHeight) { - return data, ctx.Err() - } - density, ok := baseimage.TIFFConversionDensity(data, layout) - if !ok { - return data, ctx.Err() - } - pixels, err := baseimage.DecodeTIFF(ctx, data, layout) - if err != nil { - return data, ctx.Err() - } - return optimizeRasterResource(ctx, data, pixels, options, size, density) -} diff --git a/ofdgo_editor_draw.go b/ofdgo_editor_draw.go deleted file mode 100644 index 163f143..0000000 --- a/ofdgo_editor_draw.go +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright 2025-2026 肖其顿 (XIAO QI DUN) -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ofdgo - -import ( - "context" - "encoding/xml" - "fmt" -) - -// drawParamXML 读取绘制参数原文,保留复杂颜色、扩展属性及命名空间 -// 入参: id 绘制参数标识 -// 返回: []byte 独立参数片段, error 错误信息 -func (e *Editor) drawParamXML(id string) ([]byte, error) { - return e.drawParamXMLContext(context.Background(), id) -} - -// drawParamXMLContext 读取并提取绘制参数原文,保留扩展信息并响应取消 -// 入参: ctx 取消上下文, id 绘制参数标识 -// 返回: []byte 独立参数片段, error 读取、解析或取消错误 -func (e *Editor) drawParamXMLContext(ctx context.Context, id string) ([]byte, error) { - if err := ctx.Err(); err != nil { - return nil, err - } - var data []byte - for _, resource := range e.resources { - if resource.draw != nil && sameResourceID(resource.draw.ID, id) { - data = resource.data - break - } - } - if data == nil && e.source != nil { - var err error - name, _ := resourceValue(e.source.reader.resourceFiles, id) - data, err = e.source.reader.readFileViewContext(ctx, name) - if err != nil { - return nil, err - } - } - root, err := parseEditorXMLContext(ctx, data) - if err != nil { - return nil, err - } - if node := editorResourceNode(root, "DrawParams", "DrawParam", id); node != nil { - fragment, err := editorXMLStandaloneContext(ctx, data[node.start:node.end], node) - if err != nil { - return nil, err - } - if err := ctx.Err(); err != nil { - return nil, err - } - return fragment, nil - } - return nil, fmt.Errorf("draw parameter %q not found", id) -} - -// copyDrawParam 将原绘制参数继承链接到独立基准,复用相同副本且不改写原资源 -// 入参: id 源参数标识, base 独立基准标识, visited 当前继承链 -// 返回: string 新继承链末端标识, error 错误信息 -func (e *Editor) copyDrawParam(id, base string, visited map[string]bool) (string, error) { - if id == "" || sameResourceID(id, base) { - return base, nil - } - key := id - if value := editorResourceID(id); value != "" { - key = value - } - if visited[key] { - return "", fmt.Errorf("cyclic draw parameter %q", id) - } - visited[key] = true - data, err := e.drawParamXML(id) - if err != nil { - return "", err - } - root, err := parseEditorXML(data) - if err != nil { - return "", err - } - if sameResourceID(root.attr("Relative"), base) { - return id, nil - } - base, err = e.copyDrawParam(root.attr("Relative"), base, visited) - if err != nil { - return "", err - } - for _, resource := range e.resources { - if sameResourceID(resource.drawSource, id) && sameResourceID(resource.draw.Relative, base) { - return resource.draw.ID, nil - } - } - if !editorXMLCopyable(root) { - return "", fmt.Errorf("draw parameter contains unsupported identifiers") - } - before, err := editorXMLContainer("DrawParam", ofdAttrs{{Name: xml.Name{Local: "Relative"}, Value: root.attr("Relative")}}, nil) - if err != nil { - return "", err - } - after, err := editorXMLContainer("DrawParam", ofdAttrs{{Name: xml.Name{Local: "Relative"}, Value: base}}, nil) - if err != nil { - return "", err - } - data, err = editorXMLMerge(data, root, before, after) - if err != nil { - return "", err - } - root, err = parseEditorXML(data) - if err != nil { - return "", err - } - ids := make(map[string]string) - if err := e.collectCompositeIDs(root, ids); err != nil { - return "", err - } - data, err = editorXMLRemapIDs(data, root, ids) - if err != nil { - return "", err - } - var draw DrawParam - if err := xml.Unmarshal(data, &draw); err != nil { - return "", err - } - data, err = editorXMLContainer("DrawParams", nil, data) - if err != nil { - return "", err - } - data, err = editorXMLContainer("Res", ofdAttrs{{Name: xml.Name{Local: "BaseLoc"}, Value: "."}}, data) - if err != nil { - return "", err - } - refs, err := editorResourceReferences(data) - if err != nil { - return "", err - } - e.resources = append(e.resources, editorResource{name: e.packageName("Res/DrawParams/DrawParam_" + draw.ID + ".xml"), data: data, draw: &draw, drawSource: id, references: refs}) - return draw.ID, nil -} diff --git a/ofdgo_editor_resource_style.go b/ofdgo_editor_resource_style.go index a7a6d58..a7e9c35 100644 --- a/ofdgo_editor_resource_style.go +++ b/ofdgo_editor_resource_style.go @@ -613,3 +613,131 @@ func validateResourceStyleGraph(ctx context.Context, catalog *resourceCatalog, i } return visit(resourceStyleReference{key: id}) } + +// drawParamXML 读取绘制参数原文,保留复杂颜色、扩展属性及命名空间 +// 入参: id 绘制参数标识 +// 返回: []byte 独立参数片段, error 错误信息 +func (e *Editor) drawParamXML(id string) ([]byte, error) { + return e.drawParamXMLContext(context.Background(), id) +} + +// drawParamXMLContext 读取并提取绘制参数原文,保留扩展信息并响应取消 +// 入参: ctx 取消上下文, id 绘制参数标识 +// 返回: []byte 独立参数片段, error 读取、解析或取消错误 +func (e *Editor) drawParamXMLContext(ctx context.Context, id string) ([]byte, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + var data []byte + for _, resource := range e.resources { + if resource.draw != nil && sameResourceID(resource.draw.ID, id) { + data = resource.data + break + } + } + if data == nil && e.source != nil { + var err error + name, _ := resourceValue(e.source.reader.resourceFiles, id) + data, err = e.source.reader.readFileViewContext(ctx, name) + if err != nil { + return nil, err + } + } + root, err := parseEditorXMLContext(ctx, data) + if err != nil { + return nil, err + } + if node := editorResourceNode(root, "DrawParams", "DrawParam", id); node != nil { + fragment, err := editorXMLStandaloneContext(ctx, data[node.start:node.end], node) + if err != nil { + return nil, err + } + if err := ctx.Err(); err != nil { + return nil, err + } + return fragment, nil + } + return nil, fmt.Errorf("draw parameter %q not found", id) +} + +// copyDrawParam 将原绘制参数继承链接到独立基准,复用相同副本且不改写原资源 +// 入参: id 源参数标识, base 独立基准标识, visited 当前继承链 +// 返回: string 新继承链末端标识, error 错误信息 +func (e *Editor) copyDrawParam(id, base string, visited map[string]bool) (string, error) { + if id == "" || sameResourceID(id, base) { + return base, nil + } + key := id + if value := editorResourceID(id); value != "" { + key = value + } + if visited[key] { + return "", fmt.Errorf("cyclic draw parameter %q", id) + } + visited[key] = true + data, err := e.drawParamXML(id) + if err != nil { + return "", err + } + root, err := parseEditorXML(data) + if err != nil { + return "", err + } + if sameResourceID(root.attr("Relative"), base) { + return id, nil + } + base, err = e.copyDrawParam(root.attr("Relative"), base, visited) + if err != nil { + return "", err + } + for _, resource := range e.resources { + if sameResourceID(resource.drawSource, id) && sameResourceID(resource.draw.Relative, base) { + return resource.draw.ID, nil + } + } + if !editorXMLCopyable(root) { + return "", fmt.Errorf("draw parameter contains unsupported identifiers") + } + before, err := editorXMLContainer("DrawParam", ofdAttrs{{Name: xml.Name{Local: "Relative"}, Value: root.attr("Relative")}}, nil) + if err != nil { + return "", err + } + after, err := editorXMLContainer("DrawParam", ofdAttrs{{Name: xml.Name{Local: "Relative"}, Value: base}}, nil) + if err != nil { + return "", err + } + data, err = editorXMLMerge(data, root, before, after) + if err != nil { + return "", err + } + root, err = parseEditorXML(data) + if err != nil { + return "", err + } + ids := make(map[string]string) + if err := e.collectCompositeIDs(root, ids); err != nil { + return "", err + } + data, err = editorXMLRemapIDs(data, root, ids) + if err != nil { + return "", err + } + var draw DrawParam + if err := xml.Unmarshal(data, &draw); err != nil { + return "", err + } + data, err = editorXMLContainer("DrawParams", nil, data) + if err != nil { + return "", err + } + data, err = editorXMLContainer("Res", ofdAttrs{{Name: xml.Name{Local: "BaseLoc"}, Value: "."}}, data) + if err != nil { + return "", err + } + refs, err := editorResourceReferences(data) + if err != nil { + return "", err + } + e.resources = append(e.resources, editorResource{name: e.packageName("Res/DrawParams/DrawParam_" + draw.ID + ".xml"), data: data, draw: &draw, drawSource: id, references: refs}) + return draw.ID, nil +} diff --git a/ofdgo_paint.go b/ofdgo_paint.go index d790e2e..765cd50 100644 --- a/ofdgo_paint.go +++ b/ofdgo_paint.go @@ -21,6 +21,18 @@ import ( "slices" "strconv" "strings" + + basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" +) + +// 画刷类型,区分纯色、渐变和图案填充 +const ( + PaintNone PaintKind = iota + PaintSolid + PaintLinear + PaintRadial + PaintPattern + PaintMesh ) // PatternPaint 底纹画刷 @@ -36,6 +48,36 @@ type profileGradient struct { colors []MeshVertex } +// ColorStop 保存渐变区间位置及预乘RGBA颜色节点 +type ColorStop struct { + Offset float64 + Color color.RGBA +} + +// PaintKind 区分无画刷、纯色、轴向渐变、径向渐变、图案和网格渐变 +type PaintKind uint8 + +// Paint 保存后端无关的颜色、渐变和图案,不修改源节点 +type Paint struct { + Kind PaintKind + Color color.RGBA + Gradient *Shading + Pattern *PatternPaint + Mesh *MeshShading +} + +// Shading 保存OFD对象局部坐标中的渐变,单位为毫米,纵轴向下 +// Extend、MapType和MapUnit保留标准的延伸及重复语义 +// Sample存在时按单位区间采样源颜色变换,Stops仍保存各段的显示颜色 +type Shading struct { + Start, End Point + StartRadius, EndRadius float64 + Stops []ColorStop + Extend, MapType string + MapUnit, Eccentricity, Angle float64 + Sample func(float64) color.RGBA +} + // ResolveColor 按文档颜色空间和调色板解析颜色,返回预乘透明度的RGBA // 入参: value 颜色分量, index 调色板索引, space 颜色空间标识, alpha 透明度 // 返回: color.RGBA 标准颜色, error 颜色定义或配置错误 @@ -467,3 +509,70 @@ func (g *profileGradient) At(t float64) color.RGBA { r, green, b := last.rgb(last.Values) return profileColor([3]float64{r, green, b}, last.Alpha) } + +// ResolvePaint 解析颜色、透明度、渐变分段和图案,不依赖绘图库 +// 入参: fill 填充或转换后的描边节点 +// 返回: Paint 只读画刷, error 无效渐变 +func (r *Renderer) ResolvePaint(fill *FillColor) (Paint, error) { + previous := r.renderError + r.renderError = nil + defer func() { r.renderError = previous }() + if fill == nil { + return Paint{}, nil + } + if fill.Pattern != nil { + return Paint{Kind: PaintPattern, Pattern: parsePatternPaint(fill)}, nil + } + if fill.GouraudShd != nil || fill.LaGouraudShd != nil { + mesh, err := r.resolveMesh(fill) + if err != nil { + return Paint{}, err + } + if r.renderError != nil { + return Paint{}, r.renderError + } + return Paint{Kind: PaintMesh, Mesh: mesh}, nil + } + base := r.parseFillColor(fill) + if r.renderError != nil { + return Paint{}, r.renderError + } + if base == nil { + return Paint{}, nil + } + paint := Paint{Kind: PaintSolid, Color: colorToRGBA(base)} + if node := fill.AxialShd; node != nil { + start, end := parseFloats(node.StartPoint), parseFloats(node.EndPoint) + stops := r.renderGradientStops(node.Segment, fill.Alpha) + if r.renderError != nil { + return Paint{}, r.renderError + } + sample, err := r.profileGradient(node.Segment, fill.Alpha) + if err != nil { + return Paint{}, err + } + if len(start) >= 2 && len(end) >= 2 && len(stops) != 0 && (!basegeom.Equal(start[0], end[0]) || !basegeom.Equal(start[1], end[1])) { + paint.Kind = PaintLinear + paint.Gradient = &Shading{Start: Point{X: start[0], Y: start[1]}, End: Point{X: end[0], Y: end[1]}, Stops: stops, Extend: node.Extend, MapType: node.MapType, MapUnit: node.MapUnit} + paint.Gradient.Sample = sample + return paint, nil + } + } + if node := fill.RadialShd; node != nil && node.EndRadius > 0 { + start, end := parseFloats(node.StartPoint), parseFloats(node.EndPoint) + stops := r.renderGradientStops(node.Segment, fill.Alpha) + if r.renderError != nil { + return Paint{}, r.renderError + } + sample, err := r.profileGradient(node.Segment, fill.Alpha) + if err != nil { + return Paint{}, err + } + if len(start) >= 2 && len(end) >= 2 && len(stops) != 0 { + paint.Kind = PaintRadial + paint.Gradient = &Shading{Start: Point{X: start[0], Y: start[1]}, End: Point{X: end[0], Y: end[1]}, StartRadius: node.StartRadius, EndRadius: node.EndRadius, Stops: stops, Extend: node.Extend, MapType: node.MapType, MapUnit: node.MapUnit, Eccentricity: node.Eccentricity, Angle: node.Angle} + paint.Gradient.Sample = sample + } + } + return paint, nil +} diff --git a/ofdgo_paint_model.go b/ofdgo_paint_model.go deleted file mode 100644 index 3f5d5a4..0000000 --- a/ofdgo_paint_model.go +++ /dev/null @@ -1,127 +0,0 @@ -// 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 ( - basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" - "image/color" -) - -// 画刷类型,区分纯色、渐变和图案填充 -const ( - PaintNone PaintKind = iota - PaintSolid - PaintLinear - PaintRadial - PaintPattern - PaintMesh -) - -// ColorStop 保存渐变区间位置及预乘RGBA颜色节点 -type ColorStop struct { - Offset float64 - Color color.RGBA -} - -// PaintKind 区分无画刷、纯色、轴向渐变、径向渐变、图案和网格渐变 -type PaintKind uint8 - -// Paint 保存后端无关的颜色、渐变和图案,不修改源节点 -type Paint struct { - Kind PaintKind - Color color.RGBA - Gradient *Shading - Pattern *PatternPaint - Mesh *MeshShading -} - -// Shading 保存OFD对象局部坐标中的渐变,单位为毫米,纵轴向下 -// Extend、MapType和MapUnit保留标准的延伸及重复语义 -// Sample存在时按单位区间采样源颜色变换,Stops仍保存各段的显示颜色 -type Shading struct { - Start, End Point - StartRadius, EndRadius float64 - Stops []ColorStop - Extend, MapType string - MapUnit, Eccentricity, Angle float64 - Sample func(float64) color.RGBA -} - -// ResolvePaint 解析颜色、透明度、渐变分段和图案,不依赖绘图库 -// 入参: fill 填充或转换后的描边节点 -// 返回: Paint 只读画刷, error 无效渐变 -func (r *Renderer) ResolvePaint(fill *FillColor) (Paint, error) { - previous := r.renderError - r.renderError = nil - defer func() { r.renderError = previous }() - if fill == nil { - return Paint{}, nil - } - if fill.Pattern != nil { - return Paint{Kind: PaintPattern, Pattern: parsePatternPaint(fill)}, nil - } - if fill.GouraudShd != nil || fill.LaGouraudShd != nil { - mesh, err := r.resolveMesh(fill) - if err != nil { - return Paint{}, err - } - if r.renderError != nil { - return Paint{}, r.renderError - } - return Paint{Kind: PaintMesh, Mesh: mesh}, nil - } - base := r.parseFillColor(fill) - if r.renderError != nil { - return Paint{}, r.renderError - } - if base == nil { - return Paint{}, nil - } - paint := Paint{Kind: PaintSolid, Color: colorToRGBA(base)} - if node := fill.AxialShd; node != nil { - start, end := parseFloats(node.StartPoint), parseFloats(node.EndPoint) - stops := r.renderGradientStops(node.Segment, fill.Alpha) - if r.renderError != nil { - return Paint{}, r.renderError - } - sample, err := r.profileGradient(node.Segment, fill.Alpha) - if err != nil { - return Paint{}, err - } - if len(start) >= 2 && len(end) >= 2 && len(stops) != 0 && (!basegeom.Equal(start[0], end[0]) || !basegeom.Equal(start[1], end[1])) { - paint.Kind = PaintLinear - paint.Gradient = &Shading{Start: Point{X: start[0], Y: start[1]}, End: Point{X: end[0], Y: end[1]}, Stops: stops, Extend: node.Extend, MapType: node.MapType, MapUnit: node.MapUnit} - paint.Gradient.Sample = sample - return paint, nil - } - } - if node := fill.RadialShd; node != nil && node.EndRadius > 0 { - start, end := parseFloats(node.StartPoint), parseFloats(node.EndPoint) - stops := r.renderGradientStops(node.Segment, fill.Alpha) - if r.renderError != nil { - return Paint{}, r.renderError - } - sample, err := r.profileGradient(node.Segment, fill.Alpha) - if err != nil { - return Paint{}, err - } - if len(start) >= 2 && len(end) >= 2 && len(stops) != 0 { - paint.Kind = PaintRadial - paint.Gradient = &Shading{Start: Point{X: start[0], Y: start[1]}, End: Point{X: end[0], Y: end[1]}, StartRadius: node.StartRadius, EndRadius: node.EndRadius, Stops: stops, Extend: node.Extend, MapType: node.MapType, MapUnit: node.MapUnit, Eccentricity: node.Eccentricity, Angle: node.Angle} - paint.Gradient.Sample = sample - } - } - return paint, nil -} diff --git a/ofdgo_render.go b/ofdgo_render.go index 9b9ae41..ecc0a86 100644 --- a/ofdgo_render.go +++ b/ofdgo_render.go @@ -14,7 +14,21 @@ package ofdgo -import "fmt" +import ( + "fmt" +) + +// ObjectContour 页面坐标中的填充轮廓,Path为SVG路径,EvenOdd表示奇偶填充 +type ObjectContour struct { + Path string `json:"path"` + EvenOdd bool `json:"evenOdd,omitempty"` +} + +// annotationMeasurement 按注解绘制顺序收集对象范围和轮廓 +type annotationMeasurement struct { + renderer *Renderer + result ObjectMeasurement +} // RenderPage 编译页面为与绘图库无关的只读绘制数据 // 入参: page 页面内容 @@ -190,3 +204,99 @@ func (r *Renderer) RenderPageByIndex(index int) (*RasterPage, error) { } return r.RenderPage(page) } + +// ObjectBounds 获取文字、路径、图片或复合对象在页面坐标中的轴对齐范围,不修改对象 +// 文字采用字形范围,路径包含描边与裁剪,图片采用裁剪后的几何范围,不解码像素或排除透明像素 +// 底纹按填充或描边轮廓度量,不展开图案单元 +// 不应用页面边界或父级变换;无可见范围时返回零值,不支持的对象类型返回错误 +// 入参: object 图形对象, drawParam 图层绘制参数标识,无继承时为空 +// 返回: Box 毫米坐标范围, error 错误信息 +func (r *Renderer) ObjectBounds(object GraphicObject, drawParam string) (Box, error) { + result, err := r.MeasureObject(object, MeasureOptions{Defaults: r.drawParamDefaults(drawParam, nil)}) + return result.Bounds, err +} + +// AnnotationGeometry 获取注解外观的实际内容轮廓,不将Appearance容器边界视为绘制内容 +// 链接保留透明对象定义的交互区域,图片按几何范围度量而不解码透明像素 +// 入参: annotation 注解 +// 返回: Box 页面范围, []ObjectContour 命中轮廓, error 错误信息 +func (r *Renderer) AnnotationGeometry(annotation Annotation) (Box, []ObjectContour, error) { + measurement := &annotationMeasurement{renderer: r} + if err := r.walkAnnotation(annotation, measurement); err != nil { + return Box{}, nil, err + } + box, contours := measurement.result.Bounds, measurement.result.Contours + for _, source := range r.annotationActionSources([]Annotation{annotation}) { + for _, action := range source.Actions { + if action.Event != "CLICK" { + continue + } + region, path, err := r.actionLinkRegion(source, action) + if err != nil { + return Box{}, nil, err + } + if region.W <= 0 || region.H <= 0 { + continue + } + if path == "" { + path = fmt.Sprintf("M%g %gH%gV%gH%gZ", region.X, region.Y, region.X+region.W, region.Y+region.H, region.X) + } + box = unionTextBox(box, region) + contours = append(contours, ObjectContour{Path: path}) + } + } + return box, contours, nil +} + +// DrawObject 使用对象实际继承状态度量注解内容,不附加容器裁剪 +// 入参: object 图形对象, state 绘制状态 +// 返回: error 度量错误 +func (m *annotationMeasurement) DrawObject(object *GraphicObject, state RenderState) error { + if state.Hidden { + return nil + } + result, err := m.renderer.MeasureObject(*object, MeasureOptions{Defaults: state.Defaults, Parent: state.Parent, BoundaryInCTM: state.BoundaryInCTM, Clip: state.Clip, Contours: true}) + if err != nil { + return err + } + m.result.Bounds = unionTextBox(m.result.Bounds, result.Bounds) + m.result.Contours = append(m.result.Contours, result.Contours...) + return nil +} + +// DrawStamp 满足页面访问接口,注解外观遍历不包含数字签章 +// 入参: stamp 数字签章 +// 返回: error 始终为空 +func (m *annotationMeasurement) DrawStamp(stamp Stamp) error { + return nil +} + +// ObjectContours 获取路径、图片或复合对象的实际绘制区域,包含描边、虚线、变换及对象裁剪 +// 图片不解码像素,底纹不展开图案单元;不包含页面边界或父级变换 +// 入参: object 路径、图片或复合对象, drawParam 图层绘制参数标识 +// 返回: []ObjectContour 可用于点选的轮廓, error 错误信息 +func (r *Renderer) ObjectContours(object GraphicObject, drawParam string) ([]ObjectContour, error) { + _, contours, err := r.ObjectGeometry(object, drawParam) + return contours, err +} + +// ObjectGeometry 一次度量路径、图片或复合对象的范围与轮廓,语义与ObjectBounds和ObjectContours一致 +// 入参: object 图形对象, drawParam 图层绘制参数标识 +// 返回: Box 毫米范围, []ObjectContour 绘制轮廓, error 错误信息 +func (r *Renderer) ObjectGeometry(object GraphicObject, drawParam string) (Box, []ObjectContour, error) { + if object.Type != "PathObject" && object.Type != "ImageObject" && object.Type != "CompositeObject" && object.Type != "CompositeGraphicUnit" { + return Box{}, nil, fmt.Errorf("contours require a path, image or composite object") + } + result, err := r.MeasureObject(object, MeasureOptions{Defaults: r.drawParamDefaults(drawParam, nil), Contours: true}) + return result.Bounds, result.Contours, err +} + +// MeasureObject 使用当前页面编译器度量对象,保持绘制与选区的字体和几何语义一致 +// 入参: object 图形对象, options 度量上下文 +// 返回: ObjectMeasurement 度量结果, error 错误信息 +func (r *Renderer) MeasureObject(object GraphicObject, options MeasureOptions) (ObjectMeasurement, error) { + if r.backends.Compiler == nil { + return ObjectMeasurement{}, fmt.Errorf("%w: object measurement", ErrBackendUnavailable) + } + return r.backends.Compiler.MeasureObject(r, object, options) +} diff --git a/ofdgo_render_bounds.go b/ofdgo_render_bounds.go deleted file mode 100644 index 83cddf0..0000000 --- a/ofdgo_render_bounds.go +++ /dev/null @@ -1,125 +0,0 @@ -// 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" - -// ObjectContour 页面坐标中的填充轮廓,Path为SVG路径,EvenOdd表示奇偶填充 -type ObjectContour struct { - Path string `json:"path"` - EvenOdd bool `json:"evenOdd,omitempty"` -} - -// annotationMeasurement 按注解绘制顺序收集对象范围和轮廓 -type annotationMeasurement struct { - renderer *Renderer - result ObjectMeasurement -} - -// ObjectBounds 获取文字、路径、图片或复合对象在页面坐标中的轴对齐范围,不修改对象 -// 文字采用字形范围,路径包含描边与裁剪,图片采用裁剪后的几何范围,不解码像素或排除透明像素 -// 底纹按填充或描边轮廓度量,不展开图案单元 -// 不应用页面边界或父级变换;无可见范围时返回零值,不支持的对象类型返回错误 -// 入参: object 图形对象, drawParam 图层绘制参数标识,无继承时为空 -// 返回: Box 毫米坐标范围, error 错误信息 -func (r *Renderer) ObjectBounds(object GraphicObject, drawParam string) (Box, error) { - result, err := r.MeasureObject(object, MeasureOptions{Defaults: r.drawParamDefaults(drawParam, nil)}) - return result.Bounds, err -} - -// AnnotationGeometry 获取注解外观的实际内容轮廓,不将Appearance容器边界视为绘制内容 -// 链接保留透明对象定义的交互区域,图片按几何范围度量而不解码透明像素 -// 入参: annotation 注解 -// 返回: Box 页面范围, []ObjectContour 命中轮廓, error 错误信息 -func (r *Renderer) AnnotationGeometry(annotation Annotation) (Box, []ObjectContour, error) { - measurement := &annotationMeasurement{renderer: r} - if err := r.walkAnnotation(annotation, measurement); err != nil { - return Box{}, nil, err - } - box, contours := measurement.result.Bounds, measurement.result.Contours - for _, source := range r.annotationActionSources([]Annotation{annotation}) { - for _, action := range source.Actions { - if action.Event != "CLICK" { - continue - } - region, path, err := r.actionLinkRegion(source, action) - if err != nil { - return Box{}, nil, err - } - if region.W <= 0 || region.H <= 0 { - continue - } - if path == "" { - path = fmt.Sprintf("M%g %gH%gV%gH%gZ", region.X, region.Y, region.X+region.W, region.Y+region.H, region.X) - } - box = unionTextBox(box, region) - contours = append(contours, ObjectContour{Path: path}) - } - } - return box, contours, nil -} - -// DrawObject 使用对象实际继承状态度量注解内容,不附加容器裁剪 -// 入参: object 图形对象, state 绘制状态 -// 返回: error 度量错误 -func (m *annotationMeasurement) DrawObject(object *GraphicObject, state RenderState) error { - if state.Hidden { - return nil - } - result, err := m.renderer.MeasureObject(*object, MeasureOptions{Defaults: state.Defaults, Parent: state.Parent, BoundaryInCTM: state.BoundaryInCTM, Clip: state.Clip, Contours: true}) - if err != nil { - return err - } - m.result.Bounds = unionTextBox(m.result.Bounds, result.Bounds) - m.result.Contours = append(m.result.Contours, result.Contours...) - return nil -} - -// DrawStamp 满足页面访问接口,注解外观遍历不包含数字签章 -// 入参: stamp 数字签章 -// 返回: error 始终为空 -func (m *annotationMeasurement) DrawStamp(stamp Stamp) error { - return nil -} - -// ObjectContours 获取路径、图片或复合对象的实际绘制区域,包含描边、虚线、变换及对象裁剪 -// 图片不解码像素,底纹不展开图案单元;不包含页面边界或父级变换 -// 入参: object 路径、图片或复合对象, drawParam 图层绘制参数标识 -// 返回: []ObjectContour 可用于点选的轮廓, error 错误信息 -func (r *Renderer) ObjectContours(object GraphicObject, drawParam string) ([]ObjectContour, error) { - _, contours, err := r.ObjectGeometry(object, drawParam) - return contours, err -} - -// ObjectGeometry 一次度量路径、图片或复合对象的范围与轮廓,语义与ObjectBounds和ObjectContours一致 -// 入参: object 图形对象, drawParam 图层绘制参数标识 -// 返回: Box 毫米范围, []ObjectContour 绘制轮廓, error 错误信息 -func (r *Renderer) ObjectGeometry(object GraphicObject, drawParam string) (Box, []ObjectContour, error) { - if object.Type != "PathObject" && object.Type != "ImageObject" && object.Type != "CompositeObject" && object.Type != "CompositeGraphicUnit" { - return Box{}, nil, fmt.Errorf("contours require a path, image or composite object") - } - result, err := r.MeasureObject(object, MeasureOptions{Defaults: r.drawParamDefaults(drawParam, nil), Contours: true}) - return result.Bounds, result.Contours, err -} - -// MeasureObject 使用当前页面编译器度量对象,保持绘制与选区的字体和几何语义一致 -// 入参: object 图形对象, options 度量上下文 -// 返回: ObjectMeasurement 度量结果, error 错误信息 -func (r *Renderer) MeasureObject(object GraphicObject, options MeasureOptions) (ObjectMeasurement, error) { - if r.backends.Compiler == nil { - return ObjectMeasurement{}, fmt.Errorf("%w: object measurement", ErrBackendUnavailable) - } - return r.backends.Compiler.MeasureObject(r, object, options) -} diff --git a/ofdgo_render_compile.go b/ofdgo_render_compile.go index 8ed1e40..0bc3511 100644 --- a/ofdgo_render_compile.go +++ b/ofdgo_render_compile.go @@ -578,3 +578,97 @@ func (OFDCompiler) MeasureObject(r *Renderer, object GraphicObject, options Meas err = r.WalkObject(&object, RenderState{Defaults: options.Defaults, Parent: options.Parent, BoundaryInCTM: options.BoundaryInCTM, Clip: options.Clip}, c) return c.measurement, err } + +// reflectShdPosition 获取镜像周期内的位置 +// 入参: position 渐变位置 +// 返回: float64 周期内的位置 +func reflectShdPosition(position float64) float64 { + index := math.Floor(position) + position -= index + if math.Mod(index, 2) != 0 { + return 1 - position + } + return position +} + +// shadingClip 按渐变延伸规则裁剪绘制与选区,避免只在采样时隐藏区间外像素 +// 入参: path 页面路径, paint 渐变画刷, matrix 渐变父矩阵 +// 返回: GeometryPath 可见路径, error 几何错误 +func (c *semanticCompiler) shadingClip(path GeometryPath, paint Paint, matrix Matrix) (GeometryPath, error) { + gradient, view := semanticGradient(paint, matrix) + spread := gradient.Spread + if gradient.Kind == RasterRadial && math.Hypot(gradient.End.X-gradient.Start.X, gradient.End.Y-gradient.Start.Y) >= math.Abs(gradient.R1-gradient.R0) { + return path, nil + } + if spread == nil || spread.Extend == 3 { + return path, nil + } + inverse, ok := view.Invert() + if !ok { + return nil, nil + } + local, err := c.geometry.Transform(path, inverse) + if err != nil { + return nil, err + } + if gradient.Kind == RasterLinear { + dx, dy := gradient.End.X-gradient.Start.X, gradient.End.Y-gradient.Start.Y + axis := Matrix{a: dx, b: dy, c: -dy, d: dx, e: gradient.Start.X, f: gradient.Start.Y} + inv, _ := axis.Invert() + box, err := c.geometry.Bounds(local) + if err != nil { + return nil, err + } + box = inv.TransformBox(box) + end := box.X + box.W + if spread.Extend&1 == 0 { + box.X = math.Max(box.X, 0) + } + if spread.Extend&2 == 0 { + end = math.Min(end, 1) + } + box.W = end - box.X + if box.W <= 0 { + return nil, nil + } + clip, err := c.geometry.Transform(GeometryPath(basegeom.Rectangle(box)), axis) + if err != nil { + return nil, err + } + local, err = c.geometry.Combine(local, clip, GeometryIntersect) + if err != nil { + return nil, err + } + } else { + inner, outer, r0, r1, extend := gradient.Start, gradient.End, gradient.R0, gradient.R1, spread.Extend + if r1 < r0 { + inner, outer, r0, r1 = outer, inner, r1, r0 + extend = (extend&1)<<1 | (extend&2)>>1 + } + if extend&2 == 0 { + local, err = c.geometry.Combine(local, geometryCircle(outer, r1), GeometryIntersect) + if err != nil { + return nil, err + } + } + if extend&1 == 0 && r0 > 0 { + local, err = c.geometry.Combine(local, geometryCircle(inner, r0), GeometrySubtract) + if err != nil { + return nil, err + } + } + } + return c.geometry.Transform(local, view) +} + +// geometryCircle 构建标准圆弧,不提前折线化 +// 入参: center 圆心, radius 半径 +// 返回: GeometryPath 圆路径 +func geometryCircle(center RasterPoint, radius float64) GeometryPath { + return GeometryPath{ + {Verb: GeometryMove, End: Point{X: center.X + radius, Y: center.Y}}, + {Verb: GeometryArc, RadiusX: radius, RadiusY: radius, Sweep: true, End: Point{X: center.X - radius, Y: center.Y}}, + {Verb: GeometryArc, RadiusX: radius, RadiusY: radius, Sweep: true, End: Point{X: center.X + radius, Y: center.Y}}, + {Verb: GeometryClose}, + } +} diff --git a/ofdgo_render_shading.go b/ofdgo_render_shading.go deleted file mode 100644 index 8495182..0000000 --- a/ofdgo_render_shading.go +++ /dev/null @@ -1,114 +0,0 @@ -// 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 ( - basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" - "math" -) - -// reflectShdPosition 获取镜像周期内的位置 -// 入参: position 渐变位置 -// 返回: float64 周期内的位置 -func reflectShdPosition(position float64) float64 { - index := math.Floor(position) - position -= index - if math.Mod(index, 2) != 0 { - return 1 - position - } - return position -} - -// shadingClip 按渐变延伸规则裁剪绘制与选区,避免只在采样时隐藏区间外像素 -// 入参: path 页面路径, paint 渐变画刷, matrix 渐变父矩阵 -// 返回: GeometryPath 可见路径, error 几何错误 -func (c *semanticCompiler) shadingClip(path GeometryPath, paint Paint, matrix Matrix) (GeometryPath, error) { - gradient, view := semanticGradient(paint, matrix) - spread := gradient.Spread - if gradient.Kind == RasterRadial && math.Hypot(gradient.End.X-gradient.Start.X, gradient.End.Y-gradient.Start.Y) >= math.Abs(gradient.R1-gradient.R0) { - return path, nil - } - if spread == nil || spread.Extend == 3 { - return path, nil - } - inverse, ok := view.Invert() - if !ok { - return nil, nil - } - local, err := c.geometry.Transform(path, inverse) - if err != nil { - return nil, err - } - if gradient.Kind == RasterLinear { - dx, dy := gradient.End.X-gradient.Start.X, gradient.End.Y-gradient.Start.Y - axis := Matrix{a: dx, b: dy, c: -dy, d: dx, e: gradient.Start.X, f: gradient.Start.Y} - inv, _ := axis.Invert() - box, err := c.geometry.Bounds(local) - if err != nil { - return nil, err - } - box = inv.TransformBox(box) - end := box.X + box.W - if spread.Extend&1 == 0 { - box.X = math.Max(box.X, 0) - } - if spread.Extend&2 == 0 { - end = math.Min(end, 1) - } - box.W = end - box.X - if box.W <= 0 { - return nil, nil - } - clip, err := c.geometry.Transform(GeometryPath(basegeom.Rectangle(box)), axis) - if err != nil { - return nil, err - } - local, err = c.geometry.Combine(local, clip, GeometryIntersect) - if err != nil { - return nil, err - } - } else { - inner, outer, r0, r1, extend := gradient.Start, gradient.End, gradient.R0, gradient.R1, spread.Extend - if r1 < r0 { - inner, outer, r0, r1 = outer, inner, r1, r0 - extend = (extend&1)<<1 | (extend&2)>>1 - } - if extend&2 == 0 { - local, err = c.geometry.Combine(local, geometryCircle(outer, r1), GeometryIntersect) - if err != nil { - return nil, err - } - } - if extend&1 == 0 && r0 > 0 { - local, err = c.geometry.Combine(local, geometryCircle(inner, r0), GeometrySubtract) - if err != nil { - return nil, err - } - } - } - return c.geometry.Transform(local, view) -} - -// geometryCircle 构建标准圆弧,不提前折线化 -// 入参: center 圆心, radius 半径 -// 返回: GeometryPath 圆路径 -func geometryCircle(center RasterPoint, radius float64) GeometryPath { - return GeometryPath{ - {Verb: GeometryMove, End: Point{X: center.X + radius, Y: center.Y}}, - {Verb: GeometryArc, RadiusX: radius, RadiusY: radius, Sweep: true, End: Point{X: center.X - radius, Y: center.Y}}, - {Verb: GeometryArc, RadiusX: radius, RadiusY: radius, Sweep: true, End: Point{X: center.X + radius, Y: center.Y}}, - {Verb: GeometryClose}, - } -} diff --git a/ofdgo_renderer_paint_canvas.go b/ofdgo_renderer_paint_canvas.go deleted file mode 100644 index 9fb6e44..0000000 --- a/ofdgo_renderer_paint_canvas.go +++ /dev/null @@ -1,104 +0,0 @@ -// 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 ( - "math" - - "github.com/tdewolff/canvas" -) - -// parseFillPaint 将公共画刷转换为Canvas对象 -// 入参: fillColor 填充颜色节点, x X坐标, y Y坐标, pageH 页面高度 -// 返回: any 填充画刷 -func (r *Renderer) parseFillPaint(fillColor *FillColor, x, y, pageH float64) any { - paint, err := r.ResolvePaint(fillColor) - if err != nil { - r.renderError = err - return nil - } - switch paint.Kind { - case PaintNone, PaintPattern: - return nil - case PaintLinear, PaintRadial: - return canvasShading(paint, x, y, pageH) - case PaintMesh: - return &shdPaint{gradient: newRasterGradientCanvas(&RasterGradient{Kind: RasterMesh, Mesh: paint.Mesh}), view: canvas.Matrix{{1, 0, x}, {0, -1, pageH - y}}} - default: - return paint.Color - } -} - -// parseStrokePaint 解析勾边画刷 -// 入参: strokeColor 勾边颜色节点, x X坐标, y Y坐标, pageH 页面高度 -// 返回: any 勾边画刷 -func (r *Renderer) parseStrokePaint(strokeColor *StrokeColor, x, y, pageH float64) any { - return r.parseFillPaint((*FillColor)(strokeColor), x, y, pageH) -} - -// canvasGradient 将公共颜色节点转换为Canvas渐变 -// 入参: stops 渐变色标 -// 返回: canvas.Grad 渐变色标 -func canvasGradient(stops []ColorStop) canvas.Grad { - var gradient canvas.Grad - for _, stop := range stops { - gradient.Add(stop.Offset, stop.Color) - } - return gradient -} - -// canvasShading 转换坐标与椭圆渐变变换,不重新解析OFD样式 -// 入参: paint 公共渐变画刷, x X坐标, y Y坐标, pageH 页面高度 -// 返回: *shdPaint Canvas渐变画刷 -func canvasShading(paint Paint, x, y, pageH float64) *shdPaint { - source := paint.Gradient - gradient := canvasGradient(source.Stops) - start := canvas.Point{X: x + source.Start.X, Y: pageH - y - source.Start.Y} - end := canvas.Point{X: x + source.End.X, Y: pageH - y - source.End.Y} - if paint.Kind == PaintLinear { - result := newShdPaint(gradient.ToLinear(start, end), source.Extend, source.MapType, source.MapUnit) - result.sample = source.Sample - return result - } - result := newShdPaint(gradient.ToRadial(start, source.StartRadius, end, source.EndRadius), source.Extend, source.MapType, source.MapUnit) - result.sample = source.Sample - if e := source.Eccentricity; 0 < e && e < 1 { - result.view = canvas.Identity.Translate(start.X, start.Y).Rotate(-source.Angle).Scale(1, math.Sqrt(1-e*e)).Translate(-start.X, -start.Y) - result.gradient = gradient.ToRadial(start, source.StartRadius, result.view.Inv().Dot(end), source.EndRadius) - } - return result -} - -// axialShdClip 获取轴向渐变的延伸裁剪区域 -// 入参: gradient 轴向渐变, extend 延伸方向, bounds 可见区域 -// 返回: *canvas.Path 裁剪区域 -func axialShdClip(gradient *canvas.LinearGradient, extend int, bounds canvas.Rect) *canvas.Path { - if extend == 3 { - return nil - } - d := gradient.End.Sub(gradient.Start) - axis := canvas.Matrix{{d.X, -d.Y, gradient.Start.X}, {d.Y, d.X, gradient.Start.Y}} - area := bounds.Transform(axis.Inv()) - if extend&1 == 0 { - area.X0 = math.Max(area.X0, 0) - } - if extend&2 == 0 { - area.X1 = math.Min(area.X1, 1) - } - if area.X1 <= area.X0 { - return &canvas.Path{} - } - return area.ToPath().Transform(axis) -} diff --git a/ofdgo_renderer_shading_canvas.go b/ofdgo_renderer_shading_canvas.go index 1270960..a854564 100644 --- a/ofdgo_renderer_shading_canvas.go +++ b/ofdgo_renderer_shading_canvas.go @@ -366,3 +366,86 @@ func reflectShdGrad(gradient canvas.Grad, period, lo, hi float64) canvas.Grad { } return append(result, canvas.Stop{Offset: 1, Color: gradient.At(reflectShdPosition(hi / period))}) } + +// parseFillPaint 将公共画刷转换为Canvas对象 +// 入参: fillColor 填充颜色节点, x X坐标, y Y坐标, pageH 页面高度 +// 返回: any 填充画刷 +func (r *Renderer) parseFillPaint(fillColor *FillColor, x, y, pageH float64) any { + paint, err := r.ResolvePaint(fillColor) + if err != nil { + r.renderError = err + return nil + } + switch paint.Kind { + case PaintNone, PaintPattern: + return nil + case PaintLinear, PaintRadial: + return canvasShading(paint, x, y, pageH) + case PaintMesh: + return &shdPaint{gradient: newRasterGradientCanvas(&RasterGradient{Kind: RasterMesh, Mesh: paint.Mesh}), view: canvas.Matrix{{1, 0, x}, {0, -1, pageH - y}}} + default: + return paint.Color + } +} + +// parseStrokePaint 解析勾边画刷 +// 入参: strokeColor 勾边颜色节点, x X坐标, y Y坐标, pageH 页面高度 +// 返回: any 勾边画刷 +func (r *Renderer) parseStrokePaint(strokeColor *StrokeColor, x, y, pageH float64) any { + return r.parseFillPaint((*FillColor)(strokeColor), x, y, pageH) +} + +// canvasGradient 将公共颜色节点转换为Canvas渐变 +// 入参: stops 渐变色标 +// 返回: canvas.Grad 渐变色标 +func canvasGradient(stops []ColorStop) canvas.Grad { + var gradient canvas.Grad + for _, stop := range stops { + gradient.Add(stop.Offset, stop.Color) + } + return gradient +} + +// canvasShading 转换坐标与椭圆渐变变换,不重新解析OFD样式 +// 入参: paint 公共渐变画刷, x X坐标, y Y坐标, pageH 页面高度 +// 返回: *shdPaint Canvas渐变画刷 +func canvasShading(paint Paint, x, y, pageH float64) *shdPaint { + source := paint.Gradient + gradient := canvasGradient(source.Stops) + start := canvas.Point{X: x + source.Start.X, Y: pageH - y - source.Start.Y} + end := canvas.Point{X: x + source.End.X, Y: pageH - y - source.End.Y} + if paint.Kind == PaintLinear { + result := newShdPaint(gradient.ToLinear(start, end), source.Extend, source.MapType, source.MapUnit) + result.sample = source.Sample + return result + } + result := newShdPaint(gradient.ToRadial(start, source.StartRadius, end, source.EndRadius), source.Extend, source.MapType, source.MapUnit) + result.sample = source.Sample + if e := source.Eccentricity; 0 < e && e < 1 { + result.view = canvas.Identity.Translate(start.X, start.Y).Rotate(-source.Angle).Scale(1, math.Sqrt(1-e*e)).Translate(-start.X, -start.Y) + result.gradient = gradient.ToRadial(start, source.StartRadius, result.view.Inv().Dot(end), source.EndRadius) + } + return result +} + +// axialShdClip 获取轴向渐变的延伸裁剪区域 +// 入参: gradient 轴向渐变, extend 延伸方向, bounds 可见区域 +// 返回: *canvas.Path 裁剪区域 +func axialShdClip(gradient *canvas.LinearGradient, extend int, bounds canvas.Rect) *canvas.Path { + if extend == 3 { + return nil + } + d := gradient.End.Sub(gradient.Start) + axis := canvas.Matrix{{d.X, -d.Y, gradient.Start.X}, {d.Y, d.X, gradient.Start.Y}} + area := bounds.Transform(axis.Inv()) + if extend&1 == 0 { + area.X0 = math.Max(area.X0, 0) + } + if extend&2 == 0 { + area.X1 = math.Min(area.X1, 1) + } + if area.X1 <= area.X0 { + return &canvas.Path{} + } + return area.ToPath().Transform(axis) +}