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
for _, report := range reports {
if report.Valid {
if report.IntegrityValid() {
log.Printf("签名%s验证通过", report.ID)
continue
}
+4
View File
@@ -401,6 +401,10 @@ body[data-hide-meta] .meta-panel {
background-size: 24px 24px;
}
.viewer-panel.single-page-fits-height {
overflow-y: hidden;
}
.empty-state {
position: absolute;
inset: 0;
+36 -25
View File
@@ -1003,7 +1003,7 @@ async function exportPDF() {
return;
}
setProgress("正在保存文档 PDF", 86);
const bytes = base64ToBytes(result.base64);
const bytes = result.bytes;
downloadBytes(bytes, "application/pdf", pdfFileName());
setStatus(`文档 PDF 已导出 ${formatBytes(result.size || bytes.length)}`);
} catch (err) {
@@ -1047,7 +1047,7 @@ async function exportCurrentPage() {
return;
}
setProgress(`正在保存 ${result.label || label}`, 86);
const bytes = base64ToBytes(result.base64);
const bytes = result.bytes;
downloadBytes(bytes, result.mime || info?.mime || "application/octet-stream", pageFileName(result.extension || info?.extension || format));
setStatus(`${result.label || label} 已导出 ${formatBytes(result.size || bytes.length, result.label || label)}`);
} catch (err) {
@@ -1364,6 +1364,10 @@ function pageShellAtPoint(x, y) {
function setCurrentPage(index) {
state.pageIndex = index;
if (state.fitMode === "width") {
state.scale = fitWidthScale(currentPageInfo());
el.zoomLabel.textContent = `${Math.round(state.scale * 100)}%`;
}
updatePageListCurrent();
updateControls();
if (state.doc) {
@@ -1408,13 +1412,14 @@ function layoutPages() {
function layoutPageShell(shell, page) {
const width = Math.max(1, page.width * MM_TO_PX);
const height = Math.max(1, page.height * MM_TO_PX);
shell.style.width = `${width * state.scale}px`;
shell.style.height = `${height * state.scale}px`;
const scale = state.fitMode === "width" ? state.scale * currentPageInfo().width / page.width : state.scale;
shell.style.width = `${width * scale}px`;
shell.style.height = `${height * scale}px`;
const surface = shell.querySelector(".page-surface");
if (surface) {
surface.style.width = `${width}px`;
surface.style.height = `${height}px`;
surface.style.transform = `scale(${state.scale})`;
surface.style.transform = `scale(${scale})`;
}
}
@@ -1668,7 +1673,7 @@ function renderSignatures() {
const fragment = document.createDocumentFragment();
for (const signature of signatures) {
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");
head.className = "signature-head";
@@ -1677,13 +1682,16 @@ function renderSignatures() {
const badges = document.createElement("div");
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);
row.append(head);
appendSignatureLine(row, "编号", signature.id);
appendSignatureLine(row, "版本", signature.version);
appendSignatureLine(row, "章图", signature.sealType);
appendSignatureLine(row, "章号", signature.sealId);
appendSignatureLine(row, "章名", signature.sealName);
appendSignatureLine(row, "厂商", signature.sealVendor);
appendSignatureLine(row, "签者", signature.signer);
appendSignatureLine(row, "时间", formatSignatureTime(signature.signatureDateTime));
appendSignatureLine(row, "机构", signatureAgency(signature));
@@ -1694,12 +1702,17 @@ function renderSignatures() {
appendSignatureCheck(row, "一致", signature.sealMatchOK);
}
appendSignatureCheck(row, "证书", signature.certOK);
appendSignaturePolicy(row, "签期", signature.signatureTimeChecked, signature.signatureTimeOK);
appendSignaturePolicy(row, "章期", signature.sealTimeChecked, signature.sealTimeOK);
if (signature.sealCertTimeChecked) {
appendSignatureLine(row, "制期", signature.sealCertTimeOK ? "有效" : "失效", signature.sealCertTimeOK ? "ok" : "");
}
appendSignaturePolicy(row, "时效", signature.certTimeChecked, signature.certTimeOK);
appendSignaturePolicy(row, "信任", signature.certTrustChecked, signature.certTrustOK);
appendSignatureLine(row, "保护", signatureReferenceText(signature), signatureReferenceStatus(signature));
appendSignatureLine(row, "序号", signature.signSerial);
appendSignatureLine(row, "算法", signature.signatureMethod);
appendSignatureLine(row, "散列", signature.digestMethod);
appendSignatureLine(row, "序号", signature.signSerial);
appendSignatureLine(row, "主体", signature.signSubject && signature.signSubject !== signature.signer ? signature.signSubject : "");
appendSignatureLine(row, "颁发", signature.signIssuer);
appendSignatureLine(row, "章证", signature.sealSubject);
@@ -1713,7 +1726,7 @@ function signatureSummary(signatures) {
if (!signatures.length) {
return "0";
}
const invalid = signatures.filter((signature) => !signature.integrityValid).length;
const invalid = signatures.filter((signature) => !signature.valid).length;
if (invalid) {
return `${signatures.length} · 异常 ${invalid}`;
}
@@ -1972,6 +1985,13 @@ function fitWidth(updateStatus = true) {
if (!page) {
return;
}
setScale(fitWidthScale(page), updateStatus, "width");
if (updateStatus) {
scrollToPage(state.pageIndex);
}
}
function fitWidthScale(page) {
const space = pageSpace();
const width = Math.max(1, page.width * MM_TO_PX);
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)) {
available = Math.max(1, available - scrollbarWidth());
}
setScale(available / width, updateStatus, "width");
if (updateStatus) {
scrollToPage(state.pageIndex);
}
return Math.min(4, Math.max(0.2, available / width));
}
function fitHeight(updateStatus = true) {
@@ -2015,14 +2032,15 @@ function setScale(nextScale, updateStatus = true, fitMode = "free") {
const anchor = updateStatus && fitMode === "free" ? scaleAnchor() : null;
const scale = Math.min(4, Math.max(0.2, nextScale));
const scaleChanged = Math.abs(scale - state.scale) > 0.001;
const layoutChanged = scaleChanged || fitMode !== state.fitMode;
state.fitMode = fitMode;
state.scale = scale;
if (scaleChanged) {
if (layoutChanged) {
clearStampHighlights();
layoutPages();
}
updateFitSpace();
if (scaleChanged) {
if (layoutChanged) {
restoreScaleAnchor(anchor);
}
el.zoomLabel.textContent = `${Math.round(state.scale * 100)}%`;
@@ -2092,6 +2110,7 @@ function scrollbarWidth() {
function updateFitSpace() {
const page = currentPageInfo();
if (!page || state.fitMode === "free") {
el.viewerPanel.classList.remove("single-page-fits-height");
el.pageFrame.style.removeProperty("--fit-space");
el.pageFrame.style.removeProperty("--fit-gap");
return;
@@ -2099,6 +2118,7 @@ function updateFitSpace() {
const shell = pageShell(state.pageIndex);
const height = shell ? shell.getBoundingClientRect().height : Math.max(1, page.height * MM_TO_PX * state.scale);
const base = pageSpace();
el.viewerPanel.classList.toggle("single-page-fits-height", state.doc.pageCount === 1 && height <= el.viewerPanel.clientHeight - base * 2);
const space = Math.max(base, (el.viewerPanel.clientHeight - height) / 2);
const gap = space > base ? space + 1 : space;
el.pageFrame.style.setProperty("--fit-space", `${space}px`);
@@ -2159,7 +2179,7 @@ function callWASM(name, ...args) {
}
throw err;
}
const result = JSON.parse(payload);
const result = typeof payload === "string" ? JSON.parse(payload) : payload;
if (!result.ok) {
throw new Error(result.error || "WASM 调用失败");
}
@@ -2215,15 +2235,6 @@ function formatSize(value) {
return value.toFixed(1);
}
function base64ToBytes(base64) {
const binary = atob(base64 || "");
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i += 1) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
function downloadBytes(bytes, mime, name) {
const blob = new Blob([bytes], { type: mime });
const link = document.createElement("a");
+13 -12
View File
@@ -1,13 +1,13 @@
module github.com/xiaoqidun/ofdgo
go 1.25.0
go 1.26.0
require (
github.com/tdewolff/canvas v0.0.0-20260708151538-b3beae08c578
github.com/tdewolff/font v0.0.0-20260527091451-1663e68cb8a4
github.com/xiaoqidun/jbig2 v0.0.0-20260710074125-96fddd152f49
golang.org/x/image v0.44.0
golang.org/x/text v0.40.0
github.com/tdewolff/canvas v0.0.0-20260822211237-56e622de465a
github.com/tdewolff/font v0.0.0-20260822205238-d0d2f004b117
github.com/xiaoqidun/jbig2 v0.0.0-20260823111534-2e67bcec7059
golang.org/x/image v0.45.0
golang.org/x/text v0.41.0
)
require (
@@ -19,7 +19,7 @@ require (
github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 // indirect
github.com/ByteArena/poly2tri-go v0.0.0-20170716161910-d102ad91854f // indirect
github.com/Kagami/go-avif v0.1.0 // indirect
github.com/andybalholm/brotli v1.2.1 // indirect
github.com/andybalholm/brotli v1.2.2 // indirect
github.com/benoitkugler/textlayout v0.3.2 // indirect
github.com/benoitkugler/textprocessing v0.0.6 // indirect
github.com/go-fonts/latin-modern v0.3.3 // indirect
@@ -28,13 +28,14 @@ require (
github.com/kolesa-team/go-webp v1.0.5 // indirect
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
github.com/srwiley/scanx v0.0.0-20190309010443-e94503791388 // indirect
github.com/tdewolff/minify/v2 v2.24.13 // indirect
github.com/tdewolff/parse/v2 v2.8.13 // indirect
github.com/tdewolff/minify/v2 v2.24.17 // indirect
github.com/tdewolff/parse/v2 v2.8.16 // indirect
github.com/wcharczuk/go-chart/v2 v2.1.2 // indirect
github.com/yuin/goldmark v1.8.2 // indirect
golang.org/x/net v0.55.0 // indirect
github.com/yuin/goldmark v1.8.5 // indirect
golang.org/x/net v0.58.0 // indirect
golang.org/x/sys v0.47.0 // indirect
gonum.org/v1/plot v0.17.0 // indirect
modernc.org/knuth v0.5.5 // indirect
modernc.org/knuth v0.6.0 // indirect
modernc.org/token v1.1.0 // indirect
star-tex.org/x/tex v0.7.1 // indirect
)
+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/go.mod h1:abni088lmhQJvso2Lsb7azCKzwkfcnttl6tL1UTWKzg=
codeberg.org/go-fonts/liberation v0.5.0 h1:SsKoMO1v1OZmzkG2DY+7ZkCL9U+rrWI09niOLfQ5Bo0=
codeberg.org/go-fonts/liberation v0.5.0/go.mod h1:zS/2e1354/mJ4pGzIIaEtm/59VFCFnYC7YV6YdGl5GU=
codeberg.org/go-fonts/liberation v0.6.0 h1:15Gh6SdwYve22CWCm9jYpVpRuaTh726av2TgHTHvAtQ=
codeberg.org/go-fonts/liberation v0.6.0/go.mod h1:J15VAa+lyxdcI/Je7lDDDl6QOhLk9feNBnnwXqEHXOk=
codeberg.org/go-latex/latex v0.3.0 h1:LKTaDHFbEC2PH1sh0sYv6PZ1pzs/g2aoeV1HItWj/bg=
codeberg.org/go-latex/latex v0.3.0/go.mod h1:8ETijTpK2bFtwRAXLXe1RZJrYxnc5pibibZfQBj+Lk4=
codeberg.org/go-pdf/fpdf v0.12.0 h1:g8E/1VqGqB2lZUUaqQrrTnA0IEJLPTTX1DZ0qS/ZmhU=
codeberg.org/go-pdf/fpdf v0.12.0/go.mod h1:WJNJ2bvCj81rZBdhOf7lKOGoSl+OKMXcIcXqDcP8r5Y=
git.sr.ht/~sbinet/cmpimg v0.1.0 h1:E0zPRk2muWuCqSKSVZIWsgtU9pjsw3eKHi8VmQeScxo=
git.sr.ht/~sbinet/cmpimg v0.1.0/go.mod h1:FU12psLbF4TfNXkKH2ZZQ29crIqoiqTZmeQ7dkp/pxE=
git.sr.ht/~sbinet/gg v0.7.0 h1:YmNf7YKd7diDMTPm86hZa1EM3pbkOyD/zzjl0LZUdNM=
git.sr.ht/~sbinet/gg v0.7.0/go.mod h1:VYeli15tpMM4EvqlivlVbbyvWZlOU+EZn4XZmfBGUdM=
git.sr.ht/~sbinet/gg v0.8.0 h1:PjQ4AgUWRz7Dy6PVBKMLLg96eRCos1U+H6F3FJuKWHo=
git.sr.ht/~sbinet/gg v0.8.0/go.mod h1:XhlGCvSXts+BaZ1XMZLxLSFUDvwsJ/2El5F6XzPep6o=
github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 h1:1qlsVAQJXZHsaM8b6OLVo6muQUQd4CwkH/D3fnnbHXA=
github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298/go.mod h1:D+QujdIlUNfa0igpNMk6UIvlb6C252URs4yupRUV4lQ=
github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 h1:lTG4HQym5oPKjL7nGs+csTgiDna685ZXjxijkne828g=
@@ -24,8 +24,8 @@ github.com/Kagami/go-avif v0.1.0 h1:8GHAGLxCdFfhpd4Zg8j1EqO7rtcQNenxIDerC/uu68w=
github.com/Kagami/go-avif v0.1.0/go.mod h1:OPmPqzNdQq3+sXm0HqaUJQ9W/4k+Elbc3RSfJUemDKA=
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw=
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/andybalholm/brotli v1.2.2 h1:HzTuoo2ErYQqf5qvcJInB8uvqSVxRttzkFexPWtnceM=
github.com/andybalholm/brotli v1.2.2/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/benoitkugler/pstokenizer v1.0.0/go.mod h1:l1G2Voirz0q/jj0TQfabNxVsa8HZXh/VMxFSRALWTiE=
github.com/benoitkugler/textlayout v0.3.2 h1:Y/LTkwpQ9G0Fia9yhPmZA9IR5AnE8Cq30j3C+Gx5/IE=
github.com/benoitkugler/textlayout v0.3.2/go.mod h1:o+1hFV+JSHBC9qNLIuwVoLedERU7sBPgEFcuSgfvi/w=
@@ -33,8 +33,6 @@ github.com/benoitkugler/textlayout-testdata v0.1.1/go.mod h1:i/qZl09BbUOtd7Bu/W1
github.com/benoitkugler/textprocessing v0.0.6 h1:obkMyj62GEPg3xUVYqROlCN22z1OleuZm6ULqX9Om1g=
github.com/benoitkugler/textprocessing v0.0.6/go.mod h1:Io0gN08/PXEzrSOWFa88xHx2Xv3VjvLMY7H76YoI23A=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-fonts/latin-modern v0.3.3 h1:g2xNgI8yzdNzIVm+qvbMryB6yGPe0pSMss8QT3QwlJ0=
github.com/go-fonts/latin-modern v0.3.3/go.mod h1:tHaiWDGze4EPB0Go4cLT5M3QzRY3peya09Z/8KSCrpY=
github.com/go-text/typesetting v0.3.4 h1:YYurUOtEb9kGSOz4uE3k4OpBGsp1dDL8+fjCeaFamAU=
@@ -59,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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tdewolff/canvas v0.0.0-20260708151538-b3beae08c578 h1:OKVthD6KmBTDWsxnPkmDLW35yp+lvoqWsMeteXOZW+4=
github.com/tdewolff/canvas v0.0.0-20260708151538-b3beae08c578/go.mod h1:Lz12ClttuKvo2jb1UxBfJJWmKV0LmEc8IRo/30R7z+k=
github.com/tdewolff/font v0.0.0-20260527091451-1663e68cb8a4 h1:QmSG/RtOlLPJDLJ24XHSDEu4iSgcG+s9sdRg7wTdfpk=
github.com/tdewolff/font v0.0.0-20260527091451-1663e68cb8a4/go.mod h1:tQzLTuJVLs4Pp6Vxdn3PJ7sAe4gIuph3SBm/BmdlmR0=
github.com/tdewolff/minify/v2 v2.24.13 h1:xrcF7gKDnUszseEY9WX9mUlZII2v2Go/QAcAwRASw58=
github.com/tdewolff/minify/v2 v2.24.13/go.mod h1:emvwoYeIl8bfAKqRU5ww95LX9Gpggpqv/naal9a8Yq0=
github.com/tdewolff/parse/v2 v2.8.13 h1:si/8rLw5BZZTWCCiMm9A3f6x+RmqYfrkEeXCgpX5ick=
github.com/tdewolff/parse/v2 v2.8.13/go.mod h1:XdsoSFThlVIRIajAuqz1evNY7bagZS8LBOPA3aVopwQ=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/tdewolff/canvas v0.0.0-20260822211237-56e622de465a h1:riaFBiYaIv1gM31xdcU0Yo7bdp3WebF4dggO/JK0g/8=
github.com/tdewolff/canvas v0.0.0-20260822211237-56e622de465a/go.mod h1:K9Ux3cwFtyAFRITZ46g7v+ORXgI3ENtQa5rL9yjNz5Y=
github.com/tdewolff/font v0.0.0-20260822205238-d0d2f004b117 h1:5vSJjsbV9AH0U+Ex0uej8ROrpOBlOVBrw0mGlVef7oo=
github.com/tdewolff/font v0.0.0-20260822205238-d0d2f004b117/go.mod h1:ipESUihcEdgRBhIwTRHmBTOMamccMQrp4iCxfa1aFws=
github.com/tdewolff/minify/v2 v2.24.17 h1:6AbitfVyq0M7aW6i+XL7+49DeTQZwloOMs9O574arBg=
github.com/tdewolff/minify/v2 v2.24.17/go.mod h1:kVqn9vxXUKtlHexSNrWbYePqioOT5mc4ou/KVSMpfCM=
github.com/tdewolff/parse/v2 v2.8.16 h1:bLk5svUOQRkW/Y2SJ+DeENSIkZBcTIkq+Atyv5D8feI=
github.com/tdewolff/parse/v2 v2.8.16/go.mod h1:XdsoSFThlVIRIajAuqz1evNY7bagZS8LBOPA3aVopwQ=
github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk=
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
github.com/wcharczuk/go-chart/v2 v2.1.2 h1:Y17/oYNuXwZg6TFag06qe8sBajwwsuvPiJJXcUcLL6E=
github.com/wcharczuk/go-chart/v2 v2.1.2/go.mod h1:Zi4hbaqlWpYajnXB2K22IUYVXRXaLfSGNNR7P4ukyyQ=
github.com/xiaoqidun/jbig2 v0.0.0-20260710074125-96fddd152f49 h1:h/1lOG9/75eK4atpQFcm9oGXmxekMVfsPJowtYLXu5w=
github.com/xiaoqidun/jbig2 v0.0.0-20260710074125-96fddd152f49/go.mod h1:4ieZvd1EYR3zUzaAT4H8vLDb6XO+yjnxuh33So/5iC4=
github.com/xiaoqidun/jbig2 v0.0.0-20260823111534-2e67bcec7059 h1:/JXxWbNTePSVfshS7BVRm75Kh1/5rixvLpgTRw7MMv8=
github.com/xiaoqidun/jbig2 v0.0.0-20260823111534-2e67bcec7059/go.mod h1:mdoUQnVPIA4yS0/+ZrtxJNiDdbphI0RMu+DPfxXfVhE=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
github.com/yuin/goldmark v1.8.5/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
@@ -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/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.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I=
golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY=
golang.org/x/image v0.45.0 h1:FMb1nTbH5H9vF55SriQHgFw5GnNL9Jg6L25BwXKzhB0=
golang.org/x/image v0.45.0/go.mod h1:n62x/7RqlwXDvGsSU4u6IUTUf6KghUZ9Bt7cG/T9Fx4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
@@ -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.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
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.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -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.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -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.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
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.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
@@ -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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/knuth v0.5.5 h1:6lap2U/ISm8aC/4NU58ALFCRllNPaK0EZcIGY/oDgUg=
modernc.org/knuth v0.5.5/go.mod h1:e5SBb35HQBj2aFwbBO3ClPcViLY3Wi0LzaOd7c/3qMk=
modernc.org/knuth v0.6.0 h1:zLv0hzgdcpcCUB/iMwpQvn9m/iciqO/hyNXlpWymMKU=
modernc.org/knuth v0.6.0/go.mod h1:ISAZV+URZIlE8h1BwNVg1NzpwHKndV5lQ13NPPj3tds=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4=
+41 -9
View File
@@ -17,7 +17,6 @@
package webui
import (
"encoding/base64"
"encoding/json"
"fmt"
"syscall/js"
@@ -57,6 +56,9 @@ func registerCallback(name string, fn func([]js.Value) (any, error)) {
if err != nil {
return encodeResult(apiResult{OK: false, Error: err.Error()})
}
if result, ok := data.(js.Value); ok {
return result
}
return encodeResult(apiResult{OK: true, Data: data})
})
js.Global().Set(name, cb)
@@ -113,7 +115,18 @@ func renderPage(args []js.Value) (any, error) {
if len(args) == 0 {
return nil, fmt.Errorf("missing page index")
}
return currentSession.RenderPageSVG(args[0].Int())
page, err := currentSession.RenderPageSVG(args[0].Int())
if err != nil {
return nil, err
}
return successResult(map[string]any{
"index": page.Index,
"number": page.Number,
"id": page.ID,
"width": page.Width,
"height": page.Height,
"svg": page.SVG,
}), nil
}
// exportFormats 获取导出格式
@@ -137,14 +150,14 @@ func exportPage(args []js.Value) (any, error) {
if err != nil {
return nil, err
}
return map[string]any{
"base64": base64.StdEncoding.EncodeToString(data),
return successResult(map[string]any{
"bytes": bytesToJS(data),
"size": len(data),
"format": format.Value,
"label": format.Label,
"extension": format.Extension,
"mime": format.MIME,
}, nil
}), nil
}
// exportPDF 导出OFD文档为PDF
@@ -158,10 +171,29 @@ func exportPDF(args []js.Value) (any, error) {
if err != nil {
return nil, err
}
return map[string]any{
"base64": base64.StdEncoding.EncodeToString(data),
"size": len(data),
}, nil
return successResult(map[string]any{
"bytes": bytesToJS(data),
"size": len(data),
}), nil
}
// 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 获取系统字体名称
+80 -60
View File
@@ -41,34 +41,43 @@ type FontInfo = ofdgo.FontInfo
// SignatureInfo 签名验证信息
type SignatureInfo struct {
ID string `json:"id"`
Type string `json:"type"`
IntegrityValid bool `json:"integrityValid"`
Version string `json:"version,omitempty"`
SealType string `json:"sealType,omitempty"`
Signer string `json:"signer,omitempty"`
SignatureDateTime string `json:"signatureDateTime,omitempty"`
Provider string `json:"provider,omitempty"`
Company string `json:"company,omitempty"`
DataHashOK bool `json:"dataHashOK"`
SignedValueOK bool `json:"signedValueOK"`
SealOK bool `json:"sealOK"`
SealMatchOK bool `json:"sealMatchOK"`
CertOK bool `json:"certOK"`
CertTimeChecked bool `json:"certTimeChecked,omitempty"`
CertTimeOK bool `json:"certTimeOK,omitempty"`
CertTrustChecked bool `json:"certTrustChecked,omitempty"`
CertTrustOK bool `json:"certTrustOK,omitempty"`
ReferenceCount int `json:"referenceCount"`
ReferencePassed int `json:"referencePassed"`
SignSerial string `json:"signSerial,omitempty"`
SignatureMethod string `json:"signatureMethod,omitempty"`
DigestMethod string `json:"digestMethod,omitempty"`
SignSubject string `json:"signSubject,omitempty"`
SignIssuer string `json:"signIssuer,omitempty"`
SealSubject string `json:"sealSubject,omitempty"`
Stamps []SignatureStampInfo `json:"stamps,omitempty"`
Error string `json:"error,omitempty"`
ID string `json:"id"`
Type string `json:"type"`
Valid bool `json:"valid"`
Version string `json:"version,omitempty"`
SealType string `json:"sealType,omitempty"`
SealID string `json:"sealId,omitempty"`
SealName string `json:"sealName,omitempty"`
SealVendor string `json:"sealVendor,omitempty"`
Signer string `json:"signer,omitempty"`
SignatureDateTime string `json:"signatureDateTime,omitempty"`
Provider string `json:"provider,omitempty"`
Company string `json:"company,omitempty"`
DataHashOK bool `json:"dataHashOK"`
SignedValueOK bool `json:"signedValueOK"`
SealOK bool `json:"sealOK"`
SealMatchOK bool `json:"sealMatchOK"`
CertOK bool `json:"certOK"`
SignatureTimeChecked bool `json:"signatureTimeChecked,omitempty"`
SignatureTimeOK bool `json:"signatureTimeOK,omitempty"`
SealTimeChecked bool `json:"sealTimeChecked,omitempty"`
SealTimeOK bool `json:"sealTimeOK,omitempty"`
SealCertTimeChecked bool `json:"sealCertTimeChecked,omitempty"`
SealCertTimeOK bool `json:"sealCertTimeOK,omitempty"`
CertTimeChecked bool `json:"certTimeChecked,omitempty"`
CertTimeOK bool `json:"certTimeOK,omitempty"`
CertTrustChecked bool `json:"certTrustChecked,omitempty"`
CertTrustOK bool `json:"certTrustOK,omitempty"`
ReferenceCount int `json:"referenceCount"`
ReferencePassed int `json:"referencePassed"`
SignatureMethod string `json:"signatureMethod,omitempty"`
DigestMethod string `json:"digestMethod,omitempty"`
SignSerial string `json:"signSerial,omitempty"`
SignSubject string `json:"signSubject,omitempty"`
SignIssuer string `json:"signIssuer,omitempty"`
SealSubject string `json:"sealSubject,omitempty"`
Stamps []SignatureStampInfo `json:"stamps,omitempty"`
Error string `json:"error,omitempty"`
}
// SignatureStampInfo 签名外观信息
@@ -222,10 +231,6 @@ func (s *Session) Info() DocumentInfo {
PageCount: len(s.doc.Pages.Page),
Pages: make([]PageInfo, 0, len(s.doc.Pages.Page)),
}
if fonts, err := s.Renderer.FontInfos(); err == nil {
info.Fonts = fonts
}
info.FontCount = len(info.Fonts)
if signatures, err := s.signatureInfos(); err == nil {
info.Signatures = signatures
} else {
@@ -239,9 +244,11 @@ func (s *Session) Info() DocumentInfo {
info.CreationDate = docInfo.CreationDate
info.ModDate = docInfo.ModDate
}
pages := make([]*ofdgo.PageContent, 0, len(s.doc.Pages.Page))
for index, pageRef := range s.doc.Pages.Page {
pageInfo := PageInfo{Index: index, ID: pageRef.ID}
if _, page, err := s.pageContent(index); err == nil {
pages = append(pages, page)
if box, err := s.pageBox(index, page); err == nil {
pageInfo.Width = box.W
pageInfo.Height = box.H
@@ -249,6 +256,10 @@ func (s *Session) Info() DocumentInfo {
}
info.Pages = append(info.Pages, pageInfo)
}
if fonts, err := s.Renderer.FontInfosFromPages(pages); err == nil {
info.Fonts = fonts
}
info.FontCount = len(info.Fonts)
return info
}
@@ -344,34 +355,43 @@ func (s *Session) signatureInfos() ([]SignatureInfo, error) {
// 返回: SignatureInfo 签名验证信息
func signatureInfo(report ofdgo.SignatureVerifyReport) SignatureInfo {
return SignatureInfo{
ID: report.ID,
Type: string(report.Type),
IntegrityValid: report.IntegrityValid(),
Version: report.Provider.Version,
SealType: report.SealType,
Signer: signatureSigner(report),
SignatureDateTime: report.SignatureDateTime,
Provider: report.Provider.ProviderName,
Company: report.Provider.Company,
DataHashOK: report.DataHashOK,
SignedValueOK: report.SignedValueOK,
SealOK: report.SealOK,
SealMatchOK: report.SealMatchOK,
CertOK: report.CertOK,
CertTimeChecked: report.CertTimeChecked,
CertTimeOK: report.CertTimeOK,
CertTrustChecked: report.CertTrustChecked,
CertTrustOK: report.CertTrustOK,
ReferenceCount: len(report.References),
ReferencePassed: signatureReferencePassed(report.References),
SignSerial: report.SignCert.SerialNumber,
SignatureMethod: report.SignatureMethod,
DigestMethod: report.DigestMethod,
SignSubject: report.SignCert.Subject,
SignIssuer: report.SignCert.Issuer,
SealSubject: report.SealCert.Subject,
Stamps: signatureStampInfos(report.StampPositions),
Error: signatureReportError(report),
ID: report.ID,
Type: string(report.Type),
Valid: report.Valid,
Version: report.Provider.Version,
SealType: report.SealType,
SealID: report.SealInfo.ID,
SealName: report.SealInfo.Name,
SealVendor: report.SealInfo.VendorID,
Signer: signatureSigner(report),
SignatureDateTime: report.SignatureDateTime,
Provider: report.Provider.ProviderName,
Company: report.Provider.Company,
DataHashOK: report.DataHashOK,
SignedValueOK: report.SignedValueOK,
SealOK: report.SealOK,
SealMatchOK: report.SealMatchOK,
CertOK: report.CertOK,
SignatureTimeChecked: report.SignatureTimeChecked,
SignatureTimeOK: report.SignatureTimeOK,
SealTimeChecked: report.SealTimeChecked,
SealTimeOK: report.SealTimeOK,
SealCertTimeChecked: report.SealCertTimeChecked,
SealCertTimeOK: report.SealCertTimeOK,
CertTimeChecked: report.CertTimeChecked,
CertTimeOK: report.CertTimeOK,
CertTrustChecked: report.CertTrustChecked,
CertTrustOK: report.CertTrustOK,
ReferenceCount: len(report.References),
ReferencePassed: signatureReferencePassed(report.References),
SignatureMethod: report.SignatureMethod,
DigestMethod: report.DigestMethod,
SignSerial: report.SignCert.SerialNumber,
SignSubject: report.SignCert.Subject,
SignIssuer: report.SignCert.Issuer,
SealSubject: report.SealCert.Subject,
Stamps: signatureStampInfos(report.StampPositions),
Error: signatureReportError(report),
}
}
+8
View File
@@ -71,6 +71,14 @@ func NewRenderer(reader *Reader, opts ...RendererOption) *Renderer {
DrawParams: reader.drawParamCache,
CompositeGraphicUnits: reader.compositeGraphicUnitCache,
FontMap: make(map[string]*canvas.FontFamily),
FontGIDMap: make(map[string]map[uint16]rune),
FontCIDMap: make(map[string]map[uint16]rune),
fontCache: make(map[fontCacheKey]*canvas.FontFamily),
fontSourceCache: make(map[string][]fontSource),
fontSourceUsed: make(map[string]fontSource),
fontDirCandidates: make(map[string][]fontFileCandidate),
textGlyphPathCache: make(map[textGlyphPathCacheKey]textGlyphPathCacheValue),
templatePageCache: make(map[string]*PageContent),
}
for _, opt := range opts {
opt(r)
+198
View File
@@ -0,0 +1,198 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"encoding/xml"
"strconv"
)
// Action 动作
type Action struct {
Event string `xml:"Event,attr"`
Region *Region `xml:"Region"`
Goto *Goto `xml:"Goto"`
URI *URI `xml:"URI"`
GotoA *GotoA `xml:"GotoA"`
Sound *Sound `xml:"Sound"`
Movie *Movie `xml:"Movie"`
}
// Goto 文档内跳转动作
type Goto struct {
Dest *Dest `xml:"Dest"`
Bookmark *GotoBookmark `xml:"Bookmark"`
}
// Dest 文档内跳转目标
type Dest struct {
Type string `xml:"Type,attr"`
PageID string `xml:"PageID,attr"`
Left float64 `xml:"Left"`
Right float64 `xml:"Right"`
Top float64 `xml:"Top"`
Bottom float64 `xml:"Bottom"`
Zoom float64 `xml:"Zoom"`
}
// GotoBookmark 书签跳转目标
type GotoBookmark struct {
Name string `xml:"Name,attr"`
}
// URI URI动作
type URI struct {
URI string `xml:"URI,attr"`
Base string `xml:"Base,attr"`
}
// GotoA 附件动作
type GotoA struct {
AttachID string `xml:"AttachID,attr"`
NewWindow *bool `xml:"NewWindow,attr"`
}
// Sound 音频动作
type Sound struct {
ResourceID string `xml:"ResourceID,attr"`
Volume *int `xml:"Volume,attr"`
Repeat bool `xml:"Repeat,attr"`
Synchronous bool `xml:"Synchronous,attr"`
}
// Movie 视频动作
type Movie struct {
ResourceID string `xml:"ResourceID,attr"`
Operator string `xml:"Operator,attr"`
}
// Region 动作区域
type Region struct {
Area []RegionArea `xml:"Area"`
}
// RegionArea 动作区域分路径
type RegionArea struct {
Start string `xml:"Start,attr"`
Command []RegionCommand `xml:"-"`
}
// RegionCommand 动作区域绘制指令
type RegionCommand struct {
Type string
Point1 string
Point2 string
Point3 string
EllipseSize string
RotationAngle string
LargeArc string
SweepDirection string
EndPoint string
}
// UnmarshalXML 解析跳转目标
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (dest *Dest) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var value struct {
Left *float64 `xml:"Left"`
Right *float64 `xml:"Right"`
Top *float64 `xml:"Top"`
Bottom *float64 `xml:"Bottom"`
Zoom *float64 `xml:"Zoom"`
}
dest.Type = attrValue(start, "Type")
dest.PageID = attrValue(start, "PageID")
dest.Left = actionFloatAttr(start, "Left")
dest.Right = actionFloatAttr(start, "Right")
dest.Top = actionFloatAttr(start, "Top")
dest.Bottom = actionFloatAttr(start, "Bottom")
dest.Zoom = actionFloatAttr(start, "Zoom")
if err := d.DecodeElement(&value, &start); err != nil {
return err
}
if value.Left != nil {
dest.Left = *value.Left
}
if value.Right != nil {
dest.Right = *value.Right
}
if value.Top != nil {
dest.Top = *value.Top
}
if value.Bottom != nil {
dest.Bottom = *value.Bottom
}
if value.Zoom != nil {
dest.Zoom = *value.Zoom
}
return nil
}
// UnmarshalXML 解析视频动作并应用默认值
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (m *Movie) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
type movie Movie
value := movie{Operator: "Play"}
if err := d.DecodeElement(&value, &start); err != nil {
return err
}
*m = Movie(value)
return nil
}
// UnmarshalXML 解析动作区域分路径并保留指令顺序
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (a *RegionArea) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
*a = RegionArea{Start: attrValue(start, "Start")}
for {
tok, err := d.Token()
if err != nil {
return err
}
switch node := tok.(type) {
case xml.StartElement:
command := RegionCommand{
Type: node.Name.Local,
Point1: attrValue(node, "Point1"),
Point2: attrValue(node, "Point2"),
Point3: attrValue(node, "Point3"),
EllipseSize: attrValue(node, "EllipseSize"),
RotationAngle: attrValue(node, "RotationAngle"),
LargeArc: attrValue(node, "LargeArc"),
SweepDirection: attrValue(node, "SweepDirection"),
EndPoint: attrValue(node, "EndPoint"),
}
a.Command = append(a.Command, command)
if err := d.Skip(); err != nil {
return err
}
case xml.EndElement:
if node.Name.Local == start.Name.Local {
return nil
}
}
}
}
// actionFloatAttr 获取动作浮点属性
// 入参: start 起始节点, name 属性名
// 返回: float64 属性值
func actionFloatAttr(start xml.StartElement, name string) float64 {
value, _ := strconv.ParseFloat(attrValue(start, name), 64)
return value
}
+196 -17
View File
@@ -23,26 +23,32 @@ type Document struct {
Pages Pages `xml:"Pages"`
Outlines Outlines `xml:"Outlines"`
Permissions Permissions `xml:"Permissions"`
Actions []Action `xml:"Actions>Action"`
Bookmarks Bookmarks `xml:"Bookmarks"`
Annotations string `xml:"Annotations"`
Signatures string `xml:"Signatures"`
Attachments Attachments `xml:"Attachments"`
CustomTags CustomTags `xml:"CustomTags"`
Extensions Extensions `xml:"Extensions"`
}
// Extensions 扩展集合
type Extensions struct {
XMLName xml.Name `xml:"Extensions"`
Path string `xml:",chardata"`
Extension []Extension `xml:"Extension"`
}
// Extension 扩展信息
type Extension struct {
AppName string `xml:"AppName,attr"`
Company string `xml:"Company,attr"`
AppVersion string `xml:"AppVersion,attr"`
Date string `xml:"Date,attr"`
RefID string `xml:"RefId,attr"`
Property []Property `xml:"Property"`
Data string `xml:"Data"`
AppName string `xml:"AppName,attr"`
Company string `xml:"Company,attr"`
AppVersion string `xml:"AppVersion,attr"`
Date string `xml:"Date,attr"`
RefID string `xml:"RefId,attr"`
Property []Property `xml:"Property"`
ExtendData []string `xml:"ExtendData"`
Data []ExtensionData `xml:"Data"`
}
// Property 扩展属性
@@ -52,16 +58,45 @@ type Property struct {
Type string `xml:"Type,attr"`
}
// ExtensionData 扩展数据
type ExtensionData struct {
Attr []xml.Attr `xml:",any,attr"`
Content string `xml:",innerxml"`
}
// Attachments 附件集合
type Attachments struct {
XMLName xml.Name `xml:"Attachments"`
Path string `xml:",chardata"`
Attachment []Attachment `xml:"Attachment"`
}
// Attachment 附件信息
type Attachment struct {
Name string `xml:"Name,attr"`
File string `xml:"File,attr"`
ID string `xml:"ID,attr"`
ID string `xml:"ID,attr"`
Name string `xml:"Name,attr"`
Format string `xml:"Format,attr"`
CreationDate string `xml:"CreationDate,attr"`
ModDate string `xml:"ModDate,attr"`
Size *float64 `xml:"Size,attr"`
Visible bool `xml:"Visible,attr"`
Usage string `xml:"Usage,attr"`
FileLoc string `xml:"FileLoc"`
}
// CustomTags 自定义标引集合
type CustomTags struct {
XMLName xml.Name `xml:"CustomTags"`
Path string `xml:",chardata"`
CustomTag []CustomTag `xml:"CustomTag"`
}
// CustomTag 自定义标引
type CustomTag struct {
TypeID string `xml:"TypeID,attr"`
NameSpace string `xml:"NameSpace,attr"`
SchemaLoc string `xml:"SchemaLoc"`
FileLoc string `xml:"FileLoc"`
}
// CommonData 文档公共数据
@@ -101,6 +136,17 @@ type TemplatePage struct {
ZOrder string `xml:"ZOrder,attr"`
}
// Bookmarks 书签集合
type Bookmarks struct {
Bookmark []Bookmark `xml:"Bookmark"`
}
// Bookmark 书签
type Bookmark struct {
Name string `xml:"Name,attr"`
Dest Dest `xml:"Dest"`
}
// Outlines 大纲集合
type Outlines struct {
OutlineElem []OutlineElem `xml:"OutlineElem"`
@@ -108,15 +154,148 @@ type Outlines struct {
// OutlineElem 大纲节点
type OutlineElem struct {
Title string `xml:"Title,attr"`
Count int `xml:"Count,attr"`
Actions string `xml:"Actions"`
Title string `xml:"Title,attr"`
Count int `xml:"Count,attr"`
Expanded bool `xml:"Expanded,attr"`
Actions []Action `xml:"Actions>Action"`
OutlineElem []OutlineElem `xml:"OutlineElem"`
}
// Permissions 权限声明
type Permissions struct {
Edit bool `xml:"Edit"`
Print bool `xml:"Print"`
Export bool `xml:"Export"`
Copy bool `xml:"Copy"`
Edit bool `xml:"Edit"`
Annot bool `xml:"Annot"`
Export bool `xml:"Export"`
Signature bool `xml:"Signature"`
Watermark bool `xml:"Watermark"`
PrintScreen bool `xml:"PrintScreen"`
Print bool `xml:"-"`
Copies int `xml:"-"`
Copy bool `xml:"CopyText"`
ContentRegist bool `xml:"ContentRegist"`
ValidPeriod *ValidPeriod `xml:"ValidPeriod"`
}
// ValidPeriod 文档访问有效期
type ValidPeriod struct {
StartDate string `xml:"StartDate,attr"`
EndDate string `xml:"EndDate,attr"`
}
// UnmarshalXML 解析文档并应用权限默认值
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (doc *Document) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
type document Document
value := document{Permissions: defaultPermissions()}
if err := d.DecodeElement(&value, &start); err != nil {
return err
}
*doc = Document(value)
return nil
}
// UnmarshalXML 解析附件并应用默认值
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (a *Attachment) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
type attachment Attachment
value := attachment{Visible: true, Usage: "none"}
if err := d.DecodeElement(&value, &start); err != nil {
return err
}
*a = Attachment(value)
return nil
}
// UnmarshalXML 解析大纲节点并应用默认值
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (o *OutlineElem) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
type outlineElem OutlineElem
value := outlineElem{Expanded: true}
if err := d.DecodeElement(&value, &start); err != nil {
return err
}
*o = OutlineElem(value)
return nil
}
// UnmarshalXML 解析文档权限并应用默认值
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (p *Permissions) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var value struct {
Edit *bool `xml:"Edit"`
Annot *bool `xml:"Annot"`
Export *bool `xml:"Export"`
Signature *bool `xml:"Signature"`
Watermark *bool `xml:"Watermark"`
PrintScreen *bool `xml:"PrintScreen"`
Copy *bool `xml:"CopyText"`
ContentRegist *bool `xml:"ContentRegist"`
Print *print `xml:"Print"`
ValidPeriod *ValidPeriod `xml:"ValidPeriod"`
}
if err := d.DecodeElement(&value, &start); err != nil {
return err
}
*p = defaultPermissions()
if value.Edit != nil {
p.Edit = *value.Edit
}
if value.Annot != nil {
p.Annot = *value.Annot
}
if value.Export != nil {
p.Export = *value.Export
}
if value.Signature != nil {
p.Signature = *value.Signature
}
if value.Watermark != nil {
p.Watermark = *value.Watermark
}
if value.PrintScreen != nil {
p.PrintScreen = *value.PrintScreen
}
if value.Copy != nil {
p.Copy = *value.Copy
}
if value.ContentRegist != nil {
p.ContentRegist = *value.ContentRegist
}
if value.Print != nil {
if value.Print.Printable != nil {
p.Print = *value.Print.Printable
}
if value.Print.Copies != nil {
p.Copies = *value.Print.Copies
}
}
p.ValidPeriod = value.ValidPeriod
return nil
}
// print 打印权限节点
type print struct {
Printable *bool `xml:"Printable,attr"`
Copies *int `xml:"Copies,attr"`
}
// defaultPermissions 获取默认文档权限
// 返回: Permissions 文档权限
func defaultPermissions() Permissions {
return Permissions{
Edit: true,
Annot: true,
Export: true,
Signature: true,
Watermark: true,
PrintScreen: true,
Print: true,
Copies: -1,
Copy: true,
ContentRegist: true,
}
}
+35 -18
View File
@@ -32,8 +32,9 @@ type FontFile struct {
// FontFS 内存字体文件系统
type FontFS struct {
files map[string][]byte
names []string
files map[string][]byte
names []string
candidates []fontFileCandidate
}
// NewFontFS 创建内存字体文件系统
@@ -54,6 +55,7 @@ func NewFontFS(fonts []FontFile) *FontFS {
fsys.files[name] = append([]byte(nil), font.Data...)
}
sort.Strings(fsys.names)
fsys.candidates = fontFileCandidates(fsys.names, path.Base)
return fsys
}
@@ -70,18 +72,20 @@ func (fsys *FontFS) Len() int {
// 入参: name 字体文件名
// 返回: fs.File 字体文件, error 错误信息
func (fsys *FontFS) Open(name string) (fs.File, error) {
name = cleanFontName(name)
if !fs.ValidPath(name) {
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
}
if name == "." {
return &fontDir{entries: fsys.entries()}, nil
}
data, ok := fsys.files[name]
data, ok := fsys.files[strings.ToLower(name)]
if !ok {
return nil, fs.ErrNotExist
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
}
return &fontMemFile{
Reader: bytes.NewReader(data),
info: fontFileInfo{
name: name,
name: strings.ToLower(name),
size: int64(len(data)),
},
}, nil
@@ -91,9 +95,15 @@ func (fsys *FontFS) Open(name string) (fs.File, error) {
// 入参: name 目录名
// 返回: []fs.DirEntry 目录条目, error 错误信息
func (fsys *FontFS) ReadDir(name string) ([]fs.DirEntry, error) {
name = cleanFontName(name)
if !fs.ValidPath(name) {
return nil, &fs.PathError{Op: "readdir", Path: name, Err: fs.ErrInvalid}
}
if name != "." {
return nil, fs.ErrNotExist
err := fs.ErrNotExist
if _, ok := fsys.files[strings.ToLower(name)]; ok {
err = fs.ErrInvalid
}
return nil, &fs.PathError{Op: "readdir", Path: name, Err: err}
}
return fsys.entries(), nil
}
@@ -102,10 +112,7 @@ func (fsys *FontFS) ReadDir(name string) ([]fs.DirEntry, error) {
// 入参: pattern 匹配模式
// 返回: []string 字体文件列表, error 错误信息
func (fsys *FontFS) Glob(pattern string) ([]string, error) {
if _, err := path.Match(pattern, ""); err != nil {
return nil, err
}
return fsys.match(pattern), nil
return fs.Glob(struct{ fs.FS }{fsys}, pattern)
}
// Match 匹配指定字体名称
@@ -146,11 +153,10 @@ func (fsys *FontFS) matchStyle(pattern string, bold, italic bool) []string {
// 入参: patterns 匹配模式列表, bold 是否粗体, italic 是否斜体
// 返回: []string 字体文件列表
func (fsys *FontFS) matchPatternsStyle(patterns []string, bold, italic bool) []string {
files := fontFileCandidates(fsys.names, path.Base)
matches := make([]fontFileMatch, 0, len(files))
seen := make(map[string]int, len(files))
matches := make([]fontFileMatch, 0, len(fsys.candidates))
seen := make(map[string]int, len(fsys.candidates))
for _, matcher := range newFontPatternMatchers(patterns) {
for _, file := range files {
for _, file := range fsys.candidates {
rank := matcher.rankCandidate(file)
appendFontFileMatch(&matches, seen, matcher, file, rank, bold, italic)
}
@@ -235,6 +241,7 @@ type fontPatternMatcher struct {
lowerPattern string
stem string
aliases []string
priority int
}
// newFontPatternMatcher 创建字体文件匹配器
@@ -262,8 +269,10 @@ func newFontPatternMatcher(pattern string) fontPatternMatcher {
// 返回: []fontPatternMatcher 字体文件匹配器列表
func newFontPatternMatchers(patterns []string) []fontPatternMatcher {
matchers := make([]fontPatternMatcher, 0, len(patterns))
for _, pattern := range patterns {
matchers = append(matchers, newFontPatternMatcher(pattern))
for priority, pattern := range patterns {
matcher := newFontPatternMatcher(pattern)
matcher.priority = priority
matchers = append(matchers, matcher)
}
return matchers
}
@@ -342,6 +351,7 @@ func (m fontPatternMatcher) styleSuffixNormalized(name string) string {
type fontFileMatch struct {
name string
priority int
rank int
styleRank int
sortName string
@@ -355,6 +365,7 @@ func appendFontFileMatch(matches *[]fontFileMatch, seen map[string]int, matcher
}
next := fontFileMatch{
name: file.name,
priority: matcher.priority,
rank: rank,
styleRank: fontFileStyleRank(matcher.styleSuffixNormalized(file.normalized), bold, italic),
sortName: file.lowerBase,
@@ -381,6 +392,9 @@ func sortFontFileMatches(matches []fontFileMatch) {
// 入参: left 左侧匹配结果, right 右侧匹配结果
// 返回: bool 左侧是否优先
func fontFileMatchLess(left, right fontFileMatch) bool {
if left.priority != right.priority {
return left.priority < right.priority
}
if left.rank != right.rank {
return left.rank < right.rank
}
@@ -513,6 +527,9 @@ func (d *fontDir) Close() error {
// 返回: []fs.DirEntry 目录条目, error 错误信息
func (d *fontDir) ReadDir(count int) ([]fs.DirEntry, error) {
if d.offset >= len(d.entries) {
if count <= 0 {
return nil, nil
}
return nil, io.EOF
}
if count <= 0 || d.offset+count > len(d.entries) {
+49 -78
View File
@@ -67,6 +67,30 @@ func (r *Reader) Fonts() ([]Font, error) {
// FontInfos 获取OFD字体诊断信息
// 返回: []FontInfo 字体诊断列表, error 错误信息
func (r *Renderer) FontInfos() ([]FontInfo, error) {
doc, err := r.fontInfoDocument()
if err != nil {
return nil, err
}
return r.fontInfos(doc, nil)
}
// FontInfosFromPages 从页面内容获取OFD字体诊断信息
// 入参: pages 页面内容列表
// 返回: []FontInfo 字体诊断列表, error 错误信息
func (r *Renderer) FontInfosFromPages(pages []*PageContent) ([]FontInfo, error) {
doc, err := r.fontInfoDocument()
if err != nil {
return nil, err
}
if pages == nil {
pages = []*PageContent{}
}
return r.fontInfos(doc, pages)
}
// fontInfoDocument 获取字体诊断文档结构
// 返回: *Document 文档结构, error 错误信息
func (r *Renderer) fontInfoDocument() (*Document, error) {
if r == nil || r.Reader == nil {
return nil, fmt.Errorf("ofd renderer is not initialized")
}
@@ -77,11 +101,18 @@ func (r *Renderer) FontInfos() ([]FontInfo, error) {
if doc == nil {
return nil, fmt.Errorf("ofd document is not opened")
}
return doc, nil
}
// fontInfos 获取OFD字体诊断信息
// 入参: doc 文档结构, pages 页面内容列表
// 返回: []FontInfo 字体诊断列表, error 错误信息
func (r *Renderer) fontInfos(doc *Document, pages []*PageContent) ([]FontInfo, error) {
fonts, err := r.Reader.Fonts()
if err != nil {
return nil, err
}
usage := r.fontUsage(doc)
usage := r.fontUsage(doc, pages)
infos := make([]FontInfo, 0, len(fonts)+len(usage))
seen := make(map[string]bool)
for _, font := range fonts {
@@ -137,9 +168,9 @@ func (r *Renderer) fontInfo(font Font) FontInfo {
}
return info
}
if matched, exact := r.matchFont(font.FontName, font.FamilyName); matched != "" {
info.Matched = matched
if exact {
if source, ok := r.fontSourceMatch(font.ID, &font); ok {
info.Matched = source.name
if source.exact {
info.Status = FontStatusMatched
info.Detail = "使用外部字体文件"
} else {
@@ -153,74 +184,6 @@ func (r *Renderer) fontInfo(font Font) FontInfo {
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 匹配字体文件系统中的指定样式字体文件
// 入参: fsys 字体文件系统, patterns 匹配模式列表, bold 是否粗体, italic 是否斜体
// 返回: []string 字体文件列表
@@ -240,19 +203,27 @@ func fontFSMatchesStyle(fsys fs.FS, patterns []string, bold, italic bool) []stri
}
// fontUsage 统计文档字体使用次数
// 入参: doc 文档结构
// 入参: doc 文档结构, pages 页面内容列表, nil表示读取文档页面
// 返回: map[string]int 字体使用次数
func (r *Renderer) fontUsage(doc *Document) map[string]int {
func (r *Renderer) fontUsage(doc *Document, pages []*PageContent) map[string]int {
usage := make(map[string]int)
for _, pageRef := range doc.Pages.Page {
page, err := r.Reader.PageContent(pageRef)
if err == nil {
r.countPageFonts(page, usage)
if pages == nil {
for _, pageRef := range doc.Pages.Page {
if page, err := r.Reader.PageContent(pageRef); err == nil {
r.countPageFonts(page, usage)
}
}
} else {
for _, page := range pages {
if page != nil {
r.countPageFonts(page, usage)
}
}
}
for _, tpl := range doc.CommonData.TemplatePage {
page, err := r.Reader.PageContent(Page{BaseLoc: tpl.BaseLoc})
if err == nil {
r.templatePageCache[tpl.ID] = page
r.countPageFonts(page, usage)
}
}
+183 -85
View File
@@ -22,6 +22,36 @@ import (
"github.com/tdewolff/canvas"
)
// fontSourceKind 字体来源类型
type fontSourceKind uint8
const (
fontSourceFile fontSourceKind = iota
fontSourceFS
fontSourceSystem
)
// fontSource 字体来源
type fontSource struct {
kind fontSourceKind
index int
name string
exact bool
}
// fontSourceKey 字体来源去重键
type fontSourceKey struct {
kind fontSourceKind
index int
name string
}
// fontCacheKey 字体加载缓存键
type fontCacheKey struct {
fontSourceKey
style canvas.FontStyle
}
// loadFont 加载字体
// 入参: fontID 字体ID
// 返回: *canvas.FontFamily 字体族
@@ -37,6 +67,7 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily {
if !ok {
return defaultFont
}
fontStyle := canvasFontStyle(of)
ff := canvas.NewFontFamily(of.FontName)
if of.FontFile != "" {
if fontData, err := r.Reader.ResData(of.FontFile); err == nil {
@@ -66,13 +97,6 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily {
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 {
r.FontMap[fontID] = ff
return ff
@@ -81,86 +105,157 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily {
}
return nil
}
var fontStyle canvas.FontStyle
if of.Bold {
fontStyle |= canvas.FontBold
}
if of.Italic {
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
}
for _, source := range r.fontSources(fontID, of, fontStyle) {
if loaded := r.loadFontSource(ff, source, fontStyle); loaded != nil {
r.FontMap[fontID] = loaded
r.fontSourceUsed[fontID] = source
return loaded
}
}
r.FontMap[fontID] = defaultFont
return defaultFont
}
// canvasFontStyle 获取Canvas字体样式
// 入参: font OFD字体定义
// 返回: canvas.FontStyle Canvas字体样式
func canvasFontStyle(font *Font) canvas.FontStyle {
var style canvas.FontStyle
if font.Bold {
style |= canvas.FontBold
}
if font.Italic {
style |= canvas.FontItalic
}
return style
}
// fontSources 获取字体来源列表
// 入参: fontID 字体ID, font OFD字体定义, style Canvas字体样式
// 返回: []fontSource 字体来源列表
func (r *Renderer) fontSources(fontID string, font *Font, style canvas.FontStyle) []fontSource {
if sources, ok := r.fontSourceCache[fontID]; ok {
return sources
}
bold := style&canvas.FontBold != 0
italic := style&canvas.FontItalic != 0
patterns := fontFilePatterns(font.FontName, font.FamilyName)
sources := make([]fontSource, 0)
seen := make(map[fontSourceKey]bool)
for _, dir := range r.fontDirs {
for _, name := range r.matchFontFiles(dir, patterns, bold, italic) {
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFile, name: name, exact: true})
}
}
for index, fsys := range r.fontFS {
for _, name := range fontFSMatchesStyle(fsys, patterns, bold, italic) {
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: name, exact: true})
}
}
if !canLoadSystemFonts() {
for index, fsys := range r.fontFS {
names, _ := fs.Glob(fsys, "*")
for _, name := range names {
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: name})
}
}
r.fontSourceCache[fontID] = sources
return sources
}
systemDirs := systemFontDirs()
for _, name := range []string{font.FamilyName, font.FontName} {
for _, systemName := range fontSystemNames(name) {
systemPatterns := fontFilePatterns(systemName)
for _, dir := range systemDirs {
for _, match := range r.matchFontFiles(dir, systemPatterns, bold, italic) {
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFile, name: match, exact: true})
}
}
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceSystem, name: systemName, exact: true})
}
}
r.fontSourceCache[fontID] = sources
return sources
}
// appendFontSource 追加字体来源
// 入参: sources 字体来源列表, seen 去重映射, source 字体来源
// 返回: []fontSource 字体来源列表
func appendFontSource(sources []fontSource, seen map[fontSourceKey]bool, source fontSource) []fontSource {
if source.name == "" {
return sources
}
key := fontSourceKey{kind: source.kind, index: source.index, name: source.name}
if seen[key] {
return sources
}
seen[key] = true
return append(sources, source)
}
// fontSourceMatch 获取匹配的字体来源
// 入参: fontID 字体ID, font OFD字体定义
// 返回: fontSource 字体来源, bool 是否匹配
func (r *Renderer) fontSourceMatch(fontID string, font *Font) (fontSource, bool) {
if source, ok := r.fontSourceUsed[fontID]; ok {
return source, true
}
style := canvasFontStyle(font)
for _, source := range r.fontSources(fontID, font, style) {
key := fontCacheKey{fontSourceKey: fontSourceKey{kind: source.kind, index: source.index, name: source.name}, style: style}
if cached, ok := r.fontCache[key]; ok {
if cached != nil {
return source, true
}
continue
}
family := canvas.NewFontFamily(font.FontName)
if r.loadFontSource(family, source, style) != nil {
return source, true
}
}
return fontSource{}, false
}
// loadFontSource 加载字体来源
// 入参: family 字体族, source 字体来源, style Canvas字体样式
// 返回: *canvas.FontFamily 字体族
func (r *Renderer) loadFontSource(family *canvas.FontFamily, source fontSource, style canvas.FontStyle) *canvas.FontFamily {
key := fontCacheKey{fontSourceKey: fontSourceKey{kind: source.kind, index: source.index, name: source.name}, style: style}
if cached, ok := r.fontCache[key]; ok {
return cached
}
var err error
switch source.kind {
case fontSourceFile:
err = family.LoadFontFile(source.name, style)
case fontSourceFS:
var data []byte
data, err = fs.ReadFile(r.fontFS[source.index], source.name)
if err == nil {
err = family.LoadFont(data, 0, style)
}
case fontSourceSystem:
err = family.LoadSystemFont(source.name, style)
}
if err != nil {
r.fontCache[key] = nil
return nil
}
r.fontCache[key] = family
return family
}
// matchFontFiles 查找字体文件
// 入参: dir 目录, patterns 模式列表, bold 是否粗体, italic 是否斜体
// 返回: []string 文件列表
func (r *Renderer) matchFontFiles(dir string, patterns []string, bold, italic bool) []string {
files, _ := filepath.Glob(filepath.Join(dir, "*"))
candidates := fontFileCandidates(files, filepath.Base)
candidates, ok := r.fontDirCandidates[dir]
if !ok {
files, _ := filepath.Glob(filepath.Join(dir, "*"))
candidates = fontFileCandidates(files, filepath.Base)
r.fontDirCandidates[dir] = candidates
}
matches := make([]fontFileMatch, 0, len(candidates))
index := make(map[string]int, len(candidates))
for _, matcher := range newFontPatternMatchers(patterns) {
@@ -173,15 +268,18 @@ func (r *Renderer) matchFontFiles(dir string, patterns []string, bold, italic bo
return fontFileMatchNames(matches)
}
// systemFontDir 获取系统字体目录
// 返回: string 字体目录
func systemFontDir() string {
// systemFontDirs 获取系统字体目录
// 返回: []string 字体目录列表
func systemFontDirs() []string {
switch runtime.GOOS {
case "linux":
return `/usr/share/fonts`
case "android":
return []string{"/product/fonts", "/system/fonts"}
case "darwin":
return `/Library/Fonts`
default:
return `C:\Windows\Fonts`
return []string{`/Library/Fonts`}
case "linux":
return []string{`/usr/share/fonts`}
case "windows":
return []string{`C:\Windows\Fonts`}
}
return nil
}
+12 -7
View File
@@ -15,6 +15,7 @@
package ofdgo
import (
"fmt"
"math"
"strconv"
"strings"
@@ -30,14 +31,18 @@ type Box struct {
// 返回: Box 矩形对象, error 错误信息
func ParseBox(s string) (Box, error) {
parts := strings.Fields(s)
if len(parts) < 4 {
return Box{}, nil
if len(parts) != 4 {
return Box{}, fmt.Errorf("invalid box: %s", s)
}
x, _ := strconv.ParseFloat(parts[0], 64)
y, _ := strconv.ParseFloat(parts[1], 64)
w, _ := strconv.ParseFloat(parts[2], 64)
h, _ := strconv.ParseFloat(parts[3], 64)
return Box{X: x, Y: y, W: w, H: h}, nil
var values [4]float64
for i, part := range parts {
value, err := strconv.ParseFloat(part, 64)
if err != nil {
return Box{}, fmt.Errorf("invalid box: %s", s)
}
values[i] = value
}
return Box{X: values[0], Y: values[1], W: values[2], H: values[3]}, nil
}
// Matrix 2D仿射变换矩阵
+27
View File
@@ -20,15 +20,28 @@ import (
"fmt"
"image"
"image/color"
_ "image/gif"
canvasimage "github.com/tdewolff/canvas/image"
_ "github.com/xiaoqidun/jbig2"
_ "golang.org/x/image/bmp"
_ "golang.org/x/image/tiff"
)
// decodeImageData 解码图片数据
// 入参: data 图片数据
// 返回: image.Image 图片对象, string 图片格式, error 错误信息
func decodeImageData(data []byte) (image.Image, string, error) {
if isJPEGData(data) {
if img, err := canvasimage.NewJPEGImage(bytes.NewReader(data)); err == nil {
return img, "jpeg", nil
}
}
if isPNGData(data) {
if img, err := canvasimage.NewPNGImage(bytes.NewReader(data)); err == nil {
return img, "png", nil
}
}
img, format, err := image.Decode(bytes.NewReader(data))
if err == nil {
return img, normalizeSealType(format), nil
@@ -39,6 +52,20 @@ func decodeImageData(data []byte) (image.Image, string, error) {
return nil, "", err
}
// isJPEGData 判断是否为JPEG图片数据
// 入参: data 图片数据
// 返回: bool 是否为JPEG图片数据
func isJPEGData(data []byte) bool {
return len(data) >= 2 && data[0] == 0xFF && data[1] == 0xD8
}
// isPNGData 判断是否为PNG图片数据
// 入参: data 图片数据
// 返回: bool 是否为PNG图片数据
func isPNGData(data []byte) bool {
return bytes.HasPrefix(data, []byte("\x89PNG\r\n\x1a\n"))
}
// decodeImageConfigData 解码图片尺寸
// 入参: data 图片数据
// 返回: image.Config 图片尺寸, string 图片格式, error 错误信息
+129 -280
View File
@@ -19,6 +19,76 @@ import (
"strconv"
)
// graphicObjectTarget 图形对象集合
type graphicObjectTarget struct {
objects *[]GraphicObject
text *[]TextObject
path *[]PathObject
image *[]ImageObject
composite *[]CompositeGraphicUnit
}
// decodeGraphicObject 解析图形对象
// 入参: d XML解码器, start 起始节点, target 图形对象集合
// 返回: bool 是否为图形对象, error 错误信息
func decodeGraphicObject(d *xml.Decoder, start xml.StartElement, target graphicObjectTarget) (bool, error) {
switch start.Name.Local {
case "TextObject":
var obj TextObject
if err := d.DecodeElement(&obj, &start); err != nil {
return true, err
}
*target.text = append(*target.text, obj)
*target.objects = append(*target.objects, GraphicObject{Type: start.Name.Local, TextObject: obj})
case "PathObject":
var obj PathObject
if err := d.DecodeElement(&obj, &start); err != nil {
return true, err
}
*target.path = append(*target.path, obj)
*target.objects = append(*target.objects, GraphicObject{Type: start.Name.Local, PathObject: obj})
case "ImageObject":
var obj ImageObject
if err := d.DecodeElement(&obj, &start); err != nil {
return true, err
}
*target.image = append(*target.image, obj)
*target.objects = append(*target.objects, GraphicObject{Type: start.Name.Local, ImageObject: obj})
case "CompositeGraphicUnit", "CompositeObject":
var obj CompositeGraphicUnit
if err := d.DecodeElement(&obj, &start); err != nil {
return true, err
}
*target.composite = append(*target.composite, obj)
*target.objects = append(*target.objects, GraphicObject{Type: start.Name.Local, CompositeGraphicUnit: obj})
default:
return false, nil
}
return true, nil
}
// decodeObjectContainer 解析图形对象容器
// 入参: d XML解码器, start 起始节点, decode 对象解码函数
// 返回: error 错误信息
func decodeObjectContainer(d *xml.Decoder, start xml.StartElement, decode func(*xml.Decoder, xml.StartElement) error) error {
for {
tok, err := d.Token()
if err != nil {
return err
}
switch node := tok.(type) {
case xml.StartElement:
if err := decode(d, node); err != nil {
return err
}
case xml.EndElement:
if node.Name.Local == start.Name.Local {
return nil
}
}
}
}
// UnmarshalXML 解析图层并保留对象顺序
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
@@ -26,85 +96,27 @@ func (l *Layer) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
*l = Layer{}
l.ID = attrValue(start, "ID")
l.DrawParam = attrValue(start, "DrawParam")
for {
tok, err := d.Token()
if err != nil {
return err
}
switch node := tok.(type) {
case xml.StartElement:
if err := l.decodeObject(d, node); err != nil {
return err
}
case xml.EndElement:
if node.Name.Local == start.Name.Local {
return nil
}
}
}
return decodeObjectContainer(d, start, l.decodeObject)
}
// decodeObject 解析图层子对象
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (l *Layer) decodeObject(d *xml.Decoder, start xml.StartElement) error {
switch start.Name.Local {
case "TextObject":
var obj TextObject
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
l.TextObject = append(l.TextObject, obj)
l.Objects = append(l.Objects, GraphicObject{Type: start.Name.Local, TextObject: obj})
case "PathObject":
var obj PathObject
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
l.PathObject = append(l.PathObject, obj)
l.Objects = append(l.Objects, GraphicObject{Type: start.Name.Local, PathObject: obj})
case "ImageObject":
var obj ImageObject
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
l.ImageObject = append(l.ImageObject, obj)
l.Objects = append(l.Objects, GraphicObject{Type: start.Name.Local, ImageObject: obj})
case "CompositeGraphicUnit", "CompositeObject":
var obj CompositeGraphicUnit
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
l.CompositeGraphicUnit = append(l.CompositeGraphicUnit, obj)
l.Objects = append(l.Objects, GraphicObject{Type: start.Name.Local, CompositeGraphicUnit: obj})
case "PageBlock":
return l.decodePageBlock(d, start)
default:
return d.Skip()
target := graphicObjectTarget{
objects: &l.Objects,
text: &l.TextObject,
path: &l.PathObject,
image: &l.ImageObject,
composite: &l.CompositeGraphicUnit,
}
return nil
}
// decodePageBlock 解析页块子对象
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (l *Layer) decodePageBlock(d *xml.Decoder, start xml.StartElement) error {
for {
tok, err := d.Token()
if err != nil {
return err
}
switch node := tok.(type) {
case xml.StartElement:
if err := l.decodeObject(d, node); err != nil {
return err
}
case xml.EndElement:
if node.Name.Local == start.Name.Local {
return nil
}
}
if decoded, err := decodeGraphicObject(d, start, target); decoded || err != nil {
return err
}
if start.Name.Local == "PageBlock" {
return decodeObjectContainer(d, start, l.decodeObject)
}
return d.Skip()
}
// UnmarshalXML 解析复合图元并保留对象顺序
@@ -128,178 +140,73 @@ func (c *CompositeGraphicUnit) UnmarshalXML(d *xml.Decoder, start xml.StartEleme
c.Visible = &visible
}
}
for {
tok, err := d.Token()
if err != nil {
return err
}
switch node := tok.(type) {
case xml.StartElement:
if err := c.decodeObject(d, node); err != nil {
return err
}
case xml.EndElement:
if node.Name.Local == start.Name.Local {
return nil
}
}
}
return decodeObjectContainer(d, start, c.decodeObject)
}
// decodeObject 解析复合图元子对象
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (c *CompositeGraphicUnit) decodeObject(d *xml.Decoder, start xml.StartElement) error {
target := graphicObjectTarget{
objects: &c.Objects,
text: &c.TextObject,
path: &c.PathObject,
image: &c.ImageObject,
composite: &c.CompositeGraphicUnit,
}
if decoded, err := decodeGraphicObject(d, start, target); decoded || err != nil {
return err
}
switch start.Name.Local {
case "TextObject":
var obj TextObject
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
c.TextObject = append(c.TextObject, obj)
c.Objects = append(c.Objects, GraphicObject{Type: start.Name.Local, TextObject: obj})
case "PathObject":
var obj PathObject
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
c.PathObject = append(c.PathObject, obj)
c.Objects = append(c.Objects, GraphicObject{Type: start.Name.Local, PathObject: obj})
case "ImageObject":
var obj ImageObject
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
c.ImageObject = append(c.ImageObject, obj)
c.Objects = append(c.Objects, GraphicObject{Type: start.Name.Local, ImageObject: obj})
case "CompositeGraphicUnit", "CompositeObject":
var obj CompositeGraphicUnit
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
c.CompositeGraphicUnit = append(c.CompositeGraphicUnit, obj)
c.Objects = append(c.Objects, GraphicObject{Type: start.Name.Local, CompositeGraphicUnit: obj})
case "Clips":
var clips Clips
if err := d.DecodeElement(&clips, &start); err != nil {
return err
}
c.Clips = &clips
case "Actions":
var actions struct {
Action []Action `xml:"Action"`
}
if err := d.DecodeElement(&actions, &start); err != nil {
return err
}
c.Actions = actions.Action
case "Content", "PageBlock":
return c.decodePageBlock(d, start)
return decodeObjectContainer(d, start, c.decodeObject)
default:
return d.Skip()
}
return nil
}
// decodePageBlock 解析页块子对象
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (c *CompositeGraphicUnit) decodePageBlock(d *xml.Decoder, start xml.StartElement) error {
for {
tok, err := d.Token()
if err != nil {
return err
}
switch node := tok.(type) {
case xml.StartElement:
if err := c.decodeObject(d, node); err != nil {
return err
}
case xml.EndElement:
if node.Name.Local == start.Name.Local {
return nil
}
}
}
}
// UnmarshalXML 解析注释外观并保留对象顺序
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (a *Appearance) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
*a = Appearance{}
a.Boundary = attrValue(start, "Boundary")
for {
tok, err := d.Token()
if err != nil {
return err
}
switch node := tok.(type) {
case xml.StartElement:
if err := a.decodeObject(d, node); err != nil {
return err
}
case xml.EndElement:
if node.Name.Local == start.Name.Local {
return nil
}
}
}
return decodeObjectContainer(d, start, a.decodeObject)
}
// decodeObject 解析注释外观子对象
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (a *Appearance) decodeObject(d *xml.Decoder, start xml.StartElement) error {
switch start.Name.Local {
case "TextObject":
var obj TextObject
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
a.TextObject = append(a.TextObject, obj)
a.Objects = append(a.Objects, GraphicObject{Type: start.Name.Local, TextObject: obj})
case "PathObject":
var obj PathObject
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
a.PathObject = append(a.PathObject, obj)
a.Objects = append(a.Objects, GraphicObject{Type: start.Name.Local, PathObject: obj})
case "ImageObject":
var obj ImageObject
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
a.ImageObject = append(a.ImageObject, obj)
a.Objects = append(a.Objects, GraphicObject{Type: start.Name.Local, ImageObject: obj})
case "CompositeGraphicUnit", "CompositeObject":
var obj CompositeGraphicUnit
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
a.CompositeGraphicUnit = append(a.CompositeGraphicUnit, obj)
a.Objects = append(a.Objects, GraphicObject{Type: start.Name.Local, CompositeGraphicUnit: obj})
case "PageBlock":
return a.decodePageBlock(d, start)
default:
return d.Skip()
target := graphicObjectTarget{
objects: &a.Objects,
text: &a.TextObject,
path: &a.PathObject,
image: &a.ImageObject,
composite: &a.CompositeGraphicUnit,
}
return nil
}
// decodePageBlock 解析注释外观页块子对象
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (a *Appearance) decodePageBlock(d *xml.Decoder, start xml.StartElement) error {
for {
tok, err := d.Token()
if err != nil {
return err
}
switch node := tok.(type) {
case xml.StartElement:
if err := a.decodeObject(d, node); err != nil {
return err
}
case xml.EndElement:
if node.Name.Local == start.Name.Local {
return nil
}
}
if decoded, err := decodeGraphicObject(d, start, target); decoded || err != nil {
return err
}
if start.Name.Local == "PageBlock" {
return decodeObjectContainer(d, start, a.decodeObject)
}
return d.Skip()
}
// UnmarshalXML 解析图案单元内容并保留对象顺序
@@ -307,85 +214,27 @@ func (a *Appearance) decodePageBlock(d *xml.Decoder, start xml.StartElement) err
// 返回: error 错误信息
func (p *PatternContent) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
*p = PatternContent{}
for {
tok, err := d.Token()
if err != nil {
return err
}
switch node := tok.(type) {
case xml.StartElement:
if err := p.decodeObject(d, node); err != nil {
return err
}
case xml.EndElement:
if node.Name.Local == start.Name.Local {
return nil
}
}
}
return decodeObjectContainer(d, start, p.decodeObject)
}
// decodeObject 解析图案单元内容子对象
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (p *PatternContent) decodeObject(d *xml.Decoder, start xml.StartElement) error {
switch start.Name.Local {
case "TextObject":
var obj TextObject
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
p.TextObject = append(p.TextObject, obj)
p.Objects = append(p.Objects, GraphicObject{Type: start.Name.Local, TextObject: obj})
case "PathObject":
var obj PathObject
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
p.PathObject = append(p.PathObject, obj)
p.Objects = append(p.Objects, GraphicObject{Type: start.Name.Local, PathObject: obj})
case "ImageObject":
var obj ImageObject
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
p.ImageObject = append(p.ImageObject, obj)
p.Objects = append(p.Objects, GraphicObject{Type: start.Name.Local, ImageObject: obj})
case "CompositeGraphicUnit", "CompositeObject":
var obj CompositeGraphicUnit
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
p.CompositeGraphicUnit = append(p.CompositeGraphicUnit, obj)
p.Objects = append(p.Objects, GraphicObject{Type: start.Name.Local, CompositeGraphicUnit: obj})
case "PageBlock":
return p.decodePageBlock(d, start)
default:
return d.Skip()
target := graphicObjectTarget{
objects: &p.Objects,
text: &p.TextObject,
path: &p.PathObject,
image: &p.ImageObject,
composite: &p.CompositeGraphicUnit,
}
return nil
}
// decodePageBlock 解析图案单元内容页块子对象
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (p *PatternContent) decodePageBlock(d *xml.Decoder, start xml.StartElement) error {
for {
tok, err := d.Token()
if err != nil {
return err
}
switch node := tok.(type) {
case xml.StartElement:
if err := p.decodeObject(d, node); err != nil {
return err
}
case xml.EndElement:
if node.Name.Local == start.Name.Local {
return nil
}
}
if decoded, err := decodeGraphicObject(d, start, target); decoded || err != nil {
return err
}
if start.Name.Local == "PageBlock" {
return decodeObjectContainer(d, start, p.decodeObject)
}
return d.Skip()
}
// attrValue 获取XML属性值
+13 -8
View File
@@ -23,6 +23,7 @@ type PageContent struct {
Area PageArea `xml:"Area"`
Template []Template `xml:"Template"`
Content Content `xml:"Content"`
Actions []Action `xml:"Actions>Action"`
}
// Template 页面模板引用
@@ -58,7 +59,7 @@ type GraphicObject struct {
// Clips 裁剪区域集合
type Clips struct {
TransFlag bool `xml:"TransFlag,attr"`
TransFlag *bool `xml:"TransFlag,attr"`
Clip []Clip `xml:"Clip"`
}
@@ -97,6 +98,7 @@ type TextObject struct {
CGTransform []CGTransform `xml:"CGTransform"`
TextCode []TextCode `xml:"TextCode"`
Clips *Clips `xml:"Clips"`
Actions []Action `xml:"Actions>Action"`
}
// FillColor 填充颜色
@@ -165,6 +167,7 @@ type PathObject struct {
FillColor *FillColor `xml:"FillColor"`
AbbreviatedData string `xml:"AbbreviatedData"`
Clips *Clips `xml:"Clips"`
Actions []Action `xml:"Actions>Action"`
}
// StrokeColor 勾边颜色
@@ -207,11 +210,13 @@ type ShdColor struct {
// ImageObject 图片对象
type ImageObject struct {
ID string `xml:"ID,attr"`
Boundary string `xml:"Boundary,attr"`
ResourceID string `xml:"ResourceID,attr"`
CTM string `xml:"CTM,attr"`
Alpha *int `xml:"Alpha,attr"`
Visible *bool `xml:"Visible,attr"`
Clips *Clips `xml:"Clips"`
ID string `xml:"ID,attr"`
Boundary string `xml:"Boundary,attr"`
ResourceID string `xml:"ResourceID,attr"`
ImageMask string `xml:"ImageMask,attr"`
CTM string `xml:"CTM,attr"`
Alpha *int `xml:"Alpha,attr"`
Visible *bool `xml:"Visible,attr"`
Clips *Clips `xml:"Clips"`
Actions []Action `xml:"Actions>Action"`
}
+46 -64
View File
@@ -142,10 +142,10 @@ func parseFillColor(fillColor *FillColor) color.Color {
return nil
}
if fillColor.AxialShd != nil {
return parseAxialShdColor(fillColor.AxialShd, fillColor.Alpha)
return parseShdColor(fillColor.AxialShd.Segment, fillColor.Alpha)
}
if fillColor.RadialShd != nil {
return parseRadialShdColor(fillColor.RadialShd, fillColor.Alpha)
return parseShdColor(fillColor.RadialShd.Segment, fillColor.Alpha)
}
if strings.TrimSpace(fillColor.Value) != "" {
return parseColorWithAlpha(fillColor.Value, fillColor.Alpha)
@@ -170,10 +170,10 @@ func parseFillPaint(fillColor *FillColor, x, y, pageH, originX, originY float64)
return gradient
}
if fillColor.AxialShd != nil {
return parseAxialShdColor(fillColor.AxialShd, fillColor.Alpha)
return parseShdColor(fillColor.AxialShd.Segment, fillColor.Alpha)
}
if fillColor.RadialShd != nil {
return parseRadialShdColor(fillColor.RadialShd, fillColor.Alpha)
return parseShdColor(fillColor.RadialShd.Segment, fillColor.Alpha)
}
if strings.TrimSpace(fillColor.Value) != "" {
return parseColorWithAlpha(fillColor.Value, fillColor.Alpha)
@@ -189,10 +189,10 @@ func parseStrokeColor(strokeColor *StrokeColor) color.Color {
return nil
}
if strokeColor.AxialShd != nil {
return parseAxialShdColor(strokeColor.AxialShd, strokeColor.Alpha)
return parseShdColor(strokeColor.AxialShd.Segment, strokeColor.Alpha)
}
if strokeColor.RadialShd != nil {
return parseRadialShdColor(strokeColor.RadialShd, strokeColor.Alpha)
return parseShdColor(strokeColor.RadialShd.Segment, strokeColor.Alpha)
}
if strings.TrimSpace(strokeColor.Value) != "" {
return parseColorWithAlpha(strokeColor.Value, strokeColor.Alpha)
@@ -214,10 +214,10 @@ func parseStrokePaint(strokeColor *StrokeColor, x, y, pageH, originX, originY fl
return gradient
}
if strokeColor.AxialShd != nil {
return parseAxialShdColor(strokeColor.AxialShd, strokeColor.Alpha)
return parseShdColor(strokeColor.AxialShd.Segment, strokeColor.Alpha)
}
if strokeColor.RadialShd != nil {
return parseRadialShdColor(strokeColor.RadialShd, strokeColor.Alpha)
return parseShdColor(strokeColor.RadialShd.Segment, strokeColor.Alpha)
}
if strings.TrimSpace(strokeColor.Value) != "" {
return parseColorWithAlpha(strokeColor.Value, strokeColor.Alpha)
@@ -235,24 +235,44 @@ func patternColor(fillColor *FillColor) color.Color {
return parseColorWithAlpha(fillColor.Value, fillColor.Alpha)
}
// parseAxialShdColor 解析轴向渐变颜色
// 入参: axialShd 轴向渐变节点, alpha 透明度
// parseShdColor 解析渐变颜色
// 入参: segments 渐变分段, alpha 透明度
// 返回: color.Color 颜色对象
func parseAxialShdColor(axialShd *AxialShd, alpha *int) color.Color {
if axialShd != nil {
for _, segment := range axialShd.Segment {
if strings.TrimSpace(segment.Color.Value) == "" {
continue
}
if alpha == nil {
alpha = segment.Color.Alpha
}
return parseColorWithAlpha(segment.Color.Value, alpha)
func parseShdColor(segments []ShdSegment, alpha *int) color.Color {
for _, segment := range segments {
if strings.TrimSpace(segment.Color.Value) == "" {
continue
}
segmentAlpha := alpha
if segmentAlpha == nil {
segmentAlpha = segment.Color.Alpha
}
return parseColorWithAlpha(segment.Color.Value, segmentAlpha)
}
return color.Black
}
// parseShdSegments 解析渐变分段
// 入参: segments 渐变分段, alpha 透明度
// 返回: canvas.Grad 渐变分段
func parseShdSegments(segments []ShdSegment, alpha *int) canvas.Grad {
gradient := canvas.NewGradient()
for _, segment := range segments {
if strings.TrimSpace(segment.Color.Value) == "" {
continue
}
segmentAlpha := alpha
if segmentAlpha == nil {
segmentAlpha = segment.Color.Alpha
}
gradient.Add(segment.Position, colorToRGBA(parseColorWithAlpha(segment.Color.Value, segmentAlpha)))
}
if len(gradient) == 0 {
return nil
}
return gradient
}
// parseAxialShdGradient 解析轴向渐变
// 入参: axialShd 轴向渐变节点, alpha 透明度, x X坐标, y Y坐标, pageH 页面高度, originX 原点X坐标, originY 原点Y坐标
// 返回: canvas.Gradient 渐变对象
@@ -265,18 +285,8 @@ func parseAxialShdGradient(axialShd *AxialShd, alpha *int, x, y, pageH, originX,
if len(start) < 2 || len(end) < 2 {
return nil
}
grad := canvas.NewGradient()
for _, segment := range axialShd.Segment {
if strings.TrimSpace(segment.Color.Value) == "" {
continue
}
segmentAlpha := alpha
if segmentAlpha == nil {
segmentAlpha = segment.Color.Alpha
}
grad.Add(segment.Position, colorToRGBA(parseColorWithAlpha(segment.Color.Value, segmentAlpha)))
}
if len(grad) == 0 {
gradient := parseShdSegments(axialShd.Segment, alpha)
if gradient == nil {
return nil
}
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) {
return nil
}
return grad.ToLinear(startPoint, endPoint)
}
// parseRadialShdColor 解析径向渐变颜色
// 入参: radialShd 径向渐变节点, alpha 透明度
// 返回: color.Color 颜色对象
func parseRadialShdColor(radialShd *RadialShd, alpha *int) color.Color {
if radialShd != nil {
for _, segment := range radialShd.Segment {
if strings.TrimSpace(segment.Color.Value) == "" {
continue
}
if alpha == nil {
alpha = segment.Color.Alpha
}
return parseColorWithAlpha(segment.Color.Value, alpha)
}
}
return color.Black
return gradient.ToLinear(startPoint, endPoint)
}
// parseRadialShdGradient 解析径向渐变
@@ -317,23 +309,13 @@ func parseRadialShdGradient(radialShd *RadialShd, alpha *int, x, y, pageH, origi
if len(start) < 2 || len(end) < 2 {
return nil
}
grad := canvas.NewGradient()
for _, segment := range radialShd.Segment {
if strings.TrimSpace(segment.Color.Value) == "" {
continue
}
segmentAlpha := alpha
if segmentAlpha == nil {
segmentAlpha = segment.Color.Alpha
}
grad.Add(segment.Position, colorToRGBA(parseColorWithAlpha(segment.Color.Value, segmentAlpha)))
}
if len(grad) == 0 {
gradient := parseShdSegments(radialShd.Segment, alpha)
if gradient == nil {
return nil
}
startPoint := canvas.Point{X: x + start[0] - originX, Y: pageH - (y + start[1]) - originY}
endPoint := canvas.Point{X: x + end[0] - originX, Y: pageH - (y + end[1]) - originY}
return grad.ToRadial(startPoint, radialShd.StartRadius, endPoint, radialShd.EndRadius)
return gradient.ToRadial(startPoint, radialShd.StartRadius, endPoint, radialShd.EndRadius)
}
// colorToRGBA 转换颜色对象
+99 -4
View File
@@ -176,7 +176,7 @@ func (r *Reader) loadRes(resPath string) {
if resPath == "" {
return
}
fullPath := path.Join(r.RootDir, resPath)
fullPath := r.ResPath(resPath)
data, err := r.readFile(fullPath)
if err != nil {
return
@@ -235,7 +235,7 @@ func resolveResourcePath(resPath, baseLoc, filePath string) string {
// 入参: page 页面对象
// 返回: *PageContent 页面内容, error 错误信息
func (r *Reader) PageContent(page Page) (*PageContent, error) {
fullPath := path.Join(r.RootDir, page.BaseLoc)
fullPath := r.ResPath(page.BaseLoc)
data, err := r.readFile(fullPath)
if err != nil {
return nil, err
@@ -260,7 +260,9 @@ func (r *Reader) ResPath(resLink string) string {
if resLink == "" {
return ""
}
resLink = strings.TrimPrefix(resLink, "/")
if strings.HasPrefix(resLink, "/") {
return cleanPackagePath(resLink)
}
resLink = path.Clean(resLink)
rootDir := strings.TrimPrefix(strings.ReplaceAll(r.RootDir, "\\", "/"), "/")
if rootDir != "" && (resLink == rootDir || strings.HasPrefix(resLink, rootDir+"/")) {
@@ -277,6 +279,21 @@ func (r *Reader) ResData(resLink string) ([]byte, error) {
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版本号
// 返回: string 版本号
func (r *Reader) Version() string {
@@ -324,6 +341,26 @@ func (r *Reader) Outlines() ([]OutlineElem, error) {
return doc.Outlines.OutlineElem, nil
}
// Actions 获取文档动作
// 返回: []Action 动作列表, error 错误信息
func (r *Reader) Actions() ([]Action, error) {
doc, err := r.Doc()
if err != nil {
return nil, err
}
return doc.Actions, nil
}
// Bookmarks 获取文档书签
// 返回: []Bookmark 书签列表, error 错误信息
func (r *Reader) Bookmarks() ([]Bookmark, error) {
doc, err := r.Doc()
if err != nil {
return nil, err
}
return doc.Bookmarks.Bookmark, nil
}
// Attachments 获取附件列表
// 返回: []Attachment 附件列表, error 错误信息
func (r *Reader) Attachments() ([]Attachment, error) {
@@ -331,9 +368,50 @@ func (r *Reader) Attachments() ([]Attachment, error) {
if err != nil {
return nil, err
}
if strings.TrimSpace(doc.Attachments.Path) != "" && doc.Attachments.Attachment == nil {
var attachments Attachments
partPath, err := r.readDocumentPart(doc.Attachments.Path, &attachments)
if err != nil {
return nil, err
}
for i := range attachments.Attachment {
attachment := &attachments.Attachment[i]
attachment.FileLoc = resolveResourcePath(partPath, "", attachment.FileLoc)
}
if attachments.Attachment == nil {
attachments.Attachment = []Attachment{}
}
doc.Attachments.Attachment = attachments.Attachment
}
return doc.Attachments.Attachment, nil
}
// CustomTags 获取自定义标引
// 返回: []CustomTag 自定义标引列表, error 错误信息
func (r *Reader) CustomTags() ([]CustomTag, error) {
doc, err := r.Doc()
if err != nil {
return nil, err
}
if strings.TrimSpace(doc.CustomTags.Path) != "" && doc.CustomTags.CustomTag == nil {
var customTags CustomTags
partPath, err := r.readDocumentPart(doc.CustomTags.Path, &customTags)
if err != nil {
return nil, err
}
for i := range customTags.CustomTag {
tag := &customTags.CustomTag[i]
tag.SchemaLoc = resolveResourcePath(partPath, "", tag.SchemaLoc)
tag.FileLoc = resolveResourcePath(partPath, "", tag.FileLoc)
}
if customTags.CustomTag == nil {
customTags.CustomTag = []CustomTag{}
}
doc.CustomTags.CustomTag = customTags.CustomTag
}
return doc.CustomTags.CustomTag, nil
}
// CustomDatas 获取自定义数据
// 返回: []CustomData 自定义数据列表, error 错误信息
func (r *Reader) CustomDatas() ([]CustomData, error) {
@@ -347,12 +425,29 @@ func (r *Reader) CustomDatas() ([]CustomData, error) {
return info.CustomDatas.CustomData, nil
}
// Extensions 获取分列扩展项
// Extensions 获取扩展项
// 返回: []Extension 扩展项列表, error 错误信息
func (r *Reader) Extensions() ([]Extension, error) {
doc, err := r.Doc()
if err != nil {
return nil, err
}
if strings.TrimSpace(doc.Extensions.Path) != "" && doc.Extensions.Extension == nil {
var extensions Extensions
partPath, err := r.readDocumentPart(doc.Extensions.Path, &extensions)
if err != nil {
return nil, err
}
for i := range extensions.Extension {
extension := &extensions.Extension[i]
for j := range extension.ExtendData {
extension.ExtendData[j] = resolveResourcePath(partPath, "", extension.ExtendData[j])
}
}
if extensions.Extension == nil {
extensions.Extension = []Extension{}
}
doc.Extensions.Extension = extensions.Extension
}
return doc.Extensions.Extension, nil
}
+23 -3
View File
@@ -33,8 +33,15 @@ type Renderer struct {
FontMap map[string]*canvas.FontFamily
FontGIDMap 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
fontFS []fs.FS
decodeImages bool
}
// RendererOption 渲染器配置选项
@@ -44,6 +51,15 @@ type RendererOption func(*Renderer)
// 入参: page 页面内容
// 返回: *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)
if err != nil {
return nil, err
@@ -51,7 +67,7 @@ func (r *Renderer) RenderPage(page *PageContent) (*canvas.Canvas, error) {
width, height := box.W, box.H
c := canvas.New(width, height)
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 c, nil
@@ -81,7 +97,9 @@ func (r *Renderer) GetPageBox(page *PageContent) (Box, error) {
// 入参: ctx 画布上下文, page 页面内容
// 返回: 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 渲染页面到指定上下文
@@ -165,5 +183,7 @@ func (r *Renderer) childRenderer(reader *Reader) *Renderer {
if len(r.fontFS) > 0 {
opts = append(opts, WithFontFS(r.fontFS...))
}
return NewRenderer(reader, opts...)
renderer := NewRenderer(reader, opts...)
renderer.decodeImages = r.decodeImages
return renderer
}
+214 -23
View File
@@ -15,15 +15,20 @@
package ofdgo
import (
"bufio"
"image"
"image/color"
"io"
"github.com/tdewolff/canvas"
canvasimage "github.com/tdewolff/canvas/image"
"github.com/tdewolff/canvas/renderers/rasterizer"
"golang.org/x/image/draw"
)
// renderImage 渲染图片
// 入参: ctx 画布上下文, obj 图片对象, pageH 页面高度, parentCTM 父级CTM, boundaryInCTM 边界是否参与父级CTM
func (r *Renderer) renderImage(ctx *canvas.Context, obj ImageObject, pageH float64, parentCTM *Matrix, boundaryInCTM bool) {
// 入参: ctx 画布上下文, obj 图片对象, pageH 页面高度, parentCTM 父级CTM, boundaryInCTM 边界是否参与父级CTM, parentClip 父级裁剪路径
func (r *Renderer) renderImage(ctx *canvas.Context, obj ImageObject, pageH float64, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) {
if obj.Visible != nil && !*obj.Visible {
return
}
@@ -31,27 +36,30 @@ func (r *Renderer) renderImage(ctx *canvas.Context, obj ImageObject, pageH float
if !ok {
return
}
imgData, err := r.Reader.ResData(resPath)
if err != nil {
return
}
img, _, err := decodeImageData(imgData)
img, err := r.decodeImageResource(resPath)
if err != nil {
return
}
box, _ := ParseBox(obj.Boundary)
if maskPath, ok := r.Reader.ResMap[obj.ImageMask]; ok {
if mask, err := r.decodeImageResource(maskPath); err == nil {
img = imageWithMask(img, mask)
}
}
img = imageWithAlpha(img, obj.Alpha)
imgBounds := img.Bounds()
imgW, imgH := float64(imgBounds.Dx()), float64(imgBounds.Dy())
if imgW <= 0 || imgH <= 0 {
return
}
img = imageWithAlpha(img, obj.Alpha)
pad := 0
img, pad = imageWithTransparentEdge(img)
ctm := NewMatrix(obj.CTM)
if obj.CTM == "" {
ctm = Matrix{a: box.W, d: box.H}
}
objectCTM := ctm
if parentCTM != nil {
objectCTM = parentCTM.Multiply(ctm)
}
var m canvas.Matrix
if boundaryInCTM && parentCTM != nil {
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},
}
}
clipPath := intersectClipPath(parentClip, r.buildClipPath(obj.Clips, pageH, box.X, box.Y, objectCTM))
img = imageWithClip(img, clipPath, m)
img, pad := imageWithTransparentEdge(img)
if pad > 0 {
p := float64(pad)
m[0][2] -= m[0][0]*p + m[0][1]*p
@@ -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))
}
// imageWithMask 应用图片蒙版
// 入参: img 图片对象, mask 蒙版图片
// 返回: image.Image 蒙版处理后的图片对象
func imageWithMask(img, mask image.Image) image.Image {
if img == nil || mask == nil {
return img
}
bounds := img.Bounds()
if bounds.Empty() || mask.Bounds().Empty() {
return img
}
source := imagePixelSource(img)
maskSource := imagePixelSource(mask)
maskBounds := maskSource.Bounds()
var opacity image.Image = maskSource
if bounds.Size() != maskBounds.Size() {
resized := image.NewGray(bounds)
draw.CatmullRom.Scale(resized, bounds, maskSource, maskBounds, draw.Src, nil)
opacity = resized
maskBounds = bounds
}
out := image.NewNRGBA(bounds)
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
mx := maskBounds.Min.X + x - bounds.Min.X
my := maskBounds.Min.Y + y - bounds.Min.Y
a := color.GrayModel.Convert(opacity.At(mx, my)).(color.Gray).Y
c := imageNRGBAAt(source, x, y)
c.A = uint8(int(c.A) * int(a) / 255)
out.SetNRGBA(x, y, c)
}
}
return out
}
// imageWithClip 应用图片裁剪区域
// 入参: img 图片对象, clipPath 裁剪路径, m 图片变换矩阵
// 返回: image.Image 裁剪后的图片对象
func imageWithClip(img image.Image, clipPath *canvas.Path, m canvas.Matrix) image.Image {
if img == nil || clipPath == nil || m.Det() == 0 {
return img
}
bounds := img.Bounds()
w, h := bounds.Dx(), bounds.Dy()
if w == 0 || h == 0 {
return img
}
p0 := m.Dot(canvas.Point{})
p1 := m.Dot(canvas.Point{X: float64(w)})
p2 := m.Dot(canvas.Point{X: float64(w), Y: float64(h)})
p3 := m.Dot(canvas.Point{Y: float64(h)})
imagePath := &canvas.Path{}
imagePath.MoveTo(p0.X, p0.Y)
imagePath.LineTo(p1.X, p1.Y)
imagePath.LineTo(p2.X, p2.Y)
imagePath.LineTo(p3.X, p3.Y)
imagePath.Close()
if rect, ok := rectangularPath(clipPath); ok {
if rect.Contains(imagePath.FastBounds()) {
return img
}
} else if clipPath.Contains(imagePath) {
return img
}
clip := clipPath.Copy().Transform(m.Inv())
clipCanvas := canvas.New(float64(w), float64(h))
ctx := canvas.NewContext(clipCanvas)
ctx.SetFillColor(canvas.White)
ctx.SetStrokeColor(canvas.Transparent)
ctx.DrawPath(0, 0, clip)
mask := rasterizer.Draw(clipCanvas, canvas.DPMM(1), canvas.DefaultColorSpace)
opaque := true
for y := 0; y < h && opaque; y++ {
for x := 0; x < w; x++ {
if mask.RGBAAt(x, y).A != 255 {
opaque = false
break
}
}
}
if opaque {
return img
}
source := imagePixelSource(img)
out := image.NewNRGBA(bounds)
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
c := imageNRGBAAt(source, x, y)
a := mask.RGBAAt(x-bounds.Min.X, y-bounds.Min.Y).A
c.A = uint8(int(c.A) * int(a) / 255)
out.SetNRGBA(x, y, c)
}
}
return out
}
// decodeImageResource 解码图片资源
// 入参: resPath 图片资源路径
// 返回: image.Image 图片对象, error 错误信息
func (r *Renderer) decodeImageResource(resPath string) (image.Image, error) {
rc, err := r.Reader.openFile(r.Reader.ResPath(resPath))
if err != nil {
return nil, err
}
defer rc.Close()
reader := bufio.NewReaderSize(rc, 8)
header, _ := reader.Peek(8)
if isJPEGData(header) {
img, err := canvasimage.NewJPEGImage(reader)
if err != nil {
return nil, err
}
if r.decodeImages {
return img.Image()
}
return img, nil
}
if isPNGData(header) {
img, err := canvasimage.NewPNGImage(reader)
if err != nil {
return nil, err
}
if r.decodeImages {
return img.Image()
}
return img, nil
}
data, err := io.ReadAll(reader)
if err != nil {
return nil, err
}
img, _, err := decodeImageData(data)
return img, err
}
// imageWithAlpha 合并图片透明度
// 入参: img 图片对象, alpha 对象透明度
// 返回: image.Image 合并后的图片对象
@@ -90,9 +236,10 @@ func imageWithAlpha(img image.Image, alpha *int) image.Image {
}
bounds := img.Bounds()
out := image.NewNRGBA(bounds)
source := imagePixelSource(img)
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
c := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA)
c := imageNRGBAAt(source, x, y)
c.A = uint8(int(c.A) * a / 255)
out.SetNRGBA(x, y, c)
}
@@ -112,31 +259,53 @@ func imageWithTransparentEdge(img image.Image) (image.Image, int) {
if w == 0 || h == 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
}
src := image.NewNRGBA(image.Rect(0, 0, w, h))
hasZero, hasVisible := false, false
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
c := color.NRGBAModel.Convert(img.At(bounds.Min.X+x, bounds.Min.Y+y)).(color.NRGBA)
src.SetNRGBA(x, y, c)
if c.A == 0 {
hasZero = true
} else {
hasVisible = true
src, ok := source.(*image.NRGBA)
if ok {
srcBounds := src.Bounds()
for y := srcBounds.Min.Y; y < srcBounds.Max.Y; y++ {
offset := src.PixOffset(srcBounds.Min.X, y) + 3
for x := 0; x < w; x++ {
if src.Pix[offset] == 0 {
hasZero = true
} else {
hasVisible = true
}
offset += 4
}
}
} else {
src = image.NewNRGBA(image.Rect(0, 0, w, h))
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
c := imageNRGBAAt(source, bounds.Min.X+x, bounds.Min.Y+y)
src.SetNRGBA(x, y, c)
if c.A == 0 {
hasZero = true
} else {
hasVisible = true
}
}
}
}
if !hasZero || !hasVisible {
return img, 0
}
srcBounds := src.Bounds()
out := image.NewNRGBA(image.Rect(0, 0, w+2, h+2))
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
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 edge, ok := transparentEdgeColor(src, x, y); ok {
if edge, ok := transparentEdgeColor(src, sx, sy); ok {
c = edge
}
}
@@ -158,6 +327,28 @@ func imageWithTransparentEdge(img image.Image) (image.Image, int) {
return out, 1
}
// imagePixelSource 获取图片像素源
// 入参: img 图片对象
// 返回: image.Image 图片像素源
func imagePixelSource(img image.Image) image.Image {
if src, ok := img.(*canvasimage.Image); ok {
if decoded, err := src.Image(); err == nil {
return decoded
}
}
return img
}
// imageNRGBAAt 获取图片NRGBA像素
// 入参: img 图片对象, x X坐标, y Y坐标
// 返回: color.NRGBA NRGBA像素
func imageNRGBAAt(img image.Image, x, y int) color.NRGBA {
if src, ok := img.(*image.NRGBA); ok {
return src.NRGBAAt(x, y)
}
return color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA)
}
// transparentEdgeColor 获取透明像素相邻的可见颜色
// 入参: img 图片对象, x X坐标, y Y坐标
// 返回: color.NRGBA 颜色, bool 是否存在
+39 -32
View File
@@ -30,7 +30,7 @@ func (r *Renderer) renderAnnotations(ctx *canvas.Context, pageID string, pageH f
box, _ := ParseBox(annot.Appearance.Boundary)
ctm := Matrix{a: 1, d: 1, e: box.X, f: box.Y}
for _, obj := range annot.Appearance.Objects {
r.renderObject(ctx, obj, pageH, nil, nil, 0, &ctm, false)
r.renderObject(ctx, obj, pageH, nil, nil, 0, &ctm, false, nil)
}
}
}
@@ -38,19 +38,24 @@ func (r *Renderer) renderAnnotations(ctx *canvas.Context, pageID string, pageH f
// renderTemplate 渲染模板
// 入参: ctx 画布上下文, templateID 模板ID, pageH 页面高度
func (r *Renderer) renderTemplate(ctx *canvas.Context, templateID string, pageH float64) {
var tplPage *TemplatePage
for _, tp := range r.Reader.doc.CommonData.TemplatePage {
if tp.ID == templateID {
tplPage = &tp
break
tplContent := r.templatePageCache[templateID]
if tplContent == nil {
var tplPage *TemplatePage
for i := range r.Reader.doc.CommonData.TemplatePage {
if r.Reader.doc.CommonData.TemplatePage[i].ID == templateID {
tplPage = &r.Reader.doc.CommonData.TemplatePage[i]
break
}
}
}
if tplPage == nil {
return
}
tplContent, err := r.Reader.PageContent(Page{BaseLoc: tplPage.BaseLoc})
if err != nil {
return
if tplPage == nil {
return
}
var err error
tplContent, err = r.Reader.PageContent(Page{BaseLoc: tplPage.BaseLoc})
if err != nil {
return
}
r.templatePageCache[templateID] = tplContent
}
if tplContent.Content.Layer != nil {
for _, layer := range tplContent.Content.Layer {
@@ -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)
if len(layer.Objects) > 0 {
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
}
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 {
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 {
r.renderImage(ctx, imgObj, pageH, parentCTM, false)
r.renderImage(ctx, imgObj, pageH, parentCTM, false, nil)
}
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 渲染复合图元
// 入参: ctx 画布上下文, cgu 复合图元对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽, parentCTM 父级CTM, boundaryInCTM 边界是否参与CTM变换
func (r *Renderer) renderCompositeGraphicUnit(ctx *canvas.Context, cgu CompositeGraphicUnit, pageH float64, defaultFill, defaultStroke color.Color, defaultLW float64, parentCTM *Matrix, boundaryInCTM bool) {
// 入参: ctx 画布上下文, cgu 复合图元对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽, parentCTM 父级CTM, boundaryInCTM 边界是否参与CTM变换, parentClip 父级裁剪路径
func (r *Renderer) renderCompositeGraphicUnit(ctx *canvas.Context, cgu CompositeGraphicUnit, pageH float64, defaultFill, defaultStroke color.Color, defaultLW float64, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) {
if cgu.Visible != nil && !*cgu.Visible {
return
}
@@ -94,53 +99,55 @@ func (r *Renderer) renderCompositeGraphicUnit(ctx *canvas.Context, cgu Composite
if parentCTM != nil {
currentCTM = parentCTM.Multiply(currentCTM)
}
box, _ := ParseBox(cgu.Boundary)
clipPath := intersectClipPath(parentClip, r.buildClipPath(cgu.Clips, pageH, box.X, box.Y, currentCTM))
if cgu.ResourceID != "" {
if ref, ok := r.CompositeGraphicUnits[cgu.ResourceID]; ok {
refCopy := *ref
refCopy.Alpha = mergeAlpha(refCopy.Alpha, cgu.Alpha)
r.renderCompositeGraphicUnit(ctx, refCopy, pageH, defaultFill, defaultStroke, defaultLW, &currentCTM, true)
r.renderCompositeGraphicUnit(ctx, refCopy, pageH, defaultFill, defaultStroke, defaultLW, &currentCTM, true, clipPath)
}
}
defaultFill, defaultStroke, defaultLW = r.drawParamDefaults(cgu.DrawParam, defaultFill, defaultStroke, defaultLW)
if len(cgu.Objects) > 0 {
for _, obj := range cgu.Objects {
obj = mergeGraphicObjectAlpha(obj, cgu.Alpha)
r.renderObject(ctx, obj, pageH, defaultFill, defaultStroke, defaultLW, &currentCTM, boundaryInCTM)
r.renderObject(ctx, obj, pageH, defaultFill, defaultStroke, defaultLW, &currentCTM, boundaryInCTM, clipPath)
}
ctx.Pop()
return
}
for _, imgObj := range cgu.ImageObject {
imgObj.Alpha = mergeAlpha(imgObj.Alpha, cgu.Alpha)
r.renderImage(ctx, imgObj, pageH, &currentCTM, boundaryInCTM)
r.renderImage(ctx, imgObj, pageH, &currentCTM, boundaryInCTM, clipPath)
}
for _, pathObj := range cgu.PathObject {
pathObj.Alpha = mergeAlpha(pathObj.Alpha, cgu.Alpha)
r.renderPath(ctx, pathObj, pageH, defaultFill, defaultStroke, defaultLW, &currentCTM, boundaryInCTM)
r.renderPath(ctx, pathObj, pageH, defaultFill, defaultStroke, defaultLW, &currentCTM, boundaryInCTM, clipPath)
}
for _, textObj := range cgu.TextObject {
textObj.Alpha = mergeAlpha(textObj.Alpha, cgu.Alpha)
r.renderText(ctx, textObj, pageH, defaultFill, defaultStroke, &currentCTM, boundaryInCTM)
r.renderText(ctx, textObj, pageH, defaultFill, defaultStroke, &currentCTM, boundaryInCTM, clipPath)
}
for _, subCgu := range cgu.CompositeGraphicUnit {
subCgu.Alpha = mergeAlpha(subCgu.Alpha, cgu.Alpha)
r.renderCompositeGraphicUnit(ctx, subCgu, pageH, defaultFill, defaultStroke, defaultLW, &currentCTM, boundaryInCTM)
r.renderCompositeGraphicUnit(ctx, subCgu, pageH, defaultFill, defaultStroke, defaultLW, &currentCTM, boundaryInCTM, clipPath)
}
ctx.Pop()
}
// renderObject 渲染图形对象
// 入参: ctx 画布上下文, obj 图形对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽, parentCTM 父级CTM, boundaryInCTM 边界是否参与CTM变换
func (r *Renderer) renderObject(ctx *canvas.Context, obj GraphicObject, pageH float64, defaultFill, defaultStroke color.Color, defaultLW float64, parentCTM *Matrix, boundaryInCTM bool) {
// 入参: ctx 画布上下文, obj 图形对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽, parentCTM 父级CTM, boundaryInCTM 边界是否参与CTM变换, parentClip 父级裁剪路径
func (r *Renderer) renderObject(ctx *canvas.Context, obj GraphicObject, pageH float64, defaultFill, defaultStroke color.Color, defaultLW float64, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) {
switch obj.Type {
case "TextObject":
r.renderText(ctx, obj.TextObject, pageH, defaultFill, defaultStroke, parentCTM, boundaryInCTM)
r.renderText(ctx, obj.TextObject, pageH, defaultFill, defaultStroke, parentCTM, boundaryInCTM, parentClip)
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":
r.renderImage(ctx, obj.ImageObject, pageH, parentCTM, boundaryInCTM)
r.renderImage(ctx, obj.ImageObject, pageH, parentCTM, boundaryInCTM, parentClip)
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)
}
}
+32 -20
View File
@@ -42,7 +42,7 @@ func (r *Renderer) RenderToImage(page *PageContent) (image.Image, error) {
// 入参: page 页面内容, 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 {
return err
}
@@ -59,21 +59,28 @@ func replacePDFProducer(data []byte) []byte {
return data
}
dst := []byte("/Producer(xiaoqidun/ofdgo)")
result := make([]byte, 0, len(data)-len(old)+len(dst))
return append(append(append(result, data[:idx]...), dst...), data[idx+len(old):]...)
copy(data[idx:idx+len(old)], dst)
return data
}
// RenderToPDF 渲染为PDF
// 入参: page 页面内容, writer 输出流
// 返回: error 错误信息
func (r *Renderer) RenderToPDF(page *PageContent, writer io.Writer) error {
c, err := r.RenderPage(page)
c, err := r.renderPage(page)
if err != nil {
return err
}
box, err := r.GetPageBox(page)
if err != nil {
return err
}
pages := []pdfPage{{Content: page, Box: box}}
navigation := newPDFNavigation(r, r.Reader.doc, pages)
var buf bytes.Buffer
p := pdf.New(&buf, c.W, c.H, nil)
p.SetInfo("", "", "", "", "xiaoqidun/ofdgo")
navigation.apply(p, 0)
c.RenderTo(p)
if err := p.Close(); err != nil {
return err
@@ -86,7 +93,7 @@ func (r *Renderer) RenderToPDF(page *PageContent, writer io.Writer) error {
// 入参: page 页面内容, 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 {
return err
}
@@ -104,28 +111,33 @@ func (r *Renderer) RenderToMultiPagePDF(writer io.Writer) error {
if len(doc.Pages.Page) == 0 {
return fmt.Errorf("no pages found")
}
pages := make([]pdfPage, len(doc.Pages.Page))
for i, pageRef := range doc.Pages.Page {
page, err := r.Reader.PageContent(pageRef)
if err != nil {
return fmt.Errorf("failed to read page %d: %w", i+1, err)
}
box, err := r.GetPageBox(page)
if err != nil {
return fmt.Errorf("failed to read page %d area: %w", i+1, err)
}
pages[i] = pdfPage{Content: page, Box: box}
}
navigation := newPDFNavigation(r, doc, pages)
var buf bytes.Buffer
var p *pdf.PDF
for _, pgRef := range doc.Pages.Page {
page, err := r.Reader.PageContent(pgRef)
p := pdf.New(&buf, pages[0].Box.W, pages[0].Box.H, nil)
p.SetInfo("", "", "", "", "xiaoqidun/ofdgo")
for i, page := range pages {
c, err := r.renderPage(page.Content)
if err != nil {
continue
return fmt.Errorf("failed to render page %d: %w", i+1, err)
}
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")
} else {
if i > 0 {
p.NewPage(c.W, c.H)
}
navigation.apply(p, i)
c.RenderTo(p)
}
if p == nil {
return fmt.Errorf("failed to render any page")
}
if err := p.Close(); err != nil {
return err
}
+120 -10
View File
@@ -163,8 +163,8 @@ func (s *pathStyle) scale(ctm Matrix) {
}
// renderPath 渲染路径
// 入参: ctx 画布上下文, obj 路径对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽, parentCTM 父级CTM, boundaryInCTM 边界是否参与CTM变换
func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64, defaultFill, defaultStroke color.Color, defaultLW float64, parentCTM *Matrix, boundaryInCTM bool) {
// 入参: ctx 画布上下文, obj 路径对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽, parentCTM 父级CTM, boundaryInCTM 边界是否参与CTM变换, parentClip 父级裁剪路径
func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64, defaultFill, defaultStroke color.Color, defaultLW float64, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) {
if obj.Visible != nil && !*obj.Visible {
return
}
@@ -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 {
p = rectPath
}
clipPath := r.buildClipPath(obj.Clips, pageH, bx, by, ctm)
clipPath := intersectClipPath(parentClip, r.buildClipPath(obj.Clips, pageH, bx, by, ctm))
shouldFill := false
if obj.Fill != nil {
shouldFill = *obj.Fill
@@ -204,7 +204,7 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64
if clipPath != nil {
fp = p.Copy()
fp.Close()
fp = fp.And(clipPath)
fp = applyClipPath(fp, clipPath)
}
r.renderPattern(ctx, style.fillPattern, style.fillPatternColor, pageH, fp, ctm, bx, by)
} else if shouldFill && style.fillPaint != nil {
@@ -214,7 +214,7 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64
if clipPath != nil {
fp = p.Copy()
fp.Close()
fp = fp.And(clipPath)
fp = applyClipPath(fp, clipPath)
}
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.Stroke(style.lineWidth, style.lineCap, style.lineJoin, canvas.Tolerance)
sp = sp.And(clipPath)
sp = applyClipPath(sp, clipPath)
ctx.SetFill(style.strokePaint)
ctx.SetStrokeColor(canvas.Transparent)
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++ {
tileCTM := patternCTM.Multiply(TranslationMatrix(float64(ix)*xStep, float64(iy)*yStep))
for _, obj := range pattern.CellContent.Objects {
r.renderObject(ctx, obj, pageH, defaultColor, defaultColor, 0, &tileCTM, false)
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
for _, area := range clip.Area {
areaCTM := NewMatrix(area.CTM)
if clips.TransFlag {
if clips.TransFlag == nil || *clips.TransFlag {
areaCTM = objectCTM.Multiply(areaCTM)
}
for _, pathObj := range area.Path {
@@ -422,7 +422,7 @@ func (r *Renderer) buildClipPath(clips *Clips, pageH float64, bx, by float64, ob
if clipPath == nil {
clipPath = cp
} 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 {
p = clipPath
} else {
p = p.And(clipPath)
p = intersectClipPath(p, clipPath)
}
}
}
return p
}
// intersectClipPath 求裁剪路径交集
// 入参: parent 父级裁剪路径, current 当前裁剪路径
// 返回: *canvas.Path 相交后的裁剪路径
func intersectClipPath(parent, current *canvas.Path) *canvas.Path {
if parent == nil {
return current
}
if current == nil {
return parent
}
parentRect, parentOK := rectangularPath(parent)
currentRect, currentOK := rectangularPath(current)
if parentOK && currentOK {
return parentRect.And(currentRect).ToPath()
}
if parent.Empty() || current.Empty() {
return &canvas.Path{}
}
if parentOK && parentRect.Contains(current.FastBounds()) {
return current
}
if currentOK && currentRect.Contains(parent.FastBounds()) {
return parent
}
return parent.And(current)
}
// unionClipPath 合并裁剪区域
// 入参: left 左侧裁剪路径, right 右侧裁剪路径
// 返回: *canvas.Path 合并后的裁剪路径
func unionClipPath(left, right *canvas.Path) *canvas.Path {
leftRect, leftOK := rectangularPath(left)
rightRect, rightOK := rectangularPath(right)
if leftOK && rightOK {
bounds := leftRect.Add(rightRect)
area := leftRect.Area() + rightRect.Area() - leftRect.And(rightRect).Area()
if canvas.Equal(area, bounds.Area()) {
return bounds.ToPath()
}
}
return left.Or(right)
}
// applyClipPath 应用裁剪路径
// 入参: path 绘制路径, clip 裁剪路径
// 返回: *canvas.Path 裁剪后的绘制路径
func applyClipPath(path, clip *canvas.Path) *canvas.Path {
if path == nil || clip == nil {
return path
}
if path.Empty() || clip.Empty() {
return &canvas.Path{}
}
if rect, ok := rectangularPath(clip); ok {
bounds := path.FastBounds()
if rect.Contains(bounds) {
return path
}
if !rect.Overlaps(bounds) {
return &canvas.Path{}
}
if pathRect, ok := rectangularPath(path); ok {
return pathRect.And(rect).ToPath()
}
return path.Flatten(canvas.Tolerance).Clip(rect.X0, rect.Y0, rect.X1, rect.Y1)
}
return path.And(clip)
}
// rectangularPath 获取矩形路径区域
// 入参: path 路径对象
// 返回: canvas.Rect 矩形区域, bool 是否为矩形
func rectangularPath(path *canvas.Path) (canvas.Rect, bool) {
if path == nil || path.HasSubpaths() || !path.Closed() {
return canvas.Rect{}, false
}
data := path.Data()
for i := 0; i < len(data); i += 4 {
if i+4 > len(data) || (data[i] != canvas.MoveToCmd && data[i] != canvas.LineToCmd && data[i] != canvas.CloseCmd) {
return canvas.Rect{}, false
}
}
points := path.Coords()
if len(points) != 5 || !points[0].Equals(points[4]) {
return canvas.Rect{}, false
}
rect := path.FastBounds()
corners := 0
for _, point := range points[:4] {
corner := 0
switch {
case canvas.Equal(point.X, rect.X0) && canvas.Equal(point.Y, rect.Y0):
corner = 1
case canvas.Equal(point.X, rect.X1) && canvas.Equal(point.Y, rect.Y0):
corner = 2
case canvas.Equal(point.X, rect.X1) && canvas.Equal(point.Y, rect.Y1):
corner = 4
case canvas.Equal(point.X, rect.X0) && canvas.Equal(point.Y, rect.Y1):
corner = 8
default:
return canvas.Rect{}, false
}
if corners&corner != 0 {
return canvas.Rect{}, false
}
corners |= corner
}
return rect, corners == 15
}
// buildTinyFillRectPath 构建微小填充矩形路径
// 入参: obj 路径对象, pageH 页面高度, ctm 变换矩阵, bx 边界X坐标, by 边界Y坐标
// 返回: *canvas.Path 路径对象
+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"
"image"
"image/color"
"image/draw"
"math"
"github.com/tdewolff/canvas"
"github.com/tdewolff/canvas/renderers/rasterizer"
)
// renderStamp 渲染印章
// 入参: ctx 画布上下文, s 印章对象, pageH 页面高度
func (r *Renderer) renderStamp(ctx *canvas.Context, s Stamp, pageH float64) {
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.Clip != nil {
if img := r.renderOFDStampImage(s.Data); img != nil {
r.renderStampImage(ctx, stampImageWithTransparentWhite(img), s, pageH)
}
return
}
reader, err := NewReader(bytes.NewReader(s.Data), int64(len(s.Data)))
if err == nil {
defer reader.Close()
@@ -44,8 +51,8 @@ func (r *Renderer) renderStamp(ctx *canvas.Context, s Stamp, pageH float64) {
continue
}
ctx.Push()
ctx.Translate(x, screenY)
ctx.Scale(w/sealBox.W, h/sealBox.H)
ctx.Translate(s.Box.X, pageH-(s.Box.Y+s.Box.H))
ctx.Scale(s.Box.W/sealBox.W, s.Box.H/sealBox.H)
renderer.renderPageToContext(ctx, content, false)
ctx.Pop()
}
@@ -56,17 +63,80 @@ func (r *Renderer) renderStamp(ctx *canvas.Context, s Stamp, pageH float64) {
if len(s.Data) > 0 {
img, _, err := decodeImageData(s.Data)
if err == nil {
img = stampImageWithTransparentWhite(img)
ctx.Push()
ctx.Translate(x, screenY)
ctx.Scale(w/float64(img.Bounds().Dx()), h/float64(img.Bounds().Dy()))
ctx.DrawImage(0, 0, img, canvas.DPMM(1.0))
ctx.Pop()
if r.decodeImages {
img = imagePixelSource(img)
}
r.renderStampImage(ctx, stampImageWithTransparentWhite(img), s, pageH)
return
}
}
}
// renderOFDStampImage 渲染OFD印章图像
// 入参: data OFD印章数据
// 返回: image.Image 印章图像
func (r *Renderer) renderOFDStampImage(data []byte) image.Image {
reader, err := NewReader(bytes.NewReader(data), int64(len(data)))
if err != nil {
return nil
}
defer reader.Close()
doc, err := reader.Doc()
if err != nil {
return nil
}
renderer := r.childRenderer(reader)
renderer.decodeImages = true
for _, pageRef := range doc.Pages.Page {
content, err := reader.PageContent(pageRef)
if err != nil {
continue
}
sealBox, err := renderer.GetPageBox(content)
if err != nil {
continue
}
c := canvas.New(sealBox.W, sealBox.H)
if err := renderer.renderPageToContext(canvas.NewContext(c), content, false); err != nil {
continue
}
return rasterizer.Draw(c, canvas.DPMM(r.DPI/25.4), canvas.DefaultColorSpace)
}
return nil
}
// renderStampImage 渲染印章图像
// 入参: ctx 画布上下文, img 印章图像, s 印章对象, pageH 页面高度
func (r *Renderer) renderStampImage(ctx *canvas.Context, img image.Image, s Stamp, pageH float64) {
box := s.Box
if s.Clip != nil {
img = clipStampImage(img, box, *s.Clip)
box.X += s.Clip.X
box.Y += s.Clip.Y
box.W = s.Clip.W
box.H = s.Clip.H
}
ctx.Push()
ctx.Translate(box.X, pageH-(box.Y+box.H))
ctx.Scale(box.W/float64(img.Bounds().Dx()), box.H/float64(img.Bounds().Dy()))
ctx.DrawImage(0, 0, img, canvas.DPMM(1.0))
ctx.Pop()
}
// clipStampImage 裁剪印章图像
// 入参: img 印章图像, box 印章区域, clip 裁剪区域
// 返回: image.Image 裁剪后的印章图像
func clipStampImage(img image.Image, box, clip Box) image.Image {
bounds := img.Bounds()
x0 := int(math.Floor(clip.X / box.W * float64(bounds.Dx())))
y0 := int(math.Floor(clip.Y / box.H * float64(bounds.Dy())))
x1 := int(math.Ceil((clip.X + clip.W) / box.W * float64(bounds.Dx())))
y1 := int(math.Ceil((clip.Y + clip.H) / box.H * float64(bounds.Dy())))
out := image.NewNRGBA(image.Rect(0, 0, x1-x0, y1-y0))
draw.Draw(out, out.Bounds(), img, image.Pt(bounds.Min.X+x0, bounds.Min.Y+y0), draw.Src)
return out
}
// stampImageWithTransparentWhite 处理印章图片白色底色
// 入参: img 印章图片对象
// 返回: image.Image 处理后的印章图片对象
+47 -12
View File
@@ -24,8 +24,8 @@ import (
const ptPerMM = 72.0 / 25.4
// renderText 渲染文本
// 入参: ctx 画布上下文, obj 文本对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, parentCTM 父级CTM, boundaryInCTM 边界是否参与父级CTM
func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64, defaultFill, defaultStroke color.Color, parentCTM *Matrix, boundaryInCTM bool) {
// 入参: ctx 画布上下文, obj 文本对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, parentCTM 父级CTM, boundaryInCTM 边界是否参与父级CTM, parentClip 父级裁剪路径
func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64, defaultFill, defaultStroke color.Color, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) {
if obj.Visible != nil && !*obj.Visible {
return
}
@@ -41,6 +41,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
if parentCTM != nil {
ctm = parentCTM.Multiply(ctm)
}
clipPath := intersectClipPath(parentClip, r.buildClipPath(obj.Clips, pageH, bx, by, ctm))
var dp *DrawParam
if obj.DrawParam != "" {
dp = r.getDrawParam(obj.DrawParam, nil)
@@ -115,6 +116,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
}
ff := r.loadFont(fontID)
if ff == nil {
ctx.Pop()
return
}
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)
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
if len(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 {
str := glyph.Text
drawAsGlyphPath := drawAsPath || glyph.GlyphID >= 0
var glyphPath *canvas.Path
var glyphWidth float64
if drawAsGlyphPath {
glyphPath, glyphWidth = r.cachedTextGlyphPath(face, glyph)
} else {
glyphWidth = textGlyphWidth(face, glyph)
}
if i < len(xs) {
cx = xs[i]
} else if i > 0 {
if dx, ok := textDelta(dxs, i-1); ok {
cx += dx
} else if len(dys) == 0 {
cx += textGlyphWidth(face, glyph) * hScale
cx += glyphWidth * hScale
}
}
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)
canvasX, canvasY = tx+bx, pageH-(ty+by)
}
textWidth := textGlyphWidth(face, glyph) * hScale
textWidth := glyphWidth * hScale
glyphFillPaint := fillPaint
if useGlyphFillPaint {
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)
if glyphFillPaint != nil {
ctx.SetFill(glyphFillPaint)
if clipPath != nil {
scaleX := hScale
if advanceLimit > 0 && glyphWidth*scaleX > advanceLimit {
scaleX = advanceLimit / glyphWidth
}
textWidth = glyphWidth * scaleX
textTransform := canvas.Identity.Translate(canvasX, canvasY)
if useTextMatrix {
textTransform = textTransform.Mul(textMatrix(ctm))
}
glyphPath = applyClipPath(glyphPath.Copy().Transform(textTransform.Scale(scaleX, 1)), clipPath)
ctx.DrawPath(0, 0, glyphPath)
if hasUnderline {
uw := sizeMM * 0.05
off := sizeMM * 0.1
underline := &canvas.Path{}
underline.MoveTo(0, -off)
underline.LineTo(textWidth, -off)
underline = underline.Stroke(uw, canvas.ButtCap, canvas.MiterJoin, canvas.Tolerance)
underline = applyClipPath(underline.Transform(textTransform), clipPath)
ctx.SetFillColor(fillColor)
ctx.SetStrokeColor(canvas.Transparent)
ctx.DrawPath(0, 0, underline)
}
continue
}
drawGlyph := func(x, y float64) {
if drawAsPath || glyph.GlyphID >= 0 {
path, width := textGlyphPath(face, glyph)
if drawAsGlyphPath {
scaleX := hScale
if advanceLimit > 0 && width*scaleX > advanceLimit {
scaleX = advanceLimit / width
if advanceLimit > 0 && glyphWidth*scaleX > advanceLimit {
scaleX = advanceLimit / glyphWidth
}
textWidth = width * scaleX
textWidth = glyphWidth * scaleX
if scaleX != 1 {
ctx.Push()
ctx.Translate(x, y)
ctx.Scale(scaleX, 1)
ctx.DrawPath(0, 0, path)
ctx.DrawPath(0, 0, glyphPath)
ctx.Pop()
} else {
ctx.DrawPath(x, y, path)
ctx.DrawPath(x, y, glyphPath)
}
} else {
scaled := hScale != 1
+1
View File
@@ -104,4 +104,5 @@ type CompositeGraphicUnit struct {
ImageObject []ImageObject `xml:"ImageObject"`
CompositeGraphicUnit []CompositeGraphicUnit `xml:"CompositeGraphicUnit"`
Clips *Clips `xml:"Clips"`
Actions []Action `xml:"Actions>Action"`
}
+36 -25
View File
@@ -20,7 +20,6 @@ import (
"encoding/binary"
"encoding/xml"
"fmt"
"strconv"
"strings"
)
@@ -28,14 +27,16 @@ import (
type SignType string
const (
// SignTypeSeal 印章签名
SignTypeSeal SignType = "Seal"
// SignTypeSign 数字签名
SignTypeSign SignType = "Sign"
)
// Signatures 签名列表
type Signatures struct {
XMLName xml.Name `xml:"Signatures"`
MaxSignId string `xml:"MaxSignId"`
MaxSignID string `xml:"MaxSignId"`
List []Signature `xml:"Signature"`
}
@@ -80,6 +81,7 @@ type SignatureStamp struct {
ID string `xml:"ID,attr"`
PageRef string `xml:"PageRef,attr"`
Boundary string `xml:"Boundary,attr"`
Clip string `xml:"Clip,attr"`
}
// SignatureStampPosition 签名外观位置信息
@@ -89,6 +91,8 @@ type SignatureStampPosition struct {
PageID string
Boundary string
Box Box
Clip string
ClipBox *Box
}
// SignatureReferences 签名保护文件列表
@@ -122,12 +126,22 @@ func (r *Reader) SignatureStampPositions(stamps []SignatureStamp) ([]SignatureSt
if err != nil {
return nil, err
}
var clipBox *Box
if stamp.Clip != "" {
clip, err := parseSignatureStampBox(stamp.Clip)
if err != nil {
return nil, err
}
clipBox = &clip
}
positions = append(positions, SignatureStampPosition{
ID: stamp.ID,
Page: page,
PageID: stamp.PageRef,
Boundary: stamp.Boundary,
Box: box,
Clip: stamp.Clip,
ClipBox: clipBox,
})
}
return positions, nil
@@ -137,27 +151,11 @@ func (r *Reader) SignatureStampPositions(stamps []SignatureStamp) ([]SignatureSt
// 入参: s 区域字符串
// 返回: Box 矩形对象, error 错误信息
func parseSignatureStampBox(s string) (Box, error) {
parts := strings.Fields(s)
if len(parts) != 4 {
return Box{}, fmt.Errorf("invalid signature stamp boundary: %s", s)
}
x, err := strconv.ParseFloat(parts[0], 64)
box, err := ParseBox(s)
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)
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
return box, nil
}
// parseSignatures 解析签名文件
@@ -212,8 +210,19 @@ func (r *Reader) parseSignatures(doc *Document) error {
}
for _, annot := range sigFile.SignedInfo.StampAnnot {
pageID := annot.PageRef
bbox, _ := ParseBox(annot.Boundary)
r.addStamp(pageID, bbox, sealType, sealData)
bbox, err := parseSignatureStampBox(annot.Boundary)
if err != nil {
continue
}
var clipBox *Box
if annot.Clip != "" {
clip, err := parseSignatureStampBox(annot.Clip)
if err != nil {
continue
}
clipBox = &clip
}
r.addStamp(pageID, bbox, clipBox, sealType, sealData)
}
}(sigRef)
}
@@ -415,18 +424,20 @@ func probeImageMedia(data []byte) (string, []byte) {
// Stamp 印章信息结构
type Stamp struct {
Box Box
Clip *Box
Type string
Data []byte
}
// addStamp 添加印章到页面
// 入参: pageID 页面ID, box 印章区域, sType 印章类型, data 印章数据
func (r *Reader) addStamp(pageID string, box Box, sType string, data []byte) {
// 入参: pageID 页面ID, box 印章区域, clip 裁剪区域, sType 印章类型, data 印章数据
func (r *Reader) addStamp(pageID string, box Box, clip *Box, sType string, data []byte) {
if r.Stamps == nil {
r.Stamps = make(map[string][]Stamp)
}
r.Stamps[pageID] = append(r.Stamps[pageID], Stamp{
Box: box,
Clip: clip,
Type: sType,
Data: data,
})
+132 -9
View File
@@ -66,8 +66,8 @@ type gbtSignerInfo struct {
// 入参: method 签名算法, digestMethod 摘要算法, signedValue 签名值, signedData 被签名原文, options 验证选项
// 返回: *digitalVerifyResult 验证结果, error 错误信息
func verifyDigitalSignature(method, digestMethod string, signedValue, signedData []byte, options *signatureVerifyOptions) (*digitalVerifyResult, error) {
if isGBT35275SignedValue(signedValue) {
return verifyGBT35275SignedData(signedValue, signedData, options)
if value, ok := normalizeGBT35275SignedValue(signedValue); ok {
return verifyGBT35275SignedData(value, signedData, options)
}
if isSM2SignatureMethod(method) {
return verifyRawDigitalSignature(signedValue, signedData, options)
@@ -204,27 +204,150 @@ func verifyGBT35275SignedData(signedValue, signedData []byte, options *signature
return result, nil
}
// isGBT35275SignedValue 判断签名值是否为GB/T 35275 SignedData
// normalizeGBT35275SignedValue 规范化GB/T 35275 SignedData编码
// 入参: data 签名值数据
// 返回: bool 是否为SignedData
func isGBT35275SignedValue(data []byte) bool {
// 返回: []byte 定长编码数据, bool 是否为SignedData
func normalizeGBT35275SignedValue(data []byte) ([]byte, bool) {
if contentType, ok := gbtContentType(data); ok {
return data, contentType == signContentSignedData
}
der, err := berToDefinite(data)
if err != nil {
return nil, false
}
contentType, ok := gbtContentType(der)
if !ok || contentType != signContentSignedData {
return nil, false
}
return der, true
}
// gbtContentType 读取GB/T 35275内容类型
// 入参: data DER编码数据
// 返回: string 内容类型, bool 是否完成解析
func gbtContentType(data []byte) (string, bool) {
var root asn1.RawValue
rest, err := asn1.Unmarshal(data, &root)
if err != nil || len(rest) != 0 || root.Tag != signASN1Sequence {
return false
return "", false
}
items, ok := asn1Children(root.Bytes)
if !ok || len(items) < 2 {
return false
if !ok || len(items) == 0 {
return "", false
}
if items[0].Tag != asn1.TagOID {
return "", true
}
oid, err := asn1OIDString(items[0])
return err == nil && oid == signContentSignedData
return oid, err == nil
}
// berToDefinite 将BER不定长编码转换为定长编码
// 入参: data BER编码数据
// 返回: []byte 定长编码数据, error 错误信息
func berToDefinite(data []byte) ([]byte, error) {
out, n, err := berValueToDefinite(data)
if err != nil {
return nil, err
}
if n != len(data) {
return nil, fmt.Errorf("invalid BER trailing data")
}
return out, nil
}
// berValueToDefinite 转换单个BER编码值
// 入参: data BER编码数据
// 返回: []byte 定长编码数据, int 已读取长度, error 错误信息
func berValueToDefinite(data []byte) ([]byte, int, error) {
if len(data) < 2 {
return nil, 0, fmt.Errorf("invalid BER value")
}
pos := 1
if data[0]&0x1f == 0x1f {
for pos < len(data) && data[pos]&0x80 != 0 {
pos++
}
pos++
}
if pos >= len(data) {
return nil, 0, fmt.Errorf("invalid BER tag")
}
tag := data[:pos]
firstLength := data[pos]
pos++
if firstLength == 0x80 {
if tag[0]&0x20 == 0 {
return nil, 0, fmt.Errorf("invalid BER indefinite primitive")
}
var content []byte
for {
if len(data)-pos < 2 {
return nil, 0, fmt.Errorf("invalid BER unterminated value")
}
if data[pos] == 0 && data[pos+1] == 0 {
pos += 2
break
}
child, n, err := berValueToDefinite(data[pos:])
if err != nil {
return nil, 0, err
}
content = append(content, child...)
pos += n
}
return wrapBERValue(tag, content), pos, nil
}
length := uint64(firstLength)
if firstLength&0x80 != 0 {
n := int(firstLength & 0x7f)
if n == 0 || n > 8 || len(data)-pos < n {
return nil, 0, fmt.Errorf("invalid BER length")
}
length = 0
for _, b := range data[pos : pos+n] {
length = length<<8 | uint64(b)
}
pos += n
}
if length > uint64(len(data)-pos) {
return nil, 0, fmt.Errorf("invalid BER truncated value")
}
end := pos + int(length)
content := append([]byte(nil), data[pos:end]...)
if tag[0]&0x20 != 0 {
content = content[:0]
for pos < end {
child, n, err := berValueToDefinite(data[pos:end])
if err != nil {
return nil, 0, err
}
content = append(content, child...)
pos += n
}
}
return wrapBERValue(tag, content), end, nil
}
// wrapBERValue 包装定长BER编码值
// 入参: tag 标签, content 内容
// 返回: []byte 定长编码数据
func wrapBERValue(tag, content []byte) []byte {
out := make([]byte, 0, len(tag)+len(content)+9)
out = append(out, tag...)
out = append(out, asn1LengthBytes(len(content))...)
return append(out, content...)
}
// parseGBT35275SignedData 解析GB/T 35275 SignedData
// 入参: data 签名值数据
// 返回: *gbtSignedData SignedData结构, error 错误信息
func parseGBT35275SignedData(data []byte) (*gbtSignedData, error) {
var ok bool
data, ok = normalizeGBT35275SignedValue(data)
if !ok {
return nil, fmt.Errorf("invalid signed data content type")
}
contentType, content, ok, err := parseGBTContentInfoBytes(data)
if err != nil {
return nil, err
+130 -42
View File
@@ -20,6 +20,7 @@ import (
"fmt"
"math/big"
"strings"
"time"
)
const (
@@ -42,6 +43,7 @@ type sesSignature struct {
SignAlg string
Signature []byte
DataHash []byte
Time time.Time
Seal *sesSeal
}
@@ -55,6 +57,7 @@ type sesSeal struct {
PicType string
PicData []byte
CertList sesCertList
Info SignatureSealInfo
}
// sesCertList SES印章证书列表
@@ -71,17 +74,19 @@ type sesCertDigest struct {
// sesVerifyResult SES签章验证结果
type sesVerifyResult struct {
DataHashOK bool
SignedOK bool
SealOK bool
CertOK bool
SignCert SignatureCertInfo
SealCert SignatureCertInfo
SignCertRaw []byte
SealCertRaw []byte
SealRaw []byte
Certs [][]byte
SealType string
DataHashOK bool
SignedOK bool
SealOK bool
CertOK bool
SignCert SignatureCertInfo
SealCert SignatureCertInfo
SignCertRaw []byte
SealCertRaw []byte
SealRaw []byte
Certs [][]byte
SealType string
SealInfo SignatureSealInfo
SignatureTime time.Time
}
// parseSESSignature 解析SES签章值
@@ -119,13 +124,21 @@ func parseSESSignature(data []byte) (*sesSignature, error) {
return nil, err
}
tbsItems, ok := asn1Children(items[0].Bytes)
if !ok || len(tbsItems) < 5 {
if !ok || len(tbsItems) < 5 || len(tbsItems) > 6 {
return nil, fmt.Errorf("invalid ses toSign")
}
seal, err := parseSESSeal(tbsItems[1])
if err != nil {
return nil, err
}
version, err := asn1Integer(tbsItems[0])
if err != nil || version != seal.Info.Version {
return nil, fmt.Errorf("invalid ses version")
}
signatureTime, err := asn1Time(tbsItems[2])
if err != nil {
return nil, err
}
dataHash, err := asn1BitOrOctetBytes(tbsItems[3])
if err != nil {
return nil, err
@@ -136,6 +149,7 @@ func parseSESSignature(data []byte) (*sesSignature, error) {
SignAlg: alg,
Signature: signature,
DataHash: dataHash,
Time: signatureTime,
Seal: seal,
}, nil
}
@@ -156,6 +170,11 @@ func parseSESSignatureV1(items []asn1.RawValue) (*sesSignature, error) {
if err != nil {
return nil, err
}
version, err := asn1Integer(tbsItems[0])
if err != nil || version != seal.Info.Version {
return nil, fmt.Errorf("invalid ses version")
}
signatureTime := parseSESSignatureTimeV1(tbsItems[2])
dataHash, err := asn1BitOrOctetBytes(tbsItems[3])
if err != nil {
return nil, err
@@ -174,10 +193,31 @@ func parseSESSignatureV1(items []asn1.RawValue) (*sesSignature, error) {
SignAlg: alg,
Signature: signature,
DataHash: dataHash,
Time: signatureTime,
Seal: seal,
}, nil
}
// parseSESSignatureTimeV1 解析SES V1签名时间
// 入参: raw 签名时间或时间戳数据
// 返回: time.Time 签名时间
func parseSESSignatureTimeV1(raw asn1.RawValue) time.Time {
data, err := asn1BitOrOctetBytes(raw)
if err != nil {
return time.Time{}
}
if t := parseSignatureDateTime(string(data)); !t.IsZero() {
return t
}
var value asn1.RawValue
rest, err := asn1.Unmarshal(data, &value)
if err != nil || len(rest) != 0 {
return time.Time{}
}
t, _ := asn1Time(value)
return t
}
// verifySESSignature 验证SES签章值
// 入参: data 签章值数据, signedData 被签名数据原文, options 验证选项
// 返回: *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, options.SignCerts...)
result.SealType = sig.Seal.PicType
result.SealInfo = sig.Seal.Info
result.SignatureTime = sig.Time
result.DataHashOK = bytes.Equal(sig.DataHash, signSM3(signedData))
signPub, err := parseSM2PublicKeyFromCert(sig.Cert)
if err != nil {
@@ -243,11 +285,7 @@ func parseSESSeal(raw asn1.RawValue) (*sesSeal, error) {
if !ok || len(infoItems) < 4 || len(infoItems) > 5 {
return nil, fmt.Errorf("invalid ses seal info")
}
version, err := parseSESHeaderVersion(infoItems[0])
if err != nil {
return nil, err
}
certList, err := parseSESCertList(infoItems[2], version)
info, certList, err := parseSESSealInfo(infoItems)
if err != nil {
return nil, err
}
@@ -264,6 +302,7 @@ func parseSESSeal(raw asn1.RawValue) (*sesSeal, error) {
PicType: picType,
PicData: picData,
CertList: certList,
Info: info,
}, nil
}
@@ -291,7 +330,7 @@ func parseSESSealV1(raw asn1.RawValue, items []asn1.RawValue) (*sesSeal, error)
if err != nil {
return nil, err
}
certList, err := parseSESCertListV1(infoItems[2])
info, certList, err := parseSESSealInfo(infoItems)
if err != nil {
return nil, err
}
@@ -309,43 +348,92 @@ func parseSESSealV1(raw asn1.RawValue, items []asn1.RawValue) (*sesSeal, error)
PicType: picType,
PicData: picData,
CertList: certList,
Info: info,
}, nil
}
// parseSESHeaderVersion 解析印章头版本
// 入参: raw 印章信息
// 返回: int 版本号, error 错误信息
func parseSESHeaderVersion(raw asn1.RawValue) (int, error) {
items, ok := asn1Children(raw.Bytes)
if !ok || len(items) < 2 {
return 0, fmt.Errorf("invalid ses header")
// parseSESSealInfo 解析电子印章信息
// 入参: items 印章信息ASN.1子元素
// 返回: SignatureSealInfo 印章信息, sesCertList 证书列表, error 错误信息
func parseSESSealInfo(items []asn1.RawValue) (SignatureSealInfo, sesCertList, error) {
if len(items) < 4 {
return SignatureSealInfo{}, sesCertList{}, fmt.Errorf("invalid ses seal info")
}
return asn1Integer(items[1])
header, ok := asn1Children(items[0].Bytes)
if !ok || len(header) != 3 {
return SignatureSealInfo{}, sesCertList{}, fmt.Errorf("invalid ses header")
}
if asn1String(header[0]) != "ES" {
return SignatureSealInfo{}, sesCertList{}, fmt.Errorf("invalid ses header")
}
version, err := asn1Integer(header[1])
if err != nil {
return SignatureSealInfo{}, sesCertList{}, err
}
property, ok := asn1Children(items[2].Bytes)
if !ok {
return SignatureSealInfo{}, sesCertList{}, fmt.Errorf("invalid ses property")
}
certIndex := 2
timeIndex := 3
if version >= 4 {
certIndex = 3
timeIndex = 4
}
if len(property) != timeIndex+3 {
return SignatureSealInfo{}, sesCertList{}, fmt.Errorf("invalid ses property")
}
typeValue, err := asn1Integer(property[0])
if err != nil {
return SignatureSealInfo{}, sesCertList{}, err
}
certList, err := parseSESCertList(property, version, certIndex)
if err != nil {
return SignatureSealInfo{}, sesCertList{}, err
}
createTime, err := asn1Time(property[timeIndex])
if err != nil {
return SignatureSealInfo{}, sesCertList{}, err
}
validStart, err := asn1Time(property[timeIndex+1])
if err != nil {
return SignatureSealInfo{}, sesCertList{}, err
}
validEnd, err := asn1Time(property[timeIndex+2])
if err != nil {
return SignatureSealInfo{}, sesCertList{}, err
}
return SignatureSealInfo{
Version: version,
ID: strings.TrimSpace(asn1String(items[1])),
VendorID: strings.TrimSpace(asn1String(header[2])),
Type: typeValue,
Name: strings.TrimSpace(asn1String(property[1])),
CreateTime: createTime,
ValidStart: validStart,
ValidEnd: validEnd,
}, certList, nil
}
// parseSESCertList 解析印章证书列表
// 入参: raw 印章属性信息, version 印章版本
// 入参: property 印章属性, version 印章版本, certIndex 证书列表位置
// 返回: sesCertList 证书列表, error 错误信息
func parseSESCertList(raw asn1.RawValue, version int) (sesCertList, error) {
items, ok := asn1Children(raw.Bytes)
if !ok || len(items) < 3 {
return sesCertList{}, fmt.Errorf("invalid ses property")
func parseSESCertList(property []asn1.RawValue, version, certIndex int) (sesCertList, error) {
if version == 1 {
return parseSESCertListV1(property[certIndex])
}
if version < 4 {
return parseSESCertInfoList(items[2])
return parseSESCertInfoList(property[certIndex])
}
if len(items) < 4 {
return sesCertList{}, fmt.Errorf("invalid ses cert list")
}
listType, err := asn1Integer(items[2])
listType, err := asn1Integer(property[certIndex-1])
if err != nil {
return sesCertList{}, err
}
switch listType {
case 1:
return parseSESCertInfoList(items[3])
return parseSESCertInfoList(property[certIndex])
case 2:
return parseSESCertDigestList(items[3])
return parseSESCertDigestList(property[certIndex])
default:
return sesCertList{}, fmt.Errorf("unsupported ses cert list type")
}
@@ -396,12 +484,12 @@ func parseSESCertDigestList(raw asn1.RawValue) (sesCertList, error) {
list := sesCertList{Digests: make([]sesCertDigest, 0, len(items))}
for _, item := range items {
fields, ok := asn1Children(item.Bytes)
if !ok || len(fields) < 2 {
if !ok || len(fields) != 2 {
return sesCertList{}, fmt.Errorf("invalid ses cert digest")
}
method, err := parseGBTAlgorithm(fields[0])
if err != nil {
return sesCertList{}, err
method := strings.TrimSpace(asn1String(fields[0]))
if method == "" {
return sesCertList{}, fmt.Errorf("invalid ses cert digest")
}
digest, err := asn1OctetString(fields[1])
if err != nil {
+184 -21
View File
@@ -15,7 +15,6 @@
package ofdgo
import (
"crypto/elliptic"
"encoding/asn1"
"encoding/binary"
"math/big"
@@ -31,17 +30,32 @@ type sm2PublicKey struct {
Y *big.Int
}
// sm2Curve SM2椭圆曲线
type sm2Curve struct {
P *big.Int
N *big.Int
B *big.Int
Gx *big.Int
Gy *big.Int
}
// sm2Point SM2雅可比坐标点
type sm2Point struct {
X *big.Int
Y *big.Int
Z *big.Int
}
// newSM2P256 创建SM2椭圆曲线
// 返回: elliptic.Curve SM2椭圆曲线
func newSM2P256() elliptic.Curve {
c := &elliptic.CurveParams{Name: "SM2-P-256"}
c.P = sm2Big("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF")
c.N = sm2Big("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123")
c.B = sm2Big("28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93")
c.Gx = sm2Big("32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7")
c.Gy = sm2Big("BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0")
c.BitSize = 256
return c
// 返回: *sm2Curve SM2椭圆曲线
func newSM2P256() *sm2Curve {
return &sm2Curve{
P: sm2Big("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF"),
N: sm2Big("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123"),
B: sm2Big("28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93"),
Gx: sm2Big("32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7"),
Gy: sm2Big("BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0"),
}
}
// sm2Big 解析SM2大整数常量
@@ -85,11 +99,11 @@ func parseSM2Signature(sig []byte) (*big.Int, *big.Int, bool) {
// 入参: pub 公钥, userID 用户标识, msg 原文, r R值, s S值
// 返回: bool 是否验证通过
func sm2Verify(pub sm2PublicKey, userID, msg []byte, r, s *big.Int) bool {
n := sm2P256.Params().N
n := sm2P256.N
if r.Sign() <= 0 || s.Sign() <= 0 || r.Cmp(n) >= 0 || s.Cmp(n) >= 0 {
return false
}
if pub.X == nil || pub.Y == nil || !sm2P256.IsOnCurve(pub.X, pub.Y) {
if pub.X == nil || pub.Y == nil || !sm2P256.isOnCurve(pub.X, pub.Y) {
return false
}
e := new(big.Int).SetBytes(sm2MessageDigest(pub, userID, msg))
@@ -98,10 +112,8 @@ func sm2Verify(pub sm2PublicKey, userID, msg []byte, r, s *big.Int) bool {
if t.Sign() == 0 {
return false
}
x1, y1 := sm2P256.ScalarBaseMult(s.Bytes())
x2, y2 := sm2P256.ScalarMult(pub.X, pub.Y, t.Bytes())
x, _ := sm2P256.Add(x1, y1, x2, y2)
if x == nil {
x, ok := sm2P256.combinedMult(pub.X, pub.Y, s, t)
if !ok {
return false
}
v := new(big.Int).Add(e, x)
@@ -132,9 +144,9 @@ func sm2ZA(pub sm2PublicKey, userID []byte) []byte {
h.Write(entl[:])
h.Write(userID)
h.Write(sm2Fixed(sm2A()))
h.Write(sm2Fixed(sm2P256.Params().B))
h.Write(sm2Fixed(sm2P256.Params().Gx))
h.Write(sm2Fixed(sm2P256.Params().Gy))
h.Write(sm2Fixed(sm2P256.B))
h.Write(sm2Fixed(sm2P256.Gx))
h.Write(sm2Fixed(sm2P256.Gy))
h.Write(sm2Fixed(pub.X))
h.Write(sm2Fixed(pub.Y))
return h.Sum(nil)
@@ -143,7 +155,158 @@ func sm2ZA(pub sm2PublicKey, userID []byte) []byte {
// sm2A 获取SM2曲线A参数
// 返回: *big.Int 曲线A参数
func sm2A() *big.Int {
return new(big.Int).Sub(sm2P256.Params().P, big.NewInt(3))
return new(big.Int).Sub(sm2P256.P, big.NewInt(3))
}
// isOnCurve 判断点是否位于SM2曲线
// 入参: x X坐标, y Y坐标
// 返回: bool 是否位于曲线
func (c *sm2Curve) isOnCurve(x, y *big.Int) bool {
if x.Sign() < 0 || y.Sign() < 0 || x.Cmp(c.P) >= 0 || y.Cmp(c.P) >= 0 {
return false
}
left := c.fieldSquare(y)
right := c.fieldAdd(c.fieldSub(c.fieldMul(c.fieldSquare(x), x), c.fieldScale(x, 3)), c.B)
return left.Cmp(right) == 0
}
// combinedMult 计算sG+tP
// 入参: x 公钥X坐标, y 公钥Y坐标, s 标量S, t 标量T
// 返回: *big.Int 结果X坐标, bool 是否计算成功
func (c *sm2Curve) combinedMult(x, y, s, t *big.Int) (*big.Int, bool) {
base := c.scalarMult(c.Gx, c.Gy, s.Bytes())
public := c.scalarMult(x, y, t.Bytes())
result := c.add(base, public)
affineX, _, ok := c.affine(result)
return affineX, ok
}
// scalarMult 计算椭圆曲线标量乘法
// 入参: x 点X坐标, y 点Y坐标, scalar 标量
// 返回: sm2Point 雅可比坐标点
func (c *sm2Curve) scalarMult(x, y *big.Int, scalar []byte) sm2Point {
result := c.infinity()
point := sm2Point{X: new(big.Int).Set(x), Y: new(big.Int).Set(y), Z: big.NewInt(1)}
for _, value := range scalar {
for bit := 7; bit >= 0; bit-- {
result = c.double(result)
if value&(1<<uint(bit)) != 0 {
result = c.add(result, point)
}
}
}
return result
}
// add 计算椭圆曲线点加法
// 入参: p 点P, q 点Q
// 返回: sm2Point 结果点
func (c *sm2Curve) add(p, q sm2Point) sm2Point {
if p.Z.Sign() == 0 {
return q
}
if q.Z.Sign() == 0 {
return p
}
z1z1 := c.fieldSquare(p.Z)
z2z2 := c.fieldSquare(q.Z)
u1 := c.fieldMul(p.X, z2z2)
u2 := c.fieldMul(q.X, z1z1)
s1 := c.fieldMul(p.Y, c.fieldMul(q.Z, z2z2))
s2 := c.fieldMul(q.Y, c.fieldMul(p.Z, z1z1))
if u1.Cmp(u2) == 0 {
if s1.Cmp(s2) != 0 {
return c.infinity()
}
return c.double(p)
}
h := c.fieldSub(u2, u1)
i := c.fieldSquare(c.fieldScale(h, 2))
j := c.fieldMul(h, i)
r := c.fieldScale(c.fieldSub(s2, s1), 2)
v := c.fieldMul(u1, i)
x := c.fieldSub(c.fieldSub(c.fieldSquare(r), j), c.fieldScale(v, 2))
y := c.fieldSub(c.fieldMul(r, c.fieldSub(v, x)), c.fieldScale(c.fieldMul(s1, j), 2))
z := c.fieldMul(c.fieldSub(c.fieldSub(c.fieldSquare(c.fieldAdd(p.Z, q.Z)), z1z1), z2z2), h)
return sm2Point{X: x, Y: y, Z: z}
}
// double 计算椭圆曲线点倍加
// 入参: p 点P
// 返回: sm2Point 结果点
func (c *sm2Curve) double(p sm2Point) sm2Point {
if p.Z.Sign() == 0 || p.Y.Sign() == 0 {
return c.infinity()
}
delta := c.fieldSquare(p.Z)
gamma := c.fieldSquare(p.Y)
beta := c.fieldMul(p.X, gamma)
alpha := c.fieldScale(c.fieldMul(c.fieldSub(p.X, delta), c.fieldAdd(p.X, delta)), 3)
x := c.fieldSub(c.fieldSquare(alpha), c.fieldScale(beta, 8))
z := c.fieldSub(c.fieldSub(c.fieldSquare(c.fieldAdd(p.Y, p.Z)), gamma), delta)
y := c.fieldSub(c.fieldMul(alpha, c.fieldSub(c.fieldScale(beta, 4), x)), c.fieldScale(c.fieldSquare(gamma), 8))
return sm2Point{X: x, Y: y, Z: z}
}
// affine 将雅可比坐标转换为仿射坐标
// 入参: p 雅可比坐标点
// 返回: *big.Int X坐标, *big.Int Y坐标, bool 是否转换成功
func (c *sm2Curve) affine(p sm2Point) (*big.Int, *big.Int, bool) {
if p.Z.Sign() == 0 {
return nil, nil, false
}
z := new(big.Int).ModInverse(p.Z, c.P)
if z == nil {
return nil, nil, false
}
z2 := c.fieldSquare(z)
x := c.fieldMul(p.X, z2)
y := c.fieldMul(p.Y, c.fieldMul(z2, z))
return x, y, true
}
// infinity 获取无穷远点
// 返回: sm2Point 无穷远点
func (c *sm2Curve) infinity() sm2Point {
return sm2Point{X: new(big.Int), Y: new(big.Int), Z: new(big.Int)}
}
// fieldAdd 计算有限域加法
// 入参: x 左操作数, y 右操作数
// 返回: *big.Int 计算结果
func (c *sm2Curve) fieldAdd(x, y *big.Int) *big.Int {
value := new(big.Int).Add(x, y)
return value.Mod(value, c.P)
}
// fieldSub 计算有限域减法
// 入参: x 左操作数, y 右操作数
// 返回: *big.Int 计算结果
func (c *sm2Curve) fieldSub(x, y *big.Int) *big.Int {
value := new(big.Int).Sub(x, y)
return value.Mod(value, c.P)
}
// fieldMul 计算有限域乘法
// 入参: x 左操作数, y 右操作数
// 返回: *big.Int 计算结果
func (c *sm2Curve) fieldMul(x, y *big.Int) *big.Int {
value := new(big.Int).Mul(x, y)
return value.Mod(value, c.P)
}
// fieldSquare 计算有限域平方
// 入参: x 操作数
// 返回: *big.Int 计算结果
func (c *sm2Curve) fieldSquare(x *big.Int) *big.Int {
return c.fieldMul(x, x)
}
// fieldScale 计算有限域整数倍
// 入参: x 操作数, scale 倍数
// 返回: *big.Int 计算结果
func (c *sm2Curve) fieldScale(x *big.Int, scale int64) *big.Int {
return c.fieldMul(x, big.NewInt(scale))
}
// sm2Fixed 转换为SM2固定长度字节
+123 -33
View File
@@ -18,6 +18,7 @@ import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/md5"
"crypto/rsa"
"crypto/sha1"
"crypto/sha256"
@@ -37,35 +38,44 @@ import (
)
// SignatureVerifyReport 签名验证报告
// Valid表示签名完整性及调用方指定的证书策略均通过
// Valid表示签名完整性、签名时间语义及调用方指定的证书策略均通过
// SealCertTimeOK仅提供制章证书在签名时间的状态信息, 不参与Valid判断
type SignatureVerifyReport struct {
ID string
BaseLoc string
Type SignType
Provider SignatureProvider
Signer string
SignCert SignatureCertInfo
SealCert SignatureCertInfo
SealType string
SignatureMethod string
SignatureDateTime string
DigestMethod string
References []SignatureReferenceVerify
Stamps []SignatureStamp
StampPositions []SignatureStampPosition
StampPositionError string
DigestOK bool
DataHashOK bool
SignedValueOK bool
SealOK bool
SealMatchOK bool
CertOK bool
CertTimeChecked bool
CertTimeOK bool
CertTrustChecked bool
CertTrustOK bool
Valid bool
Error string
ID string
BaseLoc string
Type SignType
Provider SignatureProvider
Signer string
SignCert SignatureCertInfo
SealCert SignatureCertInfo
SealInfo SignatureSealInfo
SealType string
SignatureMethod string
SignatureDateTime string
SignatureTime time.Time
DigestMethod string
References []SignatureReferenceVerify
Stamps []SignatureStamp
StampPositions []SignatureStampPosition
StampPositionError string
DigestOK bool
DataHashOK bool
SignedValueOK bool
SealOK bool
SealMatchOK bool
CertOK bool
SignatureTimeChecked bool
SignatureTimeOK bool
SealCertTimeChecked bool
SealCertTimeOK bool
SealTimeChecked bool
SealTimeOK bool
CertTimeChecked bool
CertTimeOK bool
CertTrustChecked bool
CertTrustOK bool
Valid bool
Error string
}
// IntegrityValid 判断签名完整性是否有效
@@ -75,13 +85,14 @@ func (report SignatureVerifyReport) IntegrityValid() bool {
}
// TrustedValid 判断签名是否可信有效
// 返回: bool 签名完整性、证书信任及证书有效期是否均验证通过
// 返回: 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 签名证书信息
type SignatureCertInfo struct {
Raw []byte
Subject string
CommonName string
Organization string
@@ -91,6 +102,18 @@ type SignatureCertInfo struct {
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 签名保护文件验证结果
type SignatureReferenceVerify struct {
FileRef string
@@ -239,6 +262,9 @@ func (r *Reader) verifySignature(sigListPath string, sigRef Signature, options *
Type: sigRef.Type,
SealMatchOK: true,
}
if report.Type == "" {
report.Type = SignTypeSeal
}
sigData, err := r.readFileExact(sigPath)
if err != nil {
report.Error = err.Error()
@@ -253,6 +279,9 @@ func (r *Reader) verifySignature(sigListPath string, sigRef Signature, options *
report.SignatureMethod = sigFile.SignedInfo.SignatureMethod
report.SignatureDateTime = sigFile.SignedInfo.SignatureDateTime
report.DigestMethod = sigFile.SignedInfo.References.CheckMethod
if report.DigestMethod == "" {
report.DigestMethod = "MD5"
}
report.References = r.verifySignatureReferences(sigPath, sigFile.SignedInfo.References)
report.Stamps = append(report.Stamps, sigFile.SignedInfo.StampAnnot...)
report.StampPositions, err = r.SignatureStampPositions(report.Stamps)
@@ -266,7 +295,7 @@ func (r *Reader) verifySignature(sigListPath string, sigRef Signature, options *
report.Error = err.Error()
return report
}
switch sigRef.Type {
switch report.Type {
case SignTypeSign:
result, err := verifyDigitalSignature(report.SignatureMethod, report.DigestMethod, signedValue, sigData, options)
if err != nil {
@@ -279,19 +308,23 @@ func (r *Reader) verifySignature(sigListPath string, sigRef Signature, options *
report.CertOK = result.CertOK
report.SignCert = result.CertInfo
report.Signer = result.CertInfo.CommonName
report.SignatureTime = parseSignatureDateTime(report.SignatureDateTime)
report.applySignatureTimePolicy()
report.applySignatureCertificatePolicy(options, result.SignerCerts, result.Certs)
report.Valid = report.IntegrityValid() && report.certificatePolicyOK()
return report
case "", SignTypeSeal:
case SignTypeSeal:
default:
report.Error = fmt.Sprintf("unsupported signature type: %s", sigRef.Type)
report.Error = fmt.Sprintf("unsupported signature type: %s", report.Type)
return report
}
sesResult, err := verifySESSignature(signedValue, sigData, options)
if sesResult != nil {
report.SignCert = sesResult.SignCert
report.SealCert = sesResult.SealCert
report.SealInfo = sesResult.SealInfo
report.SealType = sesResult.SealType
report.SignatureTime = sesResult.SignatureTime
report.Signer = sesResult.SignCert.CommonName
}
if err != nil {
@@ -302,6 +335,7 @@ func (r *Reader) verifySignature(sigListPath string, sigRef Signature, options *
report.SignedValueOK = sesResult.SignedOK
report.SealOK = sesResult.SealOK
report.CertOK = sesResult.CertOK
report.applySignatureTimePolicy()
report.applySignatureCertificatePolicy(options, [][]byte{sesResult.SignCertRaw, sesResult.SealCertRaw}, sesResult.Certs)
if sigFile.SignedInfo.Seal.BaseLoc != "" {
sealPath := signatureRefPath(sigPath, sigFile.SignedInfo.Seal.BaseLoc)
@@ -383,6 +417,10 @@ func (r *Reader) readFileExact(name string) ([]byte, error) {
// 入参: method 摘要算法, data 原文数据
// 返回: []byte 摘要值, error 错误信息
func signatureDigest(method string, data []byte) ([]byte, error) {
if strings.TrimSpace(method) == "" {
sum := md5.Sum(data)
return sum[:], nil
}
if isSM3DigestMethod(method) {
return signSM3(data), nil
}
@@ -397,6 +435,8 @@ func signatureDigest(method string, data []byte) ([]byte, error) {
// 返回: crypto.Hash 摘要算法, bool 是否支持
func signatureDigestHash(method string) (crypto.Hash, bool) {
switch signatureMethodText(method) {
case "1.2.840.113549.2.5", "MD5":
return crypto.MD5, true
case "1.3.14.3.2.26", "SHA1":
return crypto.SHA1, true
case "2.16.840.1.101.3.4.2.4", "SHA224":
@@ -453,6 +493,9 @@ func signatureMethodHash(method, digestMethod string) (crypto.Hash, error) {
// 返回: []byte 摘要值
func signatureHashBytes(h crypto.Hash, data []byte) []byte {
switch h {
case crypto.MD5:
sum := md5.Sum(data)
return sum[:]
case crypto.SHA1:
sum := sha1.Sum(data)
return sum[:]
@@ -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 判断证书策略是否通过
// 返回: bool 是否通过
func (report SignatureVerifyReport) certificatePolicyOK() bool {
if report.SignatureTimeChecked && !report.SignatureTimeOK {
return false
}
if report.SealTimeChecked && !report.SealTimeOK {
return false
}
if report.CertTimeChecked && !report.CertTimeOK {
return false
}
@@ -790,6 +862,7 @@ func signatureCertInfo(data []byte) SignatureCertInfo {
subject := certificateNameValues(cert.SubjectValue)
issuer := certificateNameValues(cert.IssuerValue)
info := SignatureCertInfo{
Raw: append([]byte(nil), data...),
Subject: certificateNameString(subject),
CommonName: certificateNameFirst(subject, "2.5.4.3"),
Organization: certificateNameFirst(subject, "2.5.4.10"),
@@ -803,6 +876,23 @@ func signatureCertInfo(data []byte) SignatureCertInfo {
return info
}
// parseSignatureDateTime 解析带时区的签名时间
// 入参: value 签名时间文本
// 返回: time.Time 签名时间
func parseSignatureDateTime(value string) time.Time {
value = strings.TrimSpace(value)
for _, layout := range []string{
time.RFC3339Nano,
"20060102150405.999999999Z07:00",
"20060102150405Z07:00",
} {
if t, err := time.Parse(layout, value); err == nil {
return t
}
}
return time.Time{}
}
const (
signatureExtensionKeyUsage = "2.5.29.15"
signatureExtensionBasicConstraints = "2.5.29.19"
+45
View File
@@ -20,6 +20,7 @@ import (
"strings"
"github.com/tdewolff/canvas"
canvastext "github.com/tdewolff/canvas/text"
"github.com/tdewolff/font"
)
@@ -29,6 +30,26 @@ type textGlyph struct {
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 字符到字形变换
type textGlyphTransform struct {
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))
}
// cachedTextGlyphPath 获取缓存的字形路径
// 入参: face 字体, glyph 绘制字形
// 返回: *canvas.Path 字形路径, float64 字形宽度
func (r *Renderer) cachedTextGlyphPath(face *canvas.FontFace, glyph textGlyph) (*canvas.Path, float64) {
key := textGlyphPathCacheKey{
font: face.Font,
size: face.Size,
fauxBold: face.FauxBold,
fauxItalic: face.FauxItalic,
xOffset: face.XOffset,
yOffset: face.YOffset,
language: face.Language,
script: face.Script,
direction: face.Direction,
glyph: glyph,
}
if cached, ok := r.textGlyphPathCache[key]; ok {
return cached.path, cached.width
}
path, width := textGlyphPath(face, glyph)
r.textGlyphPathCache[key] = textGlyphPathCacheValue{path: path, width: width}
return path, width
}
// hasTextMatrix 判断文本是否需要应用字形变换
// 入参: ctm 变换矩阵
// 返回: bool 是否需要变换