mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
feat(归并文件): 归并根包文件
This commit is contained in:
1 parent
1a8de5a3f1
commit
bfecdeb0d6
12 files changed
+702
-817
No files matched your search
@@ -15,10 +15,17 @@
|
|||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
|
"io"
|
||||||
"math"
|
"math"
|
||||||
|
|
||||||
|
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
|
||||||
|
"github.com/xiaoqidun/pdfgo"
|
||||||
)
|
)
|
||||||
|
|
||||||
// compressionPatternImageLimit 限制底纹尺寸分析的变换组合数,超限时保留原图尺寸
|
// compressionPatternImageLimit 限制底纹尺寸分析的变换组合数,超限时保留原图尺寸
|
||||||
@@ -397,3 +404,173 @@ func (v *compressionImages) patternImages(color *FillColor, object Matrix) error
|
|||||||
}
|
}
|
||||||
return nil
|
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)
|
||||||
|
}
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -613,3 +613,131 @@ func validateResourceStyleGraph(ctx context.Context, catalog *resourceCatalog, i
|
|||||||
}
|
}
|
||||||
return visit(resourceStyleReference{key: id})
|
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
|
||||||
|
}
|
||||||
+109
@@ -21,6 +21,18 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 画刷类型,区分纯色、渐变和图案填充
|
||||||
|
const (
|
||||||
|
PaintNone PaintKind = iota
|
||||||
|
PaintSolid
|
||||||
|
PaintLinear
|
||||||
|
PaintRadial
|
||||||
|
PaintPattern
|
||||||
|
PaintMesh
|
||||||
)
|
)
|
||||||
|
|
||||||
// PatternPaint 底纹画刷
|
// PatternPaint 底纹画刷
|
||||||
@@ -36,6 +48,36 @@ type profileGradient struct {
|
|||||||
colors []MeshVertex
|
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
|
// ResolveColor 按文档颜色空间和调色板解析颜色,返回预乘透明度的RGBA
|
||||||
// 入参: value 颜色分量, index 调色板索引, space 颜色空间标识, alpha 透明度
|
// 入参: value 颜色分量, index 调色板索引, space 颜色空间标识, alpha 透明度
|
||||||
// 返回: color.RGBA 标准颜色, error 颜色定义或配置错误
|
// 返回: color.RGBA 标准颜色, error 颜色定义或配置错误
|
||||||
@@ -467,3 +509,70 @@ func (g *profileGradient) At(t float64) color.RGBA {
|
|||||||
r, green, b := last.rgb(last.Values)
|
r, green, b := last.rgb(last.Values)
|
||||||
return profileColor([3]float64{r, green, b}, last.Alpha)
|
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
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
+111
-1
@@ -14,7 +14,21 @@
|
|||||||
|
|
||||||
package ofdgo
|
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 编译页面为与绘图库无关的只读绘制数据
|
// RenderPage 编译页面为与绘图库无关的只读绘制数据
|
||||||
// 入参: page 页面内容
|
// 入参: page 页面内容
|
||||||
@@ -190,3 +204,99 @@ func (r *Renderer) RenderPageByIndex(index int) (*RasterPage, error) {
|
|||||||
}
|
}
|
||||||
return r.RenderPage(page)
|
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)
|
||||||
|
}
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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)
|
err = r.WalkObject(&object, RenderState{Defaults: options.Defaults, Parent: options.Parent, BoundaryInCTM: options.BoundaryInCTM, Clip: options.Clip}, c)
|
||||||
return c.measurement, err
|
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},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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))})
|
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)
|
||||||
|
}
|
||||||
Reference in new issue
Block a user