Created
          September 8, 2025 10:12 
        
      - 
      
- 
        Save forresto/b43744e3f91c8db5f2782ac46a43ba27 to your computer and use it in GitHub Desktop. 
    Create a "blob stream" that can be used with PDFKit in the browser.
  
        
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | /** | |
| * Minimal browser-native replacement for blob-stream | |
| * Provides the same interface as [blob-stream](https://github.com/devongovett/blob-stream/issues/6) but uses browser-native APIs | |
| */ | |
| interface BlobStreamInterface { | |
| // EventEmitter interface methods that PDFKit expects | |
| on(event: string, callback: (...args: any[]) => void): this; | |
| once(event: string, callback: (...args: any[]) => void): this; | |
| emit(event: string, ...args: any[]): boolean; | |
| addListener(event: string, callback: (...args: any[]) => void): this; | |
| removeListener(event: string, callback: (...args: any[]) => void): this; | |
| removeAllListeners(event?: string): this; | |
| off(event: string, callback: (...args: any[]) => void): this; | |
| listeners(event: string): ((...args: any[]) => void)[]; | |
| listenerCount(event: string): number; | |
| setMaxListeners(n: number): this; | |
| getMaxListeners(): number; | |
| // Other EventEmitter methods | |
| rawListeners(event: string): ((...args: any[]) => void)[]; | |
| prependListener(event: string, callback: (...args: any[]) => void): this; | |
| prependOnceListener(event: string, callback: (...args: any[]) => void): this; | |
| eventNames(): (string | symbol)[]; | |
| // blob-stream specific interface | |
| toBlob(type?: string): Blob; | |
| toBlobURL(type?: string): string; | |
| // Minimal writable stream interface for PDFKit | |
| writable: boolean; | |
| write(chunk: any): boolean; | |
| end(): this; | |
| } | |
| /** | |
| * Creates a blob stream that can be used with PDFKit in the browser | |
| * | |
| * Usage: `const stream = doc.pipe(blobStream());` | |
| * | |
| * Only tested with pdfkit/js/pdfkit.standalone | |
| * | |
| * Vibe coded (🤪) 2025-09 with help from Gemini 2.5 Pro. | |
| */ | |
| function blobStream(): BlobStreamInterface { | |
| let chunks: Uint8Array[] = []; | |
| const eventListeners: { [key: string]: ((...args: any[]) => void)[] } = {}; | |
| let isFinished = false; | |
| let blob: Blob | null = null; | |
| let maxListeners = 10; | |
| const instance: BlobStreamInterface = { | |
| writable: true, | |
| on(event: string, callback: (...args: any[]) => void): BlobStreamInterface { | |
| if (!eventListeners[event]) { | |
| eventListeners[event] = []; | |
| } | |
| eventListeners[event].push(callback); | |
| if (event === "finish" && isFinished) { | |
| setTimeout(() => callback(), 0); | |
| } | |
| return instance; | |
| }, | |
| once(event: string, callback: (...args: any[]) => void): BlobStreamInterface { | |
| const wrappedCallback = (...args: any[]) => { | |
| instance.removeListener(event, wrappedCallback); | |
| callback(...args); | |
| }; | |
| return instance.on(event, wrappedCallback); | |
| }, | |
| addListener(event: string, callback: (...args: any[]) => void): BlobStreamInterface { | |
| return instance.on(event, callback); | |
| }, | |
| emit(event: string, ...args: any[]): boolean { | |
| const listeners = eventListeners[event]; | |
| if (!listeners || listeners.length === 0) { | |
| return false; | |
| } | |
| // Create a copy of the listeners array in case a listener modifies the array | |
| [...listeners].forEach((callback) => { | |
| try { | |
| callback(...args); | |
| } catch (error) { | |
| console.error("Error in event listener:", error); | |
| // We should still emit an 'error' event on the stream itself | |
| instance.emit("error", error); | |
| } | |
| }); | |
| return true; | |
| }, | |
| removeListener(event: string, callback: (...args: any[]) => void): BlobStreamInterface { | |
| const listeners = eventListeners[event]; | |
| if (listeners) { | |
| const index = listeners.indexOf(callback); | |
| if (index > -1) { | |
| listeners.splice(index, 1); | |
| } | |
| } | |
| return instance; | |
| }, | |
| off(event: string, callback: (...args: any[]) => void): BlobStreamInterface { | |
| return instance.removeListener(event, callback); | |
| }, | |
| removeAllListeners(event?: string): BlobStreamInterface { | |
| if (event) { | |
| delete eventListeners[event]; | |
| } else { | |
| Object.keys(eventListeners).forEach((key) => { | |
| delete eventListeners[key]; | |
| }); | |
| } | |
| return instance; | |
| }, | |
| listeners(event: string): ((...args: any[]) => void)[] { | |
| return eventListeners[event] ? [...eventListeners[event]] : []; | |
| }, | |
| rawListeners(event: string): ((...args: any[]) => void)[] { | |
| return instance.listeners(event); | |
| }, | |
| prependListener(event: string, callback: (...args: any[]) => void): BlobStreamInterface { | |
| if (!eventListeners[event]) { | |
| eventListeners[event] = []; | |
| } | |
| eventListeners[event].unshift(callback); | |
| return instance; | |
| }, | |
| prependOnceListener(event: string, callback: (...args: any[]) => void): BlobStreamInterface { | |
| const wrappedCallback = (...args: any[]) => { | |
| instance.removeListener(event, wrappedCallback); | |
| callback(...args); | |
| }; | |
| return instance.prependListener(event, wrappedCallback); | |
| }, | |
| eventNames(): (string | symbol)[] { | |
| return Object.keys(eventListeners); | |
| }, | |
| listenerCount(event: string): number { | |
| return eventListeners[event] ? eventListeners[event].length : 0; | |
| }, | |
| setMaxListeners(n: number): BlobStreamInterface { | |
| maxListeners = n; | |
| return instance; | |
| }, | |
| getMaxListeners(): number { | |
| return maxListeners; | |
| }, | |
| write(chunk: any): boolean { | |
| if (isFinished) return false; | |
| let uint8Chunk: Uint8Array; | |
| if (chunk instanceof Uint8Array) { | |
| uint8Chunk = chunk; | |
| } else if (typeof chunk === "string") { | |
| const encoder = new TextEncoder(); | |
| uint8Chunk = encoder.encode(chunk); | |
| } else if (chunk instanceof ArrayBuffer) { | |
| uint8Chunk = new Uint8Array(chunk); | |
| } else if (Array.isArray(chunk)) { | |
| uint8Chunk = new Uint8Array(chunk); | |
| } else { | |
| try { | |
| uint8Chunk = new Uint8Array(chunk); | |
| } catch (e) { | |
| instance.emit("error", new Error("Failed to convert chunk to Uint8Array")); | |
| return false; | |
| } | |
| } | |
| chunks.push(uint8Chunk); | |
| return true; | |
| }, | |
| end(): BlobStreamInterface { | |
| if (isFinished) { | |
| return instance; | |
| } | |
| isFinished = true; | |
| try { | |
| blob = new Blob(chunks as BlobPart[], { type: "application/pdf" }); | |
| instance.emit("finish"); | |
| } catch (error) { | |
| instance.emit("error", error); | |
| } | |
| chunks = []; // free memory | |
| return instance; | |
| }, | |
| toBlob(type: string = "application/pdf"): Blob { | |
| if (!blob) { | |
| return new Blob(chunks as BlobPart[], { type }); | |
| } | |
| if (type !== blob.type) { | |
| return blob.slice(0, blob.size, type); | |
| } | |
| return blob; | |
| }, | |
| toBlobURL(type: string = "application/pdf"): string { | |
| const blobObj = instance.toBlob(type); | |
| return URL.createObjectURL(blobObj); | |
| }, | |
| }; | |
| return instance; | |
| } | |
| export default blobStream; | 
  
    Sign up for free
    to join this conversation on GitHub.
    Already have an account?
    Sign in to comment