mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 17:27:08 +08:00
This commit is contained in:
1 parent
accd0a2973
commit
fef7e75920
8 files changed
+266
-143
No files matched your search
@@ -3618,7 +3618,7 @@ async function openOFD(file, remote = null) {
|
|||||||
await openDocument({ doc, pageIndex: remote?.pageIndex || 0, resetScroll: true, openSeq });
|
await openDocument({ doc, pageIndex: remote?.pageIndex || 0, resetScroll: true, openSeq });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (openSeq === state.openSeq) {
|
if (openSeq === state.openSeq) {
|
||||||
if (err.name === "AbortError") setStatus("转换已取消");
|
if (err.name === "AbortError") setStatus("打开已取消");
|
||||||
else showError(err, !state.doc);
|
else showError(err, !state.doc);
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ require (
|
|||||||
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
|
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
|
||||||
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
|
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
|
||||||
github.com/xiaoqidun/jbig2 v1.1.1
|
github.com/xiaoqidun/jbig2 v1.1.1
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20260929145256-b04a96874771
|
github.com/xiaoqidun/pdfgo v0.0.0-20260929154445-b9e68e6f6960
|
||||||
golang.org/x/crypto v0.57.0
|
golang.org/x/crypto v0.57.0
|
||||||
golang.org/x/image v0.46.0
|
golang.org/x/image v0.46.0
|
||||||
golang.org/x/text v0.42.0
|
golang.org/x/text v0.42.0
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk=
|
|||||||
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||||
github.com/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI=
|
github.com/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI=
|
||||||
github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ=
|
github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ=
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20260929145256-b04a96874771 h1:qddSlHkgZhRMyawpOS3TlW5nu8+IurZFjxS4Kwg8YLA=
|
github.com/xiaoqidun/pdfgo v0.0.0-20260929154445-b9e68e6f6960 h1:msQWyUBuNUnvJ+VKrUELivQPYFxSVanp/iSncN1c+yA=
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20260929145256-b04a96874771/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ=
|
github.com/xiaoqidun/pdfgo v0.0.0-20260929154445-b9e68e6f6960/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
|
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
|
||||||
|
|||||||
+38
-136
@@ -545,7 +545,12 @@ func packFiles(args []js.Value) (any, error) {
|
|||||||
source := &browserReaderAt{read: read, checkpoint: args[4], size: int64(file.Get("size").Float())}
|
source := &browserReaderAt{read: read, checkpoint: args[4], size: int64(file.Get("size").Float())}
|
||||||
entries[i] = ofdgo.ArchiveFile{Name: name, Source: source, Size: source.size}
|
entries[i] = ofdgo.ArchiveFile{Name: name, Source: source, Size: source.size}
|
||||||
}
|
}
|
||||||
options := ofdgo.ArchiveOptions{OnProgress: func(completed, total int) error { return awaitExport(args[3], completed, total) }}
|
options := ofdgo.ArchiveOptions{OnProgress: func(completed, total int) error {
|
||||||
|
if completed > 0 {
|
||||||
|
entries[completed-1].Source.(*browserReaderAt).data = nil
|
||||||
|
}
|
||||||
|
return awaitExport(args[3], completed, total)
|
||||||
|
}}
|
||||||
if len(args) > 5 && !args[5].IsUndefined() && !args[5].IsNull() {
|
if len(args) > 5 && !args[5].IsUndefined() && !args[5].IsNull() {
|
||||||
if err := compressionFromJS(args[5], &options.Compression); err != nil {
|
if err := compressionFromJS(args[5], &options.Compression); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1422,7 +1427,6 @@ func writeAnnotation(args []js.Value) (any, error) {
|
|||||||
var ids []string
|
var ids []string
|
||||||
result, err := changeObjects(func() error {
|
result, err := changeObjects(func() error {
|
||||||
font, resource := "", ""
|
font, resource := "", ""
|
||||||
var fontData []byte
|
|
||||||
if !args[2].IsNull() {
|
if !args[2].IsNull() {
|
||||||
data, err := bytesFromJS(args[2])
|
data, err := bytesFromJS(args[2])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1432,7 +1436,6 @@ func writeAnnotation(args []js.Value) (any, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fontData = data
|
|
||||||
}
|
}
|
||||||
if !args[3].IsNull() {
|
if !args[3].IsNull() {
|
||||||
data, err := bytesFromJS(args[3])
|
data, err := bytesFromJS(args[3])
|
||||||
@@ -1475,11 +1478,31 @@ func writeAnnotation(args []js.Value) (any, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var watermarkBounds ofdgo.Box
|
|
||||||
if options.Kind == "watermark" {
|
if options.Kind == "watermark" {
|
||||||
if area := options.Area; area != nil && (area.X < 0 || area.Y < 0 || area.W < 0 || area.H < 0 || area.X+area.W > 1 || area.Y+area.H > 1 || options.Tile && (area.W == 0 || area.H == 0)) {
|
boundary := fmt.Sprintf("0 0 %g %g", options.Width, options.Height)
|
||||||
return fmt.Errorf("invalid watermark area")
|
var object ofdgo.GraphicObject
|
||||||
|
if resource != "" {
|
||||||
|
object = ofdgo.GraphicObject{Type: "ImageObject", ImageObject: ofdgo.ImageObject{ResourceID: resource, Boundary: boundary, Alpha: &options.Alpha}}
|
||||||
|
} else {
|
||||||
|
text := ofdgo.TextObject{Font: font, Size: options.Size, Boundary: boundary, Alpha: &options.Alpha}
|
||||||
|
if err := currentEditor.LayoutText(&text, options.Text, ofdgo.TextLayout{Align: "center"}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := setTextColor(&text, options.Color); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
object = ofdgo.GraphicObject{Type: "TextObject", TextObject: text}
|
||||||
}
|
}
|
||||||
|
added, err := currentEditor.AddWatermarks(context.Background(), pages, object, ofdgo.WatermarkOptions{Creator: options.Creator, Angle: options.Angle, Tile: options.Tile, Area: options.Area})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i, index := range pages {
|
||||||
|
if index == page {
|
||||||
|
ids = append(ids, "annotation:"+added[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
for _, index := range pages {
|
for _, index := range pages {
|
||||||
box := ofdgo.Box{X: options.X, Y: options.Y, W: options.Width, H: options.Height}
|
box := ofdgo.Box{X: options.X, Y: options.Y, W: options.Width, H: options.Height}
|
||||||
@@ -1491,56 +1514,20 @@ func writeAnnotation(args []js.Value) (any, error) {
|
|||||||
switch options.Kind {
|
switch options.Kind {
|
||||||
case "note":
|
case "note":
|
||||||
annotation.Type, annotation.Remark = "Path", options.Text
|
annotation.Type, annotation.Remark = "Path", options.Text
|
||||||
case "watermark", "stamp":
|
case "stamp":
|
||||||
annotation.Type = "Watermark"
|
annotation.Type = "Stamp"
|
||||||
if options.Kind == "stamp" {
|
if resource == "" {
|
||||||
annotation.Type = "Stamp"
|
return fmt.Errorf("stamp requires an image")
|
||||||
}
|
|
||||||
var object ofdgo.GraphicObject
|
|
||||||
if resource != "" {
|
|
||||||
object = ofdgo.GraphicObject{Type: "ImageObject", ImageObject: ofdgo.ImageObject{ResourceID: resource, Boundary: fmt.Sprintf("0 0 %g %g", box.W, box.H), CTM: fmt.Sprintf("%g 0 0 %g 0 0", box.W, box.H), Alpha: &options.Alpha}}
|
|
||||||
} else {
|
|
||||||
if options.Kind == "stamp" {
|
|
||||||
return fmt.Errorf("stamp requires an image")
|
|
||||||
}
|
|
||||||
text := ofdgo.TextObject{Font: font, Size: options.Size, Boundary: fmt.Sprintf("0 0 %g %g", box.W, box.H), Alpha: &options.Alpha}
|
|
||||||
if err := currentEditor.LayoutText(&text, options.Text, ofdgo.TextLayout{Align: "center"}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := setTextColor(&text, options.Color); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
object = ofdgo.GraphicObject{Type: "TextObject", TextObject: text}
|
|
||||||
}
|
}
|
||||||
|
object := ofdgo.GraphicObject{Type: "ImageObject", ImageObject: ofdgo.ImageObject{ResourceID: resource, Boundary: fmt.Sprintf("0 0 %g %g", box.W, box.H), CTM: fmt.Sprintf("%g 0 0 %g 0 0", box.W, box.H), Alpha: &options.Alpha}}
|
||||||
angle := options.Angle * math.Pi / 180
|
angle := options.Angle * math.Pi / 180
|
||||||
matrix := ofdgo.NewMatrix(fmt.Sprintf("%g %g %g %g 0 0", math.Cos(angle), math.Sin(angle), -math.Sin(angle), math.Cos(angle)))
|
matrix := ofdgo.NewMatrix(fmt.Sprintf("%g %g %g %g 0 0", math.Cos(angle), math.Sin(angle), -math.Sin(angle), math.Cos(angle)))
|
||||||
matrix = ofdgo.TranslationMatrix(box.W/2, box.H/2).Multiply(matrix).Multiply(ofdgo.TranslationMatrix(-box.W/2, -box.H/2))
|
matrix = ofdgo.TranslationMatrix(box.W/2, box.H/2).Multiply(matrix).Multiply(ofdgo.TranslationMatrix(-box.W/2, -box.H/2))
|
||||||
if object.Type == "TextObject" {
|
object.ImageObject.CTM = matrix.Multiply(ofdgo.NewMatrix(object.ImageObject.CTM)).String()
|
||||||
object.TextObject.CTM = matrix.String()
|
|
||||||
} else {
|
|
||||||
object.ImageObject.CTM = matrix.Multiply(ofdgo.NewMatrix(object.ImageObject.CTM)).String()
|
|
||||||
}
|
|
||||||
annotation.Appearance.Objects = []ofdgo.GraphicObject{object}
|
annotation.Appearance.Objects = []ofdgo.GraphicObject{object}
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported annotation kind %q", options.Kind)
|
return fmt.Errorf("unsupported annotation kind %q", options.Kind)
|
||||||
}
|
}
|
||||||
if options.Kind == "watermark" {
|
|
||||||
if watermarkBounds.W == 0 {
|
|
||||||
watermarkBounds, err = measureWatermark(annotation.Appearance.Objects[0], fontData, currentEditor.Backends())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
content, err := currentSession.pageContent(index)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
page, err := currentSession.pageBox(index, content)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
layoutWatermark(&annotation, watermarkBounds, options, page)
|
|
||||||
}
|
|
||||||
id, err = currentEditor.AddAnnotation(index, annotation)
|
id, err = currentEditor.AddAnnotation(index, annotation)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1558,94 +1545,6 @@ func writeAnnotation(args []js.Value) (any, error) {
|
|||||||
return editorSelectionInfo{editorInfo: result.(editorInfo), SelectedIDs: ids}, nil
|
return editorSelectionInfo{editorInfo: result.(editorInfo), SelectedIDs: ids}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// measureWatermark 复用标准渲染度量水印,临时文档保留尚未提交的字体引用
|
|
||||||
// 入参: object 水印图元, fontData 字体字节, backends 当前后端配置
|
|
||||||
// 返回: ofdgo.Box 实际绘制范围, error 错误信息
|
|
||||||
func measureWatermark(object ofdgo.GraphicObject, fontData []byte, backends ofdgo.RenderBackends) (ofdgo.Box, error) {
|
|
||||||
if object.Type == "ImageObject" {
|
|
||||||
return ofdgo.NewMatrix(object.ImageObject.CTM).TransformBox(ofdgo.Box{W: 1, H: 1}), nil
|
|
||||||
}
|
|
||||||
editor := ofdgo.NewEditor()
|
|
||||||
if _, err := editor.AddPage(210, 297); err != nil {
|
|
||||||
return ofdgo.Box{}, err
|
|
||||||
}
|
|
||||||
font, err := editor.AddFont(FontFile{Data: fontData}, 0)
|
|
||||||
if err != nil {
|
|
||||||
return ofdgo.Box{}, err
|
|
||||||
}
|
|
||||||
object.TextObject.Font, object.TextObject.Alpha = font, nil
|
|
||||||
if _, err := editor.AddObject(0, object); err != nil {
|
|
||||||
return ofdgo.Box{}, err
|
|
||||||
}
|
|
||||||
reader, err := editor.Reader()
|
|
||||||
if err != nil {
|
|
||||||
return ofdgo.Box{}, err
|
|
||||||
}
|
|
||||||
defer reader.Close()
|
|
||||||
if _, err := reader.PageContentByIndex(0); err != nil {
|
|
||||||
return ofdgo.Box{}, err
|
|
||||||
}
|
|
||||||
bounds, err := ofdgo.NewRenderer(reader, ofdgo.WithRenderBackends(backends)).ObjectBounds(ofdgo.GraphicObject{Type: "CompositeObject", CompositeGraphicUnit: ofdgo.CompositeGraphicUnit{
|
|
||||||
Boundary: object.TextObject.Boundary, Objects: []ofdgo.GraphicObject{object},
|
|
||||||
}}, "")
|
|
||||||
if err != nil {
|
|
||||||
return ofdgo.Box{}, err
|
|
||||||
}
|
|
||||||
if bounds.W <= 0 || bounds.H <= 0 {
|
|
||||||
return ofdgo.Box{}, fmt.Errorf("watermark has no visible content")
|
|
||||||
}
|
|
||||||
return bounds, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// layoutWatermark 按实际绘制边界居中水印或均匀平铺,选区按页面比例映射
|
|
||||||
// 入参: annotation 水印注解, bounds 绘制范围, options 输入选项, page 页面范围
|
|
||||||
func layoutWatermark(annotation *ofdgo.Annotation, bounds ofdgo.Box, options annotationOptions, page ofdgo.Box) {
|
|
||||||
base := annotation.Appearance.Objects[0]
|
|
||||||
area := ofdgo.Box{W: page.W, H: page.H}
|
|
||||||
if options.Area != nil {
|
|
||||||
area = ofdgo.Box{X: options.Area.X * page.W, Y: options.Area.Y * page.H, W: options.Area.W * page.W, H: options.Area.H * page.H}
|
|
||||||
}
|
|
||||||
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 := ofdgo.NewMatrix(fmt.Sprintf("%g 0 0 %g 0 0", scale, scale))
|
|
||||||
if base.Type == "TextObject" {
|
|
||||||
base.TextObject.CTM = matrix.Multiply(ofdgo.NewMatrix(base.TextObject.CTM)).String()
|
|
||||||
} else {
|
|
||||||
base.ImageObject.CTM = matrix.Multiply(ofdgo.NewMatrix(base.ImageObject.CTM)).String()
|
|
||||||
}
|
|
||||||
bounds = matrix.TransformBox(bounds)
|
|
||||||
}
|
|
||||||
columns, rows := 1, 1
|
|
||||||
if options.Tile {
|
|
||||||
angle := options.Angle * math.Pi / 180
|
|
||||||
width := math.Max(bounds.W, (math.Abs(math.Cos(angle))*options.Width+math.Abs(math.Sin(angle))*options.Height)*scale)
|
|
||||||
height := math.Max(bounds.H, (math.Abs(math.Sin(angle))*options.Width+math.Abs(math.Cos(angle))*options.Height)*scale)
|
|
||||||
columns = max(1, int((area.W+10)/(width+10)))
|
|
||||||
rows = max(1, int((area.H+10)/(height+10)))
|
|
||||||
} else {
|
|
||||||
area = ofdgo.Box{X: area.X + (area.W-bounds.W)/2, Y: area.Y + (area.H-bounds.H)/2, W: bounds.W, H: bounds.H}
|
|
||||||
}
|
|
||||||
annotation.Appearance.Boundary = fmt.Sprintf("%g %g %g %g", area.X, area.Y, area.W, area.H)
|
|
||||||
annotation.Appearance.Objects = make([]ofdgo.GraphicObject, 0, columns*rows)
|
|
||||||
for row := range rows {
|
|
||||||
for column := range columns {
|
|
||||||
x := (float64(column)+0.5)*area.W/float64(columns) - bounds.X - bounds.W/2
|
|
||||||
y := (float64(row)+0.5)*area.H/float64(rows) - bounds.Y - bounds.H/2
|
|
||||||
object := base
|
|
||||||
boundary := fmt.Sprintf("%g %g %g %g", x, y, options.Width, options.Height)
|
|
||||||
if object.Type == "TextObject" {
|
|
||||||
object.TextObject.Boundary = boundary
|
|
||||||
} else {
|
|
||||||
object.ImageObject.Boundary = boundary
|
|
||||||
}
|
|
||||||
annotation.Appearance.Objects = append(annotation.Appearance.Objects, object)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// insertInk 将一笔采样写为一个标准矢量对象,复用内部插入与撤销逻辑
|
// insertInk 将一笔采样写为一个标准矢量对象,复用内部插入与撤销逻辑
|
||||||
// 入参: args 页面、JSON采样、线宽、压感开关、颜色及可选父路径
|
// 入参: args 页面、JSON采样、线宽、压感开关、颜色及可选父路径
|
||||||
// 返回: any 文档和选区, error 错误信息
|
// 返回: any 文档和选区, error 错误信息
|
||||||
@@ -4473,6 +4372,9 @@ func saveDocument(args []js.Value) (any, error) {
|
|||||||
// 入参: editor 待写出文档, args 写出回调、进度回调及可选页面索引、压缩配置、取消检查
|
// 入参: editor 待写出文档, args 写出回调、进度回调及可选页面索引、压缩配置、取消检查
|
||||||
// 返回: any 保存结果, error 错误信息
|
// 返回: any 保存结果, error 错误信息
|
||||||
func saveEditor(editor *ofdgo.Editor, args []js.Value) (any, error) {
|
func saveEditor(editor *ofdgo.Editor, args []js.Value) (any, error) {
|
||||||
|
if len(args) == 0 || args[0].Type() != js.TypeFunction {
|
||||||
|
return nil, fmt.Errorf("missing output callback")
|
||||||
|
}
|
||||||
if len(args) > 1 {
|
if len(args) > 1 {
|
||||||
editor.OnWriteProgress = editorOperationProgress(args[1])
|
editor.OnWriteProgress = editorOperationProgress(args[1])
|
||||||
defer func() { editor.OnWriteProgress = nil }()
|
defer func() { editor.OnWriteProgress = nil }()
|
||||||
|
|||||||
@@ -78,8 +78,11 @@ func (e *Editor) compressResourceParts(parts map[string][]byte, reader *Reader,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resource, safe := refs.scan(input, name)
|
resource, safe := refs.scan(imageInput{ReadCloser: input, context: e.output.ctx}, name)
|
||||||
input.Close()
|
input.Close()
|
||||||
|
if err := e.output.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if !safe {
|
if !safe {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,8 +37,13 @@ func (e *Editor) Transaction(edit func(*Editor) error) error {
|
|||||||
next := e.transactionSnapshot()
|
next := e.transactionSnapshot()
|
||||||
next.history, next.historyIndex, next.historyLimit = nil, 0, 0
|
next.history, next.historyIndex, next.historyLimit = nil, 0, 0
|
||||||
*e = next
|
*e = next
|
||||||
|
committed := false
|
||||||
|
defer func() {
|
||||||
|
if !committed {
|
||||||
|
*e = before
|
||||||
|
}
|
||||||
|
}()
|
||||||
if err := edit(e); err != nil {
|
if err := edit(e); err != nil {
|
||||||
*e = before
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
changed := e.revision != before.revision
|
changed := e.revision != before.revision
|
||||||
@@ -54,6 +59,7 @@ func (e *Editor) Transaction(edit func(*Editor) error) error {
|
|||||||
change.redo = func(e *Editor) { e.restoreTransaction(after) }
|
change.redo = func(e *Editor) { e.restoreTransaction(after) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
committed = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
// 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
|
||||||
|
}
|
||||||
|
} else if object.Type == "ImageObject" {
|
||||||
|
*ctm = (Matrix{a: box.W, d: box.H}).String()
|
||||||
|
}
|
||||||
|
*ctm = rotation.Multiply(NewMatrix(*ctm)).String()
|
||||||
|
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()
|
||||||
|
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}
|
||||||
|
if base.Type == "TextObject" {
|
||||||
|
base.TextObject.CTM = matrix.Multiply(NewMatrix(base.TextObject.CTM)).String()
|
||||||
|
} else {
|
||||||
|
base.ImageObject.CTM = matrix.Multiply(NewMatrix(base.ImageObject.CTM)).String()
|
||||||
|
}
|
||||||
|
bounds = matrix.TransformBox(bounds)
|
||||||
|
}
|
||||||
|
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: box.W, H: box.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
|
||||||
|
}
|
||||||
@@ -40,7 +40,7 @@ type imageOutput struct {
|
|||||||
writer io.Writer
|
writer io.Writer
|
||||||
}
|
}
|
||||||
|
|
||||||
// imageInput 在读取原始图片时检查调用方取消状态
|
// imageInput 在读取资源数据时检查调用方取消状态
|
||||||
type imageInput struct {
|
type imageInput struct {
|
||||||
io.ReadCloser
|
io.ReadCloser
|
||||||
context context.Context
|
context context.Context
|
||||||
|
|||||||
Reference in new issue
Block a user