
Susitikimo spausdinimas į PDF be PDF bibliotekos
GeekBye eksportuoja susitikimą kaip PDF, o niekur kode nėra PDF bibliotekos. Jis atvaizduoja HTML nematomame naršyklės lange ir jį spausdina. Tas pasirinkimas — visa istorija: jis padarė funkciją lengvai sukuriamą ir suteikė jai kiekvieną gedimą, kokį turi tikra naršyklė — baltą blyksnį, URL ilgio ribą ir puslapio lūžį, kuris perkirsdavo ekrano nuotraukas pusiau. Vaistas nuo bjauriausio bago buvo viena CSS eilutė.
GeekBye mygtuke „eksportuoti šį susitikimą kaip PDF” slepiasi maloni gudrybė: už jo nėra jokios PDF bibliotekos. Jokio jsPDF, jokio pdfmake, jokio pdfkit, jokio headless puppeteer. Visa funkcija — tai paslėpta naršyklė. Programa sukuria HTML puslapį, aprašantį susitikimą — transkripcijos įrašus, ekrano nuotraukas, laiko žymas — įkelia jį į nematomą Electron langą ir paprašo to lango atspausdinti save į PDF. webContents.printToPDF padaro likusią dalį. Tas vienas architektūrinis pasirinkimas — visa šio leidimo istorija, nes jis padarė funkciją pigią sukurti ir įteikė jai kiekvieną bagą, kokį turi tikra naršyklė.
„Prieš” yra tas pats leidimas
Paprastai šie įrašai atsiveria žodžiais „štai kaip tai veikė anksčiau”. Šįkart nėra jokio anksčiau. v1.8.9 changelog skaitosi kaip priežiūra — „ištaisyti puslapių lūžiai, kertantys ekrano nuotraukas, ir atfiltruoti tušti įrašai” — tarsi PDF eksportas būtų esama funkcija, kurią lopo. Nebuvo. Pirmas komitas, kuris jį įveda, feat: add PDF export via Electron printToPDF, nusileidžia trečią to paties leidimo ciklo dieną, o kiekvienas „pataisymas” changelog yra tos pačios savaitės iteracija ant kodo, kuriam buvo kelios valandos. Funkcija gimė, sulūžo ir buvo užgrūdinta visa viename žymėje. Tai verta pasakyti garsiai, nes tai sąžininga daugelio išleidimų forma: nugludintas punktas leidimo pastabose ir netvarkingas to daikto gimimas dažnai yra tas pats leidimas.
Kodėl apskritai paslėpta naršyklė
Mechanizmą verta suprasti prieš bagus, nes visi bagai iš jo kyla. Viskas gyvena viename pagrindinio proceso faile, electron/ipcHandlers/pdfExportHandler.ts. Kai rendereris iššauna 'pdf:export-report' IPC, tvarkyklė:
- Iškviečia
buildReportHtml(), kad surinktų pilną HTML eilutę iš susitikimo laiko juostos — vienas.itemblokas kiekvienam transkripcijos įrašui ar ekrano nuotraukai. - Įrašo tą HTML į laikiną failą
os.tmpdir()kataloge. - Įkelia failą į paslėptą langą —
new BrowserWindow({ show: false, opacity: 0, focusable: false, skipTaskbar: true }). - Palaukia, kol vaizdai įsikels, tada iškviečia
win.webContents.printToPDF({ printBackground: true, margins: { … } }). - Išsaugo rezultatą į
app.getPath('downloads')kaipMeeting-Report-<timestamp>.pdf.
Patrauklumas akivaizdus. PDF biblioteka verčia tave viską pozicionuoti ranka — nupiešk tekstą ties (x, y), išmatuok jį, pastumk žymeklį, pats tvarkyk puslapių lūžius. Naršyklė visa tai jau daro: tu rašai <div>-us ir CSS, o Chromium išdėstymo variklis puslapiuoja už tave. Tu ne mokaisi piešimo API; tu rašai tinklalapį. Dokumentui, kuris daugiausia yra stilizuotas tekstas ir įterpti vaizdai, tai milžiniškas trumpinys.
Kliūtis ta, kad taip pat paveldėjai naršyklę su visais jos kraštutiniais atvejais. Trys iš jų iškilo per pirmas keturias dienas.
Baltas blyksnis. Paslėptas langas macOS sistemoje nėra patikimai paslėptas. Pirmieji bandymai jį dėdavo už ekrano (x: screenWidth + 1000), o jis vis tiek blykstelėdavo baltu stačiakampiu ekrane vieną kadrą prieš spausdinimą. Vaistas, po trijų bandymų, buvo nustoti kliautis pozicija ar show: false ir nustatyti opacity: 0 — visiškai skaidrus langas neblyksi, net kai kompozitorius jį trumpam parodo. Komitas sako tai tiesiai: „show:false with off-screen positioning still flashes on macOS.”
URL ilgio riba. Pirmoji versija įterpdavo visą HTML — kartu su ekrano nuotraukomis — kaip URL data:. Tai veikia, kol susitikime neatsiranda kelios base64 užkoduotos ekrano nuotraukos, ir tuo momentu URL nuplaukia už Chromium ~2 MB data-URL ribos, o įkėlimas atvirai žlunga su ERR_INVALID_URL. Štai kodėl 2 žingsnis aukščiau įrašo laikiną failą ir daro loadFile() vietoj URL data įkėlimo: failo kelias neturi ilgio ribos. (Tie ~2 MB yra vienintelis skaičius visoje šioje istorijoje, ir tai kode komentare įrašytas įvertis, o ne kažkas, ką kas nors išmatavo benchmarku — vertink tai kaip „pakankamai didelis, kad į jį atsitrenktum”, o ne tikslų slenkstį.)
Neekranuotas tekstas. Transkripcijos tekstas yra savavališkas naudotojo turinys, o jis buvo metamas tiesiai į HTML eilutę. Bet kuri transkripcija, kuri atsitiktinai turėjo kažką panašaus į <div> ar <script>, sulaužytų puslapį arba blogiau. Tad tvarkyklė apaugo escapeHtml() praėjimu virš kiekvienos eilutės, kurią ji interpoliuoja. Tą akimirką, kai tavo PDF yra tinklalapis, HTML injekcija taip pat yra tavo problema.
Tušti langeliai, kurie nebuvo tušti
Dabar pagrindinis bagas, nes jis geriausias. Naudotojai pranešė apie eksportuotus PDF su tuščiais langeliais — įrašais, kurie rodė laiko žymą ir ženkliuką, bet jokios ekrano nuotraukos, tik tuščią vietą ten, kur turėtų būti vaizdas. Akivaizdus skaitymas yra „eksportas praranda vaizdo duomenis”, o akivaizdus vaistas yra „atfiltruok tuščius įrašus”. Komanda tą filtrą tikrai pridėjo — apie jį po sekundės — bet ne jis buvo tikroji priežastis.
Tikroji priežastis buvo puslapiavimas. Kiekvienas įrašas yra .item konteineris, laikantis laiko žymą, ženkliuką ir vaizdą. Chromium, spausdindamas į PDF, mielai laužydavo puslapį elemento viduryje — atvaizduodamas laiko žymą ir ženkliuką vieno puslapio apačioje ir stumdamas vaizdą į kitą. Tai, kas atrodė kaip tuščias langelis, buvo viršutinė elemento pusė, kurio vaizdas persiliejo į paskesnį puslapį. Duomenys visi buvo vietoje; išdėstymas juos giljotinavo. Komitas atviras dėl klaidingos diagnozės: „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.”
Pataisymas — viena CSS eilutė:
.item {
break-inside: avoid;
}
break-inside: avoid liepia išdėstymo varikliui laikyti elementą sveiką — jei jis netelpa į likusią vietą puslapyje, perkelk visą elementą į kitą puslapį, o ne skaidyk jį. Tai paslėptos naršyklės architektūros atsipirkimas, išreikštas miniatiūroje: bjauriausias, dažniausiai praneštas šios funkcijos bagas buvo ištaisytas ne puslapio aukščio aritmetika ir rankiniais addPage() kvietimais, o viena CSS savybe, kurią naršyklė jau mokėjo gerbti. Tu neparašei puslapiuotojo; tu paprašei to, kurį paveldėjai, elgtis kaip dera.
Du iš tikrųjų tuščių filtravimo sluoksniai
Iš tikrųjų tuščių įrašų taip pat buvo — tik ne tie, kurie kėlė langelius. Tušti dublikatai nuo greito dvigubo Cmd+Enter, ekrano nuotraukos be peržiūros, transkripcijos įrašai, kurie buvo vien tarpai. Šie buvo atfiltruoti, ir įdomu, kad jie buvo atfiltruoti dukart, dviejuose sluoksniuose:
- Tvarkyklėje
buildReportHtml()saugo kiekvieną elementą —if (!item.content?.trim()) return ''tekstui, o vaizdamsconst safeSrc = item.preview?.startsWith('data:image/') ? item.preview : ''; if (!safeSrc) return ''— tada.filter(Boolean)atmeta tuščius. Ta paskutinė patikra atlieka dvigubą tarnybą: tai ir galiojimo vartai, ir maža saugos patikra, kad į puslapį patenka tik tikri vaizdo duomenys. - Prie laiko juostos šaltinio, rendereryje, ta pati tuštuma filtruojama dar prieš pridedant elementą —
if (!entry.text?.trim()) return,if (!screenshot.preview) return.
Malonus istorijos akcentas — tai metodas. Pirmas komitas pridėjo logger.debug('PDF timeline item', …) ciklą aiškiai „tuščių įrašų derinimui”. Kai tik tas žurnalizavimas atskleidė, iš kur iš tikrųjų atsirasdavo tuštumos — įskaitant atskirą ekrano nuotraukų deduplikavimo bagą, kur greiti fiksavimai gimdydavo tuščius dublikatus — paskesnis komitas ištaisė šakninę priežastį aukščiau sraute ir pašalino derinimo žurnalizavimą. Pridėk instrumentaciją, kad rastum priežastį, ištaisyk priežastį, ištrink instrumentaciją. Changelog punktas „atfiltruoti tušti įrašai” tyliai talpina visą tą pridėk-o-paskui-pašalink lanką.
v1.8.12: „neįtraukia DI atsakymų”, skaityk diff
Po trijų leidimų dar vienas pokytis — ir tai gera pamoka skaityti kodą vietoj changelog. v1.8.12 pastaba sako, kad PDF eksportas dabar „neįtraukia DI atsakymų, kad susitikimų įrašai būtų švaresni”. Tiesa, bet nepilna.
Eksporto laiko juosta pažymi kiekvieną elementą lauku type: 'transcript' | 'chat_user' | 'chat_assistant' | 'screenshot'. Ištarta susitikimo eilutė yra transcript; klausimas, kurį įrašei į asistento skydelį, yra chat_user; DI atsakymas yra chat_assistant. v1.8.12 pokytis — tai devynių eilučių taisymas viename renderrio faile, o lemiama eilutė — tai filtras, padėtas tiesiai prieš sukuriant eksporto naudingąją apkrovą:
timeline.filter((item) => item.type === 'transcript' || item.type === 'screenshot')
Tai leidžiamųjų sąrašas, o ne blokuojamųjų. Jis nešalina chat_assistant elementų; jis palieka tik transcript ir screenshot ir atmeta visa kita — o tai reiškia, kad jis nuskuta paties naudotojo chat_user klausimus kartu su DI atsakymais. Komito tekstas tikslesnis nei changelog: „PDF export keeps only transcripts and screenshots, no chat messages.” Eksportuotas įrašas dabar yra grynai susitikimas — kas buvo pasakyta ir kas buvo ekrane — su iš jo pašalintu visu asistento šoninio skydelio pokalbiu.
Ir tai teisingas sprendimas produkto požiūriu. GeekBye pokalbis yra privatus asistentas, su kuriuo tariesi skambučio metu — paklausk jo apie ekrano nuotrauką, gauk greitą paaiškinimą. Tie mainai yra tavo juodraštis, ne susitikimo dalis. Susitikimo įrašas, kuriuo su kuo nors dalintumeisi, turėtų turėti susitikimą, o ne tavo šalutinius pokalbius su DI apie jį. Leidžiamųjų sąrašas tiesiog padaro „susitikimą” pažodine to, kas eksportuojama, apibrėžtimi.
Trys dalykai, kurių mus išmokė šis leidimas
- Naršyklė — tai PDF rendereris, kurį jau turi. Jei tavo dokumentas yra struktūrizuotas tekstas ir vaizdai,
printToPDFvirš paslėptoBrowserWindowgauna tau tikrą puslapiavimą, tikrą šriftų atvaizdavimą ir CSS išdėstymą nemokamai — jokio PDF piešimo API. Tu iškeiti bibliotekos priklausomybę į naršyklės kraštutinius atvejus, o tai dažnai geresnis sandoris. - Bagas, kurį matai, ir bagas, kuris egzistuoja, ne visada yra tas pats. „Tušti langeliai” rėkė trūkstami duomenys; priežastis buvo perskeltas konteineris. Instrumentuok prieš darydamas prielaidas — komandos derinimo žurnalizavimas yra tai, kas pavertė tikėtiną-bet-klaidingą vaistą (filtruok tuščius) į tikrąjį (
break-inside: avoid). - Changelog yra santrauka; filtras yra tiesa. „Neįtraukia DI atsakymų” yra leidžiamųjų sąrašas, kuris atmeta ir tavo paties pokalbio klausimus. Kai leidimo pastaba aprašo elgsenos pokytį, diff pasakys tau jos tikslius kraštus — o čia tikslus kraštas yra „išgyvena tik susitikimas”.
Ankstesniam v1 istorijos skyriui — nuo OCR iki pikselių neprarandant atsarginio kelio (v1.8.6–v1.8.7); o visai istorijos linijai — programinės įrangos tobulybės pristatymo anatomija.