mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 13:56:32 +08:00
221 lines
7.7 KiB
Go
221 lines
7.7 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 (
|
|
"context"
|
|
"fmt"
|
|
"math"
|
|
)
|
|
|
|
// WatermarkOptions 配置水印旋转、居中和平铺,Area为页面比例区域,nil使用整页
|
|
// Angle为顺时针角度,Gap为平铺最小毫米间距,零值使用10毫米
|
|
// 非平铺时Area宽高同时为零表示中心点定位,不缩放图元
|
|
type WatermarkOptions struct {
|
|
Creator string
|
|
Angle float64
|
|
Tile bool
|
|
Area *Box
|
|
Gap float64
|
|
}
|
|
|
|
// AddWatermarks 向指定页面添加文字或图片水印,复用编辑器字体来源及后端,一次操作计入一条撤销记录
|
|
// 图元Boundary提供原始尺寸,忽略其位置;按实际绘制范围居中,超出区域时等比缩小
|
|
// 失败或取消时保留原文档,输入图元不被修改
|
|
// 入参: ctx 取消上下文, indexes 不重复的页面索引, object 水印图元, options 布局选项
|
|
// 返回: []string 按页面顺序排列的注解标识, error 错误信息
|
|
func (e *Editor) AddWatermarks(ctx context.Context, indexes []int, object GraphicObject, options WatermarkOptions) ([]string, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := e.validatePageIndexes(indexes); err != nil {
|
|
return nil, err
|
|
}
|
|
if !finite(options.Angle) || !finite(options.Gap) || options.Gap < 0 {
|
|
return nil, fmt.Errorf("invalid watermark angle or gap")
|
|
}
|
|
if area := options.Area; area != nil {
|
|
point := !options.Tile && area.W == 0 && area.H == 0
|
|
if !finite(area.X) || !finite(area.Y) || !finite(area.W) || !finite(area.H) || area.X < 0 || area.Y < 0 || (!point && (area.W <= 0 || area.H <= 0)) || area.X+area.W > 1 || area.Y+area.H > 1 {
|
|
return nil, fmt.Errorf("invalid watermark area")
|
|
}
|
|
}
|
|
if options.Gap == 0 {
|
|
options.Gap = 10
|
|
}
|
|
object = cloneEditorData(object)
|
|
var boundary, ctm *string
|
|
switch object.Type {
|
|
case "TextObject":
|
|
boundary, ctm = &object.TextObject.Boundary, &object.TextObject.CTM
|
|
case "ImageObject":
|
|
boundary, ctm = &object.ImageObject.Boundary, &object.ImageObject.CTM
|
|
default:
|
|
return nil, fmt.Errorf("watermark requires text or image")
|
|
}
|
|
box, err := creationBox(*boundary)
|
|
if err != nil || box.W <= 0 || box.H <= 0 {
|
|
return nil, fmt.Errorf("invalid watermark dimensions")
|
|
}
|
|
if len(indexes) == 0 {
|
|
return nil, nil
|
|
}
|
|
box.X, box.Y = 0, 0
|
|
*boundary = editorBoxString(box)
|
|
angle := math.Mod(options.Angle, 360) * math.Pi / 180
|
|
rotation := Matrix{a: math.Cos(angle), b: math.Sin(angle), c: -math.Sin(angle), d: math.Cos(angle)}
|
|
rotation = TranslationMatrix(box.W/2, box.H/2).Multiply(rotation).Multiply(TranslationMatrix(-box.W/2, -box.H/2))
|
|
if *ctm != "" {
|
|
if _, err := creationNumbers(*ctm, 6); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
object = transformEditorMatrix(object, rotation)
|
|
if object.Type == "TextObject" {
|
|
boundary = &object.TextObject.Boundary
|
|
} else {
|
|
boundary = &object.ImageObject.Boundary
|
|
}
|
|
extent, _ := ParseBox(*boundary)
|
|
extent.X, extent.Y = 0, 0
|
|
*boundary = editorBoxString(extent)
|
|
preview := e.transactionSnapshot()
|
|
preview.SetHistoryLimit(0)
|
|
id, err := preview.AddObject(indexes[0], object)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
object, err = preview.Object(indexes[0], id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
reader, err := preview.reader(nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer reader.Close()
|
|
page, err := reader.PageContentByIndex(indexes[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
renderer := e.newRenderer(reader)
|
|
measured := cloneEditorData(object)
|
|
if measured.Type == "TextObject" {
|
|
measured.TextObject.Alpha = nil
|
|
} else {
|
|
measured.ImageObject.Alpha = nil
|
|
}
|
|
bounds, err := renderer.ObjectBounds(measured, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !finite(bounds.X) || !finite(bounds.Y) || !finite(bounds.W) || !finite(bounds.H) || bounds.W <= 0 || bounds.H <= 0 {
|
|
return nil, fmt.Errorf("watermark has no visible content")
|
|
}
|
|
var ids []string
|
|
err = e.Transaction(func(edit *Editor) error {
|
|
for _, index := range indexes {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
if index != indexes[0] {
|
|
page, err = reader.PageContentByIndex(index)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
pageBox, err := renderer.GetPageBox(page)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
annotation, err := layoutWatermark(ctx, object, bounds, box, options, pageBox)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
id, err := edit.AddAnnotation(index, annotation)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ids = append(ids, id)
|
|
}
|
|
return ctx.Err()
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ids, nil
|
|
}
|
|
|
|
// layoutWatermark 按绘制范围布局独立水印外观,保留页面原点及图元变换
|
|
// 入参: ctx 取消上下文, base 水印图元, bounds 绘制范围, box 原始尺寸, options 布局选项, page 页面范围
|
|
// 返回: Annotation 水印注解, error 取消或布局错误
|
|
func layoutWatermark(ctx context.Context, base GraphicObject, bounds, box Box, options WatermarkOptions, page Box) (Annotation, error) {
|
|
area := page
|
|
if options.Area != nil {
|
|
area = Box{X: page.X + options.Area.X*page.W, Y: page.Y + options.Area.Y*page.H, W: options.Area.W * page.W, H: options.Area.H * page.H}
|
|
}
|
|
if !finite(page.X) || !finite(page.Y) || !finite(page.W) || !finite(page.H) || page.W <= 0 || page.H <= 0 {
|
|
return Annotation{}, fmt.Errorf("invalid watermark page area")
|
|
}
|
|
scale := 1.0
|
|
if area.W > 0 && area.H > 0 {
|
|
scale = math.Min(1, math.Min(area.W/bounds.W, area.H/bounds.H))
|
|
}
|
|
if scale != 1 {
|
|
matrix := Matrix{a: scale, d: scale}
|
|
base = transformEditorMatrix(base, matrix)
|
|
bounds = matrix.TransformBox(bounds)
|
|
}
|
|
var extent Box
|
|
if base.Type == "TextObject" {
|
|
extent, _ = ParseBox(base.TextObject.Boundary)
|
|
} else {
|
|
extent, _ = ParseBox(base.ImageObject.Boundary)
|
|
}
|
|
columns, rows := 1.0, 1.0
|
|
if options.Tile {
|
|
angle := math.Mod(options.Angle, 360) * math.Pi / 180
|
|
width := math.Max(bounds.W, (math.Abs(math.Cos(angle))*box.W+math.Abs(math.Sin(angle))*box.H)*scale)
|
|
height := math.Max(bounds.H, (math.Abs(math.Sin(angle))*box.W+math.Abs(math.Cos(angle))*box.H)*scale)
|
|
columns = math.Max(1, math.Floor((area.W+options.Gap)/(width+options.Gap)))
|
|
rows = math.Max(1, math.Floor((area.H+options.Gap)/(height+options.Gap)))
|
|
} else {
|
|
area = Box{X: area.X + (area.W-bounds.W)/2, Y: area.Y + (area.H-bounds.H)/2, W: bounds.W, H: bounds.H}
|
|
}
|
|
if !finite(columns*rows) || columns*rows > 100000 {
|
|
return Annotation{}, fmt.Errorf("too many watermark tiles")
|
|
}
|
|
annotation := Annotation{Type: "Watermark", Creator: options.Creator, Appearance: Appearance{Boundary: editorBoxString(area)}}
|
|
annotation.Appearance.Objects = make([]GraphicObject, 0, int(columns*rows))
|
|
for row := 0; row < int(rows); row++ {
|
|
if err := ctx.Err(); err != nil {
|
|
return Annotation{}, err
|
|
}
|
|
for column := 0; column < int(columns); column++ {
|
|
x := (float64(column)+0.5)*area.W/columns - bounds.X - bounds.W/2
|
|
y := (float64(row)+0.5)*area.H/rows - bounds.Y - bounds.H/2
|
|
object := base
|
|
boundary := editorBoxString(Box{X: x, Y: y, W: extent.W, H: extent.H})
|
|
if object.Type == "TextObject" {
|
|
object.TextObject.Boundary = boundary
|
|
} else {
|
|
object.ImageObject.Boundary = boundary
|
|
}
|
|
annotation.Appearance.Objects = append(annotation.Appearance.Objects, object)
|
|
}
|
|
}
|
|
return annotation, nil
|
|
}
|