Compare commits

..
77 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
37 changed files with 6216 additions and 2889 deletions
+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) 授权协议
+9 -3
View File
@@ -9,7 +9,7 @@
<body>
<div class="app">
<header class="toolbar app-bar">
<input id="ofdInput" class="file-input" type="file" accept=".ofd,application/ofd,application/zip">
<input id="ofdInput" class="file-input" type="file" accept=".ofd">
<input id="fontInput" class="file-input" type="file" accept=".ttf,.otf,.ttc,font/*" multiple>
<div class="tool-group">
<button id="ofdButton" class="button primary" type="button" data-short="打开">打开</button>
@@ -36,8 +36,14 @@
</div>
<div class="tool-group">
<select id="pageExportFormat" class="format-select" title="单页格式" aria-label="单页格式" disabled></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>
<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>
+17 -6
View File
@@ -10,7 +10,7 @@
--text-muted: #66737f;
--accent: #0f7b68;
--accent-strong: #095c4e;
--focus: #c9892b;
--focus: #0f7b68;
--sidebar-width: clamp(220px, 20vw, 292px);
--bar-bg: #fbfcfd;
--bar-height: 40px;
@@ -217,6 +217,10 @@ button:not(:disabled):hover {
color: #1f2428;
}
.dpi-select {
width: 54px;
}
.format-select:focus,
.format-select:focus-visible {
border-color: var(--accent);
@@ -397,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;
@@ -472,15 +480,18 @@ body[data-hide-meta] .meta-panel {
}
.page-frame {
display: grid;
align-items: safe center;
min-width: 100%;
min-height: 100%;
padding: var(--page-space);
padding-block: var(--fit-space, var(--page-space));
}
.svg-host {
display: grid;
justify-items: center;
gap: var(--page-space);
gap: var(--fit-gap, var(--fit-space, var(--page-space)));
width: max-content;
margin: 0 auto;
}
@@ -1066,15 +1077,15 @@ body[data-hide-meta] .meta-panel {
min-width: 34px;
}
#exportButton {
padding-inline: 8px;
}
.format-select {
width: 52px;
padding: 0 3px;
}
.dpi-select {
width: 48px;
}
.footer-links {
justify-content: flex-end;
}
+364 -98
View File
@@ -18,6 +18,7 @@ const COMMON_FONT_NAMES = [
];
const LOCAL_FONT_LOAD_LIMIT = 16;
const COMPACT_LAYOUT = window.matchMedia("(max-width: 900px)");
const DEFAULT_IMAGE_DPI = 300;
const STATUS = {
ready: "选择 OFD 文件",
opening: "正在打开 OFD",
@@ -33,7 +34,7 @@ const WASM_CALLBACKS = [
"ofdgoExportFormats",
"ofdgoExportPage",
"ofdgoExportPDF",
"ofdgoFontCandidates",
"ofdgoFontSystemNames",
];
let wasmPromise = null;
@@ -61,7 +62,9 @@ const state = {
fitMode: "width",
renderAnnotations: true,
pageCache: new Map(),
pageInFlight: new Set(),
pageInFlight: new Map(),
pageRenderQueue: [],
pageRenderRunning: false,
pageObserver: null,
scrollFrame: 0,
thumbnailCache: new Map(),
@@ -90,6 +93,7 @@ const el = {
fitButton: document.querySelector("#fitButton"),
fitHeightButton: document.querySelector("#fitHeightButton"),
annotationButton: document.querySelector("#annotationButton"),
imageDPI: document.querySelector("#imageDPI"),
pageExportFormat: document.querySelector("#pageExportFormat"),
exportPageButton: document.querySelector("#exportPageButton"),
exportButton: document.querySelector("#exportButton"),
@@ -102,6 +106,7 @@ const el = {
svgHost: document.querySelector("#svgHost"),
pageListPanel: document.querySelector(".page-list-panel"),
pageList: document.querySelector("#pageList"),
metaPanel: document.querySelector(".meta-panel"),
metaTitle: document.querySelector("#metaTitle"),
metaAuthor: document.querySelector("#metaAuthor"),
metaVersion: document.querySelector("#metaVersion"),
@@ -122,7 +127,7 @@ const el = {
el.ofdButton.addEventListener("click", openOFDFile);
el.togglePagesButton.addEventListener("click", () => toggleSidebar("pages"));
el.toggleMetaButton.addEventListener("click", () => toggleSidebar("meta"));
el.fontAddButton.addEventListener("click", () => el.fontInput.click());
el.fontAddButton.addEventListener("click", openFontFile);
el.localFontButton.addEventListener("click", loadLocalFonts);
el.ofdInput.addEventListener("change", openSelectedOFD);
el.fontInput.addEventListener("change", openSelectedFonts);
@@ -133,6 +138,7 @@ el.zoomInButton.addEventListener("click", () => setScale(state.scale + 0.1));
el.fitButton.addEventListener("click", fitWidth);
el.fitHeightButton.addEventListener("click", fitHeight);
el.annotationButton.addEventListener("click", toggleAnnotations);
el.pageExportFormat.addEventListener("change", () => updateDPIControl());
el.exportPageButton.addEventListener("click", exportCurrentPage);
el.exportButton.addEventListener("click", exportPDF);
el.pageInput.addEventListener("change", () => {
@@ -160,6 +166,7 @@ async function openOFDFile() {
return;
}
await requestLocalFontsBeforeOpen();
el.ofdInput.value = "";
el.ofdInput.click();
}
@@ -170,6 +177,11 @@ async function openOFDFromViewer() {
await openOFDFile();
}
function openFontFile() {
el.fontInput.value = "";
el.fontInput.click();
}
function toggleSidebar(side) {
if (side === "pages") {
state.showPages = !state.showPages;
@@ -233,6 +245,7 @@ function loadExportFormats() {
if (formats.length) {
el.pageExportFormat.value = formats[0].value;
}
updateDPIControl();
}
async function ensureWASM() {
@@ -358,18 +371,27 @@ async function openSelectedOFD() {
if (!file) {
return;
}
if (!isOFDFile(file)) {
el.ofdInput.value = "";
showError(new Error("选择 OFD 文件"), !state.doc);
return;
}
state.wasmRecoveries = 0;
setBusy(true, "正在读取 OFD", 10, STATUS.opening);
try {
state.fileName = file.name || "ofdgo.ofd";
state.ofdBytes = new Uint8Array(await file.arrayBuffer());
await openDocument();
await openDocument({ pageIndex: 0, resetScroll: true });
} catch (err) {
showError(err, true);
setBusy(false);
}
}
function isOFDFile(file) {
return /\.ofd$/i.test(file.name || "");
}
async function openSelectedFonts() {
const files = Array.from(el.fontInput.files || []);
if (!files.length) {
@@ -616,7 +638,7 @@ function externalDocumentFontNames() {
function fontNameKeys(names) {
const keys = new Set();
for (const name of fontCandidateNames(names)) {
for (const name of fontSystemNames(names)) {
const key = normalizeFontName(name);
if (key) {
keys.add(key);
@@ -625,18 +647,18 @@ function fontNameKeys(names) {
return keys;
}
function fontCandidateNames(names) {
function fontSystemNames(names) {
const source = (Array.isArray(names) ? names : [])
.map((name) => String(name || "").trim())
.filter((name) => name !== "");
if (!source.length) {
return [];
}
if (state.ready && !state.wasmExited && typeof globalThis.ofdgoFontCandidates === "function") {
if (state.ready && !state.wasmExited && typeof globalThis.ofdgoFontSystemNames === "function") {
try {
const candidates = callWASM("ofdgoFontCandidates", source);
if (Array.isArray(candidates)) {
return candidates;
const names = callWASM("ofdgoFontSystemNames", source);
if (Array.isArray(names)) {
return names;
}
} catch {
return source;
@@ -869,7 +891,7 @@ async function openDocument(options = {}) {
const pageIndex = Math.min(Math.max(options.pageIndex || 0, 0), Math.max(pageCount - 1, 0));
state.doc = doc;
state.pageIndex = pageIndex;
state.scale = 1;
state.scale = options.scale || 1;
state.fitMode = options.fitMode || "width";
if (!options.skipAutoFonts && await autoLoadDocumentLocalFonts(openSeq)) {
if (openSeq !== state.openSeq) {
@@ -878,6 +900,7 @@ async function openDocument(options = {}) {
await openDocument({
pageIndex,
fitMode: state.fitMode,
resetScroll: options.resetScroll,
skipAutoFonts: true,
openSeq,
});
@@ -890,6 +913,14 @@ async function openDocument(options = {}) {
renderPageList();
renderMeta();
renderPageFlow();
if (options.resetScroll) {
el.viewerPanel.scrollLeft = 0;
el.viewerPanel.scrollTop = 0;
el.pageListPanel.scrollLeft = 0;
el.pageListPanel.scrollTop = 0;
el.metaPanel.scrollLeft = 0;
el.metaPanel.scrollTop = 0;
}
applyFit(false);
await nextFrame();
await renderPage(pageIndex, { keepBusy: true, scroll: false, openSeq });
@@ -925,11 +956,13 @@ async function renderPage(index, options = {}) {
}
try {
setCurrentPage(index);
if (options.fit !== false) {
applyFit(false);
}
if (options.scroll !== false) {
scrollToPage(index);
}
await renderFlowPage(index, { throwError: true, openSeq });
await renderFlowPage(index, { throwError: true, openSeq, priority: 0 });
if (openSeq !== state.openSeq) {
return;
}
@@ -937,9 +970,6 @@ async function renderPage(index, options = {}) {
scrollToPage(index);
}
queueNearbyPages(index, openSeq);
updatePageListCurrent();
updateControls();
setStatus(pageStatus(index, pageCount));
} catch (err) {
if (openSeq === state.openSeq) {
showError(err, false);
@@ -973,7 +1003,7 @@ async function exportPDF() {
return;
}
setProgress("正在保存文档 PDF", 86);
const bytes = base64ToBytes(result.base64);
const bytes = result.bytes;
downloadBytes(bytes, "application/pdf", pdfFileName());
setStatus(`文档 PDF 已导出 ${formatBytes(result.size || bytes.length)}`);
} catch (err) {
@@ -1011,12 +1041,13 @@ async function exportCurrentPage() {
if (openSeq !== state.openSeq) {
return;
}
const result = callWASM("ofdgoExportPage", state.pageIndex, format);
const dpi = exportFormatUsesDPI(format) ? currentImageDPI() : 0;
const result = callWASM("ofdgoExportPage", state.pageIndex, format, dpi);
if (openSeq !== state.openSeq) {
return;
}
setProgress(`正在保存 ${result.label || label}`, 86);
const bytes = base64ToBytes(result.base64);
const bytes = result.bytes;
downloadBytes(bytes, result.mime || info?.mime || "application/octet-stream", pageFileName(result.extension || info?.extension || format));
setStatus(`${result.label || label} 已导出 ${formatBytes(result.size || bytes.length, result.label || label)}`);
} catch (err) {
@@ -1068,6 +1099,10 @@ function renderPageFlow() {
function resetPageFlow() {
state.pageCache.clear();
for (const task of state.pageInFlight.values()) {
task.resolve(null);
}
state.pageRenderQueue = [];
state.pageInFlight.clear();
resetThumbnails();
if (state.pageObserver) {
@@ -1079,7 +1114,7 @@ function resetPageFlow() {
function observeFlowPage(shell, index) {
const openSeq = state.openSeq;
if (index < 4 && index !== state.pageIndex) {
renderFlowPage(index, { openSeq });
renderFlowPage(index, { openSeq, priority: 3 });
return;
}
const observer = flowPageObserver();
@@ -1101,7 +1136,7 @@ function flowPageObserver() {
}
const index = Number.parseInt(entry.target.dataset.pageIndex, 10);
state.pageObserver.unobserve(entry.target);
renderFlowPage(index, { openSeq });
renderFlowPage(index, { openSeq, priority: 4 });
}
}, {
root: el.viewerPanel,
@@ -1116,27 +1151,18 @@ async function renderFlowPage(index, options = {}) {
if (openSeq !== state.openSeq) {
return null;
}
if (state.pageCache.has(index)) {
return state.pageCache.get(index);
}
const key = `${openSeq}:${index}`;
if (state.pageInFlight.has(key)) {
return null;
}
state.pageInFlight.add(key);
try {
await nextFrame();
const page = await loadPageData(index, { openSeq, priority: options.priority || 2 });
if (openSeq !== state.openSeq) {
return null;
}
const page = callWASM("ofdgoRenderPage", index);
if (openSeq !== state.openSeq) {
return null;
}
state.pageCache.set(index, page);
cacheThumbnail(index, page.svg);
if (page) {
const shell = pageShell(index);
if (!shell?.classList.contains("rendered")) {
mountPageSVG(index, page, openSeq);
}
updateThumbnail(index, openSeq);
}
return page;
} catch (err) {
if (openSeq === state.openSeq) {
@@ -1146,11 +1172,100 @@ async function renderFlowPage(index, options = {}) {
throw err;
}
return null;
} finally {
state.pageInFlight.delete(key);
}
}
function loadPageData(index, options = {}) {
const openSeq = options.openSeq || state.openSeq;
if (openSeq !== state.openSeq) {
return Promise.resolve(null);
}
if (state.pageCache.has(index)) {
return Promise.resolve(state.pageCache.get(index));
}
const key = `${openSeq}:${index}`;
const priority = options.priority || 3;
const current = state.pageInFlight.get(key);
if (current) {
current.priority = Math.min(current.priority, priority);
return current.promise;
}
let resolve;
let reject;
const promise = new Promise((done, fail) => {
resolve = done;
reject = fail;
});
const task = { key, index, openSeq, priority, resolve, reject, promise };
state.pageInFlight.set(key, task);
state.pageRenderQueue.push(task);
schedulePageRender();
return promise;
}
function schedulePageRender() {
if (state.pageRenderRunning) {
return;
}
state.pageRenderRunning = true;
requestAnimationFrame(processPageRenderQueue);
}
async function processPageRenderQueue() {
try {
while (state.pageRenderQueue.length) {
state.pageRenderQueue.sort(comparePageRenderTask);
const task = state.pageRenderQueue.shift();
let delayed = false;
try {
if (task.openSeq !== state.openSeq) {
task.resolve(null);
continue;
}
if (state.pageCache.has(task.index)) {
task.resolve(state.pageCache.get(task.index));
continue;
}
await nextFrame();
if (task.openSeq !== state.openSeq) {
task.resolve(null);
continue;
}
if (shouldDelayPageTask(task)) {
state.pageRenderQueue.push(task);
delayed = true;
continue;
}
const page = callWASM("ofdgoRenderPage", task.index);
if (task.openSeq === state.openSeq) {
state.pageCache.set(task.index, page);
cacheThumbnail(task.index, page.svg);
}
task.resolve(page);
} catch (err) {
task.reject(err);
} finally {
if (!delayed) {
state.pageInFlight.delete(task.key);
}
}
}
} finally {
state.pageRenderRunning = false;
if (state.pageRenderQueue.length) {
schedulePageRender();
}
}
}
function shouldDelayPageTask(task) {
return state.pageRenderQueue.some((next) => next.openSeq === state.openSeq && comparePageRenderTask(next, task) < 0);
}
function comparePageRenderTask(a, b) {
return a.priority - b.priority || Math.abs(a.index - state.pageIndex) - Math.abs(b.index - state.pageIndex);
}
function mountPageSVG(index, page, openSeq = state.openSeq) {
if (openSeq !== state.openSeq) {
return;
@@ -1183,7 +1298,7 @@ function markFlowPageError(index, err) {
function queueNearbyPages(index, openSeq = state.openSeq) {
for (let i = Math.max(0, index - 1); i <= Math.min((state.doc?.pageCount || 1) - 1, index + 2); i += 1) {
renderFlowPage(i, { openSeq });
renderFlowPage(i, { openSeq, priority: 2 });
}
}
@@ -1194,8 +1309,23 @@ function pageShell(index) {
function scrollToPage(index) {
const shell = pageShell(index);
if (shell) {
shell.scrollIntoView({ block: state.fitMode === "height" ? "center" : "start", inline: "nearest" });
if (state.fitMode === "height") {
shell.scrollIntoView({ block: "center", inline: "nearest" });
} else {
const viewerRect = el.viewerPanel.getBoundingClientRect();
const shellRect = shell.getBoundingClientRect();
const top = el.viewerPanel.scrollTop + shellRect.top - viewerRect.top - pageBlockSpace();
const maxTop = Math.max(0, el.viewerPanel.scrollHeight - el.viewerPanel.clientHeight);
el.viewerPanel.scrollTop = Math.min(maxTop, Math.max(0, top));
}
centerPageInline(shell);
}
}
function centerPageInline(shell) {
const viewerRect = el.viewerPanel.getBoundingClientRect();
const shellRect = shell.getBoundingClientRect();
el.viewerPanel.scrollLeft += shellRect.left + shellRect.width / 2 - viewerRect.left - el.viewerPanel.clientWidth / 2;
}
function schedulePageSync() {
@@ -1209,30 +1339,35 @@ function schedulePageSync() {
}
function syncCurrentPageFromScroll() {
const shells = Array.from(el.svgHost.querySelectorAll(".page-shell"));
if (!shells.length) {
const shell = pageShellFromView();
if (!shell) {
return;
}
const viewerRect = el.viewerPanel.getBoundingClientRect();
const targetY = viewerRect.top + Math.min(viewerRect.height * 0.45, 280);
let nextIndex = state.pageIndex;
let best = Number.POSITIVE_INFINITY;
for (const shell of shells) {
const rect = shell.getBoundingClientRect();
const distance = Math.abs(rect.top - targetY);
if (distance < best) {
best = distance;
nextIndex = Number.parseInt(shell.dataset.pageIndex, 10);
}
}
const nextIndex = Number.parseInt(shell.dataset.pageIndex, 10);
if (Number.isFinite(nextIndex) && nextIndex !== state.pageIndex) {
setCurrentPage(nextIndex);
queueNearbyPages(nextIndex);
}
}
function pageShellFromView() {
const rect = el.viewerPanel.getBoundingClientRect();
const x = rect.left + rect.width / 2;
return pageShellAtPoint(x, rect.top + rect.height * 0.45)
|| pageShellAtPoint(x, rect.top + rect.height * 0.25)
|| pageShellAtPoint(x, rect.top + rect.height * 0.65);
}
function pageShellAtPoint(x, y) {
return document.elementFromPoint(x, y)?.closest?.(".page-shell") || null;
}
function setCurrentPage(index) {
state.pageIndex = index;
if (state.fitMode === "width") {
state.scale = fitWidthScale(currentPageInfo());
el.zoomLabel.textContent = `${Math.round(state.scale * 100)}%`;
}
updatePageListCurrent();
updateControls();
if (state.doc) {
@@ -1241,8 +1376,16 @@ function setCurrentPage(index) {
}
function updatePageListCurrent() {
for (const item of el.pageList.querySelectorAll(".page-list-item")) {
setPageItemCurrent(item, Number.parseInt(item.dataset.pageIndex, 10) === state.pageIndex);
const current = el.pageList.querySelector(".page-list-item[aria-current]");
if (current) {
if (Number.parseInt(current.dataset.pageIndex, 10) === state.pageIndex) {
return;
}
current.removeAttribute("aria-current");
}
const next = el.pageList.querySelector(`.page-list-item[data-page-index="${state.pageIndex}"]`);
if (next) {
setPageItemCurrent(next, true);
}
}
@@ -1269,13 +1412,14 @@ function layoutPages() {
function layoutPageShell(shell, page) {
const width = Math.max(1, page.width * MM_TO_PX);
const height = Math.max(1, page.height * MM_TO_PX);
shell.style.width = `${width * state.scale}px`;
shell.style.height = `${height * state.scale}px`;
const scale = state.fitMode === "width" ? state.scale * currentPageInfo().width / page.width : state.scale;
shell.style.width = `${width * scale}px`;
shell.style.height = `${height * scale}px`;
const surface = shell.querySelector(".page-surface");
if (surface) {
surface.style.width = `${width}px`;
surface.style.height = `${height}px`;
surface.style.transform = `scale(${state.scale})`;
surface.style.transform = `scale(${scale})`;
}
}
@@ -1463,15 +1607,13 @@ async function renderThumbnail(index, openSeq = state.openSeq) {
}
state.thumbnailInFlight.add(key);
try {
await nextFrame();
const page = await loadPageData(index, { openSeq, priority: 5 });
if (openSeq !== state.openSeq) {
return;
}
const page = callWASM("ofdgoRenderPage", index);
if (openSeq !== state.openSeq) {
if (!page) {
return;
}
cacheThumbnail(index, page.svg);
updateThumbnail(index, openSeq);
} catch {
if (openSeq === state.openSeq) {
@@ -1540,31 +1682,40 @@ function renderSignatures() {
const badges = document.createElement("div");
badges.className = "signature-badges";
badges.append(fontBadge(signature.valid ? "有效" : "无效", signature.valid ? "valid" : "invalid"));
badges.append(fontBadge(signature.valid ? "通过" : "异常", signature.valid ? "valid" : "invalid"));
head.append(name, badges);
row.append(head);
appendSignatureLine(row, "编号", signature.id);
appendSignatureLine(row, "版本", signature.version);
appendSignatureLine(row, "章图", signature.sealType);
appendSignatureLine(row, "章号", signature.sealId);
appendSignatureLine(row, "章名", signature.sealName);
appendSignatureLine(row, "厂商", signature.sealVendor);
appendSignatureLine(row, "签者", signature.signer);
appendSignatureLine(row, "时间", formatSignatureTime(signature.signatureDateTime));
appendSignatureLine(row, "机构", signatureAgency(signature));
appendSignatureLine(row, "版本", signature.version);
appendSignatureLine(row, "保护", `${signature.referencePassed || 0}/${signature.referenceCount || 0}`);
appendSignatureCheck(row, "原文", signature.dataHashOK);
appendSignatureCheck(row, "摘要", signature.digestOK);
appendSignatureCheck(row, "签名", signature.signedValueOK);
appendSignatureCheck(row, "证书", signature.certOK);
if (signature.type !== "Sign") {
appendSignatureCheck(row, "章", signature.sealOK && signature.sealMatchOK);
appendSignatureCheck(row, "匹配", signature.sealMatchOK);
appendSignatureCheck(row, "章", signature.sealOK);
appendSignatureCheck(row, "一致", signature.sealMatchOK);
}
appendSignatureCheck(row, "证书", signature.certOK);
appendSignaturePolicy(row, "签期", signature.signatureTimeChecked, signature.signatureTimeOK);
appendSignaturePolicy(row, "章期", signature.sealTimeChecked, signature.sealTimeOK);
if (signature.sealCertTimeChecked) {
appendSignatureLine(row, "制期", signature.sealCertTimeOK ? "有效" : "失效", signature.sealCertTimeOK ? "ok" : "");
}
appendSignaturePolicy(row, "时效", signature.certTimeChecked, signature.certTimeOK);
appendSignaturePolicy(row, "信任", signature.certTrustChecked, signature.certTrustOK);
appendSignatureLine(row, "保护", signatureReferenceText(signature), signatureReferenceStatus(signature));
appendSignatureLine(row, "算法", signature.signatureMethod);
appendSignatureLine(row, "散列", signature.digestMethod);
appendSignatureLine(row, "序号", signature.signSerial);
appendSignatureLine(row, "主体", signature.signSubject && signature.signSubject !== signature.signer ? signature.signSubject : "");
appendSignatureLine(row, "颁发", signature.signIssuer);
appendSignatureLine(row, "序列", signature.signSerial);
appendSignatureLine(row, "章图", signature.sealType);
appendSignatureLine(row, "章证", signature.sealSubject);
appendSignatureLine(row, "编号", signature.id);
appendSignatureLine(row, "错误", signature.error, "fail");
fragment.append(row);
}
@@ -1582,16 +1733,12 @@ function signatureSummary(signatures) {
return `通过 ${signatures.length}`;
}
function signatureName() {
return "签名";
}
function signatureNameNode(signature) {
const stamps = signature.stamps || [];
if (!stamps.length) {
const name = document.createElement("div");
name.className = "signature-name";
name.textContent = signatureName();
name.textContent = "签名";
return name;
}
const name = document.createElement("div");
@@ -1600,7 +1747,7 @@ function signatureNameNode(signature) {
const button = document.createElement("button");
button.type = "button";
button.className = "signature-name-button";
button.textContent = signatureName();
button.textContent = "签名";
button.addEventListener("click", () => focusSignatureStamp(stamps[0]));
name.append(button, signatureStampGroup(stamps));
return name;
@@ -1632,6 +1779,23 @@ function signatureAgency(signature) {
return signature.company || signature.provider || "";
}
function signatureReferenceText(signature) {
const passed = signature.referencePassed || 0;
const count = signature.referenceCount || 0;
if (!count) {
return "";
}
return `${passed === count ? "通过" : "失败"} ${passed}/${count}`;
}
function signatureReferenceStatus(signature) {
const count = signature.referenceCount || 0;
if (!count) {
return "";
}
return signature.referencePassed === count ? "ok" : "fail";
}
function formatSignatureTime(value) {
const text = String(value || "").trim();
const digits = text.match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/);
@@ -1664,12 +1828,19 @@ function appendSignatureCheck(row, label, ok) {
appendSignatureLine(row, label, ok ? "通过" : "失败", ok ? "ok" : "fail");
}
function appendSignaturePolicy(row, label, checked, ok) {
if (!checked) {
return;
}
appendSignatureCheck(row, label, ok);
}
async function focusSignatureStamp(stamp) {
const pageIndex = (stamp.page || 0) - 1;
if (!state.doc || pageIndex < 0) {
return;
}
await renderPage(pageIndex);
await renderPage(pageIndex, { fit: false, scroll: false });
await nextFrame();
highlightSignatureStamp(stamp);
setStatus(stamp.page ? `已定位签名外观 第 ${stamp.page}` : "已定位签名外观");
@@ -1689,7 +1860,7 @@ function highlightSignatureStamp(stamp) {
mark.style.width = `${stamp.width * MM_TO_PX * state.scale}px`;
mark.style.height = `${stamp.height * MM_TO_PX * state.scale}px`;
shell.append(mark);
mark.scrollIntoView({ block: "center", inline: "center" });
mark.scrollIntoView({ block: "nearest", inline: "nearest" });
window.setTimeout(() => mark.remove(), 1800);
}
@@ -1814,10 +1985,21 @@ function fitWidth(updateStatus = true) {
if (!page) {
return;
}
state.fitMode = "width";
const available = Math.max(280, el.pageFrame.clientWidth - pageSpace() * 2);
setScale(fitWidthScale(page), updateStatus, "width");
if (updateStatus) {
scrollToPage(state.pageIndex);
}
}
function fitWidthScale(page) {
const space = pageSpace();
const width = Math.max(1, page.width * MM_TO_PX);
setScale(available / width, updateStatus, "width");
const height = Math.max(1, page.height * MM_TO_PX);
let available = Math.max(1, el.viewerPanel.clientWidth - space * 2);
if (!viewerHasVerticalScrollbar() && height * (available / width) > Math.max(1, el.viewerPanel.clientHeight - space * 2)) {
available = Math.max(1, available - scrollbarWidth());
}
return Math.min(4, Math.max(0.2, available / width));
}
function fitHeight(updateStatus = true) {
@@ -1825,10 +2007,12 @@ function fitHeight(updateStatus = true) {
if (!page) {
return;
}
state.fitMode = "height";
const available = Math.max(220, el.viewerPanel.clientHeight - pageSpace() * 2);
const space = pageSpace();
const availableWidth = Math.max(1, el.viewerPanel.clientWidth - space * 2);
const availableHeight = Math.max(1, el.viewerPanel.clientHeight - space * 2);
const width = Math.max(1, page.width * MM_TO_PX);
const height = Math.max(1, page.height * MM_TO_PX);
setScale(available / height, updateStatus, "height");
setScale(Math.min(availableWidth / width, availableHeight / height), updateStatus, "height");
if (updateStatus) {
scrollToPage(state.pageIndex);
}
@@ -1845,10 +2029,20 @@ function applyFit(updateStatus = true) {
}
function setScale(nextScale, updateStatus = true, fitMode = "free") {
const anchor = updateStatus && fitMode === "free" ? scaleAnchor() : null;
const scale = Math.min(4, Math.max(0.2, nextScale));
const scaleChanged = Math.abs(scale - state.scale) > 0.001;
const layoutChanged = scaleChanged || fitMode !== state.fitMode;
state.fitMode = fitMode;
state.scale = Math.min(4, Math.max(0.2, nextScale));
state.scale = scale;
if (layoutChanged) {
clearStampHighlights();
layoutPages();
}
updateFitSpace();
if (layoutChanged) {
restoreScaleAnchor(anchor);
}
el.zoomLabel.textContent = `${Math.round(state.scale * 100)}%`;
if (updateStatus && state.doc) {
setStatus(`${state.pageIndex + 1} / ${state.doc.pageCount}`);
@@ -1856,12 +2050,79 @@ function setScale(nextScale, updateStatus = true, fitMode = "free") {
updateControls();
}
function scaleAnchor() {
const shell = pageShellFromView() || pageShell(state.pageIndex);
if (!shell) {
return null;
}
const viewerRect = el.viewerPanel.getBoundingClientRect();
const shellRect = shell.getBoundingClientRect();
return {
index: Number.parseInt(shell.dataset.pageIndex, 10),
x: (viewerRect.left + viewerRect.width / 2 - shellRect.left) / Math.max(1, shellRect.width),
y: (viewerRect.top + viewerRect.height * 0.45 - shellRect.top) / Math.max(1, shellRect.height),
};
}
function restoreScaleAnchor(anchor) {
if (!anchor) {
return;
}
const shell = pageShell(anchor.index);
if (!shell) {
return;
}
const viewerRect = el.viewerPanel.getBoundingClientRect();
const shellRect = shell.getBoundingClientRect();
el.viewerPanel.scrollLeft += shellRect.left + shellRect.width * anchor.x - viewerRect.left - viewerRect.width / 2;
el.viewerPanel.scrollTop += shellRect.top + shellRect.height * anchor.y - viewerRect.top - viewerRect.height * 0.45;
}
function currentPageInfo() {
return state.doc?.pages?.[state.pageIndex] || null;
}
function pageSpace() {
return Number.parseFloat(getComputedStyle(el.pageFrame).paddingTop) || 0;
return Number.parseFloat(getComputedStyle(el.pageFrame).paddingLeft) || 0;
}
function pageBlockSpace() {
return Number.parseFloat(getComputedStyle(el.pageFrame).paddingTop) || pageSpace();
}
function viewerHasVerticalScrollbar() {
return el.viewerPanel.scrollHeight > el.viewerPanel.clientHeight;
}
function scrollbarWidth() {
const probe = document.createElement("div");
probe.style.position = "absolute";
probe.style.width = "100px";
probe.style.height = "100px";
probe.style.overflow = "scroll";
probe.style.left = "-9999px";
document.body.append(probe);
const width = probe.offsetWidth - probe.clientWidth;
probe.remove();
return width;
}
function updateFitSpace() {
const page = currentPageInfo();
if (!page || state.fitMode === "free") {
el.viewerPanel.classList.remove("single-page-fits-height");
el.pageFrame.style.removeProperty("--fit-space");
el.pageFrame.style.removeProperty("--fit-gap");
return;
}
const shell = pageShell(state.pageIndex);
const height = shell ? shell.getBoundingClientRect().height : Math.max(1, page.height * MM_TO_PX * state.scale);
const base = pageSpace();
el.viewerPanel.classList.toggle("single-page-fits-height", state.doc.pageCount === 1 && height <= el.viewerPanel.clientHeight - base * 2);
const space = Math.max(base, (el.viewerPanel.clientHeight - height) / 2);
const gap = space > base ? space + 1 : space;
el.pageFrame.style.setProperty("--fit-space", `${space}px`);
el.pageFrame.style.setProperty("--fit-gap", `${gap}px`);
}
function updateControls() {
@@ -1882,6 +2143,7 @@ function updateControls() {
el.pageExportFormat.disabled = !hasDoc || !state.exportFormats.length;
el.exportPageButton.disabled = !hasDoc || !state.exportFormats.length;
el.exportButton.disabled = !hasDoc;
updateDPIControl();
}
function updateAnnotationButton() {
@@ -1894,6 +2156,7 @@ function setExportControlsDisabled(disabled) {
el.exportButton.disabled = disabled;
el.exportPageButton.disabled = disabled;
el.pageExportFormat.disabled = disabled;
updateDPIControl(disabled);
}
function callWASM(name, ...args) {
@@ -1916,7 +2179,7 @@ function callWASM(name, ...args) {
}
throw err;
}
const result = JSON.parse(payload);
const result = typeof payload === "string" ? JSON.parse(payload) : payload;
if (!result.ok) {
throw new Error(result.error || "WASM 调用失败");
}
@@ -1972,15 +2235,6 @@ function formatSize(value) {
return value.toFixed(1);
}
function base64ToBytes(base64) {
const binary = atob(base64 || "");
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i += 1) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
function downloadBytes(bytes, mime, name) {
const blob = new Blob([bytes], { type: mime });
const link = document.createElement("a");
@@ -2011,6 +2265,18 @@ function exportFormatInfo(value) {
return state.exportFormats.find((format) => format.value === value) || null;
}
function exportFormatUsesDPI(value) {
return value === "png" || value === "jpg";
}
function updateDPIControl(disabled = false) {
el.imageDPI.disabled = disabled || !state.doc || !exportFormatUsesDPI(el.pageExportFormat.value);
}
function currentImageDPI() {
return Number.parseFloat(el.imageDPI.value) || DEFAULT_IMAGE_DPI;
}
function formatBytes(size, fallback = "PDF") {
if (!Number.isFinite(size) || size <= 0) {
return fallback;
+15 -14
View File
@@ -1,24 +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-20260707061149-c2ff34658234
golang.org/x/image v0.43.0
golang.org/x/text v0.39.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
@@ -27,14 +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/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-20260707061149-c2ff34658234 h1:aTUfzcRBfRYrEX0gxWRRQJGd/z9LJTu15eFC9pNVT00=
github.com/xiaoqidun/jbig2 v0.0.0-20260707061149-c2ff34658234/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.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
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=
+47 -15
View File
@@ -17,7 +17,6 @@
package webui
import (
"encoding/base64"
"encoding/json"
"fmt"
"syscall/js"
@@ -45,7 +44,7 @@ func RunWASM() {
registerCallback("ofdgoExportFormats", exportFormats)
registerCallback("ofdgoExportPage", exportPage)
registerCallback("ofdgoExportPDF", exportPDF)
registerCallback("ofdgoFontCandidates", fontCandidates)
registerCallback("ofdgoFontSystemNames", fontSystemNames)
select {}
}
@@ -57,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)
@@ -113,7 +115,18 @@ 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
}
// exportFormats 获取导出格式
@@ -130,21 +143,21 @@ func exportPage(args []js.Value) (any, error) {
if currentSession == nil {
return nil, fmt.Errorf("ofd document is not opened")
}
if len(args) < 2 {
if len(args) < 3 {
return nil, fmt.Errorf("missing export page arguments")
}
data, format, err := currentSession.ExportPage(args[0].Int(), args[1].String())
data, format, err := currentSession.ExportPage(args[0].Int(), args[1].String(), args[2].Float())
if err != nil {
return nil, err
}
return map[string]any{
"base64": base64.StdEncoding.EncodeToString(data),
return successResult(map[string]any{
"bytes": bytesToJS(data),
"size": len(data),
"format": format.Value,
"label": format.Label,
"extension": format.Extension,
"mime": format.MIME,
}, nil
}), nil
}
// exportPDF 导出OFD文档为PDF
@@ -158,18 +171,37 @@ func exportPDF(args []js.Value) (any, error) {
if err != nil {
return nil, err
}
return map[string]any{
"base64": base64.StdEncoding.EncodeToString(data),
return successResult(map[string]any{
"bytes": bytesToJS(data),
"size": len(data),
}, nil
}), 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 从浏览器值读取二进制数据
+94 -48
View File
@@ -44,30 +44,40 @@ type SignatureInfo struct {
ID string `json:"id"`
Type string `json:"type"`
Valid bool `json:"valid"`
Error string `json:"error,omitempty"`
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"`
Version string `json:"version,omitempty"`
Signer string `json:"signer,omitempty"`
SignSubject string `json:"signSubject,omitempty"`
SignIssuer string `json:"signIssuer,omitempty"`
SignSerial string `json:"signSerial,omitempty"`
SealSubject string `json:"sealSubject,omitempty"`
SealIssuer string `json:"sealIssuer,omitempty"`
SealSerial string `json:"sealSerial,omitempty"`
SealType string `json:"sealType,omitempty"`
SignatureMethod string `json:"signatureMethod,omitempty"`
SignatureDateTime string `json:"signatureDateTime,omitempty"`
DigestMethod string `json:"digestMethod,omitempty"`
ReferenceCount int `json:"referenceCount"`
ReferencePassed int `json:"referencePassed"`
DigestOK bool `json:"digestOK"`
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 签名外观信息
@@ -221,10 +231,6 @@ 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
}
info.FontCount = len(info.Fonts)
if signatures, err := s.signatureInfos(); err == nil {
info.Signatures = signatures
} else {
@@ -238,9 +244,11 @@ 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.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
@@ -248,6 +256,10 @@ func (s *Session) Info() DocumentInfo {
}
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
}
@@ -271,9 +283,9 @@ func (s *Session) RenderPageSVG(index int) (PageSVG, error) {
}
// ExportPage 导出单页
// 入参: index 页面索引, value 导出格式
// 入参: index 页面索引, value 导出格式, dpi 图片DPI
// 返回: []byte 文件数据, ExportFormat 导出格式, error 错误信息
func (s *Session) ExportPage(index int, value string) ([]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)
@@ -291,13 +303,13 @@ func (s *Session) ExportPage(index int, value string) ([]byte, ExportFormat, err
case "eps":
err = s.Renderer.RenderToEPS(page, &buf)
case "png":
img, renderErr := s.Renderer.RenderToImage(page)
img, renderErr := s.renderPageImage(page, dpi)
if renderErr != nil {
return nil, ExportFormat{}, renderErr
}
err = png.Encode(&buf, img)
case "jpg":
img, renderErr := s.Renderer.RenderToImage(page)
img, renderErr := s.renderPageImage(page, dpi)
if renderErr != nil {
return nil, ExportFormat{}, renderErr
}
@@ -309,6 +321,21 @@ func (s *Session) ExportPage(index int, value string) ([]byte, 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) {
@@ -318,47 +345,53 @@ func (s *Session) signatureInfos() ([]SignatureInfo, error) {
}
infos := make([]SignatureInfo, 0, len(reports))
for _, report := range reports {
positions, err := s.Reader.SignatureStampPositions(report.Stamps)
if err != nil {
return nil, err
}
infos = append(infos, signatureInfo(report, positions))
infos = append(infos, signatureInfo(report))
}
return infos, nil
}
// signatureInfo 转换签名验证信息
// 入参: report 签名验证报告, positions 签名外观位置
// 入参: report 签名验证报告
// 返回: SignatureInfo 签名验证信息
func signatureInfo(report ofdgo.SignatureVerifyReport, positions []ofdgo.SignatureStampPosition) SignatureInfo {
func signatureInfo(report ofdgo.SignatureVerifyReport) SignatureInfo {
return SignatureInfo{
ID: report.ID,
Type: string(report.Type),
Valid: report.Valid,
Error: report.Error,
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,
Version: report.Provider.Version,
Signer: signatureSigner(report),
SignSubject: report.SignCert.Subject,
SignIssuer: report.SignCert.Issuer,
SignSerial: report.SignCert.SerialNumber,
SealSubject: report.SealCert.Subject,
SealIssuer: report.SealCert.Issuer,
SealSerial: report.SealCert.SerialNumber,
SealType: report.SealType,
SignatureMethod: report.SignatureMethod,
SignatureDateTime: report.SignatureDateTime,
DigestMethod: report.DigestMethod,
ReferenceCount: len(report.References),
ReferencePassed: signatureReferencePassed(report.References),
DigestOK: report.DigestOK,
DataHashOK: report.DataHashOK,
SignedValueOK: report.SignedValueOK,
SealOK: report.SealOK,
SealMatchOK: report.SealMatchOK,
CertOK: report.CertOK,
Stamps: signatureStampInfos(positions),
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),
}
}
@@ -391,6 +424,19 @@ func signatureReferencePassed(refs []ofdgo.SignatureReferenceVerify) int {
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 签名外观信息
+8
View File
@@ -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
}
+185 -6
View File
@@ -23,14 +23,19 @@ 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"`
}
@@ -42,7 +47,8 @@ type Extension struct {
Date string `xml:"Date,attr"`
RefID string `xml:"RefId,attr"`
Property []Property `xml:"Property"`
Data string `xml:"Data"`
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"`
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"`
@@ -110,13 +156,146 @@ type Outlines struct {
type OutlineElem struct {
Title string `xml:"Title,attr"`
Count int `xml:"Count,attr"`
Actions string `xml:"Actions"`
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"`
Annot bool `xml:"Annot"`
Export bool `xml:"Export"`
Copy bool `xml:"Copy"`
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,
}
}
+298 -43
View File
@@ -34,6 +34,7 @@ type FontFile struct {
type FontFS struct {
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) {
+61 -127
View File
@@ -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 {
@@ -137,9 +168,9 @@ func (r *Renderer) fontInfo(font Font) FontInfo {
}
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 {
@@ -153,115 +184,46 @@ func (r *Renderer) fontInfo(font Font) FontInfo {
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)
if pages == nil {
for _, pageRef := range doc.Pages.Page {
page, err := r.Reader.PageContent(pageRef)
if err == nil {
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 追加字体名称
-35
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,27 +48,6 @@ func calcTableChecksum(data []byte) uint32 {
return sum
}
// checkMissingCmap 检查是否缺失 cmap 表
// 入参: data 字体数据
// 返回: bool 是否缺失
func checkMissingCmap(data []byte) bool {
if len(data) < 12 {
return true
}
numTables := binary.BigEndian.Uint16(data[4:6])
for i := 0; i < int(numTables); i++ {
pos := 12 + i*16
if len(data) < pos+4 {
break
}
tag := string(data[pos : pos+4])
if tag == "cmap" {
return false
}
}
return true
}
// parseCmapMappings 解析 cmap 字符映射
// 入参: data cmap表数据
// 返回: map[rune]uint16 字符到字形映射
@@ -287,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仿射变换矩阵
+30
View File
@@ -20,12 +20,28 @@ import (
"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
@@ -36,6 +52,20 @@ func decodeImageData(data []byte) (image.Image, string, error) {
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 错误信息
+123 -274
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,86 +96,28 @@ 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 {
target := graphicObjectTarget{
objects: &l.Objects,
text: &l.TextObject,
path: &l.PathObject,
image: &l.ImageObject,
composite: &l.CompositeGraphicUnit,
}
if decoded, err := decodeGraphicObject(d, start, target); decoded || 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
if start.Name.Local == "PageBlock" {
return decodeObjectContainer(d, start, l.decodeObject)
}
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()
}
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
}
}
}
}
// UnmarshalXML 解析复合图元并保留对象顺序
// 入参: d XML解码器, start 起始节点
@@ -128,265 +140,102 @@ 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 {
target := graphicObjectTarget{
objects: &a.Objects,
text: &a.TextObject,
path: &a.PathObject,
image: &a.ImageObject,
composite: &a.CompositeGraphicUnit,
}
if decoded, err := decodeGraphicObject(d, start, target); decoded || 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
if start.Name.Local == "PageBlock" {
return decodeObjectContainer(d, start, a.decodeObject)
}
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()
}
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
}
}
}
}
// UnmarshalXML 解析图案单元内容并保留对象顺序
// 入参: d XML解码器, start 起始节点
// 返回: 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 {
target := graphicObjectTarget{
objects: &p.Objects,
text: &p.TextObject,
path: &p.PathObject,
image: &p.ImageObject,
composite: &p.CompositeGraphicUnit,
}
if decoded, err := decodeGraphicObject(d, start, target); decoded || 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
if start.Name.Local == "PageBlock" {
return decodeObjectContainer(d, start, p.decodeObject)
}
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()
}
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
}
}
}
}
// attrValue 获取XML属性值
// 入参: start 起始节点, name 属性名
+6 -1
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 勾边颜色
@@ -210,8 +213,10 @@ type ImageObject struct {
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 -1530
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"`
}
+54 -8
View File
@@ -19,6 +19,7 @@ import (
"encoding/asn1"
"encoding/binary"
"encoding/xml"
"fmt"
"strings"
)
@@ -26,14 +27,16 @@ import (
type SignType string
const (
// 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"`
}
@@ -59,7 +62,6 @@ type SignedInfo struct {
Seal SignatureSeal `xml:"Seal"`
StampAnnot []SignatureStamp `xml:"StampAnnot"`
References SignatureReferences `xml:"References"`
Raw []byte `xml:"-"`
}
// SignatureProvider 签名提供者信息
@@ -79,6 +81,7 @@ type SignatureStamp struct {
ID string `xml:"ID,attr"`
PageRef string `xml:"PageRef,attr"`
Boundary string `xml:"Boundary,attr"`
Clip string `xml:"Clip,attr"`
}
// SignatureStampPosition 签名外观位置信息
@@ -88,6 +91,8 @@ type SignatureStampPosition struct {
PageID string
Boundary string
Box Box
Clip string
ClipBox *Box
}
// SignatureReferences 签名保护文件列表
@@ -113,18 +118,46 @@ func (r *Reader) SignatureStampPositions(stamps []SignatureStamp) ([]SignatureSt
pages := signaturePageNumbers(doc)
positions := make([]SignatureStampPosition, 0, len(stamps))
for _, stamp := range stamps {
box, _ := ParseBox(stamp.Boundary)
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: pages[stamp.PageRef],
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 解析签名文件
// 入参: doc 文档结构
// 返回: error 错误信息
@@ -177,8 +210,19 @@ 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)
}
@@ -380,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,
})
+210 -48
View File
@@ -32,6 +32,8 @@ type digitalVerifyResult struct {
DataHashOK bool
SignedOK bool
CertOK bool
SignerCerts [][]byte
Certs [][]byte
CertInfo SignatureCertInfo
}
@@ -61,17 +63,20 @@ type gbtSignerInfo struct {
}
// verifyDigitalSignature 验证OFD数字签名
// 入参: method 签名算法, signedValue 签名值, signedData 被签名原文, options 验证选项
// 入参: method 签名算法, digestMethod 摘要算法, signedValue 签名值, signedData 被签名原文, options 验证选项
// 返回: *digitalVerifyResult 验证结果, error 错误信息
func verifyDigitalSignature(method string, signedValue, signedData []byte, options *signatureVerifyOptions) (*digitalVerifyResult, error) {
if !isSM2SignatureMethod(method) {
return nil, fmt.Errorf("unsupported signature method")
}
if isGBT35275SignedValue(signedValue) {
return verifyGBT35275SignedData(signedValue, signedData, options)
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 验证选项
@@ -87,6 +92,7 @@ func verifyRawDigitalSignature(signedValue, signedData []byte, options *signatur
continue
}
result.CertOK = true
result.SignerCerts = [][]byte{cert}
result.CertInfo = signatureCertInfo(cert)
if sm2VerifySignature(pub, nil, signedData, signedValue) {
result.SignedOK = true
@@ -96,6 +102,33 @@ func verifyRawDigitalSignature(signedValue, signedData []byte, options *signatur
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 错误信息
@@ -113,16 +146,16 @@ func verifyGBT35275SignedData(signedValue, signedData []byte, options *signature
if len(sd.Signers) == 0 {
return nil, fmt.Errorf("invalid signed data signer info")
}
result := &digitalVerifyResult{}
digest := signSM3(signedData)
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
for _, signer := range sd.Signers {
if !isSM3DigestMethod(signer.DigestAlg) {
return nil, fmt.Errorf("unsupported digest method")
}
plain := sd.ContentDigest
if len(signer.AuthAttrs) != 0 {
if !bytes.Equal(signer.AttrDigest, digest) {
@@ -136,46 +169,185 @@ func verifyGBT35275SignedData(signedValue, signedData []byte, options *signature
}
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) {
return nil, fmt.Errorf("unsupported signature method")
}
if isSM2SignatureMethod(signer.SignatureAlg) {
pub, err := parseSM2PublicKeyFromCert(cert.Raw)
if err != nil {
result.CertOK = false
return result, err
}
result.CertOK = true
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
}
// isGBT35275SignedValue 判断签名值是否为GB/T 35275 SignedData
// normalizeGBT35275SignedValue 规范化GB/T 35275 SignedData编码
// 入参: data 签名值数据
// 返回: bool 是否为SignedData
func isGBT35275SignedValue(data []byte) bool {
// 返回: []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
return "", false
}
items, ok := asn1Children(root.Bytes)
if !ok || len(items) < 2 {
return false
if !ok || len(items) == 0 {
return "", false
}
if items[0].Tag != asn1.TagOID {
return "", true
}
oid, err := asn1OIDString(items[0])
return err == nil && oid == signContentSignedData
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
@@ -282,34 +454,14 @@ func parseGBTCertificates(raw asn1.RawValue) ([]gbtCertificate, error) {
// 入参: data DER编码证书
// 返回: gbtCertificate 证书索引信息, error 错误信息
func parseGBTCertificate(data []byte) (gbtCertificate, error) {
var cert struct {
TBSCertificate asn1.RawValue
SignatureAlgorithm asn1.RawValue
SignatureValue asn1.BitString
}
rest, err := asn1.Unmarshal(data, &cert)
if err != nil || len(rest) != 0 {
return gbtCertificate{}, fmt.Errorf("invalid certificate")
}
items, ok := asn1Children(cert.TBSCertificate.Bytes)
if !ok {
return gbtCertificate{}, fmt.Errorf("invalid tbs certificate")
}
idx := 0
if len(items) > 0 && items[0].Class == asn1.ClassContextSpecific && items[0].Tag == 0 {
idx++
}
if len(items) <= idx+2 {
return gbtCertificate{}, fmt.Errorf("invalid certificate issuer")
}
serial, err := asn1IntegerBig(items[idx])
cert, err := parseSignatureCertificate(data)
if err != nil {
return gbtCertificate{}, err
}
return gbtCertificate{
Raw: append([]byte(nil), data...),
Issuer: append([]byte(nil), items[idx+2].FullBytes...),
Serial: serial,
Raw: cert.Raw,
Issuer: cert.Issuer,
Serial: cert.Serial,
}, nil
}
@@ -449,6 +601,16 @@ func (sd *gbtSignedData) findCert(issuer []byte, serial *big.Int) *gbtCertificat
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 错误信息
+159 -60
View File
@@ -20,6 +20,7 @@ import (
"fmt"
"math/big"
"strings"
"time"
)
const (
@@ -42,6 +43,7 @@ type sesSignature struct {
SignAlg string
Signature []byte
DataHash []byte
Time time.Time
Seal *sesSeal
}
@@ -55,12 +57,19 @@ type sesSeal struct {
PicType string
PicData []byte
CertList sesCertList
Info SignatureSealInfo
}
// sesCertList SES印章证书列表
type sesCertList struct {
Certs [][]byte
Digests [][]byte
Digests []sesCertDigest
}
// sesCertDigest SES印章证书摘要
type sesCertDigest struct {
Method string
Value []byte
}
// sesVerifyResult SES签章验证结果
@@ -71,7 +80,13 @@ type sesVerifyResult struct {
CertOK bool
SignCert SignatureCertInfo
SealCert SignatureCertInfo
SignCertRaw []byte
SealCertRaw []byte
SealRaw []byte
Certs [][]byte
SealType string
SealInfo SignatureSealInfo
SignatureTime time.Time
}
// parseSESSignature 解析SES签章值
@@ -109,13 +124,21 @@ func parseSESSignature(data []byte) (*sesSignature, error) {
return nil, err
}
tbsItems, ok := asn1Children(items[0].Bytes)
if !ok || len(tbsItems) < 5 {
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
@@ -126,6 +149,7 @@ func parseSESSignature(data []byte) (*sesSignature, error) {
SignAlg: alg,
Signature: signature,
DataHash: dataHash,
Time: signatureTime,
Seal: seal,
}, nil
}
@@ -146,6 +170,11 @@ func parseSESSignatureV1(items []asn1.RawValue) (*sesSignature, error) {
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
@@ -164,14 +193,35 @@ func parseSESSignatureV1(items []asn1.RawValue) (*sesSignature, error) {
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 被签名数据原文
// 入参: data 签章值数据, signedData 被签名数据原文, options 验证选项
// 返回: *sesVerifyResult 验证结果, error 错误信息
func verifySESSignature(data, signedData []byte) (*sesVerifyResult, error) {
func verifySESSignature(data, signedData []byte, options *signatureVerifyOptions) (*sesVerifyResult, error) {
sig, err := parseSESSignature(data)
if err != nil {
return nil, err
@@ -182,7 +232,14 @@ func verifySESSignature(data, signedData []byte) (*sesVerifyResult, error) {
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 {
@@ -228,11 +285,7 @@ func parseSESSeal(raw asn1.RawValue) (*sesSeal, error) {
if !ok || len(infoItems) < 4 || len(infoItems) > 5 {
return nil, fmt.Errorf("invalid ses seal info")
}
version, err := parseSESHeaderVersion(infoItems[0])
if err != nil {
return nil, err
}
certList, err := parseSESCertList(infoItems[2], version)
info, certList, err := parseSESSealInfo(infoItems)
if err != nil {
return nil, err
}
@@ -249,6 +302,7 @@ func parseSESSeal(raw asn1.RawValue) (*sesSeal, error) {
PicType: picType,
PicData: picData,
CertList: certList,
Info: info,
}, nil
}
@@ -276,7 +330,7 @@ func parseSESSealV1(raw asn1.RawValue, items []asn1.RawValue) (*sesSeal, error)
if err != nil {
return nil, err
}
certList, err := parseSESCertListV1(infoItems[2])
info, certList, err := parseSESSealInfo(infoItems)
if err != nil {
return nil, err
}
@@ -294,43 +348,92 @@ func parseSESSealV1(raw asn1.RawValue, items []asn1.RawValue) (*sesSeal, error)
PicType: picType,
PicData: picData,
CertList: certList,
Info: info,
}, nil
}
// parseSESHeaderVersion 解析印章头版本
// 入参: raw 印章信息
// 返回: int 版本号, error 错误信息
func parseSESHeaderVersion(raw asn1.RawValue) (int, error) {
items, ok := asn1Children(raw.Bytes)
if !ok || len(items) < 2 {
return 0, fmt.Errorf("invalid ses header")
// 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")
}
return asn1Integer(items[1])
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 解析印章证书列表
// 入参: raw 印章属性信息
// 入参: property 印章属性, version 印章版本, certIndex 证书列表位置
// 返回: sesCertList 证书列表, error 错误信息
func parseSESCertList(raw asn1.RawValue, version int) (sesCertList, error) {
items, ok := asn1Children(raw.Bytes)
if !ok || len(items) < 3 {
return sesCertList{}, fmt.Errorf("invalid ses property")
func parseSESCertList(property []asn1.RawValue, version, certIndex int) (sesCertList, error) {
if version == 1 {
return parseSESCertListV1(property[certIndex])
}
if version < 4 {
return parseSESCertInfoList(items[2])
return parseSESCertInfoList(property[certIndex])
}
if len(items) < 4 {
return sesCertList{}, fmt.Errorf("invalid ses cert list")
}
listType, err := asn1Integer(items[2])
listType, err := asn1Integer(property[certIndex-1])
if err != nil {
return sesCertList{}, err
}
switch listType {
case 1:
return parseSESCertInfoList(items[3])
return parseSESCertInfoList(property[certIndex])
case 2:
return parseSESCertDigestList(items[3])
return parseSESCertDigestList(property[certIndex])
default:
return sesCertList{}, fmt.Errorf("unsupported ses cert list type")
}
@@ -378,17 +481,21 @@ func parseSESCertDigestList(raw asn1.RawValue) (sesCertList, error) {
if !ok || len(items) == 0 {
return sesCertList{}, fmt.Errorf("invalid ses cert digest list")
}
list := sesCertList{Digests: make([][]byte, 0, len(items))}
list := sesCertList{Digests: make([]sesCertDigest, 0, len(items))}
for _, item := range items {
fields, ok := asn1Children(item.Bytes)
if !ok || len(fields) < 2 {
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, digest)
list.Digests = append(list.Digests, sesCertDigest{Method: method, Value: digest})
}
return list, nil
}
@@ -418,30 +525,11 @@ func parseSESPicture(raw asn1.RawValue) (string, []byte, error) {
// 入参: data DER编码证书
// 返回: sm2PublicKey SM2公钥, error 错误信息
func parseSM2PublicKeyFromCert(data []byte) (sm2PublicKey, error) {
var cert struct {
TBSCertificate asn1.RawValue
SignatureAlgorithm asn1.RawValue
SignatureValue asn1.BitString
}
rest, err := asn1.Unmarshal(data, &cert)
cert, err := parseSignatureCertificate(data)
if err != nil {
return sm2PublicKey{}, err
}
if len(rest) != 0 {
return sm2PublicKey{}, fmt.Errorf("invalid certificate")
}
items, ok := asn1Children(cert.TBSCertificate.Bytes)
if !ok {
return sm2PublicKey{}, fmt.Errorf("invalid tbs certificate")
}
idx := 0
if len(items) > 0 && items[0].Class == asn1.ClassContextSpecific && items[0].Tag == 0 {
idx++
}
if len(items) <= idx+5 {
return sm2PublicKey{}, fmt.Errorf("invalid public key info")
}
return parseSM2PublicKeyInfo(items[idx+5])
return parseSM2PublicKeyInfo(cert.PublicKey)
}
// parseSM2PublicKeyInfo 解析SM2公钥信息
@@ -497,16 +585,24 @@ func signSM3(data []byte) []byte {
// 入参: method 算法标识
// 返回: bool 是否为SM2签名算法
func isSM2SignatureMethod(method string) bool {
method = strings.TrimSpace(method)
return method == signMethodSM2SM3 || method == signMethodSM2SM3B || method == signMethodSM2Sign
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 {
method = strings.TrimSpace(method)
return method == signDigestSM3 || method == signDigestSM3NoKey || method == signDigestSM3Key || method == "SM3"
switch signatureMethodText(method) {
case signDigestSM3, signDigestSM3NoKey, signDigestSM3Key, "SM3":
return true
default:
return false
}
}
// sesCertInList 判断证书是否在印章证书列表中
@@ -518,9 +614,12 @@ func sesCertInList(cert []byte, list sesCertList) bool {
return true
}
}
digest := signSM3(cert)
for _, item := range list.Digests {
if bytes.Equal(digest, item) {
digest, err := signatureDigest(item.Method, cert)
if err != nil {
continue
}
if bytes.Equal(digest, item.Value) {
return true
}
}
+184 -21
View File
@@ -15,7 +15,6 @@
package ofdgo
import (
"crypto/elliptic"
"encoding/asn1"
"encoding/binary"
"math/big"
@@ -31,17 +30,32 @@ type sm2PublicKey struct {
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椭圆曲线
// 返回: elliptic.Curve SM2椭圆曲线
func newSM2P256() elliptic.Curve {
c := &elliptic.CurveParams{Name: "SM2-P-256"}
c.P = sm2Big("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF")
c.N = sm2Big("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123")
c.B = sm2Big("28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93")
c.Gx = sm2Big("32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7")
c.Gy = sm2Big("BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0")
c.BitSize = 256
return c
// 返回: *sm2Curve SM2椭圆曲线
func newSM2P256() *sm2Curve {
return &sm2Curve{
P: sm2Big("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF"),
N: sm2Big("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123"),
B: sm2Big("28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93"),
Gx: sm2Big("32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7"),
Gy: sm2Big("BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0"),
}
}
// sm2Big 解析SM2大整数常量
@@ -85,11 +99,11 @@ func parseSM2Signature(sig []byte) (*big.Int, *big.Int, bool) {
// 入参: pub 公钥, userID 用户标识, msg 原文, r R值, s S值
// 返回: bool 是否验证通过
func sm2Verify(pub sm2PublicKey, userID, msg []byte, r, s *big.Int) bool {
n := sm2P256.Params().N
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) {
if pub.X == nil || pub.Y == nil || !sm2P256.isOnCurve(pub.X, pub.Y) {
return false
}
e := new(big.Int).SetBytes(sm2MessageDigest(pub, userID, msg))
@@ -98,10 +112,8 @@ func sm2Verify(pub sm2PublicKey, userID, msg []byte, r, s *big.Int) bool {
if t.Sign() == 0 {
return false
}
x1, y1 := sm2P256.ScalarBaseMult(s.Bytes())
x2, y2 := sm2P256.ScalarMult(pub.X, pub.Y, t.Bytes())
x, _ := sm2P256.Add(x1, y1, x2, y2)
if x == nil {
x, ok := sm2P256.combinedMult(pub.X, pub.Y, s, t)
if !ok {
return false
}
v := new(big.Int).Add(e, x)
@@ -132,9 +144,9 @@ func sm2ZA(pub sm2PublicKey, userID []byte) []byte {
h.Write(entl[:])
h.Write(userID)
h.Write(sm2Fixed(sm2A()))
h.Write(sm2Fixed(sm2P256.Params().B))
h.Write(sm2Fixed(sm2P256.Params().Gx))
h.Write(sm2Fixed(sm2P256.Params().Gy))
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)
@@ -143,7 +155,158 @@ func sm2ZA(pub sm2PublicKey, userID []byte) []byte {
// sm2A 获取SM2曲线A参数
// 返回: *big.Int 曲线A参数
func sm2A() *big.Int {
return new(big.Int).Sub(sm2P256.Params().P, big.NewInt(3))
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固定长度字节
+761 -63
View File
@@ -16,18 +16,30 @@ package ofdgo
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/md5"
"crypto/rsa"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"crypto/subtle"
"crypto/x509"
"encoding/asn1"
"encoding/base64"
"encoding/pem"
"encoding/xml"
"fmt"
"io"
"math/big"
"path"
"strings"
"time"
)
// SignatureVerifyReport 签名验证报告
// Valid表示签名完整性、签名时间语义及调用方指定的证书策略均通过
// SealCertTimeOK仅提供制章证书在签名时间的状态信息, 不参与Valid判断
type SignatureVerifyReport struct {
ID string
BaseLoc string
@@ -36,29 +48,70 @@ type SignatureVerifyReport struct {
Signer string
SignCert SignatureCertInfo
SealCert SignatureCertInfo
SealInfo SignatureSealInfo
SealType string
SignatureMethod string
SignatureDateTime string
SignatureTime time.Time
DigestMethod string
References []SignatureReferenceVerify
Stamps []SignatureStamp
StampPositions []SignatureStampPosition
StampPositionError string
DigestOK bool
DataHashOK bool
SignedValueOK bool
SealOK bool
SealMatchOK bool
CertOK bool
SignatureTimeChecked bool
SignatureTimeOK bool
SealCertTimeChecked bool
SealCertTimeOK bool
SealTimeChecked bool
SealTimeOK bool
CertTimeChecked bool
CertTimeOK bool
CertTrustChecked bool
CertTrustOK bool
Valid bool
Error string
}
// IntegrityValid 判断签名完整性是否有效
// 返回: bool 是否有效
func (report SignatureVerifyReport) IntegrityValid() bool {
return report.Error == "" && report.DigestOK && report.DataHashOK && report.SignedValueOK && report.SealOK && report.SealMatchOK && report.CertOK
}
// TrustedValid 判断签名是否可信有效
// 返回: bool 签名完整性、时间语义、证书信任及证书有效期是否均验证通过
func (report SignatureVerifyReport) TrustedValid() bool {
return report.IntegrityValid() && report.certificatePolicyOK() && report.CertTrustChecked && report.CertTimeChecked
}
// SignatureCertInfo 签名证书信息
type SignatureCertInfo struct {
Raw []byte
Subject string
CommonName string
Organization string
Issuer string
SerialNumber string
NotBefore time.Time
NotAfter time.Time
}
// SignatureSealInfo 电子印章信息
type SignatureSealInfo struct {
Version int
ID string
VendorID string
Type int
Name string
CreateTime time.Time
ValidStart time.Time
ValidEnd time.Time
}
// SignatureReferenceVerify 签名保护文件验证结果
@@ -74,8 +127,12 @@ type SignatureReferenceVerify struct {
// signatureVerifyOptions 签名验证选项
type signatureVerifyOptions struct {
SignCerts [][]byte
TrustCerts [][]byte
VerifyTime *time.Time
}
var signatureMethodReplacer = strings.NewReplacer("-", "", "_", "", " ", "")
// SignatureVerifyOption 签名验证选项函数
type SignatureVerifyOption func(*signatureVerifyOptions)
@@ -84,7 +141,7 @@ type SignatureVerifyOption func(*signatureVerifyOptions)
// 返回: SignatureVerifyOption 签名验证选项
func WithSignatureCert(cert []byte) SignatureVerifyOption {
return func(o *signatureVerifyOptions) {
o.SignCerts = append(o.SignCerts, parseSignatureCerts(cert)...)
o.SignCerts = appendSignatureCerts(o.SignCerts, cert)
}
}
@@ -93,10 +150,45 @@ func WithSignatureCert(cert []byte) SignatureVerifyOption {
// 返回: SignatureVerifyOption 签名验证选项
func WithSignatureCerts(certs ...[]byte) SignatureVerifyOption {
return func(o *signatureVerifyOptions) {
o.SignCerts = appendSignatureCerts(o.SignCerts, certs...)
}
}
// WithSignatureTrustCert 添加签名信任证书
// 入参: cert DER或PEM编码证书
// 返回: SignatureVerifyOption 签名验证选项
func WithSignatureTrustCert(cert []byte) SignatureVerifyOption {
return func(o *signatureVerifyOptions) {
o.TrustCerts = appendSignatureCerts(o.TrustCerts, cert)
}
}
// WithSignatureTrustCerts 添加多张签名信任证书
// 入参: certs DER或PEM编码证书列表
// 返回: SignatureVerifyOption 签名验证选项
func WithSignatureTrustCerts(certs ...[]byte) SignatureVerifyOption {
return func(o *signatureVerifyOptions) {
o.TrustCerts = appendSignatureCerts(o.TrustCerts, certs...)
}
}
// WithSignatureVerifyTime 设置签名证书验证时间
// 入参: t 验证时间
// 返回: SignatureVerifyOption 签名验证选项
func WithSignatureVerifyTime(t time.Time) SignatureVerifyOption {
return func(o *signatureVerifyOptions) {
o.VerifyTime = &t
}
}
// appendSignatureCerts 追加签名证书
// 入参: dst 目标证书列表, certs DER或PEM编码证书列表
// 返回: [][]byte 证书列表
func appendSignatureCerts(dst [][]byte, certs ...[]byte) [][]byte {
for _, cert := range certs {
o.SignCerts = append(o.SignCerts, parseSignatureCerts(cert)...)
}
dst = append(dst, parseSignatureCerts(cert)...)
}
return dst
}
// VerifySignaturesBytes 验证OFD字节数据签名
@@ -170,6 +262,9 @@ func (r *Reader) verifySignature(sigListPath string, sigRef Signature, options *
Type: sigRef.Type,
SealMatchOK: true,
}
if report.Type == "" {
report.Type = SignTypeSeal
}
sigData, err := r.readFileExact(sigPath)
if err != nil {
report.Error = err.Error()
@@ -184,8 +279,15 @@ func (r *Reader) verifySignature(sigListPath string, sigRef Signature, options *
report.SignatureMethod = sigFile.SignedInfo.SignatureMethod
report.SignatureDateTime = sigFile.SignedInfo.SignatureDateTime
report.DigestMethod = sigFile.SignedInfo.References.CheckMethod
if report.DigestMethod == "" {
report.DigestMethod = "MD5"
}
report.References = r.verifySignatureReferences(sigPath, sigFile.SignedInfo.References)
report.Stamps = append(report.Stamps, sigFile.SignedInfo.StampAnnot...)
report.StampPositions, err = r.SignatureStampPositions(report.Stamps)
if err != nil {
report.StampPositionError = err.Error()
}
report.DigestOK = referencesOK(report.References)
signedValuePath := signatureRefPath(sigPath, sigFile.SignedValue)
signedValue, err := r.readFileExact(signedValuePath)
@@ -193,9 +295,9 @@ func (r *Reader) verifySignature(sigListPath string, sigRef Signature, options *
report.Error = err.Error()
return report
}
switch sigRef.Type {
switch report.Type {
case SignTypeSign:
result, err := verifyDigitalSignature(report.SignatureMethod, signedValue, sigData, options)
result, err := verifyDigitalSignature(report.SignatureMethod, report.DigestMethod, signedValue, sigData, options)
if err != nil {
report.Error = err.Error()
return report
@@ -206,18 +308,23 @@ func (r *Reader) verifySignature(sigListPath string, sigRef Signature, options *
report.CertOK = result.CertOK
report.SignCert = result.CertInfo
report.Signer = result.CertInfo.CommonName
report.Valid = report.DigestOK && report.DataHashOK && report.SignedValueOK && report.CertOK
report.SignatureTime = parseSignatureDateTime(report.SignatureDateTime)
report.applySignatureTimePolicy()
report.applySignatureCertificatePolicy(options, result.SignerCerts, result.Certs)
report.Valid = report.IntegrityValid() && report.certificatePolicyOK()
return report
case "", SignTypeSeal:
case SignTypeSeal:
default:
report.Error = fmt.Sprintf("unsupported signature type: %s", sigRef.Type)
report.Error = fmt.Sprintf("unsupported signature type: %s", report.Type)
return report
}
sesResult, err := verifySESSignature(signedValue, sigData)
sesResult, err := verifySESSignature(signedValue, sigData, options)
if sesResult != nil {
report.SignCert = sesResult.SignCert
report.SealCert = sesResult.SealCert
report.SealInfo = sesResult.SealInfo
report.SealType = sesResult.SealType
report.SignatureTime = sesResult.SignatureTime
report.Signer = sesResult.SignCert.CommonName
}
if err != nil {
@@ -228,6 +335,8 @@ func (r *Reader) verifySignature(sigListPath string, sigRef Signature, options *
report.SignedValueOK = sesResult.SignedOK
report.SealOK = sesResult.SealOK
report.CertOK = sesResult.CertOK
report.applySignatureTimePolicy()
report.applySignatureCertificatePolicy(options, [][]byte{sesResult.SignCertRaw, sesResult.SealCertRaw}, sesResult.Certs)
if sigFile.SignedInfo.Seal.BaseLoc != "" {
sealPath := signatureRefPath(sigPath, sigFile.SignedInfo.Seal.BaseLoc)
sealData, err := r.readFileExact(sealPath)
@@ -235,14 +344,9 @@ func (r *Reader) verifySignature(sigListPath string, sigRef Signature, options *
report.Error = err.Error()
return report
}
sig, err := parseSESSignature(signedValue)
if err != nil {
report.Error = err.Error()
return report
report.SealMatchOK = bytes.Equal(sealData, sesResult.SealRaw)
}
report.SealMatchOK = bytes.Equal(sealData, sig.Seal.Raw)
}
report.Valid = report.DigestOK && report.DataHashOK && report.SignedValueOK && report.SealOK && report.SealMatchOK && report.CertOK
report.Valid = report.IntegrityValid() && report.certificatePolicyOK()
return report
}
@@ -295,11 +399,6 @@ func parseSignatureFile(data []byte) (*SignatureFile, error) {
if err := xml.Unmarshal(data, &sigFile); err != nil {
return nil, err
}
raw, err := xmlElementRaw(data, "SignedInfo")
if err != nil {
return nil, err
}
sigFile.SignedInfo.Raw = raw
return &sigFile, nil
}
@@ -318,12 +417,201 @@ func (r *Reader) readFileExact(name string) ([]byte, error) {
// 入参: method 摘要算法, data 原文数据
// 返回: []byte 摘要值, error 错误信息
func signatureDigest(method string, data []byte) ([]byte, error) {
if strings.TrimSpace(method) == "" {
sum := md5.Sum(data)
return sum[:], nil
}
if isSM3DigestMethod(method) {
return signSM3(data), nil
}
if h, ok := signatureDigestHash(method); ok {
return signatureHashBytes(h, data), nil
}
return nil, fmt.Errorf("unsupported digest method: %s", method)
}
// signatureDigestHash 获取摘要算法
// 入参: method 摘要算法
// 返回: crypto.Hash 摘要算法, bool 是否支持
func signatureDigestHash(method string) (crypto.Hash, bool) {
switch signatureMethodText(method) {
case "1.2.840.113549.2.5", "MD5":
return crypto.MD5, true
case "1.3.14.3.2.26", "SHA1":
return crypto.SHA1, true
case "2.16.840.1.101.3.4.2.4", "SHA224":
return crypto.SHA224, true
case "2.16.840.1.101.3.4.2.1", "SHA256":
return crypto.SHA256, true
case "2.16.840.1.101.3.4.2.2", "SHA384":
return crypto.SHA384, true
case "2.16.840.1.101.3.4.2.3", "SHA512":
return crypto.SHA512, true
case "2.16.840.1.101.3.4.2.5", "SHA512224":
return crypto.SHA512_224, true
case "2.16.840.1.101.3.4.2.6", "SHA512256":
return crypto.SHA512_256, true
default:
return 0, false
}
}
// signatureMethodHash 获取签名算法对应摘要算法
// 入参: method 签名算法, digestMethod 摘要算法
// 返回: crypto.Hash 摘要算法, error 错误信息
func signatureMethodHash(method, digestMethod string) (crypto.Hash, error) {
switch signatureMethodText(method) {
case "1.2.840.113549.1.1.5", "RSASHA1", "SHA1RSA", "SHA1WITHRSA":
return crypto.SHA1, nil
case "1.2.840.113549.1.1.14", "RSASHA224", "SHA224RSA", "SHA224WITHRSA":
return crypto.SHA224, nil
case "1.2.840.113549.1.1.11", "RSASHA256", "SHA256RSA", "SHA256WITHRSA":
return crypto.SHA256, nil
case "1.2.840.113549.1.1.12", "RSASHA384", "SHA384RSA", "SHA384WITHRSA":
return crypto.SHA384, nil
case "1.2.840.113549.1.1.13", "RSASHA512", "SHA512RSA", "SHA512WITHRSA":
return crypto.SHA512, nil
case "1.2.840.10045.4.1", "ECDSASHA1", "SHA1ECDSA", "SHA1WITHECDSA":
return crypto.SHA1, nil
case "1.2.840.10045.4.3.1", "ECDSASHA224", "SHA224ECDSA", "SHA224WITHECDSA":
return crypto.SHA224, nil
case "1.2.840.10045.4.3.2", "ECDSASHA256", "SHA256ECDSA", "SHA256WITHECDSA":
return crypto.SHA256, nil
case "1.2.840.10045.4.3.3", "ECDSASHA384", "SHA384ECDSA", "SHA384WITHECDSA":
return crypto.SHA384, nil
case "1.2.840.10045.4.3.4", "ECDSASHA512", "SHA512ECDSA", "SHA512WITHECDSA":
return crypto.SHA512, nil
}
if h, ok := signatureDigestHash(digestMethod); ok {
return h, nil
}
return 0, fmt.Errorf("unsupported signature method: %s", method)
}
// signatureHashBytes 计算摘要
// 入参: h 摘要算法, data 原文数据
// 返回: []byte 摘要值
func signatureHashBytes(h crypto.Hash, data []byte) []byte {
switch h {
case crypto.MD5:
sum := md5.Sum(data)
return sum[:]
case crypto.SHA1:
sum := sha1.Sum(data)
return sum[:]
case crypto.SHA224:
sum := sha256.Sum224(data)
return sum[:]
case crypto.SHA256:
sum := sha256.Sum256(data)
return sum[:]
case crypto.SHA384:
sum := sha512.Sum384(data)
return sum[:]
case crypto.SHA512:
sum := sha512.Sum512(data)
return sum[:]
case crypto.SHA512_224:
sum := sha512.Sum512_224(data)
return sum[:]
case crypto.SHA512_256:
sum := sha512.Sum512_256(data)
return sum[:]
default:
return nil
}
}
// isRSASignatureMethod 判断是否为RSA签名算法
// 入参: method 算法标识
// 返回: bool 是否为RSA签名算法
func isRSASignatureMethod(method string) bool {
switch signatureMethodText(method) {
case "1.2.840.113549.1.1.1", "1.2.840.113549.1.1.5", "1.2.840.113549.1.1.11", "1.2.840.113549.1.1.12", "1.2.840.113549.1.1.13", "1.2.840.113549.1.1.14", "RSA", "RSASHA1", "RSASHA224", "RSASHA256", "RSASHA384", "RSASHA512", "SHA1RSA", "SHA224RSA", "SHA256RSA", "SHA384RSA", "SHA512RSA", "SHA1WITHRSA", "SHA224WITHRSA", "SHA256WITHRSA", "SHA384WITHRSA", "SHA512WITHRSA":
return true
default:
return false
}
}
// isECDSASignatureMethod 判断是否为ECDSA签名算法
// 入参: method 算法标识
// 返回: bool 是否为ECDSA签名算法
func isECDSASignatureMethod(method string) bool {
switch signatureMethodText(method) {
case "1.2.840.10045.4.1", "1.2.840.10045.4.3.1", "1.2.840.10045.4.3.2", "1.2.840.10045.4.3.3", "1.2.840.10045.4.3.4", "ECDSA", "ECDSASHA1", "ECDSASHA224", "ECDSASHA256", "ECDSASHA384", "ECDSASHA512", "SHA1ECDSA", "SHA224ECDSA", "SHA256ECDSA", "SHA384ECDSA", "SHA512ECDSA", "SHA1WITHECDSA", "SHA224WITHECDSA", "SHA256WITHECDSA", "SHA384WITHECDSA", "SHA512WITHECDSA":
return true
default:
return false
}
}
// signatureMethodText 规范化算法标识
// 入参: method 算法标识
// 返回: string 规范化算法标识
func signatureMethodText(method string) string {
method = strings.TrimSpace(method)
if len(method) >= len("urn:oid:") && strings.EqualFold(method[:len("urn:oid:")], "urn:oid:") {
method = method[len("urn:oid:"):]
}
if idx := strings.LastIndexAny(method, "#/"); idx >= 0 && idx+1 < len(method) {
method = method[idx+1:]
}
method = strings.ToUpper(method)
return signatureMethodReplacer.Replace(method)
}
// verifyPublicKeySignature 验证公钥签名
// 入参: method 签名算法, digestMethod 摘要算法, cert 证书, signedData 被签名数据, signedValue 签名值
// 返回: bool 是否验证通过, error 错误信息
func verifyPublicKeySignature(method, digestMethod string, cert, signedData, signedValue []byte) (bool, error) {
if !isRSASignatureMethod(method) && !isECDSASignatureMethod(method) {
return false, fmt.Errorf("unsupported signature method: %s", method)
}
h, err := signatureMethodHash(method, digestMethod)
if err != nil {
return false, err
}
digest := signatureHashBytes(h, signedData)
if len(digest) == 0 {
return false, fmt.Errorf("unsupported digest method")
}
x509Cert, err := x509.ParseCertificate(cert)
if err != nil {
return false, err
}
switch pub := x509Cert.PublicKey.(type) {
case *rsa.PublicKey:
if !isRSASignatureMethod(method) {
return false, nil
}
return rsa.VerifyPKCS1v15(pub, h, digest, signedValue) == nil, nil
case *ecdsa.PublicKey:
if !isECDSASignatureMethod(method) {
return false, nil
}
return verifyECDSASignature(pub, digest, signedValue), nil
default:
return false, fmt.Errorf("unsupported public key algorithm")
}
}
// verifyECDSASignature 验证ECDSA签名
// 入参: pub 公钥, digest 摘要, sig 签名值
// 返回: bool 是否验证通过
func verifyECDSASignature(pub *ecdsa.PublicKey, digest, sig []byte) bool {
if ecdsa.VerifyASN1(pub, digest, sig) {
return true
}
if len(sig) == 0 || len(sig)%2 != 0 {
return false
}
n := len(sig) / 2
r := new(big.Int).SetBytes(sig[:n])
s := new(big.Int).SetBytes(sig[n:])
return ecdsa.Verify(pub, digest, r, s)
}
// signatureRefPath 解析签名文件引用路径
// 入参: basePath 基准路径, refPath 引用路径
// 返回: string 包内文件路径
@@ -351,50 +639,294 @@ func referencesOK(refs []SignatureReferenceVerify) bool {
return true
}
// xmlElementRaw 提取XML元素原始字节
// 入参: data XML数据, localName 元素名称
// 返回: []byte 元素原始字节, error 错误信息
func xmlElementRaw(data []byte, localName string) ([]byte, error) {
dec := xml.NewDecoder(bytes.NewReader(data))
for {
tok, err := dec.Token()
if err == io.EOF {
// applySignatureCertificatePolicy 应用签名证书策略
// 入参: options 验证选项, certs 待验证证书, extraCerts 证书池
func (report *SignatureVerifyReport) applySignatureCertificatePolicy(options *signatureVerifyOptions, certs [][]byte, extraCerts [][]byte) {
certs = compactSignatureCerts(certs)
if options.VerifyTime != nil {
report.CertTimeChecked = true
report.CertTimeOK = signatureCertsValidAt(certs, *options.VerifyTime)
}
if len(options.TrustCerts) != 0 {
report.CertTrustChecked = true
report.CertTrustOK = true
pool := append([][]byte{}, options.TrustCerts...)
pool = append(pool, options.SignCerts...)
pool = append(pool, extraCerts...)
pool = compactSignatureCerts(pool)
for _, cert := range certs {
if !signatureCertTrustedBy(cert, pool, options.TrustCerts, options.VerifyTime) {
report.CertTrustOK = false
break
}
if err != nil {
return nil, err
}
start, ok := tok.(xml.StartElement)
if !ok || start.Name.Local != localName {
if len(certs) == 0 {
report.CertTrustOK = false
}
}
}
// applySignatureTimePolicy 应用签名时间策略
func (report *SignatureVerifyReport) applySignatureTimePolicy() {
if !report.SignatureTime.IsZero() && !report.SignCert.NotBefore.IsZero() && !report.SignCert.NotAfter.IsZero() {
report.SignatureTimeChecked = true
report.SignatureTimeOK = timeInRange(report.SignatureTime, report.SignCert.NotBefore, report.SignCert.NotAfter)
}
if !report.SignatureTime.IsZero() && !report.SealCert.NotBefore.IsZero() && !report.SealCert.NotAfter.IsZero() {
report.SealCertTimeChecked = true
report.SealCertTimeOK = timeInRange(report.SignatureTime, report.SealCert.NotBefore, report.SealCert.NotAfter)
}
if !report.SignatureTime.IsZero() && !report.SealInfo.ValidStart.IsZero() && !report.SealInfo.ValidEnd.IsZero() {
report.SealTimeChecked = true
report.SealTimeOK = timeInRange(report.SignatureTime, report.SealInfo.ValidStart, report.SealInfo.ValidEnd)
}
}
// timeInRange 判断时间是否位于闭区间
// 入参: t 待判断时间, start 起始时间, end 结束时间
// 返回: bool 是否位于区间
func timeInRange(t, start, end time.Time) bool {
return !start.After(end) && !t.Before(start) && !t.After(end)
}
// certificatePolicyOK 判断证书策略是否通过
// 返回: bool 是否通过
func (report SignatureVerifyReport) certificatePolicyOK() bool {
if report.SignatureTimeChecked && !report.SignatureTimeOK {
return false
}
if report.SealTimeChecked && !report.SealTimeOK {
return false
}
if report.CertTimeChecked && !report.CertTimeOK {
return false
}
if report.CertTrustChecked && !report.CertTrustOK {
return false
}
return true
}
// signatureCertsValidAt 判断证书是否在指定时间有效
// 入参: certs 证书列表, t 验证时间
// 返回: bool 是否有效
func signatureCertsValidAt(certs [][]byte, t time.Time) bool {
if len(certs) == 0 {
return false
}
for _, cert := range certs {
info, err := parseSignatureCertificate(cert)
if err != nil || t.Before(info.NotBefore) || t.After(info.NotAfter) {
return false
}
}
return true
}
// signatureCertMaxChecks 最大证书签名验证次数
const signatureCertMaxChecks = 100
// signatureCertPathState 证书路径状态
type signatureCertPathState struct {
Visited map[string]bool
SignatureChecks int
}
// signatureCertTrustedBy 判断证书是否可链到信任证书
// 入参: cert 证书, pool 证书池, trusts 信任证书, verifyTime 中间证书验证时间
// 返回: bool 是否受信任
func signatureCertTrustedBy(cert []byte, pool, trusts [][]byte, verifyTime *time.Time) bool {
state := signatureCertPathState{Visited: make(map[string]bool)}
return signatureCertPathTrustedBy(cert, pool, trusts, verifyTime, &state, 0, true)
}
// signatureCertPathTrustedBy 验证证书路径
// 入参: cert 证书, pool 证书池, trusts 信任证书, verifyTime 中间证书验证时间, state 路径状态, caBelow 下级非自颁发中间CA数量, target 是否目标证书
// 返回: bool 是否受信任
func signatureCertPathTrustedBy(cert []byte, pool, trusts [][]byte, verifyTime *time.Time, state *signatureCertPathState, caBelow int, target bool) bool {
if len(cert) == 0 {
return false
}
trusted := false
for _, trust := range trusts {
if bytes.Equal(cert, trust) {
trusted = true
break
}
}
if trusted && !target {
return true
}
key := string(cert)
if state.Visited[key] {
return false
}
state.Visited[key] = true
defer delete(state.Visited, key)
c, err := parseSignatureCertificate(cert)
if err != nil {
return false
}
if c.UnhandledCritical {
return false
}
if !target && verifyTime != nil && (verifyTime.Before(c.NotBefore) || verifyTime.After(c.NotAfter)) {
return false
}
if target {
if c.KeyUsage != 0 && c.KeyUsage&(x509.KeyUsageDigitalSignature|x509.KeyUsageContentCommitment) == 0 {
return false
}
} else {
if !c.IsCA {
return false
}
if c.KeyUsage != 0 && c.KeyUsage&x509.KeyUsageCertSign == 0 {
return false
}
if c.MaxPathLen != nil && c.MaxPathLen.Cmp(big.NewInt(int64(caBelow))) < 0 {
return false
}
}
if trusted {
return true
}
nextCABelow := caBelow
if !target && !bytes.Equal(c.Issuer, c.Subject) {
nextCABelow++
}
for _, issuerCert := range pool {
if state.SignatureChecks >= signatureCertMaxChecks {
return false
}
if bytes.Equal(cert, issuerCert) {
continue
}
end := int(dec.InputOffset())
begin := bytes.LastIndex(data[:end], []byte("<"))
if begin < 0 {
return nil, fmt.Errorf("xml element not found: %s", localName)
issuer, err := parseSignatureCertificate(issuerCert)
if err != nil || !bytes.Equal(c.Issuer, issuer.Subject) {
continue
}
depth := 1
for depth > 0 {
tok, err = dec.Token()
state.SignatureChecks++
if ok, err := verifyCertificateSignature(c, issuerCert); err != nil || !ok {
continue
}
if signatureCertPathTrustedBy(issuerCert, pool, trusts, verifyTime, state, nextCABelow, false) {
return true
}
}
return false
}
// verifyCertificateSignature 验证证书签名
// 入参: cert 证书信息, issuerCert 颁发者证书
// 返回: bool 是否验证通过, error 错误信息
func verifyCertificateSignature(cert signatureCertificate, issuerCert []byte) (bool, error) {
if isSM2SignatureMethod(cert.SignatureAlg) {
pub, err := parseSM2PublicKeyFromCert(issuerCert)
if err != nil {
return nil, err
return false, err
}
switch tok.(type) {
case xml.StartElement:
depth++
case xml.EndElement:
depth--
return sm2VerifySignature(pub, nil, cert.TBS, cert.Signature), nil
}
return verifyPublicKeySignature(cert.SignatureAlg, "", issuerCert, cert.TBS, cert.Signature)
}
return append([]byte(nil), data[begin:int(dec.InputOffset())]...), nil
// compactSignatureCerts 清理证书列表
// 入参: certs 证书列表
// 返回: [][]byte 清理后的证书列表
func compactSignatureCerts(certs [][]byte) [][]byte {
out := make([][]byte, 0, len(certs))
seen := make(map[string]bool)
for _, cert := range certs {
if len(cert) == 0 {
continue
}
return nil, fmt.Errorf("xml element not found: %s", localName)
key := string(cert)
if seen[key] {
continue
}
seen[key] = true
out = append(out, cert)
}
return out
}
// signatureCertInfo 解析签名证书信息
// 入参: data DER编码证书
// 返回: SignatureCertInfo 签名证书信息
func signatureCertInfo(data []byte) SignatureCertInfo {
cert, err := parseSignatureCertificate(data)
if err != nil {
return SignatureCertInfo{}
}
subject := certificateNameValues(cert.SubjectValue)
issuer := certificateNameValues(cert.IssuerValue)
info := SignatureCertInfo{
Raw: append([]byte(nil), data...),
Subject: certificateNameString(subject),
CommonName: certificateNameFirst(subject, "2.5.4.3"),
Organization: certificateNameFirst(subject, "2.5.4.10"),
Issuer: certificateNameString(issuer),
NotBefore: cert.NotBefore,
NotAfter: cert.NotAfter,
}
if cert.Serial != nil {
info.SerialNumber = cert.Serial.String()
}
return info
}
// parseSignatureDateTime 解析带时区的签名时间
// 入参: value 签名时间文本
// 返回: time.Time 签名时间
func parseSignatureDateTime(value string) time.Time {
value = strings.TrimSpace(value)
for _, layout := range []string{
time.RFC3339Nano,
"20060102150405.999999999Z07:00",
"20060102150405Z07:00",
} {
if t, err := time.Parse(layout, value); err == nil {
return t
}
}
return time.Time{}
}
const (
signatureExtensionKeyUsage = "2.5.29.15"
signatureExtensionBasicConstraints = "2.5.29.19"
)
// signatureCertificateExtensions 签名证书扩展
type signatureCertificateExtensions struct {
IsCA bool
MaxPathLen *big.Int
KeyUsage x509.KeyUsage
UnhandledCritical bool
}
// signatureCertificate 签名证书结构
type signatureCertificate struct {
Raw []byte
TBS []byte
Issuer []byte
IssuerValue asn1.RawValue
Subject []byte
SubjectValue asn1.RawValue
PublicKey asn1.RawValue
Serial *big.Int
NotBefore time.Time
NotAfter time.Time
SignatureAlg string
Signature []byte
signatureCertificateExtensions
}
// parseSignatureCertificate 解析签名证书
// 入参: data DER编码证书
// 返回: signatureCertificate 签名证书结构, error 错误信息
func parseSignatureCertificate(data []byte) (signatureCertificate, error) {
var cert struct {
TBSCertificate asn1.RawValue
SignatureAlgorithm asn1.RawValue
@@ -402,32 +934,198 @@ func signatureCertInfo(data []byte) SignatureCertInfo {
}
rest, err := asn1.Unmarshal(data, &cert)
if err != nil || len(rest) != 0 {
return SignatureCertInfo{}
return signatureCertificate{}, fmt.Errorf("invalid certificate")
}
items, ok := asn1Children(cert.TBSCertificate.Bytes)
if !ok {
return SignatureCertInfo{}
return signatureCertificate{}, fmt.Errorf("invalid tbs certificate")
}
idx := 0
if len(items) > 0 && items[0].Class == asn1.ClassContextSpecific && items[0].Tag == 0 {
idx++
}
if len(items) <= idx+4 {
return SignatureCertInfo{}
if len(items) <= idx+5 {
return signatureCertificate{}, fmt.Errorf("invalid certificate")
}
serial, _ := asn1IntegerBig(items[idx])
subject := certificateNameValues(items[idx+4])
issuer := certificateNameValues(items[idx+2])
info := SignatureCertInfo{
Subject: certificateNameString(subject),
CommonName: certificateNameFirst(subject, "2.5.4.3"),
Organization: certificateNameFirst(subject, "2.5.4.10"),
Issuer: certificateNameString(issuer),
if !bytes.Equal(items[idx+1].FullBytes, cert.SignatureAlgorithm.FullBytes) {
return signatureCertificate{}, fmt.Errorf("certificate signature algorithm mismatch")
}
if serial != nil {
info.SerialNumber = serial.String()
serial, err := asn1IntegerBig(items[idx])
if err != nil {
return signatureCertificate{}, err
}
return info
validity, err := parseCertificateValidity(items[idx+3])
if err != nil {
return signatureCertificate{}, err
}
extensions, err := parseSignatureCertificateExtensions(items[idx+6:])
if err != nil {
return signatureCertificate{}, err
}
alg, err := parseGBTAlgorithm(cert.SignatureAlgorithm)
if err != nil {
return signatureCertificate{}, err
}
if cert.SignatureValue.BitLength%8 != 0 {
return signatureCertificate{}, fmt.Errorf("invalid certificate signature")
}
return signatureCertificate{
Raw: append([]byte(nil), data...),
TBS: append([]byte(nil), cert.TBSCertificate.FullBytes...),
Issuer: append([]byte(nil), items[idx+2].FullBytes...),
IssuerValue: items[idx+2],
Subject: append([]byte(nil), items[idx+4].FullBytes...),
SubjectValue: items[idx+4],
PublicKey: items[idx+5],
Serial: serial,
NotBefore: validity[0],
NotAfter: validity[1],
SignatureAlg: alg,
Signature: append([]byte(nil), cert.SignatureValue.Bytes...),
signatureCertificateExtensions: extensions,
}, nil
}
// parseSignatureCertificateExtensions 解析签名证书扩展
// 入参: items TBS证书剩余字段
// 返回: signatureCertificateExtensions 签名证书扩展, error 错误信息
func parseSignatureCertificateExtensions(items []asn1.RawValue) (signatureCertificateExtensions, error) {
var out signatureCertificateExtensions
var extensions []struct {
ID asn1.ObjectIdentifier
Critical bool `asn1:"optional"`
Value []byte
}
found := false
for _, item := range items {
if item.Class != asn1.ClassContextSpecific || item.Tag != 3 {
continue
}
if found || !item.IsCompound {
return out, fmt.Errorf("invalid certificate extensions")
}
found = true
rest, err := asn1.Unmarshal(item.Bytes, &extensions)
if err != nil || len(rest) != 0 {
return out, fmt.Errorf("invalid certificate extensions")
}
}
seen := make(map[string]bool)
for _, extension := range extensions {
oid := extension.ID.String()
if seen[oid] {
return out, fmt.Errorf("duplicate certificate extension: %s", oid)
}
seen[oid] = true
switch oid {
case signatureExtensionBasicConstraints:
isCA, maxPathLen, err := parseSignatureBasicConstraints(extension.Value)
if err != nil {
return out, err
}
out.IsCA = isCA
out.MaxPathLen = maxPathLen
case signatureExtensionKeyUsage:
keyUsage, err := parseSignatureKeyUsage(extension.Value)
if err != nil {
return out, err
}
out.KeyUsage = keyUsage
default:
if extension.Critical {
out.UnhandledCritical = true
}
}
}
return out, nil
}
// parseSignatureBasicConstraints 解析证书基本约束
// 入参: data 扩展DER数据
// 返回: bool 是否为CA, *big.Int 路径长度限制, error 错误信息
func parseSignatureBasicConstraints(data []byte) (bool, *big.Int, error) {
var raw asn1.RawValue
rest, err := asn1.Unmarshal(data, &raw)
if err != nil || len(rest) != 0 || raw.Tag != asn1.TagSequence || !raw.IsCompound {
return false, nil, fmt.Errorf("invalid basic constraints")
}
items, ok := asn1Children(raw.Bytes)
if !ok || len(items) > 2 {
return false, nil, fmt.Errorf("invalid basic constraints")
}
idx := 0
isCA := false
if len(items) > 0 && items[0].Tag == asn1.TagBoolean {
rest, err := asn1.Unmarshal(items[0].FullBytes, &isCA)
if err != nil || len(rest) != 0 {
return false, nil, fmt.Errorf("invalid basic constraints")
}
idx++
}
var maxPathLen *big.Int
if idx < len(items) {
maxPathLen, err = asn1IntegerBig(items[idx])
if err != nil || maxPathLen.Sign() < 0 {
return false, nil, fmt.Errorf("invalid basic constraints")
}
idx++
}
if idx != len(items) || (maxPathLen != nil && !isCA) {
return false, nil, fmt.Errorf("invalid basic constraints")
}
return isCA, maxPathLen, nil
}
// parseSignatureKeyUsage 解析证书密钥用途
// 入参: data 扩展DER数据
// 返回: x509.KeyUsage 密钥用途, error 错误信息
func parseSignatureKeyUsage(data []byte) (x509.KeyUsage, error) {
var bits asn1.BitString
rest, err := asn1.Unmarshal(data, &bits)
if err != nil || len(rest) != 0 || bits.BitLength == 0 || bits.BitLength > 9 {
return 0, fmt.Errorf("invalid key usage")
}
var out x509.KeyUsage
for i := 0; i < bits.BitLength; i++ {
if bits.At(i) != 0 {
out |= 1 << uint(i)
}
}
if out == 0 {
return 0, fmt.Errorf("invalid key usage")
}
return out, nil
}
// parseCertificateValidity 解析证书有效期
// 入参: raw 证书有效期ASN.1值
// 返回: [2]time.Time 生效和失效时间, error 错误信息
func parseCertificateValidity(raw asn1.RawValue) ([2]time.Time, error) {
items, ok := asn1Children(raw.Bytes)
if !ok || len(items) != 2 {
return [2]time.Time{}, fmt.Errorf("invalid certificate validity")
}
notBefore, err := asn1Time(items[0])
if err != nil {
return [2]time.Time{}, err
}
notAfter, err := asn1Time(items[1])
if err != nil {
return [2]time.Time{}, err
}
return [2]time.Time{notBefore, notAfter}, nil
}
// asn1Time 解析ASN.1时间
// 入参: raw ASN.1原始值
// 返回: time.Time 时间, error 错误信息
func asn1Time(raw asn1.RawValue) (time.Time, error) {
var t time.Time
rest, err := asn1.Unmarshal(raw.FullBytes, &t)
if err != nil || len(rest) != 0 {
return time.Time{}, fmt.Errorf("invalid time")
}
return t, nil
}
// certificateNameValues 解析证书名称字段
+293 -304
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),
}
}
return color.Black
// 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
}
// 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
// textGlyphPathCacheValue 字形路径缓存值
type textGlyphPathCacheValue struct {
path *canvas.Path
width float64
}
// clampColor 限制颜色分量范围
// 入参: v 颜色分量
// 返回: int 颜色分量
func clampColor(v int) int {
if v < 0 {
// 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 fontID
}
// 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
}
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
}
// 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
if index+1 < len(xs) {
if advance := xs[index+1] - currentX; advance > 0 {
return advance
}
return v
}
if advance, ok := textDelta(dxs, index); ok && advance > 0 {
return advance
}
return 0
}
// 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
// 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
}
// withFillAlpha 合并填充色透明度
// 入参: fillColor 填充颜色节点, alpha 对象透明度
// 返回: *FillColor 合并后的填充颜色节点
func withFillAlpha(fillColor *FillColor, alpha *int) *FillColor {
if fillColor == nil || alpha == nil {
return fillColor
// textDelta 获取文本偏移量
// 入参: deltas 偏移量数组, index 偏移量索引
// 返回: float64 偏移量, bool 是否存在
func textDelta(deltas []float64, index int) (float64, bool) {
if len(deltas) == 0 {
return 0, false
}
merged := *fillColor
merged.Alpha = mergeAlpha(fillColor.Alpha, alpha)
return &merged
if index < len(deltas) {
return deltas[index], true
}
return deltas[len(deltas)-1], true
}
// withStrokeAlpha 合并勾边色透明度
// 入参: strokeColor 勾边颜色节点, alpha 对象透明度
// 返回: *StrokeColor 合并后的勾边颜色节点
func withStrokeAlpha(strokeColor *StrokeColor, alpha *int) *StrokeColor {
if strokeColor == nil || alpha == nil {
return strokeColor
// textCodeRunes 获取文本编码字符
// 入参: value 文本编码内容
// 返回: []rune 文本字符
func textCodeRunes(value string) []rune {
if strings.ContainsAny(value, "\r\n") {
value = strings.TrimSpace(value)
}
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)
}
}
return color.Black
}
// 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)
}
// 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)
}
}
return color.Black
}
// 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
}
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轴偏移量数组