55 const std::string& pageCropImagePath,
56 std::shared_ptr<core::ports::document_image_processing::IDocumentImageProcessor> documentImageProcessor,
57 const std::vector<uint8_t>& pageCropImageBytes,
58 std::string initialBookingDate,
59 int initialTransactionIndex) {
63 out.
debugLines.push_back(std::string(
"pageCropImagePath\t") + pageCropImagePath);
64 out.
debugLines.push_back(std::string(
"pageCropImageBytes\t") + std::to_string(pageCropImageBytes.size()));
65 out.
debugLines.push_back(std::string(
"initialBookingDate\t") + initialBookingDate);
66 out.
debugLines.push_back(std::string(
"initialTransactionIndex\t") + std::to_string(initialTransactionIndex));
68 const auto ocrLines = helpers::buildOcrLinesFromWords(ocr.
words);
69 out.
debugLines.push_back(std::string(
"ocr.words\t") + std::to_string(ocr.
words.size()));
70 out.
debugLines.push_back(std::string(
"ocr.lines.raw\t") + std::to_string(ocrLines.size()));
72 const auto seed = helpers::inferColumnModelFromLines(ocrLines);
75 std::vector<core::application::importing::internal::RawLineLite> rawLite;
76 rawLite.reserve(ocrLines.size());
77 for (
const auto& l : ocrLines) rawLite.push_back({ l.minX, l.maxX, l.minY, l.maxY, l.wordSpans, l.text });
80 std::vector<core::application::importing::statement::internal::RawLine> lines;
81 lines.reserve(merged.size());
83 out.
debugLines.push_back(std::string(
"ocr.lines\t") + std::to_string(lines.size()));
85 std::vector<core::application::importing::transaction::internal::OcrLine> ocrFromRaw;
86 ocrFromRaw.reserve(lines.size());
89 std::vector<std::pair<size_t,std::string>> pageHeaderDates;
91 for (
size_t i = 0; i < lines.size(); ++i) {
94 pageHeaderDates.emplace_back(i, *bd);
100 std::string currentBookingDate = std::move(initialBookingDate);
101 int txIndex = std::max(1, initialTransactionIndex);
103 size_t headerScanPtr = 0;
109 out.
debugLines.push_back(std::string(
"page.emptyDetected\tlines=") + std::to_string(12));
114 if (!dbg.empty()) out.
debugLines.push_back(dbg);
117 std::vector<core::application::importing::transaction::internal::TransactionBlock> blocks;
121 bool inTransactions = !currentBookingDate.empty();
126 static constexpr size_t headerScanLines = 12;
129 const int pageMaxY = headerAnalysis.
pageMaxY;
130 const bool headerFound = headerAnalysis.
headerFound;
132 if (currentBookingDate.empty()) {
134 currentBookingDate = *d;
135 out.
debugLines.push_back(std::string(
"header.defaultBookingDate\t") + currentBookingDate);
140 const auto flush = [&]() {
142 blocks.push_back(std::move(cur));
147 std::string prevLine;
148 int txStartLooseCount = 0;
150 std::vector<core::application::importing::transaction::internal::OcrLine> orphanLines;
152 for (
size_t li = 0; li < lines.size(); ++li) {
153 const auto& l = lines[li];
154 const auto txt = l.text;
155 const auto combined = prevLine.empty() ? txt : (prevLine +
" " + txt);
156 if (txt.empty())
continue;
158 if (headerScanPtr < pageHeaderDates.size() && pageHeaderDates[headerScanPtr].first == li) {
159 currentBookingDate = pageHeaderDates[headerScanPtr].second;
160 out.
debugLines.push_back(std::string(
"header.foundOnPage\tline=") + std::to_string(li) + std::string(
"\t") + currentBookingDate);
162 inTransactions =
true;
168 while (headerScanPtr < pageHeaderDates.size() && pageHeaderDates[headerScanPtr].first < li) ++headerScanPtr;
170 if (isFooterLine(txt)) { out.
debugLines.push_back(std::string(
"stop.footer\t") + txt +
"\tline=" + std::to_string(li));
break; }
171 if (isPostTransactionFootnote(txt)) {
172 bool nearBottom =
false;
173 try {
if (pageMaxY >= 0 && l.maxY >=
static_cast<int>(pageMaxY * 3 / 4)) nearBottom =
true; }
catch (...) { }
175 out.
debugLines.push_back(std::string(
"footnote.check\tpageMaxY=") + std::to_string(pageMaxY) + std::string(
"\tlineY=") + std::to_string(l.maxY) + std::string(
"\tnearBottom=") + (nearBottom ?
"1" :
"0") + std::string(
"\tblocks=") + std::to_string(blocks.size()));
178 bool reallyTerminal =
false;
179 try {
if (!blocks.empty() && nearBottom) reallyTerminal =
true; }
catch (...) { }
180 if (reallyTerminal) {
181 out.
debugLines.push_back(std::string(
"stop.footnote\t") + txt +
"\tline=" + std::to_string(li));
184 out.
debugLines.push_back(std::string(
"footnote.ignored\t") + txt +
"\tline=" + std::to_string(li));
189 if (headerBottomY >= 0 && l.maxY <= headerBottomY + headerMarginPx && !inTransactions) {
193 if (helpers::hasTokenNearX(anchorOl, seedCols.
valutaX, helpers::parserConfig.tokenNearBandForMainRow) || helpers::hasAmountNearValuta(anchorOl, seedCols.
valutaX, helpers::parserConfig.amountNearValutaBandPx)) {
194 out.
debugLines.push_back(std::string(
"header.overrideByValutaAnchor\tline=") + std::to_string(li) +
"\ttext=" + txt);
201 bool hasHeaderSignal =
false;
209 if (isValutaHeaderLine(txt)) hasHeaderSignal =
true;
212 if (!hasHeaderSignal && !(rows.inSection && cols.
hasValuta())) {
213 bool combinedStarted =
false;
215 std::string combPrev = prevLine.empty() ? txt : (prevLine + std::string(
" ") + txt);
216 std::string combNext = txt;
217 if (li + 1 < lines.size()) combNext = txt + std::string(
" ") + lines[li+1].text;
220 inTransactions =
true;
221 out.
debugLines.push_back(std::string(
"tx.start.combined.aboveHeader\tline=") + std::to_string(li) +
"\ttext=" + (cs->second ? combNext : combPrev));
222 combinedStarted =
true;
226 if (combinedStarted) {
230 bool allowAboveHeader =
false;
231 try {
if (helpers::hasAmountNearValuta(ol, cols.
valutaX, helpers::parserConfig.amountNearValutaBandPx)) allowAboveHeader =
true; }
catch (...) { }
232 try {
if (!allowAboveHeader && helpers::isLooseTransactionLine(ol, cols.
valutaX)) allowAboveHeader =
true; }
catch (...) { }
233 if (allowAboveHeader) {
234 inTransactions =
true;
236 out.
debugLines.push_back(std::string(
"tx.start.aboveHeaderDetected\tline=") + std::to_string(li) +
"\ttext=" + txt);
238 out.
debugLines.push_back(std::string(
"line.reason\taboveHeaderSkip\t") + txt +
"\tline=" + std::to_string(li));
244 out.
debugLines.push_back(std::string(
"line.reason\taboveHeaderSkip\t") + txt +
"\tline=" + std::to_string(li));
259 out.
debugLines.push_back(std::string(
"header.debitcredit\t") + txt +
"\tline=" + std::to_string(li));
260 out.
debugLines.push_back(std::string(
"cols.debitX\t") + std::to_string(cols.
debitX) +
"\tcols.creditX\t" + std::to_string(cols.
creditX));
261 try { headerBottomY = std::max(headerBottomY, l.maxY); }
catch (...) { }
266 if (!rows.inSection && (helpers::isTransactionsSectionHeader(txt) || isValutaHeaderLine(txt) || helpers::isTransactionsSectionHeader(combined))) {
267 rows.inSection =
true;
271 out.
debugLines.push_back(std::string(
"header.section\t") + combined +
"\tline=" + std::to_string(li));
272 out.
debugLines.push_back(std::string(
"cols.valutaX\t") + std::to_string(cols.
valutaX));
273 try { headerBottomY = std::max(headerBottomY, l.maxY); }
catch (...) { }
279 currentBookingDate = *bd;
280 out.
debugLines.push_back(std::string(
"header.bookingDate\t") + currentBookingDate +
"\tline=" + std::to_string(li));
283 try { headerBottomY = std::max(headerBottomY, l.maxY); }
catch (...) { }
288 bool headerNoiseSkipped =
false;
290 if (!inTransactions && headerFound && helpers::isHeaderNoiseLine(txt)) {
291 bool anchoredToValuta =
false;
294 bool looksMain =
false;
297 if (helpers::isLooseTransactionLine(_ol, seedCols.
valutaX)) looksMain =
true;
298 if (helpers::hasAmountLikeTokenInLine(_ol, seedCols.
valutaX)) looksMain =
true;
299 if (helpers::hasShortDateToken(_ol.
text) && helpers::hasLeftDescriptiveText(_ol, seedCols.
valutaX)) looksMain =
true;
302 if (!anchoredToValuta && !looksMain && !isTransactionsSectionHeader(txt) && !isDebitCreditHeaderLine(txt) && !isLikelyTransactionMainRowText(txt)) {
303 out.
debugLines.push_back(std::string(
"line.skip.headerNoise\t") + txt + std::string(
"\tline=") + std::to_string(li));
305 headerNoiseSkipped =
true;
307 out.
debugLines.push_back(std::string(
"header.noise_filter.suppressed\tline=") + std::to_string(li));
311 if (headerNoiseSkipped)
continue;
313 if (!inTransactions) {
314 const bool canStart = (!currentBookingDate.empty()) || (rows.inSection && cols.
hasValuta());
315 bool didVerticalStart =
false;
317 if (canStart && rows.inSection && cols.
hasValuta()) {
319 inTransactions =
true; didVerticalStart =
true;
322 cur.
main = vs->first;
323 if (vs->second > 0) {
324 out.
debugLines.push_back(std::string(
"tx.main.vertical.helper\tformed\tline=") + std::to_string(li));
327 out.
debugLines.push_back(std::string(
"tx.main.vertical.helper\tformed\tline=") + std::to_string(li));
333 if (canStart && (isLikelyTransactionMainRowText(txt) || isLikelyTransactionMainRowGeom(l, cols))) {
334 inTransactions =
true;
335 out.
debugLines.push_back(std::string(
"tx.start\tline=") + std::to_string(li) +
"\ttext=" + txt);
336 }
else if (didVerticalStart) {
338 bool startedCombined =
false;
340 std::string combPrev = prevLine.empty() ? txt : (prevLine + std::string(
" ") + txt);
341 std::string combNext = txt;
342 if (li + 1 < lines.size()) combNext = txt + std::string(
" ") + lines[li+1].text;
345 inTransactions =
true; startedCombined =
true;
346 out.
debugLines.push_back(std::string(
"tx.start.combined\tline=") + std::to_string(li) +
"\ttext=" + (cs->second ? combNext : combPrev));
350 if (startedCombined) {
352 bool relaxedStart =
false;
355 bool hasDate =
false;
356 try {
if (helpers::hasShortDateToken(ol.
text)) hasDate =
true; }
catch (...) { }
357 bool hasAmount =
false;
358 try {
if (helpers::hasAmountNearValuta(ol, cols.
valutaX, helpers::parserConfig.amountNearValutaBandPx)) hasAmount =
true; }
catch (...) { }
363 bool hasLeftDesc =
false;
364 try {
if (helpers::hasLeftDescriptiveText(ol, cols.
valutaX)) hasLeftDesc =
true; }
catch (...) { }
365 if (hasDate && (hasAmount || hasLeftDesc)) relaxedStart =
true;
368 inTransactions =
true;
369 out.
debugLines.push_back(std::string(
"tx.start.relaxed\tline=") + std::to_string(li) +
"\ttext=" + txt);
371 inTransactions =
true;
372 out.
debugLines.push_back(std::string(
"tx.start.forced\tline=") + std::to_string(li) +
"\ttext=" + txt);
384 const bool mainByRegex = isLikelyTransactionMainRowText(txt);
388 if (mainByRegex || mainByGeom) {
408 for (
const auto& ol : orphanLines) {
410 bool looksMain =
false;
419 blocks.push_back(std::move(nb));
420 out.
debugLines.push_back(std::string(
"tx.start.rescued\ttext=") + ol.text);
428 helpers::parserConfig.orphanAttachMaxGapPx,
432 out.
debugLines.push_back(std::string(
"blocks\t") + std::to_string(blocks.size()));