mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +08:00
1 parent
5f790e5408
commit
c2d9a349c5
5 files changed
+105
-21
No files matched your search
+37
-12
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user