feat(正式发布): 实现了OFD渲染和导出为PDF、图片、矢量图等

This commit is contained in:
2025-12-30 00:53:37 +08:00
parent 9f14eec058
commit 2e8b65b0a3
13 changed files with 1793 additions and 1 deletions
+6 -1
View File
@@ -1,5 +1,10 @@
# OFDGo
# OFDGo [![PkgGoDev](https://pkg.go.dev/badge/github.com/xiaoqidun/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) 授权协议
+34
View File
@@ -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
)
+164
View File
@@ -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=
+116
View File
@@ -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)
}
+53
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+58
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+576
View File
@@ -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
}
+82
View File
@@ -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"`
}
+48
View File
@@ -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)
}