Fix PDF export font embedding and improve sync reliability

- Replace data URL loading with temporary HTML file to avoid URL length limits
- Embed font files as data URLs in print document CSS for offline rendering
- Add font asset registry for Merriweather, Crimson Pro, Roboto Serif, and Average
- Implement font file caching and blob-to-data-URL conversion
- Clean up temporary HTML file after PDF generation
- Fix sync to refresh notes after favorite status sync completes
This commit is contained in:
drelich
2026-04-06 09:46:26 +02:00
parent 6e970f37ea
commit e21e443a59
4 changed files with 127 additions and 4 deletions

View File

@@ -11,6 +11,7 @@ import {
} from '../services/desktop';
import {
getNoteTitleFromContent,
loadPrintFontFaceCss,
PrintExportPayload,
sanitizeFileName,
} from '../printExport';
@@ -268,6 +269,7 @@ export function NoteEditor({ note, onUpdateNote, onToggleFavorite, onUnsavedChan
html: noteHtml,
previewFont,
previewFontSize,
previewFontFaceCss: await loadPrintFontFaceCss(previewFont),
};
await exportPdfDocument({

View File

@@ -4,6 +4,7 @@ export interface PrintExportPayload {
html: string;
previewFont: string;
previewFontSize: number;
previewFontFaceCss?: string;
}
export const PRINT_EXPORT_QUERY_PARAM = 'printJob';
@@ -43,6 +44,117 @@ const escapeHtml = (value: string) =>
const escapeFontFamily = (value: string) =>
value.replace(/["\\]/g, '\\$&');
interface PrintFontAsset {
fileName: string;
fontStyle: 'normal' | 'italic';
fontWeight: string;
}
const PRINT_FONT_ASSETS: Record<string, PrintFontAsset[]> = {
Merriweather: [
{
fileName: 'Merriweather-VariableFont_opsz,wdth,wght.ttf',
fontStyle: 'normal',
fontWeight: '300 900',
},
{
fileName: 'Merriweather-Italic-VariableFont_opsz,wdth,wght.ttf',
fontStyle: 'italic',
fontWeight: '300 900',
},
],
'Crimson Pro': [
{
fileName: 'CrimsonPro-VariableFont_wght.ttf',
fontStyle: 'normal',
fontWeight: '200 900',
},
{
fileName: 'CrimsonPro-Italic-VariableFont_wght.ttf',
fontStyle: 'italic',
fontWeight: '200 900',
},
],
'Roboto Serif': [
{
fileName: 'RobotoSerif-VariableFont_GRAD,opsz,wdth,wght.ttf',
fontStyle: 'normal',
fontWeight: '100 900',
},
{
fileName: 'RobotoSerif-Italic-VariableFont_GRAD,opsz,wdth,wght.ttf',
fontStyle: 'italic',
fontWeight: '100 900',
},
],
Average: [
{
fileName: 'Average-Regular.ttf',
fontStyle: 'normal',
fontWeight: '400',
},
],
};
const fontDataUrlCache = new Map<string, Promise<string>>();
const blobToDataUrl = (blob: Blob) =>
new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result as string);
reader.onerror = () => reject(reader.error ?? new Error('Failed to read font file.'));
reader.readAsDataURL(blob);
});
const getBundledFontUrl = (fileName: string) =>
new URL(`./fonts/${fileName}`, window.location.href).toString();
const loadBundledFontDataUrl = async (fileName: string) => {
const cached = fontDataUrlCache.get(fileName);
if (cached) {
return cached;
}
const pending = (async () => {
const response = await fetch(getBundledFontUrl(fileName));
if (!response.ok) {
throw new Error(`Failed to load bundled font ${fileName}: ${response.status}`);
}
return blobToDataUrl(await response.blob());
})();
fontDataUrlCache.set(fileName, pending);
return pending;
};
export const loadPrintFontFaceCss = async (fontFamily: string) => {
const fontAssets = PRINT_FONT_ASSETS[fontFamily];
if (!fontAssets) {
return '';
}
const rules = await Promise.all(
fontAssets.map(async ({ fileName, fontStyle, fontWeight }) => {
try {
const dataUrl = await loadBundledFontDataUrl(fileName);
return `@font-face {
font-family: "${escapeFontFamily(fontFamily)}";
font-style: ${fontStyle};
font-weight: ${fontWeight};
font-display: swap;
src: url("${dataUrl}") format("truetype");
}`;
} catch (error) {
console.error(`Failed to embed preview font "${fontFamily}" from ${fileName}:`, error);
return '';
}
})
);
return rules.filter(Boolean).join('\n');
};
export const buildPrintDocument = (payload: PrintExportPayload) => {
const fontFamily = `"${escapeFontFamily(payload.previewFont)}", Georgia, serif`;
@@ -54,6 +166,8 @@ export const buildPrintDocument = (payload: PrintExportPayload) => {
<meta http-equiv="Content-Security-Policy" content="${PRINT_DOCUMENT_CSP}" />
<title>${escapeHtml(payload.fileName)}</title>
<style>
${payload.previewFontFaceCss ?? ''}
:root {
color-scheme: light;
}

View File

@@ -68,7 +68,9 @@ export class SyncManager {
try {
this.notifyStatus('syncing', 0);
const notes = await this.fetchAndCacheNotes();
await this.fetchAndCacheNotes();
await this.syncFavoriteStatus();
const notes = await localDB.getAllNotes();
this.notifyStatus('idle', 0);
return notes;
} catch (error) {