mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
1 parent
bfecdeb0d6
commit
f3cc35e78c
4 files changed
+329
-372
No files matched your search
+123
@@ -22,6 +22,7 @@ import (
|
||||
"image"
|
||||
"image/color"
|
||||
"math"
|
||||
"slices"
|
||||
|
||||
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
|
||||
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
|
||||
@@ -113,6 +114,48 @@ type RasterSpread struct {
|
||||
Period float64
|
||||
}
|
||||
|
||||
// RasterScene 固定尺寸、DPI、字体和资源的可重复绘制场景
|
||||
// 每次Render返回独立图像;源图像只读借用,修改页面或配置后应重新准备
|
||||
type RasterScene interface {
|
||||
Render() (image.Image, error)
|
||||
}
|
||||
|
||||
// RasterSceneBackend 可选的光栅预编译能力,不改变后端选择
|
||||
type RasterSceneBackend interface {
|
||||
RasterBackend
|
||||
Prepare(page *RasterPage) (RasterScene, error)
|
||||
}
|
||||
|
||||
// RasterSceneUpdater 可选的增量场景能力,保持原后端与显式几何配置
|
||||
// Update按新页面的完整内容及DPI使派生数据失效,失败时原场景仍可绘制
|
||||
// 图片仍为只读借用,编辑图片应提供新资源;修改后端配置应重新Prepare
|
||||
type RasterSceneUpdater interface {
|
||||
RasterScene
|
||||
Update(page *RasterPage) error
|
||||
}
|
||||
|
||||
// RasterSceneReuseBackend 可选的跨快照复用能力,不改变显式后端选择
|
||||
// 实现必须验证前次场景及配置,返回独立快照,不能修改仍可使用的旧场景
|
||||
type RasterSceneReuseBackend interface {
|
||||
RasterSceneBackend
|
||||
PrepareReuse(page *RasterPage, previous RasterScene) (RasterScene, error)
|
||||
}
|
||||
|
||||
// rasterPreparationKey 隔离渲染器私有的前次场景
|
||||
type rasterPreparationKey struct{}
|
||||
|
||||
// rasterPreparationState 记录前次场景及实际后端配置
|
||||
type rasterPreparationState struct {
|
||||
backend RasterBackend
|
||||
scene RasterScene
|
||||
}
|
||||
|
||||
// rasterScene 复用后端无关指令,图片保持只读引用
|
||||
type rasterScene struct {
|
||||
backend RasterBackend
|
||||
page *RasterPage
|
||||
}
|
||||
|
||||
// PixelSize 检查页面尺寸并计算目标像素大小,合法正尺寸至少保留一个像素
|
||||
// 返回: int 宽度, int 高度, error 无效尺寸
|
||||
func (p *RasterPage) PixelSize() (int, int, error) {
|
||||
@@ -514,3 +557,83 @@ func drawRasterImageCommand(backend RasterBackend, page *RasterPage, dst draw.Im
|
||||
draw.DrawMask(dst, bounds, layer, bounds.Min, mask, bounds.Min, draw.Over)
|
||||
return nil
|
||||
}
|
||||
|
||||
// PreparePage 编译并准备当前配置的独立页面快照,按后端能力复用未修改指令
|
||||
// 每次重新解释源页面,按内容使模板、签章、字体及样式派生数据失效
|
||||
// 入参: page 源页面
|
||||
// 返回: RasterScene 可重复绘制场景, error 编译或准备错误
|
||||
func (r *Renderer) PreparePage(page *PageContent) (RasterScene, error) {
|
||||
if r.backends.Raster == nil {
|
||||
return nil, fmt.Errorf("raster scene: %w", ErrBackendUnavailable)
|
||||
}
|
||||
compiled, err := r.CompilePage(page)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if backend, ok := r.backends.Raster.(RasterSceneReuseBackend); ok {
|
||||
state, _ := r.backendStates[rasterPreparationKey{}].(rasterPreparationState)
|
||||
var previous RasterScene
|
||||
if sameBackend(state.backend, r.backends.Raster) {
|
||||
previous = state.scene
|
||||
}
|
||||
scene, err := backend.PrepareReuse(compiled, previous)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.backendStates[rasterPreparationKey{}] = rasterPreparationState{backend: r.backends.Raster, scene: scene}
|
||||
return scene, nil
|
||||
}
|
||||
return PrepareRasterScene(r.backends.Raster, compiled)
|
||||
}
|
||||
|
||||
// PrepareRasterScene 准备绘制快照,未实现预编译的后端复用公共指令
|
||||
// 入参: backend 光栅后端, page 绘制页面
|
||||
// 返回: RasterScene 可重复绘制场景, error 准备错误
|
||||
func PrepareRasterScene(backend RasterBackend, page *RasterPage) (RasterScene, error) {
|
||||
if backend == nil {
|
||||
return nil, fmt.Errorf("raster scene: %w", ErrBackendUnavailable)
|
||||
}
|
||||
if _, _, err := page.PixelSize(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if provider, ok := backend.(RasterSceneBackend); ok {
|
||||
return provider.Prepare(page)
|
||||
}
|
||||
return &rasterScene{backend: backend, page: cloneRasterPage(page)}, nil
|
||||
}
|
||||
|
||||
// Render 绘制固定快照
|
||||
// 返回: image.Image 独立图像, error 绘制错误
|
||||
func (s *rasterScene) Render() (image.Image, error) { return s.backend.Render(s.page) }
|
||||
|
||||
// cloneRasterPage 隔离可变指令,源图片按接口约定只读借用
|
||||
// 入参: page 绘制页面
|
||||
// 返回: *RasterPage 指令快照
|
||||
func cloneRasterPage(page *RasterPage) *RasterPage {
|
||||
result := *page
|
||||
result.Commands = slices.Clone(page.Commands)
|
||||
for i := range result.Commands {
|
||||
cmd := &result.Commands[i]
|
||||
cmd.Path, cmd.Clip = slices.Clone(cmd.Path), slices.Clone(cmd.Clip)
|
||||
if cmd.Stroke != nil {
|
||||
stroke := *cmd.Stroke
|
||||
stroke.Dashes = slices.Clone(stroke.Dashes)
|
||||
cmd.Stroke = &stroke
|
||||
}
|
||||
if cmd.Paint.Gradient != nil {
|
||||
gradient := *cmd.Paint.Gradient
|
||||
gradient.Stops = slices.Clone(gradient.Stops)
|
||||
if gradient.Mesh != nil {
|
||||
mesh := *gradient.Mesh
|
||||
mesh.Triangles = slices.Clone(mesh.Triangles)
|
||||
gradient.Mesh = &mesh
|
||||
}
|
||||
if gradient.Spread != nil {
|
||||
spread := *gradient.Spread
|
||||
gradient.Spread = &spread
|
||||
}
|
||||
cmd.Paint.Gradient = &gradient
|
||||
}
|
||||
}
|
||||
return &result
|
||||
}
|
||||
@@ -1,143 +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"
|
||||
"image"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// RasterScene 固定尺寸、DPI、字体和资源的可重复绘制场景
|
||||
// 每次Render返回独立图像;源图像只读借用,修改页面或配置后应重新准备
|
||||
type RasterScene interface {
|
||||
Render() (image.Image, error)
|
||||
}
|
||||
|
||||
// RasterSceneBackend 可选的光栅预编译能力,不改变后端选择
|
||||
type RasterSceneBackend interface {
|
||||
RasterBackend
|
||||
Prepare(page *RasterPage) (RasterScene, error)
|
||||
}
|
||||
|
||||
// RasterSceneUpdater 可选的增量场景能力,保持原后端与显式几何配置
|
||||
// Update按新页面的完整内容及DPI使派生数据失效,失败时原场景仍可绘制
|
||||
// 图片仍为只读借用,编辑图片应提供新资源;修改后端配置应重新Prepare
|
||||
type RasterSceneUpdater interface {
|
||||
RasterScene
|
||||
Update(page *RasterPage) error
|
||||
}
|
||||
|
||||
// RasterSceneReuseBackend 可选的跨快照复用能力,不改变显式后端选择
|
||||
// 实现必须验证前次场景及配置,返回独立快照,不能修改仍可使用的旧场景
|
||||
type RasterSceneReuseBackend interface {
|
||||
RasterSceneBackend
|
||||
PrepareReuse(page *RasterPage, previous RasterScene) (RasterScene, error)
|
||||
}
|
||||
|
||||
// rasterPreparationKey 隔离渲染器私有的前次场景
|
||||
type rasterPreparationKey struct{}
|
||||
|
||||
// rasterPreparationState 记录前次场景及实际后端配置
|
||||
type rasterPreparationState struct {
|
||||
backend RasterBackend
|
||||
scene RasterScene
|
||||
}
|
||||
|
||||
// rasterScene 复用后端无关指令,图片保持只读引用
|
||||
type rasterScene struct {
|
||||
backend RasterBackend
|
||||
page *RasterPage
|
||||
}
|
||||
|
||||
// PreparePage 编译并准备当前配置的独立页面快照,按后端能力复用未修改指令
|
||||
// 每次重新解释源页面,按内容使模板、签章、字体及样式派生数据失效
|
||||
// 入参: page 源页面
|
||||
// 返回: RasterScene 可重复绘制场景, error 编译或准备错误
|
||||
func (r *Renderer) PreparePage(page *PageContent) (RasterScene, error) {
|
||||
if r.backends.Raster == nil {
|
||||
return nil, fmt.Errorf("raster scene: %w", ErrBackendUnavailable)
|
||||
}
|
||||
compiled, err := r.CompilePage(page)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if backend, ok := r.backends.Raster.(RasterSceneReuseBackend); ok {
|
||||
state, _ := r.backendStates[rasterPreparationKey{}].(rasterPreparationState)
|
||||
var previous RasterScene
|
||||
if sameBackend(state.backend, r.backends.Raster) {
|
||||
previous = state.scene
|
||||
}
|
||||
scene, err := backend.PrepareReuse(compiled, previous)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.backendStates[rasterPreparationKey{}] = rasterPreparationState{backend: r.backends.Raster, scene: scene}
|
||||
return scene, nil
|
||||
}
|
||||
return PrepareRasterScene(r.backends.Raster, compiled)
|
||||
}
|
||||
|
||||
// PrepareRasterScene 准备绘制快照,未实现预编译的后端复用公共指令
|
||||
// 入参: backend 光栅后端, page 绘制页面
|
||||
// 返回: RasterScene 可重复绘制场景, error 准备错误
|
||||
func PrepareRasterScene(backend RasterBackend, page *RasterPage) (RasterScene, error) {
|
||||
if backend == nil {
|
||||
return nil, fmt.Errorf("raster scene: %w", ErrBackendUnavailable)
|
||||
}
|
||||
if _, _, err := page.PixelSize(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if provider, ok := backend.(RasterSceneBackend); ok {
|
||||
return provider.Prepare(page)
|
||||
}
|
||||
return &rasterScene{backend: backend, page: cloneRasterPage(page)}, nil
|
||||
}
|
||||
|
||||
// Render 绘制固定快照
|
||||
// 返回: image.Image 独立图像, error 绘制错误
|
||||
func (s *rasterScene) Render() (image.Image, error) { return s.backend.Render(s.page) }
|
||||
|
||||
// cloneRasterPage 隔离可变指令,源图片按接口约定只读借用
|
||||
// 入参: page 绘制页面
|
||||
// 返回: *RasterPage 指令快照
|
||||
func cloneRasterPage(page *RasterPage) *RasterPage {
|
||||
result := *page
|
||||
result.Commands = slices.Clone(page.Commands)
|
||||
for i := range result.Commands {
|
||||
cmd := &result.Commands[i]
|
||||
cmd.Path, cmd.Clip = slices.Clone(cmd.Path), slices.Clone(cmd.Clip)
|
||||
if cmd.Stroke != nil {
|
||||
stroke := *cmd.Stroke
|
||||
stroke.Dashes = slices.Clone(stroke.Dashes)
|
||||
cmd.Stroke = &stroke
|
||||
}
|
||||
if cmd.Paint.Gradient != nil {
|
||||
gradient := *cmd.Paint.Gradient
|
||||
gradient.Stops = slices.Clone(gradient.Stops)
|
||||
if gradient.Mesh != nil {
|
||||
mesh := *gradient.Mesh
|
||||
mesh.Triangles = slices.Clone(mesh.Triangles)
|
||||
gradient.Mesh = &mesh
|
||||
}
|
||||
if gradient.Spread != nil {
|
||||
spread := *gradient.Spread
|
||||
gradient.Spread = &spread
|
||||
}
|
||||
cmd.Paint.Gradient = &gradient
|
||||
}
|
||||
}
|
||||
return &result
|
||||
}
|
||||
@@ -20,7 +20,10 @@ import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// 公文标引类型标识
|
||||
@@ -447,3 +450,206 @@ func (r *Renderer) StructureTargetsContext(ctx context.Context, index int, path
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// encodeStructureNode 编码有界标引子树,引用由ObjectRef节点表达,不重复编码派生References字段
|
||||
// 入参: node 根节点
|
||||
// 返回: []byte XML片段, error 名称、属性或编码错误
|
||||
func encodeStructureNode(node StructureNode) ([]byte, error) {
|
||||
return encodeStructureNodeContext(context.Background(), node)
|
||||
}
|
||||
|
||||
// encodeStructureNodeContext 编码标引子树并检查取消状态
|
||||
// 入参: ctx 取消上下文, node 根节点
|
||||
// 返回: []byte XML片段, error 名称、深度、编码或取消错误
|
||||
func encodeStructureNodeContext(ctx context.Context, node StructureNode) ([]byte, error) {
|
||||
var output bytes.Buffer
|
||||
encoder := xml.NewEncoder(&output)
|
||||
var write func(StructureNode, int) error
|
||||
write = func(node StructureNode, depth int) error {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if depth > 128 {
|
||||
return fmt.Errorf("custom tag structure limit exceeded")
|
||||
}
|
||||
if err := validateStructureNode(node); err != nil {
|
||||
return err
|
||||
}
|
||||
start := xml.StartElement{Name: xml.Name{Space: node.Namespace, Local: node.Name}}
|
||||
switch node.Namespace {
|
||||
case "":
|
||||
start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "xmlns"}, Value: ""})
|
||||
case XMLNamespace:
|
||||
start.Name = xml.Name{Local: "xml:" + node.Name}
|
||||
}
|
||||
for _, attr := range node.Attributes {
|
||||
start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Space: attr.Namespace, Local: attr.Name}, Value: attr.Value})
|
||||
}
|
||||
if err := encoder.EncodeToken(start); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := encoder.EncodeToken(xml.CharData(node.Text)); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, child := range node.Children {
|
||||
if err := write(child, depth+1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return encoder.EncodeToken(start.End())
|
||||
}
|
||||
if err := write(node, 1); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := encoder.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return output.Bytes(), nil
|
||||
}
|
||||
|
||||
// validateStructureNode 校验标引节点名称、属性和文本
|
||||
// 入参: node 节点字段
|
||||
// 返回: error 字段校验错误
|
||||
func validateStructureNode(node StructureNode) error {
|
||||
if !ofdXMLIDValid(node.Name) || node.Name == "xmlns" {
|
||||
return fmt.Errorf("invalid custom tag node name: %s", node.Name)
|
||||
}
|
||||
if !structureXMLText(node.Text) || !structureXMLText(node.Namespace) || node.Namespace == xmlDeclarationNamespace {
|
||||
return fmt.Errorf("invalid custom tag text or namespace")
|
||||
}
|
||||
seen := make(map[xml.Name]bool)
|
||||
for _, attr := range node.Attributes {
|
||||
name := xml.Name{Space: attr.Namespace, Local: attr.Name}
|
||||
if !ofdXMLIDValid(attr.Name) || attr.Name == "xmlns" || attr.Namespace == xmlDeclarationNamespace || !structureXMLText(attr.Namespace) || !structureXMLText(attr.Value) || seen[name] {
|
||||
return fmt.Errorf("invalid or duplicate custom tag attribute: %s", attr.Name)
|
||||
}
|
||||
seen[name] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// structureXMLText 检查XML 1.0允许的字符,不以替换字符掩盖无效输入
|
||||
// 入参: value 文本
|
||||
// 返回: bool 是否可无损编码
|
||||
func structureXMLText(value string) bool {
|
||||
if !utf8.ValidString(value) {
|
||||
return false
|
||||
}
|
||||
for _, r := range value {
|
||||
if r != '\t' && r != '\n' && r != '\r' && (r < 0x20 || r == 0xfffe || r == 0xffff) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// updateStructureNodeXML 替换节点字段,保留子树继承的命名空间和非文本XML内容
|
||||
// 入参: data 原XML, source 原节点, node 新字段
|
||||
// 返回: []byte 独立节点片段, error 校验或编码错误
|
||||
func updateStructureNodeXML(data []byte, source *editorXML, node StructureNode) ([]byte, error) {
|
||||
if err := validateStructureNode(node); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fragment, err := editorXMLStandalone(data[source.start:source.end], source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
before, err := parseStructure(context.Background(), fragment, CustomTag{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if node.Name == before.Name && node.Namespace == before.Namespace && node.Text == before.Text && slices.Equal(node.Attributes, before.Attributes) {
|
||||
return data[source.start:source.end], nil
|
||||
}
|
||||
root, err := parseEditorXML(fragment)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if before.Text != node.Text {
|
||||
var text bytes.Buffer
|
||||
if err := xml.EscapeText(&text, []byte(node.Text)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fragment, err = editorXMLDirectText(fragment, root, text.Bytes())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
root, err = parseEditorXML(fragment)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var attrs []xml.Attr
|
||||
used := make(map[string]bool)
|
||||
for _, attr := range root.attrs {
|
||||
if attr.Name.Space == "xmlns" {
|
||||
attrs = append(attrs, xml.Attr{Name: xml.Name{Local: "xmlns:" + attr.Name.Local}, Value: attr.Value})
|
||||
used[attr.Name.Local] = true
|
||||
} else if attr.Name.Space == "" && attr.Name.Local == "xmlns" {
|
||||
attrs = append(attrs, attr)
|
||||
}
|
||||
}
|
||||
qualify := func(name, namespace string) string {
|
||||
if namespace == "" {
|
||||
return name
|
||||
}
|
||||
if namespace == XMLNamespace {
|
||||
return "xml:" + name
|
||||
}
|
||||
for _, attr := range attrs {
|
||||
if strings.HasPrefix(attr.Name.Local, "xmlns:") && attr.Value == namespace {
|
||||
return strings.TrimPrefix(attr.Name.Local, "xmlns:") + ":" + name
|
||||
}
|
||||
}
|
||||
prefix := "tag"
|
||||
for i := 1; used[prefix]; i++ {
|
||||
prefix = "tag" + strconv.Itoa(i)
|
||||
}
|
||||
used[prefix] = true
|
||||
attrs = append(attrs, xml.Attr{Name: xml.Name{Local: "xmlns:" + prefix}, Value: namespace})
|
||||
return prefix + ":" + name
|
||||
}
|
||||
name := qualify(node.Name, node.Namespace)
|
||||
if node.Namespace == "" {
|
||||
for i := range attrs {
|
||||
if attrs[i].Name.Local == "xmlns" {
|
||||
attrs[i].Value = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, attr := range node.Attributes {
|
||||
key := qualify(attr.Name, attr.Namespace)
|
||||
attrs = append(attrs, xml.Attr{Name: xml.Name{Local: key}, Value: attr.Value})
|
||||
}
|
||||
var output bytes.Buffer
|
||||
encoder := xml.NewEncoder(&output)
|
||||
start := xml.StartElement{Name: xml.Name{Local: name}, Attr: attrs}
|
||||
if err := encoder.EncodeToken(start); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := encoder.Flush(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if root.open != root.end {
|
||||
if node.Namespace == "" && before.Namespace != "" {
|
||||
var patches []editorXMLPatch
|
||||
for _, child := range root.children {
|
||||
standalone, err := editorXMLStandalone(fragment[child.start:child.end], child)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
patches = append(patches, editorXMLPatch{child.start - root.open, child.end - root.open, standalone})
|
||||
}
|
||||
output.Write(editorPatchXML(fragment[root.open:root.close], patches))
|
||||
} else {
|
||||
output.Write(fragment[root.open:root.close])
|
||||
}
|
||||
}
|
||||
if err := encoder.EncodeToken(start.End()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := encoder.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return output.Bytes(), nil
|
||||
}
|
||||
@@ -1,229 +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/xml"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// encodeStructureNode 编码有界标引子树,引用由ObjectRef节点表达,不重复编码派生References字段
|
||||
// 入参: node 根节点
|
||||
// 返回: []byte XML片段, error 名称、属性或编码错误
|
||||
func encodeStructureNode(node StructureNode) ([]byte, error) {
|
||||
return encodeStructureNodeContext(context.Background(), node)
|
||||
}
|
||||
|
||||
// encodeStructureNodeContext 编码标引子树并检查取消状态
|
||||
// 入参: ctx 取消上下文, node 根节点
|
||||
// 返回: []byte XML片段, error 名称、深度、编码或取消错误
|
||||
func encodeStructureNodeContext(ctx context.Context, node StructureNode) ([]byte, error) {
|
||||
var output bytes.Buffer
|
||||
encoder := xml.NewEncoder(&output)
|
||||
var write func(StructureNode, int) error
|
||||
write = func(node StructureNode, depth int) error {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if depth > 128 {
|
||||
return fmt.Errorf("custom tag structure limit exceeded")
|
||||
}
|
||||
if err := validateStructureNode(node); err != nil {
|
||||
return err
|
||||
}
|
||||
start := xml.StartElement{Name: xml.Name{Space: node.Namespace, Local: node.Name}}
|
||||
switch node.Namespace {
|
||||
case "":
|
||||
start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "xmlns"}, Value: ""})
|
||||
case XMLNamespace:
|
||||
start.Name = xml.Name{Local: "xml:" + node.Name}
|
||||
}
|
||||
for _, attr := range node.Attributes {
|
||||
start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Space: attr.Namespace, Local: attr.Name}, Value: attr.Value})
|
||||
}
|
||||
if err := encoder.EncodeToken(start); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := encoder.EncodeToken(xml.CharData(node.Text)); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, child := range node.Children {
|
||||
if err := write(child, depth+1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return encoder.EncodeToken(start.End())
|
||||
}
|
||||
if err := write(node, 1); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := encoder.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return output.Bytes(), nil
|
||||
}
|
||||
|
||||
// validateStructureNode 校验标引节点名称、属性和文本
|
||||
// 入参: node 节点字段
|
||||
// 返回: error 字段校验错误
|
||||
func validateStructureNode(node StructureNode) error {
|
||||
if !ofdXMLIDValid(node.Name) || node.Name == "xmlns" {
|
||||
return fmt.Errorf("invalid custom tag node name: %s", node.Name)
|
||||
}
|
||||
if !structureXMLText(node.Text) || !structureXMLText(node.Namespace) || node.Namespace == xmlDeclarationNamespace {
|
||||
return fmt.Errorf("invalid custom tag text or namespace")
|
||||
}
|
||||
seen := make(map[xml.Name]bool)
|
||||
for _, attr := range node.Attributes {
|
||||
name := xml.Name{Space: attr.Namespace, Local: attr.Name}
|
||||
if !ofdXMLIDValid(attr.Name) || attr.Name == "xmlns" || attr.Namespace == xmlDeclarationNamespace || !structureXMLText(attr.Namespace) || !structureXMLText(attr.Value) || seen[name] {
|
||||
return fmt.Errorf("invalid or duplicate custom tag attribute: %s", attr.Name)
|
||||
}
|
||||
seen[name] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// structureXMLText 检查XML 1.0允许的字符,不以替换字符掩盖无效输入
|
||||
// 入参: value 文本
|
||||
// 返回: bool 是否可无损编码
|
||||
func structureXMLText(value string) bool {
|
||||
if !utf8.ValidString(value) {
|
||||
return false
|
||||
}
|
||||
for _, r := range value {
|
||||
if r != '\t' && r != '\n' && r != '\r' && (r < 0x20 || r == 0xfffe || r == 0xffff) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// updateStructureNodeXML 替换节点字段,保留子树继承的命名空间和非文本XML内容
|
||||
// 入参: data 原XML, source 原节点, node 新字段
|
||||
// 返回: []byte 独立节点片段, error 校验或编码错误
|
||||
func updateStructureNodeXML(data []byte, source *editorXML, node StructureNode) ([]byte, error) {
|
||||
if err := validateStructureNode(node); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fragment, err := editorXMLStandalone(data[source.start:source.end], source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
before, err := parseStructure(context.Background(), fragment, CustomTag{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if node.Name == before.Name && node.Namespace == before.Namespace && node.Text == before.Text && slices.Equal(node.Attributes, before.Attributes) {
|
||||
return data[source.start:source.end], nil
|
||||
}
|
||||
root, err := parseEditorXML(fragment)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if before.Text != node.Text {
|
||||
var text bytes.Buffer
|
||||
if err := xml.EscapeText(&text, []byte(node.Text)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fragment, err = editorXMLDirectText(fragment, root, text.Bytes())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
root, err = parseEditorXML(fragment)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var attrs []xml.Attr
|
||||
used := make(map[string]bool)
|
||||
for _, attr := range root.attrs {
|
||||
if attr.Name.Space == "xmlns" {
|
||||
attrs = append(attrs, xml.Attr{Name: xml.Name{Local: "xmlns:" + attr.Name.Local}, Value: attr.Value})
|
||||
used[attr.Name.Local] = true
|
||||
} else if attr.Name.Space == "" && attr.Name.Local == "xmlns" {
|
||||
attrs = append(attrs, attr)
|
||||
}
|
||||
}
|
||||
qualify := func(name, namespace string) string {
|
||||
if namespace == "" {
|
||||
return name
|
||||
}
|
||||
if namespace == XMLNamespace {
|
||||
return "xml:" + name
|
||||
}
|
||||
for _, attr := range attrs {
|
||||
if strings.HasPrefix(attr.Name.Local, "xmlns:") && attr.Value == namespace {
|
||||
return strings.TrimPrefix(attr.Name.Local, "xmlns:") + ":" + name
|
||||
}
|
||||
}
|
||||
prefix := "tag"
|
||||
for i := 1; used[prefix]; i++ {
|
||||
prefix = "tag" + strconv.Itoa(i)
|
||||
}
|
||||
used[prefix] = true
|
||||
attrs = append(attrs, xml.Attr{Name: xml.Name{Local: "xmlns:" + prefix}, Value: namespace})
|
||||
return prefix + ":" + name
|
||||
}
|
||||
name := qualify(node.Name, node.Namespace)
|
||||
if node.Namespace == "" {
|
||||
for i := range attrs {
|
||||
if attrs[i].Name.Local == "xmlns" {
|
||||
attrs[i].Value = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, attr := range node.Attributes {
|
||||
key := qualify(attr.Name, attr.Namespace)
|
||||
attrs = append(attrs, xml.Attr{Name: xml.Name{Local: key}, Value: attr.Value})
|
||||
}
|
||||
var output bytes.Buffer
|
||||
encoder := xml.NewEncoder(&output)
|
||||
start := xml.StartElement{Name: xml.Name{Local: name}, Attr: attrs}
|
||||
if err := encoder.EncodeToken(start); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := encoder.Flush(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if root.open != root.end {
|
||||
if node.Namespace == "" && before.Namespace != "" {
|
||||
var patches []editorXMLPatch
|
||||
for _, child := range root.children {
|
||||
standalone, err := editorXMLStandalone(fragment[child.start:child.end], child)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
patches = append(patches, editorXMLPatch{child.start - root.open, child.end - root.open, standalone})
|
||||
}
|
||||
output.Write(editorPatchXML(fragment[root.open:root.close], patches))
|
||||
} else {
|
||||
output.Write(fragment[root.open:root.close])
|
||||
}
|
||||
}
|
||||
if err := encoder.EncodeToken(start.End()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := encoder.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return output.Bytes(), nil
|
||||
}
|
||||
Reference in new issue
Block a user