FOSSredder 0.5.0
Loading...
Searching...
No Matches
ParserHelpers.cpp
Go to the documentation of this file.
1
5
8
10using core::application::importing::transaction::internal::OcrLine;
11using core::application::importing::transaction::internal::TransactionMainRow;
13namespace {
14static const std::regex
15 g_amountRegex(R"(^\‍(?-?\d{1,3}(?:[\.,]\d{3})*[\.,]\d{1,2}-?$)");
16static const std::regex g_amountFallbackRegex(R"(^\‍(?-?\d+[\.,]\d{2}\)?$)");
17static constexpr std::string_view g_narrowNoBreakSpaceUtf8 = "\xE2\x80\xAF";
18static constexpr std::string_view g_thinSpaceUtf8 = "\xE2\x80\x89";
19
20bool tokenLooksLikeAmount(const std::string &token) noexcept {
21 try {
22 return std::regex_match(token, g_amountRegex) ||
23 std::regex_match(token, g_amountFallbackRegex);
24 } catch (...) {
25 return false;
26 }
27}
28
29void eraseAll(std::string &value, const std::string_view token) {
30 size_t position = 0;
31 while ((position = value.find(token, position)) != std::string::npos) {
32 value.erase(position, token.size());
33 }
34}
35
36} // namespace
37
38std::vector<size_t> findAmountTokenIndices(const OcrLine &line,
39 int valutaX, int bandPx) noexcept {
40 std::vector<size_t> out;
41 try {
42 const auto toks = core::utils::splitWhitespace(line.text);
43 for (size_t i = 0; i < toks.size(); ++i) {
44 try {
45 if (!tokenLooksLikeAmount(toks[i]))
46 continue;
47 } catch (...) {
48 continue;
49 }
50 if (valutaX >= 0 && bandPx > 0) {
51 if (i >= line.wordSpans.size())
52 continue;
53 const auto &sp = line.wordSpans[i];
54 const int cx = (sp.first + sp.second) / 2;
55 if (std::abs(cx - valutaX) > bandPx)
56 continue;
57 }
58 out.push_back(i);
59 }
60 } catch (...) {}
61 return out;
62}
63
64std::string normalizeAlnumLower(const std::string &s) noexcept {
65 std::string o;
66 o.reserve(s.size());
67 for (unsigned char c : core::utils::lowerAscii(s)) {
68 if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'))
69 o.push_back(static_cast<char>(c));
70 }
71 return o;
72}
73
74std::optional<int> findTokenCenterX(const OcrLine &line,
75 const std::string &tokenLower) noexcept {
76 const auto toks = core::utils::splitWhitespace(line.text);
77 if (toks.size() != line.wordSpans.size())
78 return std::nullopt;
79
80 const auto want = normalizeAlnumLower(tokenLower);
81 for (size_t i = 0; i < toks.size(); ++i) {
82 if (normalizeAlnumLower(toks[i]) != want)
83 continue;
84 const auto &sp = line.wordSpans[i];
85 return (sp.first + sp.second) / 2;
86 }
87
88 for (size_t i = 0; i < toks.size(); ++i) {
89 const auto n = normalizeAlnumLower(toks[i]);
90 if (n.find(want) == std::string::npos)
91 continue;
92 const auto &sp = line.wordSpans[i];
93 return (sp.first + sp.second) / 2;
94 }
95
96 return std::nullopt;
97}
98
99std::optional<int>
101 std::initializer_list<const char *> phraseLower) noexcept {
102 const auto toks = core::utils::splitWhitespace(line.text);
103 if (toks.size() != line.wordSpans.size())
104 return std::nullopt;
105
106 std::string want;
107 for (auto w : phraseLower)
108 want += normalizeAlnumLower(std::string(w));
109 if (want.empty() || toks.empty())
110 return std::nullopt;
111
112 for (size_t i = 0; i < toks.size(); ++i) {
113 std::string acc;
114 int left = line.wordSpans[i].first;
115 int right = line.wordSpans[i].second;
116
117 for (size_t j = i;
118 j < toks.size() &&
119 (j - i) < static_cast<size_t>(parserConfig.maxPhraseTokens);
120 ++j) {
121 acc += normalizeAlnumLower(toks[j]);
122 right = line.wordSpans[j].second;
123 if (acc.find(want) != std::string::npos) {
124 return (left + right) / 2;
125 }
126 }
127 }
128
129 return std::nullopt;
130}
131
132bool hasTokenNearX(const OcrLine &line, int x,
133 int bandPx) noexcept {
134 if (x < 0)
135 return false;
136 const auto toks = core::utils::splitWhitespace(line.text);
137 if (toks.size() != line.wordSpans.size() || toks.empty())
138 return false;
139
140 for (size_t i = 0; i < toks.size(); ++i) {
141 const auto &sp = line.wordSpans[i];
142 const int cx = (sp.first + sp.second) / 2;
143 if (std::abs(cx - x) <= bandPx)
144 return true;
145 }
146 return false;
147}
148
150 int valutaX) noexcept {
151 try {
152 if (valutaX >= 0) {
153 return !findAmountTokenIndices(line, valutaX,
154 parserConfig.amountNearValutaBandPx)
155 .empty();
156 }
157 return !findAmountTokenIndices(line, -1, 0).empty();
158 } catch (...) {}
159 return false;
160}
161
163 int valutaX) noexcept {
164 try {
165 const auto toks = core::utils::splitWhitespace(line.text);
166 if (toks.empty())
167 return false;
168 for (size_t i = 0; i < toks.size() && i < line.wordSpans.size(); ++i) {
169 const auto &sp = line.wordSpans[i];
170 const int cx = (sp.first + sp.second) / 2;
171 if (cx < valutaX - parserConfig.leftDescriptiveOffsetPx) {
172 for (unsigned char c : toks[i])
173 if (std::isalpha(c))
174 return true;
175 }
176 }
177 for (unsigned char c : toks[0])
178 if (std::isalpha(c))
179 return true;
180 } catch (...) {}
181 return false;
182}
183
184bool hasAmountNearValuta(const OcrLine &line, int valutaX,
185 int bandPx) noexcept {
186 if (valutaX < 0)
187 return false;
188 try {
189 auto idxs = findAmountTokenIndices(line, valutaX, bandPx);
190 return !idxs.empty();
191 } catch (...) {}
192 return false;
193}
194
196 int valutaX) noexcept {
197 if (valutaX < 0)
198 return false;
199 try {
200 if (!hasShortDateToken(line.text))
201 return false;
202 std::smatch md;
203 if (!std::regex_search(line.text, md, std::regex(R"((\d{2}\.\s*\d{2}))")))
204 return false;
205 const size_t datePos = static_cast<size_t>(md.position(0));
206 const auto toks = core::utils::splitWhitespace(line.text);
207 bool foundAmountAfterDate = false;
208 auto amtIdxs = findAmountTokenIndices(line, -1, 0);
209 for (auto i : amtIdxs) {
210 if (i >= toks.size())
211 continue;
212 const auto pos = line.text.find(toks[i]);
213 if (pos != std::string::npos && pos > datePos) {
214 foundAmountAfterDate = true;
215 break;
216 }
217 }
218 if (!foundAmountAfterDate)
219 return false;
220 const auto left = line.text.substr(0, datePos);
221 for (unsigned char c : left) {
222 if (std::isalpha(c))
223 return true;
224 }
225 } catch (...) {}
226 return false;
227}
228
229std::optional<double>
230findAndParseAmountInLine(const OcrLine &line, int valutaX,
231 std::vector<std::string> *debugOut) noexcept {
232 try {
233 int band = (valutaX >= 0) ? parserConfig.amountNearValutaBandPx : 0;
234 auto idxs = findAmountTokenIndices(line, valutaX, band);
235 if (idxs.empty()) {
236 idxs = findAmountTokenIndices(line, -1, 0);
237 }
238 const auto toks = core::utils::splitWhitespace(line.text);
239 auto tryParse = [&](const std::string &s) -> std::optional<double> {
240 try {
241 if (s.empty())
242 return std::nullopt;
243 if (debugOut)
244 debugOut->push_back(std::string("candidate.try\t") + s);
245
246 try {
247 if (hasShortDateToken(s)) {
248 if (debugOut)
249 debugOut->push_back(std::string("candidate.skip.shortDate\t") +
250 s);
251 return std::nullopt;
252 }
253 } catch (...) {}
254
255 try {
256 std::string norm = s;
257 norm = core::utils::trim(norm);
258 bool negative = false;
259 if (!norm.empty() && norm.front() == '(' && norm.back() == ')') {
260 negative = true;
261 norm = norm.substr(1, norm.size() - 2);
262 }
263 if (!norm.empty() && norm.front() == '-') {
264 negative = true;
265 norm = norm.substr(1);
266 }
267 if (!norm.empty() && norm.back() == '-') {
268 negative = true;
269 norm.pop_back();
270 }
271
272 eraseAll(norm, g_narrowNoBreakSpaceUtf8);
273 eraseAll(norm, g_thinSpaceUtf8);
274
275 std::string tmp;
276 tmp.reserve(norm.size());
277 for (unsigned char c : norm) {
278 if (c == '.' || c == ' ')
279 continue;
280 tmp.push_back(static_cast<char>(c));
281 }
282 std::replace(tmp.begin(), tmp.end(), ',', '.');
283
284 try {
285 if (!tmp.empty()) {
286 double v = std::stod(tmp);
287 if (negative)
288 v = -v;
289 if (debugOut)
290 debugOut->push_back(std::string("candidate.ok.norm\t") + tmp +
291 std::string(" -> ") + std::to_string(v));
292 return v;
293 }
294 } catch (...) {}
295 } catch (...) {}
296
297 if (auto v =
299 if (debugOut)
300 debugOut->push_back(std::string("candidate.ok\t") + s +
301 std::string(" -> ") + std::to_string(*v));
302 return v;
303 }
304
305 std::string nosp = s;
306 nosp.erase(std::remove(nosp.begin(), nosp.end(), ' '), nosp.end());
307 if (!nosp.empty()) {
308 if (debugOut)
309 debugOut->push_back(std::string("candidate.try.nosp\t") + nosp);
310 if (auto v2 =
312 nosp)) {
313 if (debugOut)
314 debugOut->push_back(std::string("candidate.ok.nosp\t") + nosp +
315 std::string(" -> ") + std::to_string(*v2));
316 return v2;
317 }
318 }
319 } catch (...) {}
320 return std::nullopt;
321 };
322
323 if (debugOut)
324 debugOut->push_back(
325 std::string("helper.findAndParseAmountInLine\tline=") + line.text +
326 std::string(" idxs=") + std::to_string(idxs.size()));
327
328 for (auto i : idxs) {
329 if (i >= toks.size())
330 continue;
331 if (auto r = tryParse(toks[i]))
332 return r;
333 if (i > 0) {
334 if (auto r = tryParse(toks[i - 1] + toks[i]))
335 return r;
336 if (auto r2 = tryParse(toks[i - 1] + " " + toks[i]))
337 return r2;
338 }
339 for (int len = 1; len <= 2; ++len) {
340 std::string joined;
341 for (int k = 0; k <= len; ++k) {
342 int idx = static_cast<int>(i) + k;
343 if (idx >= static_cast<int>(toks.size()))
344 break;
345 if (!joined.empty())
346 joined.push_back(' ');
347 joined += toks[idx];
348 }
349 if (!joined.empty()) {
350 if (auto r = tryParse(joined))
351 return r;
352 }
353 }
354 if (i + 2 < toks.size()) {
355 std::string three = toks[i] + toks[i + 1] + toks[i + 2];
356 if (auto r = tryParse(three))
357 return r;
358 }
359 }
360 if (debugOut)
361 debugOut->push_back(std::string("candidate.try.whole\t") + line.text);
363 line.text)) {
364 if (debugOut)
365 debugOut->push_back(std::string("candidate.ok.whole\t") + line.text +
366 std::string(" -> ") + std::to_string(*v));
367 return v;
368 }
369 } catch (...) {}
370 return std::nullopt;
371}
372
373std::vector<OcrLine> buildOcrLinesFromWords(
374 const std::vector<core::ports::text_recognition::Word> &words) {
375 struct W {
377 int cy;
378 int top;
379 int bottom;
380 int left;
381 };
382 std::vector<W> ws;
383 ws.reserve(words.size());
384 for (const auto &w : words) {
385 bool anyNonSpace = false;
386 for (unsigned char c : w.text) {
387 if (!std::isspace(c)) {
388 anyNonSpace = true;
389 break;
390 }
391 }
392 if (!anyNonSpace)
393 continue;
394 const int cy = w.bbox.y + w.bbox.height / 2;
395 ws.push_back(W{&w, cy, w.bbox.y, w.bbox.y + w.bbox.height, w.bbox.x});
396 }
397 std::sort(ws.begin(), ws.end(), [](const W &a, const W &b) {
398 if (a.top != b.top)
399 return a.top < b.top;
400 if (a.cy != b.cy)
401 return a.cy < b.cy;
402 return a.left < b.left;
403 });
404 int avgH = 0;
405 for (const auto &ww : ws)
406 avgH += (ww.bottom - ww.top);
407 avgH = ws.empty() ? 10 : std::max(1, avgH / static_cast<int>(ws.size()));
408 const int minOverlap = std::max(2, avgH / 3);
409 struct LineAcc {
410 int cy = 0;
411 int minX = std::numeric_limits<int>::max();
412 int maxX = std::numeric_limits<int>::min();
413 int minY = std::numeric_limits<int>::max();
414 int maxY = std::numeric_limits<int>::min();
415 std::vector<const core::ports::text_recognition::Word *> words;
416 };
417 std::vector<LineAcc> acc;
418 acc.reserve(std::max<size_t>(8, words.size() / 8));
419 for (const auto &ww : ws) {
420 const auto &w = *ww.w;
421 const int wMinX = w.bbox.x;
422 const int wMaxX = w.bbox.x + w.bbox.width;
423 const int wMinY = w.bbox.y;
424 const int wMaxY = w.bbox.y + w.bbox.height;
425 int bestIdx = -1;
426 int bestOverlap = -1;
427 for (int i = static_cast<int>(acc.size()) - 1; i >= 0; --i) {
428 const auto &l = acc[static_cast<size_t>(i)];
429 const int top = std::max(l.minY, wMinY);
430 const int bot = std::min(l.maxY, wMaxY);
431 const int ov = bot - top;
432 if (ov >= minOverlap && ov > bestOverlap) {
433 bestOverlap = ov;
434 bestIdx = i;
435 }
436 if (wMinY - l.maxY > avgH)
437 break;
438 }
439 if (bestIdx < 0) {
440 LineAcc nl;
441 nl.cy = ww.cy;
442 nl.minX = wMinX;
443 nl.maxX = wMaxX;
444 nl.minY = wMinY;
445 nl.maxY = wMaxY;
446 nl.words.push_back(&w);
447 acc.push_back(std::move(nl));
448 continue;
449 }
450 auto &l = acc[static_cast<size_t>(bestIdx)];
451 l.cy = (l.cy + ww.cy) / 2;
452 l.minX = std::min(l.minX, wMinX);
453 l.maxX = std::max(l.maxX, wMaxX);
454 l.minY = std::min(l.minY, wMinY);
455 l.maxY = std::max(l.maxY, wMaxY);
456 l.words.push_back(&w);
457 }
458 std::vector<OcrLine> out;
459 out.reserve(acc.size());
460 for (auto &la : acc) {
461 std::sort(la.words.begin(), la.words.end(),
464 if (a->bbox.x != b->bbox.x)
465 return a->bbox.x < b->bbox.x;
466 return a->bbox.y < b->bbox.y;
467 });
468 OcrLine ol;
469 ol.minY = la.minY;
470 ol.maxY = la.maxY;
471 ol.wordSpans.reserve(la.words.size());
472 if (!la.words.empty()) {
473 ol.minX = la.words.front()->bbox.x;
474 ol.maxX = la.words.front()->bbox.x + la.words.front()->bbox.width;
475 } else {
476 ol.minX = 0;
477 ol.maxX = 0;
478 }
479 std::string txt;
480 for (const auto *w : la.words) {
481 const int left = w->bbox.x;
482 const int right = w->bbox.x + w->bbox.width;
483 ol.wordSpans.emplace_back(left, right);
484 ol.minX = std::min(ol.minX, left);
485 ol.maxX = std::max(ol.maxX, right);
486 if (!txt.empty())
487 txt.push_back(' ');
488 txt += w->text;
489 }
490 ol.text = core::utils::trim(std::move(txt));
491 out.push_back(std::move(ol));
492 }
493 return out;
494}
495
496ColumnGuess
497inferColumnModelFromLines(const std::vector<OcrLine> &lines,
498 size_t scanN) noexcept {
499 ColumnGuess out;
500 try {
501 const size_t n = std::min(lines.size(), scanN);
502 for (size_t i = 0; i < n; ++i) {
503 const auto &l = lines[i];
504 try {
505 if (out.valutaX < 0)
506 if (auto vx = findTokenCenterX(l, "valuta"))
507 out.valutaX = *vx;
508 } catch (...) {}
509 try {
510 if (out.debitX < 0)
511 if (auto dx = findPhraseCenterX(l, {"zu", "ihren", "lasten"}))
512 out.debitX = *dx;
513 } catch (...) {}
514 try {
515 if (out.creditX < 0)
516 if (auto cx = findPhraseCenterX(l, {"zu", "ihren", "gunsten"}))
517 out.creditX = *cx;
518 } catch (...) {}
519 if (out.valutaX >= 0 && out.debitX >= 0 && out.creditX >= 0)
520 break;
521 }
522 } catch (...) {}
523 return out;
524}
525
526std::vector<RawLineLite>
527groupMergeLinesRaw(const std::vector<RawLineLite> &lines, int maxGapPx) {
528 if (lines.size() <= 1)
529 return lines;
530 std::vector<RawLineLite> merged;
531 merged.reserve(lines.size());
532 RawLineLite cur = lines.front();
533 for (size_t i = 1; i < lines.size(); ++i) {
534 const auto &next = lines[i];
535 if (next.minY - cur.maxY <= maxGapPx) {
536 cur.maxY = next.maxY;
537 cur.wordSpans.insert(cur.wordSpans.end(), next.wordSpans.begin(),
538 next.wordSpans.end());
539 cur.text += " " + next.text;
540 } else {
541 merged.push_back(std::move(cur));
542 cur = next;
543 }
544 }
545 merged.push_back(std::move(cur));
546 return merged;
547}
548
550toOcrLineFromRawWords(const RawLineLite &src, size_t i0, size_t i1) noexcept {
552 if (i0 >= i1 || i0 >= src.wordSpans.size())
553 return out;
554 i1 = std::min(i1, src.wordSpans.size());
555 out.minY = src.minY;
556 out.maxY = src.maxY;
557 out.minX = src.wordSpans[i0].first;
558 out.maxX = src.wordSpans[i0].second;
559 out.wordSpans.reserve(i1 - i0);
560 for (size_t i = i0; i < i1; ++i) {
561 const auto &sp = src.wordSpans[i];
562 out.minX = std::min(out.minX, sp.first);
563 out.maxX = std::max(out.maxX, sp.second);
564 out.wordSpans.push_back(sp);
565 }
566 const auto toks = core::utils::splitWhitespace(src.text);
567 std::string txt;
568 for (size_t i = i0; i < i1 && i < toks.size(); ++i) {
569 if (!txt.empty())
570 txt.push_back(' ');
571 txt += toks[i];
572 }
573 out.text = core::utils::trim(std::move(txt));
574 return out;
575}
576
578splitMainRowFromRaw(const RawLineLite &src, int valutaX, int debitX,
579 int creditX) noexcept {
581 const auto toks = core::utils::splitWhitespace(src.text);
582 if (toks.size() != src.wordSpans.size() || toks.empty()) {
583 row.left.line.text = src.text;
584 row.left.line.minX = src.minX;
585 row.left.line.maxX = src.maxX;
586 row.left.line.minY = src.minY;
587 row.left.line.maxY = src.maxY;
588 row.left.line.wordSpans = src.wordSpans;
589 return row;
590 }
591 auto cxAt = [&](size_t i) -> int {
592 const auto &sp = src.wordSpans[i];
593 return (sp.first + sp.second) / 2;
594 };
595 auto idxNearX = [&](int x, int skipIdx) -> int {
596 int bestIdx = -1;
597 int bestDist = std::numeric_limits<int>::max();
598 for (size_t i = 0; i < toks.size(); ++i) {
599 if ((int)i == skipIdx)
600 continue;
601 int d = std::abs(cxAt(i) - x);
602 if (d < bestDist) {
603 bestDist = d;
604 bestIdx = (int)i;
605 }
606 }
607 return bestIdx;
608 };
609 auto idxNearXPreferNumeric = [&](int x, int skipIdx) -> int {
610 int bestIdx = -1;
611 int bestDist = std::numeric_limits<int>::max();
612 int bestPriority = -1;
613 for (size_t i = 0; i < toks.size(); ++i) {
614 if ((int)i == skipIdx)
615 continue;
616 int d = std::abs(cxAt(i) - x);
617 int priority = 0;
618 try {
619 if (tokenLooksLikeAmount(toks[i]))
620 priority = 2;
621 } catch (...) {}
622 if (priority > bestPriority ||
623 (priority == bestPriority && d < bestDist)) {
624 bestPriority = priority;
625 bestDist = d;
626 bestIdx = (int)i;
627 }
628 }
629 return bestIdx;
630 };
631
632 int valutaIdx = (valutaX >= 0) ? idxNearX(valutaX, -1) : -1;
633 int debitIdx = (debitX >= 0) ? idxNearXPreferNumeric(debitX, -1) : -1;
634 int creditIdx = (creditX >= 0) ? idxNearXPreferNumeric(creditX, -1) : -1;
635 try {
636 if (valutaIdx >= 0 && debitIdx < 0 && creditIdx < 0) {
637 int found = -1;
638 for (size_t i = 0; i < toks.size(); ++i) {
639 if (cxAt(i) <= valutaX - 120)
640 continue;
641 try {
642 if (tokenLooksLikeAmount(toks[i])) {
643 found = (int)i;
644 break;
645 }
646 } catch (...) {}
647 }
648 if (found >= 0) {
649 if (creditX >= 0)
650 creditIdx = found;
651 else if (debitX >= 0)
652 debitIdx = found;
653 else
654 creditIdx = found;
655 }
656 }
657 } catch (...) {}
658 if (valutaIdx >= 0) {
659 if (debitIdx == valutaIdx && debitX >= 0)
660 debitIdx = idxNearX(debitX, valutaIdx);
661 if (creditIdx == valutaIdx && creditX >= 0)
662 creditIdx = idxNearX(creditX, valutaIdx);
663 }
664 if (debitIdx >= 0 && creditIdx == debitIdx && creditX >= 0)
665 creditIdx = idxNearX(creditX, debitIdx);
666
667 int firstRight = std::numeric_limits<int>::max();
668 if (valutaIdx >= 0)
669 firstRight = std::min(firstRight, valutaIdx);
670 if (debitIdx >= 0)
671 firstRight = std::min(firstRight, debitIdx);
672 if (creditIdx >= 0)
673 firstRight = std::min(firstRight, creditIdx);
674 if (firstRight == std::numeric_limits<int>::max())
675 firstRight = (int)toks.size();
676
678 src, 0, static_cast<size_t>(std::max(0, firstRight)));
679 if (valutaIdx >= 0) {
680 size_t v0 = static_cast<size_t>(valutaIdx);
681 size_t v1 = v0 + 1;
682 if (v1 < toks.size() &&
683 std::abs(cxAt(v1) - valutaX) <= parserConfig.valutaNeighborExpandPx)
684 v1 = v1 + 1;
685 row.valuta.line = toOcrLineFromRawWords(src, v0, v1);
686 }
687 if (debitIdx >= 0)
688 row.debit.line = toOcrLineFromRawWords(src, static_cast<size_t>(debitIdx),
689 static_cast<size_t>(debitIdx + 1));
690 if (creditIdx >= 0)
691 row.credit.line = toOcrLineFromRawWords(src, static_cast<size_t>(creditIdx),
692 static_cast<size_t>(creditIdx + 1));
693 return row;
694}
695
699 int valutaX, int debitX, int creditX) noexcept {
700 RawLineLite rl;
701 rl.minX = src.minX;
702 rl.maxX = src.maxX;
703 rl.minY = src.minY;
704 rl.maxY = src.maxY;
705 rl.wordSpans = src.wordSpans;
706 rl.text = src.text;
707 return splitMainRowFromRaw(rl, valutaX, debitX, creditX);
708}
709
710} // namespace core::application::importing::internal
Declares low-level OCR helper routines for statement parsing.
Declares private string helpers shared across core implementation files.
std::vector< RawLineLite > groupMergeLinesRaw(const std::vector< RawLineLite > &lines, int maxGapPx)
Merges raw lines using a gap-based heuristic.
std::string normalizeAlnumLower(const std::string &s) noexcept
Normalizes alphanumeric text to lower-case ASCII for matching.
std::vector< core::application::importing::transaction::internal::OcrLine > buildOcrLinesFromWords(const std::vector< core::ports::text_recognition::Word > &words)
Builds OCR lines from raw Tesseract word output.
std::optional< int > findPhraseCenterX(const core::application::importing::transaction::internal::OcrLine &line, std::initializer_list< const char * > phraseLower) noexcept
Locates the horizontal center of a multi-token phrase in an OCR line.
ColumnGuess inferColumnModelFromLines(const std::vector< core::application::importing::transaction::internal::OcrLine > &lines, size_t scanN=16) noexcept
Infers a column layout from a set of OCR lines.
ParserConfig parserConfig
Shared parser configuration instance used by import parsing helpers.
core::application::importing::transaction::internal::OcrLine toOcrLineFromRawWords(const RawLineLite &src, size_t i0, size_t i1) noexcept
Converts a range of raw words into one OCR line.
bool isLooseTransactionLine(const core::application::importing::transaction::internal::OcrLine &line, int valutaX) noexcept
Checks whether the line looks like a loosely formatted transaction row.
bool hasAmountLikeTokenInLine(const core::application::importing::transaction::internal::OcrLine &line, int valutaX) noexcept
Checks whether the line contains amount-like tokens near the valuta column.
std::vector< size_t > findAmountTokenIndices(const core::application::importing::transaction::internal::OcrLine &line, int valutaX=-1, int bandPx=0) noexcept
Finds the indices of amount-like tokens in an OCR line.
core::application::importing::transaction::internal::TransactionMainRow splitMainRowFromOcrLine(const core::application::importing::transaction::internal::OcrLine &src, int valutaX, int debitX, int creditX) noexcept
Splits an OCR line into the main transaction row representation.
bool hasAmountNearValuta(const core::application::importing::transaction::internal::OcrLine &line, int valutaX, int bandPx) noexcept
Checks whether an amount appears close to the valuta reference.
std::optional< int > findTokenCenterX(const core::application::importing::transaction::internal::OcrLine &line, const std::string &tokenLower) noexcept
Locates the horizontal center of a token inside an OCR line.
std::optional< double > findAndParseAmountInLine(const core::application::importing::transaction::internal::OcrLine &line, int valutaX=-1, std::vector< std::string > *debugOut=nullptr) noexcept
Finds and parses an amount token inside an OCR line.
bool hasLeftDescriptiveText(const core::application::importing::transaction::internal::OcrLine &line, int valutaX) noexcept
Checks whether the line contains descriptive text to the left of the valuta column.
bool hasShortDateToken(const std::string &text) noexcept
Checks whether the text contains a short date token.
bool hasTokenNearX(const core::application::importing::transaction::internal::OcrLine &line, int x, int bandPx) noexcept
Checks whether any token in the line is close to a horizontal position.
core::application::importing::transaction::internal::TransactionMainRow splitMainRowFromRaw(const RawLineLite &src, int valutaX, int debitX, int creditX) noexcept
Splits a raw line into the main transaction row representation.
std::optional< double > parseAmountString(const std::string &s)
std::vector< std::string > splitWhitespace(const std::string &s)
Splits a string on ASCII whitespace.
std::string lowerAscii(std::string s)
Converts ASCII letters in a string to lowercase.
std::string trim(std::string s)
Removes leading and trailing ASCII whitespace from a string.
Stores the inferred column positions for an OCR page.
Runtime configuration for parser heuristics and thresholds.
Lightweight raw-line representation used during grouping and splitting.
std::vector< std::pair< int, int > > wordSpans
Represents one OCR line with its bounding box and recognized text.