Compare commits

...
28 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
34 changed files with 2730 additions and 940 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
} }
+4
View File
@@ -401,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;
+36 -25
View File
@@ -1003,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) {
@@ -1047,7 +1047,7 @@ async function exportCurrentPage() {
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) {
@@ -1364,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) {
@@ -1408,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})`;
} }
} }
@@ -1668,7 +1673,7 @@ function renderSignatures() {
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
for (const signature of signatures) { for (const signature of signatures) {
const row = document.createElement("div"); const row = document.createElement("div");
row.className = `signature-row ${signature.integrityValid ? "valid" : "invalid"}`; row.className = `signature-row ${signature.valid ? "valid" : "invalid"}`;
const head = document.createElement("div"); const head = document.createElement("div");
head.className = "signature-head"; head.className = "signature-head";
@@ -1677,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.integrityValid ? "完整" : "异常", signature.integrityValid ? "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));
@@ -1694,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);
@@ -1713,7 +1726,7 @@ function signatureSummary(signatures) {
if (!signatures.length) { if (!signatures.length) {
return "0"; return "0";
} }
const invalid = signatures.filter((signature) => !signature.integrityValid).length; const invalid = signatures.filter((signature) => !signature.valid).length;
if (invalid) { if (invalid) {
return `${signatures.length} · 异常 ${invalid}`; return `${signatures.length} · 异常 ${invalid}`;
} }
@@ -1972,6 +1985,13 @@ function fitWidth(updateStatus = true) {
if (!page) { if (!page) {
return; return;
} }
setScale(fitWidthScale(page), updateStatus, "width");
if (updateStatus) {
scrollToPage(state.pageIndex);
}
}
function fitWidthScale(page) {
const space = pageSpace(); const space = pageSpace();
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);
@@ -1979,10 +1999,7 @@ function fitWidth(updateStatus = true) {
if (!viewerHasVerticalScrollbar() && height * (available / width) > Math.max(1, el.viewerPanel.clientHeight - space * 2)) { if (!viewerHasVerticalScrollbar() && height * (available / width) > Math.max(1, el.viewerPanel.clientHeight - space * 2)) {
available = Math.max(1, available - scrollbarWidth()); available = Math.max(1, available - scrollbarWidth());
} }
setScale(available / width, updateStatus, "width"); return Math.min(4, Math.max(0.2, available / width));
if (updateStatus) {
scrollToPage(state.pageIndex);
}
} }
function fitHeight(updateStatus = true) { function fitHeight(updateStatus = true) {
@@ -2015,14 +2032,15 @@ function setScale(nextScale, updateStatus = true, fitMode = "free") {
const anchor = updateStatus && fitMode === "free" ? scaleAnchor() : null; 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 (scaleChanged) { if (layoutChanged) {
restoreScaleAnchor(anchor); restoreScaleAnchor(anchor);
} }
el.zoomLabel.textContent = `${Math.round(state.scale * 100)}%`; el.zoomLabel.textContent = `${Math.round(state.scale * 100)}%`;
@@ -2092,6 +2110,7 @@ function scrollbarWidth() {
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;
@@ -2099,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`);
@@ -2159,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 调用失败");
} }
@@ -2215,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");
+13 -12
View File
@@ -1,13 +1,13 @@
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-20260708151538-b3beae08c578 github.com/tdewolff/canvas v0.0.0-20260822211237-56e622de465a
github.com/tdewolff/font v0.0.0-20260527091451-1663e68cb8a4 github.com/tdewolff/font v0.0.0-20260822205238-d0d2f004b117
github.com/xiaoqidun/jbig2 v0.0.0-20260710074125-96fddd152f49 github.com/xiaoqidun/jbig2 v0.0.0-20260823111534-2e67bcec7059
golang.org/x/image v0.44.0 golang.org/x/image v0.45.0
golang.org/x/text v0.40.0 golang.org/x/text v0.41.0
) )
require ( require (
@@ -19,7 +19,7 @@ require (
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
@@ -28,13 +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/minify/v2 v2.24.13 // indirect github.com/tdewolff/minify/v2 v2.24.17 // indirect
github.com/tdewolff/parse/v2 v2.8.13 // indirect github.com/tdewolff/parse/v2 v2.8.16 // 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.55.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
) )
+32 -32
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.3.0 h1:LKTaDHFbEC2PH1sh0sYv6PZ1pzs/g2aoeV1HItWj/bg= codeberg.org/go-latex/latex v0.3.0 h1:LKTaDHFbEC2PH1sh0sYv6PZ1pzs/g2aoeV1HItWj/bg=
codeberg.org/go-latex/latex v0.3.0/go.mod h1:8ETijTpK2bFtwRAXLXe1RZJrYxnc5pibibZfQBj+Lk4= codeberg.org/go-latex/latex v0.3.0/go.mod h1:8ETijTpK2bFtwRAXLXe1RZJrYxnc5pibibZfQBj+Lk4=
codeberg.org/go-pdf/fpdf v0.12.0 h1:g8E/1VqGqB2lZUUaqQrrTnA0IEJLPTTX1DZ0qS/ZmhU= codeberg.org/go-pdf/fpdf v0.12.0 h1:g8E/1VqGqB2lZUUaqQrrTnA0IEJLPTTX1DZ0qS/ZmhU=
codeberg.org/go-pdf/fpdf v0.12.0/go.mod h1:WJNJ2bvCj81rZBdhOf7lKOGoSl+OKMXcIcXqDcP8r5Y= 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,27 +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-20260708151538-b3beae08c578 h1:OKVthD6KmBTDWsxnPkmDLW35yp+lvoqWsMeteXOZW+4= github.com/tdewolff/canvas v0.0.0-20260822211237-56e622de465a h1:riaFBiYaIv1gM31xdcU0Yo7bdp3WebF4dggO/JK0g/8=
github.com/tdewolff/canvas v0.0.0-20260708151538-b3beae08c578/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-20260527091451-1663e68cb8a4 h1:QmSG/RtOlLPJDLJ24XHSDEu4iSgcG+s9sdRg7wTdfpk= github.com/tdewolff/font v0.0.0-20260822205238-d0d2f004b117 h1:5vSJjsbV9AH0U+Ex0uej8ROrpOBlOVBrw0mGlVef7oo=
github.com/tdewolff/font v0.0.0-20260527091451-1663e68cb8a4/go.mod h1:tQzLTuJVLs4Pp6Vxdn3PJ7sAe4gIuph3SBm/BmdlmR0= 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.13 h1:si/8rLw5BZZTWCCiMm9A3f6x+RmqYfrkEeXCgpX5ick= github.com/tdewolff/parse/v2 v2.8.16 h1:bLk5svUOQRkW/Y2SJ+DeENSIkZBcTIkq+Atyv5D8feI=
github.com/tdewolff/parse/v2 v2.8.13/go.mod h1:XdsoSFThlVIRIajAuqz1evNY7bagZS8LBOPA3aVopwQ= github.com/tdewolff/parse/v2 v2.8.16/go.mod h1:XdsoSFThlVIRIajAuqz1evNY7bagZS8LBOPA3aVopwQ=
github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk= github.com/tdewolff/test v1.0.12 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-20260710074125-96fddd152f49 h1:h/1lOG9/75eK4atpQFcm9oGXmxekMVfsPJowtYLXu5w= github.com/xiaoqidun/jbig2 v0.0.0-20260823111534-2e67bcec7059 h1:/JXxWbNTePSVfshS7BVRm75Kh1/5rixvLpgTRw7MMv8=
github.com/xiaoqidun/jbig2 v0.0.0-20260710074125-96fddd152f49/go.mod h1:4ieZvd1EYR3zUzaAT4H8vLDb6XO+yjnxuh33So/5iC4= 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=
@@ -87,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.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I= golang.org/x/image v0.45.0 h1:FMb1nTbH5H9vF55SriQHgFw5GnNL9Jg6L25BwXKzhB0=
golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY= 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=
@@ -103,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.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= 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=
@@ -122,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=
@@ -140,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.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= 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=
@@ -153,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=
+40 -8
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 获取导出格式
@@ -137,14 +150,14 @@ func exportPage(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),
"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 获取系统字体名称
+28 -8
View File
@@ -43,9 +43,12 @@ type FontInfo = ofdgo.FontInfo
type SignatureInfo struct { type SignatureInfo struct {
ID string `json:"id"` ID string `json:"id"`
Type string `json:"type"` Type string `json:"type"`
IntegrityValid bool `json:"integrityValid"` 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
} }
@@ -346,9 +357,12 @@ func signatureInfo(report ofdgo.SignatureVerifyReport) SignatureInfo {
return SignatureInfo{ return SignatureInfo{
ID: report.ID, ID: report.ID,
Type: string(report.Type), Type: string(report.Type),
IntegrityValid: report.IntegrityValid(), 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,
@@ -358,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,
}
} }
+33 -16
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,11 +153,10 @@ 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 {
files := fontFileCandidates(fsys.names, path.Base) matches := make([]fontFileMatch, 0, len(fsys.candidates))
matches := make([]fontFileMatch, 0, len(files)) seen := make(map[string]int, len(fsys.candidates))
seen := make(map[string]int, len(files))
for _, matcher := range newFontPatternMatchers(patterns) { for _, matcher := range newFontPatternMatchers(patterns) {
for _, file := range files { for _, file := range fsys.candidates {
rank := matcher.rankCandidate(file) rank := matcher.rankCandidate(file)
appendFontFileMatch(&matches, seen, matcher, file, rank, bold, italic) appendFontFileMatch(&matches, seen, matcher, file, rank, bold, italic)
} }
@@ -235,6 +241,7 @@ type fontPatternMatcher struct {
lowerPattern string lowerPattern string
stem string stem string
aliases []string aliases []string
priority int
} }
// newFontPatternMatcher 创建字体文件匹配器 // newFontPatternMatcher 创建字体文件匹配器
@@ -262,8 +269,10 @@ func newFontPatternMatcher(pattern string) fontPatternMatcher {
// 返回: []fontPatternMatcher 字体文件匹配器列表 // 返回: []fontPatternMatcher 字体文件匹配器列表
func newFontPatternMatchers(patterns []string) []fontPatternMatcher { func newFontPatternMatchers(patterns []string) []fontPatternMatcher {
matchers := make([]fontPatternMatcher, 0, len(patterns)) matchers := make([]fontPatternMatcher, 0, len(patterns))
for _, pattern := range patterns { for priority, pattern := range patterns {
matchers = append(matchers, newFontPatternMatcher(pattern)) matcher := newFontPatternMatcher(pattern)
matcher.priority = priority
matchers = append(matchers, matcher)
} }
return matchers return matchers
} }
@@ -342,6 +351,7 @@ func (m fontPatternMatcher) styleSuffixNormalized(name string) string {
type fontFileMatch struct { type fontFileMatch struct {
name string name string
priority int
rank int rank int
styleRank int styleRank int
sortName string sortName string
@@ -355,6 +365,7 @@ func appendFontFileMatch(matches *[]fontFileMatch, seen map[string]int, matcher
} }
next := fontFileMatch{ next := fontFileMatch{
name: file.name, name: file.name,
priority: matcher.priority,
rank: rank, rank: rank,
styleRank: fontFileStyleRank(matcher.styleSuffixNormalized(file.normalized), bold, italic), styleRank: fontFileStyleRank(matcher.styleSuffixNormalized(file.normalized), bold, italic),
sortName: file.lowerBase, sortName: file.lowerBase,
@@ -381,6 +392,9 @@ func sortFontFileMatches(matches []fontFileMatch) {
// 入参: left 左侧匹配结果, right 右侧匹配结果 // 入参: left 左侧匹配结果, right 右侧匹配结果
// 返回: bool 左侧是否优先 // 返回: bool 左侧是否优先
func fontFileMatchLess(left, right fontFileMatch) 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
} }
@@ -513,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) {
+47 -76
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,74 +184,6 @@ 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 字体文件列表
@@ -240,19 +203,27 @@ func fontFSMatchesStyle(fsys fs.FS, patterns []string, bold, italic bool) []stri
} }
// 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)
} }
} }
+182 -84
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,86 +105,157 @@ 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
if of.Italic { return loaded
fontStyle |= canvas.FontItalic
}
boldStyle := fontStyle&canvas.FontBold != 0
italicStyle := fontStyle&canvas.FontItalic != 0
patterns := fontFilePatterns(of.FontName, of.FamilyName)
for _, dir := range r.fontDirs {
for _, m := range r.matchFontFiles(dir, patterns, boldStyle, italicStyle) {
if err := ff.LoadFontFile(m, fontStyle); err == nil {
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
}
}
}
}
if !canLoadSystemFonts() {
for _, fsys := range r.fontFS {
if matches, err := fs.Glob(fsys, "*"); err == nil {
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 {
if name == "" {
continue
}
for _, targetName := range fontSystemNames(name) {
for _, m := range r.matchFontFiles(systemFontDir(), fontFilePatterns(targetName), boldStyle, italicStyle) {
if err := ff.LoadFontFile(m, fontStyle); err == nil {
r.FontMap[fontID] = ff
return ff
}
}
if err := ff.LoadSystemFont(targetName, fontStyle); err == nil {
r.FontMap[fontID] = ff
return ff
}
}
for _, m := range r.matchFontFiles(systemFontDir(), fontFilePatterns(name), boldStyle, italicStyle) {
if err := ff.LoadFontFile(m, fontStyle); err == nil {
r.FontMap[fontID] = ff
return ff
}
} }
} }
r.FontMap[fontID] = defaultFont r.FontMap[fontID] = defaultFont
return defaultFont return defaultFont
} }
// canvasFontStyle 获取Canvas字体样式
// 入参: font OFD字体定义
// 返回: canvas.FontStyle Canvas字体样式
func canvasFontStyle(font *Font) canvas.FontStyle {
var style canvas.FontStyle
if font.Bold {
style |= canvas.FontBold
}
if font.Italic {
style |= canvas.FontItalic
}
return style
}
// fontSources 获取字体来源列表
// 入参: fontID 字体ID, font OFD字体定义, style Canvas字体样式
// 返回: []fontSource 字体来源列表
func (r *Renderer) fontSources(fontID string, font *Font, style canvas.FontStyle) []fontSource {
if sources, ok := r.fontSourceCache[fontID]; ok {
return sources
}
bold := style&canvas.FontBold != 0
italic := style&canvas.FontItalic != 0
patterns := fontFilePatterns(font.FontName, font.FamilyName)
sources := make([]fontSource, 0)
seen := make(map[fontSourceKey]bool)
for _, dir := range r.fontDirs {
for _, name := range r.matchFontFiles(dir, patterns, bold, italic) {
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFile, name: name, exact: true})
}
}
for index, fsys := range r.fontFS {
for _, name := range fontFSMatchesStyle(fsys, patterns, bold, italic) {
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: name, exact: true})
}
}
if !canLoadSystemFonts() {
for index, fsys := range r.fontFS {
names, _ := fs.Glob(fsys, "*")
for _, name := range names {
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: name})
}
}
r.fontSourceCache[fontID] = sources
return sources
}
systemDirs := systemFontDirs()
for _, name := range []string{font.FamilyName, font.FontName} {
for _, systemName := range fontSystemNames(name) {
systemPatterns := fontFilePatterns(systemName)
for _, dir := range systemDirs {
for _, match := range r.matchFontFiles(dir, systemPatterns, bold, italic) {
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFile, name: match, exact: true})
}
}
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceSystem, name: systemName, exact: true})
}
}
r.fontSourceCache[fontID] = sources
return sources
}
// appendFontSource 追加字体来源
// 入参: sources 字体来源列表, seen 去重映射, source 字体来源
// 返回: []fontSource 字体来源列表
func appendFontSource(sources []fontSource, seen map[fontSourceKey]bool, source fontSource) []fontSource {
if source.name == "" {
return sources
}
key := fontSourceKey{kind: source.kind, index: source.index, name: source.name}
if seen[key] {
return sources
}
seen[key] = true
return append(sources, source)
}
// fontSourceMatch 获取匹配的字体来源
// 入参: fontID 字体ID, font OFD字体定义
// 返回: fontSource 字体来源, bool 是否匹配
func (r *Renderer) fontSourceMatch(fontID string, font *Font) (fontSource, bool) {
if source, ok := r.fontSourceUsed[fontID]; ok {
return source, true
}
style := canvasFontStyle(font)
for _, source := range r.fontSources(fontID, font, style) {
key := fontCacheKey{fontSourceKey: fontSourceKey{kind: source.kind, index: source.index, name: source.name}, style: style}
if cached, ok := r.fontCache[key]; ok {
if cached != nil {
return source, true
}
continue
}
family := canvas.NewFontFamily(font.FontName)
if r.loadFontSource(family, source, style) != nil {
return source, true
}
}
return fontSource{}, false
}
// loadFontSource 加载字体来源
// 入参: family 字体族, source 字体来源, style Canvas字体样式
// 返回: *canvas.FontFamily 字体族
func (r *Renderer) loadFontSource(family *canvas.FontFamily, source fontSource, style canvas.FontStyle) *canvas.FontFamily {
key := fontCacheKey{fontSourceKey: fontSourceKey{kind: source.kind, index: source.index, name: source.name}, style: style}
if cached, ok := r.fontCache[key]; ok {
return cached
}
var err error
switch source.kind {
case fontSourceFile:
err = family.LoadFontFile(source.name, style)
case fontSourceFS:
var data []byte
data, err = fs.ReadFile(r.fontFS[source.index], source.name)
if err == nil {
err = family.LoadFont(data, 0, style)
}
case fontSourceSystem:
err = family.LoadSystemFont(source.name, style)
}
if err != nil {
r.fontCache[key] = nil
return nil
}
r.fontCache[key] = family
return family
}
// matchFontFiles 查找字体文件 // 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 {
candidates, ok := r.fontDirCandidates[dir]
if !ok {
files, _ := filepath.Glob(filepath.Join(dir, "*")) files, _ := filepath.Glob(filepath.Join(dir, "*"))
candidates := fontFileCandidates(files, filepath.Base) candidates = fontFileCandidates(files, filepath.Base)
r.fontDirCandidates[dir] = candidates
}
matches := make([]fontFileMatch, 0, len(candidates)) matches := make([]fontFileMatch, 0, len(candidates))
index := make(map[string]int, len(candidates)) index := make(map[string]int, len(candidates))
for _, matcher := range newFontPatternMatchers(patterns) { for _, matcher := range newFontPatternMatchers(patterns) {
@@ -173,15 +268,18 @@ func (r *Renderer) matchFontFiles(dir string, patterns []string, bold, italic bo
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仿射变换矩阵
+27
View File
@@ -20,15 +20,28 @@ import (
"fmt" "fmt"
"image" "image"
"image/color" "image/color"
_ "image/gif"
canvasimage "github.com/tdewolff/canvas/image"
_ "github.com/xiaoqidun/jbig2" _ "github.com/xiaoqidun/jbig2"
_ "golang.org/x/image/bmp" _ "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
@@ -39,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,86 +96,28 @@ 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 解析复合图元并保留对象顺序
// 入参: d XML解码器, start 起始节点 // 入参: d XML解码器, start 起始节点
@@ -128,265 +140,102 @@ 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 解析图案单元内容并保留对象顺序
// 入参: d XML解码器, start 起始节点 // 入参: d XML解码器, start 起始节点
// 返回: 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属性值
// 入参: start 起始节点, name 属性名 // 入参: start 起始节点, name 属性名
+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"`
} }
+43 -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)
@@ -235,24 +235,44 @@ func patternColor(fillColor *FillColor) color.Color {
return parseColorWithAlpha(fillColor.Value, fillColor.Alpha) return parseColorWithAlpha(fillColor.Value, fillColor.Alpha)
} }
// parseAxialShdColor 解析轴向渐变颜色 // parseShdColor 解析渐变颜色
// 入参: axialShd 轴向渐变节点, alpha 透明度 // 入参: 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 渐变对象
@@ -265,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}
@@ -284,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 解析径向渐变
@@ -317,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
} }
+23 -3
View File
@@ -33,8 +33,15 @@ type Renderer struct {
FontMap map[string]*canvas.FontFamily FontMap map[string]*canvas.FontFamily
FontGIDMap map[string]map[uint16]rune FontGIDMap map[string]map[uint16]rune
FontCIDMap map[string]map[uint16]rune FontCIDMap map[string]map[uint16]rune
fontCache map[fontCacheKey]*canvas.FontFamily
fontSourceCache map[string][]fontSource
fontSourceUsed map[string]fontSource
fontDirCandidates map[string][]fontFileCandidate
textGlyphPathCache map[textGlyphPathCacheKey]textGlyphPathCacheValue
templatePageCache map[string]*PageContent
fontDirs []string fontDirs []string
fontFS []fs.FS fontFS []fs.FS
decodeImages bool
} }
// RendererOption 渲染器配置选项 // RendererOption 渲染器配置选项
@@ -44,6 +51,15 @@ type RendererOption func(*Renderer)
// 入参: page 页面内容 // 入参: page 页面内容
// 返回: *canvas.Canvas 画布实例, error 错误信息 // 返回: *canvas.Canvas 画布实例, error 错误信息
func (r *Renderer) RenderPage(page *PageContent) (*canvas.Canvas, error) { func (r *Renderer) RenderPage(page *PageContent) (*canvas.Canvas, error) {
renderer := *r
renderer.decodeImages = true
return renderer.renderPage(page)
}
// renderPage 渲染特定页面内容
// 入参: page 页面内容
// 返回: *canvas.Canvas 画布实例, error 错误信息
func (r *Renderer) renderPage(page *PageContent) (*canvas.Canvas, error) {
box, err := r.GetPageBox(page) box, err := r.GetPageBox(page)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -51,7 +67,7 @@ func (r *Renderer) RenderPage(page *PageContent) (*canvas.Canvas, error) {
width, height := box.W, box.H width, height := box.W, box.H
c := canvas.New(width, height) c := canvas.New(width, height)
ctx := canvas.NewContext(c) ctx := canvas.NewContext(c)
if err := r.RenderPageToContext(ctx, page); err != nil { if err := r.renderPageToContext(ctx, page, true); err != nil {
return nil, err return nil, err
} }
return c, nil return c, nil
@@ -81,7 +97,9 @@ func (r *Renderer) GetPageBox(page *PageContent) (Box, error) {
// 入参: ctx 画布上下文, page 页面内容 // 入参: ctx 画布上下文, page 页面内容
// 返回: error 错误信息 // 返回: error 错误信息
func (r *Renderer) RenderPageToContext(ctx *canvas.Context, page *PageContent) error { func (r *Renderer) RenderPageToContext(ctx *canvas.Context, page *PageContent) error {
return r.renderPageToContext(ctx, page, true) renderer := *r
renderer.decodeImages = true
return renderer.renderPageToContext(ctx, page, true)
} }
// renderPageToContext 渲染页面到指定上下文 // renderPageToContext 渲染页面到指定上下文
@@ -165,5 +183,7 @@ func (r *Renderer) childRenderer(reader *Reader) *Renderer {
if len(r.fontFS) > 0 { if len(r.fontFS) > 0 {
opts = append(opts, WithFontFS(r.fontFS...)) opts = append(opts, WithFontFS(r.fontFS...))
} }
return NewRenderer(reader, opts...) renderer := NewRenderer(reader, opts...)
renderer.decodeImages = r.decodeImages
return renderer
} }
+207 -16
View File
@@ -15,15 +15,20 @@
package ofdgo package ofdgo
import ( import (
"bufio"
"image" "image"
"image/color" "image/color"
"io"
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
canvasimage "github.com/tdewolff/canvas/image"
"github.com/tdewolff/canvas/renderers/rasterizer"
"golang.org/x/image/draw"
) )
// renderImage 渲染图片 // renderImage 渲染图片
// 入参: ctx 画布上下文, obj 图片对象, pageH 页面高度, parentCTM 父级CTM, boundaryInCTM 边界是否参与父级CTM // 入参: ctx 画布上下文, obj 图片对象, pageH 页面高度, parentCTM 父级CTM, boundaryInCTM 边界是否参与父级CTM, parentClip 父级裁剪路径
func (r *Renderer) renderImage(ctx *canvas.Context, obj ImageObject, pageH float64, parentCTM *Matrix, boundaryInCTM bool) { func (r *Renderer) renderImage(ctx *canvas.Context, obj ImageObject, pageH float64, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) {
if obj.Visible != nil && !*obj.Visible { if obj.Visible != nil && !*obj.Visible {
return return
} }
@@ -31,27 +36,30 @@ func (r *Renderer) renderImage(ctx *canvas.Context, obj ImageObject, pageH float
if !ok { if !ok {
return return
} }
imgData, err := r.Reader.ResData(resPath) img, err := r.decodeImageResource(resPath)
if err != nil {
return
}
img, _, err := decodeImageData(imgData)
if err != nil { if err != nil {
return return
} }
box, _ := ParseBox(obj.Boundary) 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() imgBounds := img.Bounds()
imgW, imgH := float64(imgBounds.Dx()), float64(imgBounds.Dy()) imgW, imgH := float64(imgBounds.Dx()), float64(imgBounds.Dy())
if imgW <= 0 || imgH <= 0 { if imgW <= 0 || imgH <= 0 {
return return
} }
img = imageWithAlpha(img, obj.Alpha)
pad := 0
img, pad = imageWithTransparentEdge(img)
ctm := NewMatrix(obj.CTM) ctm := NewMatrix(obj.CTM)
if obj.CTM == "" { if obj.CTM == "" {
ctm = Matrix{a: box.W, d: box.H} ctm = Matrix{a: box.W, d: box.H}
} }
objectCTM := ctm
if parentCTM != nil {
objectCTM = parentCTM.Multiply(ctm)
}
var m canvas.Matrix var m canvas.Matrix
if boundaryInCTM && parentCTM != nil { if boundaryInCTM && parentCTM != nil {
x0 := box.X + ctm.c + ctm.e x0 := box.X + ctm.c + ctm.e
@@ -69,6 +77,9 @@ func (r *Renderer) renderImage(ctx *canvas.Context, obj ImageObject, pageH float
{-ctm.b / imgW, ctm.d / imgH, pageH - box.Y - ctm.d - ctm.f}, {-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 { if pad > 0 {
p := float64(pad) p := float64(pad)
m[0][2] -= m[0][0]*p + m[0][1]*p m[0][2] -= m[0][0]*p + m[0][1]*p
@@ -77,6 +88,141 @@ func (r *Renderer) renderImage(ctx *canvas.Context, obj ImageObject, pageH float
ctx.RenderImage(img, ctx.CoordSystemView().Mul(ctx.View()).Mul(m)) 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 合并图片透明度 // imageWithAlpha 合并图片透明度
// 入参: img 图片对象, alpha 对象透明度 // 入参: img 图片对象, alpha 对象透明度
// 返回: image.Image 合并后的图片对象 // 返回: image.Image 合并后的图片对象
@@ -90,9 +236,10 @@ func imageWithAlpha(img image.Image, alpha *int) image.Image {
} }
bounds := img.Bounds() bounds := img.Bounds()
out := image.NewNRGBA(bounds) out := image.NewNRGBA(bounds)
source := imagePixelSource(img)
for y := bounds.Min.Y; y < bounds.Max.Y; y++ { for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ { for x := bounds.Min.X; x < bounds.Max.X; x++ {
c := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA) c := imageNRGBAAt(source, x, y)
c.A = uint8(int(c.A) * a / 255) c.A = uint8(int(c.A) * a / 255)
out.SetNRGBA(x, y, c) out.SetNRGBA(x, y, c)
} }
@@ -112,14 +259,33 @@ func imageWithTransparentEdge(img image.Image) (image.Image, int) {
if w == 0 || h == 0 { if w == 0 || h == 0 {
return img, 0 return img, 0
} }
if opaque, ok := img.(interface{ Opaque() bool }); ok && opaque.Opaque() { 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 return img, 0
} }
src := image.NewNRGBA(image.Rect(0, 0, w, h))
hasZero, hasVisible := false, false 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 y := 0; y < h; y++ {
for x := 0; x < w; x++ { for x := 0; x < w; x++ {
c := color.NRGBAModel.Convert(img.At(bounds.Min.X+x, bounds.Min.Y+y)).(color.NRGBA) c := imageNRGBAAt(source, bounds.Min.X+x, bounds.Min.Y+y)
src.SetNRGBA(x, y, c) src.SetNRGBA(x, y, c)
if c.A == 0 { if c.A == 0 {
hasZero = true hasZero = true
@@ -128,15 +294,18 @@ func imageWithTransparentEdge(img image.Image) (image.Image, int) {
} }
} }
} }
}
if !hasZero || !hasVisible { if !hasZero || !hasVisible {
return img, 0 return img, 0
} }
srcBounds := src.Bounds()
out := image.NewNRGBA(image.Rect(0, 0, w+2, h+2)) out := image.NewNRGBA(image.Rect(0, 0, w+2, h+2))
for y := 0; y < h; y++ { for y := 0; y < h; y++ {
for x := 0; x < w; x++ { for x := 0; x < w; x++ {
c := src.NRGBAAt(x, y) sx, sy := srcBounds.Min.X+x, srcBounds.Min.Y+y
c := src.NRGBAAt(sx, sy)
if c.A == 0 { if c.A == 0 {
if edge, ok := transparentEdgeColor(src, x, y); ok { if edge, ok := transparentEdgeColor(src, sx, sy); ok {
c = edge c = edge
} }
} }
@@ -158,6 +327,28 @@ func imageWithTransparentEdge(img image.Image) (image.Image, int) {
return out, 1 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 获取透明像素相邻的可见颜色 // transparentEdgeColor 获取透明像素相邻的可见颜色
// 入参: img 图片对象, x X坐标, y Y坐标 // 入参: img 图片对象, x X坐标, y Y坐标
// 返回: color.NRGBA 颜色, bool 是否存在 // 返回: color.NRGBA 颜色, bool 是否存在
+31 -24
View File
@@ -30,7 +30,7 @@ func (r *Renderer) renderAnnotations(ctx *canvas.Context, pageID string, pageH f
box, _ := ParseBox(annot.Appearance.Boundary) box, _ := ParseBox(annot.Appearance.Boundary)
ctm := Matrix{a: 1, d: 1, e: box.X, f: box.Y} ctm := Matrix{a: 1, d: 1, e: box.X, f: box.Y}
for _, obj := range annot.Appearance.Objects { for _, obj := range annot.Appearance.Objects {
r.renderObject(ctx, obj, pageH, nil, nil, 0, &ctm, false) r.renderObject(ctx, obj, pageH, nil, nil, 0, &ctm, false, nil)
} }
} }
} }
@@ -38,20 +38,25 @@ func (r *Renderer) renderAnnotations(ctx *canvas.Context, pageID string, pageH f
// renderTemplate 渲染模板 // renderTemplate 渲染模板
// 入参: ctx 画布上下文, templateID 模板ID, pageH 页面高度 // 入参: ctx 画布上下文, templateID 模板ID, pageH 页面高度
func (r *Renderer) renderTemplate(ctx *canvas.Context, templateID string, pageH float64) { func (r *Renderer) renderTemplate(ctx *canvas.Context, templateID string, pageH float64) {
tplContent := r.templatePageCache[templateID]
if tplContent == nil {
var tplPage *TemplatePage var tplPage *TemplatePage
for _, tp := range r.Reader.doc.CommonData.TemplatePage { for i := range r.Reader.doc.CommonData.TemplatePage {
if tp.ID == templateID { if r.Reader.doc.CommonData.TemplatePage[i].ID == templateID {
tplPage = &tp tplPage = &r.Reader.doc.CommonData.TemplatePage[i]
break break
} }
} }
if tplPage == nil { if tplPage == nil {
return return
} }
tplContent, err := r.Reader.PageContent(Page{BaseLoc: tplPage.BaseLoc}) var err error
tplContent, err = r.Reader.PageContent(Page{BaseLoc: tplPage.BaseLoc})
if err != nil { if err != nil {
return return
} }
r.templatePageCache[templateID] = tplContent
}
if tplContent.Content.Layer != nil { if tplContent.Content.Layer != nil {
for _, layer := range tplContent.Content.Layer { for _, layer := range tplContent.Content.Layer {
r.renderLayer(ctx, layer, pageH, nil, nil, 0, nil) r.renderLayer(ctx, layer, pageH, nil, nil, 0, nil)
@@ -65,27 +70,27 @@ func (r *Renderer) renderLayer(ctx *canvas.Context, layer Layer, pageH float64,
defaultFill, defaultStroke, defaultLW = r.drawParamDefaults(layer.DrawParam, defaultFill, defaultStroke, defaultLW) defaultFill, defaultStroke, defaultLW = r.drawParamDefaults(layer.DrawParam, defaultFill, defaultStroke, defaultLW)
if len(layer.Objects) > 0 { if len(layer.Objects) > 0 {
for _, obj := range layer.Objects { for _, obj := range layer.Objects {
r.renderObject(ctx, obj, pageH, defaultFill, defaultStroke, defaultLW, parentCTM, false) r.renderObject(ctx, obj, pageH, defaultFill, defaultStroke, defaultLW, parentCTM, false, nil)
} }
return return
} }
for _, textObj := range layer.TextObject { for _, textObj := range layer.TextObject {
r.renderText(ctx, textObj, pageH, defaultFill, defaultStroke, parentCTM, false) r.renderText(ctx, textObj, pageH, defaultFill, defaultStroke, parentCTM, false, nil)
} }
for _, pathObj := range layer.PathObject { for _, pathObj := range layer.PathObject {
r.renderPath(ctx, pathObj, pageH, defaultFill, defaultStroke, defaultLW, parentCTM, false) r.renderPath(ctx, pathObj, pageH, defaultFill, defaultStroke, defaultLW, parentCTM, false, nil)
} }
for _, imgObj := range layer.ImageObject { for _, imgObj := range layer.ImageObject {
r.renderImage(ctx, imgObj, pageH, parentCTM, false) r.renderImage(ctx, imgObj, pageH, parentCTM, false, nil)
} }
for _, cgu := range layer.CompositeGraphicUnit { for _, cgu := range layer.CompositeGraphicUnit {
r.renderCompositeGraphicUnit(ctx, cgu, pageH, defaultFill, defaultStroke, defaultLW, parentCTM, false) r.renderCompositeGraphicUnit(ctx, cgu, pageH, defaultFill, defaultStroke, defaultLW, parentCTM, false, nil)
} }
} }
// renderCompositeGraphicUnit 渲染复合图元 // renderCompositeGraphicUnit 渲染复合图元
// 入参: ctx 画布上下文, cgu 复合图元对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽, parentCTM 父级CTM, boundaryInCTM 边界是否参与CTM变换 // 入参: 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) { 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 { if cgu.Visible != nil && !*cgu.Visible {
return return
} }
@@ -94,53 +99,55 @@ func (r *Renderer) renderCompositeGraphicUnit(ctx *canvas.Context, cgu Composite
if parentCTM != nil { if parentCTM != nil {
currentCTM = parentCTM.Multiply(currentCTM) 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 cgu.ResourceID != "" {
if ref, ok := r.CompositeGraphicUnits[cgu.ResourceID]; ok { if ref, ok := r.CompositeGraphicUnits[cgu.ResourceID]; ok {
refCopy := *ref refCopy := *ref
refCopy.Alpha = mergeAlpha(refCopy.Alpha, cgu.Alpha) refCopy.Alpha = mergeAlpha(refCopy.Alpha, cgu.Alpha)
r.renderCompositeGraphicUnit(ctx, refCopy, pageH, defaultFill, defaultStroke, defaultLW, &currentCTM, true) r.renderCompositeGraphicUnit(ctx, refCopy, pageH, defaultFill, defaultStroke, defaultLW, &currentCTM, true, clipPath)
} }
} }
defaultFill, defaultStroke, defaultLW = r.drawParamDefaults(cgu.DrawParam, defaultFill, defaultStroke, defaultLW) defaultFill, defaultStroke, defaultLW = r.drawParamDefaults(cgu.DrawParam, defaultFill, defaultStroke, defaultLW)
if len(cgu.Objects) > 0 { if len(cgu.Objects) > 0 {
for _, obj := range cgu.Objects { for _, obj := range cgu.Objects {
obj = mergeGraphicObjectAlpha(obj, cgu.Alpha) obj = mergeGraphicObjectAlpha(obj, cgu.Alpha)
r.renderObject(ctx, obj, pageH, defaultFill, defaultStroke, defaultLW, &currentCTM, boundaryInCTM) r.renderObject(ctx, obj, pageH, defaultFill, defaultStroke, defaultLW, &currentCTM, boundaryInCTM, clipPath)
} }
ctx.Pop() ctx.Pop()
return return
} }
for _, imgObj := range cgu.ImageObject { for _, imgObj := range cgu.ImageObject {
imgObj.Alpha = mergeAlpha(imgObj.Alpha, cgu.Alpha) imgObj.Alpha = mergeAlpha(imgObj.Alpha, cgu.Alpha)
r.renderImage(ctx, imgObj, pageH, &currentCTM, boundaryInCTM) r.renderImage(ctx, imgObj, pageH, &currentCTM, boundaryInCTM, clipPath)
} }
for _, pathObj := range cgu.PathObject { for _, pathObj := range cgu.PathObject {
pathObj.Alpha = mergeAlpha(pathObj.Alpha, cgu.Alpha) pathObj.Alpha = mergeAlpha(pathObj.Alpha, cgu.Alpha)
r.renderPath(ctx, pathObj, pageH, defaultFill, defaultStroke, defaultLW, &currentCTM, boundaryInCTM) r.renderPath(ctx, pathObj, pageH, defaultFill, defaultStroke, defaultLW, &currentCTM, boundaryInCTM, clipPath)
} }
for _, textObj := range cgu.TextObject { for _, textObj := range cgu.TextObject {
textObj.Alpha = mergeAlpha(textObj.Alpha, cgu.Alpha) textObj.Alpha = mergeAlpha(textObj.Alpha, cgu.Alpha)
r.renderText(ctx, textObj, pageH, defaultFill, defaultStroke, &currentCTM, boundaryInCTM) r.renderText(ctx, textObj, pageH, defaultFill, defaultStroke, &currentCTM, boundaryInCTM, clipPath)
} }
for _, subCgu := range cgu.CompositeGraphicUnit { for _, subCgu := range cgu.CompositeGraphicUnit {
subCgu.Alpha = mergeAlpha(subCgu.Alpha, cgu.Alpha) subCgu.Alpha = mergeAlpha(subCgu.Alpha, cgu.Alpha)
r.renderCompositeGraphicUnit(ctx, subCgu, pageH, defaultFill, defaultStroke, defaultLW, &currentCTM, boundaryInCTM) r.renderCompositeGraphicUnit(ctx, subCgu, pageH, defaultFill, defaultStroke, defaultLW, &currentCTM, boundaryInCTM, clipPath)
} }
ctx.Pop() ctx.Pop()
} }
// renderObject 渲染图形对象 // renderObject 渲染图形对象
// 入参: ctx 画布上下文, obj 图形对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽, parentCTM 父级CTM, boundaryInCTM 边界是否参与CTM变换 // 入参: 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) { 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 { switch obj.Type {
case "TextObject": case "TextObject":
r.renderText(ctx, obj.TextObject, pageH, defaultFill, defaultStroke, parentCTM, boundaryInCTM) r.renderText(ctx, obj.TextObject, pageH, defaultFill, defaultStroke, parentCTM, boundaryInCTM, parentClip)
case "PathObject": case "PathObject":
r.renderPath(ctx, obj.PathObject, pageH, defaultFill, defaultStroke, defaultLW, parentCTM, boundaryInCTM) r.renderPath(ctx, obj.PathObject, pageH, defaultFill, defaultStroke, defaultLW, parentCTM, boundaryInCTM, parentClip)
case "ImageObject": case "ImageObject":
r.renderImage(ctx, obj.ImageObject, pageH, parentCTM, boundaryInCTM) r.renderImage(ctx, obj.ImageObject, pageH, parentCTM, boundaryInCTM, parentClip)
case "CompositeGraphicUnit", "CompositeObject": case "CompositeGraphicUnit", "CompositeObject":
r.renderCompositeGraphicUnit(ctx, obj.CompositeGraphicUnit, pageH, defaultFill, defaultStroke, defaultLW, parentCTM, boundaryInCTM) r.renderCompositeGraphicUnit(ctx, obj.CompositeGraphicUnit, pageH, defaultFill, defaultStroke, defaultLW, parentCTM, boundaryInCTM, parentClip)
} }
} }
+33 -21
View File
@@ -42,7 +42,7 @@ func (r *Renderer) RenderToImage(page *PageContent) (image.Image, error) {
// 入参: page 页面内容, writer 输出流 // 入参: page 页面内容, writer 输出流
// 返回: error 错误信息 // 返回: error 错误信息
func (r *Renderer) RenderToSVG(page *PageContent, writer io.Writer) error { func (r *Renderer) RenderToSVG(page *PageContent, writer io.Writer) error {
c, err := r.RenderPage(page) c, err := r.renderPage(page)
if err != nil { if err != nil {
return err return err
} }
@@ -59,21 +59,28 @@ func replacePDFProducer(data []byte) []byte {
return data return data
} }
dst := []byte("/Producer(xiaoqidun/ofdgo)") dst := []byte("/Producer(xiaoqidun/ofdgo)")
result := make([]byte, 0, len(data)-len(old)+len(dst)) copy(data[idx:idx+len(old)], dst)
return append(append(append(result, data[:idx]...), dst...), data[idx+len(old):]...) return data
} }
// RenderToPDF 渲染为PDF // RenderToPDF 渲染为PDF
// 入参: page 页面内容, writer 输出流 // 入参: page 页面内容, writer 输出流
// 返回: error 错误信息 // 返回: error 错误信息
func (r *Renderer) RenderToPDF(page *PageContent, writer io.Writer) error { func (r *Renderer) RenderToPDF(page *PageContent, writer io.Writer) error {
c, err := r.RenderPage(page) c, err := r.renderPage(page)
if err != nil { if err != nil {
return err 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 var buf bytes.Buffer
p := pdf.New(&buf, c.W, c.H, nil) p := pdf.New(&buf, c.W, c.H, nil)
p.SetInfo("", "", "", "", "xiaoqidun/ofdgo") p.SetInfo("", "", "", "", "xiaoqidun/ofdgo")
navigation.apply(p, 0)
c.RenderTo(p) c.RenderTo(p)
if err := p.Close(); err != nil { if err := p.Close(); err != nil {
return err return err
@@ -86,7 +93,7 @@ func (r *Renderer) RenderToPDF(page *PageContent, writer io.Writer) error {
// 入参: page 页面内容, writer 输出流 // 入参: page 页面内容, writer 输出流
// 返回: error 错误信息 // 返回: error 错误信息
func (r *Renderer) RenderToEPS(page *PageContent, writer io.Writer) error { func (r *Renderer) RenderToEPS(page *PageContent, writer io.Writer) error {
c, err := r.RenderPage(page) c, err := r.renderPage(page)
if err != nil { if err != nil {
return err return err
} }
@@ -104,28 +111,33 @@ func (r *Renderer) RenderToMultiPagePDF(writer io.Writer) error {
if len(doc.Pages.Page) == 0 { if len(doc.Pages.Page) == 0 {
return fmt.Errorf("no pages found") 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 var buf bytes.Buffer
var p *pdf.PDF p := pdf.New(&buf, pages[0].Box.W, pages[0].Box.H, nil)
for _, pgRef := range doc.Pages.Page {
page, err := r.Reader.PageContent(pgRef)
if err != nil {
continue
}
c, err := r.RenderPage(page)
if err != nil {
continue
}
if p == nil {
p = pdf.New(&buf, c.W, c.H, nil)
p.SetInfo("", "", "", "", "xiaoqidun/ofdgo") p.SetInfo("", "", "", "", "xiaoqidun/ofdgo")
} else { 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) p.NewPage(c.W, c.H)
} }
navigation.apply(p, i)
c.RenderTo(p) c.RenderTo(p)
} }
if p == nil {
return fmt.Errorf("failed to render any page")
}
if err := p.Close(); err != nil { if err := p.Close(); err != nil {
return err return err
} }
+120 -10
View File
@@ -163,8 +163,8 @@ func (s *pathStyle) scale(ctm Matrix) {
} }
// renderPath 渲染路径 // renderPath 渲染路径
// 入参: ctx 画布上下文, obj 路径对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽, parentCTM 父级CTM, boundaryInCTM 边界是否参与CTM变换 // 入参: 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) { 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 { if obj.Visible != nil && !*obj.Visible {
return return
} }
@@ -191,7 +191,7 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64
if rectPath := r.buildTinyFillRectPath(obj, pageH, ctm, bx, by); rectPath != nil { if rectPath := r.buildTinyFillRectPath(obj, pageH, ctm, bx, by); rectPath != nil {
p = rectPath p = rectPath
} }
clipPath := r.buildClipPath(obj.Clips, pageH, bx, by, ctm) clipPath := intersectClipPath(parentClip, r.buildClipPath(obj.Clips, pageH, bx, by, ctm))
shouldFill := false shouldFill := false
if obj.Fill != nil { if obj.Fill != nil {
shouldFill = *obj.Fill shouldFill = *obj.Fill
@@ -204,7 +204,7 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64
if clipPath != nil { if clipPath != nil {
fp = p.Copy() fp = p.Copy()
fp.Close() fp.Close()
fp = fp.And(clipPath) fp = applyClipPath(fp, clipPath)
} }
r.renderPattern(ctx, style.fillPattern, style.fillPatternColor, pageH, fp, ctm, bx, by) r.renderPattern(ctx, style.fillPattern, style.fillPatternColor, pageH, fp, ctm, bx, by)
} else if shouldFill && style.fillPaint != nil { } else if shouldFill && style.fillPaint != nil {
@@ -214,7 +214,7 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64
if clipPath != nil { if clipPath != nil {
fp = p.Copy() fp = p.Copy()
fp.Close() fp.Close()
fp = fp.And(clipPath) fp = applyClipPath(fp, clipPath)
} }
ctx.DrawPath(0, 0, fp) ctx.DrawPath(0, 0, fp)
} }
@@ -243,7 +243,7 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64
sp = sp.Dash(style.dashOffset, style.dashPattern...) sp = sp.Dash(style.dashOffset, style.dashPattern...)
} }
sp = sp.Stroke(style.lineWidth, style.lineCap, style.lineJoin, canvas.Tolerance) sp = sp.Stroke(style.lineWidth, style.lineCap, style.lineJoin, canvas.Tolerance)
sp = sp.And(clipPath) sp = applyClipPath(sp, clipPath)
ctx.SetFill(style.strokePaint) ctx.SetFill(style.strokePaint)
ctx.SetStrokeColor(canvas.Transparent) ctx.SetStrokeColor(canvas.Transparent)
ctx.DrawPath(0, 0, sp) ctx.DrawPath(0, 0, sp)
@@ -304,7 +304,7 @@ func (r *Renderer) renderPattern(ctx *canvas.Context, pattern *Pattern, defaultC
for iy := startY; iy <= endY; iy++ { for iy := startY; iy <= endY; iy++ {
tileCTM := patternCTM.Multiply(TranslationMatrix(float64(ix)*xStep, float64(iy)*yStep)) tileCTM := patternCTM.Multiply(TranslationMatrix(float64(ix)*xStep, float64(iy)*yStep))
for _, obj := range pattern.CellContent.Objects { for _, obj := range pattern.CellContent.Objects {
r.renderObject(ctx, obj, pageH, defaultColor, defaultColor, 0, &tileCTM, false) r.renderObject(ctx, obj, pageH, defaultColor, defaultColor, 0, &tileCTM, false, clip)
} }
} }
} }
@@ -411,7 +411,7 @@ func (r *Renderer) buildClipPath(clips *Clips, pageH float64, bx, by float64, ob
var clipPath *canvas.Path var clipPath *canvas.Path
for _, area := range clip.Area { for _, area := range clip.Area {
areaCTM := NewMatrix(area.CTM) areaCTM := NewMatrix(area.CTM)
if clips.TransFlag { if clips.TransFlag == nil || *clips.TransFlag {
areaCTM = objectCTM.Multiply(areaCTM) areaCTM = objectCTM.Multiply(areaCTM)
} }
for _, pathObj := range area.Path { for _, pathObj := range area.Path {
@@ -422,7 +422,7 @@ func (r *Renderer) buildClipPath(clips *Clips, pageH float64, bx, by float64, ob
if clipPath == nil { if clipPath == nil {
clipPath = cp clipPath = cp
} else { } else {
clipPath = clipPath.Or(cp) clipPath = unionClipPath(clipPath, cp)
} }
} }
} }
@@ -430,13 +430,123 @@ func (r *Renderer) buildClipPath(clips *Clips, pageH float64, bx, by float64, ob
if p == nil { if p == nil {
p = clipPath p = clipPath
} else { } else {
p = p.And(clipPath) p = intersectClipPath(p, clipPath)
} }
} }
} }
return p 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 构建微小填充矩形路径 // buildTinyFillRectPath 构建微小填充矩形路径
// 入参: obj 路径对象, pageH 页面高度, ctm 变换矩阵, bx 边界X坐标, by 边界Y坐标 // 入参: obj 路径对象, pageH 页面高度, ctm 变换矩阵, bx 边界X坐标, by 边界Y坐标
// 返回: *canvas.Path 路径对象 // 返回: *canvas.Path 路径对象
+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
}
+80 -10
View File
@@ -18,16 +18,23 @@ import (
"bytes" "bytes"
"image" "image"
"image/color" "image/color"
"image/draw"
"math"
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
"github.com/tdewolff/canvas/renderers/rasterizer"
) )
// renderStamp 渲染印章 // renderStamp 渲染印章
// 入参: ctx 画布上下文, s 印章对象, pageH 页面高度 // 入参: ctx 画布上下文, s 印章对象, pageH 页面高度
func (r *Renderer) renderStamp(ctx *canvas.Context, s Stamp, pageH float64) { func (r *Renderer) renderStamp(ctx *canvas.Context, s Stamp, pageH float64) {
x, y, w, h := s.Box.X, s.Box.Y, s.Box.W, s.Box.H
screenY := pageH - (y + h)
if s.Type == "ofd" && len(s.Data) > 0 { 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))) reader, err := NewReader(bytes.NewReader(s.Data), int64(len(s.Data)))
if err == nil { if err == nil {
defer reader.Close() defer reader.Close()
@@ -44,8 +51,8 @@ func (r *Renderer) renderStamp(ctx *canvas.Context, s Stamp, pageH float64) {
continue continue
} }
ctx.Push() ctx.Push()
ctx.Translate(x, screenY) ctx.Translate(s.Box.X, pageH-(s.Box.Y+s.Box.H))
ctx.Scale(w/sealBox.W, h/sealBox.H) ctx.Scale(s.Box.W/sealBox.W, s.Box.H/sealBox.H)
renderer.renderPageToContext(ctx, content, false) renderer.renderPageToContext(ctx, content, false)
ctx.Pop() ctx.Pop()
} }
@@ -56,17 +63,80 @@ func (r *Renderer) renderStamp(ctx *canvas.Context, s Stamp, pageH float64) {
if len(s.Data) > 0 { if len(s.Data) > 0 {
img, _, err := decodeImageData(s.Data) img, _, err := decodeImageData(s.Data)
if err == nil { if err == nil {
img = stampImageWithTransparentWhite(img) if r.decodeImages {
ctx.Push() img = imagePixelSource(img)
ctx.Translate(x, screenY) }
ctx.Scale(w/float64(img.Bounds().Dx()), h/float64(img.Bounds().Dy())) r.renderStampImage(ctx, stampImageWithTransparentWhite(img), s, pageH)
ctx.DrawImage(0, 0, img, canvas.DPMM(1.0))
ctx.Pop()
return 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 处理印章图片白色底色 // stampImageWithTransparentWhite 处理印章图片白色底色
// 入参: img 印章图片对象 // 入参: img 印章图片对象
// 返回: image.Image 处理后的印章图片对象 // 返回: image.Image 处理后的印章图片对象
+48 -13
View File
@@ -24,8 +24,8 @@ import (
const ptPerMM = 72.0 / 25.4 const ptPerMM = 72.0 / 25.4
// renderText 渲染文本 // renderText 渲染文本
// 入参: ctx 画布上下文, obj 文本对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, parentCTM 父级CTM, boundaryInCTM 边界是否参与父级CTM // 入参: 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) { 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 { if obj.Visible != nil && !*obj.Visible {
return return
} }
@@ -41,6 +41,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
if parentCTM != nil { if parentCTM != nil {
ctm = parentCTM.Multiply(ctm) ctm = parentCTM.Multiply(ctm)
} }
clipPath := intersectClipPath(parentClip, r.buildClipPath(obj.Clips, pageH, bx, by, ctm))
var dp *DrawParam var dp *DrawParam
if obj.DrawParam != "" { if obj.DrawParam != "" {
dp = r.getDrawParam(obj.DrawParam, nil) dp = r.getDrawParam(obj.DrawParam, nil)
@@ -115,6 +116,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
} }
ff := r.loadFont(fontID) ff := r.loadFont(fontID)
if ff == nil { if ff == nil {
ctx.Pop()
return return
} }
face := ff.Face(sizePt, fillPaint, fontStyle, canvas.FontNormal) face := ff.Face(sizePt, fillPaint, fontStyle, canvas.FontNormal)
@@ -134,7 +136,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
} }
dxs, dys := parseFloats(tc.DeltaX), parseFloats(tc.DeltaY) dxs, dys := parseFloats(tc.DeltaX), parseFloats(tc.DeltaY)
xs, ys := parseFloats(tc.X), parseFloats(tc.Y) xs, ys := parseFloats(tc.X), parseFloats(tc.Y)
drawAsPath := embeddedFont || textCodePositioned(tc, xs, ys) drawAsPath := embeddedFont || textCodePositioned(tc, xs, ys) || clipPath != nil
cx, cy := 0.0, 0.0 cx, cy := 0.0, 0.0
if len(xs) > 0 { if len(xs) > 0 {
cx = xs[0] cx = xs[0]
@@ -144,13 +146,21 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
} }
for i, glyph := range glyphs { for i, glyph := range glyphs {
str := glyph.Text 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) { if i < len(xs) {
cx = xs[i] cx = xs[i]
} else if i > 0 { } else if i > 0 {
if dx, ok := textDelta(dxs, i-1); ok { if dx, ok := textDelta(dxs, i-1); ok {
cx += dx cx += dx
} else if len(dys) == 0 { } else if len(dys) == 0 {
cx += textGlyphWidth(face, glyph) * hScale cx += glyphWidth * hScale
} }
} }
if i < len(ys) { if i < len(ys) {
@@ -169,7 +179,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
tx, ty := ctm.Transform(cx, cy) tx, ty := ctm.Transform(cx, cy)
canvasX, canvasY = tx+bx, pageH-(ty+by) canvasX, canvasY = tx+bx, pageH-(ty+by)
} }
textWidth := textGlyphWidth(face, glyph) * hScale textWidth := glyphWidth * hScale
glyphFillPaint := fillPaint glyphFillPaint := fillPaint
if useGlyphFillPaint { if useGlyphFillPaint {
glyphFillPaint = parseFillPaint(fillColorNode, bx, by, pageH, canvasX, canvasY) glyphFillPaint = parseFillPaint(fillColorNode, bx, by, pageH, canvasX, canvasY)
@@ -177,22 +187,47 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
advanceLimit := textGlyphAdvanceLimit(dxs, dys, xs, i, len(glyphs), cx) advanceLimit := textGlyphAdvanceLimit(dxs, dys, xs, i, len(glyphs), cx)
if glyphFillPaint != nil { if glyphFillPaint != nil {
ctx.SetFill(glyphFillPaint) ctx.SetFill(glyphFillPaint)
drawGlyph := func(x, y float64) { if clipPath != nil {
if drawAsPath || glyph.GlyphID >= 0 {
path, width := textGlyphPath(face, glyph)
scaleX := hScale scaleX := hScale
if advanceLimit > 0 && width*scaleX > advanceLimit { if advanceLimit > 0 && glyphWidth*scaleX > advanceLimit {
scaleX = advanceLimit / width scaleX = advanceLimit / glyphWidth
} }
textWidth = width * scaleX 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 { if scaleX != 1 {
ctx.Push() ctx.Push()
ctx.Translate(x, y) ctx.Translate(x, y)
ctx.Scale(scaleX, 1) ctx.Scale(scaleX, 1)
ctx.DrawPath(0, 0, path) ctx.DrawPath(0, 0, glyphPath)
ctx.Pop() ctx.Pop()
} else { } else {
ctx.DrawPath(x, y, path) ctx.DrawPath(x, y, glyphPath)
} }
} else { } else {
scaled := hScale != 1 scaled := hScale != 1
+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固定长度字节
+96 -6
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,7 +38,8 @@ import (
) )
// SignatureVerifyReport 签名验证报告 // SignatureVerifyReport 签名验证报告
// Valid表示签名完整性及调用方指定的证书策略均通过 // Valid表示签名完整性、签名时间语义及调用方指定的证书策略均通过
// SealCertTimeOK仅提供制章证书在签名时间的状态信息, 不参与Valid判断
type SignatureVerifyReport struct { type SignatureVerifyReport struct {
ID string ID string
BaseLoc string BaseLoc string
@@ -46,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
@@ -60,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
@@ -75,13 +85,14 @@ func (report SignatureVerifyReport) IntegrityValid() bool {
} }
// TrustedValid 判断签名是否可信有效 // TrustedValid 判断签名是否可信有效
// 返回: bool 签名完整性、证书信任及证书有效期是否均验证通过 // 返回: bool 签名完整性、时间语义、证书信任及证书有效期是否均验证通过
func (report SignatureVerifyReport) TrustedValid() bool { func (report SignatureVerifyReport) TrustedValid() bool {
return report.IntegrityValid() && report.CertTrustChecked && report.CertTrustOK && report.CertTimeChecked && report.CertTimeOK 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
@@ -91,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
@@ -239,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()
@@ -253,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)
@@ -266,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 {
@@ -279,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.IntegrityValid() && 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 {
@@ -302,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)
@@ -383,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
} }
@@ -397,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":
@@ -453,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[:]
@@ -623,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
} }
@@ -790,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"),
@@ -803,6 +876,23 @@ 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 ( const (
signatureExtensionKeyUsage = "2.5.29.15" signatureExtensionKeyUsage = "2.5.29.15"
signatureExtensionBasicConstraints = "2.5.29.19" signatureExtensionBasicConstraints = "2.5.29.19"
+45
View File
@@ -20,6 +20,7 @@ import (
"strings" "strings"
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
canvastext "github.com/tdewolff/canvas/text"
"github.com/tdewolff/font" "github.com/tdewolff/font"
) )
@@ -29,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
@@ -161,6 +182,30 @@ 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 判断文本是否需要应用字形变换 // hasTextMatrix 判断文本是否需要应用字形变换
// 入参: ctm 变换矩阵 // 入参: ctm 变换矩阵
// 返回: bool 是否需要变换 // 返回: bool 是否需要变换