mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 15:06:46 +08:00
641 lines
21 KiB
Go
641 lines
21 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 (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"image/color"
|
|
"maps"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// PatternStyle 图案尺寸、步进和变换增量,nil字段保持原值,单位为毫米
|
|
type PatternStyle struct {
|
|
Width *float64
|
|
Height *float64
|
|
XStep *float64
|
|
YStep *float64
|
|
CTM *string
|
|
}
|
|
|
|
// editorValidation 保存同次对象校验的引用路径及已验证几何,允许共享但拒绝循环
|
|
type editorValidation struct {
|
|
*Editor
|
|
patterns map[*Pattern]bool
|
|
composites map[string]bool
|
|
vectors map[string]bool
|
|
paths map[string]bool
|
|
}
|
|
|
|
// AddColorSpace 注册颜色空间和可选ICC快照,相同配置共用文件,ID与Profile由库生成
|
|
// 入参: space 模型、位深和调色板, profile ICC文件,无配置时为nil
|
|
// 返回: string 资源标识, error 定义或配置错误
|
|
func (e *Editor) AddColorSpace(space ColorSpace, profile []byte) (string, error) {
|
|
space.ID, space.Profile, space.profile = "", "", nil
|
|
space.Type = strings.ToUpper(space.Type)
|
|
count := map[string]int{"GRAY": 1, "RGB": 3, "CMYK": 4}[space.Type]
|
|
if count == 0 {
|
|
return "", fmt.Errorf("unsupported color space %q", space.Type)
|
|
}
|
|
if space.BitsPerComponent == 0 {
|
|
space.BitsPerComponent = 8
|
|
}
|
|
if !slices.Contains([]int{1, 2, 4, 8, 16}, space.BitsPerComponent) {
|
|
return "", fmt.Errorf("unsupported color depth %d", space.BitsPerComponent)
|
|
}
|
|
limit := 1<<space.BitsPerComponent - 1
|
|
for _, entry := range space.Palette {
|
|
fields := strings.Fields(entry)
|
|
if len(fields) != count {
|
|
return "", fmt.Errorf("expected %d palette components", count)
|
|
}
|
|
for _, field := range fields {
|
|
value, err := parseColorComponent(field)
|
|
if err != nil || value < 0 || value > limit {
|
|
return "", fmt.Errorf("invalid palette component %q", field)
|
|
}
|
|
}
|
|
}
|
|
var key editorResourceKey
|
|
if len(profile) != 0 {
|
|
key = editorResourceKey{checksum: sha256.Sum256(profile), index: -1, kind: "ColorSpace/" + space.Type}
|
|
if id := e.resourceID[key]; id != "" {
|
|
for _, resource := range e.resources {
|
|
if resource.space != nil && resource.space.ID == id && bytes.Equal(resource.data, profile) {
|
|
space.profile, space.Profile = resource.space.profile, resource.space.Profile
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if space.profile == nil {
|
|
var err error
|
|
space.profile, err = parseColorProfile(space.Type, profile)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
}
|
|
if err := e.prepareSourceIDs(); err != nil {
|
|
return "", err
|
|
}
|
|
space.ID = e.nextID()
|
|
space.Palette = slices.Clone(space.Palette)
|
|
resource := editorResource{space: &space}
|
|
if space.profile != nil && space.Profile == "" {
|
|
resource.name = e.packageName("Res/ColorSpaces/ColorSpace_" + space.ID + ".icc")
|
|
resource.data = bytes.Clone(profile)
|
|
space.Profile = "/" + resource.name
|
|
e.resourceID[key] = space.ID
|
|
}
|
|
e.resources = append(e.resources, resource)
|
|
return space.ID, nil
|
|
}
|
|
|
|
// GradientStops 解析当前文档的渐变分段,保留顺序并合并透明度
|
|
// 入参: segments 渐变分段, alpha 外层透明度
|
|
// 返回: []ColorStop 后端无关的色标, error 颜色解析错误
|
|
func (e *Editor) GradientStops(segments []ShdSegment, alpha *int) ([]ColorStop, error) {
|
|
positions := gradientPositions(segments)
|
|
stops := make([]ColorStop, len(segments))
|
|
for i, segment := range segments {
|
|
c, err := e.Color(&FillColor{Value: segment.Color.Value, Index: segment.Color.Index, ColorSpace: segment.Color.ColorSpace, Alpha: mergeAlpha(segment.Color.Alpha, alpha)})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
stops[i] = ColorStop{Offset: positions[i], Color: colorToRGBA(c)}
|
|
}
|
|
return stops, nil
|
|
}
|
|
|
|
// StylePatterns 修改已有图案布局,保留单元内容、资源引用和未知扩展
|
|
// 入参: page 页面索引, ids 对象标识, stroke 是否修改描边, style 图案布局增量
|
|
// 返回: error 错误信息
|
|
func (e *Editor) StylePatterns(page int, ids []string, stroke bool, style PatternStyle) error {
|
|
for _, size := range []*float64{style.Width, style.Height} {
|
|
if size != nil && (!finite(*size) || *size <= 0) {
|
|
return fmt.Errorf("pattern dimensions must be positive and finite")
|
|
}
|
|
}
|
|
for _, step := range []*float64{style.XStep, style.YStep} {
|
|
if step != nil && (!finite(*step) || *step < 0) {
|
|
return fmt.Errorf("pattern steps must be nonnegative and finite")
|
|
}
|
|
}
|
|
if style.CTM != nil && *style.CTM != "" {
|
|
if _, err := creationNumbers(*style.CTM, 6); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
objects, indexes, err := e.selectedObjects(page, ids)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i := range objects {
|
|
objects[i] = cloneEditorData(objects[i])
|
|
object := &objects[i]
|
|
var fill *FillColor
|
|
var draw string
|
|
switch object.Type {
|
|
case "TextObject":
|
|
draw = object.TextObject.DrawParam
|
|
fill = object.TextObject.FillColor
|
|
if stroke {
|
|
fill = (*FillColor)(object.TextObject.StrokeColor)
|
|
}
|
|
case "PathObject", "Path":
|
|
draw = object.PathObject.DrawParam
|
|
fill = object.PathObject.FillColor
|
|
if stroke {
|
|
fill = (*FillColor)(object.PathObject.StrokeColor)
|
|
}
|
|
default:
|
|
return fmt.Errorf("object %q does not use a pattern", ids[i])
|
|
}
|
|
if fill == nil {
|
|
base, err := e.editorDrawParam(e.pages[page].Content.Layer[indexes[i].layer].DrawParam, make(map[string]bool))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
param, err := e.editorDrawParam(draw, make(map[string]bool))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
paint := mergeDrawParam(*base, param)
|
|
fill = paint.FillColor
|
|
if stroke {
|
|
fill = (*FillColor)(paint.StrokeColor)
|
|
}
|
|
fill = cloneEditorData(fill)
|
|
if object.Type == "TextObject" {
|
|
if stroke {
|
|
object.TextObject.StrokeColor = (*StrokeColor)(fill)
|
|
} else {
|
|
object.TextObject.FillColor = fill
|
|
}
|
|
} else if object.Type == "PathObject" || object.Type == "Path" {
|
|
if stroke {
|
|
object.PathObject.StrokeColor = (*StrokeColor)(fill)
|
|
} else {
|
|
object.PathObject.FillColor = fill
|
|
}
|
|
}
|
|
}
|
|
if fill == nil || fill.Pattern == nil {
|
|
return fmt.Errorf("object %q does not use a pattern", ids[i])
|
|
}
|
|
pattern := fill.Pattern
|
|
for _, pair := range []struct{ value, target *float64 }{{style.Width, &pattern.Width}, {style.Height, &pattern.Height}, {style.XStep, &pattern.XStep}, {style.YStep, &pattern.YStep}} {
|
|
if pair.value != nil {
|
|
*pair.target = *pair.value
|
|
}
|
|
}
|
|
if style.CTM != nil {
|
|
pattern.CTM = *style.CTM
|
|
}
|
|
}
|
|
return e.updateObjects(page, objects, true)
|
|
}
|
|
|
|
// Color 解析当前文档的纯色,支持RGB、灰度、CMYK、调色板及ICC配置,不修改原颜色
|
|
// 入参: value 颜色,nil表示默认黑色
|
|
// 返回: color.NRGBA 非预乘颜色, error 错误信息
|
|
func (e *Editor) Color(value *FillColor) (color.NRGBA, error) {
|
|
space := &ColorSpace{Type: "RGB", BitsPerComponent: 8}
|
|
if value == nil {
|
|
return color.NRGBA{A: 255}, nil
|
|
}
|
|
if value.Pattern != nil || value.AxialShd != nil || value.RadialShd != nil || value.GouraudShd != nil || value.LaGouraudShd != nil || value.unsupported {
|
|
return color.NRGBA{}, fmt.Errorf("color is not a supported solid color")
|
|
}
|
|
id := value.ColorSpace
|
|
if id == "" && e.source != nil && e.source.document.CommonData.DefaultCS != 0 {
|
|
id = strconv.Itoa(e.source.document.CommonData.DefaultCS)
|
|
}
|
|
if id != "" {
|
|
var found *ColorSpace
|
|
if e.source != nil {
|
|
found, _ = resourceValue(e.source.reader.colorSpaceCache, id)
|
|
}
|
|
for _, resource := range e.resources {
|
|
if resource.space != nil && sameResourceID(resource.space.ID, id) {
|
|
found = resource.space
|
|
break
|
|
}
|
|
}
|
|
if found == nil {
|
|
return color.NRGBA{}, fmt.Errorf("color space %q not found", id)
|
|
}
|
|
space = found
|
|
}
|
|
bits := space.BitsPerComponent
|
|
if bits == 0 {
|
|
bits = 8
|
|
}
|
|
if bits != 1 && bits != 2 && bits != 4 && bits != 8 && bits != 16 {
|
|
return color.NRGBA{}, fmt.Errorf("unsupported color depth %d", bits)
|
|
}
|
|
count := 3
|
|
kind := strings.ToUpper(space.Type)
|
|
switch kind {
|
|
case "RGB":
|
|
case "GRAY":
|
|
count = 1
|
|
case "CMYK":
|
|
count = 4
|
|
default:
|
|
return color.NRGBA{}, fmt.Errorf("unsupported color space %q", space.Type)
|
|
}
|
|
text := value.Value
|
|
if strings.TrimSpace(text) == "" && value.Index != nil {
|
|
if *value.Index < 0 || *value.Index >= len(space.Palette) {
|
|
return color.NRGBA{}, fmt.Errorf("color palette index out of range")
|
|
}
|
|
text = space.Palette[*value.Index]
|
|
}
|
|
fields := strings.Fields(text)
|
|
if len(fields) != 0 && len(fields) != count {
|
|
return color.NRGBA{}, fmt.Errorf("expected %d color components", count)
|
|
}
|
|
var components [4]uint8
|
|
var normalized [4]float64
|
|
limit := 1<<bits - 1
|
|
for i, field := range fields {
|
|
v, err := parseColorComponent(field)
|
|
if err != nil || v < 0 || v > limit {
|
|
return color.NRGBA{}, fmt.Errorf("invalid color component %q", field)
|
|
}
|
|
components[i] = uint8((v*255 + limit/2) / limit)
|
|
normalized[i] = float64(v) / float64(limit)
|
|
}
|
|
r, g, b := components[0], components[1], components[2]
|
|
if kind == "GRAY" {
|
|
g, b = r, r
|
|
} else if kind == "CMYK" {
|
|
r, g, b = color.CMYKToRGB(components[0], components[1], components[2], components[3])
|
|
}
|
|
var reader *Reader
|
|
if e.source != nil {
|
|
reader = e.source.reader
|
|
}
|
|
profile, err := reader.colorProfile(space)
|
|
if err != nil {
|
|
return color.NRGBA{}, err
|
|
}
|
|
if profile != nil {
|
|
rgb, err := profile.rgb(normalized)
|
|
if err != nil {
|
|
return color.NRGBA{}, err
|
|
}
|
|
r, g, b = meshByte(rgb[0]), meshByte(rgb[1]), meshByte(rgb[2])
|
|
}
|
|
a := 255
|
|
if value.Alpha != nil {
|
|
a = *value.Alpha
|
|
if a < 0 || a > 255 {
|
|
return color.NRGBA{}, fmt.Errorf("color alpha must be between 0 and 255")
|
|
}
|
|
}
|
|
return color.NRGBA{R: r, G: g, B: b, A: uint8(a)}, nil
|
|
}
|
|
|
|
// RGBColor 创建浏览器等调用方使用的RGB纯色,非RGB文档按需注册独立颜色空间
|
|
// 不修改文档默认颜色空间,未使用的新增颜色空间不写出
|
|
// 入参: value 非预乘颜色
|
|
// 返回: *FillColor OFD颜色, error 错误信息
|
|
func (e *Editor) RGBColor(value color.NRGBA) (*FillColor, error) {
|
|
result := &FillColor{Value: fmt.Sprintf("%d %d %d", value.R, value.G, value.B)}
|
|
if value.A != 255 {
|
|
alpha := int(value.A)
|
|
result.Alpha = &alpha
|
|
}
|
|
if e.sourceRGB() {
|
|
return result, nil
|
|
}
|
|
for _, id := range slices.Sorted(maps.Keys(e.source.reader.colorSpaceCache)) {
|
|
space := e.source.reader.colorSpaceCache[id]
|
|
if space.Type == "RGB" && space.Profile == "" && (space.BitsPerComponent == 0 || space.BitsPerComponent == 8) && len(space.Palette) == 0 {
|
|
result.ColorSpace = id
|
|
return result, nil
|
|
}
|
|
}
|
|
for _, resource := range e.resources {
|
|
space := resource.space
|
|
if space != nil && space.Type == "RGB" && space.Profile == "" && (space.BitsPerComponent == 0 || space.BitsPerComponent == 8) && len(space.Palette) == 0 {
|
|
result.ColorSpace = space.ID
|
|
return result, nil
|
|
}
|
|
}
|
|
if err := e.prepareSourceIDs(); err != nil {
|
|
return nil, err
|
|
}
|
|
result.ColorSpace = e.nextID()
|
|
e.resources = append(e.resources, editorResource{space: &ColorSpace{ID: result.ColorSpace, Type: "RGB", BitsPerComponent: 8}})
|
|
return result, nil
|
|
}
|
|
|
|
// PaintObjects 更新文字或路径的填充或描边,保留定位、其他样式和原文扩展
|
|
// 入参: page 页面索引, ids 对象标识, paint 绘制颜色,nil恢复默认, stroke 是否修改描边
|
|
// 返回: error 错误信息
|
|
func (e *Editor) PaintObjects(page int, ids []string, paint *FillColor, stroke bool) error {
|
|
if err := e.editorColor(paint); err != nil {
|
|
return err
|
|
}
|
|
objects, _, err := e.selectedObjects(page, ids)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i := range objects {
|
|
object := &objects[i]
|
|
switch object.Type {
|
|
case "TextObject":
|
|
if stroke {
|
|
object.TextObject.StrokeColor = (*StrokeColor)(cloneEditorData(paint))
|
|
} else {
|
|
object.TextObject.FillColor = cloneEditorData(paint)
|
|
}
|
|
case "PathObject":
|
|
if stroke {
|
|
object.PathObject.StrokeColor = (*StrokeColor)(cloneEditorData(paint))
|
|
} else {
|
|
object.PathObject.FillColor = cloneEditorData(paint)
|
|
}
|
|
default:
|
|
return fmt.Errorf("object %q does not have text or path paint", editorObjectID(*object))
|
|
}
|
|
}
|
|
return e.updateObjects(page, objects, true)
|
|
}
|
|
|
|
// editorPaintable 判断文字与路径颜色能否在保留其他字段的前提下修改
|
|
// 入参: object 图形对象
|
|
// 返回: bool 是否支持绘制颜色编辑
|
|
func (e *Editor) editorPaintable(object GraphicObject) bool {
|
|
var fill, stroke *FillColor
|
|
switch object.Type {
|
|
case "TextObject":
|
|
fill, stroke = object.TextObject.FillColor, (*FillColor)(object.TextObject.StrokeColor)
|
|
case "PathObject":
|
|
fill, stroke = object.PathObject.FillColor, (*FillColor)(object.PathObject.StrokeColor)
|
|
default:
|
|
return false
|
|
}
|
|
for _, value := range []*FillColor{fill, stroke} {
|
|
if err := e.editorColor(value); err != nil {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// collectPaintReferences 收集纯色、渐变和图案引用的资源
|
|
// 入参: paint 绘制颜色, used 引用集合
|
|
func collectPaintReferences(paint *FillColor, used map[string]bool) {
|
|
if paint == nil {
|
|
return
|
|
}
|
|
used[paint.ColorSpace] = true
|
|
var segments []ShdSegment
|
|
if paint.AxialShd != nil {
|
|
segments = paint.AxialShd.Segment
|
|
}
|
|
if paint.RadialShd != nil {
|
|
segments = paint.RadialShd.Segment
|
|
}
|
|
for _, segment := range segments {
|
|
used[segment.Color.ColorSpace] = true
|
|
}
|
|
var points []ShdPoint
|
|
var back *ShdColor
|
|
if node := paint.GouraudShd; node != nil {
|
|
points, back = node.Point, node.BackColor
|
|
}
|
|
if node := paint.LaGouraudShd; node != nil {
|
|
points, back = node.Point, node.BackColor
|
|
}
|
|
for _, point := range points {
|
|
used[point.Color.ColorSpace] = true
|
|
}
|
|
if back != nil {
|
|
used[back.ColorSpace] = true
|
|
}
|
|
if paint.Pattern != nil {
|
|
used[paint.Pattern.CellContent.Thumbnail] = true
|
|
for object := range paint.Pattern.CellContent.objects() {
|
|
collectObjectReferences(object, used)
|
|
}
|
|
}
|
|
}
|
|
|
|
// collectObjectReferences 收集对象和嵌套绘制内容引用的资源
|
|
// 入参: object 图形对象, used 引用集合
|
|
func collectObjectReferences(object GraphicObject, used map[string]bool) {
|
|
var clips *Clips
|
|
var actions []Action
|
|
switch object.Type {
|
|
case "TextObject", "Text":
|
|
used[object.TextObject.Font], used[object.TextObject.DrawParam] = true, true
|
|
collectPaintReferences(object.TextObject.FillColor, used)
|
|
collectPaintReferences((*FillColor)(object.TextObject.StrokeColor), used)
|
|
clips = object.TextObject.Clips
|
|
actions = object.TextObject.Actions
|
|
case "PathObject", "Path":
|
|
used[object.PathObject.DrawParam] = true
|
|
collectPaintReferences(object.PathObject.FillColor, used)
|
|
collectPaintReferences((*FillColor)(object.PathObject.StrokeColor), used)
|
|
clips = object.PathObject.Clips
|
|
actions = object.PathObject.Actions
|
|
case "ImageObject":
|
|
used[object.ImageObject.ResourceID], used[object.ImageObject.ImageMask] = true, true
|
|
used[object.ImageObject.Substitution] = true
|
|
if object.ImageObject.Border != nil {
|
|
collectPaintReferences((*FillColor)(object.ImageObject.Border.BorderColor), used)
|
|
}
|
|
clips = object.ImageObject.Clips
|
|
actions = object.ImageObject.Actions
|
|
case "CompositeObject", "CompositeGraphicUnit":
|
|
collectCompositeReferences(object.CompositeGraphicUnit, used)
|
|
actions = object.CompositeGraphicUnit.Actions
|
|
}
|
|
collectActionReferences(actions, used)
|
|
if clips == nil {
|
|
return
|
|
}
|
|
for _, clip := range clips.Clip {
|
|
for _, area := range clip.Area {
|
|
used[area.DrawParam] = true
|
|
for _, path := range area.Path {
|
|
collectObjectReferences(GraphicObject{Type: "Path", PathObject: path}, used)
|
|
}
|
|
for _, text := range area.Text {
|
|
collectObjectReferences(GraphicObject{Type: "Text", TextObject: text}, used)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// editorMeshColor 校验网格拓扑和文档颜色引用
|
|
// 入参: value 网格颜色
|
|
// 返回: error 非法拓扑或颜色
|
|
func (e *Editor) editorMeshColor(value *FillColor) error {
|
|
if value.unsupported || value.Pattern != nil || value.AxialShd != nil || value.RadialShd != nil || value.GouraudShd != nil && value.LaGouraudShd != nil || value.Value != "" || value.Index != nil {
|
|
return fmt.Errorf("specify one paint source")
|
|
}
|
|
if value.Alpha != nil && (*value.Alpha < 0 || *value.Alpha > 255) {
|
|
return fmt.Errorf("color alpha must be between 0 and 255")
|
|
}
|
|
var points []ShdPoint
|
|
var back *ShdColor
|
|
var columns, extend int
|
|
if node := value.GouraudShd; node != nil {
|
|
points, back, extend = node.Point, node.BackColor, node.Extend
|
|
} else {
|
|
points, back = value.LaGouraudShd.Point, value.LaGouraudShd.BackColor
|
|
columns, extend = value.LaGouraudShd.VerticesPerRow, value.LaGouraudShd.Extend
|
|
if columns == 0 {
|
|
return fmt.Errorf("invalid lattice shading dimensions")
|
|
}
|
|
}
|
|
colors := make([]ShdColor, 0, len(points)+1)
|
|
for _, point := range points {
|
|
colors = append(colors, point.Color)
|
|
}
|
|
if back != nil {
|
|
colors = append(colors, *back)
|
|
}
|
|
for _, color := range colors {
|
|
if _, err := e.Color(&FillColor{Value: color.Value, Index: color.Index, ColorSpace: color.ColorSpace, Alpha: color.Alpha}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
_, err := meshTriangleIndices(points, columns, extend)
|
|
return err
|
|
}
|
|
|
|
// editorColor 校验当前文档的纯色、渐变和底纹
|
|
// 入参: value 颜色
|
|
// 返回: error 错误信息
|
|
func (e *Editor) editorColor(value *FillColor) error {
|
|
return (&editorValidation{Editor: e}).editorColor(value)
|
|
}
|
|
|
|
// editorColor 沿当前引用路径校验底纹单元及绘制颜色
|
|
// 入参: value 颜色
|
|
// 返回: error 错误信息
|
|
func (v *editorValidation) editorColor(value *FillColor) error {
|
|
e := v.Editor
|
|
if value != nil && value.Pattern != nil {
|
|
pattern := value.Pattern
|
|
if v.patterns[pattern] {
|
|
return fmt.Errorf("cyclic pattern paint")
|
|
}
|
|
if v.patterns == nil {
|
|
v.patterns = make(map[*Pattern]bool)
|
|
}
|
|
v.patterns[pattern] = true
|
|
defer delete(v.patterns, pattern)
|
|
}
|
|
if value != nil && (value.GouraudShd != nil || value.LaGouraudShd != nil) {
|
|
return e.editorMeshColor(value)
|
|
}
|
|
if value != nil && value.Pattern != nil {
|
|
pattern := value.Pattern
|
|
if value.unsupported || value.AxialShd != nil || value.RadialShd != nil || !finite(pattern.Width) || !finite(pattern.Height) || pattern.Width <= 0 || pattern.Height <= 0 || !finite(pattern.XStep) || !finite(pattern.YStep) {
|
|
return fmt.Errorf("invalid pattern paint")
|
|
}
|
|
if !slices.Contains([]string{"", "Normal", "Row", "Column", "RowAndColumn"}, pattern.ReflectMethod) {
|
|
return fmt.Errorf("invalid pattern reflection")
|
|
}
|
|
if pattern.RelativeTo != "" && pattern.RelativeTo != "Page" && pattern.RelativeTo != "Object" {
|
|
return fmt.Errorf("invalid pattern reference")
|
|
}
|
|
if pattern.CTM != "" {
|
|
if _, err := creationNumbers(pattern.CTM, 6); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
base := *value
|
|
base.Pattern = nil
|
|
if base.Value == "" && base.Index == nil {
|
|
if base.Alpha != nil && (*base.Alpha < 0 || *base.Alpha > 255) {
|
|
return fmt.Errorf("color alpha must be between 0 and 255")
|
|
}
|
|
} else if _, err := e.Color(&base); err != nil {
|
|
return err
|
|
}
|
|
if pattern.CellContent.Thumbnail != "" {
|
|
if _, err := e.editorImage(pattern.CellContent.Thumbnail); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for object := range pattern.CellContent.objects() {
|
|
if _, err := v.prepareObject("", object); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
if value == nil || value.AxialShd == nil && value.RadialShd == nil {
|
|
_, err := e.Color(value)
|
|
return err
|
|
}
|
|
if value.unsupported || value.Pattern != nil || value.AxialShd != nil && value.RadialShd != nil || value.Value != "" || value.Index != nil {
|
|
return fmt.Errorf("specify one paint source")
|
|
}
|
|
if value.Alpha != nil && (*value.Alpha < 0 || *value.Alpha > 255) {
|
|
return fmt.Errorf("color alpha must be between 0 and 255")
|
|
}
|
|
var mapType, extend, start, end string
|
|
var unit float64
|
|
var segments []ShdSegment
|
|
if node := value.AxialShd; node != nil {
|
|
mapType, unit, extend, start, end, segments = node.MapType, node.MapUnit, node.Extend, node.StartPoint, node.EndPoint, node.Segment
|
|
} else {
|
|
node := value.RadialShd
|
|
mapType, unit, extend, start, end, segments = node.MapType, node.MapUnit, node.Extend, node.StartPoint, node.EndPoint, node.Segment
|
|
if !finite(node.StartRadius) || !finite(node.EndRadius) || node.StartRadius < 0 || node.EndRadius < 0 || !finite(node.Eccentricity) || node.Eccentricity < 0 || node.Eccentricity >= 1 || !finite(node.Angle) {
|
|
return fmt.Errorf("invalid radial gradient geometry")
|
|
}
|
|
}
|
|
if !slices.Contains([]string{"", "Direct", "Repeat", "Reflect"}, mapType) || !finite(unit) || unit < 0 || (mapType == "Repeat" || mapType == "Reflect") && unit == 0 || !slices.Contains([]string{"", "0", "1", "2", "3"}, extend) {
|
|
return fmt.Errorf("invalid gradient mapping")
|
|
}
|
|
for _, point := range []string{start, end} {
|
|
if _, err := creationNumbers(point, 2); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if len(segments) < 2 {
|
|
return fmt.Errorf("gradient requires at least two stops")
|
|
}
|
|
previous := -1.0
|
|
positions := gradientPositions(segments)
|
|
for index, segment := range segments {
|
|
position := positions[index]
|
|
if !finite(position) || position < previous || position < 0 || position > 1 {
|
|
return fmt.Errorf("gradient stops must be ordered within zero and one")
|
|
}
|
|
previous = position
|
|
color := segment.Color
|
|
if _, err := e.Color(&FillColor{Value: color.Value, Index: color.Index, ColorSpace: color.ColorSpace, Alpha: color.Alpha}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|