feat(编辑能力): 完善已有文档编辑基础,修复跨页资源复用问题
build.yaml / build (push) Successful in 1m30s

This commit is contained in:
xiaoqidun committed 2026-09-16 11:25:38 +08:00
1 parent ce0966d5e2
commit 7f361a79cf
16 files changed
+1980 -213

No files matched your search

+36
View File
@@ -149,5 +149,41 @@ func main() {
} }
``` ```
# 编辑文档
```go
package main
import (
"log"
"os"
"github.com/xiaoqidun/ofdgo"
)
func main() {
// 1. 打开文档
reader, err := ofdgo.Open("test.ofd")
if err != nil {
log.Fatal(err)
}
defer reader.Close()
// 2. 编辑标题
editor, err := reader.Editor()
if err != nil {
log.Fatal(err)
}
editor.Info.Title = "编辑后的"
// 3. 另存文档
ofdFile, err := os.Create("edited.ofd")
if err != nil {
log.Fatal(err)
}
defer ofdFile.Close()
if _, err := editor.WriteTo(ofdFile); err != nil {
log.Fatal(err)
}
}
```
# 授权协议 # 授权协议
本项目使用 [Apache License 2.0](https://github.com/xiaoqidun/ofdgo/blob/main/LICENSE) 授权协议 本项目使用 [Apache License 2.0](https://github.com/xiaoqidun/ofdgo/blob/main/LICENSE) 授权协议
+4
View File
@@ -1071,6 +1071,10 @@ button.outline-link:hover {
outline-offset: -2px; outline-offset: -2px;
} }
.viewer-panel.pointer-focus:focus {
outline: none;
}
.viewer-panel.pan-mode, .viewer-panel.pan-mode,
.pan-mode .text-run > span { .pan-mode .text-run > span {
cursor: grab; cursor: grab;
+2
View File
@@ -488,6 +488,8 @@ el.searchInput.addEventListener("input", () => resetSearch(false));
el.searchPrev.addEventListener("click", () => selectSearchMatch(Math.max(0, state.searchIndex) - 1)); el.searchPrev.addEventListener("click", () => selectSearchMatch(Math.max(0, state.searchIndex) - 1));
el.searchNext.addEventListener("click", () => selectSearchMatch(state.searchIndex + 1)); el.searchNext.addEventListener("click", () => selectSearchMatch(state.searchIndex + 1));
document.addEventListener("keydown", handleKeyDown); document.addEventListener("keydown", handleKeyDown);
document.addEventListener("keydown", () => el.viewerPanel.classList.remove("pointer-focus"), true);
document.addEventListener("pointerdown", () => el.viewerPanel.classList.add("pointer-focus"), true);
el.fontAddButton.addEventListener("click", () => openFontFile(el.fontInput)); el.fontAddButton.addEventListener("click", () => openFontFile(el.fontInput));
el.fontDirectoryButton.addEventListener("click", () => openFontFile(el.fontDirectoryInput)); el.fontDirectoryButton.addEventListener("click", () => openFontFile(el.fontDirectoryInput));
el.localFontButton.addEventListener("click", loadLocalFonts); el.localFontButton.addEventListener("click", loadLocalFonts);
+5
View File
@@ -12,24 +12,29 @@ require (
) )
require ( require (
codeberg.org/go-latex/latex v0.3.0 // indirect
codeberg.org/go-pdf/fpdf v0.12.0 // indirect codeberg.org/go-pdf/fpdf v0.12.0 // indirect
github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 // indirect github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 // indirect
github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 // indirect github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 // indirect
github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc // indirect github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc // indirect
github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 // indirect github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 // indirect
github.com/ByteArena/poly2tri-go v0.0.0-20170716161910-d102ad91854f // indirect github.com/ByteArena/poly2tri-go v0.0.0-20170716161910-d102ad91854f // indirect
github.com/Kagami/go-avif v0.1.0 // indirect
github.com/andybalholm/brotli v1.2.2 // indirect github.com/andybalholm/brotli v1.2.2 // indirect
github.com/benoitkugler/textlayout v0.3.2 // indirect github.com/benoitkugler/textlayout v0.3.2 // indirect
github.com/benoitkugler/textprocessing v0.0.6 // indirect github.com/benoitkugler/textprocessing v0.0.6 // indirect
github.com/go-fonts/latin-modern v0.3.3 // indirect github.com/go-fonts/latin-modern v0.3.3 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/kolesa-team/go-webp v1.0.5 // indirect
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
github.com/srwiley/scanx v0.0.0-20190309010443-e94503791388 // indirect github.com/srwiley/scanx v0.0.0-20190309010443-e94503791388 // indirect
github.com/tdewolff/minify/v2 v2.24.17 // indirect github.com/tdewolff/minify/v2 v2.24.17 // indirect
github.com/tdewolff/parse/v2 v2.8.16 // indirect github.com/tdewolff/parse/v2 v2.8.16 // indirect
github.com/wcharczuk/go-chart/v2 v2.1.2 // indirect
github.com/yuin/goldmark v1.8.5 // indirect github.com/yuin/goldmark v1.8.5 // indirect
golang.org/x/net v0.58.0 // indirect golang.org/x/net v0.58.0 // indirect
golang.org/x/sys v0.48.0 // indirect golang.org/x/sys v0.48.0 // indirect
gonum.org/v1/plot v0.17.0 // indirect
modernc.org/knuth v0.6.0 // indirect modernc.org/knuth v0.6.0 // indirect
modernc.org/token v1.1.0 // indirect modernc.org/token v1.1.0 // indirect
star-tex.org/x/tex v0.7.1 // indirect star-tex.org/x/tex v0.7.1 // indirect
+74
View File
@@ -1,3 +1,5 @@
codeberg.org/go-fonts/dejavu v0.4.0 h1:2yn58Vkh4CFK3ipacWUAIE3XVBGNa0y1bc95Bmfx91I=
codeberg.org/go-fonts/dejavu v0.4.0/go.mod h1:abni088lmhQJvso2Lsb7azCKzwkfcnttl6tL1UTWKzg=
codeberg.org/go-fonts/liberation v0.6.0 h1:15Gh6SdwYve22CWCm9jYpVpRuaTh726av2TgHTHvAtQ= codeberg.org/go-fonts/liberation v0.6.0 h1:15Gh6SdwYve22CWCm9jYpVpRuaTh726av2TgHTHvAtQ=
codeberg.org/go-fonts/liberation v0.6.0/go.mod h1:J15VAa+lyxdcI/Je7lDDDl6QOhLk9feNBnnwXqEHXOk= codeberg.org/go-fonts/liberation v0.6.0/go.mod h1:J15VAa+lyxdcI/Je7lDDDl6QOhLk9feNBnnwXqEHXOk=
codeberg.org/go-latex/latex v0.3.0 h1:LKTaDHFbEC2PH1sh0sYv6PZ1pzs/g2aoeV1HItWj/bg= codeberg.org/go-latex/latex v0.3.0 h1:LKTaDHFbEC2PH1sh0sYv6PZ1pzs/g2aoeV1HItWj/bg=
@@ -18,6 +20,8 @@ github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 h1:O/r2Sj+8QcMF
github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046/go.mod h1:uw9h2sd4WWHOPdJ13MQpwK5qYWKYDumDqxWWIknEQ+k= github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046/go.mod h1:uw9h2sd4WWHOPdJ13MQpwK5qYWKYDumDqxWWIknEQ+k=
github.com/ByteArena/poly2tri-go v0.0.0-20170716161910-d102ad91854f h1:l7moT9o/v/9acCWA64Yz/HDLqjcRTvc0noQACi4MsJw= github.com/ByteArena/poly2tri-go v0.0.0-20170716161910-d102ad91854f h1:l7moT9o/v/9acCWA64Yz/HDLqjcRTvc0noQACi4MsJw=
github.com/ByteArena/poly2tri-go v0.0.0-20170716161910-d102ad91854f/go.mod h1:vIOkSdX3NDCPwgu8FIuTat2zDF0FPXXQ0RYFRy+oQic= github.com/ByteArena/poly2tri-go v0.0.0-20170716161910-d102ad91854f/go.mod h1:vIOkSdX3NDCPwgu8FIuTat2zDF0FPXXQ0RYFRy+oQic=
github.com/Kagami/go-avif v0.1.0 h1:8GHAGLxCdFfhpd4Zg8j1EqO7rtcQNenxIDerC/uu68w=
github.com/Kagami/go-avif v0.1.0/go.mod h1:OPmPqzNdQq3+sXm0HqaUJQ9W/4k+Elbc3RSfJUemDKA=
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw= github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw=
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
github.com/andybalholm/brotli v1.2.2 h1:HzTuoo2ErYQqf5qvcJInB8uvqSVxRttzkFexPWtnceM= github.com/andybalholm/brotli v1.2.2 h1:HzTuoo2ErYQqf5qvcJInB8uvqSVxRttzkFexPWtnceM=
@@ -28,6 +32,7 @@ github.com/benoitkugler/textlayout v0.3.2/go.mod h1:o+1hFV+JSHBC9qNLIuwVoLedERU7
github.com/benoitkugler/textlayout-testdata v0.1.1/go.mod h1:i/qZl09BbUOtd7Bu/W1CAubRwTWrEXWq6JwMkw8wYxo= github.com/benoitkugler/textlayout-testdata v0.1.1/go.mod h1:i/qZl09BbUOtd7Bu/W1CAubRwTWrEXWq6JwMkw8wYxo=
github.com/benoitkugler/textprocessing v0.0.6 h1:obkMyj62GEPg3xUVYqROlCN22z1OleuZm6ULqX9Om1g= github.com/benoitkugler/textprocessing v0.0.6 h1:obkMyj62GEPg3xUVYqROlCN22z1OleuZm6ULqX9Om1g=
github.com/benoitkugler/textprocessing v0.0.6/go.mod h1:Io0gN08/PXEzrSOWFa88xHx2Xv3VjvLMY7H76YoI23A= github.com/benoitkugler/textprocessing v0.0.6/go.mod h1:Io0gN08/PXEzrSOWFa88xHx2Xv3VjvLMY7H76YoI23A=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-fonts/latin-modern v0.3.3 h1:g2xNgI8yzdNzIVm+qvbMryB6yGPe0pSMss8QT3QwlJ0= github.com/go-fonts/latin-modern v0.3.3 h1:g2xNgI8yzdNzIVm+qvbMryB6yGPe0pSMss8QT3QwlJ0=
github.com/go-fonts/latin-modern v0.3.3/go.mod h1:tHaiWDGze4EPB0Go4cLT5M3QzRY3peya09Z/8KSCrpY= github.com/go-fonts/latin-modern v0.3.3/go.mod h1:tHaiWDGze4EPB0Go4cLT5M3QzRY3peya09Z/8KSCrpY=
github.com/go-text/typesetting v0.3.5 h1:XZPUooClHY0Vf/rFyUyuPRNEkawARaFzLMQcXLSEyPk= github.com/go-text/typesetting v0.3.5 h1:XZPUooClHY0Vf/rFyUyuPRNEkawARaFzLMQcXLSEyPk=
@@ -36,6 +41,10 @@ github.com/go-text/typesetting-utils v0.0.0-20260419141703-4ffe8874dabc h1:8FGo2
github.com/go-text/typesetting-utils v0.0.0-20260419141703-4ffe8874dabc/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o= github.com/go-text/typesetting-utils v0.0.0-20260419141703-4ffe8874dabc/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/kolesa-team/go-webp v1.0.5 h1:GZQHJBaE8dsNKZltfwqsL0qVJ7vqHXsfA+4AHrQW3pE=
github.com/kolesa-team/go-webp v1.0.5/go.mod h1:QmJu0YHXT3ex+4SgUvs+a+1SFCDcCqyZg+LbIuNNTnE=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE= github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
@@ -46,6 +55,10 @@ github.com/srwiley/scanFT v0.0.0-20220128184157-0d1ee492111f h1:uLR2GaV0kWYZ3Ns3
github.com/srwiley/scanFT v0.0.0-20220128184157-0d1ee492111f/go.mod h1:LZwgIPG9X6nH6j5Ef+xMFspl6Hru4b5EJxzMfeqHYJY= github.com/srwiley/scanFT v0.0.0-20220128184157-0d1ee492111f/go.mod h1:LZwgIPG9X6nH6j5Ef+xMFspl6Hru4b5EJxzMfeqHYJY=
github.com/srwiley/scanx v0.0.0-20190309010443-e94503791388 h1:ZdkidVdpLW13BQ9a+/3uerT2ezy9J7KQWH18JCfhDmI= github.com/srwiley/scanx v0.0.0-20190309010443-e94503791388 h1:ZdkidVdpLW13BQ9a+/3uerT2ezy9J7KQWH18JCfhDmI=
github.com/srwiley/scanx v0.0.0-20190309010443-e94503791388/go.mod h1:C/WY5lmWfMtPFYYBTd3Lzdn4FTLr+RxlIeiBNye+/os= github.com/srwiley/scanx v0.0.0-20190309010443-e94503791388/go.mod h1:C/WY5lmWfMtPFYYBTd3Lzdn4FTLr+RxlIeiBNye+/os=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/tdewolff/canvas v0.0.0-20260913163248-dd4999d1c76a h1:Qxn+Q+tIBhRGMkWXOXhhOmJMlj7FbSmdMabXyAOtBzI= github.com/tdewolff/canvas v0.0.0-20260913163248-dd4999d1c76a h1:Qxn+Q+tIBhRGMkWXOXhhOmJMlj7FbSmdMabXyAOtBzI=
github.com/tdewolff/canvas v0.0.0-20260913163248-dd4999d1c76a/go.mod h1:K9Ux3cwFtyAFRITZ46g7v+ORXgI3ENtQa5rL9yjNz5Y= github.com/tdewolff/canvas v0.0.0-20260913163248-dd4999d1c76a/go.mod h1:K9Ux3cwFtyAFRITZ46g7v+ORXgI3ENtQa5rL9yjNz5Y=
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6 h1:phNTWRYLLTlPpUgxkd6FDmWLlrOzpEpQAqq1ryLXYS8= github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6 h1:phNTWRYLLTlPpUgxkd6FDmWLlrOzpEpQAqq1ryLXYS8=
@@ -56,31 +69,92 @@ github.com/tdewolff/parse/v2 v2.8.16 h1:bLk5svUOQRkW/Y2SJ+DeENSIkZBcTIkq+Atyv5D8
github.com/tdewolff/parse/v2 v2.8.16/go.mod h1:XdsoSFThlVIRIajAuqz1evNY7bagZS8LBOPA3aVopwQ= github.com/tdewolff/parse/v2 v2.8.16/go.mod h1:XdsoSFThlVIRIajAuqz1evNY7bagZS8LBOPA3aVopwQ=
github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk= 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/wcharczuk/go-chart/v2 v2.1.2 h1:Y17/oYNuXwZg6TFag06qe8sBajwwsuvPiJJXcUcLL6E=
github.com/wcharczuk/go-chart/v2 v2.1.2/go.mod h1:Zi4hbaqlWpYajnXB2K22IUYVXRXaLfSGNNR7P4ukyyQ=
github.com/xiaoqidun/jbig2 v1.0.0 h1:Wt434JKS7j7LqHnMSL/3yatRCxjOfDiA+RhZA/DhAAo= github.com/xiaoqidun/jbig2 v1.0.0 h1:Wt434JKS7j7LqHnMSL/3yatRCxjOfDiA+RhZA/DhAAo=
github.com/xiaoqidun/jbig2 v1.0.0/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ= github.com/xiaoqidun/jbig2 v1.0.0/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ=
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.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA= github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
github.com/yuin/goldmark v1.8.5/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yuin/goldmark v1.8.5/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/image v0.0.0-20210504121937-7319ad40d33e/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20210504121937-7319ad40d33e/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ= golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ=
golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew= golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/plot v0.17.0 h1:d0DwPVBe9jnEGqQBoZGl/P2M9WciJbG2CnV59C9QBT4= gonum.org/v1/plot v0.17.0 h1:d0DwPVBe9jnEGqQBoZGl/P2M9WciJbG2CnV59C9QBT4=
gonum.org/v1/plot v0.17.0/go.mod h1:ipt2GUN1oqzr2O7wCjLDtw1ShfIYYNBp4o0O1Ez5B3Y= gonum.org/v1/plot v0.17.0/go.mod h1:ipt2GUN1oqzr2O7wCjLDtw1ShfIYYNBp4o0O1Ez5B3Y=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/knuth v0.6.0 h1:zLv0hzgdcpcCUB/iMwpQvn9m/iciqO/hyNXlpWymMKU= modernc.org/knuth v0.6.0 h1:zLv0hzgdcpcCUB/iMwpQvn9m/iciqO/hyNXlpWymMKU=
modernc.org/knuth v0.6.0/go.mod h1:ISAZV+URZIlE8h1BwNVg1NzpwHKndV5lQ13NPPj3tds= modernc.org/knuth v0.6.0/go.mod h1:ISAZV+URZIlE8h1BwNVg1NzpwHKndV5lQ13NPPj3tds=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+88 -44
View File
@@ -98,6 +98,7 @@ func RunWASM() {
registerExportCallback("ofdgoExportAttachment", exportAttachment) registerExportCallback("ofdgoExportAttachment", exportAttachment)
registerCallback("ofdgoMatchFontFiles", matchFontFiles) registerCallback("ofdgoMatchFontFiles", matchFontFiles)
registerCallback("ofdgoCreateDocument", createDocument) registerCallback("ofdgoCreateDocument", createDocument)
registerCallback("ofdgoEditDocument", editDocument)
registerCallback("ofdgoChangePage", changePage) registerCallback("ofdgoChangePage", changePage)
registerCallback("ofdgoInsertText", insertText) registerCallback("ofdgoInsertText", insertText)
registerCallback("ofdgoUpdateText", updateText) registerCallback("ofdgoUpdateText", updateText)
@@ -530,10 +531,10 @@ func encodeResult(result apiResult) string {
return string(data) return string(data)
} }
// currentEditor 当前新建文档 // currentEditor 当前编辑文档
var currentEditor *ofdgo.Editor var currentEditor *ofdgo.Editor
// editorClipboard 当前创作文档中的对象快照与剪贴板标识。 // editorClipboard 当前编辑文档中的对象快照与剪贴板标识。
type editorClipboard struct { type editorClipboard struct {
token string token string
objects []ofdgo.GraphicObject objects []ofdgo.GraphicObject
@@ -542,12 +543,14 @@ type editorClipboard struct {
// copiedObjects 当前对象剪贴板,不保存字体或图片的重复数据。 // copiedObjects 当前对象剪贴板,不保存字体或图片的重复数据。
var copiedObjects *editorClipboard var copiedObjects *editorClipboard
// editorInfo 创作文档信息与操作状态 // editorInfo 编辑文档信息与操作状态
type editorInfo struct { type editorInfo struct {
DocumentInfo DocumentInfo
Revision uint64 `json:"revision"` Revision uint64 `json:"revision"`
CanUndo bool `json:"canUndo"` CanUndo bool `json:"canUndo"`
CanRedo bool `json:"canRedo"` CanRedo bool `json:"canRedo"`
PageCapabilities []map[string]bool `json:"pageCapabilities"`
EditWarnings []string `json:"editWarnings,omitempty"`
} }
// editorPageInfo 页面操作结果与目标页面 // editorPageInfo 页面操作结果与目标页面
@@ -562,10 +565,21 @@ type editorSelectionInfo struct {
SelectedIDs []string `json:"selectedIDs"` SelectedIDs []string `json:"selectedIDs"`
} }
// editorSummary 获取当前创作文档状态 // editorSummary 获取当前编辑文档状态
// 返回: editorInfo 文档信息 // 返回: editorInfo 文档信息
func editorSummary() editorInfo { func editorSummary() editorInfo {
return editorInfo{currentSession.Summary(), currentEditor.Revision(), currentEditor.CanUndo(), currentEditor.CanRedo()} info := editorInfo{DocumentInfo: currentSession.Summary(), Revision: currentEditor.Revision(), CanUndo: currentEditor.CanUndo(), CanRedo: currentEditor.CanRedo()}
if !currentSession.doc.Permissions.Edit {
info.EditWarnings = append(info.EditWarnings, "原文件声明不允许编辑")
}
if currentSession.doc.Signatures != "" {
info.EditWarnings = append(info.EditWarnings, "修改文档可能使原签名失效")
}
for i := 0; i < currentEditor.PageCount(); i++ {
capability, _ := currentEditor.PageCapabilities(i)
info.PageCapabilities = append(info.PageCapabilities, map[string]bool{"insert": capability.Insert, "copy": capability.Copy, "delete": capability.Delete, "move": capability.Move, "resize": capability.Resize})
}
return info
} }
// restoreEditor 撤销或重做并更新预览 // restoreEditor 撤销或重做并更新预览
@@ -573,7 +587,7 @@ func editorSummary() editorInfo {
// 返回: any 文档信息, error 错误信息 // 返回: any 文档信息, error 错误信息
func restoreEditor(redo bool) (any, error) { func restoreEditor(redo bool) (any, error) {
if currentEditor == nil { if currentEditor == nil {
return nil, fmt.Errorf("no document is being created") return nil, fmt.Errorf("no document is being edited")
} }
apply := currentEditor.Undo apply := currentEditor.Undo
if redo { if redo {
@@ -585,7 +599,7 @@ func restoreEditor(redo bool) (any, error) {
return previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations) return previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations)
} }
// editorObjects 提供创作画布的对象范围、文字排版及原始图片边界 // editorObjects 提供编辑画布的对象范围、文字排版及原始图片边界
// 入参: index 页面索引, text 页面文字 // 入参: index 页面索引, text 页面文字
// 返回: []any 对象区域, error 错误信息 // 返回: []any 对象区域, error 错误信息
func editorObjects(index int, text *ofdgo.PageText) ([]any, error) { func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
@@ -623,28 +637,38 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
for _, layer := range page.Content.Layer { for _, layer := range page.Content.Layer {
for order, object := range layer.Objects { for order, object := range layer.Objects {
var id string var id string
var box ofdgo.Box
switch object.Type { switch object.Type {
case "TextObject": case "TextObject":
id = object.TextObject.ID id = object.TextObject.ID
box = textBoxes[id]
case "ImageObject": case "ImageObject":
id = object.ImageObject.ID id = object.ImageObject.ID
box, err = ofdgo.ParseBox(object.ImageObject.Boundary)
if err != nil {
return nil, err
}
case "PathObject": case "PathObject":
id = object.PathObject.ID id = object.PathObject.ID
box, err = ofdgo.ParseBox(object.PathObject.Boundary)
if err != nil {
return nil, err
}
default: default:
continue continue
} }
capability, err := currentEditor.ObjectCapabilities(index, id)
if err != nil {
return nil, err
}
if !capability.Transform && !capability.Update {
continue
}
var box ofdgo.Box
switch object.Type {
case "TextObject":
box = textBoxes[id]
case "ImageObject":
box, err = ofdgo.ParseBox(object.ImageObject.Boundary)
case "PathObject":
box, err = ofdgo.ParseBox(object.PathObject.Boundary)
}
if err != nil {
return nil, err
}
if box.W > 0 && box.H > 0 { if box.W > 0 && box.H > 0 {
item := map[string]any{"id": id, "type": object.Type, "x": box.X, "y": box.Y, "width": box.W, "height": box.H, "order": order, "count": len(layer.Objects)} item := map[string]any{"id": id, "type": object.Type, "x": box.X, "y": box.Y, "width": box.W, "height": box.H, "order": order, "count": len(layer.Objects), "layer": layer.ID,
"capabilities": map[string]any{"update": capability.Update, "reflow": capability.Reflow, "layoutKnown": capability.LayoutKnown, "transform": capability.Transform, "arrange": capability.Arrange, "copy": capability.Copy, "delete": capability.Delete, "order": capability.Order, "reason": capability.Reason}}
if object.Type == "ImageObject" { if object.Type == "ImageObject" {
full, err := object.ImageObject.ImageBounds() full, err := object.ImageObject.ImageBounds()
if err != nil { if err != nil {
@@ -652,7 +676,7 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
} }
item["imageBounds"] = editorBox(full) item["imageBounds"] = editorBox(full)
} }
if object.Type == "PathObject" { if object.Type == "PathObject" && capability.Update {
path := object.PathObject path := object.PathObject
kind, geometry := path.Shape() kind, geometry := path.Shape()
if kind != "" { if kind != "" {
@@ -673,6 +697,7 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
item["text"], item["font"], item["fontName"], item["size"] = value, object.TextObject.Font, fontNames[object.TextObject.Font], object.TextObject.Size item["text"], item["font"], item["fontName"], item["size"] = value, object.TextObject.Font, fontNames[object.TextObject.Font], object.TextObject.Size
item["wrap"], item["align"], item["paragraphHeight"] = layout.Wrap, layout.Align, layout.LineHeight item["wrap"], item["align"], item["paragraphHeight"] = layout.Wrap, layout.Align, layout.LineHeight
item["letterSpacing"] = layout.LetterSpacing item["letterSpacing"] = layout.LetterSpacing
if capability.Reflow {
frame, err := object.TextObject.TextFrame() frame, err := object.TextObject.TextFrame()
if err != nil { if err != nil {
return nil, err return nil, err
@@ -691,19 +716,16 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
bounds := matrix.TransformBox(frame) bounds := matrix.TransformBox(frame)
item["x"], item["y"], item["width"], item["height"] = bounds.X, bounds.Y, bounds.W, bounds.H item["x"], item["y"], item["width"], item["height"] = bounds.X, bounds.Y, bounds.W, bounds.H
} }
}
if codes := object.TextObject.TextCode; len(codes) > 1 { if codes := object.TextObject.TextCode; len(codes) > 1 {
first, _ := strconv.ParseFloat(codes[0].Y, 64) first, _ := strconv.ParseFloat(codes[0].Y, 64)
second, _ := strconv.ParseFloat(codes[1].Y, 64) second, _ := strconv.ParseFloat(codes[1].Y, 64)
item["lineHeight"] = second - first item["lineHeight"] = second - first
} }
var r, g, b float64 if capability.Update {
if fill := object.TextObject.FillColor; fill != nil { item["color"] = editorColorHex(object.TextObject.FillColor)
if _, err := fmt.Sscan(fill.Value, &r, &g, &b); err != nil {
return nil, err
} }
} }
item["color"] = fmt.Sprintf("#%02x%02x%02x", byte(r), byte(g), byte(b))
}
objects = append(objects, item) objects = append(objects, item)
} }
} }
@@ -716,7 +738,7 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
// 返回: any 字体数据, error 错误信息 // 返回: any 字体数据, error 错误信息
func editorFont(args []js.Value) (any, error) { func editorFont(args []js.Value) (any, error) {
if currentEditor == nil { if currentEditor == nil {
return nil, fmt.Errorf("no document is being created") return nil, fmt.Errorf("no document is being edited")
} }
fonts, err := currentSession.Reader.Fonts() fonts, err := currentSession.Reader.Fonts()
if err != nil { if err != nil {
@@ -739,7 +761,7 @@ func editorFont(args []js.Value) (any, error) {
// 返回: any 文档信息, error 错误信息 // 返回: any 文档信息, error 错误信息
func replaceImage(args []js.Value) (any, error) { func replaceImage(args []js.Value) (any, error) {
if currentEditor == nil { if currentEditor == nil {
return nil, fmt.Errorf("no document is being created") return nil, fmt.Errorf("no document is being edited")
} }
data, err := bytesFromJS(args[2]) data, err := bytesFromJS(args[2])
if err != nil { if err != nil {
@@ -760,7 +782,7 @@ func replaceImage(args []js.Value) (any, error) {
// 返回: any 文档信息, error 错误信息 // 返回: any 文档信息, error 错误信息
func alignObject(args []js.Value) (any, error) { func alignObject(args []js.Value) (any, error) {
if currentEditor == nil { if currentEditor == nil {
return nil, fmt.Errorf("no document is being created") return nil, fmt.Errorf("no document is being edited")
} }
revision := currentEditor.Revision() revision := currentEditor.Revision()
if err := currentEditor.AlignObject(args[0].Int(), args[1].String(), args[2].String()); err != nil { if err := currentEditor.AlignObject(args[0].Int(), args[1].String(), args[2].String()); err != nil {
@@ -777,7 +799,7 @@ func alignObject(args []js.Value) (any, error) {
// 返回: any 文档信息, error 错误信息 // 返回: any 文档信息, error 错误信息
func transformObject(args []js.Value) (any, error) { func transformObject(args []js.Value) (any, error) {
if currentEditor == nil { if currentEditor == nil {
return nil, fmt.Errorf("no document is being created") return nil, fmt.Errorf("no document is being edited")
} }
page, id := args[0].Int(), args[1].String() page, id := args[0].Int(), args[1].String()
revision := currentEditor.Revision() revision := currentEditor.Revision()
@@ -795,7 +817,7 @@ func transformObject(args []js.Value) (any, error) {
// 返回: any 文档信息, error 错误信息 // 返回: any 文档信息, error 错误信息
func reshapeObject(args []js.Value) (any, error) { func reshapeObject(args []js.Value) (any, error) {
if currentEditor == nil { if currentEditor == nil {
return nil, fmt.Errorf("no document is being created") return nil, fmt.Errorf("no document is being edited")
} }
page, id := args[0].Int(), args[1].String() page, id := args[0].Int(), args[1].String()
object, err := currentEditor.Object(page, id) object, err := currentEditor.Object(page, id)
@@ -824,7 +846,7 @@ func reshapeObject(args []js.Value) (any, error) {
// 返回: any 文档信息, error 错误信息 // 返回: any 文档信息, error 错误信息
func copyObjects(args []js.Value) (any, error) { func copyObjects(args []js.Value) (any, error) {
if currentEditor == nil { if currentEditor == nil {
return nil, fmt.Errorf("no document is being created") return nil, fmt.Errorf("no document is being edited")
} }
objects, err := currentEditor.Objects(args[0].Int(), stringsFromJS(args[1])) objects, err := currentEditor.Objects(args[0].Int(), stringsFromJS(args[1]))
if err != nil { if err != nil {
@@ -838,7 +860,7 @@ func copyObjects(args []js.Value) (any, error) {
// 返回: any 空结果, error 错误信息 // 返回: any 空结果, error 错误信息
func captureObjects(args []js.Value) (any, error) { func captureObjects(args []js.Value) (any, error) {
if currentEditor == nil { if currentEditor == nil {
return nil, fmt.Errorf("no document is being created") return nil, fmt.Errorf("no document is being edited")
} }
objects, err := currentEditor.Objects(args[0].Int(), stringsFromJS(args[1])) objects, err := currentEditor.Objects(args[0].Int(), stringsFromJS(args[1]))
if err != nil { if err != nil {
@@ -896,7 +918,7 @@ func distributeObjects(args []js.Value) (any, error) {
// 返回: any 文档信息, error 错误信息 // 返回: any 文档信息, error 错误信息
func deleteObject(args []js.Value) (any, error) { func deleteObject(args []js.Value) (any, error) {
if currentEditor == nil { if currentEditor == nil {
return nil, fmt.Errorf("no document is being created") return nil, fmt.Errorf("no document is being edited")
} }
if err := currentEditor.DeleteObject(args[0].Int(), args[1].String()); err != nil { if err := currentEditor.DeleteObject(args[0].Int(), args[1].String()); err != nil {
return nil, err return nil, err
@@ -909,7 +931,7 @@ func deleteObject(args []js.Value) (any, error) {
// 返回: any 文档信息和目标页面, error 错误信息 // 返回: any 文档信息和目标页面, error 错误信息
func changePage(args []js.Value) (any, error) { func changePage(args []js.Value) (any, error) {
if currentEditor == nil { if currentEditor == nil {
return nil, fmt.Errorf("no document is being created") return nil, fmt.Errorf("no document is being edited")
} }
index := args[1].Int() index := args[1].Int()
revision := currentEditor.Revision() revision := currentEditor.Revision()
@@ -960,8 +982,26 @@ func createDocument(args []js.Value) (any, error) {
return previewEditor(editor, args[3].Bool()) return previewEditor(editor, args[3].Bool())
} }
// editDocument 将已打开文档接入编辑器,沿用页面、资源及字体配置。
// 入参: args 浏览器参数
// 返回: any 编辑状态, error 错误信息
func editDocument(args []js.Value) (any, error) {
if currentSession == nil {
return nil, fmt.Errorf("ofd document is not opened")
}
if currentEditor != nil {
return editorSummary(), nil
}
editor, err := currentSession.Reader.Editor()
if err != nil {
return nil, err
}
editor.SetHistoryLimit(100)
return previewEditor(editor, currentSession.Renderer.RenderAnnotations)
}
// previewEditor 通过内存快照更新预览,不生成中间压缩包 // previewEditor 通过内存快照更新预览,不生成中间压缩包
// 入参: editor 新建文档, annotations 是否显示注解 // 入参: editor 编辑文档, annotations 是否显示注解
// 返回: editorInfo 文档信息, error 错误信息 // 返回: editorInfo 文档信息, error 错误信息
func previewEditor(editor *ofdgo.Editor, annotations bool) (editorInfo, error) { func previewEditor(editor *ofdgo.Editor, annotations bool) (editorInfo, error) {
reader, err := editor.Reader() reader, err := editor.Reader()
@@ -973,6 +1013,10 @@ func previewEditor(editor *ofdgo.Editor, annotations bool) (editorInfo, error) {
return editorInfo{}, err return editorInfo{}, err
} }
if currentSession != nil { if currentSession != nil {
if currentSession.fontFS != nil {
session.fontFS = currentSession.fontFS
session.Renderer.SetFontFS(session.fontFS)
}
_ = currentSession.Close() _ = currentSession.Close()
} }
currentSession = session currentSession = session
@@ -988,7 +1032,7 @@ func previewEditor(editor *ofdgo.Editor, annotations bool) (editorInfo, error) {
// 返回: any 文档信息, error 错误信息 // 返回: any 文档信息, error 错误信息
func updateText(args []js.Value) (any, error) { func updateText(args []js.Value) (any, error) {
if currentEditor == nil { if currentEditor == nil {
return nil, fmt.Errorf("no document is being created") return nil, fmt.Errorf("no document is being edited")
} }
page, id := args[0].Int(), args[1].String() page, id := args[0].Int(), args[1].String()
object, err := currentEditor.Object(page, id) object, err := currentEditor.Object(page, id)
@@ -1112,7 +1156,7 @@ func setPathStyle(object *ofdgo.PathObject, args []js.Value) error {
// 返回: any 文档信息, error 错误信息 // 返回: any 文档信息, error 错误信息
func insertShape(args []js.Value) (any, error) { func insertShape(args []js.Value) (any, error) {
if currentEditor == nil { if currentEditor == nil {
return nil, fmt.Errorf("no document is being created") return nil, fmt.Errorf("no document is being edited")
} }
path, err := ofdgo.NewShape(ofdgo.ShapeKind(args[1].String()), ofdgo.Box{X: args[2].Float(), Y: args[3].Float(), W: args[4].Float(), H: args[5].Float()}) path, err := ofdgo.NewShape(ofdgo.ShapeKind(args[1].String()), ofdgo.Box{X: args[2].Float(), Y: args[3].Float(), W: args[4].Float(), H: args[5].Float()})
if err != nil { if err != nil {
@@ -1132,7 +1176,7 @@ func insertShape(args []js.Value) (any, error) {
// 返回: any 文档信息, error 错误信息 // 返回: any 文档信息, error 错误信息
func updatePathStyle(args []js.Value) (any, error) { func updatePathStyle(args []js.Value) (any, error) {
if currentEditor == nil { if currentEditor == nil {
return nil, fmt.Errorf("no document is being created") return nil, fmt.Errorf("no document is being edited")
} }
page, id := args[0].Int(), args[1].String() page, id := args[0].Int(), args[1].String()
object, err := currentEditor.Object(page, id) object, err := currentEditor.Object(page, id)
@@ -1160,7 +1204,7 @@ func updatePathStyle(args []js.Value) (any, error) {
// 返回: any 文档信息, error 错误信息 // 返回: any 文档信息, error 错误信息
func insertText(args []js.Value) (any, error) { func insertText(args []js.Value) (any, error) {
if currentEditor == nil { if currentEditor == nil {
return nil, fmt.Errorf("no document is being created") return nil, fmt.Errorf("no document is being edited")
} }
data, err := bytesFromJS(args[2]) data, err := bytesFromJS(args[2])
if err != nil { if err != nil {
@@ -1206,7 +1250,7 @@ func insertText(args []js.Value) (any, error) {
// 返回: any 文档信息, error 错误信息 // 返回: any 文档信息, error 错误信息
func layoutText(args []js.Value) (any, error) { func layoutText(args []js.Value) (any, error) {
if currentEditor == nil { if currentEditor == nil {
return nil, fmt.Errorf("no document is being created") return nil, fmt.Errorf("no document is being edited")
} }
page, id := args[0].Int(), args[1].String() page, id := args[0].Int(), args[1].String()
object, err := currentEditor.Object(page, id) object, err := currentEditor.Object(page, id)
@@ -1294,7 +1338,7 @@ func fitImage(args []js.Value) (any, error) {
// 返回: any 页面SVG和分离的图片资源, error 错误信息 // 返回: any 页面SVG和分离的图片资源, error 错误信息
func previewImage(args []js.Value) (any, error) { func previewImage(args []js.Value) (any, error) {
if currentEditor == nil { if currentEditor == nil {
return nil, fmt.Errorf("no document is being created") return nil, fmt.Errorf("no document is being edited")
} }
page, err := currentEditor.Page(args[0].Int()) page, err := currentEditor.Page(args[0].Int())
if err != nil { if err != nil {
@@ -1365,7 +1409,7 @@ func deleteObjects(args []js.Value) (any, error) {
// 返回: any 文档信息, error 错误信息 // 返回: any 文档信息, error 错误信息
func changeObjects(apply func() error) (any, error) { func changeObjects(apply func() error) (any, error) {
if currentEditor == nil { if currentEditor == nil {
return nil, fmt.Errorf("no document is being created") return nil, fmt.Errorf("no document is being edited")
} }
revision := currentEditor.Revision() revision := currentEditor.Revision()
if err := apply(); err != nil { if err := apply(); err != nil {
@@ -1382,7 +1426,7 @@ func changeObjects(apply func() error) (any, error) {
// 返回: any 文档信息, error 错误信息 // 返回: any 文档信息, error 错误信息
func insertImage(args []js.Value) (any, error) { func insertImage(args []js.Value) (any, error) {
if currentEditor == nil { if currentEditor == nil {
return nil, fmt.Errorf("no document is being created") return nil, fmt.Errorf("no document is being edited")
} }
data, err := bytesFromJS(args[1]) data, err := bytesFromJS(args[1])
if err != nil { if err != nil {
@@ -1424,7 +1468,7 @@ func insertImage(args []js.Value) (any, error) {
// 返回: any 保存结果, error 错误信息 // 返回: any 保存结果, error 错误信息
func saveDocument(args []js.Value) (any, error) { func saveDocument(args []js.Value) (any, error) {
if currentEditor == nil { if currentEditor == nil {
return nil, fmt.Errorf("no document is being created") return nil, fmt.Errorf("no document is being edited")
} }
writer := bufio.NewWriterSize(exportWriter{write: args[0]}, 1<<20) writer := bufio.NewWriterSize(exportWriter{write: args[0]}, 1<<20)
if _, err := currentEditor.WriteTo(writer); err != nil { if _, err := currentEditor.WriteTo(writer); err != nil {
+6 -1
View File
@@ -100,6 +100,7 @@ type SignatureStampInfo struct {
type Session struct { type Session struct {
Reader *ofdgo.Reader Reader *ofdgo.Reader
Renderer *ofdgo.Renderer Renderer *ofdgo.Renderer
fontFS *ofdgo.FontFS
doc *ofdgo.Document doc *ofdgo.Document
pageCache map[int]*ofdgo.PageContent pageCache map[int]*ofdgo.PageContent
boxCache map[int]ofdgo.Box boxCache map[int]ofdgo.Box
@@ -232,8 +233,9 @@ func newSession(reader *ofdgo.Reader, opts OpenOptions) (*Session, error) {
return nil, err return nil, err
} }
var rendererOptions []ofdgo.RendererOption var rendererOptions []ofdgo.RendererOption
var fontFS *ofdgo.FontFS
if len(opts.Fonts) > 0 { if len(opts.Fonts) > 0 {
fontFS := ofdgo.NewFontFS(opts.Fonts) fontFS = ofdgo.NewFontFS(opts.Fonts)
if fontFS.Len() == 0 { if fontFS.Len() == 0 {
reader.Close() reader.Close()
return nil, fmt.Errorf("invalid font file") return nil, fmt.Errorf("invalid font file")
@@ -244,6 +246,7 @@ func newSession(reader *ofdgo.Reader, opts OpenOptions) (*Session, error) {
return &Session{ return &Session{
Reader: reader, Reader: reader,
Renderer: ofdgo.NewRenderer(reader, rendererOptions...), Renderer: ofdgo.NewRenderer(reader, rendererOptions...),
fontFS: fontFS,
doc: doc, doc: doc,
pageCache: make(map[int]*ofdgo.PageContent), pageCache: make(map[int]*ofdgo.PageContent),
boxCache: make(map[int]ofdgo.Box), boxCache: make(map[int]ofdgo.Box),
@@ -271,8 +274,10 @@ func (s *Session) SetFonts(fonts []FontFile) error {
return fmt.Errorf("invalid font file") return fmt.Errorf("invalid font file")
} }
s.Renderer.SetFontFS(fontFS) s.Renderer.SetFontFS(fontFS)
s.fontFS = fontFS
} else { } else {
s.Renderer.SetFontFS() s.Renderer.SetFontFS()
s.fontFS = nil
} }
clear(s.textCache) clear(s.textCache)
clear(s.svgFonts) clear(s.svgFonts)
+107 -123
View File
@@ -23,7 +23,6 @@ import (
"fmt" "fmt"
"image" "image"
"math" "math"
"reflect"
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
@@ -33,8 +32,8 @@ import (
"github.com/tdewolff/font" "github.com/tdewolff/font"
) )
// Editor 新建OFD文档,长度单位为毫米,页面索引从0开始,实例需串行使用 // Editor 编辑OFD文档,长度单位为毫米,页面索引从0开始,实例需串行使用
// Info可修改文档元数据,通过方法管理页面、对象和资源,不修改已有OFD文件 // Info可修改文档元数据,通过方法管理页面、对象和资源,WriteTo另存结果,不覆盖输入
type Editor struct { type Editor struct {
Info DocInfo Info DocInfo
pages []PageContent pages []PageContent
@@ -48,6 +47,7 @@ type Editor struct {
historyLimit int historyLimit int
revision uint64 revision uint64
serial uint64 serial uint64
source *editorSource
} }
// editorResource 文档内嵌资源 // editorResource 文档内嵌资源
@@ -96,18 +96,23 @@ func (e *Editor) Page(index int) (*PageContent, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
data, err := encodeOFDXML(func(x *ofdXML) { x.page(*page) }) result := cloneEditorData(*page)
if err != nil { result.XMLName = xml.Name{Space: ofdNamespace, Local: "Page"}
return nil, err for i := range result.Content.Layer {
layer := &result.Content.Layer[i]
layer.TextObject, layer.PathObject, layer.ImageObject, layer.CompositeGraphicUnit = nil, nil, nil, nil
for _, object := range layer.Objects {
switch object.Type {
case "TextObject":
object.TextObject.layout = nil
layer.TextObject = append(layer.TextObject, object.TextObject)
case "PathObject":
layer.PathObject = append(layer.PathObject, object.PathObject)
case "ImageObject":
layer.ImageObject = append(layer.ImageObject, object.ImageObject)
case "CompositeObject", "CompositeGraphicUnit":
layer.CompositeGraphicUnit = append(layer.CompositeGraphicUnit, object.CompositeGraphicUnit)
} }
var result PageContent
if err := xml.Unmarshal(data, &result); err != nil {
return nil, err
}
result.ID = page.ID
for i, layer := range page.Content.Layer {
for j, object := range layer.Objects {
result.Content.Layer[i].Objects[j].TextObject.layout = object.TextObject.layout
} }
} }
return &result, nil return &result, nil
@@ -120,6 +125,11 @@ func (e *Editor) page(index int) (*PageContent, error) {
if index < 0 || index >= len(e.pages) { if index < 0 || index >= len(e.pages) {
return nil, fmt.Errorf("page index %d out of range", index) return nil, fmt.Errorf("page index %d out of range", index)
} }
if e.source != nil {
if err := e.loadSourcePage(index); err != nil {
return nil, err
}
}
return &e.pages[index], nil return &e.pages[index], nil
} }
@@ -130,6 +140,9 @@ func (e *Editor) AddPage(width, height float64) (int, error) {
if !finite(width) || !finite(height) || width <= 0 || height <= 0 { if !finite(width) || !finite(height) || width <= 0 || height <= 0 {
return 0, fmt.Errorf("page dimensions must be finite and positive") return 0, fmt.Errorf("page dimensions must be finite and positive")
} }
if err := e.prepareSourceIDs(); err != nil {
return 0, err
}
e.pages = append(e.pages, PageContent{ e.pages = append(e.pages, PageContent{
ID: e.nextID(), ID: e.nextID(),
Area: PageArea{PhysicalBox: fmt.Sprintf("0 0 %s %s", ofdNumber(width), ofdNumber(height))}, Area: PageArea{PhysicalBox: fmt.Sprintf("0 0 %s %s", ofdNumber(width), ofdNumber(height))},
@@ -145,6 +158,9 @@ func (e *Editor) AddPage(width, height float64) (int, error) {
// 入参: index 原页面索引 // 入参: index 原页面索引
// 返回: int 副本页面索引, error 错误信息 // 返回: int 副本页面索引, error 错误信息
func (e *Editor) CopyPage(index int) (int, error) { func (e *Editor) CopyPage(index int) (int, error) {
if e.originalPage(index) {
return 0, fmt.Errorf("copying imported pages is not supported")
}
source, err := e.page(index) source, err := e.page(index)
if err != nil { if err != nil {
return 0, err return 0, err
@@ -165,7 +181,11 @@ func (e *Editor) CopyPage(index int) (int, error) {
layer.ID = e.nextID() layer.ID = e.nextID()
for j := range layer.Objects { for j := range layer.Objects {
object := &layer.Objects[j] object := &layer.Objects[j]
origin := e.objectOrigin(editorObjectID(*object))
id := e.nextID() id := e.nextID()
if origin != nil {
e.source.origins[id] = origin
}
switch object.Type { switch object.Type {
case "TextObject": case "TextObject":
object.TextObject.ID = id object.TextObject.ID = id
@@ -186,6 +206,9 @@ func (e *Editor) CopyPage(index int) (int, error) {
// 入参: index 页面索引 // 入参: index 页面索引
// 返回: error 错误信息 // 返回: error 错误信息
func (e *Editor) DeletePage(index int) error { func (e *Editor) DeletePage(index int) error {
if e.originalPage(index) {
return fmt.Errorf("deleting imported pages is not supported")
}
page, err := e.page(index) page, err := e.page(index)
if err != nil { if err != nil {
return err return err
@@ -282,13 +305,16 @@ func (e *Editor) AddFont(file FontFile, index int) (string, error) {
if name == "" { if name == "" {
return "", fmt.Errorf("font has no name") return "", fmt.Errorf("font has no name")
} }
if err := e.prepareSourceIDs(); err != nil {
return "", err
}
id := e.nextID() id := e.nextID()
extension := ".ttf" extension := ".ttf"
if sfnt.IsCFF { if sfnt.IsCFF {
extension = ".otf" extension = ".otf"
} }
resource := editorResource{ resource := editorResource{
name: "Doc_0/Res/Font_" + id + extension, name: e.resourceDirectory() + "/Font_" + id + extension,
data: data, data: data,
font: &Font{ font: &Font{
ID: id, FontName: name, ID: id, FontName: name,
@@ -299,7 +325,7 @@ func (e *Editor) AddFont(file FontFile, index int) (string, error) {
FixedWidth: sfnt.Post.IsFixedPitch != 0, FixedWidth: sfnt.Post.IsFixedPitch != 0,
}, },
} }
resource.font.FontFile = strings.TrimPrefix(resource.name, "Doc_0/Res/") resource.font.FontFile = "Font_" + id + extension
e.resources = append(e.resources, resource) e.resources = append(e.resources, resource)
e.fonts[id] = sfnt e.fonts[id] = sfnt
e.resourceID[key] = id e.resourceID[key] = id
@@ -335,6 +361,9 @@ func (e *Editor) AddImage(data []byte) (string, error) {
if id, ok := e.resourceID[key]; ok { if id, ok := e.resourceID[key]; ok {
return id, nil return id, nil
} }
if err := e.prepareSourceIDs(); err != nil {
return "", err
}
id := e.nextID() id := e.nextID()
extension := format extension := format
if extension == "jpeg" { if extension == "jpeg" {
@@ -342,7 +371,7 @@ func (e *Editor) AddImage(data []byte) (string, error) {
} }
name := "Image_" + id + "." + extension name := "Image_" + id + "." + extension
e.resources = append(e.resources, editorResource{ e.resources = append(e.resources, editorResource{
name: "Doc_0/Res/" + name, name: e.resourceDirectory() + "/" + name,
data: bytes.Clone(data), data: bytes.Clone(data),
image: &MultiMedia{ID: id, Type: "Image", Format: strings.ToUpper(format), MediaFile: name}, image: &MultiMedia{ID: id, Type: "Image", Format: strings.ToUpper(format), MediaFile: name},
}) })
@@ -356,29 +385,11 @@ func (e *Editor) AddImage(data []byte) (string, error) {
// 入参: page 页面索引, object 对象内容,添加后不再引用调用方的可变数据 // 入参: page 页面索引, object 对象内容,添加后不再引用调用方的可变数据
// 返回: string 对象标识, error 错误信息 // 返回: string 对象标识, error 错误信息
func (e *Editor) AddObject(page int, object GraphicObject) (string, error) { func (e *Editor) AddObject(page int, object GraphicObject) (string, error) {
content, err := e.page(page) ids, err := e.CopyObjects(page, []GraphicObject{object}, 0, 0)
if err != nil { if err != nil {
return "", err return "", err
} }
id := strconv.Itoa(e.maxID + 1) return ids[0], nil
object, err = e.prepareObject(id, object)
if err != nil {
return "", err
}
e.maxID++
content.Content.Layer[0].Objects = append(content.Content.Layer[0].Objects, object)
if change := e.recordChange(); change != nil {
index := len(content.Content.Layer[0].Objects) - 1
change.undo = func(e *Editor) {
layer := &e.pages[page].Content.Layer[0]
layer.Objects = slices.Delete(layer.Objects, index, index+1)
}
change.redo = func(e *Editor) {
layer := &e.pages[page].Content.Layer[0]
layer.Objects = slices.Insert(layer.Objects, index, object)
}
}
return id, nil
} }
// Object 获取对象的独立副本,修改副本不影响文档 // Object 获取对象的独立副本,修改副本不影响文档
@@ -396,39 +407,24 @@ func (e *Editor) Object(page int, id string) (GraphicObject, error) {
// 入参: page 页面索引, id 对象标识, object 新内容,忽略其ID且不引用调用方的可变数据 // 入参: page 页面索引, id 对象标识, object 新内容,忽略其ID且不引用调用方的可变数据
// 返回: error 错误信息 // 返回: error 错误信息
func (e *Editor) UpdateObject(page int, id string, object GraphicObject) error { func (e *Editor) UpdateObject(page int, id string, object GraphicObject) error {
_, index, err := e.findObject(page, id) switch object.Type {
if err != nil { case "TextObject":
return err object.TextObject.ID = id
case "PathObject":
object.PathObject.ID = id
case "ImageObject":
object.ImageObject.ID = id
default:
return fmt.Errorf("unsupported object type %q", object.Type)
} }
object, err = e.prepareObject(id, object) return e.UpdateObjects(page, []GraphicObject{object})
if err != nil {
return err
}
e.replaceObject(page, index, object)
return nil
} }
// DeleteObject 删除对象,不回收资源或复用标识 // DeleteObject 删除对象,不回收资源或复用标识
// 入参: page 页面索引, id 对象标识 // 入参: page 页面索引, id 对象标识
// 返回: error 错误信息 // 返回: error 错误信息
func (e *Editor) DeleteObject(page int, id string) error { func (e *Editor) DeleteObject(page int, id string) error {
layer, index, err := e.findObject(page, id) return e.DeleteObjects(page, []string{id})
if err != nil {
return err
}
if change := e.recordChange(); change != nil {
object := layer.Objects[index]
change.undo = func(e *Editor) {
layer := &e.pages[page].Content.Layer[0]
layer.Objects = slices.Insert(layer.Objects, index, object)
}
change.redo = func(e *Editor) {
layer := &e.pages[page].Content.Layer[0]
layer.Objects = slices.Delete(layer.Objects, index, index+1)
}
}
layer.Objects = slices.Delete(layer.Objects, index, index+1)
return nil
} }
// MoveObject 调整同页正文图层内的绘制顺序,保留对象标识和内容 // MoveObject 调整同页正文图层内的绘制顺序,保留对象标识和内容
@@ -445,30 +441,24 @@ func (e *Editor) MoveObject(page int, id string, to int) error {
if from == to { if from == to {
return nil return nil
} }
moveEditorItem(layer.Objects, from, to) capability, err := e.ObjectCapabilities(page, id)
if change := e.recordChange(); change != nil { if err != nil {
change.undo = func(e *Editor) { moveEditorItem(e.pages[page].Content.Layer[0].Objects, to, from) } return err
change.redo = func(e *Editor) { moveEditorItem(e.pages[page].Content.Layer[0].Objects, from, to) }
} }
if !capability.Order {
return fmt.Errorf("object %q cannot be reordered", id)
}
layers := copyEditorPage(e.pages[page]).Content.Layer
for i := range layers {
if layers[i].ID == layer.ID {
moveEditorItem(layers[i].Objects, from, to)
}
}
e.replaceLayers(page, layers)
return nil return nil
} }
// replaceObject 提交已校验的对象并记录前后内容 // findObject 查找页面各图层中的对象
// 入参: page 页面索引, index 对象索引, object 新内容
func (e *Editor) replaceObject(page, index int, object GraphicObject) {
layer := &e.pages[page].Content.Layer[0]
before := layer.Objects[index]
if reflect.DeepEqual(before, object) {
return
}
layer.Objects[index] = object
if change := e.recordChange(); change != nil {
change.undo = func(e *Editor) { e.pages[page].Content.Layer[0].Objects[index] = before }
change.redo = func(e *Editor) { e.pages[page].Content.Layer[0].Objects[index] = object }
}
}
// findObject 查找正文图层中的对象
// 入参: page 页面索引, id 对象标识 // 入参: page 页面索引, id 对象标识
// 返回: *Layer 所属图层, int 对象索引, error 错误信息 // 返回: *Layer 所属图层, int 对象索引, error 错误信息
func (e *Editor) findObject(page int, id string) (*Layer, int, error) { func (e *Editor) findObject(page int, id string) (*Layer, int, error) {
@@ -476,21 +466,14 @@ func (e *Editor) findObject(page int, id string) (*Layer, int, error) {
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
layer := &content.Content.Layer[0] for i := range content.Content.Layer {
layer := &content.Content.Layer[i]
for index, object := range layer.Objects { for index, object := range layer.Objects {
var objectID string if editorObjectID(object) == id && id != "" {
switch object.Type {
case "TextObject":
objectID = object.TextObject.ID
case "PathObject":
objectID = object.PathObject.ID
case "ImageObject":
objectID = object.ImageObject.ID
}
if objectID == id {
return layer, index, nil return layer, index, nil
} }
} }
}
return nil, 0, fmt.Errorf("object %q not found on page %d", id, page) return nil, 0, fmt.Errorf("object %q not found on page %d", id, page)
} }
@@ -506,9 +489,13 @@ func (e *Editor) prepareObject(id string, object GraphicObject) (GraphicObject,
switch object.Type { switch object.Type {
case "TextObject": case "TextObject":
obj := &object.TextObject obj := &object.TextObject
codes := obj.TextCode
if err := e.prepareText(obj); err != nil { if err := e.prepareText(obj); err != nil {
return GraphicObject{}, err return GraphicObject{}, err
} }
if e.objectOrigin(obj.ID) != nil {
obj.TextCode = codes
}
obj.ID = id obj.ID = id
boundary, ctm, drawParam = obj.Boundary, obj.CTM, obj.DrawParam boundary, ctm, drawParam = obj.Boundary, obj.CTM, obj.DrawParam
join = obj.Join join = obj.Join
@@ -554,8 +541,13 @@ func (e *Editor) prepareObject(id string, object GraphicObject) (GraphicObject,
fill, stroke = obj.FillColor, (*FillColor)(obj.StrokeColor) fill, stroke = obj.FillColor, (*FillColor)(obj.StrokeColor)
case "ImageObject": case "ImageObject":
obj := &object.ImageObject obj := &object.ImageObject
if e.images[obj.ResourceID].X == 0 || (obj.ImageMask != "" && e.images[obj.ImageMask].X == 0) { if _, err := e.editorImage(obj.ResourceID); err != nil {
return GraphicObject{}, fmt.Errorf("image resource not found") return GraphicObject{}, err
}
if obj.ImageMask != "" {
if _, err := e.editorImage(obj.ImageMask); err != nil {
return GraphicObject{}, err
}
} }
if obj.Border != nil { if obj.Border != nil {
return GraphicObject{}, fmt.Errorf("image borders are not supported for creation") return GraphicObject{}, fmt.Errorf("image borders are not supported for creation")
@@ -633,30 +625,21 @@ func (e *Editor) validateImageClips(clips *Clips) error {
return nil return nil
} }
// cloneEditorObject 通过统一序列化规则复制对象 // cloneEditorObject 复制对象全部已解析字段,保留不可变的段落信息
// 入参: object 对象内容 // 入参: object 对象内容
// 返回: GraphicObject 独立副本, error 错误信息 // 返回: GraphicObject 独立副本, error 错误信息
func cloneEditorObject(object GraphicObject) (GraphicObject, error) { func cloneEditorObject(object GraphicObject) (GraphicObject, error) {
layout := object.TextObject.layout
data, err := encodeOFDXML(func(x *ofdXML) { x.object(object, true) })
if err != nil {
return GraphicObject{}, err
}
object = GraphicObject{Type: object.Type}
var target any
switch object.Type { switch object.Type {
case "TextObject": case "TextObject":
target = &object.TextObject return GraphicObject{Type: object.Type, TextObject: cloneEditorData(object.TextObject)}, nil
case "PathObject": case "PathObject":
target = &object.PathObject return GraphicObject{Type: object.Type, PathObject: cloneEditorData(object.PathObject)}, nil
case "ImageObject": case "ImageObject":
target = &object.ImageObject return GraphicObject{Type: object.Type, ImageObject: cloneEditorData(object.ImageObject)}, nil
case "CompositeObject", "CompositeGraphicUnit":
return GraphicObject{Type: object.Type, CompositeGraphicUnit: cloneEditorData(object.CompositeGraphicUnit)}, nil
} }
if err := xml.Unmarshal(data, target); err != nil { return GraphicObject{}, fmt.Errorf("unsupported object type %q", object.Type)
return GraphicObject{}, err
}
object.TextObject.layout = layout
return object, nil
} }
// AddText 添加文字,按显式换行和嵌入字体度量定位,不自动折行或进行复杂文字塑形 // AddText 添加文字,按显式换行和嵌入字体度量定位,不自动折行或进行复杂文字塑形
@@ -691,12 +674,7 @@ func (e *Editor) UpdateText(page int, id, value, fontID string, size float64) er
if err := e.LayoutText(&object.TextObject, value, layout); err != nil { if err := e.LayoutText(&object.TextObject, value, layout); err != nil {
return err return err
} }
object, err = e.prepareObject(id, object) return e.UpdateObject(page, id, object)
if err != nil {
return err
}
e.replaceObject(page, index, object)
return nil
} }
// TransformObject 以页面原点等比缩放后平移对象,文字同步缩放字号和字距,保留原有排版 // TransformObject 以页面原点等比缩放后平移对象,文字同步缩放字号和字距,保留原有排版
@@ -741,11 +719,16 @@ func transformEditorObject(object GraphicObject, dx, dy, scale float64) (Graphic
if scale != 1 { if scale != 1 {
object.ImageObject.Clips = transformImageClips(object.ImageObject.Clips, Matrix{a: scale, d: scale}) object.ImageObject.Clips = transformImageClips(object.ImageObject.Clips, Matrix{a: scale, d: scale})
} }
default:
return GraphicObject{}, fmt.Errorf("unsupported object type %q", object.Type)
} }
box, err := ParseBox(*boundary) box, err := ParseBox(*boundary)
if err != nil { if err != nil {
return GraphicObject{}, err return GraphicObject{}, err
} }
if object.Type == "ImageObject" && *ctm == "" && scale != 1 {
*ctm = Matrix{a: box.W, d: box.H}.String()
}
*boundary = fmt.Sprintf("%s %s %s %s", ofdNumber(box.X*scale+dx), ofdNumber(box.Y*scale+dy), ofdNumber(box.W*scale), ofdNumber(box.H*scale)) *boundary = fmt.Sprintf("%s %s %s %s", ofdNumber(box.X*scale+dx), ofdNumber(box.Y*scale+dy), ofdNumber(box.W*scale), ofdNumber(box.H*scale))
if scale != 1 && (*ctm != "" || object.Type != "TextObject") { if scale != 1 && (*ctm != "" || object.Type != "TextObject") {
m := NewMatrix(*ctm) m := NewMatrix(*ctm)
@@ -810,8 +793,8 @@ func (e *Editor) LayoutText(obj *TextObject, value string, options TextLayout) e
if obj.ReadDirection != 0 || obj.CharDirection != 0 { if obj.ReadDirection != 0 || obj.CharDirection != 0 {
return fmt.Errorf("automatic text layout requires horizontal text") return fmt.Errorf("automatic text layout requires horizontal text")
} }
sfnt, ok := e.fonts[obj.Font] sfnt, err := e.editorFont(obj.Font)
if !ok { if err != nil {
return fmt.Errorf("font resource %q not found", obj.Font) return fmt.Errorf("font resource %q not found", obj.Font)
} }
if !finite(obj.Size) || obj.Size <= 0 || !finite(obj.HScale) || obj.HScale < 0 { if !finite(obj.Size) || obj.Size <= 0 || !finite(obj.HScale) || obj.HScale < 0 {
@@ -877,7 +860,7 @@ func (e *Editor) LayoutText(obj *TextObject, value string, options TextLayout) e
} }
obj.TextCode = codes obj.TextCode = codes
obj.layout = nil obj.layout = nil
if options != (TextLayout{}) { if options != (TextLayout{}) || e.objectOrigin(obj.ID) != nil {
obj.layout = &textLayout{value: value, options: options} obj.layout = &textLayout{value: value, options: options}
} }
return nil return nil
@@ -887,7 +870,8 @@ func (e *Editor) LayoutText(obj *TextObject, value string, options TextLayout) e
// 入参: obj 文字对象 // 入参: obj 文字对象
// 返回: error 错误信息 // 返回: error 错误信息
func (e *Editor) prepareText(obj *TextObject) error { func (e *Editor) prepareText(obj *TextObject) error {
if e.fonts[obj.Font] == nil || !finite(obj.Size) || obj.Size <= 0 { sfnt, err := e.editorFont(obj.Font)
if err != nil || !finite(obj.Size) || obj.Size <= 0 {
return fmt.Errorf("text requires an embedded font and a positive finite size") return fmt.Errorf("text requires an embedded font and a positive finite size")
} }
if len(obj.TextCode) == 0 { if len(obj.TextCode) == 0 {
@@ -928,7 +912,7 @@ func (e *Editor) prepareText(obj *TextObject) error {
code.X, code.Y = x, y code.X, code.Y = x, y
runes := textCodeRunes(code.Value) runes := textCodeRunes(code.Value)
for _, char := range runes { for _, char := range runes {
if e.fonts[obj.Font].GlyphIndex(char) == 0 { if sfnt.GlyphIndex(char) == 0 {
return fmt.Errorf("font does not contain U+%04X", char) return fmt.Errorf("font does not contain U+%04X", char)
} }
} }
+156 -34
View File
@@ -24,6 +24,22 @@ import (
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
) )
// editorObjectPosition 对象所属图层及层内索引。
type editorObjectPosition struct {
layer int
index int
}
// compareEditorPosition 比较对象在页面结构中的先后顺序。
// 入参: a、b 对象位置
// 返回: int 比较结果
func compareEditorPosition(a, b editorObjectPosition) int {
if value := cmp.Compare(a.layer, b.layer); value != 0 {
return value
}
return cmp.Compare(a.index, b.index)
}
// Objects 按绘制顺序获取选区的独立快照,保留编辑中的段落信息。 // Objects 按绘制顺序获取选区的独立快照,保留编辑中的段落信息。
// 入参: page 页面索引, ids 对象标识,不得重复 // 入参: page 页面索引, ids 对象标识,不得重复
// 返回: []GraphicObject 独立对象副本, error 错误信息 // 返回: []GraphicObject 独立对象副本, error 错误信息
@@ -32,10 +48,18 @@ func (e *Editor) Objects(page int, ids []string) ([]GraphicObject, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
slices.Sort(indexes) slices.SortFunc(indexes, compareEditorPosition)
objects := make([]GraphicObject, len(indexes)) objects := make([]GraphicObject, len(indexes))
for i, index := range indexes { for i, index := range indexes {
objects[i], err = cloneEditorObject(e.pages[page].Content.Layer[0].Objects[index]) object := e.pages[page].Content.Layer[index.layer].Objects[index.index]
capability, err := e.ObjectCapabilities(page, editorObjectID(object))
if err != nil {
return nil, err
}
if !capability.Copy {
return nil, fmt.Errorf("object %q cannot be copied: %s", editorObjectID(object), capability.Reason)
}
objects[i], err = cloneEditorObject(object)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -51,33 +75,60 @@ func (e *Editor) CopyObjects(page int, objects []GraphicObject, dx, dy float64)
if !finite(dx) || !finite(dy) { if !finite(dx) || !finite(dy) {
return nil, fmt.Errorf("copy requires finite offsets") return nil, fmt.Errorf("copy requires finite offsets")
} }
content, err := e.page(page) _, err := e.page(page)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(objects) == 0 { if len(objects) == 0 {
return nil, nil return nil, nil
} }
before := content.Content.Layer[0].Objects if !e.sourceRGB() {
return nil, fmt.Errorf("inserting objects requires an RGB default color space")
}
if err := e.prepareSourceIDs(); err != nil {
return nil, err
}
prepared := make([]GraphicObject, len(objects)) prepared := make([]GraphicObject, len(objects))
result := make([]string, len(objects)) result := make([]string, len(objects))
for i, object := range objects { for i, object := range objects {
origin := e.objectOrigin(editorObjectID(object))
result[i] = strconv.Itoa(e.maxID + i + 1) result[i] = strconv.Itoa(e.maxID + i + 1)
object, err := e.prepareObject(result[i], object) object, err := e.prepareObject(result[i], object)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if dx != 0 || dy != 0 {
object, err = transformEditorObject(object, dx, dy, 1) object, err = transformEditorObject(object, dx, dy, 1)
if err != nil { if err != nil {
return nil, err return nil, err
} }
prepared[i], err = e.prepareObject(result[i], object) codes := object.TextObject.TextCode
object, err = e.prepareObject(result[i], object)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if origin != nil {
object.TextObject.TextCode = codes
}
}
prepared[i] = object
} }
e.maxID += len(objects) e.maxID += len(objects)
e.replaceObjects(page, append(slices.Clone(before), prepared...)) for i, object := range objects {
if origin := e.objectOrigin(editorObjectID(object)); origin != nil {
e.source.origins[result[i]] = origin
}
}
layers := copyEditorPage(e.pages[page]).Content.Layer
target := 0
if e.originalPage(page) {
target = len(e.source.pages[e.pages[page].ID].original.Content.Layer)
}
if target == len(layers) {
layers = append(layers, Layer{ID: e.nextID(), Type: "Body"})
}
layers[target].Objects = append(layers[target].Objects, prepared...)
e.replaceLayers(page, layers)
return result, nil return result, nil
} }
@@ -92,10 +143,20 @@ func (e *Editor) OrderObjects(page int, ids []string, order string) error {
if err != nil || len(indexes) == 0 { if err != nil || len(indexes) == 0 {
return err return err
} }
objects := slices.Clone(e.pages[page].Content.Layer[0].Objects) for i, id := range ids {
capability, err := e.ObjectCapabilities(page, id)
if err != nil {
return err
}
if !capability.Order || indexes[i].layer != indexes[0].layer {
return fmt.Errorf("ordering requires editable objects in the same layer")
}
}
layers := copyEditorPage(e.pages[page]).Content.Layer
objects := layers[indexes[0].layer].Objects
selected := make([]bool, len(objects)) selected := make([]bool, len(objects))
for _, index := range indexes { for _, index := range indexes {
selected[index] = true selected[index.index] = true
} }
switch order { switch order {
case "up": case "up":
@@ -123,7 +184,8 @@ func (e *Editor) OrderObjects(page int, ids []string, order string) error {
} }
objects = ordered objects = ordered
} }
e.replaceObjects(page, objects) layers[indexes[0].layer].Objects = objects
e.replaceLayers(page, layers)
return nil return nil
} }
@@ -158,7 +220,7 @@ func (e *Editor) DistributeObjects(page int, ids []string, axis string) error {
if c := cmp.Compare(positions[i], positions[j]); c != 0 { if c := cmp.Compare(positions[i], positions[j]); c != 0 {
return c return c
} }
return cmp.Compare(indexes[i], indexes[j]) return compareEditorPosition(indexes[i], indexes[j])
}) })
first, last := order[0], order[len(order)-1] first, last := order[0], order[len(order)-1]
gap := (positions[last] + sizes[last] - positions[first] - total) / float64(len(order)-1) gap := (positions[last] + sizes[last] - positions[first] - total) / float64(len(order)-1)
@@ -167,7 +229,7 @@ func (e *Editor) DistributeObjects(page int, ids []string, axis string) error {
if canvas.Equal(step, 0) { if canvas.Equal(step, 0) {
step = 0 step = 0
} }
if step < 0 || step == 0 && indexes[index] > indexes[order[i+1]] { if step < 0 || step == 0 && compareEditorPosition(indexes[index], indexes[order[i+1]]) > 0 {
return fmt.Errorf("overlap prevents ordered equal-gap distribution") return fmt.Errorf("overlap prevents ordered equal-gap distribution")
} }
} }
@@ -193,21 +255,24 @@ func (e *Editor) DistributeObjects(page int, ids []string, axis string) error {
} }
updates = append(updates, object) updates = append(updates, object)
} }
return e.UpdateObjects(page, updates) return e.updateObjects(page, updates, true)
} }
// replaceObjects 替换正文对象列表,隔离历史快照与后续的增删、排序操作。 // replaceLayers 替换图层容器,隔离历史快照与后续的增删、排序操作。
// 入参: page 页面索引, objects 新的正文对象列表 // 入参: page 页面索引, layers 新图层列表
func (e *Editor) replaceObjects(page int, objects []GraphicObject) { func (e *Editor) replaceLayers(page int, layers []Layer) {
before := e.pages[page].Content.Layer[0].Objects before := copyEditorPage(e.pages[page]).Content.Layer
if reflect.DeepEqual(before, objects) { if reflect.DeepEqual(before, layers) {
return return
} }
before, after := slices.Clone(before), slices.Clone(objects) after := copyEditorPage(PageContent{Content: Content{Layer: layers}}).Content.Layer
e.pages[page].Content.Layer[0].Objects = slices.Clone(after) apply := func(e *Editor, layers []Layer) {
e.pages[page].Content.Layer = copyEditorPage(PageContent{Content: Content{Layer: layers}}).Content.Layer
}
apply(e, after)
if change := e.recordChange(); change != nil { if change := e.recordChange(); change != nil {
change.undo = func(e *Editor) { e.pages[page].Content.Layer[0].Objects = slices.Clone(before) } change.undo = func(e *Editor) { apply(e, before) }
change.redo = func(e *Editor) { e.pages[page].Content.Layer[0].Objects = slices.Clone(after) } change.redo = func(e *Editor) { apply(e, after) }
} }
} }
@@ -215,6 +280,13 @@ func (e *Editor) replaceObjects(page int, objects []GraphicObject) {
// 入参: page 页面索引, objects 新对象内容,ID不得重复 // 入参: page 页面索引, objects 新对象内容,ID不得重复
// 返回: error 错误信息 // 返回: error 错误信息
func (e *Editor) UpdateObjects(page int, objects []GraphicObject) error { func (e *Editor) UpdateObjects(page int, objects []GraphicObject) error {
return e.updateObjects(page, objects, false)
}
// updateObjects 校验内容更新或纯几何变换,原子提交跨图层选区。
// 入参: page 页面索引, objects 新对象, geometry 是否保留原内容的几何操作
// 返回: error 错误信息
func (e *Editor) updateObjects(page int, objects []GraphicObject, geometry bool) error {
ids := make([]string, len(objects)) ids := make([]string, len(objects))
for i, object := range objects { for i, object := range objects {
ids[i] = editorObjectID(object) ids[i] = editorObjectID(object)
@@ -225,17 +297,33 @@ func (e *Editor) UpdateObjects(page int, objects []GraphicObject) error {
} }
after := make([]GraphicObject, len(objects)) after := make([]GraphicObject, len(objects))
for i, object := range objects { for i, object := range objects {
if e.originalPage(page) {
capability, err := e.ObjectCapabilities(page, ids[i])
if err != nil {
return err
}
if !capability.Transform || !geometry && !e.sourceRGB() {
return fmt.Errorf("object %q is read-only for this operation: %s", ids[i], capability.Reason)
}
}
if geometry && e.originalPage(page) {
if err := validateEditorGeometry(object); err != nil {
return err
}
after[i] = cloneEditorData(object)
} else {
after[i], err = e.prepareObject(ids[i], object) after[i], err = e.prepareObject(ids[i], object)
if err != nil { if err != nil {
return err return err
} }
} }
}
if reflect.DeepEqual(before, after) { if reflect.DeepEqual(before, after) {
return nil return nil
} }
apply := func(e *Editor, objects []GraphicObject) { apply := func(e *Editor, objects []GraphicObject) {
for i, index := range indexes { for i, index := range indexes {
e.pages[page].Content.Layer[0].Objects[index] = objects[i] e.pages[page].Content.Layer[index.layer].Objects[index.index] = objects[i]
} }
} }
apply(e, after) apply(e, after)
@@ -261,6 +349,11 @@ func (e *Editor) TransformObjects(page int, ids []string, dx, dy, scale float64)
return nil return nil
} }
for i, object := range objects { for i, object := range objects {
if e.originalPage(page) {
if err := validateEditorGeometry(object); err != nil {
return err
}
}
object, err = cloneEditorObject(object) object, err = cloneEditorObject(object)
if err != nil { if err != nil {
return err return err
@@ -269,8 +362,11 @@ func (e *Editor) TransformObjects(page int, ids []string, dx, dy, scale float64)
if err != nil { if err != nil {
return err return err
} }
if origin := e.objectOrigin(editorObjectID(object)); origin != nil && origin.node.attr("LineWidth") == "0" && object.Type == "TextObject" && object.TextObject.LineWidth == 0 {
objects[i].TextObject.LineWidth = 0
} }
return e.UpdateObjects(page, objects) }
return e.updateObjects(page, objects, true)
} }
// AlignObjects 单对象对齐页面,多对象相互对齐至选区边界,文字采用实际字形范围。 // AlignObjects 单对象对齐页面,多对象相互对齐至选区边界,文字采用实际字形范围。
@@ -320,7 +416,7 @@ func (e *Editor) AlignObjects(page int, ids []string, alignment string) error {
return err return err
} }
} }
return e.UpdateObjects(page, objects) return e.updateObjects(page, objects, true)
} }
// DeleteObjects 原子删除同页对象,保留其余对象顺序,一次撤销恢复全部。 // DeleteObjects 原子删除同页对象,保留其余对象顺序,一次撤销恢复全部。
@@ -331,25 +427,34 @@ func (e *Editor) DeleteObjects(page int, ids []string) error {
if err != nil || len(indexes) == 0 { if err != nil || len(indexes) == 0 {
return err return err
} }
before := e.pages[page].Content.Layer[0].Objects for _, id := range ids {
after := slices.Clone(before) capability, err := e.ObjectCapabilities(page, id)
slices.Sort(indexes) if err != nil {
for _, index := range slices.Backward(indexes) { return err
after = slices.Delete(after, index, index+1)
} }
e.replaceObjects(page, after) if !capability.Delete {
return fmt.Errorf("object %q cannot be deleted: %s", id, capability.Reason)
}
}
layers := copyEditorPage(e.pages[page]).Content.Layer
slices.SortFunc(indexes, compareEditorPosition)
for _, index := range slices.Backward(indexes) {
layer := &layers[index.layer]
layer.Objects = slices.Delete(layer.Objects, index.index, index.index+1)
}
e.replaceLayers(page, layers)
return nil return nil
} }
// selectedObjects 校验同页选择,返回内部只读对象与对应索引。 // selectedObjects 校验同页选择,返回内部只读对象与对应索引。
// 入参: page 页面索引, ids 对象标识,不得重复 // 入参: page 页面索引, ids 对象标识,不得重复
// 返回: []GraphicObject 所选对象, []int 正文对象索引, error 错误信息 // 返回: []GraphicObject 所选对象, []editorObjectPosition 图层和对象索引, error 错误信息
func (e *Editor) selectedObjects(page int, ids []string) ([]GraphicObject, []int, error) { func (e *Editor) selectedObjects(page int, ids []string) ([]GraphicObject, []editorObjectPosition, error) {
if _, err := e.page(page); err != nil { if _, err := e.page(page); err != nil {
return nil, nil, err return nil, nil, err
} }
objects := make([]GraphicObject, len(ids)) objects := make([]GraphicObject, len(ids))
indexes := make([]int, len(ids)) indexes := make([]editorObjectPosition, len(ids))
seen := make(map[string]bool, len(ids)) seen := make(map[string]bool, len(ids))
for i, id := range ids { for i, id := range ids {
if seen[id] { if seen[id] {
@@ -360,7 +465,13 @@ func (e *Editor) selectedObjects(page int, ids []string) ([]GraphicObject, []int
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
objects[i], indexes[i] = layer.Objects[index], index for j := range e.pages[page].Content.Layer {
if &e.pages[page].Content.Layer[j] == layer {
indexes[i] = editorObjectPosition{j, index}
break
}
}
objects[i] = layer.Objects[index]
} }
return objects, indexes, nil return objects, indexes, nil
} }
@@ -376,6 +487,8 @@ func editorObjectID(object GraphicObject) string {
return object.PathObject.ID return object.PathObject.ID
case "ImageObject": case "ImageObject":
return object.ImageObject.ID return object.ImageObject.ID
case "CompositeObject", "CompositeGraphicUnit":
return object.CompositeGraphicUnit.ID
} }
return "" return ""
} }
@@ -384,6 +497,15 @@ func editorObjectID(object GraphicObject) string {
// 入参: page 页面索引, objects 待度量的对象 // 入参: page 页面索引, objects 待度量的对象
// 返回: []Box 对象范围,文字采用字形范围,其余采用Boundary, error 错误信息 // 返回: []Box 对象范围,文字采用字形范围,其余采用Boundary, error 错误信息
func (e *Editor) objectBounds(page int, objects []GraphicObject) ([]Box, error) { func (e *Editor) objectBounds(page int, objects []GraphicObject) ([]Box, error) {
for _, object := range objects {
capability, err := e.ObjectCapabilities(page, editorObjectID(object))
if err != nil {
return nil, err
}
if !capability.Arrange {
return nil, fmt.Errorf("object %q cannot be arranged: %s", editorObjectID(object), capability.Reason)
}
}
textBoxes := make(map[string]Box) textBoxes := make(map[string]Box)
if slices.ContainsFunc(objects, func(o GraphicObject) bool { return o.Type == "TextObject" }) { if slices.ContainsFunc(objects, func(o GraphicObject) bool { return o.Type == "TextObject" }) {
reader, err := e.Reader() reader, err := e.Reader()
+548
View File
@@ -0,0 +1,548 @@
// 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"
"encoding/xml"
"fmt"
"image"
"io"
"path"
"reflect"
"strconv"
"strings"
"github.com/tdewolff/font"
)
// editorSource 保留输入包及按需加载的原始页面。
type editorSource struct {
reader *Reader
document *Document
info DocInfo
directory string
pages map[string]*editorSourcePage
origins map[string]*editorObjectOrigin
idsReady bool
}
// editorObjectOrigin 保留原对象的XML语义,供复制和局部更新复用。
type editorObjectOrigin struct {
page *editorSourcePage
node *editorXML
object GraphicObject
}
// editorSourcePage 保存页面原文、解析快照及可定位的直接对象。
type editorSourcePage struct {
ref Page
data []byte
root *editorXML
original *PageContent
nodes map[string]*editorXML
}
// ObjectCapabilities 已有对象可执行的操作,Reason说明受限原因。
// Update表示完整替换内容,Transform表示保留文字定位的几何变换,Order仅限当前图层。
// Arrange表示可准确度量对齐与旋转范围;Reflow表示可重排文字,LayoutKnown表示段落选项可恢复。
type ObjectCapabilities struct {
Update bool
Reflow bool
LayoutKnown bool
Transform bool
Arrange bool
Copy bool
Delete bool
Order bool
Reason string
}
// PageCapabilities 页面可执行的操作,Insert表示可以添加RGB对象;新增空白页面始终可用。
type PageCapabilities struct {
Insert bool
Copy bool
Delete bool
Move bool
Resize bool
}
// Editor 打开主文档进行保真编辑,不修改输入包,不以权限声明或签名限制编辑。
// 原权限和签名数据保留,修改受保护内容会使原签名失效,不重新签名。
// 页面按需解析,未修改条目直接保留;编辑器及其Reader快照使用期间不得关闭输入Reader。
// 返回: *Editor 编辑器, error 错误信息
func (r *Reader) Editor() (*Editor, error) {
if r.Zip != nil {
seen := make(map[string]bool)
for _, file := range r.Zip.File {
name := cleanPackagePath(file.Name)
if seen[name] {
return nil, fmt.Errorf("ambiguous package entry %q", file.Name)
}
seen[name] = true
}
}
reader := &Reader{Zip: r.Zip, files: r.files}
if err := reader.initRoot(); err != nil {
return nil, err
}
doc, err := reader.Doc()
if err != nil {
return nil, err
}
maximum := doc.CommonData.MaxUnitID
if maximum < 0 {
return nil, fmt.Errorf("invalid MaxUnitID")
}
info, err := reader.DocInfo()
if err != nil {
return nil, err
}
e := NewEditor()
e.Info, e.maxID = cloneEditorData(*info), maximum
directory := path.Join(reader.RootDir, "Edit")
for i := 1; editorDirectoryExists(reader, directory); i++ {
directory = path.Join(reader.RootDir, "Edit_"+strconv.Itoa(i))
}
e.source = &editorSource{reader: reader, document: doc, info: cloneEditorData(*info), directory: directory, pages: make(map[string]*editorSourcePage), origins: make(map[string]*editorObjectOrigin)}
for _, page := range doc.Pages.Page {
if page.ID == "" || e.source.pages[page.ID] != nil {
return nil, fmt.Errorf("missing or duplicate page ID %q", page.ID)
}
e.source.pages[page.ID] = &editorSourcePage{ref: page}
e.pages = append(e.pages, PageContent{ID: page.ID})
}
return e, nil
}
// editorDirectoryExists 判断包内目录是否已经使用,避免覆盖原始资源。
// 入参: reader 输入包, directory 目录路径
// 返回: bool 是否存在
func editorDirectoryExists(reader *Reader, directory string) bool {
for name := range reader.fileIndex {
if strings.EqualFold(name, directory) || strings.HasPrefix(strings.ToLower(name), strings.ToLower(directory)+"/") {
return true
}
}
for name := range reader.files {
if strings.EqualFold(name, directory) || strings.HasPrefix(strings.ToLower(name), strings.ToLower(directory)+"/") {
return true
}
}
return false
}
// prepareSourceIDs 首次新增内容前流式核对原包标识,不依赖可能缺失或过期的MaxUnitID。
// 返回: error 错误信息
func (e *Editor) prepareSourceIDs() error {
if e.source == nil || e.source.idsReady {
return nil
}
reader := e.source.reader
names := make(map[string]bool)
for name := range reader.fileIndex {
names[name] = true
}
for name := range reader.files {
names[name] = true
}
maximum := e.maxID
for name := range names {
if !strings.EqualFold(path.Ext(name), ".xml") {
continue
}
input, err := reader.openFile(name)
if err != nil {
return err
}
id, err := editorXMLMaxID(input)
input.Close()
if err != nil {
return fmt.Errorf("scan IDs in %s: %w", name, err)
}
maximum = max(maximum, id)
}
e.maxID, e.source.idsReady = maximum, true
return nil
}
// editorXMLMaxID 流式读取OFD结构中的最大标识,跳过非OFD的XML附件。
// 入参: input XML输入流
// 返回: int 最大标识, error 错误信息
func editorXMLMaxID(input io.Reader) (int, error) {
decoder := xml.NewDecoder(input)
maximum, started := 0, false
for {
token, err := decoder.Token()
if err == io.EOF || err != nil && !started {
return maximum, nil
}
if err != nil {
return 0, err
}
node, ok := token.(xml.StartElement)
if !ok {
continue
}
if !started {
switch node.Name.Local {
case "OFD", "Document", "Page", "Res", "PageAnnot", "Annotations", "Attachments", "CustomTags":
default:
return 0, nil
}
started = true
}
for _, attr := range node.Attr {
if attr.Name.Space == "" && attr.Name.Local == "ID" {
id, err := strconv.Atoi(attr.Value)
if err == nil {
maximum = max(maximum, id)
}
}
}
}
}
// resourceDirectory 获取新增二进制资源的目录。
// 返回: string 包内路径
func (e *Editor) resourceDirectory() string {
if e.source != nil {
return e.source.directory + "/Res"
}
return "Doc_0/Res"
}
// originalPage 判断索引是否指向原文档页面。
// 入参: index 页面索引
// 返回: bool 是否为原始页面
func (e *Editor) originalPage(index int) bool {
return e.source != nil && index >= 0 && index < len(e.pages) && e.source.pages[e.pages[index].ID] != nil
}
// PageCapabilities 获取页面操作范围;原始页面暂不复制、删除,避免破坏模板与外部引用。
// 入参: index 页面索引
// 返回: PageCapabilities 操作能力, error 错误信息
func (e *Editor) PageCapabilities(index int) (PageCapabilities, error) {
if index < 0 || index >= len(e.pages) {
return PageCapabilities{}, fmt.Errorf("page index %d out of range", index)
}
original := e.originalPage(index)
return PageCapabilities{Insert: e.sourceRGB(), Copy: !original, Delete: !original, Move: true, Resize: true}, nil
}
// sourceRGB 判断新建RGB对象能否直接使用文档默认颜色空间。
// 返回: bool 是否为8位RGB文档
func (e *Editor) sourceRGB() bool {
if e.source == nil || e.source.document.CommonData.DefaultCS == 0 {
return true
}
space := e.source.reader.colorSpaceCache[strconv.Itoa(e.source.document.CommonData.DefaultCS)]
return space != nil && space.Type == "RGB" && (space.BitsPerComponent == 0 || space.BitsPerComponent == 8) && len(space.Palette) == 0
}
// loadSourcePage 首次访问时解析原始页面,不加载图片或字体二进制数据。
// 入参: index 页面索引
// 返回: error 错误信息
func (e *Editor) loadSourcePage(index int) error {
source := e.source.pages[e.pages[index].ID]
if source == nil || source.original != nil {
return nil
}
reader := e.source.reader
data, err := reader.readFile(reader.ResPath(source.ref.BaseLoc))
if err != nil {
return err
}
root, err := parseEditorXML(data)
if err != nil {
return err
}
page, err := reader.PageContent(source.ref)
if err != nil {
return err
}
nodes := make(map[string]*editorXML)
if content := root.child("Content"); content != nil {
for _, layer := range content.children {
if layer.name.Local != "Layer" {
continue
}
for _, node := range layer.children {
id := node.attr("ID")
if id != "" {
if nodes[id] != nil {
return fmt.Errorf("duplicate object ID %q", id)
}
nodes[id] = node
}
}
}
}
source.data, source.root, source.nodes = data, root, nodes
source.original = page
for _, layer := range page.Content.Layer {
for _, object := range layer.Objects {
id := editorObjectID(object)
if node := nodes[id]; node != nil {
e.source.origins[id] = &editorObjectOrigin{page: source, node: node, object: object}
}
}
}
e.pages[index] = copyEditorPage(*page)
return nil
}
// ObjectCapabilities 获取对象可执行的操作,不支持的内容保持原文,不强制转换。
// 字体缺失或子集不包含原文时仍可移动、删除,不允许重新排版或复制。
// 显式提供可用字体后可通过UpdateObject替换;原文布局未知时不自动推断段落选项。
// 入参: page 页面索引, id 对象标识
// 返回: ObjectCapabilities 操作能力, error 错误信息
func (e *Editor) ObjectCapabilities(page int, id string) (ObjectCapabilities, error) {
layer, index, err := e.findObject(page, id)
if err != nil {
return ObjectCapabilities{}, err
}
object := layer.Objects[index]
all := ObjectCapabilities{Update: true, Reflow: object.Type == "TextObject" && object.TextObject.ReadDirection == 0 && object.TextObject.CharDirection == 0, LayoutKnown: object.Type == "TextObject" && (e.objectOrigin(id) == nil || object.TextObject.layout != nil), Transform: true, Arrange: true, Copy: true, Delete: true, Order: true}
if !e.originalPage(page) {
return all, nil
}
source := e.source.pages[e.pages[page].ID]
node := source.nodes[id]
if node == nil {
for _, original := range source.original.Content.Layer {
for _, object := range original.Objects {
if editorObjectID(object) == id {
return ObjectCapabilities{Reason: "nested objects are read-only"}, nil
}
}
}
return all, nil
}
if layer.DrawParam != "" || !editorXMLSupported(node) {
return ObjectCapabilities{Reason: "object uses unsupported editing features"}, nil
}
for _, attr := range node.parent.attrs {
if attr.Name.Space == "xmlns" || attr.Name.Space == "" && attr.Name.Local == "xmlns" {
continue
}
if attr.Name.Space != "" || attr.Name.Local != "ID" && attr.Name.Local != "Type" && attr.Name.Local != "DrawParam" {
return ObjectCapabilities{Reason: "layer uses unsupported editing features"}, nil
}
}
if err := validateEditorGeometry(object); err != nil {
return ObjectCapabilities{Reason: err.Error()}, nil
}
all.Order = e.sourceLayerOrderable(page, layer.ID)
if _, err := e.prepareObject(id, object); err != nil {
all.Update, all.Reflow, all.Copy, all.Reason = false, false, false, err.Error()
}
if !e.sourceRGB() {
all.Update, all.Reflow, all.Copy, all.Reason = false, false, false, "document uses a non-RGB default color space"
}
if object.Type == "TextObject" {
all.Arrange = all.Update
if _, err := object.TextObject.TextFrame(); err != nil {
all.Reflow = false
}
}
return all, nil
}
// objectOrigin 获取原对象或其副本的保真来源。
// 入参: id 对象标识
// 返回: *editorObjectOrigin 原始对象,无来源时为nil
func (e *Editor) objectOrigin(id string) *editorObjectOrigin {
if e.source == nil {
return nil
}
return e.source.origins[id]
}
// sourceLayerOrderable 判断图层是否仅含可独立排序的直接对象。
// 入参: page 页面索引, id 图层标识
// 返回: bool 是否可排序
func (e *Editor) sourceLayerOrderable(page int, id string) bool {
if !e.originalPage(page) {
return true
}
content := e.source.pages[e.pages[page].ID].root.child("Content")
if content != nil {
for _, layer := range content.children {
if layer.attr("ID") == id {
for _, child := range layer.children {
if !editorXMLSupported(child) {
return false
}
}
}
}
}
return true
}
// editorGeometry 获取基本对象的边界与变换。
// 入参: object 对象
// 返回: string 边界, string 变换矩阵
func editorGeometry(object GraphicObject) (string, string) {
switch object.Type {
case "TextObject":
return object.TextObject.Boundary, object.TextObject.CTM
case "PathObject":
return object.PathObject.Boundary, object.PathObject.CTM
case "ImageObject":
return object.ImageObject.Boundary, object.ImageObject.CTM
}
return "", ""
}
// editorFont 按需解析原文档内嵌字体,不用系统回退替换原字体。
// 入参: id 字体资源标识
// 返回: *font.SFNT 字体, error 错误信息
func (e *Editor) editorFont(id string) (*font.SFNT, error) {
if sfnt := e.fonts[id]; sfnt != nil {
return sfnt, nil
}
if e.source != nil {
reader := e.source.reader
if f := reader.fontCache[id]; f != nil && f.FontFile != "" {
data, err := reader.ResData(f.FontFile)
if err != nil {
return nil, err
}
data, err = font.ToSFNT(data)
if err != nil {
return nil, err
}
sfnt, err := font.ParseSFNT(data, 0)
if err != nil {
return nil, err
}
e.fonts[id] = sfnt
return sfnt, nil
}
}
return nil, fmt.Errorf("embedded font %q not found", id)
}
// editorImage 按需读取原图片尺寸,不解码完整像素。
// 入参: id 图片资源标识
// 返回: image.Point 图片尺寸, error 错误信息
func (e *Editor) editorImage(id string) (image.Point, error) {
if size := e.images[id]; size.X > 0 {
return size, nil
}
if e.source == nil {
return image.Point{}, fmt.Errorf("image resource %q not found", id)
}
reader := e.source.reader
input, err := reader.openFile(reader.ResPath(reader.ResMap[id]))
if err != nil {
return image.Point{}, err
}
defer input.Close()
config, _, err := image.DecodeConfig(input)
if err != nil {
return image.Point{}, err
}
size := image.Pt(config.Width, config.Height)
e.images[id] = size
return size, nil
}
// cloneEditorData 深复制编辑快照,未导出的排版信息保持不可变共享。
// 入参: value 原值
// 返回: T 独立副本
func cloneEditorData[T any](value T) T {
return cloneEditorValue(reflect.ValueOf(value)).Interface().(T)
}
// cloneEditorValue 复制结构体中的可变指针和切片。
// 入参: value 原值
// 返回: reflect.Value 独立副本
func cloneEditorValue(value reflect.Value) reflect.Value {
switch value.Kind() {
case reflect.Pointer:
if !value.IsNil() {
result := reflect.New(value.Type().Elem())
result.Elem().Set(cloneEditorValue(value.Elem()))
return result
}
case reflect.Slice:
if !value.IsNil() {
result := reflect.MakeSlice(value.Type(), value.Len(), value.Len())
for i := range value.Len() {
result.Index(i).Set(cloneEditorValue(value.Index(i)))
}
return result
}
case reflect.Struct:
result := reflect.New(value.Type()).Elem()
result.Set(value)
for i := range value.NumField() {
if result.Field(i).CanSet() && value.Type().Field(i).IsExported() {
result.Field(i).Set(cloneEditorValue(value.Field(i)))
}
}
return result
}
return value
}
// editorObjectXML 编码可编辑对象,包含独立命名空间声明。
// 入参: object 对象
// 返回: []byte XML片段, error 错误信息
func editorObjectXML(object GraphicObject) ([]byte, error) {
data, err := encodeOFDXML(func(x *ofdXML) { x.object(object, true) })
return bytes.TrimPrefix(data, []byte(xml.Header)), err
}
// validateEditorGeometry 校验几何操作产生的数值,不重新排版或替换字体。
// 入参: object 变换后的对象
// 返回: error 错误信息
func validateEditorGeometry(object GraphicObject) error {
boundary, ctm := editorGeometry(object)
if _, err := creationBox(boundary); err != nil {
return err
}
if ctm != "" {
if _, err := creationNumbers(ctm, 6); err != nil {
return err
}
}
if object.Type == "TextObject" {
obj := object.TextObject
if !finite(obj.Size) || obj.Size <= 0 || !finite(obj.LineWidth) {
return fmt.Errorf("invalid text geometry")
}
for _, code := range obj.TextCode {
for _, value := range []string{code.X, code.Y} {
if value != "" {
if _, err := creationNumbers(value, 1); err != nil {
return err
}
}
}
for _, value := range []string{code.DeltaX, code.DeltaY} {
if value != "" {
if err := creationDeltas(value, len(textCodeRunes(code.Value))-1); err != nil {
return err
}
}
}
}
}
return nil
}
+450
View File
@@ -0,0 +1,450 @@
// 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 (
"archive/zip"
"bytes"
"encoding/xml"
"fmt"
"io"
"maps"
"path"
"reflect"
"slices"
"strconv"
"strings"
)
// sourceParts 生成编辑过的XML和新增资源,不读取未修改页面或原始二进制资源。
// 返回: map[string][]byte 替换及新增条目, error 错误信息
func (e *Editor) sourceParts() (map[string][]byte, error) {
parts := make(map[string][]byte)
source := e.source
reader := source.reader
pageRefs := make([]Page, len(e.pages))
for i, page := range e.pages {
original := source.pages[page.ID]
if original != nil {
pageRefs[i] = original.ref
if original.original == nil {
continue
}
data, err := e.sourcePageXML(i, original)
if err != nil {
return nil, err
}
if !bytes.Equal(data, original.data) {
parts[reader.ResPath(original.ref.BaseLoc)] = data
}
} else {
name := path.Join(source.directory, "Pages", page.ID+".xml")
data, err := e.sourceNewPageXML(page)
if err != nil {
return nil, err
}
parts[name] = data
pageRefs[i] = Page{ID: page.ID, BaseLoc: "/" + name}
}
}
fonts, images, resourceFiles := e.usedResources()
commonData := source.document.CommonData
for _, name := range append(slices.Clone(commonData.DocumentRes), commonData.PublicRes...) {
resourceFiles = slices.DeleteFunc(resourceFiles, func(file string) bool {
return strings.EqualFold(file, reader.ResPath(name))
})
}
if len(fonts)+len(images) != 0 {
resourcePath := path.Join(source.directory, "Resources.xml")
resourceFiles = append(resourceFiles, resourcePath)
data, err := encodeOFDXML(func(x *ofdXML) { x.resources(fonts, images) })
if err != nil {
return nil, err
}
parts[resourcePath] = data
for _, resource := range append(fonts, images...) {
if resource.name != "" {
parts[resource.name] = resource.data
}
}
}
pagesChanged := !reflect.DeepEqual(pageRefs, source.document.Pages.Page)
if len(parts) != 0 || pagesChanged {
name := reader.ResPath(reader.OFD.DocBody[0].DocRoot)
data, err := reader.readFile(name)
if err != nil {
return nil, err
}
root, err := parseEditorXML(data)
if err != nil {
return nil, err
}
common := root.child("CommonData")
if common == nil {
return nil, fmt.Errorf("document has no CommonData")
}
var patches []editorXMLPatch
if e.maxID != source.document.CommonData.MaxUnitID {
value := editorXMLText("MaxUnitID", strconv.Itoa(e.maxID))
if maxID := common.child("MaxUnitID"); maxID != nil {
patches = append(patches, editorXMLPatch{maxID.start, maxID.end, value})
} else {
patches = append(patches, editorXMLPatch{common.open, common.open, value})
}
}
if len(resourceFiles) != 0 {
position := common.close
for _, child := range common.children {
if child.name.Local == "TemplatePage" || child.name.Local == "DefaultCS" {
position = child.start
break
}
}
var references []byte
for _, name := range resourceFiles {
references = append(references, editorXMLText("DocumentRes", "/"+name)...)
}
patches = append(patches, editorXMLPatch{position, position, references})
}
if pagesChanged {
pages := root.child("Pages")
if pages == nil {
return nil, fmt.Errorf("document has no Pages")
}
var encoded bytes.Buffer
for _, page := range pageRefs {
var original *editorXML
for _, child := range pages.children {
if child.attr("ID") == page.ID {
original = child
break
}
}
if original != nil {
encoded.Write(data[original.start:original.end])
} else {
item, err := encodeOFDXML(func(x *ofdXML) {
x.root("Page", ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: page.ID}, {Name: xml.Name{Local: "BaseLoc"}, Value: page.BaseLoc}})
x.end("Page")
})
if err != nil {
return nil, err
}
encoded.Write(bytes.TrimPrefix(item, []byte(xml.Header)))
}
}
patches = append(patches, editorXMLContent(data, pages, encoded.Bytes()))
}
updated := editorPatchXML(data, patches)
if !bytes.Equal(data, updated) {
parts[name] = updated
}
}
if !reflect.DeepEqual(e.Info, source.info) {
data, err := e.sourceInfoXML()
if err != nil {
return nil, err
}
parts["OFD.xml"] = data
}
for name, data := range maps.Clone(parts) {
if file, ok := reader.packageFile(name); ok {
actual := cleanPackagePath(file.Name)
if actual != name {
delete(parts, name)
parts[actual] = data
}
}
}
return parts, nil
}
// sourcePageXML 仅改写已修改的直接对象、页面尺寸和新增图层。
// 入参: index 页面索引, source 原页面
// 返回: []byte 页面XML, error 错误信息
func (e *Editor) sourcePageXML(index int, source *editorSourcePage) ([]byte, error) {
page := e.pages[index]
if reflect.DeepEqual(page, *source.original) {
return source.data, nil
}
if source.root.open == source.root.end {
copy := *source
copy.data = editorPatchXML(source.data, []editorXMLPatch{editorXMLContent(source.data, source.root, nil)})
var err error
copy.root, err = parseEditorXML(copy.data)
if err != nil {
return nil, err
}
source = &copy
}
data := source.data
var patches []editorXMLPatch
if page.Area.PhysicalBox != source.original.Area.PhysicalBox {
area := source.root.child("Area")
if area != nil {
areaData := data[area.start:area.end]
areaRoot, err := parseEditorXML(areaData)
if err != nil {
return nil, err
}
updated := editorXMLSetText(areaData, areaRoot, [][2]string{{"PhysicalBox", page.Area.PhysicalBox}})
patches = append(patches, editorXMLPatch{area.start, area.end, updated})
} else {
value := []byte("<ofd:Area xmlns:ofd=\"" + ofdNamespace + "\">")
value = append(value, editorXMLText("PhysicalBox", page.Area.PhysicalBox)...)
value = append(value, []byte("</ofd:Area>")...)
patches = append(patches, editorXMLPatch{source.root.open, source.root.open, value})
}
}
for layerIndex, original := range source.original.Content.Layer {
current := page.Content.Layer[layerIndex]
before := make(map[string]GraphicObject)
for _, object := range original.Objects {
before[editorObjectID(object)] = object
}
after := make(map[string]GraphicObject)
var ordered []GraphicObject
for _, object := range current.Objects {
id := editorObjectID(object)
after[id] = object
if source.nodes[id] != nil {
ordered = append(ordered, object)
}
}
orderable := e.sourceLayerOrderable(index, original.ID)
position := 0
for _, object := range original.Objects {
id := editorObjectID(object)
node := source.nodes[id]
if node == nil {
continue
}
next, exists := after[id]
if orderable {
exists = position < len(ordered)
if exists {
next = ordered[position]
}
position++
}
var encoded []byte
if exists {
nextID := editorObjectID(next)
origin := source.nodes[nextID]
if reflect.DeepEqual(before[nextID], next) {
encoded = data[origin.start:origin.end]
} else {
var err error
encoded, err = editorXMLObject(data, origin, before[nextID], next)
if err != nil {
return nil, err
}
}
}
if !bytes.Equal(encoded, data[node.start:node.end]) {
patches = append(patches, editorXMLPatch{node.start, node.end, encoded})
}
}
}
var added []byte
for _, layer := range page.Content.Layer[len(source.original.Content.Layer):] {
if len(layer.Objects) == 0 {
continue
}
encoded, err := e.sourceLayerXML(layer)
if err != nil {
return nil, err
}
added = append(added, bytes.TrimPrefix(encoded, []byte(xml.Header))...)
}
if len(added) != 0 {
content := source.root.child("Content")
if content == nil {
value := []byte("<ofd:Content xmlns:ofd=\"" + ofdNamespace + "\">")
value = append(value, added...)
value = append(value, []byte("</ofd:Content>")...)
patches = append(patches, editorXMLPatch{source.root.close, source.root.close, value})
} else if content.open == content.end {
patches = append(patches, editorXMLContent(data, content, added))
} else {
patches = append(patches, editorXMLPatch{content.close, content.close, added})
}
}
return editorPatchXML(data, patches), nil
}
// sourceLayerXML 写出新增图层,对复制对象保留原始定位和显式默认值。
// 入参: layer 图层
// 返回: []byte 图层XML, error 错误信息
func (e *Editor) sourceLayerXML(layer Layer) ([]byte, error) {
var content []byte
for _, object := range layer.Objects {
var data []byte
var err error
if origin := e.objectOrigin(editorObjectID(object)); origin != nil {
data, err = editorXMLObject(origin.page.data, origin.node, origin.object, object)
if err == nil {
data, err = editorXMLStandalone(data, origin.node)
}
} else {
data, err = editorObjectXML(object)
}
if err != nil {
return nil, err
}
content = append(content, data...)
}
return editorXMLContainer("Layer", ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: layer.ID}, {Name: xml.Name{Local: "Type"}, Value: layer.Type}}, content)
}
// sourceNewPageXML 编码新增页面,同时保留复制对象的原文语义。
// 入参: page 页面
// 返回: []byte 页面XML, error 错误信息
func (e *Editor) sourceNewPageXML(page PageContent) ([]byte, error) {
area, err := editorXMLContainer("Area", nil, editorXMLText("PhysicalBox", page.Area.PhysicalBox))
if err != nil {
return nil, err
}
var layers []byte
for _, layer := range page.Content.Layer {
data, err := e.sourceLayerXML(layer)
if err != nil {
return nil, err
}
layers = append(layers, data...)
}
content, err := editorXMLContainer("Content", nil, layers)
if err != nil {
return nil, err
}
return editorXMLContainer("Page", nil, append(area, content...))
}
// sourceInfoXML 更新公开元数据,保留原创建程序和未识别字段。
// 返回: []byte 根索引XML, error 错误信息
func (e *Editor) sourceInfoXML() ([]byte, error) {
data, err := e.source.reader.readFile("OFD.xml")
if err != nil {
return nil, err
}
root, err := parseEditorXML(data)
if err != nil {
return nil, err
}
body := root.child("DocBody")
if body == nil {
return nil, fmt.Errorf("document has no DocBody")
}
info := body.child("DocInfo")
if info == nil {
return nil, fmt.Errorf("document has no DocInfo")
}
before, after := e.source.info, e.Info
if !reflect.DeepEqual(before.CustomDatas, after.CustomDatas) {
return nil, fmt.Errorf("editing imported custom metadata is not supported")
}
var values [][2]string
for _, field := range [][3]string{
{"DocID", before.DocID, after.DocID}, {"Title", before.Title, after.Title}, {"Author", before.Author, after.Author},
{"Subject", before.Subject, after.Subject}, {"Abstract", before.Abstract, after.Abstract},
{"CreationDate", before.CreationDate, after.CreationDate}, {"ModDate", before.ModDate, after.ModDate},
} {
if field[1] != field[2] {
values = append(values, [2]string{field[0], field[2]})
}
}
return editorXMLSetText(data, info, values), nil
}
// writeSource 将未修改ZIP条目直接复制到新包,逐项写入改动,不持有原资源解压副本。
// 入参: writer 输出流
// 返回: int64 写入字节数, error 错误信息
func (e *Editor) writeSource(writer io.Writer) (int64, error) {
parts, err := e.sourceParts()
if err != nil {
return 0, err
}
reader := e.source.reader
remaining := maps.Clone(reader.files)
if remaining == nil {
remaining = make(map[string][]byte)
}
maps.Copy(remaining, parts)
output := &ofdCountingWriter{writer: writer}
archive := zip.NewWriter(output)
if reader.Zip != nil {
_ = archive.SetComment(reader.Zip.Comment)
for _, file := range reader.Zip.File {
if file.FileInfo().IsDir() {
header := file.FileHeader
if _, err := archive.CreateHeader(&header); err != nil {
return output.count, err
}
continue
}
name := cleanPackagePath(file.Name)
if data, ok := remaining[name]; ok {
header := file.FileHeader
entry, err := archive.CreateHeader(&header)
if err != nil {
return output.count, err
}
if _, err := entry.Write(data); err != nil {
return output.count, err
}
delete(remaining, name)
} else if err := archive.Copy(file); err != nil {
return output.count, err
}
}
}
for _, name := range slices.Sorted(maps.Keys(remaining)) {
method := uint16(zip.Store)
if strings.HasSuffix(strings.ToLower(name), ".xml") {
method = zip.Deflate
}
entry, err := archive.CreateHeader(&zip.FileHeader{Name: name, Method: method})
if err != nil {
return output.count, err
}
if _, err := entry.Write(remaining[name]); err != nil {
return output.count, err
}
}
err = archive.Close()
return output.count, err
}
// sourceReader 生成原包与修改条目组成的独立预览快照,不进行ZIP压缩。
// 返回: *Reader 阅读器, error 错误信息
func (e *Editor) sourceReader() (*Reader, error) {
parts, err := e.sourceParts()
if err != nil {
return nil, err
}
files := maps.Clone(e.source.reader.files)
if files == nil {
files = make(map[string][]byte)
}
maps.Copy(files, parts)
reader := &Reader{Zip: e.source.reader.Zip, files: files}
if err := reader.initRoot(); err != nil {
return nil, err
}
if _, err := reader.Doc(); err != nil {
return nil, err
}
return reader, nil
}
+16 -2
View File
@@ -91,8 +91,13 @@ func (e *Editor) orientObjects(page int, ids []string, matrix Matrix) error {
boundary, ctm = &object.PathObject.Boundary, &object.PathObject.CTM boundary, ctm = &object.PathObject.Boundary, &object.PathObject.CTM
case "ImageObject": case "ImageObject":
boundary, ctm = &object.ImageObject.Boundary, &object.ImageObject.CTM boundary, ctm = &object.ImageObject.Boundary, &object.ImageObject.CTM
default:
return fmt.Errorf("unsupported object type %q", object.Type)
} }
before, _ := ParseBox(*boundary) before, _ := ParseBox(*boundary)
if object.Type == "ImageObject" && *ctm == "" {
*ctm = Matrix{a: before.W, d: before.H}.String()
}
after := matrix.TransformBox(before) after := matrix.TransformBox(before)
local := TranslationMatrix(-after.X, -after.Y).Multiply(matrix).Multiply(TranslationMatrix(before.X, before.Y)) local := TranslationMatrix(-after.X, -after.Y).Multiply(matrix).Multiply(TranslationMatrix(before.X, before.Y))
*ctm = local.Multiply(NewMatrix(*ctm)).String() *ctm = local.Multiply(NewMatrix(*ctm)).String()
@@ -102,7 +107,7 @@ func (e *Editor) orientObjects(page int, ids []string, matrix Matrix) error {
} }
objects[i] = object objects[i] = object
} }
return e.UpdateObjects(page, objects) return e.updateObjects(page, objects, true)
} }
// transformImageClips 同步变换不随对象CTM变化的裁剪区域,不修改原始裁剪数据。 // transformImageClips 同步变换不随对象CTM变化的裁剪区域,不修改原始裁剪数据。
@@ -174,6 +179,9 @@ func cropImageObject(image *ImageObject, box Box) error {
return fmt.Errorf("crop must stay within the image bounds") return fmt.Errorf("crop must stay within the image bounds")
} }
before, _ := ParseBox(image.Boundary) before, _ := ParseBox(image.Boundary)
if image.CTM == "" {
image.CTM = Matrix{a: before.W, d: before.H}.String()
}
if box.X != before.X || box.Y != before.Y { if box.X != before.X || box.Y != before.Y {
image.CTM = TranslationMatrix(before.X-box.X, before.Y-box.Y).Multiply(NewMatrix(image.CTM)).String() image.CTM = TranslationMatrix(before.X-box.X, before.Y-box.Y).Multiply(NewMatrix(image.CTM)).String()
} }
@@ -225,10 +233,16 @@ func (e *Editor) fitImage(obj *ImageObject, mode string) error {
return err return err
} }
m := NewMatrix(obj.CTM) m := NewMatrix(obj.CTM)
if obj.CTM == "" {
m = Matrix{a: box.W, d: box.H}
}
if !axisAlignedMatrix(m) { if !axisAlignedMatrix(m) {
return fmt.Errorf("image fitting requires an axis-aligned transform") return fmt.Errorf("image fitting requires an axis-aligned transform")
} }
size := e.images[obj.ResourceID] size, err := e.editorImage(obj.ResourceID)
if err != nil {
return err
}
w, h := float64(size.X), float64(size.Y) w, h := float64(size.X), float64(size.Y)
if m.a == 0 { if m.a == 0 {
w, h = h, w w, h = h, w
+417
View File
@@ -0,0 +1,417 @@
// 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"
"encoding/xml"
"fmt"
"io"
"slices"
"strings"
)
// editorXML 保存XML节点的原文位置,不重写未修改的内容。
type editorXML struct {
name xml.Name
attrs []xml.Attr
start, open, close, end int
children []*editorXML
parent *editorXML
}
// editorXMLPatch 替换原文中的连续区间。
type editorXMLPatch struct {
start, end int
data []byte
}
// parseEditorXML 解析节点位置,保留命名空间、注释和原始格式。
// 入参: data XML原文
// 返回: *editorXML 根节点, error 错误信息
func parseEditorXML(data []byte) (*editorXML, error) {
d := xml.NewDecoder(bytes.NewReader(data))
var root *editorXML
var stack []*editorXML
for {
start := int(d.InputOffset())
token, err := d.Token()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
switch token := token.(type) {
case xml.StartElement:
node := &editorXML{name: token.Name, attrs: token.Attr, start: start, open: int(d.InputOffset())}
if len(stack) == 0 {
if root != nil {
return nil, fmt.Errorf("multiple XML roots")
}
root = node
} else {
parent := stack[len(stack)-1]
node.parent = parent
parent.children = append(parent.children, node)
}
stack = append(stack, node)
case xml.EndElement:
node := stack[len(stack)-1]
node.close, node.end = start, int(d.InputOffset())
stack = stack[:len(stack)-1]
}
}
if root == nil {
return nil, fmt.Errorf("empty XML document")
}
return root, nil
}
// child 查找OFD直接子节点。
// 入参: name 节点名称
// 返回: *editorXML 节点,不存在时为nil
func (n *editorXML) child(name string) *editorXML {
for _, child := range n.children {
if child.name.Local == name && (child.name.Space == n.name.Space || child.name.Space == ofdNamespace || child.name.Space == "") {
return child
}
}
return nil
}
// attr 获取无命名空间的属性值。
// 入参: name 属性名称
// 返回: string 属性值
func (n *editorXML) attr(name string) string {
for _, attr := range n.attrs {
if attr.Name.Space == "" && attr.Name.Local == name {
return attr.Value
}
}
return ""
}
// editorPatchXML 按位置应用互不重叠的替换。
// 入参: data 原文, patches 修改区间
// 返回: []byte 修改后的XML
func editorPatchXML(data []byte, patches []editorXMLPatch) []byte {
slices.SortStableFunc(patches, func(a, b editorXMLPatch) int {
if a.start != b.start {
return a.start - b.start
}
return a.end - b.end
})
var result bytes.Buffer
position := 0
for _, patch := range patches {
result.Write(data[position:patch.start])
result.Write(patch.data)
position = patch.end
}
result.Write(data[position:])
return result.Bytes()
}
// editorXMLContent 替换节点内部内容,兼容自闭合节点。
// 入参: data 原文, node 节点, content 新内容
// 返回: editorXMLPatch 修改区间
func editorXMLContent(data []byte, node *editorXML, content []byte) editorXMLPatch {
if node.open != node.end {
return editorXMLPatch{node.open, node.close, content}
}
header := data[node.start:node.open]
end := bytes.IndexAny(header[1:], " \t\r\n/>") + 1
name := header[1:end]
replacement := append(bytes.Clone(header[:len(header)-2]), '>')
replacement = append(replacement, content...)
replacement = append(replacement, []byte("</"+string(name)+">")...)
return editorXMLPatch{node.start, node.end, replacement}
}
// editorXMLText 编码带独立命名空间的文本节点。
// 入参: name 节点名称, value 文本
// 返回: []byte XML片段
func editorXMLText(name, value string) []byte {
var escaped bytes.Buffer
_ = xml.EscapeText(&escaped, []byte(value))
return []byte("<ofd:" + name + " xmlns:ofd=\"" + ofdNamespace + "\">" + escaped.String() + "</ofd:" + name + ">")
}
// editorXMLSetText 更新或追加直接子节点,保留其他节点。
// 入参: data 原文, node 父节点, values 需要修改的字段
// 返回: []byte 修改后的XML
func editorXMLSetText(data []byte, node *editorXML, values [][2]string) []byte {
var patches []editorXMLPatch
var added []byte
order := []string{"PhysicalBox", "ApplicationBox", "ContentBox", "BleedBox"}
if node.name.Local == "DocInfo" {
order = []string{"DocID", "Title", "Author", "Subject", "Abstract", "CreationDate", "ModDate", "DocUsage", "Cover", "Keywords", "Creator", "CreatorVersion", "CustomDatas"}
}
for _, value := range values {
child := node.child(value[0])
var encoded []byte
if value[1] != "" {
encoded = editorXMLText(value[0], value[1])
}
if child != nil {
patches = append(patches, editorXMLPatch{child.start, child.end, encoded})
} else if node.open == node.end {
added = append(added, encoded...)
} else if len(encoded) != 0 {
position := node.close
for _, next := range node.children {
if slices.Index(order, next.name.Local) > slices.Index(order, value[0]) {
position = next.start
break
}
}
patches = append(patches, editorXMLPatch{position, position, encoded})
}
}
if len(added) != 0 {
patches = append(patches, editorXMLContent(data, node, added))
}
return editorPatchXML(data, patches)
}
// editorXMLSupported 判断原文是否仅包含创建器能够保留的对象字段。
// 入参: node 对象或子节点
// 返回: bool 是否支持完整编辑
func editorXMLSupported(node *editorXML) bool {
if node.name.Space != "" && node.name.Space != ofdNamespace {
return false
}
var allowed, children string
switch node.name.Local {
case "TextObject":
allowed, children = "ID Boundary CTM Font Size HScale Weight ReadDirection CharDirection LineWidth MiterLimit Join Italic Visible Stroke Fill Alpha", "FillColor StrokeColor TextCode"
case "PathObject", "Path":
allowed, children = "Boundary CTM LineWidth MiterLimit Join Cap Rule DashPattern DashOffset Visible Stroke Fill Alpha", "StrokeColor FillColor AbbreviatedData"
if node.name.Local == "PathObject" {
allowed += " ID"
}
case "ImageObject":
allowed, children = "ID Boundary CTM ResourceID ImageMask Visible Alpha", "Clips"
case "TextCode":
allowed = "X Y DeltaX DeltaY"
case "FillColor", "StrokeColor":
allowed = "Value Alpha"
case "Clips":
allowed, children = "TransFlag", "Clip"
case "Clip":
children = "Area"
case "Area":
allowed, children = "CTM", "Path"
case "AbbreviatedData":
default:
return false
}
for _, attr := range node.attrs {
if attr.Name.Space == "xmlns" || attr.Name.Local == "xmlns" && attr.Name.Space == "" {
continue
}
if attr.Name.Space != "" || !slices.Contains(strings.Fields(allowed), attr.Name.Local) {
return false
}
}
for _, child := range node.children {
if !slices.Contains(strings.Fields(children), child.name.Local) || !editorXMLSupported(child) {
return false
}
}
return true
}
// editorXMLObject 修改对象发生变化的属性与内容,保留原文中的显式默认值。
// 入参: data 页面原文, node 原对象节点, before 原对象, after 新对象
// 返回: []byte 对象XML, error 错误信息
func editorXMLObject(data []byte, node *editorXML, before, after GraphicObject) ([]byte, error) {
if before.Type != after.Type {
return editorObjectXML(after)
}
oldXML, err := editorObjectXML(before)
if err != nil {
return nil, err
}
newXML, err := editorObjectXML(after)
if err != nil {
return nil, err
}
return editorXMLMerge(data, node, oldXML, newXML)
}
// editorXMLMerge 按标准编码的差异更新原节点,递归保留未变化的显式默认值与命名空间。
// 入参: data 原文, node 原节点, oldXML 修改前编码, newXML 修改后编码
// 返回: []byte 更新节点, error 错误信息
func editorXMLMerge(data []byte, node *editorXML, oldXML, newXML []byte) ([]byte, error) {
oldNode, err := parseEditorXML(oldXML)
if err != nil {
return nil, err
}
newNode, err := parseEditorXML(newXML)
if err != nil {
return nil, err
}
changes := make(map[string]string)
for _, attr := range append(oldNode.attrs, newNode.attrs...) {
if attr.Name.Space == "" && oldNode.attr(attr.Name.Local) != newNode.attr(attr.Name.Local) {
changes[attr.Name.Local] = newNode.attr(attr.Name.Local)
}
}
contentChanged := !bytes.Equal(oldXML[oldNode.open:oldNode.close], newXML[newNode.open:newNode.close])
if len(changes) == 0 && !contentChanged {
return bytes.Clone(data[node.start:node.end]), nil
}
d := xml.NewDecoder(bytes.NewReader(data[node.start:node.open]))
token, err := d.RawToken()
if err != nil {
return nil, err
}
start := token.(xml.StartElement)
var result bytes.Buffer
name := start.Name.Local
if start.Name.Space != "" {
name = start.Name.Space + ":" + name
}
result.WriteString("<" + name)
for _, attr := range start.Attr {
if attr.Name.Space == "" {
if value, ok := changes[attr.Name.Local]; ok {
attr.Value = value
delete(changes, attr.Name.Local)
if value == "" {
continue
}
}
}
if contentChanged && attr.Name.Space == "xmlns" && attr.Name.Local == "ofd" {
continue
}
key := attr.Name.Local
if attr.Name.Space != "" {
key = attr.Name.Space + ":" + key
}
result.WriteString(" " + key + "=\"")
_ = xml.EscapeText(&result, []byte(attr.Value))
result.WriteByte('"')
}
for _, attr := range newNode.attrs {
if value, ok := changes[attr.Name.Local]; ok && value != "" {
result.WriteString(" " + attr.Name.Local + "=\"")
_ = xml.EscapeText(&result, []byte(value))
result.WriteByte('"')
}
}
if contentChanged {
result.WriteString(" xmlns:ofd=\"" + ofdNamespace + "\"")
}
result.WriteByte('>')
if contentChanged {
matching := len(node.children) > 0 && len(node.children) == len(oldNode.children) && len(node.children) == len(newNode.children)
if matching {
for i, child := range node.children {
if child.name.Local != oldNode.children[i].name.Local || child.name.Local != newNode.children[i].name.Local {
matching = false
break
}
}
}
if matching {
position := node.open
for i, child := range node.children {
before, after := oldNode.children[i], newNode.children[i]
updated, err := editorXMLMerge(data, child, oldXML[before.start:before.end], newXML[after.start:after.end])
if err != nil {
return nil, err
}
result.Write(data[position:child.start])
result.Write(updated)
position = child.end
}
result.Write(data[position:node.close])
} else {
result.Write(newXML[newNode.open:newNode.close])
}
} else if node.open != node.end {
result.Write(data[node.open:node.close])
}
result.WriteString("</" + name + ">")
return result.Bytes(), nil
}
// editorXMLContainer 用标准编码器封装已编码的子节点。
// 入参: name 节点名称, attrs 属性, content 子节点XML
// 返回: []byte XML片段, error 错误信息
func editorXMLContainer(name string, attrs ofdAttrs, content []byte) ([]byte, error) {
var output bytes.Buffer
attrs.add("xmlns:ofd", ofdNamespace)
value := struct {
Content []byte `xml:",innerxml"`
}{content}
err := xml.NewEncoder(&output).EncodeElement(value, xml.StartElement{Name: xml.Name{Local: "ofd:" + name}, Attr: attrs})
return output.Bytes(), err
}
// editorXMLStandalone 补齐原对象继承的命名空间,使跨页复制不依赖原父节点。
// 入参: data 对象XML, source 原节点
// 返回: []byte 独立XML片段, error 错误信息
func editorXMLStandalone(data []byte, source *editorXML) ([]byte, error) {
root, err := parseEditorXML(data)
if err != nil {
return nil, err
}
namespaces := make(map[string]string)
for node := source.parent; node != nil; node = node.parent {
for _, attr := range node.attrs {
key := ""
if attr.Name.Space == "xmlns" {
key = "xmlns:" + attr.Name.Local
}
if attr.Name.Space == "" && attr.Name.Local == "xmlns" {
key = "xmlns"
}
if key != "" {
if _, exists := namespaces[key]; !exists {
namespaces[key] = attr.Value
}
}
}
}
for _, attr := range root.attrs {
if attr.Name.Space == "xmlns" {
delete(namespaces, "xmlns:"+attr.Name.Local)
}
if attr.Name.Space == "" && attr.Name.Local == "xmlns" {
delete(namespaces, "xmlns")
}
}
var added bytes.Buffer
keys := make([]string, 0, len(namespaces))
for key := range namespaces {
keys = append(keys, key)
}
slices.Sort(keys)
for _, key := range keys {
added.WriteString(" " + key + "=\"")
_ = xml.EscapeText(&added, []byte(namespaces[key]))
added.WriteByte('"')
}
position := root.open - 1
if data[position-1] == '/' {
position--
}
return editorPatchXML(data, []editorXMLPatch{{position, position, added.Bytes()}}), nil
}
+10
View File
@@ -34,6 +34,7 @@ type Reader struct {
RootDir string RootDir string
ResMap map[string]string ResMap map[string]string
resourcesRead map[string]bool resourcesRead map[string]bool
resourceFiles map[string]string
fontCache map[string]*Font fontCache map[string]*Font
fontResourcesRead bool fontResourcesRead bool
fontFilesChecked map[string]bool fontFilesChecked map[string]bool
@@ -84,6 +85,7 @@ func (r *Reader) initRoot() error {
r.OFD = &ofd r.OFD = &ofd
r.ResMap = make(map[string]string) r.ResMap = make(map[string]string)
r.resourcesRead = make(map[string]bool) r.resourcesRead = make(map[string]bool)
r.resourceFiles = make(map[string]string)
r.fontCache = make(map[string]*Font) r.fontCache = make(map[string]*Font)
r.fontFilesChecked = make(map[string]bool) r.fontFilesChecked = make(map[string]bool)
r.colorSpaceCache = make(map[string]*ColorSpace) r.colorSpaceCache = make(map[string]*ColorSpace)
@@ -102,6 +104,9 @@ func (r *Reader) readFile(name string) ([]byte, error) {
return bytes.Clone(data), nil return bytes.Clone(data), nil
} }
if f, ok := r.packageFile(name); ok { if f, ok := r.packageFile(name); ok {
if data, ok := r.files[cleanPackagePath(f.Name)]; ok {
return bytes.Clone(data), nil
}
return readZipFile(f) return readZipFile(f)
} }
return nil, fmt.Errorf("file not found: %s", name) return nil, fmt.Errorf("file not found: %s", name)
@@ -116,6 +121,9 @@ func (r *Reader) openFile(name string) (io.ReadCloser, error) {
return io.NopCloser(bytes.NewReader(data)), nil return io.NopCloser(bytes.NewReader(data)), nil
} }
if f, ok := r.packageFile(name); ok { if f, ok := r.packageFile(name); ok {
if data, ok := r.files[cleanPackagePath(f.Name)]; ok {
return io.NopCloser(bytes.NewReader(data)), nil
}
return f.Open() return f.Open()
} }
return nil, fmt.Errorf("file not found: %s", name) return nil, fmt.Errorf("file not found: %s", name)
@@ -217,6 +225,7 @@ func (r *Reader) loadRes(resPath string) {
if mm.MediaFile != "" { if mm.MediaFile != "" {
if finalPath := resolveResourcePath(resPath, baseLoc, mm.MediaFile); finalPath != "" { if finalPath := resolveResourcePath(resPath, baseLoc, mm.MediaFile); finalPath != "" {
r.ResMap[mm.ID] = finalPath r.ResMap[mm.ID] = finalPath
r.resourceFiles[mm.ID] = fullPath
} }
} }
} }
@@ -226,6 +235,7 @@ func (r *Reader) loadRes(resPath string) {
f.FontFile = resolveResourcePath(resPath, baseLoc, f.FontFile) f.FontFile = resolveResourcePath(resPath, baseLoc, f.FontFile)
} }
r.fontCache[f.ID] = f r.fontCache[f.ID] = f
r.resourceFiles[f.ID] = fullPath
} }
for i := range res.DrawParams.DrawParam { for i := range res.DrawParams.DrawParam {
dp := &res.DrawParams.DrawParam[i] dp := &res.DrawParams.DrawParam[i]
+3
View File
@@ -440,6 +440,9 @@ func parseSignatureFile(data []byte) (*SignatureFile, error) {
// 返回: []byte 文件数据, error 错误信息 // 返回: []byte 文件数据, error 错误信息
func (r *Reader) readFileExact(name string) ([]byte, error) { func (r *Reader) readFileExact(name string) ([]byte, error) {
name = cleanPackagePath(name) name = cleanPackagePath(name)
if data, ok := r.files[name]; ok {
return bytes.Clone(data), nil
}
if f, ok := r.fileIndex[name]; ok { if f, ok := r.fileIndex[name]; ok {
return readZipFile(f) return readZipFile(f)
} }
+58 -9
View File
@@ -21,6 +21,8 @@ import (
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"io" "io"
"maps"
"slices"
"strconv" "strconv"
"time" "time"
) )
@@ -34,6 +36,9 @@ func (e *Editor) WriteTo(writer io.Writer) (int64, error) {
if err := e.validate(); err != nil { if err := e.validate(); err != nil {
return 0, err return 0, err
} }
if e.source != nil {
return e.writeSource(writer)
}
output := &ofdCountingWriter{writer: writer} output := &ofdCountingWriter{writer: writer}
archive := zip.NewWriter(output) archive := zip.NewWriter(output)
err := e.writeParts(func(name string, data []byte, compressed bool) error { err := e.writeParts(func(name string, data []byte, compressed bool) error {
@@ -61,6 +66,9 @@ func (e *Editor) Reader() (*Reader, error) {
if err := e.validate(); err != nil { if err := e.validate(); err != nil {
return nil, err return nil, err
} }
if e.source != nil {
return e.sourceReader()
}
r := &Reader{files: make(map[string][]byte)} r := &Reader{files: make(map[string][]byte)}
if err := e.writeParts(func(name string, data []byte, _ bool) error { if err := e.writeParts(func(name string, data []byte, _ bool) error {
r.files[name] = data r.files[name] = data
@@ -83,10 +91,15 @@ func (e *Editor) validate() error {
if len(e.pages) == 0 { if len(e.pages) == 0 {
return fmt.Errorf("document must contain at least one page") return fmt.Errorf("document must contain at least one page")
} }
if e.source == nil || e.Info.DocID != e.source.info.DocID {
if data, err := hex.DecodeString(e.Info.DocID); err != nil || len(data) != 16 { if data, err := hex.DecodeString(e.Info.DocID); err != nil || len(data) != 16 {
return fmt.Errorf("DocID must contain 32 hexadecimal characters") return fmt.Errorf("DocID must contain 32 hexadecimal characters")
} }
for _, date := range []string{e.Info.CreationDate, e.Info.ModDate} { }
for i, date := range []string{e.Info.CreationDate, e.Info.ModDate} {
if e.source != nil && date == []string{e.source.info.CreationDate, e.source.info.ModDate}[i] {
continue
}
if date != "" { if date != "" {
if _, err := time.Parse("2006-01-02", date); err != nil { if _, err := time.Parse("2006-01-02", date); err != nil {
return fmt.Errorf("invalid document date: %w", err) return fmt.Errorf("invalid document date: %w", err)
@@ -100,7 +113,7 @@ func (e *Editor) validate() error {
// 入参: write 条目写入方法 // 入参: write 条目写入方法
// 返回: error 错误信息 // 返回: error 错误信息
func (e *Editor) writeParts(write func(string, []byte, bool) error) error { func (e *Editor) writeParts(write func(string, []byte, bool) error) error {
fonts, images := e.usedResources() fonts, images, _ := e.usedResources()
writeXML := func(name string, encode func(*ofdXML)) error { writeXML := func(name string, encode func(*ofdXML)) error {
data, err := encodeOFDXML(encode) data, err := encodeOFDXML(encode)
if err != nil { if err != nil {
@@ -185,20 +198,42 @@ func (e *Editor) writeParts(write func(string, []byte, bool) error) error {
return nil return nil
} }
// usedResources 按注册顺序筛选当前页面引用的字体和图片,不修改资源池 // usedResources 筛选当前引用的新增资源及需要跨页复用的原资源文件,不修改资源池。
// 返回: []editorResource 字体资源, []editorResource 图片资源 // 返回: []editorResource 新增字体, []editorResource 新增图片, []string 原资源文件
func (e *Editor) usedResources() (fonts, images []editorResource) { func (e *Editor) usedResources() (fonts, images []editorResource, sourceFiles []string) {
used := make(map[string]bool) used := make(map[string]bool)
promoted := make(map[string]bool)
for _, page := range e.pages { for _, page := range e.pages {
var original map[string]GraphicObject
if e.source != nil {
original = make(map[string]GraphicObject)
if source := e.source.pages[page.ID]; source != nil && source.original != nil {
for _, layer := range source.original.Content.Layer {
for _, object := range layer.Objects {
original[editorObjectID(object)] = object
}
}
}
}
for _, layer := range page.Content.Layer { for _, layer := range page.Content.Layer {
for _, object := range layer.Objects { for _, object := range layer.Objects {
before, exists := original[editorObjectID(object)]
switch object.Type { switch object.Type {
case "TextObject": case "TextObject":
used[object.TextObject.Font] = true used[object.TextObject.Font] = true
if e.source != nil && (!exists || before.Type != object.Type || before.TextObject.Font != object.TextObject.Font) {
promoted[object.TextObject.Font] = true
}
case "ImageObject": case "ImageObject":
used[object.ImageObject.ResourceID] = true used[object.ImageObject.ResourceID] = true
if e.source != nil && (!exists || before.Type != object.Type || before.ImageObject.ResourceID != object.ImageObject.ResourceID) {
promoted[object.ImageObject.ResourceID] = true
}
if object.ImageObject.ImageMask != "" { if object.ImageObject.ImageMask != "" {
used[object.ImageObject.ImageMask] = true used[object.ImageObject.ImageMask] = true
if e.source != nil && (!exists || before.Type != object.Type || before.ImageObject.ImageMask != object.ImageObject.ImageMask) {
promoted[object.ImageObject.ImageMask] = true
}
} }
} }
} }
@@ -207,12 +242,20 @@ func (e *Editor) usedResources() (fonts, images []editorResource) {
for _, resource := range e.resources { for _, resource := range e.resources {
if resource.font != nil && used[resource.font.ID] { if resource.font != nil && used[resource.font.ID] {
fonts = append(fonts, resource) fonts = append(fonts, resource)
delete(promoted, resource.font.ID)
} }
if resource.image != nil && used[resource.image.ID] { if resource.image != nil && used[resource.image.ID] {
images = append(images, resource) images = append(images, resource)
delete(promoted, resource.image.ID)
} }
} }
return fonts, images files := make(map[string]bool)
for id := range promoted {
if name := e.source.reader.resourceFiles[id]; name != "" {
files[name] = true
}
}
return fonts, images, slices.Sorted(maps.Keys(files))
} }
// page 写出页面尺寸、图层和对象 // page 写出页面尺寸、图层和对象
@@ -224,6 +267,15 @@ func (x *ofdXML) page(page PageContent) {
x.end("Area") x.end("Area")
x.start("Content", nil) x.start("Content", nil)
for _, layer := range page.Content.Layer { for _, layer := range page.Content.Layer {
x.layer(layer)
}
x.end("Content")
x.end("Page")
}
// layer 写出新建图层及其对象。
// 入参: layer 图层
func (x *ofdXML) layer(layer Layer) {
var attrs ofdAttrs var attrs ofdAttrs
attrs.add("ID", layer.ID) attrs.add("ID", layer.ID)
attrs.add("Type", layer.Type) attrs.add("Type", layer.Type)
@@ -232,9 +284,6 @@ func (x *ofdXML) page(page PageContent) {
x.object(object, false) x.object(object, false)
} }
x.end("Layer") x.end("Layer")
}
x.end("Content")
x.end("Page")
} }
// resources 写出文档资源索引 // resources 写出文档资源索引