Steven
Steven8 min čítania

Tlač stretnutia do PDF bez knižnice PDF

GeekBye exportuje stretnutie ako PDF a nikde v kóde nie je knižnica PDF. Renderuje HTML v neviditeľnom okne prehliadača a vytlačí ho. Ten výber je celý príbeh: spravil funkciu ľahkou na zostavenie a dal jej každé zlyhanie, aké má skutočný prehliadač — biele bliknutie, limit dĺžky URL a zlom strany, ktorý rozrezal snímky obrazovky napoly. Liekom na najškaredší bug bol jeden riadok CSS.

Inžinierstvo
Electron
Desktop
Vydania GeekBye
Tlač stretnutia do PDF bez knižnice PDF

V tlačidle GeekBye „exportovať toto stretnutie ako PDF” sa skrýva uspokojivý trik: nestojí za ním žiadna knižnica PDF. Žiadny jsPDF, žiadny pdfmake, žiadny pdfkit, žiadny headless puppeteer. Celá funkcia je skrytý prehliadač. Aplikácia zostaví HTML stránku opisujúcu stretnutie — záznamy prepisu, snímky obrazovky, časové značky — načíta ju do neviditeľného okna Electronu a poprosí to okno, aby sa vytlačilo do PDF. webContents.printToPDF spraví zvyšok. Ten jediný architektonický výber je celý príbeh tohto vydania, lebo spravil funkciu lacnou na zostavenie a vručil jej každý bug, aký má skutočný prehliadač.

Predtým je to isté vydanie

Zvyčajne sa tieto posty otvárajú slovami „takto to fungovalo predtým”. Tentokrát niet žiadneho predtým. Changelog pre v1.8.9 sa číta ako údržba — „opravené zlomy strán rozrezávajúce snímky obrazovky a odfiltrované prázdne záznamy” — akoby export PDF bol existujúcou funkciou, ktorú niekto opravuje. Nebol. Prvý commit, ktorý ho zavádza, feat: add PDF export via Electron printToPDF, pristáva na tretí deň toho istého vydávacieho cyklu a každá „oprava” v changelogu je iterácia z toho istého týždňa na kóde, ktorý mal pár hodín. Funkcia sa narodila, zlomila a bola zakalená celá vnútri jedného tagu. Stojí za to povedať to nahlas, lebo to je poctivý tvar mnohých vydaní: vyleštená odrážka v poznámkach k vydaniu a chaotické zrodenie tej veci sú často to isté vydanie.

Prečo vôbec skrytý prehliadač

Mechanizmus sa oplatí pochopiť pred bugmi, lebo všetky bugy z neho vychádzajú. Všetko žije v jednom súbore hlavného procesu, electron/ipcHandlers/pdfExportHandler.ts. Keď renderer vystrelí IPC 'pdf:export-report', handler:

  1. Zavolá buildReportHtml(), aby zostavil celý reťazec HTML z časovej osi stretnutia — jeden blok .item na každý záznam prepisu alebo snímku obrazovky.
  2. Zapíše ten HTML do dočasného súboru v os.tmpdir().
  3. Načíta súbor do skrytého okna — new BrowserWindow({ show: false, opacity: 0, focusable: false, skipTaskbar: true }).
  4. Počká, kým sa obrázky načítajú, potom zavolá win.webContents.printToPDF({ printBackground: true, margins: { … } }).
  5. Uloží výsledok do app.getPath('downloads') ako Meeting-Report-<timestamp>.pdf.

Príťažlivosť je zjavná. Knižnica PDF ťa núti pozicovať všetko ručne — nakresli text na (x, y), zmeraj ho, posuň kurzor, sám spravuj zlomy strán. Prehliadač to všetko už robí: píšeš <div>-y a CSS a rozloženie Chromia stránkuje za teba. Neučíš sa API na kreslenie; píšeš webovú stránku. Pre dokument, ktorý je väčšinou štýlovaný text a vložené obrázky, je to obrovská skratka.

Háčik je v tom, že si zdedil aj prehliadač so všetkými jeho hraničnými prípadmi. Tri z nich sa ukázali v prvých štyroch dňoch.

Biele bliknutie. Skryté okno na macOS nie je spoľahlivo skryté. Prvé pokusy ho umiestňovali mimo obrazovky (x: screenWidth + 1000) a aj tak bliklo bielym obdĺžnikom na obrazovke na jeden snímok pred tlačou. Liekom, po troch pokusoch, bolo prestať sa spoliehať na pozíciu alebo show: false a nastaviť opacity: 0 — úplne priehľadné okno neblikne, ani keď ho kompozítor na chvíľu ukáže. Commit to hovorí priamo: „show:false with off-screen positioning still flashes on macOS.”

Limit dĺžky URL. Prvá verzia vkladala celé HTML — aj so snímkami obrazovky — ako URL data:. To funguje, kým stretnutie nemá v sebe pár snímok obrazovky zakódovaných v base64, a v tom bode URL prepláva za limit data-URL Chromia ~2 MB a načítanie zlyhá rovno s ERR_INVALID_URL. Preto krok 2 vyššie zapisuje dočasný súbor a robí loadFile() namiesto načítania URL data: cesta k súboru nemá limit dĺžky. (Tie ~2 MB sú jediné číslo v celom tomto príbehu a je to odhad vpísaný do komentára v kóde, nie niečo, čo niekto zmeral benchmarkom — ber to ako „dosť veľké na to, aby sa doň narazilo”, nie presný prah.)

Neescapovaný text. Text prepisu je ľubovoľný používateľský obsah a padal rovno do reťazca HTML. Akýkoľvek prepis, ktorý náhodou obsahoval niečo ako <div> či <script>, by zlomil stránku alebo horšie. Tak handler pribral prechod escapeHtml() nad každým reťazcom, ktorý interpoluje. V okamihu, keď je tvoj PDF webovou stránkou, injekcia HTML je tiež tvoj problém.

Prázdne rámčeky, ktoré neboli prázdne

Teraz titulný bug, lebo je najlepší. Používatelia hlásili exportované PDF s prázdnymi rámčekmi — záznamy, ktoré ukazovali časovú značku a odznak, ale žiadnu snímku obrazovky, len prázdne miesto tam, kde mal byť obraz. Zjavné čítanie je „export zahadzuje dáta obrazu” a zjavná oprava je „odfiltruj prázdne záznamy”. Tím ten filter naozaj pridal — o ňom o sekundu — ale nebol skutočnou príčinou.

Skutočnou príčinou bolo stránkovanie. Každý záznam je kontajner .item držiaci časovú značku, odznak a obraz. Chromium, tlačiac do PDF, s radosťou zlomil stránku uprostred položky — vykresliac časovú značku a odznak naspodku jednej strany a vytlačiac obraz na ďalšiu. To, čo vyzeralo ako prázdny rámček, bola horná polovica položky, ktorej obraz pretiekol na nasledujúcu stranu. Dáta tam všetky boli; rozloženie ich zgilotínovalo. Commit je úprimný ohľadom nesprávnej 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 riadok CSS:

.item {
  break-inside: avoid;
}

break-inside: avoid hovorí rozkladaciemu enginu, aby držal položku celú — ak sa nezmestí do zvyšného miesta na strane, presuň celú položku na ďalšiu stranu, namiesto toho, aby si ju delil. Toto je výplata architektúry skrytého prehliadača vyjadrená v miniatúre: najškaredší, najčastejšie hlásený bug tejto funkcie sa neopravil aritmetikou výšky strany a ručnými volaniami addPage(), ale jedinou vlastnosťou CSS, ktorú prehliadač už vedel rešpektovať. Nenapísal si stránkovač; poprosil si ten, ktorý si zdedil, aby sa správal.

Dve vrstvy filtrovania naozaj prázdnych

Naozaj prázdne záznamy tam boli tiež — len nie tie, čo spôsobovali rámčeky. Prázdne duplikáty z rýchleho dvojitého Cmd+Enter, snímky obrazovky bez náhľadu, záznamy prepisu, ktoré boli len medzery. Tie sa odfiltrovali, a zaujímavo sa odfiltrovali dvakrát, na dvoch vrstvách:

  • V handleri buildReportHtml() stráži každú položku — if (!item.content?.trim()) return '' pre text a pre obrázky const safeSrc = item.preview?.startsWith('data:image/') ? item.preview : ''; if (!safeSrc) return '' — potom .filter(Boolean) zahodí prázdne. Tá posledná kontrola koná dvojitú službu: je to brána platnosti a malá bezpečnostná kontrola, že na stránku sa dostanú len skutočné dáta obrazu.
  • Pri zdroji časovej osi, v rendereri, sa tá istá prázdnota filtruje ešte predtým, než sa položka vôbec pridá — if (!entry.text?.trim()) return, if (!screenshot.preview) return.

Milý detail v histórii je metóda. Prvý commit pridal slučku logger.debug('PDF timeline item', …) výslovne „na ladenie prázdnych záznamov”. Len čo to logovanie odhalilo, odkiaľ sa prázdne miesta naozaj brali — vrátane samostatného bugu deduplikácie snímok obrazovky, kde rýchle zachytenia plodili prázdne duplikáty — následný commit opravil koreňovú príčinu vyššie v prúde a odstránil ladiace logovanie. Pridaj inštrumentáciu, aby si našiel príčinu, oprav príčinu, zmaž inštrumentáciu. Odrážka changelogu „odfiltrované prázdne záznamy” ticho obsahuje celý ten oblúk pridaj-a-potom-odober.

v1.8.12: „vylučuje odpovede AI”, čítaj diff

Tri vydania neskôr ešte jedna zmena — a je to dobrá lekcia v čítaní kódu namiesto changelogu. Poznámka v1.8.12 hovorí, že export PDF teraz „vylučuje odpovede AI pre čistejšie záznamy stretnutí”. Pravda, ale neúplná.

Časová os exportu označí každú položku poľom type: 'transcript' | 'chat_user' | 'chat_assistant' | 'screenshot'. Vyslovený riadok stretnutia je transcript; otázka, ktorú si napísal do panela asistenta, je chat_user; odpoveď AI je chat_assistant. Zmena v1.8.12 je deväťriadková úprava jedného súboru rendereru a rozhodujúci riadok je filter umiestnený tesne predtým, než sa zostaví náklad exportu:

timeline.filter((item) => item.type === 'transcript' || item.type === 'screenshot')

To je zoznam povolených, nie zoznam blokovaných. Neodstraňuje položky chat_assistant; ponecháva len transcript a screenshot a zahodí všetko ostatné — čo znamená, že zoškrabne používateľove vlastné otázky chat_user spolu s odpoveďami AI. Telo commitu je presnejšie než changelog: „PDF export keeps only transcripts and screenshots, no chat messages.” Exportovaný záznam je teraz čisto stretnutie — čo bolo povedané a čo bolo na obrazovke — s celou konverzáciou z bočného panela asistenta z neho odstránenou.

A produktovo je to správne rozhodnutie. Chat GeekBye je súkromný asistent, s ktorým sa radíš počas hovoru — spýtaj sa ho na snímku obrazovky, dostaň rýchle vysvetlenie. Tie výmeny sú tvoj poznámkový blok, nie súčasť stretnutia. Záznam stretnutia, ktorý by si s niekým zdieľal, by mal obsahovať stretnutie, nie tvoje vedľajšie rozhovory s AI o ňom. Zoznam povolených jednoducho robí „stretnutie” doslovnou definíciou toho, čo sa exportuje.

Tri veci, ktoré nás toto vydanie naučilo

  1. Prehliadač je renderer PDF, ktorý už máš. Ak je tvoj dokument štruktúrovaný text a obrázky, printToPDF nad skrytým BrowserWindow ti dá skutočné stránkovanie, skutočné renderovanie písma a rozloženie CSS zadarmo — žiadne API na kreslenie PDF. Vymeníš závislosť na knižnici za hraničné prípady prehliadača, čo je často lepší obchod.
  2. Bug, ktorý vidíš, a bug, ktorý existuje, nie sú vždy to isté. „Prázdne rámčeky” kričali chýbajúce dáta; príčinou bol rozdelený kontajner. Inštrumentuj skôr, než predpokladáš — ladiace logovanie tímu je to, čo premenilo vierohodnú-ale-nesprávnu opravu (filtruj prázdne) na tú skutočnú (break-inside: avoid).
  3. Changelog je zhrnutie; filter je pravda. „Vylučuje odpovede AI” je zoznam povolených, ktorý zahodí aj tvoje vlastné otázky z chatu. Keď poznámka k vydaniu opisuje zmenu správania, diff ti povie jej presné hrany — a tu je presná hrana „prežije len stretnutie”.

Pre predchádzajúcu kapitolu príbehu v1 — od OCR k pixelom bez straty záložnej cesty (v1.8.6–v1.8.7); a pre celý oblúk — anatómiu dodávania softvéru do dokonalosti.

Súvisiace články

Dva režimy zlyhania prekrytia prepúšťajúceho kliknutia
Steven
Steven8 min čítania

Dva režimy zlyhania prekrytia prepúšťajúceho kliknutia

Okno GeekBye pláva nad všetkým a púšťa tvoje kliknutia naskrz — okrem miest, kde má tlačidlá. To je dvojstranná zmluva a v1.8.5 a v1.8.14 ukazujú, ako vyzerá zlyhanie každej zo strán: jedno vydanie, kde prekrytie prehltlo systémový dialóg, a druhé, kde ti ukradlo stlačenia klávesov. Víťazným liekom na to druhé bolo zmazanie kódu.

Inžinierstvo
Electron
Desktop
Zrodený a opravený za dvanásť minút
Steven
Steven7 min čítania

Zrodený a opravený za dvanásť minút

Changelog GeekBye v1.8.10 hovorí, že opravil pád počas úpravy klávesových skratiek. Opravil — ale pád bol zavedený a opravený v tom istom pull requeste, s odstupom dvanástich minút, a nedostal sa k jedinému používateľovi. Skutočný príbeh je kaskáda spoľahlivosti, ktorá ho vyprodukovala: malá, správna zmena toho, kedy sa skratky pozastavujú, a bug rozoberania Reactu, ktorý vypadol z jedného riadka, čo mal spraviť editor bezpečnejším.

Inžinierstvo
React
Desktop
Od OCR k pixelom bez straty záložnej cesty
Steven
Steven7 min čítania

Od OCR k pixelom bez straty záložnej cesty

GeekBye prestal čítať tvoje snímky obrazovky cez OCR a začal posielať samotný obraz vidiacim modelom — čo je tá ľahká časť. Poučná časť je optimalizácia, ktorá preskakovala OCR úplne pre používateľov s vidiacim modelom, potichu lámala záložnú cestu k modelom bez videnia a musela byť vrátená späť do verzie, kde záchranná sieť beží paralelne a nestojí nič.

Inžinierstvo
AI
Desktop