Steven
Steven8 dk okuma

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.

Mühendislik
Electron
Desktop
GeekBye Releases
Bir Toplantıyı PDF Kütüphanesi Olmadan PDF'e Yazdırmak

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:

  1. 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 .item bloğu.
  2. O HTML'i os.tmpdir() içinde geçici bir dosyaya yazar.
  3. Dosyayı gizli bir pencereye yükler — new BrowserWindow({ show: false, opacity: 0, focusable: false, skipTaskbar: true }).
  4. Görüntülerin yüklenmesini bekler, sonra win.webContents.printToPDF({ printBackground: true, margins: { … } })'i çağırır.
  5. Sonucu app.getPath('downloads')'a Meeting-Report-<timestamp>.pdf olarak 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çin if (!item.content?.trim()) return '', ve görüntüler için const 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

  1. 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 üzerinden printToPDF sana 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.
  2. 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.
  3. 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.

İlgili Makaleler

Bir Click-Through Overlay'in İki Başarısızlık Modu
Steven
Steven7 dk okuma

Bir Click-Through Overlay'in İki Başarısızlık Modu

GeekBye'nin penceresi her şeyin üstünde yüzer ve tıklamalarının içinden geçmesine izin verir — düğmelerinin olduğu yer hariç. Bu iki taraflı bir sözleşme, ve v1.8.5 ile v1.8.14, her taraf kırıldığında neye benzediğini gösteriyor: overlay'in bir sistem diyaloğunu yuttuğu bir release, tuş vuruşlarını çaldığı bir başkası. İkincisi için kazanan çözüm, kod silmekti.

Mühendislik
Electron
Desktop
On İki Dakikada Doğdu ve Düzeltildi
Steven
Steven6 dk okuma

On İki Dakikada Doğdu ve Düzeltildi

GeekBye v1.8.10'in changelog'u klavye kısayollarını düzenlerken bir çökmeyi düzelttiğini söyler. Düzeltti — ama çökme aynı pull request'te, on iki dakika arayla eklendi ve düzeltildi, ve tek bir kullanıcıya bile ulaşmadı. Asıl hikâye onu üreten güvenilirlik çağlayanıdır: kısayolların ne zaman askıya alındığına dair küçük, doğru bir değişiklik, ve editörü daha güvenli kılmak için tasarlanmış tek bir satırdan düşen React teardown bug'ı.

Mühendislik
React
Desktop
OCR'dan Piksellere, Fallback'i Kaybetmeden
Steven
Steven6 dk okuma

OCR'dan Piksellere, Fallback'i Kaybetmeden

GeekBye screenshot'larını OCR ile okumayı bıraktı ve gerçek görüntüyü vision modellerine göndermeye başladı — ki bu kolay kısım. Öğretici kısım, vision kullanıcıları için OCR'ı tamamen atlayan, vision olmayan modellere fallback'i sessizce kıran, ve güvenlik ağının paralel çalıştığı ve hiçbir maliyeti olmayan bir sürüme geri sarılması gereken optimizasyondur.

Mühendislik
AI
Desktop