diff --git a/README.md b/README.md index 4ba2c93..1ff2a96 100644 --- a/README.md +++ b/README.md @@ -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) 授权协议 \ No newline at end of file diff --git a/go.mod b/go.mod index fa238a1..c14ef5b 100644 --- a/go.mod +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3da633a --- /dev/null +++ b/go.sum @@ -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= diff --git a/ofdgo.go b/ofdgo.go new file mode 100644 index 0000000..dad9f73 --- /dev/null +++ b/ofdgo.go @@ -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) +} diff --git a/ofdgo_annot.go b/ofdgo_annot.go new file mode 100644 index 0000000..16c82a3 --- /dev/null +++ b/ofdgo_annot.go @@ -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"` +} diff --git a/ofdgo_doc.go b/ofdgo_doc.go new file mode 100644 index 0000000..fb62300 --- /dev/null +++ b/ofdgo_doc.go @@ -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"` +} diff --git a/ofdgo_geom.go b/ofdgo_geom.go new file mode 100644 index 0000000..ddd177c --- /dev/null +++ b/ofdgo_geom.go @@ -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 +} diff --git a/ofdgo_meta.go b/ofdgo_meta.go new file mode 100644 index 0000000..39b4143 --- /dev/null +++ b/ofdgo_meta.go @@ -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"` +} diff --git a/ofdgo_page.go b/ofdgo_page.go new file mode 100644 index 0000000..ebaf200 --- /dev/null +++ b/ofdgo_page.go @@ -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"` +} diff --git a/ofdgo_reader.go b/ofdgo_reader.go new file mode 100644 index 0000000..1ed66cc --- /dev/null +++ b/ofdgo_reader.go @@ -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 +} diff --git a/ofdgo_renderer.go b/ofdgo_renderer.go new file mode 100644 index 0000000..110d39c --- /dev/null +++ b/ofdgo_renderer.go @@ -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 +} diff --git a/ofdgo_res.go b/ofdgo_res.go new file mode 100644 index 0000000..5f8f5b5 --- /dev/null +++ b/ofdgo_res.go @@ -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"` +} diff --git a/ofdgo_text.go b/ofdgo_text.go new file mode 100644 index 0000000..ecd0f08 --- /dev/null +++ b/ofdgo_text.go @@ -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) +}