mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-08-30 20:22:39 +08:00
Compare commits
87
Commits
654bedbd33
...
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 | ||
|
|
5b0da8f06b | ||
|
|
4cc24722c2 | ||
|
|
19fadab4ad | ||
|
|
c20d3dd246 | ||
|
|
a62230eb93 | ||
|
|
f1a1f2e151 | ||
|
|
a1e6a86134 | ||
|
|
f4291aaa74 | ||
|
|
48aabc77ab | ||
|
|
ba1da58992 | ||
|
|
1c1173e1b7 | ||
|
|
368bafccdb | ||
|
|
51076634fa | ||
|
|
7ff1a4269a | ||
|
|
89577ea72f | ||
|
|
3fab819246 | ||
|
|
38e55d962c | ||
|
|
5b8b015bfc | ||
|
|
64536cd49b | ||
|
|
155f5c7299 | ||
|
|
9eb9104380 | ||
|
|
fa3260c108 | ||
|
|
92a53ed3ad | ||
|
|
65b37b37f0 | ||
|
|
5e4009eca5 | ||
|
|
1d99465df2 | ||
|
|
a665df062b | ||
|
|
ffd61d6412 | ||
|
|
7048bf4cb9 | ||
|
|
9199ae02d8 | ||
|
|
ef4028a2fc | ||
|
|
f191f1faec | ||
|
|
5254d22fb3 | ||
|
|
bb26adfe61 | ||
|
|
6be7f4b006 | ||
|
|
2f367e5a19 | ||
|
|
b91c6b5330 | ||
|
|
27a5da45ec | ||
|
|
cc0a627a3c | ||
|
|
2aad6818dd | ||
|
|
5c762cf1e6 | ||
|
|
e4f542c429 | ||
|
|
d7235a5078 | ||
|
|
c1e28bb1a3 | ||
|
|
725f1315fd | ||
|
|
b3b8e0bac0 | ||
|
|
06c5cea4d1 | ||
|
|
6bfb11df71 | ||
|
|
7fbad6005c | ||
|
|
61da0f3f5d | ||
|
|
98b37d865a | ||
|
|
28f3efea58 | ||
|
|
e3aa8aba77 | ||
|
|
889a6b1fae | ||
|
|
73336bcc0e | ||
|
|
67eed80507 | ||
|
|
3fd0b1bb5d | ||
|
|
3634a6125c | ||
|
|
ebfe7b5d17 |
@@ -27,7 +27,7 @@ go build -o ofdgo_webui.exe -trimpath -ldflags "-s -w -buildid=" ./cmd/webui/web
|
||||
go get -u github.com/xiaoqidun/ofdgo
|
||||
```
|
||||
|
||||
# 快速调用
|
||||
# 渲染输出
|
||||
```go
|
||||
package main
|
||||
|
||||
@@ -59,5 +59,51 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
# 签名验证
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/xiaoqidun/ofdgo"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 1. 打开OFD文件
|
||||
data, err := os.ReadFile("test.ofd")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// 2. 验证OFD签名
|
||||
reports, err := ofdgo.VerifySignaturesBytes(data)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// 3. 判断验证结果
|
||||
if len(reports) == 0 {
|
||||
log.Println("文件未发现签名")
|
||||
return
|
||||
}
|
||||
valid := true
|
||||
for _, report := range reports {
|
||||
if report.IntegrityValid() {
|
||||
log.Printf("签名%s验证通过", report.ID)
|
||||
continue
|
||||
}
|
||||
valid = false
|
||||
if report.Error == "" {
|
||||
log.Printf("签名%s验证失败", report.ID)
|
||||
} else {
|
||||
log.Printf("签名%s验证失败: %s", report.ID, report.Error)
|
||||
}
|
||||
}
|
||||
if !valid {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# 授权协议
|
||||
本项目使用 [Apache License 2.0](https://github.com/xiaoqidun/ofdgo/blob/main/LICENSE) 授权协议
|
||||
+21
-3
@@ -9,7 +9,7 @@
|
||||
<body>
|
||||
<div class="app">
|
||||
<header class="toolbar app-bar">
|
||||
<input id="ofdInput" class="file-input" type="file" accept=".ofd,application/ofd,application/zip">
|
||||
<input id="ofdInput" class="file-input" type="file" accept=".ofd">
|
||||
<input id="fontInput" class="file-input" type="file" accept=".ttf,.otf,.ttc,font/*" multiple>
|
||||
<div class="tool-group">
|
||||
<button id="ofdButton" class="button primary" type="button" data-short="打开">打开</button>
|
||||
@@ -36,8 +36,14 @@
|
||||
</div>
|
||||
<div class="tool-group">
|
||||
<select id="pageExportFormat" class="format-select" title="单页格式" aria-label="单页格式" disabled></select>
|
||||
<button id="exportPageButton" class="button" type="button" data-short="页" title="导出单页" aria-label="导出单页" disabled>单页</button>
|
||||
<button id="exportButton" class="button" type="button" data-short="文档" title="导出文档 PDF" aria-label="导出文档 PDF" disabled>文档</button>
|
||||
<select id="imageDPI" class="format-select dpi-select" title="图片 DPI" aria-label="图片 DPI" disabled>
|
||||
<option value="96">96</option>
|
||||
<option value="150">150</option>
|
||||
<option value="300" selected>300</option>
|
||||
<option value="600">600</option>
|
||||
</select>
|
||||
<button id="exportPageButton" class="button" type="button" data-short="单" title="导出单页" aria-label="导出单页" disabled>单页</button>
|
||||
<button id="exportButton" class="button" type="button" data-short="全" title="导出文档 PDF" aria-label="导出文档 PDF" disabled>文档</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -80,11 +86,22 @@
|
||||
<dt>类型</dt>
|
||||
<dd id="metaType">-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>签名</dt>
|
||||
<dd id="metaSignatures">0</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>字体</dt>
|
||||
<dd id="metaFonts">0</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<section id="signaturePanel" class="signature-panel" aria-labelledby="signaturePanelTitle" hidden>
|
||||
<div class="panel-row">
|
||||
<div id="signaturePanelTitle" class="panel-title compact">签名</div>
|
||||
<span id="signatureSummary" class="count-pill">0</span>
|
||||
</div>
|
||||
<div id="signatureList" class="signature-list"></div>
|
||||
</section>
|
||||
<section class="doc-font-panel" aria-labelledby="docFontPanelTitle">
|
||||
<div class="panel-row">
|
||||
<div id="docFontPanelTitle" class="panel-title compact">OFD 字体</div>
|
||||
@@ -101,6 +118,7 @@
|
||||
<button id="fontAddButton" class="small-button" type="button">添加</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="fontPermissionHint" class="font-permission-hint" hidden>需授权读取系统字体</div>
|
||||
<div id="fontList" class="font-list"></div>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
+164
-14
@@ -10,7 +10,7 @@
|
||||
--text-muted: #66737f;
|
||||
--accent: #0f7b68;
|
||||
--accent-strong: #095c4e;
|
||||
--focus: #c9892b;
|
||||
--focus: #0f7b68;
|
||||
--sidebar-width: clamp(220px, 20vw, 292px);
|
||||
--bar-bg: #fbfcfd;
|
||||
--bar-height: 40px;
|
||||
@@ -25,10 +25,18 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
background: #eef1f4;
|
||||
}
|
||||
|
||||
@@ -64,7 +72,7 @@ a:focus-visible {
|
||||
.app {
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||
width: 100vw;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -209,6 +217,10 @@ button:not(:disabled):hover {
|
||||
color: #1f2428;
|
||||
}
|
||||
|
||||
.dpi-select {
|
||||
width: 54px;
|
||||
}
|
||||
|
||||
.format-select:focus,
|
||||
.format-select:focus-visible {
|
||||
border-color: var(--accent);
|
||||
@@ -389,6 +401,10 @@ body[data-hide-meta] .meta-panel {
|
||||
background-size: 24px 24px;
|
||||
}
|
||||
|
||||
.viewer-panel.single-page-fits-height {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
@@ -464,15 +480,18 @@ body[data-hide-meta] .meta-panel {
|
||||
}
|
||||
|
||||
.page-frame {
|
||||
display: grid;
|
||||
align-items: safe center;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
padding: var(--page-space);
|
||||
padding-block: var(--fit-space, var(--page-space));
|
||||
}
|
||||
|
||||
.svg-host {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: var(--page-space);
|
||||
gap: var(--fit-gap, var(--fit-space, var(--page-space)));
|
||||
width: max-content;
|
||||
margin: 0 auto;
|
||||
}
|
||||
@@ -511,6 +530,23 @@ body[data-hide-meta] .meta-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.stamp-highlight {
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
border: 2px solid var(--accent);
|
||||
background: rgba(15, 123, 104, 0.08);
|
||||
box-shadow: 0 0 0 3px rgba(15, 123, 104, 0.16);
|
||||
pointer-events: none;
|
||||
animation: stampPulse 0.9s ease-in-out 2;
|
||||
}
|
||||
|
||||
@keyframes stampPulse {
|
||||
50% {
|
||||
border-color: var(--accent-strong);
|
||||
box-shadow: 0 0 0 5px rgba(15, 123, 104, 0.18);
|
||||
}
|
||||
}
|
||||
|
||||
.ofd-svg {
|
||||
display: block;
|
||||
max-width: none;
|
||||
@@ -540,7 +576,8 @@ body[data-hide-meta] .meta-panel {
|
||||
}
|
||||
|
||||
.font-panel,
|
||||
.doc-font-panel {
|
||||
.doc-font-panel,
|
||||
.signature-panel {
|
||||
margin-top: 18px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--line);
|
||||
@@ -561,12 +598,14 @@ body[data-hide-meta] .meta-panel {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.doc-font-list {
|
||||
.doc-font-list,
|
||||
.signature-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.doc-font-row {
|
||||
.doc-font-row,
|
||||
.signature-row {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: 9px;
|
||||
@@ -577,6 +616,7 @@ body[data-hide-meta] .meta-panel {
|
||||
|
||||
.page-list-item:not(:disabled):hover,
|
||||
.doc-font-row:hover,
|
||||
.signature-row:hover,
|
||||
.font-row:hover {
|
||||
border-color: var(--line-strong);
|
||||
background: #ffffff;
|
||||
@@ -592,14 +632,26 @@ body[data-hide-meta] .meta-panel {
|
||||
background: #fffdf7;
|
||||
}
|
||||
|
||||
.doc-font-head {
|
||||
.signature-row.valid {
|
||||
border-color: #bdd8cc;
|
||||
background: #fbfefd;
|
||||
}
|
||||
|
||||
.signature-row.invalid {
|
||||
border-color: #e2c7c4;
|
||||
background: #fffdfc;
|
||||
}
|
||||
|
||||
.doc-font-head,
|
||||
.signature-head {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.doc-font-name {
|
||||
.doc-font-name,
|
||||
.signature-name {
|
||||
overflow: hidden;
|
||||
color: #1f2428;
|
||||
font-size: 13px;
|
||||
@@ -608,7 +660,51 @@ body[data-hide-meta] .meta-panel {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.doc-font-badges {
|
||||
.signature-name-button {
|
||||
display: inline;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
font-weight: inherit;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.signature-name-button:not(:disabled):hover {
|
||||
background: transparent;
|
||||
color: var(--accent-strong);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.signature-name-with-stamps {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.signature-stamp-group {
|
||||
color: var(--text-muted);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.signature-stamp-link {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--accent-strong);
|
||||
font: inherit;
|
||||
font-weight: 400;
|
||||
line-height: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.signature-stamp-link:not(:disabled):hover {
|
||||
background: transparent;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.doc-font-badges,
|
||||
.signature-badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
@@ -616,6 +712,10 @@ body[data-hide-meta] .meta-panel {
|
||||
}
|
||||
|
||||
.font-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 30px;
|
||||
padding: 3px 6px;
|
||||
background: #eef1f4;
|
||||
color: var(--text-muted);
|
||||
@@ -635,11 +735,45 @@ body[data-hide-meta] .meta-panel {
|
||||
color: #705719;
|
||||
}
|
||||
|
||||
.font-badge.missing {
|
||||
.font-badge.missing,
|
||||
.font-badge.invalid {
|
||||
background: #f9e8e5;
|
||||
color: #803f38;
|
||||
}
|
||||
|
||||
.font-badge.valid {
|
||||
background: #e4f3ef;
|
||||
color: var(--accent-strong);
|
||||
}
|
||||
|
||||
.signature-line {
|
||||
display: grid;
|
||||
grid-template-columns: 30px minmax(0, 1fr);
|
||||
align-items: start;
|
||||
gap: 7px;
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.signature-label {
|
||||
color: #7b8791;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.signature-value {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.signature-line.ok .signature-value {
|
||||
color: var(--accent-strong);
|
||||
}
|
||||
|
||||
.signature-line.fail .signature-value {
|
||||
color: #803f38;
|
||||
}
|
||||
|
||||
.doc-font-detail {
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
@@ -652,6 +786,16 @@ body[data-hide-meta] .meta-panel {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.font-permission-hint {
|
||||
margin: 0 0 8px;
|
||||
padding: 7px 8px;
|
||||
border: 1px solid var(--line);
|
||||
background: #ffffff;
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.font-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
@@ -742,6 +886,7 @@ body[data-hide-meta] .meta-panel {
|
||||
|
||||
.app-footer {
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
border-top: 1px solid var(--line);
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
@@ -749,9 +894,13 @@ body[data-hide-meta] .meta-panel {
|
||||
|
||||
.footer-brand {
|
||||
flex: 0 0 var(--sidebar-width);
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
font-weight: 400;
|
||||
color: var(--accent-strong);
|
||||
text-decoration: none;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
@@ -772,6 +921,7 @@ body[data-hide-meta] .meta-panel {
|
||||
flex: 0 0 var(--sidebar-width);
|
||||
justify-content: flex-end;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-footer a {
|
||||
@@ -927,15 +1077,15 @@ body[data-hide-meta] .meta-panel {
|
||||
min-width: 34px;
|
||||
}
|
||||
|
||||
#exportButton {
|
||||
padding-inline: 8px;
|
||||
}
|
||||
|
||||
.format-select {
|
||||
width: 52px;
|
||||
padding: 0 3px;
|
||||
}
|
||||
|
||||
.dpi-select {
|
||||
width: 48px;
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
+640
-98
File diff suppressed because it is too large
Load Diff
@@ -1,24 +1,25 @@
|
||||
module github.com/xiaoqidun/ofdgo
|
||||
|
||||
go 1.25.0
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
github.com/tdewolff/canvas v0.0.0-20260617131110-529326a1955e
|
||||
github.com/xiaoqidun/jbig2 v0.0.0-20260707061149-c2ff34658234
|
||||
golang.org/x/image v0.43.0
|
||||
golang.org/x/text v0.39.0
|
||||
github.com/tdewolff/canvas v0.0.0-20260822211237-56e622de465a
|
||||
github.com/tdewolff/font v0.0.0-20260822205238-d0d2f004b117
|
||||
github.com/xiaoqidun/jbig2 v0.0.0-20260823111534-2e67bcec7059
|
||||
golang.org/x/image v0.45.0
|
||||
golang.org/x/text v0.41.0
|
||||
)
|
||||
|
||||
require (
|
||||
codeberg.org/go-latex/latex v0.2.0 // indirect
|
||||
codeberg.org/go-pdf/fpdf v0.11.1 // indirect
|
||||
codeberg.org/go-latex/latex v0.3.0 // indirect
|
||||
codeberg.org/go-pdf/fpdf v0.12.0 // indirect
|
||||
github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 // indirect
|
||||
github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 // indirect
|
||||
github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc // indirect
|
||||
github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 // indirect
|
||||
github.com/ByteArena/poly2tri-go v0.0.0-20170716161910-d102ad91854f // indirect
|
||||
github.com/Kagami/go-avif v0.1.0 // indirect
|
||||
github.com/andybalholm/brotli v1.2.1 // indirect
|
||||
github.com/andybalholm/brotli v1.2.2 // indirect
|
||||
github.com/benoitkugler/textlayout v0.3.2 // indirect
|
||||
github.com/benoitkugler/textprocessing v0.0.6 // indirect
|
||||
github.com/go-fonts/latin-modern v0.3.3 // indirect
|
||||
@@ -27,14 +28,14 @@ require (
|
||||
github.com/kolesa-team/go-webp v1.0.5 // indirect
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
|
||||
github.com/srwiley/scanx v0.0.0-20190309010443-e94503791388 // indirect
|
||||
github.com/tdewolff/font v0.0.0-20260424075104-b5eeb1e23189 // indirect
|
||||
github.com/tdewolff/minify/v2 v2.24.13 // indirect
|
||||
github.com/tdewolff/parse/v2 v2.8.12 // indirect
|
||||
github.com/tdewolff/minify/v2 v2.24.17 // indirect
|
||||
github.com/tdewolff/parse/v2 v2.8.16 // indirect
|
||||
github.com/wcharczuk/go-chart/v2 v2.1.2 // indirect
|
||||
github.com/yuin/goldmark v1.8.2 // indirect
|
||||
golang.org/x/net v0.53.0 // indirect
|
||||
github.com/yuin/goldmark v1.8.5 // indirect
|
||||
golang.org/x/net v0.58.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
gonum.org/v1/plot v0.17.0 // indirect
|
||||
modernc.org/knuth v0.5.5 // indirect
|
||||
modernc.org/knuth v0.6.0 // indirect
|
||||
modernc.org/token v1.1.0 // indirect
|
||||
star-tex.org/x/tex v0.7.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
codeberg.org/go-fonts/dejavu v0.4.0 h1:2yn58Vkh4CFK3ipacWUAIE3XVBGNa0y1bc95Bmfx91I=
|
||||
codeberg.org/go-fonts/dejavu v0.4.0/go.mod h1:abni088lmhQJvso2Lsb7azCKzwkfcnttl6tL1UTWKzg=
|
||||
codeberg.org/go-fonts/liberation v0.5.0 h1:SsKoMO1v1OZmzkG2DY+7ZkCL9U+rrWI09niOLfQ5Bo0=
|
||||
codeberg.org/go-fonts/liberation v0.5.0/go.mod h1:zS/2e1354/mJ4pGzIIaEtm/59VFCFnYC7YV6YdGl5GU=
|
||||
codeberg.org/go-latex/latex v0.2.0 h1:Ol/a6VHY06N+5gPfewswymoRb5ZcKDXWVaVegcx4hbI=
|
||||
codeberg.org/go-latex/latex v0.2.0/go.mod h1:VJAwQir7/T8LZxj7xAPivISKiVOwkMpQ8bTuPQ31X0Y=
|
||||
codeberg.org/go-pdf/fpdf v0.11.1 h1:U8+coOTDVLxHIXZgGvkfQEi/q0hYHYvEHFuGNX2GzGs=
|
||||
codeberg.org/go-pdf/fpdf v0.11.1/go.mod h1:Y0DGRAdZ0OmnZPvjbMp/1bYxmIPxm0ws4tfoPOc4LjU=
|
||||
codeberg.org/go-fonts/liberation v0.6.0 h1:15Gh6SdwYve22CWCm9jYpVpRuaTh726av2TgHTHvAtQ=
|
||||
codeberg.org/go-fonts/liberation v0.6.0/go.mod h1:J15VAa+lyxdcI/Je7lDDDl6QOhLk9feNBnnwXqEHXOk=
|
||||
codeberg.org/go-latex/latex v0.3.0 h1:LKTaDHFbEC2PH1sh0sYv6PZ1pzs/g2aoeV1HItWj/bg=
|
||||
codeberg.org/go-latex/latex v0.3.0/go.mod h1:8ETijTpK2bFtwRAXLXe1RZJrYxnc5pibibZfQBj+Lk4=
|
||||
codeberg.org/go-pdf/fpdf v0.12.0 h1:g8E/1VqGqB2lZUUaqQrrTnA0IEJLPTTX1DZ0qS/ZmhU=
|
||||
codeberg.org/go-pdf/fpdf v0.12.0/go.mod h1:WJNJ2bvCj81rZBdhOf7lKOGoSl+OKMXcIcXqDcP8r5Y=
|
||||
git.sr.ht/~sbinet/cmpimg v0.1.0 h1:E0zPRk2muWuCqSKSVZIWsgtU9pjsw3eKHi8VmQeScxo=
|
||||
git.sr.ht/~sbinet/cmpimg v0.1.0/go.mod h1:FU12psLbF4TfNXkKH2ZZQ29crIqoiqTZmeQ7dkp/pxE=
|
||||
git.sr.ht/~sbinet/gg v0.7.0 h1:YmNf7YKd7diDMTPm86hZa1EM3pbkOyD/zzjl0LZUdNM=
|
||||
git.sr.ht/~sbinet/gg v0.7.0/go.mod h1:VYeli15tpMM4EvqlivlVbbyvWZlOU+EZn4XZmfBGUdM=
|
||||
git.sr.ht/~sbinet/gg v0.8.0 h1:PjQ4AgUWRz7Dy6PVBKMLLg96eRCos1U+H6F3FJuKWHo=
|
||||
git.sr.ht/~sbinet/gg v0.8.0/go.mod h1:XhlGCvSXts+BaZ1XMZLxLSFUDvwsJ/2El5F6XzPep6o=
|
||||
github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 h1:1qlsVAQJXZHsaM8b6OLVo6muQUQd4CwkH/D3fnnbHXA=
|
||||
github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298/go.mod h1:D+QujdIlUNfa0igpNMk6UIvlb6C252URs4yupRUV4lQ=
|
||||
github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 h1:lTG4HQym5oPKjL7nGs+csTgiDna685ZXjxijkne828g=
|
||||
@@ -24,8 +24,8 @@ github.com/Kagami/go-avif v0.1.0 h1:8GHAGLxCdFfhpd4Zg8j1EqO7rtcQNenxIDerC/uu68w=
|
||||
github.com/Kagami/go-avif v0.1.0/go.mod h1:OPmPqzNdQq3+sXm0HqaUJQ9W/4k+Elbc3RSfJUemDKA=
|
||||
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw=
|
||||
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
|
||||
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
|
||||
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/andybalholm/brotli v1.2.2 h1:HzTuoo2ErYQqf5qvcJInB8uvqSVxRttzkFexPWtnceM=
|
||||
github.com/andybalholm/brotli v1.2.2/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/benoitkugler/pstokenizer v1.0.0/go.mod h1:l1G2Voirz0q/jj0TQfabNxVsa8HZXh/VMxFSRALWTiE=
|
||||
github.com/benoitkugler/textlayout v0.3.2 h1:Y/LTkwpQ9G0Fia9yhPmZA9IR5AnE8Cq30j3C+Gx5/IE=
|
||||
github.com/benoitkugler/textlayout v0.3.2/go.mod h1:o+1hFV+JSHBC9qNLIuwVoLedERU7sBPgEFcuSgfvi/w=
|
||||
@@ -33,8 +33,6 @@ github.com/benoitkugler/textlayout-testdata v0.1.1/go.mod h1:i/qZl09BbUOtd7Bu/W1
|
||||
github.com/benoitkugler/textprocessing v0.0.6 h1:obkMyj62GEPg3xUVYqROlCN22z1OleuZm6ULqX9Om1g=
|
||||
github.com/benoitkugler/textprocessing v0.0.6/go.mod h1:Io0gN08/PXEzrSOWFa88xHx2Xv3VjvLMY7H76YoI23A=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-fonts/latin-modern v0.3.3 h1:g2xNgI8yzdNzIVm+qvbMryB6yGPe0pSMss8QT3QwlJ0=
|
||||
github.com/go-fonts/latin-modern v0.3.3/go.mod h1:tHaiWDGze4EPB0Go4cLT5M3QzRY3peya09Z/8KSCrpY=
|
||||
github.com/go-text/typesetting v0.3.4 h1:YYurUOtEb9kGSOz4uE3k4OpBGsp1dDL8+fjCeaFamAU=
|
||||
@@ -59,28 +57,29 @@ github.com/srwiley/scanx v0.0.0-20190309010443-e94503791388 h1:ZdkidVdpLW13BQ9a+
|
||||
github.com/srwiley/scanx v0.0.0-20190309010443-e94503791388/go.mod h1:C/WY5lmWfMtPFYYBTd3Lzdn4FTLr+RxlIeiBNye+/os=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tdewolff/canvas v0.0.0-20260617131110-529326a1955e h1:Z9HqI3WUPBDwJEDH9BCLpQGvjmGs0XNRDBLry4shZfs=
|
||||
github.com/tdewolff/canvas v0.0.0-20260617131110-529326a1955e/go.mod h1:Lz12ClttuKvo2jb1UxBfJJWmKV0LmEc8IRo/30R7z+k=
|
||||
github.com/tdewolff/font v0.0.0-20260424075104-b5eeb1e23189 h1:wP/+8oeXiF55DUy9U0BP4iDD2mNu+o7evt5QLbLHv9E=
|
||||
github.com/tdewolff/font v0.0.0-20260424075104-b5eeb1e23189/go.mod h1:KjNTiF0TXOXJyWGS/0DPXRknEC20c12kc0JcCYNqtz4=
|
||||
github.com/tdewolff/minify/v2 v2.24.13 h1:xrcF7gKDnUszseEY9WX9mUlZII2v2Go/QAcAwRASw58=
|
||||
github.com/tdewolff/minify/v2 v2.24.13/go.mod h1:emvwoYeIl8bfAKqRU5ww95LX9Gpggpqv/naal9a8Yq0=
|
||||
github.com/tdewolff/parse/v2 v2.8.12 h1:5BBjfaCv482v3nltlS0u6wH1xJaxjR6ofDrWttNvROg=
|
||||
github.com/tdewolff/parse/v2 v2.8.12/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo=
|
||||
github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||
github.com/tdewolff/canvas v0.0.0-20260822211237-56e622de465a h1:riaFBiYaIv1gM31xdcU0Yo7bdp3WebF4dggO/JK0g/8=
|
||||
github.com/tdewolff/canvas v0.0.0-20260822211237-56e622de465a/go.mod h1:K9Ux3cwFtyAFRITZ46g7v+ORXgI3ENtQa5rL9yjNz5Y=
|
||||
github.com/tdewolff/font v0.0.0-20260822205238-d0d2f004b117 h1:5vSJjsbV9AH0U+Ex0uej8ROrpOBlOVBrw0mGlVef7oo=
|
||||
github.com/tdewolff/font v0.0.0-20260822205238-d0d2f004b117/go.mod h1:ipESUihcEdgRBhIwTRHmBTOMamccMQrp4iCxfa1aFws=
|
||||
github.com/tdewolff/minify/v2 v2.24.17 h1:6AbitfVyq0M7aW6i+XL7+49DeTQZwloOMs9O574arBg=
|
||||
github.com/tdewolff/minify/v2 v2.24.17/go.mod h1:kVqn9vxXUKtlHexSNrWbYePqioOT5mc4ou/KVSMpfCM=
|
||||
github.com/tdewolff/parse/v2 v2.8.16 h1:bLk5svUOQRkW/Y2SJ+DeENSIkZBcTIkq+Atyv5D8feI=
|
||||
github.com/tdewolff/parse/v2 v2.8.16/go.mod h1:XdsoSFThlVIRIajAuqz1evNY7bagZS8LBOPA3aVopwQ=
|
||||
github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk=
|
||||
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||
github.com/wcharczuk/go-chart/v2 v2.1.2 h1:Y17/oYNuXwZg6TFag06qe8sBajwwsuvPiJJXcUcLL6E=
|
||||
github.com/wcharczuk/go-chart/v2 v2.1.2/go.mod h1:Zi4hbaqlWpYajnXB2K22IUYVXRXaLfSGNNR7P4ukyyQ=
|
||||
github.com/xiaoqidun/jbig2 v0.0.0-20260707061149-c2ff34658234 h1:aTUfzcRBfRYrEX0gxWRRQJGd/z9LJTu15eFC9pNVT00=
|
||||
github.com/xiaoqidun/jbig2 v0.0.0-20260707061149-c2ff34658234/go.mod h1:c5zFNbM18iSRcfS9ziMZquU3lFJGmN1w6CUfuEDw0WI=
|
||||
github.com/xiaoqidun/jbig2 v0.0.0-20260823111534-2e67bcec7059 h1:/JXxWbNTePSVfshS7BVRm75Kh1/5rixvLpgTRw7MMv8=
|
||||
github.com/xiaoqidun/jbig2 v0.0.0-20260823111534-2e67bcec7059/go.mod h1:mdoUQnVPIA4yS0/+ZrtxJNiDdbphI0RMu+DPfxXfVhE=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
|
||||
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
|
||||
github.com/yuin/goldmark v1.8.5/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
@@ -88,8 +87,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/image v0.0.0-20210504121937-7319ad40d33e/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
||||
golang.org/x/image v0.43.0 h1:FLxcP4ec2350nTfOC8ysKtqYSIFbk/QGjw1ZHNP4tsY=
|
||||
golang.org/x/image v0.43.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q=
|
||||
golang.org/x/image v0.45.0 h1:FMb1nTbH5H9vF55SriQHgFw5GnNL9Jg6L25BwXKzhB0=
|
||||
golang.org/x/image v0.45.0/go.mod h1:n62x/7RqlwXDvGsSU4u6IUTUf6KghUZ9Bt7cG/T9Fx4=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
@@ -104,8 +103,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
||||
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -123,6 +122,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -141,8 +142,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
|
||||
golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
|
||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
@@ -154,10 +155,8 @@ gonum.org/v1/plot v0.17.0 h1:d0DwPVBe9jnEGqQBoZGl/P2M9WciJbG2CnV59C9QBT4=
|
||||
gonum.org/v1/plot v0.17.0/go.mod h1:ipt2GUN1oqzr2O7wCjLDtw1ShfIYYNBp4o0O1Ez5B3Y=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/knuth v0.5.5 h1:6lap2U/ISm8aC/4NU58ALFCRllNPaK0EZcIGY/oDgUg=
|
||||
modernc.org/knuth v0.5.5/go.mod h1:e5SBb35HQBj2aFwbBO3ClPcViLY3Wi0LzaOd7c/3qMk=
|
||||
modernc.org/knuth v0.6.0 h1:zLv0hzgdcpcCUB/iMwpQvn9m/iciqO/hyNXlpWymMKU=
|
||||
modernc.org/knuth v0.6.0/go.mod h1:ISAZV+URZIlE8h1BwNVg1NzpwHKndV5lQ13NPPj3tds=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4=
|
||||
|
||||
+47
-15
@@ -17,7 +17,6 @@
|
||||
package webui
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"syscall/js"
|
||||
@@ -45,7 +44,7 @@ func RunWASM() {
|
||||
registerCallback("ofdgoExportFormats", exportFormats)
|
||||
registerCallback("ofdgoExportPage", exportPage)
|
||||
registerCallback("ofdgoExportPDF", exportPDF)
|
||||
registerCallback("ofdgoFontCandidates", fontCandidates)
|
||||
registerCallback("ofdgoFontSystemNames", fontSystemNames)
|
||||
select {}
|
||||
}
|
||||
|
||||
@@ -57,6 +56,9 @@ func registerCallback(name string, fn func([]js.Value) (any, error)) {
|
||||
if err != nil {
|
||||
return encodeResult(apiResult{OK: false, Error: err.Error()})
|
||||
}
|
||||
if result, ok := data.(js.Value); ok {
|
||||
return result
|
||||
}
|
||||
return encodeResult(apiResult{OK: true, Data: data})
|
||||
})
|
||||
js.Global().Set(name, cb)
|
||||
@@ -113,7 +115,18 @@ func renderPage(args []js.Value) (any, error) {
|
||||
if len(args) == 0 {
|
||||
return nil, fmt.Errorf("missing page index")
|
||||
}
|
||||
return currentSession.RenderPageSVG(args[0].Int())
|
||||
page, err := currentSession.RenderPageSVG(args[0].Int())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return successResult(map[string]any{
|
||||
"index": page.Index,
|
||||
"number": page.Number,
|
||||
"id": page.ID,
|
||||
"width": page.Width,
|
||||
"height": page.Height,
|
||||
"svg": page.SVG,
|
||||
}), nil
|
||||
}
|
||||
|
||||
// exportFormats 获取导出格式
|
||||
@@ -130,21 +143,21 @@ func exportPage(args []js.Value) (any, error) {
|
||||
if currentSession == nil {
|
||||
return nil, fmt.Errorf("ofd document is not opened")
|
||||
}
|
||||
if len(args) < 2 {
|
||||
if len(args) < 3 {
|
||||
return nil, fmt.Errorf("missing export page arguments")
|
||||
}
|
||||
data, format, err := currentSession.ExportPage(args[0].Int(), args[1].String())
|
||||
data, format, err := currentSession.ExportPage(args[0].Int(), args[1].String(), args[2].Float())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]any{
|
||||
"base64": base64.StdEncoding.EncodeToString(data),
|
||||
return successResult(map[string]any{
|
||||
"bytes": bytesToJS(data),
|
||||
"size": len(data),
|
||||
"format": format.Value,
|
||||
"label": format.Label,
|
||||
"extension": format.Extension,
|
||||
"mime": format.MIME,
|
||||
}, nil
|
||||
}), nil
|
||||
}
|
||||
|
||||
// exportPDF 导出OFD文档为PDF
|
||||
@@ -158,18 +171,37 @@ func exportPDF(args []js.Value) (any, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]any{
|
||||
"base64": base64.StdEncoding.EncodeToString(data),
|
||||
return successResult(map[string]any{
|
||||
"bytes": bytesToJS(data),
|
||||
"size": len(data),
|
||||
}, nil
|
||||
}), nil
|
||||
}
|
||||
|
||||
// fontCandidates 获取字体候选名称
|
||||
// successResult 创建成功接口结果
|
||||
// 入参: data 返回数据
|
||||
// 返回: js.Value 接口结果
|
||||
func successResult(data any) js.Value {
|
||||
result := js.Global().Get("Object").New()
|
||||
result.Set("ok", true)
|
||||
result.Set("data", data)
|
||||
return result
|
||||
}
|
||||
|
||||
// bytesToJS 将字节数据转换为Uint8Array
|
||||
// 入参: data 字节数据
|
||||
// 返回: js.Value Uint8Array对象
|
||||
func bytesToJS(data []byte) js.Value {
|
||||
value := js.Global().Get("Uint8Array").New(len(data))
|
||||
js.CopyBytesToJS(value, data)
|
||||
return value
|
||||
}
|
||||
|
||||
// fontSystemNames 获取系统字体名称
|
||||
// 入参: args 浏览器参数
|
||||
// 返回: any 字体候选名称, error 错误信息
|
||||
func fontCandidates(args []js.Value) (any, error) {
|
||||
// 返回: any 系统字体名称, error 错误信息
|
||||
func fontSystemNames(args []js.Value) (any, error) {
|
||||
names := stringsFromJS(jsArg(args, 0))
|
||||
return ofdgo.FontCandidateNames(names...), nil
|
||||
return ofdgo.FontSystemNames(names...), nil
|
||||
}
|
||||
|
||||
// bytesFromJS 从浏览器值读取二进制数据
|
||||
|
||||
+238
-11
@@ -39,11 +39,66 @@ type FontFile = ofdgo.FontFile
|
||||
// FontInfo OFD字体信息
|
||||
type FontInfo = ofdgo.FontInfo
|
||||
|
||||
// SignatureInfo 签名验证信息
|
||||
type SignatureInfo struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Valid bool `json:"valid"`
|
||||
Version string `json:"version,omitempty"`
|
||||
SealType string `json:"sealType,omitempty"`
|
||||
SealID string `json:"sealId,omitempty"`
|
||||
SealName string `json:"sealName,omitempty"`
|
||||
SealVendor string `json:"sealVendor,omitempty"`
|
||||
Signer string `json:"signer,omitempty"`
|
||||
SignatureDateTime string `json:"signatureDateTime,omitempty"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
Company string `json:"company,omitempty"`
|
||||
DataHashOK bool `json:"dataHashOK"`
|
||||
SignedValueOK bool `json:"signedValueOK"`
|
||||
SealOK bool `json:"sealOK"`
|
||||
SealMatchOK bool `json:"sealMatchOK"`
|
||||
CertOK bool `json:"certOK"`
|
||||
SignatureTimeChecked bool `json:"signatureTimeChecked,omitempty"`
|
||||
SignatureTimeOK bool `json:"signatureTimeOK,omitempty"`
|
||||
SealTimeChecked bool `json:"sealTimeChecked,omitempty"`
|
||||
SealTimeOK bool `json:"sealTimeOK,omitempty"`
|
||||
SealCertTimeChecked bool `json:"sealCertTimeChecked,omitempty"`
|
||||
SealCertTimeOK bool `json:"sealCertTimeOK,omitempty"`
|
||||
CertTimeChecked bool `json:"certTimeChecked,omitempty"`
|
||||
CertTimeOK bool `json:"certTimeOK,omitempty"`
|
||||
CertTrustChecked bool `json:"certTrustChecked,omitempty"`
|
||||
CertTrustOK bool `json:"certTrustOK,omitempty"`
|
||||
ReferenceCount int `json:"referenceCount"`
|
||||
ReferencePassed int `json:"referencePassed"`
|
||||
SignatureMethod string `json:"signatureMethod,omitempty"`
|
||||
DigestMethod string `json:"digestMethod,omitempty"`
|
||||
SignSerial string `json:"signSerial,omitempty"`
|
||||
SignSubject string `json:"signSubject,omitempty"`
|
||||
SignIssuer string `json:"signIssuer,omitempty"`
|
||||
SealSubject string `json:"sealSubject,omitempty"`
|
||||
Stamps []SignatureStampInfo `json:"stamps,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// SignatureStampInfo 签名外观信息
|
||||
type SignatureStampInfo struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Page int `json:"page,omitempty"`
|
||||
PageID string `json:"pageId,omitempty"`
|
||||
Boundary string `json:"boundary,omitempty"`
|
||||
X float64 `json:"x,omitempty"`
|
||||
Y float64 `json:"y,omitempty"`
|
||||
Width float64 `json:"width,omitempty"`
|
||||
Height float64 `json:"height,omitempty"`
|
||||
}
|
||||
|
||||
// Session WebUI文档会话
|
||||
type Session struct {
|
||||
Reader *ofdgo.Reader
|
||||
Renderer *ofdgo.Renderer
|
||||
doc *ofdgo.Document
|
||||
pageCache map[int]*ofdgo.PageContent
|
||||
boxCache map[int]ofdgo.Box
|
||||
}
|
||||
|
||||
// DocumentInfo 文档信息
|
||||
@@ -57,7 +112,10 @@ type DocumentInfo struct {
|
||||
ModDate string `json:"modDate"`
|
||||
PageCount int `json:"pageCount"`
|
||||
FontCount int `json:"fontCount"`
|
||||
SignatureCount int `json:"signatureCount"`
|
||||
SignatureError string `json:"signatureError,omitempty"`
|
||||
Fonts []FontInfo `json:"fonts"`
|
||||
Signatures []SignatureInfo `json:"signatures"`
|
||||
Pages []PageInfo `json:"pages"`
|
||||
}
|
||||
|
||||
@@ -146,7 +204,13 @@ func Open(data []byte, opts OpenOptions) (*Session, error) {
|
||||
rendererOptions = append(rendererOptions, ofdgo.WithFontFS(fontFS))
|
||||
}
|
||||
rendererOptions = append(rendererOptions, ofdgo.WithAnnotations(opts.RenderAnnotations))
|
||||
return &Session{Reader: reader, Renderer: ofdgo.NewRenderer(reader, rendererOptions...), doc: doc}, nil
|
||||
return &Session{
|
||||
Reader: reader,
|
||||
Renderer: ofdgo.NewRenderer(reader, rendererOptions...),
|
||||
doc: doc,
|
||||
pageCache: make(map[int]*ofdgo.PageContent),
|
||||
boxCache: make(map[int]ofdgo.Box),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close 关闭文档会话
|
||||
@@ -167,10 +231,12 @@ func (s *Session) Info() DocumentInfo {
|
||||
PageCount: len(s.doc.Pages.Page),
|
||||
Pages: make([]PageInfo, 0, len(s.doc.Pages.Page)),
|
||||
}
|
||||
if fonts, err := s.Renderer.FontInfos(); err == nil {
|
||||
info.Fonts = fonts
|
||||
if signatures, err := s.signatureInfos(); err == nil {
|
||||
info.Signatures = signatures
|
||||
} else {
|
||||
info.SignatureError = err.Error()
|
||||
}
|
||||
info.FontCount = len(info.Fonts)
|
||||
info.SignatureCount = len(info.Signatures)
|
||||
if docInfo, err := s.Reader.DocInfo(); err == nil && docInfo != nil {
|
||||
info.Title = docInfo.Title
|
||||
info.Author = docInfo.Author
|
||||
@@ -178,16 +244,22 @@ func (s *Session) Info() DocumentInfo {
|
||||
info.CreationDate = docInfo.CreationDate
|
||||
info.ModDate = docInfo.ModDate
|
||||
}
|
||||
pages := make([]*ofdgo.PageContent, 0, len(s.doc.Pages.Page))
|
||||
for index, pageRef := range s.doc.Pages.Page {
|
||||
pageInfo := PageInfo{Index: index, ID: pageRef.ID}
|
||||
if page, err := s.Reader.PageContent(pageRef); err == nil {
|
||||
if box, err := s.Renderer.GetPageBox(page); err == nil {
|
||||
if _, page, err := s.pageContent(index); err == nil {
|
||||
pages = append(pages, page)
|
||||
if box, err := s.pageBox(index, page); err == nil {
|
||||
pageInfo.Width = box.W
|
||||
pageInfo.Height = box.H
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -199,7 +271,7 @@ func (s *Session) RenderPageSVG(index int) (PageSVG, error) {
|
||||
if err != nil {
|
||||
return PageSVG{}, err
|
||||
}
|
||||
box, err := s.Renderer.GetPageBox(page)
|
||||
box, err := s.pageBox(index, page)
|
||||
if err != nil {
|
||||
return PageSVG{}, err
|
||||
}
|
||||
@@ -211,9 +283,9 @@ func (s *Session) RenderPageSVG(index int) (PageSVG, error) {
|
||||
}
|
||||
|
||||
// ExportPage 导出单页
|
||||
// 入参: index 页面索引, value 导出格式
|
||||
// 入参: index 页面索引, value 导出格式, dpi 图片DPI
|
||||
// 返回: []byte 文件数据, ExportFormat 导出格式, error 错误信息
|
||||
func (s *Session) ExportPage(index int, value string) ([]byte, ExportFormat, error) {
|
||||
func (s *Session) ExportPage(index int, value string, dpi float64) ([]byte, ExportFormat, error) {
|
||||
format, ok := exportFormat(value)
|
||||
if !ok {
|
||||
return nil, ExportFormat{}, fmt.Errorf("unsupported export format %s", value)
|
||||
@@ -231,13 +303,13 @@ func (s *Session) ExportPage(index int, value string) ([]byte, ExportFormat, err
|
||||
case "eps":
|
||||
err = s.Renderer.RenderToEPS(page, &buf)
|
||||
case "png":
|
||||
img, renderErr := s.Renderer.RenderToImage(page)
|
||||
img, renderErr := s.renderPageImage(page, dpi)
|
||||
if renderErr != nil {
|
||||
return nil, ExportFormat{}, renderErr
|
||||
}
|
||||
err = png.Encode(&buf, img)
|
||||
case "jpg":
|
||||
img, renderErr := s.Renderer.RenderToImage(page)
|
||||
img, renderErr := s.renderPageImage(page, dpi)
|
||||
if renderErr != nil {
|
||||
return nil, ExportFormat{}, renderErr
|
||||
}
|
||||
@@ -249,6 +321,142 @@ func (s *Session) ExportPage(index int, value string) ([]byte, ExportFormat, err
|
||||
return buf.Bytes(), format, nil
|
||||
}
|
||||
|
||||
// renderPageImage 渲染页面图片
|
||||
// 入参: page 页面内容, dpi 图片DPI
|
||||
// 返回: image.Image 图像对象, error 错误信息
|
||||
func (s *Session) renderPageImage(page *ofdgo.PageContent, dpi float64) (image.Image, error) {
|
||||
if dpi <= 0 {
|
||||
return s.Renderer.RenderToImage(page)
|
||||
}
|
||||
oldDPI := s.Renderer.DPI
|
||||
s.Renderer.DPI = dpi
|
||||
defer func() {
|
||||
s.Renderer.DPI = oldDPI
|
||||
}()
|
||||
return s.Renderer.RenderToImage(page)
|
||||
}
|
||||
|
||||
// signatureInfos 获取签名验证信息
|
||||
// 返回: []SignatureInfo 签名验证信息, error 错误信息
|
||||
func (s *Session) signatureInfos() ([]SignatureInfo, error) {
|
||||
reports, err := s.Reader.VerifySignatures()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
infos := make([]SignatureInfo, 0, len(reports))
|
||||
for _, report := range reports {
|
||||
infos = append(infos, signatureInfo(report))
|
||||
}
|
||||
return infos, nil
|
||||
}
|
||||
|
||||
// signatureInfo 转换签名验证信息
|
||||
// 入参: report 签名验证报告
|
||||
// 返回: SignatureInfo 签名验证信息
|
||||
func signatureInfo(report ofdgo.SignatureVerifyReport) SignatureInfo {
|
||||
return SignatureInfo{
|
||||
ID: report.ID,
|
||||
Type: string(report.Type),
|
||||
Valid: report.Valid,
|
||||
Version: report.Provider.Version,
|
||||
SealType: report.SealType,
|
||||
SealID: report.SealInfo.ID,
|
||||
SealName: report.SealInfo.Name,
|
||||
SealVendor: report.SealInfo.VendorID,
|
||||
Signer: signatureSigner(report),
|
||||
SignatureDateTime: report.SignatureDateTime,
|
||||
Provider: report.Provider.ProviderName,
|
||||
Company: report.Provider.Company,
|
||||
DataHashOK: report.DataHashOK,
|
||||
SignedValueOK: report.SignedValueOK,
|
||||
SealOK: report.SealOK,
|
||||
SealMatchOK: report.SealMatchOK,
|
||||
CertOK: report.CertOK,
|
||||
SignatureTimeChecked: report.SignatureTimeChecked,
|
||||
SignatureTimeOK: report.SignatureTimeOK,
|
||||
SealTimeChecked: report.SealTimeChecked,
|
||||
SealTimeOK: report.SealTimeOK,
|
||||
SealCertTimeChecked: report.SealCertTimeChecked,
|
||||
SealCertTimeOK: report.SealCertTimeOK,
|
||||
CertTimeChecked: report.CertTimeChecked,
|
||||
CertTimeOK: report.CertTimeOK,
|
||||
CertTrustChecked: report.CertTrustChecked,
|
||||
CertTrustOK: report.CertTrustOK,
|
||||
ReferenceCount: len(report.References),
|
||||
ReferencePassed: signatureReferencePassed(report.References),
|
||||
SignatureMethod: report.SignatureMethod,
|
||||
DigestMethod: report.DigestMethod,
|
||||
SignSerial: report.SignCert.SerialNumber,
|
||||
SignSubject: report.SignCert.Subject,
|
||||
SignIssuer: report.SignCert.Issuer,
|
||||
SealSubject: report.SealCert.Subject,
|
||||
Stamps: signatureStampInfos(report.StampPositions),
|
||||
Error: signatureReportError(report),
|
||||
}
|
||||
}
|
||||
|
||||
// signatureSigner 获取签名人名称
|
||||
// 入参: report 签名验证报告
|
||||
// 返回: string 签名人名称
|
||||
func signatureSigner(report ofdgo.SignatureVerifyReport) string {
|
||||
if report.Signer != "" {
|
||||
return report.Signer
|
||||
}
|
||||
if report.SignCert.CommonName != "" {
|
||||
return report.SignCert.CommonName
|
||||
}
|
||||
if report.SignCert.Organization != "" {
|
||||
return report.SignCert.Organization
|
||||
}
|
||||
return report.SignCert.Subject
|
||||
}
|
||||
|
||||
// signatureReferencePassed 获取已通过保护文件数量
|
||||
// 入参: refs 保护文件验证结果
|
||||
// 返回: int 已通过保护文件数量
|
||||
func signatureReferencePassed(refs []ofdgo.SignatureReferenceVerify) int {
|
||||
count := 0
|
||||
for _, ref := range refs {
|
||||
if ref.OK {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// signatureReportError 获取签名错误信息
|
||||
// 入参: report 签名验证报告
|
||||
// 返回: string 错误信息
|
||||
func signatureReportError(report ofdgo.SignatureVerifyReport) string {
|
||||
if report.Error != "" && report.StampPositionError != "" {
|
||||
return report.Error + "; " + report.StampPositionError
|
||||
}
|
||||
if report.Error != "" {
|
||||
return report.Error
|
||||
}
|
||||
return report.StampPositionError
|
||||
}
|
||||
|
||||
// signatureStampInfos 转换签名外观位置
|
||||
// 入参: positions 签名外观位置
|
||||
// 返回: []SignatureStampInfo 签名外观信息
|
||||
func signatureStampInfos(positions []ofdgo.SignatureStampPosition) []SignatureStampInfo {
|
||||
infos := make([]SignatureStampInfo, 0, len(positions))
|
||||
for _, position := range positions {
|
||||
infos = append(infos, SignatureStampInfo{
|
||||
ID: position.ID,
|
||||
Page: position.Page,
|
||||
PageID: position.PageID,
|
||||
Boundary: position.Boundary,
|
||||
X: position.Box.X,
|
||||
Y: position.Box.Y,
|
||||
Width: position.Box.W,
|
||||
Height: position.Box.H,
|
||||
})
|
||||
}
|
||||
return infos
|
||||
}
|
||||
|
||||
// ExportPDF 导出文档为PDF
|
||||
// 返回: []byte PDF文件数据, error 错误信息
|
||||
func (s *Session) ExportPDF() ([]byte, error) {
|
||||
@@ -284,9 +492,28 @@ func (s *Session) pageContent(index int) (ofdgo.Page, *ofdgo.PageContent, error)
|
||||
return ofdgo.Page{}, nil, fmt.Errorf("page index %d out of range", index)
|
||||
}
|
||||
pageRef := s.doc.Pages.Page[index]
|
||||
if page, ok := s.pageCache[index]; ok {
|
||||
return pageRef, page, nil
|
||||
}
|
||||
page, err := s.Reader.PageContent(pageRef)
|
||||
if err != nil {
|
||||
return ofdgo.Page{}, nil, err
|
||||
}
|
||||
s.pageCache[index] = page
|
||||
return pageRef, page, nil
|
||||
}
|
||||
|
||||
// pageBox 获取页面物理区域
|
||||
// 入参: index 页面索引, page 页面内容
|
||||
// 返回: ofdgo.Box 页面物理区域, error 错误信息
|
||||
func (s *Session) pageBox(index int, page *ofdgo.PageContent) (ofdgo.Box, error) {
|
||||
if box, ok := s.boxCache[index]; ok {
|
||||
return box, nil
|
||||
}
|
||||
box, err := s.Renderer.GetPageBox(page)
|
||||
if err != nil {
|
||||
return ofdgo.Box{}, err
|
||||
}
|
||||
s.boxCache[index] = box
|
||||
return box, nil
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ func Open(path string) (*Reader, error) {
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
// NewReader 从流创建一个 OFD 阅读器
|
||||
// NewReader 从IO读取器创建OFD阅读器
|
||||
// 入参: r IO读取器, size 数据大小
|
||||
// 返回: *Reader 阅读器实例, error 错误信息
|
||||
func NewReader(r io.ReaderAt, size int64) (*Reader, error) {
|
||||
@@ -71,6 +71,14 @@ func NewRenderer(reader *Reader, opts ...RendererOption) *Renderer {
|
||||
DrawParams: reader.drawParamCache,
|
||||
CompositeGraphicUnits: reader.compositeGraphicUnitCache,
|
||||
FontMap: make(map[string]*canvas.FontFamily),
|
||||
FontGIDMap: make(map[string]map[uint16]rune),
|
||||
FontCIDMap: make(map[string]map[uint16]rune),
|
||||
fontCache: make(map[fontCacheKey]*canvas.FontFamily),
|
||||
fontSourceCache: make(map[string][]fontSource),
|
||||
fontSourceUsed: make(map[string]fontSource),
|
||||
fontDirCandidates: make(map[string][]fontFileCandidate),
|
||||
textGlyphPathCache: make(map[textGlyphPathCacheKey]textGlyphPathCacheValue),
|
||||
templatePageCache: make(map[string]*PageContent),
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(r)
|
||||
|
||||
+198
@@ -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"`
|
||||
Outlines Outlines `xml:"Outlines"`
|
||||
Permissions Permissions `xml:"Permissions"`
|
||||
Actions []Action `xml:"Actions>Action"`
|
||||
Bookmarks Bookmarks `xml:"Bookmarks"`
|
||||
Annotations string `xml:"Annotations"`
|
||||
Signatures string `xml:"Signatures"`
|
||||
Attachments Attachments `xml:"Attachments"`
|
||||
CustomTags CustomTags `xml:"CustomTags"`
|
||||
Extensions Extensions `xml:"Extensions"`
|
||||
}
|
||||
|
||||
// Extensions 扩展集合
|
||||
type Extensions struct {
|
||||
XMLName xml.Name `xml:"Extensions"`
|
||||
Path string `xml:",chardata"`
|
||||
Extension []Extension `xml:"Extension"`
|
||||
}
|
||||
|
||||
@@ -42,7 +47,8 @@ type Extension struct {
|
||||
Date string `xml:"Date,attr"`
|
||||
RefID string `xml:"RefId,attr"`
|
||||
Property []Property `xml:"Property"`
|
||||
Data string `xml:"Data"`
|
||||
ExtendData []string `xml:"ExtendData"`
|
||||
Data []ExtensionData `xml:"Data"`
|
||||
}
|
||||
|
||||
// Property 扩展属性
|
||||
@@ -52,16 +58,45 @@ type Property struct {
|
||||
Type string `xml:"Type,attr"`
|
||||
}
|
||||
|
||||
// ExtensionData 扩展数据
|
||||
type ExtensionData struct {
|
||||
Attr []xml.Attr `xml:",any,attr"`
|
||||
Content string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
// Attachments 附件集合
|
||||
type Attachments struct {
|
||||
XMLName xml.Name `xml:"Attachments"`
|
||||
Path string `xml:",chardata"`
|
||||
Attachment []Attachment `xml:"Attachment"`
|
||||
}
|
||||
|
||||
// Attachment 附件信息
|
||||
type Attachment struct {
|
||||
Name string `xml:"Name,attr"`
|
||||
File string `xml:"File,attr"`
|
||||
ID string `xml:"ID,attr"`
|
||||
Name string `xml:"Name,attr"`
|
||||
Format string `xml:"Format,attr"`
|
||||
CreationDate string `xml:"CreationDate,attr"`
|
||||
ModDate string `xml:"ModDate,attr"`
|
||||
Size *float64 `xml:"Size,attr"`
|
||||
Visible bool `xml:"Visible,attr"`
|
||||
Usage string `xml:"Usage,attr"`
|
||||
FileLoc string `xml:"FileLoc"`
|
||||
}
|
||||
|
||||
// CustomTags 自定义标引集合
|
||||
type CustomTags struct {
|
||||
XMLName xml.Name `xml:"CustomTags"`
|
||||
Path string `xml:",chardata"`
|
||||
CustomTag []CustomTag `xml:"CustomTag"`
|
||||
}
|
||||
|
||||
// CustomTag 自定义标引
|
||||
type CustomTag struct {
|
||||
TypeID string `xml:"TypeID,attr"`
|
||||
NameSpace string `xml:"NameSpace,attr"`
|
||||
SchemaLoc string `xml:"SchemaLoc"`
|
||||
FileLoc string `xml:"FileLoc"`
|
||||
}
|
||||
|
||||
// CommonData 文档公共数据
|
||||
@@ -101,6 +136,17 @@ type TemplatePage struct {
|
||||
ZOrder string `xml:"ZOrder,attr"`
|
||||
}
|
||||
|
||||
// Bookmarks 书签集合
|
||||
type Bookmarks struct {
|
||||
Bookmark []Bookmark `xml:"Bookmark"`
|
||||
}
|
||||
|
||||
// Bookmark 书签
|
||||
type Bookmark struct {
|
||||
Name string `xml:"Name,attr"`
|
||||
Dest Dest `xml:"Dest"`
|
||||
}
|
||||
|
||||
// Outlines 大纲集合
|
||||
type Outlines struct {
|
||||
OutlineElem []OutlineElem `xml:"OutlineElem"`
|
||||
@@ -110,13 +156,146 @@ type Outlines struct {
|
||||
type OutlineElem struct {
|
||||
Title string `xml:"Title,attr"`
|
||||
Count int `xml:"Count,attr"`
|
||||
Actions string `xml:"Actions"`
|
||||
Expanded bool `xml:"Expanded,attr"`
|
||||
Actions []Action `xml:"Actions>Action"`
|
||||
OutlineElem []OutlineElem `xml:"OutlineElem"`
|
||||
}
|
||||
|
||||
// Permissions 权限声明
|
||||
type Permissions struct {
|
||||
Edit bool `xml:"Edit"`
|
||||
Print bool `xml:"Print"`
|
||||
Annot bool `xml:"Annot"`
|
||||
Export bool `xml:"Export"`
|
||||
Copy bool `xml:"Copy"`
|
||||
Signature bool `xml:"Signature"`
|
||||
Watermark bool `xml:"Watermark"`
|
||||
PrintScreen bool `xml:"PrintScreen"`
|
||||
Print bool `xml:"-"`
|
||||
Copies int `xml:"-"`
|
||||
Copy bool `xml:"CopyText"`
|
||||
ContentRegist bool `xml:"ContentRegist"`
|
||||
ValidPeriod *ValidPeriod `xml:"ValidPeriod"`
|
||||
}
|
||||
|
||||
// ValidPeriod 文档访问有效期
|
||||
type ValidPeriod struct {
|
||||
StartDate string `xml:"StartDate,attr"`
|
||||
EndDate string `xml:"EndDate,attr"`
|
||||
}
|
||||
|
||||
// UnmarshalXML 解析文档并应用权限默认值
|
||||
// 入参: d XML解码器, start 起始节点
|
||||
// 返回: error 错误信息
|
||||
func (doc *Document) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
type document Document
|
||||
value := document{Permissions: defaultPermissions()}
|
||||
if err := d.DecodeElement(&value, &start); err != nil {
|
||||
return err
|
||||
}
|
||||
*doc = Document(value)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalXML 解析附件并应用默认值
|
||||
// 入参: d XML解码器, start 起始节点
|
||||
// 返回: error 错误信息
|
||||
func (a *Attachment) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
type attachment Attachment
|
||||
value := attachment{Visible: true, Usage: "none"}
|
||||
if err := d.DecodeElement(&value, &start); err != nil {
|
||||
return err
|
||||
}
|
||||
*a = Attachment(value)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalXML 解析大纲节点并应用默认值
|
||||
// 入参: d XML解码器, start 起始节点
|
||||
// 返回: error 错误信息
|
||||
func (o *OutlineElem) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
type outlineElem OutlineElem
|
||||
value := outlineElem{Expanded: true}
|
||||
if err := d.DecodeElement(&value, &start); err != nil {
|
||||
return err
|
||||
}
|
||||
*o = OutlineElem(value)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalXML 解析文档权限并应用默认值
|
||||
// 入参: d XML解码器, start 起始节点
|
||||
// 返回: error 错误信息
|
||||
func (p *Permissions) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
var value struct {
|
||||
Edit *bool `xml:"Edit"`
|
||||
Annot *bool `xml:"Annot"`
|
||||
Export *bool `xml:"Export"`
|
||||
Signature *bool `xml:"Signature"`
|
||||
Watermark *bool `xml:"Watermark"`
|
||||
PrintScreen *bool `xml:"PrintScreen"`
|
||||
Copy *bool `xml:"CopyText"`
|
||||
ContentRegist *bool `xml:"ContentRegist"`
|
||||
Print *print `xml:"Print"`
|
||||
ValidPeriod *ValidPeriod `xml:"ValidPeriod"`
|
||||
}
|
||||
if err := d.DecodeElement(&value, &start); err != nil {
|
||||
return err
|
||||
}
|
||||
*p = defaultPermissions()
|
||||
if value.Edit != nil {
|
||||
p.Edit = *value.Edit
|
||||
}
|
||||
if value.Annot != nil {
|
||||
p.Annot = *value.Annot
|
||||
}
|
||||
if value.Export != nil {
|
||||
p.Export = *value.Export
|
||||
}
|
||||
if value.Signature != nil {
|
||||
p.Signature = *value.Signature
|
||||
}
|
||||
if value.Watermark != nil {
|
||||
p.Watermark = *value.Watermark
|
||||
}
|
||||
if value.PrintScreen != nil {
|
||||
p.PrintScreen = *value.PrintScreen
|
||||
}
|
||||
if value.Copy != nil {
|
||||
p.Copy = *value.Copy
|
||||
}
|
||||
if value.ContentRegist != nil {
|
||||
p.ContentRegist = *value.ContentRegist
|
||||
}
|
||||
if value.Print != nil {
|
||||
if value.Print.Printable != nil {
|
||||
p.Print = *value.Print.Printable
|
||||
}
|
||||
if value.Print.Copies != nil {
|
||||
p.Copies = *value.Print.Copies
|
||||
}
|
||||
}
|
||||
p.ValidPeriod = value.ValidPeriod
|
||||
return nil
|
||||
}
|
||||
|
||||
// print 打印权限节点
|
||||
type print struct {
|
||||
Printable *bool `xml:"Printable,attr"`
|
||||
Copies *int `xml:"Copies,attr"`
|
||||
}
|
||||
|
||||
// defaultPermissions 获取默认文档权限
|
||||
// 返回: Permissions 文档权限
|
||||
func defaultPermissions() Permissions {
|
||||
return Permissions{
|
||||
Edit: true,
|
||||
Annot: true,
|
||||
Export: true,
|
||||
Signature: true,
|
||||
Watermark: true,
|
||||
PrintScreen: true,
|
||||
Print: true,
|
||||
Copies: -1,
|
||||
Copy: true,
|
||||
ContentRegist: true,
|
||||
}
|
||||
}
|
||||
|
||||
+298
-43
@@ -34,6 +34,7 @@ type FontFile struct {
|
||||
type FontFS struct {
|
||||
files map[string][]byte
|
||||
names []string
|
||||
candidates []fontFileCandidate
|
||||
}
|
||||
|
||||
// NewFontFS 创建内存字体文件系统
|
||||
@@ -54,6 +55,7 @@ func NewFontFS(fonts []FontFile) *FontFS {
|
||||
fsys.files[name] = append([]byte(nil), font.Data...)
|
||||
}
|
||||
sort.Strings(fsys.names)
|
||||
fsys.candidates = fontFileCandidates(fsys.names, path.Base)
|
||||
return fsys
|
||||
}
|
||||
|
||||
@@ -70,18 +72,20 @@ func (fsys *FontFS) Len() int {
|
||||
// 入参: name 字体文件名
|
||||
// 返回: fs.File 字体文件, error 错误信息
|
||||
func (fsys *FontFS) Open(name string) (fs.File, error) {
|
||||
name = cleanFontName(name)
|
||||
if !fs.ValidPath(name) {
|
||||
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
|
||||
}
|
||||
if name == "." {
|
||||
return &fontDir{entries: fsys.entries()}, nil
|
||||
}
|
||||
data, ok := fsys.files[name]
|
||||
data, ok := fsys.files[strings.ToLower(name)]
|
||||
if !ok {
|
||||
return nil, fs.ErrNotExist
|
||||
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
|
||||
}
|
||||
return &fontMemFile{
|
||||
Reader: bytes.NewReader(data),
|
||||
info: fontFileInfo{
|
||||
name: name,
|
||||
name: strings.ToLower(name),
|
||||
size: int64(len(data)),
|
||||
},
|
||||
}, nil
|
||||
@@ -91,9 +95,15 @@ func (fsys *FontFS) Open(name string) (fs.File, error) {
|
||||
// 入参: name 目录名
|
||||
// 返回: []fs.DirEntry 目录条目, error 错误信息
|
||||
func (fsys *FontFS) ReadDir(name string) ([]fs.DirEntry, error) {
|
||||
name = cleanFontName(name)
|
||||
if !fs.ValidPath(name) {
|
||||
return nil, &fs.PathError{Op: "readdir", Path: name, Err: fs.ErrInvalid}
|
||||
}
|
||||
if name != "." {
|
||||
return nil, fs.ErrNotExist
|
||||
err := fs.ErrNotExist
|
||||
if _, ok := fsys.files[strings.ToLower(name)]; ok {
|
||||
err = fs.ErrInvalid
|
||||
}
|
||||
return nil, &fs.PathError{Op: "readdir", Path: name, Err: err}
|
||||
}
|
||||
return fsys.entries(), nil
|
||||
}
|
||||
@@ -102,21 +112,23 @@ func (fsys *FontFS) ReadDir(name string) ([]fs.DirEntry, error) {
|
||||
// 入参: pattern 匹配模式
|
||||
// 返回: []string 字体文件列表, error 错误信息
|
||||
func (fsys *FontFS) Glob(pattern string) ([]string, error) {
|
||||
if _, err := path.Match(pattern, ""); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fsys.match(pattern), nil
|
||||
return fs.Glob(struct{ fs.FS }{fsys}, pattern)
|
||||
}
|
||||
|
||||
// Match 匹配指定字体名称
|
||||
// 入参: names 字体名称列表
|
||||
// 返回: string 匹配字体文件, bool 是否为名称匹配
|
||||
func (fsys *FontFS) Match(names ...string) (string, bool) {
|
||||
for _, pattern := range fontFilePatterns(names...) {
|
||||
if matches := fsys.match(pattern); len(matches) > 0 {
|
||||
return fsys.MatchStyle(false, false, names...)
|
||||
}
|
||||
|
||||
// MatchStyle 匹配指定样式的字体名称
|
||||
// 入参: bold 是否粗体, italic 是否斜体, names 字体名称列表
|
||||
// 返回: string 匹配字体文件, bool 是否为名称匹配
|
||||
func (fsys *FontFS) MatchStyle(bold, italic bool, names ...string) (string, bool) {
|
||||
if matches := fsys.matchPatternsStyle(fontFilePatterns(names...), bold, italic); len(matches) > 0 {
|
||||
return matches[0], true
|
||||
}
|
||||
}
|
||||
if matches := fsys.fallbackFonts(); len(matches) > 0 {
|
||||
return matches[0], false
|
||||
}
|
||||
@@ -127,17 +139,30 @@ func (fsys *FontFS) Match(names ...string) (string, bool) {
|
||||
// 入参: pattern 匹配模式
|
||||
// 返回: []string 字体文件列表
|
||||
func (fsys *FontFS) match(pattern string) []string {
|
||||
buckets := make([][]string, fontMatchFuzzy+1)
|
||||
for _, name := range fsys.names {
|
||||
if rank := matchFontPatternRank(pattern, name); rank != fontMatchNone {
|
||||
buckets[rank] = append(buckets[rank], name)
|
||||
return fsys.matchStyle(pattern, false, false)
|
||||
}
|
||||
|
||||
// matchStyle 匹配指定样式的字体文件模式
|
||||
// 入参: pattern 匹配模式, bold 是否粗体, italic 是否斜体
|
||||
// 返回: []string 字体文件列表
|
||||
func (fsys *FontFS) matchStyle(pattern string, bold, italic bool) []string {
|
||||
return fsys.matchPatternsStyle([]string{pattern}, bold, italic)
|
||||
}
|
||||
|
||||
// matchPatternsStyle 匹配指定样式的字体文件模式
|
||||
// 入参: patterns 匹配模式列表, bold 是否粗体, italic 是否斜体
|
||||
// 返回: []string 字体文件列表
|
||||
func (fsys *FontFS) matchPatternsStyle(patterns []string, bold, italic bool) []string {
|
||||
matches := make([]fontFileMatch, 0, len(fsys.candidates))
|
||||
seen := make(map[string]int, len(fsys.candidates))
|
||||
for _, matcher := range newFontPatternMatchers(patterns) {
|
||||
for _, file := range fsys.candidates {
|
||||
rank := matcher.rankCandidate(file)
|
||||
appendFontFileMatch(&matches, seen, matcher, file, rank, bold, italic)
|
||||
}
|
||||
}
|
||||
var matches []string
|
||||
for rank := fontMatchExact; rank <= fontMatchFuzzy; rank++ {
|
||||
matches = append(matches, buckets[rank]...)
|
||||
}
|
||||
return matches
|
||||
sortFontFileMatches(matches)
|
||||
return fontFileMatchNames(matches)
|
||||
}
|
||||
|
||||
// fallbackFonts 获取回退字体文件
|
||||
@@ -183,51 +208,278 @@ func cleanFontName(name string) string {
|
||||
return strings.ToLower(name)
|
||||
}
|
||||
|
||||
// matchFontPatternRank 获取字体文件匹配等级
|
||||
// 入参: pattern 匹配模式, name 字体文件名
|
||||
type fontFileCandidate struct {
|
||||
name string
|
||||
base string
|
||||
lowerBase string
|
||||
normalized string
|
||||
fold string
|
||||
}
|
||||
|
||||
// fontFileCandidates 筛选字体文件候选
|
||||
// 入参: names 文件名列表, base 文件名提取函数
|
||||
// 返回: []fontFileCandidate 字体文件候选列表
|
||||
func fontFileCandidates(names []string, base func(string) string) []fontFileCandidate {
|
||||
files := make([]fontFileCandidate, 0, len(names))
|
||||
for _, name := range names {
|
||||
if isFontFileName(name) {
|
||||
baseName := base(name)
|
||||
files = append(files, fontFileCandidate{
|
||||
name: name,
|
||||
base: baseName,
|
||||
lowerBase: strings.ToLower(baseName),
|
||||
normalized: fontNormalizeName(baseName),
|
||||
fold: strings.ToLower(name),
|
||||
})
|
||||
}
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
type fontPatternMatcher struct {
|
||||
pattern string
|
||||
lowerPattern string
|
||||
stem string
|
||||
aliases []string
|
||||
priority int
|
||||
}
|
||||
|
||||
// newFontPatternMatcher 创建字体文件匹配器
|
||||
// 入参: pattern 匹配模式
|
||||
// 返回: fontPatternMatcher 字体文件匹配器
|
||||
func newFontPatternMatcher(pattern string) fontPatternMatcher {
|
||||
matcher := fontPatternMatcher{
|
||||
pattern: pattern,
|
||||
lowerPattern: strings.ToLower(pattern),
|
||||
stem: fontPatternStem(pattern),
|
||||
}
|
||||
if matcher.stem != "" {
|
||||
for _, alias := range fontExactCandidateNames(matcher.stem) {
|
||||
alias = fontNormalizeName(alias)
|
||||
if alias != "" {
|
||||
matcher.aliases = append(matcher.aliases, alias)
|
||||
}
|
||||
}
|
||||
}
|
||||
return matcher
|
||||
}
|
||||
|
||||
// newFontPatternMatchers 创建字体文件匹配器列表
|
||||
// 入参: patterns 匹配模式列表
|
||||
// 返回: []fontPatternMatcher 字体文件匹配器列表
|
||||
func newFontPatternMatchers(patterns []string) []fontPatternMatcher {
|
||||
matchers := make([]fontPatternMatcher, 0, len(patterns))
|
||||
for priority, pattern := range patterns {
|
||||
matcher := newFontPatternMatcher(pattern)
|
||||
matcher.priority = priority
|
||||
matchers = append(matchers, matcher)
|
||||
}
|
||||
return matchers
|
||||
}
|
||||
|
||||
// rank 获取字体文件匹配等级
|
||||
// 入参: name 字体文件名
|
||||
// 返回: int 匹配等级
|
||||
func matchFontPatternRank(pattern, name string) int {
|
||||
exactPath, _ := path.Match(pattern, name)
|
||||
lowerPath, _ := path.Match(strings.ToLower(pattern), strings.ToLower(name))
|
||||
stem := fontPatternStem(pattern)
|
||||
if stem == "" {
|
||||
func (m fontPatternMatcher) rank(name string) int {
|
||||
return m.rankCandidate(fontFileCandidate{
|
||||
base: name,
|
||||
lowerBase: strings.ToLower(name),
|
||||
normalized: fontNormalizeName(name),
|
||||
})
|
||||
}
|
||||
|
||||
// rankCandidate 获取字体文件候选匹配等级
|
||||
// 入参: file 字体文件候选
|
||||
// 返回: int 匹配等级
|
||||
func (m fontPatternMatcher) rankCandidate(file fontFileCandidate) int {
|
||||
exactPath, _ := path.Match(m.pattern, file.base)
|
||||
lowerPath, _ := path.Match(m.lowerPattern, file.lowerBase)
|
||||
if m.stem == "" {
|
||||
if exactPath || lowerPath {
|
||||
return fontMatchFuzzy
|
||||
}
|
||||
return fontMatchNone
|
||||
}
|
||||
name = fontNormalizeName(name)
|
||||
if name == stem {
|
||||
name := file.normalized
|
||||
if name == m.stem {
|
||||
return fontMatchExact
|
||||
}
|
||||
aliases := fontExactCandidateNames(stem)
|
||||
for _, alias := range aliases {
|
||||
alias = fontNormalizeName(alias)
|
||||
if alias != "" && name == alias {
|
||||
for _, alias := range m.aliases {
|
||||
if name == alias {
|
||||
return fontMatchExact
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(name, stem) {
|
||||
if fontFileKnownStyleSuffix(m.styleSuffixNormalized(name)) {
|
||||
return fontMatchExact
|
||||
}
|
||||
if strings.HasPrefix(name, m.stem) {
|
||||
return fontMatchPartial
|
||||
}
|
||||
for _, alias := range aliases {
|
||||
alias = fontNormalizeName(alias)
|
||||
if alias != "" && strings.HasPrefix(name, alias) {
|
||||
for _, alias := range m.aliases {
|
||||
if strings.HasPrefix(name, alias) {
|
||||
return fontMatchPartial
|
||||
}
|
||||
}
|
||||
if exactPath || lowerPath || strings.Contains(name, stem) {
|
||||
if exactPath || lowerPath || strings.Contains(name, m.stem) {
|
||||
return fontMatchFuzzy
|
||||
}
|
||||
for _, alias := range aliases {
|
||||
alias = fontNormalizeName(alias)
|
||||
if alias != "" && strings.Contains(name, alias) {
|
||||
for _, alias := range m.aliases {
|
||||
if strings.Contains(name, alias) {
|
||||
return fontMatchFuzzy
|
||||
}
|
||||
}
|
||||
return fontMatchNone
|
||||
}
|
||||
|
||||
// styleSuffixNormalized 获取规范字体名称的样式后缀
|
||||
// 入参: name 规范字体名称
|
||||
// 返回: string 样式后缀
|
||||
func (m fontPatternMatcher) styleSuffixNormalized(name string) string {
|
||||
if m.stem == "" || name == "" {
|
||||
return ""
|
||||
}
|
||||
if strings.HasPrefix(name, m.stem) {
|
||||
return strings.TrimPrefix(name, m.stem)
|
||||
}
|
||||
for _, alias := range m.aliases {
|
||||
if strings.HasPrefix(name, alias) {
|
||||
return strings.TrimPrefix(name, alias)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type fontFileMatch struct {
|
||||
name string
|
||||
priority int
|
||||
rank int
|
||||
styleRank int
|
||||
sortName string
|
||||
}
|
||||
|
||||
// appendFontFileMatch 追加字体文件匹配结果
|
||||
// 入参: matches 匹配结果, seen 已匹配文件, matcher 匹配器, file 字体文件候选, rank 匹配等级, bold 是否粗体, italic 是否斜体
|
||||
func appendFontFileMatch(matches *[]fontFileMatch, seen map[string]int, matcher fontPatternMatcher, file fontFileCandidate, rank int, bold, italic bool) {
|
||||
if rank == fontMatchNone {
|
||||
return
|
||||
}
|
||||
next := fontFileMatch{
|
||||
name: file.name,
|
||||
priority: matcher.priority,
|
||||
rank: rank,
|
||||
styleRank: fontFileStyleRank(matcher.styleSuffixNormalized(file.normalized), bold, italic),
|
||||
sortName: file.lowerBase,
|
||||
}
|
||||
if index, ok := seen[file.fold]; ok {
|
||||
if fontFileMatchLess(next, (*matches)[index]) {
|
||||
(*matches)[index] = next
|
||||
}
|
||||
return
|
||||
}
|
||||
seen[file.fold] = len(*matches)
|
||||
*matches = append(*matches, next)
|
||||
}
|
||||
|
||||
// sortFontFileMatches 排序字体文件匹配结果
|
||||
// 入参: matches 匹配结果
|
||||
func sortFontFileMatches(matches []fontFileMatch) {
|
||||
sort.SliceStable(matches, func(i, j int) bool {
|
||||
return fontFileMatchLess(matches[i], matches[j])
|
||||
})
|
||||
}
|
||||
|
||||
// fontFileMatchLess 判断字体文件匹配结果优先级
|
||||
// 入参: left 左侧匹配结果, right 右侧匹配结果
|
||||
// 返回: bool 左侧是否优先
|
||||
func fontFileMatchLess(left, right fontFileMatch) bool {
|
||||
if left.priority != right.priority {
|
||||
return left.priority < right.priority
|
||||
}
|
||||
if left.rank != right.rank {
|
||||
return left.rank < right.rank
|
||||
}
|
||||
if left.styleRank != right.styleRank {
|
||||
return left.styleRank < right.styleRank
|
||||
}
|
||||
return left.sortName < right.sortName
|
||||
}
|
||||
|
||||
// fontFileMatchNames 获取字体文件匹配名称
|
||||
// 入参: matches 匹配结果
|
||||
// 返回: []string 字体文件列表
|
||||
func fontFileMatchNames(matches []fontFileMatch) []string {
|
||||
names := make([]string, 0, len(matches))
|
||||
for _, match := range matches {
|
||||
names = append(names, match.name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// fontFileStyleRank 获取字体文件样式匹配等级
|
||||
// 入参: suffix 样式后缀, bold 是否粗体, italic 是否斜体
|
||||
// 返回: int 样式匹配等级
|
||||
func fontFileStyleRank(suffix string, bold, italic bool) int {
|
||||
fileBold, fileItalic := fontFileStyleFromSuffix(suffix)
|
||||
rank := 0
|
||||
if fileBold != bold {
|
||||
rank += 2
|
||||
}
|
||||
if fileItalic != italic {
|
||||
rank += 2
|
||||
}
|
||||
if !bold && !italic && !fileBold && !fileItalic && suffix != "" && !fontFileRegularSuffix(suffix) {
|
||||
rank++
|
||||
}
|
||||
return rank
|
||||
}
|
||||
|
||||
// fontFileStyleFromSuffix 获取样式后缀对应的字体样式
|
||||
// 入参: suffix 样式后缀
|
||||
// 返回: bool 是否粗体, bool 是否斜体
|
||||
func fontFileStyleFromSuffix(suffix string) (bool, bool) {
|
||||
bold := suffix == "b" ||
|
||||
suffix == "bd" ||
|
||||
suffix == "bold" ||
|
||||
suffix == "bi" ||
|
||||
suffix == "bolditalic" ||
|
||||
suffix == "boldoblique"
|
||||
italic := suffix == "i" ||
|
||||
suffix == "it" ||
|
||||
suffix == "italic" ||
|
||||
suffix == "oblique" ||
|
||||
suffix == "bi" ||
|
||||
suffix == "bolditalic" ||
|
||||
suffix == "boldoblique"
|
||||
return bold, italic
|
||||
}
|
||||
|
||||
// fontFileRegularSuffix 判断是否为常规样式后缀
|
||||
// 入参: suffix 样式后缀
|
||||
// 返回: bool 是否为常规样式后缀
|
||||
func fontFileRegularSuffix(suffix string) bool {
|
||||
switch suffix {
|
||||
case "r", "regular", "normal", "常规":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// fontFileKnownStyleSuffix 判断是否为已知样式后缀
|
||||
// 入参: suffix 样式后缀
|
||||
// 返回: bool 是否为已知样式后缀
|
||||
func fontFileKnownStyleSuffix(suffix string) bool {
|
||||
switch suffix {
|
||||
case "r", "regular", "normal", "常规",
|
||||
"b", "bd", "bold",
|
||||
"i", "it", "italic", "oblique",
|
||||
"bi", "bolditalic", "boldoblique":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// fontMemFile 内存字体文件
|
||||
type fontMemFile struct {
|
||||
*bytes.Reader
|
||||
@@ -275,6 +527,9 @@ func (d *fontDir) Close() error {
|
||||
// 返回: []fs.DirEntry 目录条目, error 错误信息
|
||||
func (d *fontDir) ReadDir(count int) ([]fs.DirEntry, error) {
|
||||
if d.offset >= len(d.entries) {
|
||||
if count <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, io.EOF
|
||||
}
|
||||
if count <= 0 || d.offset+count > len(d.entries) {
|
||||
|
||||
+68
-134
@@ -22,13 +22,13 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// FontStatusEmbedded 字体使用OFD内嵌字体
|
||||
// FontStatusEmbedded 内嵌字体可用
|
||||
FontStatusEmbedded = "embedded"
|
||||
// FontStatusMatched 字体命中外部字体
|
||||
// FontStatusMatched 外部字体匹配
|
||||
FontStatusMatched = "matched"
|
||||
// FontStatusFallback 字体使用外部字体回退
|
||||
// FontStatusFallback 外部字体回退
|
||||
FontStatusFallback = "fallback"
|
||||
// FontStatusMissing 字体缺失
|
||||
// FontStatusMissing 字体资源缺失
|
||||
FontStatusMissing = "missing"
|
||||
)
|
||||
|
||||
@@ -67,6 +67,30 @@ func (r *Reader) Fonts() ([]Font, error) {
|
||||
// FontInfos 获取OFD字体诊断信息
|
||||
// 返回: []FontInfo 字体诊断列表, error 错误信息
|
||||
func (r *Renderer) FontInfos() ([]FontInfo, error) {
|
||||
doc, err := r.fontInfoDocument()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.fontInfos(doc, nil)
|
||||
}
|
||||
|
||||
// FontInfosFromPages 从页面内容获取OFD字体诊断信息
|
||||
// 入参: pages 页面内容列表
|
||||
// 返回: []FontInfo 字体诊断列表, error 错误信息
|
||||
func (r *Renderer) FontInfosFromPages(pages []*PageContent) ([]FontInfo, error) {
|
||||
doc, err := r.fontInfoDocument()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if pages == nil {
|
||||
pages = []*PageContent{}
|
||||
}
|
||||
return r.fontInfos(doc, pages)
|
||||
}
|
||||
|
||||
// fontInfoDocument 获取字体诊断文档结构
|
||||
// 返回: *Document 文档结构, error 错误信息
|
||||
func (r *Renderer) fontInfoDocument() (*Document, error) {
|
||||
if r == nil || r.Reader == nil {
|
||||
return nil, fmt.Errorf("ofd renderer is not initialized")
|
||||
}
|
||||
@@ -77,11 +101,18 @@ func (r *Renderer) FontInfos() ([]FontInfo, error) {
|
||||
if doc == nil {
|
||||
return nil, fmt.Errorf("ofd document is not opened")
|
||||
}
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
// fontInfos 获取OFD字体诊断信息
|
||||
// 入参: doc 文档结构, pages 页面内容列表
|
||||
// 返回: []FontInfo 字体诊断列表, error 错误信息
|
||||
func (r *Renderer) fontInfos(doc *Document, pages []*PageContent) ([]FontInfo, error) {
|
||||
fonts, err := r.Reader.Fonts()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
usage := r.fontUsage(doc)
|
||||
usage := r.fontUsage(doc, pages)
|
||||
infos := make([]FontInfo, 0, len(fonts)+len(usage))
|
||||
seen := make(map[string]bool)
|
||||
for _, font := range fonts {
|
||||
@@ -98,7 +129,7 @@ func (r *Renderer) FontInfos() ([]FontInfo, error) {
|
||||
ID: id,
|
||||
FontName: id,
|
||||
Status: FontStatusMissing,
|
||||
Detail: "未在OFD资源中声明",
|
||||
Detail: "字体资源未被声明",
|
||||
Used: used,
|
||||
})
|
||||
}
|
||||
@@ -130,16 +161,16 @@ func (r *Renderer) fontInfo(font Font) FontInfo {
|
||||
if _, err := r.Reader.ResData(font.FontFile); err == nil {
|
||||
info.Status = FontStatusEmbedded
|
||||
info.Matched = path.Base(font.FontFile)
|
||||
info.Detail = "使用OFD内嵌字体"
|
||||
info.Detail = "使用内嵌字体文件"
|
||||
} else {
|
||||
info.Status = FontStatusMissing
|
||||
info.Detail = "内嵌字体文件缺失"
|
||||
}
|
||||
return info
|
||||
}
|
||||
if matched, exact := r.matchFont(font.FontName, font.FamilyName); matched != "" {
|
||||
info.Matched = matched
|
||||
if exact {
|
||||
if source, ok := r.fontSourceMatch(font.ID, &font); ok {
|
||||
info.Matched = source.name
|
||||
if source.exact {
|
||||
info.Status = FontStatusMatched
|
||||
info.Detail = "使用外部字体文件"
|
||||
} else {
|
||||
@@ -149,119 +180,50 @@ func (r *Renderer) fontInfo(font Font) FontInfo {
|
||||
return info
|
||||
}
|
||||
info.Status = FontStatusMissing
|
||||
info.Detail = "未找到可用字体"
|
||||
info.Detail = "可用字体文件缺失"
|
||||
return info
|
||||
}
|
||||
|
||||
// matchFont 匹配外部字体
|
||||
// 入参: names 字体名称列表
|
||||
// 返回: string 匹配字体文件, bool 是否为名称匹配
|
||||
func (r *Renderer) matchFont(names ...string) (string, bool) {
|
||||
for _, dir := range r.fontDirs {
|
||||
for _, pattern := range fontFilePatterns(names...) {
|
||||
if matches := r.globFontFiles(dir, pattern); len(matches) > 0 {
|
||||
return matches[0], true
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, fsys := range r.fontFS {
|
||||
if matcher, ok := fsys.(interface {
|
||||
Match(...string) (string, bool)
|
||||
}); ok {
|
||||
if matched, exact := matcher.Match(names...); matched != "" {
|
||||
return matched, exact
|
||||
}
|
||||
continue
|
||||
}
|
||||
if matched := matchFontFS(fsys, names...); matched != "" {
|
||||
return matched, true
|
||||
}
|
||||
}
|
||||
if !canLoadSystemFonts() {
|
||||
for _, fsys := range r.fontFS {
|
||||
if matched := fallbackFontFS(fsys); matched != "" {
|
||||
return matched, false
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// matchFontFS 从字体文件系统匹配字体
|
||||
// 入参: fsys 字体文件系统, names 字体名称列表
|
||||
// 返回: string 匹配字体文件
|
||||
func matchFontFS(fsys fs.FS, names ...string) string {
|
||||
for _, pattern := range fontFilePatterns(names...) {
|
||||
if matches := fontFSMatches(fsys, pattern); len(matches) > 0 {
|
||||
return matches[0]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// fallbackFontFS 从字体文件系统获取回退字体
|
||||
// 入参: fsys 字体文件系统
|
||||
// 返回: string 回退字体文件
|
||||
func fallbackFontFS(fsys fs.FS) string {
|
||||
for _, pattern := range fontFallbackFiles {
|
||||
if matches := fontFSMatches(fsys, pattern); len(matches) > 0 {
|
||||
return matches[0]
|
||||
}
|
||||
}
|
||||
all, _ := fs.Glob(fsys, "*")
|
||||
for _, name := range all {
|
||||
if isFontFileName(name) {
|
||||
return name
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// fontFSMatches 匹配字体文件系统中的字体文件
|
||||
// 入参: fsys 字体文件系统, pattern 匹配模式
|
||||
// fontFSMatchesStyle 匹配字体文件系统中的指定样式字体文件
|
||||
// 入参: fsys 字体文件系统, patterns 匹配模式列表, bold 是否粗体, italic 是否斜体
|
||||
// 返回: []string 字体文件列表
|
||||
func fontFSMatches(fsys fs.FS, pattern string) []string {
|
||||
seen := make(map[string]bool)
|
||||
buckets := make([][]string, fontMatchFuzzy+1)
|
||||
add := func(name string, rank int) {
|
||||
if !isFontFileName(name) {
|
||||
return
|
||||
func fontFSMatchesStyle(fsys fs.FS, patterns []string, bold, italic bool) []string {
|
||||
names, _ := fs.Glob(fsys, "*")
|
||||
candidates := fontFileCandidates(names, path.Base)
|
||||
matches := make([]fontFileMatch, 0, len(candidates))
|
||||
seen := make(map[string]int, len(candidates))
|
||||
for _, matcher := range newFontPatternMatchers(patterns) {
|
||||
for _, file := range candidates {
|
||||
rank := matcher.rankCandidate(file)
|
||||
appendFontFileMatch(&matches, seen, matcher, file, rank, bold, italic)
|
||||
}
|
||||
if rank == fontMatchNone || seen[name] {
|
||||
return
|
||||
}
|
||||
seen[name] = true
|
||||
buckets[rank] = append(buckets[rank], name)
|
||||
}
|
||||
matches, _ := fs.Glob(fsys, pattern)
|
||||
for _, name := range matches {
|
||||
add(name, matchFontPatternRank(pattern, path.Base(name)))
|
||||
}
|
||||
all, _ := fs.Glob(fsys, "*")
|
||||
for _, name := range all {
|
||||
add(name, matchFontPatternRank(pattern, path.Base(name)))
|
||||
}
|
||||
var result []string
|
||||
for rank := fontMatchExact; rank <= fontMatchFuzzy; rank++ {
|
||||
result = append(result, buckets[rank]...)
|
||||
}
|
||||
return result
|
||||
sortFontFileMatches(matches)
|
||||
return fontFileMatchNames(matches)
|
||||
}
|
||||
|
||||
// fontUsage 统计文档字体使用次数
|
||||
// 入参: doc 文档结构
|
||||
// 入参: doc 文档结构, pages 页面内容列表, nil表示读取文档页面
|
||||
// 返回: map[string]int 字体使用次数
|
||||
func (r *Renderer) fontUsage(doc *Document) map[string]int {
|
||||
func (r *Renderer) fontUsage(doc *Document, pages []*PageContent) map[string]int {
|
||||
usage := make(map[string]int)
|
||||
if pages == nil {
|
||||
for _, pageRef := range doc.Pages.Page {
|
||||
page, err := r.Reader.PageContent(pageRef)
|
||||
if err == nil {
|
||||
if page, err := r.Reader.PageContent(pageRef); err == nil {
|
||||
r.countPageFonts(page, usage)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, page := range pages {
|
||||
if page != nil {
|
||||
r.countPageFonts(page, usage)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, tpl := range doc.CommonData.TemplatePage {
|
||||
page, err := r.Reader.PageContent(Page{BaseLoc: tpl.BaseLoc})
|
||||
if err == nil {
|
||||
r.templatePageCache[tpl.ID] = page
|
||||
r.countPageFonts(page, usage)
|
||||
}
|
||||
}
|
||||
@@ -336,35 +298,7 @@ func (r *Renderer) countCompositeFonts(cgu CompositeGraphicUnit, usage map[strin
|
||||
// countTextFont 统计文本字体使用次数
|
||||
// 入参: text 文本对象, usage 字体使用次数
|
||||
func (r *Renderer) countTextFont(text TextObject, usage map[string]int) {
|
||||
fontID := text.Font
|
||||
if fontID == "" && text.DrawParam != "" {
|
||||
fontID = r.drawParamFont(text.DrawParam, nil)
|
||||
}
|
||||
if fontID != "" {
|
||||
if fontID := r.textObjectFontID(text); fontID != "" {
|
||||
usage[fontID]++
|
||||
}
|
||||
}
|
||||
|
||||
// drawParamFont 获取绘制参数字体
|
||||
// 入参: id 绘制参数ID, visited 已访问绘制参数
|
||||
// 返回: string 字体ID
|
||||
func (r *Renderer) drawParamFont(id string, visited map[string]bool) string {
|
||||
if id == "" {
|
||||
return ""
|
||||
}
|
||||
if visited == nil {
|
||||
visited = make(map[string]bool)
|
||||
}
|
||||
if visited[id] {
|
||||
return ""
|
||||
}
|
||||
visited[id] = true
|
||||
dp := r.DrawParams[id]
|
||||
if dp == nil {
|
||||
return ""
|
||||
}
|
||||
if dp.Font != "" {
|
||||
return dp.Font
|
||||
}
|
||||
return r.drawParamFont(dp.Relative, visited)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ofdgo
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
)
|
||||
|
||||
// fontSourceKind 字体来源类型
|
||||
type fontSourceKind uint8
|
||||
|
||||
const (
|
||||
fontSourceFile fontSourceKind = iota
|
||||
fontSourceFS
|
||||
fontSourceSystem
|
||||
)
|
||||
|
||||
// fontSource 字体来源
|
||||
type fontSource struct {
|
||||
kind fontSourceKind
|
||||
index int
|
||||
name string
|
||||
exact bool
|
||||
}
|
||||
|
||||
// fontSourceKey 字体来源去重键
|
||||
type fontSourceKey struct {
|
||||
kind fontSourceKind
|
||||
index int
|
||||
name string
|
||||
}
|
||||
|
||||
// fontCacheKey 字体加载缓存键
|
||||
type fontCacheKey struct {
|
||||
fontSourceKey
|
||||
style canvas.FontStyle
|
||||
}
|
||||
|
||||
// loadFont 加载字体
|
||||
// 入参: fontID 字体ID
|
||||
// 返回: *canvas.FontFamily 字体族
|
||||
func (r *Renderer) loadFont(fontID string) *canvas.FontFamily {
|
||||
if ff, ok := r.FontMap[fontID]; ok {
|
||||
return ff
|
||||
}
|
||||
var defaultFont *canvas.FontFamily
|
||||
if r.defaultFontLoaded {
|
||||
defaultFont = r.fontFamily
|
||||
}
|
||||
of, ok := r.Reader.fontCache[fontID]
|
||||
if !ok {
|
||||
return defaultFont
|
||||
}
|
||||
fontStyle := canvasFontStyle(of)
|
||||
ff := canvas.NewFontFamily(of.FontName)
|
||||
if of.FontFile != "" {
|
||||
if fontData, err := r.Reader.ResData(of.FontFile); err == nil {
|
||||
if cidMap := getCFFCIDRuneMap(fontData); len(cidMap) > 0 {
|
||||
if r.FontCIDMap == nil {
|
||||
r.FontCIDMap = make(map[string]map[uint16]rune)
|
||||
}
|
||||
r.FontCIDMap[fontID] = cidMap
|
||||
}
|
||||
if _, fixedData, mapping, _, err := FixFontDataAggressive(fontData, true, true); err == nil {
|
||||
fontData = fixedData
|
||||
if mapping != nil {
|
||||
if r.FontGIDMap == nil {
|
||||
r.FontGIDMap = make(map[string]map[uint16]rune)
|
||||
}
|
||||
inv := make(map[uint16]rune)
|
||||
for k, v := range mapping {
|
||||
if k == packedGlyphRune(v) {
|
||||
inv[v] = k
|
||||
}
|
||||
}
|
||||
for k, v := range mapping {
|
||||
if _, ok := inv[v]; !ok {
|
||||
inv[v] = k
|
||||
}
|
||||
}
|
||||
r.FontGIDMap[fontID] = inv
|
||||
}
|
||||
}
|
||||
if err := ff.LoadFont(fontData, 0, fontStyle); err == nil {
|
||||
r.FontMap[fontID] = ff
|
||||
return ff
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
for _, source := range r.fontSources(fontID, of, fontStyle) {
|
||||
if loaded := r.loadFontSource(ff, source, fontStyle); loaded != nil {
|
||||
r.FontMap[fontID] = loaded
|
||||
r.fontSourceUsed[fontID] = source
|
||||
return loaded
|
||||
}
|
||||
}
|
||||
r.FontMap[fontID] = defaultFont
|
||||
return defaultFont
|
||||
}
|
||||
|
||||
// canvasFontStyle 获取Canvas字体样式
|
||||
// 入参: font OFD字体定义
|
||||
// 返回: canvas.FontStyle Canvas字体样式
|
||||
func canvasFontStyle(font *Font) canvas.FontStyle {
|
||||
var style canvas.FontStyle
|
||||
if font.Bold {
|
||||
style |= canvas.FontBold
|
||||
}
|
||||
if font.Italic {
|
||||
style |= canvas.FontItalic
|
||||
}
|
||||
return style
|
||||
}
|
||||
|
||||
// fontSources 获取字体来源列表
|
||||
// 入参: fontID 字体ID, font OFD字体定义, style Canvas字体样式
|
||||
// 返回: []fontSource 字体来源列表
|
||||
func (r *Renderer) fontSources(fontID string, font *Font, style canvas.FontStyle) []fontSource {
|
||||
if sources, ok := r.fontSourceCache[fontID]; ok {
|
||||
return sources
|
||||
}
|
||||
bold := style&canvas.FontBold != 0
|
||||
italic := style&canvas.FontItalic != 0
|
||||
patterns := fontFilePatterns(font.FontName, font.FamilyName)
|
||||
sources := make([]fontSource, 0)
|
||||
seen := make(map[fontSourceKey]bool)
|
||||
for _, dir := range r.fontDirs {
|
||||
for _, name := range r.matchFontFiles(dir, patterns, bold, italic) {
|
||||
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFile, name: name, exact: true})
|
||||
}
|
||||
}
|
||||
for index, fsys := range r.fontFS {
|
||||
for _, name := range fontFSMatchesStyle(fsys, patterns, bold, italic) {
|
||||
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: name, exact: true})
|
||||
}
|
||||
}
|
||||
if !canLoadSystemFonts() {
|
||||
for index, fsys := range r.fontFS {
|
||||
names, _ := fs.Glob(fsys, "*")
|
||||
for _, name := range names {
|
||||
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: name})
|
||||
}
|
||||
}
|
||||
r.fontSourceCache[fontID] = sources
|
||||
return sources
|
||||
}
|
||||
systemDirs := systemFontDirs()
|
||||
for _, name := range []string{font.FamilyName, font.FontName} {
|
||||
for _, systemName := range fontSystemNames(name) {
|
||||
systemPatterns := fontFilePatterns(systemName)
|
||||
for _, dir := range systemDirs {
|
||||
for _, match := range r.matchFontFiles(dir, systemPatterns, bold, italic) {
|
||||
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFile, name: match, exact: true})
|
||||
}
|
||||
}
|
||||
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceSystem, name: systemName, exact: true})
|
||||
}
|
||||
}
|
||||
r.fontSourceCache[fontID] = sources
|
||||
return sources
|
||||
}
|
||||
|
||||
// appendFontSource 追加字体来源
|
||||
// 入参: sources 字体来源列表, seen 去重映射, source 字体来源
|
||||
// 返回: []fontSource 字体来源列表
|
||||
func appendFontSource(sources []fontSource, seen map[fontSourceKey]bool, source fontSource) []fontSource {
|
||||
if source.name == "" {
|
||||
return sources
|
||||
}
|
||||
key := fontSourceKey{kind: source.kind, index: source.index, name: source.name}
|
||||
if seen[key] {
|
||||
return sources
|
||||
}
|
||||
seen[key] = true
|
||||
return append(sources, source)
|
||||
}
|
||||
|
||||
// fontSourceMatch 获取匹配的字体来源
|
||||
// 入参: fontID 字体ID, font OFD字体定义
|
||||
// 返回: fontSource 字体来源, bool 是否匹配
|
||||
func (r *Renderer) fontSourceMatch(fontID string, font *Font) (fontSource, bool) {
|
||||
if source, ok := r.fontSourceUsed[fontID]; ok {
|
||||
return source, true
|
||||
}
|
||||
style := canvasFontStyle(font)
|
||||
for _, source := range r.fontSources(fontID, font, style) {
|
||||
key := fontCacheKey{fontSourceKey: fontSourceKey{kind: source.kind, index: source.index, name: source.name}, style: style}
|
||||
if cached, ok := r.fontCache[key]; ok {
|
||||
if cached != nil {
|
||||
return source, true
|
||||
}
|
||||
continue
|
||||
}
|
||||
family := canvas.NewFontFamily(font.FontName)
|
||||
if r.loadFontSource(family, source, style) != nil {
|
||||
return source, true
|
||||
}
|
||||
}
|
||||
return fontSource{}, false
|
||||
}
|
||||
|
||||
// loadFontSource 加载字体来源
|
||||
// 入参: family 字体族, source 字体来源, style Canvas字体样式
|
||||
// 返回: *canvas.FontFamily 字体族
|
||||
func (r *Renderer) loadFontSource(family *canvas.FontFamily, source fontSource, style canvas.FontStyle) *canvas.FontFamily {
|
||||
key := fontCacheKey{fontSourceKey: fontSourceKey{kind: source.kind, index: source.index, name: source.name}, style: style}
|
||||
if cached, ok := r.fontCache[key]; ok {
|
||||
return cached
|
||||
}
|
||||
var err error
|
||||
switch source.kind {
|
||||
case fontSourceFile:
|
||||
err = family.LoadFontFile(source.name, style)
|
||||
case fontSourceFS:
|
||||
var data []byte
|
||||
data, err = fs.ReadFile(r.fontFS[source.index], source.name)
|
||||
if err == nil {
|
||||
err = family.LoadFont(data, 0, style)
|
||||
}
|
||||
case fontSourceSystem:
|
||||
err = family.LoadSystemFont(source.name, style)
|
||||
}
|
||||
if err != nil {
|
||||
r.fontCache[key] = nil
|
||||
return nil
|
||||
}
|
||||
r.fontCache[key] = family
|
||||
return family
|
||||
}
|
||||
|
||||
// matchFontFiles 查找字体文件
|
||||
// 入参: dir 目录, patterns 模式列表, bold 是否粗体, italic 是否斜体
|
||||
// 返回: []string 文件列表
|
||||
func (r *Renderer) matchFontFiles(dir string, patterns []string, bold, italic bool) []string {
|
||||
candidates, ok := r.fontDirCandidates[dir]
|
||||
if !ok {
|
||||
files, _ := filepath.Glob(filepath.Join(dir, "*"))
|
||||
candidates = fontFileCandidates(files, filepath.Base)
|
||||
r.fontDirCandidates[dir] = candidates
|
||||
}
|
||||
matches := make([]fontFileMatch, 0, len(candidates))
|
||||
index := make(map[string]int, len(candidates))
|
||||
for _, matcher := range newFontPatternMatchers(patterns) {
|
||||
for _, file := range candidates {
|
||||
rank := matcher.rankCandidate(file)
|
||||
appendFontFileMatch(&matches, index, matcher, file, rank, bold, italic)
|
||||
}
|
||||
}
|
||||
sortFontFileMatches(matches)
|
||||
return fontFileMatchNames(matches)
|
||||
}
|
||||
|
||||
// systemFontDirs 获取系统字体目录
|
||||
// 返回: []string 字体目录列表
|
||||
func systemFontDirs() []string {
|
||||
switch runtime.GOOS {
|
||||
case "android":
|
||||
return []string{"/product/fonts", "/system/fonts"}
|
||||
case "darwin":
|
||||
return []string{`/Library/Fonts`}
|
||||
case "linux":
|
||||
return []string{`/usr/share/fonts`}
|
||||
case "windows":
|
||||
return []string{`C:\Windows\Fonts`}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+25
-34
@@ -117,6 +117,8 @@ var fontFallbackFiles = []string{
|
||||
"arial.ttf",
|
||||
}
|
||||
|
||||
var fontNameReplacer = strings.NewReplacer(" ", "", "-", "", "_", "", "(", "", ")", "", "(", "", ")", "")
|
||||
|
||||
// FontNormalizeName 规范化字体名称
|
||||
// 入参: name 字体名称
|
||||
// 返回: string 规范化后的字体名称
|
||||
@@ -149,10 +151,10 @@ func FontSystemNames(names ...string) []string {
|
||||
// 入参: name 字体名称
|
||||
// 返回: string 规范化后的字体名称
|
||||
func fontNormalizeName(name string) string {
|
||||
name = strings.ReplaceAll(name, "\\", "/")
|
||||
name = strings.TrimSuffix(path.Base(name), path.Ext(name))
|
||||
name = strings.ToLower(strings.TrimSpace(name))
|
||||
replacer := strings.NewReplacer(" ", "", "-", "", "_", "", "(", "", ")", "", "(", "", ")", "")
|
||||
return replacer.Replace(name)
|
||||
return fontNameReplacer.Replace(name)
|
||||
}
|
||||
|
||||
// fontCandidateNames 获取字体候选名称
|
||||
@@ -310,13 +312,6 @@ func isFontFileName(name string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// fontRuleMatch 判断字体规则是否匹配
|
||||
// 入参: rule 字体匹配规则, name 字体名称
|
||||
// 返回: bool 是否匹配
|
||||
func fontRuleMatch(rule fontMatchRule, name string) bool {
|
||||
return fontRuleMatchLevel(rule, name) != fontMatchNone
|
||||
}
|
||||
|
||||
// fontRuleMatchLevel 获取字体规则匹配等级
|
||||
// 入参: rule 字体匹配规则, name 字体名称
|
||||
// 返回: int 匹配等级
|
||||
@@ -325,39 +320,35 @@ func fontRuleMatchLevel(rule fontMatchRule, name string) int {
|
||||
if name == "" {
|
||||
return fontMatchNone
|
||||
}
|
||||
items := fontRuleNames(rule)
|
||||
for _, item := range items {
|
||||
level := fontMatchNone
|
||||
match := func(item string) bool {
|
||||
item = fontNormalizeName(item)
|
||||
if item != "" && name == item {
|
||||
return fontMatchExact
|
||||
level = fontMatchExact
|
||||
return true
|
||||
}
|
||||
if item != "" && (level == fontMatchNone || level > fontMatchPartial) && strings.HasPrefix(name, item) {
|
||||
level = fontMatchPartial
|
||||
}
|
||||
if item != "" && level == fontMatchNone && strings.Contains(name, item) {
|
||||
level = fontMatchFuzzy
|
||||
}
|
||||
return false
|
||||
}
|
||||
for _, item := range rule.Keys {
|
||||
if match(item) {
|
||||
return level
|
||||
}
|
||||
}
|
||||
for _, item := range items {
|
||||
item = fontNormalizeName(item)
|
||||
if item != "" && (strings.HasPrefix(name, item) || strings.HasPrefix(item, name)) {
|
||||
return fontMatchPartial
|
||||
for _, item := range rule.Names {
|
||||
if match(item) {
|
||||
return level
|
||||
}
|
||||
}
|
||||
for _, item := range items {
|
||||
item = fontNormalizeName(item)
|
||||
if item != "" && (strings.Contains(name, item) || strings.Contains(item, name)) {
|
||||
return fontMatchFuzzy
|
||||
}
|
||||
}
|
||||
return fontMatchNone
|
||||
}
|
||||
|
||||
// fontRuleNames 获取字体规则名称列表
|
||||
// 入参: rule 字体匹配规则
|
||||
// 返回: []string 字体规则名称列表
|
||||
func fontRuleNames(rule fontMatchRule) []string {
|
||||
var result []string
|
||||
result = append(result, rule.Keys...)
|
||||
result = append(result, rule.Names...)
|
||||
if rule.System != "" {
|
||||
result = append(result, rule.System)
|
||||
match(rule.System)
|
||||
}
|
||||
return result
|
||||
return level
|
||||
}
|
||||
|
||||
// appendFontName 追加字体名称
|
||||
|
||||
@@ -20,13 +20,6 @@ import (
|
||||
"sort"
|
||||
)
|
||||
|
||||
// align4 计算 4 字节对齐后的长度
|
||||
// 入参: n 原始长度
|
||||
// 返回: uint32 对齐后的长度
|
||||
func align4(n uint32) uint32 {
|
||||
return (n + 3) & ^uint32(3)
|
||||
}
|
||||
|
||||
// packedGlyphRune 获取包装字体字符
|
||||
// 入参: gid 字形ID
|
||||
// 返回: rune 包装字体字符
|
||||
@@ -55,27 +48,6 @@ func calcTableChecksum(data []byte) uint32 {
|
||||
return sum
|
||||
}
|
||||
|
||||
// checkMissingCmap 检查是否缺失 cmap 表
|
||||
// 入参: data 字体数据
|
||||
// 返回: bool 是否缺失
|
||||
func checkMissingCmap(data []byte) bool {
|
||||
if len(data) < 12 {
|
||||
return true
|
||||
}
|
||||
numTables := binary.BigEndian.Uint16(data[4:6])
|
||||
for i := 0; i < int(numTables); i++ {
|
||||
pos := 12 + i*16
|
||||
if len(data) < pos+4 {
|
||||
break
|
||||
}
|
||||
tag := string(data[pos : pos+4])
|
||||
if tag == "cmap" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// parseCmapMappings 解析 cmap 字符映射
|
||||
// 入参: data cmap表数据
|
||||
// 返回: map[rune]uint16 字符到字形映射
|
||||
@@ -287,13 +259,6 @@ func buildHheaTable(numGlyphs uint16) []byte {
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// buildMaxpTable 构建 maxp 表
|
||||
// 入参: numGlyphs 字形数量
|
||||
// 返回: []byte maxp表数据
|
||||
func buildMaxpTable(numGlyphs uint16) []byte {
|
||||
return buildCFFMaxpTable(numGlyphs)
|
||||
}
|
||||
|
||||
// buildCFFMaxpTable 构建 CFF 轮廓使用的 maxp 0.5 表
|
||||
// 入参: numGlyphs 字形数量
|
||||
// 返回: []byte maxp表数据
|
||||
|
||||
+12
-7
@@ -15,6 +15,7 @@
|
||||
package ofdgo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -30,14 +31,18 @@ type Box struct {
|
||||
// 返回: Box 矩形对象, error 错误信息
|
||||
func ParseBox(s string) (Box, error) {
|
||||
parts := strings.Fields(s)
|
||||
if len(parts) < 4 {
|
||||
return Box{}, nil
|
||||
if len(parts) != 4 {
|
||||
return Box{}, fmt.Errorf("invalid box: %s", s)
|
||||
}
|
||||
x, _ := strconv.ParseFloat(parts[0], 64)
|
||||
y, _ := strconv.ParseFloat(parts[1], 64)
|
||||
w, _ := strconv.ParseFloat(parts[2], 64)
|
||||
h, _ := strconv.ParseFloat(parts[3], 64)
|
||||
return Box{X: x, Y: y, W: w, H: h}, nil
|
||||
var values [4]float64
|
||||
for i, part := range parts {
|
||||
value, err := strconv.ParseFloat(part, 64)
|
||||
if err != nil {
|
||||
return Box{}, fmt.Errorf("invalid box: %s", s)
|
||||
}
|
||||
values[i] = value
|
||||
}
|
||||
return Box{X: values[0], Y: values[1], W: values[2], H: values[3]}, nil
|
||||
}
|
||||
|
||||
// Matrix 2D仿射变换矩阵
|
||||
|
||||
@@ -20,12 +20,28 @@ import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
_ "image/gif"
|
||||
|
||||
canvasimage "github.com/tdewolff/canvas/image"
|
||||
_ "github.com/xiaoqidun/jbig2"
|
||||
_ "golang.org/x/image/bmp"
|
||||
_ "golang.org/x/image/tiff"
|
||||
)
|
||||
|
||||
// decodeImageData 解码图片数据
|
||||
// 入参: data 图片数据
|
||||
// 返回: image.Image 图片对象, string 图片格式, error 错误信息
|
||||
func decodeImageData(data []byte) (image.Image, string, error) {
|
||||
if isJPEGData(data) {
|
||||
if img, err := canvasimage.NewJPEGImage(bytes.NewReader(data)); err == nil {
|
||||
return img, "jpeg", nil
|
||||
}
|
||||
}
|
||||
if isPNGData(data) {
|
||||
if img, err := canvasimage.NewPNGImage(bytes.NewReader(data)); err == nil {
|
||||
return img, "png", nil
|
||||
}
|
||||
}
|
||||
img, format, err := image.Decode(bytes.NewReader(data))
|
||||
if err == nil {
|
||||
return img, normalizeSealType(format), nil
|
||||
@@ -36,6 +52,20 @@ func decodeImageData(data []byte) (image.Image, string, error) {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// isJPEGData 判断是否为JPEG图片数据
|
||||
// 入参: data 图片数据
|
||||
// 返回: bool 是否为JPEG图片数据
|
||||
func isJPEGData(data []byte) bool {
|
||||
return len(data) >= 2 && data[0] == 0xFF && data[1] == 0xD8
|
||||
}
|
||||
|
||||
// isPNGData 判断是否为PNG图片数据
|
||||
// 入参: data 图片数据
|
||||
// 返回: bool 是否为PNG图片数据
|
||||
func isPNGData(data []byte) bool {
|
||||
return bytes.HasPrefix(data, []byte("\x89PNG\r\n\x1a\n"))
|
||||
}
|
||||
|
||||
// decodeImageConfigData 解码图片尺寸
|
||||
// 入参: data 图片数据
|
||||
// 返回: image.Config 图片尺寸, string 图片格式, error 错误信息
|
||||
|
||||
+123
-274
@@ -19,6 +19,76 @@ import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// graphicObjectTarget 图形对象集合
|
||||
type graphicObjectTarget struct {
|
||||
objects *[]GraphicObject
|
||||
text *[]TextObject
|
||||
path *[]PathObject
|
||||
image *[]ImageObject
|
||||
composite *[]CompositeGraphicUnit
|
||||
}
|
||||
|
||||
// decodeGraphicObject 解析图形对象
|
||||
// 入参: d XML解码器, start 起始节点, target 图形对象集合
|
||||
// 返回: bool 是否为图形对象, error 错误信息
|
||||
func decodeGraphicObject(d *xml.Decoder, start xml.StartElement, target graphicObjectTarget) (bool, error) {
|
||||
switch start.Name.Local {
|
||||
case "TextObject":
|
||||
var obj TextObject
|
||||
if err := d.DecodeElement(&obj, &start); err != nil {
|
||||
return true, err
|
||||
}
|
||||
*target.text = append(*target.text, obj)
|
||||
*target.objects = append(*target.objects, GraphicObject{Type: start.Name.Local, TextObject: obj})
|
||||
case "PathObject":
|
||||
var obj PathObject
|
||||
if err := d.DecodeElement(&obj, &start); err != nil {
|
||||
return true, err
|
||||
}
|
||||
*target.path = append(*target.path, obj)
|
||||
*target.objects = append(*target.objects, GraphicObject{Type: start.Name.Local, PathObject: obj})
|
||||
case "ImageObject":
|
||||
var obj ImageObject
|
||||
if err := d.DecodeElement(&obj, &start); err != nil {
|
||||
return true, err
|
||||
}
|
||||
*target.image = append(*target.image, obj)
|
||||
*target.objects = append(*target.objects, GraphicObject{Type: start.Name.Local, ImageObject: obj})
|
||||
case "CompositeGraphicUnit", "CompositeObject":
|
||||
var obj CompositeGraphicUnit
|
||||
if err := d.DecodeElement(&obj, &start); err != nil {
|
||||
return true, err
|
||||
}
|
||||
*target.composite = append(*target.composite, obj)
|
||||
*target.objects = append(*target.objects, GraphicObject{Type: start.Name.Local, CompositeGraphicUnit: obj})
|
||||
default:
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// decodeObjectContainer 解析图形对象容器
|
||||
// 入参: d XML解码器, start 起始节点, decode 对象解码函数
|
||||
// 返回: error 错误信息
|
||||
func decodeObjectContainer(d *xml.Decoder, start xml.StartElement, decode func(*xml.Decoder, xml.StartElement) error) error {
|
||||
for {
|
||||
tok, err := d.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch node := tok.(type) {
|
||||
case xml.StartElement:
|
||||
if err := decode(d, node); err != nil {
|
||||
return err
|
||||
}
|
||||
case xml.EndElement:
|
||||
if node.Name.Local == start.Name.Local {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalXML 解析图层并保留对象顺序
|
||||
// 入参: d XML解码器, start 起始节点
|
||||
// 返回: error 错误信息
|
||||
@@ -26,85 +96,27 @@ func (l *Layer) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
*l = Layer{}
|
||||
l.ID = attrValue(start, "ID")
|
||||
l.DrawParam = attrValue(start, "DrawParam")
|
||||
for {
|
||||
tok, err := d.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch node := tok.(type) {
|
||||
case xml.StartElement:
|
||||
if err := l.decodeObject(d, node); err != nil {
|
||||
return err
|
||||
}
|
||||
case xml.EndElement:
|
||||
if node.Name.Local == start.Name.Local {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return decodeObjectContainer(d, start, l.decodeObject)
|
||||
}
|
||||
|
||||
// decodeObject 解析图层子对象
|
||||
// 入参: d XML解码器, start 起始节点
|
||||
// 返回: error 错误信息
|
||||
func (l *Layer) decodeObject(d *xml.Decoder, start xml.StartElement) error {
|
||||
switch start.Name.Local {
|
||||
case "TextObject":
|
||||
var obj TextObject
|
||||
if err := d.DecodeElement(&obj, &start); err != nil {
|
||||
target := graphicObjectTarget{
|
||||
objects: &l.Objects,
|
||||
text: &l.TextObject,
|
||||
path: &l.PathObject,
|
||||
image: &l.ImageObject,
|
||||
composite: &l.CompositeGraphicUnit,
|
||||
}
|
||||
if decoded, err := decodeGraphicObject(d, start, target); decoded || err != nil {
|
||||
return err
|
||||
}
|
||||
l.TextObject = append(l.TextObject, obj)
|
||||
l.Objects = append(l.Objects, GraphicObject{Type: start.Name.Local, TextObject: obj})
|
||||
case "PathObject":
|
||||
var obj PathObject
|
||||
if err := d.DecodeElement(&obj, &start); err != nil {
|
||||
return err
|
||||
if start.Name.Local == "PageBlock" {
|
||||
return decodeObjectContainer(d, start, l.decodeObject)
|
||||
}
|
||||
l.PathObject = append(l.PathObject, obj)
|
||||
l.Objects = append(l.Objects, GraphicObject{Type: start.Name.Local, PathObject: obj})
|
||||
case "ImageObject":
|
||||
var obj ImageObject
|
||||
if err := d.DecodeElement(&obj, &start); err != nil {
|
||||
return err
|
||||
}
|
||||
l.ImageObject = append(l.ImageObject, obj)
|
||||
l.Objects = append(l.Objects, GraphicObject{Type: start.Name.Local, ImageObject: obj})
|
||||
case "CompositeGraphicUnit", "CompositeObject":
|
||||
var obj CompositeGraphicUnit
|
||||
if err := d.DecodeElement(&obj, &start); err != nil {
|
||||
return err
|
||||
}
|
||||
l.CompositeGraphicUnit = append(l.CompositeGraphicUnit, obj)
|
||||
l.Objects = append(l.Objects, GraphicObject{Type: start.Name.Local, CompositeGraphicUnit: obj})
|
||||
case "PageBlock":
|
||||
return l.decodePageBlock(d, start)
|
||||
default:
|
||||
return d.Skip()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// decodePageBlock 解析页块子对象
|
||||
// 入参: d XML解码器, start 起始节点
|
||||
// 返回: error 错误信息
|
||||
func (l *Layer) decodePageBlock(d *xml.Decoder, start xml.StartElement) error {
|
||||
for {
|
||||
tok, err := d.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch node := tok.(type) {
|
||||
case xml.StartElement:
|
||||
if err := l.decodeObject(d, node); err != nil {
|
||||
return err
|
||||
}
|
||||
case xml.EndElement:
|
||||
if node.Name.Local == start.Name.Local {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalXML 解析复合图元并保留对象顺序
|
||||
@@ -128,178 +140,73 @@ func (c *CompositeGraphicUnit) UnmarshalXML(d *xml.Decoder, start xml.StartEleme
|
||||
c.Visible = &visible
|
||||
}
|
||||
}
|
||||
for {
|
||||
tok, err := d.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch node := tok.(type) {
|
||||
case xml.StartElement:
|
||||
if err := c.decodeObject(d, node); err != nil {
|
||||
return err
|
||||
}
|
||||
case xml.EndElement:
|
||||
if node.Name.Local == start.Name.Local {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return decodeObjectContainer(d, start, c.decodeObject)
|
||||
}
|
||||
|
||||
// decodeObject 解析复合图元子对象
|
||||
// 入参: d XML解码器, start 起始节点
|
||||
// 返回: error 错误信息
|
||||
func (c *CompositeGraphicUnit) decodeObject(d *xml.Decoder, start xml.StartElement) error {
|
||||
target := graphicObjectTarget{
|
||||
objects: &c.Objects,
|
||||
text: &c.TextObject,
|
||||
path: &c.PathObject,
|
||||
image: &c.ImageObject,
|
||||
composite: &c.CompositeGraphicUnit,
|
||||
}
|
||||
if decoded, err := decodeGraphicObject(d, start, target); decoded || err != nil {
|
||||
return err
|
||||
}
|
||||
switch start.Name.Local {
|
||||
case "TextObject":
|
||||
var obj TextObject
|
||||
if err := d.DecodeElement(&obj, &start); err != nil {
|
||||
return err
|
||||
}
|
||||
c.TextObject = append(c.TextObject, obj)
|
||||
c.Objects = append(c.Objects, GraphicObject{Type: start.Name.Local, TextObject: obj})
|
||||
case "PathObject":
|
||||
var obj PathObject
|
||||
if err := d.DecodeElement(&obj, &start); err != nil {
|
||||
return err
|
||||
}
|
||||
c.PathObject = append(c.PathObject, obj)
|
||||
c.Objects = append(c.Objects, GraphicObject{Type: start.Name.Local, PathObject: obj})
|
||||
case "ImageObject":
|
||||
var obj ImageObject
|
||||
if err := d.DecodeElement(&obj, &start); err != nil {
|
||||
return err
|
||||
}
|
||||
c.ImageObject = append(c.ImageObject, obj)
|
||||
c.Objects = append(c.Objects, GraphicObject{Type: start.Name.Local, ImageObject: obj})
|
||||
case "CompositeGraphicUnit", "CompositeObject":
|
||||
var obj CompositeGraphicUnit
|
||||
if err := d.DecodeElement(&obj, &start); err != nil {
|
||||
return err
|
||||
}
|
||||
c.CompositeGraphicUnit = append(c.CompositeGraphicUnit, obj)
|
||||
c.Objects = append(c.Objects, GraphicObject{Type: start.Name.Local, CompositeGraphicUnit: obj})
|
||||
case "Clips":
|
||||
var clips Clips
|
||||
if err := d.DecodeElement(&clips, &start); err != nil {
|
||||
return err
|
||||
}
|
||||
c.Clips = &clips
|
||||
case "Actions":
|
||||
var actions struct {
|
||||
Action []Action `xml:"Action"`
|
||||
}
|
||||
if err := d.DecodeElement(&actions, &start); err != nil {
|
||||
return err
|
||||
}
|
||||
c.Actions = actions.Action
|
||||
case "Content", "PageBlock":
|
||||
return c.decodePageBlock(d, start)
|
||||
return decodeObjectContainer(d, start, c.decodeObject)
|
||||
default:
|
||||
return d.Skip()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// decodePageBlock 解析页块子对象
|
||||
// 入参: d XML解码器, start 起始节点
|
||||
// 返回: error 错误信息
|
||||
func (c *CompositeGraphicUnit) decodePageBlock(d *xml.Decoder, start xml.StartElement) error {
|
||||
for {
|
||||
tok, err := d.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch node := tok.(type) {
|
||||
case xml.StartElement:
|
||||
if err := c.decodeObject(d, node); err != nil {
|
||||
return err
|
||||
}
|
||||
case xml.EndElement:
|
||||
if node.Name.Local == start.Name.Local {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalXML 解析注释外观并保留对象顺序
|
||||
// 入参: d XML解码器, start 起始节点
|
||||
// 返回: error 错误信息
|
||||
func (a *Appearance) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
*a = Appearance{}
|
||||
a.Boundary = attrValue(start, "Boundary")
|
||||
for {
|
||||
tok, err := d.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch node := tok.(type) {
|
||||
case xml.StartElement:
|
||||
if err := a.decodeObject(d, node); err != nil {
|
||||
return err
|
||||
}
|
||||
case xml.EndElement:
|
||||
if node.Name.Local == start.Name.Local {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return decodeObjectContainer(d, start, a.decodeObject)
|
||||
}
|
||||
|
||||
// decodeObject 解析注释外观子对象
|
||||
// 入参: d XML解码器, start 起始节点
|
||||
// 返回: error 错误信息
|
||||
func (a *Appearance) decodeObject(d *xml.Decoder, start xml.StartElement) error {
|
||||
switch start.Name.Local {
|
||||
case "TextObject":
|
||||
var obj TextObject
|
||||
if err := d.DecodeElement(&obj, &start); err != nil {
|
||||
target := graphicObjectTarget{
|
||||
objects: &a.Objects,
|
||||
text: &a.TextObject,
|
||||
path: &a.PathObject,
|
||||
image: &a.ImageObject,
|
||||
composite: &a.CompositeGraphicUnit,
|
||||
}
|
||||
if decoded, err := decodeGraphicObject(d, start, target); decoded || err != nil {
|
||||
return err
|
||||
}
|
||||
a.TextObject = append(a.TextObject, obj)
|
||||
a.Objects = append(a.Objects, GraphicObject{Type: start.Name.Local, TextObject: obj})
|
||||
case "PathObject":
|
||||
var obj PathObject
|
||||
if err := d.DecodeElement(&obj, &start); err != nil {
|
||||
return err
|
||||
if start.Name.Local == "PageBlock" {
|
||||
return decodeObjectContainer(d, start, a.decodeObject)
|
||||
}
|
||||
a.PathObject = append(a.PathObject, obj)
|
||||
a.Objects = append(a.Objects, GraphicObject{Type: start.Name.Local, PathObject: obj})
|
||||
case "ImageObject":
|
||||
var obj ImageObject
|
||||
if err := d.DecodeElement(&obj, &start); err != nil {
|
||||
return err
|
||||
}
|
||||
a.ImageObject = append(a.ImageObject, obj)
|
||||
a.Objects = append(a.Objects, GraphicObject{Type: start.Name.Local, ImageObject: obj})
|
||||
case "CompositeGraphicUnit", "CompositeObject":
|
||||
var obj CompositeGraphicUnit
|
||||
if err := d.DecodeElement(&obj, &start); err != nil {
|
||||
return err
|
||||
}
|
||||
a.CompositeGraphicUnit = append(a.CompositeGraphicUnit, obj)
|
||||
a.Objects = append(a.Objects, GraphicObject{Type: start.Name.Local, CompositeGraphicUnit: obj})
|
||||
case "PageBlock":
|
||||
return a.decodePageBlock(d, start)
|
||||
default:
|
||||
return d.Skip()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// decodePageBlock 解析注释外观页块子对象
|
||||
// 入参: d XML解码器, start 起始节点
|
||||
// 返回: error 错误信息
|
||||
func (a *Appearance) decodePageBlock(d *xml.Decoder, start xml.StartElement) error {
|
||||
for {
|
||||
tok, err := d.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch node := tok.(type) {
|
||||
case xml.StartElement:
|
||||
if err := a.decodeObject(d, node); err != nil {
|
||||
return err
|
||||
}
|
||||
case xml.EndElement:
|
||||
if node.Name.Local == start.Name.Local {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalXML 解析图案单元内容并保留对象顺序
|
||||
@@ -307,85 +214,27 @@ func (a *Appearance) decodePageBlock(d *xml.Decoder, start xml.StartElement) err
|
||||
// 返回: error 错误信息
|
||||
func (p *PatternContent) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
*p = PatternContent{}
|
||||
for {
|
||||
tok, err := d.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch node := tok.(type) {
|
||||
case xml.StartElement:
|
||||
if err := p.decodeObject(d, node); err != nil {
|
||||
return err
|
||||
}
|
||||
case xml.EndElement:
|
||||
if node.Name.Local == start.Name.Local {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return decodeObjectContainer(d, start, p.decodeObject)
|
||||
}
|
||||
|
||||
// decodeObject 解析图案单元内容子对象
|
||||
// 入参: d XML解码器, start 起始节点
|
||||
// 返回: error 错误信息
|
||||
func (p *PatternContent) decodeObject(d *xml.Decoder, start xml.StartElement) error {
|
||||
switch start.Name.Local {
|
||||
case "TextObject":
|
||||
var obj TextObject
|
||||
if err := d.DecodeElement(&obj, &start); err != nil {
|
||||
target := graphicObjectTarget{
|
||||
objects: &p.Objects,
|
||||
text: &p.TextObject,
|
||||
path: &p.PathObject,
|
||||
image: &p.ImageObject,
|
||||
composite: &p.CompositeGraphicUnit,
|
||||
}
|
||||
if decoded, err := decodeGraphicObject(d, start, target); decoded || err != nil {
|
||||
return err
|
||||
}
|
||||
p.TextObject = append(p.TextObject, obj)
|
||||
p.Objects = append(p.Objects, GraphicObject{Type: start.Name.Local, TextObject: obj})
|
||||
case "PathObject":
|
||||
var obj PathObject
|
||||
if err := d.DecodeElement(&obj, &start); err != nil {
|
||||
return err
|
||||
if start.Name.Local == "PageBlock" {
|
||||
return decodeObjectContainer(d, start, p.decodeObject)
|
||||
}
|
||||
p.PathObject = append(p.PathObject, obj)
|
||||
p.Objects = append(p.Objects, GraphicObject{Type: start.Name.Local, PathObject: obj})
|
||||
case "ImageObject":
|
||||
var obj ImageObject
|
||||
if err := d.DecodeElement(&obj, &start); err != nil {
|
||||
return err
|
||||
}
|
||||
p.ImageObject = append(p.ImageObject, obj)
|
||||
p.Objects = append(p.Objects, GraphicObject{Type: start.Name.Local, ImageObject: obj})
|
||||
case "CompositeGraphicUnit", "CompositeObject":
|
||||
var obj CompositeGraphicUnit
|
||||
if err := d.DecodeElement(&obj, &start); err != nil {
|
||||
return err
|
||||
}
|
||||
p.CompositeGraphicUnit = append(p.CompositeGraphicUnit, obj)
|
||||
p.Objects = append(p.Objects, GraphicObject{Type: start.Name.Local, CompositeGraphicUnit: obj})
|
||||
case "PageBlock":
|
||||
return p.decodePageBlock(d, start)
|
||||
default:
|
||||
return d.Skip()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// decodePageBlock 解析图案单元内容页块子对象
|
||||
// 入参: d XML解码器, start 起始节点
|
||||
// 返回: error 错误信息
|
||||
func (p *PatternContent) decodePageBlock(d *xml.Decoder, start xml.StartElement) error {
|
||||
for {
|
||||
tok, err := d.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch node := tok.(type) {
|
||||
case xml.StartElement:
|
||||
if err := p.decodeObject(d, node); err != nil {
|
||||
return err
|
||||
}
|
||||
case xml.EndElement:
|
||||
if node.Name.Local == start.Name.Local {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// attrValue 获取XML属性值
|
||||
|
||||
+6
-1
@@ -23,6 +23,7 @@ type PageContent struct {
|
||||
Area PageArea `xml:"Area"`
|
||||
Template []Template `xml:"Template"`
|
||||
Content Content `xml:"Content"`
|
||||
Actions []Action `xml:"Actions>Action"`
|
||||
}
|
||||
|
||||
// Template 页面模板引用
|
||||
@@ -58,7 +59,7 @@ type GraphicObject struct {
|
||||
|
||||
// Clips 裁剪区域集合
|
||||
type Clips struct {
|
||||
TransFlag bool `xml:"TransFlag,attr"`
|
||||
TransFlag *bool `xml:"TransFlag,attr"`
|
||||
Clip []Clip `xml:"Clip"`
|
||||
}
|
||||
|
||||
@@ -97,6 +98,7 @@ type TextObject struct {
|
||||
CGTransform []CGTransform `xml:"CGTransform"`
|
||||
TextCode []TextCode `xml:"TextCode"`
|
||||
Clips *Clips `xml:"Clips"`
|
||||
Actions []Action `xml:"Actions>Action"`
|
||||
}
|
||||
|
||||
// FillColor 填充颜色
|
||||
@@ -165,6 +167,7 @@ type PathObject struct {
|
||||
FillColor *FillColor `xml:"FillColor"`
|
||||
AbbreviatedData string `xml:"AbbreviatedData"`
|
||||
Clips *Clips `xml:"Clips"`
|
||||
Actions []Action `xml:"Actions>Action"`
|
||||
}
|
||||
|
||||
// StrokeColor 勾边颜色
|
||||
@@ -210,8 +213,10 @@ type ImageObject struct {
|
||||
ID string `xml:"ID,attr"`
|
||||
Boundary string `xml:"Boundary,attr"`
|
||||
ResourceID string `xml:"ResourceID,attr"`
|
||||
ImageMask string `xml:"ImageMask,attr"`
|
||||
CTM string `xml:"CTM,attr"`
|
||||
Alpha *int `xml:"Alpha,attr"`
|
||||
Visible *bool `xml:"Visible,attr"`
|
||||
Clips *Clips `xml:"Clips"`
|
||||
Actions []Action `xml:"Actions>Action"`
|
||||
}
|
||||
|
||||
+330
@@ -0,0 +1,330 @@
|
||||
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ofdgo
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
)
|
||||
|
||||
// parseColorWithAlpha 解析带透明度的颜色
|
||||
// 入参: val 颜色值, alpha 透明度
|
||||
// 返回: color.Color 颜色对象
|
||||
func parseColorWithAlpha(val string, alpha *int) color.Color {
|
||||
parts := strings.Fields(val)
|
||||
if len(parts) >= 3 {
|
||||
r := parseColorComponent(parts[0])
|
||||
g := parseColorComponent(parts[1])
|
||||
b := parseColorComponent(parts[2])
|
||||
a := 255
|
||||
if alpha != nil {
|
||||
a = *alpha
|
||||
}
|
||||
a = clampColor(a)
|
||||
return color.RGBA{
|
||||
R: uint8(clampColor(r) * a / 255),
|
||||
G: uint8(clampColor(g) * a / 255),
|
||||
B: uint8(clampColor(b) * a / 255),
|
||||
A: uint8(a),
|
||||
}
|
||||
}
|
||||
return color.Black
|
||||
}
|
||||
|
||||
// parseColorComponent 解析颜色分量
|
||||
// 入参: s 颜色分量
|
||||
// 返回: int 颜色分量值
|
||||
func parseColorComponent(s string) int {
|
||||
s = strings.TrimSpace(s)
|
||||
if strings.HasPrefix(s, "#") {
|
||||
v, _ := strconv.ParseInt(strings.TrimPrefix(s, "#"), 16, 0)
|
||||
return int(v)
|
||||
}
|
||||
v, _ := strconv.Atoi(s)
|
||||
return v
|
||||
}
|
||||
|
||||
// clampColor 限制颜色分量范围
|
||||
// 入参: v 颜色分量
|
||||
// 返回: int 颜色分量
|
||||
func clampColor(v int) int {
|
||||
if v < 0 {
|
||||
return 0
|
||||
}
|
||||
if v > 255 {
|
||||
return 255
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// mergeAlpha 合并透明度
|
||||
// 入参: colorAlpha 颜色透明度, objectAlpha 对象透明度
|
||||
// 返回: *int 合并后的透明度
|
||||
func mergeAlpha(colorAlpha, objectAlpha *int) *int {
|
||||
if colorAlpha == nil && objectAlpha == nil {
|
||||
return nil
|
||||
}
|
||||
alpha := 255
|
||||
if colorAlpha != nil {
|
||||
alpha = *colorAlpha
|
||||
}
|
||||
if objectAlpha != nil {
|
||||
alpha = alpha * *objectAlpha / 255
|
||||
}
|
||||
alpha = clampColor(alpha)
|
||||
return &alpha
|
||||
}
|
||||
|
||||
// withFillAlpha 合并填充色透明度
|
||||
// 入参: fillColor 填充颜色节点, alpha 对象透明度
|
||||
// 返回: *FillColor 合并后的填充颜色节点
|
||||
func withFillAlpha(fillColor *FillColor, alpha *int) *FillColor {
|
||||
if fillColor == nil || alpha == nil {
|
||||
return fillColor
|
||||
}
|
||||
merged := *fillColor
|
||||
merged.Alpha = mergeAlpha(fillColor.Alpha, alpha)
|
||||
return &merged
|
||||
}
|
||||
|
||||
// withStrokeAlpha 合并勾边色透明度
|
||||
// 入参: strokeColor 勾边颜色节点, alpha 对象透明度
|
||||
// 返回: *StrokeColor 合并后的勾边颜色节点
|
||||
func withStrokeAlpha(strokeColor *StrokeColor, alpha *int) *StrokeColor {
|
||||
if strokeColor == nil || alpha == nil {
|
||||
return strokeColor
|
||||
}
|
||||
merged := *strokeColor
|
||||
merged.Alpha = mergeAlpha(strokeColor.Alpha, alpha)
|
||||
return &merged
|
||||
}
|
||||
|
||||
// colorWithAlpha 合并颜色透明度
|
||||
// 入参: c 颜色对象, alpha 对象透明度
|
||||
// 返回: color.Color 合并后的颜色对象
|
||||
func colorWithAlpha(c color.Color, alpha *int) color.Color {
|
||||
if c == nil || alpha == nil {
|
||||
return c
|
||||
}
|
||||
a := clampColor(*alpha)
|
||||
rgba := colorToRGBA(c)
|
||||
return color.RGBA{
|
||||
R: uint8(int(rgba.R) * a / 255),
|
||||
G: uint8(int(rgba.G) * a / 255),
|
||||
B: uint8(int(rgba.B) * a / 255),
|
||||
A: uint8(int(rgba.A) * a / 255),
|
||||
}
|
||||
}
|
||||
|
||||
// parseFillColor 解析填充颜色
|
||||
// 入参: fillColor 填充颜色节点
|
||||
// 返回: color.Color 颜色对象
|
||||
func parseFillColor(fillColor *FillColor) color.Color {
|
||||
if fillColor == nil {
|
||||
return nil
|
||||
}
|
||||
if fillColor.Pattern != nil {
|
||||
return nil
|
||||
}
|
||||
if fillColor.AxialShd != nil {
|
||||
return parseShdColor(fillColor.AxialShd.Segment, fillColor.Alpha)
|
||||
}
|
||||
if fillColor.RadialShd != nil {
|
||||
return parseShdColor(fillColor.RadialShd.Segment, fillColor.Alpha)
|
||||
}
|
||||
if strings.TrimSpace(fillColor.Value) != "" {
|
||||
return parseColorWithAlpha(fillColor.Value, fillColor.Alpha)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseFillPaint 解析填充画刷
|
||||
// 入参: fillColor 填充颜色节点, x X坐标, y Y坐标, pageH 页面高度, originX 原点X坐标, originY 原点Y坐标
|
||||
// 返回: any 填充画刷
|
||||
func parseFillPaint(fillColor *FillColor, x, y, pageH, originX, originY float64) any {
|
||||
if fillColor == nil {
|
||||
return nil
|
||||
}
|
||||
if fillColor.Pattern != nil {
|
||||
return nil
|
||||
}
|
||||
if gradient := parseAxialShdGradient(fillColor.AxialShd, fillColor.Alpha, x, y, pageH, originX, originY); gradient != nil {
|
||||
return gradient
|
||||
}
|
||||
if gradient := parseRadialShdGradient(fillColor.RadialShd, fillColor.Alpha, x, y, pageH, originX, originY); gradient != nil {
|
||||
return gradient
|
||||
}
|
||||
if fillColor.AxialShd != nil {
|
||||
return parseShdColor(fillColor.AxialShd.Segment, fillColor.Alpha)
|
||||
}
|
||||
if fillColor.RadialShd != nil {
|
||||
return parseShdColor(fillColor.RadialShd.Segment, fillColor.Alpha)
|
||||
}
|
||||
if strings.TrimSpace(fillColor.Value) != "" {
|
||||
return parseColorWithAlpha(fillColor.Value, fillColor.Alpha)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseStrokeColor 解析勾边颜色
|
||||
// 入参: strokeColor 勾边颜色节点
|
||||
// 返回: color.Color 颜色对象
|
||||
func parseStrokeColor(strokeColor *StrokeColor) color.Color {
|
||||
if strokeColor == nil {
|
||||
return nil
|
||||
}
|
||||
if strokeColor.AxialShd != nil {
|
||||
return parseShdColor(strokeColor.AxialShd.Segment, strokeColor.Alpha)
|
||||
}
|
||||
if strokeColor.RadialShd != nil {
|
||||
return parseShdColor(strokeColor.RadialShd.Segment, strokeColor.Alpha)
|
||||
}
|
||||
if strings.TrimSpace(strokeColor.Value) != "" {
|
||||
return parseColorWithAlpha(strokeColor.Value, strokeColor.Alpha)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseStrokePaint 解析勾边画刷
|
||||
// 入参: strokeColor 勾边颜色节点, x X坐标, y Y坐标, pageH 页面高度, originX 原点X坐标, originY 原点Y坐标
|
||||
// 返回: any 勾边画刷
|
||||
func parseStrokePaint(strokeColor *StrokeColor, x, y, pageH, originX, originY float64) any {
|
||||
if strokeColor == nil {
|
||||
return nil
|
||||
}
|
||||
if gradient := parseAxialShdGradient(strokeColor.AxialShd, strokeColor.Alpha, x, y, pageH, originX, originY); gradient != nil {
|
||||
return gradient
|
||||
}
|
||||
if gradient := parseRadialShdGradient(strokeColor.RadialShd, strokeColor.Alpha, x, y, pageH, originX, originY); gradient != nil {
|
||||
return gradient
|
||||
}
|
||||
if strokeColor.AxialShd != nil {
|
||||
return parseShdColor(strokeColor.AxialShd.Segment, strokeColor.Alpha)
|
||||
}
|
||||
if strokeColor.RadialShd != nil {
|
||||
return parseShdColor(strokeColor.RadialShd.Segment, strokeColor.Alpha)
|
||||
}
|
||||
if strings.TrimSpace(strokeColor.Value) != "" {
|
||||
return parseColorWithAlpha(strokeColor.Value, strokeColor.Alpha)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// patternColor 获取图案单元默认颜色
|
||||
// 入参: fillColor 填充颜色节点
|
||||
// 返回: color.Color 默认颜色
|
||||
func patternColor(fillColor *FillColor) color.Color {
|
||||
if fillColor == nil || strings.TrimSpace(fillColor.Value) == "" {
|
||||
return nil
|
||||
}
|
||||
return parseColorWithAlpha(fillColor.Value, fillColor.Alpha)
|
||||
}
|
||||
|
||||
// parseShdColor 解析渐变颜色
|
||||
// 入参: segments 渐变分段, alpha 透明度
|
||||
// 返回: color.Color 颜色对象
|
||||
func parseShdColor(segments []ShdSegment, alpha *int) color.Color {
|
||||
for _, segment := range segments {
|
||||
if strings.TrimSpace(segment.Color.Value) == "" {
|
||||
continue
|
||||
}
|
||||
segmentAlpha := alpha
|
||||
if segmentAlpha == nil {
|
||||
segmentAlpha = segment.Color.Alpha
|
||||
}
|
||||
return parseColorWithAlpha(segment.Color.Value, segmentAlpha)
|
||||
}
|
||||
return color.Black
|
||||
}
|
||||
|
||||
// parseShdSegments 解析渐变分段
|
||||
// 入参: segments 渐变分段, alpha 透明度
|
||||
// 返回: canvas.Grad 渐变分段
|
||||
func parseShdSegments(segments []ShdSegment, alpha *int) canvas.Grad {
|
||||
gradient := canvas.NewGradient()
|
||||
for _, segment := range segments {
|
||||
if strings.TrimSpace(segment.Color.Value) == "" {
|
||||
continue
|
||||
}
|
||||
segmentAlpha := alpha
|
||||
if segmentAlpha == nil {
|
||||
segmentAlpha = segment.Color.Alpha
|
||||
}
|
||||
gradient.Add(segment.Position, colorToRGBA(parseColorWithAlpha(segment.Color.Value, segmentAlpha)))
|
||||
}
|
||||
if len(gradient) == 0 {
|
||||
return nil
|
||||
}
|
||||
return gradient
|
||||
}
|
||||
|
||||
// parseAxialShdGradient 解析轴向渐变
|
||||
// 入参: axialShd 轴向渐变节点, alpha 透明度, x X坐标, y Y坐标, pageH 页面高度, originX 原点X坐标, originY 原点Y坐标
|
||||
// 返回: canvas.Gradient 渐变对象
|
||||
func parseAxialShdGradient(axialShd *AxialShd, alpha *int, x, y, pageH, originX, originY float64) canvas.Gradient {
|
||||
if axialShd == nil {
|
||||
return nil
|
||||
}
|
||||
start := parseFloats(axialShd.StartPoint)
|
||||
end := parseFloats(axialShd.EndPoint)
|
||||
if len(start) < 2 || len(end) < 2 {
|
||||
return nil
|
||||
}
|
||||
gradient := parseShdSegments(axialShd.Segment, alpha)
|
||||
if gradient == nil {
|
||||
return nil
|
||||
}
|
||||
startPoint := canvas.Point{X: x + start[0] - originX, Y: pageH - (y + start[1]) - originY}
|
||||
endPoint := canvas.Point{X: x + end[0] - originX, Y: pageH - (y + end[1]) - originY}
|
||||
if startPoint.Equals(endPoint) {
|
||||
return nil
|
||||
}
|
||||
return gradient.ToLinear(startPoint, endPoint)
|
||||
}
|
||||
|
||||
// parseRadialShdGradient 解析径向渐变
|
||||
// 入参: radialShd 径向渐变节点, alpha 透明度, x X坐标, y Y坐标, pageH 页面高度, originX 原点X坐标, originY 原点Y坐标
|
||||
// 返回: canvas.Gradient 渐变对象
|
||||
func parseRadialShdGradient(radialShd *RadialShd, alpha *int, x, y, pageH, originX, originY float64) canvas.Gradient {
|
||||
if radialShd == nil || radialShd.EndRadius <= 0 {
|
||||
return nil
|
||||
}
|
||||
start := parseFloats(radialShd.StartPoint)
|
||||
end := parseFloats(radialShd.EndPoint)
|
||||
if len(start) < 2 || len(end) < 2 {
|
||||
return nil
|
||||
}
|
||||
gradient := parseShdSegments(radialShd.Segment, alpha)
|
||||
if gradient == nil {
|
||||
return nil
|
||||
}
|
||||
startPoint := canvas.Point{X: x + start[0] - originX, Y: pageH - (y + start[1]) - originY}
|
||||
endPoint := canvas.Point{X: x + end[0] - originX, Y: pageH - (y + end[1]) - originY}
|
||||
return gradient.ToRadial(startPoint, radialShd.StartRadius, endPoint, radialShd.EndRadius)
|
||||
}
|
||||
|
||||
// colorToRGBA 转换颜色对象
|
||||
// 入参: c 颜色对象
|
||||
// 返回: color.RGBA RGBA颜色
|
||||
func colorToRGBA(c color.Color) color.RGBA {
|
||||
if rgba, ok := c.(color.RGBA); ok {
|
||||
return rgba
|
||||
}
|
||||
r, g, b, a := c.RGBA()
|
||||
return color.RGBA{R: uint8(r >> 8), G: uint8(g >> 8), B: uint8(b >> 8), A: uint8(a >> 8)}
|
||||
}
|
||||
+95
-14
@@ -158,7 +158,6 @@ func (r *Reader) Doc() (*Document, error) {
|
||||
if doc.Signatures == "" {
|
||||
doc.Signatures = docAttr.Signatures
|
||||
}
|
||||
r.OFD.DocBody[0].DocInfo.DocID = "loaded"
|
||||
if doc.CommonData.DocumentRes != "" {
|
||||
r.loadRes(doc.CommonData.DocumentRes)
|
||||
}
|
||||
@@ -177,7 +176,7 @@ func (r *Reader) loadRes(resPath string) {
|
||||
if resPath == "" {
|
||||
return
|
||||
}
|
||||
fullPath := path.Join(r.RootDir, resPath)
|
||||
fullPath := r.ResPath(resPath)
|
||||
data, err := r.readFile(fullPath)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -236,7 +235,7 @@ func resolveResourcePath(resPath, baseLoc, filePath string) string {
|
||||
// 入参: page 页面对象
|
||||
// 返回: *PageContent 页面内容, error 错误信息
|
||||
func (r *Reader) PageContent(page Page) (*PageContent, error) {
|
||||
fullPath := path.Join(r.RootDir, page.BaseLoc)
|
||||
fullPath := r.ResPath(page.BaseLoc)
|
||||
data, err := r.readFile(fullPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -261,7 +260,9 @@ func (r *Reader) ResPath(resLink string) string {
|
||||
if resLink == "" {
|
||||
return ""
|
||||
}
|
||||
resLink = strings.TrimPrefix(resLink, "/")
|
||||
if strings.HasPrefix(resLink, "/") {
|
||||
return cleanPackagePath(resLink)
|
||||
}
|
||||
resLink = path.Clean(resLink)
|
||||
rootDir := strings.TrimPrefix(strings.ReplaceAll(r.RootDir, "\\", "/"), "/")
|
||||
if rootDir != "" && (resLink == rootDir || strings.HasPrefix(resLink, rootDir+"/")) {
|
||||
@@ -278,17 +279,19 @@ func (r *Reader) ResData(resLink string) ([]byte, error) {
|
||||
return r.readFile(fullPath)
|
||||
}
|
||||
|
||||
// DocRoots 获取所有文档根路径
|
||||
// 返回: []string 路径列表
|
||||
func (r *Reader) DocRoots() []string {
|
||||
var roots []string
|
||||
if r.OFD == nil {
|
||||
return roots
|
||||
// readDocumentPart 读取文档关联文件
|
||||
// 入参: loc 文件位置, value 文档结构
|
||||
// 返回: string 文件路径, error 错误信息
|
||||
func (r *Reader) readDocumentPart(loc string, value any) (string, error) {
|
||||
fullPath := r.ResPath(strings.TrimSpace(loc))
|
||||
data, err := r.readFile(fullPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, body := range r.OFD.DocBody {
|
||||
roots = append(roots, body.DocRoot)
|
||||
if err := xml.Unmarshal(data, value); err != nil {
|
||||
return "", fmt.Errorf("failed to unmarshal %s: %w", path.Base(fullPath), err)
|
||||
}
|
||||
return roots
|
||||
return fullPath, nil
|
||||
}
|
||||
|
||||
// Version 获取OFD版本号
|
||||
@@ -338,6 +341,26 @@ func (r *Reader) Outlines() ([]OutlineElem, error) {
|
||||
return doc.Outlines.OutlineElem, nil
|
||||
}
|
||||
|
||||
// Actions 获取文档动作
|
||||
// 返回: []Action 动作列表, error 错误信息
|
||||
func (r *Reader) Actions() ([]Action, error) {
|
||||
doc, err := r.Doc()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return doc.Actions, nil
|
||||
}
|
||||
|
||||
// Bookmarks 获取文档书签
|
||||
// 返回: []Bookmark 书签列表, error 错误信息
|
||||
func (r *Reader) Bookmarks() ([]Bookmark, error) {
|
||||
doc, err := r.Doc()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return doc.Bookmarks.Bookmark, nil
|
||||
}
|
||||
|
||||
// Attachments 获取附件列表
|
||||
// 返回: []Attachment 附件列表, error 错误信息
|
||||
func (r *Reader) Attachments() ([]Attachment, error) {
|
||||
@@ -345,9 +368,50 @@ func (r *Reader) Attachments() ([]Attachment, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.TrimSpace(doc.Attachments.Path) != "" && doc.Attachments.Attachment == nil {
|
||||
var attachments Attachments
|
||||
partPath, err := r.readDocumentPart(doc.Attachments.Path, &attachments)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range attachments.Attachment {
|
||||
attachment := &attachments.Attachment[i]
|
||||
attachment.FileLoc = resolveResourcePath(partPath, "", attachment.FileLoc)
|
||||
}
|
||||
if attachments.Attachment == nil {
|
||||
attachments.Attachment = []Attachment{}
|
||||
}
|
||||
doc.Attachments.Attachment = attachments.Attachment
|
||||
}
|
||||
return doc.Attachments.Attachment, nil
|
||||
}
|
||||
|
||||
// CustomTags 获取自定义标引
|
||||
// 返回: []CustomTag 自定义标引列表, error 错误信息
|
||||
func (r *Reader) CustomTags() ([]CustomTag, error) {
|
||||
doc, err := r.Doc()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.TrimSpace(doc.CustomTags.Path) != "" && doc.CustomTags.CustomTag == nil {
|
||||
var customTags CustomTags
|
||||
partPath, err := r.readDocumentPart(doc.CustomTags.Path, &customTags)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range customTags.CustomTag {
|
||||
tag := &customTags.CustomTag[i]
|
||||
tag.SchemaLoc = resolveResourcePath(partPath, "", tag.SchemaLoc)
|
||||
tag.FileLoc = resolveResourcePath(partPath, "", tag.FileLoc)
|
||||
}
|
||||
if customTags.CustomTag == nil {
|
||||
customTags.CustomTag = []CustomTag{}
|
||||
}
|
||||
doc.CustomTags.CustomTag = customTags.CustomTag
|
||||
}
|
||||
return doc.CustomTags.CustomTag, nil
|
||||
}
|
||||
|
||||
// CustomDatas 获取自定义数据
|
||||
// 返回: []CustomData 自定义数据列表, error 错误信息
|
||||
func (r *Reader) CustomDatas() ([]CustomData, error) {
|
||||
@@ -361,12 +425,29 @@ func (r *Reader) CustomDatas() ([]CustomData, error) {
|
||||
return info.CustomDatas.CustomData, nil
|
||||
}
|
||||
|
||||
// Extensions 获取分列扩展项
|
||||
// Extensions 获取扩展项
|
||||
// 返回: []Extension 扩展项列表, error 错误信息
|
||||
func (r *Reader) Extensions() ([]Extension, error) {
|
||||
doc, err := r.Doc()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.TrimSpace(doc.Extensions.Path) != "" && doc.Extensions.Extension == nil {
|
||||
var extensions Extensions
|
||||
partPath, err := r.readDocumentPart(doc.Extensions.Path, &extensions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range extensions.Extension {
|
||||
extension := &extensions.Extension[i]
|
||||
for j := range extension.ExtendData {
|
||||
extension.ExtendData[j] = resolveResourcePath(partPath, "", extension.ExtendData[j])
|
||||
}
|
||||
}
|
||||
if extensions.Extension == nil {
|
||||
extensions.Extension = []Extension{}
|
||||
}
|
||||
doc.Extensions.Extension = extensions.Extension
|
||||
}
|
||||
return doc.Extensions.Extension, nil
|
||||
}
|
||||
|
||||
+23
-1530
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,386 @@
|
||||
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ofdgo
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"image"
|
||||
"image/color"
|
||||
"io"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
canvasimage "github.com/tdewolff/canvas/image"
|
||||
"github.com/tdewolff/canvas/renderers/rasterizer"
|
||||
"golang.org/x/image/draw"
|
||||
)
|
||||
|
||||
// renderImage 渲染图片
|
||||
// 入参: ctx 画布上下文, obj 图片对象, pageH 页面高度, parentCTM 父级CTM, boundaryInCTM 边界是否参与父级CTM, parentClip 父级裁剪路径
|
||||
func (r *Renderer) renderImage(ctx *canvas.Context, obj ImageObject, pageH float64, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) {
|
||||
if obj.Visible != nil && !*obj.Visible {
|
||||
return
|
||||
}
|
||||
resPath, ok := r.Reader.ResMap[obj.ResourceID]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
img, err := r.decodeImageResource(resPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
box, _ := ParseBox(obj.Boundary)
|
||||
if maskPath, ok := r.Reader.ResMap[obj.ImageMask]; ok {
|
||||
if mask, err := r.decodeImageResource(maskPath); err == nil {
|
||||
img = imageWithMask(img, mask)
|
||||
}
|
||||
}
|
||||
img = imageWithAlpha(img, obj.Alpha)
|
||||
imgBounds := img.Bounds()
|
||||
imgW, imgH := float64(imgBounds.Dx()), float64(imgBounds.Dy())
|
||||
if imgW <= 0 || imgH <= 0 {
|
||||
return
|
||||
}
|
||||
ctm := NewMatrix(obj.CTM)
|
||||
if obj.CTM == "" {
|
||||
ctm = Matrix{a: box.W, d: box.H}
|
||||
}
|
||||
objectCTM := ctm
|
||||
if parentCTM != nil {
|
||||
objectCTM = parentCTM.Multiply(ctm)
|
||||
}
|
||||
var m canvas.Matrix
|
||||
if boundaryInCTM && parentCTM != nil {
|
||||
x0 := box.X + ctm.c + ctm.e
|
||||
y0 := box.Y + ctm.d + ctm.f
|
||||
m = canvas.Matrix{
|
||||
{(parentCTM.a*ctm.a + parentCTM.c*ctm.b) / imgW, -(parentCTM.a*ctm.c + parentCTM.c*ctm.d) / imgH, parentCTM.a*x0 + parentCTM.c*y0 + parentCTM.e},
|
||||
{-(parentCTM.b*ctm.a + parentCTM.d*ctm.b) / imgW, (parentCTM.b*ctm.c + parentCTM.d*ctm.d) / imgH, pageH - (parentCTM.b*x0 + parentCTM.d*y0 + parentCTM.f)},
|
||||
}
|
||||
} else {
|
||||
if parentCTM != nil {
|
||||
ctm = parentCTM.Multiply(ctm)
|
||||
}
|
||||
m = canvas.Matrix{
|
||||
{ctm.a / imgW, -ctm.c / imgH, box.X + ctm.c + ctm.e},
|
||||
{-ctm.b / imgW, ctm.d / imgH, pageH - box.Y - ctm.d - ctm.f},
|
||||
}
|
||||
}
|
||||
clipPath := intersectClipPath(parentClip, r.buildClipPath(obj.Clips, pageH, box.X, box.Y, objectCTM))
|
||||
img = imageWithClip(img, clipPath, m)
|
||||
img, pad := imageWithTransparentEdge(img)
|
||||
if pad > 0 {
|
||||
p := float64(pad)
|
||||
m[0][2] -= m[0][0]*p + m[0][1]*p
|
||||
m[1][2] -= m[1][0]*p + m[1][1]*p
|
||||
}
|
||||
ctx.RenderImage(img, ctx.CoordSystemView().Mul(ctx.View()).Mul(m))
|
||||
}
|
||||
|
||||
// imageWithMask 应用图片蒙版
|
||||
// 入参: img 图片对象, mask 蒙版图片
|
||||
// 返回: image.Image 蒙版处理后的图片对象
|
||||
func imageWithMask(img, mask image.Image) image.Image {
|
||||
if img == nil || mask == nil {
|
||||
return img
|
||||
}
|
||||
bounds := img.Bounds()
|
||||
if bounds.Empty() || mask.Bounds().Empty() {
|
||||
return img
|
||||
}
|
||||
source := imagePixelSource(img)
|
||||
maskSource := imagePixelSource(mask)
|
||||
maskBounds := maskSource.Bounds()
|
||||
var opacity image.Image = maskSource
|
||||
if bounds.Size() != maskBounds.Size() {
|
||||
resized := image.NewGray(bounds)
|
||||
draw.CatmullRom.Scale(resized, bounds, maskSource, maskBounds, draw.Src, nil)
|
||||
opacity = resized
|
||||
maskBounds = bounds
|
||||
}
|
||||
out := image.NewNRGBA(bounds)
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||
mx := maskBounds.Min.X + x - bounds.Min.X
|
||||
my := maskBounds.Min.Y + y - bounds.Min.Y
|
||||
a := color.GrayModel.Convert(opacity.At(mx, my)).(color.Gray).Y
|
||||
c := imageNRGBAAt(source, x, y)
|
||||
c.A = uint8(int(c.A) * int(a) / 255)
|
||||
out.SetNRGBA(x, y, c)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// imageWithClip 应用图片裁剪区域
|
||||
// 入参: img 图片对象, clipPath 裁剪路径, m 图片变换矩阵
|
||||
// 返回: image.Image 裁剪后的图片对象
|
||||
func imageWithClip(img image.Image, clipPath *canvas.Path, m canvas.Matrix) image.Image {
|
||||
if img == nil || clipPath == nil || m.Det() == 0 {
|
||||
return img
|
||||
}
|
||||
bounds := img.Bounds()
|
||||
w, h := bounds.Dx(), bounds.Dy()
|
||||
if w == 0 || h == 0 {
|
||||
return img
|
||||
}
|
||||
p0 := m.Dot(canvas.Point{})
|
||||
p1 := m.Dot(canvas.Point{X: float64(w)})
|
||||
p2 := m.Dot(canvas.Point{X: float64(w), Y: float64(h)})
|
||||
p3 := m.Dot(canvas.Point{Y: float64(h)})
|
||||
imagePath := &canvas.Path{}
|
||||
imagePath.MoveTo(p0.X, p0.Y)
|
||||
imagePath.LineTo(p1.X, p1.Y)
|
||||
imagePath.LineTo(p2.X, p2.Y)
|
||||
imagePath.LineTo(p3.X, p3.Y)
|
||||
imagePath.Close()
|
||||
if rect, ok := rectangularPath(clipPath); ok {
|
||||
if rect.Contains(imagePath.FastBounds()) {
|
||||
return img
|
||||
}
|
||||
} else if clipPath.Contains(imagePath) {
|
||||
return img
|
||||
}
|
||||
clip := clipPath.Copy().Transform(m.Inv())
|
||||
clipCanvas := canvas.New(float64(w), float64(h))
|
||||
ctx := canvas.NewContext(clipCanvas)
|
||||
ctx.SetFillColor(canvas.White)
|
||||
ctx.SetStrokeColor(canvas.Transparent)
|
||||
ctx.DrawPath(0, 0, clip)
|
||||
mask := rasterizer.Draw(clipCanvas, canvas.DPMM(1), canvas.DefaultColorSpace)
|
||||
opaque := true
|
||||
for y := 0; y < h && opaque; y++ {
|
||||
for x := 0; x < w; x++ {
|
||||
if mask.RGBAAt(x, y).A != 255 {
|
||||
opaque = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if opaque {
|
||||
return img
|
||||
}
|
||||
source := imagePixelSource(img)
|
||||
out := image.NewNRGBA(bounds)
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||
c := imageNRGBAAt(source, x, y)
|
||||
a := mask.RGBAAt(x-bounds.Min.X, y-bounds.Min.Y).A
|
||||
c.A = uint8(int(c.A) * int(a) / 255)
|
||||
out.SetNRGBA(x, y, c)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// decodeImageResource 解码图片资源
|
||||
// 入参: resPath 图片资源路径
|
||||
// 返回: image.Image 图片对象, error 错误信息
|
||||
func (r *Renderer) decodeImageResource(resPath string) (image.Image, error) {
|
||||
rc, err := r.Reader.openFile(r.Reader.ResPath(resPath))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rc.Close()
|
||||
reader := bufio.NewReaderSize(rc, 8)
|
||||
header, _ := reader.Peek(8)
|
||||
if isJPEGData(header) {
|
||||
img, err := canvasimage.NewJPEGImage(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r.decodeImages {
|
||||
return img.Image()
|
||||
}
|
||||
return img, nil
|
||||
}
|
||||
if isPNGData(header) {
|
||||
img, err := canvasimage.NewPNGImage(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r.decodeImages {
|
||||
return img.Image()
|
||||
}
|
||||
return img, nil
|
||||
}
|
||||
data, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
img, _, err := decodeImageData(data)
|
||||
return img, err
|
||||
}
|
||||
|
||||
// imageWithAlpha 合并图片透明度
|
||||
// 入参: img 图片对象, alpha 对象透明度
|
||||
// 返回: image.Image 合并后的图片对象
|
||||
func imageWithAlpha(img image.Image, alpha *int) image.Image {
|
||||
if img == nil || alpha == nil {
|
||||
return img
|
||||
}
|
||||
a := clampColor(*alpha)
|
||||
if a == 255 {
|
||||
return img
|
||||
}
|
||||
bounds := img.Bounds()
|
||||
out := image.NewNRGBA(bounds)
|
||||
source := imagePixelSource(img)
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||
c := imageNRGBAAt(source, x, y)
|
||||
c.A = uint8(int(c.A) * a / 255)
|
||||
out.SetNRGBA(x, y, c)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// imageWithTransparentEdge 补齐透明图片边缘颜色
|
||||
// 入参: img 图片对象
|
||||
// 返回: image.Image 补齐后的图片对象, int 补齐像素数
|
||||
func imageWithTransparentEdge(img image.Image) (image.Image, int) {
|
||||
if img == nil {
|
||||
return img, 0
|
||||
}
|
||||
bounds := img.Bounds()
|
||||
w, h := bounds.Dx(), bounds.Dy()
|
||||
if w == 0 || h == 0 {
|
||||
return img, 0
|
||||
}
|
||||
if src, ok := img.(*canvasimage.Image); ok && src.Mimetype == "image/jpeg" && src.Mask == nil {
|
||||
return img, 0
|
||||
}
|
||||
source := imagePixelSource(img)
|
||||
if opaque, ok := source.(interface{ Opaque() bool }); ok && opaque.Opaque() {
|
||||
return img, 0
|
||||
}
|
||||
hasZero, hasVisible := false, false
|
||||
src, ok := source.(*image.NRGBA)
|
||||
if ok {
|
||||
srcBounds := src.Bounds()
|
||||
for y := srcBounds.Min.Y; y < srcBounds.Max.Y; y++ {
|
||||
offset := src.PixOffset(srcBounds.Min.X, y) + 3
|
||||
for x := 0; x < w; x++ {
|
||||
if src.Pix[offset] == 0 {
|
||||
hasZero = true
|
||||
} else {
|
||||
hasVisible = true
|
||||
}
|
||||
offset += 4
|
||||
}
|
||||
}
|
||||
} else {
|
||||
src = image.NewNRGBA(image.Rect(0, 0, w, h))
|
||||
for y := 0; y < h; y++ {
|
||||
for x := 0; x < w; x++ {
|
||||
c := imageNRGBAAt(source, bounds.Min.X+x, bounds.Min.Y+y)
|
||||
src.SetNRGBA(x, y, c)
|
||||
if c.A == 0 {
|
||||
hasZero = true
|
||||
} else {
|
||||
hasVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !hasZero || !hasVisible {
|
||||
return img, 0
|
||||
}
|
||||
srcBounds := src.Bounds()
|
||||
out := image.NewNRGBA(image.Rect(0, 0, w+2, h+2))
|
||||
for y := 0; y < h; y++ {
|
||||
for x := 0; x < w; x++ {
|
||||
sx, sy := srcBounds.Min.X+x, srcBounds.Min.Y+y
|
||||
c := src.NRGBAAt(sx, sy)
|
||||
if c.A == 0 {
|
||||
if edge, ok := transparentEdgeColor(src, sx, sy); ok {
|
||||
c = edge
|
||||
}
|
||||
}
|
||||
out.SetNRGBA(x+1, y+1, c)
|
||||
}
|
||||
}
|
||||
for x := 0; x < w; x++ {
|
||||
out.SetNRGBA(x+1, 0, transparentPaddingColor(out.NRGBAAt(x+1, 1)))
|
||||
out.SetNRGBA(x+1, h+1, transparentPaddingColor(out.NRGBAAt(x+1, h)))
|
||||
}
|
||||
for y := 0; y < h; y++ {
|
||||
out.SetNRGBA(0, y+1, transparentPaddingColor(out.NRGBAAt(1, y+1)))
|
||||
out.SetNRGBA(w+1, y+1, transparentPaddingColor(out.NRGBAAt(w, y+1)))
|
||||
}
|
||||
out.SetNRGBA(0, 0, transparentPaddingColor(out.NRGBAAt(1, 1)))
|
||||
out.SetNRGBA(w+1, 0, transparentPaddingColor(out.NRGBAAt(w, 1)))
|
||||
out.SetNRGBA(0, h+1, transparentPaddingColor(out.NRGBAAt(1, h)))
|
||||
out.SetNRGBA(w+1, h+1, transparentPaddingColor(out.NRGBAAt(w, h)))
|
||||
return out, 1
|
||||
}
|
||||
|
||||
// imagePixelSource 获取图片像素源
|
||||
// 入参: img 图片对象
|
||||
// 返回: image.Image 图片像素源
|
||||
func imagePixelSource(img image.Image) image.Image {
|
||||
if src, ok := img.(*canvasimage.Image); ok {
|
||||
if decoded, err := src.Image(); err == nil {
|
||||
return decoded
|
||||
}
|
||||
}
|
||||
return img
|
||||
}
|
||||
|
||||
// imageNRGBAAt 获取图片NRGBA像素
|
||||
// 入参: img 图片对象, x X坐标, y Y坐标
|
||||
// 返回: color.NRGBA NRGBA像素
|
||||
func imageNRGBAAt(img image.Image, x, y int) color.NRGBA {
|
||||
if src, ok := img.(*image.NRGBA); ok {
|
||||
return src.NRGBAAt(x, y)
|
||||
}
|
||||
return color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA)
|
||||
}
|
||||
|
||||
// transparentEdgeColor 获取透明像素相邻的可见颜色
|
||||
// 入参: img 图片对象, x X坐标, y Y坐标
|
||||
// 返回: color.NRGBA 颜色, bool 是否存在
|
||||
func transparentEdgeColor(img *image.NRGBA, x, y int) (color.NRGBA, bool) {
|
||||
bounds := img.Bounds()
|
||||
var best color.NRGBA
|
||||
for dy := -1; dy <= 1; dy++ {
|
||||
for dx := -1; dx <= 1; dx++ {
|
||||
if dx == 0 && dy == 0 {
|
||||
continue
|
||||
}
|
||||
nx, ny := x+dx, y+dy
|
||||
if nx < bounds.Min.X || nx >= bounds.Max.X || ny < bounds.Min.Y || ny >= bounds.Max.Y {
|
||||
continue
|
||||
}
|
||||
c := img.NRGBAAt(nx, ny)
|
||||
if c.A > best.A {
|
||||
best = c
|
||||
}
|
||||
}
|
||||
}
|
||||
if best.A == 0 {
|
||||
return color.NRGBA{}, false
|
||||
}
|
||||
best.A = 1
|
||||
return best, true
|
||||
}
|
||||
|
||||
// transparentPaddingColor 获取透明补齐颜色
|
||||
// 入参: c 边缘颜色
|
||||
// 返回: color.NRGBA 补齐颜色
|
||||
func transparentPaddingColor(c color.NRGBA) color.NRGBA {
|
||||
c.A = 1
|
||||
return c
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ofdgo
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
)
|
||||
|
||||
// renderAnnotations 渲染页面注释外观
|
||||
// 入参: ctx 画布上下文, pageID 页面ID, pageH 页面高度
|
||||
func (r *Renderer) renderAnnotations(ctx *canvas.Context, pageID string, pageH float64) {
|
||||
for _, annot := range r.Reader.Annots[pageID] {
|
||||
if len(annot.Appearance.Objects) == 0 {
|
||||
continue
|
||||
}
|
||||
box, _ := ParseBox(annot.Appearance.Boundary)
|
||||
ctm := Matrix{a: 1, d: 1, e: box.X, f: box.Y}
|
||||
for _, obj := range annot.Appearance.Objects {
|
||||
r.renderObject(ctx, obj, pageH, nil, nil, 0, &ctm, false, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// renderTemplate 渲染模板
|
||||
// 入参: ctx 画布上下文, templateID 模板ID, pageH 页面高度
|
||||
func (r *Renderer) renderTemplate(ctx *canvas.Context, templateID string, pageH float64) {
|
||||
tplContent := r.templatePageCache[templateID]
|
||||
if tplContent == nil {
|
||||
var tplPage *TemplatePage
|
||||
for i := range r.Reader.doc.CommonData.TemplatePage {
|
||||
if r.Reader.doc.CommonData.TemplatePage[i].ID == templateID {
|
||||
tplPage = &r.Reader.doc.CommonData.TemplatePage[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if tplPage == nil {
|
||||
return
|
||||
}
|
||||
var err error
|
||||
tplContent, err = r.Reader.PageContent(Page{BaseLoc: tplPage.BaseLoc})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
r.templatePageCache[templateID] = tplContent
|
||||
}
|
||||
if tplContent.Content.Layer != nil {
|
||||
for _, layer := range tplContent.Content.Layer {
|
||||
r.renderLayer(ctx, layer, pageH, nil, nil, 0, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// renderLayer 渲染图层
|
||||
// 入参: ctx 画布上下文, layer 图层对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽, parentCTM 父级CTM
|
||||
func (r *Renderer) renderLayer(ctx *canvas.Context, layer Layer, pageH float64, defaultFill, defaultStroke color.Color, defaultLW float64, parentCTM *Matrix) {
|
||||
defaultFill, defaultStroke, defaultLW = r.drawParamDefaults(layer.DrawParam, defaultFill, defaultStroke, defaultLW)
|
||||
if len(layer.Objects) > 0 {
|
||||
for _, obj := range layer.Objects {
|
||||
r.renderObject(ctx, obj, pageH, defaultFill, defaultStroke, defaultLW, parentCTM, false, nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
for _, textObj := range layer.TextObject {
|
||||
r.renderText(ctx, textObj, pageH, defaultFill, defaultStroke, parentCTM, false, nil)
|
||||
}
|
||||
for _, pathObj := range layer.PathObject {
|
||||
r.renderPath(ctx, pathObj, pageH, defaultFill, defaultStroke, defaultLW, parentCTM, false, nil)
|
||||
}
|
||||
for _, imgObj := range layer.ImageObject {
|
||||
r.renderImage(ctx, imgObj, pageH, parentCTM, false, nil)
|
||||
}
|
||||
for _, cgu := range layer.CompositeGraphicUnit {
|
||||
r.renderCompositeGraphicUnit(ctx, cgu, pageH, defaultFill, defaultStroke, defaultLW, parentCTM, false, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// renderCompositeGraphicUnit 渲染复合图元
|
||||
// 入参: ctx 画布上下文, cgu 复合图元对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽, parentCTM 父级CTM, boundaryInCTM 边界是否参与CTM变换, parentClip 父级裁剪路径
|
||||
func (r *Renderer) renderCompositeGraphicUnit(ctx *canvas.Context, cgu CompositeGraphicUnit, pageH float64, defaultFill, defaultStroke color.Color, defaultLW float64, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) {
|
||||
if cgu.Visible != nil && !*cgu.Visible {
|
||||
return
|
||||
}
|
||||
ctx.Push()
|
||||
currentCTM := NewMatrix(cgu.CTM)
|
||||
if parentCTM != nil {
|
||||
currentCTM = parentCTM.Multiply(currentCTM)
|
||||
}
|
||||
box, _ := ParseBox(cgu.Boundary)
|
||||
clipPath := intersectClipPath(parentClip, r.buildClipPath(cgu.Clips, pageH, box.X, box.Y, currentCTM))
|
||||
if cgu.ResourceID != "" {
|
||||
if ref, ok := r.CompositeGraphicUnits[cgu.ResourceID]; ok {
|
||||
refCopy := *ref
|
||||
refCopy.Alpha = mergeAlpha(refCopy.Alpha, cgu.Alpha)
|
||||
r.renderCompositeGraphicUnit(ctx, refCopy, pageH, defaultFill, defaultStroke, defaultLW, ¤tCTM, true, clipPath)
|
||||
}
|
||||
}
|
||||
defaultFill, defaultStroke, defaultLW = r.drawParamDefaults(cgu.DrawParam, defaultFill, defaultStroke, defaultLW)
|
||||
if len(cgu.Objects) > 0 {
|
||||
for _, obj := range cgu.Objects {
|
||||
obj = mergeGraphicObjectAlpha(obj, cgu.Alpha)
|
||||
r.renderObject(ctx, obj, pageH, defaultFill, defaultStroke, defaultLW, ¤tCTM, boundaryInCTM, clipPath)
|
||||
}
|
||||
ctx.Pop()
|
||||
return
|
||||
}
|
||||
for _, imgObj := range cgu.ImageObject {
|
||||
imgObj.Alpha = mergeAlpha(imgObj.Alpha, cgu.Alpha)
|
||||
r.renderImage(ctx, imgObj, pageH, ¤tCTM, boundaryInCTM, clipPath)
|
||||
}
|
||||
for _, pathObj := range cgu.PathObject {
|
||||
pathObj.Alpha = mergeAlpha(pathObj.Alpha, cgu.Alpha)
|
||||
r.renderPath(ctx, pathObj, pageH, defaultFill, defaultStroke, defaultLW, ¤tCTM, boundaryInCTM, clipPath)
|
||||
}
|
||||
for _, textObj := range cgu.TextObject {
|
||||
textObj.Alpha = mergeAlpha(textObj.Alpha, cgu.Alpha)
|
||||
r.renderText(ctx, textObj, pageH, defaultFill, defaultStroke, ¤tCTM, boundaryInCTM, clipPath)
|
||||
}
|
||||
for _, subCgu := range cgu.CompositeGraphicUnit {
|
||||
subCgu.Alpha = mergeAlpha(subCgu.Alpha, cgu.Alpha)
|
||||
r.renderCompositeGraphicUnit(ctx, subCgu, pageH, defaultFill, defaultStroke, defaultLW, ¤tCTM, boundaryInCTM, clipPath)
|
||||
}
|
||||
ctx.Pop()
|
||||
}
|
||||
|
||||
// renderObject 渲染图形对象
|
||||
// 入参: ctx 画布上下文, obj 图形对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽, parentCTM 父级CTM, boundaryInCTM 边界是否参与CTM变换, parentClip 父级裁剪路径
|
||||
func (r *Renderer) renderObject(ctx *canvas.Context, obj GraphicObject, pageH float64, defaultFill, defaultStroke color.Color, defaultLW float64, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) {
|
||||
switch obj.Type {
|
||||
case "TextObject":
|
||||
r.renderText(ctx, obj.TextObject, pageH, defaultFill, defaultStroke, parentCTM, boundaryInCTM, parentClip)
|
||||
case "PathObject":
|
||||
r.renderPath(ctx, obj.PathObject, pageH, defaultFill, defaultStroke, defaultLW, parentCTM, boundaryInCTM, parentClip)
|
||||
case "ImageObject":
|
||||
r.renderImage(ctx, obj.ImageObject, pageH, parentCTM, boundaryInCTM, parentClip)
|
||||
case "CompositeGraphicUnit", "CompositeObject":
|
||||
r.renderCompositeGraphicUnit(ctx, obj.CompositeGraphicUnit, pageH, defaultFill, defaultStroke, defaultLW, parentCTM, boundaryInCTM, parentClip)
|
||||
}
|
||||
}
|
||||
|
||||
// mergeGraphicObjectAlpha 合并图形对象透明度
|
||||
// 入参: obj 图形对象, alpha 父级透明度
|
||||
// 返回: GraphicObject 合并后的图形对象
|
||||
func mergeGraphicObjectAlpha(obj GraphicObject, alpha *int) GraphicObject {
|
||||
if alpha == nil {
|
||||
return obj
|
||||
}
|
||||
switch obj.Type {
|
||||
case "TextObject":
|
||||
obj.TextObject.Alpha = mergeAlpha(obj.TextObject.Alpha, alpha)
|
||||
case "PathObject":
|
||||
obj.PathObject.Alpha = mergeAlpha(obj.PathObject.Alpha, alpha)
|
||||
case "ImageObject":
|
||||
obj.ImageObject.Alpha = mergeAlpha(obj.ImageObject.Alpha, alpha)
|
||||
case "CompositeGraphicUnit", "CompositeObject":
|
||||
obj.CompositeGraphicUnit.Alpha = mergeAlpha(obj.CompositeGraphicUnit.Alpha, alpha)
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
// drawParamDefaults 合并绘制参数默认样式
|
||||
// 入参: id 绘制参数ID, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽
|
||||
// 返回: color.Color 默认填充色, color.Color 默认描边色, float64 默认线宽
|
||||
func (r *Renderer) drawParamDefaults(id string, defaultFill, defaultStroke color.Color, defaultLW float64) (color.Color, color.Color, float64) {
|
||||
if id == "" {
|
||||
return defaultFill, defaultStroke, defaultLW
|
||||
}
|
||||
dp := r.getDrawParam(id, nil)
|
||||
if dp == nil {
|
||||
return defaultFill, defaultStroke, defaultLW
|
||||
}
|
||||
if dp.LineWidth > 0 {
|
||||
defaultLW = dp.LineWidth
|
||||
}
|
||||
if dp.FillColor != nil {
|
||||
defaultFill = parseFillColor(dp.FillColor)
|
||||
}
|
||||
if dp.StrokeColor != nil {
|
||||
defaultStroke = parseStrokeColor(dp.StrokeColor)
|
||||
}
|
||||
return defaultFill, defaultStroke, defaultLW
|
||||
}
|
||||
|
||||
// getDrawParam 获取绘制参数逻辑
|
||||
// 入参: id 参数ID, visited 访问记录
|
||||
// 返回: *DrawParam 绘制参数
|
||||
func (r *Renderer) getDrawParam(id string, visited map[string]bool) *DrawParam {
|
||||
if visited == nil {
|
||||
visited = make(map[string]bool)
|
||||
}
|
||||
if visited[id] {
|
||||
return nil
|
||||
}
|
||||
visited[id] = true
|
||||
if dp, ok := r.DrawParams[id]; ok {
|
||||
if dp.Relative != "" {
|
||||
base := r.getDrawParam(dp.Relative, visited)
|
||||
if base == nil {
|
||||
return dp
|
||||
}
|
||||
merged := *base
|
||||
if dp.LineWidth > 0 {
|
||||
merged.LineWidth = dp.LineWidth
|
||||
}
|
||||
if dp.Join != "" {
|
||||
merged.Join = dp.Join
|
||||
}
|
||||
if dp.Cap != "" {
|
||||
merged.Cap = dp.Cap
|
||||
}
|
||||
if dp.DashPattern != "" {
|
||||
merged.DashPattern = dp.DashPattern
|
||||
merged.DashOffset = dp.DashOffset
|
||||
}
|
||||
if dp.MiterLimit > 0 {
|
||||
merged.MiterLimit = dp.MiterLimit
|
||||
}
|
||||
if dp.FillColor != nil {
|
||||
merged.FillColor = dp.FillColor
|
||||
}
|
||||
if dp.StrokeColor != nil {
|
||||
merged.StrokeColor = dp.StrokeColor
|
||||
}
|
||||
if dp.Font != "" {
|
||||
merged.Font = dp.Font
|
||||
}
|
||||
if dp.Size > 0 {
|
||||
merged.Size = dp.Size
|
||||
}
|
||||
if dp.Weight > 0 {
|
||||
merged.Weight = dp.Weight
|
||||
}
|
||||
if dp.Italic {
|
||||
merged.Italic = dp.Italic
|
||||
}
|
||||
return &merged
|
||||
}
|
||||
return dp
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ofdgo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"io"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
"github.com/tdewolff/canvas/renderers"
|
||||
"github.com/tdewolff/canvas/renderers/pdf"
|
||||
"github.com/tdewolff/canvas/renderers/rasterizer"
|
||||
)
|
||||
|
||||
// RenderToImage 渲染为光栅图
|
||||
// 入参: page 页面内容
|
||||
// 返回: image.Image 图像对象, error 错误信息
|
||||
func (r *Renderer) RenderToImage(page *PageContent) (image.Image, error) {
|
||||
c, err := r.RenderPage(page)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dpmm := r.DPI / 25.4
|
||||
return rasterizer.Draw(c, canvas.DPMM(dpmm), canvas.DefaultColorSpace), nil
|
||||
}
|
||||
|
||||
// RenderToSVG 渲染为SVG
|
||||
// 入参: page 页面内容, writer 输出流
|
||||
// 返回: error 错误信息
|
||||
func (r *Renderer) RenderToSVG(page *PageContent, writer io.Writer) error {
|
||||
c, err := r.renderPage(page)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Write(writer, renderers.SVG())
|
||||
}
|
||||
|
||||
// replacePDFProducer 替换PDF的Producer属性
|
||||
// 入参: data PDF字节数据
|
||||
// 返回: []byte 替换后的PDF字节数据
|
||||
func replacePDFProducer(data []byte) []byte {
|
||||
old := []byte("/Producer(tdewolff/canvas)")
|
||||
idx := bytes.LastIndex(data, old)
|
||||
if idx < 0 {
|
||||
return data
|
||||
}
|
||||
dst := []byte("/Producer(xiaoqidun/ofdgo)")
|
||||
copy(data[idx:idx+len(old)], dst)
|
||||
return data
|
||||
}
|
||||
|
||||
// RenderToPDF 渲染为PDF
|
||||
// 入参: page 页面内容, writer 输出流
|
||||
// 返回: error 错误信息
|
||||
func (r *Renderer) RenderToPDF(page *PageContent, writer io.Writer) error {
|
||||
c, err := r.renderPage(page)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
box, err := r.GetPageBox(page)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pages := []pdfPage{{Content: page, Box: box}}
|
||||
navigation := newPDFNavigation(r, r.Reader.doc, pages)
|
||||
var buf bytes.Buffer
|
||||
p := pdf.New(&buf, c.W, c.H, nil)
|
||||
p.SetInfo("", "", "", "", "xiaoqidun/ofdgo")
|
||||
navigation.apply(p, 0)
|
||||
c.RenderTo(p)
|
||||
if err := p.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = writer.Write(replacePDFProducer(buf.Bytes()))
|
||||
return err
|
||||
}
|
||||
|
||||
// RenderToEPS 渲染为EPS
|
||||
// 入参: page 页面内容, writer 输出流
|
||||
// 返回: error 错误信息
|
||||
func (r *Renderer) RenderToEPS(page *PageContent, writer io.Writer) error {
|
||||
c, err := r.renderPage(page)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Write(writer, renderers.EPS())
|
||||
}
|
||||
|
||||
// RenderToMultiPagePDF 将整个文档导出为多页PDF
|
||||
// 入参: writer 输出流
|
||||
// 返回: error 错误信息
|
||||
func (r *Renderer) RenderToMultiPagePDF(writer io.Writer) error {
|
||||
doc, err := r.Reader.Doc()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(doc.Pages.Page) == 0 {
|
||||
return fmt.Errorf("no pages found")
|
||||
}
|
||||
pages := make([]pdfPage, len(doc.Pages.Page))
|
||||
for i, pageRef := range doc.Pages.Page {
|
||||
page, err := r.Reader.PageContent(pageRef)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read page %d: %w", i+1, err)
|
||||
}
|
||||
box, err := r.GetPageBox(page)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read page %d area: %w", i+1, err)
|
||||
}
|
||||
pages[i] = pdfPage{Content: page, Box: box}
|
||||
}
|
||||
navigation := newPDFNavigation(r, doc, pages)
|
||||
var buf bytes.Buffer
|
||||
p := pdf.New(&buf, pages[0].Box.W, pages[0].Box.H, nil)
|
||||
p.SetInfo("", "", "", "", "xiaoqidun/ofdgo")
|
||||
for i, page := range pages {
|
||||
c, err := r.renderPage(page.Content)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to render page %d: %w", i+1, err)
|
||||
}
|
||||
if i > 0 {
|
||||
p.NewPage(c.W, c.H)
|
||||
}
|
||||
navigation.apply(p, i)
|
||||
c.RenderTo(p)
|
||||
}
|
||||
if err := p.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = writer.Write(replacePDFProducer(buf.Bytes()))
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,591 @@
|
||||
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ofdgo
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
)
|
||||
|
||||
const defaultPathLineWidth = 0.353
|
||||
|
||||
type pathStyle struct {
|
||||
fillColor color.Color
|
||||
strokeColor color.Color
|
||||
fillPaint any
|
||||
strokePaint any
|
||||
fillPattern *Pattern
|
||||
fillPatternColor color.Color
|
||||
lineWidth float64
|
||||
lineCap canvas.Capper
|
||||
lineJoin canvas.Joiner
|
||||
dashOffset float64
|
||||
dashPattern []float64
|
||||
}
|
||||
|
||||
// newPathStyle 创建路径样式
|
||||
// 入参: defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽, alpha 对象透明度
|
||||
// 返回: pathStyle 路径样式
|
||||
func newPathStyle(defaultFill, defaultStroke color.Color, defaultLW float64, alpha *int) pathStyle {
|
||||
style := pathStyle{
|
||||
fillColor: colorWithAlpha(defaultFill, alpha),
|
||||
strokeColor: colorWithAlpha(defaultStroke, alpha),
|
||||
lineWidth: defaultLW,
|
||||
lineCap: canvas.ButtCap,
|
||||
lineJoin: canvas.MiterJoin,
|
||||
}
|
||||
if style.lineWidth == 0 {
|
||||
style.lineWidth = defaultPathLineWidth
|
||||
}
|
||||
style.fillPaint = style.fillColor
|
||||
style.strokePaint = style.strokeColor
|
||||
return style
|
||||
}
|
||||
|
||||
// applyFillColor 应用填充颜色
|
||||
// 入参: fill 填充颜色, bx 边界X坐标, by 边界Y坐标, pageH 页面高度, alpha 对象透明度
|
||||
func (s *pathStyle) applyFillColor(fill *FillColor, bx, by, pageH float64, alpha *int) {
|
||||
fillColorNode := withFillAlpha(fill, alpha)
|
||||
s.fillPattern = fillColorNode.Pattern
|
||||
s.fillPatternColor = patternColor(fillColorNode)
|
||||
s.fillColor = parseFillColor(fillColorNode)
|
||||
s.fillPaint = parseFillPaint(fillColorNode, bx, by, pageH, 0, 0)
|
||||
}
|
||||
|
||||
// applyStrokeColor 应用描边颜色
|
||||
// 入参: stroke 描边颜色, bx 边界X坐标, by 边界Y坐标, pageH 页面高度, alpha 对象透明度
|
||||
func (s *pathStyle) applyStrokeColor(stroke *StrokeColor, bx, by, pageH float64, alpha *int) {
|
||||
strokeColorNode := withStrokeAlpha(stroke, alpha)
|
||||
s.strokeColor = parseStrokeColor(strokeColorNode)
|
||||
s.strokePaint = parseStrokePaint(strokeColorNode, bx, by, pageH, 0, 0)
|
||||
}
|
||||
|
||||
// pathLineCap 转换线帽样式
|
||||
// 入参: cap 线帽名称, fallback 默认线帽
|
||||
// 返回: canvas.Capper 线帽样式
|
||||
func pathLineCap(cap string, fallback canvas.Capper) canvas.Capper {
|
||||
switch cap {
|
||||
case "Round":
|
||||
return canvas.RoundCap
|
||||
case "Square":
|
||||
return canvas.SquareCap
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
// pathLineJoin 转换线连接样式
|
||||
// 入参: join 线连接名称, fallback 默认线连接
|
||||
// 返回: canvas.Joiner 线连接样式
|
||||
func pathLineJoin(join string, fallback canvas.Joiner) canvas.Joiner {
|
||||
switch join {
|
||||
case "Round":
|
||||
return canvas.RoundJoin
|
||||
case "Bevel":
|
||||
return canvas.BevelJoin
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
// applyDrawParam 应用绘制参数样式
|
||||
// 入参: dp 绘制参数, bx 边界X坐标, by 边界Y坐标, pageH 页面高度, alpha 对象透明度
|
||||
func (s *pathStyle) applyDrawParam(dp *DrawParam, bx, by, pageH float64, alpha *int) {
|
||||
if dp.LineWidth > 0 {
|
||||
s.lineWidth = dp.LineWidth
|
||||
}
|
||||
if dp.FillColor != nil {
|
||||
s.applyFillColor(dp.FillColor, bx, by, pageH, alpha)
|
||||
}
|
||||
if dp.StrokeColor != nil {
|
||||
s.applyStrokeColor(dp.StrokeColor, bx, by, pageH, alpha)
|
||||
}
|
||||
if dp.Cap != "" {
|
||||
s.lineCap = pathLineCap(dp.Cap, s.lineCap)
|
||||
}
|
||||
if dp.Join != "" {
|
||||
s.lineJoin = pathLineJoin(dp.Join, s.lineJoin)
|
||||
}
|
||||
if dp.DashPattern != "" {
|
||||
s.dashPattern = parseFloats(dp.DashPattern)
|
||||
s.dashOffset = dp.DashOffset
|
||||
}
|
||||
}
|
||||
|
||||
// applyPathObject 应用路径对象样式
|
||||
// 入参: obj 路径对象, bx 边界X坐标, by 边界Y坐标, pageH 页面高度
|
||||
func (s *pathStyle) applyPathObject(obj PathObject, bx, by, pageH float64) {
|
||||
if obj.LineWidth > 0 {
|
||||
s.lineWidth = obj.LineWidth
|
||||
}
|
||||
if obj.FillColor != nil {
|
||||
s.applyFillColor(obj.FillColor, bx, by, pageH, obj.Alpha)
|
||||
}
|
||||
if obj.StrokeColor != nil {
|
||||
s.applyStrokeColor(obj.StrokeColor, bx, by, pageH, obj.Alpha)
|
||||
}
|
||||
if obj.Cap != "" {
|
||||
s.lineCap = pathLineCap(obj.Cap, canvas.ButtCap)
|
||||
}
|
||||
if obj.Join != "" {
|
||||
s.lineJoin = pathLineJoin(obj.Join, canvas.MiterJoin)
|
||||
}
|
||||
if obj.DashPattern != "" {
|
||||
s.dashPattern = parseFloats(obj.DashPattern)
|
||||
s.dashOffset = obj.DashOffset
|
||||
}
|
||||
}
|
||||
|
||||
// scale 应用路径变换缩放
|
||||
// 入参: ctm 变换矩阵
|
||||
func (s *pathStyle) scale(ctm Matrix) {
|
||||
if scale := math.Sqrt(math.Abs(ctm.a*ctm.d - ctm.b*ctm.c)); scale > 0 {
|
||||
s.lineWidth *= scale
|
||||
s.dashOffset *= scale
|
||||
for i := range s.dashPattern {
|
||||
s.dashPattern[i] *= scale
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// renderPath 渲染路径
|
||||
// 入参: ctx 画布上下文, obj 路径对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽, parentCTM 父级CTM, boundaryInCTM 边界是否参与CTM变换, parentClip 父级裁剪路径
|
||||
func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64, defaultFill, defaultStroke color.Color, defaultLW float64, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) {
|
||||
if obj.Visible != nil && !*obj.Visible {
|
||||
return
|
||||
}
|
||||
ctx.Push()
|
||||
bx, by := 0.0, 0.0
|
||||
if obj.Boundary != "" {
|
||||
if box, err := ParseBox(obj.Boundary); err == nil {
|
||||
bx, by = box.X, box.Y
|
||||
}
|
||||
}
|
||||
ctm := NewMatrix(obj.CTM)
|
||||
if parentCTM != nil {
|
||||
ctm = parentCTM.Multiply(ctm)
|
||||
}
|
||||
style := newPathStyle(defaultFill, defaultStroke, defaultLW, obj.Alpha)
|
||||
if obj.DrawParam != "" {
|
||||
if dp := r.getDrawParam(obj.DrawParam, nil); dp != nil {
|
||||
style.applyDrawParam(dp, bx, by, pageH, obj.Alpha)
|
||||
}
|
||||
}
|
||||
style.applyPathObject(obj, bx, by, pageH)
|
||||
style.scale(ctm)
|
||||
p := r.buildPath(obj, pageH, ctm, boundaryInCTM)
|
||||
if rectPath := r.buildTinyFillRectPath(obj, pageH, ctm, bx, by); rectPath != nil {
|
||||
p = rectPath
|
||||
}
|
||||
clipPath := intersectClipPath(parentClip, r.buildClipPath(obj.Clips, pageH, bx, by, ctm))
|
||||
shouldFill := false
|
||||
if obj.Fill != nil {
|
||||
shouldFill = *obj.Fill
|
||||
}
|
||||
if style.fillPaint == nil {
|
||||
style.fillPaint = style.fillColor
|
||||
}
|
||||
if shouldFill && style.fillPattern != nil {
|
||||
fp := p
|
||||
if clipPath != nil {
|
||||
fp = p.Copy()
|
||||
fp.Close()
|
||||
fp = applyClipPath(fp, clipPath)
|
||||
}
|
||||
r.renderPattern(ctx, style.fillPattern, style.fillPatternColor, pageH, fp, ctm, bx, by)
|
||||
} else if shouldFill && style.fillPaint != nil {
|
||||
ctx.SetFill(style.fillPaint)
|
||||
ctx.SetStrokeColor(canvas.Transparent)
|
||||
fp := p
|
||||
if clipPath != nil {
|
||||
fp = p.Copy()
|
||||
fp.Close()
|
||||
fp = applyClipPath(fp, clipPath)
|
||||
}
|
||||
ctx.DrawPath(0, 0, fp)
|
||||
}
|
||||
shouldStroke := true
|
||||
if obj.Stroke != nil {
|
||||
shouldStroke = *obj.Stroke
|
||||
}
|
||||
if shouldStroke {
|
||||
if style.strokePaint == nil {
|
||||
style.strokePaint = style.strokeColor
|
||||
}
|
||||
if style.strokePaint == nil {
|
||||
style.strokePaint = colorWithAlpha(canvas.Black, obj.Alpha)
|
||||
}
|
||||
ctx.SetFillColor(canvas.Transparent)
|
||||
ctx.SetStroke(style.strokePaint)
|
||||
ctx.SetStrokeWidth(style.lineWidth)
|
||||
ctx.SetStrokeCapper(style.lineCap)
|
||||
ctx.SetStrokeJoiner(style.lineJoin)
|
||||
if len(style.dashPattern) > 0 {
|
||||
ctx.SetDashes(style.dashOffset, style.dashPattern...)
|
||||
}
|
||||
if clipPath != nil {
|
||||
sp := p.Copy()
|
||||
if len(style.dashPattern) > 0 {
|
||||
sp = sp.Dash(style.dashOffset, style.dashPattern...)
|
||||
}
|
||||
sp = sp.Stroke(style.lineWidth, style.lineCap, style.lineJoin, canvas.Tolerance)
|
||||
sp = applyClipPath(sp, clipPath)
|
||||
ctx.SetFill(style.strokePaint)
|
||||
ctx.SetStrokeColor(canvas.Transparent)
|
||||
ctx.DrawPath(0, 0, sp)
|
||||
} else {
|
||||
ctx.DrawPath(0, 0, p)
|
||||
}
|
||||
}
|
||||
ctx.Pop()
|
||||
}
|
||||
|
||||
// renderPattern 渲染图案填充
|
||||
// 入参: ctx 画布上下文, pattern 图案对象, defaultColor 默认颜色, pageH 页面高度, clip 填充区域, parentCTM 父级CTM, bx 边界X坐标, by 边界Y坐标
|
||||
func (r *Renderer) renderPattern(ctx *canvas.Context, pattern *Pattern, defaultColor color.Color, pageH float64, clip *canvas.Path, parentCTM Matrix, bx, by float64) {
|
||||
if pattern == nil || clip == nil || len(pattern.CellContent.Objects) == 0 {
|
||||
return
|
||||
}
|
||||
xStep, yStep := pattern.XStep, pattern.YStep
|
||||
if xStep == 0 {
|
||||
xStep = pattern.Width
|
||||
}
|
||||
if yStep == 0 {
|
||||
yStep = pattern.Height
|
||||
}
|
||||
if xStep <= 0 || yStep <= 0 {
|
||||
return
|
||||
}
|
||||
patternCTM := TranslationMatrix(bx, by).Multiply(parentCTM).Multiply(NewMatrix(pattern.CTM))
|
||||
invCTM, ok := patternCTM.Invert()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
bounds := clip.FastBounds()
|
||||
points := [][2]float64{
|
||||
{bounds.X0, pageH - bounds.Y0},
|
||||
{bounds.X1, pageH - bounds.Y0},
|
||||
{bounds.X1, pageH - bounds.Y1},
|
||||
{bounds.X0, pageH - bounds.Y1},
|
||||
}
|
||||
minX, maxX := 0.0, 0.0
|
||||
minY, maxY := 0.0, 0.0
|
||||
for i, point := range points {
|
||||
x, y := invCTM.Transform(point[0], point[1])
|
||||
if i == 0 {
|
||||
minX, maxX = x, x
|
||||
minY, maxY = y, y
|
||||
continue
|
||||
}
|
||||
minX = math.Min(minX, x)
|
||||
maxX = math.Max(maxX, x)
|
||||
minY = math.Min(minY, y)
|
||||
maxY = math.Max(maxY, y)
|
||||
}
|
||||
startX := int(math.Floor(minX/xStep)) - 1
|
||||
endX := int(math.Ceil(maxX/xStep)) + 1
|
||||
startY := int(math.Floor(minY/yStep)) - 1
|
||||
endY := int(math.Ceil(maxY/yStep)) + 1
|
||||
for ix := startX; ix <= endX; ix++ {
|
||||
for iy := startY; iy <= endY; iy++ {
|
||||
tileCTM := patternCTM.Multiply(TranslationMatrix(float64(ix)*xStep, float64(iy)*yStep))
|
||||
for _, obj := range pattern.CellContent.Objects {
|
||||
r.renderObject(ctx, obj, pageH, defaultColor, defaultColor, 0, &tileCTM, false, clip)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// buildPath 解析路径并返回Canvas Path
|
||||
// 入参: obj 路径对象, pageH 页面高度, ctm 变换矩阵, boundaryInCTM 边界是否参与CTM变换
|
||||
// 返回: *canvas.Path 路径对象
|
||||
func (r *Renderer) buildPath(obj PathObject, pageH float64, ctm Matrix, boundaryInCTM bool) *canvas.Path {
|
||||
bx, by := 0.0, 0.0
|
||||
if obj.Boundary != "" {
|
||||
if box, err := ParseBox(obj.Boundary); err == nil {
|
||||
bx, by = box.X, box.Y
|
||||
}
|
||||
}
|
||||
point := func(x, y float64) (float64, float64) {
|
||||
if boundaryInCTM {
|
||||
tx, ty := ctm.Transform(x+bx, y+by)
|
||||
return tx, pageH - ty
|
||||
}
|
||||
tx, ty := ctm.Transform(x, y)
|
||||
return tx + bx, pageH - (ty + by)
|
||||
}
|
||||
p := &canvas.Path{}
|
||||
tokens := strings.Fields(obj.AbbreviatedData)
|
||||
for i := 0; i < len(tokens); {
|
||||
cmd := tokens[i]
|
||||
i++
|
||||
switch cmd {
|
||||
case "M", "S":
|
||||
if i+1 < len(tokens) {
|
||||
x, _ := strconv.ParseFloat(tokens[i], 64)
|
||||
y, _ := strconv.ParseFloat(tokens[i+1], 64)
|
||||
tx, ty := point(x, y)
|
||||
p.MoveTo(tx, ty)
|
||||
i += 2
|
||||
}
|
||||
case "L":
|
||||
if i+1 < len(tokens) {
|
||||
x, _ := strconv.ParseFloat(tokens[i], 64)
|
||||
y, _ := strconv.ParseFloat(tokens[i+1], 64)
|
||||
tx, ty := point(x, y)
|
||||
p.LineTo(tx, ty)
|
||||
i += 2
|
||||
}
|
||||
case "B":
|
||||
if i+5 < len(tokens) {
|
||||
x1, _ := strconv.ParseFloat(tokens[i], 64)
|
||||
y1, _ := strconv.ParseFloat(tokens[i+1], 64)
|
||||
x2, _ := strconv.ParseFloat(tokens[i+2], 64)
|
||||
y2, _ := strconv.ParseFloat(tokens[i+3], 64)
|
||||
x3, _ := strconv.ParseFloat(tokens[i+4], 64)
|
||||
y3, _ := strconv.ParseFloat(tokens[i+5], 64)
|
||||
tx1, ty1 := point(x1, y1)
|
||||
tx2, ty2 := point(x2, y2)
|
||||
tx3, ty3 := point(x3, y3)
|
||||
p.CubeTo(tx1, ty1, tx2, ty2, tx3, ty3)
|
||||
i += 6
|
||||
}
|
||||
case "Q":
|
||||
if i+3 < len(tokens) {
|
||||
x1, _ := strconv.ParseFloat(tokens[i], 64)
|
||||
y1, _ := strconv.ParseFloat(tokens[i+1], 64)
|
||||
x2, _ := strconv.ParseFloat(tokens[i+2], 64)
|
||||
y2, _ := strconv.ParseFloat(tokens[i+3], 64)
|
||||
tx1, ty1 := point(x1, y1)
|
||||
tx2, ty2 := point(x2, y2)
|
||||
p.QuadTo(tx1, ty1, tx2, ty2)
|
||||
i += 4
|
||||
}
|
||||
case "A":
|
||||
if i+6 < len(tokens) {
|
||||
rx, _ := strconv.ParseFloat(tokens[i], 64)
|
||||
ry, _ := strconv.ParseFloat(tokens[i+1], 64)
|
||||
rot, _ := strconv.ParseFloat(tokens[i+2], 64)
|
||||
large, _ := strconv.ParseBool(tokens[i+3])
|
||||
sweep, _ := strconv.ParseBool(tokens[i+4])
|
||||
x, _ := strconv.ParseFloat(tokens[i+5], 64)
|
||||
y, _ := strconv.ParseFloat(tokens[i+6], 64)
|
||||
sx := math.Hypot(ctm.a, ctm.c)
|
||||
sy := math.Hypot(ctm.b, ctm.d)
|
||||
ctmRot := math.Atan2(ctm.b, ctm.a) * 180 / math.Pi
|
||||
tx, ty := point(x, y)
|
||||
sweep = !sweep
|
||||
p.ArcTo(rx*sx, ry*sy, -(rot + ctmRot), large, sweep, tx, ty)
|
||||
i += 7
|
||||
}
|
||||
case "C":
|
||||
p.Close()
|
||||
}
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// buildClipPath 构建裁剪路径
|
||||
// 入参: clips 裁剪对象, pageH 页面高度, bx 边界X坐标, by 边界Y坐标, objectCTM 对象CTM
|
||||
// 返回: *canvas.Path 路径对象
|
||||
func (r *Renderer) buildClipPath(clips *Clips, pageH float64, bx, by float64, objectCTM Matrix) *canvas.Path {
|
||||
if clips == nil {
|
||||
return nil
|
||||
}
|
||||
var p *canvas.Path
|
||||
for _, clip := range clips.Clip {
|
||||
var clipPath *canvas.Path
|
||||
for _, area := range clip.Area {
|
||||
areaCTM := NewMatrix(area.CTM)
|
||||
if clips.TransFlag == nil || *clips.TransFlag {
|
||||
areaCTM = objectCTM.Multiply(areaCTM)
|
||||
}
|
||||
for _, pathObj := range area.Path {
|
||||
ctm := areaCTM.Multiply(NewMatrix(pathObj.CTM))
|
||||
cp := r.buildPath(pathObj, pageH, ctm, true)
|
||||
cp.Translate(bx, -by)
|
||||
cp.Close()
|
||||
if clipPath == nil {
|
||||
clipPath = cp
|
||||
} else {
|
||||
clipPath = unionClipPath(clipPath, cp)
|
||||
}
|
||||
}
|
||||
}
|
||||
if clipPath != nil {
|
||||
if p == nil {
|
||||
p = clipPath
|
||||
} else {
|
||||
p = intersectClipPath(p, clipPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// intersectClipPath 求裁剪路径交集
|
||||
// 入参: parent 父级裁剪路径, current 当前裁剪路径
|
||||
// 返回: *canvas.Path 相交后的裁剪路径
|
||||
func intersectClipPath(parent, current *canvas.Path) *canvas.Path {
|
||||
if parent == nil {
|
||||
return current
|
||||
}
|
||||
if current == nil {
|
||||
return parent
|
||||
}
|
||||
parentRect, parentOK := rectangularPath(parent)
|
||||
currentRect, currentOK := rectangularPath(current)
|
||||
if parentOK && currentOK {
|
||||
return parentRect.And(currentRect).ToPath()
|
||||
}
|
||||
if parent.Empty() || current.Empty() {
|
||||
return &canvas.Path{}
|
||||
}
|
||||
if parentOK && parentRect.Contains(current.FastBounds()) {
|
||||
return current
|
||||
}
|
||||
if currentOK && currentRect.Contains(parent.FastBounds()) {
|
||||
return parent
|
||||
}
|
||||
return parent.And(current)
|
||||
}
|
||||
|
||||
// unionClipPath 合并裁剪区域
|
||||
// 入参: left 左侧裁剪路径, right 右侧裁剪路径
|
||||
// 返回: *canvas.Path 合并后的裁剪路径
|
||||
func unionClipPath(left, right *canvas.Path) *canvas.Path {
|
||||
leftRect, leftOK := rectangularPath(left)
|
||||
rightRect, rightOK := rectangularPath(right)
|
||||
if leftOK && rightOK {
|
||||
bounds := leftRect.Add(rightRect)
|
||||
area := leftRect.Area() + rightRect.Area() - leftRect.And(rightRect).Area()
|
||||
if canvas.Equal(area, bounds.Area()) {
|
||||
return bounds.ToPath()
|
||||
}
|
||||
}
|
||||
return left.Or(right)
|
||||
}
|
||||
|
||||
// applyClipPath 应用裁剪路径
|
||||
// 入参: path 绘制路径, clip 裁剪路径
|
||||
// 返回: *canvas.Path 裁剪后的绘制路径
|
||||
func applyClipPath(path, clip *canvas.Path) *canvas.Path {
|
||||
if path == nil || clip == nil {
|
||||
return path
|
||||
}
|
||||
if path.Empty() || clip.Empty() {
|
||||
return &canvas.Path{}
|
||||
}
|
||||
if rect, ok := rectangularPath(clip); ok {
|
||||
bounds := path.FastBounds()
|
||||
if rect.Contains(bounds) {
|
||||
return path
|
||||
}
|
||||
if !rect.Overlaps(bounds) {
|
||||
return &canvas.Path{}
|
||||
}
|
||||
if pathRect, ok := rectangularPath(path); ok {
|
||||
return pathRect.And(rect).ToPath()
|
||||
}
|
||||
return path.Flatten(canvas.Tolerance).Clip(rect.X0, rect.Y0, rect.X1, rect.Y1)
|
||||
}
|
||||
return path.And(clip)
|
||||
}
|
||||
|
||||
// rectangularPath 获取矩形路径区域
|
||||
// 入参: path 路径对象
|
||||
// 返回: canvas.Rect 矩形区域, bool 是否为矩形
|
||||
func rectangularPath(path *canvas.Path) (canvas.Rect, bool) {
|
||||
if path == nil || path.HasSubpaths() || !path.Closed() {
|
||||
return canvas.Rect{}, false
|
||||
}
|
||||
data := path.Data()
|
||||
for i := 0; i < len(data); i += 4 {
|
||||
if i+4 > len(data) || (data[i] != canvas.MoveToCmd && data[i] != canvas.LineToCmd && data[i] != canvas.CloseCmd) {
|
||||
return canvas.Rect{}, false
|
||||
}
|
||||
}
|
||||
points := path.Coords()
|
||||
if len(points) != 5 || !points[0].Equals(points[4]) {
|
||||
return canvas.Rect{}, false
|
||||
}
|
||||
rect := path.FastBounds()
|
||||
corners := 0
|
||||
for _, point := range points[:4] {
|
||||
corner := 0
|
||||
switch {
|
||||
case canvas.Equal(point.X, rect.X0) && canvas.Equal(point.Y, rect.Y0):
|
||||
corner = 1
|
||||
case canvas.Equal(point.X, rect.X1) && canvas.Equal(point.Y, rect.Y0):
|
||||
corner = 2
|
||||
case canvas.Equal(point.X, rect.X1) && canvas.Equal(point.Y, rect.Y1):
|
||||
corner = 4
|
||||
case canvas.Equal(point.X, rect.X0) && canvas.Equal(point.Y, rect.Y1):
|
||||
corner = 8
|
||||
default:
|
||||
return canvas.Rect{}, false
|
||||
}
|
||||
if corners&corner != 0 {
|
||||
return canvas.Rect{}, false
|
||||
}
|
||||
corners |= corner
|
||||
}
|
||||
return rect, corners == 15
|
||||
}
|
||||
|
||||
// buildTinyFillRectPath 构建微小填充矩形路径
|
||||
// 入参: obj 路径对象, pageH 页面高度, ctm 变换矩阵, bx 边界X坐标, by 边界Y坐标
|
||||
// 返回: *canvas.Path 路径对象
|
||||
func (r *Renderer) buildTinyFillRectPath(obj PathObject, pageH float64, ctm Matrix, bx, by float64) *canvas.Path {
|
||||
if obj.Fill == nil || !*obj.Fill || obj.Stroke == nil || *obj.Stroke {
|
||||
return nil
|
||||
}
|
||||
box, err := ParseBox(obj.Boundary)
|
||||
if err != nil || box.W <= 0 || box.H <= 0 || box.W > 0.6 || box.H > 0.6 {
|
||||
return nil
|
||||
}
|
||||
tokens := strings.Fields(obj.AbbreviatedData)
|
||||
if len(tokens) != 11 || tokens[0] != "M" || tokens[3] != "L" || tokens[6] != "L" || tokens[9] != "L" || tokens[10] != "C" {
|
||||
return nil
|
||||
}
|
||||
points := make([][2]float64, 0, 4)
|
||||
for i := 1; i < 10; i += 3 {
|
||||
x, errX := strconv.ParseFloat(tokens[i], 64)
|
||||
y, errY := strconv.ParseFloat(tokens[i+1], 64)
|
||||
if errX != nil || errY != nil {
|
||||
return nil
|
||||
}
|
||||
tx, ty := ctm.Transform(x, y)
|
||||
points = append(points, [2]float64{tx + bx, pageH - (ty + by)})
|
||||
}
|
||||
minX, maxX := points[0][0], points[0][0]
|
||||
minY, maxY := points[0][1], points[0][1]
|
||||
for _, point := range points[1:] {
|
||||
minX = math.Min(minX, point[0])
|
||||
maxX = math.Max(maxX, point[0])
|
||||
minY = math.Min(minY, point[1])
|
||||
maxY = math.Max(maxY, point[1])
|
||||
}
|
||||
expand := math.Min(box.W, box.H) * 0.08
|
||||
p := &canvas.Path{}
|
||||
p.MoveTo(minX-expand, minY-expand)
|
||||
p.LineTo(maxX+expand, minY-expand)
|
||||
p.LineTo(maxX+expand, maxY+expand)
|
||||
p.LineTo(minX-expand, maxY+expand)
|
||||
p.Close()
|
||||
return p
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ofdgo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"math"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
"github.com/tdewolff/canvas/renderers/rasterizer"
|
||||
)
|
||||
|
||||
// renderStamp 渲染印章
|
||||
// 入参: ctx 画布上下文, s 印章对象, pageH 页面高度
|
||||
func (r *Renderer) renderStamp(ctx *canvas.Context, s Stamp, pageH float64) {
|
||||
if s.Type == "ofd" && len(s.Data) > 0 {
|
||||
if s.Clip != nil {
|
||||
if img := r.renderOFDStampImage(s.Data); img != nil {
|
||||
r.renderStampImage(ctx, stampImageWithTransparentWhite(img), s, pageH)
|
||||
}
|
||||
return
|
||||
}
|
||||
reader, err := NewReader(bytes.NewReader(s.Data), int64(len(s.Data)))
|
||||
if err == nil {
|
||||
defer reader.Close()
|
||||
doc, err := reader.Doc()
|
||||
if err == nil {
|
||||
renderer := r.childRenderer(reader)
|
||||
for _, pageRef := range doc.Pages.Page {
|
||||
content, err := reader.PageContent(pageRef)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
sealBox, err := renderer.GetPageBox(content)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
ctx.Push()
|
||||
ctx.Translate(s.Box.X, pageH-(s.Box.Y+s.Box.H))
|
||||
ctx.Scale(s.Box.W/sealBox.W, s.Box.H/sealBox.H)
|
||||
renderer.renderPageToContext(ctx, content, false)
|
||||
ctx.Pop()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(s.Data) > 0 {
|
||||
img, _, err := decodeImageData(s.Data)
|
||||
if err == nil {
|
||||
if r.decodeImages {
|
||||
img = imagePixelSource(img)
|
||||
}
|
||||
r.renderStampImage(ctx, stampImageWithTransparentWhite(img), s, pageH)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// renderOFDStampImage 渲染OFD印章图像
|
||||
// 入参: data OFD印章数据
|
||||
// 返回: image.Image 印章图像
|
||||
func (r *Renderer) renderOFDStampImage(data []byte) image.Image {
|
||||
reader, err := NewReader(bytes.NewReader(data), int64(len(data)))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer reader.Close()
|
||||
doc, err := reader.Doc()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
renderer := r.childRenderer(reader)
|
||||
renderer.decodeImages = true
|
||||
for _, pageRef := range doc.Pages.Page {
|
||||
content, err := reader.PageContent(pageRef)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
sealBox, err := renderer.GetPageBox(content)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
c := canvas.New(sealBox.W, sealBox.H)
|
||||
if err := renderer.renderPageToContext(canvas.NewContext(c), content, false); err != nil {
|
||||
continue
|
||||
}
|
||||
return rasterizer.Draw(c, canvas.DPMM(r.DPI/25.4), canvas.DefaultColorSpace)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// renderStampImage 渲染印章图像
|
||||
// 入参: ctx 画布上下文, img 印章图像, s 印章对象, pageH 页面高度
|
||||
func (r *Renderer) renderStampImage(ctx *canvas.Context, img image.Image, s Stamp, pageH float64) {
|
||||
box := s.Box
|
||||
if s.Clip != nil {
|
||||
img = clipStampImage(img, box, *s.Clip)
|
||||
box.X += s.Clip.X
|
||||
box.Y += s.Clip.Y
|
||||
box.W = s.Clip.W
|
||||
box.H = s.Clip.H
|
||||
}
|
||||
ctx.Push()
|
||||
ctx.Translate(box.X, pageH-(box.Y+box.H))
|
||||
ctx.Scale(box.W/float64(img.Bounds().Dx()), box.H/float64(img.Bounds().Dy()))
|
||||
ctx.DrawImage(0, 0, img, canvas.DPMM(1.0))
|
||||
ctx.Pop()
|
||||
}
|
||||
|
||||
// clipStampImage 裁剪印章图像
|
||||
// 入参: img 印章图像, box 印章区域, clip 裁剪区域
|
||||
// 返回: image.Image 裁剪后的印章图像
|
||||
func clipStampImage(img image.Image, box, clip Box) image.Image {
|
||||
bounds := img.Bounds()
|
||||
x0 := int(math.Floor(clip.X / box.W * float64(bounds.Dx())))
|
||||
y0 := int(math.Floor(clip.Y / box.H * float64(bounds.Dy())))
|
||||
x1 := int(math.Ceil((clip.X + clip.W) / box.W * float64(bounds.Dx())))
|
||||
y1 := int(math.Ceil((clip.Y + clip.H) / box.H * float64(bounds.Dy())))
|
||||
out := image.NewNRGBA(image.Rect(0, 0, x1-x0, y1-y0))
|
||||
draw.Draw(out, out.Bounds(), img, image.Pt(bounds.Min.X+x0, bounds.Min.Y+y0), draw.Src)
|
||||
return out
|
||||
}
|
||||
|
||||
// stampImageWithTransparentWhite 处理印章图片白色底色
|
||||
// 入参: img 印章图片对象
|
||||
// 返回: image.Image 处理后的印章图片对象
|
||||
func stampImageWithTransparentWhite(img image.Image) image.Image {
|
||||
if opaque, ok := img.(interface{ Opaque() bool }); ok && !opaque.Opaque() {
|
||||
return img
|
||||
}
|
||||
bounds := img.Bounds()
|
||||
out := image.NewNRGBA(bounds)
|
||||
hasAlpha := false
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||
c := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA)
|
||||
if c.A < 255 {
|
||||
hasAlpha = true
|
||||
}
|
||||
out.SetNRGBA(x, y, c)
|
||||
}
|
||||
}
|
||||
if hasAlpha {
|
||||
return img
|
||||
}
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||
c := out.NRGBAAt(x, y)
|
||||
if c.R >= 250 && c.G >= 250 && c.B >= 250 {
|
||||
c.A = 0
|
||||
out.SetNRGBA(x, y, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ofdgo
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"strings"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
)
|
||||
|
||||
const ptPerMM = 72.0 / 25.4
|
||||
|
||||
// renderText 渲染文本
|
||||
// 入参: ctx 画布上下文, obj 文本对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, parentCTM 父级CTM, boundaryInCTM 边界是否参与父级CTM, parentClip 父级裁剪路径
|
||||
func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64, defaultFill, defaultStroke color.Color, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) {
|
||||
if obj.Visible != nil && !*obj.Visible {
|
||||
return
|
||||
}
|
||||
ctx.Push()
|
||||
bx, by := 0.0, 0.0
|
||||
if obj.Boundary != "" {
|
||||
if box, err := ParseBox(obj.Boundary); err == nil {
|
||||
bx, by = box.X, box.Y
|
||||
}
|
||||
}
|
||||
localCTM := NewMatrix(obj.CTM)
|
||||
ctm := localCTM
|
||||
if parentCTM != nil {
|
||||
ctm = parentCTM.Multiply(ctm)
|
||||
}
|
||||
clipPath := intersectClipPath(parentClip, r.buildClipPath(obj.Clips, pageH, bx, by, ctm))
|
||||
var dp *DrawParam
|
||||
if obj.DrawParam != "" {
|
||||
dp = r.getDrawParam(obj.DrawParam, nil)
|
||||
}
|
||||
sizeMM := obj.Size
|
||||
if sizeMM == 0 && dp != nil && dp.Size > 0 {
|
||||
sizeMM = dp.Size
|
||||
}
|
||||
if sizeMM == 0 {
|
||||
sizeMM = 3.5
|
||||
}
|
||||
if obj.VScale != 0 {
|
||||
sizeMM *= obj.VScale
|
||||
}
|
||||
hScale := obj.HScale
|
||||
if hScale == 0 {
|
||||
hScale = 1
|
||||
}
|
||||
useTextMatrix := hasTextMatrix(ctm)
|
||||
if scale := ctm.YScale(); scale > 0 && !useTextMatrix {
|
||||
sizeMM *= scale
|
||||
}
|
||||
sizePt := sizeMM * ptPerMM
|
||||
fillColor := colorWithAlpha(defaultFill, obj.Alpha)
|
||||
if fillColor == nil {
|
||||
fillColor = colorWithAlpha(canvas.Black, obj.Alpha)
|
||||
}
|
||||
var fillPaint any = fillColor
|
||||
var fillColorNode *FillColor
|
||||
if dp != nil && dp.FillColor != nil {
|
||||
fillColorNode = withFillAlpha(dp.FillColor, obj.Alpha)
|
||||
fillColor = parseFillColor(fillColorNode)
|
||||
fillPaint = parseFillPaint(fillColorNode, bx, by, pageH, 0, 0)
|
||||
}
|
||||
if obj.FillColor != nil {
|
||||
fillColorNode = withFillAlpha(obj.FillColor, obj.Alpha)
|
||||
fillColor = parseFillColor(fillColorNode)
|
||||
fillPaint = parseFillPaint(fillColorNode, bx, by, pageH, 0, 0)
|
||||
}
|
||||
if fillPaint == nil {
|
||||
fillPaint = fillColor
|
||||
}
|
||||
fontStyle := canvas.FontRegular
|
||||
weight := obj.Weight
|
||||
if weight == 0 && dp != nil && dp.Weight > 0 {
|
||||
weight = dp.Weight
|
||||
}
|
||||
syntheticBold := weight >= 700
|
||||
italic := obj.Italic
|
||||
if !italic && dp != nil && dp.Italic {
|
||||
italic = true
|
||||
}
|
||||
if italic {
|
||||
fontStyle |= canvas.FontItalic
|
||||
}
|
||||
fontID := r.textObjectFontID(obj)
|
||||
embeddedFont := false
|
||||
if of, ok := r.Reader.fontCache[fontID]; ok {
|
||||
embeddedFont = of.FontFile != ""
|
||||
if !embeddedFont && fontNoSyntheticBold(of.FontName, of.FamilyName) {
|
||||
syntheticBold = false
|
||||
}
|
||||
if of.Bold {
|
||||
fontStyle |= canvas.FontBold
|
||||
}
|
||||
if of.Italic {
|
||||
fontStyle |= canvas.FontItalic
|
||||
}
|
||||
}
|
||||
if syntheticBold {
|
||||
fontStyle |= canvas.FontBold
|
||||
}
|
||||
ff := r.loadFont(fontID)
|
||||
if ff == nil {
|
||||
ctx.Pop()
|
||||
return
|
||||
}
|
||||
face := ff.Face(sizePt, fillPaint, fontStyle, canvas.FontNormal)
|
||||
glyphTransforms := r.textObjectGlyphTransforms(fontID, obj)
|
||||
hasUnderline := strings.Contains(obj.Decoration, "Underline")
|
||||
useGlyphFillPaint := fillColorNode != nil && fillColorNode.AxialShd != nil
|
||||
codePos := 0
|
||||
for _, tc := range obj.TextCode {
|
||||
var runes []rune
|
||||
var glyphs []textGlyph
|
||||
if tc.Index != "" {
|
||||
runes = r.parseIndexRunes(tc.Index, fontID)
|
||||
glyphs = textRuneGlyphs(runes)
|
||||
} else {
|
||||
runes = textCodeRunes(tc.Value)
|
||||
glyphs = textCodeGlyphs(runes, glyphTransforms, codePos)
|
||||
}
|
||||
dxs, dys := parseFloats(tc.DeltaX), parseFloats(tc.DeltaY)
|
||||
xs, ys := parseFloats(tc.X), parseFloats(tc.Y)
|
||||
drawAsPath := embeddedFont || textCodePositioned(tc, xs, ys) || clipPath != nil
|
||||
cx, cy := 0.0, 0.0
|
||||
if len(xs) > 0 {
|
||||
cx = xs[0]
|
||||
}
|
||||
if len(ys) > 0 {
|
||||
cy = ys[0]
|
||||
}
|
||||
for i, glyph := range glyphs {
|
||||
str := glyph.Text
|
||||
drawAsGlyphPath := drawAsPath || glyph.GlyphID >= 0
|
||||
var glyphPath *canvas.Path
|
||||
var glyphWidth float64
|
||||
if drawAsGlyphPath {
|
||||
glyphPath, glyphWidth = r.cachedTextGlyphPath(face, glyph)
|
||||
} else {
|
||||
glyphWidth = textGlyphWidth(face, glyph)
|
||||
}
|
||||
if i < len(xs) {
|
||||
cx = xs[i]
|
||||
} else if i > 0 {
|
||||
if dx, ok := textDelta(dxs, i-1); ok {
|
||||
cx += dx
|
||||
} else if len(dys) == 0 {
|
||||
cx += glyphWidth * hScale
|
||||
}
|
||||
}
|
||||
if i < len(ys) {
|
||||
cy = ys[i]
|
||||
} else if i > 0 {
|
||||
if dy, ok := textDelta(dys, i-1); ok {
|
||||
cy += dy
|
||||
}
|
||||
}
|
||||
var canvasX, canvasY float64
|
||||
if boundaryInCTM && parentCTM != nil {
|
||||
tx, ty := localCTM.Transform(cx, cy)
|
||||
tx, ty = parentCTM.Transform(tx+bx, ty+by)
|
||||
canvasX, canvasY = tx, pageH-ty
|
||||
} else {
|
||||
tx, ty := ctm.Transform(cx, cy)
|
||||
canvasX, canvasY = tx+bx, pageH-(ty+by)
|
||||
}
|
||||
textWidth := glyphWidth * hScale
|
||||
glyphFillPaint := fillPaint
|
||||
if useGlyphFillPaint {
|
||||
glyphFillPaint = parseFillPaint(fillColorNode, bx, by, pageH, canvasX, canvasY)
|
||||
}
|
||||
advanceLimit := textGlyphAdvanceLimit(dxs, dys, xs, i, len(glyphs), cx)
|
||||
if glyphFillPaint != nil {
|
||||
ctx.SetFill(glyphFillPaint)
|
||||
if clipPath != nil {
|
||||
scaleX := hScale
|
||||
if advanceLimit > 0 && glyphWidth*scaleX > advanceLimit {
|
||||
scaleX = advanceLimit / glyphWidth
|
||||
}
|
||||
textWidth = glyphWidth * scaleX
|
||||
textTransform := canvas.Identity.Translate(canvasX, canvasY)
|
||||
if useTextMatrix {
|
||||
textTransform = textTransform.Mul(textMatrix(ctm))
|
||||
}
|
||||
glyphPath = applyClipPath(glyphPath.Copy().Transform(textTransform.Scale(scaleX, 1)), clipPath)
|
||||
ctx.DrawPath(0, 0, glyphPath)
|
||||
if hasUnderline {
|
||||
uw := sizeMM * 0.05
|
||||
off := sizeMM * 0.1
|
||||
underline := &canvas.Path{}
|
||||
underline.MoveTo(0, -off)
|
||||
underline.LineTo(textWidth, -off)
|
||||
underline = underline.Stroke(uw, canvas.ButtCap, canvas.MiterJoin, canvas.Tolerance)
|
||||
underline = applyClipPath(underline.Transform(textTransform), clipPath)
|
||||
ctx.SetFillColor(fillColor)
|
||||
ctx.SetStrokeColor(canvas.Transparent)
|
||||
ctx.DrawPath(0, 0, underline)
|
||||
}
|
||||
continue
|
||||
}
|
||||
drawGlyph := func(x, y float64) {
|
||||
if drawAsGlyphPath {
|
||||
scaleX := hScale
|
||||
if advanceLimit > 0 && glyphWidth*scaleX > advanceLimit {
|
||||
scaleX = advanceLimit / glyphWidth
|
||||
}
|
||||
textWidth = glyphWidth * scaleX
|
||||
if scaleX != 1 {
|
||||
ctx.Push()
|
||||
ctx.Translate(x, y)
|
||||
ctx.Scale(scaleX, 1)
|
||||
ctx.DrawPath(0, 0, glyphPath)
|
||||
ctx.Pop()
|
||||
} else {
|
||||
ctx.DrawPath(x, y, glyphPath)
|
||||
}
|
||||
} else {
|
||||
scaled := hScale != 1
|
||||
if scaled {
|
||||
ctx.Push()
|
||||
ctx.Translate(x, y)
|
||||
ctx.Scale(hScale, 1)
|
||||
x, y = 0, 0
|
||||
}
|
||||
textFace := face
|
||||
if useGlyphFillPaint {
|
||||
textFace = ff.Face(sizePt, glyphFillPaint, fontStyle, canvas.FontNormal)
|
||||
}
|
||||
text := canvas.NewTextLine(textFace, str, canvas.Left)
|
||||
ctx.DrawText(x, y, text)
|
||||
if scaled {
|
||||
ctx.Pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
if useTextMatrix {
|
||||
ctx.Push()
|
||||
ctx.Translate(canvasX, canvasY)
|
||||
ctx.ComposeView(textMatrix(ctm))
|
||||
drawGlyph(0, 0)
|
||||
if hasUnderline {
|
||||
uw := sizeMM * 0.05
|
||||
ctx.SetStrokeWidth(uw)
|
||||
ctx.SetStrokeColor(fillColor)
|
||||
off := sizeMM * 0.1
|
||||
ctx.MoveTo(0, -off)
|
||||
ctx.LineTo(textWidth, -off)
|
||||
ctx.Stroke()
|
||||
}
|
||||
ctx.Pop()
|
||||
continue
|
||||
}
|
||||
drawGlyph(canvasX, canvasY)
|
||||
}
|
||||
if hasUnderline {
|
||||
uw := sizeMM * 0.05
|
||||
ctx.SetStrokeWidth(uw)
|
||||
ctx.SetStrokeColor(fillColor)
|
||||
off := sizeMM * 0.1
|
||||
ctx.MoveTo(canvasX, canvasY-off)
|
||||
ctx.LineTo(canvasX+textWidth, canvasY-off)
|
||||
ctx.Stroke()
|
||||
}
|
||||
}
|
||||
codePos += len(runes)
|
||||
}
|
||||
ctx.Pop()
|
||||
}
|
||||
@@ -104,4 +104,5 @@ type CompositeGraphicUnit struct {
|
||||
ImageObject []ImageObject `xml:"ImageObject"`
|
||||
CompositeGraphicUnit []CompositeGraphicUnit `xml:"CompositeGraphicUnit"`
|
||||
Clips *Clips `xml:"Clips"`
|
||||
Actions []Action `xml:"Actions>Action"`
|
||||
}
|
||||
|
||||
+142
-13
@@ -19,20 +19,24 @@ import (
|
||||
"encoding/asn1"
|
||||
"encoding/binary"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SignType 签名类型
|
||||
type SignType string
|
||||
|
||||
const (
|
||||
SignTypeSeal = "Seal"
|
||||
SignTypeSign = "Sign"
|
||||
// SignTypeSeal 印章签名
|
||||
SignTypeSeal SignType = "Seal"
|
||||
// SignTypeSign 数字签名
|
||||
SignTypeSign SignType = "Sign"
|
||||
)
|
||||
|
||||
// Signatures 签名列表
|
||||
type Signatures struct {
|
||||
XMLName xml.Name `xml:"Signatures"`
|
||||
MaxSignId string `xml:"MaxSignId"`
|
||||
MaxSignID string `xml:"MaxSignId"`
|
||||
List []Signature `xml:"Signature"`
|
||||
}
|
||||
|
||||
@@ -47,16 +51,111 @@ type Signature struct {
|
||||
type SignatureFile struct {
|
||||
XMLName xml.Name `xml:"Signature"`
|
||||
SignedValue string `xml:"SignedValue"`
|
||||
SignedInfo struct {
|
||||
Seal struct {
|
||||
SignedInfo SignedInfo
|
||||
}
|
||||
|
||||
// SignedInfo 签名信息
|
||||
type SignedInfo struct {
|
||||
Provider SignatureProvider `xml:"Provider"`
|
||||
SignatureMethod string `xml:"SignatureMethod"`
|
||||
SignatureDateTime string `xml:"SignatureDateTime"`
|
||||
Seal SignatureSeal `xml:"Seal"`
|
||||
StampAnnot []SignatureStamp `xml:"StampAnnot"`
|
||||
References SignatureReferences `xml:"References"`
|
||||
}
|
||||
|
||||
// SignatureProvider 签名提供者信息
|
||||
type SignatureProvider struct {
|
||||
ProviderName string `xml:"ProviderName,attr"`
|
||||
Company string `xml:"Company,attr"`
|
||||
Version string `xml:"Version,attr"`
|
||||
}
|
||||
|
||||
// SignatureSeal 签名印章引用
|
||||
type SignatureSeal struct {
|
||||
BaseLoc string `xml:"BaseLoc"`
|
||||
} `xml:"Seal"`
|
||||
StampAnnot []struct {
|
||||
}
|
||||
|
||||
// SignatureStamp 签名印章注释
|
||||
type SignatureStamp struct {
|
||||
ID string `xml:"ID,attr"`
|
||||
PageRef string `xml:"PageRef,attr"`
|
||||
Boundary string `xml:"Boundary,attr"`
|
||||
} `xml:"StampAnnot"`
|
||||
} `xml:"SignedInfo"`
|
||||
Clip string `xml:"Clip,attr"`
|
||||
}
|
||||
|
||||
// SignatureStampPosition 签名外观位置信息
|
||||
type SignatureStampPosition struct {
|
||||
ID string
|
||||
Page int
|
||||
PageID string
|
||||
Boundary string
|
||||
Box Box
|
||||
Clip string
|
||||
ClipBox *Box
|
||||
}
|
||||
|
||||
// SignatureReferences 签名保护文件列表
|
||||
type SignatureReferences struct {
|
||||
CheckMethod string `xml:"CheckMethod,attr"`
|
||||
Reference []SignatureReference `xml:"Reference"`
|
||||
}
|
||||
|
||||
// SignatureReference 签名保护文件引用
|
||||
type SignatureReference struct {
|
||||
FileRef string `xml:"FileRef,attr"`
|
||||
CheckValue string `xml:"CheckValue"`
|
||||
}
|
||||
|
||||
// SignatureStampPositions 获取签名外观位置信息
|
||||
// 入参: stamps 签名外观列表
|
||||
// 返回: []SignatureStampPosition 签名外观位置信息, error 错误信息
|
||||
func (r *Reader) SignatureStampPositions(stamps []SignatureStamp) ([]SignatureStampPosition, error) {
|
||||
doc, err := r.Doc()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pages := signaturePageNumbers(doc)
|
||||
positions := make([]SignatureStampPosition, 0, len(stamps))
|
||||
for _, stamp := range stamps {
|
||||
page, ok := pages[stamp.PageRef]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("signature stamp page not found: %s", stamp.PageRef)
|
||||
}
|
||||
box, err := parseSignatureStampBox(stamp.Boundary)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var clipBox *Box
|
||||
if stamp.Clip != "" {
|
||||
clip, err := parseSignatureStampBox(stamp.Clip)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clipBox = &clip
|
||||
}
|
||||
positions = append(positions, SignatureStampPosition{
|
||||
ID: stamp.ID,
|
||||
Page: page,
|
||||
PageID: stamp.PageRef,
|
||||
Boundary: stamp.Boundary,
|
||||
Box: box,
|
||||
Clip: stamp.Clip,
|
||||
ClipBox: clipBox,
|
||||
})
|
||||
}
|
||||
return positions, nil
|
||||
}
|
||||
|
||||
// parseSignatureStampBox 解析签名外观区域
|
||||
// 入参: s 区域字符串
|
||||
// 返回: Box 矩形对象, error 错误信息
|
||||
func parseSignatureStampBox(s string) (Box, error) {
|
||||
box, err := ParseBox(s)
|
||||
if err != nil {
|
||||
return Box{}, fmt.Errorf("invalid signature stamp box: %s", s)
|
||||
}
|
||||
return box, nil
|
||||
}
|
||||
|
||||
// parseSignatures 解析签名文件
|
||||
@@ -99,22 +198,50 @@ func (r *Reader) parseSignatures(doc *Document) error {
|
||||
if len(sealData) == 0 && sigFile.SignedValue != "" {
|
||||
signedValuePath := resolveResourcePath(sigPath, "", sigFile.SignedValue)
|
||||
if data, err := r.ResData(signedValuePath); err == nil {
|
||||
if sig, err := parseSESSignature(data); err == nil {
|
||||
sealType, sealData = sig.Seal.PicType, sig.Seal.PicData
|
||||
} else {
|
||||
sealType, sealData = extractSeal(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(sealData) == 0 {
|
||||
return
|
||||
}
|
||||
for _, annot := range sigFile.SignedInfo.StampAnnot {
|
||||
pageID := annot.PageRef
|
||||
bbox, _ := ParseBox(annot.Boundary)
|
||||
r.addStamp(pageID, bbox, sealType, sealData)
|
||||
bbox, err := parseSignatureStampBox(annot.Boundary)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var clipBox *Box
|
||||
if annot.Clip != "" {
|
||||
clip, err := parseSignatureStampBox(annot.Clip)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
clipBox = &clip
|
||||
}
|
||||
r.addStamp(pageID, bbox, clipBox, sealType, sealData)
|
||||
}
|
||||
}(sigRef)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// signaturePageNumbers 获取签名页面编号索引
|
||||
// 入参: doc 文档结构
|
||||
// 返回: map[string]int 页面编号索引
|
||||
func signaturePageNumbers(doc *Document) map[string]int {
|
||||
pages := make(map[string]int, len(doc.Pages.Page))
|
||||
for index, page := range doc.Pages.Page {
|
||||
if page.ID != "" {
|
||||
pages[page.ID] = index + 1
|
||||
}
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
// extractSeal 尝试提取印章数据
|
||||
// 入参: data 签名值数据
|
||||
// 返回: string 印章类型, []byte 印章数据
|
||||
@@ -297,18 +424,20 @@ func probeImageMedia(data []byte) (string, []byte) {
|
||||
// Stamp 印章信息结构
|
||||
type Stamp struct {
|
||||
Box Box
|
||||
Clip *Box
|
||||
Type string
|
||||
Data []byte
|
||||
}
|
||||
|
||||
// addStamp 添加印章到页面
|
||||
// 入参: pageID 页面ID, box 印章区域, sType 印章类型, data 印章数据
|
||||
func (r *Reader) addStamp(pageID string, box Box, sType string, data []byte) {
|
||||
// 入参: pageID 页面ID, box 印章区域, clip 裁剪区域, sType 印章类型, data 印章数据
|
||||
func (r *Reader) addStamp(pageID string, box Box, clip *Box, sType string, data []byte) {
|
||||
if r.Stamps == nil {
|
||||
r.Stamps = make(map[string][]Stamp)
|
||||
}
|
||||
r.Stamps[pageID] = append(r.Stamps[pageID], Stamp{
|
||||
Box: box,
|
||||
Clip: clip,
|
||||
Type: sType,
|
||||
Data: data,
|
||||
})
|
||||
|
||||
@@ -0,0 +1,639 @@
|
||||
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ofdgo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/asn1"
|
||||
"fmt"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
const (
|
||||
signContentData = "1.2.156.10197.6.1.4.2.1"
|
||||
signContentSignedData = "1.2.156.10197.6.1.4.2.2"
|
||||
signAttrMessageDigest = "1.2.840.113549.1.9.4"
|
||||
)
|
||||
|
||||
// digitalVerifyResult 数字签名验证结果
|
||||
type digitalVerifyResult struct {
|
||||
DataHashOK bool
|
||||
SignedOK bool
|
||||
CertOK bool
|
||||
SignerCerts [][]byte
|
||||
Certs [][]byte
|
||||
CertInfo SignatureCertInfo
|
||||
}
|
||||
|
||||
// gbtSignedData GB/T 35275 SignedData结构
|
||||
type gbtSignedData struct {
|
||||
ContentDigest []byte
|
||||
Certs []gbtCertificate
|
||||
Signers []gbtSignerInfo
|
||||
}
|
||||
|
||||
// gbtCertificate SignedData证书索引信息
|
||||
type gbtCertificate struct {
|
||||
Raw []byte
|
||||
Issuer []byte
|
||||
Serial *big.Int
|
||||
}
|
||||
|
||||
// gbtSignerInfo SignedData签名者信息
|
||||
type gbtSignerInfo struct {
|
||||
Issuer []byte
|
||||
Serial *big.Int
|
||||
DigestAlg string
|
||||
SignatureAlg string
|
||||
Signature []byte
|
||||
AuthAttrs []byte
|
||||
AttrDigest []byte
|
||||
}
|
||||
|
||||
// verifyDigitalSignature 验证OFD数字签名
|
||||
// 入参: method 签名算法, digestMethod 摘要算法, signedValue 签名值, signedData 被签名原文, options 验证选项
|
||||
// 返回: *digitalVerifyResult 验证结果, error 错误信息
|
||||
func verifyDigitalSignature(method, digestMethod string, signedValue, signedData []byte, options *signatureVerifyOptions) (*digitalVerifyResult, error) {
|
||||
if value, ok := normalizeGBT35275SignedValue(signedValue); ok {
|
||||
return verifyGBT35275SignedData(value, signedData, options)
|
||||
}
|
||||
if isSM2SignatureMethod(method) {
|
||||
return verifyRawDigitalSignature(signedValue, signedData, options)
|
||||
}
|
||||
if isRSASignatureMethod(method) || isECDSASignatureMethod(method) {
|
||||
return verifyRawPublicKeySignature(method, digestMethod, signedValue, signedData, options)
|
||||
}
|
||||
return nil, fmt.Errorf("unsupported signature method: %s", method)
|
||||
}
|
||||
|
||||
// verifyRawDigitalSignature 验证裸SM2数字签名
|
||||
// 入参: signedValue 签名值, signedData 被签名原文, options 验证选项
|
||||
// 返回: *digitalVerifyResult 验证结果, error 错误信息
|
||||
func verifyRawDigitalSignature(signedValue, signedData []byte, options *signatureVerifyOptions) (*digitalVerifyResult, error) {
|
||||
if len(options.SignCerts) == 0 {
|
||||
return nil, fmt.Errorf("signature certificate not found")
|
||||
}
|
||||
result := &digitalVerifyResult{DataHashOK: true}
|
||||
for _, cert := range options.SignCerts {
|
||||
pub, err := parseSM2PublicKeyFromCert(cert)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
result.CertOK = true
|
||||
result.SignerCerts = [][]byte{cert}
|
||||
result.CertInfo = signatureCertInfo(cert)
|
||||
if sm2VerifySignature(pub, nil, signedData, signedValue) {
|
||||
result.SignedOK = true
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// verifyRawPublicKeySignature 验证裸RSA或ECDSA数字签名
|
||||
// 入参: method 签名算法, digestMethod 摘要算法, signedValue 签名值, signedData 被签名原文, options 验证选项
|
||||
// 返回: *digitalVerifyResult 验证结果, error 错误信息
|
||||
func verifyRawPublicKeySignature(method, digestMethod string, signedValue, signedData []byte, options *signatureVerifyOptions) (*digitalVerifyResult, error) {
|
||||
if len(options.SignCerts) == 0 {
|
||||
return nil, fmt.Errorf("signature certificate not found")
|
||||
}
|
||||
if _, err := signatureMethodHash(method, digestMethod); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &digitalVerifyResult{DataHashOK: true}
|
||||
for _, cert := range options.SignCerts {
|
||||
ok, err := verifyPublicKeySignature(method, digestMethod, cert, signedData, signedValue)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
result.CertOK = true
|
||||
result.SignerCerts = [][]byte{cert}
|
||||
result.CertInfo = signatureCertInfo(cert)
|
||||
if ok {
|
||||
result.SignedOK = true
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// verifyGBT35275SignedData 验证GB/T 35275 SignedData签名值
|
||||
// 入参: signedValue 签名值, signedData 被签名原文, options 验证选项
|
||||
// 返回: *digitalVerifyResult 验证结果, error 错误信息
|
||||
func verifyGBT35275SignedData(signedValue, signedData []byte, options *signatureVerifyOptions) (*digitalVerifyResult, error) {
|
||||
sd, err := parseGBT35275SignedData(signedValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, cert := range options.SignCerts {
|
||||
c, err := parseGBTCertificate(cert)
|
||||
if err == nil {
|
||||
sd.Certs = append(sd.Certs, c)
|
||||
}
|
||||
}
|
||||
if len(sd.Signers) == 0 {
|
||||
return nil, fmt.Errorf("invalid signed data signer info")
|
||||
}
|
||||
result := &digitalVerifyResult{Certs: sd.rawCerts()}
|
||||
for _, signer := range sd.Signers {
|
||||
digest, err := signatureDigest(signer.DigestAlg, signedData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(sd.ContentDigest) != 0 && !bytes.Equal(sd.ContentDigest, digest) {
|
||||
return result, nil
|
||||
}
|
||||
result.DataHashOK = true
|
||||
plain := sd.ContentDigest
|
||||
if len(signer.AuthAttrs) != 0 {
|
||||
if !bytes.Equal(signer.AttrDigest, digest) {
|
||||
result.DataHashOK = false
|
||||
return result, nil
|
||||
}
|
||||
plain = signer.AuthAttrs
|
||||
}
|
||||
if len(plain) == 0 {
|
||||
return nil, fmt.Errorf("invalid signed data content")
|
||||
}
|
||||
cert := sd.findCert(signer.Issuer, signer.Serial)
|
||||
if cert == nil {
|
||||
result.CertOK = false
|
||||
return result, nil
|
||||
}
|
||||
result.SignerCerts = append(result.SignerCerts, cert.Raw)
|
||||
result.CertInfo = signatureCertInfo(cert.Raw)
|
||||
if isSM2SignatureMethod(signer.SignatureAlg) {
|
||||
pub, err := parseSM2PublicKeyFromCert(cert.Raw)
|
||||
if err != nil {
|
||||
result.CertOK = false
|
||||
return result, err
|
||||
}
|
||||
if !sm2VerifySignature(pub, nil, plain, signer.Signature) {
|
||||
result.CertOK = true
|
||||
return result, nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !isRSASignatureMethod(signer.SignatureAlg) && !isECDSASignatureMethod(signer.SignatureAlg) {
|
||||
return nil, fmt.Errorf("unsupported signature method: %s", signer.SignatureAlg)
|
||||
}
|
||||
ok, err := verifyPublicKeySignature(signer.SignatureAlg, signer.DigestAlg, cert.Raw, plain, signer.Signature)
|
||||
if err != nil {
|
||||
result.CertOK = false
|
||||
return result, err
|
||||
}
|
||||
if !ok {
|
||||
result.CertOK = true
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
result.CertOK = true
|
||||
result.SignedOK = true
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// normalizeGBT35275SignedValue 规范化GB/T 35275 SignedData编码
|
||||
// 入参: data 签名值数据
|
||||
// 返回: []byte 定长编码数据, bool 是否为SignedData
|
||||
func normalizeGBT35275SignedValue(data []byte) ([]byte, bool) {
|
||||
if contentType, ok := gbtContentType(data); ok {
|
||||
return data, contentType == signContentSignedData
|
||||
}
|
||||
der, err := berToDefinite(data)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
contentType, ok := gbtContentType(der)
|
||||
if !ok || contentType != signContentSignedData {
|
||||
return nil, false
|
||||
}
|
||||
return der, true
|
||||
}
|
||||
|
||||
// gbtContentType 读取GB/T 35275内容类型
|
||||
// 入参: data DER编码数据
|
||||
// 返回: string 内容类型, bool 是否完成解析
|
||||
func gbtContentType(data []byte) (string, bool) {
|
||||
var root asn1.RawValue
|
||||
rest, err := asn1.Unmarshal(data, &root)
|
||||
if err != nil || len(rest) != 0 || root.Tag != signASN1Sequence {
|
||||
return "", false
|
||||
}
|
||||
items, ok := asn1Children(root.Bytes)
|
||||
if !ok || len(items) == 0 {
|
||||
return "", false
|
||||
}
|
||||
if items[0].Tag != asn1.TagOID {
|
||||
return "", true
|
||||
}
|
||||
oid, err := asn1OIDString(items[0])
|
||||
return oid, err == nil
|
||||
}
|
||||
|
||||
// berToDefinite 将BER不定长编码转换为定长编码
|
||||
// 入参: data BER编码数据
|
||||
// 返回: []byte 定长编码数据, error 错误信息
|
||||
func berToDefinite(data []byte) ([]byte, error) {
|
||||
out, n, err := berValueToDefinite(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if n != len(data) {
|
||||
return nil, fmt.Errorf("invalid BER trailing data")
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// berValueToDefinite 转换单个BER编码值
|
||||
// 入参: data BER编码数据
|
||||
// 返回: []byte 定长编码数据, int 已读取长度, error 错误信息
|
||||
func berValueToDefinite(data []byte) ([]byte, int, error) {
|
||||
if len(data) < 2 {
|
||||
return nil, 0, fmt.Errorf("invalid BER value")
|
||||
}
|
||||
pos := 1
|
||||
if data[0]&0x1f == 0x1f {
|
||||
for pos < len(data) && data[pos]&0x80 != 0 {
|
||||
pos++
|
||||
}
|
||||
pos++
|
||||
}
|
||||
if pos >= len(data) {
|
||||
return nil, 0, fmt.Errorf("invalid BER tag")
|
||||
}
|
||||
tag := data[:pos]
|
||||
firstLength := data[pos]
|
||||
pos++
|
||||
if firstLength == 0x80 {
|
||||
if tag[0]&0x20 == 0 {
|
||||
return nil, 0, fmt.Errorf("invalid BER indefinite primitive")
|
||||
}
|
||||
var content []byte
|
||||
for {
|
||||
if len(data)-pos < 2 {
|
||||
return nil, 0, fmt.Errorf("invalid BER unterminated value")
|
||||
}
|
||||
if data[pos] == 0 && data[pos+1] == 0 {
|
||||
pos += 2
|
||||
break
|
||||
}
|
||||
child, n, err := berValueToDefinite(data[pos:])
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
content = append(content, child...)
|
||||
pos += n
|
||||
}
|
||||
return wrapBERValue(tag, content), pos, nil
|
||||
}
|
||||
length := uint64(firstLength)
|
||||
if firstLength&0x80 != 0 {
|
||||
n := int(firstLength & 0x7f)
|
||||
if n == 0 || n > 8 || len(data)-pos < n {
|
||||
return nil, 0, fmt.Errorf("invalid BER length")
|
||||
}
|
||||
length = 0
|
||||
for _, b := range data[pos : pos+n] {
|
||||
length = length<<8 | uint64(b)
|
||||
}
|
||||
pos += n
|
||||
}
|
||||
if length > uint64(len(data)-pos) {
|
||||
return nil, 0, fmt.Errorf("invalid BER truncated value")
|
||||
}
|
||||
end := pos + int(length)
|
||||
content := append([]byte(nil), data[pos:end]...)
|
||||
if tag[0]&0x20 != 0 {
|
||||
content = content[:0]
|
||||
for pos < end {
|
||||
child, n, err := berValueToDefinite(data[pos:end])
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
content = append(content, child...)
|
||||
pos += n
|
||||
}
|
||||
}
|
||||
return wrapBERValue(tag, content), end, nil
|
||||
}
|
||||
|
||||
// wrapBERValue 包装定长BER编码值
|
||||
// 入参: tag 标签, content 内容
|
||||
// 返回: []byte 定长编码数据
|
||||
func wrapBERValue(tag, content []byte) []byte {
|
||||
out := make([]byte, 0, len(tag)+len(content)+9)
|
||||
out = append(out, tag...)
|
||||
out = append(out, asn1LengthBytes(len(content))...)
|
||||
return append(out, content...)
|
||||
}
|
||||
|
||||
// parseGBT35275SignedData 解析GB/T 35275 SignedData
|
||||
// 入参: data 签名值数据
|
||||
// 返回: *gbtSignedData SignedData结构, error 错误信息
|
||||
func parseGBT35275SignedData(data []byte) (*gbtSignedData, error) {
|
||||
var ok bool
|
||||
data, ok = normalizeGBT35275SignedValue(data)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid signed data content type")
|
||||
}
|
||||
contentType, content, ok, err := parseGBTContentInfoBytes(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok || contentType != signContentSignedData {
|
||||
return nil, fmt.Errorf("invalid signed data content type")
|
||||
}
|
||||
items, ok := asn1Children(content.Bytes)
|
||||
if !ok || len(items) < 4 {
|
||||
return nil, fmt.Errorf("invalid signed data")
|
||||
}
|
||||
sd := &gbtSignedData{}
|
||||
contentOID, content, hasContent, err := parseGBTContentInfo(items[2])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if hasContent {
|
||||
if contentOID != signContentData {
|
||||
return nil, fmt.Errorf("invalid signed data inner content type")
|
||||
}
|
||||
if content.Tag == asn1.TagOctetString {
|
||||
sd.ContentDigest, err = asn1OctetString(content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := 3; i < len(items); i++ {
|
||||
item := items[i]
|
||||
if item.Class == asn1.ClassContextSpecific && item.Tag == 0 {
|
||||
certs, err := parseGBTCertificates(item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sd.Certs = certs
|
||||
continue
|
||||
}
|
||||
if item.Class == asn1.ClassContextSpecific && item.Tag == 1 {
|
||||
continue
|
||||
}
|
||||
signers, err := parseGBTSignerInfos(item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sd.Signers = signers
|
||||
}
|
||||
return sd, nil
|
||||
}
|
||||
|
||||
// parseGBTContentInfoBytes 解析ContentInfo字节
|
||||
// 入参: data DER编码数据
|
||||
// 返回: string 内容类型, asn1.RawValue 内容, bool 是否存在内容, error 错误信息
|
||||
func parseGBTContentInfoBytes(data []byte) (string, asn1.RawValue, bool, error) {
|
||||
var raw asn1.RawValue
|
||||
rest, err := asn1.Unmarshal(data, &raw)
|
||||
if err != nil || len(rest) != 0 {
|
||||
return "", asn1.RawValue{}, false, fmt.Errorf("invalid content info")
|
||||
}
|
||||
return parseGBTContentInfo(raw)
|
||||
}
|
||||
|
||||
// parseGBTContentInfo 解析ContentInfo结构
|
||||
// 入参: raw ASN.1原始值
|
||||
// 返回: string 内容类型, asn1.RawValue 内容, bool 是否存在内容, error 错误信息
|
||||
func parseGBTContentInfo(raw asn1.RawValue) (string, asn1.RawValue, bool, error) {
|
||||
items, ok := asn1Children(raw.Bytes)
|
||||
if !ok || len(items) == 0 {
|
||||
return "", asn1.RawValue{}, false, fmt.Errorf("invalid content info")
|
||||
}
|
||||
oid, err := asn1OIDString(items[0])
|
||||
if err != nil {
|
||||
return "", asn1.RawValue{}, false, err
|
||||
}
|
||||
if len(items) == 1 {
|
||||
return oid, asn1.RawValue{}, false, nil
|
||||
}
|
||||
content, err := asn1Explicit(items[1])
|
||||
if err != nil {
|
||||
return "", asn1.RawValue{}, false, err
|
||||
}
|
||||
return oid, content, true, nil
|
||||
}
|
||||
|
||||
// parseGBTCertificates 解析SignedData证书集合
|
||||
// 入参: raw ASN.1原始值
|
||||
// 返回: []gbtCertificate 证书列表, error 错误信息
|
||||
func parseGBTCertificates(raw asn1.RawValue) ([]gbtCertificate, error) {
|
||||
items, ok := asn1Children(raw.Bytes)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid signed data certificates")
|
||||
}
|
||||
certs := make([]gbtCertificate, 0, len(items))
|
||||
for _, item := range items {
|
||||
cert, err := parseGBTCertificate(item.FullBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certs = append(certs, cert)
|
||||
}
|
||||
return certs, nil
|
||||
}
|
||||
|
||||
// parseGBTCertificate 解析X.509证书索引字段
|
||||
// 入参: data DER编码证书
|
||||
// 返回: gbtCertificate 证书索引信息, error 错误信息
|
||||
func parseGBTCertificate(data []byte) (gbtCertificate, error) {
|
||||
cert, err := parseSignatureCertificate(data)
|
||||
if err != nil {
|
||||
return gbtCertificate{}, err
|
||||
}
|
||||
return gbtCertificate{
|
||||
Raw: cert.Raw,
|
||||
Issuer: cert.Issuer,
|
||||
Serial: cert.Serial,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseGBTSignerInfos 解析签名者信息集合
|
||||
// 入参: raw ASN.1原始值
|
||||
// 返回: []gbtSignerInfo 签名者列表, error 错误信息
|
||||
func parseGBTSignerInfos(raw asn1.RawValue) ([]gbtSignerInfo, error) {
|
||||
items, ok := asn1Children(raw.Bytes)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid signer infos")
|
||||
}
|
||||
signers := make([]gbtSignerInfo, 0, len(items))
|
||||
for _, item := range items {
|
||||
signer, err := parseGBTSignerInfo(item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signers = append(signers, signer)
|
||||
}
|
||||
return signers, nil
|
||||
}
|
||||
|
||||
// parseGBTSignerInfo 解析签名者信息
|
||||
// 入参: raw ASN.1原始值
|
||||
// 返回: gbtSignerInfo 签名者信息, error 错误信息
|
||||
func parseGBTSignerInfo(raw asn1.RawValue) (gbtSignerInfo, error) {
|
||||
items, ok := asn1Children(raw.Bytes)
|
||||
if !ok || len(items) < 5 {
|
||||
return gbtSignerInfo{}, fmt.Errorf("invalid signer info")
|
||||
}
|
||||
issuer, serial, err := parseGBTIssuerAndSerial(items[1])
|
||||
if err != nil {
|
||||
return gbtSignerInfo{}, err
|
||||
}
|
||||
digestAlg, err := parseGBTAlgorithm(items[2])
|
||||
if err != nil {
|
||||
return gbtSignerInfo{}, err
|
||||
}
|
||||
idx := 3
|
||||
var authAttrs []byte
|
||||
var attrDigest []byte
|
||||
if items[idx].Class == asn1.ClassContextSpecific && items[idx].Tag == 0 {
|
||||
authAttrs = asn1SetBytes(items[idx].Bytes)
|
||||
attrDigest, err = parseGBTMessageDigestAttr(items[idx])
|
||||
if err != nil {
|
||||
return gbtSignerInfo{}, err
|
||||
}
|
||||
idx++
|
||||
}
|
||||
if len(items) <= idx+1 {
|
||||
return gbtSignerInfo{}, fmt.Errorf("invalid signer info")
|
||||
}
|
||||
signatureAlg, err := parseGBTAlgorithm(items[idx])
|
||||
if err != nil {
|
||||
return gbtSignerInfo{}, err
|
||||
}
|
||||
signature, err := asn1OctetString(items[idx+1])
|
||||
if err != nil {
|
||||
return gbtSignerInfo{}, err
|
||||
}
|
||||
return gbtSignerInfo{
|
||||
Issuer: issuer,
|
||||
Serial: serial,
|
||||
DigestAlg: digestAlg,
|
||||
SignatureAlg: signatureAlg,
|
||||
Signature: signature,
|
||||
AuthAttrs: authAttrs,
|
||||
AttrDigest: attrDigest,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseGBTIssuerAndSerial 解析证书颁发者和序列号
|
||||
// 入参: raw ASN.1原始值
|
||||
// 返回: []byte 颁发者DN, *big.Int 序列号, error 错误信息
|
||||
func parseGBTIssuerAndSerial(raw asn1.RawValue) ([]byte, *big.Int, error) {
|
||||
items, ok := asn1Children(raw.Bytes)
|
||||
if !ok || len(items) < 2 {
|
||||
return nil, nil, fmt.Errorf("invalid issuer and serial")
|
||||
}
|
||||
serial, err := asn1IntegerBig(items[1])
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return append([]byte(nil), items[0].FullBytes...), serial, nil
|
||||
}
|
||||
|
||||
// parseGBTAlgorithm 解析算法标识
|
||||
// 入参: raw ASN.1原始值
|
||||
// 返回: string 算法OID, error 错误信息
|
||||
func parseGBTAlgorithm(raw asn1.RawValue) (string, error) {
|
||||
if raw.Tag == asn1.TagOID {
|
||||
return asn1OIDString(raw)
|
||||
}
|
||||
items, ok := asn1Children(raw.Bytes)
|
||||
if !ok || len(items) == 0 {
|
||||
return "", fmt.Errorf("invalid algorithm identifier")
|
||||
}
|
||||
return asn1OIDString(items[0])
|
||||
}
|
||||
|
||||
// parseGBTMessageDigestAttr 解析认证属性中的message-digest
|
||||
// 入参: raw ASN.1原始值
|
||||
// 返回: []byte 摘要值, error 错误信息
|
||||
func parseGBTMessageDigestAttr(raw asn1.RawValue) ([]byte, error) {
|
||||
attrs, ok := asn1Children(raw.Bytes)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid authenticated attributes")
|
||||
}
|
||||
for _, attr := range attrs {
|
||||
items, ok := asn1Children(attr.Bytes)
|
||||
if !ok || len(items) < 2 {
|
||||
continue
|
||||
}
|
||||
oid, err := asn1OIDString(items[0])
|
||||
if err != nil || oid != signAttrMessageDigest {
|
||||
continue
|
||||
}
|
||||
values, ok := asn1Children(items[1].Bytes)
|
||||
if !ok || len(values) == 0 {
|
||||
return nil, fmt.Errorf("invalid message digest attribute")
|
||||
}
|
||||
return asn1OctetString(values[0])
|
||||
}
|
||||
return nil, fmt.Errorf("message digest attribute not found")
|
||||
}
|
||||
|
||||
// findCert 查找签名者证书
|
||||
// 入参: issuer 颁发者DN, serial 证书序列号
|
||||
// 返回: *gbtCertificate 证书信息
|
||||
func (sd *gbtSignedData) findCert(issuer []byte, serial *big.Int) *gbtCertificate {
|
||||
for i := range sd.Certs {
|
||||
cert := &sd.Certs[i]
|
||||
if bytes.Equal(cert.Issuer, issuer) && cert.Serial.Cmp(serial) == 0 {
|
||||
return cert
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// rawCerts 获取SignedData证书原文
|
||||
// 返回: [][]byte 证书列表
|
||||
func (sd *gbtSignedData) rawCerts() [][]byte {
|
||||
certs := make([][]byte, 0, len(sd.Certs))
|
||||
for _, cert := range sd.Certs {
|
||||
certs = append(certs, cert.Raw)
|
||||
}
|
||||
return certs
|
||||
}
|
||||
|
||||
// asn1Explicit 解析显式标签内容
|
||||
// 入参: raw ASN.1原始值
|
||||
// 返回: asn1.RawValue 标签内容, error 错误信息
|
||||
func asn1Explicit(raw asn1.RawValue) (asn1.RawValue, error) {
|
||||
if raw.Class != asn1.ClassContextSpecific || raw.Tag != 0 || !raw.IsCompound {
|
||||
return asn1.RawValue{}, fmt.Errorf("invalid explicit content")
|
||||
}
|
||||
var out asn1.RawValue
|
||||
rest, err := asn1.Unmarshal(raw.Bytes, &out)
|
||||
if err != nil || len(rest) != 0 {
|
||||
return asn1.RawValue{}, fmt.Errorf("invalid explicit content")
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// asn1IntegerBig 解析ASN.1整数
|
||||
// 入参: raw ASN.1原始值
|
||||
// 返回: *big.Int 整数值, error 错误信息
|
||||
func asn1IntegerBig(raw asn1.RawValue) (*big.Int, error) {
|
||||
var out *big.Int
|
||||
rest, err := asn1.Unmarshal(raw.FullBytes, &out)
|
||||
if err != nil || len(rest) != 0 || out == nil {
|
||||
return nil, fmt.Errorf("invalid integer")
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -0,0 +1,742 @@
|
||||
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ofdgo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/asn1"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
signDigestSM3 = "1.2.156.10197.1.401"
|
||||
signDigestSM3NoKey = "1.2.156.10197.1.401.1"
|
||||
signDigestSM3Key = "1.2.156.10197.1.401.2"
|
||||
signMethodSM2SM3 = "1.2.156.10197.1.501"
|
||||
signMethodSM2SM3B = "1.2.156.10197.501"
|
||||
signMethodSM2Sign = "1.2.156.10197.1.301.1"
|
||||
signCurveSM2P256 = "1.2.156.10197.1.301"
|
||||
signECPublicKey = "1.2.840.10045.2.1"
|
||||
signASN1Sequence = 16
|
||||
signPublicKeySize = 65
|
||||
)
|
||||
|
||||
// sesSignature SES签章值
|
||||
type sesSignature struct {
|
||||
ToSign []byte
|
||||
Cert []byte
|
||||
SignAlg string
|
||||
Signature []byte
|
||||
DataHash []byte
|
||||
Time time.Time
|
||||
Seal *sesSeal
|
||||
}
|
||||
|
||||
// sesSeal SES电子印章
|
||||
type sesSeal struct {
|
||||
Raw []byte
|
||||
SignData []byte
|
||||
Cert []byte
|
||||
SignAlg string
|
||||
Signature []byte
|
||||
PicType string
|
||||
PicData []byte
|
||||
CertList sesCertList
|
||||
Info SignatureSealInfo
|
||||
}
|
||||
|
||||
// sesCertList SES印章证书列表
|
||||
type sesCertList struct {
|
||||
Certs [][]byte
|
||||
Digests []sesCertDigest
|
||||
}
|
||||
|
||||
// sesCertDigest SES印章证书摘要
|
||||
type sesCertDigest struct {
|
||||
Method string
|
||||
Value []byte
|
||||
}
|
||||
|
||||
// sesVerifyResult SES签章验证结果
|
||||
type sesVerifyResult struct {
|
||||
DataHashOK bool
|
||||
SignedOK bool
|
||||
SealOK bool
|
||||
CertOK bool
|
||||
SignCert SignatureCertInfo
|
||||
SealCert SignatureCertInfo
|
||||
SignCertRaw []byte
|
||||
SealCertRaw []byte
|
||||
SealRaw []byte
|
||||
Certs [][]byte
|
||||
SealType string
|
||||
SealInfo SignatureSealInfo
|
||||
SignatureTime time.Time
|
||||
}
|
||||
|
||||
// parseSESSignature 解析SES签章值
|
||||
// 入参: data 签章值数据
|
||||
// 返回: *sesSignature SES签章值, error 错误信息
|
||||
func parseSESSignature(data []byte) (*sesSignature, error) {
|
||||
var root asn1.RawValue
|
||||
rest, err := asn1.Unmarshal(data, &root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(rest) != 0 || root.Tag != signASN1Sequence || !root.IsCompound {
|
||||
return nil, fmt.Errorf("invalid ses signature")
|
||||
}
|
||||
items, ok := asn1Children(root.Bytes)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid ses signature items")
|
||||
}
|
||||
if len(items) == 2 {
|
||||
return parseSESSignatureV1(items)
|
||||
}
|
||||
if len(items) < 4 || len(items) > 5 {
|
||||
return nil, fmt.Errorf("invalid ses signature items")
|
||||
}
|
||||
cert, err := asn1OctetString(items[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
alg, err := asn1OIDString(items[2])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signature, err := asn1BitStringBytes(items[3])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tbsItems, ok := asn1Children(items[0].Bytes)
|
||||
if !ok || len(tbsItems) < 5 || len(tbsItems) > 6 {
|
||||
return nil, fmt.Errorf("invalid ses toSign")
|
||||
}
|
||||
seal, err := parseSESSeal(tbsItems[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
version, err := asn1Integer(tbsItems[0])
|
||||
if err != nil || version != seal.Info.Version {
|
||||
return nil, fmt.Errorf("invalid ses version")
|
||||
}
|
||||
signatureTime, err := asn1Time(tbsItems[2])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dataHash, err := asn1BitOrOctetBytes(tbsItems[3])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sesSignature{
|
||||
ToSign: append([]byte(nil), items[0].FullBytes...),
|
||||
Cert: cert,
|
||||
SignAlg: alg,
|
||||
Signature: signature,
|
||||
DataHash: dataHash,
|
||||
Time: signatureTime,
|
||||
Seal: seal,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseSESSignatureV1 解析SES V1签章值
|
||||
// 入参: items ASN.1子元素
|
||||
// 返回: *sesSignature SES签章值, error 错误信息
|
||||
func parseSESSignatureV1(items []asn1.RawValue) (*sesSignature, error) {
|
||||
signature, err := asn1BitStringBytes(items[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tbsItems, ok := asn1Children(items[0].Bytes)
|
||||
if !ok || len(tbsItems) != 7 {
|
||||
return nil, fmt.Errorf("invalid ses v1 toSign")
|
||||
}
|
||||
seal, err := parseSESSeal(tbsItems[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
version, err := asn1Integer(tbsItems[0])
|
||||
if err != nil || version != seal.Info.Version {
|
||||
return nil, fmt.Errorf("invalid ses version")
|
||||
}
|
||||
signatureTime := parseSESSignatureTimeV1(tbsItems[2])
|
||||
dataHash, err := asn1BitOrOctetBytes(tbsItems[3])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cert, err := asn1OctetString(tbsItems[5])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
alg, err := asn1OIDString(tbsItems[6])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sesSignature{
|
||||
ToSign: append([]byte(nil), items[0].FullBytes...),
|
||||
Cert: cert,
|
||||
SignAlg: alg,
|
||||
Signature: signature,
|
||||
DataHash: dataHash,
|
||||
Time: signatureTime,
|
||||
Seal: seal,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseSESSignatureTimeV1 解析SES V1签名时间
|
||||
// 入参: raw 签名时间或时间戳数据
|
||||
// 返回: time.Time 签名时间
|
||||
func parseSESSignatureTimeV1(raw asn1.RawValue) time.Time {
|
||||
data, err := asn1BitOrOctetBytes(raw)
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
if t := parseSignatureDateTime(string(data)); !t.IsZero() {
|
||||
return t
|
||||
}
|
||||
var value asn1.RawValue
|
||||
rest, err := asn1.Unmarshal(data, &value)
|
||||
if err != nil || len(rest) != 0 {
|
||||
return time.Time{}
|
||||
}
|
||||
t, _ := asn1Time(value)
|
||||
return t
|
||||
}
|
||||
|
||||
// verifySESSignature 验证SES签章值
|
||||
// 入参: data 签章值数据, signedData 被签名数据原文, options 验证选项
|
||||
// 返回: *sesVerifyResult 验证结果, error 错误信息
|
||||
func verifySESSignature(data, signedData []byte, options *signatureVerifyOptions) (*sesVerifyResult, error) {
|
||||
sig, err := parseSESSignature(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !isSM2SignatureMethod(sig.SignAlg) || !isSM2SignatureMethod(sig.Seal.SignAlg) {
|
||||
return nil, fmt.Errorf("unsupported signature method")
|
||||
}
|
||||
result := &sesVerifyResult{}
|
||||
result.SignCert = signatureCertInfo(sig.Cert)
|
||||
result.SealCert = signatureCertInfo(sig.Seal.Cert)
|
||||
result.SignCertRaw = sig.Cert
|
||||
result.SealCertRaw = sig.Seal.Cert
|
||||
result.SealRaw = sig.Seal.Raw
|
||||
result.Certs = append(result.Certs, sig.Seal.CertList.Certs...)
|
||||
result.Certs = append(result.Certs, options.SignCerts...)
|
||||
result.SealType = sig.Seal.PicType
|
||||
result.SealInfo = sig.Seal.Info
|
||||
result.SignatureTime = sig.Time
|
||||
result.DataHashOK = bytes.Equal(sig.DataHash, signSM3(signedData))
|
||||
signPub, err := parseSM2PublicKeyFromCert(sig.Cert)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
sealPub, err := parseSM2PublicKeyFromCert(sig.Seal.Cert)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
result.SignedOK = sm2VerifySignature(signPub, nil, sig.ToSign, sig.Signature)
|
||||
result.SealOK = sm2VerifySignature(sealPub, nil, sig.Seal.SignData, sig.Seal.Signature)
|
||||
result.CertOK = sesCertInList(sig.Cert, sig.Seal.CertList)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// parseSESSeal 解析SES电子印章
|
||||
// 入参: raw ASN.1原始值
|
||||
// 返回: *sesSeal SES电子印章, error 错误信息
|
||||
func parseSESSeal(raw asn1.RawValue) (*sesSeal, error) {
|
||||
items, ok := asn1Children(raw.Bytes)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid ses seal")
|
||||
}
|
||||
if len(items) == 2 {
|
||||
return parseSESSealV1(raw, items)
|
||||
}
|
||||
if len(items) < 4 || len(items) > 5 {
|
||||
return nil, fmt.Errorf("invalid ses seal")
|
||||
}
|
||||
cert, err := asn1OctetString(items[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
alg, err := asn1OIDString(items[2])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signature, err := asn1BitStringBytes(items[3])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
infoItems, ok := asn1Children(items[0].Bytes)
|
||||
if !ok || len(infoItems) < 4 || len(infoItems) > 5 {
|
||||
return nil, fmt.Errorf("invalid ses seal info")
|
||||
}
|
||||
info, certList, err := parseSESSealInfo(infoItems)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
picType, picData, err := parseSESPicture(infoItems[3])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sesSeal{
|
||||
Raw: append([]byte(nil), raw.FullBytes...),
|
||||
SignData: append([]byte(nil), items[0].FullBytes...),
|
||||
Cert: cert,
|
||||
SignAlg: alg,
|
||||
Signature: signature,
|
||||
PicType: picType,
|
||||
PicData: picData,
|
||||
CertList: certList,
|
||||
Info: info,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseSESSealV1 解析SES V1电子印章
|
||||
// 入参: raw ASN.1原始值, items ASN.1子元素
|
||||
// 返回: *sesSeal SES电子印章, error 错误信息
|
||||
func parseSESSealV1(raw asn1.RawValue, items []asn1.RawValue) (*sesSeal, error) {
|
||||
infoItems, ok := asn1Children(items[0].Bytes)
|
||||
if !ok || len(infoItems) < 4 || len(infoItems) > 5 {
|
||||
return nil, fmt.Errorf("invalid ses v1 seal info")
|
||||
}
|
||||
signItems, ok := asn1Children(items[1].Bytes)
|
||||
if !ok || len(signItems) != 3 {
|
||||
return nil, fmt.Errorf("invalid ses v1 sign info")
|
||||
}
|
||||
cert, err := asn1OctetString(signItems[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
alg, err := asn1OIDString(signItems[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signature, err := asn1BitStringBytes(signItems[2])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info, certList, err := parseSESSealInfo(infoItems)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
picType, picData, err := parseSESPicture(infoItems[3])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signData := asn1SequenceBytes(items[0].FullBytes, signItems[0].FullBytes, signItems[1].FullBytes)
|
||||
return &sesSeal{
|
||||
Raw: append([]byte(nil), raw.FullBytes...),
|
||||
SignData: signData,
|
||||
Cert: cert,
|
||||
SignAlg: alg,
|
||||
Signature: signature,
|
||||
PicType: picType,
|
||||
PicData: picData,
|
||||
CertList: certList,
|
||||
Info: info,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseSESSealInfo 解析电子印章信息
|
||||
// 入参: items 印章信息ASN.1子元素
|
||||
// 返回: SignatureSealInfo 印章信息, sesCertList 证书列表, error 错误信息
|
||||
func parseSESSealInfo(items []asn1.RawValue) (SignatureSealInfo, sesCertList, error) {
|
||||
if len(items) < 4 {
|
||||
return SignatureSealInfo{}, sesCertList{}, fmt.Errorf("invalid ses seal info")
|
||||
}
|
||||
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 解析印章证书列表
|
||||
// 入参: property 印章属性, version 印章版本, certIndex 证书列表位置
|
||||
// 返回: sesCertList 证书列表, error 错误信息
|
||||
func parseSESCertList(property []asn1.RawValue, version, certIndex int) (sesCertList, error) {
|
||||
if version == 1 {
|
||||
return parseSESCertListV1(property[certIndex])
|
||||
}
|
||||
if version < 4 {
|
||||
return parseSESCertInfoList(property[certIndex])
|
||||
}
|
||||
listType, err := asn1Integer(property[certIndex-1])
|
||||
if err != nil {
|
||||
return sesCertList{}, err
|
||||
}
|
||||
switch listType {
|
||||
case 1:
|
||||
return parseSESCertInfoList(property[certIndex])
|
||||
case 2:
|
||||
return parseSESCertDigestList(property[certIndex])
|
||||
default:
|
||||
return sesCertList{}, fmt.Errorf("unsupported ses cert list type")
|
||||
}
|
||||
}
|
||||
|
||||
// parseSESCertListV1 解析SES V1证书列表
|
||||
// 入参: raw 证书列表ASN.1值
|
||||
// 返回: sesCertList 证书列表, error 错误信息
|
||||
func parseSESCertListV1(raw asn1.RawValue) (sesCertList, error) {
|
||||
certs, ok := asn1Children(raw.Bytes)
|
||||
if !ok || len(certs) == 0 {
|
||||
return sesCertList{}, fmt.Errorf("invalid ses cert list")
|
||||
}
|
||||
list := sesCertList{Certs: make([][]byte, 0, len(certs))}
|
||||
for _, item := range certs {
|
||||
list.Certs = append(list.Certs, append([]byte(nil), item.FullBytes...))
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// parseSESCertInfoList 解析SES证书信息列表
|
||||
// 入参: raw 证书信息列表ASN.1值
|
||||
// 返回: sesCertList 证书列表, error 错误信息
|
||||
func parseSESCertInfoList(raw asn1.RawValue) (sesCertList, error) {
|
||||
certs, ok := asn1Children(raw.Bytes)
|
||||
if !ok || len(certs) == 0 {
|
||||
return sesCertList{}, fmt.Errorf("invalid ses cert list")
|
||||
}
|
||||
list := sesCertList{Certs: make([][]byte, 0, len(certs))}
|
||||
for _, item := range certs {
|
||||
cert, err := asn1OctetString(item)
|
||||
if err != nil {
|
||||
return sesCertList{}, err
|
||||
}
|
||||
list.Certs = append(list.Certs, cert)
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// parseSESCertDigestList 解析SES证书摘要列表
|
||||
// 入参: raw 证书摘要列表ASN.1值
|
||||
// 返回: sesCertList 证书列表, error 错误信息
|
||||
func parseSESCertDigestList(raw asn1.RawValue) (sesCertList, error) {
|
||||
items, ok := asn1Children(raw.Bytes)
|
||||
if !ok || len(items) == 0 {
|
||||
return sesCertList{}, fmt.Errorf("invalid ses cert digest list")
|
||||
}
|
||||
list := sesCertList{Digests: make([]sesCertDigest, 0, len(items))}
|
||||
for _, item := range items {
|
||||
fields, ok := asn1Children(item.Bytes)
|
||||
if !ok || len(fields) != 2 {
|
||||
return sesCertList{}, fmt.Errorf("invalid ses cert digest")
|
||||
}
|
||||
method := strings.TrimSpace(asn1String(fields[0]))
|
||||
if method == "" {
|
||||
return sesCertList{}, fmt.Errorf("invalid ses cert digest")
|
||||
}
|
||||
digest, err := asn1OctetString(fields[1])
|
||||
if err != nil {
|
||||
return sesCertList{}, err
|
||||
}
|
||||
list.Digests = append(list.Digests, sesCertDigest{Method: method, Value: digest})
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// parseSESPicture 解析印章图片
|
||||
// 入参: raw 印章图片信息
|
||||
// 返回: string 图片类型, []byte 图片数据, error 错误信息
|
||||
func parseSESPicture(raw asn1.RawValue) (string, []byte, error) {
|
||||
items, ok := asn1Children(raw.Bytes)
|
||||
if !ok || len(items) < 4 {
|
||||
return "", nil, fmt.Errorf("invalid ses picture")
|
||||
}
|
||||
picType := normalizeSealType(strings.ToLower(strings.TrimSpace(asn1String(items[0]))))
|
||||
picData, err := asn1OctetString(items[1])
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
if picType == "ofd" {
|
||||
if data := trimOFDPackage(picData); len(data) > 0 {
|
||||
picData = data
|
||||
}
|
||||
}
|
||||
return picType, picData, nil
|
||||
}
|
||||
|
||||
// parseSM2PublicKeyFromCert 从证书解析SM2公钥
|
||||
// 入参: data DER编码证书
|
||||
// 返回: sm2PublicKey SM2公钥, error 错误信息
|
||||
func parseSM2PublicKeyFromCert(data []byte) (sm2PublicKey, error) {
|
||||
cert, err := parseSignatureCertificate(data)
|
||||
if err != nil {
|
||||
return sm2PublicKey{}, err
|
||||
}
|
||||
return parseSM2PublicKeyInfo(cert.PublicKey)
|
||||
}
|
||||
|
||||
// parseSM2PublicKeyInfo 解析SM2公钥信息
|
||||
// 入参: raw SubjectPublicKeyInfo原始值
|
||||
// 返回: sm2PublicKey SM2公钥, error 错误信息
|
||||
func parseSM2PublicKeyInfo(raw asn1.RawValue) (sm2PublicKey, error) {
|
||||
var spki struct {
|
||||
Algorithm asn1.RawValue
|
||||
SubjectPublicKey asn1.BitString
|
||||
}
|
||||
rest, err := asn1.Unmarshal(raw.FullBytes, &spki)
|
||||
if err != nil {
|
||||
return sm2PublicKey{}, err
|
||||
}
|
||||
if len(rest) != 0 {
|
||||
return sm2PublicKey{}, fmt.Errorf("invalid subject public key info")
|
||||
}
|
||||
algItems, ok := asn1Children(spki.Algorithm.Bytes)
|
||||
if !ok || len(algItems) < 2 {
|
||||
return sm2PublicKey{}, fmt.Errorf("invalid public key algorithm")
|
||||
}
|
||||
alg, err := asn1OIDString(algItems[0])
|
||||
if err != nil {
|
||||
return sm2PublicKey{}, err
|
||||
}
|
||||
curve, err := asn1OIDString(algItems[1])
|
||||
if err != nil {
|
||||
return sm2PublicKey{}, err
|
||||
}
|
||||
if alg != signECPublicKey || curve != signCurveSM2P256 {
|
||||
return sm2PublicKey{}, fmt.Errorf("unsupported public key algorithm")
|
||||
}
|
||||
key := spki.SubjectPublicKey.Bytes
|
||||
if len(key) != signPublicKeySize || key[0] != 4 {
|
||||
return sm2PublicKey{}, fmt.Errorf("invalid sm2 public key")
|
||||
}
|
||||
return sm2PublicKey{
|
||||
X: new(big.Int).SetBytes(key[1:33]),
|
||||
Y: new(big.Int).SetBytes(key[33:65]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// signSM3 计算SM3摘要
|
||||
// 入参: data 原文数据
|
||||
// 返回: []byte 摘要值
|
||||
func signSM3(data []byte) []byte {
|
||||
h := newSM3()
|
||||
h.Write(data)
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
// isSM2SignatureMethod 判断是否为SM2签名算法
|
||||
// 入参: method 算法标识
|
||||
// 返回: bool 是否为SM2签名算法
|
||||
func isSM2SignatureMethod(method string) bool {
|
||||
switch signatureMethodText(method) {
|
||||
case signMethodSM2SM3, signMethodSM2SM3B, signMethodSM2Sign, "SM2", "SM2SM3", "SM3SM2", "SM2WITHSM3", "SM3WITHSM2":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// isSM3DigestMethod 判断是否为SM3摘要算法
|
||||
// 入参: method 算法标识
|
||||
// 返回: bool 是否为SM3摘要算法
|
||||
func isSM3DigestMethod(method string) bool {
|
||||
switch signatureMethodText(method) {
|
||||
case signDigestSM3, signDigestSM3NoKey, signDigestSM3Key, "SM3":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// sesCertInList 判断证书是否在印章证书列表中
|
||||
// 入参: cert DER编码证书, list 证书列表
|
||||
// 返回: bool 是否存在
|
||||
func sesCertInList(cert []byte, list sesCertList) bool {
|
||||
for _, item := range list.Certs {
|
||||
if bytes.Equal(cert, item) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, item := range list.Digests {
|
||||
digest, err := signatureDigest(item.Method, cert)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if bytes.Equal(digest, item.Value) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// asn1OctetString 解析ASN.1八位字符串
|
||||
// 入参: raw ASN.1原始值
|
||||
// 返回: []byte 字节数据, error 错误信息
|
||||
func asn1OctetString(raw asn1.RawValue) ([]byte, error) {
|
||||
var out []byte
|
||||
rest, err := asn1.Unmarshal(raw.FullBytes, &out)
|
||||
if err != nil || len(rest) != 0 {
|
||||
return nil, fmt.Errorf("invalid octet string")
|
||||
}
|
||||
return append([]byte(nil), out...), nil
|
||||
}
|
||||
|
||||
// asn1BitStringBytes 解析ASN.1位字符串
|
||||
// 入参: raw ASN.1原始值
|
||||
// 返回: []byte 字节数据, error 错误信息
|
||||
func asn1BitStringBytes(raw asn1.RawValue) ([]byte, error) {
|
||||
var bits asn1.BitString
|
||||
rest, err := asn1.Unmarshal(raw.FullBytes, &bits)
|
||||
if err != nil || len(rest) != 0 || bits.BitLength%8 != 0 {
|
||||
return nil, fmt.Errorf("invalid bit string")
|
||||
}
|
||||
return append([]byte(nil), bits.Bytes...), nil
|
||||
}
|
||||
|
||||
// asn1BitOrOctetBytes 解析ASN.1位字符串或八位字符串
|
||||
// 入参: raw ASN.1原始值
|
||||
// 返回: []byte 字节数据, error 错误信息
|
||||
func asn1BitOrOctetBytes(raw asn1.RawValue) ([]byte, error) {
|
||||
if raw.Tag == asn1.TagOctetString {
|
||||
return asn1OctetString(raw)
|
||||
}
|
||||
return asn1BitStringBytes(raw)
|
||||
}
|
||||
|
||||
// asn1OIDString 解析ASN.1对象标识符
|
||||
// 入参: raw ASN.1原始值
|
||||
// 返回: string OID字符串, error 错误信息
|
||||
func asn1OIDString(raw asn1.RawValue) (string, error) {
|
||||
var oid asn1.ObjectIdentifier
|
||||
rest, err := asn1.Unmarshal(raw.FullBytes, &oid)
|
||||
if err != nil || len(rest) != 0 {
|
||||
return "", fmt.Errorf("invalid oid")
|
||||
}
|
||||
return oid.String(), nil
|
||||
}
|
||||
|
||||
// asn1Integer 解析ASN.1整数
|
||||
// 入参: raw ASN.1原始值
|
||||
// 返回: int 整数值, error 错误信息
|
||||
func asn1Integer(raw asn1.RawValue) (int, error) {
|
||||
var out int
|
||||
rest, err := asn1.Unmarshal(raw.FullBytes, &out)
|
||||
if err != nil || len(rest) != 0 {
|
||||
return 0, fmt.Errorf("invalid integer")
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// asn1String 解析ASN.1字符串
|
||||
// 入参: raw ASN.1原始值
|
||||
// 返回: string 字符串
|
||||
func asn1String(raw asn1.RawValue) string {
|
||||
var s string
|
||||
if _, err := asn1.Unmarshal(raw.FullBytes, &s); err != nil {
|
||||
return strings.TrimSpace(string(raw.Bytes))
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// asn1SequenceBytes 编码ASN.1序列
|
||||
// 入参: parts 序列元素DER数据
|
||||
// 返回: []byte ASN.1序列DER数据
|
||||
func asn1SequenceBytes(parts ...[]byte) []byte {
|
||||
var content []byte
|
||||
for _, part := range parts {
|
||||
content = append(content, part...)
|
||||
}
|
||||
return asn1Wrap(0x30, content)
|
||||
}
|
||||
|
||||
// asn1SetBytes 编码ASN.1集合
|
||||
// 入参: content 集合内容DER数据
|
||||
// 返回: []byte ASN.1集合DER数据
|
||||
func asn1SetBytes(content []byte) []byte {
|
||||
return asn1Wrap(0x31, content)
|
||||
}
|
||||
|
||||
// asn1Wrap 包装ASN.1标签
|
||||
// 入参: tag ASN.1标签, content 内容DER数据
|
||||
// 返回: []byte ASN.1 DER数据
|
||||
func asn1Wrap(tag byte, content []byte) []byte {
|
||||
out := []byte{tag}
|
||||
out = append(out, asn1LengthBytes(len(content))...)
|
||||
out = append(out, content...)
|
||||
return out
|
||||
}
|
||||
|
||||
// asn1LengthBytes 编码ASN.1长度
|
||||
// 入参: n 内容长度
|
||||
// 返回: []byte ASN.1长度编码
|
||||
func asn1LengthBytes(n int) []byte {
|
||||
if n < 128 {
|
||||
return []byte{byte(n)}
|
||||
}
|
||||
var buf [8]byte
|
||||
i := len(buf)
|
||||
for n > 0 {
|
||||
i--
|
||||
buf[i] = byte(n)
|
||||
n >>= 8
|
||||
}
|
||||
out := []byte{0x80 | byte(len(buf)-i)}
|
||||
return append(out, buf[i:]...)
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
// 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/asn1"
|
||||
"encoding/binary"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
const sm2DefaultUserID = "1234567812345678"
|
||||
|
||||
var sm2P256 = newSM2P256()
|
||||
|
||||
// sm2PublicKey SM2公钥
|
||||
type sm2PublicKey struct {
|
||||
X *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椭圆曲线
|
||||
// 返回: *sm2Curve SM2椭圆曲线
|
||||
func newSM2P256() *sm2Curve {
|
||||
return &sm2Curve{
|
||||
P: sm2Big("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF"),
|
||||
N: sm2Big("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123"),
|
||||
B: sm2Big("28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93"),
|
||||
Gx: sm2Big("32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7"),
|
||||
Gy: sm2Big("BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0"),
|
||||
}
|
||||
}
|
||||
|
||||
// sm2Big 解析SM2大整数常量
|
||||
// 入参: s 十六进制字符串
|
||||
// 返回: *big.Int 大整数
|
||||
func sm2Big(s string) *big.Int {
|
||||
n, _ := new(big.Int).SetString(s, 16)
|
||||
return n
|
||||
}
|
||||
|
||||
// sm2VerifySignature 验证SM2签名值
|
||||
// 入参: pub 公钥, userID 用户标识, msg 原文, sig 签名值
|
||||
// 返回: bool 是否验证通过
|
||||
func sm2VerifySignature(pub sm2PublicKey, userID, msg, sig []byte) bool {
|
||||
r, s, ok := parseSM2Signature(sig)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return sm2Verify(pub, userID, msg, r, s)
|
||||
}
|
||||
|
||||
// parseSM2Signature 解析SM2签名值
|
||||
// 入参: sig 签名值
|
||||
// 返回: *big.Int R值, *big.Int S值, bool 是否解析成功
|
||||
func parseSM2Signature(sig []byte) (*big.Int, *big.Int, bool) {
|
||||
if len(sig) == 64 {
|
||||
return new(big.Int).SetBytes(sig[:32]), new(big.Int).SetBytes(sig[32:]), true
|
||||
}
|
||||
var rs struct {
|
||||
R *big.Int
|
||||
S *big.Int
|
||||
}
|
||||
rest, err := asn1.Unmarshal(sig, &rs)
|
||||
if err != nil || len(rest) != 0 || rs.R == nil || rs.S == nil {
|
||||
return nil, nil, false
|
||||
}
|
||||
return rs.R, rs.S, true
|
||||
}
|
||||
|
||||
// sm2Verify 验证SM2签名
|
||||
// 入参: pub 公钥, userID 用户标识, msg 原文, r R值, s S值
|
||||
// 返回: bool 是否验证通过
|
||||
func sm2Verify(pub sm2PublicKey, userID, msg []byte, r, s *big.Int) bool {
|
||||
n := sm2P256.N
|
||||
if r.Sign() <= 0 || s.Sign() <= 0 || r.Cmp(n) >= 0 || s.Cmp(n) >= 0 {
|
||||
return false
|
||||
}
|
||||
if pub.X == nil || pub.Y == nil || !sm2P256.isOnCurve(pub.X, pub.Y) {
|
||||
return false
|
||||
}
|
||||
e := new(big.Int).SetBytes(sm2MessageDigest(pub, userID, msg))
|
||||
t := new(big.Int).Add(r, s)
|
||||
t.Mod(t, n)
|
||||
if t.Sign() == 0 {
|
||||
return false
|
||||
}
|
||||
x, ok := sm2P256.combinedMult(pub.X, pub.Y, s, t)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
v := new(big.Int).Add(e, x)
|
||||
v.Mod(v, n)
|
||||
return v.Cmp(r) == 0
|
||||
}
|
||||
|
||||
// sm2MessageDigest 计算SM2签名摘要
|
||||
// 入参: pub 公钥, userID 用户标识, msg 原文
|
||||
// 返回: []byte 摘要值
|
||||
func sm2MessageDigest(pub sm2PublicKey, userID, msg []byte) []byte {
|
||||
h := newSM3()
|
||||
h.Write(sm2ZA(pub, userID))
|
||||
h.Write(msg)
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
// sm2ZA 计算SM2用户标识杂凑值
|
||||
// 入参: pub 公钥, userID 用户标识
|
||||
// 返回: []byte ZA值
|
||||
func sm2ZA(pub sm2PublicKey, userID []byte) []byte {
|
||||
if len(userID) == 0 {
|
||||
userID = []byte(sm2DefaultUserID)
|
||||
}
|
||||
h := newSM3()
|
||||
var entl [2]byte
|
||||
binary.BigEndian.PutUint16(entl[:], uint16(len(userID)*8))
|
||||
h.Write(entl[:])
|
||||
h.Write(userID)
|
||||
h.Write(sm2Fixed(sm2A()))
|
||||
h.Write(sm2Fixed(sm2P256.B))
|
||||
h.Write(sm2Fixed(sm2P256.Gx))
|
||||
h.Write(sm2Fixed(sm2P256.Gy))
|
||||
h.Write(sm2Fixed(pub.X))
|
||||
h.Write(sm2Fixed(pub.Y))
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
// sm2A 获取SM2曲线A参数
|
||||
// 返回: *big.Int 曲线A参数
|
||||
func sm2A() *big.Int {
|
||||
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固定长度字节
|
||||
// 入参: n 大整数
|
||||
// 返回: []byte 固定长度字节
|
||||
func sm2Fixed(n *big.Int) []byte {
|
||||
out := make([]byte, 32)
|
||||
if n == nil {
|
||||
return out
|
||||
}
|
||||
b := n.Bytes()
|
||||
if len(b) > len(out) {
|
||||
b = b[len(b)-len(out):]
|
||||
}
|
||||
copy(out[len(out)-len(b):], b)
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
// 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/binary"
|
||||
"hash"
|
||||
"math/bits"
|
||||
)
|
||||
|
||||
const (
|
||||
sm3Size = 32
|
||||
sm3BlockSize = 64
|
||||
)
|
||||
|
||||
var sm3Init = [8]uint32{
|
||||
0x7380166f,
|
||||
0x4914b2b9,
|
||||
0x172442d7,
|
||||
0xda8a0600,
|
||||
0xa96f30bc,
|
||||
0x163138aa,
|
||||
0xe38dee4d,
|
||||
0xb0fb0e4e,
|
||||
}
|
||||
|
||||
// sm3Digest SM3杂凑值计算器
|
||||
type sm3Digest struct {
|
||||
h [8]uint32
|
||||
x [sm3BlockSize]byte
|
||||
nx int
|
||||
len uint64
|
||||
}
|
||||
|
||||
// newSM3 创建SM3杂凑值计算器
|
||||
// 返回: hash.Hash SM3杂凑值计算器
|
||||
func newSM3() hash.Hash {
|
||||
d := new(sm3Digest)
|
||||
d.Reset()
|
||||
return d
|
||||
}
|
||||
|
||||
// Reset 重置SM3状态
|
||||
func (d *sm3Digest) Reset() {
|
||||
d.h = sm3Init
|
||||
d.nx = 0
|
||||
d.len = 0
|
||||
}
|
||||
|
||||
// Size 获取SM3杂凑值长度
|
||||
// 返回: int 杂凑值长度
|
||||
func (d *sm3Digest) Size() int {
|
||||
return sm3Size
|
||||
}
|
||||
|
||||
// BlockSize 获取SM3分组长度
|
||||
// 返回: int 分组长度
|
||||
func (d *sm3Digest) BlockSize() int {
|
||||
return sm3BlockSize
|
||||
}
|
||||
|
||||
// Write 写入待计算数据
|
||||
// 入参: p 待计算数据
|
||||
// 返回: int 写入长度, error 错误信息
|
||||
func (d *sm3Digest) Write(p []byte) (int, error) {
|
||||
nn := len(p)
|
||||
d.len += uint64(nn)
|
||||
if d.nx > 0 {
|
||||
n := copy(d.x[d.nx:], p)
|
||||
d.nx += n
|
||||
if d.nx == sm3BlockSize {
|
||||
sm3Block(d, d.x[:])
|
||||
d.nx = 0
|
||||
}
|
||||
p = p[n:]
|
||||
}
|
||||
if len(p) >= sm3BlockSize {
|
||||
n := len(p) &^ (sm3BlockSize - 1)
|
||||
sm3Block(d, p[:n])
|
||||
p = p[n:]
|
||||
}
|
||||
if len(p) > 0 {
|
||||
d.nx = copy(d.x[:], p)
|
||||
}
|
||||
return nn, nil
|
||||
}
|
||||
|
||||
// Sum 返回SM3杂凑值
|
||||
// 入参: in 前缀数据
|
||||
// 返回: []byte 杂凑值
|
||||
func (d *sm3Digest) Sum(in []byte) []byte {
|
||||
dd := *d
|
||||
hash := dd.checkSum()
|
||||
return append(in, hash[:]...)
|
||||
}
|
||||
|
||||
// checkSum 计算SM3最终杂凑值
|
||||
// 返回: [sm3Size]byte 杂凑值
|
||||
func (d *sm3Digest) checkSum() [sm3Size]byte {
|
||||
lenBits := d.len << 3
|
||||
var tmp [64]byte
|
||||
tmp[0] = 0x80
|
||||
if d.nx < 56 {
|
||||
d.Write(tmp[:56-d.nx])
|
||||
} else {
|
||||
d.Write(tmp[:64+56-d.nx])
|
||||
}
|
||||
binary.BigEndian.PutUint64(tmp[:8], lenBits)
|
||||
d.Write(tmp[:8])
|
||||
var digest [sm3Size]byte
|
||||
for i, v := range d.h {
|
||||
binary.BigEndian.PutUint32(digest[i*4:], v)
|
||||
}
|
||||
return digest
|
||||
}
|
||||
|
||||
// sm3Block 处理SM3消息分组
|
||||
// 入参: d SM3杂凑值计算器, p 消息分组数据
|
||||
func sm3Block(d *sm3Digest, p []byte) {
|
||||
var w [68]uint32
|
||||
var w1 [64]uint32
|
||||
for len(p) >= sm3BlockSize {
|
||||
for i := 0; i < 16; i++ {
|
||||
w[i] = binary.BigEndian.Uint32(p[i*4:])
|
||||
}
|
||||
for i := 16; i < 68; i++ {
|
||||
x := w[i-16] ^ w[i-9] ^ bits.RotateLeft32(w[i-3], 15)
|
||||
w[i] = sm3P1(x) ^ bits.RotateLeft32(w[i-13], 7) ^ w[i-6]
|
||||
}
|
||||
for i := 0; i < 64; i++ {
|
||||
w1[i] = w[i] ^ w[i+4]
|
||||
}
|
||||
a, b, c, e := d.h[0], d.h[1], d.h[2], d.h[4]
|
||||
dd, f, g, hh := d.h[3], d.h[5], d.h[6], d.h[7]
|
||||
for i := 0; i < 64; i++ {
|
||||
t := uint32(0x7a879d8a)
|
||||
if i < 16 {
|
||||
t = 0x79cc4519
|
||||
}
|
||||
ss1 := bits.RotateLeft32(bits.RotateLeft32(a, 12)+e+bits.RotateLeft32(t, i), 7)
|
||||
ss2 := ss1 ^ bits.RotateLeft32(a, 12)
|
||||
tt1 := sm3FF(i, a, b, c) + dd + ss2 + w1[i]
|
||||
tt2 := sm3GG(i, e, f, g) + hh + ss1 + w[i]
|
||||
dd = c
|
||||
c = bits.RotateLeft32(b, 9)
|
||||
b = a
|
||||
a = tt1
|
||||
hh = g
|
||||
g = bits.RotateLeft32(f, 19)
|
||||
f = e
|
||||
e = sm3P0(tt2)
|
||||
}
|
||||
d.h[0] ^= a
|
||||
d.h[1] ^= b
|
||||
d.h[2] ^= c
|
||||
d.h[3] ^= dd
|
||||
d.h[4] ^= e
|
||||
d.h[5] ^= f
|
||||
d.h[6] ^= g
|
||||
d.h[7] ^= hh
|
||||
p = p[sm3BlockSize:]
|
||||
}
|
||||
}
|
||||
|
||||
// sm3P0 计算SM3置换函数P0
|
||||
// 入参: x 输入值
|
||||
// 返回: uint32 置换结果
|
||||
func sm3P0(x uint32) uint32 {
|
||||
return x ^ bits.RotateLeft32(x, 9) ^ bits.RotateLeft32(x, 17)
|
||||
}
|
||||
|
||||
// sm3P1 计算SM3置换函数P1
|
||||
// 入参: x 输入值
|
||||
// 返回: uint32 置换结果
|
||||
func sm3P1(x uint32) uint32 {
|
||||
return x ^ bits.RotateLeft32(x, 15) ^ bits.RotateLeft32(x, 23)
|
||||
}
|
||||
|
||||
// sm3FF 计算SM3布尔函数FF
|
||||
// 入参: i 轮数, x 输入值, y 输入值, z 输入值
|
||||
// 返回: uint32 计算结果
|
||||
func sm3FF(i int, x, y, z uint32) uint32 {
|
||||
if i < 16 {
|
||||
return x ^ y ^ z
|
||||
}
|
||||
return (x & y) | (x & z) | (y & z)
|
||||
}
|
||||
|
||||
// sm3GG 计算SM3布尔函数GG
|
||||
// 入参: i 轮数, x 输入值, y 输入值, z 输入值
|
||||
// 返回: uint32 计算结果
|
||||
func sm3GG(i int, x, y, z uint32) uint32 {
|
||||
if i < 16 {
|
||||
return x ^ y ^ z
|
||||
}
|
||||
return (x & y) | (^x & z)
|
||||
}
|
||||
|
||||
var _ hash.Hash = (*sm3Digest)(nil)
|
||||
File diff suppressed because it is too large
Load Diff
+293
-304
@@ -15,333 +15,322 @@
|
||||
package ofdgo
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
canvastext "github.com/tdewolff/canvas/text"
|
||||
"github.com/tdewolff/font"
|
||||
)
|
||||
|
||||
// parseColor 解析颜色字符串
|
||||
// 入参: val 颜色值(R G B)
|
||||
// 返回: color.Color 颜色对象
|
||||
func parseColor(val string) color.Color {
|
||||
return parseColorWithAlpha(val, nil)
|
||||
// textGlyph 绘制字形
|
||||
type textGlyph struct {
|
||||
Text string
|
||||
GlyphID int
|
||||
}
|
||||
|
||||
// parseColorWithAlpha 解析带透明度的颜色
|
||||
// 入参: val 颜色值, alpha 透明度
|
||||
// 返回: color.Color 颜色对象
|
||||
func parseColorWithAlpha(val string, alpha *int) color.Color {
|
||||
parts := strings.Fields(val)
|
||||
if len(parts) >= 3 {
|
||||
r := parseColorComponent(parts[0])
|
||||
g := parseColorComponent(parts[1])
|
||||
b := parseColorComponent(parts[2])
|
||||
a := 255
|
||||
if alpha != nil {
|
||||
a = *alpha
|
||||
}
|
||||
a = clampColor(a)
|
||||
return color.RGBA{
|
||||
R: uint8(clampColor(r) * a / 255),
|
||||
G: uint8(clampColor(g) * a / 255),
|
||||
B: uint8(clampColor(b) * a / 255),
|
||||
A: uint8(a),
|
||||
}
|
||||
}
|
||||
return color.Black
|
||||
// textGlyphPathCacheKey 字形路径缓存键
|
||||
type textGlyphPathCacheKey struct {
|
||||
font *canvas.Font
|
||||
size float64
|
||||
fauxBold float64
|
||||
fauxItalic float64
|
||||
xOffset int32
|
||||
yOffset int32
|
||||
language string
|
||||
script canvastext.Script
|
||||
direction canvastext.Direction
|
||||
glyph textGlyph
|
||||
}
|
||||
|
||||
// parseColorComponent 解析颜色分量
|
||||
// 入参: s 颜色分量
|
||||
// 返回: int 颜色分量值
|
||||
func parseColorComponent(s string) int {
|
||||
s = strings.TrimSpace(s)
|
||||
if strings.HasPrefix(s, "#") {
|
||||
v, _ := strconv.ParseInt(strings.TrimPrefix(s, "#"), 16, 0)
|
||||
return int(v)
|
||||
}
|
||||
v, _ := strconv.Atoi(s)
|
||||
return v
|
||||
// textGlyphPathCacheValue 字形路径缓存值
|
||||
type textGlyphPathCacheValue struct {
|
||||
path *canvas.Path
|
||||
width float64
|
||||
}
|
||||
|
||||
// clampColor 限制颜色分量范围
|
||||
// 入参: v 颜色分量
|
||||
// 返回: int 颜色分量
|
||||
func clampColor(v int) int {
|
||||
if v < 0 {
|
||||
// textGlyphTransform 字符到字形变换
|
||||
type textGlyphTransform struct {
|
||||
CodeCount int
|
||||
Glyphs []textGlyph
|
||||
}
|
||||
|
||||
// textObjectFontID 获取文本对象字体ID
|
||||
// 入参: text 文本对象
|
||||
// 返回: string 字体ID
|
||||
func (r *Renderer) textObjectFontID(text TextObject) string {
|
||||
fontID := text.Font
|
||||
if fontID == "" && text.DrawParam != "" {
|
||||
if dp := r.getDrawParam(text.DrawParam, nil); dp != nil && dp.Font != "" {
|
||||
fontID = dp.Font
|
||||
}
|
||||
}
|
||||
return fontID
|
||||
}
|
||||
|
||||
// textObjectGlyphTransforms 获取文本对象的字形变换
|
||||
// 入参: fontID 字体ID, text 文本对象
|
||||
// 返回: map[int]textGlyphTransform 文本位置到字形变换的映射
|
||||
func (r *Renderer) textObjectGlyphTransforms(fontID string, text TextObject) map[int]textGlyphTransform {
|
||||
if fontID == "" || len(text.CGTransform) == 0 {
|
||||
return nil
|
||||
}
|
||||
result := make(map[int]textGlyphTransform, len(text.CGTransform))
|
||||
for _, transform := range text.CGTransform {
|
||||
ids := parseInts(transform.Glyphs)
|
||||
glyphCount := len(ids)
|
||||
if transform.GlyphCount > 0 && transform.GlyphCount < glyphCount {
|
||||
glyphCount = transform.GlyphCount
|
||||
}
|
||||
if glyphCount == 0 {
|
||||
continue
|
||||
}
|
||||
codeCount := transform.CodeCount
|
||||
if codeCount <= 0 {
|
||||
codeCount = 1
|
||||
}
|
||||
glyphs := make([]textGlyph, glyphCount)
|
||||
for i := range glyphs {
|
||||
glyphs[i] = r.textGlyphFromID(fontID, ids[i])
|
||||
}
|
||||
result[transform.CodePosition] = textGlyphTransform{
|
||||
CodeCount: codeCount,
|
||||
Glyphs: glyphs,
|
||||
}
|
||||
}
|
||||
if len(result) == 0 {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// textCodeGlyphs 获取文本编码对应的绘制字形
|
||||
// 入参: runes 文本字符, transforms 字形变换, codeOffset 文本编码偏移
|
||||
// 返回: []textGlyph 绘制字形列表
|
||||
func textCodeGlyphs(runes []rune, transforms map[int]textGlyphTransform, codeOffset int) []textGlyph {
|
||||
if len(transforms) == 0 {
|
||||
return textRuneGlyphs(runes)
|
||||
}
|
||||
glyphs := make([]textGlyph, 0, len(runes))
|
||||
for i := 0; i < len(runes); {
|
||||
if transform, ok := transforms[codeOffset+i]; ok && i+transform.CodeCount <= len(runes) {
|
||||
glyphs = append(glyphs, transform.Glyphs...)
|
||||
i += transform.CodeCount
|
||||
continue
|
||||
}
|
||||
glyphs = append(glyphs, textGlyph{Text: string(runes[i]), GlyphID: -1})
|
||||
i++
|
||||
}
|
||||
return glyphs
|
||||
}
|
||||
|
||||
// textRuneGlyphs 转换文本字符为绘制字形
|
||||
// 入参: runes 文本字符
|
||||
// 返回: []textGlyph 绘制字形列表
|
||||
func textRuneGlyphs(runes []rune) []textGlyph {
|
||||
glyphs := make([]textGlyph, len(runes))
|
||||
for i, r := range runes {
|
||||
glyphs[i] = textGlyph{Text: string(r), GlyphID: -1}
|
||||
}
|
||||
return glyphs
|
||||
}
|
||||
|
||||
// textGlyphFromID 获取字形ID对应的绘制字形
|
||||
// 入参: fontID 字体ID, glyphID 字形ID或CID
|
||||
// 返回: textGlyph 绘制字形
|
||||
func (r *Renderer) textGlyphFromID(fontID string, glyphID int) textGlyph {
|
||||
if mapped, ok := r.fontGlyphRune(fontID, glyphID); ok {
|
||||
return textGlyph{Text: string(mapped), GlyphID: -1}
|
||||
}
|
||||
return textGlyph{GlyphID: glyphID}
|
||||
}
|
||||
|
||||
// textGlyphWidth 获取绘制字形宽度
|
||||
// 入参: face 字体, glyph 绘制字形
|
||||
// 返回: float64 字形宽度
|
||||
func textGlyphWidth(face *canvas.FontFace, glyph textGlyph) float64 {
|
||||
if glyph.GlyphID >= 0 && glyph.GlyphID <= 0xFFFF {
|
||||
return face.MmPerEm * float64(face.Font.GlyphAdvance(uint16(glyph.GlyphID)))
|
||||
}
|
||||
return face.TextWidth(glyph.Text)
|
||||
}
|
||||
|
||||
// textGlyphPath 获取绘制字形路径
|
||||
// 入参: face 字体, glyph 绘制字形
|
||||
// 返回: *canvas.Path 字形路径, float64 字形宽度
|
||||
func textGlyphPath(face *canvas.FontFace, glyph textGlyph) (*canvas.Path, float64) {
|
||||
if glyph.GlyphID < 0 || glyph.GlyphID > 0xFFFF {
|
||||
return face.ToPath(glyph.Text)
|
||||
}
|
||||
p := &canvas.Path{}
|
||||
glyphID := uint16(glyph.GlyphID)
|
||||
_ = face.Font.GlyphPath(p, glyphID, face.PPEM(canvas.DefaultResolution), 0, 0, face.MmPerEm, font.NoHinting)
|
||||
if face.FauxBold != 0 {
|
||||
d := face.FauxBold * face.Size
|
||||
if face.Font.IsTrueType {
|
||||
d = -d
|
||||
}
|
||||
origFastStroke := canvas.FastStroke
|
||||
canvas.FastStroke = true
|
||||
p = p.Offset(d, canvas.Tolerance)
|
||||
canvas.FastStroke = origFastStroke
|
||||
}
|
||||
if face.FauxItalic != 0 {
|
||||
p = p.Transform(canvas.Identity.Shear(face.FauxItalic, 0))
|
||||
}
|
||||
return p, face.MmPerEm * float64(face.Font.GlyphAdvance(glyphID))
|
||||
}
|
||||
|
||||
// cachedTextGlyphPath 获取缓存的字形路径
|
||||
// 入参: face 字体, glyph 绘制字形
|
||||
// 返回: *canvas.Path 字形路径, float64 字形宽度
|
||||
func (r *Renderer) cachedTextGlyphPath(face *canvas.FontFace, glyph textGlyph) (*canvas.Path, float64) {
|
||||
key := textGlyphPathCacheKey{
|
||||
font: face.Font,
|
||||
size: face.Size,
|
||||
fauxBold: face.FauxBold,
|
||||
fauxItalic: face.FauxItalic,
|
||||
xOffset: face.XOffset,
|
||||
yOffset: face.YOffset,
|
||||
language: face.Language,
|
||||
script: face.Script,
|
||||
direction: face.Direction,
|
||||
glyph: glyph,
|
||||
}
|
||||
if cached, ok := r.textGlyphPathCache[key]; ok {
|
||||
return cached.path, cached.width
|
||||
}
|
||||
path, width := textGlyphPath(face, glyph)
|
||||
r.textGlyphPathCache[key] = textGlyphPathCacheValue{path: path, width: width}
|
||||
return path, width
|
||||
}
|
||||
|
||||
// hasTextMatrix 判断文本是否需要应用字形变换
|
||||
// 入参: ctm 变换矩阵
|
||||
// 返回: bool 是否需要变换
|
||||
func hasTextMatrix(ctm Matrix) bool {
|
||||
const eps = 1e-9
|
||||
return math.Abs(ctm.a-1) > eps || math.Abs(ctm.b) > eps || math.Abs(ctm.c) > eps || math.Abs(ctm.d-1) > eps
|
||||
}
|
||||
|
||||
// textMatrix 获取文本字形变换矩阵
|
||||
// 入参: ctm OFD变换矩阵
|
||||
// 返回: canvas.Matrix 画布变换矩阵
|
||||
func textMatrix(ctm Matrix) canvas.Matrix {
|
||||
return canvas.Matrix{
|
||||
{ctm.a, -ctm.c, 0},
|
||||
{-ctm.b, ctm.d, 0},
|
||||
}
|
||||
}
|
||||
|
||||
// fontGlyphRune 获取字形ID对应的包装字体字符
|
||||
// 入参: fontID 字体ID, glyphID 字形ID或CID
|
||||
// 返回: rune 包装字体字符, bool 是否存在
|
||||
func (r *Renderer) fontGlyphRune(fontID string, glyphID int) (rune, bool) {
|
||||
if glyphID < 0 || glyphID > 0xFFFF {
|
||||
return 0, false
|
||||
}
|
||||
id := uint16(glyphID)
|
||||
if r.FontCIDMap != nil {
|
||||
if mapping := r.FontCIDMap[fontID]; mapping != nil {
|
||||
if mapped, ok := mapping[id]; ok {
|
||||
return mapped, true
|
||||
}
|
||||
}
|
||||
}
|
||||
if r.FontGIDMap != nil {
|
||||
if mapping := r.FontGIDMap[fontID]; mapping != nil {
|
||||
if mapped, ok := mapping[id]; ok {
|
||||
return mapped, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// parseIndexRunes 解析索引字形
|
||||
// 入参: indexStr 索引字符串, fontID 字体ID
|
||||
// 返回: []rune 字形列表
|
||||
func (r *Renderer) parseIndexRunes(indexStr string, fontID string) []rune {
|
||||
parts := strings.Fields(indexStr)
|
||||
result := make([]rune, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
if startText, endText, ok := strings.Cut(p, "-"); ok {
|
||||
if !strings.Contains(endText, "-") {
|
||||
start, _ := strconv.Atoi(startText)
|
||||
end, _ := strconv.Atoi(endText)
|
||||
for k := start; k <= end; k++ {
|
||||
result = append(result, r.textIndexRune(fontID, k))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val, _ := strconv.Atoi(p)
|
||||
result = append(result, r.textIndexRune(fontID, val))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// textIndexRune 获取索引字形对应的字符
|
||||
// 入参: fontID 字体ID, glyphID 字形ID或CID
|
||||
// 返回: rune 字符
|
||||
func (r *Renderer) textIndexRune(fontID string, glyphID int) rune {
|
||||
if mapped, ok := r.fontGlyphRune(fontID, glyphID); ok {
|
||||
return mapped
|
||||
}
|
||||
return rune(glyphID)
|
||||
}
|
||||
|
||||
// textGlyphAdvanceLimit 获取显式字形推进宽度
|
||||
// 入参: dxs X方向偏移, dys Y方向偏移, xs X坐标列表, index 字形索引, count 字形数量, currentX 当前X坐标
|
||||
// 返回: float64 推进宽度
|
||||
func textGlyphAdvanceLimit(dxs, dys, xs []float64, index int, count int, currentX float64) float64 {
|
||||
if index+1 >= count || len(dys) > 0 {
|
||||
return 0
|
||||
}
|
||||
if v > 255 {
|
||||
return 255
|
||||
if index+1 < len(xs) {
|
||||
if advance := xs[index+1] - currentX; advance > 0 {
|
||||
return advance
|
||||
}
|
||||
return v
|
||||
}
|
||||
if advance, ok := textDelta(dxs, index); ok && advance > 0 {
|
||||
return advance
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// mergeAlpha 合并透明度
|
||||
// 入参: colorAlpha 颜色透明度, objectAlpha 对象透明度
|
||||
// 返回: *int 合并后的透明度
|
||||
func mergeAlpha(colorAlpha, objectAlpha *int) *int {
|
||||
if colorAlpha == nil && objectAlpha == nil {
|
||||
return nil
|
||||
}
|
||||
alpha := 255
|
||||
if colorAlpha != nil {
|
||||
alpha = *colorAlpha
|
||||
}
|
||||
if objectAlpha != nil {
|
||||
alpha = alpha * *objectAlpha / 255
|
||||
}
|
||||
alpha = clampColor(alpha)
|
||||
return &alpha
|
||||
// textCodePositioned 判断文本编码是否带显式定位
|
||||
// 入参: textCode 文本编码, xs X坐标列表, ys Y坐标列表
|
||||
// 返回: bool 是否带显式定位
|
||||
func textCodePositioned(textCode TextCode, xs, ys []float64) bool {
|
||||
return strings.TrimSpace(textCode.DeltaX) != "" ||
|
||||
strings.TrimSpace(textCode.DeltaY) != "" ||
|
||||
len(xs) > 1 ||
|
||||
len(ys) > 1
|
||||
}
|
||||
|
||||
// withFillAlpha 合并填充色透明度
|
||||
// 入参: fillColor 填充颜色节点, alpha 对象透明度
|
||||
// 返回: *FillColor 合并后的填充颜色节点
|
||||
func withFillAlpha(fillColor *FillColor, alpha *int) *FillColor {
|
||||
if fillColor == nil || alpha == nil {
|
||||
return fillColor
|
||||
// textDelta 获取文本偏移量
|
||||
// 入参: deltas 偏移量数组, index 偏移量索引
|
||||
// 返回: float64 偏移量, bool 是否存在
|
||||
func textDelta(deltas []float64, index int) (float64, bool) {
|
||||
if len(deltas) == 0 {
|
||||
return 0, false
|
||||
}
|
||||
merged := *fillColor
|
||||
merged.Alpha = mergeAlpha(fillColor.Alpha, alpha)
|
||||
return &merged
|
||||
if index < len(deltas) {
|
||||
return deltas[index], true
|
||||
}
|
||||
return deltas[len(deltas)-1], true
|
||||
}
|
||||
|
||||
// withStrokeAlpha 合并勾边色透明度
|
||||
// 入参: strokeColor 勾边颜色节点, alpha 对象透明度
|
||||
// 返回: *StrokeColor 合并后的勾边颜色节点
|
||||
func withStrokeAlpha(strokeColor *StrokeColor, alpha *int) *StrokeColor {
|
||||
if strokeColor == nil || alpha == nil {
|
||||
return strokeColor
|
||||
// textCodeRunes 获取文本编码字符
|
||||
// 入参: value 文本编码内容
|
||||
// 返回: []rune 文本字符
|
||||
func textCodeRunes(value string) []rune {
|
||||
if strings.ContainsAny(value, "\r\n") {
|
||||
value = strings.TrimSpace(value)
|
||||
}
|
||||
merged := *strokeColor
|
||||
merged.Alpha = mergeAlpha(strokeColor.Alpha, alpha)
|
||||
return &merged
|
||||
}
|
||||
|
||||
// colorWithAlpha 合并颜色透明度
|
||||
// 入参: c 颜色对象, alpha 对象透明度
|
||||
// 返回: color.Color 合并后的颜色对象
|
||||
func colorWithAlpha(c color.Color, alpha *int) color.Color {
|
||||
if c == nil || alpha == nil {
|
||||
return c
|
||||
}
|
||||
a := clampColor(*alpha)
|
||||
rgba := colorToRGBA(c)
|
||||
return color.RGBA{
|
||||
R: uint8(int(rgba.R) * a / 255),
|
||||
G: uint8(int(rgba.G) * a / 255),
|
||||
B: uint8(int(rgba.B) * a / 255),
|
||||
A: uint8(int(rgba.A) * a / 255),
|
||||
}
|
||||
}
|
||||
|
||||
// parseFillColor 解析填充颜色
|
||||
// 入参: fillColor 填充颜色节点
|
||||
// 返回: color.Color 颜色对象
|
||||
func parseFillColor(fillColor *FillColor) color.Color {
|
||||
if fillColor == nil {
|
||||
return nil
|
||||
}
|
||||
if fillColor.Pattern != nil {
|
||||
return nil
|
||||
}
|
||||
if fillColor.AxialShd != nil {
|
||||
return parseAxialShdColor(fillColor.AxialShd, fillColor.Alpha)
|
||||
}
|
||||
if fillColor.RadialShd != nil {
|
||||
return parseRadialShdColor(fillColor.RadialShd, fillColor.Alpha)
|
||||
}
|
||||
if strings.TrimSpace(fillColor.Value) != "" {
|
||||
return parseColorWithAlpha(fillColor.Value, fillColor.Alpha)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseFillPaint 解析填充画刷
|
||||
// 入参: fillColor 填充颜色节点, x X坐标, y Y坐标, pageH 页面高度, originX 原点X坐标, originY 原点Y坐标
|
||||
// 返回: any 填充画刷
|
||||
func parseFillPaint(fillColor *FillColor, x, y, pageH, originX, originY float64) any {
|
||||
if fillColor == nil {
|
||||
return nil
|
||||
}
|
||||
if fillColor.Pattern != nil {
|
||||
return nil
|
||||
}
|
||||
if gradient := parseAxialShdGradient(fillColor.AxialShd, fillColor.Alpha, x, y, pageH, originX, originY); gradient != nil {
|
||||
return gradient
|
||||
}
|
||||
if gradient := parseRadialShdGradient(fillColor.RadialShd, fillColor.Alpha, x, y, pageH, originX, originY); gradient != nil {
|
||||
return gradient
|
||||
}
|
||||
if fillColor.AxialShd != nil {
|
||||
return parseAxialShdColor(fillColor.AxialShd, fillColor.Alpha)
|
||||
}
|
||||
if fillColor.RadialShd != nil {
|
||||
return parseRadialShdColor(fillColor.RadialShd, fillColor.Alpha)
|
||||
}
|
||||
if strings.TrimSpace(fillColor.Value) != "" {
|
||||
return parseColorWithAlpha(fillColor.Value, fillColor.Alpha)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseStrokeColor 解析勾边颜色
|
||||
// 入参: strokeColor 勾边颜色节点
|
||||
// 返回: color.Color 颜色对象
|
||||
func parseStrokeColor(strokeColor *StrokeColor) color.Color {
|
||||
if strokeColor == nil {
|
||||
return nil
|
||||
}
|
||||
if strokeColor.AxialShd != nil {
|
||||
return parseAxialShdColor(strokeColor.AxialShd, strokeColor.Alpha)
|
||||
}
|
||||
if strokeColor.RadialShd != nil {
|
||||
return parseRadialShdColor(strokeColor.RadialShd, strokeColor.Alpha)
|
||||
}
|
||||
if strings.TrimSpace(strokeColor.Value) != "" {
|
||||
return parseColorWithAlpha(strokeColor.Value, strokeColor.Alpha)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseStrokePaint 解析勾边画刷
|
||||
// 入参: strokeColor 勾边颜色节点, x X坐标, y Y坐标, pageH 页面高度, originX 原点X坐标, originY 原点Y坐标
|
||||
// 返回: any 勾边画刷
|
||||
func parseStrokePaint(strokeColor *StrokeColor, x, y, pageH, originX, originY float64) any {
|
||||
if strokeColor == nil {
|
||||
return nil
|
||||
}
|
||||
if gradient := parseAxialShdGradient(strokeColor.AxialShd, strokeColor.Alpha, x, y, pageH, originX, originY); gradient != nil {
|
||||
return gradient
|
||||
}
|
||||
if gradient := parseRadialShdGradient(strokeColor.RadialShd, strokeColor.Alpha, x, y, pageH, originX, originY); gradient != nil {
|
||||
return gradient
|
||||
}
|
||||
if strokeColor.AxialShd != nil {
|
||||
return parseAxialShdColor(strokeColor.AxialShd, strokeColor.Alpha)
|
||||
}
|
||||
if strokeColor.RadialShd != nil {
|
||||
return parseRadialShdColor(strokeColor.RadialShd, strokeColor.Alpha)
|
||||
}
|
||||
if strings.TrimSpace(strokeColor.Value) != "" {
|
||||
return parseColorWithAlpha(strokeColor.Value, strokeColor.Alpha)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseAxialShdColor 解析轴向渐变颜色
|
||||
// 入参: axialShd 轴向渐变节点, alpha 透明度
|
||||
// 返回: color.Color 颜色对象
|
||||
func parseAxialShdColor(axialShd *AxialShd, alpha *int) color.Color {
|
||||
if axialShd != nil {
|
||||
for _, segment := range axialShd.Segment {
|
||||
if strings.TrimSpace(segment.Color.Value) == "" {
|
||||
continue
|
||||
}
|
||||
if alpha == nil {
|
||||
alpha = segment.Color.Alpha
|
||||
}
|
||||
return parseColorWithAlpha(segment.Color.Value, alpha)
|
||||
}
|
||||
}
|
||||
return color.Black
|
||||
}
|
||||
|
||||
// parseAxialShdGradient 解析轴向渐变
|
||||
// 入参: axialShd 轴向渐变节点, alpha 透明度, x X坐标, y Y坐标, pageH 页面高度, originX 原点X坐标, originY 原点Y坐标
|
||||
// 返回: canvas.Gradient 渐变对象
|
||||
func parseAxialShdGradient(axialShd *AxialShd, alpha *int, x, y, pageH, originX, originY float64) canvas.Gradient {
|
||||
if axialShd == nil {
|
||||
return nil
|
||||
}
|
||||
start := parseFloats(axialShd.StartPoint)
|
||||
end := parseFloats(axialShd.EndPoint)
|
||||
if len(start) < 2 || len(end) < 2 {
|
||||
return nil
|
||||
}
|
||||
grad := canvas.NewGradient()
|
||||
for _, segment := range axialShd.Segment {
|
||||
if strings.TrimSpace(segment.Color.Value) == "" {
|
||||
continue
|
||||
}
|
||||
segmentAlpha := alpha
|
||||
if segmentAlpha == nil {
|
||||
segmentAlpha = segment.Color.Alpha
|
||||
}
|
||||
grad.Add(segment.Position, colorToRGBA(parseColorWithAlpha(segment.Color.Value, segmentAlpha)))
|
||||
}
|
||||
if len(grad) == 0 {
|
||||
return nil
|
||||
}
|
||||
startPoint := canvas.Point{X: x + start[0] - originX, Y: pageH - (y + start[1]) - originY}
|
||||
endPoint := canvas.Point{X: x + end[0] - originX, Y: pageH - (y + end[1]) - originY}
|
||||
if startPoint.Equals(endPoint) {
|
||||
return nil
|
||||
}
|
||||
return grad.ToLinear(startPoint, endPoint)
|
||||
}
|
||||
|
||||
// parseRadialShdColor 解析径向渐变颜色
|
||||
// 入参: radialShd 径向渐变节点, alpha 透明度
|
||||
// 返回: color.Color 颜色对象
|
||||
func parseRadialShdColor(radialShd *RadialShd, alpha *int) color.Color {
|
||||
if radialShd != nil {
|
||||
for _, segment := range radialShd.Segment {
|
||||
if strings.TrimSpace(segment.Color.Value) == "" {
|
||||
continue
|
||||
}
|
||||
if alpha == nil {
|
||||
alpha = segment.Color.Alpha
|
||||
}
|
||||
return parseColorWithAlpha(segment.Color.Value, alpha)
|
||||
}
|
||||
}
|
||||
return color.Black
|
||||
}
|
||||
|
||||
// parseRadialShdGradient 解析径向渐变
|
||||
// 入参: radialShd 径向渐变节点, alpha 透明度, x X坐标, y Y坐标, pageH 页面高度, originX 原点X坐标, originY 原点Y坐标
|
||||
// 返回: canvas.Gradient 渐变对象
|
||||
func parseRadialShdGradient(radialShd *RadialShd, alpha *int, x, y, pageH, originX, originY float64) canvas.Gradient {
|
||||
if radialShd == nil || radialShd.EndRadius <= 0 {
|
||||
return nil
|
||||
}
|
||||
start := parseFloats(radialShd.StartPoint)
|
||||
end := parseFloats(radialShd.EndPoint)
|
||||
if len(start) < 2 || len(end) < 2 {
|
||||
return nil
|
||||
}
|
||||
grad := canvas.NewGradient()
|
||||
for _, segment := range radialShd.Segment {
|
||||
if strings.TrimSpace(segment.Color.Value) == "" {
|
||||
continue
|
||||
}
|
||||
segmentAlpha := alpha
|
||||
if segmentAlpha == nil {
|
||||
segmentAlpha = segment.Color.Alpha
|
||||
}
|
||||
grad.Add(segment.Position, colorToRGBA(parseColorWithAlpha(segment.Color.Value, segmentAlpha)))
|
||||
}
|
||||
if len(grad) == 0 {
|
||||
return nil
|
||||
}
|
||||
startPoint := canvas.Point{X: x + start[0] - originX, Y: pageH - (y + start[1]) - originY}
|
||||
endPoint := canvas.Point{X: x + end[0] - originX, Y: pageH - (y + end[1]) - originY}
|
||||
return grad.ToRadial(startPoint, radialShd.StartRadius, endPoint, radialShd.EndRadius)
|
||||
}
|
||||
|
||||
// colorToRGBA 转换颜色对象
|
||||
// 入参: c 颜色对象
|
||||
// 返回: color.RGBA RGBA颜色
|
||||
func colorToRGBA(c color.Color) color.RGBA {
|
||||
if rgba, ok := c.(color.RGBA); ok {
|
||||
return rgba
|
||||
}
|
||||
r, g, b, a := c.RGBA()
|
||||
return color.RGBA{R: uint8(r >> 8), G: uint8(g >> 8), B: uint8(b >> 8), A: uint8(a >> 8)}
|
||||
return []rune(value)
|
||||
}
|
||||
|
||||
// GetDeltaX 获取X轴偏移量数组
|
||||
|
||||
Reference in New Issue
Block a user