Steven
Steven10 मिनट पढ़ें

बिना किसी PDF library के एक meeting को PDF में print करना

GeekBye एक meeting को PDF के रूप में export करती है, और code में कहीं भी कोई PDF library नहीं है। वह एक अदृश्य browser window में HTML render करती है और उसे print कर देती है। वही चुनाव पूरी कहानी है: उसने feature को बनाना आसान कर दिया और उसे हर वह ख़राबी दे दी जो एक असली browser में होती है — एक सफ़ेद flash, एक URL length limit, और एक page break जो screenshots को बीच से काट देता था। सबसे बदसूरत bug का fix CSS की एक लाइन थी।

इंजीनियरिंग
Electron
डेस्कटॉप
GeekBye रिलीज़
बिना किसी PDF library के एक meeting को PDF में print करना

GeekBye के «इस meeting को PDF के रूप में export करें» बटन में एक संतोषजनक तरक़ीब छिपी है: उसके पीछे कोई PDF library नहीं है। न jsPDF, न pdfmake, न pdfkit, न कोई headless puppeteer। पूरा feature एक hidden browser है। App एक HTML page बनाता है जो meeting का वर्णन करता है — transcript entries, screenshots, timestamps — उसे एक अदृश्य Electron window में load करता है, और उस window से कहता है कि वह ख़ुद को PDF में print कर ले। बाक़ी webContents.printToPDF कर देता है। वह अकेला architectural चुनाव ही इस release की पूरी कहानी है, क्योंकि उसने feature को सस्ते में बनवा दिया और साथ ही उसे हर वह bug थमा दिया जो एक असली browser में होता है।

«पहले» भी वही release है

आम तौर पर ये posts «पहले यह ऐसे काम करता था» से शुरू होते हैं। इस बार कोई पहले है ही नहीं। v1.8.9 का changelog maintenance जैसा पढ़ा जाता है — "fixed page breaks splitting screenshots and filtered empty entries" — मानो PDF export कोई मौजूदा feature हो जिसे patch किया जा रहा हो। वह नहीं था। जो पहला commit इसे लाता है, feat: add PDF export via Electron printToPDF, उसी release cycle के तीसरे दिन उतरता है, और changelog का हर «fix» उस code पर उसी-हफ़्ते का iteration है जो घंटों पुराना था। feature जन्मा, टूटा, और मज़बूत बना — सब एक ही tag के भीतर। इसे ज़ोर से कहना इसलिए ज़रूरी है क्योंकि यह बहुत सारे shipping की ईमानदार शक्ल है: release notes का चमकाया हुआ bullet और उस चीज़ का बिखरा हुआ जन्म अक्सर एक ही release होते हैं।

आख़िर एक hidden browser ही क्यों

bugs से पहले इस तंत्र को समझ लेना फ़ायदेमंद है, क्योंकि सारे bugs इसी से उतरते हैं। सब कुछ एक ही main-process file, electron/ipcHandlers/pdfExportHandler.ts, में रहता है। जब renderer 'pdf:export-report' IPC दागता है, तो handler:

  1. buildReportHtml() को call करता है ताकि meeting की timeline से एक पूरा HTML string जोड़ा जाए — हर transcript entry या screenshot के लिए एक .item block।
  2. उस HTML को os.tmpdir() में एक temp file में लिखता है।
  3. उस file को एक hidden window में load करता है — new BrowserWindow({ show: false, opacity: 0, focusable: false, skipTaskbar: true })
  4. images के load होने का इंतज़ार करता है, फिर win.webContents.printToPDF({ printBackground: true, margins: { … } }) call करता है।
  5. नतीजे को app.getPath('downloads') में Meeting-Report-<timestamp>.pdf के रूप में save करता है।

आकर्षण साफ़ है। एक PDF library आपसे हर चीज़ हाथ से position करवाती है — (x, y) पर text खींचो, उसे नापो, cursor आगे बढ़ाओ, page breaks ख़ुद संभालो। एक browser यह सब पहले से करता है: आप <div> और CSS लिखते हैं, और Chromium का layout engine आपके लिए pagination कर देता है। आप कोई drawing API नहीं सीख रहे; आप एक web page लिख रहे हैं। एक ऐसे document के लिए जो ज़्यादातर styled text और embedded images है, वह एक ज़बरदस्त shortcut है।

पेच यह है कि आपने एक browser भी विरासत में ले लिया, उसके सारे edge cases समेत। उनमें से तीन पहले चार दिनों के भीतर सामने आ गए।

सफ़ेद flash। macOS पर एक hidden window भरोसे से hidden नहीं रहती। पहली कोशिशों ने उसे off-screen (x: screenWidth + 1000) रखा, और वह फिर भी print से पहले एक frame के लिए screen पर एक सफ़ेद आयत की तरह चमक जाती थी। तीन कोशिशों के बाद fix यह था कि position या show: false पर भरोसा करना छोड़ो और opacity: 0 set करो — एक पूरी तरह transparent window flash नहीं करती, भले ही compositor उसे पल भर के लिए दिखा दे। commit इसे साफ़ कहता है: "show:false with off-screen positioning still flashes on macOS."

URL length limit। पहले version ने पूरे HTML को — screenshots समेत — एक data: URL के रूप में inline कर दिया। वह तब तक चलता है जब तक एक meeting में कुछ base64-encoded screenshots न हों, जिस बिंदु पर URL Chromium की ~2 MB data-URL limit को पार करके निकल जाता है और load सीधे ERR_INVALID_URL के साथ fail हो जाता है। इसीलिए ऊपर का step 2 एक data URL load करने के बजाय एक temp file लिखता है और loadFile() करता है: एक file path की कोई length limit नहीं होती। (वह ~2 MB इस पूरी कहानी का इकलौता number है, और वह एक code comment में एक inline अनुमान है, कोई ऐसी चीज़ नहीं जिसे किसी ने benchmark किया हो — इसे «टकराने के लिए काफ़ी बड़ा» मानिए, कोई सटीक threshold नहीं।)

बिना escape किया text। transcript text मनमाना user content है, और उसे सीधे HTML string में गिराया जा रहा था। कोई भी transcript जिसमें संयोगवश <div> या <script> जैसा कुछ हो, वह page को तोड़ देगा या उससे बदतर। तो handler ने हर उस string पर एक escapeHtml() pass उगा लिया जिसे वह interpolate करता है। जिस पल आपका PDF एक web page है, HTML injection भी आपकी समस्या है।

वे खाली box जो खाली नहीं थे

अब मुख्य bug, क्योंकि यह सबसे अच्छा वाला है। users ने ऐसे export किए PDF की शिकायत की जिनमें खाली box थे — ऐसी entries जो एक timestamp और एक badge दिखाती थीं पर कोई screenshot नहीं, बस खाली जगह जहाँ image होनी चाहिए थी। ज़ाहिर पढ़त यह है कि «export image data गिरा रहा है», और ज़ाहिर fix यह कि «खाली entries को filter कर दो»। team ने वह filter जोड़ा भी — उस पर एक पल में और — पर वह असली कारण नहीं था।

असली कारण pagination था। हर entry एक .item container है जो एक timestamp, एक badge, और एक image को थामे रहता है। Chromium, PDF में print करते हुए, ख़ुशी-ख़ुशी एक page को एक item के बीचोंबीच तोड़ देता था — timestamp और badge को एक page के नीचे render करके और image को अगले पर धकेलकर। जो एक खाली box जैसा दिखता था वह उस item का ऊपरी आधा था जिसकी image अगले page पर overflow हो गई थी। data पूरा वहीं था; layout ने उसे guillotine पर चढ़ा दिया था। commit इस ग़लत-निदान के बारे में बेलाग है: "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."

fix CSS की एक लाइन है:

.item {
  break-inside: avoid;
}

break-inside: avoid layout engine से कहता है कि एक item को समूचा रखो — अगर वह एक page पर बची जगह में नहीं समाता, तो उसे तोड़ने के बजाय पूरे item को अगले page पर ले जाओ। यह hidden-browser architecture के प्रतिफल को लघु रूप में कहा गया है: feature का सबसे बदसूरत, सबसे ज़्यादा report किया गया bug page-height के हिसाब-किताब और हाथ से किए addPage() calls से नहीं, बल्कि एक अकेली CSS property से fix हुआ जिसका सम्मान करना browser पहले से जानता था। आपने कोई paginator नहीं लिखा; आपने जो विरासत में मिला उससे तमीज़ से पेश आने को कहा।

सचमुच-खाली को filter करने की दो परतें

सचमुच खाली entries भी थीं — बस वे नहीं जो box पैदा कर रही थीं। एक तेज़ double Cmd+Enter से आए ख़ाली duplicates, बिना preview वाले screenshots, ऐसी transcript entries जो सिर्फ़ whitespace थीं। ये filter हुईं, और दिलचस्प बात यह कि दो बार filter हुईं, दो परतों पर:

  • handler में, buildReportHtml() हर item की रखवाली करता है — text के लिए if (!item.content?.trim()) return '', और images के लिए const safeSrc = item.preview?.startsWith('data:image/') ? item.preview : ''; if (!safeSrc) return '' — फिर .filter(Boolean) ख़ाली को गिरा देता है। वह आख़िरी check दोहरा काम कर रहा है: वह एक validity gate है और एक छोटा safety check भी कि page तक सिर्फ़ असली image data ही पहुँचे।
  • timeline source पर, renderer में, वही ख़ालीपन किसी item के जुड़ने से पहले ही filter हो जाता है — if (!entry.text?.trim()) return, if (!screenshot.preview) return

इतिहास में सुंदर बारीकी वह तरीक़ा है। पहले commit ने एक logger.debug('PDF timeline item', …) loop स्पष्ट रूप से "for debugging empty entries" के लिए जोड़ा। जैसे ही उस logging ने बताया कि ख़ाली जगहें असल में कहाँ से आ रही थीं — एक अलग screenshot-deduplication bug समेत जहाँ तेज़ captures ख़ाली duplicates पैदा कर रहे थे — अगले commit ने root cause को upstream पर fix किया और debug logging हटा दी। कारण ढूँढने के लिए instrumentation जोड़ो, कारण fix करो, instrumentation हटाओ। changelog का bullet "filtered empty entries" उस पूरे जोड़ो-फिर-हटाओ चाप को चुपचाप अपने भीतर समेटे है।

v1.8.12: "excludes AI responses", diff पढ़ो

तीन releases बाद, एक और बदलाव — और यह changelog के बजाय code पढ़ने का एक अच्छा सबक़ है। v1.8.12 का note कहता है कि PDF export अब "excludes AI responses for cleaner meeting records"। सच, पर अधूरा।

export timeline हर item को एक type से tag करती है: 'transcript' | 'chat_user' | 'chat_assistant' | 'screenshot'। एक बोली गई meeting line एक transcript है; assistant panel में आपके टाइप किए सवाल एक chat_user है; AI का जवाब एक chat_assistant है। v1.8.12 का बदलाव एक renderer file में नौ-लाइन का edit है, और असरदार लाइन एक filter है जो export payload बनने से ठीक पहले रखी गई है:

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

वह एक allowlist है, blocklist नहीं। वह chat_assistant items को हटाती नहीं; वह सिर्फ़ transcript और screenshot रखती है और बाक़ी सब गिरा देती है — जिसका मतलब है कि वह AI के जवाबों के साथ-साथ user के अपने chat_user सवाल भी छील देती है। commit body changelog से ज़्यादा सटीक है: "PDF export keeps only transcripts and screenshots, no chat messages." export किया गया record अब शुद्ध रूप से meeting है — क्या कहा गया और screen पर क्या था — और assistant के side-panel की पूरी बातचीत उसमें से हटा दी गई है।

और product के लिहाज़ से यही सही फ़ैसला है। GeekBye का chat एक निजी assistant है जिससे आप एक call के दौरान सलाह लेते हैं — किसी screenshot के बारे में पूछो, एक झटपट व्याख्या पाओ। वे आदान-प्रदान आपकी scratchpad हैं, meeting का हिस्सा नहीं। एक meeting record जो आप किसी के साथ साझा करेंगे, उसमें meeting होनी चाहिए, उसके बारे में किसी AI के साथ आपकी बग़ल की बातचीत नहीं। allowlist बस «meeting» को इस बात की शाब्दिक परिभाषा बना देती है कि export क्या होता है।

इस release ने हमें तीन बातें सिखाईं

  1. एक browser एक PDF renderer है जो आपके पास पहले से है। अगर आपका document संरचित text और images है, तो एक hidden BrowserWindow पर printToPDF आपको असली pagination, असली font rendering, और CSS layout मुफ़्त देता है — कोई PDF drawing API नहीं। आप एक library dependency को browser edge cases के बदले देते हैं, जो अक्सर बेहतर सौदा है।
  2. जो bug आप देख सकते हैं और जो bug मौजूद है, वे हमेशा एक नहीं होते। «खाली box» गुम data चिल्ला रहे थे; कारण एक बँटा हुआ container था। मान लेने से पहले instrument करो — team की debug logging ही वह थी जिसने एक मानने-लायक़-पर-ग़लत fix (ख़ाली filter) को असली वाले (break-inside: avoid) में बदला।
  3. changelog एक सारांश है; filter सच है। "Excludes AI responses" एक allowlist है जो आपके अपने chat सवाल भी गिरा देती है। जब एक release note किसी behavior बदलाव का वर्णन करता है, तो diff आपको उसके ठीक किनारे बताएगा — और यहाँ ठीक किनारा है «सिर्फ़ meeting बचती है»।

v1 कहानी के पिछले अध्याय के लिए, देखें OCR से pixels तक, fallback गँवाए बिना (v1.8.6–v1.8.7); और पूरे चाप के लिए, सॉफ़्टवेयर को परिपूर्णता तक पहुँचाकर भेजने की शारीर-रचना

संबंधित लेख

एक click-through overlay के दो failure modes
Steven
Steven10 मिनट पढ़ें

एक click-through overlay के दो failure modes

GeekBye की window हर चीज़ के ऊपर तैरती है और आपके clicks को अपने आर-पार जाने देती है — सिवाय वहाँ जहाँ उसके buttons हैं। यह एक दो-तरफ़ा contract है, और v1.8.5 तथा v1.8.14 वही हैं जो हर तरफ़ के टूटने पर दिखता है: एक release जहाँ overlay ने एक system dialog निगल लिया, एक जहाँ उसने आपके keystrokes चुरा लिए। दूसरे का जीतने वाला fix code मिटाना था।

इंजीनियरिंग
Electron
डेस्कटॉप
बारह मिनट में जन्मा और ठीक हुआ
Steven
Steven9 मिनट पढ़ें

बारह मिनट में जन्मा और ठीक हुआ

GeekBye v1.8.10 का changelog कहता है कि उसने keyboard shortcuts को edit करते वक़्त होने वाले एक crash को ठीक किया। किया भी — पर वह crash एक ही pull request में, बारह मिनट के फ़ासले पर, आया और ठीक हुआ, और एक भी user तक कभी नहीं पहुँचा। असली कहानी वह reliability श्रृंखला है जिसने उसे पैदा किया: shortcuts कब suspend हों, इस पर एक छोटा, सही बदलाव, और editor को ज़्यादा सुरक्षित बनाने के इरादे वाली एक लाइन से गिरा वह React teardown bug।

इंजीनियरिंग
React
डेस्कटॉप
OCR से pixels तक, fallback गँवाए बिना
Steven
Steven9 मिनट पढ़ें

OCR से pixels तक, fallback गँवाए बिना

GeekBye ने आपके screenshots को OCR से पढ़ना बंद किया और असली image को vision models तक भेजना शुरू किया — और वह आसान हिस्सा है। सीखने लायक़ हिस्सा वह optimization है जिसने vision users के लिए OCR को पूरा छोड़ दिया, non-vision models तक के fallback को चुपके से तोड़ दिया, और उसे एक ऐसे version तक वापस समेटना पड़ा जहाँ safety net समानांतर में चलता है और कुछ भी ख़र्च नहीं करता।

इंजीनियरिंग
AI
डेस्कटॉप