Steven
Steven9 menit baca

Mencetak Rapat ke PDF Tanpa Library PDF

GeekBye mengekspor sebuah rapat sebagai PDF, dan tak ada library PDF di mana pun dalam kodenya. Ia me-render HTML di sebuah jendela browser yang tak terlihat dan mencetaknya. Pilihan itu adalah keseluruhan kisahnya: ia membuat fitur ini mudah dibangun dan memberinya setiap kegagalan yang dimiliki sebuah browser sungguhan — sebuah kilatan putih, sebuah batas panjang URL, dan sebuah page break yang memotong screenshot menjadi dua. Perbaikan untuk bug paling jelek adalah satu baris CSS.

Engineering
Electron
Desktop
GeekBye Releases
Mencetak Rapat ke PDF Tanpa Library PDF

Ada sebuah trik yang memuaskan tersembunyi di tombol "ekspor rapat ini sebagai PDF" milik GeekBye: tak ada library PDF di baliknya. Tak ada jsPDF, tak ada pdfmake, tak ada pdfkit, tak ada puppeteer headless. Seluruh fiturnya adalah sebuah browser tersembunyi. Aplikasi membangun sebuah halaman HTML yang menggambarkan rapat itu — entri transkrip, screenshot, timestamp — memuatnya ke dalam sebuah jendela Electron yang tak terlihat, dan meminta jendela itu untuk mencetak dirinya sendiri ke PDF. webContents.printToPDF mengerjakan sisanya. Satu pilihan arsitektur itu adalah keseluruhan kisah rilis ini, karena ia membuat fitur ini murah dibangun dan menyerahkan padanya setiap bug yang dimiliki sebuah browser sungguhan.

Sebelumnya adalah rilis yang sama

Biasanya tulisan-tulisan ini dibuka dengan "beginilah cara kerjanya dulu." Kali ini tak ada sebelumnya. Changelog untuk v1.8.9 terbaca seperti pemeliharaan — "fixed page breaks splitting screenshots and filtered empty entries" — seolah-olah ekspor PDF adalah sebuah fitur yang sudah ada yang sedang di-patch. Bukan. Commit pertama yang memperkenalkannya, feat: add PDF export via Electron printToPDF, mendarat tiga hari ke dalam siklus rilis yang sama, dan setiap "fix" di changelog adalah iterasi minggu-yang-sama pada kode yang baru berumur jam. Fitur ini lahir, patah, dan diperkuat, semuanya di dalam satu tag. Itu layak dikatakan dengan lantang karena itulah bentuk jujur dari banyak hal yang dirilis: butir yang dipoles di catatan rilis dan kelahiran berantakan dari benda itu seringkali adalah rilis yang sama.

Kenapa harus sebuah browser tersembunyi sama sekali

Mekanismenya layak dipahami sebelum bug-bugnya, karena bug-bug itu semuanya turun darinya. Semuanya hidup di dalam satu file main-process, electron/ipcHandlers/pdfExportHandler.ts. Ketika renderer menembakkan IPC 'pdf:export-report', handler:

  1. Memanggil buildReportHtml() untuk merakit sebuah string HTML lengkap dari timeline rapat — satu blok .item per entri transkrip atau screenshot.
  2. Menulis HTML itu ke sebuah file sementara di os.tmpdir().
  3. Memuat file itu ke dalam sebuah jendela tersembunyi — new BrowserWindow({ show: false, opacity: 0, focusable: false, skipTaskbar: true }).
  4. Menunggu gambar-gambarnya termuat, lalu memanggil win.webContents.printToPDF({ printBackground: true, margins: { … } }).
  5. Menyimpan hasilnya ke app.getPath('downloads') sebagai Meeting-Report-<timestamp>.pdf.

Daya tariknya jelas. Sebuah library PDF membuatmu memosisikan segalanya dengan tangan — gambar teks di (x, y), ukur, majukan kursor, kelola sendiri page break-nya. Sebuah browser sudah melakukan semua itu: kamu menulis <div> dan CSS, dan mesin tata letak Chromium menghalamani untukmu. Kamu tak sedang mempelajari sebuah API gambar; kamu sedang menulis sebuah halaman web. Untuk sebuah dokumen yang sebagian besar adalah teks bergaya dan gambar tertanam, itu adalah sebuah jalan pintas yang besar.

Jebakannya adalah kamu juga mewarisi sebuah browser, dengan semua kasus tepinya. Tiga di antaranya muncul dalam empat hari pertama.

Kilatan putih. Sebuah jendela tersembunyi di macOS tak tersembunyi secara andal. Percobaan-percobaan pertama memosisikannya di luar layar (x: screenWidth + 1000), dan ia tetap mengilatkan sebuah persegi putih di layar selama satu frame sebelum mencetak. Perbaikannya, setelah tiga percobaan, adalah berhenti bersandar pada posisi atau show: false dan menyetel opacity: 0 — sebuah jendela yang sepenuhnya transparan tak berkilat bahkan ketika compositor sekilas menampilkannya. Commit-nya mengatakannya dengan terus terang: "show:false with off-screen positioning still flashes on macOS."

Batas panjang URL. Versi pertama menyematkan seluruh HTML — screenshot dan segalanya — sebagai sebuah URL data:. Itu bekerja sampai sebuah rapat memiliki beberapa screenshot yang dikodekan base64 di dalamnya, di titik mana URL itu berlayar melewati batas data-URL ~2 MB milik Chromium dan pemuatan gagal total dengan ERR_INVALID_URL. Itulah kenapa langkah 2 di atas menulis sebuah file sementara dan melakukan loadFile() alih-alih memuat sebuah data URL: sebuah path file tak punya batas panjang. (Angka ~2 MB itu adalah satu-satunya angka dalam keseluruhan kisah ini, dan ia adalah sebuah estimasi inline dalam sebuah komentar kode, bukan sesuatu yang di-benchmark siapa pun — perlakukan ia sebagai "cukup besar untuk ditabrak," bukan sebuah ambang yang presisi.)

Teks tanpa escape. Teks transkrip adalah konten pengguna yang sembarang, dan ia sedang dijatuhkan langsung ke dalam string HTML. Transkrip apa pun yang kebetulan mengandung sesuatu seperti <div> atau <script> akan memecah halaman atau lebih buruk. Jadi handler menumbuhkan sebuah lintasan escapeHtml() atas setiap string yang ia interpolasi. Saat PDF-mu adalah sebuah halaman web, injeksi HTML juga adalah masalahmu.

Kotak kosong yang tak kosong

Sekarang bug utamanya, karena ia yang terbaik. Pengguna melaporkan PDF yang diekspor dengan kotak kosong — entri yang menampilkan sebuah timestamp dan sebuah badge tapi tanpa screenshot, hanya ruang kosong di tempat gambar seharusnya berada. Pembacaan yang jelas adalah "ekspor menjatuhkan data gambar," dan perbaikan yang jelas adalah "saring entri yang kosong." Tim memang menambahkan filter itu — lebih lanjut soal itu sebentar lagi — tapi itu bukan penyebab sebenarnya.

Penyebab sebenarnya adalah paginasi. Setiap entri adalah sebuah kontainer .item yang menampung sebuah timestamp, sebuah badge, dan sebuah gambar. Chromium, mencetak ke PDF, dengan senang hati memecah sebuah halaman di tengah sebuah item — me-render timestamp dan badge di bawah satu halaman dan mendorong gambar ke halaman berikutnya. Yang tampak seperti sebuah kotak kosong adalah paruh atas sebuah item yang gambarnya telah meluap ke halaman berikutnya. Datanya semua ada di sana; tata letaknya telah menggilotinnya. Commit-nya jujur tentang kesalahan 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."

Perbaikannya adalah satu baris CSS:

.item {
  break-inside: avoid;
}

break-inside: avoid memberi tahu mesin tata letak untuk menjaga sebuah item tetap utuh — jika ia tak muat di ruang tersisa pada sebuah halaman, pindahkan seluruh item ke halaman berikutnya alih-alih membelahnya. Ini adalah imbalan dari arsitektur browser-tersembunyi yang dinyatakan dalam miniatur: bug paling jelek dan paling banyak dilaporkan dalam fitur ini diperbaiki bukan dengan aritmetika tinggi-halaman dan panggilan addPage() manual, tapi dengan satu properti CSS yang sudah tahu bagaimana browser menghormatinya. Kamu tak menulis sebuah paginator; kamu meminta yang kamu warisi untuk berperilaku.

Dua lapis penyaringan hal yang benar-benar-kosong

Memang ada entri yang benar-benar kosong juga — hanya bukan yang menyebabkan kotak-kotaknya. Duplikat kosong dari Cmd+Enter ganda yang cepat, screenshot tanpa preview, entri transkrip yang hanya berupa spasi. Ini disaring, dan menariknya mereka disaring dua kali, di dua lapis:

  • Di handler, buildReportHtml() menjaga setiap item — if (!item.content?.trim()) return '' untuk teks, dan untuk gambar const safeSrc = item.preview?.startsWith('data:image/') ? item.preview : ''; if (!safeSrc) return '' — lalu .filter(Boolean) menjatuhkan yang kosong. Pemeriksaan terakhir itu bekerja ganda: ia adalah sebuah gerbang validitas dan sebuah pemeriksaan keamanan kecil bahwa hanya data gambar sungguhan yang pernah mencapai halaman.
  • Di sumber timeline, di renderer, kekosongan yang sama disaring sebelum sebuah item pernah ditambahkan — if (!entry.text?.trim()) return, if (!screenshot.preview) return.

Detail yang bagus dalam riwayatnya adalah metodenya. Commit pertama menambahkan sebuah loop logger.debug('PDF timeline item', …) secara eksplisit "for debugging empty entries." Begitu logging itu mengungkap dari mana kekosongan itu sebenarnya berasal — termasuk sebuah bug deduplikasi-screenshot terpisah di mana tangkapan cepat menghasilkan duplikat kosong — commit lanjutan memperbaiki akar penyebabnya di hulu dan menghapus logging debug-nya. Tambahkan instrumentasi untuk menemukan penyebab, perbaiki penyebab, hapus instrumentasi. Butir changelog "filtered empty entries" secara diam-diam memuat keseluruhan busur tambah-lalu-hapus itu.

v1.8.12: "excludes AI responses," baca diff-nya

Tiga rilis kemudian, satu perubahan lagi — dan ia adalah pelajaran yang bagus dalam membaca kode alih-alih changelog. Catatan v1.8.12 mengatakan ekspor PDF kini "excludes AI responses for cleaner meeting records." Benar, tapi tak lengkap.

Timeline ekspor menandai setiap item dengan sebuah type: 'transcript' | 'chat_user' | 'chat_assistant' | 'screenshot'. Sebuah baris rapat yang diucapkan adalah sebuah transcript; sebuah pertanyaan yang kamu ketik ke panel asisten adalah sebuah chat_user; jawaban AI adalah sebuah chat_assistant. Perubahan v1.8.12 adalah sebuah suntingan sembilan-baris pada satu file renderer, dan baris operatifnya adalah sebuah filter yang ditempatkan tepat sebelum payload ekspor dibangun:

timeline.filter((item) => item.type === 'transcript' || item.type === 'screenshot')

Itu adalah sebuah allowlist, bukan sebuah blocklist. Ia tak menghapus item chat_assistant; ia hanya menyimpan transcript dan screenshot dan menjatuhkan segala yang lain — yang berarti ia membuang pertanyaan chat_user milik pengguna sendiri bersama jawaban AI. Badan commit-nya lebih akurat daripada changelog: "PDF export keeps only transcripts and screenshots, no chat messages." Catatan yang diekspor kini murni rapat — apa yang dikatakan dan apa yang ada di layar — dengan seluruh percakapan panel samping asisten dihapus darinya.

Dan itu adalah keputusan yang benar, secara produk. Chat GeekBye adalah sebuah asisten pribadi yang kamu konsultasikan selama sebuah panggilan — tanyai ia tentang sebuah screenshot, dapatkan sebuah penjelasan cepat. Pertukaran-pertukaran itu adalah kertas coretmu, bukan bagian dari rapat. Sebuah catatan rapat yang akan kamu bagikan dengan seseorang seharusnya memuat rapatnya, bukan percakapan sampinganmu dengan sebuah AI tentangnya. Allowlist itu sekadar menjadikan "rapat" sebagai definisi harfiah dari apa yang diekspor.

Tiga hal yang diajarkan rilis ini kepada kami

  1. Sebuah browser adalah renderer PDF yang sudah kamu miliki. Jika dokumenmu adalah teks terstruktur dan gambar, printToPDF atas sebuah BrowserWindow tersembunyi memberimu paginasi sungguhan, render font sungguhan, dan tata letak CSS gratis — tanpa API gambar PDF. Kamu menukar sebuah ketergantungan library dengan kasus tepi browser, yang seringkali adalah tukar yang lebih baik.
  2. Bug yang bisa kamu lihat dan bug yang ada tak selalu sama. "Kotak kosong" berteriak data hilang; penyebabnya adalah sebuah kontainer terbelah. Instrumentasi sebelum kamu berasumsi — logging debug tim adalah yang mengubah sebuah perbaikan yang masuk-akal-tapi-salah (saring yang kosong) menjadi yang sebenarnya (break-inside: avoid).
  3. Changelog adalah sebuah ringkasan; filter adalah kebenarannya. "Excludes AI responses" adalah sebuah allowlist yang juga menjatuhkan pertanyaan chat-mu sendiri. Ketika sebuah catatan rilis menggambarkan sebuah perubahan perilaku, diff akan memberi tahu tepiannya yang persis — dan di sini tepi persisnya adalah "hanya rapat yang bertahan."

Untuk bab sebelumnya dalam kisah v1, dari OCR ke piksel tanpa kehilangan fallback (v1.8.6–v1.8.7); dan untuk keseluruhan alurnya, anatomi merilis software sampai sempurna.

Artikel Terkait

Dua Mode Kegagalan Sebuah Overlay Click-Through
Steven
Steven8 menit baca

Dua Mode Kegagalan Sebuah Overlay Click-Through

Jendela GeekBye mengambang di atas segalanya dan membiarkan klikmu menembusnya — kecuali di tempat ia punya tombol. Itu kontrak dua sisi, dan v1.8.5 serta v1.8.14 adalah wujudnya ketika masing-masing sisi patah: satu release di mana overlay menelan sebuah dialog sistem, satu lagi di mana ia mencuri ketikan tombolmu. Perbaikan yang menang untuk yang kedua adalah menghapus kode.

Engineering
Electron
Desktop
Lahir dan Diperbaiki dalam Dua Belas Menit
Steven
Steven7 menit baca

Lahir dan Diperbaiki dalam Dua Belas Menit

Changelog GeekBye v1.8.10 bilang ia memperbaiki sebuah crash saat mengedit pintasan keyboard. Memang begitu — tapi crash itu diperkenalkan dan diperbaiki dalam pull request yang sama, terpaut dua belas menit, dan tak pernah mencapai satu pun pengguna. Kisah sebenarnya adalah kaskade keandalan yang menghasilkannya: sebuah perubahan kecil dan benar tentang kapan pintasan ditangguhkan, dan bug teardown React yang jatuh dari satu baris yang dimaksudkan untuk membuat editor lebih aman.

Engineering
React
Desktop
Dari OCR ke Piksel Tanpa Kehilangan Fallback
Steven
Steven7 menit baca

Dari OCR ke Piksel Tanpa Kehilangan Fallback

GeekBye berhenti membaca screenshot-mu dengan OCR dan mulai mengirim gambar sebenarnya ke model vision — yang merupakan bagian mudahnya. Bagian yang memberi pelajaran adalah optimisasi yang melewati OCR sepenuhnya untuk pengguna vision, diam-diam mematahkan fallback ke model non-vision, dan harus ditarik kembali ke sebuah versi di mana jaring pengaman berjalan paralel dan tak berbiaya apa pun.

Engineering
AI
Desktop