// Run with Electron: electron --ozone-platform=x11 export.cjs const { app, BrowserWindow, session } = require('electron'); const fs = require('node:fs/promises'); const path = require('node:path'); const assert = require('node:assert/strict'); app.disableHardwareAcceleration(); const timeout = setTimeout(() => { console.error('PDF export timed out after 60 seconds'); app.exit(1); }, 60000); app.whenReady().then(async () => { console.log('Chromium ready'); const failures = []; session.defaultSession.webRequest.onBeforeRequest((details, callback) => { const remote = /^https?:/.test(details.url); if (remote) failures.push(`External request: ${details.url}`); callback({ cancel: remote }); }); session.defaultSession.webRequest.onErrorOccurred(details => failures.push(`${details.error}: ${details.url}`)); const win = new BrowserWindow({ show: false, width: 1000, height: 1200, webPreferences: { nodeIntegration: false, contextIsolation: true, sandbox: true, backgroundThrottling: false } }); await win.loadFile(path.join(__dirname, 'index.html')); console.log('HTML loaded'); const result = await win.webContents.executeJavaScript(`(async () => { await document.fonts.ready; await Promise.all([...document.images].map(image => image.decode())); const pages = [...document.querySelectorAll('.page')]; const overflow = pages.flatMap((page, index) => { const footer = page.querySelector('.folio').getBoundingClientRect(); return [...page.children].filter(child => !child.classList.contains('folio')) .filter(child => child.getBoundingClientRect().bottom > footer.top - 8) .map(child => ({ page: index + 1, element: child.tagName, text: child.textContent.slice(0, 50) })); }); return { blocks: pages.map(page => page.id), pages: pages.length, overflow, familiesLoaded: ['Podkova', 'Oswald', 'Noto'].every(family => [...document.fonts].some(face => face.family === family && face.status === 'loaded')), fonts: ['14px Noto', '700 14px Noto', '600 48px Oswald', '600 32px Podkova'].every(font => document.fonts.check(font)), images: [...document.images].length, unloaded: [...document.images].filter(image => !image.naturalWidth).length }; })()`); assert.equal(result.pages, 6, 'Expected cover and five content pages'); assert.deepEqual(result.blocks, ['cover', 'logo', 'logo-mono', 'fonts', 'colors', 'voice']); assert.deepEqual(result.overflow, [], 'Content reaches the page footer'); assert.equal(result.fonts, true, 'Local fonts must load'); assert.equal(result.familiesLoaded, true, 'All three bundled font families must load'); assert.equal(result.unloaded, 0, 'All images must load'); assert.deepEqual(failures, [], 'All resources must load locally'); console.log('Layout and local resources checked'); const pdf = await win.webContents.printToPDF({ printBackground: true, preferCSSPageSize: true, pageSize: 'A4', margins: { top: 0, bottom: 0, left: 0, right: 0 }, generateTaggedPDF: true }); await fs.writeFile(path.join(__dirname, 'brandbook.pdf'), pdf); console.log(JSON.stringify({ ...result, bytes: pdf.length, output: 'brandbook.pdf' }, null, 2)); win.destroy(); clearTimeout(timeout); app.quit(); }).catch(error => { console.error(error); app.exit(1); });