
Bir Toplantıyı PDF Kütüphanesi Olmadan PDF'e Yazdırmak
GeekBye bir toplantıyı PDF olarak dışa aktarır, ve kodun hiçbir yerinde bir PDF kütüphanesi yoktur. HTML'i görünmez bir tarayıcı penceresinde render eder ve onu yazdırır. O tercih bütün hikâyedir: özelliği inşa etmeyi kolaylaştırdı ve ona gerçek bir tarayıcının sahip olduğu her arızayı verdi — bir beyaz parlama, bir URL uzunluğu sınırı, ve screenshot'ları ikiye bölen bir sayfa sonu. En çirkin bug'ın çözümü bir satır CSS'ti.
GeekBye'ın "bu toplantıyı PDF olarak dışa aktar" düğmesinde saklı tatmin edici bir numara var: arkasında bir PDF kütüphanesi yok. jsPDF yok, pdfmake yok, pdfkit yok, headless puppeteer yok. Bütün özellik gizli bir tarayıcı. Uygulama toplantıyı betimleyen bir HTML sayfası inşa eder — transcript girdileri, screenshot'lar, zaman damgaları — onu görünmez bir Electron penceresine yükler, ve o pencereden kendini PDF'e yazdırmasını ister. webContents.printToPDF gerisini halleder. O tek mimari tercih bu sürümün bütün hikâyesidir, çünkü özelliği inşa etmeyi ucuzlattı ve ona gerçek bir tarayıcının sahip olduğu her bug'ı devretti.
Öncesi aynı sürüm
Genellikle bu yazılar "işte eskiden nasıl çalışıyordu" diye açılır. Bu sefer bir öncesi yok. v1.8.9'un changelog'u bakım gibi okunur — "fixed page breaks splitting screenshots and filtered empty entries" — sanki PDF dışa aktarma yama alan mevcut bir özellikmiş gibi. Öyle değildi. Onu tanıtan ilk commit, feat: add PDF export via Electron printToPDF, aynı sürüm döngüsünün üç gün içinde iner, ve changelog'daki her "fix" saatlik yaşta olan kod üzerinde aynı-hafta yinelemesidir. Özellik doğdu, kırıldı, ve sertleştirildi, hepsi tek bir etiket içinde. Bunu yüksek sesle söylemeye değer, çünkü çok fazla sevkiyatın dürüst şeklidir bu: sürüm notlarındaki cilalı madde ve o şeyin dağınık doğumu sıklıkla aynı sürümdür.
Neden hiç gizli bir tarayıcı
Mekanizmayı bug'lardan önce anlamaya değer, çünkü bug'lar hepsi ondan türer. Her şey tek bir ana-süreç dosyasında yaşar, electron/ipcHandlers/pdfExportHandler.ts. Renderer 'pdf:export-report' IPC'sini ateşlediğinde, handler:
- Toplantının zaman çizelgesinden tam bir HTML string'i toplamak için
buildReportHtml()'i çağırır — transcript girdisi ya da screenshot başına bir.itembloğu. - O HTML'i
os.tmpdir()içinde geçici bir dosyaya yazar. - Dosyayı gizli bir pencereye yükler —
new BrowserWindow({ show: false, opacity: 0, focusable: false, skipTaskbar: true }). - Görüntülerin yüklenmesini bekler, sonra
win.webContents.printToPDF({ printBackground: true, margins: { … } })'i çağırır. - Sonucu
app.getPath('downloads')'aMeeting-Report-<timestamp>.pdfolarak kaydeder.
Cazibesi bariz. Bir PDF kütüphanesi her şeyi elle konumlandırmanı sağlar — metni (x, y)'de çiz, ölç, imleci ilerlet, sayfa sonlarını kendin yönet. Bir tarayıcı bunların hepsini zaten yapar: <div>'ler ve CSS yazarsın, ve Chromium'un düzen motoru senin için sayfalar. Bir çizim API'si öğrenmiyorsun; bir web sayfası yazıyorsun. Çoğunlukla stilli metin ve gömülü görüntüler olan bir belge için bu devasa bir kısayol.
İşin cılkı şu ki bir tarayıcıyı da miras aldın, bütün uç durumlarıyla. Bunlardan üçü ilk dört gün içinde ortaya çıktı.
Beyaz parlama. macOS'ta gizli bir pencere güvenilir biçimde gizli değildir. İlk denemeler onu ekran dışına konumlandırdı (x: screenWidth + 1000), ve yine de yazdırmadan önce bir kare boyunca ekranda beyaz bir dikdörtgen parladı. Çözüm, üç denemeden sonra, konuma ya da show: false'a güvenmeyi bırakıp opacity: 0 ayarlamaktı — tamamen saydam bir pencere, compositor onu kısaca gösterse bile parlamaz. Commit bunu açıkça söyler: "show:false with off-screen positioning still flashes on macOS."
URL uzunluk sınırı. İlk sürüm bütün HTML'i — screenshot'lar dâhil — bir data: URL olarak inline etti. Bu, bir toplantının içinde birkaç base64 kodlu screenshot olana dek çalışır, o noktada URL Chromium'un ~2 MB data-URL sınırını aşıp geçer ve yükleme ERR_INVALID_URL ile doğrudan başarısız olur. İşte bu yüzden yukarıdaki 2. adım geçici bir dosya yazar ve bir data URL yüklemek yerine loadFile() yapar: bir dosya yolunun uzunluk sınırı yoktur. (O ~2 MB bütün bu hikâyedeki tek sayı, ve bir kod yorumundaki inline bir tahmin, kimsenin kıyasladığı bir şey değil — onu "çarpacak kadar büyük" olarak ele al, kesin bir eşik olarak değil.)
Kaçış yapılmamış metin. Transcript metni gelişigüzel kullanıcı içeriğidir, ve doğrudan HTML string'inin içine bırakılıyordu. <div> ya da <script> gibi bir şey içermesi denk gelen herhangi bir transcript sayfayı kırar ya da daha kötüsünü yapardı. Bu yüzden handler, interpolate ettiği her string üzerinde bir escapeHtml() geçişi kazandı. PDF'in bir web sayfası olduğu an, HTML enjeksiyonu da senin problemin.
Boş olmayan boş kutular
Şimdi manşet bug, çünkü en iyisi bu. Kullanıcılar boş kutular içeren dışa aktarılmış PDF'ler bildirdi — bir zaman damgası ve bir rozet gösteren ama hiç screenshot göstermeyen girdiler, sadece görüntünün olması gereken yerde boş alan. Bariz okuma "dışa aktarma görüntü verisini düşürüyor," ve bariz çözüm "boş girdileri filtrele." Ekip o filtreyi gerçekten ekledi — bir saniye içinde onun hakkında daha fazlası — ama gerçek neden o değildi.
Gerçek neden sayfalamaydı. Her girdi bir zaman damgası, bir rozet ve bir görüntü tutan bir .item konteyneridir. Chromium, PDF'e yazdırırken, bir sayfayı bir öğenin ortasında kırmakta beis görmüyordu — zaman damgası ve rozeti bir sayfanın altında render edip görüntüyü sonrakine itiyordu. Boş bir kutuya benzeyen şey, görüntüsü sonraki sayfaya taşmış bir öğenin üst yarısıydı. Verinin hepsi oradaydı; düzen onu giyotinle kesmişti. Commit yanlış teşhis konusunda açık sözlü: "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."
Çözüm bir satır CSS:
.item {
break-inside: avoid;
}
break-inside: avoid düzen motoruna bir öğeyi bütün tutmasını söyler — bir sayfada kalan alana sığmayacaksa, onu bölmek yerine tüm öğeyi sonraki sayfaya taşı. Bu, gizli-tarayıcı mimarisinin ödülünün minyatürde ifade edilmiş hâlidir: özellikteki en çirkin, en çok bildirilen bug, sayfa-yüksekliği aritmetiği ve elle addPage() çağrılarıyla değil, tarayıcının zaten onurlandırmayı bildiği tek bir CSS özelliğiyle çözüldü. Bir sayfalayıcı yazmadın; miras aldığından uslu durmasını istedin.
Gerçekten-boş olanı filtrelemenin iki katmanı
Gerçekten boş girdiler de vardı — sadece kutulara neden olanlar değil. Hızlı bir çift Cmd+Enter'dan gelen boş kopyalar, önizlemesi olmayan screenshot'lar, yalnızca boşluktan ibaret transcript girdileri. Bunlar filtrelendi, ve ilginç biçimde iki kez, iki katmanda filtrelendi:
- Handler'da,
buildReportHtml()her öğeyi korur — metin içinif (!item.content?.trim()) return '', ve görüntüler içinconst safeSrc = item.preview?.startsWith('data:image/') ? item.preview : ''; if (!safeSrc) return ''— sonra.filter(Boolean)boşları düşürür. O son kontrol çifte iş görür: bir geçerlilik kapısı ve sayfaya yalnızca gerçek görüntü verisinin ulaşmasını sağlayan küçük bir güvenlik kontrolü. - Zaman çizelgesi kaynağında, renderer'da, aynı boşluk bir öğe eklenmeden önce filtrelenir —
if (!entry.text?.trim()) return,if (!screenshot.preview) return.
Tarihçedeki güzel detay yöntemdir. İlk commit açıkça "for debugging empty entries" diyerek bir logger.debug('PDF timeline item', …) döngüsü ekledi. O günlük boşlukların gerçekte nereden geldiğini ortaya çıkardığında — hızlı yakalamaların boş kopyalar ürettiği ayrı bir screenshot-tekilleştirme bug'ı dâhil — takip commit'i kök nedeni yukarı akışta düzeltti ve hata ayıklama günlüğünü kaldırdı. Nedeni bulmak için ölçüm ekle, nedeni düzelt, ölçümü sil. Changelog maddesi "filtered empty entries" o bütün ekle-sonra-kaldır yayını sessizce içerir.
v1.8.12: "excludes AI responses," diff'i oku
Üç sürüm sonra, bir değişiklik daha — ve changelog yerine kodu okumak konusunda güzel bir ders. v1.8.12 notu PDF dışa aktarmanın artık "excludes AI responses for cleaner meeting records." dediğini söyler. Doğru, ama eksik.
Dışa aktarma zaman çizelgesi her öğeyi bir type ile etiketler: 'transcript' | 'chat_user' | 'chat_assistant' | 'screenshot'. Konuşulan bir toplantı satırı bir transcript'tir; asistan paneline yazdığın bir soru bir chat_user'dır; AI'ın cevabı bir chat_assistant'tır. v1.8.12 değişikliği tek bir renderer dosyasına dokuz satırlık bir düzenlemedir, ve işlevsel satır, dışa aktarma yükü inşa edilmeden hemen önce yerleştirilmiş bir filtredir:
timeline.filter((item) => item.type === 'transcript' || item.type === 'screenshot')
Bu bir allowlist'tir, bir blocklist değil. chat_assistant öğelerini kaldırmaz; yalnızca transcript ve screenshot'ı tutar ve geri kalan her şeyi düşürür — ki bu, AI'ın cevaplarıyla birlikte kullanıcının kendi chat_user sorularını da çıkardığı anlamına gelir. Commit gövdesi changelog'dan daha doğrudur: "PDF export keeps only transcripts and screenshots, no chat messages." Dışa aktarılan kayıt artık tamamen toplantıdır — ne söylendiği ve ekranda ne olduğu — asistan yan panelindeki bütün konuşma ondan çıkarılmış hâlde.
Ve bu, ürün açısından doğru karar. GeekBye'ın sohbeti bir aramanın sırasında danıştığın özel bir asistandır — ona bir screenshot hakkında sor, hızlı bir açıklama al. O alışverişler senin müsvedde defterindir, toplantının parçası değil. Biriyle paylaşacağın bir toplantı kaydı toplantıyı içermeli, onun hakkında bir AI ile yan konuşmalarını değil. Allowlist yalnızca "toplantı"yı dışa aktarılanın gerçek tanımı yapar.
Bu sürümün bize öğrettiği üç şey
- Bir tarayıcı, zaten sahip olduğun bir PDF renderer'ıdır. Belgen yapılandırılmış metin ve görüntülerse, gizli bir
BrowserWindowüzerindenprintToPDFsana gerçek sayfalama, gerçek font render'ı, ve CSS düzeni bedava verir — hiç PDF çizim API'si yok. Bir kütüphane bağımlılığını tarayıcı uç durumlarıyla takas edersin, ki bu çoğu zaman daha iyi takastır. - Görebildiğin bug ile var olan bug her zaman aynı değildir. "Boş kutular" eksik veri diye bağırıyordu; neden bir bölünmüş konteyner'di. Varsaymadan önce ölçümle — ekibin hata ayıklama günlüğü, makul-ama-yanlış bir çözümü (boşları filtrele) gerçek olana (
break-inside: avoid) çeviren şeydir. - Changelog bir özettir; filtre gerçektir. "Excludes AI responses" senin kendi sohbet sorularını da düşüren bir allowlist'tir. Bir sürüm notu bir davranış değişikliğini betimlediğinde, diff sana onun tam kenarlarını söyleyecektir — ve burada tam kenar "yalnızca toplantı hayatta kalır."
v1 hikâyesinin bir önceki bölümü için, OCR'dan piksellere, fallback'i kaybetmeden (v1.8.6–v1.8.7); ve tüm yay için, yazılımı kusursuzluğa kadar sevk etmenin anatomisi.