mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 16:44:23 +08:00
314 lines
11 KiB
Go
314 lines
11 KiB
Go
// 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 (
|
|
"encoding/xml"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// Res 资源文件结构
|
|
type Res struct {
|
|
XMLName xml.Name `xml:"Res"`
|
|
BaseLoc string `xml:"BaseLoc,attr"`
|
|
ColorSpaces ColorSpaces `xml:"ColorSpaces"`
|
|
Fonts Fonts `xml:"Fonts"`
|
|
MultiMedias MultiMedias `xml:"MultiMedias"`
|
|
DrawParams DrawParams `xml:"DrawParams"`
|
|
CompositeGraphicUnits CompositeGraphicUnits `xml:"CompositeGraphicUnits"`
|
|
}
|
|
|
|
// ColorSpaces 颜色空间集合
|
|
type ColorSpaces struct {
|
|
ColorSpace []ColorSpace `xml:"ColorSpace"`
|
|
}
|
|
|
|
// ColorSpace 颜色空间定义
|
|
type ColorSpace struct {
|
|
ID string `xml:"ID,attr"`
|
|
Type string `xml:"Type,attr"`
|
|
BitsPerComponent int `xml:"BitsPerComponent,attr"`
|
|
Profile string `xml:"Profile,attr,omitempty"`
|
|
Palette []string `xml:"Palette>CV"`
|
|
profile *colorProfile
|
|
}
|
|
|
|
// Fonts 字体集合
|
|
type Fonts struct {
|
|
Font []Font `xml:"Font"`
|
|
}
|
|
|
|
// Font 字体定义
|
|
type Font struct {
|
|
ID string `xml:"ID,attr"`
|
|
FontName string `xml:"FontName,attr"`
|
|
FamilyName string `xml:"FamilyName,attr"`
|
|
Charset string `xml:"Charset,attr"`
|
|
Italic bool `xml:"Italic,attr"`
|
|
Bold bool `xml:"Bold,attr"`
|
|
Serif bool `xml:"Serif,attr"`
|
|
FixedWidth bool `xml:"FixedWidth,attr"`
|
|
FontFile string `xml:"FontFile"`
|
|
}
|
|
|
|
// MultiMedias 多媒体集合
|
|
type MultiMedias struct {
|
|
MultiMedia []MultiMedia `xml:"MultiMedia"`
|
|
}
|
|
|
|
// MultiMedia 多媒体定义
|
|
type MultiMedia struct {
|
|
ID string `xml:"ID,attr"`
|
|
Type string `xml:"Type,attr"`
|
|
Format string `xml:"Format,attr"`
|
|
MediaFile string `xml:"MediaFile"`
|
|
}
|
|
|
|
// DrawParams 绘制参数集合
|
|
type DrawParams struct {
|
|
DrawParam []DrawParam `xml:"DrawParam"`
|
|
}
|
|
|
|
// DrawParam 绘制参数
|
|
// Cap为端点样式,Join为转角样式,MiterLimit为尖角截断值,DashPattern为虚线间隔序列
|
|
// MiterLimitSet为true时,零截断值覆盖继承值,否则零值表示未指定
|
|
type DrawParam struct {
|
|
ID string `xml:"ID,attr"`
|
|
Relative string `xml:"Relative,attr"`
|
|
ResourceID string `xml:"ResourceID,attr"`
|
|
BaseLoc string `xml:"BaseLoc,attr"`
|
|
Link string `xml:"Link,attr"`
|
|
LineWidth float64 `xml:"LineWidth,attr"`
|
|
Join string `xml:"Join,attr"`
|
|
Cap string `xml:"Cap,attr"`
|
|
DashOffset *float64 `xml:"DashOffset,attr"`
|
|
DashPattern string `xml:"DashPattern,attr"`
|
|
MiterLimit float64 `xml:"MiterLimit,attr"`
|
|
Font string `xml:"Font,attr"`
|
|
Size float64 `xml:"Size,attr"`
|
|
Weight int `xml:"Weight,attr"`
|
|
Italic bool `xml:"Italic,attr"`
|
|
FillColor *FillColor `xml:"FillColor"`
|
|
StrokeColor *StrokeColor `xml:"StrokeColor"`
|
|
LineWidthSet bool `xml:"-"`
|
|
MiterLimitSet bool `xml:"-"`
|
|
dashPatternSet bool
|
|
}
|
|
|
|
// CompositeGraphicUnits 复合图元集合
|
|
type CompositeGraphicUnits struct {
|
|
CompositeGraphicUnit []CompositeGraphicUnit `xml:"CompositeGraphicUnit"`
|
|
}
|
|
|
|
// CompositeGraphicUnit 复合图元
|
|
// Width、Height、Thumbnail和Substitution用于矢量资源定义
|
|
type CompositeGraphicUnit struct {
|
|
ID string `xml:"ID,attr"`
|
|
BaseLoc string `xml:"BaseLoc,attr"`
|
|
ResourceID string `xml:"ResourceID,attr"`
|
|
Boundary string `xml:"Boundary,attr"`
|
|
CTM string `xml:"CTM,attr"`
|
|
DrawParam string `xml:"DrawParam,attr"`
|
|
Alpha *int `xml:"Alpha,attr"`
|
|
Visible *bool `xml:"Visible,attr"`
|
|
Width float64 `xml:"Width,attr"`
|
|
Height float64 `xml:"Height,attr"`
|
|
Thumbnail string `xml:"Thumbnail"`
|
|
Substitution string `xml:"Substitution"`
|
|
Objects []GraphicObject `xml:"-"`
|
|
TextObject []TextObject `xml:"TextObject"`
|
|
PathObject []PathObject `xml:"PathObject"`
|
|
ImageObject []ImageObject `xml:"ImageObject"`
|
|
CompositeGraphicUnit []CompositeGraphicUnit `xml:"CompositeGraphicUnit"`
|
|
Clips *Clips `xml:"Clips"`
|
|
Actions []Action `xml:"Actions>Action"`
|
|
states map[string]editorCompositeState
|
|
extentSet bool
|
|
}
|
|
|
|
// UnmarshalXML 解析资源文件,隔离外部命名空间的同名集合
|
|
// 入参: d XML解码器, start 起始节点
|
|
// 返回: error 解码错误
|
|
func (res *Res) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
|
if start.Name.Local != "Res" {
|
|
return fmt.Errorf("expected Res element, got %s", start.Name.Local)
|
|
}
|
|
*res = Res{XMLName: start.Name, BaseLoc: attrValue(start, "BaseLoc")}
|
|
return decodeOFDChildren(d, start, func(d *xml.Decoder, node xml.StartElement) error {
|
|
var target any
|
|
switch node.Name.Local {
|
|
case "ColorSpaces":
|
|
target = &res.ColorSpaces
|
|
case "Fonts":
|
|
target = &res.Fonts
|
|
case "MultiMedias":
|
|
target = &res.MultiMedias
|
|
case "DrawParams":
|
|
target = &res.DrawParams
|
|
case "CompositeGraphicUnits":
|
|
target = &res.CompositeGraphicUnits
|
|
default:
|
|
return d.Skip()
|
|
}
|
|
return d.DecodeElement(target, &node)
|
|
})
|
|
}
|
|
|
|
// UnmarshalXML 解析颜色空间集合
|
|
// 入参: d XML解码器, start 起始节点
|
|
// 返回: error 解码错误
|
|
func (spaces *ColorSpaces) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
|
return decodeOFDCollection(d, start, "ColorSpace", &spaces.ColorSpace)
|
|
}
|
|
|
|
// UnmarshalXML 解析颜色空间属性及调色板
|
|
// 入参: d XML解码器, start 起始节点
|
|
// 返回: error 解码错误
|
|
func (space *ColorSpace) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
|
*space = ColorSpace{ID: attrValue(start, "ID"), Type: attrValue(start, "Type"), Profile: attrValue(start, "Profile")}
|
|
if value := strings.TrimSpace(attrValue(start, "BitsPerComponent")); value != "" {
|
|
bits, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
space.BitsPerComponent = bits
|
|
}
|
|
return decodeOFDChildren(d, start, func(d *xml.Decoder, node xml.StartElement) error {
|
|
if node.Name.Local == "Palette" {
|
|
return decodeOFDCollection(d, node, "CV", &space.Palette)
|
|
}
|
|
return d.Skip()
|
|
})
|
|
}
|
|
|
|
// UnmarshalXML 解析字体集合
|
|
// 入参: d XML解码器, start 起始节点
|
|
// 返回: error 解码错误
|
|
func (fonts *Fonts) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
|
return decodeOFDCollection(d, start, "Font", &fonts.Font)
|
|
}
|
|
|
|
// UnmarshalXML 解析字体声明及内嵌字体路径
|
|
// 入参: d XML解码器, start 起始节点
|
|
// 返回: error 解码错误
|
|
func (font *Font) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
|
*font = Font{ID: attrValue(start, "ID"), FontName: attrValue(start, "FontName"), FamilyName: attrValue(start, "FamilyName"), Charset: attrValue(start, "Charset")}
|
|
for _, attr := range start.Attr {
|
|
if attr.Name.Space != "" {
|
|
continue
|
|
}
|
|
var target *bool
|
|
switch attr.Name.Local {
|
|
case "Italic":
|
|
target = &font.Italic
|
|
case "Bold":
|
|
target = &font.Bold
|
|
case "Serif":
|
|
target = &font.Serif
|
|
case "FixedWidth":
|
|
target = &font.FixedWidth
|
|
}
|
|
if value := strings.TrimSpace(attr.Value); target != nil && value != "" {
|
|
flag, err := strconv.ParseBool(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
*target = flag
|
|
}
|
|
}
|
|
return decodeOFDChildren(d, start, func(d *xml.Decoder, node xml.StartElement) error {
|
|
if node.Name.Local == "FontFile" {
|
|
return d.DecodeElement(&font.FontFile, &node)
|
|
}
|
|
return d.Skip()
|
|
})
|
|
}
|
|
|
|
// UnmarshalXML 解析多媒体集合
|
|
// 入参: d XML解码器, start 起始节点
|
|
// 返回: error 解码错误
|
|
func (media *MultiMedias) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
|
return decodeOFDCollection(d, start, "MultiMedia", &media.MultiMedia)
|
|
}
|
|
|
|
// UnmarshalXML 解析多媒体声明及包内文件路径
|
|
// 入参: d XML解码器, start 起始节点
|
|
// 返回: error 解码错误
|
|
func (media *MultiMedia) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
|
*media = MultiMedia{ID: attrValue(start, "ID"), Type: attrValue(start, "Type"), Format: attrValue(start, "Format")}
|
|
return decodeOFDChildren(d, start, func(d *xml.Decoder, node xml.StartElement) error {
|
|
if node.Name.Local == "MediaFile" {
|
|
return d.DecodeElement(&media.MediaFile, &node)
|
|
}
|
|
return d.Skip()
|
|
})
|
|
}
|
|
|
|
// UnmarshalXML 解析绘制参数集合
|
|
// 入参: d XML解码器, start 起始节点
|
|
// 返回: error 解码错误
|
|
func (params *DrawParams) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
|
return decodeOFDCollection(d, start, "DrawParam", ¶ms.DrawParam)
|
|
}
|
|
|
|
// UnmarshalXML 解析复合图元资源集合
|
|
// 入参: d XML解码器, start 起始节点
|
|
// 返回: error 解码错误
|
|
func (units *CompositeGraphicUnits) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
|
return decodeOFDCollection(d, start, "CompositeGraphicUnit", &units.CompositeGraphicUnit)
|
|
}
|
|
|
|
// sameResourceID 判断资源标识是否相同,数字标识按数值比较
|
|
// 入参: first 第一个标识, second 第二个标识
|
|
// 返回: bool 是否相同
|
|
func sameResourceID(first, second string) bool {
|
|
if first == second {
|
|
return true
|
|
}
|
|
id := editorResourceID(first)
|
|
return id != "" && id == editorResourceID(second)
|
|
}
|
|
|
|
// resourceValue 优先查找原标识与标准十进制标识,回退时检查别名冲突
|
|
// 入参: values 已加载资源, id 标识文本
|
|
// 返回: T 资源值, bool 是否存在唯一结果
|
|
func resourceValue[T comparable](values map[string]T, id string) (T, bool) {
|
|
if value, ok := values[id]; ok {
|
|
return value, true
|
|
}
|
|
var value T
|
|
key := editorResourceID(id)
|
|
if key == "" {
|
|
return value, false
|
|
}
|
|
if value, ok := values[key]; ok {
|
|
return value, true
|
|
}
|
|
found := false
|
|
for alias, candidate := range values {
|
|
if editorResourceID(alias) != key {
|
|
continue
|
|
}
|
|
if found && value != candidate {
|
|
var zero T
|
|
return zero, false
|
|
}
|
|
value, found = candidate, true
|
|
}
|
|
return value, found
|
|
}
|