
Kokouksen tulostaminen PDF:ksi ilman PDF-kirjastoa
GeekBye vie kokouksen PDF:nä, eikä koodissa ole missään PDF-kirjastoa. Se renderöi HTML:n näkymättömässä selainikkunassa ja tulostaa sen. Tuo valinta on koko tarina: se teki ominaisuudesta helpon rakentaa ja antoi sille jokaisen vian joka oikealla selaimella on — valkoisen välähdyksen, URL:n pituusrajan ja sivunvaihdon joka leikkasi kuvakaappaukset kahtia. Rumimman bugin korjaus oli yksi rivi CSS:ää.
GeekByen "vie tämä kokous PDF:nä" -napissa piileskelee tyydyttävä temppu: sen takana ei ole yhtään PDF-kirjastoa. Ei jsPDF:ää, ei pdfmakea, ei pdfkitiä, ei headless-puppeteeria. Koko ominaisuus on piilotettu selain. Sovellus rakentaa HTML-sivun joka kuvaa kokouksen — litterointimerkinnät, kuvakaappaukset, aikaleimat — lataa sen näkymättömään Electron-ikkunaan ja pyytää tuota ikkunaa tulostamaan itsensä PDF:ksi. webContents.printToPDF tekee loput. Tuo yksi arkkitehtuurivalinta on koko tämän julkaisun tarina, koska se teki ominaisuudesta halvan rakentaa ja ojensi sille jokaisen bugin joka oikealla selaimella on.
Ennen on sama julkaisu
Yleensä nämä postaukset alkavat sanoilla "näin se toimi ennen." Tällä kertaa ei ole mitään ennen. v1.8.9:n muutosloki lukee kuin ylläpitoa — "fixed page breaks splitting screenshots and filtered empty entries" — ikään kuin PDF-vienti olisi olemassa oleva ominaisuus jota paikataan. Se ei ollut. Ensimmäinen commit joka sen esittelee, feat: add PDF export via Electron printToPDF, laskeutuu kolme päivää samaan julkaisusykliin, ja jokainen "korjaus" muutoslokissa on saman viikon iterointia koodiin joka oli tunteja vanhaa. Ominaisuus syntyi, hajosi ja karaistiin — kaikki yhden tagin sisällä. Se kannattaa sanoa ääneen, koska se on monen toimituksen rehellinen muoto: hiottu luoti julkaisumuistiinpanoissa ja asian sotkuinen syntymä ovat usein sama julkaisu.
Miksi ylipäätään piilotettu selain
Mekanismi kannattaa ymmärtää ennen bugeja, koska kaikki bugit polveutuvat siitä. Kaikki elää yhdessä pääprosessin tiedostossa, electron/ipcHandlers/pdfExportHandler.ts. Kun renderöijä laukaisee 'pdf:export-report' IPC:n, käsittelijä:
- Kutsuu
buildReportHtml():ää koostaakseen kokouksen aikajanasta täyden HTML-merkkijonon — yksi.item-lohko litterointimerkintää tai kuvakaappausta kohti. - Kirjoittaa tuon HTML:n väliaikaistiedostoon kansiossa
os.tmpdir(). - Lataa tiedoston piilotettuun ikkunaan —
new BrowserWindow({ show: false, opacity: 0, focusable: false, skipTaskbar: true }). - Odottaa kuvien latautumista, sitten kutsuu
win.webContents.printToPDF({ printBackground: true, margins: { … } }). - Tallentaa tuloksen kansioon
app.getPath('downloads')nimelläMeeting-Report-<timestamp>.pdf.
Vetovoima on ilmeinen. PDF-kirjasto pakottaa sinut sijoittamaan kaiken käsin — piirrä teksti kohtaan (x, y), mittaa se, siirrä kursoria, hallitse sivunvaihdot itse. Selain tekee kaiken sen jo: sinä kirjoitat <div>-elementtejä ja CSS:ää, ja Chromiumin asettelumoottori sivuttaa puolestasi. Et opettele piirrustus-API:a; kirjoitat verkkosivua. Dokumentille joka on enimmäkseen tyyliteltyä tekstiä ja upotettuja kuvia se on valtava oikopolku.
Koukku on siinä että perit myös selaimen kaikkine reunatapauksineen. Kolme niistä ilmaantui ensimmäisen neljän päivän aikana.
Valkoinen välähdys. Piilotettu ikkuna macOS:llä ei ole luotettavasti piilotettu. Ensimmäiset yritykset sijoittivat sen ruudun ulkopuolelle (x: screenWidth + 1000), ja se välähti silti valkoisen suorakulmion ruudulle yhden framen ajaksi ennen tulostusta. Korjaus, kolmen yrityksen jälkeen, oli lakata luottamasta sijaintiin tai show: false:aan ja asettaa opacity: 0 — täysin läpinäkyvä ikkuna ei välähdä edes silloin kun kompositori näyttää sen hetkeksi. Commit sanoo sen suoraan: "show:false with off-screen positioning still flashes on macOS."
URL:n pituusraja. Ensimmäinen versio upotti koko HTML:n — kuvakaappaukset ja kaiken — data: URL:nä. Se toimii kunnes kokouksessa on muutama base64-koodattu kuvakaappaus, jolloin URL purjehtii Chromiumin ~2 MB data-URL-rajan ohi ja lataus epäonnistuu suoraan virheellä ERR_INVALID_URL. Siksi yllä oleva askel 2 kirjoittaa väliaikaisen tiedoston ja tekee loadFile():n sen sijaan että lataisi data URL:n: tiedostopolulla ei ole pituusrajaa. (Tuo ~2 MB on tämän koko tarinan ainoa luku, ja se on rivinsisäinen arvio koodikommentissa, ei mitään mitä kukaan benchmarkasi — kohtele sitä kuin "tarpeeksi iso osuakseen," ei tarkkana kynnyksenä.)
Escapaamaton teksti. Litterointiteksti on mielivaltaista käyttäjäsisältöä, ja se pudotettiin suoraan HTML-merkkijonoon. Mikä tahansa litterointi joka sattui sisältämään jotain kuten <div> tai <script> rikkoisi sivun tai pahempaa. Joten käsittelijälle kasvoi escapeHtml()-läpiajo yli jokaisen merkkijonon jonka se interpoloi. Sillä hetkellä kun PDF:si on verkkosivu, HTML-injektio on myös sinun ongelmasi.
Tyhjät laatikot jotka eivät olleet tyhjiä
Nyt pääbugi, koska se on paras. Käyttäjät raportoivat viedyistä PDF:istä tyhjillä laatikoilla — merkinnät jotka näyttivät aikaleiman ja merkin mutta ei kuvakaappausta, vain tyhjää tilaa siinä missä kuvan pitäisi olla. Ilmeinen tulkinta on "vienti pudottaa kuvadataa," ja ilmeinen korjaus on "suodata tyhjät merkinnät pois." Tiimi lisäsi tuon suodattimen — siitä lisää hetken päästä — mutta se ei ollut todellinen syy.
Todellinen syy oli sivutus. Jokainen merkintä on .item-kontti joka pitää sisällään aikaleiman, merkin ja kuvan. Chromium, PDF:ää tulostaessaan, oli mielellään valmis vaihtamaan sivua merkinnän keskellä — renderöiden aikaleiman ja merkin yhden sivun alalaitaan ja työntäen kuvan seuraavalle. Se mikä näytti tyhjältä laatikolta oli merkinnän yläpuolisko jonka kuva oli ylivuotanut seuraavalle sivulle. Data oli kaikki siellä; asettelu oli giljotinoinut sen. Commit on rehellinen väärästä diagnoosista: "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."
Korjaus on yksi rivi CSS:ää:
.item {
break-inside: avoid;
}
break-inside: avoid kertoo asettelumoottorille että pidä merkintä kokonaisena — jos se ei mahdu sivulle jäljellä olevaan tilaan, siirrä koko merkintä seuraavalle sivulle sen sijaan että jakaisit sen. Tämä on piilotetun selaimen arkkitehtuurin palkinto miniatyyrinä sanottuna: ominaisuuden rumin, eniten raportoitu bugi korjattiin ei sivunkorkeusaritmetiikalla ja manuaalisilla addPage()-kutsuilla, vaan yhdellä CSS-ominaisuudella jonka selain jo osasi kunnioittaa. Et kirjoittanut sivuttajaa; pyysit perimääsi käyttäytymään.
Kaksi kerrosta oikeasti-tyhjän suodatusta
Siellä oli myös aidosti tyhjiä merkintöjä — vain ei niitä jotka aiheuttivat laatikot. Tyhjät duplikaatit nopeasta tuplasta Cmd+Enter, kuvakaappaukset ilman esikatselua, litterointimerkinnät jotka olivat pelkkää tyhjätilaa. Nämä suodatettiin, ja kiinnostavasti ne suodatettiin kahdesti, kahdessa kerroksessa:
- Käsittelijässä
buildReportHtml()vartioi jokaista merkintää —if (!item.content?.trim()) return ''tekstille, ja kuvilleconst safeSrc = item.preview?.startsWith('data:image/') ? item.preview : ''; if (!safeSrc) return ''— sitten.filter(Boolean)pudottaa tyhjät. Tuo viimeinen tarkistus tekee kaksoistyötä: se on kelvollisuusportti ja pieni turvatarkistus että sivulle päätyy vain oikeaa kuvadataa. - Aikajanan lähteellä, renderöijässä, sama tyhjyys suodatetaan ennen kuin merkintää edes lisätään —
if (!entry.text?.trim()) return,if (!screenshot.preview) return.
Historian mukava yksityiskohta on menetelmä. Ensimmäinen commit lisäsi logger.debug('PDF timeline item', …) -silmukan nimenomaisesti "for debugging empty entries." Kun tuo lokitus paljasti mistä tyhjät oikeasti tulivat — mukaan lukien erillinen kuvakaappausten deduplikointibugi jossa nopeat kaappaukset tuottivat tyhjiä duplikaatteja — seuraava commit korjasi juurisyyn ylävirrassa ja poisti debug-lokituksen. Lisää instrumentointi löytääksesi syyn, korjaa syy, poista instrumentointi. Muutoslokin luoti "filtered empty entries" sisältää hiljaa koko tuon lisää-sitten-poista kaaren.
v1.8.12: "excludes AI responses," lue diff
Kolme julkaisua myöhemmin vielä yksi muutos — ja se on hyvä oppitunti koodin lukemisessa muutoslokin sijaan. v1.8.12:n muistiinpano sanoo että PDF-vienti nyt "excludes AI responses for cleaner meeting records." Totta, mutta vaillinaista.
Vientiaikajana merkitsee jokaisen merkinnän type:llä: 'transcript' | 'chat_user' | 'chat_assistant' | 'screenshot'. Puhuttu kokousrivi on transcript; kysymys jonka kirjoitit assistenttipaneeliin on chat_user; AI:n vastaus on chat_assistant. v1.8.12:n muutos on yhdeksänrivinen muokkaus yhteen renderöijätiedostoon, ja ratkaiseva rivi on suodatin joka on sijoitettu juuri ennen vientikuorman rakentamista:
timeline.filter((item) => item.type === 'transcript' || item.type === 'screenshot')
Se on sallittujen lista, ei estettyjen lista. Se ei poista chat_assistant-merkintöjä; se pitää vain transcript:n ja screenshot:n ja pudottaa kaiken muun — mikä tarkoittaa että se riisuu käyttäjän omat chat_user-kysymykset AI:n vastausten mukana. Commitin runko on tarkempi kuin muutosloki: "PDF export keeps only transcripts and screenshots, no chat messages." Viety tallenne on nyt puhtaasti kokous — mitä sanottiin ja mitä oli ruudulla — koko assistentin sivupaneelin keskustelu siitä poistettuna.
Ja se on tuotemielessä oikea valinta. GeekByen chat on yksityinen assistentti jota konsultoit puhelun aikana — kysy siltä kuvakaappauksesta, saa nopea selitys. Nuo vaihdot ovat sinun luonnoslehtiösi, eivät osa kokousta. Kokoustallenne jonka jakaisit jollekulle pitäisi sisältää kokous, ei sivukeskustelujasi AI:n kanssa siitä. Sallittujen lista tekee "kokouksesta" vain sen kirjaimellisen määritelmän mitä viedään.
Kolme asiaa jotka tämä julkaisu opetti meille
- Selain on PDF-renderöijä joka sinulla jo on. Jos dokumenttisi on jäsenneltyä tekstiä ja kuvia,
printToPDFpiilotetunBrowserWindow:n yli antaa sinulle oikean sivutuksen, oikean fonttien renderöinnin ja CSS-asettelun ilmaiseksi — ei PDF:n piirrustus-API:a. Vaihdat kirjastoriippuvuuden selaimen reunatapauksiin, mikä on usein parempi vaihto. - Bugi jonka näet ja bugi joka on olemassa eivät aina ole sama. "Tyhjät laatikot" huusivat puuttuvaa dataa; syy oli jaettu kontti. Instrumentoi ennen kuin oletat — tiimin debug-lokitus on se joka muutti uskottavan-mutta-väärän korjauksen (suodata tyhjät) oikeaksi (
break-inside: avoid). - Muutosloki on tiivistelmä; suodatin on totuus. "Excludes AI responses" on sallittujen lista joka pudottaa myös omat chat-kysymyksesi. Kun julkaisumuistiinpano kuvaa käyttäytymismuutosta, diff kertoo sinulle sen tarkat reunat — ja tässä tarkka reuna on "vain kokous selviää."
Edellinen luku v1-tarinassa: OCR:stä pikseleihin vararatkaisua menettämättä (v1.8.6–v1.8.7); ja koko kaarelle, ohjelmiston toimittamisen anatomia täydellisyyteen asti.