/** * PDF text extraction (SPEC §1): auction houses that publish prices-realised lists as PDFs. * pdfjs-dist is loaded lazily so connectors that never touch PDFs pay nothing. */ export interface PdfPage { page: number; /** lines reconstructed from text items grouped by y coordinate */ lines: string[]; } export async function extractPdfPages(data: Uint8Array, opts: { maxPages?: number } = {}): Promise { const pdfjs = (await import('pdfjs-dist/legacy/build/pdf.mjs')) as typeof import('pdfjs-dist/legacy/build/pdf.mjs'); const doc = await pdfjs.getDocument({ data, useSystemFonts: true, disableFontFace: true, isEvalSupported: false }).promise; const pages: PdfPage[] = []; const n = Math.min(doc.numPages, opts.maxPages ?? doc.numPages); for (let i = 1; i <= n; i++) { const page = await doc.getPage(i); const content = await page.getTextContent(); const rows = new Map>(); for (const it of content.items as Array<{ str?: string; transform?: number[] }>) { if (!it.str || !it.transform) continue; const y = Math.round(it.transform[5]! / 2) * 2; // 2pt buckets const x = it.transform[4]!; const row = rows.get(y) ?? []; row.push({ x, s: it.str }); rows.set(y, row); } const lines = [...rows.entries()] .sort((a, b) => b[0] - a[0]) .map(([, cells]) => cells.sort((a, b) => a.x - b.x).map((c) => c.s).join(' ').replace(/\s+/g, ' ').trim()) .filter(Boolean); pages.push({ page: i, lines }); } await doc.destroy(); return pages; } export async function extractPdfText(data: Uint8Array, opts: { maxPages?: number } = {}): Promise { const pages = await extractPdfPages(data, opts); return pages.map((p) => p.lines.join('\n')).join('\n\f\n'); } /** * Parse "lot number … price" rows from prices-realised text. Returns null-safe rows; the connector * still decides currency and date. Handles "123 1,250", "Lot 123: $1,250", "123 ........ 1.250,00". */ export function parsePricesRealised(lines: string[]): Array<{ lot: string; priceText: string; line: string }> { const out: Array<{ lot: string; priceText: string; line: string }> = []; const re = /^(?:lot\s*)?(\d{1,6}[A-Za-z]?)\b[\s.:\-–]*.*?([$€£¥]?\s?\d{1,3}(?:[.,\s]\d{3})*(?:[.,]\d{2})?)\s*$/i; for (const line of lines) { const m = line.match(re); if (!m) continue; if (m[1] === m[2]) continue; out.push({ lot: m[1]!, priceText: m[2]!.trim(), line }); } return out; }