feat(字体处理): 修复字体转换,完善批量进度
build.yaml / build (push) Successful in 47s

This commit is contained in:
xiaoqidun committed 2026-10-10 11:22:13 +08:00
1 parent 5f790e5408
commit c2d9a349c5
5 files changed
+105 -21

No files matched your search

+37 -12
View File
@@ -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;
+3 -3
View File
@@ -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());
+7 -2
View File
@@ -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
}
+49 -2
View File
@@ -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
+9 -2
View File
@@ -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)
}