Compare commits

..
98 Commits
Author SHA1 Message Date
xiaoqidun 0887ff1a04 feat(前端界面): 优化单页适高滚动条
build.yaml / build (push) Successful in 47s
2026-08-24 09:37:21 +08:00
xiaoqidun 39f4fffeb7 feat(更新依赖): 更新jbig2、canvas
build.yaml / build (push) Successful in 44s
2026-08-23 21:25:54 +08:00
xiaoqidun e47d9bdffc feat(升级依赖): 升级jbig2、canvas
build.yaml / build (push) Successful in 3m26s
2026-08-21 23:00:56 +08:00
xiaoqidun 3d03448b6c feat(升级依赖): 更新jbig2和canvas
build.yaml / build (push) Successful in 41s
2026-07-28 11:52:42 +08:00
xiaoqidun 9462425a25 refactor(重构代码): 专注于性能提升 2026-07-28 11:12:21 +08:00
xiaoqidun efe3324d4b feat(系统字体): 添加android系统
build.yaml / build (push) Successful in 40s
2026-07-24 17:22:20 +08:00
xiaoqidun 78b66f85fe style(代码风格): 微调代码风格
build.yaml / build (push) Successful in 38s
2026-07-23 20:36:26 +08:00
xiaoqidun 0784a404f8 refactor(重构代码): 提升代码质量 2026-07-23 20:22:10 +08:00
xiaoqidun c4f7c448f8 feat(清理代码): 清理无效代码
build.yaml / build (push) Successful in 35s
2026-07-23 17:01:47 +08:00
xiaoqidun a60a935f50 feat(字体加载): 优化字体加载
build.yaml / build (push) Successful in 35s
2026-07-23 16:19:58 +08:00
xiaoqidun 960bb2b1e0 refactor(重构提升): 提升代码质量
build.yaml / build (push) Successful in 50s
2026-07-23 15:51:48 +08:00
xiaoqidun 6aa4e99530 feat(渲染优化): 完善渲染逻辑
build.yaml / build (push) Successful in 34s
2026-07-16 11:15:29 +08:00
xiaoqidun bc02d9e7f6 feat(图像渲染): 优化图像渲染
build.yaml / build (push) Successful in 38s
2026-07-15 16:07:20 +08:00
xiaoqidun e2896381e2 fix(图片渲染): 修复一个图片渲染的回归问题
build.yaml / build (push) Successful in 36s
2026-07-15 14:23:04 +08:00
xiaoqidun e5265a21d5 feat(前端界面): 优化签名展示
build.yaml / build (push) Successful in 37s
2026-07-15 12:43:00 +08:00
xiaoqidun b23ee90d31 feat(签名验证): 完善签名验证功能
build.yaml / build (push) Successful in 38s
2026-07-15 12:23:28 +08:00
xiaoqidun fa0c933f01 fix(字体匹配): 修复一个回归问题
build.yaml / build (push) Successful in 35s
2026-07-14 11:19:37 +08:00
xiaoqidun 3fecdbf89e feat(提升性能): 继续优化性能
build.yaml / build (push) Successful in 36s
2026-07-14 10:21:21 +08:00
xiaoqidun c17aae5a74 feat(优化性能): 提升渲染速度
build.yaml / build (push) Successful in 33s
2026-07-14 09:25:41 +08:00
xiaoqidun 08b801d351 feat(优化性能): 继续优化性能
build.yaml / build (push) Successful in 34s
2026-07-14 00:24:48 +08:00
xiaoqidun 8a68014262 feat(优化性能): 继续提升性能
build.yaml / build (push) Successful in 34s
2026-07-13 23:38:58 +08:00
xiaoqidun c8c5ce2cfc feat(前端界面): 调整字段显示顺序
build.yaml / build (push) Successful in 36s
2026-07-13 22:32:09 +08:00
xiaoqidun abc3549f7d feat(前端界面): 优化横向滚动条
build.yaml / build (push) Successful in 34s
2026-07-13 17:19:46 +08:00
xiaoqidun 7d8333b292 feat(前端界面): 优化横向滚动条
build.yaml / build (push) Successful in 34s
2026-07-13 17:07:12 +08:00
xiaoqidun 23849aee86 feat(性能优化): 提升速度
build.yaml / build (push) Successful in 34s
2026-07-13 16:51:25 +08:00
xiaoqidun 4e651619f8 style(代码风格): 优化代码风格
build.yaml / build (push) Successful in 36s
2026-07-13 16:20:01 +08:00
xiaoqidun 1004781aed feat(提升性能): 更快的加载图片
build.yaml / build (push) Successful in 38s
2026-07-13 16:17:08 +08:00
xiaoqidun 19ba4562a1 docs(更新文档): 更新文档
build.yaml / build (push) Successful in 36s
2026-07-10 21:13:10 +08:00
xiaoqidun 5b0da8f06b feat(前端界面): 优化前端签名展示
build.yaml / build (push) Successful in 39s
2026-07-10 20:46:51 +08:00
xiaoqidun 4cc24722c2 feat(签名验签): 优化签名验签 2026-07-10 20:39:28 +08:00
xiaoqidun 19fadab4ad feat(签名验签): 优化签名验签
build.yaml / build (push) Successful in 37s
2026-07-10 19:46:44 +08:00
xiaoqidun c20d3dd246 feat(签名验证): 完善签名签证功能
build.yaml / build (push) Successful in 37s
2026-07-10 19:13:19 +08:00
xiaoqidun a62230eb93 feat(提升性能): 提升JBIG2库性能
build.yaml / build (push) Successful in 39s
2026-07-10 16:27:59 +08:00
xiaoqidun f1a1f2e151 refactor(重构提升): 优化性能
build.yaml / build (push) Successful in 35s
2026-07-10 09:19:20 +08:00
xiaoqidun a1e6a86134 feat(前端界面): 优化前端界面
build.yaml / build (push) Successful in 33s
2026-07-09 19:06:22 +08:00
xiaoqidun f4291aaa74 feat(字体加载): 优化字体加载
build.yaml / build (push) Successful in 34s
2026-07-09 17:12:48 +08:00
xiaoqidun 48aabc77ab feat(前端界面): 优化DPI支持
build.yaml / build (push) Successful in 33s
2026-07-09 16:46:48 +08:00
xiaoqidun ba1da58992 feat(前端界面): 优化前端体验
build.yaml / build (push) Successful in 32s
2026-07-09 15:48:54 +08:00
xiaoqidun 1c1173e1b7 feat(前端界面): 优化OFDGo WebUI体验
build.yaml / build (push) Successful in 35s
2026-07-09 15:20:57 +08:00
xiaoqidun 368bafccdb feat(前端界面): OFDGo WebUI移除打印功能
build.yaml / build (push) Successful in 1m27s
2026-07-09 15:13:18 +08:00
xiaoqidun 51076634fa feat(前端界面): OFDGo WebUI支持调用浏览器打印接口直接打印
build.yaml / build (push) Successful in 32s
2026-07-09 14:42:17 +08:00
xiaoqidun 7ff1a4269a feat(前端界面): 优化按比例缩放
build.yaml / build (push) Successful in 33s
2026-07-09 14:22:17 +08:00
xiaoqidun 89577ea72f feat(前端界面): OFDGo WebUI支持设置DPI
build.yaml / build (push) Successful in 32s
2026-07-09 14:10:25 +08:00
xiaoqidun 3fab819246 feat(前端界面): 统一文案
build.yaml / build (push) Successful in 34s
2026-07-09 13:37:56 +08:00
xiaoqidun 38e55d962c feat(前端界面): 优化前端界面
build.yaml / build (push) Successful in 35s
2026-07-09 13:07:59 +08:00
xiaoqidun 5b8b015bfc feat(前端界面): 优化前端展示
build.yaml / build (push) Successful in 36s
2026-07-09 13:02:02 +08:00
xiaoqidun 64536cd49b feat(前端界面): 优化前端界面展示
build.yaml / build (push) Successful in 34s
2026-07-09 12:48:22 +08:00
xiaoqidun 155f5c7299 refactor(重构代码): 拆分代码,提升代码质量
build.yaml / build (push) Successful in 33s
2026-07-09 12:17:31 +08:00
xiaoqidun 9eb9104380 refactor(重构代码): 提升代码水平
build.yaml / build (push) Successful in 33s
2026-07-09 11:10:47 +08:00
xiaoqidun fa3260c108 refactor(重构代码): 提升代码质量
build.yaml / build (push) Successful in 34s
2026-07-09 10:33:21 +08:00
xiaoqidun 92a53ed3ad feat(升级依赖): 升级所有依赖 2026-07-09 10:06:54 +08:00
xiaoqidun 65b37b37f0 refactor(文件拆分): 考虑到可维护性进行了文件拆分
build.yaml / build (push) Successful in 35s
2026-07-09 00:08:28 +08:00
xiaoqidun 5e4009eca5 feat(字形绘制): 支持更多的字形绘制
build.yaml / build (push) Successful in 35s
2026-07-08 23:48:47 +08:00
xiaoqidun 1d99465df2 feat(前端界面): 优化性能
build.yaml / build (push) Successful in 36s
2026-07-08 22:23:18 +08:00
xiaoqidun a665df062b feat(前端界面): 优化前端性能
build.yaml / build (push) Successful in 35s
2026-07-08 22:11:30 +08:00
xiaoqidun ffd61d6412 feat(前端界面): 重置打开文件状态
build.yaml / build (push) Successful in 33s
2026-07-08 21:49:12 +08:00
xiaoqidun 7048bf4cb9 feat(前端界面): 优化OFDGo WebUI适高和适宽
build.yaml / build (push) Successful in 33s
2026-07-08 21:40:45 +08:00
xiaoqidun 9199ae02d8 feat(前端界面): 优化OFDGo WebUI适高、适宽
build.yaml / build (push) Successful in 35s
2026-07-08 21:19:51 +08:00
xiaoqidun ef4028a2fc feat(字体渲染): 改进字体匹配逻辑
build.yaml / build (push) Successful in 35s
2026-07-08 20:21:32 +08:00
xiaoqidun f191f1faec feat(字体绘制): 优化字体绘制
build.yaml / build (push) Successful in 35s
2026-07-08 17:12:39 +08:00
xiaoqidun 5254d22fb3 refactor(字体匹配): 优化字体匹配逻辑
build.yaml / build (push) Successful in 32s
2026-07-08 16:34:56 +08:00
xiaoqidun bb26adfe61 refactor(性能优化): 提升代码质量
build.yaml / build (push) Successful in 35s
2026-07-08 16:27:30 +08:00
xiaoqidun 6be7f4b006 refactor(优化代码): 提升代码质量
build.yaml / build (push) Successful in 34s
2026-07-08 16:21:02 +08:00
xiaoqidun 2f367e5a19 docs(更新文档): 优化文档描述
build.yaml / build (push) Successful in 34s
2026-07-08 16:12:07 +08:00
xiaoqidun b91c6b5330 docs(更新文档): 添加签名验证部分
build.yaml / build (push) Successful in 34s
2026-07-08 15:47:53 +08:00
xiaoqidun 27a5da45ec refactor(清理代码): 增加可维护性
build.yaml / build (push) Successful in 33s
2026-07-08 15:22:05 +08:00
xiaoqidun cc0a627a3c feat(前端界面): 优化签章展示
build.yaml / build (push) Successful in 35s
2026-07-08 15:07:11 +08:00
xiaoqidun 2aad6818dd feat(前端界面): 优化签名验签展示
build.yaml / build (push) Successful in 35s
2026-07-08 15:02:10 +08:00
xiaoqidun 5c762cf1e6 feat(签名验签): 持续优化签名验签功能
build.yaml / build (push) Successful in 35s
2026-07-08 14:51:39 +08:00
xiaoqidun e4f542c429 feat(前端界面): 优化点击印章处
build.yaml / build (push) Successful in 33s
2026-07-08 14:09:45 +08:00
xiaoqidun d7235a5078 feat(前端界面): 优化签名处点击印章位置
build.yaml / build (push) Successful in 33s
2026-07-08 13:59:06 +08:00
xiaoqidun c1e28bb1a3 feat(签名验签): 优化WebUI签名验签
build.yaml / build (push) Successful in 34s
2026-07-08 13:47:19 +08:00
xiaoqidun 725f1315fd feat(签名验签): 优化签名验签
build.yaml / build (push) Successful in 35s
2026-07-08 13:28:49 +08:00
xiaoqidun b3b8e0bac0 feat(签名验签): 继续优化逻辑
build.yaml / build (push) Successful in 34s
2026-07-08 13:12:45 +08:00
xiaoqidun 06c5cea4d1 feat(签名验签): 完善签名验签功能
build.yaml / build (push) Successful in 33s
2026-07-08 13:02:08 +08:00
xiaoqidun 6bfb11df71 feat(前端界面): 优化签名展示
build.yaml / build (push) Successful in 33s
2026-07-08 11:47:09 +08:00
xiaoqidun 7fbad6005c feat(更新依赖): 更新canvas版本
build.yaml / build (push) Successful in 36s
2026-07-08 11:10:34 +08:00
xiaoqidun 61da0f3f5d feat(签名展示): OFDGo WebUI支持签名展示
build.yaml / build (push) Successful in 35s
2026-07-08 11:02:20 +08:00
xiaoqidun 98b37d865a feat(签名验签): 支持签名验签 2026-07-08 09:42:16 +08:00
xiaoqidun 28f3efea58 feat(前端界面): 避免偶尔出现的底部横向滚动条
build.yaml / build (push) Successful in 37s
2026-07-07 21:47:41 +08:00
xiaoqidun e3aa8aba77 feat(前端界面): 优化OFDGo WebUI性能
build.yaml / build (push) Successful in 37s
2026-07-07 21:35:30 +08:00
xiaoqidun 889a6b1fae feat(前端界面): 优化前端文案
build.yaml / build (push) Successful in 33s
2026-07-07 16:12:02 +08:00
xiaoqidun 73336bcc0e feat(前端界面): 统一前端界面文案
build.yaml / build (push) Successful in 35s
2026-07-07 16:03:25 +08:00
xiaoqidun 67eed80507 feat(前端界面): 统一前端界面文字
build.yaml / build (push) Successful in 37s
2026-07-07 15:42:07 +08:00
xiaoqidun 3fd0b1bb5d feat(前端界面): 优化文案
build.yaml / build (push) Successful in 35s
2026-07-07 15:37:42 +08:00
xiaoqidun 3634a6125c feat(前端界面): 优化WebUI系统字体权限请求
build.yaml / build (push) Successful in 35s
2026-07-07 15:30:18 +08:00
xiaoqidun ebfe7b5d17 feat(前端界面): OFDGo WebUI优化系统字体权限请求 2026-07-07 15:14:25 +08:00
xiaoqidun 654bedbd33 feat(升级依赖): 更新JBIG2解码
build.yaml / build (push) Successful in 35s
2026-07-07 14:13:54 +08:00
xiaoqidun 749c71502e feat(前端界面): 优化OFDGo WebUI适宽适高
build.yaml / build (push) Successful in 37s
2026-07-07 14:07:30 +08:00
xiaoqidun f1d1f5ed2d feat(前端界面): OFDGo WebUI可选是否渲染注解
build.yaml / build (push) Successful in 35s
2026-07-07 13:10:55 +08:00
xiaoqidun 98244f2bb7 feat(前端界面): 优化工具栏分隔符
build.yaml / build (push) Successful in 33s
2026-07-07 11:06:01 +08:00
xiaoqidun dee56bd0fc feat(前端界面): 优化格式选择下拉框
build.yaml / build (push) Successful in 36s
2026-07-07 11:01:47 +08:00
xiaoqidun db860a22f1 feat(前端界面): OFDGo WebUI支持单页输出
build.yaml / build (push) Successful in 33s
2026-07-07 10:45:17 +08:00
xiaoqidun 2fc360e048 feat(前端界面): 避免加载过程中的横线滚动条
build.yaml / build (push) Successful in 37s
2026-07-07 09:52:23 +08:00
xiaoqidun 7fced50a34 feat(输出标识): 统一PDF输出标识
build.yaml / build (push) Successful in 39s
2026-07-06 09:02:33 +08:00
xiaoqidun 388b01dade feat(强化兼容): 支持位图和优化字体渲染
build.yaml / build (push) Successful in 32s
2026-07-04 00:35:59 +08:00
xiaoqidun d42d4394d3 feat(前端界面): 优化前端系统字体读取
build.yaml / build (push) Successful in 35s
2026-07-04 00:13:39 +08:00
xiaoqidun 63bb9d3cff feat(排除文件): 先精确后模糊
build.yaml / build (push) Successful in 35s
2026-07-03 21:48:56 +08:00
41 changed files with 9392 additions and 2692 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
.idea/
.vscode/
.devcontainer/
assets/webui/ofdgo.wasm
*.exe
*.exe~
assets/webui/ofdgo.wasm
+47 -1
View File
@@ -27,7 +27,7 @@ go build -o ofdgo_webui.exe -trimpath -ldflags "-s -w -buildid=" ./cmd/webui/web
go get -u github.com/xiaoqidun/ofdgo
```
# 快速调用
# 渲染输出
```go
package main
@@ -59,5 +59,51 @@ func main() {
}
```
# 签名验证
```go
package main
import (
"log"
"os"
"github.com/xiaoqidun/ofdgo"
)
func main() {
// 1. 打开OFD文件
data, err := os.ReadFile("test.ofd")
if err != nil {
log.Fatal(err)
}
// 2. 验证OFD签名
reports, err := ofdgo.VerifySignaturesBytes(data)
if err != nil {
log.Fatal(err)
}
// 3. 判断验证结果
if len(reports) == 0 {
log.Println("文件未发现签名")
return
}
valid := true
for _, report := range reports {
if report.IntegrityValid() {
log.Printf("签名%s验证通过", report.ID)
continue
}
valid = false
if report.Error == "" {
log.Printf("签名%s验证失败", report.ID)
} else {
log.Printf("签名%s验证失败: %s", report.ID, report.Error)
}
}
if !valid {
os.Exit(1)
}
}
```
# 授权协议
本项目使用 [Apache License 2.0](https://github.com/xiaoqidun/ofdgo/blob/main/LICENSE) 授权协议
+46 -18
View File
@@ -9,26 +9,42 @@
<body>
<div class="app">
<header class="toolbar app-bar">
<input id="ofdInput" class="file-input" type="file" accept=".ofd,application/ofd,application/zip">
<button id="ofdButton" class="button primary" type="button" data-short="打开">打开</button>
<input id="ofdInput" class="file-input" type="file" accept=".ofd">
<input id="fontInput" class="file-input" type="file" accept=".ttf,.otf,.ttc,font/*" multiple>
<button id="togglePagesButton" class="icon-button" type="button" title="页面栏" aria-label="页面栏" aria-pressed="true">&#9776;</button>
<button id="toggleMetaButton" class="icon-button" type="button" title="信息栏" aria-label="信息栏" aria-pressed="true">&#9432;</button>
<div class="tool-group">
<button id="ofdButton" class="button primary" type="button" data-short="打开">打开</button>
<button id="togglePagesButton" class="icon-button" type="button" title="页面栏" aria-label="页面栏" aria-pressed="true">&#9776;</button>
<button id="toggleMetaButton" class="icon-button" type="button" title="信息栏" aria-label="信息栏" aria-pressed="true">&#9432;</button>
</div>
<div class="toolbar-space"></div>
<button id="prevButton" class="icon-button" type="button" title="上一页" disabled>&lsaquo;</button>
<label class="page-control">
<input id="pageInput" type="number" min="1" value="1" disabled>
<span>/</span>
<span id="pageTotal">0</span>
</label>
<button id="nextButton" class="icon-button" type="button" title="下一页" disabled>&rsaquo;</button>
<div class="divider"></div>
<button id="zoomOutButton" class="icon-button" type="button" title="缩小" disabled>-</button>
<span id="zoomLabel" class="zoom-label">100%</span>
<button id="zoomInButton" class="icon-button" type="button" title="放大" disabled>+</button>
<button id="fitButton" class="button" type="button" data-short="宽" disabled>适宽</button>
<button id="fitHeightButton" class="button" type="button" data-short="" disabled>适高</button>
<button id="exportButton" class="button" type="button" data-short="PDF" title="导出 PDF" aria-label="导出 PDF" disabled>PDF</button>
<div class="tool-group">
<button id="prevButton" class="icon-button" type="button" title="上一页" disabled>&lsaquo;</button>
<label class="page-control">
<input id="pageInput" type="number" min="1" value="1" disabled>
<span>/</span>
<span id="pageTotal">0</span>
</label>
<button id="nextButton" class="icon-button" type="button" title="下一页" disabled>&rsaquo;</button>
</div>
<div class="tool-group">
<button id="zoomOutButton" class="icon-button" type="button" title="缩小" disabled>-</button>
<span id="zoomLabel" class="zoom-label">100%</span>
<button id="zoomInButton" class="icon-button" type="button" title="放大" disabled>+</button>
<button id="fitButton" class="button" type="button" data-short="" disabled>适宽</button>
<button id="fitHeightButton" class="button" type="button" data-short="高" disabled>适高</button>
<button id="annotationButton" class="button" type="button" data-short="注" title="渲染注解" aria-label="渲染注解" aria-pressed="true">注解</button>
</div>
<div class="tool-group">
<select id="pageExportFormat" class="format-select" title="单页格式" aria-label="单页格式" disabled></select>
<select id="imageDPI" class="format-select dpi-select" title="图片 DPI" aria-label="图片 DPI" disabled>
<option value="96">96</option>
<option value="150">150</option>
<option value="300" selected>300</option>
<option value="600">600</option>
</select>
<button id="exportPageButton" class="button" type="button" data-short="单" title="导出单页" aria-label="导出单页" disabled>单页</button>
<button id="exportButton" class="button" type="button" data-short="全" title="导出文档 PDF" aria-label="导出文档 PDF" disabled>文档</button>
</div>
</header>
<main class="workspace">
@@ -70,11 +86,22 @@
<dt>类型</dt>
<dd id="metaType">-</dd>
</div>
<div>
<dt>签名</dt>
<dd id="metaSignatures">0</dd>
</div>
<div>
<dt>字体</dt>
<dd id="metaFonts">0</dd>
</div>
</dl>
<section id="signaturePanel" class="signature-panel" aria-labelledby="signaturePanelTitle" hidden>
<div class="panel-row">
<div id="signaturePanelTitle" class="panel-title compact">签名</div>
<span id="signatureSummary" class="count-pill">0</span>
</div>
<div id="signatureList" class="signature-list"></div>
</section>
<section class="doc-font-panel" aria-labelledby="docFontPanelTitle">
<div class="panel-row">
<div id="docFontPanelTitle" class="panel-title compact">OFD 字体</div>
@@ -91,6 +118,7 @@
<button id="fontAddButton" class="small-button" type="button">添加</button>
</div>
</div>
<div id="fontPermissionHint" class="font-permission-hint" hidden>需授权读取系统字体</div>
<div id="fontList" class="font-list"></div>
</section>
</aside>
+219 -27
View File
@@ -10,12 +10,13 @@
--text-muted: #66737f;
--accent: #0f7b68;
--accent-strong: #095c4e;
--focus: #c9892b;
--focus: #0f7b68;
--sidebar-width: clamp(220px, 20vw, 292px);
--bar-bg: #fbfcfd;
--bar-height: 40px;
--bar-pad: 5px 10px;
--control-height: 28px;
--page-space: clamp(14px, 2.5vw, 28px);
--shadow-soft: 0 10px 28px rgba(31, 36, 40, 0.12);
--shadow-page: 0 14px 30px rgba(31, 36, 40, 0.16);
}
@@ -24,15 +25,24 @@
box-sizing: border-box;
}
html {
width: 100%;
height: 100%;
overflow: hidden;
}
body {
margin: 0;
width: 100%;
min-width: 320px;
min-height: 100vh;
overflow: hidden;
background: #eef1f4;
}
button,
input {
input,
select {
font: inherit;
}
@@ -45,13 +55,15 @@ button {
}
button:disabled,
input:disabled {
input:disabled,
select:disabled {
cursor: not-allowed;
opacity: 0.48;
}
button:focus-visible,
input:focus-visible,
select:focus-visible,
a:focus-visible {
outline: 2px solid var(--focus);
outline-offset: 2px;
@@ -60,7 +72,7 @@ a:focus-visible {
.app {
display: grid;
grid-template-rows: auto minmax(0, 1fr) auto;
width: 100vw;
width: 100%;
height: 100vh;
overflow: hidden;
}
@@ -81,10 +93,12 @@ a:focus-visible {
}
.toolbar {
--tool-gap: 8px;
position: relative;
z-index: 12;
border-bottom: 1px solid var(--line);
font-size: 14px;
gap: var(--tool-gap);
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
@@ -149,10 +163,26 @@ button:not(:disabled):hover {
flex: 1 1 auto;
}
.divider {
.tool-group {
display: inline-flex;
align-items: center;
gap: var(--tool-gap);
flex: 0 0 auto;
min-width: 0;
}
.tool-group + .tool-group {
margin-left: 0;
padding-left: 0;
}
.tool-group + .tool-group::before {
flex: 0 0 1px;
width: 1px;
height: 22px;
margin: 0 calc(12px - var(--tool-gap));
background: var(--line);
content: "";
}
.page-control {
@@ -176,6 +206,33 @@ button:not(:disabled):hover {
background: transparent;
}
.format-select {
width: 58px;
height: var(--control-height);
min-width: 0;
padding: 0 4px;
border: 1px solid var(--line);
border-radius: 0;
background: #ffffff;
color: #1f2428;
}
.dpi-select {
width: 54px;
}
.format-select:focus,
.format-select:focus-visible {
border-color: var(--accent);
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.format-select option:checked {
background-color: #e4f3ef;
color: var(--accent-strong);
}
.zoom-label {
min-width: 42px;
text-align: center;
@@ -344,6 +401,10 @@ body[data-hide-meta] .meta-panel {
background-size: 24px 24px;
}
.viewer-panel.single-page-fits-height {
overflow-y: hidden;
}
.empty-state {
position: absolute;
inset: 0;
@@ -419,15 +480,18 @@ body[data-hide-meta] .meta-panel {
}
.page-frame {
display: grid;
align-items: safe center;
min-width: 100%;
min-height: 100%;
padding: 34px 28px;
padding: var(--page-space);
padding-block: var(--fit-space, var(--page-space));
}
.svg-host {
display: grid;
justify-items: center;
gap: 28px;
gap: var(--fit-gap, var(--fit-space, var(--page-space)));
width: max-content;
margin: 0 auto;
}
@@ -466,6 +530,23 @@ body[data-hide-meta] .meta-panel {
display: none;
}
.stamp-highlight {
position: absolute;
z-index: 4;
border: 2px solid var(--accent);
background: rgba(15, 123, 104, 0.08);
box-shadow: 0 0 0 3px rgba(15, 123, 104, 0.16);
pointer-events: none;
animation: stampPulse 0.9s ease-in-out 2;
}
@keyframes stampPulse {
50% {
border-color: var(--accent-strong);
box-shadow: 0 0 0 5px rgba(15, 123, 104, 0.18);
}
}
.ofd-svg {
display: block;
max-width: none;
@@ -495,7 +576,8 @@ body[data-hide-meta] .meta-panel {
}
.font-panel,
.doc-font-panel {
.doc-font-panel,
.signature-panel {
margin-top: 18px;
padding-top: 12px;
border-top: 1px solid var(--line);
@@ -516,12 +598,14 @@ body[data-hide-meta] .meta-panel {
white-space: nowrap;
}
.doc-font-list {
.doc-font-list,
.signature-list {
display: grid;
gap: 8px;
}
.doc-font-row {
.doc-font-row,
.signature-row {
display: grid;
gap: 6px;
padding: 9px;
@@ -532,6 +616,7 @@ body[data-hide-meta] .meta-panel {
.page-list-item:not(:disabled):hover,
.doc-font-row:hover,
.signature-row:hover,
.font-row:hover {
border-color: var(--line-strong);
background: #ffffff;
@@ -547,14 +632,26 @@ body[data-hide-meta] .meta-panel {
background: #fffdf7;
}
.doc-font-head {
.signature-row.valid {
border-color: #bdd8cc;
background: #fbfefd;
}
.signature-row.invalid {
border-color: #e2c7c4;
background: #fffdfc;
}
.doc-font-head,
.signature-head {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: start;
gap: 8px;
}
.doc-font-name {
.doc-font-name,
.signature-name {
overflow: hidden;
color: #1f2428;
font-size: 13px;
@@ -563,7 +660,51 @@ body[data-hide-meta] .meta-panel {
white-space: nowrap;
}
.doc-font-badges {
.signature-name-button {
display: inline;
padding: 0;
border: 0;
background: transparent;
color: inherit;
font: inherit;
font-weight: inherit;
cursor: pointer;
text-align: left;
}
.signature-name-button:not(:disabled):hover {
background: transparent;
color: var(--accent-strong);
text-decoration: underline;
}
.signature-name-with-stamps {
white-space: normal;
}
.signature-stamp-group {
color: var(--text-muted);
font-weight: 400;
}
.signature-stamp-link {
padding: 0;
border: 0;
background: transparent;
color: var(--accent-strong);
font: inherit;
font-weight: 400;
line-height: inherit;
cursor: pointer;
}
.signature-stamp-link:not(:disabled):hover {
background: transparent;
text-decoration: underline;
}
.doc-font-badges,
.signature-badges {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
@@ -571,6 +712,10 @@ body[data-hide-meta] .meta-panel {
}
.font-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 30px;
padding: 3px 6px;
background: #eef1f4;
color: var(--text-muted);
@@ -590,11 +735,45 @@ body[data-hide-meta] .meta-panel {
color: #705719;
}
.font-badge.missing {
.font-badge.missing,
.font-badge.invalid {
background: #f9e8e5;
color: #803f38;
}
.font-badge.valid {
background: #e4f3ef;
color: var(--accent-strong);
}
.signature-line {
display: grid;
grid-template-columns: 30px minmax(0, 1fr);
align-items: start;
gap: 7px;
color: var(--text-muted);
font-size: 11px;
line-height: 1.35;
}
.signature-label {
color: #7b8791;
white-space: nowrap;
}
.signature-value {
min-width: 0;
overflow-wrap: anywhere;
}
.signature-line.ok .signature-value {
color: var(--accent-strong);
}
.signature-line.fail .signature-value {
color: #803f38;
}
.doc-font-detail {
color: var(--text-muted);
font-size: 11px;
@@ -607,6 +786,16 @@ body[data-hide-meta] .meta-panel {
gap: 8px;
}
.font-permission-hint {
margin: 0 0 8px;
padding: 7px 8px;
border: 1px solid var(--line);
background: #ffffff;
color: var(--text-muted);
font-size: 12px;
line-height: 1.35;
}
.font-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
@@ -695,12 +884,9 @@ body[data-hide-meta] .meta-panel {
text-align: center;
}
body[aria-busy="true"] .viewer-panel {
cursor: progress;
}
.app-footer {
justify-content: space-between;
overflow: hidden;
border-top: 1px solid var(--line);
color: var(--text-muted);
font-size: 12px;
@@ -708,9 +894,13 @@ body[aria-busy="true"] .viewer-panel {
.footer-brand {
flex: 0 0 var(--sidebar-width);
min-width: 0;
overflow: hidden;
font-weight: 400;
color: var(--accent-strong);
text-decoration: none;
text-overflow: ellipsis;
white-space: nowrap;
}
.status-text {
@@ -731,6 +921,7 @@ body[aria-busy="true"] .viewer-panel {
flex: 0 0 var(--sidebar-width);
justify-content: flex-end;
min-width: 0;
overflow: hidden;
}
.app-footer a {
@@ -749,9 +940,9 @@ body[aria-busy="true"] .viewer-panel {
}
.toolbar {
--tool-gap: 6px;
flex-wrap: nowrap;
overflow-x: auto;
gap: 6px;
}
.toolbar > * {
@@ -842,11 +1033,11 @@ body[aria-busy="true"] .viewer-panel {
row-gap: 0;
}
.divider {
.toolbar-space {
display: none;
}
.toolbar-space {
.tool-group + .tool-group::before {
display: none;
}
@@ -882,16 +1073,17 @@ body[aria-busy="true"] .viewer-panel {
width: 38px;
}
.page-frame {
padding: 22px 14px;
}
.zoom-label {
min-width: 34px;
}
#exportButton {
padding-inline: 8px;
.format-select {
width: 52px;
padding: 0 3px;
}
.dpi-select {
width: 48px;
}
.footer-links {
+814 -149
View File
File diff suppressed because it is too large Load Diff
+15 -14
View File
@@ -1,23 +1,25 @@
module github.com/xiaoqidun/ofdgo
go 1.25.0
go 1.26.0
require (
github.com/tdewolff/canvas v0.0.0-20260617131110-529326a1955e
github.com/xiaoqidun/jbig2 v0.0.0-20260629062926-bdd4ee3a892d
golang.org/x/text v0.38.0
github.com/tdewolff/canvas v0.0.0-20260822211237-56e622de465a
github.com/tdewolff/font v0.0.0-20260822205238-d0d2f004b117
github.com/xiaoqidun/jbig2 v0.0.0-20260823111534-2e67bcec7059
golang.org/x/image v0.45.0
golang.org/x/text v0.41.0
)
require (
codeberg.org/go-latex/latex v0.2.0 // indirect
codeberg.org/go-pdf/fpdf v0.11.1 // indirect
codeberg.org/go-latex/latex v0.3.0 // indirect
codeberg.org/go-pdf/fpdf v0.12.0 // indirect
github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 // indirect
github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 // indirect
github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc // indirect
github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 // indirect
github.com/ByteArena/poly2tri-go v0.0.0-20170716161910-d102ad91854f // indirect
github.com/Kagami/go-avif v0.1.0 // indirect
github.com/andybalholm/brotli v1.2.1 // indirect
github.com/andybalholm/brotli v1.2.2 // indirect
github.com/benoitkugler/textlayout v0.3.2 // indirect
github.com/benoitkugler/textprocessing v0.0.6 // indirect
github.com/go-fonts/latin-modern v0.3.3 // indirect
@@ -26,15 +28,14 @@ require (
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-20260424075104-b5eeb1e23189 // indirect
github.com/tdewolff/minify/v2 v2.24.13 // indirect
github.com/tdewolff/parse/v2 v2.8.12 // indirect
github.com/tdewolff/minify/v2 v2.24.17 // indirect
github.com/tdewolff/parse/v2 v2.8.16 // indirect
github.com/wcharczuk/go-chart/v2 v2.1.2 // indirect
github.com/yuin/goldmark v1.8.2 // indirect
golang.org/x/image v0.43.0 // indirect
golang.org/x/net v0.53.0 // indirect
github.com/yuin/goldmark v1.8.5 // indirect
golang.org/x/net v0.58.0 // indirect
golang.org/x/sys v0.47.0 // indirect
gonum.org/v1/plot v0.17.0 // indirect
modernc.org/knuth v0.5.5 // indirect
modernc.org/knuth v0.6.0 // indirect
modernc.org/token v1.1.0 // indirect
star-tex.org/x/tex v0.7.1 // indirect
)
+36 -37
View File
@@ -1,15 +1,15 @@
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=
codeberg.org/go-fonts/liberation v0.6.0 h1:15Gh6SdwYve22CWCm9jYpVpRuaTh726av2TgHTHvAtQ=
codeberg.org/go-fonts/liberation v0.6.0/go.mod h1:J15VAa+lyxdcI/Je7lDDDl6QOhLk9feNBnnwXqEHXOk=
codeberg.org/go-latex/latex v0.3.0 h1:LKTaDHFbEC2PH1sh0sYv6PZ1pzs/g2aoeV1HItWj/bg=
codeberg.org/go-latex/latex v0.3.0/go.mod h1:8ETijTpK2bFtwRAXLXe1RZJrYxnc5pibibZfQBj+Lk4=
codeberg.org/go-pdf/fpdf v0.12.0 h1:g8E/1VqGqB2lZUUaqQrrTnA0IEJLPTTX1DZ0qS/ZmhU=
codeberg.org/go-pdf/fpdf v0.12.0/go.mod h1:WJNJ2bvCj81rZBdhOf7lKOGoSl+OKMXcIcXqDcP8r5Y=
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=
git.sr.ht/~sbinet/gg v0.8.0 h1:PjQ4AgUWRz7Dy6PVBKMLLg96eRCos1U+H6F3FJuKWHo=
git.sr.ht/~sbinet/gg v0.8.0/go.mod h1:XhlGCvSXts+BaZ1XMZLxLSFUDvwsJ/2El5F6XzPep6o=
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=
@@ -24,8 +24,8 @@ 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.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/andybalholm/brotli v1.2.2 h1:HzTuoo2ErYQqf5qvcJInB8uvqSVxRttzkFexPWtnceM=
github.com/andybalholm/brotli v1.2.2/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/benoitkugler/pstokenizer v1.0.0/go.mod h1:l1G2Voirz0q/jj0TQfabNxVsa8HZXh/VMxFSRALWTiE=
github.com/benoitkugler/textlayout v0.3.2 h1:Y/LTkwpQ9G0Fia9yhPmZA9IR5AnE8Cq30j3C+Gx5/IE=
github.com/benoitkugler/textlayout v0.3.2/go.mod h1:o+1hFV+JSHBC9qNLIuwVoLedERU7sBPgEFcuSgfvi/w=
@@ -33,8 +33,6 @@ github.com/benoitkugler/textlayout-testdata v0.1.1/go.mod h1:i/qZl09BbUOtd7Bu/W1
github.com/benoitkugler/textprocessing v0.0.6 h1:obkMyj62GEPg3xUVYqROlCN22z1OleuZm6ULqX9Om1g=
github.com/benoitkugler/textprocessing v0.0.6/go.mod h1:Io0gN08/PXEzrSOWFa88xHx2Xv3VjvLMY7H76YoI23A=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/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.4 h1:YYurUOtEb9kGSOz4uE3k4OpBGsp1dDL8+fjCeaFamAU=
@@ -59,28 +57,29 @@ github.com/srwiley/scanx v0.0.0-20190309010443-e94503791388 h1:ZdkidVdpLW13BQ9a+
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-20260617131110-529326a1955e h1:Z9HqI3WUPBDwJEDH9BCLpQGvjmGs0XNRDBLry4shZfs=
github.com/tdewolff/canvas v0.0.0-20260617131110-529326a1955e/go.mod h1:Lz12ClttuKvo2jb1UxBfJJWmKV0LmEc8IRo/30R7z+k=
github.com/tdewolff/font v0.0.0-20260424075104-b5eeb1e23189 h1:wP/+8oeXiF55DUy9U0BP4iDD2mNu+o7evt5QLbLHv9E=
github.com/tdewolff/font v0.0.0-20260424075104-b5eeb1e23189/go.mod h1:KjNTiF0TXOXJyWGS/0DPXRknEC20c12kc0JcCYNqtz4=
github.com/tdewolff/minify/v2 v2.24.13 h1:xrcF7gKDnUszseEY9WX9mUlZII2v2Go/QAcAwRASw58=
github.com/tdewolff/minify/v2 v2.24.13/go.mod h1:emvwoYeIl8bfAKqRU5ww95LX9Gpggpqv/naal9a8Yq0=
github.com/tdewolff/parse/v2 v2.8.12 h1:5BBjfaCv482v3nltlS0u6wH1xJaxjR6ofDrWttNvROg=
github.com/tdewolff/parse/v2 v2.8.12/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo=
github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/tdewolff/canvas v0.0.0-20260822211237-56e622de465a h1:riaFBiYaIv1gM31xdcU0Yo7bdp3WebF4dggO/JK0g/8=
github.com/tdewolff/canvas v0.0.0-20260822211237-56e622de465a/go.mod h1:K9Ux3cwFtyAFRITZ46g7v+ORXgI3ENtQa5rL9yjNz5Y=
github.com/tdewolff/font v0.0.0-20260822205238-d0d2f004b117 h1:5vSJjsbV9AH0U+Ex0uej8ROrpOBlOVBrw0mGlVef7oo=
github.com/tdewolff/font v0.0.0-20260822205238-d0d2f004b117/go.mod h1:ipESUihcEdgRBhIwTRHmBTOMamccMQrp4iCxfa1aFws=
github.com/tdewolff/minify/v2 v2.24.17 h1:6AbitfVyq0M7aW6i+XL7+49DeTQZwloOMs9O574arBg=
github.com/tdewolff/minify/v2 v2.24.17/go.mod h1:kVqn9vxXUKtlHexSNrWbYePqioOT5mc4ou/KVSMpfCM=
github.com/tdewolff/parse/v2 v2.8.16 h1:bLk5svUOQRkW/Y2SJ+DeENSIkZBcTIkq+Atyv5D8feI=
github.com/tdewolff/parse/v2 v2.8.16/go.mod h1:XdsoSFThlVIRIajAuqz1evNY7bagZS8LBOPA3aVopwQ=
github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk=
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
github.com/wcharczuk/go-chart/v2 v2.1.2 h1:Y17/oYNuXwZg6TFag06qe8sBajwwsuvPiJJXcUcLL6E=
github.com/wcharczuk/go-chart/v2 v2.1.2/go.mod h1:Zi4hbaqlWpYajnXB2K22IUYVXRXaLfSGNNR7P4ukyyQ=
github.com/xiaoqidun/jbig2 v0.0.0-20260629062926-bdd4ee3a892d h1:EZ8kadE4iOd2HTYW/yg6tlv0yODsHiUkerXJ8Clm9gw=
github.com/xiaoqidun/jbig2 v0.0.0-20260629062926-bdd4ee3a892d/go.mod h1:c5zFNbM18iSRcfS9ziMZquU3lFJGmN1w6CUfuEDw0WI=
github.com/xiaoqidun/jbig2 v0.0.0-20260823111534-2e67bcec7059 h1:/JXxWbNTePSVfshS7BVRm75Kh1/5rixvLpgTRw7MMv8=
github.com/xiaoqidun/jbig2 v0.0.0-20260823111534-2e67bcec7059/go.mod h1:mdoUQnVPIA4yS0/+ZrtxJNiDdbphI0RMu+DPfxXfVhE=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
github.com/yuin/goldmark v1.8.5/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
@@ -88,8 +87,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
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.43.0 h1:FLxcP4ec2350nTfOC8ysKtqYSIFbk/QGjw1ZHNP4tsY=
golang.org/x/image v0.43.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q=
golang.org/x/image v0.45.0 h1:FMb1nTbH5H9vF55SriQHgFw5GnNL9Jg6L25BwXKzhB0=
golang.org/x/image v0.45.0/go.mod h1:n62x/7RqlwXDvGsSU4u6IUTUf6KghUZ9Bt7cG/T9Fx4=
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=
@@ -104,8 +103,8 @@ 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.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -123,6 +122,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
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=
@@ -141,8 +142,8 @@ 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.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
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=
@@ -154,10 +155,8 @@ gonum.org/v1/plot v0.17.0 h1:d0DwPVBe9jnEGqQBoZGl/P2M9WciJbG2CnV59C9QBT4=
gonum.org/v1/plot v0.17.0/go.mod h1:ipt2GUN1oqzr2O7wCjLDtw1ShfIYYNBp4o0O1Ez5B3Y=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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/knuth v0.6.0 h1:zLv0hzgdcpcCUB/iMwpQvn9m/iciqO/hyNXlpWymMKU=
modernc.org/knuth v0.6.0/go.mod h1:ISAZV+URZIlE8h1BwNVg1NzpwHKndV5lQ13NPPj3tds=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4=
+82 -13
View File
@@ -17,7 +17,6 @@
package webui
import (
"encoding/base64"
"encoding/json"
"fmt"
"syscall/js"
@@ -42,8 +41,10 @@ type apiResult struct {
func RunWASM() {
registerCallback("ofdgoOpen", openDocument)
registerCallback("ofdgoRenderPage", renderPage)
registerCallback("ofdgoExportFormats", exportFormats)
registerCallback("ofdgoExportPage", exportPage)
registerCallback("ofdgoExportPDF", exportPDF)
registerCallback("ofdgoFontCandidates", fontCandidates)
registerCallback("ofdgoFontSystemNames", fontSystemNames)
select {}
}
@@ -55,6 +56,9 @@ func registerCallback(name string, fn func([]js.Value) (any, error)) {
if err != nil {
return encodeResult(apiResult{OK: false, Error: err.Error()})
}
if result, ok := data.(js.Value); ok {
return result
}
return encodeResult(apiResult{OK: true, Data: data})
})
js.Global().Set(name, cb)
@@ -77,22 +81,23 @@ func safeCall(fn func([]js.Value) (any, error), args []js.Value) (data any, err
// 入参: args 浏览器参数
// 返回: any 文档信息, error 错误信息
func openDocument(args []js.Value) (any, error) {
if len(args) == 0 {
return nil, fmt.Errorf("missing ofd data")
if len(args) < 3 {
return nil, fmt.Errorf("missing open arguments")
}
data, err := bytesFromJS(args[0])
if err != nil {
return nil, err
}
fonts, err := fontsFromJS(jsArg(args, 1))
fonts, err := fontsFromJS(args[1])
if err != nil {
return nil, err
}
renderAnnotations := args[2].Bool()
if currentSession != nil {
_ = currentSession.Close()
currentSession = nil
}
session, err := Open(data, OpenOptions{Fonts: fonts})
session, err := Open(data, OpenOptions{Fonts: fonts, RenderAnnotations: renderAnnotations})
if err != nil {
return nil, err
}
@@ -110,10 +115,52 @@ func renderPage(args []js.Value) (any, error) {
if len(args) == 0 {
return nil, fmt.Errorf("missing page index")
}
return currentSession.RenderPageSVG(args[0].Int())
page, err := currentSession.RenderPageSVG(args[0].Int())
if err != nil {
return nil, err
}
return successResult(map[string]any{
"index": page.Index,
"number": page.Number,
"id": page.ID,
"width": page.Width,
"height": page.Height,
"svg": page.SVG,
}), nil
}
// exportPDF 导出OFD为PDF
// exportFormats 获取导出格式
// 入参: args 浏览器参数
// 返回: any 导出格式, error 错误信息
func exportFormats(args []js.Value) (any, error) {
return ExportFormats(), nil
}
// exportPage 导出OFD单页
// 入参: args 浏览器参数
// 返回: any 导出结果, error 错误信息
func exportPage(args []js.Value) (any, error) {
if currentSession == nil {
return nil, fmt.Errorf("ofd document is not opened")
}
if len(args) < 3 {
return nil, fmt.Errorf("missing export page arguments")
}
data, format, err := currentSession.ExportPage(args[0].Int(), args[1].String(), args[2].Float())
if err != nil {
return nil, err
}
return successResult(map[string]any{
"bytes": bytesToJS(data),
"size": len(data),
"format": format.Value,
"label": format.Label,
"extension": format.Extension,
"mime": format.MIME,
}), nil
}
// exportPDF 导出OFD文档为PDF
// 入参: args 浏览器参数
// 返回: any PDF结果, error 错误信息
func exportPDF(args []js.Value) (any, error) {
@@ -124,15 +171,37 @@ func exportPDF(args []js.Value) (any, error) {
if err != nil {
return nil, err
}
return map[string]any{"base64": base64.StdEncoding.EncodeToString(data), "size": len(data)}, nil
return successResult(map[string]any{
"bytes": bytesToJS(data),
"size": len(data),
}), nil
}
// fontCandidates 获取字体候选名称
// successResult 创建成功接口结果
// 入参: data 返回数据
// 返回: js.Value 接口结果
func successResult(data any) js.Value {
result := js.Global().Get("Object").New()
result.Set("ok", true)
result.Set("data", data)
return result
}
// bytesToJS 将字节数据转换为Uint8Array
// 入参: data 字节数据
// 返回: js.Value Uint8Array对象
func bytesToJS(data []byte) js.Value {
value := js.Global().Get("Uint8Array").New(len(data))
js.CopyBytesToJS(value, data)
return value
}
// fontSystemNames 获取系统字体名称
// 入参: args 浏览器参数
// 返回: any 字体候选名称, error 错误信息
func fontCandidates(args []js.Value) (any, error) {
// 返回: any 系统字体名称, error 错误信息
func fontSystemNames(args []js.Value) (any, error) {
names := stringsFromJS(jsArg(args, 0))
return ofdgo.FontCandidateNames(names...), nil
return ofdgo.FontSystemNames(names...), nil
}
// bytesFromJS 从浏览器值读取二进制数据
+368 -31
View File
@@ -17,13 +17,20 @@ package webui
import (
"bytes"
"fmt"
"image"
"image/color"
"image/draw"
"image/jpeg"
"image/png"
"strings"
"github.com/xiaoqidun/ofdgo"
)
// OpenOptions 打开OFD文档选项
type OpenOptions struct {
Fonts []FontFile
Fonts []FontFile
RenderAnnotations bool
}
// FontFile 字体文件
@@ -32,26 +39,84 @@ type FontFile = ofdgo.FontFile
// FontInfo OFD字体信息
type FontInfo = ofdgo.FontInfo
// SignatureInfo 签名验证信息
type SignatureInfo struct {
ID string `json:"id"`
Type string `json:"type"`
Valid bool `json:"valid"`
Version string `json:"version,omitempty"`
SealType string `json:"sealType,omitempty"`
SealID string `json:"sealId,omitempty"`
SealName string `json:"sealName,omitempty"`
SealVendor string `json:"sealVendor,omitempty"`
Signer string `json:"signer,omitempty"`
SignatureDateTime string `json:"signatureDateTime,omitempty"`
Provider string `json:"provider,omitempty"`
Company string `json:"company,omitempty"`
DataHashOK bool `json:"dataHashOK"`
SignedValueOK bool `json:"signedValueOK"`
SealOK bool `json:"sealOK"`
SealMatchOK bool `json:"sealMatchOK"`
CertOK bool `json:"certOK"`
SignatureTimeChecked bool `json:"signatureTimeChecked,omitempty"`
SignatureTimeOK bool `json:"signatureTimeOK,omitempty"`
SealTimeChecked bool `json:"sealTimeChecked,omitempty"`
SealTimeOK bool `json:"sealTimeOK,omitempty"`
SealCertTimeChecked bool `json:"sealCertTimeChecked,omitempty"`
SealCertTimeOK bool `json:"sealCertTimeOK,omitempty"`
CertTimeChecked bool `json:"certTimeChecked,omitempty"`
CertTimeOK bool `json:"certTimeOK,omitempty"`
CertTrustChecked bool `json:"certTrustChecked,omitempty"`
CertTrustOK bool `json:"certTrustOK,omitempty"`
ReferenceCount int `json:"referenceCount"`
ReferencePassed int `json:"referencePassed"`
SignatureMethod string `json:"signatureMethod,omitempty"`
DigestMethod string `json:"digestMethod,omitempty"`
SignSerial string `json:"signSerial,omitempty"`
SignSubject string `json:"signSubject,omitempty"`
SignIssuer string `json:"signIssuer,omitempty"`
SealSubject string `json:"sealSubject,omitempty"`
Stamps []SignatureStampInfo `json:"stamps,omitempty"`
Error string `json:"error,omitempty"`
}
// SignatureStampInfo 签名外观信息
type SignatureStampInfo struct {
ID string `json:"id,omitempty"`
Page int `json:"page,omitempty"`
PageID string `json:"pageId,omitempty"`
Boundary string `json:"boundary,omitempty"`
X float64 `json:"x,omitempty"`
Y float64 `json:"y,omitempty"`
Width float64 `json:"width,omitempty"`
Height float64 `json:"height,omitempty"`
}
// Session WebUI文档会话
type Session struct {
Reader *ofdgo.Reader
Renderer *ofdgo.Renderer
doc *ofdgo.Document
Reader *ofdgo.Reader
Renderer *ofdgo.Renderer
doc *ofdgo.Document
pageCache map[int]*ofdgo.PageContent
boxCache map[int]ofdgo.Box
}
// DocumentInfo 文档信息
type DocumentInfo struct {
Version string `json:"version"`
DocType string `json:"docType"`
Title string `json:"title"`
Author string `json:"author"`
Subject string `json:"subject"`
CreationDate string `json:"creationDate"`
ModDate string `json:"modDate"`
PageCount int `json:"pageCount"`
FontCount int `json:"fontCount"`
Fonts []FontInfo `json:"fonts"`
Pages []PageInfo `json:"pages"`
Version string `json:"version"`
DocType string `json:"docType"`
Title string `json:"title"`
Author string `json:"author"`
Subject string `json:"subject"`
CreationDate string `json:"creationDate"`
ModDate string `json:"modDate"`
PageCount int `json:"pageCount"`
FontCount int `json:"fontCount"`
SignatureCount int `json:"signatureCount"`
SignatureError string `json:"signatureError,omitempty"`
Fonts []FontInfo `json:"fonts"`
Signatures []SignatureInfo `json:"signatures"`
Pages []PageInfo `json:"pages"`
}
// PageInfo 页面信息
@@ -72,6 +137,47 @@ type PageSVG struct {
SVG string `json:"svg"`
}
// ExportFormat 导出格式
type ExportFormat struct {
Value string `json:"value"`
Label string `json:"label"`
Extension string `json:"extension"`
MIME string `json:"mime"`
}
// supportedExportFormats 导出格式列表
var supportedExportFormats = []ExportFormat{
{Value: "svg", Label: "SVG", Extension: "svg", MIME: "image/svg+xml"},
{Value: "pdf", Label: "PDF", Extension: "pdf", MIME: "application/pdf"},
{Value: "eps", Label: "EPS", Extension: "eps", MIME: "application/postscript"},
{Value: "png", Label: "PNG", Extension: "png", MIME: "image/png"},
{Value: "jpg", Label: "JPEG", Extension: "jpg", MIME: "image/jpeg"},
}
// ExportFormats 获取导出格式
// 返回: []ExportFormat 导出格式列表
func ExportFormats() []ExportFormat {
formats := make([]ExportFormat, len(supportedExportFormats))
copy(formats, supportedExportFormats)
return formats
}
// exportFormat 获取导出格式
// 入参: value 格式值
// 返回: ExportFormat 导出格式, bool 是否支持
func exportFormat(value string) (ExportFormat, bool) {
value = strings.ToLower(strings.TrimSpace(value))
if value == "jpeg" {
value = "jpg"
}
for _, format := range supportedExportFormats {
if format.Value == value {
return format, true
}
}
return ExportFormat{}, false
}
// Open 打开浏览器内存中的OFD文档
// 入参: data OFD文件数据, opts 打开选项
// 返回: *Session 文档会话, error 错误信息
@@ -97,7 +203,14 @@ func Open(data []byte, opts OpenOptions) (*Session, error) {
}
rendererOptions = append(rendererOptions, ofdgo.WithFontFS(fontFS))
}
return &Session{Reader: reader, Renderer: ofdgo.NewRenderer(reader, rendererOptions...), doc: doc}, nil
rendererOptions = append(rendererOptions, ofdgo.WithAnnotations(opts.RenderAnnotations))
return &Session{
Reader: reader,
Renderer: ofdgo.NewRenderer(reader, rendererOptions...),
doc: doc,
pageCache: make(map[int]*ofdgo.PageContent),
boxCache: make(map[int]ofdgo.Box),
}, nil
}
// Close 关闭文档会话
@@ -118,10 +231,12 @@ func (s *Session) Info() DocumentInfo {
PageCount: len(s.doc.Pages.Page),
Pages: make([]PageInfo, 0, len(s.doc.Pages.Page)),
}
if fonts, err := s.Renderer.FontInfos(); err == nil {
info.Fonts = fonts
if signatures, err := s.signatureInfos(); err == nil {
info.Signatures = signatures
} else {
info.SignatureError = err.Error()
}
info.FontCount = len(info.Fonts)
info.SignatureCount = len(info.Signatures)
if docInfo, err := s.Reader.DocInfo(); err == nil && docInfo != nil {
info.Title = docInfo.Title
info.Author = docInfo.Author
@@ -129,16 +244,22 @@ func (s *Session) Info() DocumentInfo {
info.CreationDate = docInfo.CreationDate
info.ModDate = docInfo.ModDate
}
pages := make([]*ofdgo.PageContent, 0, len(s.doc.Pages.Page))
for index, pageRef := range s.doc.Pages.Page {
pageInfo := PageInfo{Index: index, ID: pageRef.ID}
if page, err := s.Reader.PageContent(pageRef); err == nil {
if box, err := s.Renderer.GetPageBox(page); err == nil {
if _, page, err := s.pageContent(index); err == nil {
pages = append(pages, page)
if box, err := s.pageBox(index, page); err == nil {
pageInfo.Width = box.W
pageInfo.Height = box.H
}
}
info.Pages = append(info.Pages, pageInfo)
}
if fonts, err := s.Renderer.FontInfosFromPages(pages); err == nil {
info.Fonts = fonts
}
info.FontCount = len(info.Fonts)
return info
}
@@ -146,18 +267,11 @@ func (s *Session) Info() DocumentInfo {
// 入参: index 页面索引
// 返回: PageSVG 页面SVG结果, error 错误信息
func (s *Session) RenderPageSVG(index int) (PageSVG, error) {
if s == nil || s.Reader == nil || s.Renderer == nil || s.doc == nil {
return PageSVG{}, fmt.Errorf("ofd document is not opened")
}
if index < 0 || index >= len(s.doc.Pages.Page) {
return PageSVG{}, fmt.Errorf("page index %d out of range", index)
}
pageRef := s.doc.Pages.Page[index]
page, err := s.Reader.PageContent(pageRef)
pageRef, page, err := s.pageContent(index)
if err != nil {
return PageSVG{}, err
}
box, err := s.Renderer.GetPageBox(page)
box, err := s.pageBox(index, page)
if err != nil {
return PageSVG{}, err
}
@@ -168,7 +282,182 @@ func (s *Session) RenderPageSVG(index int) (PageSVG, error) {
return PageSVG{Index: index, Number: index + 1, ID: pageRef.ID, Width: box.W, Height: box.H, SVG: buf.String()}, nil
}
// ExportPDF 导出整个文档为PDF
// ExportPage 导出单页
// 入参: index 页面索引, value 导出格式, dpi 图片DPI
// 返回: []byte 文件数据, ExportFormat 导出格式, error 错误信息
func (s *Session) ExportPage(index int, value string, dpi float64) ([]byte, ExportFormat, error) {
format, ok := exportFormat(value)
if !ok {
return nil, ExportFormat{}, fmt.Errorf("unsupported export format %s", value)
}
_, page, err := s.pageContent(index)
if err != nil {
return nil, ExportFormat{}, err
}
var buf bytes.Buffer
switch format.Value {
case "svg":
err = s.Renderer.RenderToSVG(page, &buf)
case "pdf":
err = s.Renderer.RenderToPDF(page, &buf)
case "eps":
err = s.Renderer.RenderToEPS(page, &buf)
case "png":
img, renderErr := s.renderPageImage(page, dpi)
if renderErr != nil {
return nil, ExportFormat{}, renderErr
}
err = png.Encode(&buf, img)
case "jpg":
img, renderErr := s.renderPageImage(page, dpi)
if renderErr != nil {
return nil, ExportFormat{}, renderErr
}
err = jpeg.Encode(&buf, imageWithWhiteBackground(img), &jpeg.Options{Quality: 95})
}
if err != nil {
return nil, ExportFormat{}, err
}
return buf.Bytes(), format, nil
}
// renderPageImage 渲染页面图片
// 入参: page 页面内容, dpi 图片DPI
// 返回: image.Image 图像对象, error 错误信息
func (s *Session) renderPageImage(page *ofdgo.PageContent, dpi float64) (image.Image, error) {
if dpi <= 0 {
return s.Renderer.RenderToImage(page)
}
oldDPI := s.Renderer.DPI
s.Renderer.DPI = dpi
defer func() {
s.Renderer.DPI = oldDPI
}()
return s.Renderer.RenderToImage(page)
}
// signatureInfos 获取签名验证信息
// 返回: []SignatureInfo 签名验证信息, error 错误信息
func (s *Session) signatureInfos() ([]SignatureInfo, error) {
reports, err := s.Reader.VerifySignatures()
if err != nil {
return nil, err
}
infos := make([]SignatureInfo, 0, len(reports))
for _, report := range reports {
infos = append(infos, signatureInfo(report))
}
return infos, nil
}
// signatureInfo 转换签名验证信息
// 入参: report 签名验证报告
// 返回: SignatureInfo 签名验证信息
func signatureInfo(report ofdgo.SignatureVerifyReport) SignatureInfo {
return SignatureInfo{
ID: report.ID,
Type: string(report.Type),
Valid: report.Valid,
Version: report.Provider.Version,
SealType: report.SealType,
SealID: report.SealInfo.ID,
SealName: report.SealInfo.Name,
SealVendor: report.SealInfo.VendorID,
Signer: signatureSigner(report),
SignatureDateTime: report.SignatureDateTime,
Provider: report.Provider.ProviderName,
Company: report.Provider.Company,
DataHashOK: report.DataHashOK,
SignedValueOK: report.SignedValueOK,
SealOK: report.SealOK,
SealMatchOK: report.SealMatchOK,
CertOK: report.CertOK,
SignatureTimeChecked: report.SignatureTimeChecked,
SignatureTimeOK: report.SignatureTimeOK,
SealTimeChecked: report.SealTimeChecked,
SealTimeOK: report.SealTimeOK,
SealCertTimeChecked: report.SealCertTimeChecked,
SealCertTimeOK: report.SealCertTimeOK,
CertTimeChecked: report.CertTimeChecked,
CertTimeOK: report.CertTimeOK,
CertTrustChecked: report.CertTrustChecked,
CertTrustOK: report.CertTrustOK,
ReferenceCount: len(report.References),
ReferencePassed: signatureReferencePassed(report.References),
SignatureMethod: report.SignatureMethod,
DigestMethod: report.DigestMethod,
SignSerial: report.SignCert.SerialNumber,
SignSubject: report.SignCert.Subject,
SignIssuer: report.SignCert.Issuer,
SealSubject: report.SealCert.Subject,
Stamps: signatureStampInfos(report.StampPositions),
Error: signatureReportError(report),
}
}
// signatureSigner 获取签名人名称
// 入参: report 签名验证报告
// 返回: string 签名人名称
func signatureSigner(report ofdgo.SignatureVerifyReport) string {
if report.Signer != "" {
return report.Signer
}
if report.SignCert.CommonName != "" {
return report.SignCert.CommonName
}
if report.SignCert.Organization != "" {
return report.SignCert.Organization
}
return report.SignCert.Subject
}
// signatureReferencePassed 获取已通过保护文件数量
// 入参: refs 保护文件验证结果
// 返回: int 已通过保护文件数量
func signatureReferencePassed(refs []ofdgo.SignatureReferenceVerify) int {
count := 0
for _, ref := range refs {
if ref.OK {
count++
}
}
return count
}
// signatureReportError 获取签名错误信息
// 入参: report 签名验证报告
// 返回: string 错误信息
func signatureReportError(report ofdgo.SignatureVerifyReport) string {
if report.Error != "" && report.StampPositionError != "" {
return report.Error + "; " + report.StampPositionError
}
if report.Error != "" {
return report.Error
}
return report.StampPositionError
}
// signatureStampInfos 转换签名外观位置
// 入参: positions 签名外观位置
// 返回: []SignatureStampInfo 签名外观信息
func signatureStampInfos(positions []ofdgo.SignatureStampPosition) []SignatureStampInfo {
infos := make([]SignatureStampInfo, 0, len(positions))
for _, position := range positions {
infos = append(infos, SignatureStampInfo{
ID: position.ID,
Page: position.Page,
PageID: position.PageID,
Boundary: position.Boundary,
X: position.Box.X,
Y: position.Box.Y,
Width: position.Box.W,
Height: position.Box.H,
})
}
return infos
}
// ExportPDF 导出文档为PDF
// 返回: []byte PDF文件数据, error 错误信息
func (s *Session) ExportPDF() ([]byte, error) {
if s == nil || s.Reader == nil || s.Renderer == nil || s.doc == nil {
@@ -180,3 +469,51 @@ func (s *Session) ExportPDF() ([]byte, error) {
}
return buf.Bytes(), nil
}
// imageWithWhiteBackground 填充图片白色背景
// 入参: img 图片对象
// 返回: image.Image 图片对象
func imageWithWhiteBackground(img image.Image) image.Image {
bounds := img.Bounds()
dst := image.NewRGBA(bounds)
draw.Draw(dst, bounds, &image.Uniform{C: color.White}, image.Point{}, draw.Src)
draw.Draw(dst, bounds, img, bounds.Min, draw.Over)
return dst
}
// pageContent 获取页面内容
// 入参: index 页面索引
// 返回: ofdgo.Page 页面引用, *ofdgo.PageContent 页面内容, error 错误信息
func (s *Session) pageContent(index int) (ofdgo.Page, *ofdgo.PageContent, error) {
if s == nil || s.Reader == nil || s.Renderer == nil || s.doc == nil {
return ofdgo.Page{}, nil, fmt.Errorf("ofd document is not opened")
}
if index < 0 || index >= len(s.doc.Pages.Page) {
return ofdgo.Page{}, nil, fmt.Errorf("page index %d out of range", index)
}
pageRef := s.doc.Pages.Page[index]
if page, ok := s.pageCache[index]; ok {
return pageRef, page, nil
}
page, err := s.Reader.PageContent(pageRef)
if err != nil {
return ofdgo.Page{}, nil, err
}
s.pageCache[index] = page
return pageRef, page, nil
}
// pageBox 获取页面物理区域
// 入参: index 页面索引, page 页面内容
// 返回: ofdgo.Box 页面物理区域, error 错误信息
func (s *Session) pageBox(index int, page *ofdgo.PageContent) (ofdgo.Box, error) {
if box, ok := s.boxCache[index]; ok {
return box, nil
}
box, err := s.Renderer.GetPageBox(page)
if err != nil {
return ofdgo.Box{}, err
}
s.boxCache[index] = box
return box, nil
}
+9 -1
View File
@@ -43,7 +43,7 @@ func Open(path string) (*Reader, error) {
return reader, nil
}
// NewReader 从流创建一个 OFD 阅读器
// NewReader 从IO读取器创建OFD阅读器
// 入参: r IO读取器, size 数据大小
// 返回: *Reader 阅读器实例, error 错误信息
func NewReader(r io.ReaderAt, size int64) (*Reader, error) {
@@ -71,6 +71,14 @@ func NewRenderer(reader *Reader, opts ...RendererOption) *Renderer {
DrawParams: reader.drawParamCache,
CompositeGraphicUnits: reader.compositeGraphicUnitCache,
FontMap: make(map[string]*canvas.FontFamily),
FontGIDMap: make(map[string]map[uint16]rune),
FontCIDMap: make(map[string]map[uint16]rune),
fontCache: make(map[fontCacheKey]*canvas.FontFamily),
fontSourceCache: make(map[string][]fontSource),
fontSourceUsed: make(map[string]fontSource),
fontDirCandidates: make(map[string][]fontFileCandidate),
textGlyphPathCache: make(map[textGlyphPathCacheKey]textGlyphPathCacheValue),
templatePageCache: make(map[string]*PageContent),
}
for _, opt := range opts {
opt(r)
+198
View File
@@ -0,0 +1,198 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"encoding/xml"
"strconv"
)
// Action 动作
type Action struct {
Event string `xml:"Event,attr"`
Region *Region `xml:"Region"`
Goto *Goto `xml:"Goto"`
URI *URI `xml:"URI"`
GotoA *GotoA `xml:"GotoA"`
Sound *Sound `xml:"Sound"`
Movie *Movie `xml:"Movie"`
}
// Goto 文档内跳转动作
type Goto struct {
Dest *Dest `xml:"Dest"`
Bookmark *GotoBookmark `xml:"Bookmark"`
}
// Dest 文档内跳转目标
type Dest struct {
Type string `xml:"Type,attr"`
PageID string `xml:"PageID,attr"`
Left float64 `xml:"Left"`
Right float64 `xml:"Right"`
Top float64 `xml:"Top"`
Bottom float64 `xml:"Bottom"`
Zoom float64 `xml:"Zoom"`
}
// GotoBookmark 书签跳转目标
type GotoBookmark struct {
Name string `xml:"Name,attr"`
}
// URI URI动作
type URI struct {
URI string `xml:"URI,attr"`
Base string `xml:"Base,attr"`
}
// GotoA 附件动作
type GotoA struct {
AttachID string `xml:"AttachID,attr"`
NewWindow *bool `xml:"NewWindow,attr"`
}
// Sound 音频动作
type Sound struct {
ResourceID string `xml:"ResourceID,attr"`
Volume *int `xml:"Volume,attr"`
Repeat bool `xml:"Repeat,attr"`
Synchronous bool `xml:"Synchronous,attr"`
}
// Movie 视频动作
type Movie struct {
ResourceID string `xml:"ResourceID,attr"`
Operator string `xml:"Operator,attr"`
}
// Region 动作区域
type Region struct {
Area []RegionArea `xml:"Area"`
}
// RegionArea 动作区域分路径
type RegionArea struct {
Start string `xml:"Start,attr"`
Command []RegionCommand `xml:"-"`
}
// RegionCommand 动作区域绘制指令
type RegionCommand struct {
Type string
Point1 string
Point2 string
Point3 string
EllipseSize string
RotationAngle string
LargeArc string
SweepDirection string
EndPoint string
}
// UnmarshalXML 解析跳转目标
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (dest *Dest) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var value struct {
Left *float64 `xml:"Left"`
Right *float64 `xml:"Right"`
Top *float64 `xml:"Top"`
Bottom *float64 `xml:"Bottom"`
Zoom *float64 `xml:"Zoom"`
}
dest.Type = attrValue(start, "Type")
dest.PageID = attrValue(start, "PageID")
dest.Left = actionFloatAttr(start, "Left")
dest.Right = actionFloatAttr(start, "Right")
dest.Top = actionFloatAttr(start, "Top")
dest.Bottom = actionFloatAttr(start, "Bottom")
dest.Zoom = actionFloatAttr(start, "Zoom")
if err := d.DecodeElement(&value, &start); err != nil {
return err
}
if value.Left != nil {
dest.Left = *value.Left
}
if value.Right != nil {
dest.Right = *value.Right
}
if value.Top != nil {
dest.Top = *value.Top
}
if value.Bottom != nil {
dest.Bottom = *value.Bottom
}
if value.Zoom != nil {
dest.Zoom = *value.Zoom
}
return nil
}
// UnmarshalXML 解析视频动作并应用默认值
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (m *Movie) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
type movie Movie
value := movie{Operator: "Play"}
if err := d.DecodeElement(&value, &start); err != nil {
return err
}
*m = Movie(value)
return nil
}
// UnmarshalXML 解析动作区域分路径并保留指令顺序
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (a *RegionArea) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
*a = RegionArea{Start: attrValue(start, "Start")}
for {
tok, err := d.Token()
if err != nil {
return err
}
switch node := tok.(type) {
case xml.StartElement:
command := RegionCommand{
Type: node.Name.Local,
Point1: attrValue(node, "Point1"),
Point2: attrValue(node, "Point2"),
Point3: attrValue(node, "Point3"),
EllipseSize: attrValue(node, "EllipseSize"),
RotationAngle: attrValue(node, "RotationAngle"),
LargeArc: attrValue(node, "LargeArc"),
SweepDirection: attrValue(node, "SweepDirection"),
EndPoint: attrValue(node, "EndPoint"),
}
a.Command = append(a.Command, command)
if err := d.Skip(); err != nil {
return err
}
case xml.EndElement:
if node.Name.Local == start.Name.Local {
return nil
}
}
}
}
// actionFloatAttr 获取动作浮点属性
// 入参: start 起始节点, name 属性名
// 返回: float64 属性值
func actionFloatAttr(start xml.StartElement, name string) float64 {
value, _ := strconv.ParseFloat(attrValue(start, name), 64)
return value
}
+196 -17
View File
@@ -23,26 +23,32 @@ type Document struct {
Pages Pages `xml:"Pages"`
Outlines Outlines `xml:"Outlines"`
Permissions Permissions `xml:"Permissions"`
Actions []Action `xml:"Actions>Action"`
Bookmarks Bookmarks `xml:"Bookmarks"`
Annotations string `xml:"Annotations"`
Signatures string `xml:"Signatures"`
Attachments Attachments `xml:"Attachments"`
CustomTags CustomTags `xml:"CustomTags"`
Extensions Extensions `xml:"Extensions"`
}
// Extensions 扩展集合
type Extensions struct {
XMLName xml.Name `xml:"Extensions"`
Path string `xml:",chardata"`
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"`
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"`
ExtendData []string `xml:"ExtendData"`
Data []ExtensionData `xml:"Data"`
}
// Property 扩展属性
@@ -52,16 +58,45 @@ type Property struct {
Type string `xml:"Type,attr"`
}
// ExtensionData 扩展数据
type ExtensionData struct {
Attr []xml.Attr `xml:",any,attr"`
Content string `xml:",innerxml"`
}
// Attachments 附件集合
type Attachments struct {
XMLName xml.Name `xml:"Attachments"`
Path string `xml:",chardata"`
Attachment []Attachment `xml:"Attachment"`
}
// Attachment 附件信息
type Attachment struct {
Name string `xml:"Name,attr"`
File string `xml:"File,attr"`
ID string `xml:"ID,attr"`
ID string `xml:"ID,attr"`
Name string `xml:"Name,attr"`
Format string `xml:"Format,attr"`
CreationDate string `xml:"CreationDate,attr"`
ModDate string `xml:"ModDate,attr"`
Size *float64 `xml:"Size,attr"`
Visible bool `xml:"Visible,attr"`
Usage string `xml:"Usage,attr"`
FileLoc string `xml:"FileLoc"`
}
// CustomTags 自定义标引集合
type CustomTags struct {
XMLName xml.Name `xml:"CustomTags"`
Path string `xml:",chardata"`
CustomTag []CustomTag `xml:"CustomTag"`
}
// CustomTag 自定义标引
type CustomTag struct {
TypeID string `xml:"TypeID,attr"`
NameSpace string `xml:"NameSpace,attr"`
SchemaLoc string `xml:"SchemaLoc"`
FileLoc string `xml:"FileLoc"`
}
// CommonData 文档公共数据
@@ -101,6 +136,17 @@ type TemplatePage struct {
ZOrder string `xml:"ZOrder,attr"`
}
// Bookmarks 书签集合
type Bookmarks struct {
Bookmark []Bookmark `xml:"Bookmark"`
}
// Bookmark 书签
type Bookmark struct {
Name string `xml:"Name,attr"`
Dest Dest `xml:"Dest"`
}
// Outlines 大纲集合
type Outlines struct {
OutlineElem []OutlineElem `xml:"OutlineElem"`
@@ -108,15 +154,148 @@ type Outlines struct {
// OutlineElem 大纲节点
type OutlineElem struct {
Title string `xml:"Title,attr"`
Count int `xml:"Count,attr"`
Actions string `xml:"Actions"`
Title string `xml:"Title,attr"`
Count int `xml:"Count,attr"`
Expanded bool `xml:"Expanded,attr"`
Actions []Action `xml:"Actions>Action"`
OutlineElem []OutlineElem `xml:"OutlineElem"`
}
// Permissions 权限声明
type Permissions struct {
Edit bool `xml:"Edit"`
Print bool `xml:"Print"`
Export bool `xml:"Export"`
Copy bool `xml:"Copy"`
Edit bool `xml:"Edit"`
Annot bool `xml:"Annot"`
Export bool `xml:"Export"`
Signature bool `xml:"Signature"`
Watermark bool `xml:"Watermark"`
PrintScreen bool `xml:"PrintScreen"`
Print bool `xml:"-"`
Copies int `xml:"-"`
Copy bool `xml:"CopyText"`
ContentRegist bool `xml:"ContentRegist"`
ValidPeriod *ValidPeriod `xml:"ValidPeriod"`
}
// ValidPeriod 文档访问有效期
type ValidPeriod struct {
StartDate string `xml:"StartDate,attr"`
EndDate string `xml:"EndDate,attr"`
}
// UnmarshalXML 解析文档并应用权限默认值
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (doc *Document) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
type document Document
value := document{Permissions: defaultPermissions()}
if err := d.DecodeElement(&value, &start); err != nil {
return err
}
*doc = Document(value)
return nil
}
// UnmarshalXML 解析附件并应用默认值
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (a *Attachment) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
type attachment Attachment
value := attachment{Visible: true, Usage: "none"}
if err := d.DecodeElement(&value, &start); err != nil {
return err
}
*a = Attachment(value)
return nil
}
// UnmarshalXML 解析大纲节点并应用默认值
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (o *OutlineElem) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
type outlineElem OutlineElem
value := outlineElem{Expanded: true}
if err := d.DecodeElement(&value, &start); err != nil {
return err
}
*o = OutlineElem(value)
return nil
}
// UnmarshalXML 解析文档权限并应用默认值
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (p *Permissions) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var value struct {
Edit *bool `xml:"Edit"`
Annot *bool `xml:"Annot"`
Export *bool `xml:"Export"`
Signature *bool `xml:"Signature"`
Watermark *bool `xml:"Watermark"`
PrintScreen *bool `xml:"PrintScreen"`
Copy *bool `xml:"CopyText"`
ContentRegist *bool `xml:"ContentRegist"`
Print *print `xml:"Print"`
ValidPeriod *ValidPeriod `xml:"ValidPeriod"`
}
if err := d.DecodeElement(&value, &start); err != nil {
return err
}
*p = defaultPermissions()
if value.Edit != nil {
p.Edit = *value.Edit
}
if value.Annot != nil {
p.Annot = *value.Annot
}
if value.Export != nil {
p.Export = *value.Export
}
if value.Signature != nil {
p.Signature = *value.Signature
}
if value.Watermark != nil {
p.Watermark = *value.Watermark
}
if value.PrintScreen != nil {
p.PrintScreen = *value.PrintScreen
}
if value.Copy != nil {
p.Copy = *value.Copy
}
if value.ContentRegist != nil {
p.ContentRegist = *value.ContentRegist
}
if value.Print != nil {
if value.Print.Printable != nil {
p.Print = *value.Print.Printable
}
if value.Print.Copies != nil {
p.Copies = *value.Print.Copies
}
}
p.ValidPeriod = value.ValidPeriod
return nil
}
// print 打印权限节点
type print struct {
Printable *bool `xml:"Printable,attr"`
Copies *int `xml:"Copies,attr"`
}
// defaultPermissions 获取默认文档权限
// 返回: Permissions 文档权限
func defaultPermissions() Permissions {
return Permissions{
Edit: true,
Annot: true,
Export: true,
Signature: true,
Watermark: true,
PrintScreen: true,
Print: true,
Copies: -1,
Copy: true,
ContentRegist: true,
}
}
+1
View File
@@ -20,6 +20,7 @@ import (
// FixFontDataAggressive 激进修复字体数据
// 尝试修复缺失表(OS/2, cmap等)的TrueType字体或包装CFF裸数据
// 对显式字形映射补充私有字符cmap,便于按glyph id渲染
// 入参: data 原始字体数据, fixCmap 是否修复cmap, fixName 是否修复name
// 返回: bool 是否修复, []byte 修复后数据, map[rune]uint16 字符映射, bool 是否缺失cmap, error 错误信息
func FixFontDataAggressive(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uint16, bool, error) {
+301 -46
View File
@@ -32,8 +32,9 @@ type FontFile struct {
// FontFS 内存字体文件系统
type FontFS struct {
files map[string][]byte
names []string
files map[string][]byte
names []string
candidates []fontFileCandidate
}
// NewFontFS 创建内存字体文件系统
@@ -54,6 +55,7 @@ func NewFontFS(fonts []FontFile) *FontFS {
fsys.files[name] = append([]byte(nil), font.Data...)
}
sort.Strings(fsys.names)
fsys.candidates = fontFileCandidates(fsys.names, path.Base)
return fsys
}
@@ -70,18 +72,20 @@ func (fsys *FontFS) Len() int {
// 入参: name 字体文件名
// 返回: fs.File 字体文件, error 错误信息
func (fsys *FontFS) Open(name string) (fs.File, error) {
name = cleanFontName(name)
if !fs.ValidPath(name) {
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
}
if name == "." {
return &fontDir{entries: fsys.entries()}, nil
}
data, ok := fsys.files[name]
data, ok := fsys.files[strings.ToLower(name)]
if !ok {
return nil, fs.ErrNotExist
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
}
return &fontMemFile{
Reader: bytes.NewReader(data),
info: fontFileInfo{
name: name,
name: strings.ToLower(name),
size: int64(len(data)),
},
}, nil
@@ -91,9 +95,15 @@ func (fsys *FontFS) Open(name string) (fs.File, error) {
// 入参: name 目录名
// 返回: []fs.DirEntry 目录条目, error 错误信息
func (fsys *FontFS) ReadDir(name string) ([]fs.DirEntry, error) {
name = cleanFontName(name)
if !fs.ValidPath(name) {
return nil, &fs.PathError{Op: "readdir", Path: name, Err: fs.ErrInvalid}
}
if name != "." {
return nil, fs.ErrNotExist
err := fs.ErrNotExist
if _, ok := fsys.files[strings.ToLower(name)]; ok {
err = fs.ErrInvalid
}
return nil, &fs.PathError{Op: "readdir", Path: name, Err: err}
}
return fsys.entries(), nil
}
@@ -102,20 +112,22 @@ func (fsys *FontFS) ReadDir(name string) ([]fs.DirEntry, error) {
// 入参: pattern 匹配模式
// 返回: []string 字体文件列表, error 错误信息
func (fsys *FontFS) Glob(pattern string) ([]string, error) {
if _, err := path.Match(pattern, ""); err != nil {
return nil, err
}
return fsys.match(pattern), nil
return fs.Glob(struct{ fs.FS }{fsys}, pattern)
}
// Match 匹配指定字体名称
// 入参: names 字体名称列表
// 返回: string 匹配字体文件, bool 是否为名称匹配
func (fsys *FontFS) Match(names ...string) (string, bool) {
for _, pattern := range fontFilePatterns(names...) {
if matches := fsys.match(pattern); len(matches) > 0 {
return matches[0], true
}
return fsys.MatchStyle(false, false, names...)
}
// MatchStyle 匹配指定样式的字体名称
// 入参: bold 是否粗体, italic 是否斜体, names 字体名称列表
// 返回: string 匹配字体文件, bool 是否为名称匹配
func (fsys *FontFS) MatchStyle(bold, italic bool, names ...string) (string, bool) {
if matches := fsys.matchPatternsStyle(fontFilePatterns(names...), bold, italic); len(matches) > 0 {
return matches[0], true
}
if matches := fsys.fallbackFonts(); len(matches) > 0 {
return matches[0], false
@@ -127,17 +139,30 @@ func (fsys *FontFS) Match(names ...string) (string, bool) {
// 入参: pattern 匹配模式
// 返回: []string 字体文件列表
func (fsys *FontFS) match(pattern string) []string {
buckets := make([][]string, fontMatchFuzzy+1)
for _, name := range fsys.names {
if rank := matchFontPatternRank(pattern, name); rank != fontMatchNone {
buckets[rank] = append(buckets[rank], name)
return fsys.matchStyle(pattern, false, false)
}
// matchStyle 匹配指定样式的字体文件模式
// 入参: pattern 匹配模式, bold 是否粗体, italic 是否斜体
// 返回: []string 字体文件列表
func (fsys *FontFS) matchStyle(pattern string, bold, italic bool) []string {
return fsys.matchPatternsStyle([]string{pattern}, bold, italic)
}
// matchPatternsStyle 匹配指定样式的字体文件模式
// 入参: patterns 匹配模式列表, bold 是否粗体, italic 是否斜体
// 返回: []string 字体文件列表
func (fsys *FontFS) matchPatternsStyle(patterns []string, bold, italic bool) []string {
matches := make([]fontFileMatch, 0, len(fsys.candidates))
seen := make(map[string]int, len(fsys.candidates))
for _, matcher := range newFontPatternMatchers(patterns) {
for _, file := range fsys.candidates {
rank := matcher.rankCandidate(file)
appendFontFileMatch(&matches, seen, matcher, file, rank, bold, italic)
}
}
var matches []string
for rank := fontMatchExact; rank <= fontMatchFuzzy; rank++ {
matches = append(matches, buckets[rank]...)
}
return matches
sortFontFileMatches(matches)
return fontFileMatchNames(matches)
}
// fallbackFonts 获取回退字体文件
@@ -183,51 +208,278 @@ func cleanFontName(name string) string {
return strings.ToLower(name)
}
// matchFontPatternRank 获取字体文件匹配等级
// 入参: pattern 匹配模式, name 字体文件名
type fontFileCandidate struct {
name string
base string
lowerBase string
normalized string
fold string
}
// fontFileCandidates 筛选字体文件候选
// 入参: names 文件名列表, base 文件名提取函数
// 返回: []fontFileCandidate 字体文件候选列表
func fontFileCandidates(names []string, base func(string) string) []fontFileCandidate {
files := make([]fontFileCandidate, 0, len(names))
for _, name := range names {
if isFontFileName(name) {
baseName := base(name)
files = append(files, fontFileCandidate{
name: name,
base: baseName,
lowerBase: strings.ToLower(baseName),
normalized: fontNormalizeName(baseName),
fold: strings.ToLower(name),
})
}
}
return files
}
type fontPatternMatcher struct {
pattern string
lowerPattern string
stem string
aliases []string
priority int
}
// newFontPatternMatcher 创建字体文件匹配器
// 入参: pattern 匹配模式
// 返回: fontPatternMatcher 字体文件匹配器
func newFontPatternMatcher(pattern string) fontPatternMatcher {
matcher := fontPatternMatcher{
pattern: pattern,
lowerPattern: strings.ToLower(pattern),
stem: fontPatternStem(pattern),
}
if matcher.stem != "" {
for _, alias := range fontExactCandidateNames(matcher.stem) {
alias = fontNormalizeName(alias)
if alias != "" {
matcher.aliases = append(matcher.aliases, alias)
}
}
}
return matcher
}
// newFontPatternMatchers 创建字体文件匹配器列表
// 入参: patterns 匹配模式列表
// 返回: []fontPatternMatcher 字体文件匹配器列表
func newFontPatternMatchers(patterns []string) []fontPatternMatcher {
matchers := make([]fontPatternMatcher, 0, len(patterns))
for priority, pattern := range patterns {
matcher := newFontPatternMatcher(pattern)
matcher.priority = priority
matchers = append(matchers, matcher)
}
return matchers
}
// rank 获取字体文件匹配等级
// 入参: name 字体文件名
// 返回: int 匹配等级
func matchFontPatternRank(pattern, name string) int {
exactPath, _ := path.Match(pattern, name)
lowerPath, _ := path.Match(strings.ToLower(pattern), strings.ToLower(name))
stem := fontPatternStem(pattern)
if stem == "" {
func (m fontPatternMatcher) rank(name string) int {
return m.rankCandidate(fontFileCandidate{
base: name,
lowerBase: strings.ToLower(name),
normalized: fontNormalizeName(name),
})
}
// rankCandidate 获取字体文件候选匹配等级
// 入参: file 字体文件候选
// 返回: int 匹配等级
func (m fontPatternMatcher) rankCandidate(file fontFileCandidate) int {
exactPath, _ := path.Match(m.pattern, file.base)
lowerPath, _ := path.Match(m.lowerPattern, file.lowerBase)
if m.stem == "" {
if exactPath || lowerPath {
return fontMatchFuzzy
}
return fontMatchNone
}
name = fontNormalizeName(name)
if name == stem {
name := file.normalized
if name == m.stem {
return fontMatchExact
}
aliases := fontExactCandidateNames(stem)
for _, alias := range aliases {
alias = fontNormalizeName(alias)
if alias != "" && name == alias {
for _, alias := range m.aliases {
if name == alias {
return fontMatchExact
}
}
if strings.HasPrefix(name, stem) {
if fontFileKnownStyleSuffix(m.styleSuffixNormalized(name)) {
return fontMatchExact
}
if strings.HasPrefix(name, m.stem) {
return fontMatchPartial
}
for _, alias := range aliases {
alias = fontNormalizeName(alias)
if alias != "" && strings.HasPrefix(name, alias) {
for _, alias := range m.aliases {
if strings.HasPrefix(name, alias) {
return fontMatchPartial
}
}
if exactPath || lowerPath || strings.Contains(name, stem) {
if exactPath || lowerPath || strings.Contains(name, m.stem) {
return fontMatchFuzzy
}
for _, alias := range aliases {
alias = fontNormalizeName(alias)
if alias != "" && strings.Contains(name, alias) {
for _, alias := range m.aliases {
if strings.Contains(name, alias) {
return fontMatchFuzzy
}
}
return fontMatchNone
}
// styleSuffixNormalized 获取规范字体名称的样式后缀
// 入参: name 规范字体名称
// 返回: string 样式后缀
func (m fontPatternMatcher) styleSuffixNormalized(name string) string {
if m.stem == "" || name == "" {
return ""
}
if strings.HasPrefix(name, m.stem) {
return strings.TrimPrefix(name, m.stem)
}
for _, alias := range m.aliases {
if strings.HasPrefix(name, alias) {
return strings.TrimPrefix(name, alias)
}
}
return ""
}
type fontFileMatch struct {
name string
priority int
rank int
styleRank int
sortName string
}
// appendFontFileMatch 追加字体文件匹配结果
// 入参: matches 匹配结果, seen 已匹配文件, matcher 匹配器, file 字体文件候选, rank 匹配等级, bold 是否粗体, italic 是否斜体
func appendFontFileMatch(matches *[]fontFileMatch, seen map[string]int, matcher fontPatternMatcher, file fontFileCandidate, rank int, bold, italic bool) {
if rank == fontMatchNone {
return
}
next := fontFileMatch{
name: file.name,
priority: matcher.priority,
rank: rank,
styleRank: fontFileStyleRank(matcher.styleSuffixNormalized(file.normalized), bold, italic),
sortName: file.lowerBase,
}
if index, ok := seen[file.fold]; ok {
if fontFileMatchLess(next, (*matches)[index]) {
(*matches)[index] = next
}
return
}
seen[file.fold] = len(*matches)
*matches = append(*matches, next)
}
// sortFontFileMatches 排序字体文件匹配结果
// 入参: matches 匹配结果
func sortFontFileMatches(matches []fontFileMatch) {
sort.SliceStable(matches, func(i, j int) bool {
return fontFileMatchLess(matches[i], matches[j])
})
}
// fontFileMatchLess 判断字体文件匹配结果优先级
// 入参: left 左侧匹配结果, right 右侧匹配结果
// 返回: bool 左侧是否优先
func fontFileMatchLess(left, right fontFileMatch) bool {
if left.priority != right.priority {
return left.priority < right.priority
}
if left.rank != right.rank {
return left.rank < right.rank
}
if left.styleRank != right.styleRank {
return left.styleRank < right.styleRank
}
return left.sortName < right.sortName
}
// fontFileMatchNames 获取字体文件匹配名称
// 入参: matches 匹配结果
// 返回: []string 字体文件列表
func fontFileMatchNames(matches []fontFileMatch) []string {
names := make([]string, 0, len(matches))
for _, match := range matches {
names = append(names, match.name)
}
return names
}
// fontFileStyleRank 获取字体文件样式匹配等级
// 入参: suffix 样式后缀, bold 是否粗体, italic 是否斜体
// 返回: int 样式匹配等级
func fontFileStyleRank(suffix string, bold, italic bool) int {
fileBold, fileItalic := fontFileStyleFromSuffix(suffix)
rank := 0
if fileBold != bold {
rank += 2
}
if fileItalic != italic {
rank += 2
}
if !bold && !italic && !fileBold && !fileItalic && suffix != "" && !fontFileRegularSuffix(suffix) {
rank++
}
return rank
}
// fontFileStyleFromSuffix 获取样式后缀对应的字体样式
// 入参: suffix 样式后缀
// 返回: bool 是否粗体, bool 是否斜体
func fontFileStyleFromSuffix(suffix string) (bool, bool) {
bold := suffix == "b" ||
suffix == "bd" ||
suffix == "bold" ||
suffix == "bi" ||
suffix == "bolditalic" ||
suffix == "boldoblique"
italic := suffix == "i" ||
suffix == "it" ||
suffix == "italic" ||
suffix == "oblique" ||
suffix == "bi" ||
suffix == "bolditalic" ||
suffix == "boldoblique"
return bold, italic
}
// fontFileRegularSuffix 判断是否为常规样式后缀
// 入参: suffix 样式后缀
// 返回: bool 是否为常规样式后缀
func fontFileRegularSuffix(suffix string) bool {
switch suffix {
case "r", "regular", "normal", "常规":
return true
default:
return false
}
}
// fontFileKnownStyleSuffix 判断是否为已知样式后缀
// 入参: suffix 样式后缀
// 返回: bool 是否为已知样式后缀
func fontFileKnownStyleSuffix(suffix string) bool {
switch suffix {
case "r", "regular", "normal", "常规",
"b", "bd", "bold",
"i", "it", "italic", "oblique",
"bi", "bolditalic", "boldoblique":
return true
default:
return false
}
}
// fontMemFile 内存字体文件
type fontMemFile struct {
*bytes.Reader
@@ -275,6 +527,9 @@ func (d *fontDir) Close() error {
// 返回: []fs.DirEntry 目录条目, error 错误信息
func (d *fontDir) ReadDir(count int) ([]fs.DirEntry, error) {
if d.offset >= len(d.entries) {
if count <= 0 {
return nil, nil
}
return nil, io.EOF
}
if count <= 0 || d.offset+count > len(d.entries) {
+70 -136
View File
@@ -22,13 +22,13 @@ import (
)
const (
// FontStatusEmbedded 字体使用OFD内嵌字体
// FontStatusEmbedded 内嵌字体可用
FontStatusEmbedded = "embedded"
// FontStatusMatched 字体命中外部字体
// FontStatusMatched 外部字体匹配
FontStatusMatched = "matched"
// FontStatusFallback 字体使用外部字体回退
// FontStatusFallback 外部字体回退
FontStatusFallback = "fallback"
// FontStatusMissing 字体缺失
// FontStatusMissing 字体资源缺失
FontStatusMissing = "missing"
)
@@ -67,6 +67,30 @@ func (r *Reader) Fonts() ([]Font, error) {
// FontInfos 获取OFD字体诊断信息
// 返回: []FontInfo 字体诊断列表, error 错误信息
func (r *Renderer) FontInfos() ([]FontInfo, error) {
doc, err := r.fontInfoDocument()
if err != nil {
return nil, err
}
return r.fontInfos(doc, nil)
}
// FontInfosFromPages 从页面内容获取OFD字体诊断信息
// 入参: pages 页面内容列表
// 返回: []FontInfo 字体诊断列表, error 错误信息
func (r *Renderer) FontInfosFromPages(pages []*PageContent) ([]FontInfo, error) {
doc, err := r.fontInfoDocument()
if err != nil {
return nil, err
}
if pages == nil {
pages = []*PageContent{}
}
return r.fontInfos(doc, pages)
}
// fontInfoDocument 获取字体诊断文档结构
// 返回: *Document 文档结构, error 错误信息
func (r *Renderer) fontInfoDocument() (*Document, error) {
if r == nil || r.Reader == nil {
return nil, fmt.Errorf("ofd renderer is not initialized")
}
@@ -77,11 +101,18 @@ func (r *Renderer) FontInfos() ([]FontInfo, error) {
if doc == nil {
return nil, fmt.Errorf("ofd document is not opened")
}
return doc, nil
}
// fontInfos 获取OFD字体诊断信息
// 入参: doc 文档结构, pages 页面内容列表
// 返回: []FontInfo 字体诊断列表, error 错误信息
func (r *Renderer) fontInfos(doc *Document, pages []*PageContent) ([]FontInfo, error) {
fonts, err := r.Reader.Fonts()
if err != nil {
return nil, err
}
usage := r.fontUsage(doc)
usage := r.fontUsage(doc, pages)
infos := make([]FontInfo, 0, len(fonts)+len(usage))
seen := make(map[string]bool)
for _, font := range fonts {
@@ -98,7 +129,7 @@ func (r *Renderer) FontInfos() ([]FontInfo, error) {
ID: id,
FontName: id,
Status: FontStatusMissing,
Detail: "未在OFD资源中声明",
Detail: "字体资源未被声明",
Used: used,
})
}
@@ -130,16 +161,16 @@ func (r *Renderer) fontInfo(font Font) FontInfo {
if _, err := r.Reader.ResData(font.FontFile); err == nil {
info.Status = FontStatusEmbedded
info.Matched = path.Base(font.FontFile)
info.Detail = "使用OFD内嵌字体"
info.Detail = "使用内嵌字体文件"
} else {
info.Status = FontStatusMissing
info.Detail = "内嵌字体文件缺失"
}
return info
}
if matched, exact := r.matchFont(font.FontName, font.FamilyName); matched != "" {
info.Matched = matched
if exact {
if source, ok := r.fontSourceMatch(font.ID, &font); ok {
info.Matched = source.name
if source.exact {
info.Status = FontStatusMatched
info.Detail = "使用外部字体文件"
} else {
@@ -149,119 +180,50 @@ func (r *Renderer) fontInfo(font Font) FontInfo {
return info
}
info.Status = FontStatusMissing
info.Detail = "未找到可用字体"
info.Detail = "可用字体文件缺失"
return info
}
// matchFont 匹配外部字体
// 入参: names 字体名称列表
// 返回: string 匹配字体文件, bool 是否为名称匹配
func (r *Renderer) matchFont(names ...string) (string, bool) {
for _, dir := range r.fontDirs {
for _, pattern := range fontFilePatterns(names...) {
if matches := r.globFontFiles(dir, pattern); len(matches) > 0 {
return matches[0], true
}
}
}
for _, fsys := range r.fontFS {
if matcher, ok := fsys.(interface {
Match(...string) (string, bool)
}); ok {
if matched, exact := matcher.Match(names...); matched != "" {
return matched, exact
}
continue
}
if matched := matchFontFS(fsys, names...); matched != "" {
return matched, true
}
}
if !canLoadSystemFonts() {
for _, fsys := range r.fontFS {
if matched := fallbackFontFS(fsys); matched != "" {
return matched, false
}
}
}
return "", false
}
// matchFontFS 从字体文件系统匹配字体
// 入参: fsys 字体文件系统, names 字体名称列表
// 返回: string 匹配字体文件
func matchFontFS(fsys fs.FS, names ...string) string {
for _, pattern := range fontFilePatterns(names...) {
if matches := fontFSMatches(fsys, pattern); len(matches) > 0 {
return matches[0]
}
}
return ""
}
// fallbackFontFS 从字体文件系统获取回退字体
// 入参: fsys 字体文件系统
// 返回: string 回退字体文件
func fallbackFontFS(fsys fs.FS) string {
for _, pattern := range fontFallbackFiles {
if matches := fontFSMatches(fsys, pattern); len(matches) > 0 {
return matches[0]
}
}
all, _ := fs.Glob(fsys, "*")
for _, name := range all {
if isFontFileName(name) {
return name
}
}
return ""
}
// fontFSMatches 匹配字体文件系统中的字体文件
// 入参: fsys 字体文件系统, pattern 匹配模式
// fontFSMatchesStyle 匹配字体文件系统中的指定样式字体文件
// 入参: fsys 字体文件系统, patterns 匹配模式列表, bold 是否粗体, italic 是否斜体
// 返回: []string 字体文件列表
func fontFSMatches(fsys fs.FS, pattern string) []string {
seen := make(map[string]bool)
buckets := make([][]string, fontMatchFuzzy+1)
add := func(name string, rank int) {
if !isFontFileName(name) {
return
func fontFSMatchesStyle(fsys fs.FS, patterns []string, bold, italic bool) []string {
names, _ := fs.Glob(fsys, "*")
candidates := fontFileCandidates(names, path.Base)
matches := make([]fontFileMatch, 0, len(candidates))
seen := make(map[string]int, len(candidates))
for _, matcher := range newFontPatternMatchers(patterns) {
for _, file := range candidates {
rank := matcher.rankCandidate(file)
appendFontFileMatch(&matches, seen, matcher, file, rank, bold, italic)
}
if rank == fontMatchNone || seen[name] {
return
}
seen[name] = true
buckets[rank] = append(buckets[rank], name)
}
matches, _ := fs.Glob(fsys, pattern)
for _, name := range matches {
add(name, matchFontPatternRank(pattern, path.Base(name)))
}
all, _ := fs.Glob(fsys, "*")
for _, name := range all {
add(name, matchFontPatternRank(pattern, path.Base(name)))
}
var result []string
for rank := fontMatchExact; rank <= fontMatchFuzzy; rank++ {
result = append(result, buckets[rank]...)
}
return result
sortFontFileMatches(matches)
return fontFileMatchNames(matches)
}
// fontUsage 统计文档字体使用次数
// 入参: doc 文档结构
// 入参: doc 文档结构, pages 页面内容列表, nil表示读取文档页面
// 返回: map[string]int 字体使用次数
func (r *Renderer) fontUsage(doc *Document) map[string]int {
func (r *Renderer) fontUsage(doc *Document, pages []*PageContent) map[string]int {
usage := make(map[string]int)
for _, pageRef := range doc.Pages.Page {
page, err := r.Reader.PageContent(pageRef)
if err == nil {
r.countPageFonts(page, usage)
if pages == nil {
for _, pageRef := range doc.Pages.Page {
if page, err := r.Reader.PageContent(pageRef); err == nil {
r.countPageFonts(page, usage)
}
}
} else {
for _, page := range pages {
if page != nil {
r.countPageFonts(page, usage)
}
}
}
for _, tpl := range doc.CommonData.TemplatePage {
page, err := r.Reader.PageContent(Page{BaseLoc: tpl.BaseLoc})
if err == nil {
r.templatePageCache[tpl.ID] = page
r.countPageFonts(page, usage)
}
}
@@ -336,35 +298,7 @@ func (r *Renderer) countCompositeFonts(cgu CompositeGraphicUnit, usage map[strin
// countTextFont 统计文本字体使用次数
// 入参: text 文本对象, usage 字体使用次数
func (r *Renderer) countTextFont(text TextObject, usage map[string]int) {
fontID := text.Font
if fontID == "" && text.DrawParam != "" {
fontID = r.drawParamFont(text.DrawParam, nil)
}
if fontID != "" {
if fontID := r.textObjectFontID(text); fontID != "" {
usage[fontID]++
}
}
// drawParamFont 获取绘制参数字体
// 入参: id 绘制参数ID, visited 已访问绘制参数
// 返回: string 字体ID
func (r *Renderer) drawParamFont(id string, visited map[string]bool) string {
if id == "" {
return ""
}
if visited == nil {
visited = make(map[string]bool)
}
if visited[id] {
return ""
}
visited[id] = true
dp := r.DrawParams[id]
if dp == nil {
return ""
}
if dp.Font != "" {
return dp.Font
}
return r.drawParamFont(dp.Relative, visited)
}
+285
View File
@@ -0,0 +1,285 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"io/fs"
"path/filepath"
"runtime"
"github.com/tdewolff/canvas"
)
// fontSourceKind 字体来源类型
type fontSourceKind uint8
const (
fontSourceFile fontSourceKind = iota
fontSourceFS
fontSourceSystem
)
// fontSource 字体来源
type fontSource struct {
kind fontSourceKind
index int
name string
exact bool
}
// fontSourceKey 字体来源去重键
type fontSourceKey struct {
kind fontSourceKind
index int
name string
}
// fontCacheKey 字体加载缓存键
type fontCacheKey struct {
fontSourceKey
style canvas.FontStyle
}
// loadFont 加载字体
// 入参: fontID 字体ID
// 返回: *canvas.FontFamily 字体族
func (r *Renderer) loadFont(fontID string) *canvas.FontFamily {
if ff, ok := r.FontMap[fontID]; ok {
return ff
}
var defaultFont *canvas.FontFamily
if r.defaultFontLoaded {
defaultFont = r.fontFamily
}
of, ok := r.Reader.fontCache[fontID]
if !ok {
return defaultFont
}
fontStyle := canvasFontStyle(of)
ff := canvas.NewFontFamily(of.FontName)
if of.FontFile != "" {
if fontData, err := r.Reader.ResData(of.FontFile); err == nil {
if cidMap := getCFFCIDRuneMap(fontData); len(cidMap) > 0 {
if r.FontCIDMap == nil {
r.FontCIDMap = make(map[string]map[uint16]rune)
}
r.FontCIDMap[fontID] = cidMap
}
if _, fixedData, mapping, _, err := FixFontDataAggressive(fontData, true, true); err == nil {
fontData = fixedData
if mapping != nil {
if r.FontGIDMap == nil {
r.FontGIDMap = make(map[string]map[uint16]rune)
}
inv := make(map[uint16]rune)
for k, v := range mapping {
if k == packedGlyphRune(v) {
inv[v] = k
}
}
for k, v := range mapping {
if _, ok := inv[v]; !ok {
inv[v] = k
}
}
r.FontGIDMap[fontID] = inv
}
}
if err := ff.LoadFont(fontData, 0, fontStyle); err == nil {
r.FontMap[fontID] = ff
return ff
}
return nil
}
return nil
}
for _, source := range r.fontSources(fontID, of, fontStyle) {
if loaded := r.loadFontSource(ff, source, fontStyle); loaded != nil {
r.FontMap[fontID] = loaded
r.fontSourceUsed[fontID] = source
return loaded
}
}
r.FontMap[fontID] = defaultFont
return defaultFont
}
// canvasFontStyle 获取Canvas字体样式
// 入参: font OFD字体定义
// 返回: canvas.FontStyle Canvas字体样式
func canvasFontStyle(font *Font) canvas.FontStyle {
var style canvas.FontStyle
if font.Bold {
style |= canvas.FontBold
}
if font.Italic {
style |= canvas.FontItalic
}
return style
}
// fontSources 获取字体来源列表
// 入参: fontID 字体ID, font OFD字体定义, style Canvas字体样式
// 返回: []fontSource 字体来源列表
func (r *Renderer) fontSources(fontID string, font *Font, style canvas.FontStyle) []fontSource {
if sources, ok := r.fontSourceCache[fontID]; ok {
return sources
}
bold := style&canvas.FontBold != 0
italic := style&canvas.FontItalic != 0
patterns := fontFilePatterns(font.FontName, font.FamilyName)
sources := make([]fontSource, 0)
seen := make(map[fontSourceKey]bool)
for _, dir := range r.fontDirs {
for _, name := range r.matchFontFiles(dir, patterns, bold, italic) {
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFile, name: name, exact: true})
}
}
for index, fsys := range r.fontFS {
for _, name := range fontFSMatchesStyle(fsys, patterns, bold, italic) {
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: name, exact: true})
}
}
if !canLoadSystemFonts() {
for index, fsys := range r.fontFS {
names, _ := fs.Glob(fsys, "*")
for _, name := range names {
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: name})
}
}
r.fontSourceCache[fontID] = sources
return sources
}
systemDirs := systemFontDirs()
for _, name := range []string{font.FamilyName, font.FontName} {
for _, systemName := range fontSystemNames(name) {
systemPatterns := fontFilePatterns(systemName)
for _, dir := range systemDirs {
for _, match := range r.matchFontFiles(dir, systemPatterns, bold, italic) {
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFile, name: match, exact: true})
}
}
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceSystem, name: systemName, exact: true})
}
}
r.fontSourceCache[fontID] = sources
return sources
}
// appendFontSource 追加字体来源
// 入参: sources 字体来源列表, seen 去重映射, source 字体来源
// 返回: []fontSource 字体来源列表
func appendFontSource(sources []fontSource, seen map[fontSourceKey]bool, source fontSource) []fontSource {
if source.name == "" {
return sources
}
key := fontSourceKey{kind: source.kind, index: source.index, name: source.name}
if seen[key] {
return sources
}
seen[key] = true
return append(sources, source)
}
// fontSourceMatch 获取匹配的字体来源
// 入参: fontID 字体ID, font OFD字体定义
// 返回: fontSource 字体来源, bool 是否匹配
func (r *Renderer) fontSourceMatch(fontID string, font *Font) (fontSource, bool) {
if source, ok := r.fontSourceUsed[fontID]; ok {
return source, true
}
style := canvasFontStyle(font)
for _, source := range r.fontSources(fontID, font, style) {
key := fontCacheKey{fontSourceKey: fontSourceKey{kind: source.kind, index: source.index, name: source.name}, style: style}
if cached, ok := r.fontCache[key]; ok {
if cached != nil {
return source, true
}
continue
}
family := canvas.NewFontFamily(font.FontName)
if r.loadFontSource(family, source, style) != nil {
return source, true
}
}
return fontSource{}, false
}
// loadFontSource 加载字体来源
// 入参: family 字体族, source 字体来源, style Canvas字体样式
// 返回: *canvas.FontFamily 字体族
func (r *Renderer) loadFontSource(family *canvas.FontFamily, source fontSource, style canvas.FontStyle) *canvas.FontFamily {
key := fontCacheKey{fontSourceKey: fontSourceKey{kind: source.kind, index: source.index, name: source.name}, style: style}
if cached, ok := r.fontCache[key]; ok {
return cached
}
var err error
switch source.kind {
case fontSourceFile:
err = family.LoadFontFile(source.name, style)
case fontSourceFS:
var data []byte
data, err = fs.ReadFile(r.fontFS[source.index], source.name)
if err == nil {
err = family.LoadFont(data, 0, style)
}
case fontSourceSystem:
err = family.LoadSystemFont(source.name, style)
}
if err != nil {
r.fontCache[key] = nil
return nil
}
r.fontCache[key] = family
return family
}
// matchFontFiles 查找字体文件
// 入参: dir 目录, patterns 模式列表, bold 是否粗体, italic 是否斜体
// 返回: []string 文件列表
func (r *Renderer) matchFontFiles(dir string, patterns []string, bold, italic bool) []string {
candidates, ok := r.fontDirCandidates[dir]
if !ok {
files, _ := filepath.Glob(filepath.Join(dir, "*"))
candidates = fontFileCandidates(files, filepath.Base)
r.fontDirCandidates[dir] = candidates
}
matches := make([]fontFileMatch, 0, len(candidates))
index := make(map[string]int, len(candidates))
for _, matcher := range newFontPatternMatchers(patterns) {
for _, file := range candidates {
rank := matcher.rankCandidate(file)
appendFontFileMatch(&matches, index, matcher, file, rank, bold, italic)
}
}
sortFontFileMatches(matches)
return fontFileMatchNames(matches)
}
// systemFontDirs 获取系统字体目录
// 返回: []string 字体目录列表
func systemFontDirs() []string {
switch runtime.GOOS {
case "android":
return []string{"/product/fonts", "/system/fonts"}
case "darwin":
return []string{`/Library/Fonts`}
case "linux":
return []string{`/usr/share/fonts`}
case "windows":
return []string{`C:\Windows\Fonts`}
}
return nil
}
+25 -34
View File
@@ -117,6 +117,8 @@ var fontFallbackFiles = []string{
"arial.ttf",
}
var fontNameReplacer = strings.NewReplacer(" ", "", "-", "", "_", "", "(", "", ")", "", "", "", "", "")
// FontNormalizeName 规范化字体名称
// 入参: name 字体名称
// 返回: string 规范化后的字体名称
@@ -149,10 +151,10 @@ func FontSystemNames(names ...string) []string {
// 入参: name 字体名称
// 返回: string 规范化后的字体名称
func fontNormalizeName(name string) string {
name = strings.ReplaceAll(name, "\\", "/")
name = strings.TrimSuffix(path.Base(name), path.Ext(name))
name = strings.ToLower(strings.TrimSpace(name))
replacer := strings.NewReplacer(" ", "", "-", "", "_", "", "(", "", ")", "", "", "", "", "")
return replacer.Replace(name)
return fontNameReplacer.Replace(name)
}
// fontCandidateNames 获取字体候选名称
@@ -310,13 +312,6 @@ func isFontFileName(name string) bool {
}
}
// fontRuleMatch 判断字体规则是否匹配
// 入参: rule 字体匹配规则, name 字体名称
// 返回: bool 是否匹配
func fontRuleMatch(rule fontMatchRule, name string) bool {
return fontRuleMatchLevel(rule, name) != fontMatchNone
}
// fontRuleMatchLevel 获取字体规则匹配等级
// 入参: rule 字体匹配规则, name 字体名称
// 返回: int 匹配等级
@@ -325,39 +320,35 @@ func fontRuleMatchLevel(rule fontMatchRule, name string) int {
if name == "" {
return fontMatchNone
}
items := fontRuleNames(rule)
for _, item := range items {
level := fontMatchNone
match := func(item string) bool {
item = fontNormalizeName(item)
if item != "" && name == item {
return fontMatchExact
level = fontMatchExact
return true
}
if item != "" && (level == fontMatchNone || level > fontMatchPartial) && strings.HasPrefix(name, item) {
level = fontMatchPartial
}
if item != "" && level == fontMatchNone && strings.Contains(name, item) {
level = fontMatchFuzzy
}
return false
}
for _, item := range rule.Keys {
if match(item) {
return level
}
}
for _, item := range items {
item = fontNormalizeName(item)
if item != "" && (strings.HasPrefix(name, item) || strings.HasPrefix(item, name)) {
return fontMatchPartial
for _, item := range rule.Names {
if match(item) {
return level
}
}
for _, item := range items {
item = fontNormalizeName(item)
if item != "" && (strings.Contains(name, item) || strings.Contains(item, name)) {
return fontMatchFuzzy
}
}
return fontMatchNone
}
// fontRuleNames 获取字体规则名称列表
// 入参: rule 字体匹配规则
// 返回: []string 字体规则名称列表
func fontRuleNames(rule fontMatchRule) []string {
var result []string
result = append(result, rule.Keys...)
result = append(result, rule.Names...)
if rule.System != "" {
result = append(result, rule.System)
match(rule.System)
}
return result
return level
}
// appendFontName 追加字体名称
+7 -6
View File
@@ -120,7 +120,7 @@ func fixTrueType(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uin
if !missingPost && hasBadPostTable(existingTables["post"], numGlyphs, !isCFFSfnt) {
missingPost = true
}
if !missingHead && !missingMaxp && !missingHhea && !missingHmtx &&
if !fixCmap && !missingHead && !missingMaxp && !missingHhea && !missingHmtx &&
!missingOS2 && !missingCmap && !missingName && !missingPost && !malformedDirectory {
return false, data, nil, false, nil
}
@@ -166,16 +166,17 @@ func fixTrueType(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uin
newTables["OS/2"] = buildOS2TableWithMetrics(ascender, descender)
}
var mapping map[rune]uint16
if missingCmap && fixCmap {
if isCFFSfnt {
if fixCmap {
if !missingCmap {
mapping = parseCmapMappings(newTables["cmap"])
}
if len(mapping) == 0 && isCFFSfnt {
mapping = getCmapFromCFF(newTables["CFF "], int(numGlyphs))
}
if len(mapping) == 0 {
mapping = make(map[rune]uint16)
for i := uint16(0); i < numGlyphs; i++ {
mapping[packedGlyphRune(i)] = i
}
}
addPackedGlyphMapping(mapping, numGlyphs)
newTables["cmap"] = buildCmapTable(numGlyphs, mapping)
}
if missingName && fixName {
+152 -28
View File
@@ -20,13 +20,6 @@ import (
"sort"
)
// align4 计算 4 字节对齐后的长度
// 入参: n 原始长度
// 返回: uint32 对齐后的长度
func align4(n uint32) uint32 {
return (n + 3) & ^uint32(3)
}
// packedGlyphRune 获取包装字体字符
// 入参: gid 字形ID
// 返回: rune 包装字体字符
@@ -55,25 +48,163 @@ func calcTableChecksum(data []byte) uint32 {
return sum
}
// checkMissingCmap 检查是否缺失 cmap
// 入参: data 字体数据
// 返回: bool 是否缺失
func checkMissingCmap(data []byte) bool {
if len(data) < 12 {
return true
// parseCmapMappings 解析 cmap 字符映射
// 入参: data cmap表数据
// 返回: map[rune]uint16 字符到字形映射
func parseCmapMappings(data []byte) map[rune]uint16 {
if len(data) < 4 {
return nil
}
numTables := binary.BigEndian.Uint16(data[4:6])
for i := 0; i < int(numTables); i++ {
pos := 12 + i*16
if len(data) < pos+4 {
numTables := int(binary.BigEndian.Uint16(data[2:4]))
result := make(map[rune]uint16)
for i := 0; i < numTables; i++ {
pos := 4 + i*8
if pos+8 > len(data) {
break
}
tag := string(data[pos : pos+4])
if tag == "cmap" {
return false
platformID := binary.BigEndian.Uint16(data[pos : pos+2])
if platformID != 0 && platformID != 3 {
continue
}
offset := int(binary.BigEndian.Uint32(data[pos+4 : pos+8]))
if offset+2 > len(data) {
continue
}
switch binary.BigEndian.Uint16(data[offset : offset+2]) {
case 0:
parseCmapFormat0(data[offset:], result)
case 4:
parseCmapFormat4(data[offset:], result)
case 6:
parseCmapFormat6(data[offset:], result)
case 12:
parseCmapFormat12(data[offset:], result)
}
}
return true
if len(result) == 0 {
return nil
}
return result
}
// parseCmapFormat0 解析 cmap format 0
// 入参: data 子表数据, result 字符映射
func parseCmapFormat0(data []byte, result map[rune]uint16) {
if len(data) < 262 {
return
}
for i := 0; i < 256; i++ {
gid := uint16(data[6+i])
if gid != 0 {
result[rune(i)] = gid
}
}
}
// parseCmapFormat4 解析 cmap format 4
// 入参: data 子表数据, result 字符映射
func parseCmapFormat4(data []byte, result map[rune]uint16) {
if len(data) < 16 {
return
}
length := int(binary.BigEndian.Uint16(data[2:4]))
if length > len(data) {
length = len(data)
}
segCount := int(binary.BigEndian.Uint16(data[6:8]) / 2)
endPos := 14
startPos := endPos + segCount*2 + 2
deltaPos := startPos + segCount*2
rangePos := deltaPos + segCount*2
if rangePos+segCount*2 > length {
return
}
for i := 0; i < segCount; i++ {
end := binary.BigEndian.Uint16(data[endPos+i*2:])
start := binary.BigEndian.Uint16(data[startPos+i*2:])
delta := int16(binary.BigEndian.Uint16(data[deltaPos+i*2:]))
rangeOffsetPos := rangePos + i*2
rangeOffset := binary.BigEndian.Uint16(data[rangeOffsetPos:])
if start == 0xFFFF && end == 0xFFFF {
continue
}
for c := start; c <= end; c++ {
var gid uint16
if rangeOffset == 0 {
gid = uint16(int(c) + int(delta))
} else {
gidPos := rangeOffsetPos + int(rangeOffset) + int(c-start)*2
if gidPos+2 > length {
continue
}
gid = binary.BigEndian.Uint16(data[gidPos:])
if gid != 0 {
gid = uint16(int(gid) + int(delta))
}
}
if gid != 0 {
result[rune(c)] = gid
}
if c == 0xFFFF {
break
}
}
}
}
// parseCmapFormat6 解析 cmap format 6
// 入参: data 子表数据, result 字符映射
func parseCmapFormat6(data []byte, result map[rune]uint16) {
if len(data) < 10 {
return
}
firstCode := int(binary.BigEndian.Uint16(data[6:8]))
entryCount := int(binary.BigEndian.Uint16(data[8:10]))
for i := 0; i < entryCount && 10+i*2+2 <= len(data); i++ {
gid := binary.BigEndian.Uint16(data[10+i*2:])
if gid != 0 {
result[rune(firstCode+i)] = gid
}
}
}
// parseCmapFormat12 解析 cmap format 12
// 入参: data 子表数据, result 字符映射
func parseCmapFormat12(data []byte, result map[rune]uint16) {
if len(data) < 16 {
return
}
length := int(binary.BigEndian.Uint32(data[4:8]))
if length > len(data) {
length = len(data)
}
nGroups := int(binary.BigEndian.Uint32(data[12:16]))
for i := 0; i < nGroups; i++ {
pos := 16 + i*12
if pos+12 > length {
break
}
startChar := binary.BigEndian.Uint32(data[pos:])
endChar := binary.BigEndian.Uint32(data[pos+4:])
startGID := binary.BigEndian.Uint32(data[pos+8:])
for c := startChar; c <= endChar; c++ {
gid := startGID + c - startChar
if gid != 0 && gid <= 0xFFFF {
result[rune(c)] = uint16(gid)
}
if c == endChar {
break
}
}
}
}
// addPackedGlyphMapping 添加字形ID私有映射
// 入参: mapping 字符映射, numGlyphs 字形数量
func addPackedGlyphMapping(mapping map[rune]uint16, numGlyphs uint16) {
for i := uint16(0); i < numGlyphs; i++ {
mapping[packedGlyphRune(i)] = i
}
}
// buildHeadTable 构建 head 表
@@ -128,13 +259,6 @@ func buildHheaTable(numGlyphs uint16) []byte {
return buf.Bytes()
}
// buildMaxpTable 构建 maxp 表
// 入参: numGlyphs 字形数量
// 返回: []byte maxp表数据
func buildMaxpTable(numGlyphs uint16) []byte {
return buildCFFMaxpTable(numGlyphs)
}
// buildCFFMaxpTable 构建 CFF 轮廓使用的 maxp 0.5 表
// 入参: numGlyphs 字形数量
// 返回: []byte maxp表数据
+12 -7
View File
@@ -15,6 +15,7 @@
package ofdgo
import (
"fmt"
"math"
"strconv"
"strings"
@@ -30,14 +31,18 @@ type Box struct {
// 返回: Box 矩形对象, error 错误信息
func ParseBox(s string) (Box, error) {
parts := strings.Fields(s)
if len(parts) < 4 {
return Box{}, nil
if len(parts) != 4 {
return Box{}, fmt.Errorf("invalid box: %s", s)
}
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
var values [4]float64
for i, part := range parts {
value, err := strconv.ParseFloat(part, 64)
if err != nil {
return Box{}, fmt.Errorf("invalid box: %s", s)
}
values[i] = value
}
return Box{X: values[0], Y: values[1], W: values[2], H: values[3]}, nil
}
// Matrix 2D仿射变换矩阵
+177
View File
@@ -0,0 +1,177 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"bytes"
"encoding/binary"
"fmt"
"image"
"image/color"
_ "image/gif"
canvasimage "github.com/tdewolff/canvas/image"
_ "github.com/xiaoqidun/jbig2"
_ "golang.org/x/image/bmp"
_ "golang.org/x/image/tiff"
)
// decodeImageData 解码图片数据
// 入参: data 图片数据
// 返回: image.Image 图片对象, string 图片格式, error 错误信息
func decodeImageData(data []byte) (image.Image, string, error) {
if isJPEGData(data) {
if img, err := canvasimage.NewJPEGImage(bytes.NewReader(data)); err == nil {
return img, "jpeg", nil
}
}
if isPNGData(data) {
if img, err := canvasimage.NewPNGImage(bytes.NewReader(data)); err == nil {
return img, "png", nil
}
}
img, format, err := image.Decode(bytes.NewReader(data))
if err == nil {
return img, normalizeSealType(format), nil
}
if img, err := decodeBMPImage(data); err == nil {
return img, "bmp", nil
}
return nil, "", err
}
// isJPEGData 判断是否为JPEG图片数据
// 入参: data 图片数据
// 返回: bool 是否为JPEG图片数据
func isJPEGData(data []byte) bool {
return len(data) >= 2 && data[0] == 0xFF && data[1] == 0xD8
}
// isPNGData 判断是否为PNG图片数据
// 入参: data 图片数据
// 返回: bool 是否为PNG图片数据
func isPNGData(data []byte) bool {
return bytes.HasPrefix(data, []byte("\x89PNG\r\n\x1a\n"))
}
// decodeImageConfigData 解码图片尺寸
// 入参: data 图片数据
// 返回: image.Config 图片尺寸, string 图片格式, error 错误信息
func decodeImageConfigData(data []byte) (image.Config, string, error) {
cfg, format, err := image.DecodeConfig(bytes.NewReader(data))
if err == nil {
return cfg, normalizeSealType(format), nil
}
if cfg, err := decodeBMPConfig(data); err == nil {
return cfg, "bmp", nil
}
return image.Config{}, "", err
}
// decodeBMPConfig 解码 BMP 尺寸
// 入参: data 图片数据
// 返回: image.Config 图片尺寸, error 错误信息
func decodeBMPConfig(data []byte) (image.Config, error) {
offset, width, height, bpp, compression, err := parseBMPHeader(data)
if err != nil {
return image.Config{}, err
}
if offset <= 0 || width <= 0 || height == 0 || compression != 0 {
return image.Config{}, fmt.Errorf("unsupported bmp")
}
if bpp != 16 && bpp != 24 && bpp != 32 {
return image.Config{}, fmt.Errorf("unsupported bmp")
}
if height < 0 {
height = -height
}
return image.Config{ColorModel: color.NRGBAModel, Width: width, Height: height}, nil
}
// decodeBMPImage 解码 BMP 图片
// 入参: data 图片数据
// 返回: image.Image 图片对象, error 错误信息
func decodeBMPImage(data []byte) (image.Image, error) {
offset, width, height, bpp, compression, err := parseBMPHeader(data)
if err != nil {
return nil, err
}
if compression != 0 {
return nil, fmt.Errorf("unsupported bmp compression")
}
topDown := height < 0
if topDown {
height = -height
}
if width <= 0 || height <= 0 {
return nil, fmt.Errorf("invalid bmp size")
}
rowStride := ((width*int(bpp) + 31) / 32) * 4
if offset+rowStride*height > len(data) {
return nil, fmt.Errorf("truncated bmp")
}
img := image.NewNRGBA(image.Rect(0, 0, width, height))
for y := 0; y < height; y++ {
srcY := y
if !topDown {
srcY = height - 1 - y
}
row := data[offset+srcY*rowStride:]
for x := 0; x < width; x++ {
switch bpp {
case 16:
v := binary.LittleEndian.Uint16(row[x*2:])
img.SetNRGBA(x, y, color.NRGBA{
R: uint8(((v >> 10) & 0x1F) * 255 / 31),
G: uint8(((v >> 5) & 0x1F) * 255 / 31),
B: uint8((v & 0x1F) * 255 / 31),
A: 255,
})
case 24:
p := x * 3
img.SetNRGBA(x, y, color.NRGBA{R: row[p+2], G: row[p+1], B: row[p], A: 255})
case 32:
p := x * 4
img.SetNRGBA(x, y, color.NRGBA{R: row[p+2], G: row[p+1], B: row[p], A: 255})
default:
return nil, fmt.Errorf("unsupported bmp")
}
}
}
return img, nil
}
// parseBMPHeader 解析 BMP 文件头
// 入参: data 图片数据
// 返回: int 像素偏移, int 宽度, int 高度, uint16 位数, uint32 压缩方式, error 错误信息
func parseBMPHeader(data []byte) (int, int, int, uint16, uint32, error) {
if len(data) < 54 || string(data[:2]) != "BM" {
return 0, 0, 0, 0, 0, fmt.Errorf("not bmp")
}
offset := int(binary.LittleEndian.Uint32(data[10:14]))
dibSize := binary.LittleEndian.Uint32(data[14:18])
if dibSize < 40 {
return 0, 0, 0, 0, 0, fmt.Errorf("unsupported bmp")
}
width := int(int32(binary.LittleEndian.Uint32(data[18:22])))
height := int(int32(binary.LittleEndian.Uint32(data[22:26])))
planes := binary.LittleEndian.Uint16(data[26:28])
bpp := binary.LittleEndian.Uint16(data[28:30])
compression := binary.LittleEndian.Uint32(data[30:34])
if planes != 1 {
return 0, 0, 0, 0, 0, fmt.Errorf("invalid bmp")
}
return offset, width, height, bpp, compression, nil
}
+129 -280
View File
@@ -19,6 +19,76 @@ import (
"strconv"
)
// graphicObjectTarget 图形对象集合
type graphicObjectTarget struct {
objects *[]GraphicObject
text *[]TextObject
path *[]PathObject
image *[]ImageObject
composite *[]CompositeGraphicUnit
}
// decodeGraphicObject 解析图形对象
// 入参: d XML解码器, start 起始节点, target 图形对象集合
// 返回: bool 是否为图形对象, error 错误信息
func decodeGraphicObject(d *xml.Decoder, start xml.StartElement, target graphicObjectTarget) (bool, error) {
switch start.Name.Local {
case "TextObject":
var obj TextObject
if err := d.DecodeElement(&obj, &start); err != nil {
return true, err
}
*target.text = append(*target.text, obj)
*target.objects = append(*target.objects, GraphicObject{Type: start.Name.Local, TextObject: obj})
case "PathObject":
var obj PathObject
if err := d.DecodeElement(&obj, &start); err != nil {
return true, err
}
*target.path = append(*target.path, obj)
*target.objects = append(*target.objects, GraphicObject{Type: start.Name.Local, PathObject: obj})
case "ImageObject":
var obj ImageObject
if err := d.DecodeElement(&obj, &start); err != nil {
return true, err
}
*target.image = append(*target.image, obj)
*target.objects = append(*target.objects, GraphicObject{Type: start.Name.Local, ImageObject: obj})
case "CompositeGraphicUnit", "CompositeObject":
var obj CompositeGraphicUnit
if err := d.DecodeElement(&obj, &start); err != nil {
return true, err
}
*target.composite = append(*target.composite, obj)
*target.objects = append(*target.objects, GraphicObject{Type: start.Name.Local, CompositeGraphicUnit: obj})
default:
return false, nil
}
return true, nil
}
// decodeObjectContainer 解析图形对象容器
// 入参: d XML解码器, start 起始节点, decode 对象解码函数
// 返回: error 错误信息
func decodeObjectContainer(d *xml.Decoder, start xml.StartElement, decode func(*xml.Decoder, xml.StartElement) error) error {
for {
tok, err := d.Token()
if err != nil {
return err
}
switch node := tok.(type) {
case xml.StartElement:
if err := decode(d, node); err != nil {
return err
}
case xml.EndElement:
if node.Name.Local == start.Name.Local {
return nil
}
}
}
}
// UnmarshalXML 解析图层并保留对象顺序
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
@@ -26,85 +96,27 @@ func (l *Layer) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
*l = Layer{}
l.ID = attrValue(start, "ID")
l.DrawParam = attrValue(start, "DrawParam")
for {
tok, err := d.Token()
if err != nil {
return err
}
switch node := tok.(type) {
case xml.StartElement:
if err := l.decodeObject(d, node); err != nil {
return err
}
case xml.EndElement:
if node.Name.Local == start.Name.Local {
return nil
}
}
}
return decodeObjectContainer(d, start, l.decodeObject)
}
// decodeObject 解析图层子对象
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (l *Layer) decodeObject(d *xml.Decoder, start xml.StartElement) error {
switch start.Name.Local {
case "TextObject":
var obj TextObject
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
l.TextObject = append(l.TextObject, obj)
l.Objects = append(l.Objects, GraphicObject{Type: start.Name.Local, TextObject: obj})
case "PathObject":
var obj PathObject
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
l.PathObject = append(l.PathObject, obj)
l.Objects = append(l.Objects, GraphicObject{Type: start.Name.Local, PathObject: obj})
case "ImageObject":
var obj ImageObject
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
l.ImageObject = append(l.ImageObject, obj)
l.Objects = append(l.Objects, GraphicObject{Type: start.Name.Local, ImageObject: obj})
case "CompositeGraphicUnit", "CompositeObject":
var obj CompositeGraphicUnit
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
l.CompositeGraphicUnit = append(l.CompositeGraphicUnit, obj)
l.Objects = append(l.Objects, GraphicObject{Type: start.Name.Local, CompositeGraphicUnit: obj})
case "PageBlock":
return l.decodePageBlock(d, start)
default:
return d.Skip()
target := graphicObjectTarget{
objects: &l.Objects,
text: &l.TextObject,
path: &l.PathObject,
image: &l.ImageObject,
composite: &l.CompositeGraphicUnit,
}
return nil
}
// decodePageBlock 解析页块子对象
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (l *Layer) decodePageBlock(d *xml.Decoder, start xml.StartElement) error {
for {
tok, err := d.Token()
if err != nil {
return err
}
switch node := tok.(type) {
case xml.StartElement:
if err := l.decodeObject(d, node); err != nil {
return err
}
case xml.EndElement:
if node.Name.Local == start.Name.Local {
return nil
}
}
if decoded, err := decodeGraphicObject(d, start, target); decoded || err != nil {
return err
}
if start.Name.Local == "PageBlock" {
return decodeObjectContainer(d, start, l.decodeObject)
}
return d.Skip()
}
// UnmarshalXML 解析复合图元并保留对象顺序
@@ -128,178 +140,73 @@ func (c *CompositeGraphicUnit) UnmarshalXML(d *xml.Decoder, start xml.StartEleme
c.Visible = &visible
}
}
for {
tok, err := d.Token()
if err != nil {
return err
}
switch node := tok.(type) {
case xml.StartElement:
if err := c.decodeObject(d, node); err != nil {
return err
}
case xml.EndElement:
if node.Name.Local == start.Name.Local {
return nil
}
}
}
return decodeObjectContainer(d, start, c.decodeObject)
}
// decodeObject 解析复合图元子对象
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (c *CompositeGraphicUnit) decodeObject(d *xml.Decoder, start xml.StartElement) error {
target := graphicObjectTarget{
objects: &c.Objects,
text: &c.TextObject,
path: &c.PathObject,
image: &c.ImageObject,
composite: &c.CompositeGraphicUnit,
}
if decoded, err := decodeGraphicObject(d, start, target); decoded || err != nil {
return err
}
switch start.Name.Local {
case "TextObject":
var obj TextObject
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
c.TextObject = append(c.TextObject, obj)
c.Objects = append(c.Objects, GraphicObject{Type: start.Name.Local, TextObject: obj})
case "PathObject":
var obj PathObject
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
c.PathObject = append(c.PathObject, obj)
c.Objects = append(c.Objects, GraphicObject{Type: start.Name.Local, PathObject: obj})
case "ImageObject":
var obj ImageObject
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
c.ImageObject = append(c.ImageObject, obj)
c.Objects = append(c.Objects, GraphicObject{Type: start.Name.Local, ImageObject: obj})
case "CompositeGraphicUnit", "CompositeObject":
var obj CompositeGraphicUnit
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
c.CompositeGraphicUnit = append(c.CompositeGraphicUnit, obj)
c.Objects = append(c.Objects, GraphicObject{Type: start.Name.Local, CompositeGraphicUnit: obj})
case "Clips":
var clips Clips
if err := d.DecodeElement(&clips, &start); err != nil {
return err
}
c.Clips = &clips
case "Actions":
var actions struct {
Action []Action `xml:"Action"`
}
if err := d.DecodeElement(&actions, &start); err != nil {
return err
}
c.Actions = actions.Action
case "Content", "PageBlock":
return c.decodePageBlock(d, start)
return decodeObjectContainer(d, start, c.decodeObject)
default:
return d.Skip()
}
return nil
}
// decodePageBlock 解析页块子对象
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (c *CompositeGraphicUnit) decodePageBlock(d *xml.Decoder, start xml.StartElement) error {
for {
tok, err := d.Token()
if err != nil {
return err
}
switch node := tok.(type) {
case xml.StartElement:
if err := c.decodeObject(d, node); err != nil {
return err
}
case xml.EndElement:
if node.Name.Local == start.Name.Local {
return nil
}
}
}
}
// UnmarshalXML 解析注释外观并保留对象顺序
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (a *Appearance) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
*a = Appearance{}
a.Boundary = attrValue(start, "Boundary")
for {
tok, err := d.Token()
if err != nil {
return err
}
switch node := tok.(type) {
case xml.StartElement:
if err := a.decodeObject(d, node); err != nil {
return err
}
case xml.EndElement:
if node.Name.Local == start.Name.Local {
return nil
}
}
}
return decodeObjectContainer(d, start, a.decodeObject)
}
// decodeObject 解析注释外观子对象
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (a *Appearance) decodeObject(d *xml.Decoder, start xml.StartElement) error {
switch start.Name.Local {
case "TextObject":
var obj TextObject
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
a.TextObject = append(a.TextObject, obj)
a.Objects = append(a.Objects, GraphicObject{Type: start.Name.Local, TextObject: obj})
case "PathObject":
var obj PathObject
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
a.PathObject = append(a.PathObject, obj)
a.Objects = append(a.Objects, GraphicObject{Type: start.Name.Local, PathObject: obj})
case "ImageObject":
var obj ImageObject
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
a.ImageObject = append(a.ImageObject, obj)
a.Objects = append(a.Objects, GraphicObject{Type: start.Name.Local, ImageObject: obj})
case "CompositeGraphicUnit", "CompositeObject":
var obj CompositeGraphicUnit
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
a.CompositeGraphicUnit = append(a.CompositeGraphicUnit, obj)
a.Objects = append(a.Objects, GraphicObject{Type: start.Name.Local, CompositeGraphicUnit: obj})
case "PageBlock":
return a.decodePageBlock(d, start)
default:
return d.Skip()
target := graphicObjectTarget{
objects: &a.Objects,
text: &a.TextObject,
path: &a.PathObject,
image: &a.ImageObject,
composite: &a.CompositeGraphicUnit,
}
return nil
}
// decodePageBlock 解析注释外观页块子对象
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (a *Appearance) decodePageBlock(d *xml.Decoder, start xml.StartElement) error {
for {
tok, err := d.Token()
if err != nil {
return err
}
switch node := tok.(type) {
case xml.StartElement:
if err := a.decodeObject(d, node); err != nil {
return err
}
case xml.EndElement:
if node.Name.Local == start.Name.Local {
return nil
}
}
if decoded, err := decodeGraphicObject(d, start, target); decoded || err != nil {
return err
}
if start.Name.Local == "PageBlock" {
return decodeObjectContainer(d, start, a.decodeObject)
}
return d.Skip()
}
// UnmarshalXML 解析图案单元内容并保留对象顺序
@@ -307,85 +214,27 @@ func (a *Appearance) decodePageBlock(d *xml.Decoder, start xml.StartElement) err
// 返回: error 错误信息
func (p *PatternContent) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
*p = PatternContent{}
for {
tok, err := d.Token()
if err != nil {
return err
}
switch node := tok.(type) {
case xml.StartElement:
if err := p.decodeObject(d, node); err != nil {
return err
}
case xml.EndElement:
if node.Name.Local == start.Name.Local {
return nil
}
}
}
return decodeObjectContainer(d, start, p.decodeObject)
}
// decodeObject 解析图案单元内容子对象
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (p *PatternContent) decodeObject(d *xml.Decoder, start xml.StartElement) error {
switch start.Name.Local {
case "TextObject":
var obj TextObject
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
p.TextObject = append(p.TextObject, obj)
p.Objects = append(p.Objects, GraphicObject{Type: start.Name.Local, TextObject: obj})
case "PathObject":
var obj PathObject
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
p.PathObject = append(p.PathObject, obj)
p.Objects = append(p.Objects, GraphicObject{Type: start.Name.Local, PathObject: obj})
case "ImageObject":
var obj ImageObject
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
p.ImageObject = append(p.ImageObject, obj)
p.Objects = append(p.Objects, GraphicObject{Type: start.Name.Local, ImageObject: obj})
case "CompositeGraphicUnit", "CompositeObject":
var obj CompositeGraphicUnit
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
p.CompositeGraphicUnit = append(p.CompositeGraphicUnit, obj)
p.Objects = append(p.Objects, GraphicObject{Type: start.Name.Local, CompositeGraphicUnit: obj})
case "PageBlock":
return p.decodePageBlock(d, start)
default:
return d.Skip()
target := graphicObjectTarget{
objects: &p.Objects,
text: &p.TextObject,
path: &p.PathObject,
image: &p.ImageObject,
composite: &p.CompositeGraphicUnit,
}
return nil
}
// decodePageBlock 解析图案单元内容页块子对象
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (p *PatternContent) decodePageBlock(d *xml.Decoder, start xml.StartElement) error {
for {
tok, err := d.Token()
if err != nil {
return err
}
switch node := tok.(type) {
case xml.StartElement:
if err := p.decodeObject(d, node); err != nil {
return err
}
case xml.EndElement:
if node.Name.Local == start.Name.Local {
return nil
}
}
if decoded, err := decodeGraphicObject(d, start, target); decoded || err != nil {
return err
}
if start.Name.Local == "PageBlock" {
return decodeObjectContainer(d, start, p.decodeObject)
}
return d.Skip()
}
// attrValue 获取XML属性值
+13 -8
View File
@@ -23,6 +23,7 @@ type PageContent struct {
Area PageArea `xml:"Area"`
Template []Template `xml:"Template"`
Content Content `xml:"Content"`
Actions []Action `xml:"Actions>Action"`
}
// Template 页面模板引用
@@ -58,7 +59,7 @@ type GraphicObject struct {
// Clips 裁剪区域集合
type Clips struct {
TransFlag bool `xml:"TransFlag,attr"`
TransFlag *bool `xml:"TransFlag,attr"`
Clip []Clip `xml:"Clip"`
}
@@ -97,6 +98,7 @@ type TextObject struct {
CGTransform []CGTransform `xml:"CGTransform"`
TextCode []TextCode `xml:"TextCode"`
Clips *Clips `xml:"Clips"`
Actions []Action `xml:"Actions>Action"`
}
// FillColor 填充颜色
@@ -165,6 +167,7 @@ type PathObject struct {
FillColor *FillColor `xml:"FillColor"`
AbbreviatedData string `xml:"AbbreviatedData"`
Clips *Clips `xml:"Clips"`
Actions []Action `xml:"Actions>Action"`
}
// StrokeColor 勾边颜色
@@ -207,11 +210,13 @@ type ShdColor struct {
// ImageObject 图片对象
type ImageObject struct {
ID string `xml:"ID,attr"`
Boundary string `xml:"Boundary,attr"`
ResourceID string `xml:"ResourceID,attr"`
CTM string `xml:"CTM,attr"`
Alpha *int `xml:"Alpha,attr"`
Visible *bool `xml:"Visible,attr"`
Clips *Clips `xml:"Clips"`
ID string `xml:"ID,attr"`
Boundary string `xml:"Boundary,attr"`
ResourceID string `xml:"ResourceID,attr"`
ImageMask string `xml:"ImageMask,attr"`
CTM string `xml:"CTM,attr"`
Alpha *int `xml:"Alpha,attr"`
Visible *bool `xml:"Visible,attr"`
Clips *Clips `xml:"Clips"`
Actions []Action `xml:"Actions>Action"`
}
+330
View File
@@ -0,0 +1,330 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"image/color"
"strconv"
"strings"
"github.com/tdewolff/canvas"
)
// parseColorWithAlpha 解析带透明度的颜色
// 入参: val 颜色值, alpha 透明度
// 返回: color.Color 颜色对象
func parseColorWithAlpha(val string, alpha *int) color.Color {
parts := strings.Fields(val)
if len(parts) >= 3 {
r := parseColorComponent(parts[0])
g := parseColorComponent(parts[1])
b := parseColorComponent(parts[2])
a := 255
if alpha != nil {
a = *alpha
}
a = clampColor(a)
return color.RGBA{
R: uint8(clampColor(r) * a / 255),
G: uint8(clampColor(g) * a / 255),
B: uint8(clampColor(b) * a / 255),
A: uint8(a),
}
}
return color.Black
}
// parseColorComponent 解析颜色分量
// 入参: s 颜色分量
// 返回: int 颜色分量值
func parseColorComponent(s string) int {
s = strings.TrimSpace(s)
if strings.HasPrefix(s, "#") {
v, _ := strconv.ParseInt(strings.TrimPrefix(s, "#"), 16, 0)
return int(v)
}
v, _ := strconv.Atoi(s)
return v
}
// clampColor 限制颜色分量范围
// 入参: v 颜色分量
// 返回: int 颜色分量
func clampColor(v int) int {
if v < 0 {
return 0
}
if v > 255 {
return 255
}
return v
}
// mergeAlpha 合并透明度
// 入参: colorAlpha 颜色透明度, objectAlpha 对象透明度
// 返回: *int 合并后的透明度
func mergeAlpha(colorAlpha, objectAlpha *int) *int {
if colorAlpha == nil && objectAlpha == nil {
return nil
}
alpha := 255
if colorAlpha != nil {
alpha = *colorAlpha
}
if objectAlpha != nil {
alpha = alpha * *objectAlpha / 255
}
alpha = clampColor(alpha)
return &alpha
}
// withFillAlpha 合并填充色透明度
// 入参: fillColor 填充颜色节点, alpha 对象透明度
// 返回: *FillColor 合并后的填充颜色节点
func withFillAlpha(fillColor *FillColor, alpha *int) *FillColor {
if fillColor == nil || alpha == nil {
return fillColor
}
merged := *fillColor
merged.Alpha = mergeAlpha(fillColor.Alpha, alpha)
return &merged
}
// withStrokeAlpha 合并勾边色透明度
// 入参: strokeColor 勾边颜色节点, alpha 对象透明度
// 返回: *StrokeColor 合并后的勾边颜色节点
func withStrokeAlpha(strokeColor *StrokeColor, alpha *int) *StrokeColor {
if strokeColor == nil || alpha == nil {
return strokeColor
}
merged := *strokeColor
merged.Alpha = mergeAlpha(strokeColor.Alpha, alpha)
return &merged
}
// colorWithAlpha 合并颜色透明度
// 入参: c 颜色对象, alpha 对象透明度
// 返回: color.Color 合并后的颜色对象
func colorWithAlpha(c color.Color, alpha *int) color.Color {
if c == nil || alpha == nil {
return c
}
a := clampColor(*alpha)
rgba := colorToRGBA(c)
return color.RGBA{
R: uint8(int(rgba.R) * a / 255),
G: uint8(int(rgba.G) * a / 255),
B: uint8(int(rgba.B) * a / 255),
A: uint8(int(rgba.A) * a / 255),
}
}
// parseFillColor 解析填充颜色
// 入参: fillColor 填充颜色节点
// 返回: color.Color 颜色对象
func parseFillColor(fillColor *FillColor) color.Color {
if fillColor == nil {
return nil
}
if fillColor.Pattern != nil {
return nil
}
if fillColor.AxialShd != nil {
return parseShdColor(fillColor.AxialShd.Segment, fillColor.Alpha)
}
if fillColor.RadialShd != nil {
return parseShdColor(fillColor.RadialShd.Segment, fillColor.Alpha)
}
if strings.TrimSpace(fillColor.Value) != "" {
return parseColorWithAlpha(fillColor.Value, fillColor.Alpha)
}
return nil
}
// parseFillPaint 解析填充画刷
// 入参: fillColor 填充颜色节点, x X坐标, y Y坐标, pageH 页面高度, originX 原点X坐标, originY 原点Y坐标
// 返回: any 填充画刷
func parseFillPaint(fillColor *FillColor, x, y, pageH, originX, originY float64) any {
if fillColor == nil {
return nil
}
if fillColor.Pattern != nil {
return nil
}
if gradient := parseAxialShdGradient(fillColor.AxialShd, fillColor.Alpha, x, y, pageH, originX, originY); gradient != nil {
return gradient
}
if gradient := parseRadialShdGradient(fillColor.RadialShd, fillColor.Alpha, x, y, pageH, originX, originY); gradient != nil {
return gradient
}
if fillColor.AxialShd != nil {
return parseShdColor(fillColor.AxialShd.Segment, fillColor.Alpha)
}
if fillColor.RadialShd != nil {
return parseShdColor(fillColor.RadialShd.Segment, fillColor.Alpha)
}
if strings.TrimSpace(fillColor.Value) != "" {
return parseColorWithAlpha(fillColor.Value, fillColor.Alpha)
}
return nil
}
// parseStrokeColor 解析勾边颜色
// 入参: strokeColor 勾边颜色节点
// 返回: color.Color 颜色对象
func parseStrokeColor(strokeColor *StrokeColor) color.Color {
if strokeColor == nil {
return nil
}
if strokeColor.AxialShd != nil {
return parseShdColor(strokeColor.AxialShd.Segment, strokeColor.Alpha)
}
if strokeColor.RadialShd != nil {
return parseShdColor(strokeColor.RadialShd.Segment, strokeColor.Alpha)
}
if strings.TrimSpace(strokeColor.Value) != "" {
return parseColorWithAlpha(strokeColor.Value, strokeColor.Alpha)
}
return nil
}
// parseStrokePaint 解析勾边画刷
// 入参: strokeColor 勾边颜色节点, x X坐标, y Y坐标, pageH 页面高度, originX 原点X坐标, originY 原点Y坐标
// 返回: any 勾边画刷
func parseStrokePaint(strokeColor *StrokeColor, x, y, pageH, originX, originY float64) any {
if strokeColor == nil {
return nil
}
if gradient := parseAxialShdGradient(strokeColor.AxialShd, strokeColor.Alpha, x, y, pageH, originX, originY); gradient != nil {
return gradient
}
if gradient := parseRadialShdGradient(strokeColor.RadialShd, strokeColor.Alpha, x, y, pageH, originX, originY); gradient != nil {
return gradient
}
if strokeColor.AxialShd != nil {
return parseShdColor(strokeColor.AxialShd.Segment, strokeColor.Alpha)
}
if strokeColor.RadialShd != nil {
return parseShdColor(strokeColor.RadialShd.Segment, strokeColor.Alpha)
}
if strings.TrimSpace(strokeColor.Value) != "" {
return parseColorWithAlpha(strokeColor.Value, strokeColor.Alpha)
}
return nil
}
// patternColor 获取图案单元默认颜色
// 入参: fillColor 填充颜色节点
// 返回: color.Color 默认颜色
func patternColor(fillColor *FillColor) color.Color {
if fillColor == nil || strings.TrimSpace(fillColor.Value) == "" {
return nil
}
return parseColorWithAlpha(fillColor.Value, fillColor.Alpha)
}
// parseShdColor 解析渐变颜色
// 入参: segments 渐变分段, alpha 透明度
// 返回: color.Color 颜色对象
func parseShdColor(segments []ShdSegment, alpha *int) color.Color {
for _, segment := range segments {
if strings.TrimSpace(segment.Color.Value) == "" {
continue
}
segmentAlpha := alpha
if segmentAlpha == nil {
segmentAlpha = segment.Color.Alpha
}
return parseColorWithAlpha(segment.Color.Value, segmentAlpha)
}
return color.Black
}
// parseShdSegments 解析渐变分段
// 入参: segments 渐变分段, alpha 透明度
// 返回: canvas.Grad 渐变分段
func parseShdSegments(segments []ShdSegment, alpha *int) canvas.Grad {
gradient := canvas.NewGradient()
for _, segment := range segments {
if strings.TrimSpace(segment.Color.Value) == "" {
continue
}
segmentAlpha := alpha
if segmentAlpha == nil {
segmentAlpha = segment.Color.Alpha
}
gradient.Add(segment.Position, colorToRGBA(parseColorWithAlpha(segment.Color.Value, segmentAlpha)))
}
if len(gradient) == 0 {
return nil
}
return gradient
}
// parseAxialShdGradient 解析轴向渐变
// 入参: axialShd 轴向渐变节点, alpha 透明度, x X坐标, y Y坐标, pageH 页面高度, originX 原点X坐标, originY 原点Y坐标
// 返回: canvas.Gradient 渐变对象
func parseAxialShdGradient(axialShd *AxialShd, alpha *int, x, y, pageH, originX, originY float64) canvas.Gradient {
if axialShd == nil {
return nil
}
start := parseFloats(axialShd.StartPoint)
end := parseFloats(axialShd.EndPoint)
if len(start) < 2 || len(end) < 2 {
return nil
}
gradient := parseShdSegments(axialShd.Segment, alpha)
if gradient == nil {
return nil
}
startPoint := canvas.Point{X: x + start[0] - originX, Y: pageH - (y + start[1]) - originY}
endPoint := canvas.Point{X: x + end[0] - originX, Y: pageH - (y + end[1]) - originY}
if startPoint.Equals(endPoint) {
return nil
}
return gradient.ToLinear(startPoint, endPoint)
}
// parseRadialShdGradient 解析径向渐变
// 入参: radialShd 径向渐变节点, alpha 透明度, x X坐标, y Y坐标, pageH 页面高度, originX 原点X坐标, originY 原点Y坐标
// 返回: canvas.Gradient 渐变对象
func parseRadialShdGradient(radialShd *RadialShd, alpha *int, x, y, pageH, originX, originY float64) canvas.Gradient {
if radialShd == nil || radialShd.EndRadius <= 0 {
return nil
}
start := parseFloats(radialShd.StartPoint)
end := parseFloats(radialShd.EndPoint)
if len(start) < 2 || len(end) < 2 {
return nil
}
gradient := parseShdSegments(radialShd.Segment, alpha)
if gradient == nil {
return nil
}
startPoint := canvas.Point{X: x + start[0] - originX, Y: pageH - (y + start[1]) - originY}
endPoint := canvas.Point{X: x + end[0] - originX, Y: pageH - (y + end[1]) - originY}
return gradient.ToRadial(startPoint, radialShd.StartRadius, endPoint, radialShd.EndRadius)
}
// colorToRGBA 转换颜色对象
// 入参: c 颜色对象
// 返回: color.RGBA RGBA颜色
func colorToRGBA(c color.Color) color.RGBA {
if rgba, ok := c.(color.RGBA); ok {
return rgba
}
r, g, b, a := c.RGBA()
return color.RGBA{R: uint8(r >> 8), G: uint8(g >> 8), B: uint8(b >> 8), A: uint8(a >> 8)}
}
+95 -14
View File
@@ -158,7 +158,6 @@ func (r *Reader) Doc() (*Document, error) {
if doc.Signatures == "" {
doc.Signatures = docAttr.Signatures
}
r.OFD.DocBody[0].DocInfo.DocID = "loaded"
if doc.CommonData.DocumentRes != "" {
r.loadRes(doc.CommonData.DocumentRes)
}
@@ -177,7 +176,7 @@ func (r *Reader) loadRes(resPath string) {
if resPath == "" {
return
}
fullPath := path.Join(r.RootDir, resPath)
fullPath := r.ResPath(resPath)
data, err := r.readFile(fullPath)
if err != nil {
return
@@ -236,7 +235,7 @@ func resolveResourcePath(resPath, baseLoc, filePath string) string {
// 入参: page 页面对象
// 返回: *PageContent 页面内容, error 错误信息
func (r *Reader) PageContent(page Page) (*PageContent, error) {
fullPath := path.Join(r.RootDir, page.BaseLoc)
fullPath := r.ResPath(page.BaseLoc)
data, err := r.readFile(fullPath)
if err != nil {
return nil, err
@@ -261,7 +260,9 @@ func (r *Reader) ResPath(resLink string) string {
if resLink == "" {
return ""
}
resLink = strings.TrimPrefix(resLink, "/")
if strings.HasPrefix(resLink, "/") {
return cleanPackagePath(resLink)
}
resLink = path.Clean(resLink)
rootDir := strings.TrimPrefix(strings.ReplaceAll(r.RootDir, "\\", "/"), "/")
if rootDir != "" && (resLink == rootDir || strings.HasPrefix(resLink, rootDir+"/")) {
@@ -278,17 +279,19 @@ func (r *Reader) ResData(resLink string) ([]byte, error) {
return r.readFile(fullPath)
}
// DocRoots 获取所有文档根路径
// 返回: []string 路径列表
func (r *Reader) DocRoots() []string {
var roots []string
if r.OFD == nil {
return roots
// readDocumentPart 读取文档关联文件
// 入参: loc 文件位置, value 文档结构
// 返回: string 文件路径, error 错误信息
func (r *Reader) readDocumentPart(loc string, value any) (string, error) {
fullPath := r.ResPath(strings.TrimSpace(loc))
data, err := r.readFile(fullPath)
if err != nil {
return "", err
}
for _, body := range r.OFD.DocBody {
roots = append(roots, body.DocRoot)
if err := xml.Unmarshal(data, value); err != nil {
return "", fmt.Errorf("failed to unmarshal %s: %w", path.Base(fullPath), err)
}
return roots
return fullPath, nil
}
// Version 获取OFD版本号
@@ -338,6 +341,26 @@ func (r *Reader) Outlines() ([]OutlineElem, error) {
return doc.Outlines.OutlineElem, nil
}
// Actions 获取文档动作
// 返回: []Action 动作列表, error 错误信息
func (r *Reader) Actions() ([]Action, error) {
doc, err := r.Doc()
if err != nil {
return nil, err
}
return doc.Actions, nil
}
// Bookmarks 获取文档书签
// 返回: []Bookmark 书签列表, error 错误信息
func (r *Reader) Bookmarks() ([]Bookmark, error) {
doc, err := r.Doc()
if err != nil {
return nil, err
}
return doc.Bookmarks.Bookmark, nil
}
// Attachments 获取附件列表
// 返回: []Attachment 附件列表, error 错误信息
func (r *Reader) Attachments() ([]Attachment, error) {
@@ -345,9 +368,50 @@ func (r *Reader) Attachments() ([]Attachment, error) {
if err != nil {
return nil, err
}
if strings.TrimSpace(doc.Attachments.Path) != "" && doc.Attachments.Attachment == nil {
var attachments Attachments
partPath, err := r.readDocumentPart(doc.Attachments.Path, &attachments)
if err != nil {
return nil, err
}
for i := range attachments.Attachment {
attachment := &attachments.Attachment[i]
attachment.FileLoc = resolveResourcePath(partPath, "", attachment.FileLoc)
}
if attachments.Attachment == nil {
attachments.Attachment = []Attachment{}
}
doc.Attachments.Attachment = attachments.Attachment
}
return doc.Attachments.Attachment, nil
}
// CustomTags 获取自定义标引
// 返回: []CustomTag 自定义标引列表, error 错误信息
func (r *Reader) CustomTags() ([]CustomTag, error) {
doc, err := r.Doc()
if err != nil {
return nil, err
}
if strings.TrimSpace(doc.CustomTags.Path) != "" && doc.CustomTags.CustomTag == nil {
var customTags CustomTags
partPath, err := r.readDocumentPart(doc.CustomTags.Path, &customTags)
if err != nil {
return nil, err
}
for i := range customTags.CustomTag {
tag := &customTags.CustomTag[i]
tag.SchemaLoc = resolveResourcePath(partPath, "", tag.SchemaLoc)
tag.FileLoc = resolveResourcePath(partPath, "", tag.FileLoc)
}
if customTags.CustomTag == nil {
customTags.CustomTag = []CustomTag{}
}
doc.CustomTags.CustomTag = customTags.CustomTag
}
return doc.CustomTags.CustomTag, nil
}
// CustomDatas 获取自定义数据
// 返回: []CustomData 自定义数据列表, error 错误信息
func (r *Reader) CustomDatas() ([]CustomData, error) {
@@ -361,12 +425,29 @@ func (r *Reader) CustomDatas() ([]CustomData, error) {
return info.CustomDatas.CustomData, nil
}
// Extensions 获取分列扩展项
// Extensions 获取扩展项
// 返回: []Extension 扩展项列表, error 错误信息
func (r *Reader) Extensions() ([]Extension, error) {
doc, err := r.Doc()
if err != nil {
return nil, err
}
if strings.TrimSpace(doc.Extensions.Path) != "" && doc.Extensions.Extension == nil {
var extensions Extensions
partPath, err := r.readDocumentPart(doc.Extensions.Path, &extensions)
if err != nil {
return nil, err
}
for i := range extensions.Extension {
extension := &extensions.Extension[i]
for j := range extension.ExtendData {
extension.ExtendData[j] = resolveResourcePath(partPath, "", extension.ExtendData[j])
}
}
if extensions.Extension == nil {
extensions.Extension = []Extension{}
}
doc.Extensions.Extension = extensions.Extension
}
return doc.Extensions.Extension, nil
}
+23 -1501
View File
File diff suppressed because it is too large Load Diff
+386
View File
@@ -0,0 +1,386 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"bufio"
"image"
"image/color"
"io"
"github.com/tdewolff/canvas"
canvasimage "github.com/tdewolff/canvas/image"
"github.com/tdewolff/canvas/renderers/rasterizer"
"golang.org/x/image/draw"
)
// renderImage 渲染图片
// 入参: ctx 画布上下文, obj 图片对象, pageH 页面高度, parentCTM 父级CTM, boundaryInCTM 边界是否参与父级CTM, parentClip 父级裁剪路径
func (r *Renderer) renderImage(ctx *canvas.Context, obj ImageObject, pageH float64, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) {
if obj.Visible != nil && !*obj.Visible {
return
}
resPath, ok := r.Reader.ResMap[obj.ResourceID]
if !ok {
return
}
img, err := r.decodeImageResource(resPath)
if err != nil {
return
}
box, _ := ParseBox(obj.Boundary)
if maskPath, ok := r.Reader.ResMap[obj.ImageMask]; ok {
if mask, err := r.decodeImageResource(maskPath); err == nil {
img = imageWithMask(img, mask)
}
}
img = imageWithAlpha(img, obj.Alpha)
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}
}
objectCTM := ctm
if parentCTM != nil {
objectCTM = parentCTM.Multiply(ctm)
}
var m canvas.Matrix
if boundaryInCTM && parentCTM != nil {
x0 := box.X + ctm.c + ctm.e
y0 := box.Y + ctm.d + ctm.f
m = canvas.Matrix{
{(parentCTM.a*ctm.a + parentCTM.c*ctm.b) / imgW, -(parentCTM.a*ctm.c + parentCTM.c*ctm.d) / imgH, parentCTM.a*x0 + parentCTM.c*y0 + parentCTM.e},
{-(parentCTM.b*ctm.a + parentCTM.d*ctm.b) / imgW, (parentCTM.b*ctm.c + parentCTM.d*ctm.d) / imgH, pageH - (parentCTM.b*x0 + parentCTM.d*y0 + parentCTM.f)},
}
} else {
if parentCTM != nil {
ctm = parentCTM.Multiply(ctm)
}
m = canvas.Matrix{
{ctm.a / imgW, -ctm.c / imgH, box.X + ctm.c + ctm.e},
{-ctm.b / imgW, ctm.d / imgH, pageH - box.Y - ctm.d - ctm.f},
}
}
clipPath := intersectClipPath(parentClip, r.buildClipPath(obj.Clips, pageH, box.X, box.Y, objectCTM))
img = imageWithClip(img, clipPath, m)
img, pad := imageWithTransparentEdge(img)
if pad > 0 {
p := float64(pad)
m[0][2] -= m[0][0]*p + m[0][1]*p
m[1][2] -= m[1][0]*p + m[1][1]*p
}
ctx.RenderImage(img, ctx.CoordSystemView().Mul(ctx.View()).Mul(m))
}
// imageWithMask 应用图片蒙版
// 入参: img 图片对象, mask 蒙版图片
// 返回: image.Image 蒙版处理后的图片对象
func imageWithMask(img, mask image.Image) image.Image {
if img == nil || mask == nil {
return img
}
bounds := img.Bounds()
if bounds.Empty() || mask.Bounds().Empty() {
return img
}
source := imagePixelSource(img)
maskSource := imagePixelSource(mask)
maskBounds := maskSource.Bounds()
var opacity image.Image = maskSource
if bounds.Size() != maskBounds.Size() {
resized := image.NewGray(bounds)
draw.CatmullRom.Scale(resized, bounds, maskSource, maskBounds, draw.Src, nil)
opacity = resized
maskBounds = bounds
}
out := image.NewNRGBA(bounds)
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
mx := maskBounds.Min.X + x - bounds.Min.X
my := maskBounds.Min.Y + y - bounds.Min.Y
a := color.GrayModel.Convert(opacity.At(mx, my)).(color.Gray).Y
c := imageNRGBAAt(source, x, y)
c.A = uint8(int(c.A) * int(a) / 255)
out.SetNRGBA(x, y, c)
}
}
return out
}
// imageWithClip 应用图片裁剪区域
// 入参: img 图片对象, clipPath 裁剪路径, m 图片变换矩阵
// 返回: image.Image 裁剪后的图片对象
func imageWithClip(img image.Image, clipPath *canvas.Path, m canvas.Matrix) image.Image {
if img == nil || clipPath == nil || m.Det() == 0 {
return img
}
bounds := img.Bounds()
w, h := bounds.Dx(), bounds.Dy()
if w == 0 || h == 0 {
return img
}
p0 := m.Dot(canvas.Point{})
p1 := m.Dot(canvas.Point{X: float64(w)})
p2 := m.Dot(canvas.Point{X: float64(w), Y: float64(h)})
p3 := m.Dot(canvas.Point{Y: float64(h)})
imagePath := &canvas.Path{}
imagePath.MoveTo(p0.X, p0.Y)
imagePath.LineTo(p1.X, p1.Y)
imagePath.LineTo(p2.X, p2.Y)
imagePath.LineTo(p3.X, p3.Y)
imagePath.Close()
if rect, ok := rectangularPath(clipPath); ok {
if rect.Contains(imagePath.FastBounds()) {
return img
}
} else if clipPath.Contains(imagePath) {
return img
}
clip := clipPath.Copy().Transform(m.Inv())
clipCanvas := canvas.New(float64(w), float64(h))
ctx := canvas.NewContext(clipCanvas)
ctx.SetFillColor(canvas.White)
ctx.SetStrokeColor(canvas.Transparent)
ctx.DrawPath(0, 0, clip)
mask := rasterizer.Draw(clipCanvas, canvas.DPMM(1), canvas.DefaultColorSpace)
opaque := true
for y := 0; y < h && opaque; y++ {
for x := 0; x < w; x++ {
if mask.RGBAAt(x, y).A != 255 {
opaque = false
break
}
}
}
if opaque {
return img
}
source := imagePixelSource(img)
out := image.NewNRGBA(bounds)
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
c := imageNRGBAAt(source, x, y)
a := mask.RGBAAt(x-bounds.Min.X, y-bounds.Min.Y).A
c.A = uint8(int(c.A) * int(a) / 255)
out.SetNRGBA(x, y, c)
}
}
return out
}
// decodeImageResource 解码图片资源
// 入参: resPath 图片资源路径
// 返回: image.Image 图片对象, error 错误信息
func (r *Renderer) decodeImageResource(resPath string) (image.Image, error) {
rc, err := r.Reader.openFile(r.Reader.ResPath(resPath))
if err != nil {
return nil, err
}
defer rc.Close()
reader := bufio.NewReaderSize(rc, 8)
header, _ := reader.Peek(8)
if isJPEGData(header) {
img, err := canvasimage.NewJPEGImage(reader)
if err != nil {
return nil, err
}
if r.decodeImages {
return img.Image()
}
return img, nil
}
if isPNGData(header) {
img, err := canvasimage.NewPNGImage(reader)
if err != nil {
return nil, err
}
if r.decodeImages {
return img.Image()
}
return img, nil
}
data, err := io.ReadAll(reader)
if err != nil {
return nil, err
}
img, _, err := decodeImageData(data)
return img, err
}
// imageWithAlpha 合并图片透明度
// 入参: img 图片对象, alpha 对象透明度
// 返回: image.Image 合并后的图片对象
func imageWithAlpha(img image.Image, alpha *int) image.Image {
if img == nil || alpha == nil {
return img
}
a := clampColor(*alpha)
if a == 255 {
return img
}
bounds := img.Bounds()
out := image.NewNRGBA(bounds)
source := imagePixelSource(img)
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
c := imageNRGBAAt(source, x, y)
c.A = uint8(int(c.A) * a / 255)
out.SetNRGBA(x, y, c)
}
}
return out
}
// imageWithTransparentEdge 补齐透明图片边缘颜色
// 入参: img 图片对象
// 返回: image.Image 补齐后的图片对象, int 补齐像素数
func imageWithTransparentEdge(img image.Image) (image.Image, int) {
if img == nil {
return img, 0
}
bounds := img.Bounds()
w, h := bounds.Dx(), bounds.Dy()
if w == 0 || h == 0 {
return img, 0
}
if src, ok := img.(*canvasimage.Image); ok && src.Mimetype == "image/jpeg" && src.Mask == nil {
return img, 0
}
source := imagePixelSource(img)
if opaque, ok := source.(interface{ Opaque() bool }); ok && opaque.Opaque() {
return img, 0
}
hasZero, hasVisible := false, false
src, ok := source.(*image.NRGBA)
if ok {
srcBounds := src.Bounds()
for y := srcBounds.Min.Y; y < srcBounds.Max.Y; y++ {
offset := src.PixOffset(srcBounds.Min.X, y) + 3
for x := 0; x < w; x++ {
if src.Pix[offset] == 0 {
hasZero = true
} else {
hasVisible = true
}
offset += 4
}
}
} else {
src = image.NewNRGBA(image.Rect(0, 0, w, h))
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
c := imageNRGBAAt(source, bounds.Min.X+x, bounds.Min.Y+y)
src.SetNRGBA(x, y, c)
if c.A == 0 {
hasZero = true
} else {
hasVisible = true
}
}
}
}
if !hasZero || !hasVisible {
return img, 0
}
srcBounds := src.Bounds()
out := image.NewNRGBA(image.Rect(0, 0, w+2, h+2))
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
sx, sy := srcBounds.Min.X+x, srcBounds.Min.Y+y
c := src.NRGBAAt(sx, sy)
if c.A == 0 {
if edge, ok := transparentEdgeColor(src, sx, sy); ok {
c = edge
}
}
out.SetNRGBA(x+1, y+1, c)
}
}
for x := 0; x < w; x++ {
out.SetNRGBA(x+1, 0, transparentPaddingColor(out.NRGBAAt(x+1, 1)))
out.SetNRGBA(x+1, h+1, transparentPaddingColor(out.NRGBAAt(x+1, h)))
}
for y := 0; y < h; y++ {
out.SetNRGBA(0, y+1, transparentPaddingColor(out.NRGBAAt(1, y+1)))
out.SetNRGBA(w+1, y+1, transparentPaddingColor(out.NRGBAAt(w, y+1)))
}
out.SetNRGBA(0, 0, transparentPaddingColor(out.NRGBAAt(1, 1)))
out.SetNRGBA(w+1, 0, transparentPaddingColor(out.NRGBAAt(w, 1)))
out.SetNRGBA(0, h+1, transparentPaddingColor(out.NRGBAAt(1, h)))
out.SetNRGBA(w+1, h+1, transparentPaddingColor(out.NRGBAAt(w, h)))
return out, 1
}
// imagePixelSource 获取图片像素源
// 入参: img 图片对象
// 返回: image.Image 图片像素源
func imagePixelSource(img image.Image) image.Image {
if src, ok := img.(*canvasimage.Image); ok {
if decoded, err := src.Image(); err == nil {
return decoded
}
}
return img
}
// imageNRGBAAt 获取图片NRGBA像素
// 入参: img 图片对象, x X坐标, y Y坐标
// 返回: color.NRGBA NRGBA像素
func imageNRGBAAt(img image.Image, x, y int) color.NRGBA {
if src, ok := img.(*image.NRGBA); ok {
return src.NRGBAAt(x, y)
}
return color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA)
}
// transparentEdgeColor 获取透明像素相邻的可见颜色
// 入参: img 图片对象, x X坐标, y Y坐标
// 返回: color.NRGBA 颜色, bool 是否存在
func transparentEdgeColor(img *image.NRGBA, x, y int) (color.NRGBA, bool) {
bounds := img.Bounds()
var best color.NRGBA
for dy := -1; dy <= 1; dy++ {
for dx := -1; dx <= 1; dx++ {
if dx == 0 && dy == 0 {
continue
}
nx, ny := x+dx, y+dy
if nx < bounds.Min.X || nx >= bounds.Max.X || ny < bounds.Min.Y || ny >= bounds.Max.Y {
continue
}
c := img.NRGBAAt(nx, ny)
if c.A > best.A {
best = c
}
}
}
if best.A == 0 {
return color.NRGBA{}, false
}
best.A = 1
return best, true
}
// transparentPaddingColor 获取透明补齐颜色
// 入参: c 边缘颜色
// 返回: color.NRGBA 补齐颜色
func transparentPaddingColor(c color.NRGBA) color.NRGBA {
c.A = 1
return c
}
+254
View File
@@ -0,0 +1,254 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"image/color"
"github.com/tdewolff/canvas"
)
// renderAnnotations 渲染页面注释外观
// 入参: ctx 画布上下文, pageID 页面ID, pageH 页面高度
func (r *Renderer) renderAnnotations(ctx *canvas.Context, pageID string, pageH float64) {
for _, annot := range r.Reader.Annots[pageID] {
if len(annot.Appearance.Objects) == 0 {
continue
}
box, _ := ParseBox(annot.Appearance.Boundary)
ctm := Matrix{a: 1, d: 1, e: box.X, f: box.Y}
for _, obj := range annot.Appearance.Objects {
r.renderObject(ctx, obj, pageH, nil, nil, 0, &ctm, false, nil)
}
}
}
// renderTemplate 渲染模板
// 入参: ctx 画布上下文, templateID 模板ID, pageH 页面高度
func (r *Renderer) renderTemplate(ctx *canvas.Context, templateID string, pageH float64) {
tplContent := r.templatePageCache[templateID]
if tplContent == nil {
var tplPage *TemplatePage
for i := range r.Reader.doc.CommonData.TemplatePage {
if r.Reader.doc.CommonData.TemplatePage[i].ID == templateID {
tplPage = &r.Reader.doc.CommonData.TemplatePage[i]
break
}
}
if tplPage == nil {
return
}
var err error
tplContent, err = r.Reader.PageContent(Page{BaseLoc: tplPage.BaseLoc})
if err != nil {
return
}
r.templatePageCache[templateID] = tplContent
}
if tplContent.Content.Layer != nil {
for _, layer := range tplContent.Content.Layer {
r.renderLayer(ctx, layer, pageH, nil, nil, 0, nil)
}
}
}
// renderLayer 渲染图层
// 入参: ctx 画布上下文, layer 图层对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽, parentCTM 父级CTM
func (r *Renderer) renderLayer(ctx *canvas.Context, layer Layer, pageH float64, defaultFill, defaultStroke color.Color, defaultLW float64, parentCTM *Matrix) {
defaultFill, defaultStroke, defaultLW = r.drawParamDefaults(layer.DrawParam, defaultFill, defaultStroke, defaultLW)
if len(layer.Objects) > 0 {
for _, obj := range layer.Objects {
r.renderObject(ctx, obj, pageH, defaultFill, defaultStroke, defaultLW, parentCTM, false, nil)
}
return
}
for _, textObj := range layer.TextObject {
r.renderText(ctx, textObj, pageH, defaultFill, defaultStroke, parentCTM, false, nil)
}
for _, pathObj := range layer.PathObject {
r.renderPath(ctx, pathObj, pageH, defaultFill, defaultStroke, defaultLW, parentCTM, false, nil)
}
for _, imgObj := range layer.ImageObject {
r.renderImage(ctx, imgObj, pageH, parentCTM, false, nil)
}
for _, cgu := range layer.CompositeGraphicUnit {
r.renderCompositeGraphicUnit(ctx, cgu, pageH, defaultFill, defaultStroke, defaultLW, parentCTM, false, nil)
}
}
// renderCompositeGraphicUnit 渲染复合图元
// 入参: ctx 画布上下文, cgu 复合图元对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽, parentCTM 父级CTM, boundaryInCTM 边界是否参与CTM变换, parentClip 父级裁剪路径
func (r *Renderer) renderCompositeGraphicUnit(ctx *canvas.Context, cgu CompositeGraphicUnit, pageH float64, defaultFill, defaultStroke color.Color, defaultLW float64, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) {
if cgu.Visible != nil && !*cgu.Visible {
return
}
ctx.Push()
currentCTM := NewMatrix(cgu.CTM)
if parentCTM != nil {
currentCTM = parentCTM.Multiply(currentCTM)
}
box, _ := ParseBox(cgu.Boundary)
clipPath := intersectClipPath(parentClip, r.buildClipPath(cgu.Clips, pageH, box.X, box.Y, currentCTM))
if cgu.ResourceID != "" {
if ref, ok := r.CompositeGraphicUnits[cgu.ResourceID]; ok {
refCopy := *ref
refCopy.Alpha = mergeAlpha(refCopy.Alpha, cgu.Alpha)
r.renderCompositeGraphicUnit(ctx, refCopy, pageH, defaultFill, defaultStroke, defaultLW, &currentCTM, true, clipPath)
}
}
defaultFill, defaultStroke, defaultLW = r.drawParamDefaults(cgu.DrawParam, defaultFill, defaultStroke, defaultLW)
if len(cgu.Objects) > 0 {
for _, obj := range cgu.Objects {
obj = mergeGraphicObjectAlpha(obj, cgu.Alpha)
r.renderObject(ctx, obj, pageH, defaultFill, defaultStroke, defaultLW, &currentCTM, boundaryInCTM, clipPath)
}
ctx.Pop()
return
}
for _, imgObj := range cgu.ImageObject {
imgObj.Alpha = mergeAlpha(imgObj.Alpha, cgu.Alpha)
r.renderImage(ctx, imgObj, pageH, &currentCTM, boundaryInCTM, clipPath)
}
for _, pathObj := range cgu.PathObject {
pathObj.Alpha = mergeAlpha(pathObj.Alpha, cgu.Alpha)
r.renderPath(ctx, pathObj, pageH, defaultFill, defaultStroke, defaultLW, &currentCTM, boundaryInCTM, clipPath)
}
for _, textObj := range cgu.TextObject {
textObj.Alpha = mergeAlpha(textObj.Alpha, cgu.Alpha)
r.renderText(ctx, textObj, pageH, defaultFill, defaultStroke, &currentCTM, boundaryInCTM, clipPath)
}
for _, subCgu := range cgu.CompositeGraphicUnit {
subCgu.Alpha = mergeAlpha(subCgu.Alpha, cgu.Alpha)
r.renderCompositeGraphicUnit(ctx, subCgu, pageH, defaultFill, defaultStroke, defaultLW, &currentCTM, boundaryInCTM, clipPath)
}
ctx.Pop()
}
// renderObject 渲染图形对象
// 入参: ctx 画布上下文, obj 图形对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽, parentCTM 父级CTM, boundaryInCTM 边界是否参与CTM变换, parentClip 父级裁剪路径
func (r *Renderer) renderObject(ctx *canvas.Context, obj GraphicObject, pageH float64, defaultFill, defaultStroke color.Color, defaultLW float64, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) {
switch obj.Type {
case "TextObject":
r.renderText(ctx, obj.TextObject, pageH, defaultFill, defaultStroke, parentCTM, boundaryInCTM, parentClip)
case "PathObject":
r.renderPath(ctx, obj.PathObject, pageH, defaultFill, defaultStroke, defaultLW, parentCTM, boundaryInCTM, parentClip)
case "ImageObject":
r.renderImage(ctx, obj.ImageObject, pageH, parentCTM, boundaryInCTM, parentClip)
case "CompositeGraphicUnit", "CompositeObject":
r.renderCompositeGraphicUnit(ctx, obj.CompositeGraphicUnit, pageH, defaultFill, defaultStroke, defaultLW, parentCTM, boundaryInCTM, parentClip)
}
}
// mergeGraphicObjectAlpha 合并图形对象透明度
// 入参: obj 图形对象, alpha 父级透明度
// 返回: GraphicObject 合并后的图形对象
func mergeGraphicObjectAlpha(obj GraphicObject, alpha *int) GraphicObject {
if alpha == nil {
return obj
}
switch obj.Type {
case "TextObject":
obj.TextObject.Alpha = mergeAlpha(obj.TextObject.Alpha, alpha)
case "PathObject":
obj.PathObject.Alpha = mergeAlpha(obj.PathObject.Alpha, alpha)
case "ImageObject":
obj.ImageObject.Alpha = mergeAlpha(obj.ImageObject.Alpha, alpha)
case "CompositeGraphicUnit", "CompositeObject":
obj.CompositeGraphicUnit.Alpha = mergeAlpha(obj.CompositeGraphicUnit.Alpha, alpha)
}
return obj
}
// drawParamDefaults 合并绘制参数默认样式
// 入参: id 绘制参数ID, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽
// 返回: color.Color 默认填充色, color.Color 默认描边色, float64 默认线宽
func (r *Renderer) drawParamDefaults(id string, defaultFill, defaultStroke color.Color, defaultLW float64) (color.Color, color.Color, float64) {
if id == "" {
return defaultFill, defaultStroke, defaultLW
}
dp := r.getDrawParam(id, nil)
if dp == nil {
return defaultFill, defaultStroke, defaultLW
}
if dp.LineWidth > 0 {
defaultLW = dp.LineWidth
}
if dp.FillColor != nil {
defaultFill = parseFillColor(dp.FillColor)
}
if dp.StrokeColor != nil {
defaultStroke = parseStrokeColor(dp.StrokeColor)
}
return defaultFill, defaultStroke, defaultLW
}
// 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.Join != "" {
merged.Join = dp.Join
}
if dp.Cap != "" {
merged.Cap = dp.Cap
}
if dp.DashPattern != "" {
merged.DashPattern = dp.DashPattern
merged.DashOffset = dp.DashOffset
}
if dp.MiterLimit > 0 {
merged.MiterLimit = dp.MiterLimit
}
if dp.FillColor != nil {
merged.FillColor = dp.FillColor
}
if dp.StrokeColor != nil {
merged.StrokeColor = dp.StrokeColor
}
if dp.Font != "" {
merged.Font = dp.Font
}
if dp.Size > 0 {
merged.Size = dp.Size
}
if dp.Weight > 0 {
merged.Weight = dp.Weight
}
if dp.Italic {
merged.Italic = dp.Italic
}
return &merged
}
return dp
}
return nil
}
+146
View File
@@ -0,0 +1,146 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"bytes"
"fmt"
"image"
"io"
"github.com/tdewolff/canvas"
"github.com/tdewolff/canvas/renderers"
"github.com/tdewolff/canvas/renderers/pdf"
"github.com/tdewolff/canvas/renderers/rasterizer"
)
// RenderToImage 渲染为光栅图
// 入参: page 页面内容
// 返回: image.Image 图像对象, error 错误信息
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 输出流
// 返回: error 错误信息
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())
}
// replacePDFProducer 替换PDF的Producer属性
// 入参: data PDF字节数据
// 返回: []byte 替换后的PDF字节数据
func replacePDFProducer(data []byte) []byte {
old := []byte("/Producer(tdewolff/canvas)")
idx := bytes.LastIndex(data, old)
if idx < 0 {
return data
}
dst := []byte("/Producer(xiaoqidun/ofdgo)")
copy(data[idx:idx+len(old)], dst)
return data
}
// RenderToPDF 渲染为PDF
// 入参: page 页面内容, writer 输出流
// 返回: error 错误信息
func (r *Renderer) RenderToPDF(page *PageContent, writer io.Writer) error {
c, err := r.renderPage(page)
if err != nil {
return err
}
box, err := r.GetPageBox(page)
if err != nil {
return err
}
pages := []pdfPage{{Content: page, Box: box}}
navigation := newPDFNavigation(r, r.Reader.doc, pages)
var buf bytes.Buffer
p := pdf.New(&buf, c.W, c.H, nil)
p.SetInfo("", "", "", "", "xiaoqidun/ofdgo")
navigation.apply(p, 0)
c.RenderTo(p)
if err := p.Close(); err != nil {
return err
}
_, err = writer.Write(replacePDFProducer(buf.Bytes()))
return err
}
// RenderToEPS 渲染为EPS
// 入参: page 页面内容, writer 输出流
// 返回: error 错误信息
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")
}
pages := make([]pdfPage, len(doc.Pages.Page))
for i, pageRef := range doc.Pages.Page {
page, err := r.Reader.PageContent(pageRef)
if err != nil {
return fmt.Errorf("failed to read page %d: %w", i+1, err)
}
box, err := r.GetPageBox(page)
if err != nil {
return fmt.Errorf("failed to read page %d area: %w", i+1, err)
}
pages[i] = pdfPage{Content: page, Box: box}
}
navigation := newPDFNavigation(r, doc, pages)
var buf bytes.Buffer
p := pdf.New(&buf, pages[0].Box.W, pages[0].Box.H, nil)
p.SetInfo("", "", "", "", "xiaoqidun/ofdgo")
for i, page := range pages {
c, err := r.renderPage(page.Content)
if err != nil {
return fmt.Errorf("failed to render page %d: %w", i+1, err)
}
if i > 0 {
p.NewPage(c.W, c.H)
}
navigation.apply(p, i)
c.RenderTo(p)
}
if err := p.Close(); err != nil {
return err
}
_, err = writer.Write(replacePDFProducer(buf.Bytes()))
return err
}
+591
View File
@@ -0,0 +1,591 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"image/color"
"math"
"strconv"
"strings"
"github.com/tdewolff/canvas"
)
const defaultPathLineWidth = 0.353
type pathStyle struct {
fillColor color.Color
strokeColor color.Color
fillPaint any
strokePaint any
fillPattern *Pattern
fillPatternColor color.Color
lineWidth float64
lineCap canvas.Capper
lineJoin canvas.Joiner
dashOffset float64
dashPattern []float64
}
// newPathStyle 创建路径样式
// 入参: defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽, alpha 对象透明度
// 返回: pathStyle 路径样式
func newPathStyle(defaultFill, defaultStroke color.Color, defaultLW float64, alpha *int) pathStyle {
style := pathStyle{
fillColor: colorWithAlpha(defaultFill, alpha),
strokeColor: colorWithAlpha(defaultStroke, alpha),
lineWidth: defaultLW,
lineCap: canvas.ButtCap,
lineJoin: canvas.MiterJoin,
}
if style.lineWidth == 0 {
style.lineWidth = defaultPathLineWidth
}
style.fillPaint = style.fillColor
style.strokePaint = style.strokeColor
return style
}
// applyFillColor 应用填充颜色
// 入参: fill 填充颜色, bx 边界X坐标, by 边界Y坐标, pageH 页面高度, alpha 对象透明度
func (s *pathStyle) applyFillColor(fill *FillColor, bx, by, pageH float64, alpha *int) {
fillColorNode := withFillAlpha(fill, alpha)
s.fillPattern = fillColorNode.Pattern
s.fillPatternColor = patternColor(fillColorNode)
s.fillColor = parseFillColor(fillColorNode)
s.fillPaint = parseFillPaint(fillColorNode, bx, by, pageH, 0, 0)
}
// applyStrokeColor 应用描边颜色
// 入参: stroke 描边颜色, bx 边界X坐标, by 边界Y坐标, pageH 页面高度, alpha 对象透明度
func (s *pathStyle) applyStrokeColor(stroke *StrokeColor, bx, by, pageH float64, alpha *int) {
strokeColorNode := withStrokeAlpha(stroke, alpha)
s.strokeColor = parseStrokeColor(strokeColorNode)
s.strokePaint = parseStrokePaint(strokeColorNode, bx, by, pageH, 0, 0)
}
// pathLineCap 转换线帽样式
// 入参: cap 线帽名称, fallback 默认线帽
// 返回: canvas.Capper 线帽样式
func pathLineCap(cap string, fallback canvas.Capper) canvas.Capper {
switch cap {
case "Round":
return canvas.RoundCap
case "Square":
return canvas.SquareCap
}
return fallback
}
// pathLineJoin 转换线连接样式
// 入参: join 线连接名称, fallback 默认线连接
// 返回: canvas.Joiner 线连接样式
func pathLineJoin(join string, fallback canvas.Joiner) canvas.Joiner {
switch join {
case "Round":
return canvas.RoundJoin
case "Bevel":
return canvas.BevelJoin
}
return fallback
}
// applyDrawParam 应用绘制参数样式
// 入参: dp 绘制参数, bx 边界X坐标, by 边界Y坐标, pageH 页面高度, alpha 对象透明度
func (s *pathStyle) applyDrawParam(dp *DrawParam, bx, by, pageH float64, alpha *int) {
if dp.LineWidth > 0 {
s.lineWidth = dp.LineWidth
}
if dp.FillColor != nil {
s.applyFillColor(dp.FillColor, bx, by, pageH, alpha)
}
if dp.StrokeColor != nil {
s.applyStrokeColor(dp.StrokeColor, bx, by, pageH, alpha)
}
if dp.Cap != "" {
s.lineCap = pathLineCap(dp.Cap, s.lineCap)
}
if dp.Join != "" {
s.lineJoin = pathLineJoin(dp.Join, s.lineJoin)
}
if dp.DashPattern != "" {
s.dashPattern = parseFloats(dp.DashPattern)
s.dashOffset = dp.DashOffset
}
}
// applyPathObject 应用路径对象样式
// 入参: obj 路径对象, bx 边界X坐标, by 边界Y坐标, pageH 页面高度
func (s *pathStyle) applyPathObject(obj PathObject, bx, by, pageH float64) {
if obj.LineWidth > 0 {
s.lineWidth = obj.LineWidth
}
if obj.FillColor != nil {
s.applyFillColor(obj.FillColor, bx, by, pageH, obj.Alpha)
}
if obj.StrokeColor != nil {
s.applyStrokeColor(obj.StrokeColor, bx, by, pageH, obj.Alpha)
}
if obj.Cap != "" {
s.lineCap = pathLineCap(obj.Cap, canvas.ButtCap)
}
if obj.Join != "" {
s.lineJoin = pathLineJoin(obj.Join, canvas.MiterJoin)
}
if obj.DashPattern != "" {
s.dashPattern = parseFloats(obj.DashPattern)
s.dashOffset = obj.DashOffset
}
}
// scale 应用路径变换缩放
// 入参: ctm 变换矩阵
func (s *pathStyle) scale(ctm Matrix) {
if scale := math.Sqrt(math.Abs(ctm.a*ctm.d - ctm.b*ctm.c)); scale > 0 {
s.lineWidth *= scale
s.dashOffset *= scale
for i := range s.dashPattern {
s.dashPattern[i] *= scale
}
}
}
// renderPath 渲染路径
// 入参: ctx 画布上下文, obj 路径对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽, parentCTM 父级CTM, boundaryInCTM 边界是否参与CTM变换, parentClip 父级裁剪路径
func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64, defaultFill, defaultStroke color.Color, defaultLW float64, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) {
if obj.Visible != nil && !*obj.Visible {
return
}
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)
if parentCTM != nil {
ctm = parentCTM.Multiply(ctm)
}
style := newPathStyle(defaultFill, defaultStroke, defaultLW, obj.Alpha)
if obj.DrawParam != "" {
if dp := r.getDrawParam(obj.DrawParam, nil); dp != nil {
style.applyDrawParam(dp, bx, by, pageH, obj.Alpha)
}
}
style.applyPathObject(obj, bx, by, pageH)
style.scale(ctm)
p := r.buildPath(obj, pageH, ctm, boundaryInCTM)
if rectPath := r.buildTinyFillRectPath(obj, pageH, ctm, bx, by); rectPath != nil {
p = rectPath
}
clipPath := intersectClipPath(parentClip, r.buildClipPath(obj.Clips, pageH, bx, by, ctm))
shouldFill := false
if obj.Fill != nil {
shouldFill = *obj.Fill
}
if style.fillPaint == nil {
style.fillPaint = style.fillColor
}
if shouldFill && style.fillPattern != nil {
fp := p
if clipPath != nil {
fp = p.Copy()
fp.Close()
fp = applyClipPath(fp, clipPath)
}
r.renderPattern(ctx, style.fillPattern, style.fillPatternColor, pageH, fp, ctm, bx, by)
} else if shouldFill && style.fillPaint != nil {
ctx.SetFill(style.fillPaint)
ctx.SetStrokeColor(canvas.Transparent)
fp := p
if clipPath != nil {
fp = p.Copy()
fp.Close()
fp = applyClipPath(fp, clipPath)
}
ctx.DrawPath(0, 0, fp)
}
shouldStroke := true
if obj.Stroke != nil {
shouldStroke = *obj.Stroke
}
if shouldStroke {
if style.strokePaint == nil {
style.strokePaint = style.strokeColor
}
if style.strokePaint == nil {
style.strokePaint = colorWithAlpha(canvas.Black, obj.Alpha)
}
ctx.SetFillColor(canvas.Transparent)
ctx.SetStroke(style.strokePaint)
ctx.SetStrokeWidth(style.lineWidth)
ctx.SetStrokeCapper(style.lineCap)
ctx.SetStrokeJoiner(style.lineJoin)
if len(style.dashPattern) > 0 {
ctx.SetDashes(style.dashOffset, style.dashPattern...)
}
if clipPath != nil {
sp := p.Copy()
if len(style.dashPattern) > 0 {
sp = sp.Dash(style.dashOffset, style.dashPattern...)
}
sp = sp.Stroke(style.lineWidth, style.lineCap, style.lineJoin, canvas.Tolerance)
sp = applyClipPath(sp, clipPath)
ctx.SetFill(style.strokePaint)
ctx.SetStrokeColor(canvas.Transparent)
ctx.DrawPath(0, 0, sp)
} else {
ctx.DrawPath(0, 0, p)
}
}
ctx.Pop()
}
// renderPattern 渲染图案填充
// 入参: ctx 画布上下文, pattern 图案对象, defaultColor 默认颜色, pageH 页面高度, clip 填充区域, parentCTM 父级CTM, bx 边界X坐标, by 边界Y坐标
func (r *Renderer) renderPattern(ctx *canvas.Context, pattern *Pattern, defaultColor color.Color, pageH float64, clip *canvas.Path, parentCTM Matrix, bx, by float64) {
if pattern == nil || clip == nil || len(pattern.CellContent.Objects) == 0 {
return
}
xStep, yStep := pattern.XStep, pattern.YStep
if xStep == 0 {
xStep = pattern.Width
}
if yStep == 0 {
yStep = pattern.Height
}
if xStep <= 0 || yStep <= 0 {
return
}
patternCTM := TranslationMatrix(bx, by).Multiply(parentCTM).Multiply(NewMatrix(pattern.CTM))
invCTM, ok := patternCTM.Invert()
if !ok {
return
}
bounds := clip.FastBounds()
points := [][2]float64{
{bounds.X0, pageH - bounds.Y0},
{bounds.X1, pageH - bounds.Y0},
{bounds.X1, pageH - bounds.Y1},
{bounds.X0, pageH - bounds.Y1},
}
minX, maxX := 0.0, 0.0
minY, maxY := 0.0, 0.0
for i, point := range points {
x, y := invCTM.Transform(point[0], point[1])
if i == 0 {
minX, maxX = x, x
minY, maxY = y, y
continue
}
minX = math.Min(minX, x)
maxX = math.Max(maxX, x)
minY = math.Min(minY, y)
maxY = math.Max(maxY, y)
}
startX := int(math.Floor(minX/xStep)) - 1
endX := int(math.Ceil(maxX/xStep)) + 1
startY := int(math.Floor(minY/yStep)) - 1
endY := int(math.Ceil(maxY/yStep)) + 1
for ix := startX; ix <= endX; ix++ {
for iy := startY; iy <= endY; iy++ {
tileCTM := patternCTM.Multiply(TranslationMatrix(float64(ix)*xStep, float64(iy)*yStep))
for _, obj := range pattern.CellContent.Objects {
r.renderObject(ctx, obj, pageH, defaultColor, defaultColor, 0, &tileCTM, false, clip)
}
}
}
}
// buildPath 解析路径并返回Canvas Path
// 入参: obj 路径对象, pageH 页面高度, ctm 变换矩阵, boundaryInCTM 边界是否参与CTM变换
// 返回: *canvas.Path 路径对象
func (r *Renderer) buildPath(obj PathObject, pageH float64, ctm Matrix, boundaryInCTM bool) *canvas.Path {
bx, by := 0.0, 0.0
if obj.Boundary != "" {
if box, err := ParseBox(obj.Boundary); err == nil {
bx, by = box.X, box.Y
}
}
point := func(x, y float64) (float64, float64) {
if boundaryInCTM {
tx, ty := ctm.Transform(x+bx, y+by)
return tx, pageH - ty
}
tx, ty := ctm.Transform(x, y)
return tx + bx, pageH - (ty + by)
}
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 := point(x, y)
p.MoveTo(tx, ty)
i += 2
}
case "L":
if i+1 < len(tokens) {
x, _ := strconv.ParseFloat(tokens[i], 64)
y, _ := strconv.ParseFloat(tokens[i+1], 64)
tx, ty := point(x, y)
p.LineTo(tx, ty)
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 := point(x1, y1)
tx2, ty2 := point(x2, y2)
tx3, ty3 := point(x3, y3)
p.CubeTo(tx1, ty1, tx2, ty2, tx3, ty3)
i += 6
}
case "Q":
if i+3 < 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)
tx1, ty1 := point(x1, y1)
tx2, ty2 := point(x2, y2)
p.QuadTo(tx1, ty1, tx2, ty2)
i += 4
}
case "A":
if i+6 < len(tokens) {
rx, _ := strconv.ParseFloat(tokens[i], 64)
ry, _ := strconv.ParseFloat(tokens[i+1], 64)
rot, _ := strconv.ParseFloat(tokens[i+2], 64)
large, _ := strconv.ParseBool(tokens[i+3])
sweep, _ := strconv.ParseBool(tokens[i+4])
x, _ := strconv.ParseFloat(tokens[i+5], 64)
y, _ := strconv.ParseFloat(tokens[i+6], 64)
sx := math.Hypot(ctm.a, ctm.c)
sy := math.Hypot(ctm.b, ctm.d)
ctmRot := math.Atan2(ctm.b, ctm.a) * 180 / math.Pi
tx, ty := point(x, y)
sweep = !sweep
p.ArcTo(rx*sx, ry*sy, -(rot + ctmRot), large, sweep, tx, ty)
i += 7
}
case "C":
p.Close()
}
}
return p
}
// buildClipPath 构建裁剪路径
// 入参: clips 裁剪对象, pageH 页面高度, bx 边界X坐标, by 边界Y坐标, objectCTM 对象CTM
// 返回: *canvas.Path 路径对象
func (r *Renderer) buildClipPath(clips *Clips, pageH float64, bx, by float64, objectCTM Matrix) *canvas.Path {
if clips == nil {
return nil
}
var p *canvas.Path
for _, clip := range clips.Clip {
var clipPath *canvas.Path
for _, area := range clip.Area {
areaCTM := NewMatrix(area.CTM)
if clips.TransFlag == nil || *clips.TransFlag {
areaCTM = objectCTM.Multiply(areaCTM)
}
for _, pathObj := range area.Path {
ctm := areaCTM.Multiply(NewMatrix(pathObj.CTM))
cp := r.buildPath(pathObj, pageH, ctm, true)
cp.Translate(bx, -by)
cp.Close()
if clipPath == nil {
clipPath = cp
} else {
clipPath = unionClipPath(clipPath, cp)
}
}
}
if clipPath != nil {
if p == nil {
p = clipPath
} else {
p = intersectClipPath(p, clipPath)
}
}
}
return p
}
// intersectClipPath 求裁剪路径交集
// 入参: parent 父级裁剪路径, current 当前裁剪路径
// 返回: *canvas.Path 相交后的裁剪路径
func intersectClipPath(parent, current *canvas.Path) *canvas.Path {
if parent == nil {
return current
}
if current == nil {
return parent
}
parentRect, parentOK := rectangularPath(parent)
currentRect, currentOK := rectangularPath(current)
if parentOK && currentOK {
return parentRect.And(currentRect).ToPath()
}
if parent.Empty() || current.Empty() {
return &canvas.Path{}
}
if parentOK && parentRect.Contains(current.FastBounds()) {
return current
}
if currentOK && currentRect.Contains(parent.FastBounds()) {
return parent
}
return parent.And(current)
}
// unionClipPath 合并裁剪区域
// 入参: left 左侧裁剪路径, right 右侧裁剪路径
// 返回: *canvas.Path 合并后的裁剪路径
func unionClipPath(left, right *canvas.Path) *canvas.Path {
leftRect, leftOK := rectangularPath(left)
rightRect, rightOK := rectangularPath(right)
if leftOK && rightOK {
bounds := leftRect.Add(rightRect)
area := leftRect.Area() + rightRect.Area() - leftRect.And(rightRect).Area()
if canvas.Equal(area, bounds.Area()) {
return bounds.ToPath()
}
}
return left.Or(right)
}
// applyClipPath 应用裁剪路径
// 入参: path 绘制路径, clip 裁剪路径
// 返回: *canvas.Path 裁剪后的绘制路径
func applyClipPath(path, clip *canvas.Path) *canvas.Path {
if path == nil || clip == nil {
return path
}
if path.Empty() || clip.Empty() {
return &canvas.Path{}
}
if rect, ok := rectangularPath(clip); ok {
bounds := path.FastBounds()
if rect.Contains(bounds) {
return path
}
if !rect.Overlaps(bounds) {
return &canvas.Path{}
}
if pathRect, ok := rectangularPath(path); ok {
return pathRect.And(rect).ToPath()
}
return path.Flatten(canvas.Tolerance).Clip(rect.X0, rect.Y0, rect.X1, rect.Y1)
}
return path.And(clip)
}
// rectangularPath 获取矩形路径区域
// 入参: path 路径对象
// 返回: canvas.Rect 矩形区域, bool 是否为矩形
func rectangularPath(path *canvas.Path) (canvas.Rect, bool) {
if path == nil || path.HasSubpaths() || !path.Closed() {
return canvas.Rect{}, false
}
data := path.Data()
for i := 0; i < len(data); i += 4 {
if i+4 > len(data) || (data[i] != canvas.MoveToCmd && data[i] != canvas.LineToCmd && data[i] != canvas.CloseCmd) {
return canvas.Rect{}, false
}
}
points := path.Coords()
if len(points) != 5 || !points[0].Equals(points[4]) {
return canvas.Rect{}, false
}
rect := path.FastBounds()
corners := 0
for _, point := range points[:4] {
corner := 0
switch {
case canvas.Equal(point.X, rect.X0) && canvas.Equal(point.Y, rect.Y0):
corner = 1
case canvas.Equal(point.X, rect.X1) && canvas.Equal(point.Y, rect.Y0):
corner = 2
case canvas.Equal(point.X, rect.X1) && canvas.Equal(point.Y, rect.Y1):
corner = 4
case canvas.Equal(point.X, rect.X0) && canvas.Equal(point.Y, rect.Y1):
corner = 8
default:
return canvas.Rect{}, false
}
if corners&corner != 0 {
return canvas.Rect{}, false
}
corners |= corner
}
return rect, corners == 15
}
// buildTinyFillRectPath 构建微小填充矩形路径
// 入参: obj 路径对象, pageH 页面高度, ctm 变换矩阵, bx 边界X坐标, by 边界Y坐标
// 返回: *canvas.Path 路径对象
func (r *Renderer) buildTinyFillRectPath(obj PathObject, pageH float64, ctm Matrix, bx, by float64) *canvas.Path {
if obj.Fill == nil || !*obj.Fill || obj.Stroke == nil || *obj.Stroke {
return nil
}
box, err := ParseBox(obj.Boundary)
if err != nil || box.W <= 0 || box.H <= 0 || box.W > 0.6 || box.H > 0.6 {
return nil
}
tokens := strings.Fields(obj.AbbreviatedData)
if len(tokens) != 11 || tokens[0] != "M" || tokens[3] != "L" || tokens[6] != "L" || tokens[9] != "L" || tokens[10] != "C" {
return nil
}
points := make([][2]float64, 0, 4)
for i := 1; i < 10; i += 3 {
x, errX := strconv.ParseFloat(tokens[i], 64)
y, errY := strconv.ParseFloat(tokens[i+1], 64)
if errX != nil || errY != nil {
return nil
}
tx, ty := ctm.Transform(x, y)
points = append(points, [2]float64{tx + bx, pageH - (ty + by)})
}
minX, maxX := points[0][0], points[0][0]
minY, maxY := points[0][1], points[0][1]
for _, point := range points[1:] {
minX = math.Min(minX, point[0])
maxX = math.Max(maxX, point[0])
minY = math.Min(minY, point[1])
maxY = math.Max(maxY, point[1])
}
expand := math.Min(box.W, box.H) * 0.08
p := &canvas.Path{}
p.MoveTo(minX-expand, minY-expand)
p.LineTo(maxX+expand, minY-expand)
p.LineTo(maxX+expand, maxY+expand)
p.LineTo(minX-expand, maxY+expand)
p.Close()
return p
}
+322
View File
@@ -0,0 +1,322 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"fmt"
"net/url"
"github.com/tdewolff/canvas"
"github.com/tdewolff/canvas/renderers/pdf"
)
// pdfPage PDF页面数据
type pdfPage struct {
Content *PageContent
Box Box
}
// pdfNavigation PDF导航信息
type pdfNavigation struct {
Anchor map[int][]pdfAnchor
Link map[int][]pdfLink
Outline map[int][]pdfOutline
nextID int
}
// pdfAnchor PDF跳转目标
type pdfAnchor struct {
Name string
Rect canvas.Rect
}
// pdfLink PDF链接
type pdfLink struct {
URI string
Rect canvas.Rect
}
// pdfOutline PDF大纲
type pdfOutline struct {
Name string
Level int
Y float64
}
// pdfActionSource PDF动作来源
type pdfActionSource struct {
Box Box
Actions []Action
}
// newPDFNavigation 创建PDF导航信息
// 入参: renderer 渲染器, doc 文档结构, pages 页面数据
// 返回: *pdfNavigation PDF导航信息
func newPDFNavigation(renderer *Renderer, doc *Document, pages []pdfPage) *pdfNavigation {
navigation := &pdfNavigation{
Anchor: make(map[int][]pdfAnchor),
Link: make(map[int][]pdfLink),
Outline: make(map[int][]pdfOutline),
}
pageIndex := make(map[string]int, len(pages))
for i, page := range pages {
pageIndex[page.Content.ID] = i
}
bookmarks := make(map[string]Dest)
if doc != nil {
for _, bookmark := range doc.Bookmarks.Bookmark {
bookmarks[bookmark.Name] = bookmark.Dest
}
}
for i, page := range pages {
sources := pageActionSources(page)
if renderer.RenderAnnotations {
sources = append(sources, annotationActionSources(renderer.Reader.Annots[page.Content.ID])...)
}
for _, source := range sources {
rect := pdfSourceRect(source.Box, page.Box.H)
for _, action := range source.Actions {
if action.Event != "CLICK" {
continue
}
navigation.addAction(i, rect, action, bookmarks, pageIndex, pages)
}
}
}
if doc != nil {
navigation.addOutlines(doc.Outlines.OutlineElem, 0, bookmarks, pageIndex, pages)
}
return navigation
}
// addAction 添加PDF动作
// 入参: page 页面索引, rect 动作区域, action 动作, bookmarks 书签, pageIndex 页面索引表, pages 页面数据
func (n *pdfNavigation) addAction(page int, rect canvas.Rect, action Action, bookmarks map[string]Dest, pageIndex map[string]int, pages []pdfPage) {
if action.Goto != nil {
dest := gotoDest(action.Goto, bookmarks)
if dest == nil {
return
}
target, ok := pageIndex[dest.PageID]
if !ok {
return
}
targetRect, ok := pdfDestRect(*dest, pages[target].Box.H)
if !ok {
return
}
name := fmt.Sprintf("ofdgo-dest-%d", n.nextID)
n.nextID++
n.Anchor[target] = append(n.Anchor[target], pdfAnchor{Name: name, Rect: targetRect})
n.Link[page] = append(n.Link[page], pdfLink{URI: "#" + name, Rect: rect})
return
}
if action.URI != nil && action.URI.URI != "" {
n.Link[page] = append(n.Link[page], pdfLink{URI: resolveActionURI(*action.URI), Rect: rect})
}
}
// addOutlines 添加PDF大纲
// 入参: outlines 大纲节点, level 节点层级, bookmarks 书签, pageIndex 页面索引表, pages 页面数据
func (n *pdfNavigation) addOutlines(outlines []OutlineElem, level int, bookmarks map[string]Dest, pageIndex map[string]int, pages []pdfPage) {
for _, outline := range outlines {
if dest := outlineDest(outline, bookmarks); dest != nil {
if page, ok := pageIndex[dest.PageID]; ok {
n.Outline[page] = append(n.Outline[page], pdfOutline{
Name: outline.Title,
Level: level,
Y: pdfDestY(*dest, pages[page].Box.H),
})
}
}
n.addOutlines(outline.OutlineElem, level+1, bookmarks, pageIndex, pages)
}
}
// apply 应用PDF导航信息
// 入参: renderer PDF渲染器, page 页面索引
func (n *pdfNavigation) apply(renderer *pdf.PDF, page int) {
for _, anchor := range n.Anchor[page] {
renderer.AddAnchor(anchor.Name, anchor.Rect)
}
for _, link := range n.Link[page] {
renderer.AddLink(link.URI, link.Rect)
}
for _, outline := range n.Outline[page] {
renderer.AddOutline(outline.Name, outline.Level, outline.Y)
}
}
// pageActionSources 获取页面动作来源
// 入参: page 页面数据
// 返回: []pdfActionSource 动作来源
func pageActionSources(page pdfPage) []pdfActionSource {
sources := make([]pdfActionSource, 0)
if len(page.Content.Actions) > 0 {
sources = append(sources, pdfActionSource{
Box: Box{W: page.Box.W, H: page.Box.H},
Actions: page.Content.Actions,
})
}
for _, layer := range page.Content.Content.Layer {
for _, object := range layer.Objects {
sources = appendGraphicActionSources(sources, object, nil)
}
}
return sources
}
// annotationActionSources 获取注释动作来源
// 入参: annotations 页面注释
// 返回: []pdfActionSource 动作来源
func annotationActionSources(annotations []Annotation) []pdfActionSource {
sources := make([]pdfActionSource, 0)
for _, annotation := range annotations {
box, err := ParseBox(annotation.Appearance.Boundary)
if err != nil {
continue
}
for _, object := range annotation.Appearance.Objects {
sources = appendGraphicActionSources(sources, object, &box)
}
}
return sources
}
// appendGraphicActionSources 添加图形对象动作来源
// 入参: sources 动作来源, object 图形对象, box 指定动作区域
// 返回: []pdfActionSource 动作来源
func appendGraphicActionSources(sources []pdfActionSource, object GraphicObject, box *Box) []pdfActionSource {
var boundary string
var actions []Action
var children []GraphicObject
switch object.Type {
case "TextObject":
boundary = object.TextObject.Boundary
actions = object.TextObject.Actions
case "PathObject":
boundary = object.PathObject.Boundary
actions = object.PathObject.Actions
case "ImageObject":
boundary = object.ImageObject.Boundary
actions = object.ImageObject.Actions
case "CompositeGraphicUnit", "CompositeObject":
boundary = object.CompositeGraphicUnit.Boundary
actions = object.CompositeGraphicUnit.Actions
children = object.CompositeGraphicUnit.Objects
}
sourceBox := box
if sourceBox == nil && boundary != "" {
if value, err := ParseBox(boundary); err == nil {
sourceBox = &value
}
}
if sourceBox != nil && len(actions) > 0 {
sources = append(sources, pdfActionSource{Box: *sourceBox, Actions: actions})
}
for _, child := range children {
sources = appendGraphicActionSources(sources, child, box)
}
return sources
}
// gotoDest 获取文档内跳转目标
// 入参: action 跳转动作, bookmarks 书签
// 返回: *Dest 跳转目标
func gotoDest(action *Goto, bookmarks map[string]Dest) *Dest {
if action.Dest != nil {
return action.Dest
}
if action.Bookmark != nil {
if dest, ok := bookmarks[action.Bookmark.Name]; ok {
return &dest
}
}
return nil
}
// outlineDest 获取大纲跳转目标
// 入参: outline 大纲节点, bookmarks 书签
// 返回: *Dest 跳转目标
func outlineDest(outline OutlineElem, bookmarks map[string]Dest) *Dest {
for _, action := range outline.Actions {
if action.Goto != nil {
if dest := gotoDest(action.Goto, bookmarks); dest != nil {
return dest
}
}
}
for _, child := range outline.OutlineElem {
if dest := outlineDest(child, bookmarks); dest != nil {
return dest
}
}
return nil
}
// resolveActionURI 解析URI动作地址
// 入参: action URI动作
// 返回: string URI地址
func resolveActionURI(action URI) string {
if action.Base == "" {
return action.URI
}
base, err := url.Parse(action.Base)
if err != nil {
return action.URI
}
target, err := url.Parse(action.URI)
if err != nil {
return action.URI
}
return base.ResolveReference(target).String()
}
// pdfSourceRect 转换PDF动作区域
// 入参: box OFD区域, pageH 页面高度
// 返回: canvas.Rect PDF区域
func pdfSourceRect(box Box, pageH float64) canvas.Rect {
return canvas.RectFromSize(box.X, pageH-box.Y-box.H, box.W, box.H)
}
// pdfDestRect 转换PDF跳转目标
// 入参: dest OFD跳转目标, pageH 页面高度
// 返回: canvas.Rect PDF目标区域, bool 是否支持
func pdfDestRect(dest Dest, pageH float64) (canvas.Rect, bool) {
switch dest.Type {
case "XYZ":
return canvas.Rect{X0: dest.Left, Y0: pageH - dest.Top, X1: dest.Left, Y1: pageH - dest.Top}, true
case "Fit":
return canvas.Rect{}, true
case "FitH":
return canvas.Rect{Y0: pageH - dest.Top, Y1: pageH - dest.Top}, true
case "FitV":
return canvas.Rect{X0: dest.Left, X1: dest.Left}, true
case "FitR":
return canvas.Rect{X0: dest.Left, Y0: pageH - dest.Bottom, X1: dest.Right, Y1: pageH - dest.Top}, true
}
return canvas.Rect{}, false
}
// pdfDestY 获取PDF大纲目标位置
// 入参: dest OFD跳转目标, pageH 页面高度
// 返回: float64 PDF纵坐标
func pdfDestY(dest Dest, pageH float64) float64 {
switch dest.Type {
case "XYZ", "FitH", "FitR":
return pageH - dest.Top
}
return 0
}
+172
View File
@@ -0,0 +1,172 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"bytes"
"image"
"image/color"
"image/draw"
"math"
"github.com/tdewolff/canvas"
"github.com/tdewolff/canvas/renderers/rasterizer"
)
// renderStamp 渲染印章
// 入参: ctx 画布上下文, s 印章对象, pageH 页面高度
func (r *Renderer) renderStamp(ctx *canvas.Context, s Stamp, pageH float64) {
if s.Type == "ofd" && len(s.Data) > 0 {
if s.Clip != nil {
if img := r.renderOFDStampImage(s.Data); img != nil {
r.renderStampImage(ctx, stampImageWithTransparentWhite(img), s, pageH)
}
return
}
reader, err := NewReader(bytes.NewReader(s.Data), int64(len(s.Data)))
if err == nil {
defer reader.Close()
doc, err := reader.Doc()
if err == nil {
renderer := r.childRenderer(reader)
for _, pageRef := range doc.Pages.Page {
content, err := reader.PageContent(pageRef)
if err != nil {
continue
}
sealBox, err := renderer.GetPageBox(content)
if err != nil {
continue
}
ctx.Push()
ctx.Translate(s.Box.X, pageH-(s.Box.Y+s.Box.H))
ctx.Scale(s.Box.W/sealBox.W, s.Box.H/sealBox.H)
renderer.renderPageToContext(ctx, content, false)
ctx.Pop()
}
return
}
}
}
if len(s.Data) > 0 {
img, _, err := decodeImageData(s.Data)
if err == nil {
if r.decodeImages {
img = imagePixelSource(img)
}
r.renderStampImage(ctx, stampImageWithTransparentWhite(img), s, pageH)
return
}
}
}
// renderOFDStampImage 渲染OFD印章图像
// 入参: data OFD印章数据
// 返回: image.Image 印章图像
func (r *Renderer) renderOFDStampImage(data []byte) image.Image {
reader, err := NewReader(bytes.NewReader(data), int64(len(data)))
if err != nil {
return nil
}
defer reader.Close()
doc, err := reader.Doc()
if err != nil {
return nil
}
renderer := r.childRenderer(reader)
renderer.decodeImages = true
for _, pageRef := range doc.Pages.Page {
content, err := reader.PageContent(pageRef)
if err != nil {
continue
}
sealBox, err := renderer.GetPageBox(content)
if err != nil {
continue
}
c := canvas.New(sealBox.W, sealBox.H)
if err := renderer.renderPageToContext(canvas.NewContext(c), content, false); err != nil {
continue
}
return rasterizer.Draw(c, canvas.DPMM(r.DPI/25.4), canvas.DefaultColorSpace)
}
return nil
}
// renderStampImage 渲染印章图像
// 入参: ctx 画布上下文, img 印章图像, s 印章对象, pageH 页面高度
func (r *Renderer) renderStampImage(ctx *canvas.Context, img image.Image, s Stamp, pageH float64) {
box := s.Box
if s.Clip != nil {
img = clipStampImage(img, box, *s.Clip)
box.X += s.Clip.X
box.Y += s.Clip.Y
box.W = s.Clip.W
box.H = s.Clip.H
}
ctx.Push()
ctx.Translate(box.X, pageH-(box.Y+box.H))
ctx.Scale(box.W/float64(img.Bounds().Dx()), box.H/float64(img.Bounds().Dy()))
ctx.DrawImage(0, 0, img, canvas.DPMM(1.0))
ctx.Pop()
}
// clipStampImage 裁剪印章图像
// 入参: img 印章图像, box 印章区域, clip 裁剪区域
// 返回: image.Image 裁剪后的印章图像
func clipStampImage(img image.Image, box, clip Box) image.Image {
bounds := img.Bounds()
x0 := int(math.Floor(clip.X / box.W * float64(bounds.Dx())))
y0 := int(math.Floor(clip.Y / box.H * float64(bounds.Dy())))
x1 := int(math.Ceil((clip.X + clip.W) / box.W * float64(bounds.Dx())))
y1 := int(math.Ceil((clip.Y + clip.H) / box.H * float64(bounds.Dy())))
out := image.NewNRGBA(image.Rect(0, 0, x1-x0, y1-y0))
draw.Draw(out, out.Bounds(), img, image.Pt(bounds.Min.X+x0, bounds.Min.Y+y0), draw.Src)
return out
}
// stampImageWithTransparentWhite 处理印章图片白色底色
// 入参: img 印章图片对象
// 返回: image.Image 处理后的印章图片对象
func stampImageWithTransparentWhite(img image.Image) image.Image {
if opaque, ok := img.(interface{ Opaque() bool }); ok && !opaque.Opaque() {
return img
}
bounds := img.Bounds()
out := image.NewNRGBA(bounds)
hasAlpha := false
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
c := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA)
if c.A < 255 {
hasAlpha = true
}
out.SetNRGBA(x, y, c)
}
}
if hasAlpha {
return img
}
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
c := out.NRGBAAt(x, y)
if c.R >= 250 && c.G >= 250 && c.B >= 250 {
c.A = 0
out.SetNRGBA(x, y, c)
}
}
}
return out
}
+283
View File
@@ -0,0 +1,283 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"image/color"
"strings"
"github.com/tdewolff/canvas"
)
const ptPerMM = 72.0 / 25.4
// renderText 渲染文本
// 入参: ctx 画布上下文, obj 文本对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, parentCTM 父级CTM, boundaryInCTM 边界是否参与父级CTM, parentClip 父级裁剪路径
func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64, defaultFill, defaultStroke color.Color, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) {
if obj.Visible != nil && !*obj.Visible {
return
}
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
}
}
localCTM := NewMatrix(obj.CTM)
ctm := localCTM
if parentCTM != nil {
ctm = parentCTM.Multiply(ctm)
}
clipPath := intersectClipPath(parentClip, r.buildClipPath(obj.Clips, pageH, bx, by, ctm))
var dp *DrawParam
if obj.DrawParam != "" {
dp = r.getDrawParam(obj.DrawParam, nil)
}
sizeMM := obj.Size
if sizeMM == 0 && dp != nil && dp.Size > 0 {
sizeMM = dp.Size
}
if sizeMM == 0 {
sizeMM = 3.5
}
if obj.VScale != 0 {
sizeMM *= obj.VScale
}
hScale := obj.HScale
if hScale == 0 {
hScale = 1
}
useTextMatrix := hasTextMatrix(ctm)
if scale := ctm.YScale(); scale > 0 && !useTextMatrix {
sizeMM *= scale
}
sizePt := sizeMM * ptPerMM
fillColor := colorWithAlpha(defaultFill, obj.Alpha)
if fillColor == nil {
fillColor = colorWithAlpha(canvas.Black, obj.Alpha)
}
var fillPaint any = fillColor
var fillColorNode *FillColor
if dp != nil && dp.FillColor != nil {
fillColorNode = withFillAlpha(dp.FillColor, obj.Alpha)
fillColor = parseFillColor(fillColorNode)
fillPaint = parseFillPaint(fillColorNode, bx, by, pageH, 0, 0)
}
if obj.FillColor != nil {
fillColorNode = withFillAlpha(obj.FillColor, obj.Alpha)
fillColor = parseFillColor(fillColorNode)
fillPaint = parseFillPaint(fillColorNode, bx, by, pageH, 0, 0)
}
if fillPaint == nil {
fillPaint = fillColor
}
fontStyle := canvas.FontRegular
weight := obj.Weight
if weight == 0 && dp != nil && dp.Weight > 0 {
weight = dp.Weight
}
syntheticBold := weight >= 700
italic := obj.Italic
if !italic && dp != nil && dp.Italic {
italic = true
}
if italic {
fontStyle |= canvas.FontItalic
}
fontID := r.textObjectFontID(obj)
embeddedFont := false
if of, ok := r.Reader.fontCache[fontID]; ok {
embeddedFont = of.FontFile != ""
if !embeddedFont && fontNoSyntheticBold(of.FontName, of.FamilyName) {
syntheticBold = false
}
if of.Bold {
fontStyle |= canvas.FontBold
}
if of.Italic {
fontStyle |= canvas.FontItalic
}
}
if syntheticBold {
fontStyle |= canvas.FontBold
}
ff := r.loadFont(fontID)
if ff == nil {
ctx.Pop()
return
}
face := ff.Face(sizePt, fillPaint, fontStyle, canvas.FontNormal)
glyphTransforms := r.textObjectGlyphTransforms(fontID, obj)
hasUnderline := strings.Contains(obj.Decoration, "Underline")
useGlyphFillPaint := fillColorNode != nil && fillColorNode.AxialShd != nil
codePos := 0
for _, tc := range obj.TextCode {
var runes []rune
var glyphs []textGlyph
if tc.Index != "" {
runes = r.parseIndexRunes(tc.Index, fontID)
glyphs = textRuneGlyphs(runes)
} else {
runes = textCodeRunes(tc.Value)
glyphs = textCodeGlyphs(runes, glyphTransforms, codePos)
}
dxs, dys := parseFloats(tc.DeltaX), parseFloats(tc.DeltaY)
xs, ys := parseFloats(tc.X), parseFloats(tc.Y)
drawAsPath := embeddedFont || textCodePositioned(tc, xs, ys) || clipPath != nil
cx, cy := 0.0, 0.0
if len(xs) > 0 {
cx = xs[0]
}
if len(ys) > 0 {
cy = ys[0]
}
for i, glyph := range glyphs {
str := glyph.Text
drawAsGlyphPath := drawAsPath || glyph.GlyphID >= 0
var glyphPath *canvas.Path
var glyphWidth float64
if drawAsGlyphPath {
glyphPath, glyphWidth = r.cachedTextGlyphPath(face, glyph)
} else {
glyphWidth = textGlyphWidth(face, glyph)
}
if i < len(xs) {
cx = xs[i]
} else if i > 0 {
if dx, ok := textDelta(dxs, i-1); ok {
cx += dx
} else if len(dys) == 0 {
cx += glyphWidth * hScale
}
}
if i < len(ys) {
cy = ys[i]
} else if i > 0 {
if dy, ok := textDelta(dys, i-1); ok {
cy += dy
}
}
var canvasX, canvasY float64
if boundaryInCTM && parentCTM != nil {
tx, ty := localCTM.Transform(cx, cy)
tx, ty = parentCTM.Transform(tx+bx, ty+by)
canvasX, canvasY = tx, pageH-ty
} else {
tx, ty := ctm.Transform(cx, cy)
canvasX, canvasY = tx+bx, pageH-(ty+by)
}
textWidth := glyphWidth * hScale
glyphFillPaint := fillPaint
if useGlyphFillPaint {
glyphFillPaint = parseFillPaint(fillColorNode, bx, by, pageH, canvasX, canvasY)
}
advanceLimit := textGlyphAdvanceLimit(dxs, dys, xs, i, len(glyphs), cx)
if glyphFillPaint != nil {
ctx.SetFill(glyphFillPaint)
if clipPath != nil {
scaleX := hScale
if advanceLimit > 0 && glyphWidth*scaleX > advanceLimit {
scaleX = advanceLimit / glyphWidth
}
textWidth = glyphWidth * scaleX
textTransform := canvas.Identity.Translate(canvasX, canvasY)
if useTextMatrix {
textTransform = textTransform.Mul(textMatrix(ctm))
}
glyphPath = applyClipPath(glyphPath.Copy().Transform(textTransform.Scale(scaleX, 1)), clipPath)
ctx.DrawPath(0, 0, glyphPath)
if hasUnderline {
uw := sizeMM * 0.05
off := sizeMM * 0.1
underline := &canvas.Path{}
underline.MoveTo(0, -off)
underline.LineTo(textWidth, -off)
underline = underline.Stroke(uw, canvas.ButtCap, canvas.MiterJoin, canvas.Tolerance)
underline = applyClipPath(underline.Transform(textTransform), clipPath)
ctx.SetFillColor(fillColor)
ctx.SetStrokeColor(canvas.Transparent)
ctx.DrawPath(0, 0, underline)
}
continue
}
drawGlyph := func(x, y float64) {
if drawAsGlyphPath {
scaleX := hScale
if advanceLimit > 0 && glyphWidth*scaleX > advanceLimit {
scaleX = advanceLimit / glyphWidth
}
textWidth = glyphWidth * scaleX
if scaleX != 1 {
ctx.Push()
ctx.Translate(x, y)
ctx.Scale(scaleX, 1)
ctx.DrawPath(0, 0, glyphPath)
ctx.Pop()
} else {
ctx.DrawPath(x, y, glyphPath)
}
} else {
scaled := hScale != 1
if scaled {
ctx.Push()
ctx.Translate(x, y)
ctx.Scale(hScale, 1)
x, y = 0, 0
}
textFace := face
if useGlyphFillPaint {
textFace = ff.Face(sizePt, glyphFillPaint, fontStyle, canvas.FontNormal)
}
text := canvas.NewTextLine(textFace, str, canvas.Left)
ctx.DrawText(x, y, text)
if scaled {
ctx.Pop()
}
}
}
if useTextMatrix {
ctx.Push()
ctx.Translate(canvasX, canvasY)
ctx.ComposeView(textMatrix(ctm))
drawGlyph(0, 0)
if hasUnderline {
uw := sizeMM * 0.05
ctx.SetStrokeWidth(uw)
ctx.SetStrokeColor(fillColor)
off := sizeMM * 0.1
ctx.MoveTo(0, -off)
ctx.LineTo(textWidth, -off)
ctx.Stroke()
}
ctx.Pop()
continue
}
drawGlyph(canvasX, canvasY)
}
if hasUnderline {
uw := sizeMM * 0.05
ctx.SetStrokeWidth(uw)
ctx.SetStrokeColor(fillColor)
off := sizeMM * 0.1
ctx.MoveTo(canvasX, canvasY-off)
ctx.LineTo(canvasX+textWidth, canvasY-off)
ctx.Stroke()
}
}
codePos += len(runes)
}
ctx.Pop()
}
+1
View File
@@ -104,4 +104,5 @@ type CompositeGraphicUnit struct {
ImageObject []ImageObject `xml:"ImageObject"`
CompositeGraphicUnit []CompositeGraphicUnit `xml:"CompositeGraphicUnit"`
Clips *Clips `xml:"Clips"`
Actions []Action `xml:"Actions>Action"`
}
+151 -21
View File
@@ -19,21 +19,24 @@ import (
"encoding/asn1"
"encoding/binary"
"encoding/xml"
"image"
"fmt"
"strings"
)
// SignType 签名类型
type SignType string
const (
SignTypeSeal = "Seal"
SignTypeSign = "Sign"
// SignTypeSeal 印章签名
SignTypeSeal SignType = "Seal"
// SignTypeSign 数字签名
SignTypeSign SignType = "Sign"
)
// Signatures 签名列表
type Signatures struct {
XMLName xml.Name `xml:"Signatures"`
MaxSignId string `xml:"MaxSignId"`
MaxSignID string `xml:"MaxSignId"`
List []Signature `xml:"Signature"`
}
@@ -48,16 +51,111 @@ type Signature struct {
type SignatureFile struct {
XMLName xml.Name `xml:"Signature"`
SignedValue string `xml:"SignedValue"`
SignedInfo struct {
Seal struct {
BaseLoc string `xml:"BaseLoc"`
} `xml:"Seal"`
StampAnnot []struct {
ID string `xml:"ID,attr"`
PageRef string `xml:"PageRef,attr"`
Boundary string `xml:"Boundary,attr"`
} `xml:"StampAnnot"`
} `xml:"SignedInfo"`
SignedInfo SignedInfo
}
// SignedInfo 签名信息
type SignedInfo struct {
Provider SignatureProvider `xml:"Provider"`
SignatureMethod string `xml:"SignatureMethod"`
SignatureDateTime string `xml:"SignatureDateTime"`
Seal SignatureSeal `xml:"Seal"`
StampAnnot []SignatureStamp `xml:"StampAnnot"`
References SignatureReferences `xml:"References"`
}
// SignatureProvider 签名提供者信息
type SignatureProvider struct {
ProviderName string `xml:"ProviderName,attr"`
Company string `xml:"Company,attr"`
Version string `xml:"Version,attr"`
}
// SignatureSeal 签名印章引用
type SignatureSeal struct {
BaseLoc string `xml:"BaseLoc"`
}
// SignatureStamp 签名印章注释
type SignatureStamp struct {
ID string `xml:"ID,attr"`
PageRef string `xml:"PageRef,attr"`
Boundary string `xml:"Boundary,attr"`
Clip string `xml:"Clip,attr"`
}
// SignatureStampPosition 签名外观位置信息
type SignatureStampPosition struct {
ID string
Page int
PageID string
Boundary string
Box Box
Clip string
ClipBox *Box
}
// SignatureReferences 签名保护文件列表
type SignatureReferences struct {
CheckMethod string `xml:"CheckMethod,attr"`
Reference []SignatureReference `xml:"Reference"`
}
// SignatureReference 签名保护文件引用
type SignatureReference struct {
FileRef string `xml:"FileRef,attr"`
CheckValue string `xml:"CheckValue"`
}
// SignatureStampPositions 获取签名外观位置信息
// 入参: stamps 签名外观列表
// 返回: []SignatureStampPosition 签名外观位置信息, error 错误信息
func (r *Reader) SignatureStampPositions(stamps []SignatureStamp) ([]SignatureStampPosition, error) {
doc, err := r.Doc()
if err != nil {
return nil, err
}
pages := signaturePageNumbers(doc)
positions := make([]SignatureStampPosition, 0, len(stamps))
for _, stamp := range stamps {
page, ok := pages[stamp.PageRef]
if !ok {
return nil, fmt.Errorf("signature stamp page not found: %s", stamp.PageRef)
}
box, err := parseSignatureStampBox(stamp.Boundary)
if err != nil {
return nil, err
}
var clipBox *Box
if stamp.Clip != "" {
clip, err := parseSignatureStampBox(stamp.Clip)
if err != nil {
return nil, err
}
clipBox = &clip
}
positions = append(positions, SignatureStampPosition{
ID: stamp.ID,
Page: page,
PageID: stamp.PageRef,
Boundary: stamp.Boundary,
Box: box,
Clip: stamp.Clip,
ClipBox: clipBox,
})
}
return positions, nil
}
// parseSignatureStampBox 解析签名外观区域
// 入参: s 区域字符串
// 返回: Box 矩形对象, error 错误信息
func parseSignatureStampBox(s string) (Box, error) {
box, err := ParseBox(s)
if err != nil {
return Box{}, fmt.Errorf("invalid signature stamp box: %s", s)
}
return box, nil
}
// parseSignatures 解析签名文件
@@ -100,7 +198,11 @@ func (r *Reader) parseSignatures(doc *Document) error {
if len(sealData) == 0 && sigFile.SignedValue != "" {
signedValuePath := resolveResourcePath(sigPath, "", sigFile.SignedValue)
if data, err := r.ResData(signedValuePath); err == nil {
sealType, sealData = extractSeal(data)
if sig, err := parseSESSignature(data); err == nil {
sealType, sealData = sig.Seal.PicType, sig.Seal.PicData
} else {
sealType, sealData = extractSeal(data)
}
}
}
if len(sealData) == 0 {
@@ -108,14 +210,38 @@ func (r *Reader) parseSignatures(doc *Document) error {
}
for _, annot := range sigFile.SignedInfo.StampAnnot {
pageID := annot.PageRef
bbox, _ := ParseBox(annot.Boundary)
r.addStamp(pageID, bbox, sealType, sealData)
bbox, err := parseSignatureStampBox(annot.Boundary)
if err != nil {
continue
}
var clipBox *Box
if annot.Clip != "" {
clip, err := parseSignatureStampBox(annot.Clip)
if err != nil {
continue
}
clipBox = &clip
}
r.addStamp(pageID, bbox, clipBox, sealType, sealData)
}
}(sigRef)
}
return nil
}
// signaturePageNumbers 获取签名页面编号索引
// 入参: doc 文档结构
// 返回: map[string]int 页面编号索引
func signaturePageNumbers(doc *Document) map[string]int {
pages := make(map[string]int, len(doc.Pages.Page))
for index, page := range doc.Pages.Page {
if page.ID != "" {
pages[page.ID] = index + 1
}
}
return pages
}
// extractSeal 尝试提取印章数据
// 入参: data 签名值数据
// 返回: string 印章类型, []byte 印章数据
@@ -220,6 +346,8 @@ func normalizeSealType(s string) string {
switch s {
case "jpg":
return "jpeg"
case "bmp":
return "bmp"
case "jb2", "gbig2":
return "jbig2"
default:
@@ -232,7 +360,7 @@ func normalizeSealType(s string) string {
// 返回: bool 是否可渲染
func isSealMediaType(s string) bool {
switch s {
case "png", "jpeg", "jbig2", "ofd":
case "png", "jpeg", "bmp", "jbig2", "ofd":
return true
default:
return false
@@ -286,7 +414,7 @@ func zipDataEnd(data []byte) int {
// 入参: data 原始数据
// 返回: string 媒体类型, []byte 图片数据
func probeImageMedia(data []byte) (string, []byte) {
_, format, err := image.DecodeConfig(bytes.NewReader(data))
_, format, err := decodeImageConfigData(data)
if err != nil {
return "", nil
}
@@ -296,18 +424,20 @@ func probeImageMedia(data []byte) (string, []byte) {
// Stamp 印章信息结构
type Stamp struct {
Box Box
Clip *Box
Type string
Data []byte
}
// addStamp 添加印章到页面
// 入参: pageID 页面ID, box 印章区域, sType 印章类型, data 印章数据
func (r *Reader) addStamp(pageID string, box Box, sType string, data []byte) {
// 入参: pageID 页面ID, box 印章区域, clip 裁剪区域, sType 印章类型, data 印章数据
func (r *Reader) addStamp(pageID string, box Box, clip *Box, sType string, data []byte) {
if r.Stamps == nil {
r.Stamps = make(map[string][]Stamp)
}
r.Stamps[pageID] = append(r.Stamps[pageID], Stamp{
Box: box,
Clip: clip,
Type: sType,
Data: data,
})
+639
View File
@@ -0,0 +1,639 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"bytes"
"encoding/asn1"
"fmt"
"math/big"
)
const (
signContentData = "1.2.156.10197.6.1.4.2.1"
signContentSignedData = "1.2.156.10197.6.1.4.2.2"
signAttrMessageDigest = "1.2.840.113549.1.9.4"
)
// digitalVerifyResult 数字签名验证结果
type digitalVerifyResult struct {
DataHashOK bool
SignedOK bool
CertOK bool
SignerCerts [][]byte
Certs [][]byte
CertInfo SignatureCertInfo
}
// gbtSignedData GB/T 35275 SignedData结构
type gbtSignedData struct {
ContentDigest []byte
Certs []gbtCertificate
Signers []gbtSignerInfo
}
// gbtCertificate SignedData证书索引信息
type gbtCertificate struct {
Raw []byte
Issuer []byte
Serial *big.Int
}
// gbtSignerInfo SignedData签名者信息
type gbtSignerInfo struct {
Issuer []byte
Serial *big.Int
DigestAlg string
SignatureAlg string
Signature []byte
AuthAttrs []byte
AttrDigest []byte
}
// verifyDigitalSignature 验证OFD数字签名
// 入参: method 签名算法, digestMethod 摘要算法, signedValue 签名值, signedData 被签名原文, options 验证选项
// 返回: *digitalVerifyResult 验证结果, error 错误信息
func verifyDigitalSignature(method, digestMethod string, signedValue, signedData []byte, options *signatureVerifyOptions) (*digitalVerifyResult, error) {
if value, ok := normalizeGBT35275SignedValue(signedValue); ok {
return verifyGBT35275SignedData(value, signedData, options)
}
if isSM2SignatureMethod(method) {
return verifyRawDigitalSignature(signedValue, signedData, options)
}
if isRSASignatureMethod(method) || isECDSASignatureMethod(method) {
return verifyRawPublicKeySignature(method, digestMethod, signedValue, signedData, options)
}
return nil, fmt.Errorf("unsupported signature method: %s", method)
}
// verifyRawDigitalSignature 验证裸SM2数字签名
// 入参: signedValue 签名值, signedData 被签名原文, options 验证选项
// 返回: *digitalVerifyResult 验证结果, error 错误信息
func verifyRawDigitalSignature(signedValue, signedData []byte, options *signatureVerifyOptions) (*digitalVerifyResult, error) {
if len(options.SignCerts) == 0 {
return nil, fmt.Errorf("signature certificate not found")
}
result := &digitalVerifyResult{DataHashOK: true}
for _, cert := range options.SignCerts {
pub, err := parseSM2PublicKeyFromCert(cert)
if err != nil {
continue
}
result.CertOK = true
result.SignerCerts = [][]byte{cert}
result.CertInfo = signatureCertInfo(cert)
if sm2VerifySignature(pub, nil, signedData, signedValue) {
result.SignedOK = true
return result, nil
}
}
return result, nil
}
// verifyRawPublicKeySignature 验证裸RSA或ECDSA数字签名
// 入参: method 签名算法, digestMethod 摘要算法, signedValue 签名值, signedData 被签名原文, options 验证选项
// 返回: *digitalVerifyResult 验证结果, error 错误信息
func verifyRawPublicKeySignature(method, digestMethod string, signedValue, signedData []byte, options *signatureVerifyOptions) (*digitalVerifyResult, error) {
if len(options.SignCerts) == 0 {
return nil, fmt.Errorf("signature certificate not found")
}
if _, err := signatureMethodHash(method, digestMethod); err != nil {
return nil, err
}
result := &digitalVerifyResult{DataHashOK: true}
for _, cert := range options.SignCerts {
ok, err := verifyPublicKeySignature(method, digestMethod, cert, signedData, signedValue)
if err != nil {
continue
}
result.CertOK = true
result.SignerCerts = [][]byte{cert}
result.CertInfo = signatureCertInfo(cert)
if ok {
result.SignedOK = true
return result, nil
}
}
return result, nil
}
// verifyGBT35275SignedData 验证GB/T 35275 SignedData签名值
// 入参: signedValue 签名值, signedData 被签名原文, options 验证选项
// 返回: *digitalVerifyResult 验证结果, error 错误信息
func verifyGBT35275SignedData(signedValue, signedData []byte, options *signatureVerifyOptions) (*digitalVerifyResult, error) {
sd, err := parseGBT35275SignedData(signedValue)
if err != nil {
return nil, err
}
for _, cert := range options.SignCerts {
c, err := parseGBTCertificate(cert)
if err == nil {
sd.Certs = append(sd.Certs, c)
}
}
if len(sd.Signers) == 0 {
return nil, fmt.Errorf("invalid signed data signer info")
}
result := &digitalVerifyResult{Certs: sd.rawCerts()}
for _, signer := range sd.Signers {
digest, err := signatureDigest(signer.DigestAlg, signedData)
if err != nil {
return nil, err
}
if len(sd.ContentDigest) != 0 && !bytes.Equal(sd.ContentDigest, digest) {
return result, nil
}
result.DataHashOK = true
plain := sd.ContentDigest
if len(signer.AuthAttrs) != 0 {
if !bytes.Equal(signer.AttrDigest, digest) {
result.DataHashOK = false
return result, nil
}
plain = signer.AuthAttrs
}
if len(plain) == 0 {
return nil, fmt.Errorf("invalid signed data content")
}
cert := sd.findCert(signer.Issuer, signer.Serial)
if cert == nil {
result.CertOK = false
return result, nil
}
result.SignerCerts = append(result.SignerCerts, cert.Raw)
result.CertInfo = signatureCertInfo(cert.Raw)
if isSM2SignatureMethod(signer.SignatureAlg) {
pub, err := parseSM2PublicKeyFromCert(cert.Raw)
if err != nil {
result.CertOK = false
return result, err
}
if !sm2VerifySignature(pub, nil, plain, signer.Signature) {
result.CertOK = true
return result, nil
}
continue
}
if !isRSASignatureMethod(signer.SignatureAlg) && !isECDSASignatureMethod(signer.SignatureAlg) {
return nil, fmt.Errorf("unsupported signature method: %s", signer.SignatureAlg)
}
ok, err := verifyPublicKeySignature(signer.SignatureAlg, signer.DigestAlg, cert.Raw, plain, signer.Signature)
if err != nil {
result.CertOK = false
return result, err
}
if !ok {
result.CertOK = true
return result, nil
}
}
result.CertOK = true
result.SignedOK = true
return result, nil
}
// normalizeGBT35275SignedValue 规范化GB/T 35275 SignedData编码
// 入参: data 签名值数据
// 返回: []byte 定长编码数据, bool 是否为SignedData
func normalizeGBT35275SignedValue(data []byte) ([]byte, bool) {
if contentType, ok := gbtContentType(data); ok {
return data, contentType == signContentSignedData
}
der, err := berToDefinite(data)
if err != nil {
return nil, false
}
contentType, ok := gbtContentType(der)
if !ok || contentType != signContentSignedData {
return nil, false
}
return der, true
}
// gbtContentType 读取GB/T 35275内容类型
// 入参: data DER编码数据
// 返回: string 内容类型, bool 是否完成解析
func gbtContentType(data []byte) (string, bool) {
var root asn1.RawValue
rest, err := asn1.Unmarshal(data, &root)
if err != nil || len(rest) != 0 || root.Tag != signASN1Sequence {
return "", false
}
items, ok := asn1Children(root.Bytes)
if !ok || len(items) == 0 {
return "", false
}
if items[0].Tag != asn1.TagOID {
return "", true
}
oid, err := asn1OIDString(items[0])
return oid, err == nil
}
// berToDefinite 将BER不定长编码转换为定长编码
// 入参: data BER编码数据
// 返回: []byte 定长编码数据, error 错误信息
func berToDefinite(data []byte) ([]byte, error) {
out, n, err := berValueToDefinite(data)
if err != nil {
return nil, err
}
if n != len(data) {
return nil, fmt.Errorf("invalid BER trailing data")
}
return out, nil
}
// berValueToDefinite 转换单个BER编码值
// 入参: data BER编码数据
// 返回: []byte 定长编码数据, int 已读取长度, error 错误信息
func berValueToDefinite(data []byte) ([]byte, int, error) {
if len(data) < 2 {
return nil, 0, fmt.Errorf("invalid BER value")
}
pos := 1
if data[0]&0x1f == 0x1f {
for pos < len(data) && data[pos]&0x80 != 0 {
pos++
}
pos++
}
if pos >= len(data) {
return nil, 0, fmt.Errorf("invalid BER tag")
}
tag := data[:pos]
firstLength := data[pos]
pos++
if firstLength == 0x80 {
if tag[0]&0x20 == 0 {
return nil, 0, fmt.Errorf("invalid BER indefinite primitive")
}
var content []byte
for {
if len(data)-pos < 2 {
return nil, 0, fmt.Errorf("invalid BER unterminated value")
}
if data[pos] == 0 && data[pos+1] == 0 {
pos += 2
break
}
child, n, err := berValueToDefinite(data[pos:])
if err != nil {
return nil, 0, err
}
content = append(content, child...)
pos += n
}
return wrapBERValue(tag, content), pos, nil
}
length := uint64(firstLength)
if firstLength&0x80 != 0 {
n := int(firstLength & 0x7f)
if n == 0 || n > 8 || len(data)-pos < n {
return nil, 0, fmt.Errorf("invalid BER length")
}
length = 0
for _, b := range data[pos : pos+n] {
length = length<<8 | uint64(b)
}
pos += n
}
if length > uint64(len(data)-pos) {
return nil, 0, fmt.Errorf("invalid BER truncated value")
}
end := pos + int(length)
content := append([]byte(nil), data[pos:end]...)
if tag[0]&0x20 != 0 {
content = content[:0]
for pos < end {
child, n, err := berValueToDefinite(data[pos:end])
if err != nil {
return nil, 0, err
}
content = append(content, child...)
pos += n
}
}
return wrapBERValue(tag, content), end, nil
}
// wrapBERValue 包装定长BER编码值
// 入参: tag 标签, content 内容
// 返回: []byte 定长编码数据
func wrapBERValue(tag, content []byte) []byte {
out := make([]byte, 0, len(tag)+len(content)+9)
out = append(out, tag...)
out = append(out, asn1LengthBytes(len(content))...)
return append(out, content...)
}
// parseGBT35275SignedData 解析GB/T 35275 SignedData
// 入参: data 签名值数据
// 返回: *gbtSignedData SignedData结构, error 错误信息
func parseGBT35275SignedData(data []byte) (*gbtSignedData, error) {
var ok bool
data, ok = normalizeGBT35275SignedValue(data)
if !ok {
return nil, fmt.Errorf("invalid signed data content type")
}
contentType, content, ok, err := parseGBTContentInfoBytes(data)
if err != nil {
return nil, err
}
if !ok || contentType != signContentSignedData {
return nil, fmt.Errorf("invalid signed data content type")
}
items, ok := asn1Children(content.Bytes)
if !ok || len(items) < 4 {
return nil, fmt.Errorf("invalid signed data")
}
sd := &gbtSignedData{}
contentOID, content, hasContent, err := parseGBTContentInfo(items[2])
if err != nil {
return nil, err
}
if hasContent {
if contentOID != signContentData {
return nil, fmt.Errorf("invalid signed data inner content type")
}
if content.Tag == asn1.TagOctetString {
sd.ContentDigest, err = asn1OctetString(content)
if err != nil {
return nil, err
}
}
}
for i := 3; i < len(items); i++ {
item := items[i]
if item.Class == asn1.ClassContextSpecific && item.Tag == 0 {
certs, err := parseGBTCertificates(item)
if err != nil {
return nil, err
}
sd.Certs = certs
continue
}
if item.Class == asn1.ClassContextSpecific && item.Tag == 1 {
continue
}
signers, err := parseGBTSignerInfos(item)
if err != nil {
return nil, err
}
sd.Signers = signers
}
return sd, nil
}
// parseGBTContentInfoBytes 解析ContentInfo字节
// 入参: data DER编码数据
// 返回: string 内容类型, asn1.RawValue 内容, bool 是否存在内容, error 错误信息
func parseGBTContentInfoBytes(data []byte) (string, asn1.RawValue, bool, error) {
var raw asn1.RawValue
rest, err := asn1.Unmarshal(data, &raw)
if err != nil || len(rest) != 0 {
return "", asn1.RawValue{}, false, fmt.Errorf("invalid content info")
}
return parseGBTContentInfo(raw)
}
// parseGBTContentInfo 解析ContentInfo结构
// 入参: raw ASN.1原始值
// 返回: string 内容类型, asn1.RawValue 内容, bool 是否存在内容, error 错误信息
func parseGBTContentInfo(raw asn1.RawValue) (string, asn1.RawValue, bool, error) {
items, ok := asn1Children(raw.Bytes)
if !ok || len(items) == 0 {
return "", asn1.RawValue{}, false, fmt.Errorf("invalid content info")
}
oid, err := asn1OIDString(items[0])
if err != nil {
return "", asn1.RawValue{}, false, err
}
if len(items) == 1 {
return oid, asn1.RawValue{}, false, nil
}
content, err := asn1Explicit(items[1])
if err != nil {
return "", asn1.RawValue{}, false, err
}
return oid, content, true, nil
}
// parseGBTCertificates 解析SignedData证书集合
// 入参: raw ASN.1原始值
// 返回: []gbtCertificate 证书列表, error 错误信息
func parseGBTCertificates(raw asn1.RawValue) ([]gbtCertificate, error) {
items, ok := asn1Children(raw.Bytes)
if !ok {
return nil, fmt.Errorf("invalid signed data certificates")
}
certs := make([]gbtCertificate, 0, len(items))
for _, item := range items {
cert, err := parseGBTCertificate(item.FullBytes)
if err != nil {
return nil, err
}
certs = append(certs, cert)
}
return certs, nil
}
// parseGBTCertificate 解析X.509证书索引字段
// 入参: data DER编码证书
// 返回: gbtCertificate 证书索引信息, error 错误信息
func parseGBTCertificate(data []byte) (gbtCertificate, error) {
cert, err := parseSignatureCertificate(data)
if err != nil {
return gbtCertificate{}, err
}
return gbtCertificate{
Raw: cert.Raw,
Issuer: cert.Issuer,
Serial: cert.Serial,
}, nil
}
// parseGBTSignerInfos 解析签名者信息集合
// 入参: raw ASN.1原始值
// 返回: []gbtSignerInfo 签名者列表, error 错误信息
func parseGBTSignerInfos(raw asn1.RawValue) ([]gbtSignerInfo, error) {
items, ok := asn1Children(raw.Bytes)
if !ok {
return nil, fmt.Errorf("invalid signer infos")
}
signers := make([]gbtSignerInfo, 0, len(items))
for _, item := range items {
signer, err := parseGBTSignerInfo(item)
if err != nil {
return nil, err
}
signers = append(signers, signer)
}
return signers, nil
}
// parseGBTSignerInfo 解析签名者信息
// 入参: raw ASN.1原始值
// 返回: gbtSignerInfo 签名者信息, error 错误信息
func parseGBTSignerInfo(raw asn1.RawValue) (gbtSignerInfo, error) {
items, ok := asn1Children(raw.Bytes)
if !ok || len(items) < 5 {
return gbtSignerInfo{}, fmt.Errorf("invalid signer info")
}
issuer, serial, err := parseGBTIssuerAndSerial(items[1])
if err != nil {
return gbtSignerInfo{}, err
}
digestAlg, err := parseGBTAlgorithm(items[2])
if err != nil {
return gbtSignerInfo{}, err
}
idx := 3
var authAttrs []byte
var attrDigest []byte
if items[idx].Class == asn1.ClassContextSpecific && items[idx].Tag == 0 {
authAttrs = asn1SetBytes(items[idx].Bytes)
attrDigest, err = parseGBTMessageDigestAttr(items[idx])
if err != nil {
return gbtSignerInfo{}, err
}
idx++
}
if len(items) <= idx+1 {
return gbtSignerInfo{}, fmt.Errorf("invalid signer info")
}
signatureAlg, err := parseGBTAlgorithm(items[idx])
if err != nil {
return gbtSignerInfo{}, err
}
signature, err := asn1OctetString(items[idx+1])
if err != nil {
return gbtSignerInfo{}, err
}
return gbtSignerInfo{
Issuer: issuer,
Serial: serial,
DigestAlg: digestAlg,
SignatureAlg: signatureAlg,
Signature: signature,
AuthAttrs: authAttrs,
AttrDigest: attrDigest,
}, nil
}
// parseGBTIssuerAndSerial 解析证书颁发者和序列号
// 入参: raw ASN.1原始值
// 返回: []byte 颁发者DN, *big.Int 序列号, error 错误信息
func parseGBTIssuerAndSerial(raw asn1.RawValue) ([]byte, *big.Int, error) {
items, ok := asn1Children(raw.Bytes)
if !ok || len(items) < 2 {
return nil, nil, fmt.Errorf("invalid issuer and serial")
}
serial, err := asn1IntegerBig(items[1])
if err != nil {
return nil, nil, err
}
return append([]byte(nil), items[0].FullBytes...), serial, nil
}
// parseGBTAlgorithm 解析算法标识
// 入参: raw ASN.1原始值
// 返回: string 算法OID, error 错误信息
func parseGBTAlgorithm(raw asn1.RawValue) (string, error) {
if raw.Tag == asn1.TagOID {
return asn1OIDString(raw)
}
items, ok := asn1Children(raw.Bytes)
if !ok || len(items) == 0 {
return "", fmt.Errorf("invalid algorithm identifier")
}
return asn1OIDString(items[0])
}
// parseGBTMessageDigestAttr 解析认证属性中的message-digest
// 入参: raw ASN.1原始值
// 返回: []byte 摘要值, error 错误信息
func parseGBTMessageDigestAttr(raw asn1.RawValue) ([]byte, error) {
attrs, ok := asn1Children(raw.Bytes)
if !ok {
return nil, fmt.Errorf("invalid authenticated attributes")
}
for _, attr := range attrs {
items, ok := asn1Children(attr.Bytes)
if !ok || len(items) < 2 {
continue
}
oid, err := asn1OIDString(items[0])
if err != nil || oid != signAttrMessageDigest {
continue
}
values, ok := asn1Children(items[1].Bytes)
if !ok || len(values) == 0 {
return nil, fmt.Errorf("invalid message digest attribute")
}
return asn1OctetString(values[0])
}
return nil, fmt.Errorf("message digest attribute not found")
}
// findCert 查找签名者证书
// 入参: issuer 颁发者DN, serial 证书序列号
// 返回: *gbtCertificate 证书信息
func (sd *gbtSignedData) findCert(issuer []byte, serial *big.Int) *gbtCertificate {
for i := range sd.Certs {
cert := &sd.Certs[i]
if bytes.Equal(cert.Issuer, issuer) && cert.Serial.Cmp(serial) == 0 {
return cert
}
}
return nil
}
// rawCerts 获取SignedData证书原文
// 返回: [][]byte 证书列表
func (sd *gbtSignedData) rawCerts() [][]byte {
certs := make([][]byte, 0, len(sd.Certs))
for _, cert := range sd.Certs {
certs = append(certs, cert.Raw)
}
return certs
}
// asn1Explicit 解析显式标签内容
// 入参: raw ASN.1原始值
// 返回: asn1.RawValue 标签内容, error 错误信息
func asn1Explicit(raw asn1.RawValue) (asn1.RawValue, error) {
if raw.Class != asn1.ClassContextSpecific || raw.Tag != 0 || !raw.IsCompound {
return asn1.RawValue{}, fmt.Errorf("invalid explicit content")
}
var out asn1.RawValue
rest, err := asn1.Unmarshal(raw.Bytes, &out)
if err != nil || len(rest) != 0 {
return asn1.RawValue{}, fmt.Errorf("invalid explicit content")
}
return out, nil
}
// asn1IntegerBig 解析ASN.1整数
// 入参: raw ASN.1原始值
// 返回: *big.Int 整数值, error 错误信息
func asn1IntegerBig(raw asn1.RawValue) (*big.Int, error) {
var out *big.Int
rest, err := asn1.Unmarshal(raw.FullBytes, &out)
if err != nil || len(rest) != 0 || out == nil {
return nil, fmt.Errorf("invalid integer")
}
return out, nil
}
+742
View File
@@ -0,0 +1,742 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"bytes"
"encoding/asn1"
"fmt"
"math/big"
"strings"
"time"
)
const (
signDigestSM3 = "1.2.156.10197.1.401"
signDigestSM3NoKey = "1.2.156.10197.1.401.1"
signDigestSM3Key = "1.2.156.10197.1.401.2"
signMethodSM2SM3 = "1.2.156.10197.1.501"
signMethodSM2SM3B = "1.2.156.10197.501"
signMethodSM2Sign = "1.2.156.10197.1.301.1"
signCurveSM2P256 = "1.2.156.10197.1.301"
signECPublicKey = "1.2.840.10045.2.1"
signASN1Sequence = 16
signPublicKeySize = 65
)
// sesSignature SES签章值
type sesSignature struct {
ToSign []byte
Cert []byte
SignAlg string
Signature []byte
DataHash []byte
Time time.Time
Seal *sesSeal
}
// sesSeal SES电子印章
type sesSeal struct {
Raw []byte
SignData []byte
Cert []byte
SignAlg string
Signature []byte
PicType string
PicData []byte
CertList sesCertList
Info SignatureSealInfo
}
// sesCertList SES印章证书列表
type sesCertList struct {
Certs [][]byte
Digests []sesCertDigest
}
// sesCertDigest SES印章证书摘要
type sesCertDigest struct {
Method string
Value []byte
}
// sesVerifyResult SES签章验证结果
type sesVerifyResult struct {
DataHashOK bool
SignedOK bool
SealOK bool
CertOK bool
SignCert SignatureCertInfo
SealCert SignatureCertInfo
SignCertRaw []byte
SealCertRaw []byte
SealRaw []byte
Certs [][]byte
SealType string
SealInfo SignatureSealInfo
SignatureTime time.Time
}
// parseSESSignature 解析SES签章值
// 入参: data 签章值数据
// 返回: *sesSignature SES签章值, error 错误信息
func parseSESSignature(data []byte) (*sesSignature, error) {
var root asn1.RawValue
rest, err := asn1.Unmarshal(data, &root)
if err != nil {
return nil, err
}
if len(rest) != 0 || root.Tag != signASN1Sequence || !root.IsCompound {
return nil, fmt.Errorf("invalid ses signature")
}
items, ok := asn1Children(root.Bytes)
if !ok {
return nil, fmt.Errorf("invalid ses signature items")
}
if len(items) == 2 {
return parseSESSignatureV1(items)
}
if len(items) < 4 || len(items) > 5 {
return nil, fmt.Errorf("invalid ses signature items")
}
cert, err := asn1OctetString(items[1])
if err != nil {
return nil, err
}
alg, err := asn1OIDString(items[2])
if err != nil {
return nil, err
}
signature, err := asn1BitStringBytes(items[3])
if err != nil {
return nil, err
}
tbsItems, ok := asn1Children(items[0].Bytes)
if !ok || len(tbsItems) < 5 || len(tbsItems) > 6 {
return nil, fmt.Errorf("invalid ses toSign")
}
seal, err := parseSESSeal(tbsItems[1])
if err != nil {
return nil, err
}
version, err := asn1Integer(tbsItems[0])
if err != nil || version != seal.Info.Version {
return nil, fmt.Errorf("invalid ses version")
}
signatureTime, err := asn1Time(tbsItems[2])
if err != nil {
return nil, err
}
dataHash, err := asn1BitOrOctetBytes(tbsItems[3])
if err != nil {
return nil, err
}
return &sesSignature{
ToSign: append([]byte(nil), items[0].FullBytes...),
Cert: cert,
SignAlg: alg,
Signature: signature,
DataHash: dataHash,
Time: signatureTime,
Seal: seal,
}, nil
}
// parseSESSignatureV1 解析SES V1签章值
// 入参: items ASN.1子元素
// 返回: *sesSignature SES签章值, error 错误信息
func parseSESSignatureV1(items []asn1.RawValue) (*sesSignature, error) {
signature, err := asn1BitStringBytes(items[1])
if err != nil {
return nil, err
}
tbsItems, ok := asn1Children(items[0].Bytes)
if !ok || len(tbsItems) != 7 {
return nil, fmt.Errorf("invalid ses v1 toSign")
}
seal, err := parseSESSeal(tbsItems[1])
if err != nil {
return nil, err
}
version, err := asn1Integer(tbsItems[0])
if err != nil || version != seal.Info.Version {
return nil, fmt.Errorf("invalid ses version")
}
signatureTime := parseSESSignatureTimeV1(tbsItems[2])
dataHash, err := asn1BitOrOctetBytes(tbsItems[3])
if err != nil {
return nil, err
}
cert, err := asn1OctetString(tbsItems[5])
if err != nil {
return nil, err
}
alg, err := asn1OIDString(tbsItems[6])
if err != nil {
return nil, err
}
return &sesSignature{
ToSign: append([]byte(nil), items[0].FullBytes...),
Cert: cert,
SignAlg: alg,
Signature: signature,
DataHash: dataHash,
Time: signatureTime,
Seal: seal,
}, nil
}
// parseSESSignatureTimeV1 解析SES V1签名时间
// 入参: raw 签名时间或时间戳数据
// 返回: time.Time 签名时间
func parseSESSignatureTimeV1(raw asn1.RawValue) time.Time {
data, err := asn1BitOrOctetBytes(raw)
if err != nil {
return time.Time{}
}
if t := parseSignatureDateTime(string(data)); !t.IsZero() {
return t
}
var value asn1.RawValue
rest, err := asn1.Unmarshal(data, &value)
if err != nil || len(rest) != 0 {
return time.Time{}
}
t, _ := asn1Time(value)
return t
}
// verifySESSignature 验证SES签章值
// 入参: data 签章值数据, signedData 被签名数据原文, options 验证选项
// 返回: *sesVerifyResult 验证结果, error 错误信息
func verifySESSignature(data, signedData []byte, options *signatureVerifyOptions) (*sesVerifyResult, error) {
sig, err := parseSESSignature(data)
if err != nil {
return nil, err
}
if !isSM2SignatureMethod(sig.SignAlg) || !isSM2SignatureMethod(sig.Seal.SignAlg) {
return nil, fmt.Errorf("unsupported signature method")
}
result := &sesVerifyResult{}
result.SignCert = signatureCertInfo(sig.Cert)
result.SealCert = signatureCertInfo(sig.Seal.Cert)
result.SignCertRaw = sig.Cert
result.SealCertRaw = sig.Seal.Cert
result.SealRaw = sig.Seal.Raw
result.Certs = append(result.Certs, sig.Seal.CertList.Certs...)
result.Certs = append(result.Certs, options.SignCerts...)
result.SealType = sig.Seal.PicType
result.SealInfo = sig.Seal.Info
result.SignatureTime = sig.Time
result.DataHashOK = bytes.Equal(sig.DataHash, signSM3(signedData))
signPub, err := parseSM2PublicKeyFromCert(sig.Cert)
if err != nil {
return result, err
}
sealPub, err := parseSM2PublicKeyFromCert(sig.Seal.Cert)
if err != nil {
return result, err
}
result.SignedOK = sm2VerifySignature(signPub, nil, sig.ToSign, sig.Signature)
result.SealOK = sm2VerifySignature(sealPub, nil, sig.Seal.SignData, sig.Seal.Signature)
result.CertOK = sesCertInList(sig.Cert, sig.Seal.CertList)
return result, nil
}
// parseSESSeal 解析SES电子印章
// 入参: raw ASN.1原始值
// 返回: *sesSeal SES电子印章, error 错误信息
func parseSESSeal(raw asn1.RawValue) (*sesSeal, error) {
items, ok := asn1Children(raw.Bytes)
if !ok {
return nil, fmt.Errorf("invalid ses seal")
}
if len(items) == 2 {
return parseSESSealV1(raw, items)
}
if len(items) < 4 || len(items) > 5 {
return nil, fmt.Errorf("invalid ses seal")
}
cert, err := asn1OctetString(items[1])
if err != nil {
return nil, err
}
alg, err := asn1OIDString(items[2])
if err != nil {
return nil, err
}
signature, err := asn1BitStringBytes(items[3])
if err != nil {
return nil, err
}
infoItems, ok := asn1Children(items[0].Bytes)
if !ok || len(infoItems) < 4 || len(infoItems) > 5 {
return nil, fmt.Errorf("invalid ses seal info")
}
info, certList, err := parseSESSealInfo(infoItems)
if err != nil {
return nil, err
}
picType, picData, err := parseSESPicture(infoItems[3])
if err != nil {
return nil, err
}
return &sesSeal{
Raw: append([]byte(nil), raw.FullBytes...),
SignData: append([]byte(nil), items[0].FullBytes...),
Cert: cert,
SignAlg: alg,
Signature: signature,
PicType: picType,
PicData: picData,
CertList: certList,
Info: info,
}, nil
}
// parseSESSealV1 解析SES V1电子印章
// 入参: raw ASN.1原始值, items ASN.1子元素
// 返回: *sesSeal SES电子印章, error 错误信息
func parseSESSealV1(raw asn1.RawValue, items []asn1.RawValue) (*sesSeal, error) {
infoItems, ok := asn1Children(items[0].Bytes)
if !ok || len(infoItems) < 4 || len(infoItems) > 5 {
return nil, fmt.Errorf("invalid ses v1 seal info")
}
signItems, ok := asn1Children(items[1].Bytes)
if !ok || len(signItems) != 3 {
return nil, fmt.Errorf("invalid ses v1 sign info")
}
cert, err := asn1OctetString(signItems[0])
if err != nil {
return nil, err
}
alg, err := asn1OIDString(signItems[1])
if err != nil {
return nil, err
}
signature, err := asn1BitStringBytes(signItems[2])
if err != nil {
return nil, err
}
info, certList, err := parseSESSealInfo(infoItems)
if err != nil {
return nil, err
}
picType, picData, err := parseSESPicture(infoItems[3])
if err != nil {
return nil, err
}
signData := asn1SequenceBytes(items[0].FullBytes, signItems[0].FullBytes, signItems[1].FullBytes)
return &sesSeal{
Raw: append([]byte(nil), raw.FullBytes...),
SignData: signData,
Cert: cert,
SignAlg: alg,
Signature: signature,
PicType: picType,
PicData: picData,
CertList: certList,
Info: info,
}, nil
}
// parseSESSealInfo 解析电子印章信息
// 入参: items 印章信息ASN.1子元素
// 返回: SignatureSealInfo 印章信息, sesCertList 证书列表, error 错误信息
func parseSESSealInfo(items []asn1.RawValue) (SignatureSealInfo, sesCertList, error) {
if len(items) < 4 {
return SignatureSealInfo{}, sesCertList{}, fmt.Errorf("invalid ses seal info")
}
header, ok := asn1Children(items[0].Bytes)
if !ok || len(header) != 3 {
return SignatureSealInfo{}, sesCertList{}, fmt.Errorf("invalid ses header")
}
if asn1String(header[0]) != "ES" {
return SignatureSealInfo{}, sesCertList{}, fmt.Errorf("invalid ses header")
}
version, err := asn1Integer(header[1])
if err != nil {
return SignatureSealInfo{}, sesCertList{}, err
}
property, ok := asn1Children(items[2].Bytes)
if !ok {
return SignatureSealInfo{}, sesCertList{}, fmt.Errorf("invalid ses property")
}
certIndex := 2
timeIndex := 3
if version >= 4 {
certIndex = 3
timeIndex = 4
}
if len(property) != timeIndex+3 {
return SignatureSealInfo{}, sesCertList{}, fmt.Errorf("invalid ses property")
}
typeValue, err := asn1Integer(property[0])
if err != nil {
return SignatureSealInfo{}, sesCertList{}, err
}
certList, err := parseSESCertList(property, version, certIndex)
if err != nil {
return SignatureSealInfo{}, sesCertList{}, err
}
createTime, err := asn1Time(property[timeIndex])
if err != nil {
return SignatureSealInfo{}, sesCertList{}, err
}
validStart, err := asn1Time(property[timeIndex+1])
if err != nil {
return SignatureSealInfo{}, sesCertList{}, err
}
validEnd, err := asn1Time(property[timeIndex+2])
if err != nil {
return SignatureSealInfo{}, sesCertList{}, err
}
return SignatureSealInfo{
Version: version,
ID: strings.TrimSpace(asn1String(items[1])),
VendorID: strings.TrimSpace(asn1String(header[2])),
Type: typeValue,
Name: strings.TrimSpace(asn1String(property[1])),
CreateTime: createTime,
ValidStart: validStart,
ValidEnd: validEnd,
}, certList, nil
}
// parseSESCertList 解析印章证书列表
// 入参: property 印章属性, version 印章版本, certIndex 证书列表位置
// 返回: sesCertList 证书列表, error 错误信息
func parseSESCertList(property []asn1.RawValue, version, certIndex int) (sesCertList, error) {
if version == 1 {
return parseSESCertListV1(property[certIndex])
}
if version < 4 {
return parseSESCertInfoList(property[certIndex])
}
listType, err := asn1Integer(property[certIndex-1])
if err != nil {
return sesCertList{}, err
}
switch listType {
case 1:
return parseSESCertInfoList(property[certIndex])
case 2:
return parseSESCertDigestList(property[certIndex])
default:
return sesCertList{}, fmt.Errorf("unsupported ses cert list type")
}
}
// parseSESCertListV1 解析SES V1证书列表
// 入参: raw 证书列表ASN.1值
// 返回: sesCertList 证书列表, error 错误信息
func parseSESCertListV1(raw asn1.RawValue) (sesCertList, error) {
certs, ok := asn1Children(raw.Bytes)
if !ok || len(certs) == 0 {
return sesCertList{}, fmt.Errorf("invalid ses cert list")
}
list := sesCertList{Certs: make([][]byte, 0, len(certs))}
for _, item := range certs {
list.Certs = append(list.Certs, append([]byte(nil), item.FullBytes...))
}
return list, nil
}
// parseSESCertInfoList 解析SES证书信息列表
// 入参: raw 证书信息列表ASN.1值
// 返回: sesCertList 证书列表, error 错误信息
func parseSESCertInfoList(raw asn1.RawValue) (sesCertList, error) {
certs, ok := asn1Children(raw.Bytes)
if !ok || len(certs) == 0 {
return sesCertList{}, fmt.Errorf("invalid ses cert list")
}
list := sesCertList{Certs: make([][]byte, 0, len(certs))}
for _, item := range certs {
cert, err := asn1OctetString(item)
if err != nil {
return sesCertList{}, err
}
list.Certs = append(list.Certs, cert)
}
return list, nil
}
// parseSESCertDigestList 解析SES证书摘要列表
// 入参: raw 证书摘要列表ASN.1值
// 返回: sesCertList 证书列表, error 错误信息
func parseSESCertDigestList(raw asn1.RawValue) (sesCertList, error) {
items, ok := asn1Children(raw.Bytes)
if !ok || len(items) == 0 {
return sesCertList{}, fmt.Errorf("invalid ses cert digest list")
}
list := sesCertList{Digests: make([]sesCertDigest, 0, len(items))}
for _, item := range items {
fields, ok := asn1Children(item.Bytes)
if !ok || len(fields) != 2 {
return sesCertList{}, fmt.Errorf("invalid ses cert digest")
}
method := strings.TrimSpace(asn1String(fields[0]))
if method == "" {
return sesCertList{}, fmt.Errorf("invalid ses cert digest")
}
digest, err := asn1OctetString(fields[1])
if err != nil {
return sesCertList{}, err
}
list.Digests = append(list.Digests, sesCertDigest{Method: method, Value: digest})
}
return list, nil
}
// parseSESPicture 解析印章图片
// 入参: raw 印章图片信息
// 返回: string 图片类型, []byte 图片数据, error 错误信息
func parseSESPicture(raw asn1.RawValue) (string, []byte, error) {
items, ok := asn1Children(raw.Bytes)
if !ok || len(items) < 4 {
return "", nil, fmt.Errorf("invalid ses picture")
}
picType := normalizeSealType(strings.ToLower(strings.TrimSpace(asn1String(items[0]))))
picData, err := asn1OctetString(items[1])
if err != nil {
return "", nil, err
}
if picType == "ofd" {
if data := trimOFDPackage(picData); len(data) > 0 {
picData = data
}
}
return picType, picData, nil
}
// parseSM2PublicKeyFromCert 从证书解析SM2公钥
// 入参: data DER编码证书
// 返回: sm2PublicKey SM2公钥, error 错误信息
func parseSM2PublicKeyFromCert(data []byte) (sm2PublicKey, error) {
cert, err := parseSignatureCertificate(data)
if err != nil {
return sm2PublicKey{}, err
}
return parseSM2PublicKeyInfo(cert.PublicKey)
}
// parseSM2PublicKeyInfo 解析SM2公钥信息
// 入参: raw SubjectPublicKeyInfo原始值
// 返回: sm2PublicKey SM2公钥, error 错误信息
func parseSM2PublicKeyInfo(raw asn1.RawValue) (sm2PublicKey, error) {
var spki struct {
Algorithm asn1.RawValue
SubjectPublicKey asn1.BitString
}
rest, err := asn1.Unmarshal(raw.FullBytes, &spki)
if err != nil {
return sm2PublicKey{}, err
}
if len(rest) != 0 {
return sm2PublicKey{}, fmt.Errorf("invalid subject public key info")
}
algItems, ok := asn1Children(spki.Algorithm.Bytes)
if !ok || len(algItems) < 2 {
return sm2PublicKey{}, fmt.Errorf("invalid public key algorithm")
}
alg, err := asn1OIDString(algItems[0])
if err != nil {
return sm2PublicKey{}, err
}
curve, err := asn1OIDString(algItems[1])
if err != nil {
return sm2PublicKey{}, err
}
if alg != signECPublicKey || curve != signCurveSM2P256 {
return sm2PublicKey{}, fmt.Errorf("unsupported public key algorithm")
}
key := spki.SubjectPublicKey.Bytes
if len(key) != signPublicKeySize || key[0] != 4 {
return sm2PublicKey{}, fmt.Errorf("invalid sm2 public key")
}
return sm2PublicKey{
X: new(big.Int).SetBytes(key[1:33]),
Y: new(big.Int).SetBytes(key[33:65]),
}, nil
}
// signSM3 计算SM3摘要
// 入参: data 原文数据
// 返回: []byte 摘要值
func signSM3(data []byte) []byte {
h := newSM3()
h.Write(data)
return h.Sum(nil)
}
// isSM2SignatureMethod 判断是否为SM2签名算法
// 入参: method 算法标识
// 返回: bool 是否为SM2签名算法
func isSM2SignatureMethod(method string) bool {
switch signatureMethodText(method) {
case signMethodSM2SM3, signMethodSM2SM3B, signMethodSM2Sign, "SM2", "SM2SM3", "SM3SM2", "SM2WITHSM3", "SM3WITHSM2":
return true
default:
return false
}
}
// isSM3DigestMethod 判断是否为SM3摘要算法
// 入参: method 算法标识
// 返回: bool 是否为SM3摘要算法
func isSM3DigestMethod(method string) bool {
switch signatureMethodText(method) {
case signDigestSM3, signDigestSM3NoKey, signDigestSM3Key, "SM3":
return true
default:
return false
}
}
// sesCertInList 判断证书是否在印章证书列表中
// 入参: cert DER编码证书, list 证书列表
// 返回: bool 是否存在
func sesCertInList(cert []byte, list sesCertList) bool {
for _, item := range list.Certs {
if bytes.Equal(cert, item) {
return true
}
}
for _, item := range list.Digests {
digest, err := signatureDigest(item.Method, cert)
if err != nil {
continue
}
if bytes.Equal(digest, item.Value) {
return true
}
}
return false
}
// asn1OctetString 解析ASN.1八位字符串
// 入参: raw ASN.1原始值
// 返回: []byte 字节数据, error 错误信息
func asn1OctetString(raw asn1.RawValue) ([]byte, error) {
var out []byte
rest, err := asn1.Unmarshal(raw.FullBytes, &out)
if err != nil || len(rest) != 0 {
return nil, fmt.Errorf("invalid octet string")
}
return append([]byte(nil), out...), nil
}
// asn1BitStringBytes 解析ASN.1位字符串
// 入参: raw ASN.1原始值
// 返回: []byte 字节数据, error 错误信息
func asn1BitStringBytes(raw asn1.RawValue) ([]byte, error) {
var bits asn1.BitString
rest, err := asn1.Unmarshal(raw.FullBytes, &bits)
if err != nil || len(rest) != 0 || bits.BitLength%8 != 0 {
return nil, fmt.Errorf("invalid bit string")
}
return append([]byte(nil), bits.Bytes...), nil
}
// asn1BitOrOctetBytes 解析ASN.1位字符串或八位字符串
// 入参: raw ASN.1原始值
// 返回: []byte 字节数据, error 错误信息
func asn1BitOrOctetBytes(raw asn1.RawValue) ([]byte, error) {
if raw.Tag == asn1.TagOctetString {
return asn1OctetString(raw)
}
return asn1BitStringBytes(raw)
}
// asn1OIDString 解析ASN.1对象标识符
// 入参: raw ASN.1原始值
// 返回: string OID字符串, error 错误信息
func asn1OIDString(raw asn1.RawValue) (string, error) {
var oid asn1.ObjectIdentifier
rest, err := asn1.Unmarshal(raw.FullBytes, &oid)
if err != nil || len(rest) != 0 {
return "", fmt.Errorf("invalid oid")
}
return oid.String(), nil
}
// asn1Integer 解析ASN.1整数
// 入参: raw ASN.1原始值
// 返回: int 整数值, error 错误信息
func asn1Integer(raw asn1.RawValue) (int, error) {
var out int
rest, err := asn1.Unmarshal(raw.FullBytes, &out)
if err != nil || len(rest) != 0 {
return 0, fmt.Errorf("invalid integer")
}
return out, nil
}
// asn1String 解析ASN.1字符串
// 入参: raw ASN.1原始值
// 返回: string 字符串
func asn1String(raw asn1.RawValue) string {
var s string
if _, err := asn1.Unmarshal(raw.FullBytes, &s); err != nil {
return strings.TrimSpace(string(raw.Bytes))
}
return s
}
// asn1SequenceBytes 编码ASN.1序列
// 入参: parts 序列元素DER数据
// 返回: []byte ASN.1序列DER数据
func asn1SequenceBytes(parts ...[]byte) []byte {
var content []byte
for _, part := range parts {
content = append(content, part...)
}
return asn1Wrap(0x30, content)
}
// asn1SetBytes 编码ASN.1集合
// 入参: content 集合内容DER数据
// 返回: []byte ASN.1集合DER数据
func asn1SetBytes(content []byte) []byte {
return asn1Wrap(0x31, content)
}
// asn1Wrap 包装ASN.1标签
// 入参: tag ASN.1标签, content 内容DER数据
// 返回: []byte ASN.1 DER数据
func asn1Wrap(tag byte, content []byte) []byte {
out := []byte{tag}
out = append(out, asn1LengthBytes(len(content))...)
out = append(out, content...)
return out
}
// asn1LengthBytes 编码ASN.1长度
// 入参: n 内容长度
// 返回: []byte ASN.1长度编码
func asn1LengthBytes(n int) []byte {
if n < 128 {
return []byte{byte(n)}
}
var buf [8]byte
i := len(buf)
for n > 0 {
i--
buf[i] = byte(n)
n >>= 8
}
out := []byte{0x80 | byte(len(buf)-i)}
return append(out, buf[i:]...)
}
+326
View File
@@ -0,0 +1,326 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"encoding/asn1"
"encoding/binary"
"math/big"
)
const sm2DefaultUserID = "1234567812345678"
var sm2P256 = newSM2P256()
// sm2PublicKey SM2公钥
type sm2PublicKey struct {
X *big.Int
Y *big.Int
}
// sm2Curve SM2椭圆曲线
type sm2Curve struct {
P *big.Int
N *big.Int
B *big.Int
Gx *big.Int
Gy *big.Int
}
// sm2Point SM2雅可比坐标点
type sm2Point struct {
X *big.Int
Y *big.Int
Z *big.Int
}
// newSM2P256 创建SM2椭圆曲线
// 返回: *sm2Curve SM2椭圆曲线
func newSM2P256() *sm2Curve {
return &sm2Curve{
P: sm2Big("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF"),
N: sm2Big("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123"),
B: sm2Big("28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93"),
Gx: sm2Big("32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7"),
Gy: sm2Big("BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0"),
}
}
// sm2Big 解析SM2大整数常量
// 入参: s 十六进制字符串
// 返回: *big.Int 大整数
func sm2Big(s string) *big.Int {
n, _ := new(big.Int).SetString(s, 16)
return n
}
// sm2VerifySignature 验证SM2签名值
// 入参: pub 公钥, userID 用户标识, msg 原文, sig 签名值
// 返回: bool 是否验证通过
func sm2VerifySignature(pub sm2PublicKey, userID, msg, sig []byte) bool {
r, s, ok := parseSM2Signature(sig)
if !ok {
return false
}
return sm2Verify(pub, userID, msg, r, s)
}
// parseSM2Signature 解析SM2签名值
// 入参: sig 签名值
// 返回: *big.Int R值, *big.Int S值, bool 是否解析成功
func parseSM2Signature(sig []byte) (*big.Int, *big.Int, bool) {
if len(sig) == 64 {
return new(big.Int).SetBytes(sig[:32]), new(big.Int).SetBytes(sig[32:]), true
}
var rs struct {
R *big.Int
S *big.Int
}
rest, err := asn1.Unmarshal(sig, &rs)
if err != nil || len(rest) != 0 || rs.R == nil || rs.S == nil {
return nil, nil, false
}
return rs.R, rs.S, true
}
// sm2Verify 验证SM2签名
// 入参: pub 公钥, userID 用户标识, msg 原文, r R值, s S值
// 返回: bool 是否验证通过
func sm2Verify(pub sm2PublicKey, userID, msg []byte, r, s *big.Int) bool {
n := sm2P256.N
if r.Sign() <= 0 || s.Sign() <= 0 || r.Cmp(n) >= 0 || s.Cmp(n) >= 0 {
return false
}
if pub.X == nil || pub.Y == nil || !sm2P256.isOnCurve(pub.X, pub.Y) {
return false
}
e := new(big.Int).SetBytes(sm2MessageDigest(pub, userID, msg))
t := new(big.Int).Add(r, s)
t.Mod(t, n)
if t.Sign() == 0 {
return false
}
x, ok := sm2P256.combinedMult(pub.X, pub.Y, s, t)
if !ok {
return false
}
v := new(big.Int).Add(e, x)
v.Mod(v, n)
return v.Cmp(r) == 0
}
// sm2MessageDigest 计算SM2签名摘要
// 入参: pub 公钥, userID 用户标识, msg 原文
// 返回: []byte 摘要值
func sm2MessageDigest(pub sm2PublicKey, userID, msg []byte) []byte {
h := newSM3()
h.Write(sm2ZA(pub, userID))
h.Write(msg)
return h.Sum(nil)
}
// sm2ZA 计算SM2用户标识杂凑值
// 入参: pub 公钥, userID 用户标识
// 返回: []byte ZA值
func sm2ZA(pub sm2PublicKey, userID []byte) []byte {
if len(userID) == 0 {
userID = []byte(sm2DefaultUserID)
}
h := newSM3()
var entl [2]byte
binary.BigEndian.PutUint16(entl[:], uint16(len(userID)*8))
h.Write(entl[:])
h.Write(userID)
h.Write(sm2Fixed(sm2A()))
h.Write(sm2Fixed(sm2P256.B))
h.Write(sm2Fixed(sm2P256.Gx))
h.Write(sm2Fixed(sm2P256.Gy))
h.Write(sm2Fixed(pub.X))
h.Write(sm2Fixed(pub.Y))
return h.Sum(nil)
}
// sm2A 获取SM2曲线A参数
// 返回: *big.Int 曲线A参数
func sm2A() *big.Int {
return new(big.Int).Sub(sm2P256.P, big.NewInt(3))
}
// isOnCurve 判断点是否位于SM2曲线
// 入参: x X坐标, y Y坐标
// 返回: bool 是否位于曲线
func (c *sm2Curve) isOnCurve(x, y *big.Int) bool {
if x.Sign() < 0 || y.Sign() < 0 || x.Cmp(c.P) >= 0 || y.Cmp(c.P) >= 0 {
return false
}
left := c.fieldSquare(y)
right := c.fieldAdd(c.fieldSub(c.fieldMul(c.fieldSquare(x), x), c.fieldScale(x, 3)), c.B)
return left.Cmp(right) == 0
}
// combinedMult 计算sG+tP
// 入参: x 公钥X坐标, y 公钥Y坐标, s 标量S, t 标量T
// 返回: *big.Int 结果X坐标, bool 是否计算成功
func (c *sm2Curve) combinedMult(x, y, s, t *big.Int) (*big.Int, bool) {
base := c.scalarMult(c.Gx, c.Gy, s.Bytes())
public := c.scalarMult(x, y, t.Bytes())
result := c.add(base, public)
affineX, _, ok := c.affine(result)
return affineX, ok
}
// scalarMult 计算椭圆曲线标量乘法
// 入参: x 点X坐标, y 点Y坐标, scalar 标量
// 返回: sm2Point 雅可比坐标点
func (c *sm2Curve) scalarMult(x, y *big.Int, scalar []byte) sm2Point {
result := c.infinity()
point := sm2Point{X: new(big.Int).Set(x), Y: new(big.Int).Set(y), Z: big.NewInt(1)}
for _, value := range scalar {
for bit := 7; bit >= 0; bit-- {
result = c.double(result)
if value&(1<<uint(bit)) != 0 {
result = c.add(result, point)
}
}
}
return result
}
// add 计算椭圆曲线点加法
// 入参: p 点P, q 点Q
// 返回: sm2Point 结果点
func (c *sm2Curve) add(p, q sm2Point) sm2Point {
if p.Z.Sign() == 0 {
return q
}
if q.Z.Sign() == 0 {
return p
}
z1z1 := c.fieldSquare(p.Z)
z2z2 := c.fieldSquare(q.Z)
u1 := c.fieldMul(p.X, z2z2)
u2 := c.fieldMul(q.X, z1z1)
s1 := c.fieldMul(p.Y, c.fieldMul(q.Z, z2z2))
s2 := c.fieldMul(q.Y, c.fieldMul(p.Z, z1z1))
if u1.Cmp(u2) == 0 {
if s1.Cmp(s2) != 0 {
return c.infinity()
}
return c.double(p)
}
h := c.fieldSub(u2, u1)
i := c.fieldSquare(c.fieldScale(h, 2))
j := c.fieldMul(h, i)
r := c.fieldScale(c.fieldSub(s2, s1), 2)
v := c.fieldMul(u1, i)
x := c.fieldSub(c.fieldSub(c.fieldSquare(r), j), c.fieldScale(v, 2))
y := c.fieldSub(c.fieldMul(r, c.fieldSub(v, x)), c.fieldScale(c.fieldMul(s1, j), 2))
z := c.fieldMul(c.fieldSub(c.fieldSub(c.fieldSquare(c.fieldAdd(p.Z, q.Z)), z1z1), z2z2), h)
return sm2Point{X: x, Y: y, Z: z}
}
// double 计算椭圆曲线点倍加
// 入参: p 点P
// 返回: sm2Point 结果点
func (c *sm2Curve) double(p sm2Point) sm2Point {
if p.Z.Sign() == 0 || p.Y.Sign() == 0 {
return c.infinity()
}
delta := c.fieldSquare(p.Z)
gamma := c.fieldSquare(p.Y)
beta := c.fieldMul(p.X, gamma)
alpha := c.fieldScale(c.fieldMul(c.fieldSub(p.X, delta), c.fieldAdd(p.X, delta)), 3)
x := c.fieldSub(c.fieldSquare(alpha), c.fieldScale(beta, 8))
z := c.fieldSub(c.fieldSub(c.fieldSquare(c.fieldAdd(p.Y, p.Z)), gamma), delta)
y := c.fieldSub(c.fieldMul(alpha, c.fieldSub(c.fieldScale(beta, 4), x)), c.fieldScale(c.fieldSquare(gamma), 8))
return sm2Point{X: x, Y: y, Z: z}
}
// affine 将雅可比坐标转换为仿射坐标
// 入参: p 雅可比坐标点
// 返回: *big.Int X坐标, *big.Int Y坐标, bool 是否转换成功
func (c *sm2Curve) affine(p sm2Point) (*big.Int, *big.Int, bool) {
if p.Z.Sign() == 0 {
return nil, nil, false
}
z := new(big.Int).ModInverse(p.Z, c.P)
if z == nil {
return nil, nil, false
}
z2 := c.fieldSquare(z)
x := c.fieldMul(p.X, z2)
y := c.fieldMul(p.Y, c.fieldMul(z2, z))
return x, y, true
}
// infinity 获取无穷远点
// 返回: sm2Point 无穷远点
func (c *sm2Curve) infinity() sm2Point {
return sm2Point{X: new(big.Int), Y: new(big.Int), Z: new(big.Int)}
}
// fieldAdd 计算有限域加法
// 入参: x 左操作数, y 右操作数
// 返回: *big.Int 计算结果
func (c *sm2Curve) fieldAdd(x, y *big.Int) *big.Int {
value := new(big.Int).Add(x, y)
return value.Mod(value, c.P)
}
// fieldSub 计算有限域减法
// 入参: x 左操作数, y 右操作数
// 返回: *big.Int 计算结果
func (c *sm2Curve) fieldSub(x, y *big.Int) *big.Int {
value := new(big.Int).Sub(x, y)
return value.Mod(value, c.P)
}
// fieldMul 计算有限域乘法
// 入参: x 左操作数, y 右操作数
// 返回: *big.Int 计算结果
func (c *sm2Curve) fieldMul(x, y *big.Int) *big.Int {
value := new(big.Int).Mul(x, y)
return value.Mod(value, c.P)
}
// fieldSquare 计算有限域平方
// 入参: x 操作数
// 返回: *big.Int 计算结果
func (c *sm2Curve) fieldSquare(x *big.Int) *big.Int {
return c.fieldMul(x, x)
}
// fieldScale 计算有限域整数倍
// 入参: x 操作数, scale 倍数
// 返回: *big.Int 计算结果
func (c *sm2Curve) fieldScale(x *big.Int, scale int64) *big.Int {
return c.fieldMul(x, big.NewInt(scale))
}
// sm2Fixed 转换为SM2固定长度字节
// 入参: n 大整数
// 返回: []byte 固定长度字节
func sm2Fixed(n *big.Int) []byte {
out := make([]byte, 32)
if n == nil {
return out
}
b := n.Bytes()
if len(b) > len(out) {
b = b[len(b)-len(out):]
}
copy(out[len(out)-len(b):], b)
return out
}
+211
View File
@@ -0,0 +1,211 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"encoding/binary"
"hash"
"math/bits"
)
const (
sm3Size = 32
sm3BlockSize = 64
)
var sm3Init = [8]uint32{
0x7380166f,
0x4914b2b9,
0x172442d7,
0xda8a0600,
0xa96f30bc,
0x163138aa,
0xe38dee4d,
0xb0fb0e4e,
}
// sm3Digest SM3杂凑值计算器
type sm3Digest struct {
h [8]uint32
x [sm3BlockSize]byte
nx int
len uint64
}
// newSM3 创建SM3杂凑值计算器
// 返回: hash.Hash SM3杂凑值计算器
func newSM3() hash.Hash {
d := new(sm3Digest)
d.Reset()
return d
}
// Reset 重置SM3状态
func (d *sm3Digest) Reset() {
d.h = sm3Init
d.nx = 0
d.len = 0
}
// Size 获取SM3杂凑值长度
// 返回: int 杂凑值长度
func (d *sm3Digest) Size() int {
return sm3Size
}
// BlockSize 获取SM3分组长度
// 返回: int 分组长度
func (d *sm3Digest) BlockSize() int {
return sm3BlockSize
}
// Write 写入待计算数据
// 入参: p 待计算数据
// 返回: int 写入长度, error 错误信息
func (d *sm3Digest) Write(p []byte) (int, error) {
nn := len(p)
d.len += uint64(nn)
if d.nx > 0 {
n := copy(d.x[d.nx:], p)
d.nx += n
if d.nx == sm3BlockSize {
sm3Block(d, d.x[:])
d.nx = 0
}
p = p[n:]
}
if len(p) >= sm3BlockSize {
n := len(p) &^ (sm3BlockSize - 1)
sm3Block(d, p[:n])
p = p[n:]
}
if len(p) > 0 {
d.nx = copy(d.x[:], p)
}
return nn, nil
}
// Sum 返回SM3杂凑值
// 入参: in 前缀数据
// 返回: []byte 杂凑值
func (d *sm3Digest) Sum(in []byte) []byte {
dd := *d
hash := dd.checkSum()
return append(in, hash[:]...)
}
// checkSum 计算SM3最终杂凑值
// 返回: [sm3Size]byte 杂凑值
func (d *sm3Digest) checkSum() [sm3Size]byte {
lenBits := d.len << 3
var tmp [64]byte
tmp[0] = 0x80
if d.nx < 56 {
d.Write(tmp[:56-d.nx])
} else {
d.Write(tmp[:64+56-d.nx])
}
binary.BigEndian.PutUint64(tmp[:8], lenBits)
d.Write(tmp[:8])
var digest [sm3Size]byte
for i, v := range d.h {
binary.BigEndian.PutUint32(digest[i*4:], v)
}
return digest
}
// sm3Block 处理SM3消息分组
// 入参: d SM3杂凑值计算器, p 消息分组数据
func sm3Block(d *sm3Digest, p []byte) {
var w [68]uint32
var w1 [64]uint32
for len(p) >= sm3BlockSize {
for i := 0; i < 16; i++ {
w[i] = binary.BigEndian.Uint32(p[i*4:])
}
for i := 16; i < 68; i++ {
x := w[i-16] ^ w[i-9] ^ bits.RotateLeft32(w[i-3], 15)
w[i] = sm3P1(x) ^ bits.RotateLeft32(w[i-13], 7) ^ w[i-6]
}
for i := 0; i < 64; i++ {
w1[i] = w[i] ^ w[i+4]
}
a, b, c, e := d.h[0], d.h[1], d.h[2], d.h[4]
dd, f, g, hh := d.h[3], d.h[5], d.h[6], d.h[7]
for i := 0; i < 64; i++ {
t := uint32(0x7a879d8a)
if i < 16 {
t = 0x79cc4519
}
ss1 := bits.RotateLeft32(bits.RotateLeft32(a, 12)+e+bits.RotateLeft32(t, i), 7)
ss2 := ss1 ^ bits.RotateLeft32(a, 12)
tt1 := sm3FF(i, a, b, c) + dd + ss2 + w1[i]
tt2 := sm3GG(i, e, f, g) + hh + ss1 + w[i]
dd = c
c = bits.RotateLeft32(b, 9)
b = a
a = tt1
hh = g
g = bits.RotateLeft32(f, 19)
f = e
e = sm3P0(tt2)
}
d.h[0] ^= a
d.h[1] ^= b
d.h[2] ^= c
d.h[3] ^= dd
d.h[4] ^= e
d.h[5] ^= f
d.h[6] ^= g
d.h[7] ^= hh
p = p[sm3BlockSize:]
}
}
// sm3P0 计算SM3置换函数P0
// 入参: x 输入值
// 返回: uint32 置换结果
func sm3P0(x uint32) uint32 {
return x ^ bits.RotateLeft32(x, 9) ^ bits.RotateLeft32(x, 17)
}
// sm3P1 计算SM3置换函数P1
// 入参: x 输入值
// 返回: uint32 置换结果
func sm3P1(x uint32) uint32 {
return x ^ bits.RotateLeft32(x, 15) ^ bits.RotateLeft32(x, 23)
}
// sm3FF 计算SM3布尔函数FF
// 入参: i 轮数, x 输入值, y 输入值, z 输入值
// 返回: uint32 计算结果
func sm3FF(i int, x, y, z uint32) uint32 {
if i < 16 {
return x ^ y ^ z
}
return (x & y) | (x & z) | (y & z)
}
// sm3GG 计算SM3布尔函数GG
// 入参: i 轮数, x 输入值, y 输入值, z 输入值
// 返回: uint32 计算结果
func sm3GG(i int, x, y, z uint32) uint32 {
if i < 16 {
return x ^ y ^ z
}
return (x & y) | (^x & z)
}
var _ hash.Hash = (*sm3Digest)(nil)
+1226
View File
File diff suppressed because it is too large Load Diff
+289 -300
View File
@@ -15,333 +15,322 @@
package ofdgo
import (
"image/color"
"math"
"strconv"
"strings"
"github.com/tdewolff/canvas"
canvastext "github.com/tdewolff/canvas/text"
"github.com/tdewolff/font"
)
// parseColor 解析颜色字符串
// 入参: val 颜色值(R G B)
// 返回: color.Color 颜色对象
func parseColor(val string) color.Color {
return parseColorWithAlpha(val, nil)
// textGlyph 绘制字形
type textGlyph struct {
Text string
GlyphID int
}
// parseColorWithAlpha 解析带透明度的颜色
// 入参: val 颜色值, alpha 透明度
// 返回: color.Color 颜色对象
func parseColorWithAlpha(val string, alpha *int) color.Color {
parts := strings.Fields(val)
if len(parts) >= 3 {
r := parseColorComponent(parts[0])
g := parseColorComponent(parts[1])
b := parseColorComponent(parts[2])
a := 255
if alpha != nil {
a = *alpha
}
a = clampColor(a)
return color.RGBA{
R: uint8(clampColor(r) * a / 255),
G: uint8(clampColor(g) * a / 255),
B: uint8(clampColor(b) * a / 255),
A: uint8(a),
// textGlyphPathCacheKey 字形路径缓存键
type textGlyphPathCacheKey struct {
font *canvas.Font
size float64
fauxBold float64
fauxItalic float64
xOffset int32
yOffset int32
language string
script canvastext.Script
direction canvastext.Direction
glyph textGlyph
}
// textGlyphPathCacheValue 字形路径缓存值
type textGlyphPathCacheValue struct {
path *canvas.Path
width float64
}
// textGlyphTransform 字符到字形变换
type textGlyphTransform struct {
CodeCount int
Glyphs []textGlyph
}
// textObjectFontID 获取文本对象字体ID
// 入参: text 文本对象
// 返回: string 字体ID
func (r *Renderer) textObjectFontID(text TextObject) string {
fontID := text.Font
if fontID == "" && text.DrawParam != "" {
if dp := r.getDrawParam(text.DrawParam, nil); dp != nil && dp.Font != "" {
fontID = dp.Font
}
}
return color.Black
return fontID
}
// parseColorComponent 解析颜色分量
// 入参: s 颜色分量
// 返回: int 颜色分量值
func parseColorComponent(s string) int {
s = strings.TrimSpace(s)
if strings.HasPrefix(s, "#") {
v, _ := strconv.ParseInt(strings.TrimPrefix(s, "#"), 16, 0)
return int(v)
// textObjectGlyphTransforms 获取文本对象的字形变换
// 入参: fontID 字体ID, text 文本对象
// 返回: map[int]textGlyphTransform 文本位置到字形变换的映射
func (r *Renderer) textObjectGlyphTransforms(fontID string, text TextObject) map[int]textGlyphTransform {
if fontID == "" || len(text.CGTransform) == 0 {
return nil
}
v, _ := strconv.Atoi(s)
return v
result := make(map[int]textGlyphTransform, len(text.CGTransform))
for _, transform := range text.CGTransform {
ids := parseInts(transform.Glyphs)
glyphCount := len(ids)
if transform.GlyphCount > 0 && transform.GlyphCount < glyphCount {
glyphCount = transform.GlyphCount
}
if glyphCount == 0 {
continue
}
codeCount := transform.CodeCount
if codeCount <= 0 {
codeCount = 1
}
glyphs := make([]textGlyph, glyphCount)
for i := range glyphs {
glyphs[i] = r.textGlyphFromID(fontID, ids[i])
}
result[transform.CodePosition] = textGlyphTransform{
CodeCount: codeCount,
Glyphs: glyphs,
}
}
if len(result) == 0 {
return nil
}
return result
}
// clampColor 限制颜色分量范围
// 入参: v 颜色分量
// 返回: int 颜色分量
func clampColor(v int) int {
if v < 0 {
// textCodeGlyphs 获取文本编码对应的绘制字形
// 入参: runes 文本字符, transforms 字形变换, codeOffset 文本编码偏移
// 返回: []textGlyph 绘制字形列表
func textCodeGlyphs(runes []rune, transforms map[int]textGlyphTransform, codeOffset int) []textGlyph {
if len(transforms) == 0 {
return textRuneGlyphs(runes)
}
glyphs := make([]textGlyph, 0, len(runes))
for i := 0; i < len(runes); {
if transform, ok := transforms[codeOffset+i]; ok && i+transform.CodeCount <= len(runes) {
glyphs = append(glyphs, transform.Glyphs...)
i += transform.CodeCount
continue
}
glyphs = append(glyphs, textGlyph{Text: string(runes[i]), GlyphID: -1})
i++
}
return glyphs
}
// textRuneGlyphs 转换文本字符为绘制字形
// 入参: runes 文本字符
// 返回: []textGlyph 绘制字形列表
func textRuneGlyphs(runes []rune) []textGlyph {
glyphs := make([]textGlyph, len(runes))
for i, r := range runes {
glyphs[i] = textGlyph{Text: string(r), GlyphID: -1}
}
return glyphs
}
// textGlyphFromID 获取字形ID对应的绘制字形
// 入参: fontID 字体ID, glyphID 字形ID或CID
// 返回: textGlyph 绘制字形
func (r *Renderer) textGlyphFromID(fontID string, glyphID int) textGlyph {
if mapped, ok := r.fontGlyphRune(fontID, glyphID); ok {
return textGlyph{Text: string(mapped), GlyphID: -1}
}
return textGlyph{GlyphID: glyphID}
}
// textGlyphWidth 获取绘制字形宽度
// 入参: face 字体, glyph 绘制字形
// 返回: float64 字形宽度
func textGlyphWidth(face *canvas.FontFace, glyph textGlyph) float64 {
if glyph.GlyphID >= 0 && glyph.GlyphID <= 0xFFFF {
return face.MmPerEm * float64(face.Font.GlyphAdvance(uint16(glyph.GlyphID)))
}
return face.TextWidth(glyph.Text)
}
// textGlyphPath 获取绘制字形路径
// 入参: face 字体, glyph 绘制字形
// 返回: *canvas.Path 字形路径, float64 字形宽度
func textGlyphPath(face *canvas.FontFace, glyph textGlyph) (*canvas.Path, float64) {
if glyph.GlyphID < 0 || glyph.GlyphID > 0xFFFF {
return face.ToPath(glyph.Text)
}
p := &canvas.Path{}
glyphID := uint16(glyph.GlyphID)
_ = face.Font.GlyphPath(p, glyphID, face.PPEM(canvas.DefaultResolution), 0, 0, face.MmPerEm, font.NoHinting)
if face.FauxBold != 0 {
d := face.FauxBold * face.Size
if face.Font.IsTrueType {
d = -d
}
origFastStroke := canvas.FastStroke
canvas.FastStroke = true
p = p.Offset(d, canvas.Tolerance)
canvas.FastStroke = origFastStroke
}
if face.FauxItalic != 0 {
p = p.Transform(canvas.Identity.Shear(face.FauxItalic, 0))
}
return p, face.MmPerEm * float64(face.Font.GlyphAdvance(glyphID))
}
// cachedTextGlyphPath 获取缓存的字形路径
// 入参: face 字体, glyph 绘制字形
// 返回: *canvas.Path 字形路径, float64 字形宽度
func (r *Renderer) cachedTextGlyphPath(face *canvas.FontFace, glyph textGlyph) (*canvas.Path, float64) {
key := textGlyphPathCacheKey{
font: face.Font,
size: face.Size,
fauxBold: face.FauxBold,
fauxItalic: face.FauxItalic,
xOffset: face.XOffset,
yOffset: face.YOffset,
language: face.Language,
script: face.Script,
direction: face.Direction,
glyph: glyph,
}
if cached, ok := r.textGlyphPathCache[key]; ok {
return cached.path, cached.width
}
path, width := textGlyphPath(face, glyph)
r.textGlyphPathCache[key] = textGlyphPathCacheValue{path: path, width: width}
return path, width
}
// hasTextMatrix 判断文本是否需要应用字形变换
// 入参: ctm 变换矩阵
// 返回: bool 是否需要变换
func hasTextMatrix(ctm Matrix) bool {
const eps = 1e-9
return math.Abs(ctm.a-1) > eps || math.Abs(ctm.b) > eps || math.Abs(ctm.c) > eps || math.Abs(ctm.d-1) > eps
}
// textMatrix 获取文本字形变换矩阵
// 入参: ctm OFD变换矩阵
// 返回: canvas.Matrix 画布变换矩阵
func textMatrix(ctm Matrix) canvas.Matrix {
return canvas.Matrix{
{ctm.a, -ctm.c, 0},
{-ctm.b, ctm.d, 0},
}
}
// fontGlyphRune 获取字形ID对应的包装字体字符
// 入参: fontID 字体ID, glyphID 字形ID或CID
// 返回: rune 包装字体字符, bool 是否存在
func (r *Renderer) fontGlyphRune(fontID string, glyphID int) (rune, bool) {
if glyphID < 0 || glyphID > 0xFFFF {
return 0, false
}
id := uint16(glyphID)
if r.FontCIDMap != nil {
if mapping := r.FontCIDMap[fontID]; mapping != nil {
if mapped, ok := mapping[id]; ok {
return mapped, true
}
}
}
if r.FontGIDMap != nil {
if mapping := r.FontGIDMap[fontID]; mapping != nil {
if mapped, ok := mapping[id]; ok {
return mapped, true
}
}
}
return 0, false
}
// parseIndexRunes 解析索引字形
// 入参: indexStr 索引字符串, fontID 字体ID
// 返回: []rune 字形列表
func (r *Renderer) parseIndexRunes(indexStr string, fontID string) []rune {
parts := strings.Fields(indexStr)
result := make([]rune, 0, len(parts))
for _, p := range parts {
if startText, endText, ok := strings.Cut(p, "-"); ok {
if !strings.Contains(endText, "-") {
start, _ := strconv.Atoi(startText)
end, _ := strconv.Atoi(endText)
for k := start; k <= end; k++ {
result = append(result, r.textIndexRune(fontID, k))
}
}
} else {
val, _ := strconv.Atoi(p)
result = append(result, r.textIndexRune(fontID, val))
}
}
return result
}
// textIndexRune 获取索引字形对应的字符
// 入参: fontID 字体ID, glyphID 字形ID或CID
// 返回: rune 字符
func (r *Renderer) textIndexRune(fontID string, glyphID int) rune {
if mapped, ok := r.fontGlyphRune(fontID, glyphID); ok {
return mapped
}
return rune(glyphID)
}
// textGlyphAdvanceLimit 获取显式字形推进宽度
// 入参: dxs X方向偏移, dys Y方向偏移, xs X坐标列表, index 字形索引, count 字形数量, currentX 当前X坐标
// 返回: float64 推进宽度
func textGlyphAdvanceLimit(dxs, dys, xs []float64, index int, count int, currentX float64) float64 {
if index+1 >= count || len(dys) > 0 {
return 0
}
if v > 255 {
return 255
}
return v
}
// mergeAlpha 合并透明度
// 入参: colorAlpha 颜色透明度, objectAlpha 对象透明度
// 返回: *int 合并后的透明度
func mergeAlpha(colorAlpha, objectAlpha *int) *int {
if colorAlpha == nil && objectAlpha == nil {
return nil
}
alpha := 255
if colorAlpha != nil {
alpha = *colorAlpha
}
if objectAlpha != nil {
alpha = alpha * *objectAlpha / 255
}
alpha = clampColor(alpha)
return &alpha
}
// withFillAlpha 合并填充色透明度
// 入参: fillColor 填充颜色节点, alpha 对象透明度
// 返回: *FillColor 合并后的填充颜色节点
func withFillAlpha(fillColor *FillColor, alpha *int) *FillColor {
if fillColor == nil || alpha == nil {
return fillColor
}
merged := *fillColor
merged.Alpha = mergeAlpha(fillColor.Alpha, alpha)
return &merged
}
// withStrokeAlpha 合并勾边色透明度
// 入参: strokeColor 勾边颜色节点, alpha 对象透明度
// 返回: *StrokeColor 合并后的勾边颜色节点
func withStrokeAlpha(strokeColor *StrokeColor, alpha *int) *StrokeColor {
if strokeColor == nil || alpha == nil {
return strokeColor
}
merged := *strokeColor
merged.Alpha = mergeAlpha(strokeColor.Alpha, alpha)
return &merged
}
// colorWithAlpha 合并颜色透明度
// 入参: c 颜色对象, alpha 对象透明度
// 返回: color.Color 合并后的颜色对象
func colorWithAlpha(c color.Color, alpha *int) color.Color {
if c == nil || alpha == nil {
return c
}
a := clampColor(*alpha)
rgba := colorToRGBA(c)
return color.RGBA{
R: uint8(int(rgba.R) * a / 255),
G: uint8(int(rgba.G) * a / 255),
B: uint8(int(rgba.B) * a / 255),
A: uint8(int(rgba.A) * a / 255),
}
}
// parseFillColor 解析填充颜色
// 入参: fillColor 填充颜色节点
// 返回: color.Color 颜色对象
func parseFillColor(fillColor *FillColor) color.Color {
if fillColor == nil {
return nil
}
if fillColor.Pattern != nil {
return nil
}
if fillColor.AxialShd != nil {
return parseAxialShdColor(fillColor.AxialShd, fillColor.Alpha)
}
if fillColor.RadialShd != nil {
return parseRadialShdColor(fillColor.RadialShd, fillColor.Alpha)
}
if strings.TrimSpace(fillColor.Value) != "" {
return parseColorWithAlpha(fillColor.Value, fillColor.Alpha)
}
return nil
}
// parseFillPaint 解析填充画刷
// 入参: fillColor 填充颜色节点, x X坐标, y Y坐标, pageH 页面高度, originX 原点X坐标, originY 原点Y坐标
// 返回: any 填充画刷
func parseFillPaint(fillColor *FillColor, x, y, pageH, originX, originY float64) any {
if fillColor == nil {
return nil
}
if fillColor.Pattern != nil {
return nil
}
if gradient := parseAxialShdGradient(fillColor.AxialShd, fillColor.Alpha, x, y, pageH, originX, originY); gradient != nil {
return gradient
}
if gradient := parseRadialShdGradient(fillColor.RadialShd, fillColor.Alpha, x, y, pageH, originX, originY); gradient != nil {
return gradient
}
if fillColor.AxialShd != nil {
return parseAxialShdColor(fillColor.AxialShd, fillColor.Alpha)
}
if fillColor.RadialShd != nil {
return parseRadialShdColor(fillColor.RadialShd, fillColor.Alpha)
}
if strings.TrimSpace(fillColor.Value) != "" {
return parseColorWithAlpha(fillColor.Value, fillColor.Alpha)
}
return nil
}
// parseStrokeColor 解析勾边颜色
// 入参: strokeColor 勾边颜色节点
// 返回: color.Color 颜色对象
func parseStrokeColor(strokeColor *StrokeColor) color.Color {
if strokeColor == nil {
return nil
}
if strokeColor.AxialShd != nil {
return parseAxialShdColor(strokeColor.AxialShd, strokeColor.Alpha)
}
if strokeColor.RadialShd != nil {
return parseRadialShdColor(strokeColor.RadialShd, strokeColor.Alpha)
}
if strings.TrimSpace(strokeColor.Value) != "" {
return parseColorWithAlpha(strokeColor.Value, strokeColor.Alpha)
}
return nil
}
// parseStrokePaint 解析勾边画刷
// 入参: strokeColor 勾边颜色节点, x X坐标, y Y坐标, pageH 页面高度, originX 原点X坐标, originY 原点Y坐标
// 返回: any 勾边画刷
func parseStrokePaint(strokeColor *StrokeColor, x, y, pageH, originX, originY float64) any {
if strokeColor == nil {
return nil
}
if gradient := parseAxialShdGradient(strokeColor.AxialShd, strokeColor.Alpha, x, y, pageH, originX, originY); gradient != nil {
return gradient
}
if gradient := parseRadialShdGradient(strokeColor.RadialShd, strokeColor.Alpha, x, y, pageH, originX, originY); gradient != nil {
return gradient
}
if strokeColor.AxialShd != nil {
return parseAxialShdColor(strokeColor.AxialShd, strokeColor.Alpha)
}
if strokeColor.RadialShd != nil {
return parseRadialShdColor(strokeColor.RadialShd, strokeColor.Alpha)
}
if strings.TrimSpace(strokeColor.Value) != "" {
return parseColorWithAlpha(strokeColor.Value, strokeColor.Alpha)
}
return nil
}
// parseAxialShdColor 解析轴向渐变颜色
// 入参: axialShd 轴向渐变节点, alpha 透明度
// 返回: color.Color 颜色对象
func parseAxialShdColor(axialShd *AxialShd, alpha *int) color.Color {
if axialShd != nil {
for _, segment := range axialShd.Segment {
if strings.TrimSpace(segment.Color.Value) == "" {
continue
}
if alpha == nil {
alpha = segment.Color.Alpha
}
return parseColorWithAlpha(segment.Color.Value, alpha)
if index+1 < len(xs) {
if advance := xs[index+1] - currentX; advance > 0 {
return advance
}
}
return color.Black
if advance, ok := textDelta(dxs, index); ok && advance > 0 {
return advance
}
return 0
}
// parseAxialShdGradient 解析轴向渐变
// 入参: axialShd 轴向渐变节点, alpha 透明度, x X坐标, y Y坐标, pageH 页面高度, originX 原点X坐标, originY 原点Y坐标
// 返回: canvas.Gradient 渐变对象
func parseAxialShdGradient(axialShd *AxialShd, alpha *int, x, y, pageH, originX, originY float64) canvas.Gradient {
if axialShd == nil {
return nil
}
start := parseFloats(axialShd.StartPoint)
end := parseFloats(axialShd.EndPoint)
if len(start) < 2 || len(end) < 2 {
return nil
}
grad := canvas.NewGradient()
for _, segment := range axialShd.Segment {
if strings.TrimSpace(segment.Color.Value) == "" {
continue
}
segmentAlpha := alpha
if segmentAlpha == nil {
segmentAlpha = segment.Color.Alpha
}
grad.Add(segment.Position, colorToRGBA(parseColorWithAlpha(segment.Color.Value, segmentAlpha)))
}
if len(grad) == 0 {
return nil
}
startPoint := canvas.Point{X: x + start[0] - originX, Y: pageH - (y + start[1]) - originY}
endPoint := canvas.Point{X: x + end[0] - originX, Y: pageH - (y + end[1]) - originY}
if startPoint.Equals(endPoint) {
return nil
}
return grad.ToLinear(startPoint, endPoint)
// textCodePositioned 判断文本编码是否带显式定位
// 入参: textCode 文本编码, xs X坐标列表, ys Y坐标列表
// 返回: bool 是否带显式定位
func textCodePositioned(textCode TextCode, xs, ys []float64) bool {
return strings.TrimSpace(textCode.DeltaX) != "" ||
strings.TrimSpace(textCode.DeltaY) != "" ||
len(xs) > 1 ||
len(ys) > 1
}
// parseRadialShdColor 解析径向渐变颜色
// 入参: radialShd 径向渐变节点, alpha 透明度
// 返回: color.Color 颜色对象
func parseRadialShdColor(radialShd *RadialShd, alpha *int) color.Color {
if radialShd != nil {
for _, segment := range radialShd.Segment {
if strings.TrimSpace(segment.Color.Value) == "" {
continue
}
if alpha == nil {
alpha = segment.Color.Alpha
}
return parseColorWithAlpha(segment.Color.Value, alpha)
}
// textDelta 获取文本偏移量
// 入参: deltas 偏移量数组, index 偏移量索引
// 返回: float64 偏移量, bool 是否存在
func textDelta(deltas []float64, index int) (float64, bool) {
if len(deltas) == 0 {
return 0, false
}
return color.Black
if index < len(deltas) {
return deltas[index], true
}
return deltas[len(deltas)-1], true
}
// parseRadialShdGradient 解析径向渐变
// 入参: radialShd 径向渐变节点, alpha 透明度, x X坐标, y Y坐标, pageH 页面高度, originX 原点X坐标, originY 原点Y坐标
// 返回: canvas.Gradient 渐变对象
func parseRadialShdGradient(radialShd *RadialShd, alpha *int, x, y, pageH, originX, originY float64) canvas.Gradient {
if radialShd == nil || radialShd.EndRadius <= 0 {
return nil
// textCodeRunes 获取文本编码字符
// 入参: value 文本编码内容
// 返回: []rune 文本字符
func textCodeRunes(value string) []rune {
if strings.ContainsAny(value, "\r\n") {
value = strings.TrimSpace(value)
}
start := parseFloats(radialShd.StartPoint)
end := parseFloats(radialShd.EndPoint)
if len(start) < 2 || len(end) < 2 {
return nil
}
grad := canvas.NewGradient()
for _, segment := range radialShd.Segment {
if strings.TrimSpace(segment.Color.Value) == "" {
continue
}
segmentAlpha := alpha
if segmentAlpha == nil {
segmentAlpha = segment.Color.Alpha
}
grad.Add(segment.Position, colorToRGBA(parseColorWithAlpha(segment.Color.Value, segmentAlpha)))
}
if len(grad) == 0 {
return nil
}
startPoint := canvas.Point{X: x + start[0] - originX, Y: pageH - (y + start[1]) - originY}
endPoint := canvas.Point{X: x + end[0] - originX, Y: pageH - (y + end[1]) - originY}
return grad.ToRadial(startPoint, radialShd.StartRadius, endPoint, radialShd.EndRadius)
}
// colorToRGBA 转换颜色对象
// 入参: c 颜色对象
// 返回: color.RGBA RGBA颜色
func colorToRGBA(c color.Color) color.RGBA {
if rgba, ok := c.(color.RGBA); ok {
return rgba
}
r, g, b, a := c.RGBA()
return color.RGBA{R: uint8(r >> 8), G: uint8(g >> 8), B: uint8(b >> 8), A: uint8(a >> 8)}
return []rune(value)
}
// GetDeltaX 获取X轴偏移量数组