feat(前端功能): 支持链接打开远程文档
build.yaml / build (push) Successful in 42s

This commit is contained in:
xiaoqidun committed 2026-09-28 09:15:41 +08:00
1 parent 655f725bbc
commit 7545fc10c1
2 files changed
+110 -6

No files matched your search

+19
View File
@@ -4,6 +4,25 @@
# 在线体验 # 在线体验
[OFDGo WebUI](https://ofdgo.aite.me/),将OFDGo编译为WASM提供服务 [OFDGo WebUI](https://ofdgo.aite.me/),将OFDGo编译为WASM提供服务
# 链接接入
```text
https://ofdgo.aite.me/#url=<URL编码后的文件地址>
```
| 参数 | 必填 | 说明 |
| --- | --- | --- |
| `url` | 是 | 文件地址,支持OFD、PDF |
| `name` | 否 | 文件名称,默认自动获取 |
| `page` | 否 | 物理页码,默认1,超过总页数取末页 |
参数用`&`连接,参数值需URL编码。跨域访问时,响应头需包含:
```http
Access-Control-Allow-Origin: https://ofdgo.aite.me
```
公开资源可设为`*`;自建阅读器替换为对应源(协议、域名及端口)。
# 一键部署 # 一键部署
```shell ```shell
docker run -d -p 80:80 ccr.ccs.tencentyun.com/xiaoqidun/ofdgo:latest docker run -d -p 80:80 ccr.ccs.tencentyun.com/xiaoqidun/ofdgo:latest
+91 -6
View File
@@ -23,6 +23,7 @@ const batch = { items: [], formats: [], running: false, canceled: false, worker:
const batchElements = Object.fromEntries(["Button", "Panel", "Form", "Input", "Add", "Clear", "Count", "Format", "Destination", "DPIRow", "DPI", "List", "Empty", "Progress", "Status", "Close", "Cancel", "Start"].map(name => [name, document.querySelector(`#convert${name}`)])); const batchElements = Object.fromEntries(["Button", "Panel", "Form", "Input", "Add", "Clear", "Count", "Format", "Destination", "DPIRow", "DPI", "List", "Empty", "Progress", "Status", "Close", "Cancel", "Start"].map(name => [name, document.querySelector(`#convert${name}`)]));
let wasmPromise = null; let wasmPromise = null;
let remoteDownload = null;
let wasmWorker = null; let wasmWorker = null;
let wasmRequestID = 0; let wasmRequestID = 0;
let wasmRecoveryTimer = 0; let wasmRecoveryTimer = 0;
@@ -3050,6 +3051,12 @@ async function boot() {
}).catch((err) => showError(err, !state.doc)); }).catch((err) => showError(err, !state.doc));
}); });
} }
try {
const remote = remoteDocumentOptions(window.location.hash);
if (remote) await openOFD({ name: remote.name }, remote);
} catch (err) {
showError(err, !state.doc);
}
} catch (err) { } catch (err) {
setStatus("引擎加载失败"); setStatus("引擎加载失败");
setEmpty(String(err.message || err)); setEmpty(String(err.message || err));
@@ -3361,11 +3368,79 @@ async function recoverWASM() {
} }
} }
async function openOFD(file) { function remoteDocumentOptions(hash) {
const params = new URLSearchParams(hash.replace(/^#/, ""));
for (const key of ["url", "name", "page"]) {
if (params.getAll(key).length > 1) throw new Error(`重复的链接参数:${key}`);
}
if (!params.has("url")) return null;
let url;
try { url = new URL(params.get("url")); } catch { throw new Error("文件地址无效"); }
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) throw new Error("文件地址必须为不含账号密码的 HTTP 或 HTTPS 地址");
if (window.location.protocol === "https:" && url.protocol !== "https:") throw new Error("请使用 HTTPS 文件地址");
const page = params.get("page") || "1";
return { url: url.href, name: params.get("name") || "", pageIndex: /^[1-9]\d*$/.test(page) ? Math.min(Number(page), Number.MAX_SAFE_INTEGER) - 1 : 0 };
}
function remoteDocumentName(name) {
return String(name || "").split(/[\\/]/).pop().replace(/[\u0000-\u001f\u007f]/g, "").trim();
}
async function downloadRemoteDocument(options) {
const controller = new AbortController();
remoteDownload = controller;
el.cancelExportButton.hidden = false;
el.cancelExportButton.disabled = false;
el.cancelExportButton.title = "取消下载";
el.cancelExportButton.setAttribute("aria-label", "取消下载");
setProgress("正在下载文档", null);
try {
const response = await fetch(options.url, { mode: "cors", credentials: "omit", referrerPolicy: "no-referrer", cache: "no-store", signal: controller.signal });
if (!response.ok) throw new Error(`文件下载失败(HTTP ${response.status})`);
const total = Number(response.headers.get("Content-Length")) || 0;
if (!response.body) throw new Error("下载内容为空");
const chunks = [];
let received = 0;
const reader = response.body.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
received += value.byteLength;
setProgress(`正在下载 ${(received / 1048576).toFixed(1)} MB`, total > 0 && received <= total ? received / total * 20 : null);
}
} finally { reader.releaseLock(); }
controller.signal.throwIfAborted();
const bytes = new Uint8Array(received);
let offset = 0;
for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
const pdf = new TextDecoder("latin1").decode(bytes.subarray(0, 1024)).includes("%PDF-");
if (!pdf && !(bytes[0] === 0x50 && bytes[1] === 0x4b && bytes[2] === 3 && bytes[3] === 4)) throw new Error("下载内容不是 OFD 或 PDF 文档");
const disposition = response.headers.get("Content-Disposition") || "";
let supplied = /(?:^|;)\s*filename\*=UTF-8'[^']*'([^;]+)/i.exec(disposition)?.[1];
if (supplied) { try { supplied = decodeURIComponent(supplied.trim()); } catch { supplied = ""; } }
if (!supplied) supplied = /(?:^|;)\s*filename\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([^;]*))/i.exec(disposition)?.slice(1).find(value => value !== undefined)?.replace(/\\(.)/g, "$1");
let pathName = new URL(response.url || options.url).pathname.split("/").pop();
try { pathName = decodeURIComponent(pathName); } catch { pathName = ""; }
const name = remoteDocumentName(options.name) || remoteDocumentName(supplied) || remoteDocumentName(pathName) || `ofdgo.${pdf ? "pdf" : "ofd"}`;
return { bytes, pdf, name };
} catch (err) {
if (err instanceof TypeError) throw new Error("下载失败,请检查网络、链接及跨域配置");
throw err;
} finally {
if (remoteDownload === controller) {
remoteDownload = null;
el.cancelExportButton.hidden = true;
}
}
}
async function openOFD(file, remote = null) {
if (!file) { if (!file) {
return; return;
} }
if (!isDocumentFile(file)) { if (!remote && !isDocumentFile(file)) {
el.ofdInput.value = ""; el.ofdInput.value = "";
showError(new Error("选择 OFD 文件"), !state.doc); showError(new Error("选择 OFD 文件"), !state.doc);
return; return;
@@ -3373,17 +3448,20 @@ async function openOFD(file) {
if (!discardChanges()) { if (!discardChanges()) {
return; return;
} }
remoteDownload?.abort();
clearSecurityForms(); clearSecurityForms();
state.wasmRecoveries = 0; state.wasmRecoveries = 0;
const openSeq = ++state.openSeq; const openSeq = ++state.openSeq;
setBusy(true, "正在读取文档", 10, STATUS.opening); setBusy(true, "正在读取文档", 10, STATUS.opening);
try { try {
let bytes = new Uint8Array(await file.arrayBuffer()); const downloaded = remote ? await downloadRemoteDocument(remote) : null;
let bytes = downloaded ? downloaded.bytes : new Uint8Array(await file.arrayBuffer());
const name = downloaded ? downloaded.name : file.name;
let warnings = []; let warnings = [];
if (openSeq !== state.openSeq) { if (openSeq !== state.openSeq) {
return; return;
} }
if (/\.pdf$/i.test(file.name || "")) { if (downloaded ? downloaded.pdf : /\.pdf$/i.test(file.name || "")) {
setProgress("正在转换 PDF", 25); setProgress("正在转换 PDF", 25);
await ensureWASM(); await ensureWASM();
if (openSeq !== state.openSeq) return; if (openSeq !== state.openSeq) return;
@@ -3400,9 +3478,12 @@ async function openOFD(file) {
bytes = converted.bytes; bytes = converted.bytes;
warnings = JSON.parse(converted.warnings || "null") || []; warnings = JSON.parse(converted.warnings || "null") || [];
} }
const remoteDoc = remote ? await openWithCredentials(await fontManager.files(fontManager.records()), openSeq, bytes) : null;
if (openSeq !== state.openSeq) return;
state.ofdBytes = bytes; state.ofdBytes = bytes;
state.conversionWarnings = warnings; state.conversionWarnings = warnings;
state.fileName = (file.name || "ofdgo.ofd").replace(/\.pdf$/i, ".ofd"); state.fileName = (name || "ofdgo.ofd").replace(/\.pdf$/i, ".ofd");
if (remote && !/\.ofd$/i.test(state.fileName)) state.fileName += ".ofd";
state.editing = false; state.editing = false;
state.composite = null; state.composite = null;
state.annotationCreate = null; state.annotationCreate = null;
@@ -3432,7 +3513,7 @@ async function openOFD(file) {
el.objectBoundsPanel.close(); el.objectBoundsPanel.close();
el.outlinePanel.close(); el.outlinePanel.close();
updateControls(); updateControls();
await openDocument({ pageIndex: 0, resetScroll: true, openSeq }); await openDocument({ ...(remoteDoc ? { doc: remoteDoc } : {}), pageIndex: remote?.pageIndex || 0, resetScroll: true, openSeq });
} catch (err) { } catch (err) {
if (openSeq === state.openSeq) { if (openSeq === state.openSeq) {
if (err.name === "AbortError") setStatus("转换已取消"); if (err.name === "AbortError") setStatus("转换已取消");
@@ -4975,6 +5056,10 @@ async function exportFile(whole, indices = null, value = el.exportFormat.value,
} }
function cancelExport() { function cancelExport() {
if (remoteDownload) {
remoteDownload.abort();
return;
}
if (!state.exportRequestID || el.cancelExportButton.disabled) { if (!state.exportRequestID || el.cancelExportButton.disabled) {
return; return;
} }