* Extract text from a PDF content stream. The stream contains a sequence of * operators in PostScript-ish syntax. We care about: * - (literal string) Tj → show literal * - Tj → show hex-encoded literal * - [array] TJ → show array of strings with kerning
(stream: string)
| 135 | * or MacRoman encoding) this works fine. |
| 136 | */ |
| 137 | function extractTextFromStream(stream: string): string { |
| 138 | const out: string[] = []; |
| 139 | |
| 140 | // Process line-by-line-ish: scan for text operators |
| 141 | // (literal) Tj | (literal) ' | (literal) " |
| 142 | const singleStringRe = /\(((?:\\\)|\\\(|\\\\|[^()\\]|\\[0-9]{1,3}|\\[nrtbf])*)\)\s*(Tj|'|")/g; |
| 143 | // <hex> Tj |
| 144 | const hexStringRe = /<([0-9A-Fa-f\s]+)>\s*Tj/g; |
| 145 | // [arrays] TJ |
| 146 | const arrayRe = /\[([^\]]*)\]\s*TJ/g; |
| 147 | // T* and ET = line/block end |
| 148 | const newlineRe = /\bT\*\b/g; |
| 149 | const blockEndRe = /\bET\b/g; |
| 150 | |
| 151 | // We want to walk the stream in order, mixing all operators. Easiest: |
| 152 | // tokenize by scanning forward and detecting which kind of op is next. |
| 153 | // For simplicity here we do passes and merge — this loses some ordering |
| 154 | // precision but works for most documents. |
| 155 | |
| 156 | // Pass 1: literals |
| 157 | let m: RegExpExecArray | null; |
| 158 | const tokens: Array<{ pos: number; text: string }> = []; |
| 159 | singleStringRe.lastIndex = 0; |
| 160 | while ((m = singleStringRe.exec(stream))) { |
| 161 | tokens.push({ pos: m.index, text: decodePdfLiteral(m[1]!) + (m[2] === "'" || m[2] === '"' ? '\n' : '') }); |
| 162 | } |
| 163 | hexStringRe.lastIndex = 0; |
| 164 | while ((m = hexStringRe.exec(stream))) { |
| 165 | tokens.push({ pos: m.index, text: decodeHex(m[1]!) }); |
| 166 | } |
| 167 | arrayRe.lastIndex = 0; |
| 168 | while ((m = arrayRe.exec(stream))) { |
| 169 | const arrContent = m[1]!; |
| 170 | // Pull out (literals) inside the array; ignore numeric kerning offsets |
| 171 | const inner: string[] = []; |
| 172 | const innerRe = /\(((?:\\\)|\\\(|\\\\|[^()\\]|\\[0-9]{1,3}|\\[nrtbf])*)\)|<([0-9A-Fa-f\s]+)>/g; |
| 173 | let n: RegExpExecArray | null; |
| 174 | while ((n = innerRe.exec(arrContent))) { |
| 175 | if (n[1] !== undefined) inner.push(decodePdfLiteral(n[1])); |
| 176 | else if (n[2]) inner.push(decodeHex(n[2])); |
| 177 | } |
| 178 | tokens.push({ pos: m.index, text: inner.join('') }); |
| 179 | } |
| 180 | newlineRe.lastIndex = 0; |
| 181 | while ((m = newlineRe.exec(stream))) { |
| 182 | tokens.push({ pos: m.index, text: '\n' }); |
| 183 | } |
| 184 | blockEndRe.lastIndex = 0; |
| 185 | while ((m = blockEndRe.exec(stream))) { |
| 186 | tokens.push({ pos: m.index, text: '\n' }); |
| 187 | } |
| 188 | |
| 189 | tokens.sort((a, b) => a.pos - b.pos); |
| 190 | for (const t of tokens) out.push(t.text); |
| 191 | return out.join(''); |
| 192 | } |
| 193 | |
| 194 | function decodePdfLiteral(s: string): string { |
no test coverage detected