mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-08-30 12:12:40 +08:00
feat(正式发布): 实现了OFD渲染和导出为PDF、图片、矢量图等
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
# OFDGo
|
||||
# OFDGo [](https://pkg.go.dev/github.com/xiaoqidun/ofdgo)
|
||||
首款原生、全平台兼容的纯 Go 语言 OFD 渲染库
|
||||
|
||||
# 安装指南
|
||||
```shell
|
||||
go get -u github.com/xiaoqidun/ofdgo
|
||||
```
|
||||
|
||||
# 授权协议
|
||||
本项目使用 [Apache License 2.0](https://github.com/xiaoqidun/ofdgo/blob/main/LICENSE) 授权协议
|
||||
@@ -1,3 +1,37 @@
|
||||
module github.com/xiaoqidun/ofdgo
|
||||
|
||||
go 1.25.5
|
||||
|
||||
require github.com/tdewolff/canvas v0.0.0-20251224152029-436d73bc8f67
|
||||
|
||||
require (
|
||||
codeberg.org/go-latex/latex v0.2.0 // indirect
|
||||
codeberg.org/go-pdf/fpdf v0.11.1 // 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.0 // indirect
|
||||
github.com/benoitkugler/textlayout v0.3.1 // indirect
|
||||
github.com/benoitkugler/textprocessing v0.0.3 // indirect
|
||||
github.com/go-fonts/latin-modern v0.3.3 // indirect
|
||||
github.com/go-text/typesetting v0.3.0 // 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/font v0.0.0-20250902141222-fb72ecc1bc0a // indirect
|
||||
github.com/tdewolff/minify/v2 v2.24.4 // indirect
|
||||
github.com/tdewolff/parse/v2 v2.8.4 // indirect
|
||||
github.com/wcharczuk/go-chart/v2 v2.1.2 // indirect
|
||||
github.com/yuin/goldmark v1.7.13 // indirect
|
||||
golang.org/x/image v0.32.0 // indirect
|
||||
golang.org/x/net v0.46.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
gonum.org/v1/plot v0.16.0 // indirect
|
||||
modernc.org/knuth v0.5.5 // indirect
|
||||
modernc.org/token v1.1.0 // indirect
|
||||
star-tex.org/x/tex v0.7.1 // indirect
|
||||
)
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
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.5.0 h1:SsKoMO1v1OZmzkG2DY+7ZkCL9U+rrWI09niOLfQ5Bo0=
|
||||
codeberg.org/go-fonts/liberation v0.5.0/go.mod h1:zS/2e1354/mJ4pGzIIaEtm/59VFCFnYC7YV6YdGl5GU=
|
||||
codeberg.org/go-latex/latex v0.2.0 h1:Ol/a6VHY06N+5gPfewswymoRb5ZcKDXWVaVegcx4hbI=
|
||||
codeberg.org/go-latex/latex v0.2.0/go.mod h1:VJAwQir7/T8LZxj7xAPivISKiVOwkMpQ8bTuPQ31X0Y=
|
||||
codeberg.org/go-pdf/fpdf v0.11.1 h1:U8+coOTDVLxHIXZgGvkfQEi/q0hYHYvEHFuGNX2GzGs=
|
||||
codeberg.org/go-pdf/fpdf v0.11.1/go.mod h1:Y0DGRAdZ0OmnZPvjbMp/1bYxmIPxm0ws4tfoPOc4LjU=
|
||||
git.sr.ht/~sbinet/cmpimg v0.1.0 h1:E0zPRk2muWuCqSKSVZIWsgtU9pjsw3eKHi8VmQeScxo=
|
||||
git.sr.ht/~sbinet/cmpimg v0.1.0/go.mod h1:FU12psLbF4TfNXkKH2ZZQ29crIqoiqTZmeQ7dkp/pxE=
|
||||
git.sr.ht/~sbinet/gg v0.7.0 h1:YmNf7YKd7diDMTPm86hZa1EM3pbkOyD/zzjl0LZUdNM=
|
||||
git.sr.ht/~sbinet/gg v0.7.0/go.mod h1:VYeli15tpMM4EvqlivlVbbyvWZlOU+EZn4XZmfBGUdM=
|
||||
github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 h1:1qlsVAQJXZHsaM8b6OLVo6muQUQd4CwkH/D3fnnbHXA=
|
||||
github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298/go.mod h1:D+QujdIlUNfa0igpNMk6UIvlb6C252URs4yupRUV4lQ=
|
||||
github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 h1:lTG4HQym5oPKjL7nGs+csTgiDna685ZXjxijkne828g=
|
||||
github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966/go.mod h1:Mid70uvE93zn9wgF92A/r5ixgnvX8Lh68fxp9KQBaI0=
|
||||
github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc h1:7D+Bh06CRPCJO3gr2F7h1sriovOZ8BMhca2Rg85c2nk=
|
||||
github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 h1:O/r2Sj+8QcMF7V5IcmiE2sMFV2q3J47BEirxbXJAdzA=
|
||||
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.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/benoitkugler/pstokenizer v1.0.0/go.mod h1:l1G2Voirz0q/jj0TQfabNxVsa8HZXh/VMxFSRALWTiE=
|
||||
github.com/benoitkugler/textlayout v0.3.0/go.mod h1:o+1hFV+JSHBC9qNLIuwVoLedERU7sBPgEFcuSgfvi/w=
|
||||
github.com/benoitkugler/textlayout v0.3.1 h1:hXCAJv3/8oF2mm68jledvbq85l6dA+aOYkwnzH5v4F8=
|
||||
github.com/benoitkugler/textlayout v0.3.1/go.mod h1:o+1hFV+JSHBC9qNLIuwVoLedERU7sBPgEFcuSgfvi/w=
|
||||
github.com/benoitkugler/textlayout-testdata v0.1.1/go.mod h1:i/qZl09BbUOtd7Bu/W1CAubRwTWrEXWq6JwMkw8wYxo=
|
||||
github.com/benoitkugler/textprocessing v0.0.3 h1:Q2X+Z6vxuW5Bxn1R9RaNt0qcprBfpc2hEUDeTlz90Ng=
|
||||
github.com/benoitkugler/textprocessing v0.0.3/go.mod h1:/4bLyCf1QYywunMK3Gf89Nhb50YI/9POewqrLxWhxd4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/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.0 h1:OWCgYpp8njoxSRpwrdd1bQOxdjOXDj9Rqart9ML4iF4=
|
||||
github.com/go-text/typesetting v0.3.0/go.mod h1:qjZLkhRgOEYMhU9eHBr3AR4sfnGJvOXNLt8yRAySFuY=
|
||||
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0=
|
||||
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
|
||||
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=
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
|
||||
github.com/srwiley/scanFT v0.0.0-20220128184157-0d1ee492111f h1:uLR2GaV0kWYZ3Ns3l3sjtiN+mOWAQadvrL8HXcyKjl0=
|
||||
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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tdewolff/canvas v0.0.0-20251224152029-436d73bc8f67 h1:yqkrKShEWyV2lhAu30DITj9G/ByMjW+Ft1D93//40Zo=
|
||||
github.com/tdewolff/canvas v0.0.0-20251224152029-436d73bc8f67/go.mod h1:JUnBKQtnaYE12uB8NR8KsEJl06JJoAyPZAqOV/UdRuw=
|
||||
github.com/tdewolff/font v0.0.0-20250902141222-fb72ecc1bc0a h1:IuR6wFg9mSxhxcCogXcG5bte813psi1PE4KTjMAkM6k=
|
||||
github.com/tdewolff/font v0.0.0-20250902141222-fb72ecc1bc0a/go.mod h1:lGIMHKyJnHCmJeb9MqdWnudFoPDVz8COuALmILs95xY=
|
||||
github.com/tdewolff/minify/v2 v2.24.4 h1:pQyr6eWDa+RXtAoZg+6wurh0jB9ojqw/qc5LlU7/z6c=
|
||||
github.com/tdewolff/minify/v2 v2.24.4/go.mod h1:iD9Qn7/brhKY9d0KLKMkZrqS8/bqxSxRKruBi7V6m+w=
|
||||
github.com/tdewolff/parse/v2 v2.8.4 h1:A6slgBLGGDPBMGA28KQZfHpaKffuNvhOe7zSag+x/rw=
|
||||
github.com/tdewolff/parse/v2 v2.8.4/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo=
|
||||
github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE=
|
||||
github.com/tdewolff/test v1.0.11/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/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.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
|
||||
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
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.32.0 h1:6lZQWq75h7L5IWNk0r+SCpUJ6tUVd3v4ZHnbRKLkUDQ=
|
||||
golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc=
|
||||
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.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
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/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.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
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.16.0 h1:dK28Qx/Ky4VmPUN/2zeW0ELyM6ucDnBAj5yun7M9n1g=
|
||||
gonum.org/v1/plot v0.16.0/go.mod h1:Xz6U1yDMi6Ni6aaXILqmVIb6Vro8E+K7Q/GeeH+Pn0c=
|
||||
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=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/knuth v0.5.5 h1:6lap2U/ISm8aC/4NU58ALFCRllNPaK0EZcIGY/oDgUg=
|
||||
modernc.org/knuth v0.5.5/go.mod h1:e5SBb35HQBj2aFwbBO3ClPcViLY3Wi0LzaOd7c/3qMk=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
star-tex.org/x/tex v0.7.1 h1:4qGAByRyY0WQsOjtcHlxz+FgrYxz8fzxIds2Gjepp5U=
|
||||
star-tex.org/x/tex v0.7.1/go.mod h1:Y3y0U7sZTltTh/CDZIx0oAtMjG7eMaTuTtvDZGdyhJo=
|
||||
@@ -0,0 +1,116 @@
|
||||
// Copyright 2025 肖其顿 (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 首款原生、全平台兼容的纯 Go 语言 OFD 渲染库
|
||||
package ofdgo
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"io"
|
||||
"io/fs"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
)
|
||||
|
||||
// Open 打开OFD文件
|
||||
// 入参: path 文件路径
|
||||
// 返回: *Reader 阅读器实例, error 错误信息
|
||||
func Open(path string) (*Reader, error) {
|
||||
r, err := zip.OpenReader(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reader := &Reader{
|
||||
Path: path,
|
||||
Zip: &r.Reader,
|
||||
Closer: r,
|
||||
}
|
||||
if err := reader.initRoot(); err != nil {
|
||||
reader.Close()
|
||||
return nil, err
|
||||
}
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
// NewReader 从流创建一个 OFD 阅读器
|
||||
// 入参: r IO读取器, size 数据大小
|
||||
// 返回: *Reader 阅读器实例, error 错误信息
|
||||
func NewReader(r io.ReaderAt, size int64) (*Reader, error) {
|
||||
zr, err := zip.NewReader(r, size)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reader := &Reader{
|
||||
Zip: zr,
|
||||
}
|
||||
if err := reader.initRoot(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
// NewRenderer 创建渲染器
|
||||
// 入参: reader 阅读器, opts 渲染选项
|
||||
// 返回: *Renderer 渲染器实例
|
||||
func NewRenderer(reader *Reader, opts ...RendererOption) *Renderer {
|
||||
r := &Renderer{
|
||||
Reader: reader,
|
||||
DPI: 300.0,
|
||||
DrawParams: reader.drawParamCache,
|
||||
FontMap: make(map[string]*canvas.FontFamily),
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(r)
|
||||
}
|
||||
r.initCommon()
|
||||
return r
|
||||
}
|
||||
|
||||
// WithDPI 设置渲染DPI
|
||||
// 入参: dpi DPI值
|
||||
// 返回: RendererOption 渲染选项
|
||||
func WithDPI(dpi float64) RendererOption {
|
||||
return func(r *Renderer) {
|
||||
r.DPI = dpi
|
||||
}
|
||||
}
|
||||
|
||||
// WithFontDirs 设置外部字体查找目录
|
||||
// 入参: dirs 字体目录列表
|
||||
// 返回: RendererOption 渲染选项
|
||||
func WithFontDirs(dirs ...string) RendererOption {
|
||||
return func(r *Renderer) {
|
||||
r.fontDirs = append(r.fontDirs, dirs...)
|
||||
}
|
||||
}
|
||||
|
||||
// WithFontFS 设置外部字体文件系统
|
||||
// 入参: fs 字体文件系统
|
||||
// 返回: RendererOption 渲染选项
|
||||
func WithFontFS(fs ...fs.FS) RendererOption {
|
||||
return func(r *Renderer) {
|
||||
r.fontFS = append(r.fontFS, fs...)
|
||||
}
|
||||
}
|
||||
|
||||
// PageCount 获取文档总页数
|
||||
// 入参: reader 阅读器
|
||||
// 返回: int 页数
|
||||
func PageCount(reader *Reader) int {
|
||||
doc, _ := reader.Doc()
|
||||
if doc == nil {
|
||||
return 0
|
||||
}
|
||||
return len(doc.Pages.Page)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright 2025 肖其顿 (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 首款原生、全平台兼容的纯 Go 语言 OFD 渲染库
|
||||
package ofdgo
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
// Signatures 签名集合
|
||||
type Signatures struct {
|
||||
XMLName xml.Name `xml:"Signatures"`
|
||||
MaxSignID string `xml:"MaxSignId,attr"`
|
||||
Signature []Signature `xml:"Signature"`
|
||||
}
|
||||
|
||||
// Signature 签名信息
|
||||
type Signature struct {
|
||||
ID string `xml:"ID,attr"`
|
||||
Type string `xml:"Type,attr"`
|
||||
BaseLoc string `xml:"BaseLoc,attr"`
|
||||
}
|
||||
|
||||
// Annotations 注释集合
|
||||
type Annotations struct {
|
||||
XMLName xml.Name `xml:"Annotations"`
|
||||
Page []AnnPage `xml:"Page"`
|
||||
}
|
||||
|
||||
// AnnPage 页面注释引用
|
||||
type AnnPage struct {
|
||||
PageID string `xml:"PageID,attr"`
|
||||
Annotation []Annotation `xml:"Annotation"`
|
||||
}
|
||||
|
||||
// Annotation 注释定义
|
||||
type Annotation struct {
|
||||
ID string `xml:"ID,attr"`
|
||||
Type string `xml:"Type,attr"`
|
||||
Creator string `xml:"Creator,attr"`
|
||||
LastModDate string `xml:"LastModDate,attr"`
|
||||
Loc string `xml:"Loc,attr"`
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
// Copyright 2025 肖其顿 (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 首款原生、全平台兼容的纯 Go 语言 OFD 渲染库
|
||||
package ofdgo
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
// Document 文档结构
|
||||
type Document struct {
|
||||
XMLName xml.Name `xml:"Document"`
|
||||
CommonData CommonData `xml:"CommonData"`
|
||||
Pages Pages `xml:"Pages"`
|
||||
Outlines Outlines `xml:"Outlines"`
|
||||
Permissions Permissions `xml:"Permissions"`
|
||||
Annotations string `xml:"Annotations"`
|
||||
Attachments Attachments `xml:"Attachments"`
|
||||
Extensions Extensions `xml:"Extensions"`
|
||||
}
|
||||
|
||||
// Extensions 扩展集合
|
||||
type Extensions struct {
|
||||
Extension []Extension `xml:"Extension"`
|
||||
}
|
||||
|
||||
// Extension 扩展信息
|
||||
type Extension struct {
|
||||
AppName string `xml:"AppName,attr"`
|
||||
Company string `xml:"Company,attr"`
|
||||
AppVersion string `xml:"AppVersion,attr"`
|
||||
Date string `xml:"Date,attr"`
|
||||
RefID string `xml:"RefId,attr"`
|
||||
Property []Property `xml:"Property"`
|
||||
Data string `xml:"Data"`
|
||||
}
|
||||
|
||||
// Property 扩展属性
|
||||
type Property struct {
|
||||
Name string `xml:"Name,attr"`
|
||||
Value string `xml:",chardata"`
|
||||
Type string `xml:"Type,attr"`
|
||||
}
|
||||
|
||||
// Attachments 附件集合
|
||||
type Attachments struct {
|
||||
Attachment []Attachment `xml:"Attachment"`
|
||||
}
|
||||
|
||||
// Attachment 附件信息
|
||||
type Attachment struct {
|
||||
Name string `xml:"Name,attr"`
|
||||
File string `xml:"File,attr"`
|
||||
ID string `xml:"ID,attr"`
|
||||
}
|
||||
|
||||
// CommonData 文档公共数据
|
||||
type CommonData struct {
|
||||
MaxUnitID int `xml:"MaxUnitID"`
|
||||
PageArea PageArea `xml:"PageArea"`
|
||||
PublicRes string `xml:"PublicRes"`
|
||||
DocumentRes string `xml:"DocumentRes"`
|
||||
TemplatePage []TemplatePage `xml:"TemplatePage"`
|
||||
DefaultCS int `xml:"DefaultCS"`
|
||||
}
|
||||
|
||||
// PageArea 页面区域定义
|
||||
type PageArea struct {
|
||||
PhysicalBox string `xml:"PhysicalBox"`
|
||||
ApplicationBox string `xml:"ApplicationBox"`
|
||||
ContentBox string `xml:"ContentBox"`
|
||||
BleedBox string `xml:"BleedBox"`
|
||||
}
|
||||
|
||||
// Pages 页面引用集合
|
||||
type Pages struct {
|
||||
Page []Page `xml:"Page"`
|
||||
}
|
||||
|
||||
// Page 页面引用
|
||||
type Page struct {
|
||||
ID string `xml:"ID,attr"`
|
||||
BaseLoc string `xml:"BaseLoc,attr"`
|
||||
}
|
||||
|
||||
// TemplatePage 模板页
|
||||
type TemplatePage struct {
|
||||
ID string `xml:"ID,attr"`
|
||||
Name string `xml:"Name,attr"`
|
||||
BaseLoc string `xml:"BaseLoc,attr"`
|
||||
ZOrder string `xml:"ZOrder,attr"`
|
||||
}
|
||||
|
||||
// Outlines 大纲集合
|
||||
type Outlines struct {
|
||||
OutlineElem []OutlineElem `xml:"OutlineElem"`
|
||||
}
|
||||
|
||||
// OutlineElem 大纲节点
|
||||
type OutlineElem struct {
|
||||
Title string `xml:"Title,attr"`
|
||||
Count int `xml:"Count,attr"`
|
||||
Actions string `xml:"Actions"`
|
||||
}
|
||||
|
||||
// Permissions 权限声明
|
||||
type Permissions struct {
|
||||
Edit bool `xml:"Edit"`
|
||||
Print bool `xml:"Print"`
|
||||
Export bool `xml:"Export"`
|
||||
Copy bool `xml:"Copy"`
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
// Copyright 2025 肖其顿 (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 首款原生、全平台兼容的纯 Go 语言 OFD 渲染库
|
||||
package ofdgo
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Box 矩形区域
|
||||
type Box struct {
|
||||
X, Y, W, H float64
|
||||
}
|
||||
|
||||
// ParseBox 解析Box字符串
|
||||
// 入参: s 字符串
|
||||
// 返回: Box 矩形对象, error 错误信息
|
||||
func ParseBox(s string) (Box, error) {
|
||||
parts := strings.Fields(s)
|
||||
if len(parts) < 4 {
|
||||
return Box{}, nil
|
||||
}
|
||||
x, _ := strconv.ParseFloat(parts[0], 64)
|
||||
y, _ := strconv.ParseFloat(parts[1], 64)
|
||||
w, _ := strconv.ParseFloat(parts[2], 64)
|
||||
h, _ := strconv.ParseFloat(parts[3], 64)
|
||||
return Box{X: x, Y: y, W: w, H: h}, nil
|
||||
}
|
||||
|
||||
// Matrix 2D仿射变换矩阵
|
||||
type Matrix struct {
|
||||
a, b, c, d, e, f float64
|
||||
}
|
||||
|
||||
// IdentityMatrix 单位矩阵
|
||||
var IdentityMatrix = Matrix{1, 0, 0, 1, 0, 0}
|
||||
|
||||
// NewMatrix 解析CTM字符串
|
||||
// 入参: s 字符串
|
||||
// 返回: Matrix 矩阵对象
|
||||
func NewMatrix(s string) Matrix {
|
||||
floats := parseFloats(s)
|
||||
if len(floats) != 6 {
|
||||
return IdentityMatrix
|
||||
}
|
||||
return Matrix{
|
||||
a: floats[0], b: floats[1],
|
||||
c: floats[2], d: floats[3],
|
||||
e: floats[4], f: floats[5],
|
||||
}
|
||||
}
|
||||
|
||||
// Transform 应用变换矩阵
|
||||
// 入参: x X坐标, y Y坐标
|
||||
// 返回: float64 变换后X, float64 变换后Y
|
||||
func (m Matrix) Transform(x, y float64) (float64, float64) {
|
||||
nx := m.a*x + m.c*y + m.e
|
||||
ny := m.b*x + m.d*y + m.f
|
||||
return nx, ny
|
||||
}
|
||||
|
||||
// YScale 获取Y轴缩放比例
|
||||
// 返回: float64 缩放比例
|
||||
func (m Matrix) YScale() float64 {
|
||||
return math.Sqrt(m.c*m.c + m.d*m.d)
|
||||
}
|
||||
|
||||
// parseFloats 解析浮点数数组
|
||||
// 入参: s 字符串
|
||||
// 返回: []float64 浮点数数组
|
||||
func parseFloats(s string) []float64 {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
if strings.Contains(s, "g") {
|
||||
return parseFloatsWithG(s)
|
||||
}
|
||||
s = strings.ReplaceAll(s, ",", " ")
|
||||
parts := strings.Fields(s)
|
||||
result := make([]float64, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
if v, err := strconv.ParseFloat(p, 64); err == nil {
|
||||
result = append(result, v)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// parseFloatsWithG 解析带g的压缩浮点数数组
|
||||
// 入参: s 字符串
|
||||
// 返回: []float64 浮点数数组
|
||||
func parseFloatsWithG(s string) []float64 {
|
||||
parts := strings.Fields(s)
|
||||
var result []float64
|
||||
gFlag := false
|
||||
gCount := 0
|
||||
for _, p := range parts {
|
||||
if p == "g" {
|
||||
gFlag = true
|
||||
continue
|
||||
}
|
||||
if gFlag {
|
||||
gCount, _ = strconv.Atoi(p)
|
||||
gFlag = false
|
||||
continue
|
||||
}
|
||||
if gCount > 0 {
|
||||
v, _ := strconv.ParseFloat(p, 64)
|
||||
for j := 0; j < gCount; j++ {
|
||||
result = append(result, v)
|
||||
}
|
||||
gCount = 0
|
||||
} else {
|
||||
if v, err := strconv.ParseFloat(p, 64); err == nil {
|
||||
result = append(result, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright 2025 肖其顿 (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 首款原生、全平台兼容的纯 Go 语言 OFD 渲染库
|
||||
package ofdgo
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
// OFD OFD入口结构
|
||||
// 代表OFD文档的根节点
|
||||
type OFD struct {
|
||||
XMLName xml.Name `xml:"OFD"`
|
||||
Version string `xml:"Version,attr"`
|
||||
DocType string `xml:"DocType,attr"`
|
||||
DocBody []DocBody `xml:"DocBody"`
|
||||
}
|
||||
|
||||
// DocBody 文档体信息
|
||||
// 包含文档元数据和根节点路径
|
||||
type DocBody struct {
|
||||
DocInfo DocInfo `xml:"DocInfo"`
|
||||
DocRoot string `xml:"DocRoot"`
|
||||
Signatures string `xml:"Signatures"`
|
||||
}
|
||||
|
||||
// DocInfo 文档元数据
|
||||
type DocInfo struct {
|
||||
DocID string `xml:"DocID"`
|
||||
Title string `xml:"Title"`
|
||||
Author string `xml:"Author"`
|
||||
Subject string `xml:"Subject"`
|
||||
Abstract string `xml:"Abstract"`
|
||||
CreationDate string `xml:"CreationDate"`
|
||||
ModDate string `xml:"ModDate"`
|
||||
CustomDatas *CustomDatas `xml:"CustomDatas"`
|
||||
}
|
||||
|
||||
// CustomDatas 自定义数据集合
|
||||
type CustomDatas struct {
|
||||
CustomData []CustomData `xml:"CustomData"`
|
||||
}
|
||||
|
||||
// CustomData 自定义数据项
|
||||
type CustomData struct {
|
||||
Name string `xml:"Name,attr"`
|
||||
Value string `xml:",chardata"`
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
// Copyright 2025 肖其顿 (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 首款原生、全平台兼容的纯 Go 语言 OFD 渲染库
|
||||
package ofdgo
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
// PageContent 页面内容
|
||||
type PageContent struct {
|
||||
XMLName xml.Name `xml:"Page"`
|
||||
Area PageArea `xml:"Area"`
|
||||
Template []Template `xml:"Template"`
|
||||
Content Content `xml:"Content"`
|
||||
}
|
||||
|
||||
// Template 页面模板引用
|
||||
type Template struct {
|
||||
TemplateID string `xml:"TemplateID,attr"`
|
||||
ZOrder string `xml:"ZOrder,attr"`
|
||||
}
|
||||
|
||||
// Content 页面内容节点
|
||||
type Content struct {
|
||||
Layer []Layer `xml:"Layer"`
|
||||
}
|
||||
|
||||
// Layer 图层
|
||||
type Layer struct {
|
||||
ID string `xml:"ID,attr"`
|
||||
DrawParam string `xml:"DrawParam,attr"`
|
||||
TextObject []TextObject `xml:"TextObject"`
|
||||
PathObject []PathObject `xml:"PathObject"`
|
||||
ImageObject []ImageObject `xml:"ImageObject"`
|
||||
}
|
||||
|
||||
// TextObject 文本对象
|
||||
type TextObject struct {
|
||||
ID string `xml:"ID,attr"`
|
||||
Boundary string `xml:"Boundary,attr"`
|
||||
DrawParam string `xml:"DrawParam,attr"`
|
||||
Font string `xml:"Font,attr"`
|
||||
Size float64 `xml:"Size,attr"`
|
||||
Weight int `xml:"Weight,attr"`
|
||||
Italic bool `xml:"Italic,attr"`
|
||||
Decoration string `xml:"Decoration,attr"`
|
||||
HScale float64 `xml:"HScale,attr"`
|
||||
CTM string `xml:"CTM,attr"`
|
||||
FillColor *FillColor `xml:"FillColor"`
|
||||
TextCode []TextCode `xml:"TextCode"`
|
||||
}
|
||||
|
||||
// FillColor 填充颜色
|
||||
type FillColor struct {
|
||||
Value string `xml:"Value,attr"`
|
||||
}
|
||||
|
||||
// TextCode 文本内容节点
|
||||
type TextCode struct {
|
||||
X float64 `xml:"X,attr"`
|
||||
Y float64 `xml:"Y,attr"`
|
||||
DeltaX string `xml:"DeltaX,attr"`
|
||||
DeltaY string `xml:"DeltaY,attr"`
|
||||
Value string `xml:",chardata"`
|
||||
}
|
||||
|
||||
// PathObject 路径对象
|
||||
type PathObject struct {
|
||||
ID string `xml:"ID,attr"`
|
||||
Boundary string `xml:"Boundary,attr"`
|
||||
DrawParam string `xml:"DrawParam,attr"`
|
||||
LineWidth float64 `xml:"LineWidth,attr"`
|
||||
CTM string `xml:"CTM,attr"`
|
||||
StrokeColor *StrokeColor `xml:"StrokeColor"`
|
||||
FillColor *FillColor `xml:"FillColor"`
|
||||
AbbreviatedData string `xml:"AbbreviatedData"`
|
||||
}
|
||||
|
||||
// StrokeColor 勾边颜色
|
||||
type StrokeColor struct {
|
||||
Value string `xml:"Value,attr"`
|
||||
}
|
||||
|
||||
// ImageObject 图片对象
|
||||
type ImageObject struct {
|
||||
ID string `xml:"ID,attr"`
|
||||
Boundary string `xml:"Boundary,attr"`
|
||||
ResourceID string `xml:"ResourceID,attr"`
|
||||
CTM string `xml:"CTM,attr"`
|
||||
}
|
||||
+299
@@ -0,0 +1,299 @@
|
||||
// Copyright 2025 肖其顿 (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 首款原生、全平台兼容的纯 Go 语言 OFD 渲染库
|
||||
package ofdgo
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Reader OFD文件阅读器
|
||||
type Reader struct {
|
||||
Path string
|
||||
Zip *zip.Reader
|
||||
Closer io.Closer
|
||||
OFD *OFD
|
||||
RootDir string
|
||||
ResMap map[string]string
|
||||
fontCache map[string]*Font
|
||||
drawParamCache map[string]*DrawParam
|
||||
doc *Document
|
||||
}
|
||||
|
||||
// Close 关闭阅读器
|
||||
// 返回: error 错误信息
|
||||
func (r *Reader) Close() error {
|
||||
if r.Closer != nil {
|
||||
return r.Closer.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// initRoot 读取根节点信息
|
||||
// 返回: error 错误信息
|
||||
func (r *Reader) initRoot() error {
|
||||
data, err := r.readFile("OFD.xml")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read ofd.xml: %w", err)
|
||||
}
|
||||
var ofd OFD
|
||||
if err := xml.Unmarshal(data, &ofd); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal ofd.xml: %w", err)
|
||||
}
|
||||
r.OFD = &ofd
|
||||
return nil
|
||||
}
|
||||
|
||||
// readFile 读取压缩包内的文件
|
||||
// 入参: name 文件名
|
||||
// 返回: []byte 文件内容, error 错误信息
|
||||
func (r *Reader) readFile(name string) ([]byte, error) {
|
||||
name = strings.ReplaceAll(name, "\\", "/")
|
||||
for _, f := range r.Zip.File {
|
||||
if f.Name == name {
|
||||
return readZipFile(f)
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("file not found: %s", name)
|
||||
}
|
||||
|
||||
// readZipFile 读取zip文件内容
|
||||
// 入参: f zip文件对象
|
||||
// 返回: []byte 文件内容, error 错误信息
|
||||
func readZipFile(f *zip.File) ([]byte, error) {
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rc.Close()
|
||||
return io.ReadAll(rc)
|
||||
}
|
||||
|
||||
// Doc 获取主文档结构
|
||||
// 返回: *Document 文档结构, error 错误信息
|
||||
func (r *Reader) Doc() (*Document, error) {
|
||||
if r.doc != nil {
|
||||
return r.doc, nil
|
||||
}
|
||||
if r.OFD == nil || len(r.OFD.DocBody) == 0 {
|
||||
return nil, fmt.Errorf("no docbody found")
|
||||
}
|
||||
docAttr := r.OFD.DocBody[0]
|
||||
docRootPath := docAttr.DocRoot
|
||||
r.RootDir = path.Dir(docRootPath)
|
||||
data, err := r.readFile(docRootPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var doc Document
|
||||
if err := xml.Unmarshal(data, &doc); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal document.xml: %w", err)
|
||||
}
|
||||
r.OFD.DocBody[0].DocInfo.DocID = "loaded"
|
||||
r.ResMap = make(map[string]string)
|
||||
r.fontCache = make(map[string]*Font)
|
||||
r.drawParamCache = make(map[string]*DrawParam)
|
||||
if doc.CommonData.DocumentRes != "" {
|
||||
r.loadRes(doc.CommonData.DocumentRes)
|
||||
}
|
||||
if doc.CommonData.PublicRes != "" {
|
||||
r.loadRes(doc.CommonData.PublicRes)
|
||||
}
|
||||
r.doc = &doc
|
||||
return &doc, nil
|
||||
}
|
||||
|
||||
// loadRes 加载资源文件
|
||||
// 入参: resPath 资源路径
|
||||
func (r *Reader) loadRes(resPath string) {
|
||||
if resPath == "" {
|
||||
return
|
||||
}
|
||||
fullPath := path.Join(r.RootDir, resPath)
|
||||
data, err := r.readFile(fullPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var res Res
|
||||
if err := xml.Unmarshal(data, &res); err != nil {
|
||||
return
|
||||
}
|
||||
baseLoc := res.BaseLoc
|
||||
for _, mm := range res.MultiMedias.MultiMedia {
|
||||
if mm.MediaFile != "" {
|
||||
p := strings.TrimSpace(mm.MediaFile)
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
dir := path.Dir(resPath)
|
||||
if baseLoc != "" {
|
||||
if dir != baseLoc {
|
||||
dir = path.Join(dir, baseLoc)
|
||||
}
|
||||
}
|
||||
finalPath := path.Join(dir, p)
|
||||
r.ResMap[mm.ID] = finalPath
|
||||
}
|
||||
}
|
||||
for i := range res.Fonts.Font {
|
||||
f := &res.Fonts.Font[i]
|
||||
if f.FontFile != "" {
|
||||
dir := path.Dir(resPath)
|
||||
if baseLoc != "" {
|
||||
if dir != baseLoc {
|
||||
dir = path.Join(dir, baseLoc)
|
||||
}
|
||||
}
|
||||
f.FontFile = path.Join(dir, f.FontFile)
|
||||
}
|
||||
r.fontCache[f.ID] = f
|
||||
}
|
||||
for i := range res.DrawParams.DrawParam {
|
||||
dp := &res.DrawParams.DrawParam[i]
|
||||
r.drawParamCache[dp.ID] = dp
|
||||
}
|
||||
}
|
||||
|
||||
// PageContent 获取页面内容
|
||||
// 入参: page 页面对象
|
||||
// 返回: *PageContent 页面内容, error 错误信息
|
||||
func (r *Reader) PageContent(page Page) (*PageContent, error) {
|
||||
fullPath := path.Join(r.RootDir, page.BaseLoc)
|
||||
data, err := r.readFile(fullPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var content PageContent
|
||||
if err := xml.Unmarshal(data, &content); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal page content: %w", err)
|
||||
}
|
||||
return &content, nil
|
||||
}
|
||||
|
||||
// ResPath 获取资源的完整路径
|
||||
// 入参: resLink 资源链接
|
||||
// 返回: string 完整路径
|
||||
func (r *Reader) ResPath(resLink string) string {
|
||||
if resLink == "" {
|
||||
return ""
|
||||
}
|
||||
return path.Join(r.RootDir, resLink)
|
||||
}
|
||||
|
||||
// ResData 获取资源文件数据
|
||||
// 入参: resLink 资源链接
|
||||
// 返回: []byte 资源数据, error 错误信息
|
||||
func (r *Reader) ResData(resLink string) ([]byte, error) {
|
||||
fullPath := r.ResPath(resLink)
|
||||
return r.readFile(fullPath)
|
||||
}
|
||||
|
||||
// DocRoots 获取所有文档根路径
|
||||
// 返回: []string 路径列表
|
||||
func (r *Reader) DocRoots() []string {
|
||||
var roots []string
|
||||
if r.OFD == nil {
|
||||
return roots
|
||||
}
|
||||
for _, body := range r.OFD.DocBody {
|
||||
roots = append(roots, body.DocRoot)
|
||||
}
|
||||
return roots
|
||||
}
|
||||
|
||||
// Version 获取OFD版本号
|
||||
// 返回: string 版本号
|
||||
func (r *Reader) Version() string {
|
||||
if r.OFD == nil {
|
||||
return ""
|
||||
}
|
||||
return r.OFD.Version
|
||||
}
|
||||
|
||||
// DocType 获取文档类型
|
||||
// 返回: string 文档类型
|
||||
func (r *Reader) DocType() string {
|
||||
if r.OFD == nil {
|
||||
return ""
|
||||
}
|
||||
return r.OFD.DocType
|
||||
}
|
||||
|
||||
// DocInfo 获取文档元数据
|
||||
// 返回: *DocInfo 元数据, error 错误信息
|
||||
func (r *Reader) DocInfo() (*DocInfo, error) {
|
||||
if r.OFD == nil || len(r.OFD.DocBody) == 0 {
|
||||
return nil, fmt.Errorf("no docbody found")
|
||||
}
|
||||
return &r.OFD.DocBody[0].DocInfo, nil
|
||||
}
|
||||
|
||||
// Permissions 获取文档权限信息
|
||||
// 返回: *Permissions 权限信息, error 错误信息
|
||||
func (r *Reader) Permissions() (*Permissions, error) {
|
||||
doc, err := r.Doc()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &doc.Permissions, nil
|
||||
}
|
||||
|
||||
// Outlines 获取文档大纲
|
||||
// 返回: []OutlineElem 大纲列表, error 错误信息
|
||||
func (r *Reader) Outlines() ([]OutlineElem, error) {
|
||||
doc, err := r.Doc()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return doc.Outlines.OutlineElem, nil
|
||||
}
|
||||
|
||||
// Attachments 获取附件列表
|
||||
// 返回: []Attachment 附件列表, error 错误信息
|
||||
func (r *Reader) Attachments() ([]Attachment, error) {
|
||||
doc, err := r.Doc()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return doc.Attachments.Attachment, nil
|
||||
}
|
||||
|
||||
// CustomDatas 获取自定义数据
|
||||
// 返回: []CustomData 自定义数据列表, error 错误信息
|
||||
func (r *Reader) CustomDatas() ([]CustomData, error) {
|
||||
info, err := r.DocInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if info.CustomDatas == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return info.CustomDatas.CustomData, nil
|
||||
}
|
||||
|
||||
// Extensions 获取分列扩展项
|
||||
// 返回: []Extension 扩展项列表, error 错误信息
|
||||
func (r *Reader) Extensions() ([]Extension, error) {
|
||||
doc, err := r.Doc()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return doc.Extensions.Extension, nil
|
||||
}
|
||||
@@ -0,0 +1,576 @@
|
||||
// Copyright 2025 肖其顿 (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 首款原生、全平台兼容的纯 Go 语言 OFD 渲染库
|
||||
package ofdgo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"io"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
"github.com/tdewolff/canvas/renderers"
|
||||
"github.com/tdewolff/canvas/renderers/pdf"
|
||||
"github.com/tdewolff/canvas/renderers/rasterizer"
|
||||
)
|
||||
|
||||
// Renderer 渲染器实现
|
||||
type Renderer struct {
|
||||
Reader *Reader
|
||||
DPI float64
|
||||
fontFamily *canvas.FontFamily
|
||||
DrawParams map[string]*DrawParam
|
||||
FontMap map[string]*canvas.FontFamily
|
||||
fontDirs []string
|
||||
fontFS []fs.FS
|
||||
}
|
||||
|
||||
// RendererOption 渲染器配置选项
|
||||
type RendererOption func(*Renderer)
|
||||
|
||||
// RenderPage 渲染特定页面内容
|
||||
// 入参: page 页面内容
|
||||
// 返回: *canvas.Canvas 画布实例, err 错误信息
|
||||
func (r *Renderer) RenderPage(page *PageContent) (*canvas.Canvas, error) {
|
||||
boxStr := page.Area.PhysicalBox
|
||||
if boxStr == "" {
|
||||
boxStr = page.Area.ApplicationBox
|
||||
}
|
||||
if boxStr == "" {
|
||||
boxStr = page.Area.ContentBox
|
||||
}
|
||||
if boxStr == "" && r.Reader.doc != nil {
|
||||
boxStr = r.Reader.doc.CommonData.PageArea.PhysicalBox
|
||||
}
|
||||
if boxStr == "" {
|
||||
boxStr = "0 0 210 297"
|
||||
}
|
||||
box, err := ParseBox(boxStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c := canvas.New(box.W, box.H)
|
||||
ctx := canvas.NewContext(c)
|
||||
pageH := box.H
|
||||
ctx.SetFillColor(canvas.White)
|
||||
ctx.DrawPath(0, 0, canvas.Rectangle(box.W, box.H))
|
||||
if len(page.Template) > 0 && r.Reader.doc != nil {
|
||||
for _, tplRef := range page.Template {
|
||||
r.renderTemplate(ctx, tplRef.TemplateID, pageH)
|
||||
}
|
||||
}
|
||||
if page.Content.Layer != nil {
|
||||
for _, layer := range page.Content.Layer {
|
||||
r.renderLayer(ctx, layer, pageH, nil, nil, 0)
|
||||
}
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// RenderPageByIndex 按索引渲染页面
|
||||
// 入参: index 页面索引
|
||||
// 返回: *canvas.Canvas 画布实例, err 错误信息
|
||||
func (r *Renderer) RenderPageByIndex(index int) (*canvas.Canvas, error) {
|
||||
doc, err := r.Reader.Doc()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if index < 0 || index >= len(doc.Pages.Page) {
|
||||
return nil, fmt.Errorf("page index %d out of range", index)
|
||||
}
|
||||
page, err := r.Reader.PageContent(doc.Pages.Page[index])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.RenderPage(page)
|
||||
}
|
||||
|
||||
// RenderToImage 渲染为光栅图
|
||||
// 入参: page 页面内容
|
||||
// 返回: image.Image 图像对象, err 错误信息
|
||||
func (r *Renderer) RenderToImage(page *PageContent) (image.Image, error) {
|
||||
c, err := r.RenderPage(page)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dpmm := r.DPI / 25.4
|
||||
return rasterizer.Draw(c, canvas.DPMM(dpmm), canvas.DefaultColorSpace), nil
|
||||
}
|
||||
|
||||
// RenderToSVG 渲染为SVG
|
||||
// 入参: page 页面内容, writer 输出流
|
||||
// 返回: err 错误信息
|
||||
func (r *Renderer) RenderToSVG(page *PageContent, writer io.Writer) error {
|
||||
c, err := r.RenderPage(page)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Write(writer, renderers.SVG())
|
||||
}
|
||||
|
||||
// RenderToPDF 渲染为PDF
|
||||
// 入参: page 页面内容, writer 输出流
|
||||
// 返回: err 错误信息
|
||||
func (r *Renderer) RenderToPDF(page *PageContent, writer io.Writer) error {
|
||||
c, err := r.RenderPage(page)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Write(writer, renderers.PDF())
|
||||
}
|
||||
|
||||
// RenderToEPS 渲染为EPS
|
||||
// 入参: page 页面内容, writer 输出流
|
||||
// 返回: err 错误信息
|
||||
func (r *Renderer) RenderToEPS(page *PageContent, writer io.Writer) error {
|
||||
c, err := r.RenderPage(page)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Write(writer, renderers.EPS())
|
||||
}
|
||||
|
||||
// RenderToMultiPagePDF 将整个文档导出为多页PDF
|
||||
// 入参: writer 输出流
|
||||
// 返回: error 错误信息
|
||||
func (r *Renderer) RenderToMultiPagePDF(writer io.Writer) error {
|
||||
doc, err := r.Reader.Doc()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(doc.Pages.Page) == 0 {
|
||||
return fmt.Errorf("no pages found")
|
||||
}
|
||||
var p *pdf.PDF
|
||||
for i, pgRef := range doc.Pages.Page {
|
||||
page, err := r.Reader.PageContent(pgRef)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
c, err := r.RenderPage(page)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if i == 0 {
|
||||
p = pdf.New(writer, c.W, c.H, nil)
|
||||
} else {
|
||||
p.NewPage(c.W, c.H)
|
||||
}
|
||||
c.RenderTo(p)
|
||||
}
|
||||
if p == nil {
|
||||
return fmt.Errorf("failed to render any page")
|
||||
}
|
||||
return p.Close()
|
||||
}
|
||||
|
||||
// renderTemplate 渲染模板
|
||||
// 入参: ctx 画布上下文, templateID 模板ID, pageH 页面高度
|
||||
func (r *Renderer) renderTemplate(ctx *canvas.Context, templateID string, pageH float64) {
|
||||
var tplPage *TemplatePage
|
||||
for _, tp := range r.Reader.doc.CommonData.TemplatePage {
|
||||
if tp.ID == templateID {
|
||||
tplPage = &tp
|
||||
break
|
||||
}
|
||||
}
|
||||
if tplPage == nil {
|
||||
return
|
||||
}
|
||||
tplContent, err := r.Reader.PageContent(Page{BaseLoc: tplPage.BaseLoc})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if tplContent.Content.Layer != nil {
|
||||
for _, layer := range tplContent.Content.Layer {
|
||||
r.renderLayer(ctx, layer, pageH, nil, nil, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// renderLayer 渲染图层
|
||||
// 入参: ctx 画布上下文, layer 图层对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽
|
||||
func (r *Renderer) renderLayer(ctx *canvas.Context, layer Layer, pageH float64, defaultFill color.Color, defaultStroke color.Color, defaultLW float64) {
|
||||
if layer.DrawParam != "" {
|
||||
if dp := r.getDrawParam(layer.DrawParam, nil); dp != nil {
|
||||
if dp.LineWidth > 0 {
|
||||
defaultLW = dp.LineWidth
|
||||
}
|
||||
if dp.FillColor != nil {
|
||||
defaultFill = parseColor(dp.FillColor.Value)
|
||||
}
|
||||
if dp.StrokeColor != nil {
|
||||
defaultStroke = parseColor(dp.StrokeColor.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, imgObj := range layer.ImageObject {
|
||||
r.renderImage(ctx, imgObj, pageH)
|
||||
}
|
||||
for _, pathObj := range layer.PathObject {
|
||||
r.renderPath(ctx, pathObj, pageH, defaultFill, defaultStroke, defaultLW)
|
||||
}
|
||||
for _, textObj := range layer.TextObject {
|
||||
r.renderText(ctx, textObj, pageH, defaultFill, defaultStroke)
|
||||
}
|
||||
}
|
||||
|
||||
// getDrawParam 获取绘制参数逻辑
|
||||
// 入参: id 参数ID, visited 访问记录
|
||||
// 返回: *DrawParam 绘制参数
|
||||
func (r *Renderer) getDrawParam(id string, visited map[string]bool) *DrawParam {
|
||||
if visited == nil {
|
||||
visited = make(map[string]bool)
|
||||
}
|
||||
if visited[id] {
|
||||
return nil
|
||||
}
|
||||
visited[id] = true
|
||||
if dp, ok := r.DrawParams[id]; ok {
|
||||
if dp.Relative != "" {
|
||||
base := r.getDrawParam(dp.Relative, visited)
|
||||
if base == nil {
|
||||
return dp
|
||||
}
|
||||
merged := *base
|
||||
if dp.LineWidth > 0 {
|
||||
merged.LineWidth = dp.LineWidth
|
||||
}
|
||||
if dp.FillColor != nil {
|
||||
merged.FillColor = dp.FillColor
|
||||
}
|
||||
if dp.StrokeColor != nil {
|
||||
merged.StrokeColor = dp.StrokeColor
|
||||
}
|
||||
return &merged
|
||||
}
|
||||
return dp
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// initCommon 初始化公共资源
|
||||
func (r *Renderer) initCommon() {
|
||||
r.fontFamily = canvas.NewFontFamily("default")
|
||||
sysFonts := []string{
|
||||
"SimHei", "Microsoft YaHei", "SimSun", "KaiTi", "FangSong",
|
||||
"Arial", "Segoe UI", "Times New Roman",
|
||||
}
|
||||
for _, name := range sysFonts {
|
||||
if err := r.fontFamily.LoadSystemFont(name, canvas.FontRegular); err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// renderImage 渲染图片
|
||||
// 入参: ctx 画布上下文, obj 图片对象, pageH 页面高度
|
||||
func (r *Renderer) renderImage(ctx *canvas.Context, obj ImageObject, pageH float64) {
|
||||
resPath, ok := r.Reader.ResMap[obj.ResourceID]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
imgData, err := r.Reader.ResData(resPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
img, _, err := image.Decode(bytes.NewReader(imgData))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
box, _ := ParseBox(obj.Boundary)
|
||||
imgBounds := img.Bounds()
|
||||
imgW, imgH := float64(imgBounds.Dx()), float64(imgBounds.Dy())
|
||||
if imgW <= 0 || imgH <= 0 {
|
||||
return
|
||||
}
|
||||
ctm := NewMatrix(obj.CTM)
|
||||
if obj.CTM == "" {
|
||||
ctm = Matrix{a: box.W, d: box.H}
|
||||
}
|
||||
tx, ty := ctm.Transform(0, 1)
|
||||
canvasX, canvasY := tx+box.X, pageH-(ty+box.Y)
|
||||
ctx.Push()
|
||||
ctx.Translate(canvasX, canvasY)
|
||||
ctx.Scale(ctm.a/imgW, ctm.d/imgH)
|
||||
ctx.DrawImage(0, 0, img, canvas.DPMM(1.0))
|
||||
ctx.Pop()
|
||||
}
|
||||
|
||||
// renderPath 渲染路径
|
||||
// 入参: ctx 画布上下文, obj 路径对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽
|
||||
func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64, defaultFill, defaultStroke color.Color, defaultLW float64) {
|
||||
ctx.Push()
|
||||
bx, by := 0.0, 0.0
|
||||
if obj.Boundary != "" {
|
||||
if box, err := ParseBox(obj.Boundary); err == nil {
|
||||
bx, by = box.X, box.Y
|
||||
}
|
||||
}
|
||||
ctm := NewMatrix(obj.CTM)
|
||||
fillColor, strokeColor := defaultFill, defaultStroke
|
||||
if strokeColor == nil {
|
||||
strokeColor = canvas.Black
|
||||
}
|
||||
lineWidth := defaultLW
|
||||
if lineWidth == 0 {
|
||||
lineWidth = 0.353
|
||||
}
|
||||
if obj.DrawParam != "" {
|
||||
if dp := r.getDrawParam(obj.DrawParam, nil); dp != nil {
|
||||
if dp.LineWidth > 0 {
|
||||
lineWidth = dp.LineWidth
|
||||
}
|
||||
if dp.FillColor != nil {
|
||||
fillColor = parseColor(dp.FillColor.Value)
|
||||
}
|
||||
if dp.StrokeColor != nil {
|
||||
strokeColor = parseColor(dp.StrokeColor.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
if obj.LineWidth > 0 {
|
||||
lineWidth = obj.LineWidth
|
||||
}
|
||||
if obj.FillColor != nil {
|
||||
fillColor = parseColor(obj.FillColor.Value)
|
||||
}
|
||||
if obj.StrokeColor != nil {
|
||||
strokeColor = parseColor(obj.StrokeColor.Value)
|
||||
}
|
||||
p := &canvas.Path{}
|
||||
tokens := strings.Fields(obj.AbbreviatedData)
|
||||
for i := 0; i < len(tokens); {
|
||||
cmd := tokens[i]
|
||||
i++
|
||||
switch cmd {
|
||||
case "M", "S":
|
||||
if i+1 < len(tokens) {
|
||||
x, _ := strconv.ParseFloat(tokens[i], 64)
|
||||
y, _ := strconv.ParseFloat(tokens[i+1], 64)
|
||||
tx, ty := ctm.Transform(x, y)
|
||||
p.MoveTo(tx+bx, pageH-(ty+by))
|
||||
i += 2
|
||||
}
|
||||
case "L":
|
||||
if i+1 < len(tokens) {
|
||||
x, _ := strconv.ParseFloat(tokens[i], 64)
|
||||
y, _ := strconv.ParseFloat(tokens[i+1], 64)
|
||||
tx, ty := ctm.Transform(x, y)
|
||||
p.LineTo(tx+bx, pageH-(ty+by))
|
||||
i += 2
|
||||
}
|
||||
case "B":
|
||||
if i+5 < len(tokens) {
|
||||
x1, _ := strconv.ParseFloat(tokens[i], 64)
|
||||
y1, _ := strconv.ParseFloat(tokens[i+1], 64)
|
||||
x2, _ := strconv.ParseFloat(tokens[i+2], 64)
|
||||
y2, _ := strconv.ParseFloat(tokens[i+3], 64)
|
||||
x3, _ := strconv.ParseFloat(tokens[i+4], 64)
|
||||
y3, _ := strconv.ParseFloat(tokens[i+5], 64)
|
||||
tx1, ty1 := ctm.Transform(x1, y1)
|
||||
tx2, ty2 := ctm.Transform(x2, y2)
|
||||
tx3, ty3 := ctm.Transform(x3, y3)
|
||||
p.CubeTo(tx1+bx, pageH-(ty1+by), tx2+bx, pageH-(ty2+by), tx3+bx, pageH-(ty3+by))
|
||||
i += 6
|
||||
}
|
||||
case "C":
|
||||
p.Close()
|
||||
}
|
||||
}
|
||||
if fillColor != nil {
|
||||
ctx.SetFillColor(fillColor)
|
||||
ctx.DrawPath(0, 0, p)
|
||||
}
|
||||
ctx.SetStrokeColor(strokeColor)
|
||||
ctx.SetStrokeWidth(lineWidth)
|
||||
ctx.DrawPath(0, 0, p.Stroke(lineWidth, canvas.RoundCap, canvas.RoundJoin, canvas.Tolerance))
|
||||
ctx.Pop()
|
||||
}
|
||||
|
||||
// renderText 渲染文本
|
||||
// 入参: ctx 画布上下文, obj 文本对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色
|
||||
func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64, defaultFill, defaultStroke color.Color) {
|
||||
ctx.Push()
|
||||
bx, by := 0.0, 0.0
|
||||
if obj.Boundary != "" {
|
||||
if box, err := ParseBox(obj.Boundary); err == nil {
|
||||
bx, by = box.X, box.Y
|
||||
}
|
||||
}
|
||||
ctm := NewMatrix(obj.CTM)
|
||||
sizeMM := obj.Size
|
||||
if sizeMM == 0 {
|
||||
sizeMM = 3.5
|
||||
}
|
||||
if scale := ctm.YScale(); scale > 0 {
|
||||
sizeMM *= scale
|
||||
}
|
||||
sizePt := sizeMM * 2.83465
|
||||
textColor := defaultFill
|
||||
if textColor == nil {
|
||||
textColor = canvas.Black
|
||||
}
|
||||
if obj.DrawParam != "" {
|
||||
if dp := r.getDrawParam(obj.DrawParam, nil); dp != nil && dp.FillColor != nil {
|
||||
textColor = parseColor(dp.FillColor.Value)
|
||||
}
|
||||
}
|
||||
if obj.FillColor != nil {
|
||||
textColor = parseColor(obj.FillColor.Value)
|
||||
}
|
||||
fontStyle := canvas.FontRegular
|
||||
if obj.Weight >= 700 || (obj.Weight == 0 && (strings.Contains(strings.ToLower(obj.Font), "bold"))) {
|
||||
fontStyle |= canvas.FontBold
|
||||
}
|
||||
if obj.Italic {
|
||||
fontStyle |= canvas.FontItalic
|
||||
}
|
||||
ff := r.loadFont(obj.Font)
|
||||
face := ff.Face(sizePt, textColor, fontStyle, canvas.FontNormal)
|
||||
for _, tc := range obj.TextCode {
|
||||
runes := []rune(tc.Value)
|
||||
dxs, dys := parseFloats(tc.DeltaX), parseFloats(tc.DeltaY)
|
||||
cx, cy := tc.X, tc.Y
|
||||
for i, run := range runes {
|
||||
str := string(run)
|
||||
if i > 0 {
|
||||
if i-1 < len(dxs) {
|
||||
cx += dxs[i-1]
|
||||
} else {
|
||||
cx += face.TextWidth(str)
|
||||
}
|
||||
if i-1 < len(dys) {
|
||||
cy += dys[i-1]
|
||||
}
|
||||
}
|
||||
tx, ty := ctm.Transform(cx, cy)
|
||||
canvasX, canvasY := tx+bx, pageH-(ty+by)
|
||||
text := canvas.NewTextLine(face, str, canvas.Left)
|
||||
ctx.DrawText(canvasX, canvasY, text)
|
||||
if strings.Contains(obj.Decoration, "Underline") {
|
||||
uw := sizeMM * 0.05
|
||||
if uw < 0.05 {
|
||||
uw = 0.05
|
||||
}
|
||||
ctx.SetStrokeWidth(uw)
|
||||
ctx.SetStrokeColor(textColor)
|
||||
off := sizeMM * 0.1
|
||||
ctx.MoveTo(canvasX, canvasY-off)
|
||||
ctx.LineTo(canvasX+face.TextWidth(str), canvasY-off)
|
||||
ctx.Stroke()
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.Pop()
|
||||
}
|
||||
|
||||
// loadFont 加载字体
|
||||
// 入参: fontID 字体ID
|
||||
// 返回: *canvas.FontFamily 字体族
|
||||
func (r *Renderer) loadFont(fontID string) *canvas.FontFamily {
|
||||
if ff, ok := r.FontMap[fontID]; ok {
|
||||
return ff
|
||||
}
|
||||
defaultFont := r.fontFamily
|
||||
of, ok := r.Reader.fontCache[fontID]
|
||||
if !ok {
|
||||
return defaultFont
|
||||
}
|
||||
ff := canvas.NewFontFamily(of.FontName)
|
||||
if of.FontFile != "" {
|
||||
if fontData, err := r.Reader.ResData(of.FontFile); err == nil {
|
||||
if err := ff.LoadFont(fontData, 0, canvas.FontRegular); err == nil {
|
||||
r.FontMap[fontID] = ff
|
||||
return ff
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, dir := range r.fontDirs {
|
||||
matches, _ := filepath.Glob(filepath.Join(dir, of.FontName+"*"))
|
||||
for _, m := range matches {
|
||||
ext := strings.ToLower(filepath.Ext(m))
|
||||
if ext == ".ttf" || ext == ".otf" || ext == ".ttc" {
|
||||
if err := ff.LoadFontFile(m, canvas.FontRegular); err == nil {
|
||||
r.FontMap[fontID] = ff
|
||||
return ff
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, fsys := range r.fontFS {
|
||||
if matches, err := fs.Glob(fsys, of.FontName+"*"); err == nil {
|
||||
for _, m := range matches {
|
||||
resData, err := fs.ReadFile(fsys, m)
|
||||
if err == nil {
|
||||
if err := ff.LoadFont(resData, 0, canvas.FontRegular); err == nil {
|
||||
r.FontMap[fontID] = ff
|
||||
return ff
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
names := []string{of.FamilyName, of.FontName}
|
||||
aliases := map[string]string{
|
||||
"simhei": "SimHei",
|
||||
"黑体": "SimHei",
|
||||
"microsoft yahei": "Microsoft YaHei",
|
||||
"微软雅黑": "Microsoft YaHei",
|
||||
"simsun": "SimSun",
|
||||
"宋体": "SimSun",
|
||||
"kaiti": "KaiTi",
|
||||
"楷体": "KaiTi",
|
||||
"fangsong": "FangSong",
|
||||
"仿宋": "FangSong",
|
||||
"arial": "Arial",
|
||||
"segoe ui": "Segoe UI",
|
||||
"times new roman": "Times New Roman",
|
||||
}
|
||||
for _, name := range names {
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
targetName := name
|
||||
lower := strings.ToLower(name)
|
||||
if mapped, ok := aliases[lower]; ok {
|
||||
targetName = mapped
|
||||
} else {
|
||||
for k, v := range aliases {
|
||||
if strings.Contains(lower, k) {
|
||||
targetName = v
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := ff.LoadSystemFont(targetName, canvas.FontRegular); err == nil {
|
||||
r.FontMap[fontID] = ff
|
||||
return ff
|
||||
}
|
||||
if targetName != name {
|
||||
if err := ff.LoadSystemFont(name, canvas.FontRegular); err == nil {
|
||||
r.FontMap[fontID] = ff
|
||||
return ff
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaultFont
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright 2025 肖其顿 (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 首款原生、全平台兼容的纯 Go 语言 OFD 渲染库
|
||||
package ofdgo
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
// Res 资源文件结构
|
||||
type Res struct {
|
||||
XMLName xml.Name `xml:"Res"`
|
||||
BaseLoc string `xml:"BaseLoc,attr"`
|
||||
Fonts Fonts `xml:"Fonts"`
|
||||
MultiMedias MultiMedias `xml:"MultiMedias"`
|
||||
DrawParams DrawParams `xml:"DrawParams"`
|
||||
CompositeGraphicUnits CompositeGraphicUnits `xml:"CompositeGraphicUnits"`
|
||||
}
|
||||
|
||||
// Fonts 字体集合
|
||||
type Fonts struct {
|
||||
Font []Font `xml:"Font"`
|
||||
}
|
||||
|
||||
// Font 字体定义
|
||||
type Font struct {
|
||||
ID string `xml:"ID,attr"`
|
||||
FontName string `xml:"FontName,attr"`
|
||||
FamilyName string `xml:"FamilyName,attr"`
|
||||
FontFile string `xml:"FontFile"`
|
||||
}
|
||||
|
||||
// MultiMedias 多媒体集合
|
||||
type MultiMedias struct {
|
||||
MultiMedia []MultiMedia `xml:"MultiMedia"`
|
||||
}
|
||||
|
||||
// MultiMedia 多媒体定义
|
||||
type MultiMedia struct {
|
||||
ID string `xml:"ID,attr"`
|
||||
Type string `xml:"Type,attr"`
|
||||
Format string `xml:"Format,attr"`
|
||||
MediaFile string `xml:"MediaFile"`
|
||||
}
|
||||
|
||||
// DrawParams 绘制参数集合
|
||||
type DrawParams struct {
|
||||
DrawParam []DrawParam `xml:"DrawParam"`
|
||||
}
|
||||
|
||||
// DrawParam 绘制参数
|
||||
type DrawParam struct {
|
||||
ID string `xml:"ID,attr"`
|
||||
Relative string `xml:"Relative,attr"`
|
||||
ResourceID string `xml:"ResourceID,attr"`
|
||||
BaseLoc string `xml:"BaseLoc,attr"`
|
||||
LineWidth float64 `xml:"LineWidth,attr"`
|
||||
FillColor *FillColor `xml:"FillColor"`
|
||||
StrokeColor *StrokeColor `xml:"StrokeColor"`
|
||||
}
|
||||
|
||||
// CompositeGraphicUnits 复合图元集合
|
||||
type CompositeGraphicUnits struct {
|
||||
CompositeGraphicUnit []CompositeGraphicUnit `xml:"CompositeGraphicUnit"`
|
||||
}
|
||||
|
||||
// CompositeGraphicUnit 复合图元
|
||||
type CompositeGraphicUnit struct {
|
||||
ID string `xml:"ID,attr"`
|
||||
BaseLoc string `xml:"BaseLoc,attr"`
|
||||
ResourceID string `xml:"ResourceID,attr"`
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright 2025 肖其顿 (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 首款原生、全平台兼容的纯 Go 语言 OFD 渲染库
|
||||
package ofdgo
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// parseColor 解析颜色字符串
|
||||
// 入参: val 颜色值(R G B)
|
||||
// 返回: color.Color 颜色对象
|
||||
func parseColor(val string) color.Color {
|
||||
parts := strings.Fields(val)
|
||||
if len(parts) >= 3 {
|
||||
r, _ := strconv.Atoi(parts[0])
|
||||
g, _ := strconv.Atoi(parts[1])
|
||||
b, _ := strconv.Atoi(parts[2])
|
||||
return color.RGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: 255}
|
||||
}
|
||||
return color.Black
|
||||
}
|
||||
|
||||
// GetDeltaX 获取X轴偏移量数组
|
||||
// 返回: []float64 偏移量数组
|
||||
func (tc *TextCode) GetDeltaX() []float64 {
|
||||
return parseFloats(tc.DeltaX)
|
||||
}
|
||||
|
||||
// GetDeltaY 获取Y轴偏移量数组
|
||||
// 返回: []float64 偏移量数组
|
||||
func (tc *TextCode) GetDeltaY() []float64 {
|
||||
return parseFloats(tc.DeltaY)
|
||||
}
|
||||
Reference in New Issue
Block a user