mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 14:53:44 +08:00
1 parent
5183ba2ab0
commit
50da1d9144
10 files changed
+1358
-594
No files matched your search
@@ -12,9 +12,14 @@
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
<div id="documentTitleBar" class="document-titlebar">
|
||||
<div id="documentTabs" class="document-tabs" role="tablist" aria-label="文件"></div>
|
||||
<button id="documentNewTab" class="document-new-tab" type="button" title="新建标签"><span class="new-icon"></span></button>
|
||||
<div class="document-title-drag"></div>
|
||||
</div>
|
||||
<header class="toolbar-frame">
|
||||
<div class="toolbar app-bar">
|
||||
<input id="ofdInput" class="file-input" type="file" accept=".ofd,.pdf">
|
||||
<input id="ofdInput" class="file-input" type="file" accept=".ofd,.pdf" multiple>
|
||||
<input id="fontInput" class="file-input" type="file" accept=".ttf,.otf,.ttc,font/*" multiple>
|
||||
<input id="fontDirectoryInput" class="file-input" type="file" webkitdirectory multiple>
|
||||
<div class="tool-group">
|
||||
|
||||
+95
-2
@@ -14,6 +14,10 @@
|
||||
--focus: #0f7b68;
|
||||
--sidebar-width: clamp(220px, 20vw, 292px);
|
||||
--bar-bg: #fbfcfd;
|
||||
--titlebar-background: #e8edf0;
|
||||
--titlebar-foreground: #1f2428;
|
||||
--titlebar-inactive-background: #eef1f4;
|
||||
--titlebar-inactive-foreground: #66737f;
|
||||
--bar-height: 40px;
|
||||
--bar-pad: 5px 10px;
|
||||
--control-height: 28px;
|
||||
@@ -26,6 +30,91 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.document-titlebar {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: stretch;
|
||||
height: var(--titlebar-height, 36px);
|
||||
min-height: 0;
|
||||
flex: 0 0 auto;
|
||||
padding-left: var(--titlebar-left, 8px);
|
||||
padding-right: var(--titlebar-right, 8px);
|
||||
background: var(--titlebar-background);
|
||||
color: var(--titlebar-foreground);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.document-tabs {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.document-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0 0 210px;
|
||||
width: 210px;
|
||||
min-width: 100px;
|
||||
max-width: 240px;
|
||||
gap: 8px;
|
||||
padding: 0 8px 0 12px;
|
||||
border: 0;
|
||||
border-right: 1px solid var(--line);
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.document-tab[data-selected="true"] {
|
||||
background: var(--bar-bg);
|
||||
color: var(--titlebar-foreground);
|
||||
box-shadow: inset 0 2px var(--accent);
|
||||
}
|
||||
|
||||
.document-tab-name {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.document-titlebar[data-active="false"] {
|
||||
background: var(--titlebar-inactive-background);
|
||||
color: var(--titlebar-inactive-foreground);
|
||||
}
|
||||
|
||||
.document-titlebar[data-active="false"] .document-tab {
|
||||
color: var(--titlebar-inactive-foreground);
|
||||
}
|
||||
|
||||
.document-tab-close,
|
||||
.document-new-tab {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex: 0 0 28px;
|
||||
width: 28px;
|
||||
min-width: 28px;
|
||||
align-self: center;
|
||||
height: min(26px, calc(var(--titlebar-height, 36px) - 6px));
|
||||
border: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.document-tab-close:hover,
|
||||
.document-new-tab:hover {
|
||||
background: var(--accent-soft);
|
||||
}
|
||||
|
||||
.document-title-drag {
|
||||
flex: 1;
|
||||
min-width: 32px;
|
||||
}
|
||||
|
||||
html {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -81,14 +170,15 @@ a:focus-visible {
|
||||
|
||||
.app {
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-template-rows: auto auto minmax(0, 1fr) auto;
|
||||
width: 100%;
|
||||
height: 100dvh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app:has(.editor-toolbar:not([hidden])) {
|
||||
grid-template-rows: auto auto minmax(0, 1fr) auto;
|
||||
grid-template-rows: auto auto auto minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.editor-toolbar {
|
||||
@@ -455,12 +545,14 @@ a:focus-visible {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
background: var(--bar-bg);
|
||||
border-top: 1px solid var(--line);
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.toolbar-frame > .toolbar {
|
||||
flex: 1 1 auto;
|
||||
border-bottom: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.app-update {
|
||||
@@ -754,6 +846,7 @@ button:not(:disabled):hover {
|
||||
}
|
||||
|
||||
.form-dialog::backdrop {
|
||||
top: var(--titlebar-height, 36px);
|
||||
background: rgb(31 36 40 / 18%);
|
||||
}
|
||||
|
||||
|
||||
+873
-475
File diff suppressed because it is too large.
Load diff
+119
-21
@@ -1,5 +1,11 @@
|
||||
let engineModule = null;
|
||||
let engineScheduler = null;
|
||||
let engineSequence = 0;
|
||||
const engineRuntimes = new Map();
|
||||
|
||||
function requestKey(worker, data) {
|
||||
return worker.shared ? JSON.stringify([data.engineID, data.id]) : data.id;
|
||||
}
|
||||
|
||||
export function engineLimits() {
|
||||
engineScheduler ||= new BrowserScheduler();
|
||||
@@ -31,19 +37,20 @@ class BrowserScheduler {
|
||||
|
||||
send(task) {
|
||||
const {worker, data, transfer} = task;
|
||||
worker.requests.set(data.id, performance.now());
|
||||
worker.requests.set(requestKey(worker, data), performance.now());
|
||||
try { worker.worker.postMessage(data, transfer); }
|
||||
catch (error) {
|
||||
worker.requests.delete(data.id);
|
||||
worker.onmessage?.({data:{id:data.id, ok:false, error:error.message}});
|
||||
worker.requests.delete(requestKey(worker, data));
|
||||
worker.receive({data:{engineID:data.engineID, id:data.id, ok:false, error:error.message}});
|
||||
}
|
||||
}
|
||||
|
||||
complete(worker, data) {
|
||||
if (data.memory) worker.memory = data.memory;
|
||||
if (data.id == null || data.type) return;
|
||||
const started = worker.requests.get(data.id);
|
||||
worker.requests.delete(data.id);
|
||||
const key = requestKey(worker, data);
|
||||
const started = worker.requests.get(key);
|
||||
worker.requests.delete(key);
|
||||
if (this.memory > this.budget) this.limit = Math.max(1, this.limit - 1);
|
||||
else if (started !== undefined && data.ok && performance.now() - started > 100 && this.memory + Math.max(128 << 20, worker.memory) < this.budget) this.limit = Math.min(this.capacity, this.limit + 1);
|
||||
}
|
||||
@@ -72,7 +79,7 @@ class BrowserScheduler {
|
||||
const index = this.queue.findIndex(task => task.worker === worker && task.data.id === id);
|
||||
if (index < 0) return false;
|
||||
this.queue.splice(index, 1);
|
||||
worker.onmessage?.({data:{id, ok:false, canceled:true, error:"操作已取消"}});
|
||||
worker.receive({data:{engineID:worker.engineID, id, ok:false, canceled:true, error:"操作已取消"}});
|
||||
this.drain();
|
||||
return true;
|
||||
}
|
||||
@@ -89,32 +96,63 @@ class BrowserWorker {
|
||||
constructor(options) {
|
||||
engineScheduler ||= new BrowserScheduler();
|
||||
this.primary = !!options.primary;
|
||||
this.shared = !!options.shared;
|
||||
this.engineID = this.shared ? null : `engine-${++engineSequence}`;
|
||||
this.sessions = new Map();
|
||||
this.closingSessions = new Set();
|
||||
this.batch = !options.primary && !options.page;
|
||||
this.requests = new Map();
|
||||
this.memory = 64 << 20;
|
||||
this.reservation = 0;
|
||||
this.closed = false;
|
||||
this.worker = new Worker("./ofdgo_wasm.js");
|
||||
this.url = String(options.url || "./ofdgo_wasm.js");
|
||||
this.worker = new Worker(this.url);
|
||||
engineScheduler.workers.add(this);
|
||||
this.worker.onmessage = event => {
|
||||
if (this.closed || event.data.type === "module") return;
|
||||
engineScheduler.complete(this, event.data);
|
||||
if (event.data.type === "exit") this.terminate();
|
||||
try { this.onmessage?.(event); }
|
||||
if (event.data.type === "exit" && (!this.shared || !event.data.engineID)) this.terminate();
|
||||
try { this.receive(event); }
|
||||
finally { engineScheduler.drain(); }
|
||||
};
|
||||
this.worker.onerror = event => { this.terminate(); this.onerror?.(event); };
|
||||
this.worker.onmessageerror = event => { this.terminate(); this.onmessageerror?.(event); };
|
||||
this.worker.onerror = event => { this.terminate(); this.fail("onerror", event); };
|
||||
this.worker.onmessageerror = event => { this.terminate(); this.fail("onmessageerror", event); };
|
||||
this.worker.addEventListener("message", event => {
|
||||
if (event.data.type !== "module") return;
|
||||
event.stopImmediatePropagation();
|
||||
engineModule ||= WebAssembly.compileStreaming(fetch("./ofdgo.wasm")).catch(error => { engineModule = null; throw error; });
|
||||
engineModule.then(module => { if (!this.closed) this.worker.postMessage({ type:"module", module }); }, error => { if (!this.closed) this.worker.postMessage({ type:"module", error:error.message }); });
|
||||
});
|
||||
if (!this.shared) this.worker.postMessage({type:"open", engineID:this.engineID});
|
||||
}
|
||||
|
||||
receive(event) {
|
||||
if (!event.data.engineID && !["progress", "exit"].includes(event.data.type)) return;
|
||||
if (!this.shared) {
|
||||
if (event.data.engineID && event.data.engineID !== this.engineID) return;
|
||||
this.onmessage?.(event);
|
||||
return;
|
||||
}
|
||||
if (event.data.type === "closed") {
|
||||
if (!this.closingSessions.delete(event.data.engineID)) return;
|
||||
if (!this.sessions.size && !this.closingSessions.size) this.terminate();
|
||||
return;
|
||||
}
|
||||
const targets = event.data.engineID ? [this.sessions.get(event.data.engineID)] : [...this.sessions.values()];
|
||||
for (const session of targets) session?.receive(event);
|
||||
}
|
||||
|
||||
fail(name, event) {
|
||||
if (!this.shared) { this[name]?.(event); return; }
|
||||
for (const session of [...this.sessions.values()]) {
|
||||
session.terminate();
|
||||
session[name]?.(event);
|
||||
}
|
||||
}
|
||||
|
||||
postMessage(data, transfer) {
|
||||
if (this.closed) throw new DOMException("引擎已关闭", "AbortError");
|
||||
if (!this.shared) data = {...data, engineID:this.engineID};
|
||||
if (data.type) {
|
||||
if (data.type === "cancel" && engineScheduler.cancel(this, data.id)) return;
|
||||
this.worker.postMessage(data, transfer);
|
||||
@@ -124,15 +162,61 @@ class BrowserWorker {
|
||||
terminate() {
|
||||
if (this.closed) return;
|
||||
this.closed = true;
|
||||
if (engineRuntimes.get(this.url) === this) engineRuntimes.delete(this.url);
|
||||
this.closingSessions.clear();
|
||||
this.worker.terminate();
|
||||
engineScheduler.remove(this);
|
||||
}
|
||||
}
|
||||
|
||||
class BrowserSession {
|
||||
constructor(options) {
|
||||
const url = String(options.url || "./ofdgo_wasm.js");
|
||||
let runtime = engineRuntimes.get(url);
|
||||
if (!runtime || runtime.closed) {
|
||||
runtime = new BrowserWorker({primary:true, shared:true, url});
|
||||
engineRuntimes.set(url, runtime);
|
||||
}
|
||||
this.runtime = runtime;
|
||||
this.engineID = `engine-${++engineSequence}`;
|
||||
this.requests = new Set();
|
||||
this.closed = false;
|
||||
runtime.sessions.set(this.engineID, this);
|
||||
runtime.postMessage({type:"open", engineID:this.engineID});
|
||||
}
|
||||
|
||||
receive(event) {
|
||||
if (this.closed) return;
|
||||
if (event.data.id != null && !event.data.type) this.requests.delete(event.data.id);
|
||||
if (event.data.type === "exit") this.terminate();
|
||||
this.onmessage?.(event);
|
||||
}
|
||||
|
||||
postMessage(data, transfer) {
|
||||
if (this.closed) throw new DOMException("引擎已关闭", "AbortError");
|
||||
if (!data.type) this.requests.add(data.id);
|
||||
this.runtime.postMessage({...data, engineID:this.engineID}, transfer);
|
||||
}
|
||||
|
||||
terminate() {
|
||||
if (this.closed) return;
|
||||
this.closed = true;
|
||||
this.runtime.sessions.delete(this.engineID);
|
||||
for (const id of this.requests) this.runtime.requests.delete(requestKey(this.runtime, {engineID:this.engineID, id}));
|
||||
this.requests.clear();
|
||||
if (!this.runtime.closed) {
|
||||
this.runtime.closingSessions.add(this.engineID);
|
||||
this.runtime.postMessage({type:"close", engineID:this.engineID});
|
||||
}
|
||||
engineScheduler.drain();
|
||||
}
|
||||
}
|
||||
|
||||
export function createEngineWorker(options = {}) {
|
||||
if (nativeHost) return nativeHost.worker();
|
||||
if (options.pages) return new BrowserEngine();
|
||||
return new BrowserWorker(options);
|
||||
if (options.page || options.batch) return new BrowserWorker(options);
|
||||
if (options.primary) return new BrowserSession(options);
|
||||
return new BrowserEngine(options);
|
||||
}
|
||||
|
||||
export async function documentBytes(file) {
|
||||
@@ -142,8 +226,10 @@ export async function documentBytes(file) {
|
||||
}
|
||||
|
||||
class BrowserEngine {
|
||||
constructor() {
|
||||
this.primary = createEngineWorker({primary:true});
|
||||
constructor(options) {
|
||||
this.primary = createEngineWorker({primary:true, url:options.url});
|
||||
this.engineID = this.primary.engineID;
|
||||
this.workerURL = options.url;
|
||||
this.primary.onmessage = event => {
|
||||
const data = event.data;
|
||||
if (data.memory) this.memory = data.memory;
|
||||
@@ -187,7 +273,7 @@ class BrowserEngine {
|
||||
}
|
||||
|
||||
createSlot(source) {
|
||||
const worker = createEngineWorker({page:true});
|
||||
const worker = createEngineWorker({page:true, url:this.workerURL});
|
||||
const slot = {worker, pending: new Map(), busy: false, memory: Math.max(128 << 20, this.memory), pages:new Set(), lastPage:0};
|
||||
worker.reservation = slot.memory;
|
||||
worker.reclaim = () => { if (!slot.busy) this.closeSlot(slot); };
|
||||
@@ -269,7 +355,7 @@ class BrowserEngine {
|
||||
slot.pages.delete(slot.lastPage);
|
||||
slot.pages.add(slot.lastPage);
|
||||
if (slot.pages.size > 16) slot.pages.delete(slot.pages.values().next().value);
|
||||
if (!this.closed) this.onmessage?.({data: {...result, id: data.id}});
|
||||
if (!this.closed) this.onmessage?.({data: {...result, engineID:this.engineID, id: data.id}});
|
||||
}).catch(error => {
|
||||
if (!this.closed && revision === this.revision && error.name !== "AbortError") {
|
||||
this.closeSlot(slot, error);
|
||||
@@ -279,7 +365,7 @@ class BrowserEngine {
|
||||
this.primary.postMessage(data, transfer);
|
||||
return;
|
||||
}
|
||||
if (!this.closed) this.onmessage?.({data: {id: data.id, ok: false, error: error.message, canceled: error.name === "AbortError"}});
|
||||
if (!this.closed) this.onmessage?.({data: {engineID:this.engineID, id: data.id, ok: false, error: error.message, canceled: error.name === "AbortError"}});
|
||||
}).finally(() => {
|
||||
if (!fallback) this.requests.delete(data.id);
|
||||
slot.busy = false;
|
||||
@@ -436,14 +522,17 @@ class NativeHost {
|
||||
let closed = false;
|
||||
let unsubscribe;
|
||||
let pending = Promise.resolve();
|
||||
let termination = null;
|
||||
const readers = new Set();
|
||||
const inputs = new Set();
|
||||
const requests = new Map();
|
||||
const completions = new Map();
|
||||
const emit = data => {
|
||||
if (data.type === "result") { completions.get(data.id)?.(data.result); return; }
|
||||
if (!closed) worker.onmessage?.({data});
|
||||
if (!closed) worker.onmessage?.({data:{...data, engineID:worker.engineID}});
|
||||
};
|
||||
const ready = host.call("engine.open", {}).then(({engine, parallelism, cacheBytes}) => {
|
||||
worker.engineID = engine;
|
||||
if (closed) return engine;
|
||||
host.parallelism = Math.max(1, parallelism || 1);
|
||||
host.cacheBytes = cacheBytes || host.cacheBytes;
|
||||
@@ -452,8 +541,10 @@ class NativeHost {
|
||||
return engine;
|
||||
}).catch(error => { if (!closed) worker.onerror?.({message:error.message}); throw error; });
|
||||
const worker = {
|
||||
engineID:null,
|
||||
onmessage:null,
|
||||
onerror:null,
|
||||
configurePages() {},
|
||||
postMessage(data) {
|
||||
if (closed) return;
|
||||
if (data.type === "cancel") {
|
||||
@@ -461,6 +552,10 @@ class NativeHost {
|
||||
return;
|
||||
}
|
||||
if (data.type === "font") return;
|
||||
if (["ofdgoDetectFormat", "ofdgoOpen", "ofdgoConvertPDF"].includes(data.name)) {
|
||||
const input = data.args?.[0];
|
||||
if (input?.nativeFile && !input.temporary) inputs.add(input.nativeFile);
|
||||
}
|
||||
const controller = new AbortController();
|
||||
requests.set(data.id, controller);
|
||||
const parallel = data.name === "ofdgoRenderPage";
|
||||
@@ -502,14 +597,17 @@ class NativeHost {
|
||||
} else if (data.name !== "ofdgoSaveSnapshot") pending = operation;
|
||||
},
|
||||
terminate() {
|
||||
if (closed) return;
|
||||
if (closed) return termination;
|
||||
closed = true;
|
||||
for (const controller of requests.values()) controller.abort();
|
||||
ready.then(engine => host.call("engine.close", {engine})).catch(() => {}).finally(() => {
|
||||
termination = ready.then(engine => host.call("engine.close", {engine})).catch(() => {}).finally(async () => {
|
||||
unsubscribe?.();
|
||||
for (const resolve of completions.values()) resolve({ok:false, canceled:true, error:"会话已关闭"});
|
||||
completions.clear();
|
||||
await Promise.all([...inputs].map(file => host.call("release", {file}).catch(() => {})));
|
||||
inputs.clear();
|
||||
});
|
||||
return termination;
|
||||
},
|
||||
};
|
||||
ready.catch(() => {});
|
||||
|
||||
+98
-44
@@ -1,12 +1,15 @@
|
||||
importScripts("./wasm_exec.js");
|
||||
|
||||
let pending = Promise.resolve();
|
||||
const operations = new Map();
|
||||
const fontRequests = new Map();
|
||||
const engines = new Map();
|
||||
let resolveRuntime;
|
||||
let rejectRuntime;
|
||||
const runtimeReady = new Promise((resolve, reject) => { resolveRuntime = resolve; rejectRuntime = reject; });
|
||||
runtimeReady.catch(() => {});
|
||||
let fontSequence = 0;
|
||||
let engineMemory = null;
|
||||
|
||||
function requestFont(id, signal, name, done) {
|
||||
function requestFont(engine, id, signal, name, done) {
|
||||
const fontRequests = engine.fontRequests;
|
||||
if (signal.aborted) { done(null, "", true); return; }
|
||||
const request = ++fontSequence;
|
||||
const cancel = () => {
|
||||
@@ -18,7 +21,7 @@ function requestFont(id, signal, name, done) {
|
||||
signal.removeEventListener("abort", cancel);
|
||||
done(result.bytes, result.error || "", signal.aborted);
|
||||
});
|
||||
self.postMessage({ id, type: "font", request, name });
|
||||
self.postMessage({ engineID: engine.id, id, type: "font", request, name });
|
||||
}
|
||||
self.onmessage = ({ data }) => {
|
||||
if (data.type === "module") {
|
||||
@@ -26,24 +29,71 @@ self.onmessage = ({ data }) => {
|
||||
else loadWASM(data.module).catch(exitWASM);
|
||||
return;
|
||||
}
|
||||
const engineID = data.engineID;
|
||||
if (typeof engineID !== "string" || !engineID) {
|
||||
self.postMessage({ engineID, id: data.id, ok: false, error: "missing engine ID" });
|
||||
return;
|
||||
}
|
||||
if (data.type === "open") {
|
||||
if (engines.has(engineID)) {
|
||||
self.postMessage({ engineID, type: "exit", error: "engine already exists" });
|
||||
return;
|
||||
}
|
||||
const engine = { id: engineID, operations: new Map(), fontRequests: new Map(), active: new Set(), closed: false };
|
||||
engines.set(engineID, engine);
|
||||
engine.pending = runtimeReady.then(async () => {
|
||||
if (engine.closed) return;
|
||||
const opened = JSON.parse(globalThis.ofdgoEngineOpen(engineID));
|
||||
if (!opened.ok) throw new Error(opened.error);
|
||||
const backends = JSON.parse(await globalThis.ofdgoEngineCall(engineID, "ofdgoRenderBackends"));
|
||||
if (!backends.ok) throw new Error(backends.error);
|
||||
if (!engine.closed) self.postMessage({ engineID, type: "ready", backends: backends.data, memory: engineMemory.buffer.byteLength });
|
||||
}).catch(error => {
|
||||
engine.closed = true;
|
||||
self.postMessage({ engineID, type: "exit", error: error.message });
|
||||
});
|
||||
return;
|
||||
}
|
||||
const engine = engines.get(engineID);
|
||||
if (!engine && data.type === "close") {
|
||||
self.postMessage({ engineID, type: "closed" });
|
||||
return;
|
||||
}
|
||||
if (!engine || engine.closed && data.type !== "close") {
|
||||
self.postMessage({ engineID, id: data.id, ok: false, error: "unknown or closed engine" });
|
||||
return;
|
||||
}
|
||||
if (data.type === "close") {
|
||||
engine.closed = true;
|
||||
for (const operation of engine.operations.values()) operation.abort();
|
||||
engine.pending.then(async () => {
|
||||
await Promise.all([...engine.active]);
|
||||
await runtimeReady;
|
||||
globalThis.ofdgoEngineClose(engineID);
|
||||
engines.delete(engineID);
|
||||
self.postMessage({ engineID, type: "closed" });
|
||||
}).catch(() => engines.delete(engineID));
|
||||
return;
|
||||
}
|
||||
if (data.type === "font") {
|
||||
fontRequests.get(data.request)?.(data);
|
||||
fontRequests.delete(data.request);
|
||||
engine.fontRequests.get(data.request)?.(data);
|
||||
engine.fontRequests.delete(data.request);
|
||||
return;
|
||||
}
|
||||
if (data.type === "cancel") {
|
||||
operations.get(data.id)?.abort();
|
||||
engine.operations.get(data.id)?.abort();
|
||||
return;
|
||||
}
|
||||
if (["ofdgoResources", "ofdgoPrepareSave", "ofdgoSaveSnapshot"].includes(data.name)) operations.set(data.id, new AbortController());
|
||||
if (data.name === "ofdgoSaveSnapshot") {
|
||||
pending = pending.then(() => { void handleMessage(data); });
|
||||
return;
|
||||
}
|
||||
if (data.name === "ofdgoImportDocuments" || data.name === "ofdgoImportImages" || data.name === "ofdgoExportImages" || data.name === "ofdgoConvertFile" || data.name === "ofdgoPackFiles" || data.name === "ofdgoConvertPDF" || data.name === "ofdgoExportPage" || data.name === "ofdgoExportDocument" || data.name === "ofdgoExportAttachment" || data.name === "ofdgoExportResource" || data.name === "ofdgoExportMedia" || data.name === "ofdgoExportVersion" || data.name === "ofdgoSaveDocument" || data.name === "ofdgoSaveSigned" || data.name === "ofdgoImportPages") {
|
||||
operations.set(data.id, new AbortController());
|
||||
}
|
||||
pending = pending.then(() => data.name === "ofdgoConvertFile" || data.name === "ofdgoPackFiles" ? handleBatchMessage(data) : handleMessage(data));
|
||||
engine.operations.set(data.id, new AbortController());
|
||||
engine.pending = engine.pending.then(() => {
|
||||
const operation = (data.name === "ofdgoConvertFile" || data.name === "ofdgoPackFiles" ? handleBatchMessage(engine, data) : handleMessage(engine, data)).catch(error => {
|
||||
engine.operations.delete(data.id);
|
||||
self.postMessage({ engineID, id: data.id, ok: false, error: error.message });
|
||||
});
|
||||
engine.active.add(operation);
|
||||
operation.finally(() => engine.active.delete(operation));
|
||||
if (data.name !== "ofdgoSaveSnapshot") return operation;
|
||||
});
|
||||
};
|
||||
|
||||
async function availableOutput(directory, base, format) {
|
||||
@@ -59,7 +109,9 @@ async function availableOutput(directory, base, format) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBatchMessage({ id, name, args }) {
|
||||
async function handleBatchMessage(engine, { id, name, args }) {
|
||||
const operations = engine.operations;
|
||||
const postMessage = (data, transfer) => self.postMessage({ ...data, engineID: engine.id }, transfer);
|
||||
const signal = operations.get(id).signal;
|
||||
const channel = new MessageChannel();
|
||||
const finish = (done, err) => {
|
||||
@@ -89,7 +141,7 @@ async function handleBatchMessage({ id, name, args }) {
|
||||
if (name === "ofdgoConvertFile") {
|
||||
const [file, options, fonts, destination] = args;
|
||||
credentials = options.credentials;
|
||||
const formats = JSON.parse(globalThis.ofdgoOutputFormats()).data;
|
||||
const formats = JSON.parse(await globalThis.ofdgoEngineCall(engine.id, "ofdgoOutputFormats")).data;
|
||||
const format = formats.find(item => item.value === options.format);
|
||||
if (!format) throw new Error("不支持此格式");
|
||||
root = destination;
|
||||
@@ -100,7 +152,7 @@ async function handleBatchMessage({ id, name, args }) {
|
||||
if (format.paged) directory = await root.getDirectoryHandle(target.name, { create: true });
|
||||
else { await root.getFileHandle(target.name, { create: true }); directory = root; }
|
||||
created = target.name;
|
||||
self.postMessage({ id, type: "output", name: created });
|
||||
postMessage({ id, type: "output", name: created });
|
||||
return target;
|
||||
}) : { stem: base, name: format.paged ? base : `${base}.${format.extension}` };
|
||||
const boundary = async (action, pageName) => {
|
||||
@@ -113,7 +165,7 @@ async function handleBatchMessage({ id, name, args }) {
|
||||
const handle = await directory.getFileHandle(fileName, { create: true });
|
||||
if (!format.paged) {
|
||||
created = fileName;
|
||||
self.postMessage({ id, type: "output", name: created });
|
||||
postMessage({ id, type: "output", name: created });
|
||||
}
|
||||
current.handle = handle;
|
||||
output = await handle.createWritable();
|
||||
@@ -126,19 +178,19 @@ async function handleBatchMessage({ id, name, args }) {
|
||||
chunks = [];
|
||||
}
|
||||
};
|
||||
payload = await globalThis.ofdgoConvertFile(read(file), file.size, options, write,
|
||||
payload = await globalThis.ofdgoEngineCall(engine.id, "ofdgoConvertFile", read(file), file.size, options, write,
|
||||
(phase, completed, total, done) => {
|
||||
self.postMessage({ id, type: "conversion", phase, completed, total, memory: engineMemory.buffer.byteLength });
|
||||
postMessage({ id, type: "conversion", phase, completed, total, memory: engineMemory.buffer.byteLength });
|
||||
finish(done);
|
||||
},
|
||||
(action, pageName, done) => boundary(action, pageName).then(() => finish(done), err => finish(done, err)),
|
||||
done => finish(done), fonts, (name, done) => requestFont(id, signal, name, done));
|
||||
done => finish(done), fonts, (name, done) => requestFont(engine, id, signal, name, done));
|
||||
} else {
|
||||
const [entries, handle, compression] = args;
|
||||
if (handle) output = await handle.createWritable();
|
||||
payload = await globalThis.ofdgoPackFiles(entries.map(entry => ({ name: entry.name, size: entry.file.size })),
|
||||
payload = await globalThis.ofdgoEngineCall(engine.id, "ofdgoPackFiles", entries.map(entry => ({ name: entry.name, size: entry.file.size })),
|
||||
index => read(entries[index].file), write,
|
||||
(completed, total, done) => { self.postMessage({ id, type: "conversion", phase: "pack", completed, total, memory: engineMemory.buffer.byteLength }); finish(done); },
|
||||
(completed, total, done) => { postMessage({ id, type: "conversion", phase: "pack", completed, total, memory: engineMemory.buffer.byteLength }); finish(done); },
|
||||
done => finish(done), compression);
|
||||
}
|
||||
signal.throwIfAborted();
|
||||
@@ -157,7 +209,7 @@ async function handleBatchMessage({ id, name, args }) {
|
||||
}
|
||||
result.data.size = size;
|
||||
operations.delete(id);
|
||||
self.postMessage({ id, ...result, memory: engineMemory.buffer.byteLength });
|
||||
postMessage({ id, ...result, memory: engineMemory.buffer.byteLength });
|
||||
} catch (err) {
|
||||
await output?.abort().catch(() => {});
|
||||
let message = signal.aborted ? "转换已取消" : err.message;
|
||||
@@ -165,7 +217,7 @@ async function handleBatchMessage({ id, name, args }) {
|
||||
try { await root.removeEntry(created, { recursive: true }); }
|
||||
catch (cleanupError) { message += `;清理失败:${cleanupError.message}`; }
|
||||
}
|
||||
self.postMessage({ id, ok: false, error: message, code: err.code, canceled: signal.aborted, memory: engineMemory.buffer.byteLength });
|
||||
postMessage({ id, ok: false, error: message, code: err.code, canceled: signal.aborted, memory: engineMemory.buffer.byteLength });
|
||||
} finally {
|
||||
credentials?.password?.fill(0);
|
||||
credentials?.key?.fill(0);
|
||||
@@ -175,12 +227,15 @@ async function handleBatchMessage({ id, name, args }) {
|
||||
operations.delete(id);
|
||||
}
|
||||
}
|
||||
async function handleMessage({ id, name, args }) {
|
||||
async function handleMessage(engine, { id, name, args }) {
|
||||
const operations = engine.operations;
|
||||
const postMessage = (data, transfer) => self.postMessage({ ...data, engineID: engine.id }, transfer);
|
||||
let output;
|
||||
let saveFile;
|
||||
let saveStamp;
|
||||
let channel;
|
||||
const signal = operations.get(id)?.signal;
|
||||
const cancellable = ["ofdgoResources", "ofdgoPrepareSave", "ofdgoSaveSnapshot", "ofdgoImportDocuments", "ofdgoImportImages", "ofdgoExportImages", "ofdgoConvertPDF", "ofdgoExportPage", "ofdgoExportDocument", "ofdgoExportAttachment", "ofdgoExportResource", "ofdgoExportMedia", "ofdgoExportVersion", "ofdgoSaveDocument", "ofdgoSaveSigned", "ofdgoImportPages"].includes(name);
|
||||
const importing = name === "ofdgoImportPages" || name === "ofdgoImportImages" || name === "ofdgoImportDocuments";
|
||||
const converting = name === "ofdgoConvertPDF";
|
||||
const saving = name === "ofdgoSaveDocument" || name === "ofdgoSaveSnapshot" || name === "ofdgoSaveSigned";
|
||||
@@ -222,7 +277,7 @@ async function handleMessage({ id, name, args }) {
|
||||
}
|
||||
const chunks = [];
|
||||
let size = 0;
|
||||
if (signal) {
|
||||
if (cancellable) {
|
||||
channel = new MessageChannel();
|
||||
const finish = (done, err, commit = false) => {
|
||||
channel.port1.onmessage = () => {
|
||||
@@ -235,10 +290,10 @@ async function handleMessage({ id, name, args }) {
|
||||
args.push(done => finish(done));
|
||||
} else if (importing || converting) {
|
||||
args.push((phase, completed, total, done) => {
|
||||
self.postMessage({ id, type: importing ? "import" : "conversion", phase, completed, total });
|
||||
postMessage({ id, type: importing ? "import" : "conversion", phase, completed, total });
|
||||
finish(done, null, importing && phase === "commit");
|
||||
});
|
||||
if (converting) args.push((name, done) => requestFont(id, signal, name, done));
|
||||
if (converting) args.push((name, done) => requestFont(engine, id, signal, name, done));
|
||||
} else {
|
||||
const destination = args.pop();
|
||||
const file = destination?.fileHandle || destination;
|
||||
@@ -263,17 +318,17 @@ async function handleMessage({ id, name, args }) {
|
||||
});
|
||||
if (name === "ofdgoExportDocument") {
|
||||
args.push((completed, total, done) => {
|
||||
self.postMessage({ id, type: "export", stage: "pages", completed, total });
|
||||
postMessage({ id, type: "export", stage: "pages", completed, total });
|
||||
finish(done);
|
||||
});
|
||||
} else if (name === "ofdgoExportVersion") {
|
||||
args.push((phase, completed, total, done) => {
|
||||
self.postMessage({ id, type: "export", stage: "prepare", phase, completed, total });
|
||||
postMessage({ id, type: "export", stage: "prepare", phase, completed, total });
|
||||
finish(done);
|
||||
}, done => finish(done));
|
||||
} else if (saving) {
|
||||
args.push((phase, completed, total, done) => {
|
||||
self.postMessage({ id, type: "export", stage: "prepare", phase, completed, total });
|
||||
postMessage({ id, type: "export", stage: "prepare", phase, completed, total });
|
||||
finish(done);
|
||||
});
|
||||
if (compression || name === "ofdgoSaveSnapshot") args.push(indices, compression || {mode: 0}, done => finish(done));
|
||||
@@ -282,12 +337,12 @@ async function handleMessage({ id, name, args }) {
|
||||
if (!saving && args.some(value => value?.mode !== undefined)) args.push(done => finish(done));
|
||||
}
|
||||
}
|
||||
const payload = await globalThis[name](...args);
|
||||
const payload = await globalThis.ofdgoEngineCall(engine.id, name, ...args);
|
||||
signal?.throwIfAborted();
|
||||
const result = typeof payload === "string" ? JSON.parse(payload) : payload;
|
||||
if (signal && !importing && !converting && !resources && result.ok) {
|
||||
if (cancellable && !importing && !converting && !resources && result.ok) {
|
||||
result.data.size = size;
|
||||
if (name !== "ofdgoExportMedia") self.postMessage({ id, type: "export", stage: "save" });
|
||||
if (name !== "ofdgoExportMedia") postMessage({ id, type: "export", stage: "save" });
|
||||
if (output) {
|
||||
if (saveStamp) {
|
||||
const current = await saveFile.getFile();
|
||||
@@ -304,15 +359,15 @@ async function handleMessage({ id, name, args }) {
|
||||
}
|
||||
output = null;
|
||||
const transfer = [...new Set([result.data?.bytes, ...(result.data?.images || []).map(image => image.bytes), ...(result.data?.fonts || []).map(font => font.data)].filter(bytes => bytes?.byteLength).map(bytes => bytes.buffer))];
|
||||
self.postMessage({ id, ...result, memory: engineMemory.buffer.byteLength }, transfer);
|
||||
postMessage({ id, ...result, memory: engineMemory.buffer.byteLength }, transfer);
|
||||
} catch (err) {
|
||||
await output?.abort().catch(() => {});
|
||||
const result = { id, ok: false, error: err.message, errorName: err.name };
|
||||
if (signal?.aborted) {
|
||||
result.error = resources ? "查询已取消" : importing ? "导入已取消" : converting ? "转换已取消" : "导出已取消";
|
||||
result.error = resources ? "查询已取消" : importing ? "导入已取消" : converting ? "转换已取消" : cancellable ? "导出已取消" : "操作已取消";
|
||||
result.canceled = true;
|
||||
}
|
||||
self.postMessage(result);
|
||||
postMessage(result);
|
||||
} finally {
|
||||
password?.fill(0);
|
||||
key?.fill(0);
|
||||
@@ -324,6 +379,7 @@ async function handleMessage({ id, name, args }) {
|
||||
}
|
||||
|
||||
function exitWASM(err) {
|
||||
rejectRuntime(err || new Error("引擎运行中断"));
|
||||
self.postMessage({ type: "exit", error: err?.message || "引擎运行中断" });
|
||||
self.close();
|
||||
}
|
||||
@@ -335,9 +391,7 @@ async function loadWASM(module) {
|
||||
self.postMessage({ type: "progress", phase: "start", text: "正在启动引擎", percent: 58 });
|
||||
go.run(instance).then(exitWASM, exitWASM);
|
||||
if (!go.exited) {
|
||||
const backends = JSON.parse(globalThis.ofdgoRenderBackends());
|
||||
if (!backends.ok) throw new Error(backends.error);
|
||||
self.postMessage({ type: "ready", backends: backends.data, memory: engineMemory.buffer.byteLength });
|
||||
resolveRuntime();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ require (
|
||||
github.com/go-text/typesetting v0.3.5
|
||||
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
|
||||
github.com/tdewolff/font v0.0.0-20261010204837-88a7f6b56918
|
||||
github.com/xiaoqidun/guigo v0.0.0-20261020075840-622fd8853d72
|
||||
github.com/xiaoqidun/guigo v0.0.0-20261022051313-f2a6a53e3537
|
||||
github.com/xiaoqidun/j2kgo v1.0.2
|
||||
github.com/xiaoqidun/jbig2 v1.1.8
|
||||
github.com/xiaoqidun/pdfgo v0.0.0-20261021071541-3fdb0cab9d95
|
||||
|
||||
@@ -60,8 +60,8 @@ github.com/tdewolff/parse/v2 v2.8.16 h1:bLk5svUOQRkW/Y2SJ+DeENSIkZBcTIkq+Atyv5D8
|
||||
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/xiaoqidun/guigo v0.0.0-20261020075840-622fd8853d72 h1:rzIbnmVre37b9G4EYyznItUBy+pjw6wz5Lcg50cDPf4=
|
||||
github.com/xiaoqidun/guigo v0.0.0-20261020075840-622fd8853d72/go.mod h1:xJF8xE6Bjw+NcOZNvWJEVX/XXIkngs3ltc9F6vNcLls=
|
||||
github.com/xiaoqidun/guigo v0.0.0-20261022051313-f2a6a53e3537 h1:s96G2hbG89CN3eWpoLD4jc9K1U6OSvD9ly30eQJXY5c=
|
||||
github.com/xiaoqidun/guigo v0.0.0-20261022051313-f2a6a53e3537/go.mod h1:xJF8xE6Bjw+NcOZNvWJEVX/XXIkngs3ltc9F6vNcLls=
|
||||
github.com/xiaoqidun/j2kgo v1.0.2 h1:OAjjEOfaBaQzBNcj9wx+eM68UI5pewR6SzNZa1R1DIw=
|
||||
github.com/xiaoqidun/j2kgo v1.0.2/go.mod h1:DCwAfuDzCDYqUiAlZ7BDwigTjaXw7o9L1FtEma/D1aA=
|
||||
github.com/xiaoqidun/jbig2 v1.1.8 h1:dERPW7F8gs18a/DOD9gXHR3fP1Vl6KhCNsLnq5l3BIo=
|
||||
|
||||
@@ -37,42 +37,74 @@ const (
|
||||
// platformValue 浏览器参数值
|
||||
type platformValue = js.Value
|
||||
|
||||
// wasmEngines 管理共享资源的独立浏览器业务会话
|
||||
type wasmEngines struct {
|
||||
resources *ofdgo.Resources
|
||||
engines map[string]*businessEngine
|
||||
}
|
||||
|
||||
// RunWASM 注册浏览器WASM接口并阻塞运行
|
||||
func RunWASM() {
|
||||
resources, _ := ofdgo.NewResources(ofdgo.ResourceOptions{Parallelism: 1, MemoryLimit: 64 << 20, CacheBytes: 48 << 20, SnapshotBytes: 32 << 20})
|
||||
e := &businessEngine{resources: resources}
|
||||
asynchronous := map[string]bool{
|
||||
"ofdgoDetectFormat": true,
|
||||
"ofdgoExportVersion": true,
|
||||
"ofdgoConvertPDF": true,
|
||||
"ofdgoConvertFile": true,
|
||||
"ofdgoPackFiles": true,
|
||||
"ofdgoExportPage": true,
|
||||
"ofdgoExportDocument": true,
|
||||
"ofdgoExportAttachment": true,
|
||||
"ofdgoExportMedia": true,
|
||||
"ofdgoResources": true,
|
||||
"ofdgoExportResource": true,
|
||||
"ofdgoExportImages": true,
|
||||
"ofdgoImportImages": true,
|
||||
"ofdgoImportPages": true,
|
||||
"ofdgoImportDocuments": true,
|
||||
"ofdgoSaveDocument": true,
|
||||
"ofdgoPrepareSave": true,
|
||||
"ofdgoSaveSnapshot": true,
|
||||
"ofdgoSetEncryption": true,
|
||||
"ofdgoSaveSigned": true,
|
||||
}
|
||||
for name, callback := range e.businessCommands() {
|
||||
if asynchronous[name] {
|
||||
registerAsyncCallback(name, callback)
|
||||
} else {
|
||||
registerCallback(name, callback)
|
||||
}
|
||||
}
|
||||
engines := &wasmEngines{resources: resources, engines: make(map[string]*businessEngine)}
|
||||
registerCallback("ofdgoEngineOpen", engines.open)
|
||||
registerAsyncCallback("ofdgoEngineCall", engines.call)
|
||||
registerCallback("ofdgoEngineClose", engines.close)
|
||||
select {}
|
||||
}
|
||||
|
||||
// open 创建具有稳定标识的独立业务会话
|
||||
// 入参: args 会话标识
|
||||
// 返回: any 空结果, error 参数或会话错误
|
||||
func (w *wasmEngines) open(args []js.Value) (any, error) {
|
||||
if len(args) != 1 || args[0].Type() != js.TypeString || args[0].String() == "" {
|
||||
return nil, fmt.Errorf("missing engine ID")
|
||||
}
|
||||
id := args[0].String()
|
||||
if w.engines[id] != nil {
|
||||
return nil, fmt.Errorf("engine already exists: %s", id)
|
||||
}
|
||||
w.engines[id] = &businessEngine{resources: w.resources}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// call 按会话标识分派业务命令,不提供默认会话
|
||||
// 入参: args 会话标识、命令及业务参数
|
||||
// 返回: any 业务结果, error 调用错误
|
||||
func (w *wasmEngines) call(args []js.Value) (any, error) {
|
||||
if len(args) < 2 || args[0].Type() != js.TypeString || args[1].Type() != js.TypeString {
|
||||
return nil, fmt.Errorf("missing engine ID or operation")
|
||||
}
|
||||
e := w.engines[args[0].String()]
|
||||
if e == nil {
|
||||
return nil, fmt.Errorf("unknown engine: %s", args[0].String())
|
||||
}
|
||||
fn := e.businessCommands()[args[1].String()]
|
||||
if fn == nil {
|
||||
return nil, fmt.Errorf("unknown operation: %s", args[1].String())
|
||||
}
|
||||
return fn(args[2:])
|
||||
}
|
||||
|
||||
// close 释放会话的文档、编辑器、导入及剪贴板,不关闭共享资源
|
||||
// 入参: args 会话标识
|
||||
// 返回: any 空结果, error 参数或关闭错误
|
||||
func (w *wasmEngines) close(args []js.Value) (any, error) {
|
||||
if len(args) != 1 || args[0].Type() != js.TypeString || args[0].String() == "" {
|
||||
return nil, fmt.Errorf("missing engine ID")
|
||||
}
|
||||
id := args[0].String()
|
||||
e := w.engines[id]
|
||||
if e == nil {
|
||||
return nil, nil
|
||||
}
|
||||
delete(w.engines, id)
|
||||
e.clearImport()
|
||||
err := e.currentSession.Close()
|
||||
*e = businessEngine{}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// awaitExport 等待浏览器处理导出检查点
|
||||
// 入参: fn 浏览器回调, args 回调参数
|
||||
// 返回: error 写入错误或取消原因
|
||||
|
||||
+98
-15
@@ -50,6 +50,7 @@ type Host struct {
|
||||
files *fileStore
|
||||
engines map[string]*webui.NativeEngine
|
||||
engineStops map[string]func() bool
|
||||
closingEngines map[string]*engineClose
|
||||
conversionPool *ofdgo.ConversionPool
|
||||
resources *ofdgo.Resources
|
||||
cancels map[string]context.CancelFunc
|
||||
@@ -59,6 +60,14 @@ type Host struct {
|
||||
fontFiles map[string]string
|
||||
fontError error
|
||||
pendingPaths []string
|
||||
frontendReady bool
|
||||
closeAllowed bool
|
||||
}
|
||||
|
||||
// engineClose 保存并发关闭共享的完成通知及结果
|
||||
type engineClose struct {
|
||||
done chan struct{}
|
||||
err error
|
||||
}
|
||||
|
||||
// resourceReader 保存资源读取位置及关闭操作
|
||||
@@ -92,7 +101,7 @@ func Run(ctx context.Context, paths []string, dataDir string) error {
|
||||
h.conversionPool = h.resources.ConversionPool()
|
||||
defer h.close()
|
||||
bindings := make(map[string]guigo.HandlerFunc)
|
||||
for _, name := range []string{"engine.open", "engine.close", "engine.call", "engine.start", "engine.cancel", "engine.finish", "batch", "open", "save", "directory", "file", "release", "drop.release", "child", "remove", "write.begin", "write.data", "write.commit", "write.abort", "fonts", "ready", "title"} {
|
||||
for _, name := range []string{"engine.open", "engine.close", "engine.call", "engine.start", "engine.cancel", "engine.finish", "batch", "open", "save", "directory", "file", "release", "drop.release", "child", "remove", "write.begin", "write.data", "write.commit", "write.abort", "fonts", "ready", "title", "window.info", "window.drag", "window.close"} {
|
||||
bindings["ofdgo.host."+name] = func(ctx context.Context, args any) (any, error) { return h.call(ctx, name, args) }
|
||||
}
|
||||
static := http.FileServerFS(webuiassets.NativeFS)
|
||||
@@ -124,6 +133,18 @@ func Run(ctx context.Context, paths []string, dataDir string) error {
|
||||
h.mu.Unlock()
|
||||
}
|
||||
options.OnDrop = h.openPaths
|
||||
options.OnTitleBarChanged = func(info guigo.TitleBarInfo) {
|
||||
h.emit("ofdgo.window", info)
|
||||
}
|
||||
options.OnClosing = func(ctx context.Context) bool {
|
||||
h.mu.Lock()
|
||||
allowed := h.closeAllowed || !h.frontendReady
|
||||
h.mu.Unlock()
|
||||
if !allowed {
|
||||
h.emit("ofdgo.closing", nil)
|
||||
}
|
||||
return allowed
|
||||
}
|
||||
options.OnError = func(err error) {
|
||||
h.emit("ofdgo.error", err.Error())
|
||||
}
|
||||
@@ -514,10 +535,47 @@ func (h *Host) call(ctx context.Context, name string, value any) (any, error) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, window.SetTitle(text("title"))
|
||||
case "window.info", "window.drag", "window.close":
|
||||
h.mu.Lock()
|
||||
window := h.window
|
||||
h.mu.Unlock()
|
||||
if window == nil {
|
||||
return nil, fmt.Errorf("窗口尚未就绪")
|
||||
}
|
||||
if name == "window.info" {
|
||||
return window.TitleBarInfo()
|
||||
}
|
||||
if name == "window.close" {
|
||||
h.mu.Lock()
|
||||
h.closeAllowed = true
|
||||
h.mu.Unlock()
|
||||
return nil, window.Close()
|
||||
}
|
||||
values, ok := args["regions"].([]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("拖动区域无效")
|
||||
}
|
||||
regions := make([]guigo.Rect, 0, len(values))
|
||||
for _, value := range values {
|
||||
item, ok := value.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("拖动区域无效")
|
||||
}
|
||||
x, xOK := item["x"].(float64)
|
||||
y, yOK := item["y"].(float64)
|
||||
width, widthOK := item["width"].(float64)
|
||||
height, heightOK := item["height"].(float64)
|
||||
if !xOK || !yOK || !widthOK || !heightOK {
|
||||
return nil, fmt.Errorf("拖动区域无效")
|
||||
}
|
||||
regions = append(regions, guigo.Rect{X: x, Y: y, Width: width, Height: height})
|
||||
}
|
||||
return nil, window.SetDragRegions(regions)
|
||||
case "ready":
|
||||
h.mu.Lock()
|
||||
paths := h.pendingPaths
|
||||
h.pendingPaths = nil
|
||||
h.frontendReady = true
|
||||
h.mu.Unlock()
|
||||
if len(paths) != 0 {
|
||||
h.openPaths(paths)
|
||||
@@ -764,7 +822,21 @@ func (h *Host) loadFont(ctx context.Context, name string) ([]byte, error) {
|
||||
func (h *Host) closeEngine(id string) error {
|
||||
var results []any
|
||||
h.mu.Lock()
|
||||
if closing := h.closingEngines[id]; closing != nil {
|
||||
h.mu.Unlock()
|
||||
<-closing.done
|
||||
return closing.err
|
||||
}
|
||||
engine := h.engines[id]
|
||||
if engine == nil {
|
||||
h.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
closing := &engineClose{done: make(chan struct{})}
|
||||
if h.closingEngines == nil {
|
||||
h.closingEngines = make(map[string]*engineClose)
|
||||
}
|
||||
h.closingEngines[id] = closing
|
||||
delete(h.engines, id)
|
||||
if stop := h.engineStops[id]; stop != nil {
|
||||
stop()
|
||||
@@ -785,10 +857,13 @@ func (h *Host) closeEngine(id string) error {
|
||||
for _, result := range results {
|
||||
h.discard(result)
|
||||
}
|
||||
if engine != nil {
|
||||
return engine.Close()
|
||||
}
|
||||
return nil
|
||||
err := engine.Close()
|
||||
h.mu.Lock()
|
||||
closing.err = err
|
||||
delete(h.closingEngines, id)
|
||||
close(closing.done)
|
||||
h.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
// close 取消任务并释放当前窗口持有的会话
|
||||
@@ -797,17 +872,18 @@ func (h *Host) close() {
|
||||
for _, cancel := range h.cancels {
|
||||
cancel()
|
||||
}
|
||||
engines := h.engines
|
||||
engines := make([]string, 0, len(h.engines)+len(h.closingEngines))
|
||||
for id := range h.engines {
|
||||
engines = append(engines, id)
|
||||
}
|
||||
for id := range h.closingEngines {
|
||||
engines = append(engines, id)
|
||||
}
|
||||
results := h.batchResults
|
||||
h.batchResults = nil
|
||||
for _, stop := range h.engineStops {
|
||||
stop()
|
||||
}
|
||||
h.engineStops = nil
|
||||
h.engines = make(map[string]*webui.NativeEngine)
|
||||
h.mu.Unlock()
|
||||
for _, engine := range engines {
|
||||
_ = engine.Close()
|
||||
for _, id := range engines {
|
||||
_ = h.closeEngine(id)
|
||||
}
|
||||
for _, result := range results {
|
||||
h.discard(result)
|
||||
@@ -842,8 +918,15 @@ func windowOptions(dataDir string) (guigo.Options, error) {
|
||||
return guigo.Options{}, err
|
||||
}
|
||||
return guigo.Options{
|
||||
AppID: applicationID,
|
||||
Title: "OFDGo - OFD编辑器",
|
||||
AppID: applicationID,
|
||||
Title: "OFDGo - OFD编辑器",
|
||||
TitleBar: guigo.TitleBarContent,
|
||||
TitleBarColors: guigo.TitleBarColors{
|
||||
Background: "#e8edf0",
|
||||
Foreground: "#1f2428",
|
||||
InactiveBackground: "#eef1f4",
|
||||
InactiveForeground: "#66737f",
|
||||
},
|
||||
Icon: icon,
|
||||
Width: 1440,
|
||||
Height: 900,
|
||||
|
||||
@@ -333,17 +333,18 @@ func (s *fileStore) resource(id string) (io.ReaderAt, int64, func() error, error
|
||||
return file, info.Size(), file.Close, nil
|
||||
}
|
||||
|
||||
// release 释放内存结果或临时文件并取消页面清理回调
|
||||
// release 释放读取快照、内存结果或临时文件,保留在途读取及源文件
|
||||
// 入参: id 资源标识
|
||||
func (s *fileStore) release(id string) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if entry := s.entries[id]; entry != nil && (entry.data != nil || entry.temporary) {
|
||||
if entry := s.entries[id]; entry != nil && (entry.data != nil || entry.temporary || entry.file != nil) {
|
||||
delete(s.entries, id)
|
||||
entry.retired = true
|
||||
if entry.stop != nil {
|
||||
entry.stop()
|
||||
}
|
||||
if entry.file != nil {
|
||||
if entry.file != nil && entry.readers == 0 {
|
||||
entry.file.Close()
|
||||
}
|
||||
if entry.temporary {
|
||||
|
||||
Reference in new issue
Block a user