diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index e21c8ea..1a81569 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -19,7 +19,7 @@ const STATUS = { }; const wasmRequests = new Map(); const metaContents = new WeakMap(); -const batch = { items: [], formats: [], running: false, canceled: false, worker: null, requests: new Map(), sequence: 0, activeID: 0, progressTime: 0 }; +const batch = { items: [], formats: [], running: false, canceled: false, worker: null, requests: new Map(), sequence: 0, activeID: 0, progressTime: 0, progress: null }; const batchElements = Object.fromEntries(["Button", "Panel", "Form", "Input", "Add", "Clear", "Count", "Format", "Compression", "FontRow", "FontMode", "Destination", "DPIRow", "DPI", "List", "Empty", "Progress", "Status", "Close", "Cancel", "Start"].map(name => [name, document.querySelector(`#convert${name}`)])); const fontBinding = { font: null, choices: [], sequence: 0 }; const fontBindingElements = Object.fromEntries(["Panel", "Form", "Title", "Name", "SourceRow", "Source", "File", "Hint", "Status", "Cancel", "Submit"].map(name => [name, document.querySelector(`#fontBinding${name}`)])); @@ -5374,16 +5374,30 @@ function batchStatus(item, status, text) { } function batchProgress(item, position, count, progress) { + if (batch.canceled) return; const now = performance.now(); - if (now - batch.progressTime < 80 && progress.completed !== progress.total) return; + const previous = batch.progress; + const sameStage = previous?.item === item && previous?.progress.phase === progress.phase; + if (sameStage && now - batch.progressTime < 80 && progress.completed !== progress.total) return; batch.progressTime = now; + batch.progress = { item, position, count, progress, started: sameStage ? previous.started : now }; + updateBatchProgress(); +} + +function updateBatchProgress() { + if (!batch.progress) return; + const { item, position, count, progress, started } = batch.progress; const compressing = progress.phase === "compress" || progress.phase === "write.compress"; - const stage = compressing ? "压缩" : progress.phase?.startsWith("write.") ? "写入" : { open: "读取", pages: "解析", convert: "转换", prepare: "准备", fonts: "字体", resources: "资源", references: "检查", write: "写入", export: "导出", pack: "打包", commit: "保存" }[progress.phase] || "处理"; - const detail = progress.total > 0 ? `${progress.completed}/${progress.total}` : compressing && progress.completed ? `已处理 ${progress.completed} 项` : ""; + const fontMode = (item?.format || batchElements.Format.value) === "ofd" ? batchElements.FontMode.value : "0"; + const stage = compressing ? "压缩" : progress.phase?.startsWith("write.") ? "写入" : { open: "读取", pages: "解析", convert: "转换", prepare: "准备", fonts: { 1: "嵌入", 2: "外置" }[fontMode] || "整理", "fonts.prepare": "准备", "fonts.scan": "检查", "fonts.commit": "整理", resources: "资源", references: "检查", write: "写入", export: "导出", pack: "打包", commit: "保存" }[progress.phase] || "处理"; + const detail = progress.total > 0 ? `${progress.completed}/${progress.total}` : (compressing || progress.phase === "fonts") ? `已处理 ${progress.completed || 0} 项` : ""; + const seconds = Math.floor((performance.now() - started) / 1000); if (item) batchStatus(item, "running", stage); - batchElements.Status.textContent = progress.phase === "pack" ? `打包 ${detail}` : `文件 ${position + 1}/${count} · ${stage}${detail ? ` ${detail}` : ""}`; - batchElements.Progress.max = count; - batchElements.Progress.value = progress.phase === "pack" && progress.total > 0 ? progress.completed / progress.total : position; + batchElements.Status.textContent = `${progress.phase === "pack" ? "" : `文件 ${position + 1}/${count} · `}正在${stage}${detail ? ` · ${detail}` : ""}${seconds ? ` · ${seconds}秒` : ""}`; + batchElements.Progress.setAttribute("aria-label", "阶段进度"); + batchElements.Progress.max = progress.total || 1; + if (progress.total > 0) batchElements.Progress.value = Math.min(progress.completed, progress.total); + else batchElements.Progress.removeAttribute("value"); } function batchCall(name, args, onProgress) { @@ -5421,7 +5435,10 @@ function startBatchWorker() { worker.onmessage = async ({ data }) => { if (data.type === "ready") { resolve(); return; } if (data.type === "exit") { fail(new Error(data.error)); return; } - if (data.type === "progress") { batchElements.Status.textContent = data.text; return; } + if (data.type === "progress") { + if (!batch.canceled) batchElements.Status.textContent = { download: "正在下载", compile: "正在编译", start: "正在启动" }[data.phase] || "正在准备"; + return; + } const request = batch.requests.get(data.id); if (!request) return; if (data.type === "font") { @@ -5449,7 +5466,8 @@ function startBatchWorker() { function cancelBatch() { batch.canceled = true; batchElements.Cancel.disabled = true; - batchElements.Status.textContent = "正在取消操作"; + batch.progress = null; + batchElements.Status.textContent = "正在取消"; if (batch.activeID) batch.worker.postMessage({ type: "cancel", id: batch.activeID }); } @@ -5485,6 +5503,8 @@ async function runBatch() { } batch.running = true; batch.canceled = false; + batch.progress = null; + const progressTimer = window.setInterval(updateBatchProgress, 1000); renderBatchList(); window.addEventListener("beforeunload", warnBatch); let destination = null; @@ -5510,7 +5530,7 @@ async function runBatch() { if (batch.canceled) return; batch.localFonts = fontManager.localEntries(); batch.fontData = new Map(); - batchElements.Status.textContent = "正在准备引擎"; + batchElements.Status.textContent = "正在准备"; await startBatchWorker(); const fonts = await fontManager.files(fontManager.records()); const names = new Set(); @@ -5554,11 +5574,13 @@ async function runBatch() { item.details = [err.message]; if (batch.workerError) throw err; } finally { + batch.progress = null; options.credentials?.password?.fill(0); options.credentials?.key?.fill(0); options.credentials?.keyPassword?.fill(0); } batchElements.Progress.max = jobs.length; + batchElements.Progress.setAttribute("aria-label", "转换进度"); batchElements.Progress.value = position + 1; } if (archive && entries.length && !batch.canceled) { @@ -5570,10 +5592,13 @@ async function runBatch() { const done = jobs.filter(item => item.status === "done").length; const failed = jobs.filter(item => item.status === "failed").length; const unchanged = batch.items.filter(item => item.status === "unchanged").length; - batchElements.Status.textContent = `${batch.canceled ? "已取消 · " : ""}完成 ${done}${failed ? ` · 失败 ${failed}` : ""}${unchanged ? ` · 原样 ${unchanged}` : ""}`; + batchElements.Status.textContent = `${batch.canceled ? "取消 · " : ""}完成 ${done}${failed ? ` · 失败 ${failed}` : ""}${unchanged ? ` · 原样 ${unchanged}` : ""}`; } catch (err) { - batchElements.Status.textContent = err.name === "AbortError" ? "转换已取消" : err.message; + batchElements.Status.textContent = err.name === "AbortError" ? "转换取消" : err.message; } finally { + window.clearInterval(progressTimer); + batch.progress = null; + if (!batchElements.Progress.hasAttribute("value")) batchElements.Progress.value = 0; if (archive && !packed) for (const item of jobs) if (item.status === "ready") { item.status = "pending"; item.text = "待转"; } batch.worker?.terminate(); batch.worker = null; diff --git a/assets/webui/ofdgo_wasm.js b/assets/webui/ofdgo_wasm.js index 3e2763a..c46c75f 100644 --- a/assets/webui/ofdgo_wasm.js +++ b/assets/webui/ofdgo_wasm.js @@ -291,11 +291,11 @@ function exitWASM(err) { async function loadWASM() { const go = new Go(); - self.postMessage({ type: "progress", text: "正在下载引擎", percent: 16 }); + self.postMessage({ type: "progress", phase: "download", text: "正在下载引擎", percent: 16 }); const response = await fetch("./ofdgo.wasm"); - self.postMessage({ type: "progress", text: "正在编译引擎", percent: 35 }); + self.postMessage({ type: "progress", phase: "compile", text: "正在编译引擎", percent: 35 }); const { instance } = await WebAssembly.instantiateStreaming(response, go.importObject); - self.postMessage({ type: "progress", text: "正在启动引擎", percent: 58 }); + self.postMessage({ type: "progress", phase: "start", text: "正在启动引擎", percent: 58 }); go.run(instance).then(exitWASM, exitWASM); if (!go.exited) { const backends = JSON.parse(globalThis.ofdgoRenderBackends()); diff --git a/ofdgo_convert.go b/ofdgo_convert.go index a02157b..0bfee60 100644 --- a/ofdgo_convert.go +++ b/ofdgo_convert.go @@ -34,7 +34,7 @@ type OutputFormat struct { Raster bool `json:"raster,omitempty"` } -// ConvertProgress 回报open、pages、convert、fonts、prepare、write、write.*及export阶段,Total为0表示总量未知 +// ConvertProgress 回报open、pages、convert、fonts、fonts.*、prepare、write、write.*及export阶段,Total为0表示总量未知 type ConvertProgress struct { Stage string `json:"stage"` Completed int `json:"completed"` @@ -279,7 +279,12 @@ func Convert(ctx context.Context, source io.ReaderAt, size int64, output io.Writ editor.SetRenderBackends(configuration.Backends()) editor.SetFontDirs(configuration.fontDirs...) editor.SetFontFS(configuration.fontFS...) - report.Fonts, err = editor.ProcessFonts(ctx, options.Fonts) + report.Fonts, err = editor.ProcessFontsWithOptions(ctx, FontProcessOptions{Mode: options.Fonts, OnProgress: func(stage string, completed, total int) error { + if stage == "fonts" { + return progress(stage, completed, total) + } + return progress("fonts."+stage, completed, total) + }}) if err != nil { return report, err } diff --git a/ofdgo_editor_font_binding.go b/ofdgo_editor_font_binding.go index 49a7a2b..be16b8b 100644 --- a/ofdgo_editor_font_binding.go +++ b/ofdgo_editor_font_binding.go @@ -48,6 +48,14 @@ type FontChange struct { Reason string `json:"reason,omitempty"` } +// FontProcessOptions 设置全包字体处理方式及进度回调 +// OnProgress按prepare、scan、fonts、commit阶段同步回报进度,total为0表示总量未知 +// 回调返回错误时不提交修改,不可在回调中修改编辑器 +type FontProcessOptions struct { + Mode FontMode + OnProgress func(stage string, completed, total int) error +} + // EmbedFont 嵌入当前文档指定字体,保留资源标识及全部使用位置,成功后可撤销 // file为空时从编辑器字体来源精确匹配;显式文件表示调用方确认替换,仍校验字符和字形 // 入参: ctx 取消上下文, id 字体标识, file 字体文件, index 集合索引 @@ -91,6 +99,14 @@ func (e *Editor) changeFont(ctx context.Context, id string, mode FontMode, file // 入参: ctx 取消上下文, mode 保存方式 // 返回: []FontChange 字体处理结果, error 取消或结构错误 func (e *Editor) ProcessFonts(ctx context.Context, mode FontMode) ([]FontChange, error) { + return e.ProcessFontsWithOptions(ctx, FontProcessOptions{Mode: mode}) +} + +// ProcessFontsWithOptions 处理包内字体并回报已处理数量,失败或取消时不提交修改 +// 入参: ctx 取消上下文, options 字体处理选项 +// 返回: []FontChange 字体处理结果, error 取消、回调或结构错误 +func (e *Editor) ProcessFontsWithOptions(ctx context.Context, options FontProcessOptions) ([]FontChange, error) { + mode := options.Mode if mode > FontExternal { return nil, fmt.Errorf("invalid font mode") } @@ -100,6 +116,18 @@ func (e *Editor) ProcessFonts(ctx context.Context, mode FontMode) ([]FontChange, if mode == FontKeep { return nil, nil } + progress := func(stage string, completed, total int) error { + if err := ctx.Err(); err != nil { + return err + } + if err := editorProgress(options.OnProgress).report(stage, completed, total); err != nil { + return err + } + return ctx.Err() + } + if err := progress("prepare", 0, 0); err != nil { + return nil, err + } reader, err := e.Reader() if err != nil { return nil, err @@ -107,18 +135,28 @@ func (e *Editor) ProcessFonts(ctx context.Context, mode FontMode) ([]FontChange, var report []FontChange changed := false for document := range reader.DocumentCount() { + if err := progress("scan", document, reader.DocumentCount()); err != nil { + return nil, err + } scan, err := scanEditorFonts(ctx, reader, document) if err != nil { return nil, err } ids := slices.Sorted(maps.Keys(scan.fonts)) + ids = slices.DeleteFunc(ids, func(id string) bool { + return (mode == FontEmbed) == (scan.fonts[id].location != "") + }) + total := 0 + if document == reader.DocumentCount()-1 { + total = len(report) + len(ids) + } for _, id := range ids { if err := ctx.Err(); err != nil { return nil, err } declaration := scan.fonts[id] - if (mode == FontEmbed) == (declaration.location != "") { - continue + if err := progress("fonts", len(report), total); err != nil { + return nil, err } item := FontChange{Document: document, ID: id, Name: declaration.font.FontName} item.Changed, err = e.changeFontSnapshot(ctx, scan, id, mode, FontFile{}, 0) @@ -129,6 +167,9 @@ func (e *Editor) ProcessFonts(ctx context.Context, mode FontMode) ([]FontChange, item.Reason = err.Error() } report = append(report, item) + if err := progress("fonts", len(report), total); err != nil { + return nil, err + } if item.Changed { changed = true scan, err = scanEditorFonts(ctx, reader, document) @@ -138,7 +179,13 @@ func (e *Editor) ProcessFonts(ctx context.Context, mode FontMode) ([]FontChange, } } } + if err := progress("fonts", len(report), len(report)); err != nil { + return nil, err + } if changed { + if err := progress("commit", 0, 1); err != nil { + return nil, err + } err = e.commitFontSnapshot(ctx, reader) } return report, err diff --git a/ofdgo_font_cff.go b/ofdgo_font_cff.go index 91efd7d..ee03b16 100644 --- a/ofdgo_font_cff.go +++ b/ofdgo_font_cff.go @@ -282,7 +282,9 @@ func sanitizeCFF(data []byte) ([]byte, error) { if err != nil { return nil, err } - if kind == 1 || fdMatrix != [6]float64{1, 0, 0, 1, 0, 0} { + _, topHasMatrix := topDict[1207] + _, fdHasMatrix := fontDict[1207] + if kind == 1 || fdMatrix != [6]float64{1, 0, 0, 1, 0, 0} || !topHasMatrix && fdHasMatrix { return sanitizeMultiFDCFF(data, hdrSize, nameIndexData, topDict, stringIndexData, globalSubrIndexData, fdArrOff, fdCount) } charStringsOff, err := cffDictOffset(data, topDict, 17) @@ -407,6 +409,7 @@ func sanitizeMultiFDCFF(data []byte, hdrSize int, nameIndexData []byte, topDict return nil, err } units := cffUnitsPerEm(topMatrix) + _, topHasMatrix := topDict[1207] for i := range topMatrix { topMatrix[i] *= float64(units) } @@ -420,7 +423,11 @@ func sanitizeMultiFDCFF(data []byte, hdrSize int, nameIndexData []byte, topDict if matrixErr != nil { return nil, fmt.Errorf("CFF font dictionary %d: %w", fd, matrixErr) } - matrices[fd], err = readCFFMatrix(cffDict{1207: multiplyAffine(topMatrix[:], fdMatrix[:])}, [6]float64{}) + baseMatrix := topMatrix + if _, fdHasMatrix := fontDict[1207]; !topHasMatrix && fdHasMatrix { + baseMatrix = [6]float64{float64(units), 0, 0, float64(units), 0, 0} + } + matrices[fd], err = readCFFMatrix(cffDict{1207: multiplyAffine(baseMatrix[:], fdMatrix[:])}, [6]float64{}) if err != nil { return nil, fmt.Errorf("CFF font dictionary %d: %w", fd, err) }