import zlib from 'node:zlib'; function escapeXml(text) { return String(text ?? '') .replace(/</g, '<') .replace(/>/g, '>') .replace(/&/g, '&') .replace(/"/g, '"') .replace(/'/g, "'"); } function extractZipEntry(buffer, targetName) { let offset = 0; while (offset + 30 <= buffer.length) { if (buffer.subarray(offset, offset + 2).toString('ascii') !== 'PK') break; const compressionMethod = buffer.readUInt16LE(offset + 8); const compressedSize = buffer.readUInt32LE(offset + 18); const nameLength = buffer.readUInt16LE(offset + 26); const extraLength = buffer.readUInt16LE(offset + 28); const name = buffer.subarray(offset + 30, offset + 30 + nameLength).toString('utf8'); const dataStart = offset + 30 + nameLength + extraLength; if (name === targetName) { const compressed = buffer.subarray(dataStart, dataStart + compressedSize); if (compressionMethod === 0) return compressed; if (compressionMethod === 8) return zlib.inflateRawSync(compressed); return null; } offset = dataStart + compressedSize; } return null; } function extractXmlText(xml) { const paragraphs = []; for (const block of String(xml ?? '').split('')) { const texts = [...block.matchAll(/]*>([\s\S]*?)<\/w:t>/g)].map((match) => escapeXml(match[1]), ); const line = texts.join('').replace(/\s+/g, ' ').trim(); if (line) paragraphs.push(line); } return paragraphs.join('\n\n'); } function extractDocxText(buffer) { const xmlBuffer = extractZipEntry(buffer, 'word/document.xml'); return xmlBuffer ? extractXmlText(xmlBuffer.toString('utf8')) : ''; } function extractXlsxText(buffer) { const sharedStringsXml = extractZipEntry(buffer, 'xl/sharedStrings.xml'); const sharedStrings = sharedStringsXml ? [...sharedStringsXml.toString('utf8').matchAll(/]*>([\s\S]*?)<\/t>/g)].map((match) => escapeXml(match[1]), ) : []; const sheetEntries = []; let index = 1; while (true) { const sheet = extractZipEntry(buffer, `xl/worksheets/sheet${index}.xml`); if (!sheet) break; const rows = []; for (const rowBlock of sheet.toString('utf8').split('')) { const cells = []; for (const cell of rowBlock.matchAll(/]*?(?:t="([^"]+)")?[^>]*>(?:[\s\S]*?([\s\S]*?)<\/v>)?/g)) { const type = cell[1] ?? ''; const value = cell[2] ?? ''; if (type === 's') { const sharedIndex = Number(value); cells.push(sharedStrings[sharedIndex] ?? value); } else { cells.push(escapeXml(value)); } } if (cells.length) rows.push(cells.join('\t')); } if (rows.length) { sheetEntries.push(`Sheet ${index}\n${rows.join('\n')}`); } index += 1; } return sheetEntries.join('\n\n'); } function extractPptxText(buffer) { const slides = []; let index = 1; while (true) { const slide = extractZipEntry(buffer, `ppt/slides/slide${index}.xml`); if (!slide) break; const texts = [...slide.toString('utf8').matchAll(/]*>([\s\S]*?)<\/a:t>/g)].map((match) => escapeXml(match[1]), ); const text = texts.join(' ').replace(/\s+/g, ' ').trim(); if (text) slides.push(`Slide ${index}\n${text}`); index += 1; } return slides.join('\n\n'); } function extractPdfText(buffer) { const text = buffer.toString('latin1'); const segments = []; for (const streamMatch of text.matchAll(/stream\r?\n([\s\S]*?)\r?\nendstream/g)) { const stream = streamMatch[1]; for (const textMatch of stream.matchAll(/\(([^()\\]*(?:\\.[^()\\]*)*)\)\s*Tj/g)) { const raw = textMatch[1] .replace(/\\n/g, '\n') .replace(/\\r/g, '\r') .replace(/\\t/g, '\t') .replace(/\\\\/g, '\\') .replace(/\\\(/g, '(') .replace(/\\\)/g, ')'); const cleaned = raw.replace(/\s+/g, ' ').trim(); if (cleaned) segments.push(cleaned); } } return segments.join('\n'); } export function extractAttachmentText(buffer, mimeType, filename = '') { const normalizedMimeType = String(mimeType ?? ''); const extension = String(filename ?? '').toLowerCase(); if (normalizedMimeType === 'application/pdf') { const text = extractPdfText(buffer); return { text, format: 'pdf', warnings: text ? [] : ['pdf_text_not_detected'] }; } if ( normalizedMimeType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || extension.endsWith('.docx') ) { const text = extractDocxText(buffer); return { text, format: 'docx', warnings: text ? [] : ['docx_text_not_detected'] }; } if ( normalizedMimeType === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' || extension.endsWith('.xlsx') ) { const text = extractXlsxText(buffer); return { text, format: 'xlsx', warnings: text ? [] : ['xlsx_text_not_detected'] }; } if ( normalizedMimeType === 'application/vnd.openxmlformats-officedocument.presentationml.presentation' || extension.endsWith('.pptx') ) { const text = extractPptxText(buffer); return { text, format: 'pptx', warnings: text ? [] : ['pptx_text_not_detected'] }; } if (normalizedMimeType.startsWith('text/')) { return { text: buffer.toString('utf8'), format: 'text', warnings: [] }; } return { text: '', format: 'unsupported', warnings: ['unsupported_attachment_type'] }; } export const attachmentTextInternals = { extractZipEntry, extractDocxText, extractXlsxText, extractPptxText, extractPdfText, };