Steven
Steven8 min. skaitymo

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ė.

Inžinerija
Electron
Desktop
GeekBye leidimai
Susitikimo spausdinimas į PDF be PDF bibliotekos

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ė:

  1. Iškviečia buildReportHtml(), kad surinktų pilną HTML eilutę iš susitikimo laiko juostos — vienas .item blokas kiekvienam transkripcijos įrašui ar ekrano nuotraukai.
  2. Įrašo tą HTML į laikiną failą os.tmpdir() kataloge.
  3. Įkelia failą į paslėptą langą — new BrowserWindow({ show: false, opacity: 0, focusable: false, skipTaskbar: true }).
  4. Palaukia, kol vaizdai įsikels, tada iškviečia win.webContents.printToPDF({ printBackground: true, margins: { … } }).
  5. Išsaugo rezultatą į app.getPath('downloads') kaip Meeting-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 vaizdams const 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

  1. Naršyklė — tai PDF rendereris, kurį jau turi. Jei tavo dokumentas yra struktūrizuotas tekstas ir vaizdai, printToPDF virš paslėpto BrowserWindow gauna 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.
  2. 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).
  3. 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.

Susiję straipsniai

Du paspaudimus praleidžiančios perdangos gedimo režimai
Steven
Steven7 min. skaitymo

Du paspaudimus praleidžiančios perdangos gedimo režimai

GeekBye langas plaukioja virš visko ir praleidžia tavo paspaudimus kiaurai — išskyrus ten, kur turi mygtukus. Tai dvipusė sutartis, o v1.8.5 ir v1.8.14 rodo, kaip atrodo, kai lūžta kiekviena pusė: vienas leidimas, kur perdanga prarijo sisteminį dialogą, ir kitas, kur ji pavogė tavo klavišų paspaudimus. Nugalinčiu vaistu antrajam tapo kodo ištrynimas.

Inžinerija
Electron
Desktop
Gimęs ir ištaisytas per dvylika minučių
Steven
Steven6 min. skaitymo

Gimęs ir ištaisytas per dvylika minučių

GeekBye v1.8.10 changelog sako, kad ištaisė strigtį redaguojant klaviatūros sparčiuosius klavišus. Ištaisė — bet strigtis buvo įvesta ir ištaisyta tame pačiame pull request, dvylikos minučių skirtumu, ir nepasiekė nė vieno naudotojo. Tikroji istorija — patikimumo kaskada, kuri ją pagimdė: mažas, teisingas pakeitimas to, kada spartieji klavišai pristabdomi, ir React išardymo bagas, iškritęs iš vienos eilutės, skirtos padaryti redaktorių saugesnį.

Inžinerija
React
Desktop
Nuo OCR iki pikselių neprarandant atsarginio kelio
Steven
Steven7 min. skaitymo

Nuo OCR iki pikselių neprarandant atsarginio kelio

GeekBye nustojo skaityti tavo ekrano nuotraukas per OCR ir ėmė siųsti patį vaizdą regintiems modeliams — o tai yra lengvoji dalis. Pamokanti dalis yra optimizacija, kuri praleisdavo OCR visiškai regintį modelį turintiems naudotojams, tyliai laužė atsarginį kelią į neregintius modelius ir turėjo būti atsukta iki versijos, kur apsauginis tinklas bėga lygiagrečiai ir nekainuoja nieko.

Inžinerija
AI
Desktop