Compare commits

...
51 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
36 changed files with 4634 additions and 2306 deletions
+1 -1
View File
@@ -88,7 +88,7 @@ func main() {
} }
valid := true valid := true
for _, report := range reports { for _, report := range reports {
if report.Valid { if report.IntegrityValid() {
log.Printf("签名%s验证通过", report.ID) log.Printf("签名%s验证通过", report.ID)
continue continue
} }
+9 -3
View File
@@ -9,7 +9,7 @@
<body> <body>
<div class="app"> <div class="app">
<header class="toolbar app-bar"> <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> <input id="fontInput" class="file-input" type="file" accept=".ttf,.otf,.ttc,font/*" multiple>
<div class="tool-group"> <div class="tool-group">
<button id="ofdButton" class="button primary" type="button" data-short="打开">打开</button> <button id="ofdButton" class="button primary" type="button" data-short="打开">打开</button>
@@ -36,8 +36,14 @@
</div> </div>
<div class="tool-group"> <div class="tool-group">
<select id="pageExportFormat" class="format-select" title="单页格式" aria-label="单页格式" disabled></select> <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> <select id="imageDPI" class="format-select dpi-select" title="图片 DPI" aria-label="图片 DPI" disabled>
<button id="exportButton" class="button" type="button" data-short="文档" title="导出文档 PDF" aria-label="导出文档 PDF" disabled>文档</button> <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> </div>
</header> </header>
+13 -5
View File
@@ -10,7 +10,7 @@
--text-muted: #66737f; --text-muted: #66737f;
--accent: #0f7b68; --accent: #0f7b68;
--accent-strong: #095c4e; --accent-strong: #095c4e;
--focus: #c9892b; --focus: #0f7b68;
--sidebar-width: clamp(220px, 20vw, 292px); --sidebar-width: clamp(220px, 20vw, 292px);
--bar-bg: #fbfcfd; --bar-bg: #fbfcfd;
--bar-height: 40px; --bar-height: 40px;
@@ -217,6 +217,10 @@ button:not(:disabled):hover {
color: #1f2428; color: #1f2428;
} }
.dpi-select {
width: 54px;
}
.format-select:focus, .format-select:focus,
.format-select:focus-visible { .format-select:focus-visible {
border-color: var(--accent); border-color: var(--accent);
@@ -397,6 +401,10 @@ body[data-hide-meta] .meta-panel {
background-size: 24px 24px; background-size: 24px 24px;
} }
.viewer-panel.single-page-fits-height {
overflow-y: hidden;
}
.empty-state { .empty-state {
position: absolute; position: absolute;
inset: 0; inset: 0;
@@ -1069,15 +1077,15 @@ body[data-hide-meta] .meta-panel {
min-width: 34px; min-width: 34px;
} }
#exportButton {
padding-inline: 8px;
}
.format-select { .format-select {
width: 52px; width: 52px;
padding: 0 3px; padding: 0 3px;
} }
.dpi-select {
width: 48px;
}
.footer-links { .footer-links {
justify-content: flex-end; justify-content: flex-end;
} }
+138 -28
View File
@@ -18,6 +18,7 @@ const COMMON_FONT_NAMES = [
]; ];
const LOCAL_FONT_LOAD_LIMIT = 16; const LOCAL_FONT_LOAD_LIMIT = 16;
const COMPACT_LAYOUT = window.matchMedia("(max-width: 900px)"); const COMPACT_LAYOUT = window.matchMedia("(max-width: 900px)");
const DEFAULT_IMAGE_DPI = 300;
const STATUS = { const STATUS = {
ready: "选择 OFD 文件", ready: "选择 OFD 文件",
opening: "正在打开 OFD", opening: "正在打开 OFD",
@@ -92,6 +93,7 @@ const el = {
fitButton: document.querySelector("#fitButton"), fitButton: document.querySelector("#fitButton"),
fitHeightButton: document.querySelector("#fitHeightButton"), fitHeightButton: document.querySelector("#fitHeightButton"),
annotationButton: document.querySelector("#annotationButton"), annotationButton: document.querySelector("#annotationButton"),
imageDPI: document.querySelector("#imageDPI"),
pageExportFormat: document.querySelector("#pageExportFormat"), pageExportFormat: document.querySelector("#pageExportFormat"),
exportPageButton: document.querySelector("#exportPageButton"), exportPageButton: document.querySelector("#exportPageButton"),
exportButton: document.querySelector("#exportButton"), exportButton: document.querySelector("#exportButton"),
@@ -125,7 +127,7 @@ const el = {
el.ofdButton.addEventListener("click", openOFDFile); el.ofdButton.addEventListener("click", openOFDFile);
el.togglePagesButton.addEventListener("click", () => toggleSidebar("pages")); el.togglePagesButton.addEventListener("click", () => toggleSidebar("pages"));
el.toggleMetaButton.addEventListener("click", () => toggleSidebar("meta")); el.toggleMetaButton.addEventListener("click", () => toggleSidebar("meta"));
el.fontAddButton.addEventListener("click", () => el.fontInput.click()); el.fontAddButton.addEventListener("click", openFontFile);
el.localFontButton.addEventListener("click", loadLocalFonts); el.localFontButton.addEventListener("click", loadLocalFonts);
el.ofdInput.addEventListener("change", openSelectedOFD); el.ofdInput.addEventListener("change", openSelectedOFD);
el.fontInput.addEventListener("change", openSelectedFonts); el.fontInput.addEventListener("change", openSelectedFonts);
@@ -136,6 +138,7 @@ el.zoomInButton.addEventListener("click", () => setScale(state.scale + 0.1));
el.fitButton.addEventListener("click", fitWidth); el.fitButton.addEventListener("click", fitWidth);
el.fitHeightButton.addEventListener("click", fitHeight); el.fitHeightButton.addEventListener("click", fitHeight);
el.annotationButton.addEventListener("click", toggleAnnotations); el.annotationButton.addEventListener("click", toggleAnnotations);
el.pageExportFormat.addEventListener("change", () => updateDPIControl());
el.exportPageButton.addEventListener("click", exportCurrentPage); el.exportPageButton.addEventListener("click", exportCurrentPage);
el.exportButton.addEventListener("click", exportPDF); el.exportButton.addEventListener("click", exportPDF);
el.pageInput.addEventListener("change", () => { el.pageInput.addEventListener("change", () => {
@@ -163,6 +166,7 @@ async function openOFDFile() {
return; return;
} }
await requestLocalFontsBeforeOpen(); await requestLocalFontsBeforeOpen();
el.ofdInput.value = "";
el.ofdInput.click(); el.ofdInput.click();
} }
@@ -173,6 +177,11 @@ async function openOFDFromViewer() {
await openOFDFile(); await openOFDFile();
} }
function openFontFile() {
el.fontInput.value = "";
el.fontInput.click();
}
function toggleSidebar(side) { function toggleSidebar(side) {
if (side === "pages") { if (side === "pages") {
state.showPages = !state.showPages; state.showPages = !state.showPages;
@@ -236,6 +245,7 @@ function loadExportFormats() {
if (formats.length) { if (formats.length) {
el.pageExportFormat.value = formats[0].value; el.pageExportFormat.value = formats[0].value;
} }
updateDPIControl();
} }
async function ensureWASM() { async function ensureWASM() {
@@ -361,6 +371,11 @@ async function openSelectedOFD() {
if (!file) { if (!file) {
return; return;
} }
if (!isOFDFile(file)) {
el.ofdInput.value = "";
showError(new Error("选择 OFD 文件"), !state.doc);
return;
}
state.wasmRecoveries = 0; state.wasmRecoveries = 0;
setBusy(true, "正在读取 OFD", 10, STATUS.opening); setBusy(true, "正在读取 OFD", 10, STATUS.opening);
try { try {
@@ -373,6 +388,10 @@ async function openSelectedOFD() {
} }
} }
function isOFDFile(file) {
return /\.ofd$/i.test(file.name || "");
}
async function openSelectedFonts() { async function openSelectedFonts() {
const files = Array.from(el.fontInput.files || []); const files = Array.from(el.fontInput.files || []);
if (!files.length) { if (!files.length) {
@@ -872,7 +891,7 @@ async function openDocument(options = {}) {
const pageIndex = Math.min(Math.max(options.pageIndex || 0, 0), Math.max(pageCount - 1, 0)); const pageIndex = Math.min(Math.max(options.pageIndex || 0, 0), Math.max(pageCount - 1, 0));
state.doc = doc; state.doc = doc;
state.pageIndex = pageIndex; state.pageIndex = pageIndex;
state.scale = 1; state.scale = options.scale || 1;
state.fitMode = options.fitMode || "width"; state.fitMode = options.fitMode || "width";
if (!options.skipAutoFonts && await autoLoadDocumentLocalFonts(openSeq)) { if (!options.skipAutoFonts && await autoLoadDocumentLocalFonts(openSeq)) {
if (openSeq !== state.openSeq) { if (openSeq !== state.openSeq) {
@@ -984,7 +1003,7 @@ async function exportPDF() {
return; return;
} }
setProgress("正在保存文档 PDF", 86); setProgress("正在保存文档 PDF", 86);
const bytes = base64ToBytes(result.base64); const bytes = result.bytes;
downloadBytes(bytes, "application/pdf", pdfFileName()); downloadBytes(bytes, "application/pdf", pdfFileName());
setStatus(`文档 PDF 已导出 ${formatBytes(result.size || bytes.length)}`); setStatus(`文档 PDF 已导出 ${formatBytes(result.size || bytes.length)}`);
} catch (err) { } catch (err) {
@@ -1022,12 +1041,13 @@ async function exportCurrentPage() {
if (openSeq !== state.openSeq) { if (openSeq !== state.openSeq) {
return; 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) { if (openSeq !== state.openSeq) {
return; return;
} }
setProgress(`正在保存 ${result.label || label}`, 86); 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)); 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)}`); setStatus(`${result.label || label} 已导出 ${formatBytes(result.size || bytes.length, result.label || label)}`);
} catch (err) { } catch (err) {
@@ -1289,13 +1309,25 @@ function pageShell(index) {
function scrollToPage(index) { function scrollToPage(index) {
const shell = pageShell(index); const shell = pageShell(index);
if (shell) { if (shell) {
shell.scrollIntoView({ block: state.fitMode === "height" ? "center" : "start", inline: "nearest" }); if (state.fitMode === "height") {
if (state.fitMode !== "height") { shell.scrollIntoView({ block: "center", inline: "nearest" });
el.viewerPanel.scrollTop = Math.max(0, el.viewerPanel.scrollTop - pageBlockSpace()); } 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() { function schedulePageSync() {
if (state.scrollFrame || !state.doc) { if (state.scrollFrame || !state.doc) {
return; return;
@@ -1332,6 +1364,10 @@ function pageShellAtPoint(x, y) {
function setCurrentPage(index) { function setCurrentPage(index) {
state.pageIndex = index; state.pageIndex = index;
if (state.fitMode === "width") {
state.scale = fitWidthScale(currentPageInfo());
el.zoomLabel.textContent = `${Math.round(state.scale * 100)}%`;
}
updatePageListCurrent(); updatePageListCurrent();
updateControls(); updateControls();
if (state.doc) { if (state.doc) {
@@ -1376,13 +1412,14 @@ function layoutPages() {
function layoutPageShell(shell, page) { function layoutPageShell(shell, page) {
const width = Math.max(1, page.width * MM_TO_PX); const width = Math.max(1, page.width * MM_TO_PX);
const height = Math.max(1, page.height * MM_TO_PX); const height = Math.max(1, page.height * MM_TO_PX);
shell.style.width = `${width * state.scale}px`; const scale = state.fitMode === "width" ? state.scale * currentPageInfo().width / page.width : state.scale;
shell.style.height = `${height * state.scale}px`; shell.style.width = `${width * scale}px`;
shell.style.height = `${height * scale}px`;
const surface = shell.querySelector(".page-surface"); const surface = shell.querySelector(".page-surface");
if (surface) { if (surface) {
surface.style.width = `${width}px`; surface.style.width = `${width}px`;
surface.style.height = `${height}px`; surface.style.height = `${height}px`;
surface.style.transform = `scale(${state.scale})`; surface.style.transform = `scale(${scale})`;
} }
} }
@@ -1645,13 +1682,16 @@ function renderSignatures() {
const badges = document.createElement("div"); const badges = document.createElement("div");
badges.className = "signature-badges"; 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); head.append(name, badges);
row.append(head); row.append(head);
appendSignatureLine(row, "编号", signature.id); appendSignatureLine(row, "编号", signature.id);
appendSignatureLine(row, "版本", signature.version); appendSignatureLine(row, "版本", signature.version);
appendSignatureLine(row, "章图", signature.sealType); appendSignatureLine(row, "章图", signature.sealType);
appendSignatureLine(row, "章号", signature.sealId);
appendSignatureLine(row, "章名", signature.sealName);
appendSignatureLine(row, "厂商", signature.sealVendor);
appendSignatureLine(row, "签者", signature.signer); appendSignatureLine(row, "签者", signature.signer);
appendSignatureLine(row, "时间", formatSignatureTime(signature.signatureDateTime)); appendSignatureLine(row, "时间", formatSignatureTime(signature.signatureDateTime));
appendSignatureLine(row, "机构", signatureAgency(signature)); appendSignatureLine(row, "机构", signatureAgency(signature));
@@ -1662,12 +1702,17 @@ function renderSignatures() {
appendSignatureCheck(row, "一致", signature.sealMatchOK); appendSignatureCheck(row, "一致", signature.sealMatchOK);
} }
appendSignatureCheck(row, "证书", signature.certOK); 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.certTimeChecked, signature.certTimeOK);
appendSignaturePolicy(row, "信任", signature.certTrustChecked, signature.certTrustOK); appendSignaturePolicy(row, "信任", signature.certTrustChecked, signature.certTrustOK);
appendSignatureLine(row, "保护", signatureReferenceText(signature), signatureReferenceStatus(signature)); appendSignatureLine(row, "保护", signatureReferenceText(signature), signatureReferenceStatus(signature));
appendSignatureLine(row, "序号", signature.signSerial);
appendSignatureLine(row, "算法", signature.signatureMethod); appendSignatureLine(row, "算法", signature.signatureMethod);
appendSignatureLine(row, "散列", signature.digestMethod); appendSignatureLine(row, "散列", signature.digestMethod);
appendSignatureLine(row, "序号", signature.signSerial);
appendSignatureLine(row, "主体", signature.signSubject && signature.signSubject !== signature.signer ? signature.signSubject : ""); appendSignatureLine(row, "主体", signature.signSubject && signature.signSubject !== signature.signer ? signature.signSubject : "");
appendSignatureLine(row, "颁发", signature.signIssuer); appendSignatureLine(row, "颁发", signature.signIssuer);
appendSignatureLine(row, "章证", signature.sealSubject); appendSignatureLine(row, "章证", signature.sealSubject);
@@ -1940,15 +1985,23 @@ function fitWidth(updateStatus = true) {
if (!page) { if (!page) {
return; return;
} }
const space = pageSpace(); setScale(fitWidthScale(page), updateStatus, "width");
const available = Math.max(1, el.viewerPanel.clientWidth - space * 2);
const width = Math.max(1, page.width * MM_TO_PX);
setScale(available / width, updateStatus, "width");
if (updateStatus) { if (updateStatus) {
scrollToPage(state.pageIndex); scrollToPage(state.pageIndex);
} }
} }
function fitWidthScale(page) {
const space = pageSpace();
const width = Math.max(1, page.width * MM_TO_PX);
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) { function fitHeight(updateStatus = true) {
const page = currentPageInfo(); const page = currentPageInfo();
if (!page) { if (!page) {
@@ -1976,15 +2029,20 @@ function applyFit(updateStatus = true) {
} }
function setScale(nextScale, updateStatus = true, fitMode = "free") { 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 scale = Math.min(4, Math.max(0.2, nextScale));
const scaleChanged = Math.abs(scale - state.scale) > 0.001; const scaleChanged = Math.abs(scale - state.scale) > 0.001;
const layoutChanged = scaleChanged || fitMode !== state.fitMode;
state.fitMode = fitMode; state.fitMode = fitMode;
state.scale = scale; state.scale = scale;
if (scaleChanged) { if (layoutChanged) {
clearStampHighlights(); clearStampHighlights();
layoutPages(); layoutPages();
} }
updateFitSpace(); updateFitSpace();
if (layoutChanged) {
restoreScaleAnchor(anchor);
}
el.zoomLabel.textContent = `${Math.round(state.scale * 100)}%`; el.zoomLabel.textContent = `${Math.round(state.scale * 100)}%`;
if (updateStatus && state.doc) { if (updateStatus && state.doc) {
setStatus(`${state.pageIndex + 1} / ${state.doc.pageCount}`); setStatus(`${state.pageIndex + 1} / ${state.doc.pageCount}`);
@@ -1992,6 +2050,34 @@ function setScale(nextScale, updateStatus = true, fitMode = "free") {
updateControls(); 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() { function currentPageInfo() {
return state.doc?.pages?.[state.pageIndex] || null; return state.doc?.pages?.[state.pageIndex] || null;
} }
@@ -2004,9 +2090,27 @@ function pageBlockSpace() {
return Number.parseFloat(getComputedStyle(el.pageFrame).paddingTop) || pageSpace(); 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() { function updateFitSpace() {
const page = currentPageInfo(); const page = currentPageInfo();
if (!page || state.fitMode === "free") { if (!page || state.fitMode === "free") {
el.viewerPanel.classList.remove("single-page-fits-height");
el.pageFrame.style.removeProperty("--fit-space"); el.pageFrame.style.removeProperty("--fit-space");
el.pageFrame.style.removeProperty("--fit-gap"); el.pageFrame.style.removeProperty("--fit-gap");
return; return;
@@ -2014,6 +2118,7 @@ function updateFitSpace() {
const shell = pageShell(state.pageIndex); const shell = pageShell(state.pageIndex);
const height = shell ? shell.getBoundingClientRect().height : Math.max(1, page.height * MM_TO_PX * state.scale); const height = shell ? shell.getBoundingClientRect().height : Math.max(1, page.height * MM_TO_PX * state.scale);
const base = pageSpace(); 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 space = Math.max(base, (el.viewerPanel.clientHeight - height) / 2);
const gap = space > base ? space + 1 : space; const gap = space > base ? space + 1 : space;
el.pageFrame.style.setProperty("--fit-space", `${space}px`); el.pageFrame.style.setProperty("--fit-space", `${space}px`);
@@ -2038,6 +2143,7 @@ function updateControls() {
el.pageExportFormat.disabled = !hasDoc || !state.exportFormats.length; el.pageExportFormat.disabled = !hasDoc || !state.exportFormats.length;
el.exportPageButton.disabled = !hasDoc || !state.exportFormats.length; el.exportPageButton.disabled = !hasDoc || !state.exportFormats.length;
el.exportButton.disabled = !hasDoc; el.exportButton.disabled = !hasDoc;
updateDPIControl();
} }
function updateAnnotationButton() { function updateAnnotationButton() {
@@ -2050,6 +2156,7 @@ function setExportControlsDisabled(disabled) {
el.exportButton.disabled = disabled; el.exportButton.disabled = disabled;
el.exportPageButton.disabled = disabled; el.exportPageButton.disabled = disabled;
el.pageExportFormat.disabled = disabled; el.pageExportFormat.disabled = disabled;
updateDPIControl(disabled);
} }
function callWASM(name, ...args) { function callWASM(name, ...args) {
@@ -2072,7 +2179,7 @@ function callWASM(name, ...args) {
} }
throw err; throw err;
} }
const result = JSON.parse(payload); const result = typeof payload === "string" ? JSON.parse(payload) : payload;
if (!result.ok) { if (!result.ok) {
throw new Error(result.error || "WASM 调用失败"); throw new Error(result.error || "WASM 调用失败");
} }
@@ -2128,15 +2235,6 @@ function formatSize(value) {
return value.toFixed(1); 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) { function downloadBytes(bytes, mime, name) {
const blob = new Blob([bytes], { type: mime }); const blob = new Blob([bytes], { type: mime });
const link = document.createElement("a"); const link = document.createElement("a");
@@ -2167,6 +2265,18 @@ function exportFormatInfo(value) {
return state.exportFormats.find((format) => format.value === value) || null; 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") { function formatBytes(size, fallback = "PDF") {
if (!Number.isFinite(size) || size <= 0) { if (!Number.isFinite(size) || size <= 0) {
return fallback; return fallback;
+15 -14
View File
@@ -1,24 +1,25 @@
module github.com/xiaoqidun/ofdgo module github.com/xiaoqidun/ofdgo
go 1.25.0 go 1.26.0
require ( require (
github.com/tdewolff/canvas v0.0.0-20260707223012-a1cbc6a74235 github.com/tdewolff/canvas v0.0.0-20260822211237-56e622de465a
github.com/xiaoqidun/jbig2 v0.0.0-20260707061149-c2ff34658234 github.com/tdewolff/font v0.0.0-20260822205238-d0d2f004b117
golang.org/x/image v0.43.0 github.com/xiaoqidun/jbig2 v0.0.0-20260823111534-2e67bcec7059
golang.org/x/text v0.39.0 golang.org/x/image v0.45.0
golang.org/x/text v0.41.0
) )
require ( require (
codeberg.org/go-latex/latex v0.2.0 // indirect codeberg.org/go-latex/latex v0.3.0 // indirect
codeberg.org/go-pdf/fpdf v0.11.1 // indirect codeberg.org/go-pdf/fpdf v0.12.0 // indirect
github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 // 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/graphics-go v0.0.0-20160129215708-b43f31a4a966 // indirect
github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc // indirect github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc // indirect
github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 // indirect github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 // indirect
github.com/ByteArena/poly2tri-go v0.0.0-20170716161910-d102ad91854f // indirect github.com/ByteArena/poly2tri-go v0.0.0-20170716161910-d102ad91854f // indirect
github.com/Kagami/go-avif v0.1.0 // 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/textlayout v0.3.2 // indirect
github.com/benoitkugler/textprocessing v0.0.6 // indirect github.com/benoitkugler/textprocessing v0.0.6 // indirect
github.com/go-fonts/latin-modern v0.3.3 // 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/kolesa-team/go-webp v1.0.5 // indirect
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
github.com/srwiley/scanx v0.0.0-20190309010443-e94503791388 // 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.17 // indirect
github.com/tdewolff/minify/v2 v2.24.13 // indirect github.com/tdewolff/parse/v2 v2.8.16 // indirect
github.com/tdewolff/parse/v2 v2.8.12 // indirect
github.com/wcharczuk/go-chart/v2 v2.1.2 // indirect github.com/wcharczuk/go-chart/v2 v2.1.2 // indirect
github.com/yuin/goldmark v1.8.2 // indirect github.com/yuin/goldmark v1.8.5 // indirect
golang.org/x/net v0.53.0 // 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 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 modernc.org/token v1.1.0 // indirect
star-tex.org/x/tex v0.7.1 // 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 h1:2yn58Vkh4CFK3ipacWUAIE3XVBGNa0y1bc95Bmfx91I=
codeberg.org/go-fonts/dejavu v0.4.0/go.mod h1:abni088lmhQJvso2Lsb7azCKzwkfcnttl6tL1UTWKzg= 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.6.0 h1:15Gh6SdwYve22CWCm9jYpVpRuaTh726av2TgHTHvAtQ=
codeberg.org/go-fonts/liberation v0.5.0/go.mod h1:zS/2e1354/mJ4pGzIIaEtm/59VFCFnYC7YV6YdGl5GU= codeberg.org/go-fonts/liberation v0.6.0/go.mod h1:J15VAa+lyxdcI/Je7lDDDl6QOhLk9feNBnnwXqEHXOk=
codeberg.org/go-latex/latex v0.2.0 h1:Ol/a6VHY06N+5gPfewswymoRb5ZcKDXWVaVegcx4hbI= codeberg.org/go-latex/latex v0.3.0 h1:LKTaDHFbEC2PH1sh0sYv6PZ1pzs/g2aoeV1HItWj/bg=
codeberg.org/go-latex/latex v0.2.0/go.mod h1:VJAwQir7/T8LZxj7xAPivISKiVOwkMpQ8bTuPQ31X0Y= codeberg.org/go-latex/latex v0.3.0/go.mod h1:8ETijTpK2bFtwRAXLXe1RZJrYxnc5pibibZfQBj+Lk4=
codeberg.org/go-pdf/fpdf v0.11.1 h1:U8+coOTDVLxHIXZgGvkfQEi/q0hYHYvEHFuGNX2GzGs= codeberg.org/go-pdf/fpdf v0.12.0 h1:g8E/1VqGqB2lZUUaqQrrTnA0IEJLPTTX1DZ0qS/ZmhU=
codeberg.org/go-pdf/fpdf v0.11.1/go.mod h1:Y0DGRAdZ0OmnZPvjbMp/1bYxmIPxm0ws4tfoPOc4LjU= 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 h1:E0zPRk2muWuCqSKSVZIWsgtU9pjsw3eKHi8VmQeScxo=
git.sr.ht/~sbinet/cmpimg v0.1.0/go.mod h1:FU12psLbF4TfNXkKH2ZZQ29crIqoiqTZmeQ7dkp/pxE= 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.8.0 h1:PjQ4AgUWRz7Dy6PVBKMLLg96eRCos1U+H6F3FJuKWHo=
git.sr.ht/~sbinet/gg v0.7.0/go.mod h1:VYeli15tpMM4EvqlivlVbbyvWZlOU+EZn4XZmfBGUdM= 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 h1:1qlsVAQJXZHsaM8b6OLVo6muQUQd4CwkH/D3fnnbHXA=
github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298/go.mod h1:D+QujdIlUNfa0igpNMk6UIvlb6C252URs4yupRUV4lQ= github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298/go.mod h1:D+QujdIlUNfa0igpNMk6UIvlb6C252URs4yupRUV4lQ=
github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 h1:lTG4HQym5oPKjL7nGs+csTgiDna685ZXjxijkne828g= github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 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/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 h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw=
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= 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.2 h1:HzTuoo2ErYQqf5qvcJInB8uvqSVxRttzkFexPWtnceM=
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= 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/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 h1:Y/LTkwpQ9G0Fia9yhPmZA9IR5AnE8Cq30j3C+Gx5/IE=
github.com/benoitkugler/textlayout v0.3.2/go.mod h1:o+1hFV+JSHBC9qNLIuwVoLedERU7sBPgEFcuSgfvi/w= 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 h1:obkMyj62GEPg3xUVYqROlCN22z1OleuZm6ULqX9Om1g=
github.com/benoitkugler/textprocessing v0.0.6/go.mod h1:Io0gN08/PXEzrSOWFa88xHx2Xv3VjvLMY7H76YoI23A= 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.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 h1:g2xNgI8yzdNzIVm+qvbMryB6yGPe0pSMss8QT3QwlJ0=
github.com/go-fonts/latin-modern v0.3.3/go.mod h1:tHaiWDGze4EPB0Go4cLT5M3QzRY3peya09Z/8KSCrpY= 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= 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/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/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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/tdewolff/canvas v0.0.0-20260707223012-a1cbc6a74235 h1:h2FIMfIs+VEJltMADi0Vh8UwvoJ+34gLhfn77peMX0U= github.com/tdewolff/canvas v0.0.0-20260822211237-56e622de465a h1:riaFBiYaIv1gM31xdcU0Yo7bdp3WebF4dggO/JK0g/8=
github.com/tdewolff/canvas v0.0.0-20260707223012-a1cbc6a74235/go.mod h1:Lz12ClttuKvo2jb1UxBfJJWmKV0LmEc8IRo/30R7z+k= github.com/tdewolff/canvas v0.0.0-20260822211237-56e622de465a/go.mod h1:K9Ux3cwFtyAFRITZ46g7v+ORXgI3ENtQa5rL9yjNz5Y=
github.com/tdewolff/font v0.0.0-20260424075104-b5eeb1e23189 h1:wP/+8oeXiF55DUy9U0BP4iDD2mNu+o7evt5QLbLHv9E= github.com/tdewolff/font v0.0.0-20260822205238-d0d2f004b117 h1:5vSJjsbV9AH0U+Ex0uej8ROrpOBlOVBrw0mGlVef7oo=
github.com/tdewolff/font v0.0.0-20260424075104-b5eeb1e23189/go.mod h1:KjNTiF0TXOXJyWGS/0DPXRknEC20c12kc0JcCYNqtz4= github.com/tdewolff/font v0.0.0-20260822205238-d0d2f004b117/go.mod h1:ipESUihcEdgRBhIwTRHmBTOMamccMQrp4iCxfa1aFws=
github.com/tdewolff/minify/v2 v2.24.13 h1:xrcF7gKDnUszseEY9WX9mUlZII2v2Go/QAcAwRASw58= github.com/tdewolff/minify/v2 v2.24.17 h1:6AbitfVyq0M7aW6i+XL7+49DeTQZwloOMs9O574arBg=
github.com/tdewolff/minify/v2 v2.24.13/go.mod h1:emvwoYeIl8bfAKqRU5ww95LX9Gpggpqv/naal9a8Yq0= github.com/tdewolff/minify/v2 v2.24.17/go.mod h1:kVqn9vxXUKtlHexSNrWbYePqioOT5mc4ou/KVSMpfCM=
github.com/tdewolff/parse/v2 v2.8.12 h1:5BBjfaCv482v3nltlS0u6wH1xJaxjR6ofDrWttNvROg= github.com/tdewolff/parse/v2 v2.8.16 h1:bLk5svUOQRkW/Y2SJ+DeENSIkZBcTIkq+Atyv5D8feI=
github.com/tdewolff/parse/v2 v2.8.12/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo= github.com/tdewolff/parse/v2 v2.8.16/go.mod h1:XdsoSFThlVIRIajAuqz1evNY7bagZS8LBOPA3aVopwQ=
github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk= github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk=
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= 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 h1:Y17/oYNuXwZg6TFag06qe8sBajwwsuvPiJJXcUcLL6E=
github.com/wcharczuk/go-chart/v2 v2.1.2/go.mod h1:Zi4hbaqlWpYajnXB2K22IUYVXRXaLfSGNNR7P4ukyyQ= 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-20260823111534-2e67bcec7059 h1:/JXxWbNTePSVfshS7BVRm75Kh1/5rixvLpgTRw7MMv8=
github.com/xiaoqidun/jbig2 v0.0.0-20260707061149-c2ff34658234/go.mod h1:c5zFNbM18iSRcfS9ziMZquU3lFJGmN1w6CUfuEDw0WI= 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 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= 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.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE= github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= 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-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.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.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/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.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.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/image v0.43.0 h1:FLxcP4ec2350nTfOC8ysKtqYSIFbk/QGjw1ZHNP4tsY= golang.org/x/image v0.45.0 h1:FMb1nTbH5H9vF55SriQHgFw5GnNL9Jg6L25BwXKzhB0=
golang.org/x/image v0.43.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q= 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.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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.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.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 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.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= 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-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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.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.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.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.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/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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.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.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= 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-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.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.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= 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/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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= modernc.org/knuth v0.6.0 h1:zLv0hzgdcpcCUB/iMwpQvn9m/iciqO/hyNXlpWymMKU=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= modernc.org/knuth v0.6.0/go.mod h1:ISAZV+URZIlE8h1BwNVg1NzpwHKndV5lQ13NPPj3tds=
modernc.org/knuth v0.5.5 h1:6lap2U/ISm8aC/4NU58ALFCRllNPaK0EZcIGY/oDgUg=
modernc.org/knuth v0.5.5/go.mod h1:e5SBb35HQBj2aFwbBO3ClPcViLY3Wi0LzaOd7c/3qMk=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4=
+42 -10
View File
@@ -17,7 +17,6 @@
package webui package webui
import ( import (
"encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"syscall/js" "syscall/js"
@@ -57,6 +56,9 @@ func registerCallback(name string, fn func([]js.Value) (any, error)) {
if err != nil { if err != nil {
return encodeResult(apiResult{OK: false, Error: err.Error()}) return encodeResult(apiResult{OK: false, Error: err.Error()})
} }
if result, ok := data.(js.Value); ok {
return result
}
return encodeResult(apiResult{OK: true, Data: data}) return encodeResult(apiResult{OK: true, Data: data})
}) })
js.Global().Set(name, cb) js.Global().Set(name, cb)
@@ -113,7 +115,18 @@ func renderPage(args []js.Value) (any, error) {
if len(args) == 0 { if len(args) == 0 {
return nil, fmt.Errorf("missing page index") 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 获取导出格式 // exportFormats 获取导出格式
@@ -130,21 +143,21 @@ func exportPage(args []js.Value) (any, error) {
if currentSession == nil { if currentSession == nil {
return nil, fmt.Errorf("ofd document is not opened") 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") 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 { if err != nil {
return nil, err return nil, err
} }
return map[string]any{ return successResult(map[string]any{
"base64": base64.StdEncoding.EncodeToString(data), "bytes": bytesToJS(data),
"size": len(data), "size": len(data),
"format": format.Value, "format": format.Value,
"label": format.Label, "label": format.Label,
"extension": format.Extension, "extension": format.Extension,
"mime": format.MIME, "mime": format.MIME,
}, nil }), nil
} }
// exportPDF 导出OFD文档为PDF // exportPDF 导出OFD文档为PDF
@@ -158,10 +171,29 @@ func exportPDF(args []js.Value) (any, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return map[string]any{ return successResult(map[string]any{
"base64": base64.StdEncoding.EncodeToString(data), "bytes": bytesToJS(data),
"size": len(data), "size": len(data),
}, nil }), nil
}
// 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 获取系统字体名称 // fontSystemNames 获取系统字体名称
+45 -10
View File
@@ -46,6 +46,9 @@ type SignatureInfo struct {
Valid bool `json:"valid"` Valid bool `json:"valid"`
Version string `json:"version,omitempty"` Version string `json:"version,omitempty"`
SealType string `json:"sealType,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"` Signer string `json:"signer,omitempty"`
SignatureDateTime string `json:"signatureDateTime,omitempty"` SignatureDateTime string `json:"signatureDateTime,omitempty"`
Provider string `json:"provider,omitempty"` Provider string `json:"provider,omitempty"`
@@ -55,15 +58,21 @@ type SignatureInfo struct {
SealOK bool `json:"sealOK"` SealOK bool `json:"sealOK"`
SealMatchOK bool `json:"sealMatchOK"` SealMatchOK bool `json:"sealMatchOK"`
CertOK bool `json:"certOK"` 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"` CertTimeChecked bool `json:"certTimeChecked,omitempty"`
CertTimeOK bool `json:"certTimeOK,omitempty"` CertTimeOK bool `json:"certTimeOK,omitempty"`
CertTrustChecked bool `json:"certTrustChecked,omitempty"` CertTrustChecked bool `json:"certTrustChecked,omitempty"`
CertTrustOK bool `json:"certTrustOK,omitempty"` CertTrustOK bool `json:"certTrustOK,omitempty"`
ReferenceCount int `json:"referenceCount"` ReferenceCount int `json:"referenceCount"`
ReferencePassed int `json:"referencePassed"` ReferencePassed int `json:"referencePassed"`
SignSerial string `json:"signSerial,omitempty"`
SignatureMethod string `json:"signatureMethod,omitempty"` SignatureMethod string `json:"signatureMethod,omitempty"`
DigestMethod string `json:"digestMethod,omitempty"` DigestMethod string `json:"digestMethod,omitempty"`
SignSerial string `json:"signSerial,omitempty"`
SignSubject string `json:"signSubject,omitempty"` SignSubject string `json:"signSubject,omitempty"`
SignIssuer string `json:"signIssuer,omitempty"` SignIssuer string `json:"signIssuer,omitempty"`
SealSubject string `json:"sealSubject,omitempty"` SealSubject string `json:"sealSubject,omitempty"`
@@ -222,10 +231,6 @@ func (s *Session) Info() DocumentInfo {
PageCount: len(s.doc.Pages.Page), PageCount: len(s.doc.Pages.Page),
Pages: make([]PageInfo, 0, 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 { if signatures, err := s.signatureInfos(); err == nil {
info.Signatures = signatures info.Signatures = signatures
} else { } else {
@@ -239,9 +244,11 @@ func (s *Session) Info() DocumentInfo {
info.CreationDate = docInfo.CreationDate info.CreationDate = docInfo.CreationDate
info.ModDate = docInfo.ModDate info.ModDate = docInfo.ModDate
} }
pages := make([]*ofdgo.PageContent, 0, len(s.doc.Pages.Page))
for index, pageRef := range s.doc.Pages.Page { for index, pageRef := range s.doc.Pages.Page {
pageInfo := PageInfo{Index: index, ID: pageRef.ID} pageInfo := PageInfo{Index: index, ID: pageRef.ID}
if _, page, err := s.pageContent(index); err == nil { if _, page, err := s.pageContent(index); err == nil {
pages = append(pages, page)
if box, err := s.pageBox(index, page); err == nil { if box, err := s.pageBox(index, page); err == nil {
pageInfo.Width = box.W pageInfo.Width = box.W
pageInfo.Height = box.H pageInfo.Height = box.H
@@ -249,6 +256,10 @@ func (s *Session) Info() DocumentInfo {
} }
info.Pages = append(info.Pages, pageInfo) 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 return info
} }
@@ -272,9 +283,9 @@ func (s *Session) RenderPageSVG(index int) (PageSVG, error) {
} }
// ExportPage 导出单页 // ExportPage 导出单页
// 入参: index 页面索引, value 导出格式 // 入参: index 页面索引, value 导出格式, dpi 图片DPI
// 返回: []byte 文件数据, ExportFormat 导出格式, error 错误信息 // 返回: []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) format, ok := exportFormat(value)
if !ok { if !ok {
return nil, ExportFormat{}, fmt.Errorf("unsupported export format %s", value) return nil, ExportFormat{}, fmt.Errorf("unsupported export format %s", value)
@@ -292,13 +303,13 @@ func (s *Session) ExportPage(index int, value string) ([]byte, ExportFormat, err
case "eps": case "eps":
err = s.Renderer.RenderToEPS(page, &buf) err = s.Renderer.RenderToEPS(page, &buf)
case "png": case "png":
img, renderErr := s.Renderer.RenderToImage(page) img, renderErr := s.renderPageImage(page, dpi)
if renderErr != nil { if renderErr != nil {
return nil, ExportFormat{}, renderErr return nil, ExportFormat{}, renderErr
} }
err = png.Encode(&buf, img) err = png.Encode(&buf, img)
case "jpg": case "jpg":
img, renderErr := s.Renderer.RenderToImage(page) img, renderErr := s.renderPageImage(page, dpi)
if renderErr != nil { if renderErr != nil {
return nil, ExportFormat{}, renderErr return nil, ExportFormat{}, renderErr
} }
@@ -310,6 +321,21 @@ func (s *Session) ExportPage(index int, value string) ([]byte, ExportFormat, err
return buf.Bytes(), format, nil 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 获取签名验证信息 // signatureInfos 获取签名验证信息
// 返回: []SignatureInfo 签名验证信息, error 错误信息 // 返回: []SignatureInfo 签名验证信息, error 错误信息
func (s *Session) signatureInfos() ([]SignatureInfo, error) { func (s *Session) signatureInfos() ([]SignatureInfo, error) {
@@ -334,6 +360,9 @@ func signatureInfo(report ofdgo.SignatureVerifyReport) SignatureInfo {
Valid: report.Valid, Valid: report.Valid,
Version: report.Provider.Version, Version: report.Provider.Version,
SealType: report.SealType, SealType: report.SealType,
SealID: report.SealInfo.ID,
SealName: report.SealInfo.Name,
SealVendor: report.SealInfo.VendorID,
Signer: signatureSigner(report), Signer: signatureSigner(report),
SignatureDateTime: report.SignatureDateTime, SignatureDateTime: report.SignatureDateTime,
Provider: report.Provider.ProviderName, Provider: report.Provider.ProviderName,
@@ -343,15 +372,21 @@ func signatureInfo(report ofdgo.SignatureVerifyReport) SignatureInfo {
SealOK: report.SealOK, SealOK: report.SealOK,
SealMatchOK: report.SealMatchOK, SealMatchOK: report.SealMatchOK,
CertOK: report.CertOK, CertOK: report.CertOK,
SignatureTimeChecked: report.SignatureTimeChecked,
SignatureTimeOK: report.SignatureTimeOK,
SealTimeChecked: report.SealTimeChecked,
SealTimeOK: report.SealTimeOK,
SealCertTimeChecked: report.SealCertTimeChecked,
SealCertTimeOK: report.SealCertTimeOK,
CertTimeChecked: report.CertTimeChecked, CertTimeChecked: report.CertTimeChecked,
CertTimeOK: report.CertTimeOK, CertTimeOK: report.CertTimeOK,
CertTrustChecked: report.CertTrustChecked, CertTrustChecked: report.CertTrustChecked,
CertTrustOK: report.CertTrustOK, CertTrustOK: report.CertTrustOK,
ReferenceCount: len(report.References), ReferenceCount: len(report.References),
ReferencePassed: signatureReferencePassed(report.References), ReferencePassed: signatureReferencePassed(report.References),
SignSerial: report.SignCert.SerialNumber,
SignatureMethod: report.SignatureMethod, SignatureMethod: report.SignatureMethod,
DigestMethod: report.DigestMethod, DigestMethod: report.DigestMethod,
SignSerial: report.SignCert.SerialNumber,
SignSubject: report.SignCert.Subject, SignSubject: report.SignCert.Subject,
SignIssuer: report.SignCert.Issuer, SignIssuer: report.SignCert.Issuer,
SealSubject: report.SealCert.Subject, SealSubject: report.SealCert.Subject,
+8
View File
@@ -71,6 +71,14 @@ func NewRenderer(reader *Reader, opts ...RendererOption) *Renderer {
DrawParams: reader.drawParamCache, DrawParams: reader.drawParamCache,
CompositeGraphicUnits: reader.compositeGraphicUnitCache, CompositeGraphicUnits: reader.compositeGraphicUnitCache,
FontMap: make(map[string]*canvas.FontFamily), 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 { for _, opt := range opts {
opt(r) 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"` Pages Pages `xml:"Pages"`
Outlines Outlines `xml:"Outlines"` Outlines Outlines `xml:"Outlines"`
Permissions Permissions `xml:"Permissions"` Permissions Permissions `xml:"Permissions"`
Actions []Action `xml:"Actions>Action"`
Bookmarks Bookmarks `xml:"Bookmarks"`
Annotations string `xml:"Annotations"` Annotations string `xml:"Annotations"`
Signatures string `xml:"Signatures"` Signatures string `xml:"Signatures"`
Attachments Attachments `xml:"Attachments"` Attachments Attachments `xml:"Attachments"`
CustomTags CustomTags `xml:"CustomTags"`
Extensions Extensions `xml:"Extensions"` Extensions Extensions `xml:"Extensions"`
} }
// Extensions 扩展集合 // Extensions 扩展集合
type Extensions struct { type Extensions struct {
XMLName xml.Name `xml:"Extensions"`
Path string `xml:",chardata"`
Extension []Extension `xml:"Extension"` Extension []Extension `xml:"Extension"`
} }
@@ -42,7 +47,8 @@ type Extension struct {
Date string `xml:"Date,attr"` Date string `xml:"Date,attr"`
RefID string `xml:"RefId,attr"` RefID string `xml:"RefId,attr"`
Property []Property `xml:"Property"` Property []Property `xml:"Property"`
Data string `xml:"Data"` ExtendData []string `xml:"ExtendData"`
Data []ExtensionData `xml:"Data"`
} }
// Property 扩展属性 // Property 扩展属性
@@ -52,16 +58,45 @@ type Property struct {
Type string `xml:"Type,attr"` Type string `xml:"Type,attr"`
} }
// ExtensionData 扩展数据
type ExtensionData struct {
Attr []xml.Attr `xml:",any,attr"`
Content string `xml:",innerxml"`
}
// Attachments 附件集合 // Attachments 附件集合
type Attachments struct { type Attachments struct {
XMLName xml.Name `xml:"Attachments"`
Path string `xml:",chardata"`
Attachment []Attachment `xml:"Attachment"` Attachment []Attachment `xml:"Attachment"`
} }
// Attachment 附件信息 // Attachment 附件信息
type Attachment struct { type Attachment struct {
Name string `xml:"Name,attr"`
File string `xml:"File,attr"`
ID string `xml:"ID,attr"` ID string `xml:"ID,attr"`
Name string `xml:"Name,attr"`
Format string `xml:"Format,attr"`
CreationDate string `xml:"CreationDate,attr"`
ModDate string `xml:"ModDate,attr"`
Size *float64 `xml:"Size,attr"`
Visible bool `xml:"Visible,attr"`
Usage string `xml:"Usage,attr"`
FileLoc string `xml:"FileLoc"`
}
// CustomTags 自定义标引集合
type CustomTags struct {
XMLName xml.Name `xml:"CustomTags"`
Path string `xml:",chardata"`
CustomTag []CustomTag `xml:"CustomTag"`
}
// CustomTag 自定义标引
type CustomTag struct {
TypeID string `xml:"TypeID,attr"`
NameSpace string `xml:"NameSpace,attr"`
SchemaLoc string `xml:"SchemaLoc"`
FileLoc string `xml:"FileLoc"`
} }
// CommonData 文档公共数据 // CommonData 文档公共数据
@@ -101,6 +136,17 @@ type TemplatePage struct {
ZOrder string `xml:"ZOrder,attr"` 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 大纲集合 // Outlines 大纲集合
type Outlines struct { type Outlines struct {
OutlineElem []OutlineElem `xml:"OutlineElem"` OutlineElem []OutlineElem `xml:"OutlineElem"`
@@ -110,13 +156,146 @@ type Outlines struct {
type OutlineElem struct { type OutlineElem struct {
Title string `xml:"Title,attr"` Title string `xml:"Title,attr"`
Count int `xml:"Count,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 权限声明 // Permissions 权限声明
type Permissions struct { type Permissions struct {
Edit bool `xml:"Edit"` Edit bool `xml:"Edit"`
Print bool `xml:"Print"` Annot bool `xml:"Annot"`
Export bool `xml:"Export"` 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,
}
} }
+175 -88
View File
@@ -34,6 +34,7 @@ type FontFile struct {
type FontFS struct { type FontFS struct {
files map[string][]byte files map[string][]byte
names []string names []string
candidates []fontFileCandidate
} }
// NewFontFS 创建内存字体文件系统 // NewFontFS 创建内存字体文件系统
@@ -54,6 +55,7 @@ func NewFontFS(fonts []FontFile) *FontFS {
fsys.files[name] = append([]byte(nil), font.Data...) fsys.files[name] = append([]byte(nil), font.Data...)
} }
sort.Strings(fsys.names) sort.Strings(fsys.names)
fsys.candidates = fontFileCandidates(fsys.names, path.Base)
return fsys return fsys
} }
@@ -70,18 +72,20 @@ func (fsys *FontFS) Len() int {
// 入参: name 字体文件名 // 入参: name 字体文件名
// 返回: fs.File 字体文件, error 错误信息 // 返回: fs.File 字体文件, error 错误信息
func (fsys *FontFS) Open(name string) (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 == "." { if name == "." {
return &fontDir{entries: fsys.entries()}, nil return &fontDir{entries: fsys.entries()}, nil
} }
data, ok := fsys.files[name] data, ok := fsys.files[strings.ToLower(name)]
if !ok { if !ok {
return nil, fs.ErrNotExist return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
} }
return &fontMemFile{ return &fontMemFile{
Reader: bytes.NewReader(data), Reader: bytes.NewReader(data),
info: fontFileInfo{ info: fontFileInfo{
name: name, name: strings.ToLower(name),
size: int64(len(data)), size: int64(len(data)),
}, },
}, nil }, nil
@@ -91,9 +95,15 @@ func (fsys *FontFS) Open(name string) (fs.File, error) {
// 入参: name 目录名 // 入参: name 目录名
// 返回: []fs.DirEntry 目录条目, error 错误信息 // 返回: []fs.DirEntry 目录条目, error 错误信息
func (fsys *FontFS) ReadDir(name string) ([]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 != "." { 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 return fsys.entries(), nil
} }
@@ -102,10 +112,7 @@ func (fsys *FontFS) ReadDir(name string) ([]fs.DirEntry, error) {
// 入参: pattern 匹配模式 // 入参: pattern 匹配模式
// 返回: []string 字体文件列表, error 错误信息 // 返回: []string 字体文件列表, error 错误信息
func (fsys *FontFS) Glob(pattern string) ([]string, error) { func (fsys *FontFS) Glob(pattern string) ([]string, error) {
if _, err := path.Match(pattern, ""); err != nil { return fs.Glob(struct{ fs.FS }{fsys}, pattern)
return nil, err
}
return fsys.match(pattern), nil
} }
// Match 匹配指定字体名称 // Match 匹配指定字体名称
@@ -146,15 +153,15 @@ func (fsys *FontFS) matchStyle(pattern string, bold, italic bool) []string {
// 入参: patterns 匹配模式列表, bold 是否粗体, italic 是否斜体 // 入参: patterns 匹配模式列表, bold 是否粗体, italic 是否斜体
// 返回: []string 字体文件列表 // 返回: []string 字体文件列表
func (fsys *FontFS) matchPatternsStyle(patterns []string, bold, italic bool) []string { func (fsys *FontFS) matchPatternsStyle(patterns []string, bold, italic bool) []string {
var matches []fontFileMatch matches := make([]fontFileMatch, 0, len(fsys.candidates))
seen := make(map[string]int) seen := make(map[string]int, len(fsys.candidates))
for _, pattern := range patterns { for _, matcher := range newFontPatternMatchers(patterns) {
for _, name := range fsys.names { for _, file := range fsys.candidates {
rank := matchFontPatternRank(pattern, name) rank := matcher.rankCandidate(file)
appendFontFileMatch(&matches, seen, pattern, name, rank, bold, italic) appendFontFileMatch(&matches, seen, matcher, file, rank, bold, italic)
} }
} }
sortFontFileMatches(matches, bold, italic) sortFontFileMatches(matches)
return fontFileMatchNames(matches) return fontFileMatchNames(matches)
} }
@@ -201,100 +208,200 @@ func cleanFontName(name string) string {
return strings.ToLower(name) return strings.ToLower(name)
} }
// matchFontPatternRank 获取字体文件匹配等级 type fontFileCandidate struct {
// 入参: pattern 匹配模式, name 字体文件名 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 匹配等级 // 返回: int 匹配等级
func matchFontPatternRank(pattern, name string) int { func (m fontPatternMatcher) rank(name string) int {
rawName := name return m.rankCandidate(fontFileCandidate{
exactPath, _ := path.Match(pattern, name) base: name,
lowerPath, _ := path.Match(strings.ToLower(pattern), strings.ToLower(name)) lowerBase: strings.ToLower(name),
stem := fontPatternStem(pattern) normalized: fontNormalizeName(name),
if stem == "" { })
}
// 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 { if exactPath || lowerPath {
return fontMatchFuzzy return fontMatchFuzzy
} }
return fontMatchNone return fontMatchNone
} }
name = fontNormalizeName(name) name := file.normalized
if name == stem { if name == m.stem {
return fontMatchExact return fontMatchExact
} }
aliases := fontExactCandidateNames(stem) for _, alias := range m.aliases {
for _, alias := range aliases { if name == alias {
alias = fontNormalizeName(alias)
if alias != "" && name == alias {
return fontMatchExact return fontMatchExact
} }
} }
if fontFileKnownStyleSuffix(fontFileStyleSuffix(pattern, rawName)) { if fontFileKnownStyleSuffix(m.styleSuffixNormalized(name)) {
return fontMatchExact return fontMatchExact
} }
if strings.HasPrefix(name, stem) { if strings.HasPrefix(name, m.stem) {
return fontMatchPartial return fontMatchPartial
} }
for _, alias := range aliases { for _, alias := range m.aliases {
alias = fontNormalizeName(alias) if strings.HasPrefix(name, alias) {
if alias != "" && strings.HasPrefix(name, alias) {
return fontMatchPartial return fontMatchPartial
} }
} }
if exactPath || lowerPath || strings.Contains(name, stem) { if exactPath || lowerPath || strings.Contains(name, m.stem) {
return fontMatchFuzzy return fontMatchFuzzy
} }
for _, alias := range aliases { for _, alias := range m.aliases {
alias = fontNormalizeName(alias) if strings.Contains(name, alias) {
if alias != "" && strings.Contains(name, alias) {
return fontMatchFuzzy return fontMatchFuzzy
} }
} }
return fontMatchNone 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 { type fontFileMatch struct {
name string name string
pattern string priority int
rank int rank int
styleRank int
sortName string
} }
// appendFontFileMatch 追加字体文件匹配结果 // appendFontFileMatch 追加字体文件匹配结果
// 入参: matches 匹配结果, seen 已匹配文件, pattern 匹配模式, name 字体文件, rank 匹配等级, bold 是否粗体, italic 是否斜体 // 入参: matches 匹配结果, seen 已匹配文件, matcher 匹配器, file 字体文件候选, rank 匹配等级, bold 是否粗体, italic 是否斜体
func appendFontFileMatch(matches *[]fontFileMatch, seen map[string]int, pattern, name string, rank int, bold, italic bool) { func appendFontFileMatch(matches *[]fontFileMatch, seen map[string]int, matcher fontPatternMatcher, file fontFileCandidate, rank int, bold, italic bool) {
if rank == fontMatchNone || !isFontFileName(name) { if rank == fontMatchNone {
return return
} }
key := strings.ToLower(name) next := fontFileMatch{
next := fontFileMatch{name: name, pattern: pattern, rank: rank} name: file.name,
if index, ok := seen[key]; ok { priority: matcher.priority,
if fontFileMatchLess(next, (*matches)[index], bold, italic) { 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 (*matches)[index] = next
} }
return return
} }
seen[key] = len(*matches) seen[file.fold] = len(*matches)
*matches = append(*matches, next) *matches = append(*matches, next)
} }
// sortFontFileMatches 排序字体文件匹配结果 // sortFontFileMatches 排序字体文件匹配结果
// 入参: matches 匹配结果, bold 是否粗体, italic 是否斜体 // 入参: matches 匹配结果
func sortFontFileMatches(matches []fontFileMatch, bold, italic bool) { func sortFontFileMatches(matches []fontFileMatch) {
sort.SliceStable(matches, func(i, j int) bool { sort.SliceStable(matches, func(i, j int) bool {
return fontFileMatchLess(matches[i], matches[j], bold, italic) return fontFileMatchLess(matches[i], matches[j])
}) })
} }
// fontFileMatchLess 判断字体文件匹配结果优先级 // fontFileMatchLess 判断字体文件匹配结果优先级
// 入参: left 左侧匹配结果, right 右侧匹配结果, bold 是否粗体, italic 是否斜体 // 入参: left 左侧匹配结果, right 右侧匹配结果
// 返回: bool 左侧是否优先 // 返回: bool 左侧是否优先
func fontFileMatchLess(left, right fontFileMatch, bold, italic bool) bool { func fontFileMatchLess(left, right fontFileMatch) bool {
if left.priority != right.priority {
return left.priority < right.priority
}
if left.rank != right.rank { if left.rank != right.rank {
return left.rank < right.rank return left.rank < right.rank
} }
leftStyle := fontFileStyleRank(left.pattern, left.name, bold, italic) if left.styleRank != right.styleRank {
rightStyle := fontFileStyleRank(right.pattern, right.name, bold, italic) return left.styleRank < right.styleRank
if leftStyle != rightStyle {
return leftStyle < rightStyle
} }
return strings.ToLower(path.Base(left.name)) < strings.ToLower(path.Base(right.name)) return left.sortName < right.sortName
} }
// fontFileMatchNames 获取字体文件匹配名称 // fontFileMatchNames 获取字体文件匹配名称
@@ -309,10 +416,10 @@ func fontFileMatchNames(matches []fontFileMatch) []string {
} }
// fontFileStyleRank 获取字体文件样式匹配等级 // fontFileStyleRank 获取字体文件样式匹配等级
// 入参: pattern 匹配模式, name 字体文件名, bold 是否粗体, italic 是否斜体 // 入参: suffix 样式后缀, bold 是否粗体, italic 是否斜体
// 返回: int 样式匹配等级 // 返回: int 样式匹配等级
func fontFileStyleRank(pattern, name string, bold, italic bool) int { func fontFileStyleRank(suffix string, bold, italic bool) int {
fileBold, fileItalic := fontFileStyle(pattern, name) fileBold, fileItalic := fontFileStyleFromSuffix(suffix)
rank := 0 rank := 0
if fileBold != bold { if fileBold != bold {
rank += 2 rank += 2
@@ -320,18 +427,16 @@ func fontFileStyleRank(pattern, name string, bold, italic bool) int {
if fileItalic != italic { if fileItalic != italic {
rank += 2 rank += 2
} }
suffix := fontFileStyleSuffix(pattern, name)
if !bold && !italic && !fileBold && !fileItalic && suffix != "" && !fontFileRegularSuffix(suffix) { if !bold && !italic && !fileBold && !fileItalic && suffix != "" && !fontFileRegularSuffix(suffix) {
rank++ rank++
} }
return rank return rank
} }
// fontFileStyle 获取字体文件样式 // fontFileStyleFromSuffix 获取样式后缀对应的字体样式
// 入参: pattern 匹配模式, name 字体文件名 // 入参: suffix 样式后缀
// 返回: bool 是否粗体, bool 是否斜体 // 返回: bool 是否粗体, bool 是否斜体
func fontFileStyle(pattern, name string) (bool, bool) { func fontFileStyleFromSuffix(suffix string) (bool, bool) {
suffix := fontFileStyleSuffix(pattern, name)
bold := suffix == "b" || bold := suffix == "b" ||
suffix == "bd" || suffix == "bd" ||
suffix == "bold" || suffix == "bold" ||
@@ -375,27 +480,6 @@ func fontFileKnownStyleSuffix(suffix string) bool {
} }
} }
// fontFileStyleSuffix 获取字体文件样式后缀
// 入参: pattern 匹配模式, name 字体文件名
// 返回: string 样式后缀
func fontFileStyleSuffix(pattern, name string) string {
key := fontNormalizeName(name)
stem := fontPatternStem(pattern)
if stem == "" || key == "" {
return ""
}
if strings.HasPrefix(key, stem) {
return strings.TrimPrefix(key, stem)
}
for _, alias := range fontExactCandidateNames(stem) {
alias = fontNormalizeName(alias)
if alias != "" && strings.HasPrefix(key, alias) {
return strings.TrimPrefix(key, alias)
}
}
return ""
}
// fontMemFile 内存字体文件 // fontMemFile 内存字体文件
type fontMemFile struct { type fontMemFile struct {
*bytes.Reader *bytes.Reader
@@ -443,6 +527,9 @@ func (d *fontDir) Close() error {
// 返回: []fs.DirEntry 目录条目, error 错误信息 // 返回: []fs.DirEntry 目录条目, error 错误信息
func (d *fontDir) ReadDir(count int) ([]fs.DirEntry, error) { func (d *fontDir) ReadDir(count int) ([]fs.DirEntry, error) {
if d.offset >= len(d.entries) { if d.offset >= len(d.entries) {
if count <= 0 {
return nil, nil
}
return nil, io.EOF return nil, io.EOF
} }
if count <= 0 || d.offset+count > len(d.entries) { if count <= 0 || d.offset+count > len(d.entries) {
+56 -117
View File
@@ -67,6 +67,30 @@ func (r *Reader) Fonts() ([]Font, error) {
// FontInfos 获取OFD字体诊断信息 // FontInfos 获取OFD字体诊断信息
// 返回: []FontInfo 字体诊断列表, error 错误信息 // 返回: []FontInfo 字体诊断列表, error 错误信息
func (r *Renderer) FontInfos() ([]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 { if r == nil || r.Reader == nil {
return nil, fmt.Errorf("ofd renderer is not initialized") return nil, fmt.Errorf("ofd renderer is not initialized")
} }
@@ -77,11 +101,18 @@ func (r *Renderer) FontInfos() ([]FontInfo, error) {
if doc == nil { if doc == nil {
return nil, fmt.Errorf("ofd document is not opened") 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() fonts, err := r.Reader.Fonts()
if err != nil { if err != nil {
return nil, err return nil, err
} }
usage := r.fontUsage(doc) usage := r.fontUsage(doc, pages)
infos := make([]FontInfo, 0, len(fonts)+len(usage)) infos := make([]FontInfo, 0, len(fonts)+len(usage))
seen := make(map[string]bool) seen := make(map[string]bool)
for _, font := range fonts { for _, font := range fonts {
@@ -137,9 +168,9 @@ func (r *Renderer) fontInfo(font Font) FontInfo {
} }
return info return info
} }
if matched, exact := r.matchFont(font.FontName, font.FamilyName); matched != "" { if source, ok := r.fontSourceMatch(font.ID, &font); ok {
info.Matched = matched info.Matched = source.name
if exact { if source.exact {
info.Status = FontStatusMatched info.Status = FontStatusMatched
info.Detail = "使用外部字体文件" info.Detail = "使用外部字体文件"
} else { } else {
@@ -153,110 +184,46 @@ func (r *Renderer) fontInfo(font Font) FontInfo {
return info return info
} }
// matchFont 匹配外部字体
// 入参: names 字体名称列表
// 返回: string 匹配字体文件, bool 是否为名称匹配
func (r *Renderer) matchFont(names ...string) (string, bool) {
patterns := fontFilePatterns(names...)
for _, dir := range r.fontDirs {
if matches := r.matchFontFiles(dir, patterns, false, false); 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 {
if matches := fontFSMatches(fsys, fontFilePatterns(names...)); len(matches) > 0 {
return matches[0]
}
return ""
}
// fallbackFontFS 从字体文件系统获取回退字体
// 入参: fsys 字体文件系统
// 返回: string 回退字体文件
func fallbackFontFS(fsys fs.FS) string {
for _, pattern := range fontFallbackFiles {
if matches := fontFSMatches(fsys, []string{pattern}); len(matches) > 0 {
return matches[0]
}
}
all, _ := fs.Glob(fsys, "*")
for _, name := range all {
if isFontFileName(name) {
return name
}
}
return ""
}
// fontFSMatches 匹配字体文件系统中的字体文件
// 入参: fsys 字体文件系统, patterns 匹配模式列表
// 返回: []string 字体文件列表
func fontFSMatches(fsys fs.FS, patterns []string) []string {
return fontFSMatchesStyle(fsys, patterns, false, false)
}
// fontFSMatchesStyle 匹配字体文件系统中的指定样式字体文件 // fontFSMatchesStyle 匹配字体文件系统中的指定样式字体文件
// 入参: fsys 字体文件系统, patterns 匹配模式列表, bold 是否粗体, italic 是否斜体 // 入参: fsys 字体文件系统, patterns 匹配模式列表, bold 是否粗体, italic 是否斜体
// 返回: []string 字体文件列表 // 返回: []string 字体文件列表
func fontFSMatchesStyle(fsys fs.FS, patterns []string, bold, italic bool) []string { func fontFSMatchesStyle(fsys fs.FS, patterns []string, bold, italic bool) []string {
var matches []fontFileMatch
seen := make(map[string]int)
add := func(pattern, name string, rank int) {
if !isFontFileName(name) {
return
}
appendFontFileMatch(&matches, seen, pattern, name, rank, bold, italic)
}
names, _ := fs.Glob(fsys, "*") names, _ := fs.Glob(fsys, "*")
for _, pattern := range patterns { candidates := fontFileCandidates(names, path.Base)
for _, name := range names { matches := make([]fontFileMatch, 0, len(candidates))
add(pattern, name, matchFontPatternRank(pattern, path.Base(name))) 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)
} }
} }
sortFontFileMatches(matches, bold, italic) sortFontFileMatches(matches)
return fontFileMatchNames(matches) return fontFileMatchNames(matches)
} }
// fontUsage 统计文档字体使用次数 // fontUsage 统计文档字体使用次数
// 入参: doc 文档结构 // 入参: doc 文档结构, pages 页面内容列表, nil表示读取文档页面
// 返回: map[string]int 字体使用次数 // 返回: 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) usage := make(map[string]int)
if pages == nil {
for _, pageRef := range doc.Pages.Page { for _, pageRef := range doc.Pages.Page {
page, err := r.Reader.PageContent(pageRef) if page, err := r.Reader.PageContent(pageRef); err == nil {
if err == nil {
r.countPageFonts(page, usage) r.countPageFonts(page, usage)
} }
} }
} else {
for _, page := range pages {
if page != nil {
r.countPageFonts(page, usage)
}
}
}
for _, tpl := range doc.CommonData.TemplatePage { for _, tpl := range doc.CommonData.TemplatePage {
page, err := r.Reader.PageContent(Page{BaseLoc: tpl.BaseLoc}) page, err := r.Reader.PageContent(Page{BaseLoc: tpl.BaseLoc})
if err == nil { if err == nil {
r.templatePageCache[tpl.ID] = page
r.countPageFonts(page, usage) r.countPageFonts(page, usage)
} }
} }
@@ -331,35 +298,7 @@ func (r *Renderer) countCompositeFonts(cgu CompositeGraphicUnit, usage map[strin
// countTextFont 统计文本字体使用次数 // countTextFont 统计文本字体使用次数
// 入参: text 文本对象, usage 字体使用次数 // 入参: text 文本对象, usage 字体使用次数
func (r *Renderer) countTextFont(text TextObject, usage map[string]int) { func (r *Renderer) countTextFont(text TextObject, usage map[string]int) {
fontID := text.Font if fontID := r.textObjectFontID(text); fontID != "" {
if fontID == "" && text.DrawParam != "" {
fontID = r.drawParamFont(text.DrawParam, nil)
}
if fontID != "" {
usage[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)
}
+176 -82
View File
@@ -22,6 +22,36 @@ import (
"github.com/tdewolff/canvas" "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 加载字体 // loadFont 加载字体
// 入参: fontID 字体ID // 入参: fontID 字体ID
// 返回: *canvas.FontFamily 字体族 // 返回: *canvas.FontFamily 字体族
@@ -37,6 +67,7 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily {
if !ok { if !ok {
return defaultFont return defaultFont
} }
fontStyle := canvasFontStyle(of)
ff := canvas.NewFontFamily(of.FontName) ff := canvas.NewFontFamily(of.FontName)
if of.FontFile != "" { if of.FontFile != "" {
if fontData, err := r.Reader.ResData(of.FontFile); err == nil { if fontData, err := r.Reader.ResData(of.FontFile); err == nil {
@@ -66,13 +97,6 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily {
r.FontGIDMap[fontID] = inv r.FontGIDMap[fontID] = inv
} }
} }
var fontStyle canvas.FontStyle
if of.Bold {
fontStyle |= canvas.FontBold
}
if of.Italic {
fontStyle |= canvas.FontItalic
}
if err := ff.LoadFont(fontData, 0, fontStyle); err == nil { if err := ff.LoadFont(fontData, 0, fontStyle); err == nil {
r.FontMap[fontID] = ff r.FontMap[fontID] = ff
return ff return ff
@@ -81,111 +105,181 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily {
} }
return nil return nil
} }
var fontStyle canvas.FontStyle for _, source := range r.fontSources(fontID, of, fontStyle) {
if of.Bold { if loaded := r.loadFontSource(ff, source, fontStyle); loaded != nil {
fontStyle |= canvas.FontBold r.FontMap[fontID] = loaded
r.fontSourceUsed[fontID] = source
return loaded
} }
if of.Italic {
fontStyle |= canvas.FontItalic
} }
boldStyle := fontStyle&canvas.FontBold != 0 r.FontMap[fontID] = defaultFont
italicStyle := fontStyle&canvas.FontItalic != 0 return defaultFont
patterns := fontFilePatterns(of.FontName, of.FamilyName) }
// 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 _, dir := range r.fontDirs {
for _, m := range r.matchFontFiles(dir, patterns, boldStyle, italicStyle) { for _, name := range r.matchFontFiles(dir, patterns, bold, italic) {
if err := ff.LoadFontFile(m, fontStyle); err == nil { sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFile, name: name, exact: true})
r.FontMap[fontID] = ff
return ff
}
}
}
for _, fsys := range r.fontFS {
for _, m := range fontFSMatchesStyle(fsys, patterns, boldStyle, italicStyle) {
resData, err := fs.ReadFile(fsys, m)
if err == nil {
if err := ff.LoadFont(resData, 0, fontStyle); err == nil {
r.FontMap[fontID] = ff
return ff
} }
} }
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() { if !canLoadSystemFonts() {
for _, fsys := range r.fontFS { for index, fsys := range r.fontFS {
if matches, err := fs.Glob(fsys, "*"); err == nil { names, _ := fs.Glob(fsys, "*")
for _, m := range matches {
resData, err := fs.ReadFile(fsys, m)
if err == nil {
if err := ff.LoadFont(resData, 0, fontStyle); err == nil {
r.FontMap[fontID] = ff
return ff
}
}
}
}
}
r.FontMap[fontID] = defaultFont
return defaultFont
}
names := []string{of.FamilyName, of.FontName}
for _, name := range names { for _, name := range names {
if name == "" { 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 continue
} }
for _, targetName := range fontSystemNames(name) { family := canvas.NewFontFamily(font.FontName)
for _, m := range r.matchFontFiles(systemFontDir(), fontFilePatterns(targetName), boldStyle, italicStyle) { if r.loadFontSource(family, source, style) != nil {
if err := ff.LoadFontFile(m, fontStyle); err == nil { return source, true
r.FontMap[fontID] = ff
return ff
} }
} }
if err := ff.LoadSystemFont(targetName, fontStyle); err == nil { return fontSource{}, false
r.FontMap[fontID] = ff }
return ff
// 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)
} }
for _, m := range r.matchFontFiles(systemFontDir(), fontFilePatterns(name), boldStyle, italicStyle) { case fontSourceSystem:
if err := ff.LoadFontFile(m, fontStyle); err == nil { err = family.LoadSystemFont(source.name, style)
r.FontMap[fontID] = ff
return ff
} }
if err != nil {
r.fontCache[key] = nil
return nil
} }
} r.fontCache[key] = family
r.FontMap[fontID] = defaultFont return family
return defaultFont
} }
// matchFontFiles 查找字体文件 // matchFontFiles 查找字体文件
// 入参: dir 目录, patterns 模式列表, bold 是否粗体, italic 是否斜体 // 入参: dir 目录, patterns 模式列表, bold 是否粗体, italic 是否斜体
// 返回: []string 文件列表 // 返回: []string 文件列表
func (r *Renderer) matchFontFiles(dir string, patterns []string, bold, italic bool) []string { func (r *Renderer) matchFontFiles(dir string, patterns []string, bold, italic bool) []string {
var matches []fontFileMatch candidates, ok := r.fontDirCandidates[dir]
index := make(map[string]int) if !ok {
add := func(pattern, name string, rank int) {
if !isFontFileName(name) {
return
}
appendFontFileMatch(&matches, index, pattern, name, rank, bold, italic)
}
files, _ := filepath.Glob(filepath.Join(dir, "*")) files, _ := filepath.Glob(filepath.Join(dir, "*"))
for _, pattern := range patterns { candidates = fontFileCandidates(files, filepath.Base)
for _, name := range files { r.fontDirCandidates[dir] = candidates
add(pattern, name, matchFontPatternRank(pattern, filepath.Base(name))) }
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, bold, italic) sortFontFileMatches(matches)
return fontFileMatchNames(matches) return fontFileMatchNames(matches)
} }
// systemFontDir 获取系统字体目录 // systemFontDirs 获取系统字体目录
// 返回: string 字体目录 // 返回: []string 字体目录列表
func systemFontDir() string { func systemFontDirs() []string {
switch runtime.GOOS { switch runtime.GOOS {
case "linux": case "android":
return `/usr/share/fonts` return []string{"/product/fonts", "/system/fonts"}
case "darwin": case "darwin":
return `/Library/Fonts` return []string{`/Library/Fonts`}
default: case "linux":
return `C:\Windows\Fonts` return []string{`/usr/share/fonts`}
case "windows":
return []string{`C:\Windows\Fonts`}
} }
return nil
} }
+12 -7
View File
@@ -15,6 +15,7 @@
package ofdgo package ofdgo
import ( import (
"fmt"
"math" "math"
"strconv" "strconv"
"strings" "strings"
@@ -30,14 +31,18 @@ type Box struct {
// 返回: Box 矩形对象, error 错误信息 // 返回: Box 矩形对象, error 错误信息
func ParseBox(s string) (Box, error) { func ParseBox(s string) (Box, error) {
parts := strings.Fields(s) parts := strings.Fields(s)
if len(parts) < 4 { if len(parts) != 4 {
return Box{}, nil return Box{}, fmt.Errorf("invalid box: %s", s)
} }
x, _ := strconv.ParseFloat(parts[0], 64) var values [4]float64
y, _ := strconv.ParseFloat(parts[1], 64) for i, part := range parts {
w, _ := strconv.ParseFloat(parts[2], 64) value, err := strconv.ParseFloat(part, 64)
h, _ := strconv.ParseFloat(parts[3], 64) if err != nil {
return Box{X: x, Y: y, W: w, H: h}, 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仿射变换矩阵 // Matrix 2D仿射变换矩阵
+30
View File
@@ -20,12 +20,28 @@ import (
"fmt" "fmt"
"image" "image"
"image/color" "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 解码图片数据 // decodeImageData 解码图片数据
// 入参: data 图片数据 // 入参: data 图片数据
// 返回: image.Image 图片对象, string 图片格式, error 错误信息 // 返回: image.Image 图片对象, string 图片格式, error 错误信息
func decodeImageData(data []byte) (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)) img, format, err := image.Decode(bytes.NewReader(data))
if err == nil { if err == nil {
return img, normalizeSealType(format), nil return img, normalizeSealType(format), nil
@@ -36,6 +52,20 @@ func decodeImageData(data []byte) (image.Image, string, error) {
return nil, "", err 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 解码图片尺寸 // decodeImageConfigData 解码图片尺寸
// 入参: data 图片数据 // 入参: data 图片数据
// 返回: image.Config 图片尺寸, string 图片格式, error 错误信息 // 返回: image.Config 图片尺寸, string 图片格式, error 错误信息
+123 -274
View File
@@ -19,6 +19,76 @@ import (
"strconv" "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 解析图层并保留对象顺序 // UnmarshalXML 解析图层并保留对象顺序
// 入参: d XML解码器, start 起始节点 // 入参: d XML解码器, start 起始节点
// 返回: error 错误信息 // 返回: error 错误信息
@@ -26,85 +96,27 @@ func (l *Layer) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
*l = Layer{} *l = Layer{}
l.ID = attrValue(start, "ID") l.ID = attrValue(start, "ID")
l.DrawParam = attrValue(start, "DrawParam") l.DrawParam = attrValue(start, "DrawParam")
for { return decodeObjectContainer(d, start, l.decodeObject)
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
}
}
}
} }
// decodeObject 解析图层子对象 // decodeObject 解析图层子对象
// 入参: d XML解码器, start 起始节点 // 入参: d XML解码器, start 起始节点
// 返回: error 错误信息 // 返回: error 错误信息
func (l *Layer) decodeObject(d *xml.Decoder, start xml.StartElement) error { func (l *Layer) decodeObject(d *xml.Decoder, start xml.StartElement) error {
switch start.Name.Local { target := graphicObjectTarget{
case "TextObject": objects: &l.Objects,
var obj TextObject text: &l.TextObject,
if err := d.DecodeElement(&obj, &start); err != nil { path: &l.PathObject,
image: &l.ImageObject,
composite: &l.CompositeGraphicUnit,
}
if decoded, err := decodeGraphicObject(d, start, target); decoded || err != nil {
return err return err
} }
l.TextObject = append(l.TextObject, obj) if start.Name.Local == "PageBlock" {
l.Objects = append(l.Objects, GraphicObject{Type: start.Name.Local, TextObject: obj}) return decodeObjectContainer(d, start, l.decodeObject)
case "PathObject":
var obj PathObject
if err := d.DecodeElement(&obj, &start); err != nil {
return err
} }
l.PathObject = append(l.PathObject, obj)
l.Objects = append(l.Objects, GraphicObject{Type: start.Name.Local, PathObject: obj})
case "ImageObject":
var obj ImageObject
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
l.ImageObject = append(l.ImageObject, obj)
l.Objects = append(l.Objects, GraphicObject{Type: start.Name.Local, ImageObject: obj})
case "CompositeGraphicUnit", "CompositeObject":
var obj CompositeGraphicUnit
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
l.CompositeGraphicUnit = append(l.CompositeGraphicUnit, obj)
l.Objects = append(l.Objects, GraphicObject{Type: start.Name.Local, CompositeGraphicUnit: obj})
case "PageBlock":
return l.decodePageBlock(d, start)
default:
return d.Skip() 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 解析复合图元并保留对象顺序 // UnmarshalXML 解析复合图元并保留对象顺序
@@ -128,178 +140,73 @@ func (c *CompositeGraphicUnit) UnmarshalXML(d *xml.Decoder, start xml.StartEleme
c.Visible = &visible c.Visible = &visible
} }
} }
for { return decodeObjectContainer(d, start, c.decodeObject)
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
}
}
}
} }
// decodeObject 解析复合图元子对象 // decodeObject 解析复合图元子对象
// 入参: d XML解码器, start 起始节点 // 入参: d XML解码器, start 起始节点
// 返回: error 错误信息 // 返回: error 错误信息
func (c *CompositeGraphicUnit) decodeObject(d *xml.Decoder, start xml.StartElement) 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 { 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": case "Clips":
var clips Clips var clips Clips
if err := d.DecodeElement(&clips, &start); err != nil { if err := d.DecodeElement(&clips, &start); err != nil {
return err return err
} }
c.Clips = &clips 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": case "Content", "PageBlock":
return c.decodePageBlock(d, start) return decodeObjectContainer(d, start, c.decodeObject)
default: default:
return d.Skip() return d.Skip()
} }
return nil 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 解析注释外观并保留对象顺序 // UnmarshalXML 解析注释外观并保留对象顺序
// 入参: d XML解码器, start 起始节点 // 入参: d XML解码器, start 起始节点
// 返回: error 错误信息 // 返回: error 错误信息
func (a *Appearance) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { func (a *Appearance) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
*a = Appearance{} *a = Appearance{}
a.Boundary = attrValue(start, "Boundary") a.Boundary = attrValue(start, "Boundary")
for { return decodeObjectContainer(d, start, a.decodeObject)
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
}
}
}
} }
// decodeObject 解析注释外观子对象 // decodeObject 解析注释外观子对象
// 入参: d XML解码器, start 起始节点 // 入参: d XML解码器, start 起始节点
// 返回: error 错误信息 // 返回: error 错误信息
func (a *Appearance) decodeObject(d *xml.Decoder, start xml.StartElement) error { func (a *Appearance) decodeObject(d *xml.Decoder, start xml.StartElement) error {
switch start.Name.Local { target := graphicObjectTarget{
case "TextObject": objects: &a.Objects,
var obj TextObject text: &a.TextObject,
if err := d.DecodeElement(&obj, &start); err != nil { path: &a.PathObject,
image: &a.ImageObject,
composite: &a.CompositeGraphicUnit,
}
if decoded, err := decodeGraphicObject(d, start, target); decoded || err != nil {
return err return err
} }
a.TextObject = append(a.TextObject, obj) if start.Name.Local == "PageBlock" {
a.Objects = append(a.Objects, GraphicObject{Type: start.Name.Local, TextObject: obj}) return decodeObjectContainer(d, start, a.decodeObject)
case "PathObject":
var obj PathObject
if err := d.DecodeElement(&obj, &start); err != nil {
return err
} }
a.PathObject = append(a.PathObject, obj)
a.Objects = append(a.Objects, GraphicObject{Type: start.Name.Local, PathObject: obj})
case "ImageObject":
var obj ImageObject
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
a.ImageObject = append(a.ImageObject, obj)
a.Objects = append(a.Objects, GraphicObject{Type: start.Name.Local, ImageObject: obj})
case "CompositeGraphicUnit", "CompositeObject":
var obj CompositeGraphicUnit
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
a.CompositeGraphicUnit = append(a.CompositeGraphicUnit, obj)
a.Objects = append(a.Objects, GraphicObject{Type: start.Name.Local, CompositeGraphicUnit: obj})
case "PageBlock":
return a.decodePageBlock(d, start)
default:
return d.Skip() 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 解析图案单元内容并保留对象顺序 // UnmarshalXML 解析图案单元内容并保留对象顺序
@@ -307,85 +214,27 @@ func (a *Appearance) decodePageBlock(d *xml.Decoder, start xml.StartElement) err
// 返回: error 错误信息 // 返回: error 错误信息
func (p *PatternContent) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { func (p *PatternContent) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
*p = PatternContent{} *p = PatternContent{}
for { return decodeObjectContainer(d, start, p.decodeObject)
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
}
}
}
} }
// decodeObject 解析图案单元内容子对象 // decodeObject 解析图案单元内容子对象
// 入参: d XML解码器, start 起始节点 // 入参: d XML解码器, start 起始节点
// 返回: error 错误信息 // 返回: error 错误信息
func (p *PatternContent) decodeObject(d *xml.Decoder, start xml.StartElement) error { func (p *PatternContent) decodeObject(d *xml.Decoder, start xml.StartElement) error {
switch start.Name.Local { target := graphicObjectTarget{
case "TextObject": objects: &p.Objects,
var obj TextObject text: &p.TextObject,
if err := d.DecodeElement(&obj, &start); err != nil { path: &p.PathObject,
image: &p.ImageObject,
composite: &p.CompositeGraphicUnit,
}
if decoded, err := decodeGraphicObject(d, start, target); decoded || err != nil {
return err return err
} }
p.TextObject = append(p.TextObject, obj) if start.Name.Local == "PageBlock" {
p.Objects = append(p.Objects, GraphicObject{Type: start.Name.Local, TextObject: obj}) return decodeObjectContainer(d, start, p.decodeObject)
case "PathObject":
var obj PathObject
if err := d.DecodeElement(&obj, &start); err != nil {
return err
} }
p.PathObject = append(p.PathObject, obj)
p.Objects = append(p.Objects, GraphicObject{Type: start.Name.Local, PathObject: obj})
case "ImageObject":
var obj ImageObject
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
p.ImageObject = append(p.ImageObject, obj)
p.Objects = append(p.Objects, GraphicObject{Type: start.Name.Local, ImageObject: obj})
case "CompositeGraphicUnit", "CompositeObject":
var obj CompositeGraphicUnit
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
p.CompositeGraphicUnit = append(p.CompositeGraphicUnit, obj)
p.Objects = append(p.Objects, GraphicObject{Type: start.Name.Local, CompositeGraphicUnit: obj})
case "PageBlock":
return p.decodePageBlock(d, start)
default:
return d.Skip() 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属性值 // attrValue 获取XML属性值
+6 -1
View File
@@ -23,6 +23,7 @@ type PageContent struct {
Area PageArea `xml:"Area"` Area PageArea `xml:"Area"`
Template []Template `xml:"Template"` Template []Template `xml:"Template"`
Content Content `xml:"Content"` Content Content `xml:"Content"`
Actions []Action `xml:"Actions>Action"`
} }
// Template 页面模板引用 // Template 页面模板引用
@@ -58,7 +59,7 @@ type GraphicObject struct {
// Clips 裁剪区域集合 // Clips 裁剪区域集合
type Clips struct { type Clips struct {
TransFlag bool `xml:"TransFlag,attr"` TransFlag *bool `xml:"TransFlag,attr"`
Clip []Clip `xml:"Clip"` Clip []Clip `xml:"Clip"`
} }
@@ -97,6 +98,7 @@ type TextObject struct {
CGTransform []CGTransform `xml:"CGTransform"` CGTransform []CGTransform `xml:"CGTransform"`
TextCode []TextCode `xml:"TextCode"` TextCode []TextCode `xml:"TextCode"`
Clips *Clips `xml:"Clips"` Clips *Clips `xml:"Clips"`
Actions []Action `xml:"Actions>Action"`
} }
// FillColor 填充颜色 // FillColor 填充颜色
@@ -165,6 +167,7 @@ type PathObject struct {
FillColor *FillColor `xml:"FillColor"` FillColor *FillColor `xml:"FillColor"`
AbbreviatedData string `xml:"AbbreviatedData"` AbbreviatedData string `xml:"AbbreviatedData"`
Clips *Clips `xml:"Clips"` Clips *Clips `xml:"Clips"`
Actions []Action `xml:"Actions>Action"`
} }
// StrokeColor 勾边颜色 // StrokeColor 勾边颜色
@@ -210,8 +213,10 @@ type ImageObject struct {
ID string `xml:"ID,attr"` ID string `xml:"ID,attr"`
Boundary string `xml:"Boundary,attr"` Boundary string `xml:"Boundary,attr"`
ResourceID string `xml:"ResourceID,attr"` ResourceID string `xml:"ResourceID,attr"`
ImageMask string `xml:"ImageMask,attr"`
CTM string `xml:"CTM,attr"` CTM string `xml:"CTM,attr"`
Alpha *int `xml:"Alpha,attr"` Alpha *int `xml:"Alpha,attr"`
Visible *bool `xml:"Visible,attr"` Visible *bool `xml:"Visible,attr"`
Clips *Clips `xml:"Clips"` Clips *Clips `xml:"Clips"`
Actions []Action `xml:"Actions>Action"`
} }
+53 -61
View File
@@ -142,10 +142,10 @@ func parseFillColor(fillColor *FillColor) color.Color {
return nil return nil
} }
if fillColor.AxialShd != nil { if fillColor.AxialShd != nil {
return parseAxialShdColor(fillColor.AxialShd, fillColor.Alpha) return parseShdColor(fillColor.AxialShd.Segment, fillColor.Alpha)
} }
if fillColor.RadialShd != nil { if fillColor.RadialShd != nil {
return parseRadialShdColor(fillColor.RadialShd, fillColor.Alpha) return parseShdColor(fillColor.RadialShd.Segment, fillColor.Alpha)
} }
if strings.TrimSpace(fillColor.Value) != "" { if strings.TrimSpace(fillColor.Value) != "" {
return parseColorWithAlpha(fillColor.Value, fillColor.Alpha) return parseColorWithAlpha(fillColor.Value, fillColor.Alpha)
@@ -170,10 +170,10 @@ func parseFillPaint(fillColor *FillColor, x, y, pageH, originX, originY float64)
return gradient return gradient
} }
if fillColor.AxialShd != nil { if fillColor.AxialShd != nil {
return parseAxialShdColor(fillColor.AxialShd, fillColor.Alpha) return parseShdColor(fillColor.AxialShd.Segment, fillColor.Alpha)
} }
if fillColor.RadialShd != nil { if fillColor.RadialShd != nil {
return parseRadialShdColor(fillColor.RadialShd, fillColor.Alpha) return parseShdColor(fillColor.RadialShd.Segment, fillColor.Alpha)
} }
if strings.TrimSpace(fillColor.Value) != "" { if strings.TrimSpace(fillColor.Value) != "" {
return parseColorWithAlpha(fillColor.Value, fillColor.Alpha) return parseColorWithAlpha(fillColor.Value, fillColor.Alpha)
@@ -189,10 +189,10 @@ func parseStrokeColor(strokeColor *StrokeColor) color.Color {
return nil return nil
} }
if strokeColor.AxialShd != nil { if strokeColor.AxialShd != nil {
return parseAxialShdColor(strokeColor.AxialShd, strokeColor.Alpha) return parseShdColor(strokeColor.AxialShd.Segment, strokeColor.Alpha)
} }
if strokeColor.RadialShd != nil { if strokeColor.RadialShd != nil {
return parseRadialShdColor(strokeColor.RadialShd, strokeColor.Alpha) return parseShdColor(strokeColor.RadialShd.Segment, strokeColor.Alpha)
} }
if strings.TrimSpace(strokeColor.Value) != "" { if strings.TrimSpace(strokeColor.Value) != "" {
return parseColorWithAlpha(strokeColor.Value, strokeColor.Alpha) return parseColorWithAlpha(strokeColor.Value, strokeColor.Alpha)
@@ -214,10 +214,10 @@ func parseStrokePaint(strokeColor *StrokeColor, x, y, pageH, originX, originY fl
return gradient return gradient
} }
if strokeColor.AxialShd != nil { if strokeColor.AxialShd != nil {
return parseAxialShdColor(strokeColor.AxialShd, strokeColor.Alpha) return parseShdColor(strokeColor.AxialShd.Segment, strokeColor.Alpha)
} }
if strokeColor.RadialShd != nil { if strokeColor.RadialShd != nil {
return parseRadialShdColor(strokeColor.RadialShd, strokeColor.Alpha) return parseShdColor(strokeColor.RadialShd.Segment, strokeColor.Alpha)
} }
if strings.TrimSpace(strokeColor.Value) != "" { if strings.TrimSpace(strokeColor.Value) != "" {
return parseColorWithAlpha(strokeColor.Value, strokeColor.Alpha) return parseColorWithAlpha(strokeColor.Value, strokeColor.Alpha)
@@ -225,24 +225,54 @@ func parseStrokePaint(strokeColor *StrokeColor, x, y, pageH, originX, originY fl
return nil return nil
} }
// parseAxialShdColor 解析轴向渐变颜色 // patternColor 获取图案单元默认颜色
// 入参: axialShd 轴向渐变节点, alpha 透明度 // 入参: 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 颜色对象 // 返回: color.Color 颜色对象
func parseAxialShdColor(axialShd *AxialShd, alpha *int) color.Color { func parseShdColor(segments []ShdSegment, alpha *int) color.Color {
if axialShd != nil { for _, segment := range segments {
for _, segment := range axialShd.Segment {
if strings.TrimSpace(segment.Color.Value) == "" { if strings.TrimSpace(segment.Color.Value) == "" {
continue continue
} }
if alpha == nil { segmentAlpha := alpha
alpha = segment.Color.Alpha if segmentAlpha == nil {
} segmentAlpha = segment.Color.Alpha
return parseColorWithAlpha(segment.Color.Value, alpha)
} }
return parseColorWithAlpha(segment.Color.Value, segmentAlpha)
} }
return color.Black 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 解析轴向渐变 // parseAxialShdGradient 解析轴向渐变
// 入参: axialShd 轴向渐变节点, alpha 透明度, x X坐标, y Y坐标, pageH 页面高度, originX 原点X坐标, originY 原点Y坐标 // 入参: axialShd 轴向渐变节点, alpha 透明度, x X坐标, y Y坐标, pageH 页面高度, originX 原点X坐标, originY 原点Y坐标
// 返回: canvas.Gradient 渐变对象 // 返回: canvas.Gradient 渐变对象
@@ -255,18 +285,8 @@ func parseAxialShdGradient(axialShd *AxialShd, alpha *int, x, y, pageH, originX,
if len(start) < 2 || len(end) < 2 { if len(start) < 2 || len(end) < 2 {
return nil return nil
} }
grad := canvas.NewGradient() gradient := parseShdSegments(axialShd.Segment, alpha)
for _, segment := range axialShd.Segment { if gradient == nil {
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 return nil
} }
startPoint := canvas.Point{X: x + start[0] - originX, Y: pageH - (y + start[1]) - originY} startPoint := canvas.Point{X: x + start[0] - originX, Y: pageH - (y + start[1]) - originY}
@@ -274,25 +294,7 @@ func parseAxialShdGradient(axialShd *AxialShd, alpha *int, x, y, pageH, originX,
if startPoint.Equals(endPoint) { if startPoint.Equals(endPoint) {
return nil return nil
} }
return grad.ToLinear(startPoint, endPoint) return gradient.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 解析径向渐变 // parseRadialShdGradient 解析径向渐变
@@ -307,23 +309,13 @@ func parseRadialShdGradient(radialShd *RadialShd, alpha *int, x, y, pageH, origi
if len(start) < 2 || len(end) < 2 { if len(start) < 2 || len(end) < 2 {
return nil return nil
} }
grad := canvas.NewGradient() gradient := parseShdSegments(radialShd.Segment, alpha)
for _, segment := range radialShd.Segment { if gradient == nil {
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 return nil
} }
startPoint := canvas.Point{X: x + start[0] - originX, Y: pageH - (y + start[1]) - originY} 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} endPoint := canvas.Point{X: x + end[0] - originX, Y: pageH - (y + end[1]) - originY}
return grad.ToRadial(startPoint, radialShd.StartRadius, endPoint, radialShd.EndRadius) return gradient.ToRadial(startPoint, radialShd.StartRadius, endPoint, radialShd.EndRadius)
} }
// colorToRGBA 转换颜色对象 // colorToRGBA 转换颜色对象
+99 -4
View File
@@ -176,7 +176,7 @@ func (r *Reader) loadRes(resPath string) {
if resPath == "" { if resPath == "" {
return return
} }
fullPath := path.Join(r.RootDir, resPath) fullPath := r.ResPath(resPath)
data, err := r.readFile(fullPath) data, err := r.readFile(fullPath)
if err != nil { if err != nil {
return return
@@ -235,7 +235,7 @@ func resolveResourcePath(resPath, baseLoc, filePath string) string {
// 入参: page 页面对象 // 入参: page 页面对象
// 返回: *PageContent 页面内容, error 错误信息 // 返回: *PageContent 页面内容, error 错误信息
func (r *Reader) PageContent(page 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) data, err := r.readFile(fullPath)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -260,7 +260,9 @@ func (r *Reader) ResPath(resLink string) string {
if resLink == "" { if resLink == "" {
return "" return ""
} }
resLink = strings.TrimPrefix(resLink, "/") if strings.HasPrefix(resLink, "/") {
return cleanPackagePath(resLink)
}
resLink = path.Clean(resLink) resLink = path.Clean(resLink)
rootDir := strings.TrimPrefix(strings.ReplaceAll(r.RootDir, "\\", "/"), "/") rootDir := strings.TrimPrefix(strings.ReplaceAll(r.RootDir, "\\", "/"), "/")
if rootDir != "" && (resLink == rootDir || strings.HasPrefix(resLink, rootDir+"/")) { if rootDir != "" && (resLink == rootDir || strings.HasPrefix(resLink, rootDir+"/")) {
@@ -277,6 +279,21 @@ func (r *Reader) ResData(resLink string) ([]byte, error) {
return r.readFile(fullPath) return r.readFile(fullPath)
} }
// 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
}
if err := xml.Unmarshal(data, value); err != nil {
return "", fmt.Errorf("failed to unmarshal %s: %w", path.Base(fullPath), err)
}
return fullPath, nil
}
// Version 获取OFD版本号 // Version 获取OFD版本号
// 返回: string 版本号 // 返回: string 版本号
func (r *Reader) Version() string { func (r *Reader) Version() string {
@@ -324,6 +341,26 @@ func (r *Reader) Outlines() ([]OutlineElem, error) {
return doc.Outlines.OutlineElem, nil 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 获取附件列表 // Attachments 获取附件列表
// 返回: []Attachment 附件列表, error 错误信息 // 返回: []Attachment 附件列表, error 错误信息
func (r *Reader) Attachments() ([]Attachment, error) { func (r *Reader) Attachments() ([]Attachment, error) {
@@ -331,9 +368,50 @@ func (r *Reader) Attachments() ([]Attachment, error) {
if err != nil { if err != nil {
return nil, err 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 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 获取自定义数据 // CustomDatas 获取自定义数据
// 返回: []CustomData 自定义数据列表, error 错误信息 // 返回: []CustomData 自定义数据列表, error 错误信息
func (r *Reader) CustomDatas() ([]CustomData, error) { func (r *Reader) CustomDatas() ([]CustomData, error) {
@@ -347,12 +425,29 @@ func (r *Reader) CustomDatas() ([]CustomData, error) {
return info.CustomDatas.CustomData, nil return info.CustomDatas.CustomData, nil
} }
// Extensions 获取分列扩展项 // Extensions 获取扩展项
// 返回: []Extension 扩展项列表, error 错误信息 // 返回: []Extension 扩展项列表, error 错误信息
func (r *Reader) Extensions() ([]Extension, error) { func (r *Reader) Extensions() ([]Extension, error) {
doc, err := r.Doc() doc, err := r.Doc()
if err != nil { if err != nil {
return nil, err 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 return doc.Extensions.Extension, nil
} }
-189
View File
@@ -1,189 +0,0 @@
// 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"
"image/color"
"github.com/tdewolff/canvas"
)
// renderImage 渲染图片
// 入参: ctx 画布上下文, obj 图片对象, pageH 页面高度, parentCTM 父级CTM, boundaryInCTM 边界是否参与父级CTM
func (r *Renderer) renderImage(ctx *canvas.Context, obj ImageObject, pageH float64, parentCTM *Matrix, boundaryInCTM bool) {
if obj.Visible != nil && !*obj.Visible {
return
}
resPath, ok := r.Reader.ResMap[obj.ResourceID]
if !ok {
return
}
imgData, err := r.Reader.ResData(resPath)
if err != nil {
return
}
img, _, err := decodeImageData(imgData)
if err != nil {
return
}
box, _ := ParseBox(obj.Boundary)
imgBounds := img.Bounds()
imgW, imgH := float64(imgBounds.Dx()), float64(imgBounds.Dy())
if imgW <= 0 || imgH <= 0 {
return
}
img = imageWithAlpha(img, obj.Alpha)
pad := 0
img, pad = imageWithTransparentEdge(img)
ctm := NewMatrix(obj.CTM)
if obj.CTM == "" {
ctm = Matrix{a: box.W, d: box.H}
}
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},
}
}
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))
}
// imageWithAlpha 合并图片透明度
// 入参: img 图片对象, alpha 对象透明度
// 返回: image.Image 合并后的图片对象
func imageWithAlpha(img image.Image, alpha *int) image.Image {
if img == nil || alpha == nil {
return img
}
a := clampColor(*alpha)
bounds := img.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++ {
c := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA)
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
}
src := image.NewNRGBA(image.Rect(0, 0, w, h))
hasZero, hasVisible := false, false
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
c := color.NRGBAModel.Convert(img.At(bounds.Min.X+x, bounds.Min.Y+y)).(color.NRGBA)
src.SetNRGBA(x, y, c)
if c.A == 0 {
hasZero = true
} else {
hasVisible = true
}
}
}
if !hasZero || !hasVisible {
return img, 0
}
out := image.NewNRGBA(image.Rect(0, 0, w+2, h+2))
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
c := src.NRGBAAt(x, y)
if c.A == 0 {
if edge, ok := transparentEdgeColor(src, x, y); 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
}
// 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
}
+23 -1107
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"` ImageObject []ImageObject `xml:"ImageObject"`
CompositeGraphicUnit []CompositeGraphicUnit `xml:"CompositeGraphicUnit"` CompositeGraphicUnit []CompositeGraphicUnit `xml:"CompositeGraphicUnit"`
Clips *Clips `xml:"Clips"` Clips *Clips `xml:"Clips"`
Actions []Action `xml:"Actions>Action"`
} }
+36 -25
View File
@@ -20,7 +20,6 @@ import (
"encoding/binary" "encoding/binary"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"strconv"
"strings" "strings"
) )
@@ -28,14 +27,16 @@ import (
type SignType string type SignType string
const ( const (
// SignTypeSeal 印章签名
SignTypeSeal SignType = "Seal" SignTypeSeal SignType = "Seal"
// SignTypeSign 数字签名
SignTypeSign SignType = "Sign" SignTypeSign SignType = "Sign"
) )
// Signatures 签名列表 // Signatures 签名列表
type Signatures struct { type Signatures struct {
XMLName xml.Name `xml:"Signatures"` XMLName xml.Name `xml:"Signatures"`
MaxSignId string `xml:"MaxSignId"` MaxSignID string `xml:"MaxSignId"`
List []Signature `xml:"Signature"` List []Signature `xml:"Signature"`
} }
@@ -80,6 +81,7 @@ type SignatureStamp struct {
ID string `xml:"ID,attr"` ID string `xml:"ID,attr"`
PageRef string `xml:"PageRef,attr"` PageRef string `xml:"PageRef,attr"`
Boundary string `xml:"Boundary,attr"` Boundary string `xml:"Boundary,attr"`
Clip string `xml:"Clip,attr"`
} }
// SignatureStampPosition 签名外观位置信息 // SignatureStampPosition 签名外观位置信息
@@ -89,6 +91,8 @@ type SignatureStampPosition struct {
PageID string PageID string
Boundary string Boundary string
Box Box Box Box
Clip string
ClipBox *Box
} }
// SignatureReferences 签名保护文件列表 // SignatureReferences 签名保护文件列表
@@ -122,12 +126,22 @@ func (r *Reader) SignatureStampPositions(stamps []SignatureStamp) ([]SignatureSt
if err != nil { if err != nil {
return nil, err 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{ positions = append(positions, SignatureStampPosition{
ID: stamp.ID, ID: stamp.ID,
Page: page, Page: page,
PageID: stamp.PageRef, PageID: stamp.PageRef,
Boundary: stamp.Boundary, Boundary: stamp.Boundary,
Box: box, Box: box,
Clip: stamp.Clip,
ClipBox: clipBox,
}) })
} }
return positions, nil return positions, nil
@@ -137,27 +151,11 @@ func (r *Reader) SignatureStampPositions(stamps []SignatureStamp) ([]SignatureSt
// 入参: s 区域字符串 // 入参: s 区域字符串
// 返回: Box 矩形对象, error 错误信息 // 返回: Box 矩形对象, error 错误信息
func parseSignatureStampBox(s string) (Box, error) { func parseSignatureStampBox(s string) (Box, error) {
parts := strings.Fields(s) box, err := ParseBox(s)
if len(parts) != 4 {
return Box{}, fmt.Errorf("invalid signature stamp boundary: %s", s)
}
x, err := strconv.ParseFloat(parts[0], 64)
if err != nil { if err != nil {
return Box{}, fmt.Errorf("invalid signature stamp boundary: %s", s) return Box{}, fmt.Errorf("invalid signature stamp box: %s", s)
} }
y, err := strconv.ParseFloat(parts[1], 64) return box, nil
if err != nil {
return Box{}, fmt.Errorf("invalid signature stamp boundary: %s", s)
}
w, err := strconv.ParseFloat(parts[2], 64)
if err != nil {
return Box{}, fmt.Errorf("invalid signature stamp boundary: %s", s)
}
h, err := strconv.ParseFloat(parts[3], 64)
if err != nil {
return Box{}, fmt.Errorf("invalid signature stamp boundary: %s", s)
}
return Box{X: x, Y: y, W: w, H: h}, nil
} }
// parseSignatures 解析签名文件 // parseSignatures 解析签名文件
@@ -212,8 +210,19 @@ func (r *Reader) parseSignatures(doc *Document) error {
} }
for _, annot := range sigFile.SignedInfo.StampAnnot { for _, annot := range sigFile.SignedInfo.StampAnnot {
pageID := annot.PageRef pageID := annot.PageRef
bbox, _ := ParseBox(annot.Boundary) bbox, err := parseSignatureStampBox(annot.Boundary)
r.addStamp(pageID, bbox, sealType, sealData) 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) }(sigRef)
} }
@@ -415,18 +424,20 @@ func probeImageMedia(data []byte) (string, []byte) {
// Stamp 印章信息结构 // Stamp 印章信息结构
type Stamp struct { type Stamp struct {
Box Box Box Box
Clip *Box
Type string Type string
Data []byte Data []byte
} }
// addStamp 添加印章到页面 // addStamp 添加印章到页面
// 入参: pageID 页面ID, box 印章区域, sType 印章类型, data 印章数据 // 入参: pageID 页面ID, box 印章区域, clip 裁剪区域, sType 印章类型, data 印章数据
func (r *Reader) addStamp(pageID string, box Box, sType string, data []byte) { func (r *Reader) addStamp(pageID string, box Box, clip *Box, sType string, data []byte) {
if r.Stamps == nil { if r.Stamps == nil {
r.Stamps = make(map[string][]Stamp) r.Stamps = make(map[string][]Stamp)
} }
r.Stamps[pageID] = append(r.Stamps[pageID], Stamp{ r.Stamps[pageID] = append(r.Stamps[pageID], Stamp{
Box: box, Box: box,
Clip: clip,
Type: sType, Type: sType,
Data: data, Data: data,
}) })
+132 -9
View File
@@ -66,8 +66,8 @@ type gbtSignerInfo struct {
// 入参: method 签名算法, digestMethod 摘要算法, signedValue 签名值, signedData 被签名原文, options 验证选项 // 入参: method 签名算法, digestMethod 摘要算法, signedValue 签名值, signedData 被签名原文, options 验证选项
// 返回: *digitalVerifyResult 验证结果, error 错误信息 // 返回: *digitalVerifyResult 验证结果, error 错误信息
func verifyDigitalSignature(method, digestMethod string, signedValue, signedData []byte, options *signatureVerifyOptions) (*digitalVerifyResult, error) { func verifyDigitalSignature(method, digestMethod string, signedValue, signedData []byte, options *signatureVerifyOptions) (*digitalVerifyResult, error) {
if isGBT35275SignedValue(signedValue) { if value, ok := normalizeGBT35275SignedValue(signedValue); ok {
return verifyGBT35275SignedData(signedValue, signedData, options) return verifyGBT35275SignedData(value, signedData, options)
} }
if isSM2SignatureMethod(method) { if isSM2SignatureMethod(method) {
return verifyRawDigitalSignature(signedValue, signedData, options) return verifyRawDigitalSignature(signedValue, signedData, options)
@@ -204,27 +204,150 @@ func verifyGBT35275SignedData(signedValue, signedData []byte, options *signature
return result, nil return result, nil
} }
// isGBT35275SignedValue 判断签名值是否为GB/T 35275 SignedData // normalizeGBT35275SignedValue 规范化GB/T 35275 SignedData编码
// 入参: data 签名值数据 // 入参: data 签名值数据
// 返回: bool 是否为SignedData // 返回: []byte 定长编码数据, bool 是否为SignedData
func isGBT35275SignedValue(data []byte) bool { 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 var root asn1.RawValue
rest, err := asn1.Unmarshal(data, &root) rest, err := asn1.Unmarshal(data, &root)
if err != nil || len(rest) != 0 || root.Tag != signASN1Sequence { if err != nil || len(rest) != 0 || root.Tag != signASN1Sequence {
return false return "", false
} }
items, ok := asn1Children(root.Bytes) items, ok := asn1Children(root.Bytes)
if !ok || len(items) < 2 { if !ok || len(items) == 0 {
return false return "", false
}
if items[0].Tag != asn1.TagOID {
return "", true
} }
oid, err := asn1OIDString(items[0]) 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 // parseGBT35275SignedData 解析GB/T 35275 SignedData
// 入参: data 签名值数据 // 入参: data 签名值数据
// 返回: *gbtSignedData SignedData结构, error 错误信息 // 返回: *gbtSignedData SignedData结构, error 错误信息
func parseGBT35275SignedData(data []byte) (*gbtSignedData, 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) contentType, content, ok, err := parseGBTContentInfoBytes(data)
if err != nil { if err != nil {
return nil, err return nil, err
+119 -31
View File
@@ -20,6 +20,7 @@ import (
"fmt" "fmt"
"math/big" "math/big"
"strings" "strings"
"time"
) )
const ( const (
@@ -42,6 +43,7 @@ type sesSignature struct {
SignAlg string SignAlg string
Signature []byte Signature []byte
DataHash []byte DataHash []byte
Time time.Time
Seal *sesSeal Seal *sesSeal
} }
@@ -55,6 +57,7 @@ type sesSeal struct {
PicType string PicType string
PicData []byte PicData []byte
CertList sesCertList CertList sesCertList
Info SignatureSealInfo
} }
// sesCertList SES印章证书列表 // sesCertList SES印章证书列表
@@ -82,6 +85,8 @@ type sesVerifyResult struct {
SealRaw []byte SealRaw []byte
Certs [][]byte Certs [][]byte
SealType string SealType string
SealInfo SignatureSealInfo
SignatureTime time.Time
} }
// parseSESSignature 解析SES签章值 // parseSESSignature 解析SES签章值
@@ -119,13 +124,21 @@ func parseSESSignature(data []byte) (*sesSignature, error) {
return nil, err return nil, err
} }
tbsItems, ok := asn1Children(items[0].Bytes) 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") return nil, fmt.Errorf("invalid ses toSign")
} }
seal, err := parseSESSeal(tbsItems[1]) seal, err := parseSESSeal(tbsItems[1])
if err != nil { if err != nil {
return nil, err 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]) dataHash, err := asn1BitOrOctetBytes(tbsItems[3])
if err != nil { if err != nil {
return nil, err return nil, err
@@ -136,6 +149,7 @@ func parseSESSignature(data []byte) (*sesSignature, error) {
SignAlg: alg, SignAlg: alg,
Signature: signature, Signature: signature,
DataHash: dataHash, DataHash: dataHash,
Time: signatureTime,
Seal: seal, Seal: seal,
}, nil }, nil
} }
@@ -156,6 +170,11 @@ func parseSESSignatureV1(items []asn1.RawValue) (*sesSignature, error) {
if err != nil { if err != nil {
return nil, err 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]) dataHash, err := asn1BitOrOctetBytes(tbsItems[3])
if err != nil { if err != nil {
return nil, err return nil, err
@@ -174,10 +193,31 @@ func parseSESSignatureV1(items []asn1.RawValue) (*sesSignature, error) {
SignAlg: alg, SignAlg: alg,
Signature: signature, Signature: signature,
DataHash: dataHash, DataHash: dataHash,
Time: signatureTime,
Seal: seal, Seal: seal,
}, nil }, 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签章值 // verifySESSignature 验证SES签章值
// 入参: data 签章值数据, signedData 被签名数据原文, options 验证选项 // 入参: data 签章值数据, signedData 被签名数据原文, options 验证选项
// 返回: *sesVerifyResult 验证结果, error 错误信息 // 返回: *sesVerifyResult 验证结果, error 错误信息
@@ -198,6 +238,8 @@ func verifySESSignature(data, signedData []byte, options *signatureVerifyOptions
result.Certs = append(result.Certs, sig.Seal.CertList.Certs...) result.Certs = append(result.Certs, sig.Seal.CertList.Certs...)
result.Certs = append(result.Certs, options.SignCerts...) result.Certs = append(result.Certs, options.SignCerts...)
result.SealType = sig.Seal.PicType result.SealType = sig.Seal.PicType
result.SealInfo = sig.Seal.Info
result.SignatureTime = sig.Time
result.DataHashOK = bytes.Equal(sig.DataHash, signSM3(signedData)) result.DataHashOK = bytes.Equal(sig.DataHash, signSM3(signedData))
signPub, err := parseSM2PublicKeyFromCert(sig.Cert) signPub, err := parseSM2PublicKeyFromCert(sig.Cert)
if err != nil { if err != nil {
@@ -243,11 +285,7 @@ func parseSESSeal(raw asn1.RawValue) (*sesSeal, error) {
if !ok || len(infoItems) < 4 || len(infoItems) > 5 { if !ok || len(infoItems) < 4 || len(infoItems) > 5 {
return nil, fmt.Errorf("invalid ses seal info") return nil, fmt.Errorf("invalid ses seal info")
} }
version, err := parseSESHeaderVersion(infoItems[0]) info, certList, err := parseSESSealInfo(infoItems)
if err != nil {
return nil, err
}
certList, err := parseSESCertList(infoItems[2], version)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -264,6 +302,7 @@ func parseSESSeal(raw asn1.RawValue) (*sesSeal, error) {
PicType: picType, PicType: picType,
PicData: picData, PicData: picData,
CertList: certList, CertList: certList,
Info: info,
}, nil }, nil
} }
@@ -291,7 +330,7 @@ func parseSESSealV1(raw asn1.RawValue, items []asn1.RawValue) (*sesSeal, error)
if err != nil { if err != nil {
return nil, err return nil, err
} }
certList, err := parseSESCertListV1(infoItems[2]) info, certList, err := parseSESSealInfo(infoItems)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -309,43 +348,92 @@ func parseSESSealV1(raw asn1.RawValue, items []asn1.RawValue) (*sesSeal, error)
PicType: picType, PicType: picType,
PicData: picData, PicData: picData,
CertList: certList, CertList: certList,
Info: info,
}, nil }, nil
} }
// parseSESHeaderVersion 解析印章头版本 // parseSESSealInfo 解析电子印章信息
// 入参: raw 印章信息 // 入参: items 印章信息ASN.1子元素
// 返回: int 版本号, error 错误信息 // 返回: SignatureSealInfo 印章信息, sesCertList 证书列表, error 错误信息
func parseSESHeaderVersion(raw asn1.RawValue) (int, error) { func parseSESSealInfo(items []asn1.RawValue) (SignatureSealInfo, sesCertList, error) {
items, ok := asn1Children(raw.Bytes) if len(items) < 4 {
if !ok || len(items) < 2 { return SignatureSealInfo{}, sesCertList{}, fmt.Errorf("invalid ses seal info")
return 0, fmt.Errorf("invalid ses header")
} }
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 解析印章证书列表 // parseSESCertList 解析印章证书列表
// 入参: raw 印章属性信息, version 印章版本 // 入参: property 印章属性, version 印章版本, certIndex 证书列表位置
// 返回: sesCertList 证书列表, error 错误信息 // 返回: sesCertList 证书列表, error 错误信息
func parseSESCertList(raw asn1.RawValue, version int) (sesCertList, error) { func parseSESCertList(property []asn1.RawValue, version, certIndex int) (sesCertList, error) {
items, ok := asn1Children(raw.Bytes) if version == 1 {
if !ok || len(items) < 3 { return parseSESCertListV1(property[certIndex])
return sesCertList{}, fmt.Errorf("invalid ses property")
} }
if version < 4 { if version < 4 {
return parseSESCertInfoList(items[2]) return parseSESCertInfoList(property[certIndex])
} }
if len(items) < 4 { listType, err := asn1Integer(property[certIndex-1])
return sesCertList{}, fmt.Errorf("invalid ses cert list")
}
listType, err := asn1Integer(items[2])
if err != nil { if err != nil {
return sesCertList{}, err return sesCertList{}, err
} }
switch listType { switch listType {
case 1: case 1:
return parseSESCertInfoList(items[3]) return parseSESCertInfoList(property[certIndex])
case 2: case 2:
return parseSESCertDigestList(items[3]) return parseSESCertDigestList(property[certIndex])
default: default:
return sesCertList{}, fmt.Errorf("unsupported ses cert list type") return sesCertList{}, fmt.Errorf("unsupported ses cert list type")
} }
@@ -396,12 +484,12 @@ func parseSESCertDigestList(raw asn1.RawValue) (sesCertList, error) {
list := sesCertList{Digests: make([]sesCertDigest, 0, len(items))} list := sesCertList{Digests: make([]sesCertDigest, 0, len(items))}
for _, item := range items { for _, item := range items {
fields, ok := asn1Children(item.Bytes) fields, ok := asn1Children(item.Bytes)
if !ok || len(fields) < 2 { if !ok || len(fields) != 2 {
return sesCertList{}, fmt.Errorf("invalid ses cert digest") return sesCertList{}, fmt.Errorf("invalid ses cert digest")
} }
method, err := parseGBTAlgorithm(fields[0]) method := strings.TrimSpace(asn1String(fields[0]))
if err != nil { if method == "" {
return sesCertList{}, err return sesCertList{}, fmt.Errorf("invalid ses cert digest")
} }
digest, err := asn1OctetString(fields[1]) digest, err := asn1OctetString(fields[1])
if err != nil { if err != nil {
+184 -21
View File
@@ -15,7 +15,6 @@
package ofdgo package ofdgo
import ( import (
"crypto/elliptic"
"encoding/asn1" "encoding/asn1"
"encoding/binary" "encoding/binary"
"math/big" "math/big"
@@ -31,17 +30,32 @@ type sm2PublicKey struct {
Y *big.Int Y *big.Int
} }
// sm2Curve SM2椭圆曲线
type sm2Curve struct {
P *big.Int
N *big.Int
B *big.Int
Gx *big.Int
Gy *big.Int
}
// sm2Point SM2雅可比坐标点
type sm2Point struct {
X *big.Int
Y *big.Int
Z *big.Int
}
// newSM2P256 创建SM2椭圆曲线 // newSM2P256 创建SM2椭圆曲线
// 返回: elliptic.Curve SM2椭圆曲线 // 返回: *sm2Curve SM2椭圆曲线
func newSM2P256() elliptic.Curve { func newSM2P256() *sm2Curve {
c := &elliptic.CurveParams{Name: "SM2-P-256"} return &sm2Curve{
c.P = sm2Big("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF") P: sm2Big("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF"),
c.N = sm2Big("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123") N: sm2Big("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123"),
c.B = sm2Big("28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93") B: sm2Big("28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93"),
c.Gx = sm2Big("32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7") Gx: sm2Big("32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7"),
c.Gy = sm2Big("BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0") Gy: sm2Big("BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0"),
c.BitSize = 256 }
return c
} }
// sm2Big 解析SM2大整数常量 // sm2Big 解析SM2大整数常量
@@ -85,11 +99,11 @@ func parseSM2Signature(sig []byte) (*big.Int, *big.Int, bool) {
// 入参: pub 公钥, userID 用户标识, msg 原文, r R值, s S值 // 入参: pub 公钥, userID 用户标识, msg 原文, r R值, s S值
// 返回: bool 是否验证通过 // 返回: bool 是否验证通过
func sm2Verify(pub sm2PublicKey, userID, msg []byte, r, s *big.Int) 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 { if r.Sign() <= 0 || s.Sign() <= 0 || r.Cmp(n) >= 0 || s.Cmp(n) >= 0 {
return false 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 return false
} }
e := new(big.Int).SetBytes(sm2MessageDigest(pub, userID, msg)) 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 { if t.Sign() == 0 {
return false return false
} }
x1, y1 := sm2P256.ScalarBaseMult(s.Bytes()) x, ok := sm2P256.combinedMult(pub.X, pub.Y, s, t)
x2, y2 := sm2P256.ScalarMult(pub.X, pub.Y, t.Bytes()) if !ok {
x, _ := sm2P256.Add(x1, y1, x2, y2)
if x == nil {
return false return false
} }
v := new(big.Int).Add(e, x) v := new(big.Int).Add(e, x)
@@ -132,9 +144,9 @@ func sm2ZA(pub sm2PublicKey, userID []byte) []byte {
h.Write(entl[:]) h.Write(entl[:])
h.Write(userID) h.Write(userID)
h.Write(sm2Fixed(sm2A())) h.Write(sm2Fixed(sm2A()))
h.Write(sm2Fixed(sm2P256.Params().B)) h.Write(sm2Fixed(sm2P256.B))
h.Write(sm2Fixed(sm2P256.Params().Gx)) h.Write(sm2Fixed(sm2P256.Gx))
h.Write(sm2Fixed(sm2P256.Params().Gy)) h.Write(sm2Fixed(sm2P256.Gy))
h.Write(sm2Fixed(pub.X)) h.Write(sm2Fixed(pub.X))
h.Write(sm2Fixed(pub.Y)) h.Write(sm2Fixed(pub.Y))
return h.Sum(nil) return h.Sum(nil)
@@ -143,7 +155,158 @@ func sm2ZA(pub sm2PublicKey, userID []byte) []byte {
// sm2A 获取SM2曲线A参数 // sm2A 获取SM2曲线A参数
// 返回: *big.Int 曲线A参数 // 返回: *big.Int 曲线A参数
func sm2A() *big.Int { 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固定长度字节 // sm2Fixed 转换为SM2固定长度字节
+305 -14
View File
@@ -18,6 +18,7 @@ import (
"bytes" "bytes"
"crypto" "crypto"
"crypto/ecdsa" "crypto/ecdsa"
"crypto/md5"
"crypto/rsa" "crypto/rsa"
"crypto/sha1" "crypto/sha1"
"crypto/sha256" "crypto/sha256"
@@ -37,6 +38,8 @@ import (
) )
// SignatureVerifyReport 签名验证报告 // SignatureVerifyReport 签名验证报告
// Valid表示签名完整性、签名时间语义及调用方指定的证书策略均通过
// SealCertTimeOK仅提供制章证书在签名时间的状态信息, 不参与Valid判断
type SignatureVerifyReport struct { type SignatureVerifyReport struct {
ID string ID string
BaseLoc string BaseLoc string
@@ -45,9 +48,11 @@ type SignatureVerifyReport struct {
Signer string Signer string
SignCert SignatureCertInfo SignCert SignatureCertInfo
SealCert SignatureCertInfo SealCert SignatureCertInfo
SealInfo SignatureSealInfo
SealType string SealType string
SignatureMethod string SignatureMethod string
SignatureDateTime string SignatureDateTime string
SignatureTime time.Time
DigestMethod string DigestMethod string
References []SignatureReferenceVerify References []SignatureReferenceVerify
Stamps []SignatureStamp Stamps []SignatureStamp
@@ -59,6 +64,12 @@ type SignatureVerifyReport struct {
SealOK bool SealOK bool
SealMatchOK bool SealMatchOK bool
CertOK bool CertOK bool
SignatureTimeChecked bool
SignatureTimeOK bool
SealCertTimeChecked bool
SealCertTimeOK bool
SealTimeChecked bool
SealTimeOK bool
CertTimeChecked bool CertTimeChecked bool
CertTimeOK bool CertTimeOK bool
CertTrustChecked bool CertTrustChecked bool
@@ -67,8 +78,21 @@ type SignatureVerifyReport struct {
Error string 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 签名证书信息 // SignatureCertInfo 签名证书信息
type SignatureCertInfo struct { type SignatureCertInfo struct {
Raw []byte
Subject string Subject string
CommonName string CommonName string
Organization string Organization string
@@ -78,6 +102,18 @@ type SignatureCertInfo struct {
NotAfter 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 签名保护文件验证结果 // SignatureReferenceVerify 签名保护文件验证结果
type SignatureReferenceVerify struct { type SignatureReferenceVerify struct {
FileRef string FileRef string
@@ -226,6 +262,9 @@ func (r *Reader) verifySignature(sigListPath string, sigRef Signature, options *
Type: sigRef.Type, Type: sigRef.Type,
SealMatchOK: true, SealMatchOK: true,
} }
if report.Type == "" {
report.Type = SignTypeSeal
}
sigData, err := r.readFileExact(sigPath) sigData, err := r.readFileExact(sigPath)
if err != nil { if err != nil {
report.Error = err.Error() report.Error = err.Error()
@@ -240,6 +279,9 @@ func (r *Reader) verifySignature(sigListPath string, sigRef Signature, options *
report.SignatureMethod = sigFile.SignedInfo.SignatureMethod report.SignatureMethod = sigFile.SignedInfo.SignatureMethod
report.SignatureDateTime = sigFile.SignedInfo.SignatureDateTime report.SignatureDateTime = sigFile.SignedInfo.SignatureDateTime
report.DigestMethod = sigFile.SignedInfo.References.CheckMethod report.DigestMethod = sigFile.SignedInfo.References.CheckMethod
if report.DigestMethod == "" {
report.DigestMethod = "MD5"
}
report.References = r.verifySignatureReferences(sigPath, sigFile.SignedInfo.References) report.References = r.verifySignatureReferences(sigPath, sigFile.SignedInfo.References)
report.Stamps = append(report.Stamps, sigFile.SignedInfo.StampAnnot...) report.Stamps = append(report.Stamps, sigFile.SignedInfo.StampAnnot...)
report.StampPositions, err = r.SignatureStampPositions(report.Stamps) report.StampPositions, err = r.SignatureStampPositions(report.Stamps)
@@ -253,7 +295,7 @@ func (r *Reader) verifySignature(sigListPath string, sigRef Signature, options *
report.Error = err.Error() report.Error = err.Error()
return report return report
} }
switch sigRef.Type { switch report.Type {
case SignTypeSign: case SignTypeSign:
result, err := verifyDigitalSignature(report.SignatureMethod, report.DigestMethod, signedValue, sigData, options) result, err := verifyDigitalSignature(report.SignatureMethod, report.DigestMethod, signedValue, sigData, options)
if err != nil { if err != nil {
@@ -266,19 +308,23 @@ func (r *Reader) verifySignature(sigListPath string, sigRef Signature, options *
report.CertOK = result.CertOK report.CertOK = result.CertOK
report.SignCert = result.CertInfo report.SignCert = result.CertInfo
report.Signer = result.CertInfo.CommonName report.Signer = result.CertInfo.CommonName
report.SignatureTime = parseSignatureDateTime(report.SignatureDateTime)
report.applySignatureTimePolicy()
report.applySignatureCertificatePolicy(options, result.SignerCerts, result.Certs) report.applySignatureCertificatePolicy(options, result.SignerCerts, result.Certs)
report.Valid = report.DigestOK && report.DataHashOK && report.SignedValueOK && report.CertOK && report.certificatePolicyOK() report.Valid = report.IntegrityValid() && report.certificatePolicyOK()
return report return report
case "", SignTypeSeal: case SignTypeSeal:
default: default:
report.Error = fmt.Sprintf("unsupported signature type: %s", sigRef.Type) report.Error = fmt.Sprintf("unsupported signature type: %s", report.Type)
return report return report
} }
sesResult, err := verifySESSignature(signedValue, sigData, options) sesResult, err := verifySESSignature(signedValue, sigData, options)
if sesResult != nil { if sesResult != nil {
report.SignCert = sesResult.SignCert report.SignCert = sesResult.SignCert
report.SealCert = sesResult.SealCert report.SealCert = sesResult.SealCert
report.SealInfo = sesResult.SealInfo
report.SealType = sesResult.SealType report.SealType = sesResult.SealType
report.SignatureTime = sesResult.SignatureTime
report.Signer = sesResult.SignCert.CommonName report.Signer = sesResult.SignCert.CommonName
} }
if err != nil { if err != nil {
@@ -289,6 +335,7 @@ func (r *Reader) verifySignature(sigListPath string, sigRef Signature, options *
report.SignedValueOK = sesResult.SignedOK report.SignedValueOK = sesResult.SignedOK
report.SealOK = sesResult.SealOK report.SealOK = sesResult.SealOK
report.CertOK = sesResult.CertOK report.CertOK = sesResult.CertOK
report.applySignatureTimePolicy()
report.applySignatureCertificatePolicy(options, [][]byte{sesResult.SignCertRaw, sesResult.SealCertRaw}, sesResult.Certs) report.applySignatureCertificatePolicy(options, [][]byte{sesResult.SignCertRaw, sesResult.SealCertRaw}, sesResult.Certs)
if sigFile.SignedInfo.Seal.BaseLoc != "" { if sigFile.SignedInfo.Seal.BaseLoc != "" {
sealPath := signatureRefPath(sigPath, sigFile.SignedInfo.Seal.BaseLoc) sealPath := signatureRefPath(sigPath, sigFile.SignedInfo.Seal.BaseLoc)
@@ -299,7 +346,7 @@ func (r *Reader) verifySignature(sigListPath string, sigRef Signature, options *
} }
report.SealMatchOK = bytes.Equal(sealData, sesResult.SealRaw) report.SealMatchOK = bytes.Equal(sealData, sesResult.SealRaw)
} }
report.Valid = report.DigestOK && report.DataHashOK && report.SignedValueOK && report.SealOK && report.SealMatchOK && report.CertOK && report.certificatePolicyOK() report.Valid = report.IntegrityValid() && report.certificatePolicyOK()
return report return report
} }
@@ -370,6 +417,10 @@ func (r *Reader) readFileExact(name string) ([]byte, error) {
// 入参: method 摘要算法, data 原文数据 // 入参: method 摘要算法, data 原文数据
// 返回: []byte 摘要值, error 错误信息 // 返回: []byte 摘要值, error 错误信息
func signatureDigest(method string, data []byte) ([]byte, error) { func signatureDigest(method string, data []byte) ([]byte, error) {
if strings.TrimSpace(method) == "" {
sum := md5.Sum(data)
return sum[:], nil
}
if isSM3DigestMethod(method) { if isSM3DigestMethod(method) {
return signSM3(data), nil return signSM3(data), nil
} }
@@ -384,6 +435,8 @@ func signatureDigest(method string, data []byte) ([]byte, error) {
// 返回: crypto.Hash 摘要算法, bool 是否支持 // 返回: crypto.Hash 摘要算法, bool 是否支持
func signatureDigestHash(method string) (crypto.Hash, bool) { func signatureDigestHash(method string) (crypto.Hash, bool) {
switch signatureMethodText(method) { switch signatureMethodText(method) {
case "1.2.840.113549.2.5", "MD5":
return crypto.MD5, true
case "1.3.14.3.2.26", "SHA1": case "1.3.14.3.2.26", "SHA1":
return crypto.SHA1, true return crypto.SHA1, true
case "2.16.840.1.101.3.4.2.4", "SHA224": case "2.16.840.1.101.3.4.2.4", "SHA224":
@@ -440,6 +493,9 @@ func signatureMethodHash(method, digestMethod string) (crypto.Hash, error) {
// 返回: []byte 摘要值 // 返回: []byte 摘要值
func signatureHashBytes(h crypto.Hash, data []byte) []byte { func signatureHashBytes(h crypto.Hash, data []byte) []byte {
switch h { switch h {
case crypto.MD5:
sum := md5.Sum(data)
return sum[:]
case crypto.SHA1: case crypto.SHA1:
sum := sha1.Sum(data) sum := sha1.Sum(data)
return sum[:] return sum[:]
@@ -594,12 +650,12 @@ func (report *SignatureVerifyReport) applySignatureCertificatePolicy(options *si
if len(options.TrustCerts) != 0 { if len(options.TrustCerts) != 0 {
report.CertTrustChecked = true report.CertTrustChecked = true
report.CertTrustOK = true report.CertTrustOK = true
pool := append([][]byte{}, options.SignCerts...) pool := append([][]byte{}, options.TrustCerts...)
pool = append(pool, options.SignCerts...)
pool = append(pool, extraCerts...) pool = append(pool, extraCerts...)
pool = append(pool, options.TrustCerts...)
pool = compactSignatureCerts(pool) pool = compactSignatureCerts(pool)
for _, cert := range certs { for _, cert := range certs {
if !signatureCertTrustedBy(cert, pool, options.TrustCerts, make(map[string]bool)) { if !signatureCertTrustedBy(cert, pool, options.TrustCerts, options.VerifyTime) {
report.CertTrustOK = false report.CertTrustOK = false
break break
} }
@@ -610,9 +666,38 @@ func (report *SignatureVerifyReport) applySignatureCertificatePolicy(options *si
} }
} }
// 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 判断证书策略是否通过 // certificatePolicyOK 判断证书策略是否通过
// 返回: bool 是否通过 // 返回: bool 是否通过
func (report SignatureVerifyReport) 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 { if report.CertTimeChecked && !report.CertTimeOK {
return false return false
} }
@@ -638,28 +723,82 @@ func signatureCertsValidAt(certs [][]byte, t time.Time) bool {
return true return true
} }
// signatureCertMaxChecks 最大证书签名验证次数
const signatureCertMaxChecks = 100
// signatureCertPathState 证书路径状态
type signatureCertPathState struct {
Visited map[string]bool
SignatureChecks int
}
// signatureCertTrustedBy 判断证书是否可链到信任证书 // signatureCertTrustedBy 判断证书是否可链到信任证书
// 入参: cert 证书, pool 证书池, trusts 信任证书, visited 已访问证书 // 入参: cert 证书, pool 证书池, trusts 信任证书, verifyTime 中间证书验证时间
// 返回: bool 是否受信任 // 返回: bool 是否受信任
func signatureCertTrustedBy(cert []byte, pool, trusts [][]byte, visited map[string]bool) 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 { if len(cert) == 0 {
return false return false
} }
trusted := false
for _, trust := range trusts { for _, trust := range trusts {
if bytes.Equal(cert, trust) { if bytes.Equal(cert, trust) {
trusted = true
break
}
}
if trusted && !target {
return true return true
} }
}
key := string(cert) key := string(cert)
if visited[key] { if state.Visited[key] {
return false return false
} }
visited[key] = true state.Visited[key] = true
defer delete(state.Visited, key)
c, err := parseSignatureCertificate(cert) c, err := parseSignatureCertificate(cert)
if err != nil { if err != nil {
return false 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 { for _, issuerCert := range pool {
if state.SignatureChecks >= signatureCertMaxChecks {
return false
}
if bytes.Equal(cert, issuerCert) { if bytes.Equal(cert, issuerCert) {
continue continue
} }
@@ -667,10 +806,11 @@ func signatureCertTrustedBy(cert []byte, pool, trusts [][]byte, visited map[stri
if err != nil || !bytes.Equal(c.Issuer, issuer.Subject) { if err != nil || !bytes.Equal(c.Issuer, issuer.Subject) {
continue continue
} }
state.SignatureChecks++
if ok, err := verifyCertificateSignature(c, issuerCert); err != nil || !ok { if ok, err := verifyCertificateSignature(c, issuerCert); err != nil || !ok {
continue continue
} }
if signatureCertTrustedBy(issuerCert, pool, trusts, visited) { if signatureCertPathTrustedBy(issuerCert, pool, trusts, verifyTime, state, nextCABelow, false) {
return true return true
} }
} }
@@ -722,6 +862,7 @@ func signatureCertInfo(data []byte) SignatureCertInfo {
subject := certificateNameValues(cert.SubjectValue) subject := certificateNameValues(cert.SubjectValue)
issuer := certificateNameValues(cert.IssuerValue) issuer := certificateNameValues(cert.IssuerValue)
info := SignatureCertInfo{ info := SignatureCertInfo{
Raw: append([]byte(nil), data...),
Subject: certificateNameString(subject), Subject: certificateNameString(subject),
CommonName: certificateNameFirst(subject, "2.5.4.3"), CommonName: certificateNameFirst(subject, "2.5.4.3"),
Organization: certificateNameFirst(subject, "2.5.4.10"), Organization: certificateNameFirst(subject, "2.5.4.10"),
@@ -735,6 +876,36 @@ func signatureCertInfo(data []byte) SignatureCertInfo {
return info 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 签名证书结构 // signatureCertificate 签名证书结构
type signatureCertificate struct { type signatureCertificate struct {
Raw []byte Raw []byte
@@ -749,6 +920,7 @@ type signatureCertificate struct {
NotAfter time.Time NotAfter time.Time
SignatureAlg string SignatureAlg string
Signature []byte Signature []byte
signatureCertificateExtensions
} }
// parseSignatureCertificate 解析签名证书 // parseSignatureCertificate 解析签名证书
@@ -775,6 +947,9 @@ func parseSignatureCertificate(data []byte) (signatureCertificate, error) {
if len(items) <= idx+5 { if len(items) <= idx+5 {
return signatureCertificate{}, fmt.Errorf("invalid certificate") return signatureCertificate{}, fmt.Errorf("invalid certificate")
} }
if !bytes.Equal(items[idx+1].FullBytes, cert.SignatureAlgorithm.FullBytes) {
return signatureCertificate{}, fmt.Errorf("certificate signature algorithm mismatch")
}
serial, err := asn1IntegerBig(items[idx]) serial, err := asn1IntegerBig(items[idx])
if err != nil { if err != nil {
return signatureCertificate{}, err return signatureCertificate{}, err
@@ -783,6 +958,10 @@ func parseSignatureCertificate(data []byte) (signatureCertificate, error) {
if err != nil { if err != nil {
return signatureCertificate{}, err return signatureCertificate{}, err
} }
extensions, err := parseSignatureCertificateExtensions(items[idx+6:])
if err != nil {
return signatureCertificate{}, err
}
alg, err := parseGBTAlgorithm(cert.SignatureAlgorithm) alg, err := parseGBTAlgorithm(cert.SignatureAlgorithm)
if err != nil { if err != nil {
return signatureCertificate{}, err return signatureCertificate{}, err
@@ -803,9 +982,121 @@ func parseSignatureCertificate(data []byte) (signatureCertificate, error) {
NotAfter: validity[1], NotAfter: validity[1],
SignatureAlg: alg, SignatureAlg: alg,
Signature: append([]byte(nil), cert.SignatureValue.Bytes...), Signature: append([]byte(nil), cert.SignatureValue.Bytes...),
signatureCertificateExtensions: extensions,
}, nil }, 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 解析证书有效期 // parseCertificateValidity 解析证书有效期
// 入参: raw 证书有效期ASN.1值 // 入参: raw 证书有效期ASN.1值
// 返回: [2]time.Time 生效和失效时间, error 错误信息 // 返回: [2]time.Time 生效和失效时间, error 错误信息
+109 -11
View File
@@ -15,9 +15,12 @@
package ofdgo package ofdgo
import ( import (
"math"
"strconv"
"strings" "strings"
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
canvastext "github.com/tdewolff/canvas/text"
"github.com/tdewolff/font" "github.com/tdewolff/font"
) )
@@ -27,6 +30,26 @@ type textGlyph struct {
GlyphID int GlyphID int
} }
// textGlyphPathCacheKey 字形路径缓存键
type textGlyphPathCacheKey struct {
font *canvas.Font
size float64
fauxBold float64
fauxItalic float64
xOffset int32
yOffset int32
language string
script canvastext.Script
direction canvastext.Direction
glyph textGlyph
}
// textGlyphPathCacheValue 字形路径缓存值
type textGlyphPathCacheValue struct {
path *canvas.Path
width float64
}
// textGlyphTransform 字符到字形变换 // textGlyphTransform 字符到字形变换
type textGlyphTransform struct { type textGlyphTransform struct {
CodeCount int CodeCount int
@@ -53,7 +76,7 @@ func (r *Renderer) textObjectGlyphTransforms(fontID string, text TextObject) map
if fontID == "" || len(text.CGTransform) == 0 { if fontID == "" || len(text.CGTransform) == 0 {
return nil return nil
} }
result := make(map[int]textGlyphTransform) result := make(map[int]textGlyphTransform, len(text.CGTransform))
for _, transform := range text.CGTransform { for _, transform := range text.CGTransform {
ids := parseInts(transform.Glyphs) ids := parseInts(transform.Glyphs)
glyphCount := len(ids) glyphCount := len(ids)
@@ -67,9 +90,9 @@ func (r *Renderer) textObjectGlyphTransforms(fontID string, text TextObject) map
if codeCount <= 0 { if codeCount <= 0 {
codeCount = 1 codeCount = 1
} }
glyphs := make([]textGlyph, 0, glyphCount) glyphs := make([]textGlyph, glyphCount)
for i := 0; i < glyphCount; i++ { for i := range glyphs {
glyphs = append(glyphs, r.textGlyphFromID(fontID, ids[i])) glyphs[i] = r.textGlyphFromID(fontID, ids[i])
} }
result[transform.CodePosition] = textGlyphTransform{ result[transform.CodePosition] = textGlyphTransform{
CodeCount: codeCount, CodeCount: codeCount,
@@ -106,9 +129,9 @@ func textCodeGlyphs(runes []rune, transforms map[int]textGlyphTransform, codeOff
// 入参: runes 文本字符 // 入参: runes 文本字符
// 返回: []textGlyph 绘制字形列表 // 返回: []textGlyph 绘制字形列表
func textRuneGlyphs(runes []rune) []textGlyph { func textRuneGlyphs(runes []rune) []textGlyph {
glyphs := make([]textGlyph, 0, len(runes)) glyphs := make([]textGlyph, len(runes))
for _, r := range runes { for i, r := range runes {
glyphs = append(glyphs, textGlyph{Text: string(r), GlyphID: -1}) glyphs[i] = textGlyph{Text: string(r), GlyphID: -1}
} }
return glyphs return glyphs
} }
@@ -159,6 +182,48 @@ func textGlyphPath(face *canvas.FontFace, glyph textGlyph) (*canvas.Path, float6
return p, face.MmPerEm * float64(face.Font.GlyphAdvance(glyphID)) 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对应的包装字体字符 // fontGlyphRune 获取字形ID对应的包装字体字符
// 入参: fontID 字体ID, glyphID 字形ID或CID // 入参: fontID 字体ID, glyphID 字形ID或CID
// 返回: rune 包装字体字符, bool 是否存在 // 返回: rune 包装字体字符, bool 是否存在
@@ -184,6 +249,39 @@ func (r *Renderer) fontGlyphRune(fontID string, glyphID int) (rune, bool) {
return 0, false 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 获取显式字形推进宽度 // textGlyphAdvanceLimit 获取显式字形推进宽度
// 入参: dxs X方向偏移, dys Y方向偏移, xs X坐标列表, index 字形索引, count 字形数量, currentX 当前X坐标 // 入参: dxs X方向偏移, dys Y方向偏移, xs X坐标列表, index 字形索引, count 字形数量, currentX 当前X坐标
// 返回: float64 推进宽度 // 返回: float64 推进宽度
@@ -203,13 +301,13 @@ func textGlyphAdvanceLimit(dxs, dys, xs []float64, index int, count int, current
} }
// textCodePositioned 判断文本编码是否带显式定位 // textCodePositioned 判断文本编码是否带显式定位
// 入参: textCode 文本编码 // 入参: textCode 文本编码, xs X坐标列表, ys Y坐标列表
// 返回: bool 是否带显式定位 // 返回: bool 是否带显式定位
func textCodePositioned(textCode TextCode) bool { func textCodePositioned(textCode TextCode, xs, ys []float64) bool {
return strings.TrimSpace(textCode.DeltaX) != "" || return strings.TrimSpace(textCode.DeltaX) != "" ||
strings.TrimSpace(textCode.DeltaY) != "" || strings.TrimSpace(textCode.DeltaY) != "" ||
len(parseFloats(textCode.X)) > 1 || len(xs) > 1 ||
len(parseFloats(textCode.Y)) > 1 len(ys) > 1
} }
// textDelta 获取文本偏移量 // textDelta 获取文本偏移量