21using core::application::importing::statement::DefaultStatementParser;
22using core::application::importing::transaction::DefaultTransactionParser;
23using core::application::importing::internal::ColumnGuess;
24using core::application::importing::internal::RawLineLite;
25using core::application::importing::transaction::internal::OcrLine;
26using core::application::importing::transaction::internal::TransactionBlock;
27using core::application::importing::transaction::internal::TransactionMainRow;
28namespace helpers = core::application::importing::internal;
29namespace heuristics = core::application::importing::internal;
33template <
typename Action>
34bool tryParserAction(Action&& action)
37 std::forward<Action>(action)();
44std::string fullBlockMetadata(
const TransactionBlock& block)
46 std::vector<std::string> parts;
47 auto append = [&](
const std::string& text) {
49 if (value.empty())
return;
50 if (std::find(parts.begin(), parts.end(), value) != parts.end())
return;
51 parts.push_back(value);
59 std::ostringstream out;
60 for (std::size_t i = 0; i < parts.size(); ++i) {
61 if (i > 0) out <<
'\n';
92 merged.
text += std::string(
" ") + second.
text;
100 return helpers::splitMainRowFromRaw(mergeRawLines(first, second), cols.valutaX, cols.debitX, cols.creditX);
103bool hasVerticalStartEvidence(
const OcrLine& descriptiveLine,
108 return helpers::hasLeftDescriptiveText(descriptiveLine, valutaX)
109 && !helpers::hasAmountLikeTokenInLine(descriptiveLine, valutaX)
110 && helpers::hasAmountNearValuta(amountLine, valutaX, bandPx);
113const std::regex& combinedMainRowRegex()
115 static const std::regex pattern(R
"((\d{2}\.\s*\d{2})\s+\d{1,3}(?:[\.,]\d{3})*[\.,]\d{1,2}-?\b)", std::regex::ECMAScript);
119void appendDebugValue(std::vector<std::string>& debugOut,
const char* prefix,
const std::string& value)
121 if (value.empty())
return;
122 debugOut.push_back(std::string(prefix) + value);
125bool looksLikeMetadataIdentifier(
const std::string& value)
noexcept
128 static const std::regex ibanPattern(R
"(^[A-Z]{2}\d{2}[A-Z0-9]{10,32}$)", std::regex::icase);
129 static const std::regex bicPattern(R
"(^[A-Z]{6}[A-Z0-9]{2}(?:[A-Z0-9]{3})?$)", std::regex::icase);
130 static const std::regex referencePattern(R
"((end-to-end|kundenreferenz|verwendungszweck|vertragskont|iban|bic|dauerauftrag|mandat|referenz|zweck))", std::regex::icase);
131 return std::regex_match(value, ibanPattern)
132 || std::regex_match(value, bicPattern)
133 || std::regex_search(value, referencePattern);
139bool looksLikeStandaloneMetadataLine(
const std::string& value)
noexcept
143 if (trimmed.empty())
return false;
144 if (looksLikeMetadataIdentifier(trimmed))
return true;
146 static const std::regex dateOnlyPattern(R
"(^\d{1,2}[\./-]\d{1,2}(?:[\./-]\d{2,4})?$)", std::regex::icase);
147 static const std::regex labelPattern(R
"((end-to-end|kundenreferenz|verwendungszweck|vertragskont|iban|bic|dauerauftrag|mandat|referenz|zweck))", std::regex::icase);
148 return std::regex_match(trimmed, dateOnlyPattern) || std::regex_search(trimmed, labelPattern);
154bool isCurrencyToken(
const std::string& token)
noexcept
156 const auto normalized = helpers::normalizeAlnumLower(token);
157 return normalized ==
"eur"
158 || normalized ==
"usd"
159 || normalized ==
"chf"
160 || normalized ==
"gbp";
163bool looksLikePureAmountColumnTail(
const OcrLine& line)
noexcept
167 if (
tokens.empty())
return false;
169 bool sawAmount =
false;
170 for (
const auto& token : tokens) {
171 if (!helpers::findAmountTokenIndices(
OcrLine{0, 0, 0, 0, {}, token}, -1, 0).empty()) {
175 if (isCurrencyToken(token))
continue;
177 for (
const unsigned char ch : token) {
178 if (std::isalpha(ch))
return false;
205 if (tokens.size() != line.
wordSpans.size() || tokens.size() < 3)
return false;
207 const int band = helpers::parserConfig.tokenNearBandForMainRow;
212 if (!hasDebit && !hasCredit) {
213 tryParserAction([&] {
214 auto amountIndices = helpers::findAmountTokenIndices(
rawToOcrLine(line), cols.
valutaX, helpers::parserConfig.amountNearValutaBandPx);
215 if (!amountIndices.empty()) hasDebit =
true;
219 bool hasLeft =
false;
220 for (
size_t i = 0; i < tokens.size(); ++i) {
222 const int centerX = (span.first + span.second) / 2;
223 if (centerX < cols.
valutaX - helpers::parserConfig.leftDescriptiveOffsetPx) {
229 return hasLeft && hasValuta && (hasDebit || hasCredit);
241 for (
const auto& line : lines) {
243 if (idx > scanLines)
break;
244 const auto& text = line.text;
245 if (text.empty())
continue;
247 bool isStrongHeaderLike =
false;
248 bool isNoise =
false;
250 if (heuristics::isTransactionsSectionHeader(text)) isStrongHeaderLike =
true;
251 if (helpers::normalizeAlnumLower(text).find(
"valuta") != std::string::npos) isStrongHeaderLike =
true;
252 if (heuristics::isDebitCreditHeaderLine(text)) isStrongHeaderLike =
true;
253 if (helpers::findFirstFullDate(text).has_value()) isStrongHeaderLike =
true;
254 if (heuristics::isHeaderNoiseLine(text)) isNoise =
true;
257 if (isStrongHeaderLike) {
258 lastBlockY = std::max(lastBlockY, line.maxY);
259 }
else if (isNoise) {
261 if (noiseCount >= 2) lastBlockY = std::max(lastBlockY, line.maxY);
262 }
else if (lastBlockY >= 0) {
267 return {lastBlockY, lastBlockY >= 0};
273 static const std::regex reDate(R
"((\d{2}\.\d{2}\.\d{4}))");
274 const size_t n = std::min(lines.size(), scanLines);
275 for (
size_t i = 0; i < n; ++i) {
276 const auto& text = lines[i].text;
277 if (text.empty())
continue;
278 if (heuristics::isPostTransactionFootnote(text))
continue;
281 const auto combinedPrev = (i > 0) ? (lines[i - 1].text + std::string(
" ") + text) : text;
282 const auto combinedNext = (i + 1 < lines.size()) ? (text + std::string(
" ") + lines[i + 1].text) : text;
283 if (std::regex_search(text, match, reDate)
284 || std::regex_search(combinedPrev, match, reDate)
285 || std::regex_search(combinedNext, match, reDate)) {
297 const auto normalized = helpers::normalizeAlnumLower(trimmed);
298 if (normalized.find(
"buchungsdat") == std::string::npos && normalized.find(
"buchungsdatum") == std::string::npos)
return std::nullopt;
300 if (
auto date = helpers::findFirstFullDate(trimmed))
return date;
302 std::string compact = trimmed;
303 compact.erase(std::remove_if(compact.begin(), compact.end(), [](
char c) {
304 return !(std::isdigit(static_cast<unsigned char>(c)) || c ==
'.' || std::isspace(static_cast<unsigned char>(c)));
306 return helpers::findFirstFullDate(compact);
314 const size_t checkLines = std::min(lines.size(),
static_cast<size_t>(helpers::parserConfig.headerScanLines));
316 int amountLikeCount = 0;
317 const size_t start = lines.size() > checkLines ? lines.size() - checkLines : 0;
318 for (
size_t i = start; i < lines.size(); ++i) {
319 const auto& text = lines[i].text;
320 if (text.empty())
continue;
321 if (heuristics::isPostTransactionFootnote(text)) ++footLike;
324 for (
const auto& token : tokens) {
325 if (helpers::findAmountTokenIndices(
OcrLine{0, 0, 0, 0, {}, token}, -1, 0).empty())
continue;
331 std::ostringstream summary;
332 summary <<
"page.earlyEmptyCheckBottom\tfootLike=" << footLike <<
"\tamts=" << amountLikeCount;
333 outDebug = summary.str();
334 return footLike >=
static_cast<int>(checkLines * 2 / 3) && amountLikeCount == 0;
341 const helpers::ColumnGuess& seedCols)
343 if (lines.size() <= 1)
return lines;
345 std::vector<helpers::RawLineLite> merged;
346 merged.reserve(lines.size());
347 auto current = lines.front();
348 for (
size_t i = 1; i < lines.size(); ++i) {
349 const auto& next = lines[i];
350 if (next.minY - current.maxY <= maxGapPx) {
351 bool evidence =
false;
353 const auto currentLine = rawLiteToOcrLine(current);
354 const auto nextLine = rawLiteToOcrLine(next);
355 const bool currentStandaloneMetadata = looksLikeStandaloneMetadataLine(currentLine.text);
356 const bool nextStandaloneMetadata = looksLikeStandaloneMetadataLine(nextLine.text);
357 const bool currentHasAmount = helpers::hasAmountLikeTokenInLine(currentLine, seedCols.valutaX);
358 const bool nextHasAmount = helpers::hasAmountLikeTokenInLine(nextLine, seedCols.valutaX);
359 const bool currentNearValuta = helpers::hasTokenNearX(currentLine, seedCols.valutaX, helpers::parserConfig.tokenNearMergeBandPx);
360 const bool nextNearValuta = helpers::hasTokenNearX(nextLine, seedCols.valutaX, helpers::parserConfig.tokenNearMergeBandPx);
362 if (!currentStandaloneMetadata && !nextStandaloneMetadata) {
363 if (currentHasAmount && nextHasAmount) evidence =
true;
364 if (!evidence && helpers::hasLeftDescriptiveText(currentLine, seedCols.valutaX) && nextNearValuta && nextHasAmount) evidence =
true;
366 if (!evidence && !currentStandaloneMetadata && !nextStandaloneMetadata && helpers::isLooseTransactionLine(currentLine, seedCols.valutaX) && nextNearValuta && nextHasAmount) evidence =
true;
367 if (!evidence && !currentStandaloneMetadata && !nextStandaloneMetadata && helpers::isLooseTransactionLine(nextLine, seedCols.valutaX) && currentNearValuta && currentHasAmount) evidence =
true;
371 current.maxX = std::max(current.maxX, next.maxX);
372 current.maxY = next.maxY;
373 current.wordSpans.insert(current.wordSpans.end(), next.wordSpans.begin(), next.wordSpans.end());
374 current.text +=
" " + next.text;
379 merged.push_back(std::move(current));
383 merged.push_back(std::move(current));
388 const std::vector<RawLine>& lines,
393 static constexpr size_t headerScanLines = 12;
400 out.
debugLines.push_back(std::string(
"header.prebottomY\t") + std::to_string(preHeaderBottomY));
401 out.
debugLines.push_back(std::string(
"header.found\t") + (headerFound ?
"1" :
"0"));
404 const size_t debugCount = std::min(ocrLines.size(), headerScanLines);
405 for (
size_t headerIndex = 0; headerIndex < debugCount; ++headerIndex) {
406 const auto& line = ocrLines[headerIndex];
407 const auto& text = line.text;
409 const bool isTxSection = heuristics::isTransactionsSectionHeader(text);
410 const bool isDebitCredit = heuristics::isDebitCreditHeaderLine(text);
411 const bool isNoise = heuristics::isHeaderNoiseLine(text);
412 const bool hasFullDate = helpers::findFirstFullDate(text).has_value();
413 bool hasAmount = helpers::hasAmountLikeTokenInLine(line, seedCols.
valutaX);
414 bool hasValutaTokenNear =
false;
415 tryParserAction([&] {
416 if (seedCols.
valutaX >= 0) hasValutaTokenNear = helpers::hasTokenNearX(line, seedCols.
valutaX, helpers::parserConfig.tokenNearBandForMainRow);
418 std::ostringstream summary;
419 summary <<
"header.candidate\tline=" << headerIndex <<
"\ttext=" << text <<
"\ttxSection=" << (isTxSection ?
"1" :
"0") <<
"\tdebitcredit=" << (isDebitCredit ?
"1" :
"0") <<
"\tnoise=" << (isNoise ?
"1" :
"0") <<
"\tfullDate=" << (hasFullDate ?
"1" :
"0") <<
"\thasAmt=" << (hasAmount ?
"1" :
"0") <<
"\tvalutaNear=" << (hasValutaTokenNear ?
"1" :
"0");
426 int earliestAmountY = std::numeric_limits<int>::max();
427 bool foundAmountLine =
false;
428 for (
const auto& line : lines) {
431 const bool hasAmount = helpers::hasAmountLikeTokenInLine(ocrLine, seedCols.
valutaX);
432 const bool dateLeft = helpers::hasShortDateToken(ocrLine.
text) && helpers::hasLeftDescriptiveText(ocrLine, seedCols.
valutaX);
433 if (!hasAmount && !dateLeft)
continue;
434 earliestAmountY = std::min(earliestAmountY, line.maxY);
435 foundAmountLine =
true;
438 if (foundAmountLine && earliestAmountY != std::numeric_limits<int>::max()) {
440 out.
debugLines.push_back(std::string(
"header.adjustedForEarliestAmount\t") + std::to_string(result.
headerBottomY));
445 for (
const auto& line : lines) result.
pageMaxY = std::max(result.
pageMaxY, line.maxY);
446 }
catch (...) { result.
pageMaxY = -1; }
453 bool looksMain =
false;
454 try {
if (helpers::hasAmountLikeTokenInLine(line, cols.
valutaX)) looksMain =
true; }
catch (...) { }
455 try {
if (!looksMain && helpers::isLooseTransactionLine(line, cols.
valutaX)) looksMain =
true; }
catch (...) { }
456 try {
if (!looksMain && helpers::hasShortDateToken(line.
text) && helpers::hasLeftDescriptiveText(line, cols.
valutaX)) looksMain =
true; }
catch (...) { }
461 const std::vector<OcrLine>& orphanLines,
463 const std::string& currentBookingDate,
467 for (
const auto& orphanLine : orphanLines) {
474 blocks.push_back(std::move(block));
475 out.
debugLines.push_back(std::string(
"tx.start.rescued\ttext=") + orphanLine.text);
481std::optional<std::pair<TransactionMainRow, int>>
tryVerticalStart(
const std::vector<OcrLine>& lines,
483 const helpers::ColumnGuess& cols)
noexcept
486 if (li >= lines.size())
return std::nullopt;
488 const int band = std::max(helpers::parserConfig.amountNearValutaBandPx, helpers::parserConfig.tokenNearMergeBandPx);
489 const auto& current = lines[li];
492 const auto& previous = lines[li - 1];
493 if (hasVerticalStartEvidence(previous, current, cols.valutaX, band)) {
494 return std::make_optional(std::make_pair(splitMergedMainRow(previous, current, cols), 0));
496 if (helpers::hasShortDateToken(previous.text) && helpers::hasAmountNearValuta(current, cols.valutaX, band)) {
497 return std::make_optional(std::make_pair(splitMergedMainRow(previous, current, cols), 0));
501 if (li + 1 < lines.size()) {
502 const auto& next = lines[li + 1];
503 if (hasVerticalStartEvidence(current, next, cols.valutaX, band)) {
504 return std::make_optional(std::make_pair(splitMergedMainRow(current, next, cols), 1));
506 if (helpers::hasShortDateToken(current.text) && helpers::hasAmountNearValuta(next, cols.valutaX, band)) {
507 return std::make_optional(std::make_pair(splitMergedMainRow(current, next, cols), 1));
514std::optional<std::pair<TransactionMainRow, int>>
tryCombinedStart(
const std::vector<OcrLine>& lines,
516 const helpers::ColumnGuess& cols)
noexcept
519 if (li >= lines.size())
return std::nullopt;
521 const auto& current = lines[li];
522 const auto combinedPrev = (li > 0) ? (lines[li - 1].text + std::string(
" ") + current.text) : current.text;
523 const auto combinedNext = (li + 1 < lines.size()) ? (current.text + std::string(
" ") + lines[li + 1].text) : current.text;
525 if (!combinedPrev.empty() && std::regex_search(combinedPrev, combinedMainRowRegex())) {
526 return std::make_optional(std::make_pair(helpers::splitMainRowFromOcrLine(current, cols.valutaX, cols.debitX, cols.creditX), 0));
528 if (!combinedNext.empty() && std::regex_search(combinedNext, combinedMainRowRegex())) {
529 return std::make_optional(std::make_pair(helpers::splitMainRowFromOcrLine(current, cols.valutaX, cols.debitX, cols.creditX), 1));
537 const helpers::ColumnGuess& cols,
538 std::vector<std::string>* debugOut)
noexcept
541 if (cols.valutaX >= 0 && helpers::hasLeftDescriptiveText(line, cols.valutaX)) {
542 cur.detailLines.push_back(line);
543 if (debugOut) debugOut->push_back(std::string(
"detail.append.helper.whole.left\ttext=") + line.text);
547 if (cols.valutaX >= 0) {
549 if (tokens.size() == line.wordSpans.size()) {
550 size_t cut = tokens.size();
551 for (
size_t i = 0; i < tokens.size(); ++i) {
552 const auto& span = line.wordSpans[i];
553 const int centerX = (span.first + span.second) / 2;
554 if (centerX >= cols.valutaX) {
560 const auto rawLine = toRawLineLite(line);
561 const auto leftPart = helpers::toOcrLineFromRawWords(rawLine, 0, cut);
562 const auto rightPart = helpers::toOcrLineFromRawWords(rawLine, cut, tokens.size());
567 bool keepWholeLine = leftText.empty() || looksLikeMetadataIdentifier(fullText);
568 if (!keepWholeLine && !rightText.empty()) {
569 keepWholeLine = !looksLikePureAmountColumnTail(rightPart);
573 cur.detailLines.push_back(line);
574 if (debugOut) debugOut->push_back(std::string(
"detail.append.helper.whole\ttext=") + line.text + std::string(
"\tcut=") + std::to_string(cut));
578 if (!leftText.empty()) cur.detailLines.push_back(leftPart);
579 if (debugOut) debugOut->push_back(std::string(
"detail.append.helper\ttext=") + line.text + std::string(
"\tcut=") + std::to_string(cut));
584 cur.detailLines.push_back(line);
585 if (debugOut) debugOut->push_back(std::string(
"detail.append.helper.whole\t") + line.text);
590 const helpers::ColumnGuess& cols,
592 std::vector<std::string>& debugOut)
noexcept
595 auto mainRow = helpers::splitMainRowFromOcrLine(line, cols.valutaX, cols.debitX, cols.creditX);
596 std::ostringstream selection;
597 selection <<
"tx.main.split\tline?\tvalutaX=" << cols.valutaX <<
"\tdebitX=" << cols.debitX <<
"\tcreditX=" << cols.creditX;
598 debugOut.push_back(selection.str());
599 if (isGeom) debugOut.push_back(std::string(
"tx.main.geom\t") + line.text);
600 debugOut.push_back(std::string(
"tx.main\t") + line.text);
601 appendDebugValue(debugOut,
"tx.main.left\t", mainRow.left.line.text);
602 appendDebugValue(debugOut,
"tx.main.valuta\t", mainRow.valuta.line.text);
603 appendDebugValue(debugOut,
"tx.main.debit\t", mainRow.debit.line.text);
604 appendDebugValue(debugOut,
"tx.main.credit\t", mainRow.credit.line.text);
611 const std::vector<OcrLine>& orphans,
614 std::vector<std::string>* debugOut)
noexcept
617 for (
const auto& orphan : orphans) {
619 int bestGap = std::numeric_limits<int>::max();
620 for (
size_t blockIndex = 0; blockIndex < blocks.size(); ++blockIndex) {
621 const auto& block = blocks[blockIndex];
624 if (orphan.minY < mainBottom)
continue;
625 const int gap = orphan.minY - mainBottom;
626 if (gap <= maxGapPx && gap < bestGap) {
628 bestBlock =
static_cast<int>(blockIndex);
633 if (debugOut) debugOut->push_back(std::string(
"orphan.unmatched\ttext=") + orphan.text);
637 const bool crossesValuta = valutaX >= 0 && orphan.maxX >= valutaX + 40;
638 const bool hasAmountLike = crossesValuta && helpers::hasAmountLikeTokenInLine(orphan, valutaX);
639 if (!crossesValuta || !hasAmountLike) {
640 appendDetailLine(blocks[
static_cast<size_t>(bestBlock)], orphan, {valutaX, -1, -1}, debugOut);
641 if (debugOut) debugOut->push_back(std::string(
"orphan.attach\tblock=") + std::to_string(bestBlock) + std::string(
"\tgap=") + std::to_string(bestGap) + std::string(
"\ttext=") + orphan.text);
642 }
else if (debugOut) {
643 debugOut->push_back(std::string(
"orphan.drop\treason=amount-column-overlap\ttext=") + orphan.text);
651 const std::vector<TransactionBlock>& blocks,
652 int txStartLooseCount,
656 std::ostringstream summary;
657 summary <<
"page.summary\tocr.lines=" << lines.size() <<
"\theader.prebottomY=" << headerBottomY <<
"\tblocks=" << blocks.size() <<
"\ttxStartLoose=" << txStartLooseCount;
675 out.
debugLines.push_back(std::string(
"cols.state\tvalutaX=") + std::to_string(cols.
valutaX) + std::string(
"\tdebitX=") + std::to_string(cols.
debitX) + std::string(
"\tcreditX=") + std::to_string(cols.
creditX));
686 if (ocr.
tables.empty() || ocr.
tables[0].cells.empty())
return;
688 const auto& table = ocr.
tables[0];
690 const int cellTop = cell.bbox.y;
691 const int cellBottom = cell.bbox.y + cell.bbox.height;
695 bool usedCell =
false;
697 for (
const auto& cell : table.cells) {
698 if (cell.col != cols.
creditCol || !overlapsLine(cell))
continue;
699 out.
debugLines.push_back(std::string(
"cell.amount_used\t") + cell.text);
703 out.
debugLines.push_back(std::string(
"cell.override->") + std::to_string(tx.
amount));
710 for (
const auto& cell : table.cells) {
711 if (cell.col != cols.
debitCol || !overlapsLine(cell))
continue;
712 out.
debugLines.push_back(std::string(
"cell.amount_used\t") + cell.text);
714 tx.
amount = -std::abs(*value);
715 out.
debugLines.push_back(std::string(
"cell.override->") + std::to_string(tx.
amount));
726 const std::shared_ptr<core::ports::document_image_processing::IDocumentImageProcessor>& documentImageProcessor,
727 const std::string& pageCropImagePath,
728 const std::vector<uint8_t>& pageCropImageBytes,
732 if (!documentImageProcessor || (pageCropImagePath.empty() && pageCropImageBytes.empty()))
return;
734 int minX = std::numeric_limits<int>::max();
735 int maxX = std::numeric_limits<int>::min();
736 int minY = std::numeric_limits<int>::max();
737 int maxY = std::numeric_limits<int>::min();
739 auto accBounds = [&](
const OcrLine& line) {
740 minX = std::min(minX, line.minX);
741 maxX = std::max(maxX, line.maxX);
742 minY = std::min(minY, line.minY);
743 maxY = std::max(maxY, line.maxY);
747 for (
const auto& line : block.
detailLines) accBounds(line);
748 if (minX == std::numeric_limits<int>::max())
return;
751 if (!pageCropImagePath.empty()) request.
imagePath = std::filesystem::path(pageCropImagePath);
754 request.
filePrefix = std::string(
"document_image_processing_proof_tx") + std::to_string(txIndex);
758 request.
bbox.
y = std::max(0, minY - 20);
760 request.
bbox.
height = std::max(1, (maxY - minY) + 24);
764 const auto response = documentImageProcessor->crop(request);
765 out.
debugLines.push_back(std::string(
"crop.result.count\t") + std::to_string(response.croppedImagePaths.size()));
766 if (!response.croppedImageBytes.empty() && !response.croppedImageBytes.front().empty()) {
769 }
catch (
const std::exception& ex) {
770 out.
debugLines.push_back(std::string(
"crop.exception\t") + ex.what());
772 out.
debugLines.push_back(std::string(
"crop.exception\tunknown"));
780 const std::shared_ptr<core::ports::document_image_processing::IDocumentImageProcessor>& documentImageProcessor,
781 const std::string& pageCropImagePath,
782 const std::vector<uint8_t>& pageCropImageBytes,
786 for (
const auto& block : blocks) {
791 std::vector<std::string> txDebug;
793 for (
const auto& line : txDebug) out.
debugLines.push_back(std::string(
"txdbg\t") + line);
796 tx.
name = parsed.name;
798 tx.
valuta = parsed.valuta;
799 tx.
amount = parsed.amount;
800 const auto blockMetadata = fullBlockMetadata(block);
801 tx.
metadata = !blockMetadata.empty() ? blockMetadata : parsed.metadata;
804 attachProofCrop(tx, block, txIndex, documentImageProcessor, pageCropImagePath, pageCropImageBytes, out);
Declares helpers for parsing imported transaction amount strings.
Declares the default transaction parser used by the import pipeline.
Declares low-level OCR helper routines for statement parsing.
Declares helpers for the statement parsing workflow.
Declares private string helpers shared across core implementation files.
Declares private unique identifier helpers for transient import/job artifacts.
std::vector< uint8_t > proofImageData
std::pair< int, bool > detectHeaderRegion(const std::vector< core::application::importing::transaction::internal::OcrLine > &lines, size_t scanLines)
Detects the most likely header region in the supplied OCR lines.
static void attachProofCrop(core::application::importing::draft::TransactionDraft &tx, const TransactionBlock &block, int txIndex, const std::shared_ptr< core::ports::document_image_processing::IDocumentImageProcessor > &documentImageProcessor, const std::string &pageCropImagePath, const std::vector< uint8_t > &pageCropImageBytes, DefaultStatementParser::ParseResult &out)
void appendPageSummary(const std::vector< core::application::importing::statement::internal::RawLine > &lines, int headerBottomY, const std::vector< core::application::importing::transaction::internal::TransactionBlock > &blocks, int txStartLooseCount, core::application::importing::statement::DefaultStatementParser::ParseResult &out)
Appends a page-level summary to the parser result.
bool isLikelyTransactionMainRowGeom(const core::application::importing::statement::internal::RawLine &line, const core::application::importing::statement::internal::ColumnModel &cols)
Checks whether a raw line likely contains a main transaction row.
HeaderAnalysis analyzeHeaderWindow(const std::vector< core::application::importing::transaction::internal::OcrLine > &ocrLines, const std::vector< core::application::importing::statement::internal::RawLine > &lines, const core::application::importing::statement::internal::ColumnModel &seedCols, core::application::importing::statement::DefaultStatementParser::ParseResult &out)
Analyzes the page header window and updates parser output state.
core::application::importing::transaction::internal::OcrLine rawToOcrLine(const core::application::importing::statement::internal::RawLine &line)
Converts a raw OCR line into a parser line model.
static bool looksLikeRescuedMainLine(const OcrLine &line, const ColumnModel &cols)
void attachOrphansToBlocks(std::vector< core::application::importing::transaction::internal::TransactionBlock > &blocks, const std::vector< core::application::importing::transaction::internal::OcrLine > &orphans, int maxGapPx, int valutaX, std::vector< std::string > *debugOut=nullptr) noexcept
Attaches orphan OCR lines to existing transaction blocks.
static void appendBlockDebug(const TransactionBlock &block, const ColumnModel &cols, DefaultStatementParser::ParseResult &out)
std::optional< std::string > findBookingDateInHeader(const std::string &line) noexcept
Extracts a booking date from a header line.
void rescueOrphanBlocks(std::vector< core::application::importing::transaction::internal::TransactionBlock > &blocks, const std::vector< core::application::importing::transaction::internal::OcrLine > &orphanLines, const core::application::importing::statement::internal::ColumnModel &cols, const std::string ¤tBookingDate, core::application::importing::statement::DefaultStatementParser::ParseResult &out)
Reprocesses orphan transaction blocks against the current booking date and column model.
void appendDetailLine(core::application::importing::transaction::internal::TransactionBlock &cur, const core::application::importing::transaction::internal::OcrLine &line, const core::application::importing::internal::ColumnGuess &cols, std::vector< std::string > *debugOut=nullptr) noexcept
Appends a detail line to the current transaction block.
std::optional< std::pair< core::application::importing::transaction::internal::TransactionMainRow, int > > tryVerticalStart(const std::vector< core::application::importing::transaction::internal::OcrLine > &lines, size_t li, const core::application::importing::internal::ColumnGuess &cols) noexcept
Attempts to start a transaction block from a vertically aligned line.
static void applyCellAmountOverride(core::application::importing::draft::TransactionDraft &tx, const TransactionBlock &block, const ColumnModel &cols, const core::ports::text_recognition::ExtractResult &ocr, DefaultStatementParser::ParseResult &out)
std::optional< std::string > findDefaultBookingDate(const std::vector< core::application::importing::transaction::internal::OcrLine > &lines, size_t scanLines) noexcept
Finds a default booking date near the top of the parsed page.
bool detectEarlyEmptyPage(const std::vector< core::application::importing::transaction::internal::OcrLine > &lines, std::string &outDebug) noexcept
Detects whether a page is effectively empty before parsing continues.
std::vector< core::application::importing::internal::RawLineLite > selectiveGroupMergeLinesRaw(const std::vector< core::application::importing::internal::RawLineLite > &lines, int maxGapPx, const core::application::importing::internal::ColumnGuess &seedCols)
Merges nearby raw lines using the parser's selective grouping heuristic.
bool isLikelyTransactionHeaderLine(const core::application::importing::transaction::internal::OcrLine &line, const core::application::importing::statement::internal::ColumnModel &cols)
Checks whether an OCR line likely represents a transaction header row.
core::application::importing::transaction::internal::TransactionMainRow handleMainRow(const core::application::importing::transaction::internal::OcrLine &line, const core::application::importing::internal::ColumnGuess &cols, bool isGeom, std::vector< std::string > &debugOut) noexcept
Converts one OCR line into a transaction main row.
void appendTransactionsFromBlocks(const std::vector< core::application::importing::transaction::internal::TransactionBlock > &blocks, const core::application::importing::statement::internal::ColumnModel &cols, const core::ports::text_recognition::ExtractResult &ocr, const std::shared_ptr< core::ports::document_image_processing::IDocumentImageProcessor > &documentImageProcessor, const std::string &pageCropImagePath, const std::vector< uint8_t > &pageCropImageBytes, int &txIndex, core::application::importing::statement::DefaultStatementParser::ParseResult &out)
Converts parsed transaction blocks into drafts and extraction artifacts.
std::optional< std::pair< core::application::importing::transaction::internal::TransactionMainRow, int > > tryCombinedStart(const std::vector< core::application::importing::transaction::internal::OcrLine > &lines, size_t li, const core::application::importing::internal::ColumnGuess &cols) noexcept
Attempts to start a transaction block from a combined text pattern.
std::optional< double > parseAmountString(const std::string &s)
std::vector< std::string > tokens(const std::string &text)
Splits normalized text into tokens.
std::string makeTransientId()
Generates a transient unique identifier for import and job artifacts.
std::vector< std::string > splitWhitespace(const std::string &s)
Splits a string on ASCII whitespace.
std::string collapseWhitespace(std::string s)
Collapses consecutive whitespace to single spaces and trims the result.
std::string trim(std::string s)
Removes leading and trailing ASCII whitespace from a string.
Stores the inferred column positions for an OCR page.
Lightweight raw-line representation used during grouping and splitting.
Represents the inferred transaction column layout for a page.
Holds the parser output for one imported statement page.
std::vector< std::string > debugLines
std::vector< core::application::importing::draft::TransactionDraft > transactions
static DefaultTransactionParser parseTransaction(const TransactionBlock &block, std::vector< std::string > *debugOut=nullptr)
Parses a transaction block into a structured transaction model.
std::vector< std::pair< int, int > > wordSpans
Represents a merged raw OCR line before it is turned into parser lines.
std::vector< std::pair< int, int > > wordSpans
std::vector< std::pair< int, int > > wordSpans
std::vector< OcrLine > detailLines
Represents one OCR line with its bounding box and recognized text.
std::vector< std::pair< int, int > > wordSpans
Groups the main row and detail rows of one parsed transaction block.
std::string bookingDateGroup
std::vector< OcrLine > detailLines
Represents the parsed main row of a transaction block.
enum core::ports::document_image_processing::CropRequest::OutputFormat outputFormat
std::filesystem::path imagePath
std::vector< uint8_t > imageBytes