feat: add fallback font loading and handling for various languages

This commit is contained in:
alam00000 committed 2026-09-17 13:56:16 +05:30
1 parent 9ae5c93886
commit 5a7927572f
28 files changed
+375 -4

No files matched your search

+2
View File
@@ -10260,4 +10260,6 @@ export {
getZoom,
getDocDescription,
setOnSaved,
endEdit,
saveFile,
};
+13
View File
@@ -6,6 +6,7 @@ const devBust = '';
import { applyType3LeftoverSurgery } from './type3surgery.js';
import { probePdfBytes } from './shadingsurgery.js';
import { needsFontEmbedding } from '../utils/freetext-script';
const RUN_SIZE = 60;
const FMT_SIZE = 36;
@@ -338,6 +339,7 @@ const PAGE_CACHE_MAX = 6;
export class PdfEngine {
static localFonts = new Map();
static fallbackFonts = new Map();
constructor(module) {
this.M = module;
@@ -396,6 +398,17 @@ export class PdfEngine {
if (bytes) break;
}
if (!bytes) bytes = normLookup(fam, styleKey, want);
if (
!bytes &&
want.some((cp) => needsFontEmbedding(String.fromCodePoint(cp)))
) {
for (const b of PdfEngine.fallbackFonts.values()) {
if (b && b.length && sfntCovers(b, want)) {
bytes = b;
break;
}
}
}
if (!bytes || !bytes.length) return 0;
const buf = M._ec_buffer_alloc(bytes.length);
if (!buf) return 0;
+161
View File
@@ -3,6 +3,11 @@ import { createIcons, icons } from 'lucide';
import { showAlert, showLoader, hideLoader } from '../ui.js';
import { t } from '../i18n/i18n';
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
import {
fallbackFontLanguages,
hasFallbackFonts,
loadFallbackFonts,
} from '../utils/font-loader.js';
import { setupFormatDock, setupFindSheet } from './edit-pdf-text-dock';
interface DocDescription {
@@ -276,6 +281,8 @@ interface EditorAppModule {
getZoom: () => number;
getDocDescription: () => DocDescription;
setOnSaved: (fn: ((kb: number, fileName: string) => void) | null) => void;
endEdit: (commit: boolean) => void;
saveFile: () => Promise<void>;
}
function relocateAddText() {
@@ -422,6 +429,155 @@ function fitMobileWidth() {
mod.setZoom(target);
}
function setupFallbackFonts(
app: EditorAppModule,
fonts: Map<string, Uint8Array>
) {
const pending = new Set<Promise<boolean>>();
let failed = false;
let warned = false;
let loaderShown = false;
let swallowGesture = false;
let resume: (() => void) | null = null;
const saveIfEnabled = () => {
const save = document.getElementById('save');
if (save instanceof HTMLButtonElement && !save.disabled) {
void app.saveFile();
}
};
const commitEdit = () => app.endEdit(true);
const track = (text: string | null | undefined) => {
const langs = fallbackFontLanguages(text ?? '');
if (langs.size === 0 || hasFallbackFonts(fonts, langs)) return;
const load = loadFallbackFonts(fonts, langs);
if (warned) return;
pending.add(load);
void load.then((ok) => {
pending.delete(load);
if (!ok) failed = true;
if (pending.size > 0) return;
const next = resume;
const actionHeld = loaderShown;
resume = null;
if (loaderShown) {
loaderShown = false;
hideLoader();
}
if (failed) {
failed = false;
if (actionHeld || !warned) {
warned = true;
showAlert(
t('warning.title'),
t('tools:editPdfText.fontsUnavailable'),
'warning'
);
}
return;
}
next?.();
});
};
const hold = (e: Event, next: (() => void) | null) => {
e.preventDefault();
e.stopImmediatePropagation();
if (next) resume = next;
if (!loaderShown) {
loaderShown = true;
showLoader(t('tools:editPdfText.loadingFonts'));
}
};
void loadFallbackFonts(fonts, ['eng']);
document.addEventListener(
'beforeinput',
(e) => track(e.data ?? e.dataTransfer?.getData('text/plain')),
true
);
document.addEventListener(
'paste',
(e) => {
const data = e.clipboardData;
track(data?.getData('text/plain') || data?.getData('text/html'));
const target = e.target;
const intoField =
target instanceof HTMLElement &&
(target.isContentEditable ||
target instanceof HTMLInputElement ||
target instanceof HTMLTextAreaElement);
if (pending.size > 0 && !intoField) hold(e, null);
},
true
);
window.addEventListener(
'pointerdown',
(e) => {
const target = e.target instanceof Element ? e.target : null;
swallowGesture =
pending.size > 0 && !target?.closest('.editor, .edit-chrome');
if (!swallowGesture) return;
hold(
e,
target?.closest('#save')
? saveIfEnabled
: target?.closest('#stage')
? commitEdit
: null
);
},
true
);
for (const type of [
'mousedown',
'mouseup',
'pointerup',
'click',
'dblclick',
'contextmenu',
'touchstart',
'touchend',
]) {
window.addEventListener(
type,
(e) => {
if (!swallowGesture) return;
e.preventDefault();
e.stopImmediatePropagation();
if (type === 'click') swallowGesture = false;
},
{ capture: true, passive: false }
);
}
window.addEventListener(
'keydown',
(e) => {
swallowGesture = false;
if (pending.size === 0) return;
const key = e.key.toLowerCase();
const meta = e.metaKey || e.ctrlKey;
if (meta && key === 's') hold(e, saveIfEnabled);
else if (e.key === 'Escape') hold(e, commitEdit);
else if (meta && (key === 'z' || key === 'y' || key === 'f')) {
hold(e, null);
} else if (
(e.key === 'Enter' || e.key === ' ') &&
e.target instanceof HTMLButtonElement
) {
hold(e, e.target.id === 'save' ? saveIfEnabled : null);
}
},
true
);
}
async function launchEditor(file: File) {
if (launching) return;
launching = true;
@@ -436,6 +592,11 @@ async function launchEditor(file: File) {
document.getElementById('text-editor-app')?.removeAttribute('hidden');
if (!appModule) {
appModule = (await import('../editcore/app.js')) as EditorAppModule;
const { PdfEngine } =
(await import('../editcore/core.js')) as unknown as {
PdfEngine: { fallbackFonts: Map<string, Uint8Array> };
};
setupFallbackFonts(appModule, PdfEngine.fallbackFonts);
appModule.setOnSaved((kb) => {
showAlert(
t('common.success'),
+51
View File
@@ -3,6 +3,7 @@ import {
getFontUrlForFamily,
languageToFontFamily,
} from '../config/font-mappings.js';
import { needsFontEmbedding } from './freetext-script.js';
const fontCache: Map<string, ArrayBuffer> = new Map();
@@ -134,6 +135,56 @@ export async function getFontForLanguage(lang: string): Promise<ArrayBuffer> {
}
}
const fallbackFontLoads = new Map<string, Promise<boolean>>();
const fallbackFontFamily = (lang: string): string =>
languageToFontFamily[lang] || 'Noto Sans';
export function fallbackFontLanguages(text: string): Set<string> {
const langs = new Set<string>();
for (const ch of text) {
if (needsFontEmbedding(ch)) langs.add(getLanguageForChar(ch));
}
return langs;
}
export function hasFallbackFonts(
loaded: Map<string, Uint8Array>,
langs: Iterable<string>
): boolean {
return [...langs].every((lang) => loaded.has(fallbackFontFamily(lang)));
}
export async function loadFallbackFonts(
into: Map<string, Uint8Array>,
langs: Iterable<string>
): Promise<boolean> {
const results = await Promise.all(
[...langs].map((lang) => {
const family = fallbackFontFamily(lang);
let load = fallbackFontLoads.get(family);
if (!load) {
load = getFontForLanguage(lang)
.then((buffer) => {
if (!fontCache.has(family)) {
throw new Error('Only a substitute font was available');
}
into.set(family, new Uint8Array(buffer));
return true;
})
.catch((error: unknown) => {
fallbackFontLoads.delete(family);
console.warn(`Fallback font unavailable for ${family}`, error);
return false;
});
fallbackFontLoads.set(family, load);
}
return load;
})
);
return results.every(Boolean);
}
export function detectScripts(text: string): string[] {
const scripts = new Set<string>();
+7 -1
View File
@@ -1,5 +1,6 @@
import type { FreeTextSystemFontAnnotation } from '@/types';
import { needsFontEmbedding } from './freetext-script.js';
import { fallbackFontLanguages, loadFallbackFonts } from './font-loader.js';
export const FPDF_ANNOT_FREETEXT = 3;
@@ -153,7 +154,7 @@ function buildRuns(
return [
{
text: contents,
family: 'Helvetica',
family: needsFontEmbedding(contents) ? 'Noto Sans' : 'Helvetica',
size,
rgba: hexToRgba(style?.fontColor ?? '#000000'),
bold: false,
@@ -196,6 +197,7 @@ export async function flattenFreeTextToPageText(
PdfEngine: {
create: () => Promise<EngineLike>;
localFonts: Map<string, Uint8Array>;
fallbackFonts: Map<string, Uint8Array>;
};
scriptFallbackFamilies: (cps: number[]) => string[];
};
@@ -245,6 +247,10 @@ export async function flattenFreeTextToPageText(
return { bytes, flattened: 0 };
}
await primeScriptFonts(mod, found);
await loadFallbackFonts(
mod.PdfEngine.fallbackFonts,
fallbackFontLanguages(found.join('\n'))
);
for (const pageIndex of [...pages].sort((x, y) => x - y)) {
if (pageIndex < 0 || pageIndex >= eng.pageCount) continue;