
Mencetak Mesyuarat ke PDF Tanpa Pustaka PDF
GeekBye mengeksport sebuah mesyuarat sebagai PDF, dan tiada pustaka PDF di mana-mana dalam kodnya. Ia me-render HTML dalam satu tetingkap pelayar yang tidak kelihatan dan mencetaknya. Pilihan itu ialah keseluruhan ceritanya: ia menjadikan ciri ini mudah dibina dan memberinya setiap kegagalan yang dimiliki sebuah pelayar sebenar — satu kilatan putih, satu had panjang URL, dan satu page break yang memotong screenshot menjadi dua. Penyelesaian untuk bug paling hodoh ialah satu baris CSS.
Ada satu helah yang memuaskan tersembunyi di butang "eksport mesyuarat ini sebagai PDF" milik GeekBye: tiada pustaka PDF di belakangnya. Tiada jsPDF, tiada pdfmake, tiada pdfkit, tiada puppeteer headless. Keseluruhan ciri ini ialah sebuah pelayar tersembunyi. Aplikasi membina satu halaman HTML yang menggambarkan mesyuarat itu — entri transkrip, screenshot, timestamp — memuatkannya ke dalam satu tetingkap Electron yang tidak kelihatan, dan meminta tetingkap itu mencetak dirinya sendiri ke PDF. webContents.printToPDF melakukan selebihnya. Satu pilihan seni bina itu ialah keseluruhan cerita keluaran ini, kerana ia menjadikan ciri ini murah dibina dan menyerahkan kepadanya setiap bug yang dimiliki sebuah pelayar sebenar.
Sebelumnya ialah keluaran yang sama
Biasanya tulisan-tulisan ini dibuka dengan "beginilah cara ia berfungsi dahulu." Kali ini tiada sebelumnya. Changelog untuk v1.8.9 terbaca seperti penyelenggaraan — "fixed page breaks splitting screenshots and filtered empty entries" — seolah-olah eksport PDF ialah satu ciri sedia ada yang sedang ditampal. Ia bukan. Commit pertama yang memperkenalkannya, feat: add PDF export via Electron printToPDF, mendarat tiga hari ke dalam kitaran keluaran yang sama, dan setiap "fix" dalam changelog ialah lelaran minggu-yang-sama pada kod yang baru berumur jam. Ciri ini lahir, patah, dan diperkukuh, semuanya di dalam satu tag. Itu berbaloi disebut dengan kuat kerana itulah bentuk jujur bagi banyak perkara yang dikeluarkan: butir yang digilap dalam nota keluaran dan kelahiran bersepah bagi benda itu selalunya ialah keluaran yang sama.
Kenapa perlu sebuah pelayar tersembunyi langsung
Mekanismenya berbaloi difahami sebelum bug-bugnya, kerana bug-bug itu semuanya turun daripadanya. Segala-galanya hidup dalam satu fail proses-utama, electron/ipcHandlers/pdfExportHandler.ts. Apabila renderer menembak IPC 'pdf:export-report', handler:
- Memanggil
buildReportHtml()untuk memasang satu string HTML lengkap daripada timeline mesyuarat — satu blok.itembagi setiap entri transkrip atau screenshot. - Menulis HTML itu ke satu fail sementara di
os.tmpdir(). - Memuatkan fail itu ke dalam satu tetingkap tersembunyi —
new BrowserWindow({ show: false, opacity: 0, focusable: false, skipTaskbar: true }). - Menunggu imej-imej termuat, kemudian memanggil
win.webContents.printToPDF({ printBackground: true, margins: { … } }). - Menyimpan hasilnya ke
app.getPath('downloads')sebagaiMeeting-Report-<timestamp>.pdf.
Tarikannya jelas. Sebuah pustaka PDF membuatkan anda meletakkan segala-galanya dengan tangan — lukis teks pada (x, y), ukur ia, majukan kursor, urus sendiri page break-nya. Sebuah pelayar sudah melakukan semua itu: anda menulis <div> dan CSS, dan enjin susun atur Chromium menghalamankan untuk anda. Anda tidak sedang mempelajari satu API lukisan; anda sedang menulis satu halaman web. Bagi satu dokumen yang kebanyakannya teks bergaya dan imej terbenam, itu satu jalan pintas yang besar.
Tangkapannya ialah anda juga mewarisi sebuah pelayar, dengan segala kes tepinya. Tiga daripadanya muncul dalam empat hari pertama.
Kilatan putih. Sebuah tetingkap tersembunyi di macOS tidak tersembunyi secara boleh dipercayai. Percubaan-percubaan pertama meletakkannya di luar skrin (x: screenWidth + 1000), dan ia masih mengilatkan satu segi empat putih pada skrin selama satu frame sebelum mencetak. Penyelesaiannya, selepas tiga percubaan, ialah berhenti bersandar pada kedudukan atau show: false dan menetapkan opacity: 0 — sebuah tetingkap yang sepenuhnya lutsinar tidak berkilat walaupun ketika compositor menunjukkannya seketika. Commit-nya mengatakannya dengan jelas: "show:false with off-screen positioning still flashes on macOS."
Had panjang URL. Versi pertama membenamkan keseluruhan HTML — screenshot dan segalanya — sebagai satu URL data:. Itu berfungsi sehingga sebuah mesyuarat mempunyai beberapa screenshot berkod base64 di dalamnya, pada titik mana URL itu belayar melepasi had data-URL ~2 MB milik Chromium dan pemuatan gagal terus dengan ERR_INVALID_URL. Itulah sebabnya langkah 2 di atas menulis satu fail sementara dan melakukan loadFile() dan bukan memuatkan satu data URL: satu laluan fail tidak mempunyai had panjang. (Angka ~2 MB itu ialah satu-satunya nombor dalam keseluruhan cerita ini, dan ia ialah satu anggaran inline dalam satu komen kod, bukan sesuatu yang sesiapa pun telah benchmark — anggap ia sebagai "cukup besar untuk dilanggar," bukan satu ambang yang tepat.)
Teks tanpa escape. Teks transkrip ialah kandungan pengguna yang sewenang-wenangnya, dan ia sedang dijatuhkan terus ke dalam string HTML. Mana-mana transkrip yang kebetulan mengandungi sesuatu seperti <div> atau <script> akan memecahkan halaman atau lebih teruk. Jadi handler menumbuhkan satu laluan escapeHtml() ke atas setiap string yang ia interpolasi. Pada saat PDF anda ialah satu halaman web, suntikan HTML juga ialah masalah anda.
Kotak kosong yang tidak kosong
Kini bug utamanya, kerana ia yang terbaik. Pengguna melaporkan PDF yang dieksport dengan kotak kosong — entri yang menunjukkan satu timestamp dan satu lencana tetapi tiada screenshot, hanya ruang kosong di tempat imej sepatutnya berada. Bacaan yang jelas ialah "eksport sedang menjatuhkan data imej," dan penyelesaian yang jelas ialah "tapis entri yang kosong." Pasukan memang menambah penapis itu — lebih lanjut mengenainya sebentar lagi — tetapi ia bukan punca sebenar.
Punca sebenar ialah penghalamanan. Setiap entri ialah satu kontena .item yang memegang satu timestamp, satu lencana, dan satu imej. Chromium, mencetak ke PDF, dengan senang hati memecah satu halaman di tengah-tengah satu item — me-render timestamp dan lencana di bawah satu halaman dan menolak imej ke halaman berikutnya. Apa yang kelihatan seperti satu kotak kosong ialah separuh atas satu item yang imejnya telah melimpah ke halaman berikutnya. Datanya semua ada di sana; susun atur telah menggilotinnya. Commit-nya jujur tentang salah diagnosisnya: "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."
Penyelesaiannya ialah satu baris CSS:
.item {
break-inside: avoid;
}
break-inside: avoid memberitahu enjin susun atur untuk mengekalkan satu item utuh — jika ia tidak muat dalam ruang yang tinggal pada satu halaman, alihkan seluruh item ke halaman berikutnya dan bukan membelahnya. Ini ialah ganjaran seni bina pelayar-tersembunyi yang dinyatakan dalam miniatur: bug paling hodoh dan paling banyak dilaporkan dalam ciri ini diselesaikan bukan dengan aritmetik tinggi-halaman dan panggilan addPage() manual, tetapi dengan satu sifat CSS yang pelayar sudah tahu cara menghormatinya. Anda tidak menulis satu penghalaman; anda meminta yang anda warisi untuk berkelakuan.
Dua lapisan penapisan benda yang betul-betul-kosong
Memang ada entri yang benar-benar kosong juga — cuma bukan yang menyebabkan kotak-kotaknya. Pendua kosong daripada satu Cmd+Enter berganda yang pantas, screenshot tanpa pratonton, entri transkrip yang hanya ruang kosong. Ini ditapis, dan menariknya ia ditapis dua kali, di dua lapisan:
- Di handler,
buildReportHtml()mengawal setiap item —if (!item.content?.trim()) return ''untuk teks, dan untuk imejconst safeSrc = item.preview?.startsWith('data:image/') ? item.preview : ''; if (!safeSrc) return ''— kemudian.filter(Boolean)menjatuhkan yang kosong. Pemeriksaan terakhir itu bekerja dua kali ganda: ia ialah satu pintu kesahihan dan satu pemeriksaan keselamatan kecil bahawa hanya data imej sebenar yang pernah mencapai halaman. - Di sumber timeline, di renderer, kekosongan yang sama ditapis sebelum satu item pernah ditambah —
if (!entry.text?.trim()) return,if (!screenshot.preview) return.
Butiran yang bagus dalam sejarahnya ialah kaedahnya. Commit pertama menambah satu gelung logger.debug('PDF timeline item', …) secara eksplisit "for debugging empty entries." Sebaik sahaja logging itu mendedahkan dari mana kekosongan itu sebenarnya datang — termasuk satu bug penyahduaan-screenshot berasingan di mana tangkapan pantas menghasilkan pendua kosong — commit susulan membetulkan punca akar di hulu dan memadamkan logging nyahpepijat. Tambah instrumentasi untuk mencari punca, betulkan punca, padam instrumentasi. Butir changelog "filtered empty entries" secara senyap memuatkan keseluruhan busur tambah-kemudian-buang itu.
v1.8.12: "excludes AI responses," baca diff-nya
Tiga keluaran kemudian, satu perubahan lagi — dan ia satu pengajaran yang baik dalam membaca kod dan bukan changelog. Nota v1.8.12 berkata eksport PDF kini "excludes AI responses for cleaner meeting records." Benar, tetapi tidak lengkap.
Timeline eksport menandakan setiap item dengan satu type: 'transcript' | 'chat_user' | 'chat_assistant' | 'screenshot'. Satu baris mesyuarat yang dituturkan ialah satu transcript; satu soalan yang anda taip ke panel pembantu ialah satu chat_user; jawapan AI ialah satu chat_assistant. Perubahan v1.8.12 ialah satu suntingan sembilan-baris pada satu fail renderer, dan baris operatifnya ialah satu penapis yang diletakkan tepat sebelum payload eksport dibina:
timeline.filter((item) => item.type === 'transcript' || item.type === 'screenshot')
Itu ialah satu allowlist, bukan satu blocklist. Ia tidak membuang item chat_assistant; ia hanya menyimpan transcript dan screenshot dan menjatuhkan segala yang lain — yang bermaksud ia membuang soalan chat_user milik pengguna sendiri bersama jawapan AI. Badan commit-nya lebih tepat daripada changelog: "PDF export keeps only transcripts and screenshots, no chat messages." Rekod yang dieksport kini semata-mata mesyuarat — apa yang dikatakan dan apa yang ada pada skrin — dengan keseluruhan perbualan panel sisi pembantu dibuang daripadanya.
Dan itu keputusan yang betul, dari segi produk. Chat GeekBye ialah satu pembantu peribadi yang anda rujuk semasa satu panggilan — tanya ia tentang satu screenshot, dapatkan satu penjelasan pantas. Pertukaran-pertukaran itu ialah kertas conteng anda, bukan sebahagian daripada mesyuarat. Satu rekod mesyuarat yang anda akan kongsi dengan seseorang sepatutnya mengandungi mesyuaratnya, bukan perbualan sampingan anda dengan sebuah AI mengenainya. Allowlist itu sekadar menjadikan "mesyuarat" sebagai takrifan literal bagi apa yang dieksport.
Tiga perkara yang diajarkan keluaran ini kepada kami
- Sebuah pelayar ialah renderer PDF yang anda sudah miliki. Jika dokumen anda ialah teks berstruktur dan imej,
printToPDFke atas satuBrowserWindowtersembunyi memberi anda penghalamanan sebenar, render fon sebenar, dan susun atur CSS secara percuma — tiada API lukisan PDF. Anda menukar satu kebergantungan pustaka dengan kes tepi pelayar, yang selalunya ialah pertukaran yang lebih baik. - Bug yang anda boleh lihat dan bug yang wujud tidak selalu sama. "Kotak kosong" menjerit data hilang; puncanya ialah satu kontena terbelah. Instrumen sebelum anda menganggap — logging nyahpepijat pasukan ialah yang menukar satu penyelesaian yang munasabah-tetapi-salah (tapis yang kosong) kepada yang sebenar (
break-inside: avoid). - Changelog ialah satu ringkasan; penapis ialah kebenarannya. "Excludes AI responses" ialah satu allowlist yang turut menjatuhkan soalan chat anda sendiri. Apabila satu nota keluaran menggambarkan satu perubahan tingkah laku, diff akan memberitahu anda tepi-tepinya yang tepat — dan di sini tepi tepatnya ialah "hanya mesyuarat yang terselamat."
Untuk bab sebelumnya dalam kisah v1, daripada OCR ke piksel tanpa kehilangan fallback (v1.8.6–v1.8.7); dan untuk keseluruhan lengkungnya, anatomi mengeluarkan perisian hingga ke kesempurnaan.