// Light previews of office files for the file manager, with no libraries: // docx, xlsx and ods are zip archives of XML, opened with the browser's own // DecompressionStream; csv and tsv are parsed here; an old binary .doc only // gives up its text. Each turns into plain HTML the page styles itself. (function () { 'use strict'; const esc = s => String(s).replace(/[&<>"]/g, c => ({'&': '&', '<': '<', '>': '>', '"': '"'}[c])); // ---------- zip ---------- // Reads the central directory; a file is inflated only when asked for. function openZip(buf) { const b = buf instanceof Uint8Array ? buf : new Uint8Array(buf); const dv = new DataView(b.buffer, b.byteOffset, b.byteLength); let eocd = -1; for (let i = b.length - 22; i >= Math.max(0, b.length - 65557); i--) { if (dv.getUint32(i, true) === 0x06054b50) { eocd = i; break; } } if (eocd < 0) throw new Error('не zip-архив'); const count = dv.getUint16(eocd + 10, true); let p = dv.getUint32(eocd + 16, true); const files = new Map(); const dec = new TextDecoder(); for (let n = 0; n < count && dv.getUint32(p, true) === 0x02014b50; n++) { const method = dv.getUint16(p + 10, true), size = dv.getUint32(p + 20, true); const nameLen = dv.getUint16(p + 28, true), extra = dv.getUint16(p + 30, true), comment = dv.getUint16(p + 32, true); const local = dv.getUint32(p + 42, true); files.set(dec.decode(b.subarray(p + 46, p + 46 + nameLen)), {method, size, local}); p += 46 + nameLen + extra + comment; } async function bytes(name) { const f = files.get(name); if (!f) return null; const start = f.local + 30 + dv.getUint16(f.local + 26, true) + dv.getUint16(f.local + 28, true); const raw = b.subarray(start, start + f.size); if (f.method === 0) return raw; if (f.method !== 8) throw new Error('сжатие zip, которое браузер не знает'); const out = new Blob([raw]).stream().pipeThrough(new DecompressionStream('deflate-raw')); return new Uint8Array(await new Response(out).arrayBuffer()); } async function xml(name) { const raw = await bytes(name); return raw ? new DOMParser().parseFromString(new TextDecoder().decode(raw), 'application/xml') : null; } return {files, bytes, xml}; } // Children of an element by local name, whatever the namespace prefix. const kids = (el, name) => el ? Array.from(el.children).filter(c => c.localName === name) : []; const kid = (el, name) => kids(el, name)[0] || null; const attr = (el, name) => { if (!el) return null; for (const a of el.attributes) if (a.localName === name) return a.value; return null; }; // ---------- docx ---------- async function docx(buf) { const z = openZip(buf); const doc = await z.xml('word/document.xml'); if (!doc) throw new Error('в архиве нет word/document.xml'); // Style ids to names, to know headings whatever the language of Word. const styles = {}; const st = await z.xml('word/styles.xml'); if (st) for (const s of st.getElementsByTagNameNS('*', 'style')) { styles[attr(s, 'styleId')] = (attr(kid(s, 'name'), 'val') || '').toLowerCase(); } const body = doc.getElementsByTagNameNS('*', 'body')[0]; return '
' + blocks(body, styles) + '
'; } function blocks(el, styles) { let html = '', list = false; for (const c of el ? el.children : []) { if (c.localName === 'p') { const pPr = kid(c, 'pPr'); const isList = !!kid(pPr, 'numPr'); if (isList && !list) { html += ''; list = false; } html += para(c, pPr, styles, isList); } else { if (list) { html += ''; list = false; } if (c.localName === 'tbl') html += table(c, styles); else if (c.localName === 'sdt') html += blocks(kid(c, 'sdtContent'), styles); } } return html + (list ? '' : ''); } function para(p, pPr, styles, isList) { const name = styles[attr(kid(pPr, 'pStyle'), 'val')] || ''; const h = /^heading (\d)/.exec(name); const tag = isList ? 'li' : name === 'title' ? 'h1' : h ? 'h' + Math.min(6, +h[1] + 1) : 'p'; const jc = attr(kid(pPr, 'jc'), 'val'); const align = jc === 'center' ? 'center' : (jc === 'right' || jc === 'end') ? 'right' : (jc === 'both' ? 'justify' : ''); const inner = runs(p); return '<' + tag + (align ? ' style="text-align:' + align + '"' : '') + '>' + (inner || ' ') + ''; } function runs(el) { let html = ''; for (const c of el.children) { if (c.localName === 'r') html += run(c); else if (c.localName === 'hyperlink' || c.localName === 'smartTag' || c.localName === 'ins' || c.localName === 'sdt' || c.localName === 'sdtContent' || c.localName === 'fldSimple') { const inner = runs(c); html += c.localName === 'hyperlink' ? '' + inner + '' : inner; } } return html; } function run(r) { const pr = kid(r, 'rPr'); const on = n => { const e = kid(pr, n); return !!e && attr(e, 'val') !== '0' && attr(e, 'val') !== 'false'; }; let t = ''; for (const c of r.children) { if (c.localName === 't') t += esc(c.textContent); else if (c.localName === 'tab') t += ' '; else if (c.localName === 'br' || c.localName === 'cr') t += '
'; } if (!t) return ''; if (on('b')) t = '' + t + ''; if (on('i')) t = '' + t + ''; if (kid(pr, 'u') && attr(kid(pr, 'u'), 'val') !== 'none') t = '' + t + ''; if (on('strike')) t = '' + t + ''; // A size set on the run itself, as many documents do instead of styles. const sz = +attr(kid(pr, 'sz'), 'val'); if (sz && sz !== 24) t = '' + t + ''; const va = attr(kid(pr, 'vertAlign'), 'val'); if (va === 'superscript') t = '' + t + ''; else if (va === 'subscript') t = '' + t + ''; return t; } function table(tbl, styles) { let html = '
'; for (const tr of kids(tbl, 'tr')) { html += ''; for (const tc of kids(tr, 'tc')) { const span = attr(kid(kid(tc, 'tcPr'), 'gridSpan'), 'val'); html += ' 1 ? ' colspan="' + span + '"' : '') + '>' + blocks(tc, styles) + ''; } html += ''; } return html + '
'; } // ---------- tables: one grid per sheet ---------- const MAX_ROWS = 2000, MAX_COLS = 60; function colName(i) { let s = ''; for (i++; i > 0; i = Math.floor((i - 1) / 26)) s = String.fromCharCode(65 + (i - 1) % 26) + s; return s; } function grid(rows, names) { let cols = names ? names.length : 0; for (const r of rows) cols = Math.max(cols, r.length); cols = Math.min(cols, MAX_COLS); const cut = rows.length > MAX_ROWS || rows.some(r => r.length > MAX_COLS); let html = '
'; for (let c = 0; c < cols; c++) html += ''; html += ''; rows.slice(0, MAX_ROWS).forEach((r, i) => { html += ''; for (let c = 0; c < cols; c++) { const v = r[c] == null ? '' : r[c]; html += '' + esc(v) + ''; } html += ''; }); html += '
' + esc(names && names[c] != null ? names[c] : colName(c)) + '
' + (i + 1) + '
'; return {html, cut}; } // Several sheets: tabs over the grid. function sheets(list) { if (!list.length) return '

в книге нет листов

'; let html = ''; if (list.length > 1) { html += '
' + list.map((s, i) => '').join('') + '
'; } list.forEach((s, i) => { const g = grid(s.rows, s.cols); html += '
' + (s.note ? '

' + esc(s.note) + '

' : '') + g.html + (g.cut ? '

показаны первые ' + MAX_ROWS + ' строк и ' + MAX_COLS + ' столбцов

' : '') + '
'; }); return html; } function bindTabs(root) { root.querySelectorAll('[data-sheet]').forEach(b => b.onclick = () => { root.querySelectorAll('[data-sheet]').forEach(x => x.classList.toggle('on', x === b)); root.querySelectorAll('[data-pane]').forEach(p => p.hidden = p.dataset.pane !== b.dataset.sheet); }); } // ---------- xlsx ---------- async function xlsx(buf) { const z = openZip(buf); const wb = await z.xml('xl/workbook.xml'); if (!wb) throw new Error('в архиве нет xl/workbook.xml'); const rels = {}; const rx = await z.xml('xl/_rels/workbook.xml.rels'); if (rx) for (const r of rx.getElementsByTagNameNS('*', 'Relationship')) rels[r.getAttribute('Id')] = r.getAttribute('Target'); const shared = []; const ss = await z.xml('xl/sharedStrings.xml'); if (ss) for (const si of ss.getElementsByTagNameNS('*', 'si')) { shared.push(Array.from(si.getElementsByTagNameNS('*', 't')).filter(t => t.parentElement.localName !== 'rPh').map(t => t.textContent).join('')); } const list = []; for (const s of wb.getElementsByTagNameNS('*', 'sheet')) { let target = rels[attr(s, 'id')] || ''; target = target.startsWith('/') ? target.slice(1) : 'xl/' + target.replace(/^\.\//, ''); const doc = await z.xml(target); list.push({name: s.getAttribute('name') || '?', rows: doc ? xlsxRows(doc, shared) : []}); } return sheets(list); } function xlsxRows(doc, shared) { const rows = []; for (const row of doc.getElementsByTagNameNS('*', 'row')) { const ri = (+row.getAttribute('r') || rows.length + 1) - 1; if (ri >= MAX_ROWS) break; const out = rows[ri] = rows[ri] || []; let next = 0; for (const c of kids(row, 'c')) { const ref = /^([A-Z]+)/.exec(c.getAttribute('r') || ''); let ci = next; if (ref) { ci = 0; for (const ch of ref[1]) ci = ci * 26 + ch.charCodeAt(0) - 64; ci--; } next = ci + 1; if (ci >= MAX_COLS + 1) continue; const t = c.getAttribute('t'), v = kid(c, 'v'); let val = v ? v.textContent : ''; if (t === 's') val = shared[+val] || ''; else if (t === 'inlineStr') val = Array.from(c.getElementsByTagNameNS('*', 't')).map(x => x.textContent).join(''); else if (t === 'b') val = val === '1' ? 'TRUE' : 'FALSE'; else if (t !== 'str' && t !== 'e' && val !== '' && !isNaN(val)) val = +val; out[ci] = val; } } for (let i = 0; i < rows.length; i++) rows[i] = rows[i] || []; return rows; } // ---------- ods / odt ---------- async function ods(buf) { const z = openZip(buf); const doc = await z.xml('content.xml'); if (!doc) throw new Error('в архиве нет content.xml'); const list = []; for (const t of doc.getElementsByTagNameNS('*', 'table')) { if (t.parentElement && t.parentElement.localName === 'table-cell') continue; const rows = []; for (const r of t.getElementsByTagNameNS('*', 'table-row')) { const cells = []; for (const c of r.children) { if (c.localName !== 'table-cell' && c.localName !== 'covered-table-cell') continue; const rep = Math.min(+attr(c, 'number-columns-repeated') || 1, MAX_COLS + 1 - cells.length); const num = attr(c, 'value'); const text = Array.from(c.getElementsByTagNameNS('*', 'p')).map(p => p.textContent).join('\n'); const val = attr(c, 'value-type') === 'float' && num != null ? +num : text; for (let i = 0; i < rep; i++) cells.push(val); if (cells.length > MAX_COLS) break; } while (cells.length && cells[cells.length - 1] === '') cells.pop(); const rep = Math.min(+attr(r, 'number-rows-repeated') || 1, MAX_ROWS + 1 - rows.length); for (let i = 0; i < rep; i++) rows.push(cells); if (rows.length > MAX_ROWS) break; } while (rows.length && !rows[rows.length - 1].length) rows.pop(); list.push({name: attr(t, 'name') || '?', rows}); } return sheets(list); } async function odt(buf) { const z = openZip(buf); const doc = await z.xml('content.xml'); const body = doc && doc.getElementsByTagNameNS('*', 'text')[0]; if (!body) throw new Error('в архиве нет текста'); // Runs of spaces, tabs and breaks are elements of their own in ODF. const inline = el => { let html = ''; for (const n of el.childNodes) { if (n.nodeType === 3) html += esc(n.nodeValue); else if (n.localName === 's') html += ' '.repeat(+attr(n, 'c') || 1); else if (n.localName === 'tab') html += ' '; else if (n.localName === 'line-break') html += '
'; else if (n.nodeType === 1) html += inline(n); } return html; }; const walk = el => { let html = ''; for (const c of el.children) { if (c.localName === 'h') { const l = Math.min(6, (+attr(c, 'outline-level') || 1) + 1); html += '' + inline(c) + ''; } else if (c.localName === 'p') html += '

' + (inline(c) || ' ') + '

'; else if (c.localName === 'list') html += ''; else if (c.localName === 'table') html += '
' + Array.from(c.getElementsByTagNameNS('*', 'table-row')).map(r => '' + kids(r, 'table-cell').map(td => '').join('') + '').join('') + '
' + walk(td) + '
'; else if (c.localName === 'section') html += walk(c); } return html; }; return '
' + walk(body) + '
'; } // ---------- csv / tsv ---------- function csv(text, tab) { const first = text.slice(0, text.indexOf('\n') >>> 0 || text.length); const sep = tab ? '\t' : (first.split(';').length > first.split(',').length ? ';' : ','); const rows = []; let row = [], f = '', q = false; for (let i = 0; i < text.length; i++) { const c = text[i]; if (q) { if (c === '"') { if (text[i + 1] === '"') { f += '"'; i++; } else q = false; } else f += c; } else if (c === '"' && f === '') q = true; else if (c === sep) { row.push(f); f = ''; } else if (c === '\n' || c === '\r') { if (c === '\r' && text[i + 1] === '\n') i++; row.push(f); f = ''; rows.push(row); row = []; if (rows.length > MAX_ROWS) break; } else f += c; } if (f !== '' || row.length) { row.push(f); rows.push(row); } return sheets([{name: '', rows: rows.map(r => r.map(v => v !== '' && !isNaN(v) && /^-?[\d.]+$/.test(v.trim()) ? +v : v))}]); } // ---------- old .doc ---------- // Word 97-2003 is a binary format; without a full parser all that can be // shown is the text, picked out of the document's WordDocument stream as // the runs that read as words. function cfbStream(b, want) { const dv = new DataView(b.buffer, b.byteOffset, b.byteLength); if (dv.getUint32(0, true) !== 0xE011CFD0) return null; const ss = 1 << dv.getUint16(0x1E, true); const sector = n => 512 + n * ss; const fat = []; const addFat = n => { const o = sector(n); for (let i = 0; i < ss / 4; i++) fat.push(dv.getUint32(o + i * 4, true)); }; for (let i = 0; i < 109; i++) { const n = dv.getUint32(0x4C + i * 4, true); if (n < 0xFFFFFFFA) addFat(n); } for (let n = dv.getUint32(0x44, true), k = 0; n < 0xFFFFFFFA && k < 1000; k++) { const o = sector(n); for (let i = 0; i < ss / 4 - 1; i++) { const m = dv.getUint32(o + i * 4, true); if (m < 0xFFFFFFFA) addFat(m); } n = dv.getUint32(o + ss - 4, true); } const chain = (start, size) => { const out = new Uint8Array(size); let n = start, at = 0; for (let guard = 0; n < 0xFFFFFFFA && at < size && guard < 1e6; guard++) { const o = sector(n), len = Math.min(ss, size - at); if (o + len > b.length) break; out.set(b.subarray(o, o + len), at); at += len; n = fat[n]; } return out.subarray(0, at); }; const dirStart = dv.getUint32(0x30, true); const dir = chain(dirStart, ss * 64); const dd = new DataView(dir.buffer, dir.byteOffset, dir.byteLength); for (let o = 0; o + 128 <= dir.length; o += 128) { const nameLen = dd.getUint16(o + 0x40, true); let name = ''; for (let i = 0; i + 2 < nameLen; i += 2) name += String.fromCharCode(dd.getUint16(o + i, true)); if (name === want && dd.getUint8(o + 0x42) === 2) return chain(dd.getUint32(o + 0x74, true), dd.getUint32(o + 0x78, true)); } return null; } function doc(buf) { const all = new Uint8Array(buf); const b = cfbStream(all, 'WordDocument') || all; const ok = c => c === 9 || c === 10 || c === 13 || (c >= 32 && c < 127) || (c >= 0xA0 && c <= 0x24F) || (c >= 0x400 && c <= 0x4FF) || (c >= 0x2010 && c <= 0x2026) || c === 0x2116; const pieces = []; let cur = ''; const flush = min => { const letters = (cur.match(/[A-Za-zА-Яа-яЁё0-9]/g) || []).length; if (cur.trim().length >= min && letters >= cur.trim().length * 0.5) pieces.push(cur); cur = ''; }; for (let i = 0; i + 1 < b.length; i += 2) { const c = b[i] | (b[i + 1] << 8); if (ok(c)) cur += String.fromCharCode(c); else flush(3); } flush(4); let text = pieces.join('\n'); if (text.length < 40) { // a document that keeps its text 8-bit const p8 = []; for (let i = 0; i < b.length; i++) { const c = b[i]; if (c === 9 || c === 10 || c === 13 || (c >= 32 && c < 127)) cur += String.fromCharCode(c); else { if (cur.trim().length >= 8 && (cur.match(/[A-Za-z]/g) || []).length >= cur.length * 0.5) p8.push(cur); cur = ''; } } text = p8.join('\n'); } if (!text.trim()) throw new Error('текст не нашёлся'); return '

старый формат .doc: показан только текст, без оформления

' + text.split(/\r\n|\r|\n|\x07/).filter(l => l.trim()).map(l => '

' + esc(l) + '

').join('') + '
'; } // ---------- sqlite ---------- // Reads the database file itself: the b-tree of sqlite_master for the // tables, then each table's b-tree for its first rows. No SQL engine. function sqlite(buf) { const b = new Uint8Array(buf); if (new TextDecoder().decode(b.subarray(0, 15)) !== 'SQLite format 3') throw new Error('не база SQLite'); const dv = new DataView(b.buffer, b.byteOffset, b.byteLength); let ps = dv.getUint16(16); if (ps === 1) ps = 65536; const usable = ps - b[20]; const enc = dv.getUint32(56); const dec = new TextDecoder(enc === 2 ? 'utf-16le' : enc === 3 ? 'utf-16be' : 'utf-8'); const page = n => (n - 1) * ps; function varint(o) { let v = 0; for (let i = 0; i < 9; i++) { const c = b[o + i]; if (i === 8) return [v * 256 + c, o + 9]; v = v * 128 + (c & 0x7f); if (!(c & 0x80)) return [v, o + i + 1]; } } // A cell's payload, gathered from its overflow pages when it spills. function payload(o, size) { const max = usable - 35; if (size <= max) return b.subarray(o, o + size); const min = Math.floor((usable - 12) * 32 / 255) - 23; let local = min + (size - min) % (usable - 4); if (local > max) local = min; const out = new Uint8Array(size); out.set(b.subarray(o, o + local)); let at = local, next = dv.getUint32(o + local); for (let guard = 0; next && at < size && guard < 1e6; guard++) { const po = page(next), len = Math.min(usable - 4, size - at); out.set(b.subarray(po + 4, po + 4 + len), at); at += len; next = dv.getUint32(po); } return out; } function record(p) { const pv = new DataView(p.buffer, p.byteOffset, p.byteLength); let [hlen, o] = vi(p, 0); const types = []; while (o < hlen) { const r = vi(p, o); types.push(r[0]); o = r[1]; } let d = hlen; return types.map(t => { let v = null; if (t >= 1 && t <= 6) { const n = [0, 1, 2, 3, 4, 6, 8][t]; let x = 0; for (let i = 0; i < n; i++) x = x * 256 + p[d + i]; if (p[d] & 0x80) x -= Math.pow(2, 8 * n); v = n === 8 && Math.abs(x) > Number.MAX_SAFE_INTEGER ? pv.getBigInt64(d).toString() : x; d += n; } else if (t === 7) { v = pv.getFloat64(d); d += 8; } else if (t === 8) v = 0; else if (t === 9) v = 1; else if (t >= 12 && t % 2 === 0) { const n = (t - 12) / 2; v = '[blob ' + n + ' B]'; d += n; } else if (t >= 13) { const n = (t - 13) / 2; v = dec.decode(p.subarray(d, d + n)); d += n; } return v; }); } // varint over a payload buffer rather than the file function vi(p, o) { let v = 0; for (let i = 0; i < 9; i++) { const c = p[o + i]; if (i === 8) return [v * 256 + c, o + 9]; v = v * 128 + (c & 0x7f); if (!(c & 0x80)) return [v, o + i + 1]; } } // Walks a table b-tree in rowid order, up to limit rows; also counts all. function table(root, limit) { const rows = []; let count = 0; const walk = (n, depth) => { if (depth > 40) return; const h = page(n) + (n === 1 ? 100 : 0); const type = b[h], cells = dv.getUint16(h + 3); const ptrs = h + (type === 0x05 ? 12 : 8); if (type === 0x0d) { count += cells; for (let i = 0; i < cells && rows.length < limit; i++) { let o = page(n) + dv.getUint16(ptrs + i * 2), size, rowid; [size, o] = varint(o); [rowid, o] = varint(o); rows.push({rowid, values: record(payload(o, size))}); } } else if (type === 0x05) { for (let i = 0; i < cells; i++) walk(dv.getUint32(page(n) + dv.getUint16(ptrs + i * 2)), depth + 1); walk(dv.getUint32(h + 8), depth + 1); } }; walk(root, 0); return {rows, count}; } // Column names out of CREATE TABLE; an INTEGER PRIMARY KEY is the rowid. function columns(sql) { const m = /\(([\s\S]*)\)/.exec(sql || ''); if (!m) return {names: [], rowidCol: -1}; const parts = []; let depth = 0, cur = ''; for (const c of m[1]) { if (c === '(') depth++; if (c === ')') depth--; if (c === ',' && !depth) { parts.push(cur); cur = ''; } else cur += c; } parts.push(cur); const names = []; let rowidCol = -1; for (const p of parts) { const t = p.trim(); if (!t || /^(constraint|primary\s+key|unique|check|foreign\s+key)\b/i.test(t)) continue; const nm = /^("(?:[^"]|"")+"|`[^`]+`|\[[^\]]+\]|\S+)/.exec(t)[1].replace(/^["`\[]|["`\]]$/g, ''); if (/^\S+\s+integer\s+primary\s+key\b/i.test(t.replace(/^("(?:[^"]|"")+"|`[^`]+`|\[[^\]]+\])/, 'x'))) rowidCol = names.length; names.push(nm); } return {names, rowidCol}; } const master = table(1, 100000).rows.map(r => ({type: r.values[0], name: r.values[1], root: r.values[3], sql: r.values[4] || ''})); const list = []; for (const t of master.filter(x => x.type === 'table' && x.root).sort((a, c) => String(a.name).localeCompare(String(c.name)))) { if (/without\s+rowid/i.test(t.sql)) { list.push({name: t.name, rows: [], note: 'таблица WITHOUT ROWID - её строки здесь не показываются'}); continue; } const {names, rowidCol} = columns(t.sql); let data; try { data = table(t.root, MAX_ROWS); } catch (e) { list.push({name: t.name, rows: [], note: 'не удалось прочитать: ' + e.message}); continue; } const rows = data.rows.map(r => { const v = r.values.slice(); if (rowidCol >= 0 && v[rowidCol] == null) v[rowidCol] = r.rowid; return v; }); list.push({name: t.name, cols: names, rows, note: data.count + ' ' + (data.count % 10 === 1 && data.count % 100 !== 11 ? 'строка' : data.count % 10 >= 2 && data.count % 10 <= 4 && (data.count % 100 < 12 || data.count % 100 > 14) ? 'строки' : 'строк') + (data.count > rows.length ? ', показаны первые ' + rows.length : '')}); } if (!list.length) return '

в базе нет таблиц

'; return sheets(list); } // Extract text from PDF content streams without a heavyweight renderer or OCR. // Flate-compressed page streams are inflated with the browser's native API. async function pdf(buf) { const input = new Uint8Array(buf); let source = ''; for (let i = 0; i < input.length; i += 0x8000) source += String.fromCharCode(...input.subarray(i, i + 0x8000)); if (!source.startsWith('%PDF-')) throw new Error('это не PDF-файл'); const objects = /\b(\d+)\s+\d+\s+obj\b/g, pages = []; let inflatedBytes = 0, textBytes = 0, match; async function inflate(data) { const reader = new Blob([data]).stream().pipeThrough(new DecompressionStream('deflate')).getReader(); const chunks = []; let total = 0; for (;;) { const part = await reader.read(); if (part.done) break; total += part.value.length; if (total > 16 * 1024 * 1024 || inflatedBytes + total > 32 * 1024 * 1024) { await reader.cancel(); throw new Error('PDF stream слишком велик'); } chunks.push(part.value); } const result = new Uint8Array(total); let at = 0; chunks.forEach(chunk => { result.set(chunk, at); at += chunk.length; }); inflatedBytes += total; return result; } while ((match = objects.exec(source))) { const bodyStart = objects.lastIndex, possibleEnd = source.indexOf('endobj', bodyStart); if (possibleEnd < 0) break; const probe = source.slice(bodyStart, possibleEnd), stream = /\bstream\r?\n/.exec(probe); if (!stream) { objects.lastIndex = possibleEnd + 6; continue; } const dict = probe.slice(0, stream.index); let start = bodyStart + stream.index + stream[0].length; const length = /\/Length\s+(\d+)\b/.exec(dict); let end = length ? start + Number(length[1]) : source.indexOf('endstream', start); if (end < start) { objects.lastIndex = possibleEnd + 6; continue; } let endObj = source.indexOf('endobj', length ? end : end + 9); if (endObj < 0) break; if (/\/Subtype\s*\/Image|\/Type\s*\/(?:ObjStm|XObject|Font|FontFile)/.test(dict)) { objects.lastIndex = endObj + 6; continue; } let raw = source.slice(start, end); if (!length) raw = raw.replace(/[\r\n]+$/, ''); let data = Uint8Array.from(raw, c => c.charCodeAt(0) & 255); if (dict.includes('/FlateDecode') && !dict.includes('/ASCII85Decode')) { try { data = await inflate(data); } catch (e) { objects.lastIndex = endObj + 6; continue; } } else if (/\/Filter\b/.test(dict)) { objects.lastIndex = endObj + 6; continue; } let content = ''; for (let i = 0; i < data.length; i += 0x8000) content += String.fromCharCode(...data.subarray(i, i + 0x8000)); if (content.includes('BT') && (content.includes('Tj') || content.includes('TJ'))) { pages.push(content); textBytes += content.length; if (textBytes >= 2 * 1024 * 1024) break; } objects.lastIndex = endObj + 6; } const decode = bytes => { if (bytes.length > 1 && bytes[0] === 0xfe && bytes[1] === 0xff) { const swapped = new Uint8Array(bytes.length - 2); for (let i = 2; i + 1 < bytes.length; i += 2) { swapped[i - 2] = bytes[i + 1]; swapped[i - 1] = bytes[i]; } return new TextDecoder('utf-16le').decode(swapped); } return new TextDecoder('windows-1252').decode(bytes); }; function readString(s, at, literal) { const bytes = []; if (literal) { let depth = 1, i = at + 1; for (; i < s.length && depth; i++) { let c = s.charCodeAt(i); if (c === 92) { c = s.charCodeAt(++i); if (c === 110) bytes.push(10); else if (c === 114) bytes.push(13); else if (c === 116) bytes.push(9); else if (c === 98) bytes.push(8); else if (c === 102) bytes.push(12); else if (c === 10) { /* escaped line break */ } else if (c === 13) { if (s.charCodeAt(i + 1) === 10) i++; } else if (c >= 48 && c <= 55) { let oct = String.fromCharCode(c); for (let n = 0; n < 2 && /[0-7]/.test(s.charAt(i + 1)); n++) oct += s.charAt(++i); bytes.push(parseInt(oct, 8) & 255); } else bytes.push(c & 255); } else if (c === 40) { depth++; bytes.push(c); } else if (c === 41) { if (--depth) bytes.push(c); } else bytes.push(c & 255); } return {value: decode(Uint8Array.from(bytes)), end: i}; } let hex = '', i = at + 1; for (; i < s.length && s.charAt(i) !== '>'; i++) if (!/\s/.test(s.charAt(i))) hex += s.charAt(i); if (hex.length % 2) hex += '0'; for (let i = 0; i + 1 < hex.length; i += 2) bytes.push(parseInt(hex.slice(i, i + 2), 16)); return {value: decode(Uint8Array.from(bytes)), end: i + 1}; } function textFrom(content) { let out = '', inText = false, i = 0, operands = []; const token = () => { while (i < content.length && /[\x00\t\n\f\r ]/.test(content.charAt(i))) i++; if (content.charAt(i) === '%') { while (i < content.length && content.charAt(i) !== '\n') i++; return token(); } if (i >= content.length) return null; const c = content.charAt(i); if (c === '(') { const v = readString(content, i, true); i = v.end; return {type: 'str', value: v.value}; } if (c === '<' && content.charAt(i + 1) !== '<') { const v = readString(content, i, false); i = v.end; return {type: 'str', value: v.value}; } if (c === '[') { i++; const values = []; while (i < content.length) { while (/[\x00\t\n\f\r ]/.test(content.charAt(i))) i++; if (content.charAt(i) === ']') { i++; break; } if (content.charAt(i) === '(' || (content.charAt(i) === '<' && content.charAt(i + 1) !== '<')) { const v = readString(content, i, content.charAt(i) === '('); i = v.end; values.push(v.value); } else { const number = /^[+-]?(?:\d+\.?\d*|\.\d+)/.exec(content.slice(i)); if (number) { values.push(Number(number[0])); i += number[0].length; } else i++; } } return {type: 'array', value: values}; } if (c === '/') { i++; while (i < content.length && !/[\x00\t\n\f\r ()<>\[\]{}\/%]/.test(content.charAt(i))) i++; return {type: 'name'}; } const start = i++; while (i < content.length && !/[\x00\t\n\f\r ()<>\[\]{}\/%]/.test(content.charAt(i))) i++; return {type: 'word', value: content.slice(start, i)}; }; while (i < content.length) { const t = token(); if (!t) break; if (t.type !== 'word' || !/^[A-Za-z*'"]+$/.test(t.value)) { operands.push(t); continue; } const op = t.value; if (op === 'BT') { inText = true; operands = []; continue; } if (op === 'ET') { inText = false; out += '\n'; operands = []; continue; } if (inText && ['Tj', 'TJ', "'", '"'].includes(op)) { if (op === "'" || op === '"') out += '\n'; const v = operands[operands.length - 1]; if (v && v.type === 'str') out += v.value; else if (v && v.type === 'array') out += v.value.map(x => typeof x === 'number' ? (x < -120 ? ' ' : '') : x).join(''); } else if (inText && op === 'Tm') { if (out && !out.endsWith('\n')) out += '\n'; } else if (inText && (op === 'T*' || op === 'Td' || op === 'TD')) { if (op === 'T*' || Number(operands[operands.length - 1]?.value) !== 0) out += '\n'; else out += ' '; } operands = []; } return out; } const text = pages.map(textFrom).join('\n').replace(/[\t ]+\n/g, '\n').replace(/ {2,}/g, ' ').replace(/\n{3,}/g, '\n\n').trim(); if (!text) throw new Error('в PDF не найден извлекаемый текст (сканированные страницы без OCR не читаются)'); return text.slice(0, 120000); } window.DocView = {docx, xlsx, ods, odt, csv, doc, pdf, sqlite, bindTabs}; })();