mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-08-30 12:12:40 +08:00
This commit is contained in:
+12
-3
@@ -10,11 +10,14 @@
|
|||||||
<div class="app">
|
<div class="app">
|
||||||
<header class="toolbar app-bar">
|
<header class="toolbar app-bar">
|
||||||
<input id="ofdInput" class="file-input" type="file" accept=".ofd,application/ofd,application/zip">
|
<input id="ofdInput" class="file-input" type="file" accept=".ofd,application/ofd,application/zip">
|
||||||
<button id="ofdButton" class="button primary" type="button" data-short="打开">打开</button>
|
|
||||||
<input id="fontInput" class="file-input" type="file" accept=".ttf,.otf,.ttc,font/*" multiple>
|
<input id="fontInput" class="file-input" type="file" accept=".ttf,.otf,.ttc,font/*" multiple>
|
||||||
|
<div class="tool-group">
|
||||||
|
<button id="ofdButton" class="button primary" type="button" data-short="打开">打开</button>
|
||||||
<button id="togglePagesButton" class="icon-button" type="button" title="页面栏" aria-label="页面栏" aria-pressed="true">☰</button>
|
<button id="togglePagesButton" class="icon-button" type="button" title="页面栏" aria-label="页面栏" aria-pressed="true">☰</button>
|
||||||
<button id="toggleMetaButton" class="icon-button" type="button" title="信息栏" aria-label="信息栏" aria-pressed="true">ⓘ</button>
|
<button id="toggleMetaButton" class="icon-button" type="button" title="信息栏" aria-label="信息栏" aria-pressed="true">ⓘ</button>
|
||||||
|
</div>
|
||||||
<div class="toolbar-space"></div>
|
<div class="toolbar-space"></div>
|
||||||
|
<div class="tool-group">
|
||||||
<button id="prevButton" class="icon-button" type="button" title="上一页" disabled>‹</button>
|
<button id="prevButton" class="icon-button" type="button" title="上一页" disabled>‹</button>
|
||||||
<label class="page-control">
|
<label class="page-control">
|
||||||
<input id="pageInput" type="number" min="1" value="1" disabled>
|
<input id="pageInput" type="number" min="1" value="1" disabled>
|
||||||
@@ -22,13 +25,19 @@
|
|||||||
<span id="pageTotal">0</span>
|
<span id="pageTotal">0</span>
|
||||||
</label>
|
</label>
|
||||||
<button id="nextButton" class="icon-button" type="button" title="下一页" disabled>›</button>
|
<button id="nextButton" class="icon-button" type="button" title="下一页" disabled>›</button>
|
||||||
<div class="divider"></div>
|
</div>
|
||||||
|
<div class="tool-group">
|
||||||
<button id="zoomOutButton" class="icon-button" type="button" title="缩小" disabled>-</button>
|
<button id="zoomOutButton" class="icon-button" type="button" title="缩小" disabled>-</button>
|
||||||
<span id="zoomLabel" class="zoom-label">100%</span>
|
<span id="zoomLabel" class="zoom-label">100%</span>
|
||||||
<button id="zoomInButton" class="icon-button" type="button" title="放大" disabled>+</button>
|
<button id="zoomInButton" class="icon-button" type="button" title="放大" disabled>+</button>
|
||||||
<button id="fitButton" class="button" type="button" data-short="宽" disabled>适宽</button>
|
<button id="fitButton" class="button" type="button" data-short="宽" disabled>适宽</button>
|
||||||
<button id="fitHeightButton" class="button" type="button" data-short="高" disabled>适高</button>
|
<button id="fitHeightButton" class="button" type="button" data-short="高" disabled>适高</button>
|
||||||
<button id="exportButton" class="button" type="button" data-short="PDF" title="导出 PDF" aria-label="导出 PDF" disabled>PDF</button>
|
</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>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="workspace">
|
<main class="workspace">
|
||||||
|
|||||||
+51
-5
@@ -32,7 +32,8 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
button,
|
button,
|
||||||
input {
|
input,
|
||||||
|
select {
|
||||||
font: inherit;
|
font: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,13 +46,15 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
button:disabled,
|
button:disabled,
|
||||||
input:disabled {
|
input:disabled,
|
||||||
|
select:disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
opacity: 0.48;
|
opacity: 0.48;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:focus-visible,
|
button:focus-visible,
|
||||||
input:focus-visible,
|
input:focus-visible,
|
||||||
|
select:focus-visible,
|
||||||
a:focus-visible {
|
a:focus-visible {
|
||||||
outline: 2px solid var(--focus);
|
outline: 2px solid var(--focus);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
@@ -149,10 +152,29 @@ button:not(:disabled):hover {
|
|||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider {
|
.tool-group {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-group + .tool-group {
|
||||||
|
margin-left: 2px;
|
||||||
|
padding-left: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-group + .tool-group::before {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 5px;
|
||||||
width: 1px;
|
width: 1px;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
background: var(--line);
|
background: var(--line);
|
||||||
|
content: "";
|
||||||
|
transform: translateY(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-control {
|
.page-control {
|
||||||
@@ -176,6 +198,16 @@ button:not(:disabled):hover {
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.format-select {
|
||||||
|
height: var(--control-height);
|
||||||
|
min-width: 64px;
|
||||||
|
padding: 0 7px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 0;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #1f2428;
|
||||||
|
}
|
||||||
|
|
||||||
.zoom-label {
|
.zoom-label {
|
||||||
min-width: 42px;
|
min-width: 42px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -842,11 +874,20 @@ body[aria-busy="true"] .viewer-panel {
|
|||||||
row-gap: 0;
|
row-gap: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider {
|
.toolbar-space {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar-space {
|
.tool-group {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-group + .tool-group {
|
||||||
|
margin-left: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-group + .tool-group::before {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -894,6 +935,11 @@ body[aria-busy="true"] .viewer-panel {
|
|||||||
padding-inline: 8px;
|
padding-inline: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.format-select {
|
||||||
|
min-width: 56px;
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.footer-links {
|
.footer-links {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|||||||
+111
-21
@@ -26,11 +26,14 @@ const STATUS = {
|
|||||||
engine: "正在准备引擎",
|
engine: "正在准备引擎",
|
||||||
recovering: "正在恢复引擎",
|
recovering: "正在恢复引擎",
|
||||||
fonts: "正在匹配字体",
|
fonts: "正在匹配字体",
|
||||||
exporting: "正在导出 PDF",
|
exporting: "正在导出文档 PDF",
|
||||||
|
pageExporting: "正在导出单页",
|
||||||
};
|
};
|
||||||
const WASM_CALLBACKS = [
|
const WASM_CALLBACKS = [
|
||||||
"ofdgoOpen",
|
"ofdgoOpen",
|
||||||
"ofdgoRenderPage",
|
"ofdgoRenderPage",
|
||||||
|
"ofdgoExportFormats",
|
||||||
|
"ofdgoExportPage",
|
||||||
"ofdgoExportPDF",
|
"ofdgoExportPDF",
|
||||||
"ofdgoFontCandidates",
|
"ofdgoFontCandidates",
|
||||||
];
|
];
|
||||||
@@ -63,6 +66,7 @@ const state = {
|
|||||||
thumbnailCache: new Map(),
|
thumbnailCache: new Map(),
|
||||||
thumbnailInFlight: new Set(),
|
thumbnailInFlight: new Set(),
|
||||||
thumbnailObserver: null,
|
thumbnailObserver: null,
|
||||||
|
exportFormats: [],
|
||||||
showPages: !COMPACT_LAYOUT.matches,
|
showPages: !COMPACT_LAYOUT.matches,
|
||||||
showMeta: !COMPACT_LAYOUT.matches,
|
showMeta: !COMPACT_LAYOUT.matches,
|
||||||
};
|
};
|
||||||
@@ -84,6 +88,8 @@ const el = {
|
|||||||
zoomLabel: document.querySelector("#zoomLabel"),
|
zoomLabel: document.querySelector("#zoomLabel"),
|
||||||
fitButton: document.querySelector("#fitButton"),
|
fitButton: document.querySelector("#fitButton"),
|
||||||
fitHeightButton: document.querySelector("#fitHeightButton"),
|
fitHeightButton: document.querySelector("#fitHeightButton"),
|
||||||
|
pageExportFormat: document.querySelector("#pageExportFormat"),
|
||||||
|
exportPageButton: document.querySelector("#exportPageButton"),
|
||||||
exportButton: document.querySelector("#exportButton"),
|
exportButton: document.querySelector("#exportButton"),
|
||||||
emptyState: document.querySelector("#emptyState"),
|
emptyState: document.querySelector("#emptyState"),
|
||||||
progressPanel: document.querySelector("#progressPanel"),
|
progressPanel: document.querySelector("#progressPanel"),
|
||||||
@@ -119,6 +125,7 @@ el.zoomOutButton.addEventListener("click", () => setScale(state.scale - 0.1));
|
|||||||
el.zoomInButton.addEventListener("click", () => setScale(state.scale + 0.1));
|
el.zoomInButton.addEventListener("click", () => setScale(state.scale + 0.1));
|
||||||
el.fitButton.addEventListener("click", fitWidth);
|
el.fitButton.addEventListener("click", fitWidth);
|
||||||
el.fitHeightButton.addEventListener("click", fitHeight);
|
el.fitHeightButton.addEventListener("click", fitHeight);
|
||||||
|
el.exportPageButton.addEventListener("click", exportCurrentPage);
|
||||||
el.exportButton.addEventListener("click", exportPDF);
|
el.exportButton.addEventListener("click", exportPDF);
|
||||||
el.pageInput.addEventListener("change", () => {
|
el.pageInput.addEventListener("change", () => {
|
||||||
const page = Number.parseInt(el.pageInput.value, 10);
|
const page = Number.parseInt(el.pageInput.value, 10);
|
||||||
@@ -180,6 +187,7 @@ async function boot() {
|
|||||||
try {
|
try {
|
||||||
await ensureWASM();
|
await ensureWASM();
|
||||||
setProgress("引擎准备完成", 70);
|
setProgress("引擎准备完成", 70);
|
||||||
|
loadExportFormats();
|
||||||
setEmpty("选择 OFD 文件");
|
setEmpty("选择 OFD 文件");
|
||||||
updateFontSummary();
|
updateFontSummary();
|
||||||
renderFontList();
|
renderFontList();
|
||||||
@@ -195,6 +203,21 @@ async function boot() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadExportFormats() {
|
||||||
|
const formats = callWASM("ofdgoExportFormats") || [];
|
||||||
|
state.exportFormats = formats;
|
||||||
|
el.pageExportFormat.replaceChildren();
|
||||||
|
for (const format of formats) {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = format.value;
|
||||||
|
option.textContent = format.label;
|
||||||
|
el.pageExportFormat.append(option);
|
||||||
|
}
|
||||||
|
if (formats.length) {
|
||||||
|
el.pageExportFormat.value = formats[0].value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function ensureWASM() {
|
async function ensureWASM() {
|
||||||
if (state.ready && !state.wasmExited) {
|
if (state.ready && !state.wasmExited) {
|
||||||
return;
|
return;
|
||||||
@@ -858,14 +881,14 @@ async function exportPDF() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const openSeq = state.openSeq;
|
const openSeq = state.openSeq;
|
||||||
el.exportButton.disabled = true;
|
setExportControlsDisabled(true);
|
||||||
setBusy(true, "正在准备 PDF", 18, STATUS.exporting);
|
setBusy(true, "正在准备文档 PDF", 18, STATUS.exporting);
|
||||||
try {
|
try {
|
||||||
await waitForPaint();
|
await waitForPaint();
|
||||||
if (openSeq !== state.openSeq) {
|
if (openSeq !== state.openSeq) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setProgress("正在生成 PDF", 45);
|
setProgress("正在生成文档 PDF", 45);
|
||||||
await waitForPaint();
|
await waitForPaint();
|
||||||
if (openSeq !== state.openSeq) {
|
if (openSeq !== state.openSeq) {
|
||||||
return;
|
return;
|
||||||
@@ -874,18 +897,53 @@ async function exportPDF() {
|
|||||||
if (openSeq !== state.openSeq) {
|
if (openSeq !== state.openSeq) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setProgress("正在保存 PDF", 86);
|
setProgress("正在保存文档 PDF", 86);
|
||||||
const bytes = base64ToBytes(result.base64);
|
const bytes = base64ToBytes(result.base64);
|
||||||
const blob = new Blob([bytes], { type: "application/pdf" });
|
downloadBytes(bytes, "application/pdf", pdfFileName());
|
||||||
const link = document.createElement("a");
|
setStatus(`文档 PDF 已导出 ${formatBytes(result.size || bytes.length)}`);
|
||||||
const url = URL.createObjectURL(blob);
|
} catch (err) {
|
||||||
link.href = url;
|
if (openSeq === state.openSeq) {
|
||||||
link.download = pdfFileName();
|
showError(err, false);
|
||||||
document.body.append(link);
|
}
|
||||||
link.click();
|
} finally {
|
||||||
link.remove();
|
if (openSeq === state.openSeq) {
|
||||||
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
|
setBusy(false);
|
||||||
setStatus(`PDF 已导出 ${formatBytes(result.size || bytes.length)}`);
|
updateControls();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportCurrentPage() {
|
||||||
|
if (!state.doc) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const format = el.pageExportFormat.value;
|
||||||
|
if (!format) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const info = exportFormatInfo(format);
|
||||||
|
const label = info?.label || String(format || "").toUpperCase();
|
||||||
|
const openSeq = state.openSeq;
|
||||||
|
setExportControlsDisabled(true);
|
||||||
|
setBusy(true, `正在准备 ${label}`, 18, STATUS.pageExporting);
|
||||||
|
try {
|
||||||
|
await waitForPaint();
|
||||||
|
if (openSeq !== state.openSeq) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setProgress(`正在生成 ${label}`, 45);
|
||||||
|
await waitForPaint();
|
||||||
|
if (openSeq !== state.openSeq) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = callWASM("ofdgoExportPage", state.pageIndex, format);
|
||||||
|
if (openSeq !== state.openSeq) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setProgress(`正在保存 ${result.label || label}`, 86);
|
||||||
|
const bytes = base64ToBytes(result.base64);
|
||||||
|
downloadBytes(bytes, result.mime || info?.mime || "application/octet-stream", pageFileName(result.extension || info?.extension || format));
|
||||||
|
setStatus(`${result.label || label} 已导出 ${formatBytes(result.size || bytes.length, result.label || label)}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (openSeq === state.openSeq) {
|
if (openSeq === state.openSeq) {
|
||||||
showError(err, false);
|
showError(err, false);
|
||||||
@@ -1536,9 +1594,17 @@ function updateControls() {
|
|||||||
el.fitHeightButton.disabled = !hasDoc;
|
el.fitHeightButton.disabled = !hasDoc;
|
||||||
el.fitButton.toggleAttribute("aria-pressed", hasDoc && state.fitMode === "width");
|
el.fitButton.toggleAttribute("aria-pressed", hasDoc && state.fitMode === "width");
|
||||||
el.fitHeightButton.toggleAttribute("aria-pressed", hasDoc && state.fitMode === "height");
|
el.fitHeightButton.toggleAttribute("aria-pressed", hasDoc && state.fitMode === "height");
|
||||||
|
el.pageExportFormat.disabled = !hasDoc || !state.exportFormats.length;
|
||||||
|
el.exportPageButton.disabled = !hasDoc || !state.exportFormats.length;
|
||||||
el.exportButton.disabled = !hasDoc;
|
el.exportButton.disabled = !hasDoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setExportControlsDisabled(disabled) {
|
||||||
|
el.exportButton.disabled = disabled;
|
||||||
|
el.exportPageButton.disabled = disabled;
|
||||||
|
el.pageExportFormat.disabled = disabled;
|
||||||
|
}
|
||||||
|
|
||||||
function callWASM(name, ...args) {
|
function callWASM(name, ...args) {
|
||||||
const fn = globalThis[name];
|
const fn = globalThis[name];
|
||||||
if (typeof fn !== "function") {
|
if (typeof fn !== "function") {
|
||||||
@@ -1624,15 +1690,39 @@ function base64ToBytes(base64) {
|
|||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
function pdfFileName() {
|
function downloadBytes(bytes, mime, name) {
|
||||||
const base = (state.fileName || state.doc?.title || "ofdgo").replace(/\.[^.]+$/, "");
|
const blob = new Blob([bytes], { type: mime });
|
||||||
const safe = base.replace(/[\\/:*?"<>|]+/g, "_").trim() || "ofdgo";
|
const link = document.createElement("a");
|
||||||
return `${safe}.pdf`;
|
const url = URL.createObjectURL(blob);
|
||||||
|
link.href = url;
|
||||||
|
link.download = name;
|
||||||
|
document.body.append(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatBytes(size) {
|
function baseFileName() {
|
||||||
|
const base = (state.fileName || state.doc?.title || "ofdgo").replace(/\.[^.]+$/, "");
|
||||||
|
return base.replace(/[\\/:*?"<>|]+/g, "_").trim() || "ofdgo";
|
||||||
|
}
|
||||||
|
|
||||||
|
function pdfFileName() {
|
||||||
|
return `${baseFileName()}.pdf`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pageFileName(extension) {
|
||||||
|
const page = String(state.pageIndex + 1).padStart(String(state.doc?.pageCount || 1).length, "0");
|
||||||
|
return `${baseFileName()}_p${page}.${extension || "bin"}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportFormatInfo(value) {
|
||||||
|
return state.exportFormats.find((format) => format.value === value) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBytes(size, fallback = "PDF") {
|
||||||
if (!Number.isFinite(size) || size <= 0) {
|
if (!Number.isFinite(size) || size <= 0) {
|
||||||
return "PDF";
|
return fallback;
|
||||||
}
|
}
|
||||||
if (size < 1024 * 1024) {
|
if (size < 1024 * 1024) {
|
||||||
return `${Math.round(size / 1024)} KB`;
|
return `${Math.round(size / 1024)} KB`;
|
||||||
|
|||||||
+38
-2
@@ -42,6 +42,8 @@ type apiResult struct {
|
|||||||
func RunWASM() {
|
func RunWASM() {
|
||||||
registerCallback("ofdgoOpen", openDocument)
|
registerCallback("ofdgoOpen", openDocument)
|
||||||
registerCallback("ofdgoRenderPage", renderPage)
|
registerCallback("ofdgoRenderPage", renderPage)
|
||||||
|
registerCallback("ofdgoExportFormats", exportFormats)
|
||||||
|
registerCallback("ofdgoExportPage", exportPage)
|
||||||
registerCallback("ofdgoExportPDF", exportPDF)
|
registerCallback("ofdgoExportPDF", exportPDF)
|
||||||
registerCallback("ofdgoFontCandidates", fontCandidates)
|
registerCallback("ofdgoFontCandidates", fontCandidates)
|
||||||
select {}
|
select {}
|
||||||
@@ -113,7 +115,38 @@ func renderPage(args []js.Value) (any, error) {
|
|||||||
return currentSession.RenderPageSVG(args[0].Int())
|
return currentSession.RenderPageSVG(args[0].Int())
|
||||||
}
|
}
|
||||||
|
|
||||||
// exportPDF 导出OFD为PDF
|
// exportFormats 获取导出格式
|
||||||
|
// 入参: args 浏览器参数
|
||||||
|
// 返回: any 导出格式, error 错误信息
|
||||||
|
func exportFormats(args []js.Value) (any, error) {
|
||||||
|
return ExportFormats(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// exportPage 导出OFD单页
|
||||||
|
// 入参: args 浏览器参数
|
||||||
|
// 返回: any 导出结果, error 错误信息
|
||||||
|
func exportPage(args []js.Value) (any, error) {
|
||||||
|
if currentSession == nil {
|
||||||
|
return nil, fmt.Errorf("ofd document is not opened")
|
||||||
|
}
|
||||||
|
if len(args) < 2 {
|
||||||
|
return nil, fmt.Errorf("missing export page arguments")
|
||||||
|
}
|
||||||
|
data, format, err := currentSession.ExportPage(args[0].Int(), args[1].String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return map[string]any{
|
||||||
|
"base64": base64.StdEncoding.EncodeToString(data),
|
||||||
|
"size": len(data),
|
||||||
|
"format": format.Value,
|
||||||
|
"label": format.Label,
|
||||||
|
"extension": format.Extension,
|
||||||
|
"mime": format.MIME,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// exportPDF 导出OFD文档为PDF
|
||||||
// 入参: args 浏览器参数
|
// 入参: args 浏览器参数
|
||||||
// 返回: any PDF结果, error 错误信息
|
// 返回: any PDF结果, error 错误信息
|
||||||
func exportPDF(args []js.Value) (any, error) {
|
func exportPDF(args []js.Value) (any, error) {
|
||||||
@@ -124,7 +157,10 @@ func exportPDF(args []js.Value) (any, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return map[string]any{"base64": base64.StdEncoding.EncodeToString(data), "size": len(data)}, nil
|
return map[string]any{
|
||||||
|
"base64": base64.StdEncoding.EncodeToString(data),
|
||||||
|
"size": len(data),
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// fontCandidates 获取字体候选名称
|
// fontCandidates 获取字体候选名称
|
||||||
|
|||||||
+117
-9
@@ -17,6 +17,12 @@ package webui
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"image/draw"
|
||||||
|
"image/jpeg"
|
||||||
|
"image/png"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/xiaoqidun/ofdgo"
|
"github.com/xiaoqidun/ofdgo"
|
||||||
)
|
)
|
||||||
@@ -72,6 +78,47 @@ type PageSVG struct {
|
|||||||
SVG string `json:"svg"`
|
SVG string `json:"svg"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExportFormat 导出格式
|
||||||
|
type ExportFormat struct {
|
||||||
|
Value string `json:"value"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
Extension string `json:"extension"`
|
||||||
|
MIME string `json:"mime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// supportedExportFormats 导出格式列表
|
||||||
|
var supportedExportFormats = []ExportFormat{
|
||||||
|
{Value: "svg", Label: "SVG", Extension: "svg", MIME: "image/svg+xml"},
|
||||||
|
{Value: "pdf", Label: "PDF", Extension: "pdf", MIME: "application/pdf"},
|
||||||
|
{Value: "eps", Label: "EPS", Extension: "eps", MIME: "application/postscript"},
|
||||||
|
{Value: "png", Label: "PNG", Extension: "png", MIME: "image/png"},
|
||||||
|
{Value: "jpg", Label: "JPEG", Extension: "jpg", MIME: "image/jpeg"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExportFormats 获取导出格式
|
||||||
|
// 返回: []ExportFormat 导出格式列表
|
||||||
|
func ExportFormats() []ExportFormat {
|
||||||
|
formats := make([]ExportFormat, len(supportedExportFormats))
|
||||||
|
copy(formats, supportedExportFormats)
|
||||||
|
return formats
|
||||||
|
}
|
||||||
|
|
||||||
|
// exportFormat 获取导出格式
|
||||||
|
// 入参: value 格式值
|
||||||
|
// 返回: ExportFormat 导出格式, bool 是否支持
|
||||||
|
func exportFormat(value string) (ExportFormat, bool) {
|
||||||
|
value = strings.ToLower(strings.TrimSpace(value))
|
||||||
|
if value == "jpeg" {
|
||||||
|
value = "jpg"
|
||||||
|
}
|
||||||
|
for _, format := range supportedExportFormats {
|
||||||
|
if format.Value == value {
|
||||||
|
return format, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ExportFormat{}, false
|
||||||
|
}
|
||||||
|
|
||||||
// Open 打开浏览器内存中的OFD文档
|
// Open 打开浏览器内存中的OFD文档
|
||||||
// 入参: data OFD文件数据, opts 打开选项
|
// 入参: data OFD文件数据, opts 打开选项
|
||||||
// 返回: *Session 文档会话, error 错误信息
|
// 返回: *Session 文档会话, error 错误信息
|
||||||
@@ -146,14 +193,7 @@ func (s *Session) Info() DocumentInfo {
|
|||||||
// 入参: index 页面索引
|
// 入参: index 页面索引
|
||||||
// 返回: PageSVG 页面SVG结果, error 错误信息
|
// 返回: PageSVG 页面SVG结果, error 错误信息
|
||||||
func (s *Session) RenderPageSVG(index int) (PageSVG, error) {
|
func (s *Session) RenderPageSVG(index int) (PageSVG, error) {
|
||||||
if s == nil || s.Reader == nil || s.Renderer == nil || s.doc == nil {
|
pageRef, page, err := s.pageContent(index)
|
||||||
return PageSVG{}, fmt.Errorf("ofd document is not opened")
|
|
||||||
}
|
|
||||||
if index < 0 || index >= len(s.doc.Pages.Page) {
|
|
||||||
return PageSVG{}, fmt.Errorf("page index %d out of range", index)
|
|
||||||
}
|
|
||||||
pageRef := s.doc.Pages.Page[index]
|
|
||||||
page, err := s.Reader.PageContent(pageRef)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return PageSVG{}, err
|
return PageSVG{}, err
|
||||||
}
|
}
|
||||||
@@ -168,7 +208,46 @@ func (s *Session) RenderPageSVG(index int) (PageSVG, error) {
|
|||||||
return PageSVG{Index: index, Number: index + 1, ID: pageRef.ID, Width: box.W, Height: box.H, SVG: buf.String()}, nil
|
return PageSVG{Index: index, Number: index + 1, ID: pageRef.ID, Width: box.W, Height: box.H, SVG: buf.String()}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExportPDF 导出整个文档为PDF
|
// ExportPage 导出单页
|
||||||
|
// 入参: index 页面索引, value 导出格式
|
||||||
|
// 返回: []byte 文件数据, ExportFormat 导出格式, error 错误信息
|
||||||
|
func (s *Session) ExportPage(index int, value string) ([]byte, ExportFormat, error) {
|
||||||
|
format, ok := exportFormat(value)
|
||||||
|
if !ok {
|
||||||
|
return nil, ExportFormat{}, fmt.Errorf("unsupported export format %s", value)
|
||||||
|
}
|
||||||
|
_, page, err := s.pageContent(index)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ExportFormat{}, err
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
switch format.Value {
|
||||||
|
case "svg":
|
||||||
|
err = s.Renderer.RenderToSVG(page, &buf)
|
||||||
|
case "pdf":
|
||||||
|
err = s.Renderer.RenderToPDF(page, &buf)
|
||||||
|
case "eps":
|
||||||
|
err = s.Renderer.RenderToEPS(page, &buf)
|
||||||
|
case "png":
|
||||||
|
img, renderErr := s.Renderer.RenderToImage(page)
|
||||||
|
if renderErr != nil {
|
||||||
|
return nil, ExportFormat{}, renderErr
|
||||||
|
}
|
||||||
|
err = png.Encode(&buf, img)
|
||||||
|
case "jpg":
|
||||||
|
img, renderErr := s.Renderer.RenderToImage(page)
|
||||||
|
if renderErr != nil {
|
||||||
|
return nil, ExportFormat{}, renderErr
|
||||||
|
}
|
||||||
|
err = jpeg.Encode(&buf, imageWithWhiteBackground(img), &jpeg.Options{Quality: 95})
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, ExportFormat{}, err
|
||||||
|
}
|
||||||
|
return buf.Bytes(), format, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExportPDF 导出文档为PDF
|
||||||
// 返回: []byte PDF文件数据, error 错误信息
|
// 返回: []byte PDF文件数据, error 错误信息
|
||||||
func (s *Session) ExportPDF() ([]byte, error) {
|
func (s *Session) ExportPDF() ([]byte, error) {
|
||||||
if s == nil || s.Reader == nil || s.Renderer == nil || s.doc == nil {
|
if s == nil || s.Reader == nil || s.Renderer == nil || s.doc == nil {
|
||||||
@@ -180,3 +259,32 @@ func (s *Session) ExportPDF() ([]byte, error) {
|
|||||||
}
|
}
|
||||||
return buf.Bytes(), nil
|
return buf.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// imageWithWhiteBackground 填充图片白色背景
|
||||||
|
// 入参: img 图片对象
|
||||||
|
// 返回: image.Image 图片对象
|
||||||
|
func imageWithWhiteBackground(img image.Image) image.Image {
|
||||||
|
bounds := img.Bounds()
|
||||||
|
dst := image.NewRGBA(bounds)
|
||||||
|
draw.Draw(dst, bounds, &image.Uniform{C: color.White}, image.Point{}, draw.Src)
|
||||||
|
draw.Draw(dst, bounds, img, bounds.Min, draw.Over)
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
|
||||||
|
// pageContent 获取页面内容
|
||||||
|
// 入参: index 页面索引
|
||||||
|
// 返回: ofdgo.Page 页面引用, *ofdgo.PageContent 页面内容, error 错误信息
|
||||||
|
func (s *Session) pageContent(index int) (ofdgo.Page, *ofdgo.PageContent, error) {
|
||||||
|
if s == nil || s.Reader == nil || s.Renderer == nil || s.doc == nil {
|
||||||
|
return ofdgo.Page{}, nil, fmt.Errorf("ofd document is not opened")
|
||||||
|
}
|
||||||
|
if index < 0 || index >= len(s.doc.Pages.Page) {
|
||||||
|
return ofdgo.Page{}, nil, fmt.Errorf("page index %d out of range", index)
|
||||||
|
}
|
||||||
|
pageRef := s.doc.Pages.Page[index]
|
||||||
|
page, err := s.Reader.PageContent(pageRef)
|
||||||
|
if err != nil {
|
||||||
|
return ofdgo.Page{}, nil, err
|
||||||
|
}
|
||||||
|
return pageRef, page, nil
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user