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:
@@ -121,6 +121,11 @@ ipcMain.handle('desktop:export-pdf', async (event, payload) => {
|
||||
return { canceled: true };
|
||||
}
|
||||
|
||||
const tempHtmlPath = path.join(
|
||||
app.getPath('temp'),
|
||||
`nextcloud-notes-export-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.html`
|
||||
);
|
||||
|
||||
const pdfWindow = new BrowserWindow({
|
||||
width: 960,
|
||||
height: 1100,
|
||||
@@ -135,9 +140,8 @@ ipcMain.handle('desktop:export-pdf', async (event, payload) => {
|
||||
});
|
||||
|
||||
try {
|
||||
await pdfWindow.loadURL(
|
||||
`data:text/html;charset=utf-8,${encodeURIComponent(payload.documentHtml)}`
|
||||
);
|
||||
await fs.writeFile(tempHtmlPath, payload.documentHtml, 'utf8');
|
||||
await pdfWindow.loadFile(tempHtmlPath);
|
||||
await pdfWindow.webContents.executeJavaScript(waitForPrintDocument(), true);
|
||||
|
||||
const pdfData = await pdfWindow.webContents.printToPDF({
|
||||
@@ -152,6 +156,7 @@ ipcMain.handle('desktop:export-pdf', async (event, payload) => {
|
||||
filePath: saveResult.filePath,
|
||||
};
|
||||
} finally {
|
||||
await fs.unlink(tempHtmlPath).catch(() => undefined);
|
||||
if (!pdfWindow.isDestroyed()) {
|
||||
pdfWindow.destroy();
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user