dfgdfg
This commit is contained in:
@@ -33,15 +33,15 @@ const LABELS = [
|
||||
];
|
||||
|
||||
export class DeterministicParser {
|
||||
|
||||
|
||||
public async parsePdf(filePath: string): Promise<BuyerSheetData> {
|
||||
const dataBuffer = fs.readFileSync(filePath);
|
||||
const loadingTask = pdfjsLib.getDocument({ data: new Uint8Array(dataBuffer) });
|
||||
const pdfDocument = await loadingTask.promise;
|
||||
const page = await pdfDocument.getPage(1);
|
||||
|
||||
|
||||
const textContent = await page.getTextContent();
|
||||
|
||||
|
||||
// 1. Textfragmente mit X, Y und Breite (Width) auslesen
|
||||
const mappedItems = textContent.items
|
||||
.filter((item: any) => item.str.trim() !== '')
|
||||
@@ -54,7 +54,7 @@ export class DeterministicParser {
|
||||
.sort((a, b) => b.y - a.y || a.x - b.x); // Y absteigend (oben nach unten)
|
||||
|
||||
// 2. Zeilen bilden (Y-Toleranz)
|
||||
const linesGrouped: {y: number, items: any[]}[] = [];
|
||||
const linesGrouped: { y: number, items: any[] }[] = [];
|
||||
let currentY: number | null = null;
|
||||
let currentItems = [];
|
||||
|
||||
@@ -78,7 +78,7 @@ export class DeterministicParser {
|
||||
group.items.sort((a, b) => a.x - b.x);
|
||||
let lineStr = "";
|
||||
let prevEnd = -1;
|
||||
|
||||
|
||||
for (const item of group.items) {
|
||||
if (prevEnd !== -1) {
|
||||
const gap = item.x - prevEnd;
|
||||
@@ -96,14 +96,14 @@ export class DeterministicParser {
|
||||
// 4. Y-Intervall Parsing (Die "Nutzer-Idee")
|
||||
const rawResults: Record<string, string[]> = {};
|
||||
LABELS.forEach(lbl => rawResults[lbl] = []);
|
||||
|
||||
|
||||
let currentLabel: string | null = null;
|
||||
// Längste Labels zuerst suchen, damit "EMAIL ADDRESS" vor "ADDRESS" gefunden wird
|
||||
const sortedLabels = [...LABELS].sort((a, b) => b.length - a.length);
|
||||
|
||||
for (const line of lines) {
|
||||
const textUpper = line.text.toUpperCase().replace(/\s+/g, '');
|
||||
|
||||
|
||||
// Boilerplate ignorieren, der Labels enthält ("VERIFICATION OF DOWN PAYMENT")
|
||||
if (textUpper.includes("SELLERMAYREQUIREVERIFICATION")) continue;
|
||||
|
||||
@@ -118,7 +118,7 @@ export class DeterministicParser {
|
||||
|
||||
if (foundLabel) {
|
||||
currentLabel = foundLabel;
|
||||
|
||||
|
||||
// Falls der Wert direkt auf derselben Zeile steht (z.B. "NAME/COMPANY: Gunnar Schultz")
|
||||
if (line.text.includes(':')) {
|
||||
const parts = line.text.split(':');
|
||||
@@ -130,55 +130,178 @@ export class DeterministicParser {
|
||||
} else if (currentLabel) {
|
||||
// Diese Zeile ist kein Label, gehört also zum aktuellen Bereich!
|
||||
const cleanVal = line.text.replace(/_+/g, '').trim();
|
||||
|
||||
|
||||
// Footer und Artefakte ignorieren
|
||||
if (cleanVal.length > 0 && !cleanVal.includes("Doc ID") && !cleanVal.includes("Bizmatch")) {
|
||||
rawResults[currentLabel].push(cleanVal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.mapToTargetStructure(rawResults, filePath);
|
||||
// --- NEU: Visuelle Checkbox Erkennung ---
|
||||
const visualCheckboxResult = await this.detectGraphicCheckbox(page, mappedItems);
|
||||
return this.mapToTargetStructure(rawResults, filePath, visualCheckboxResult);
|
||||
}
|
||||
|
||||
private mapToTargetStructure(raw: Record<string, string[]>, filePath: string): BuyerSheetData {
|
||||
private mapToTargetStructure(
|
||||
raw: Record<string, string[]>,
|
||||
filePath: string,
|
||||
visualCheckboxResult: boolean | null // <-- Neuer Parameter
|
||||
): BuyerSheetData {
|
||||
const getVal = (label: string) => raw[label] && raw[label].length > 0 ? raw[label].join(' ') : null;
|
||||
|
||||
|
||||
const nameCompany = getVal("NAME / COMPANY");
|
||||
|
||||
// Down Payment normalisieren
|
||||
|
||||
// ==========================================
|
||||
// 1. Telefon & Handy (Cell) separieren
|
||||
// ==========================================
|
||||
let phoneRaw = getVal("PHONE");
|
||||
let phoneClean: string | null = null;
|
||||
let cellClean: string | null = null;
|
||||
|
||||
if (phoneRaw) {
|
||||
// Alles vor "FAX:" oder "CELL:" ist die Telefonnummer
|
||||
const phoneMatch = phoneRaw.split(/FAX:|CELL:/)[0];
|
||||
phoneClean = phoneMatch ? phoneMatch.trim() : null;
|
||||
|
||||
// Alles nach "CELL:" ist die Handynummer
|
||||
const cellMatch = phoneRaw.match(/CELL:\s*(.*)/);
|
||||
cellClean = cellMatch && cellMatch[1] ? cellMatch[1].trim() : null;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 2. Adresse bereinigen
|
||||
// ==========================================
|
||||
let addressClean = getVal("ADDRESS");
|
||||
if (addressClean) {
|
||||
// Den statischen Formulart-Subtext entfernen
|
||||
addressClean = addressClean.replace(/PO BOX \/ STREET\s+CITY \/ STATE \/ ZIP/g, '').trim();
|
||||
if (addressClean === '') addressClean = null;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 3. Checkbox Auswertung (Hybrid)
|
||||
// ==========================================
|
||||
const interestedRaw = getVal("ARE YOU INTERESTED");
|
||||
let interestedInUpdates: boolean | null = null;
|
||||
let checkboxPending = true;
|
||||
|
||||
if (interestedRaw) {
|
||||
// 1. Zuerst Text-Prüfung versuchen (wie vorher)
|
||||
if (/(✔|☑|X|✓)\s*YES/i.test(interestedRaw) || /YES\s*(✔|☑|X|✓)/i.test(interestedRaw)) {
|
||||
interestedInUpdates = true;
|
||||
checkboxPending = false;
|
||||
} else if (/(✔|☑|X|✓)\s*NO/i.test(interestedRaw) || /NO\s*(✔|☑|X|✓)/i.test(interestedRaw)) {
|
||||
interestedInUpdates = false;
|
||||
checkboxPending = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Wenn Text-Prüfung versagt hat, nutzen wir unser visuelles X-Koordinaten Ergebnis!
|
||||
if (checkboxPending && visualCheckboxResult !== null) {
|
||||
interestedInUpdates = visualCheckboxResult;
|
||||
checkboxPending = false;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 4. Down Payment & Datum
|
||||
// ==========================================
|
||||
const downPaymentRaw = getVal("DOWN PAYMENT")?.replace(/[^0-9,]/g, '') || null;
|
||||
const downPaymentClean = downPaymentRaw ? downPaymentRaw.replace(/,/g, '') : null;
|
||||
|
||||
// Datum aus Dateinamen extrahieren
|
||||
let dateOfIntro = null;
|
||||
const dateMatch = filePath.match(/(\d{2})(\d{2})(\d{2})\.pdf$/);
|
||||
if (dateMatch) {
|
||||
dateOfIntro = `20${dateMatch[3]}-${dateMatch[1]}-${dateMatch[2]}`;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 5. JSON Return mit dynamischen Flags
|
||||
// ==========================================
|
||||
return {
|
||||
is_buyer_sheet: true,
|
||||
is_buyer_sheet: true, // Wird in der Praxis dynamisch gesetzt
|
||||
name_company: nameCompany,
|
||||
prospective_buyer: nameCompany ? nameCompany.split('/')[0].trim() : null,
|
||||
company: null,
|
||||
phone: getVal("PHONE"),
|
||||
cell: null,
|
||||
phone: phoneClean,
|
||||
cell: cellClean,
|
||||
email: getVal("EMAIL ADDRESS"),
|
||||
address: getVal("ADDRESS"),
|
||||
address: addressClean,
|
||||
state: null,
|
||||
how_did_you_hear: getVal("HOW DID YOU HEAR"),
|
||||
interested_in_updates: null,
|
||||
interested_in_updates: interestedInUpdates,
|
||||
types_of_business_raw: getVal("TYPES OF BUSINESSES"),
|
||||
background_experience: getVal("BACKGROUND"),
|
||||
total_purchase_price: getVal("TOTAL PURCHASE PRICE"),
|
||||
down_payment: downPaymentClean,
|
||||
down_payment_raw: downPaymentRaw,
|
||||
date_of_introduction: dateOfIntro,
|
||||
_checkbox_pending: true,
|
||||
_checkbox_pending: checkboxPending,
|
||||
_parser: "deterministic",
|
||||
_info_page: 1,
|
||||
_ca_page: 2
|
||||
_info_page: 1, // Wird in der Praxis über eine Schleife ermittelt
|
||||
_ca_page: 2 // Wird in der Praxis über eine Schleife ermittelt
|
||||
};
|
||||
}
|
||||
}
|
||||
private async detectGraphicCheckbox(page: any, textItems: any[]): Promise<boolean | null> {
|
||||
let yesX = null, noX = null, targetY = null;
|
||||
|
||||
// 1. Koordinaten von YES und NO suchen
|
||||
for (const item of textItems) {
|
||||
const textUpper = item.text.toUpperCase().trim();
|
||||
if (textUpper.includes("YES")) { yesX = item.x; targetY = item.y; }
|
||||
}
|
||||
for (const item of textItems) {
|
||||
const textUpper = item.text.toUpperCase().trim();
|
||||
// Wir suchen NO auf derselben Höhe (Toleranz 5px)
|
||||
if (textUpper.includes("NO") && targetY !== null && Math.abs(item.y - targetY) < 5) {
|
||||
noX = item.x;
|
||||
}
|
||||
}
|
||||
|
||||
if (yesX === null || noX === null || targetY === null) return null;
|
||||
|
||||
// ==========================================
|
||||
// BILD-ERKENNUNG (Der 16x16 Stempel-Trick)
|
||||
// ==========================================
|
||||
const opList = await page.getOperatorList();
|
||||
let currentTransform = [1, 0, 0, 1, 0, 0];
|
||||
|
||||
for (let i = 0; i < opList.fnArray.length; i++) {
|
||||
const fn = opList.fnArray[i];
|
||||
const args = opList.argsArray[i];
|
||||
|
||||
if (fn === pdfjsLib.OPS.transform) {
|
||||
currentTransform = args;
|
||||
}
|
||||
// Wenn ein Bild auf die Seite gezeichnet wird
|
||||
else if (
|
||||
fn === pdfjsLib.OPS.paintImageXObject ||
|
||||
fn === pdfjsLib.OPS.paintInlineImageXObject ||
|
||||
fn === pdfjsLib.OPS.paintJpegXObject
|
||||
) {
|
||||
const width = Math.abs(currentTransform[0]);
|
||||
const height = Math.abs(currentTransform[3]);
|
||||
const imgX = currentTransform[4];
|
||||
const imgY = currentTransform[5];
|
||||
|
||||
// Wir filtern nach "Stempeln" (kleine Bilder unter 40x40 Pixeln)
|
||||
if (width < 40 && height < 40) {
|
||||
// Befindet sich der Stempel auf unserer Ziel-Zeile?
|
||||
// (Wir erlauben 50px Toleranz, da Y=378 vs Y=417)
|
||||
if (Math.abs(imgY - targetY) < 50) {
|
||||
const distToYes = Math.abs(imgX - yesX);
|
||||
const distToNo = Math.abs(imgX - noX);
|
||||
|
||||
// Wenn das Bildchen näher an YES ist
|
||||
if (distToYes < distToNo) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
88
find_images.ts
Normal file
88
find_images.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.js';
|
||||
import * as fs from 'fs';
|
||||
|
||||
async function analyzeImages(filePath: string) {
|
||||
console.log(`\nLese PDF für Bild-Analyse: ${filePath}`);
|
||||
|
||||
const dataBuffer = fs.readFileSync(filePath);
|
||||
const loadingTask = pdfjsLib.getDocument({ data: new Uint8Array(dataBuffer) });
|
||||
const pdfDocument = await loadingTask.promise;
|
||||
|
||||
// Wir analysieren nur Seite 1
|
||||
const page = await pdfDocument.getPage(1);
|
||||
const viewport = page.getViewport({ scale: 1.0 });
|
||||
console.log(`Seitengröße: Breite = ${viewport.width.toFixed(2)}, Höhe = ${viewport.height.toFixed(2)}\n`);
|
||||
|
||||
const opList = await page.getOperatorList();
|
||||
|
||||
// Um die Koordinaten zu berechnen, müssen wir den Grafik-Status (Stack) verfolgen
|
||||
let transformStack: number[][] = [[1, 0, 0, 1, 0, 0]]; // Standard-Matrix
|
||||
let currentTransform = [1, 0, 0, 1, 0, 0];
|
||||
let imageCount = 0;
|
||||
|
||||
console.log("--- GEFUNDENE BILDER AUF SEITE 1 ---");
|
||||
|
||||
for (let i = 0; i < opList.fnArray.length; i++) {
|
||||
const fn = opList.fnArray[i];
|
||||
const args = opList.argsArray[i];
|
||||
|
||||
// Status speichern (q)
|
||||
if (fn === pdfjsLib.OPS.save) {
|
||||
transformStack.push([...currentTransform]);
|
||||
}
|
||||
// Status wiederherstellen (Q)
|
||||
else if (fn === pdfjsLib.OPS.restore) {
|
||||
if (transformStack.length > 0) {
|
||||
currentTransform = transformStack.pop()!;
|
||||
}
|
||||
}
|
||||
// Transformation anwenden (cm) - In PDFs werden Matrizen multipliziert,
|
||||
// für die einfache Bildanalyse reicht oft der letzte Transform-Befehl vor dem Bild,
|
||||
// da dieser die Skalierung (Breite/Höhe) und Position (X/Y) des 1x1 Pixel Objekts setzt.
|
||||
else if (fn === pdfjsLib.OPS.transform) {
|
||||
currentTransform = args;
|
||||
}
|
||||
// Wenn ein Bild gezeichnet wird!
|
||||
else if (
|
||||
fn === pdfjsLib.OPS.paintImageXObject ||
|
||||
fn === pdfjsLib.OPS.paintInlineImageXObject ||
|
||||
fn === pdfjsLib.OPS.paintJpegXObject
|
||||
) {
|
||||
imageCount++;
|
||||
|
||||
// In der PDF-Matrix:
|
||||
// currentTransform[0] = Skalierung X (entspricht der Bild-Breite)
|
||||
// currentTransform[3] = Skalierung Y (entspricht der Bild-Höhe)
|
||||
// currentTransform[4] = Position X
|
||||
// currentTransform[5] = Position Y
|
||||
const width = currentTransform[0];
|
||||
const height = currentTransform[3];
|
||||
const x = currentTransform[4];
|
||||
const y = currentTransform[5];
|
||||
|
||||
const isPageFilling = (width >= viewport.width - 10 && height >= viewport.height - 10);
|
||||
const mark = isPageFilling ? " <-- SEITENFÜLLENDER SCAN / HINTERGRUND" : "";
|
||||
|
||||
console.log(`Bild ${imageCount}:`);
|
||||
console.log(` Position: X = ${x.toFixed(2)}, Y = ${y.toFixed(2)}`);
|
||||
console.log(` Größe: Breite = ${width.toFixed(2)}, Höhe = ${height.toFixed(2)}${mark}`);
|
||||
console.log(` Interner Name: ${args[0]}`);
|
||||
console.log(`--------------------------------------------------`);
|
||||
}
|
||||
}
|
||||
|
||||
if (imageCount === 0) {
|
||||
console.log("Keine Bilder gefunden. Das Dokument besteht rein aus Vektoren und Text.");
|
||||
} else {
|
||||
console.log(`\nInsgesamt ${imageCount} Bild(er) gefunden.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Aufruf mit dem Pfad aus den Parametern
|
||||
const args = process.argv.slice(2);
|
||||
const pdfArgIndex = args.indexOf('--pdf');
|
||||
if (pdfArgIndex !== -1 && args[pdfArgIndex + 1]) {
|
||||
analyzeImages(args[pdfArgIndex + 1]).catch(console.error);
|
||||
} else {
|
||||
console.error("Bitte --pdf Parameter angeben.");
|
||||
}
|
||||
Reference in New Issue
Block a user