
Tisk schůzky do PDF bez PDF knihovny
GeekBye exportuje schůzku jako PDF a nikde v kódu není žádná PDF knihovna. Renderuje HTML v neviditelném okně prohlížeče a vytiskne ho. Ta volba je celý příběh: udělala funkci snadnou na postavení a dala jí každé selhání, které má skutečný prohlížeč — bílý záblesk, limit délky URL a zalomení stránky, které rozřízlo screenshoty napůl. Oprava toho nejošklivějšího bugu byla jeden řádek CSS.
Ve tlačítku GeekBye „exportovat tuto schůzku jako PDF" se skrývá uspokojivý trik: není za ním žádná PDF knihovna. Žádný jsPDF, žádný pdfmake, žádný pdfkit, žádný headless puppeteer. Celá funkce je skrytý prohlížeč. Aplikace postaví HTML stránku popisující schůzku — záznamy přepisu, screenshoty, časová razítka — načte ji do neviditelného okna Electronu a požádá to okno, aby se samo vytisklo do PDF. webContents.printToPDF udělá zbytek. Ta jediná architektonická volba je celý příběh tohoto vydání, protože udělala funkci levnou na postavení a předala jí každý bug, který má skutečný prohlížeč.
„Předtím" je totéž vydání
Obvykle tyhle příspěvky otevírají slovy „takhle to fungovalo předtím". Tentokrát žádné předtím není. Changelog pro v1.8.9 se čte jako údržba — "fixed page breaks splitting screenshots and filtered empty entries" — jako by PDF export byl existující funkcí, která dostává záplatu. Nebyl. První commit, který ji zavádí, feat: add PDF export via Electron printToPDF, přistává tři dny do téhož cyklu vydání a každý „fix" v changelogu je iterace ze stejného týdne na kódu, který byl staré hodiny. Funkce se zrodila, rozbila a zpevnila, všechno v rámci jednoho tagu. Stojí za to říct to nahlas, protože to je poctivý tvar spousty dodávání: vyleštěná odrážka v poznámkách k vydání a nepořádné zrození té věci jsou často totéž vydání.
Proč vůbec skrytý prohlížeč
Mechanismus stojí za pochopení před bugy, protože všechny bugy z něj pocházejí. Všechno žije v jednom souboru hlavního procesu, electron/ipcHandlers/pdfExportHandler.ts. Když renderer spustí 'pdf:export-report' IPC, handler:
- Zavolá
buildReportHtml(), aby sestavil kompletní HTML řetězec z timeline schůzky — jeden blok.itemna záznam přepisu nebo screenshot. - Zapíše ten HTML do dočasného souboru v
os.tmpdir(). - Načte soubor do skrytého okna —
new BrowserWindow({ show: false, opacity: 0, focusable: false, skipTaskbar: true }). - Počká na načtení obrázků, pak zavolá
win.webContents.printToPDF({ printBackground: true, margins: { … } }). - Uloží výsledek do
app.getPath('downloads')jakoMeeting-Report-<timestamp>.pdf.
Přitažlivost je zřejmá. PDF knihovna tě nutí pozicovat všechno ručně — nakresli text na (x, y), změř ho, posuň kurzor, spravuj zalomení stránek sám. Prohlížeč tohle všechno už dělá: píšeš <div>y a CSS a layout engine Chromia stránkuje za tebe. Neučíš se drawing API; píšeš webovou stránku. Pro dokument, který je většinou stylovaný text a vložené obrázky, je to obrovská zkratka.
Háček je v tom, že jsi zdědil taky prohlížeč, se všemi jeho hraničními případy. Tři z nich se objevily během prvních čtyř dnů.
Bílý záblesk. Skryté okno na macOS není spolehlivě skryté. První pokusy ho umístily mimo obrazovku (x: screenWidth + 1000), a přesto na obrazovce na jeden snímek bliklo bílým obdélníkem před tiskem. Oprava, po třech pokusech, byla přestat se spoléhat na pozici nebo na show: false a nastavit opacity: 0 — plně průhledné okno nebliká, ani když ho kompozitor na chvíli ukáže. Commit to říká natvrdo: "show:false with off-screen positioning still flashes on macOS."
Limit délky URL. První verze vkládala celý HTML — se screenshoty a vším — jako data: URL. To funguje, dokud schůzka nemá v sobě pár base64-kódovaných screenshotů, v kterémžto okamžiku URL propluje za limit data-URL Chromia o ~2 MB a načtení rovnou selže s ERR_INVALID_URL. Proto krok 2 výše zapisuje dočasný soubor a dělá loadFile() místo načítání data URL: cesta k souboru nemá limit délky. (Ty ~2 MB jsou jediné číslo v celém tomhle příběhu a je to inline odhad v komentáři v kódu, ne něco, co někdo benchmarkoval — ber to jako „dost velké na to, abys to trefil", ne jako přesný práh.)
Neescapovaný text. Text přepisu je libovolný uživatelský obsah a padal rovnou do HTML řetězce. Jakýkoli přepis, který náhodou obsahoval něco jako <div> nebo <script>, by rozbil stránku nebo hůř. Tak handler vyrostl o průchod escapeHtml() přes každý řetězec, který interpoluje. Ve chvíli, kdy je tvůj PDF webová stránka, HTML injekce je taky tvůj problém.
Prázdné boxy, které nebyly prázdné
Teď hlavní bug, protože je nejlepší. Uživatelé hlásili exportované PDF s prázdnými boxy — záznamy, které ukazovaly časové razítko a badge, ale žádný screenshot, jen prázdné místo tam, kde měl být obrázek. Zřejmé čtení je „export zahazuje obrazová data" a zřejmá oprava je „vyfiltruj prázdné záznamy". Tým ten filtr opravdu přidal — víc o něm za vteřinu — ale nebyla to skutečná příčina.
Skutečná příčina bylo stránkování. Každý záznam je .item kontejner držící časové razítko, badge a obrázek. Chromium, tisknoucí do PDF, s radostí zalomil stránku uprostřed itemu — renderoval časové razítko a badge dole na jedné stránce a obrázek přesunul na další. Co vypadalo jako prázdný box, byla horní polovina itemu, jehož obrázek přetekl na následující stránku. Data byla všechna tam; rozvržení je zgilotinovalo. Commit je upřímný ohledně chybné diagnózy: "The empty boxes in exported PDFs were caused by Chromium's printToPDF splitting .item containers across pages — the time/badge rendered on one page while the image overflowed to the next."
Oprava je jeden řádek CSS:
.item {
break-inside: avoid;
}
break-inside: avoid říká layout enginu, aby držel item vcelku — pokud se nevejde do zbývajícího místa na stránce, přesuň celý item na další stránku, místo abys ho dělil. Tohle je odměna architektury se skrytým prohlížečem řečená v miniatuře: nejošklivější, nejhlášenější bug ve funkci nebyl opraven aritmetikou výšky stránky a ručními voláními addPage(), ale jedinou CSS vlastností, kterou prohlížeč už uměl ctít. Nenapsal jsi stránkovač; požádal jsi ten, který jsi zdědil, aby se choval slušně.
Dvě vrstvy filtrování skutečně prázdných
Byly tam i opravdu prázdné záznamy — jen ne ty, které způsobovaly boxy. Prázdné duplikáty z rychlého dvojitého Cmd+Enter, screenshoty bez náhledu, záznamy přepisu, které byly jen whitespace. Tyhle se vyfiltrovaly, a zajímavé je, že se vyfiltrovaly dvakrát, ve dvou vrstvách:
- V handleru
buildReportHtml()hlídá každý item —if (!item.content?.trim()) return ''pro text a pro obrázkyconst safeSrc = item.preview?.startsWith('data:image/') ? item.preview : ''; if (!safeSrc) return ''— pak.filter(Boolean)zahodí prázdné. Ta poslední kontrola dělá dvojí službu: je to brána validity a malá bezpečnostní kontrola, že na stránku kdy dojdou jen skutečná obrazová data. - U zdroje timeline, v rendereru, se stejná prázdnota filtruje, ještě než je item vůbec přidán —
if (!entry.text?.trim()) return,if (!screenshot.preview) return.
Pěkný detail v historii je metoda. První commit přidal smyčku logger.debug('PDF timeline item', …) výslovně „pro debugování prázdných záznamů". Jakmile to logování odhalilo, odkud prázdná místa vlastně přicházela — včetně samostatného bugu deduplikace screenshotů, kde rychlá zachycení produkovala prázdné duplikáty — následný commit opravil kořenovou příčinu výše a odstranil debug logování. Přidej instrumentaci, abys našel příčinu, oprav příčinu, smaž instrumentaci. Odrážka changelogu "filtered empty entries" tiše obsahuje celý ten oblouk přidej-pak-odeber.
v1.8.12: "excludes AI responses," čti diff
O tři vydání později, ještě jedna změna — a je to dobrá lekce ve čtení kódu místo changelogu. Poznámka v1.8.12 říká, že PDF export teď "excludes AI responses for cleaner meeting records." Pravda, ale neúplná.
Export timeline taguje každý item typeem: 'transcript' | 'chat_user' | 'chat_assistant' | 'screenshot'. Mluvený řádek schůzky je transcript; otázka, kterou jsi napsal do panelu asistenta, je chat_user; odpověď AI je chat_assistant. Změna v1.8.12 je devítiřádková editace jednoho souboru rendereru a operativní řádek je filtr umístěný těsně před sestavením export payloadu:
timeline.filter((item) => item.type === 'transcript' || item.type === 'screenshot')
To je allowlist, ne blocklist. Neodstraňuje chat_assistant položky; ponechává jen transcript a screenshot a zahazuje všechno ostatní — což znamená, že strhne uživatelovy vlastní chat_user otázky spolu s odpověďmi AI. Tělo commitu je přesnější než changelog: "PDF export keeps only transcripts and screenshots, no chat messages." Exportovaný záznam je teď čistě schůzka — co bylo řečeno a co bylo na obrazovce — s celou konverzací postranního panelu asistenta z něj odstraněnou.
A to je správné rozhodnutí, z produktového hlediska. Chat GeekBye je soukromý asistent, kterého konzultuješ během hovoru — zeptej se ho na screenshot, dostaň rychlé vysvětlení. Ty výměny jsou tvůj poznámkový blok, ne součást schůzky. Záznam schůzky, který bys s někým sdílel, by měl obsahovat schůzku, ne tvoje postranní konverzace s AI o ní. Allowlist prostě dělá ze „schůzky" doslovnou definici toho, co se exportuje.
Tři věci, které nás tohle vydání naučilo
- Prohlížeč je PDF renderer, který už máš. Pokud je tvůj dokument strukturovaný text a obrázky,
printToPDFnad skrytýmBrowserWindowti dá skutečné stránkování, skutečné renderování fontů a CSS rozvržení zdarma — žádné PDF drawing API. Vyměníš závislost na knihovně za hraniční případy prohlížeče, což je často lepší výměna. - Bug, který vidíš, a bug, který existuje, nejsou vždy tentýž. „Prázdné boxy" křičely chybějící data; příčina byl rozdělený kontejner. Instrumentuj, než začneš předpokládat — debug logování týmu je to, co proměnilo věrohodnou-ale-špatnou opravu (filtruj prázdné) v tu skutečnou (
break-inside: avoid). - Changelog je souhrn; filtr je pravda. "Excludes AI responses" je allowlist, který zahazuje i tvoje vlastní chat otázky. Když poznámka k vydání popisuje změnu chování, diff ti řekne její přesné hrany — a tady je přesná hrana „přežije jen schůzka".
Předchozí kapitolu příběhu v1 najdete v od OCR k pixelům, aniž bychom ztratili fallback (v1.8.6–v1.8.7); a celý oblouk v anatomii dodávání softwaru k dokonalosti.