mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 13:56:32 +08:00
This commit is contained in:
1 parent
ce0966d5e2
commit
7f361a79cf
16 files changed
+1980
-213
No files matched your search
@@ -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) 授权协议
|
||||
@@ -1071,6 +1071,10 @@ button.outline-link:hover {
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.viewer-panel.pointer-focus:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.viewer-panel.pan-mode,
|
||||
.pan-mode .text-run > span {
|
||||
cursor: grab;
|
||||
|
||||
@@ -488,6 +488,8 @@ el.searchInput.addEventListener("input", () => resetSearch(false));
|
||||
el.searchPrev.addEventListener("click", () => selectSearchMatch(Math.max(0, state.searchIndex) - 1));
|
||||
el.searchNext.addEventListener("click", () => selectSearchMatch(state.searchIndex + 1));
|
||||
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.fontDirectoryButton.addEventListener("click", () => openFontFile(el.fontDirectoryInput));
|
||||
el.localFontButton.addEventListener("click", loadLocalFonts);
|
||||
|
||||
@@ -12,24 +12,29 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
codeberg.org/go-latex/latex v0.3.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/graphics-go v0.0.0-20160129215708-b43f31a4a966 // indirect
|
||||
github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc // indirect
|
||||
github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 // 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/benoitkugler/textlayout v0.3.2 // indirect
|
||||
github.com/benoitkugler/textprocessing v0.0.6 // indirect
|
||||
github.com/go-fonts/latin-modern v0.3.3 // 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/scanx v0.0.0-20190309010443-e94503791388 // indirect
|
||||
github.com/tdewolff/minify/v2 v2.24.17 // 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
|
||||
golang.org/x/net v0.58.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/token v1.1.0 // indirect
|
||||
star-tex.org/x/tex v0.7.1 // indirect
|
||||
|
||||
@@ -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/go.mod h1:J15VAa+lyxdcI/Je7lDDDl6QOhLk9feNBnnwXqEHXOk=
|
||||
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/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/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/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
|
||||
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/textprocessing v0.0.6 h1:obkMyj62GEPg3xUVYqROlCN22z1OleuZm6ULqX9Om1g=
|
||||
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/go.mod h1:tHaiWDGze4EPB0Go4cLT5M3QzRY3peya09Z/8KSCrpY=
|
||||
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/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/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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
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/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/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/go.mod h1:K9Ux3cwFtyAFRITZ46g7v+ORXgI3ENtQa5rL9yjNz5Y=
|
||||
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/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk=
|
||||
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/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
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/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.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/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-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/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-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/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-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.3/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.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/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-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/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/go.mod h1:ISAZV+URZIlE8h1BwNVg1NzpwHKndV5lQ13NPPj3tds=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
+88
-44
@@ -98,6 +98,7 @@ func RunWASM() {
|
||||
registerExportCallback("ofdgoExportAttachment", exportAttachment)
|
||||
registerCallback("ofdgoMatchFontFiles", matchFontFiles)
|
||||
registerCallback("ofdgoCreateDocument", createDocument)
|
||||
registerCallback("ofdgoEditDocument", editDocument)
|
||||
registerCallback("ofdgoChangePage", changePage)
|
||||
registerCallback("ofdgoInsertText", insertText)
|
||||
registerCallback("ofdgoUpdateText", updateText)
|
||||
@@ -530,10 +531,10 @@ func encodeResult(result apiResult) string {
|
||||
return string(data)
|
||||
}
|
||||
|
||||
// currentEditor 当前新建文档
|
||||
// currentEditor 当前编辑文档
|
||||
var currentEditor *ofdgo.Editor
|
||||
|
||||
// editorClipboard 当前创作文档中的对象快照与剪贴板标识。
|
||||
// editorClipboard 当前编辑文档中的对象快照与剪贴板标识。
|
||||
type editorClipboard struct {
|
||||
token string
|
||||
objects []ofdgo.GraphicObject
|
||||
@@ -542,12 +543,14 @@ type editorClipboard struct {
|
||||
// copiedObjects 当前对象剪贴板,不保存字体或图片的重复数据。
|
||||
var copiedObjects *editorClipboard
|
||||
|
||||
// editorInfo 创作文档信息与操作状态
|
||||
// editorInfo 编辑文档信息与操作状态
|
||||
type editorInfo struct {
|
||||
DocumentInfo
|
||||
Revision uint64 `json:"revision"`
|
||||
CanUndo bool `json:"canUndo"`
|
||||
CanRedo bool `json:"canRedo"`
|
||||
PageCapabilities []map[string]bool `json:"pageCapabilities"`
|
||||
EditWarnings []string `json:"editWarnings,omitempty"`
|
||||
}
|
||||
|
||||
// editorPageInfo 页面操作结果与目标页面
|
||||
@@ -562,10 +565,21 @@ type editorSelectionInfo struct {
|
||||
SelectedIDs []string `json:"selectedIDs"`
|
||||
}
|
||||
|
||||
// editorSummary 获取当前创作文档状态
|
||||
// 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 撤销或重做并更新预览
|
||||
@@ -573,7 +587,7 @@ func editorSummary() editorInfo {
|
||||
// 返回: any 文档信息, error 错误信息
|
||||
func restoreEditor(redo bool) (any, error) {
|
||||
if currentEditor == nil {
|
||||
return nil, fmt.Errorf("no document is being created")
|
||||
return nil, fmt.Errorf("no document is being edited")
|
||||
}
|
||||
apply := currentEditor.Undo
|
||||
if redo {
|
||||
@@ -585,7 +599,7 @@ func restoreEditor(redo bool) (any, error) {
|
||||
return previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations)
|
||||
}
|
||||
|
||||
// editorObjects 提供创作画布的对象范围、文字排版及原始图片边界
|
||||
// editorObjects 提供编辑画布的对象范围、文字排版及原始图片边界
|
||||
// 入参: index 页面索引, text 页面文字
|
||||
// 返回: []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 order, object := range layer.Objects {
|
||||
var id string
|
||||
var box ofdgo.Box
|
||||
switch object.Type {
|
||||
case "TextObject":
|
||||
id = object.TextObject.ID
|
||||
box = textBoxes[id]
|
||||
case "ImageObject":
|
||||
id = object.ImageObject.ID
|
||||
box, err = ofdgo.ParseBox(object.ImageObject.Boundary)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case "PathObject":
|
||||
id = object.PathObject.ID
|
||||
box, err = ofdgo.ParseBox(object.PathObject.Boundary)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
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 {
|
||||
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" {
|
||||
full, err := object.ImageObject.ImageBounds()
|
||||
if err != nil {
|
||||
@@ -652,7 +676,7 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
|
||||
}
|
||||
item["imageBounds"] = editorBox(full)
|
||||
}
|
||||
if object.Type == "PathObject" {
|
||||
if object.Type == "PathObject" && capability.Update {
|
||||
path := object.PathObject
|
||||
kind, geometry := path.Shape()
|
||||
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["wrap"], item["align"], item["paragraphHeight"] = layout.Wrap, layout.Align, layout.LineHeight
|
||||
item["letterSpacing"] = layout.LetterSpacing
|
||||
if capability.Reflow {
|
||||
frame, err := object.TextObject.TextFrame()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -691,19 +716,16 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
|
||||
bounds := matrix.TransformBox(frame)
|
||||
item["x"], item["y"], item["width"], item["height"] = bounds.X, bounds.Y, bounds.W, bounds.H
|
||||
}
|
||||
}
|
||||
if codes := object.TextObject.TextCode; len(codes) > 1 {
|
||||
first, _ := strconv.ParseFloat(codes[0].Y, 64)
|
||||
second, _ := strconv.ParseFloat(codes[1].Y, 64)
|
||||
item["lineHeight"] = second - first
|
||||
}
|
||||
var r, g, b float64
|
||||
if fill := object.TextObject.FillColor; fill != nil {
|
||||
if _, err := fmt.Sscan(fill.Value, &r, &g, &b); err != nil {
|
||||
return nil, err
|
||||
if capability.Update {
|
||||
item["color"] = editorColorHex(object.TextObject.FillColor)
|
||||
}
|
||||
}
|
||||
item["color"] = fmt.Sprintf("#%02x%02x%02x", byte(r), byte(g), byte(b))
|
||||
}
|
||||
objects = append(objects, item)
|
||||
}
|
||||
}
|
||||
@@ -716,7 +738,7 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
|
||||
// 返回: any 字体数据, error 错误信息
|
||||
func editorFont(args []js.Value) (any, error) {
|
||||
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()
|
||||
if err != nil {
|
||||
@@ -739,7 +761,7 @@ func editorFont(args []js.Value) (any, error) {
|
||||
// 返回: any 文档信息, error 错误信息
|
||||
func replaceImage(args []js.Value) (any, error) {
|
||||
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])
|
||||
if err != nil {
|
||||
@@ -760,7 +782,7 @@ func replaceImage(args []js.Value) (any, error) {
|
||||
// 返回: any 文档信息, error 错误信息
|
||||
func alignObject(args []js.Value) (any, error) {
|
||||
if currentEditor == nil {
|
||||
return nil, fmt.Errorf("no document is being created")
|
||||
return nil, fmt.Errorf("no document is being edited")
|
||||
}
|
||||
revision := currentEditor.Revision()
|
||||
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 错误信息
|
||||
func transformObject(args []js.Value) (any, error) {
|
||||
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()
|
||||
revision := currentEditor.Revision()
|
||||
@@ -795,7 +817,7 @@ func transformObject(args []js.Value) (any, error) {
|
||||
// 返回: any 文档信息, error 错误信息
|
||||
func reshapeObject(args []js.Value) (any, error) {
|
||||
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()
|
||||
object, err := currentEditor.Object(page, id)
|
||||
@@ -824,7 +846,7 @@ func reshapeObject(args []js.Value) (any, error) {
|
||||
// 返回: any 文档信息, error 错误信息
|
||||
func copyObjects(args []js.Value) (any, error) {
|
||||
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]))
|
||||
if err != nil {
|
||||
@@ -838,7 +860,7 @@ func copyObjects(args []js.Value) (any, error) {
|
||||
// 返回: any 空结果, error 错误信息
|
||||
func captureObjects(args []js.Value) (any, error) {
|
||||
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]))
|
||||
if err != nil {
|
||||
@@ -896,7 +918,7 @@ func distributeObjects(args []js.Value) (any, error) {
|
||||
// 返回: any 文档信息, error 错误信息
|
||||
func deleteObject(args []js.Value) (any, error) {
|
||||
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 {
|
||||
return nil, err
|
||||
@@ -909,7 +931,7 @@ func deleteObject(args []js.Value) (any, error) {
|
||||
// 返回: any 文档信息和目标页面, error 错误信息
|
||||
func changePage(args []js.Value) (any, error) {
|
||||
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()
|
||||
revision := currentEditor.Revision()
|
||||
@@ -960,8 +982,26 @@ func createDocument(args []js.Value) (any, error) {
|
||||
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 通过内存快照更新预览,不生成中间压缩包
|
||||
// 入参: editor 新建文档, annotations 是否显示注解
|
||||
// 入参: editor 编辑文档, annotations 是否显示注解
|
||||
// 返回: editorInfo 文档信息, error 错误信息
|
||||
func previewEditor(editor *ofdgo.Editor, annotations bool) (editorInfo, error) {
|
||||
reader, err := editor.Reader()
|
||||
@@ -973,6 +1013,10 @@ func previewEditor(editor *ofdgo.Editor, annotations bool) (editorInfo, error) {
|
||||
return editorInfo{}, err
|
||||
}
|
||||
if currentSession != nil {
|
||||
if currentSession.fontFS != nil {
|
||||
session.fontFS = currentSession.fontFS
|
||||
session.Renderer.SetFontFS(session.fontFS)
|
||||
}
|
||||
_ = currentSession.Close()
|
||||
}
|
||||
currentSession = session
|
||||
@@ -988,7 +1032,7 @@ func previewEditor(editor *ofdgo.Editor, annotations bool) (editorInfo, error) {
|
||||
// 返回: any 文档信息, error 错误信息
|
||||
func updateText(args []js.Value) (any, error) {
|
||||
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()
|
||||
object, err := currentEditor.Object(page, id)
|
||||
@@ -1112,7 +1156,7 @@ func setPathStyle(object *ofdgo.PathObject, args []js.Value) error {
|
||||
// 返回: any 文档信息, error 错误信息
|
||||
func insertShape(args []js.Value) (any, error) {
|
||||
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()})
|
||||
if err != nil {
|
||||
@@ -1132,7 +1176,7 @@ func insertShape(args []js.Value) (any, error) {
|
||||
// 返回: any 文档信息, error 错误信息
|
||||
func updatePathStyle(args []js.Value) (any, error) {
|
||||
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()
|
||||
object, err := currentEditor.Object(page, id)
|
||||
@@ -1160,7 +1204,7 @@ func updatePathStyle(args []js.Value) (any, error) {
|
||||
// 返回: any 文档信息, error 错误信息
|
||||
func insertText(args []js.Value) (any, error) {
|
||||
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])
|
||||
if err != nil {
|
||||
@@ -1206,7 +1250,7 @@ func insertText(args []js.Value) (any, error) {
|
||||
// 返回: any 文档信息, error 错误信息
|
||||
func layoutText(args []js.Value) (any, error) {
|
||||
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()
|
||||
object, err := currentEditor.Object(page, id)
|
||||
@@ -1294,7 +1338,7 @@ func fitImage(args []js.Value) (any, error) {
|
||||
// 返回: any 页面SVG和分离的图片资源, error 错误信息
|
||||
func previewImage(args []js.Value) (any, error) {
|
||||
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())
|
||||
if err != nil {
|
||||
@@ -1365,7 +1409,7 @@ func deleteObjects(args []js.Value) (any, error) {
|
||||
// 返回: any 文档信息, error 错误信息
|
||||
func changeObjects(apply func() error) (any, error) {
|
||||
if currentEditor == nil {
|
||||
return nil, fmt.Errorf("no document is being created")
|
||||
return nil, fmt.Errorf("no document is being edited")
|
||||
}
|
||||
revision := currentEditor.Revision()
|
||||
if err := apply(); err != nil {
|
||||
@@ -1382,7 +1426,7 @@ func changeObjects(apply func() error) (any, error) {
|
||||
// 返回: any 文档信息, error 错误信息
|
||||
func insertImage(args []js.Value) (any, error) {
|
||||
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])
|
||||
if err != nil {
|
||||
@@ -1424,7 +1468,7 @@ func insertImage(args []js.Value) (any, error) {
|
||||
// 返回: any 保存结果, error 错误信息
|
||||
func saveDocument(args []js.Value) (any, error) {
|
||||
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)
|
||||
if _, err := currentEditor.WriteTo(writer); err != nil {
|
||||
|
||||
@@ -100,6 +100,7 @@ type SignatureStampInfo struct {
|
||||
type Session struct {
|
||||
Reader *ofdgo.Reader
|
||||
Renderer *ofdgo.Renderer
|
||||
fontFS *ofdgo.FontFS
|
||||
doc *ofdgo.Document
|
||||
pageCache map[int]*ofdgo.PageContent
|
||||
boxCache map[int]ofdgo.Box
|
||||
@@ -232,8 +233,9 @@ func newSession(reader *ofdgo.Reader, opts OpenOptions) (*Session, error) {
|
||||
return nil, err
|
||||
}
|
||||
var rendererOptions []ofdgo.RendererOption
|
||||
var fontFS *ofdgo.FontFS
|
||||
if len(opts.Fonts) > 0 {
|
||||
fontFS := ofdgo.NewFontFS(opts.Fonts)
|
||||
fontFS = ofdgo.NewFontFS(opts.Fonts)
|
||||
if fontFS.Len() == 0 {
|
||||
reader.Close()
|
||||
return nil, fmt.Errorf("invalid font file")
|
||||
@@ -244,6 +246,7 @@ func newSession(reader *ofdgo.Reader, opts OpenOptions) (*Session, error) {
|
||||
return &Session{
|
||||
Reader: reader,
|
||||
Renderer: ofdgo.NewRenderer(reader, rendererOptions...),
|
||||
fontFS: fontFS,
|
||||
doc: doc,
|
||||
pageCache: make(map[int]*ofdgo.PageContent),
|
||||
boxCache: make(map[int]ofdgo.Box),
|
||||
@@ -271,8 +274,10 @@ func (s *Session) SetFonts(fonts []FontFile) error {
|
||||
return fmt.Errorf("invalid font file")
|
||||
}
|
||||
s.Renderer.SetFontFS(fontFS)
|
||||
s.fontFS = fontFS
|
||||
} else {
|
||||
s.Renderer.SetFontFS()
|
||||
s.fontFS = nil
|
||||
}
|
||||
clear(s.textCache)
|
||||
clear(s.svgFonts)
|
||||
|
||||
+107
-123
@@ -23,7 +23,6 @@ import (
|
||||
"fmt"
|
||||
"image"
|
||||
"math"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -33,8 +32,8 @@ import (
|
||||
"github.com/tdewolff/font"
|
||||
)
|
||||
|
||||
// Editor 新建OFD文档,长度单位为毫米,页面索引从0开始,实例需串行使用
|
||||
// Info可修改文档元数据,通过方法管理页面、对象和资源,不修改已有OFD文件
|
||||
// Editor 编辑OFD文档,长度单位为毫米,页面索引从0开始,实例需串行使用
|
||||
// Info可修改文档元数据,通过方法管理页面、对象和资源,WriteTo另存结果,不覆盖输入
|
||||
type Editor struct {
|
||||
Info DocInfo
|
||||
pages []PageContent
|
||||
@@ -48,6 +47,7 @@ type Editor struct {
|
||||
historyLimit int
|
||||
revision uint64
|
||||
serial uint64
|
||||
source *editorSource
|
||||
}
|
||||
|
||||
// editorResource 文档内嵌资源
|
||||
@@ -96,18 +96,23 @@ func (e *Editor) Page(index int) (*PageContent, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := encodeOFDXML(func(x *ofdXML) { x.page(*page) })
|
||||
if err != nil {
|
||||
return nil, err
|
||||
result := cloneEditorData(*page)
|
||||
result.XMLName = xml.Name{Space: ofdNamespace, Local: "Page"}
|
||||
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
|
||||
@@ -120,6 +125,11 @@ func (e *Editor) page(index int) (*PageContent, error) {
|
||||
if index < 0 || index >= len(e.pages) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -130,6 +140,9 @@ func (e *Editor) AddPage(width, height float64) (int, error) {
|
||||
if !finite(width) || !finite(height) || width <= 0 || height <= 0 {
|
||||
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{
|
||||
ID: e.nextID(),
|
||||
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 原页面索引
|
||||
// 返回: 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)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -165,7 +181,11 @@ func (e *Editor) CopyPage(index int) (int, error) {
|
||||
layer.ID = e.nextID()
|
||||
for j := range layer.Objects {
|
||||
object := &layer.Objects[j]
|
||||
origin := e.objectOrigin(editorObjectID(*object))
|
||||
id := e.nextID()
|
||||
if origin != nil {
|
||||
e.source.origins[id] = origin
|
||||
}
|
||||
switch object.Type {
|
||||
case "TextObject":
|
||||
object.TextObject.ID = id
|
||||
@@ -186,6 +206,9 @@ func (e *Editor) CopyPage(index int) (int, error) {
|
||||
// 入参: index 页面索引
|
||||
// 返回: 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)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -282,13 +305,16 @@ func (e *Editor) AddFont(file FontFile, index int) (string, error) {
|
||||
if name == "" {
|
||||
return "", fmt.Errorf("font has no name")
|
||||
}
|
||||
if err := e.prepareSourceIDs(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
id := e.nextID()
|
||||
extension := ".ttf"
|
||||
if sfnt.IsCFF {
|
||||
extension = ".otf"
|
||||
}
|
||||
resource := editorResource{
|
||||
name: "Doc_0/Res/Font_" + id + extension,
|
||||
name: e.resourceDirectory() + "/Font_" + id + extension,
|
||||
data: data,
|
||||
font: &Font{
|
||||
ID: id, FontName: name,
|
||||
@@ -299,7 +325,7 @@ func (e *Editor) AddFont(file FontFile, index int) (string, error) {
|
||||
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.fonts[id] = sfnt
|
||||
e.resourceID[key] = id
|
||||
@@ -335,6 +361,9 @@ func (e *Editor) AddImage(data []byte) (string, error) {
|
||||
if id, ok := e.resourceID[key]; ok {
|
||||
return id, nil
|
||||
}
|
||||
if err := e.prepareSourceIDs(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
id := e.nextID()
|
||||
extension := format
|
||||
if extension == "jpeg" {
|
||||
@@ -342,7 +371,7 @@ func (e *Editor) AddImage(data []byte) (string, error) {
|
||||
}
|
||||
name := "Image_" + id + "." + extension
|
||||
e.resources = append(e.resources, editorResource{
|
||||
name: "Doc_0/Res/" + name,
|
||||
name: e.resourceDirectory() + "/" + name,
|
||||
data: bytes.Clone(data),
|
||||
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 对象内容,添加后不再引用调用方的可变数据
|
||||
// 返回: 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 {
|
||||
return "", err
|
||||
}
|
||||
id := strconv.Itoa(e.maxID + 1)
|
||||
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
|
||||
return ids[0], nil
|
||||
}
|
||||
|
||||
// Object 获取对象的独立副本,修改副本不影响文档
|
||||
@@ -396,39 +407,24 @@ func (e *Editor) Object(page int, id string) (GraphicObject, error) {
|
||||
// 入参: page 页面索引, id 对象标识, object 新内容,忽略其ID且不引用调用方的可变数据
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) UpdateObject(page int, id string, object GraphicObject) error {
|
||||
_, index, err := e.findObject(page, id)
|
||||
if err != nil {
|
||||
return err
|
||||
switch object.Type {
|
||||
case "TextObject":
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e.replaceObject(page, index, object)
|
||||
return nil
|
||||
return e.UpdateObjects(page, []GraphicObject{object})
|
||||
}
|
||||
|
||||
// DeleteObject 删除对象,不回收资源或复用标识
|
||||
// 入参: page 页面索引, id 对象标识
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) DeleteObject(page int, id string) error {
|
||||
layer, index, err := e.findObject(page, 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
|
||||
return e.DeleteObjects(page, []string{id})
|
||||
}
|
||||
|
||||
// MoveObject 调整同页正文图层内的绘制顺序,保留对象标识和内容
|
||||
@@ -445,30 +441,24 @@ func (e *Editor) MoveObject(page int, id string, to int) error {
|
||||
if from == to {
|
||||
return nil
|
||||
}
|
||||
moveEditorItem(layer.Objects, from, to)
|
||||
if change := e.recordChange(); change != nil {
|
||||
change.undo = func(e *Editor) { moveEditorItem(e.pages[page].Content.Layer[0].Objects, to, from) }
|
||||
change.redo = func(e *Editor) { moveEditorItem(e.pages[page].Content.Layer[0].Objects, from, to) }
|
||||
capability, err := e.ObjectCapabilities(page, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// replaceObject 提交已校验的对象并记录前后内容
|
||||
// 入参: 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 查找正文图层中的对象
|
||||
// findObject 查找页面各图层中的对象
|
||||
// 入参: page 页面索引, id 对象标识
|
||||
// 返回: *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 {
|
||||
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 {
|
||||
var objectID string
|
||||
switch object.Type {
|
||||
case "TextObject":
|
||||
objectID = object.TextObject.ID
|
||||
case "PathObject":
|
||||
objectID = object.PathObject.ID
|
||||
case "ImageObject":
|
||||
objectID = object.ImageObject.ID
|
||||
}
|
||||
if objectID == id {
|
||||
if editorObjectID(object) == id && id != "" {
|
||||
return layer, index, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
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 {
|
||||
case "TextObject":
|
||||
obj := &object.TextObject
|
||||
codes := obj.TextCode
|
||||
if err := e.prepareText(obj); err != nil {
|
||||
return GraphicObject{}, err
|
||||
}
|
||||
if e.objectOrigin(obj.ID) != nil {
|
||||
obj.TextCode = codes
|
||||
}
|
||||
obj.ID = id
|
||||
boundary, ctm, drawParam = obj.Boundary, obj.CTM, obj.DrawParam
|
||||
join = obj.Join
|
||||
@@ -554,8 +541,13 @@ func (e *Editor) prepareObject(id string, object GraphicObject) (GraphicObject,
|
||||
fill, stroke = obj.FillColor, (*FillColor)(obj.StrokeColor)
|
||||
case "ImageObject":
|
||||
obj := &object.ImageObject
|
||||
if e.images[obj.ResourceID].X == 0 || (obj.ImageMask != "" && e.images[obj.ImageMask].X == 0) {
|
||||
return GraphicObject{}, fmt.Errorf("image resource not found")
|
||||
if _, err := e.editorImage(obj.ResourceID); err != nil {
|
||||
return GraphicObject{}, err
|
||||
}
|
||||
if obj.ImageMask != "" {
|
||||
if _, err := e.editorImage(obj.ImageMask); err != nil {
|
||||
return GraphicObject{}, err
|
||||
}
|
||||
}
|
||||
if obj.Border != nil {
|
||||
return GraphicObject{}, fmt.Errorf("image borders are not supported for creation")
|
||||
@@ -633,30 +625,21 @@ func (e *Editor) validateImageClips(clips *Clips) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// cloneEditorObject 通过统一序列化规则复制对象
|
||||
// cloneEditorObject 复制对象全部已解析字段,保留不可变的段落信息
|
||||
// 入参: object 对象内容
|
||||
// 返回: 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 {
|
||||
case "TextObject":
|
||||
target = &object.TextObject
|
||||
return GraphicObject{Type: object.Type, TextObject: cloneEditorData(object.TextObject)}, nil
|
||||
case "PathObject":
|
||||
target = &object.PathObject
|
||||
return GraphicObject{Type: object.Type, PathObject: cloneEditorData(object.PathObject)}, nil
|
||||
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{}, err
|
||||
}
|
||||
object.TextObject.layout = layout
|
||||
return object, nil
|
||||
return GraphicObject{}, fmt.Errorf("unsupported object type %q", object.Type)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return err
|
||||
}
|
||||
object, err = e.prepareObject(id, object)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e.replaceObject(page, index, object)
|
||||
return nil
|
||||
return e.UpdateObject(page, id, object)
|
||||
}
|
||||
|
||||
// TransformObject 以页面原点等比缩放后平移对象,文字同步缩放字号和字距,保留原有排版
|
||||
@@ -741,11 +719,16 @@ func transformEditorObject(object GraphicObject, dx, dy, scale float64) (Graphic
|
||||
if scale != 1 {
|
||||
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)
|
||||
if err != nil {
|
||||
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))
|
||||
if scale != 1 && (*ctm != "" || object.Type != "TextObject") {
|
||||
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 {
|
||||
return fmt.Errorf("automatic text layout requires horizontal text")
|
||||
}
|
||||
sfnt, ok := e.fonts[obj.Font]
|
||||
if !ok {
|
||||
sfnt, err := e.editorFont(obj.Font)
|
||||
if err != nil {
|
||||
return fmt.Errorf("font resource %q not found", obj.Font)
|
||||
}
|
||||
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.layout = nil
|
||||
if options != (TextLayout{}) {
|
||||
if options != (TextLayout{}) || e.objectOrigin(obj.ID) != nil {
|
||||
obj.layout = &textLayout{value: value, options: options}
|
||||
}
|
||||
return nil
|
||||
@@ -887,7 +870,8 @@ func (e *Editor) LayoutText(obj *TextObject, value string, options TextLayout) e
|
||||
// 入参: obj 文字对象
|
||||
// 返回: 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")
|
||||
}
|
||||
if len(obj.TextCode) == 0 {
|
||||
@@ -928,7 +912,7 @@ func (e *Editor) prepareText(obj *TextObject) error {
|
||||
code.X, code.Y = x, y
|
||||
runes := textCodeRunes(code.Value)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
+156
-34
@@ -24,6 +24,22 @@ import (
|
||||
"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 按绘制顺序获取选区的独立快照,保留编辑中的段落信息。
|
||||
// 入参: page 页面索引, ids 对象标识,不得重复
|
||||
// 返回: []GraphicObject 独立对象副本, error 错误信息
|
||||
@@ -32,10 +48,18 @@ func (e *Editor) Objects(page int, ids []string) ([]GraphicObject, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
slices.Sort(indexes)
|
||||
slices.SortFunc(indexes, compareEditorPosition)
|
||||
objects := make([]GraphicObject, len(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 {
|
||||
return nil, err
|
||||
}
|
||||
@@ -51,33 +75,60 @@ func (e *Editor) CopyObjects(page int, objects []GraphicObject, dx, dy float64)
|
||||
if !finite(dx) || !finite(dy) {
|
||||
return nil, fmt.Errorf("copy requires finite offsets")
|
||||
}
|
||||
content, err := e.page(page)
|
||||
_, err := e.page(page)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(objects) == 0 {
|
||||
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))
|
||||
result := make([]string, len(objects))
|
||||
for i, object := range objects {
|
||||
origin := e.objectOrigin(editorObjectID(object))
|
||||
result[i] = strconv.Itoa(e.maxID + i + 1)
|
||||
object, err := e.prepareObject(result[i], object)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if dx != 0 || dy != 0 {
|
||||
object, err = transformEditorObject(object, dx, dy, 1)
|
||||
if err != nil {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
if origin != nil {
|
||||
object.TextObject.TextCode = codes
|
||||
}
|
||||
}
|
||||
prepared[i] = object
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -92,10 +143,20 @@ func (e *Editor) OrderObjects(page int, ids []string, order string) error {
|
||||
if err != nil || len(indexes) == 0 {
|
||||
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))
|
||||
for _, index := range indexes {
|
||||
selected[index] = true
|
||||
selected[index.index] = true
|
||||
}
|
||||
switch order {
|
||||
case "up":
|
||||
@@ -123,7 +184,8 @@ func (e *Editor) OrderObjects(page int, ids []string, order string) error {
|
||||
}
|
||||
objects = ordered
|
||||
}
|
||||
e.replaceObjects(page, objects)
|
||||
layers[indexes[0].layer].Objects = objects
|
||||
e.replaceLayers(page, layers)
|
||||
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 {
|
||||
return c
|
||||
}
|
||||
return cmp.Compare(indexes[i], indexes[j])
|
||||
return compareEditorPosition(indexes[i], indexes[j])
|
||||
})
|
||||
first, last := order[0], order[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) {
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -193,21 +255,24 @@ func (e *Editor) DistributeObjects(page int, ids []string, axis string) error {
|
||||
}
|
||||
updates = append(updates, object)
|
||||
}
|
||||
return e.UpdateObjects(page, updates)
|
||||
return e.updateObjects(page, updates, true)
|
||||
}
|
||||
|
||||
// replaceObjects 替换正文对象列表,隔离历史快照与后续的增删、排序操作。
|
||||
// 入参: page 页面索引, objects 新的正文对象列表
|
||||
func (e *Editor) replaceObjects(page int, objects []GraphicObject) {
|
||||
before := e.pages[page].Content.Layer[0].Objects
|
||||
if reflect.DeepEqual(before, objects) {
|
||||
// replaceLayers 替换图层容器,隔离历史快照与后续的增删、排序操作。
|
||||
// 入参: page 页面索引, layers 新图层列表
|
||||
func (e *Editor) replaceLayers(page int, layers []Layer) {
|
||||
before := copyEditorPage(e.pages[page]).Content.Layer
|
||||
if reflect.DeepEqual(before, layers) {
|
||||
return
|
||||
}
|
||||
before, after := slices.Clone(before), slices.Clone(objects)
|
||||
e.pages[page].Content.Layer[0].Objects = slices.Clone(after)
|
||||
after := copyEditorPage(PageContent{Content: Content{Layer: layers}}).Content.Layer
|
||||
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 {
|
||||
change.undo = func(e *Editor) { e.pages[page].Content.Layer[0].Objects = slices.Clone(before) }
|
||||
change.redo = func(e *Editor) { e.pages[page].Content.Layer[0].Objects = slices.Clone(after) }
|
||||
change.undo = func(e *Editor) { apply(e, before) }
|
||||
change.redo = func(e *Editor) { apply(e, after) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,6 +280,13 @@ func (e *Editor) replaceObjects(page int, objects []GraphicObject) {
|
||||
// 入参: page 页面索引, objects 新对象内容,ID不得重复
|
||||
// 返回: 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))
|
||||
for i, object := range objects {
|
||||
ids[i] = editorObjectID(object)
|
||||
@@ -225,17 +297,33 @@ func (e *Editor) UpdateObjects(page int, objects []GraphicObject) error {
|
||||
}
|
||||
after := make([]GraphicObject, len(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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if reflect.DeepEqual(before, after) {
|
||||
return nil
|
||||
}
|
||||
apply := func(e *Editor, objects []GraphicObject) {
|
||||
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)
|
||||
@@ -261,6 +349,11 @@ func (e *Editor) TransformObjects(page int, ids []string, dx, dy, scale float64)
|
||||
return nil
|
||||
}
|
||||
for i, object := range objects {
|
||||
if e.originalPage(page) {
|
||||
if err := validateEditorGeometry(object); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
object, err = cloneEditorObject(object)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -269,8 +362,11 @@ func (e *Editor) TransformObjects(page int, ids []string, dx, dy, scale float64)
|
||||
if err != nil {
|
||||
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 单对象对齐页面,多对象相互对齐至选区边界,文字采用实际字形范围。
|
||||
@@ -320,7 +416,7 @@ func (e *Editor) AlignObjects(page int, ids []string, alignment string) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return e.UpdateObjects(page, objects)
|
||||
return e.updateObjects(page, objects, true)
|
||||
}
|
||||
|
||||
// DeleteObjects 原子删除同页对象,保留其余对象顺序,一次撤销恢复全部。
|
||||
@@ -331,25 +427,34 @@ func (e *Editor) DeleteObjects(page int, ids []string) error {
|
||||
if err != nil || len(indexes) == 0 {
|
||||
return err
|
||||
}
|
||||
before := e.pages[page].Content.Layer[0].Objects
|
||||
after := slices.Clone(before)
|
||||
slices.Sort(indexes)
|
||||
for _, index := range slices.Backward(indexes) {
|
||||
after = slices.Delete(after, index, index+1)
|
||||
for _, id := range ids {
|
||||
capability, err := e.ObjectCapabilities(page, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// selectedObjects 校验同页选择,返回内部只读对象与对应索引。
|
||||
// 入参: page 页面索引, ids 对象标识,不得重复
|
||||
// 返回: []GraphicObject 所选对象, []int 正文对象索引, error 错误信息
|
||||
func (e *Editor) selectedObjects(page int, ids []string) ([]GraphicObject, []int, error) {
|
||||
// 返回: []GraphicObject 所选对象, []editorObjectPosition 图层和对象索引, error 错误信息
|
||||
func (e *Editor) selectedObjects(page int, ids []string) ([]GraphicObject, []editorObjectPosition, error) {
|
||||
if _, err := e.page(page); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
objects := make([]GraphicObject, len(ids))
|
||||
indexes := make([]int, len(ids))
|
||||
indexes := make([]editorObjectPosition, len(ids))
|
||||
seen := make(map[string]bool, len(ids))
|
||||
for i, id := range ids {
|
||||
if seen[id] {
|
||||
@@ -360,7 +465,13 @@ func (e *Editor) selectedObjects(page int, ids []string) ([]GraphicObject, []int
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
@@ -376,6 +487,8 @@ func editorObjectID(object GraphicObject) string {
|
||||
return object.PathObject.ID
|
||||
case "ImageObject":
|
||||
return object.ImageObject.ID
|
||||
case "CompositeObject", "CompositeGraphicUnit":
|
||||
return object.CompositeGraphicUnit.ID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -384,6 +497,15 @@ func editorObjectID(object GraphicObject) string {
|
||||
// 入参: page 页面索引, objects 待度量的对象
|
||||
// 返回: []Box 对象范围,文字采用字形范围,其余采用Boundary, 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)
|
||||
if slices.ContainsFunc(objects, func(o GraphicObject) bool { return o.Type == "TextObject" }) {
|
||||
reader, err := e.Reader()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 = ©
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -91,8 +91,13 @@ func (e *Editor) orientObjects(page int, ids []string, matrix Matrix) error {
|
||||
boundary, ctm = &object.PathObject.Boundary, &object.PathObject.CTM
|
||||
case "ImageObject":
|
||||
boundary, ctm = &object.ImageObject.Boundary, &object.ImageObject.CTM
|
||||
default:
|
||||
return fmt.Errorf("unsupported object type %q", object.Type)
|
||||
}
|
||||
before, _ := ParseBox(*boundary)
|
||||
if object.Type == "ImageObject" && *ctm == "" {
|
||||
*ctm = Matrix{a: before.W, d: before.H}.String()
|
||||
}
|
||||
after := matrix.TransformBox(before)
|
||||
local := TranslationMatrix(-after.X, -after.Y).Multiply(matrix).Multiply(TranslationMatrix(before.X, before.Y))
|
||||
*ctm = local.Multiply(NewMatrix(*ctm)).String()
|
||||
@@ -102,7 +107,7 @@ func (e *Editor) orientObjects(page int, ids []string, matrix Matrix) error {
|
||||
}
|
||||
objects[i] = object
|
||||
}
|
||||
return e.UpdateObjects(page, objects)
|
||||
return e.updateObjects(page, objects, true)
|
||||
}
|
||||
|
||||
// transformImageClips 同步变换不随对象CTM变化的裁剪区域,不修改原始裁剪数据。
|
||||
@@ -174,6 +179,9 @@ func cropImageObject(image *ImageObject, box Box) error {
|
||||
return fmt.Errorf("crop must stay within the image bounds")
|
||||
}
|
||||
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 {
|
||||
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
|
||||
}
|
||||
m := NewMatrix(obj.CTM)
|
||||
if obj.CTM == "" {
|
||||
m = Matrix{a: box.W, d: box.H}
|
||||
}
|
||||
if !axisAlignedMatrix(m) {
|
||||
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)
|
||||
if m.a == 0 {
|
||||
w, h = h, w
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -34,6 +34,7 @@ type Reader struct {
|
||||
RootDir string
|
||||
ResMap map[string]string
|
||||
resourcesRead map[string]bool
|
||||
resourceFiles map[string]string
|
||||
fontCache map[string]*Font
|
||||
fontResourcesRead bool
|
||||
fontFilesChecked map[string]bool
|
||||
@@ -84,6 +85,7 @@ func (r *Reader) initRoot() error {
|
||||
r.OFD = &ofd
|
||||
r.ResMap = make(map[string]string)
|
||||
r.resourcesRead = make(map[string]bool)
|
||||
r.resourceFiles = make(map[string]string)
|
||||
r.fontCache = make(map[string]*Font)
|
||||
r.fontFilesChecked = make(map[string]bool)
|
||||
r.colorSpaceCache = make(map[string]*ColorSpace)
|
||||
@@ -102,6 +104,9 @@ func (r *Reader) readFile(name string) ([]byte, error) {
|
||||
return bytes.Clone(data), nil
|
||||
}
|
||||
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 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
|
||||
}
|
||||
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 nil, fmt.Errorf("file not found: %s", name)
|
||||
@@ -217,6 +225,7 @@ func (r *Reader) loadRes(resPath string) {
|
||||
if mm.MediaFile != "" {
|
||||
if finalPath := resolveResourcePath(resPath, baseLoc, mm.MediaFile); 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)
|
||||
}
|
||||
r.fontCache[f.ID] = f
|
||||
r.resourceFiles[f.ID] = fullPath
|
||||
}
|
||||
for i := range res.DrawParams.DrawParam {
|
||||
dp := &res.DrawParams.DrawParam[i]
|
||||
|
||||
@@ -440,6 +440,9 @@ func parseSignatureFile(data []byte) (*SignatureFile, error) {
|
||||
// 返回: []byte 文件数据, error 错误信息
|
||||
func (r *Reader) readFileExact(name string) ([]byte, error) {
|
||||
name = cleanPackagePath(name)
|
||||
if data, ok := r.files[name]; ok {
|
||||
return bytes.Clone(data), nil
|
||||
}
|
||||
if f, ok := r.fileIndex[name]; ok {
|
||||
return readZipFile(f)
|
||||
}
|
||||
|
||||
+58
-9
@@ -21,6 +21,8 @@ import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"slices"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
@@ -34,6 +36,9 @@ func (e *Editor) WriteTo(writer io.Writer) (int64, error) {
|
||||
if err := e.validate(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if e.source != nil {
|
||||
return e.writeSource(writer)
|
||||
}
|
||||
output := &ofdCountingWriter{writer: writer}
|
||||
archive := zip.NewWriter(output)
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
if e.source != nil {
|
||||
return e.sourceReader()
|
||||
}
|
||||
r := &Reader{files: make(map[string][]byte)}
|
||||
if err := e.writeParts(func(name string, data []byte, _ bool) error {
|
||||
r.files[name] = data
|
||||
@@ -83,10 +91,15 @@ func (e *Editor) validate() error {
|
||||
if len(e.pages) == 0 {
|
||||
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 {
|
||||
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 _, err := time.Parse("2006-01-02", date); err != nil {
|
||||
return fmt.Errorf("invalid document date: %w", err)
|
||||
@@ -100,7 +113,7 @@ func (e *Editor) validate() error {
|
||||
// 入参: write 条目写入方法
|
||||
// 返回: 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 {
|
||||
data, err := encodeOFDXML(encode)
|
||||
if err != nil {
|
||||
@@ -185,20 +198,42 @@ func (e *Editor) writeParts(write func(string, []byte, bool) error) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// usedResources 按注册顺序筛选当前页面引用的字体和图片,不修改资源池
|
||||
// 返回: []editorResource 字体资源, []editorResource 图片资源
|
||||
func (e *Editor) usedResources() (fonts, images []editorResource) {
|
||||
// usedResources 筛选当前引用的新增资源及需要跨页复用的原资源文件,不修改资源池。
|
||||
// 返回: []editorResource 新增字体, []editorResource 新增图片, []string 原资源文件
|
||||
func (e *Editor) usedResources() (fonts, images []editorResource, sourceFiles []string) {
|
||||
used := make(map[string]bool)
|
||||
promoted := make(map[string]bool)
|
||||
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 _, object := range layer.Objects {
|
||||
before, exists := original[editorObjectID(object)]
|
||||
switch object.Type {
|
||||
case "TextObject":
|
||||
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":
|
||||
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 != "" {
|
||||
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 {
|
||||
if resource.font != nil && used[resource.font.ID] {
|
||||
fonts = append(fonts, resource)
|
||||
delete(promoted, resource.font.ID)
|
||||
}
|
||||
if resource.image != nil && used[resource.image.ID] {
|
||||
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 写出页面尺寸、图层和对象
|
||||
@@ -224,6 +267,15 @@ func (x *ofdXML) page(page PageContent) {
|
||||
x.end("Area")
|
||||
x.start("Content", nil)
|
||||
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
|
||||
attrs.add("ID", layer.ID)
|
||||
attrs.add("Type", layer.Type)
|
||||
@@ -232,9 +284,6 @@ func (x *ofdXML) page(page PageContent) {
|
||||
x.object(object, false)
|
||||
}
|
||||
x.end("Layer")
|
||||
}
|
||||
x.end("Content")
|
||||
x.end("Page")
|
||||
}
|
||||
|
||||
// resources 写出文档资源索引
|
||||
|
||||
Reference in new issue
Block a user