(buf)
| 20 | * @returns {string} Extracted text, or empty string if no text layer |
| 21 | */ |
| 22 | export function extractPdfText(buf) { |
| 23 | const pages = []; |
| 24 | let streamCount = 0; |
| 25 | let totalDecoded = 0; |
| 26 | |
| 27 | // Find all stream...endstream blocks |
| 28 | let pos = 0; |
| 29 | while (pos < buf.length) { |
| 30 | const streamStart = buf.indexOf('stream\n', pos); |
| 31 | if (streamStart === -1) break; |
| 32 | |
| 33 | const dataStart = streamStart + 7; // skip "stream\n" |
| 34 | // Handle \r\n after "stream" |
| 35 | const actualStart = buf[streamStart + 6] === 0x0d ? dataStart + 1 : dataStart; |
| 36 | |
| 37 | const endStream = buf.indexOf('\nendstream', actualStart); |
| 38 | if (endStream === -1) break; |
| 39 | |
| 40 | const streamData = buf.subarray(actualStart, endStream); |
| 41 | streamCount++; |
| 42 | if (streamCount > MAX_STREAMS) throw new Error('PDF stream count exceeds safety limit'); |
| 43 | |
| 44 | // Check if this stream has FlateDecode by looking back at the dictionary |
| 45 | const dictStart = Math.max(0, streamStart - 500); |
| 46 | const dictText = buf.subarray(dictStart, streamStart).toString('latin1'); |
| 47 | const isFlate = dictText.includes('FlateDecode'); |
| 48 | |
| 49 | let decoded; |
| 50 | try { |
| 51 | if (isFlate) { |
| 52 | const inflated = inflateSync(streamData, { maxOutputLength: MAX_STREAM_DECODED }); |
| 53 | if (inflated.length > MAX_STREAM_DECODED) throw new Error('PDF decoded content exceeds safety limit'); |
| 54 | totalDecoded += inflated.length; |
| 55 | if (totalDecoded > MAX_TOTAL_DECODED) throw new Error('PDF decoded content exceeds safety limit'); |
| 56 | decoded = inflated.toString('latin1'); |
| 57 | } else { |
| 58 | totalDecoded += streamData.length; |
| 59 | if (streamData.length > MAX_STREAM_DECODED || totalDecoded > MAX_TOTAL_DECODED) throw new Error('PDF decoded content exceeds safety limit'); |
| 60 | decoded = streamData.toString('latin1'); |
| 61 | } |
| 62 | } catch (e) { |
| 63 | if (/limit|exceed|maxOutputLength|Buffer larger/i.test(e.message) || e.code === 'ERR_BUFFER_TOO_LARGE') throw e; |
| 64 | pos = endStream + 10; |
| 65 | continue; |
| 66 | } |
| 67 | |
| 68 | // Extract text from PDF operators |
| 69 | const text = extractTextOps(decoded); |
| 70 | if (text.trim()) pages.push(text.trim()); |
| 71 | |
| 72 | pos = endStream + 10; |
| 73 | } |
| 74 | |
| 75 | return pages.join('\n\n'); |
| 76 | } |
| 77 | |
| 78 | /** |
| 79 | * Extract text from PDF content stream operators. |
no test coverage detected