
Sanāksmes drukāšana PDF formātā bez PDF bibliotēkas
GeekBye eksportē sanāksmi kā PDF, un kodā nekur nav PDF bibliotēkas. Tas atveido HTML neredzamā pārlūka logā un izdrukā to. Šī izvēle ir viss stāsts: tā padarīja funkciju viegli uzbūvējamu un iedeva tai katru atteici, kāda ir īstam pārlūkam — baltu zibsni, URL garuma ierobežojumu un lapas pārtraukumu, kas ekrānuzņēmumus pārgrieza uz pusēm. Neglītākās kļūdas labojums bija viena CSS rindiņa.
GeekBye pogā "eksportēt šo sanāksmi kā PDF" slēpjas gandarījumu sniedzošs triks: aiz tās nav nekādas PDF bibliotēkas. Nav jsPDF, nav pdfmake, nav pdfkit, nav headless puppeteer. Visa funkcija ir slēpts pārlūks. Lietotne uzbūvē HTML lapu, kas apraksta sanāksmi — transkripta ierakstus, ekrānuzņēmumus, laika zīmogus — ielādē to neredzamā Electron logā un lūdz šim logam izdrukāt sevi PDF formātā. webContents.printToPDF paveic pārējo. Šī vienīgā arhitektūras izvēle ir viss šī laidiena stāsts, jo tā padarīja funkciju lētu uzbūvēšanā un iedeva tai katru kļūdu, kāda ir īstam pārlūkam.
Iepriekš ir tas pats laidiens
Parasti šie raksti sākas ar "lūk, kā tas darbojās agrāk." Šoreiz nav nekāda agrāk. v1.8.9 izmaiņu žurnāls lasās kā uzturēšana — "fixed page breaks splitting screenshots and filtered empty entries" — it kā PDF eksports būtu esoša funkcija, kas tiek ielāpota. Tā nebija. Pirmais komits, kas to ievieš, feat: add PDF export via Electron printToPDF, ienāk trīs dienas tajā pašā laidiena ciklā, un katrs "labojums" izmaiņu žurnālā ir tās pašas nedēļas iterācija uz koda, kas bija dažas stundas vecs. Funkcija piedzima, salūza un tika norūdīta — viss vienā tagā. To ir vērts pateikt skaļi, jo tā ir godīgā forma daudzai piegādei: nogludinātais punkts laidiena piezīmēs un lietas haotiskā dzimšana bieži ir viens un tas pats laidiens.
Kāpēc vispār slēpts pārlūks
Mehānisms ir vērts saprašanas pirms kļūdām, jo visas kļūdas nāk no tā. Viss dzīvo vienā galvenā procesa failā, electron/ipcHandlers/pdfExportHandler.ts. Kad renderētājs izsauc 'pdf:export-report' IPC, apstrādātājs:
- Izsauc
buildReportHtml(), lai no sanāksmes laika joslas saliktu pilnu HTML virkni — viens.itembloks katram transkripta ierakstam vai ekrānuzņēmumam. - Ieraksta šo HTML pagaidu failā
os.tmpdir(). - Ielādē failu slēptā logā —
new BrowserWindow({ show: false, opacity: 0, focusable: false, skipTaskbar: true }). - Gaida, kamēr attēli ielādējas, tad izsauc
win.webContents.printToPDF({ printBackground: true, margins: { … } }). - Saglabā rezultātu
app.getPath('downloads')kāMeeting-Report-<timestamp>.pdf.
Pievilcība ir acīmredzama. PDF bibliotēka liek tev visu novietot ar roku — zīmēt tekstu pie (x, y), izmērīt to, virzīt kursoru, pašam pārvaldīt lapu pārtraukumus. Pārlūks jau dara to visu: tu raksti <div>-us un CSS, un Chromium izkārtojuma dzinējs sadala lapās tavā vietā. Tu nemācies zīmēšanas API; tu raksti tīmekļa lapu. Dokumentam, kas galvenokārt ir stilizēts teksts un iegulti attēli, tas ir milzīgs īsceļš.
Āķis ir tas, ka tu arī mantoji pārlūku ar visiem tā robežgadījumiem. Trīs no tiem parādījās pirmo četru dienu laikā.
Baltais zibsnis. Slēpts logs uz macOS nav uzticami slēpts. Pirmie mēģinājumi to novietoja ārpus ekrāna (x: screenWidth + 1000), un tas joprojām uz vienu kadru pirms drukāšanas iemirdzēja baltu taisnstūri ekrānā. Labojums, pēc trim mēģinājumiem, bija pārstāt paļauties uz pozīciju vai show: false un iestatīt opacity: 0 — pilnīgi caurspīdīgs logs nezibsnī pat tad, kad kompozitors to īsi parāda. Komits to saka tieši: "show:false with off-screen positioning still flashes on macOS."
URL garuma ierobežojums. Pirmā versija iekļāva visu HTML — ekrānuzņēmumus un visu — kā data: URL. Tas darbojas, līdz sanāksmē ir daži base64 kodēti ekrānuzņēmumi, un tajā brīdī URL aizpeld garām Chromium ~2 MB data-URL ierobežojumam un ielāde tieši neizdodas ar ERR_INVALID_URL. Tāpēc 2. solis augstāk ieraksta pagaidu failu un izmanto loadFile(), nevis ielādē data URL: faila ceļam nav garuma ierobežojuma. (Šie ~2 MB ir vienīgais skaitlis visā šajā stāstā, un tas ir novērtējums koda komentārā, nevis kaut kas, ko kāds etalonēja — uztver to kā "pietiekami liels, lai trāpītu," nevis precīzu slieksni.)
Neekranētais teksts. Transkripta teksts ir patvaļīgs lietotāja saturs, un tas tika iemests tieši HTML virknē. Jebkurš transkripts, kas gadījās saturēt kaut ko līdzīgu <div> vai <script>, salauztu lapu vai vēl ļaunāk. Tāpēc apstrādātājam izauga escapeHtml() caurlaide pār katru virkni, ko tas interpolē. Brīdī, kad tavs PDF ir tīmekļa lapa, HTML injekcija ir arī tava problēma.
Tukšās kastes, kas nebija tukšas
Tagad galvenā kļūda, jo tā ir labākā. Lietotāji ziņoja par eksportētiem PDF ar tukšām kastēm — ierakstiem, kas rādīja laika zīmogu un nozīmīti, bet nekādu ekrānuzņēmumu, tikai tukšu vietu, kur vajadzētu būt attēlam. Acīmredzamais lasījums ir "eksports nomet attēla datus," un acīmredzamais labojums ir "izfiltrēt tukšos ierakstus." Komanda šo filtru pievienoja — vairāk par to pēc mirkļa — bet tas nebija īstais cēlonis.
Īstais cēlonis bija sadalīšana lapās. Katrs ieraksts ir .item konteiners, kas satur laika zīmogu, nozīmīti un attēlu. Chromium, drukājot PDF, labprāt pārtrauca lapu ieraksta vidū — atveidojot laika zīmogu un nozīmīti vienas lapas apakšā un iestumjot attēlu nākamajā. Tas, kas izskatījās kā tukša kaste, bija ieraksta augšējā puse, kura attēls bija pārplūdis nākamajā lapā. Dati visi bija klāt; izkārtojums tos bija giljotinējis. Komits ir atklāts par nepareizo diagnozi: "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."
Labojums ir viena CSS rindiņa:
.item {
break-inside: avoid;
}
break-inside: avoid liek izkārtojuma dzinējam saglabāt ierakstu veselu — ja tas neietilpst atlikušajā vietā lapā, pārvieto visu ierakstu uz nākamo lapu, nevis to sadala. Šī ir slēptā pārlūka arhitektūras atmaksa miniatūrā: neglītākā, visvairāk ziņotā kļūda funkcijā tika labota nevis ar lapas augstuma aritmētiku un manuāliem addPage() izsaukumiem, bet ar vienu CSS īpašību, ko pārlūks jau prata ievērot. Tu neuzrakstīji lapotāju; tu palūdzi tam, ko mantoji, uzvesties.
Divi patiešām tukšo filtrēšanas slāņi
Tur bija arī patiešām tukši ieraksti — tikai ne tie, kas radīja kastes. Tukši dublikāti no ātra dubultā Cmd+Enter, ekrānuzņēmumi bez priekšskatījuma, transkripta ieraksti, kas bija tikai tukšumzīmes. Šie tika izfiltrēti, un interesanti, ka tie tika izfiltrēti divreiz, divos slāņos:
- Apstrādātājā
buildReportHtml()sargā katru ierakstu —if (!item.content?.trim()) return ''tekstam, un attēliemconst safeSrc = item.preview?.startsWith('data:image/') ? item.preview : ''; if (!safeSrc) return ''— tad.filter(Boolean)nomet tukšos. Šī pēdējā pārbaude pilda dubultu pienākumu: tā ir derīguma vārti un neliela drošības pārbaude, ka lapu sasniedz tikai īsti attēla dati. - Laika joslas avotā, renderētājā, tas pats tukšums tiek izfiltrēts pirms ieraksts vispār tiek pievienots —
if (!entry.text?.trim()) return,if (!screenshot.preview) return.
Jaukā detaļa vēsturē ir metode. Pirmais komits pievienoja logger.debug('PDF timeline item', …) ciklu tieši "for debugging empty entries." Kad šī žurnālēšana atklāja, no kurienes tukšie patiesībā nāca — ieskaitot atsevišķu ekrānuzņēmumu deduplikācijas kļūdu, kur ātri tvērumi radīja tukšus dublikātus — sekojošais komits izlaboja pamatcēloni augšpusē un noņēma atkļūdošanas žurnālēšanu. Pievieno instrumentāciju, lai atrastu cēloni, izlabo cēloni, izdzēs instrumentāciju. Izmaiņu žurnāla punkts "filtered empty entries" klusi satur visu šo pievienot-tad-noņemt loku.
v1.8.12: "excludes AI responses," lasi diff
Trīs laidienus vēlāk vēl viena izmaiņa — un tā ir laba mācība koda lasīšanā izmaiņu žurnāla vietā. v1.8.12 piezīme saka, ka PDF eksports tagad "excludes AI responses for cleaner meeting records." Tiesa, bet nepilnīgi.
Eksporta laika josla katru ierakstu apzīmē ar type: 'transcript' | 'chat_user' | 'chat_assistant' | 'screenshot'. Runāta sanāksmes rindiņa ir transcript; jautājums, ko tu ieraksti asistenta panelī, ir chat_user; AI atbilde ir chat_assistant. v1.8.12 izmaiņa ir deviņu rindiņu labojums vienā renderētāja failā, un noteicošā rindiņa ir filtrs, kas novietots tieši pirms eksporta kravas uzbūvēšanas:
timeline.filter((item) => item.type === 'transcript' || item.type === 'screenshot')
Tas ir atļauto saraksts, nevis bloķēto saraksts. Tas nenoņem chat_assistant ierakstus; tas patur tikai transcript un screenshot un nomet visu pārējo — kas nozīmē, ka tas noņem lietotāja paša chat_user jautājumus līdz ar AI atbildēm. Komita ķermenis ir precīzāks par izmaiņu žurnālu: "PDF export keeps only transcripts and screenshots, no chat messages." Eksportētais ieraksts tagad ir tīri sanāksme — kas tika teikts un kas bija ekrānā — ar visu asistenta sānu paneļa sarunu no tā izņemtu.
Un tas produkta ziņā ir pareizais lēmums. GeekBye čats ir privāts asistents, ar ko konsultējies zvana laikā — pajautā tam par ekrānuzņēmumu, saņem ātru skaidrojumu. Šīs apmaiņas ir tavs skiču bloks, nevis sanāksmes daļa. Sanāksmes ierakstam, ko ar kādu dalītos, vajadzētu saturēt sanāksmi, nevis tavas blakussarunas ar AI par to. Atļauto saraksts vienkārši padara "sanāksmi" par to, kas tiek eksportēts, burtisko definīciju.
Trīs lietas, ko šis laidiens mums iemācīja
- Pārlūks ir PDF renderētājs, kas tev jau ir. Ja tavs dokuments ir strukturēts teksts un attēli,
printToPDFpār slēptuBrowserWindowbez maksas dod tev īstu sadalīšanu lapās, īstu fontu atveidošanu un CSS izkārtojumu — nekāda PDF zīmēšanas API. Tu maini bibliotēkas atkarību pret pārlūka robežgadījumiem, kas bieži ir labākais darījums. - Kļūda, ko tu redzi, un kļūda, kas eksistē, ne vienmēr ir viena un tā pati. "Tukšās kastes" kliedza trūkstoši dati; cēlonis bija sadalīts konteiners. Instrumentē, pirms pieņem — komandas atkļūdošanas žurnālēšana ir tā, kas pārvērta ticamu-bet-nepareizu labojumu (filtrēt tukšos) īstajā (
break-inside: avoid). - Izmaiņu žurnāls ir kopsavilkums; filtrs ir patiesība. "Excludes AI responses" ir atļauto saraksts, kas nomet arī tavus paša čata jautājumus. Kad laidiena piezīme apraksta uzvedības izmaiņu, diff pateiks tev tās precīzās malas — un šeit precīzā mala ir "izdzīvo tikai sanāksme."
Iepriekšējai nodaļai v1 stāstā — no OCR līdz pikseļiem, nezaudējot rezerves ceļu (v1.8.6–v1.8.7); un visai lokai — programmatūras piegādes līdz pilnībai anatomiju.