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 };
|
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({
|
const pdfWindow = new BrowserWindow({
|
||||||
width: 960,
|
width: 960,
|
||||||
height: 1100,
|
height: 1100,
|
||||||
@@ -135,9 +140,8 @@ ipcMain.handle('desktop:export-pdf', async (event, payload) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await pdfWindow.loadURL(
|
await fs.writeFile(tempHtmlPath, payload.documentHtml, 'utf8');
|
||||||
`data:text/html;charset=utf-8,${encodeURIComponent(payload.documentHtml)}`
|
await pdfWindow.loadFile(tempHtmlPath);
|
||||||
);
|
|
||||||
await pdfWindow.webContents.executeJavaScript(waitForPrintDocument(), true);
|
await pdfWindow.webContents.executeJavaScript(waitForPrintDocument(), true);
|
||||||
|
|
||||||
const pdfData = await pdfWindow.webContents.printToPDF({
|
const pdfData = await pdfWindow.webContents.printToPDF({
|
||||||
@@ -152,6 +156,7 @@ ipcMain.handle('desktop:export-pdf', async (event, payload) => {
|
|||||||
filePath: saveResult.filePath,
|
filePath: saveResult.filePath,
|
||||||
};
|
};
|
||||||
} finally {
|
} finally {
|
||||||
|
await fs.unlink(tempHtmlPath).catch(() => undefined);
|
||||||
if (!pdfWindow.isDestroyed()) {
|
if (!pdfWindow.isDestroyed()) {
|
||||||
pdfWindow.destroy();
|
pdfWindow.destroy();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from '../services/desktop';
|
} from '../services/desktop';
|
||||||
import {
|
import {
|
||||||
getNoteTitleFromContent,
|
getNoteTitleFromContent,
|
||||||
|
loadPrintFontFaceCss,
|
||||||
PrintExportPayload,
|
PrintExportPayload,
|
||||||
sanitizeFileName,
|
sanitizeFileName,
|
||||||
} from '../printExport';
|
} from '../printExport';
|
||||||
@@ -268,6 +269,7 @@ export function NoteEditor({ note, onUpdateNote, onToggleFavorite, onUnsavedChan
|
|||||||
html: noteHtml,
|
html: noteHtml,
|
||||||
previewFont,
|
previewFont,
|
||||||
previewFontSize,
|
previewFontSize,
|
||||||
|
previewFontFaceCss: await loadPrintFontFaceCss(previewFont),
|
||||||
};
|
};
|
||||||
|
|
||||||
await exportPdfDocument({
|
await exportPdfDocument({
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export interface PrintExportPayload {
|
|||||||
html: string;
|
html: string;
|
||||||
previewFont: string;
|
previewFont: string;
|
||||||
previewFontSize: number;
|
previewFontSize: number;
|
||||||
|
previewFontFaceCss?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PRINT_EXPORT_QUERY_PARAM = 'printJob';
|
export const PRINT_EXPORT_QUERY_PARAM = 'printJob';
|
||||||
@@ -43,6 +44,117 @@ const escapeHtml = (value: string) =>
|
|||||||
const escapeFontFamily = (value: string) =>
|
const escapeFontFamily = (value: string) =>
|
||||||
value.replace(/["\\]/g, '\\$&');
|
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) => {
|
export const buildPrintDocument = (payload: PrintExportPayload) => {
|
||||||
const fontFamily = `"${escapeFontFamily(payload.previewFont)}", Georgia, serif`;
|
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}" />
|
<meta http-equiv="Content-Security-Policy" content="${PRINT_DOCUMENT_CSP}" />
|
||||||
<title>${escapeHtml(payload.fileName)}</title>
|
<title>${escapeHtml(payload.fileName)}</title>
|
||||||
<style>
|
<style>
|
||||||
|
${payload.previewFontFaceCss ?? ''}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,9 @@ export class SyncManager {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
this.notifyStatus('syncing', 0);
|
this.notifyStatus('syncing', 0);
|
||||||
const notes = await this.fetchAndCacheNotes();
|
await this.fetchAndCacheNotes();
|
||||||
|
await this.syncFavoriteStatus();
|
||||||
|
const notes = await localDB.getAllNotes();
|
||||||
this.notifyStatus('idle', 0);
|
this.notifyStatus('idle', 0);
|
||||||
return notes;
|
return notes;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user