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