Initial xentral_oss_20.3.c9ffacf
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Xentral\Modules\HocrParser;
|
||||
|
||||
use Xentral\Core\DependencyInjection\ContainerInterface;
|
||||
use Xentral\Modules\HocrParser\Service\HocrDataExtractor;
|
||||
use Xentral\Modules\HocrParser\Service\HocrParser;
|
||||
|
||||
final class Bootstrap
|
||||
{
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function registerServices(): array
|
||||
{
|
||||
return [
|
||||
'HocrParser' => 'onInitHocrParser',
|
||||
'HocrDataExtractor' => 'onInitHocrDataExtractor',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HocrParser
|
||||
*/
|
||||
public static function onInitHocrParser(): HocrParser
|
||||
{
|
||||
return new HocrParser();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ContainerInterface $container
|
||||
*
|
||||
* @return HocrDataExtractor
|
||||
*/
|
||||
public static function onInitHocrDataExtractor(ContainerInterface $container): HocrDataExtractor
|
||||
{
|
||||
$app = $container->get('LegacyApplication');
|
||||
|
||||
return new HocrDataExtractor($container->get('HocrParser'), $app->erp->GetWaehrung());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Modules\HocrParser\Data;
|
||||
|
||||
use JsonSerializable;
|
||||
|
||||
class BoundingBox implements JsonSerializable
|
||||
{
|
||||
/** @var array $data */
|
||||
private $data;
|
||||
private $tlx;
|
||||
private $tly;
|
||||
private $brx;
|
||||
private $bry;
|
||||
|
||||
/**
|
||||
* @param int $tlx
|
||||
* @param int $tly
|
||||
* @param int $brx
|
||||
* @param int $bry
|
||||
* @param array $data Zusätzliche Nutzdaten
|
||||
*/
|
||||
public function __construct($tlx, $tly, $brx, $bry, array $data = [])
|
||||
{
|
||||
$this->tlx = $tlx;
|
||||
$this->tly = $tly;
|
||||
$this->brx = $brx;
|
||||
$this->bry = $bry;
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function GetDataAll()
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function GetData($name)
|
||||
{
|
||||
return $this->data[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function SetData($name, $value)
|
||||
{
|
||||
$this->data[$name] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function HasData($name)
|
||||
{
|
||||
return isset($this->data[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Liegt Punkt innerhalb der Box?
|
||||
*
|
||||
* @param int $x
|
||||
* @param int $y
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function IsPointWithin($x, $y)
|
||||
{
|
||||
if ($x > $this->tlx && $x < $this->brx &&
|
||||
$y > $this->tly && $y < $this->bry) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Befindet sich Box rechts von übergebenen Koordinaten
|
||||
*
|
||||
* @param int $x
|
||||
* @param int $y
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function IsRightFromPoint($x, $y)
|
||||
{
|
||||
$isSameHeight = ($y > $this->tly && $y < $this->bry) ? true : false;
|
||||
if ($isSameHeight === true) {
|
||||
return ($x < $this->tlx) ? true : false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Befindet sich Box rechts von übergebenen Koordinaten
|
||||
*
|
||||
* @param int $x
|
||||
* @param int $y
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function IsLeftFromPoint($x, $y)
|
||||
{
|
||||
$isSameHeight = ($y > $this->tly && $y < $this->bry) ? true : false;
|
||||
if ($isSameHeight === true) {
|
||||
return ($x > $this->brx) ? true : false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Befindet sich die Box oberhalb der übergebenen Koordinaten
|
||||
*
|
||||
* @param int $x
|
||||
* @param int $y
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function IsAbovePoint($x, $y)
|
||||
{
|
||||
$isSameColumn = ($x > $this->tlx && $x < $this->brx) ? true : false;
|
||||
if ($isSameColumn === true) {
|
||||
return ($this->bry < $y) ? true : false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Befindet sich die Box unterhalb der übergebenen Koordinaten
|
||||
*
|
||||
* @param int $x
|
||||
* @param int $y
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function IsBelowPoint($x, $y)
|
||||
{
|
||||
$isSameColumn = ($x > $this->tlx && $x < $this->brx) ? true : false;
|
||||
if ($isSameColumn === true) {
|
||||
return ($this->tly > $y) ? true : false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet die Entfernung zwischen Mittelpunkt der Box und dem übergebenen Punkt
|
||||
*
|
||||
* @param int $x
|
||||
* @param int $y
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function GetDistanceFromPoint($x, $y)
|
||||
{
|
||||
$center = $this->GetCenterPoint();
|
||||
|
||||
return sqrt(pow($center['x'] - $x, 2) + pow($center['y'] - $y, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt Mittelpunkt der Box zurück
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function GetCenterPoint()
|
||||
{
|
||||
return [
|
||||
'x' => (int)($this->tlx + (($this->brx - $this->tlx) / 2)),
|
||||
'y' => (int)($this->tly + (($this->bry - $this->tly) / 2)),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return [
|
||||
'tlx' => $this->tlx,
|
||||
'tly' => $this->tly,
|
||||
'brx' => $this->brx,
|
||||
'bry' => $this->bry,
|
||||
'data' => $this->data,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Modules\HocrParser\Data;
|
||||
|
||||
use Xentral\Components\ScanbotApi\Exception\RuntimeException;
|
||||
|
||||
class BoundingBoxCollection
|
||||
{
|
||||
/** @var array|BoundingBox[] $boxes */
|
||||
private $boxes;
|
||||
|
||||
/**
|
||||
* @param array|BoundingBox[] $boxes
|
||||
*/
|
||||
public function __construct(array $boxes = [])
|
||||
{
|
||||
$this->boxes = $boxes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|BoundingBox[]
|
||||
*/
|
||||
public function GetBoxes()
|
||||
{
|
||||
return $this->boxes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Box mit der kürzesten Entfernung rechts vom übergebenen Punkt zurückgeben
|
||||
*
|
||||
* @param int $x
|
||||
* @param int $y
|
||||
*
|
||||
* @return BoundingBox|false
|
||||
*/
|
||||
public function GetNearestBoxRightFromPoint($x, $y)
|
||||
{
|
||||
// Erstmal alle Elemente rechts vom Punkt finden; inkl. Entfernung zum Punkt
|
||||
$candidates = $this->GetBoxesRightFromPoint($x, $y);
|
||||
|
||||
return $this->GetBoxWithLowestDistance($candidates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle Boxen zurückgeben die sich rechts vom übergebenen Punkt befinden; inkl. Entfernung
|
||||
*
|
||||
* @param int $x
|
||||
* @param int $y
|
||||
*
|
||||
* @return array|BoundingBox[]
|
||||
*/
|
||||
public function GetBoxesRightFromPoint($x, $y)
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ($this->boxes as $box) {
|
||||
if ($box->IsRightFromPoint($x, $y)) {
|
||||
$distance = $box->GetDistanceFromPoint($x, $y);
|
||||
$box->SetData('distance', $distance);
|
||||
$result[] = $box;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Box mit dem geringsten Abstand ermitteln
|
||||
*
|
||||
* Hinweis: Data-Eigenschaft "distance" muss dafür gesetzt sein.
|
||||
*
|
||||
* @param array|BoundingBox[] $boxes
|
||||
*
|
||||
* @return BoundingBox|false
|
||||
*/
|
||||
private function GetBoxWithLowestDistance(array $boxes)
|
||||
{
|
||||
// Index neu aufbauen
|
||||
$boxes = array_values($boxes);
|
||||
|
||||
// Keine Box gefunden
|
||||
if (count($boxes) === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Nur eine Box gefunden
|
||||
if (count($boxes) === 1) {
|
||||
return $boxes[0];
|
||||
}
|
||||
|
||||
$shortestDistance = $boxes[0]->GetData('distance');
|
||||
$shortestDistanceKey = 0;
|
||||
|
||||
// Prüfen ob Entfernung gesetzt sind
|
||||
if ($shortestDistance === null) {
|
||||
throw new RuntimeException('Distance not set.');
|
||||
}
|
||||
|
||||
// Box mit der geringsten Entfernung suchen
|
||||
/** @var BoundingBox $box */
|
||||
foreach ($boxes as $key => $box) {
|
||||
$boxDistance = $box->GetData('distance');
|
||||
if ($boxDistance < $shortestDistance) {
|
||||
$shortestDistance = $boxDistance;
|
||||
$shortestDistanceKey = $key;
|
||||
}
|
||||
}
|
||||
|
||||
return $boxes[$shortestDistanceKey];
|
||||
}
|
||||
|
||||
/**
|
||||
* Box mit der kürzesten Entfernung links vom übergebenen Punkt zurückgeben
|
||||
*
|
||||
* @param int $x
|
||||
* @param int $y
|
||||
*
|
||||
* @return BoundingBox|false
|
||||
*/
|
||||
public function GetNearestBoxLeftFromPoint($x, $y)
|
||||
{
|
||||
// Erstmal alle Elemente rechts vom Punkt finden; inkl. Entfernung zum Punkt
|
||||
$candidates = $this->GetBoxesLeftFromPoint($x, $y);
|
||||
|
||||
return $this->GetBoxWithLowestDistance($candidates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle Boxen zurückgeben die sich links vom übergebenen Punkt befinden; inkl. Entfernung
|
||||
*
|
||||
* @param int $x
|
||||
* @param int $y
|
||||
*
|
||||
* @return array|BoundingBox[]
|
||||
*/
|
||||
public function GetBoxesLeftFromPoint($x, $y)
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ($this->boxes as $box) {
|
||||
if ($box->IsLeftFromPoint($x, $y)) {
|
||||
$distance = $box->GetDistanceFromPoint($x, $y);
|
||||
$box->SetData('distance', $distance);
|
||||
$result[] = $box;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Box mit der kürzesten Entfernung oberhalb vom übergebenen Punkt zurückgeben
|
||||
*
|
||||
* @param int $x
|
||||
* @param int $y
|
||||
*
|
||||
* @return BoundingBox|false
|
||||
*/
|
||||
public function GetNearestBoxAboveFromPoint($x, $y)
|
||||
{
|
||||
$candidates = $this->GetBoxesAboveFromPoint($x, $y);
|
||||
|
||||
return $this->GetBoxWithLowestDistance($candidates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle Boxen zurückgeben die sich oberhalb vom übergebenen Punkt befinden; inkl. Entfernung
|
||||
*
|
||||
* @param int $x
|
||||
* @param int $y
|
||||
*
|
||||
* @return array|BoundingBox[]
|
||||
*/
|
||||
public function GetBoxesAboveFromPoint($x, $y)
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ($this->boxes as $box) {
|
||||
if ($box->IsAbovePoint($x, $y)) {
|
||||
$distance = $box->GetDistanceFromPoint($x, $y);
|
||||
$box->SetData('distance', $distance);
|
||||
$result[] = $box;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Box mit der kürzesten Entfernung unterhalb vom übergebenen Punkt zurückgeben
|
||||
*
|
||||
* @param int $x
|
||||
* @param int $y
|
||||
*
|
||||
* @return BoundingBox|false
|
||||
*/
|
||||
public function GetNearestBoxBelowFromPoint($x, $y)
|
||||
{
|
||||
$candidates = $this->GetBoxesBelowFromPoint($x, $y);
|
||||
|
||||
return $this->GetBoxWithLowestDistance($candidates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle Boxen zurückgeben die sich unterhalb vom übergebenen Punkt befinden; inkl. Entfernung
|
||||
*
|
||||
* @param int $x
|
||||
* @param int $y
|
||||
*
|
||||
* @return array|BoundingBox[]
|
||||
*/
|
||||
public function GetBoxesBelowFromPoint($x, $y)
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ($this->boxes as $box) {
|
||||
if ($box->IsBelowPoint($x, $y)) {
|
||||
$distance = $box->GetDistanceFromPoint($x, $y);
|
||||
$box->SetData('distance', $distance);
|
||||
$result[] = $box;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Modules\HocrParser\Exception;
|
||||
|
||||
use Xentral\Core\Exception\ModuleExceptionInterface;
|
||||
|
||||
interface HocrParserException extends ModuleExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Modules\HocrParser\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class InvalidArgumentException extends RuntimeException implements HocrParserException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Modules\HocrParser\Finder;
|
||||
|
||||
use Xentral\Modules\HocrParser\Data\BoundingBox;
|
||||
use Xentral\Modules\HocrParser\Data\BoundingBoxCollection;
|
||||
|
||||
class BoundingBoxFinder
|
||||
{
|
||||
/** @var BoundingBoxCollection $boxes */
|
||||
private $boxes;
|
||||
|
||||
/** @var array|RelativePositionFinderFacet[] $criteria */
|
||||
private $criteria;
|
||||
|
||||
/** @var array $currencies */
|
||||
private $currencies;
|
||||
|
||||
/**
|
||||
* @param BoundingBoxCollection $boxes
|
||||
* @param array|RelativePositionFinderFacet[] $criteria
|
||||
* @param array $validCurrencies
|
||||
*/
|
||||
public function __construct(BoundingBoxCollection $boxes, array $criteria, array $validCurrencies = [])
|
||||
{
|
||||
$this->boxes = $boxes;
|
||||
$this->criteria = $criteria;
|
||||
$this->currencies = $validCurrencies;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function Find()
|
||||
{
|
||||
$search = [];
|
||||
$result = [];
|
||||
|
||||
// Alle Boxen finden die den jeweiligen Suchbegriff enthalten
|
||||
/** @var BoundingBox $box */
|
||||
foreach ($this->boxes->GetBoxes() as $box) {
|
||||
foreach ($this->criteria as $searchKey => $criteria) {
|
||||
$result[$searchKey] = null;
|
||||
|
||||
// Alle Boxen sammeln die den Vorbedingungen entsprechen
|
||||
if ($criteria->MatchPreCondition($box->GetData('text'))) {
|
||||
$search[$searchKey]['boxes'][] = $box;
|
||||
$search[$searchKey]['criteria'] = $criteria;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Schauen welche der Kandidaten die genauen Vorgaben erfüllt; der erste Treffer gewinnt
|
||||
foreach ($search as $searchKey => $searchInfo) {
|
||||
$criteria = $searchInfo['criteria'];
|
||||
$candidates = $searchInfo['boxes'];
|
||||
$result[$searchKey] = $criteria->Select($candidates, $this->boxes);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Modules\HocrParser\Finder;
|
||||
|
||||
use Xentral\Modules\HocrParser\Data\BoundingBox;
|
||||
use Xentral\Modules\HocrParser\Data\BoundingBoxCollection;
|
||||
|
||||
class CurrencyCodeFinderFacet implements FinderFacetInterface
|
||||
{
|
||||
/** @var array $validCodes */
|
||||
private $validCodes;
|
||||
|
||||
/**
|
||||
* @param array $validCodes Array mit gültigen Währungscodes (drei-stelliger ISO-Code)
|
||||
*/
|
||||
public function __construct(array $validCodes = ['EUR'])
|
||||
{
|
||||
$this->validCodes = $validCodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function MatchPreCondition($text)
|
||||
{
|
||||
if (!$this->IsCurrencyLikeValue($text)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->IsValidCurrency($text)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $value
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function IsCurrencyLikeValue($value)
|
||||
{
|
||||
return (bool)preg_match('/^[A-Z]{3}$/', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $value
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function IsValidCurrency($value)
|
||||
{
|
||||
return in_array($value, $this->validCodes, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|BoundingBox[] $candidates
|
||||
* @param BoundingBoxCollection $boxes
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
public function Select(array $candidates, BoundingBoxCollection $boxes)
|
||||
{
|
||||
if (empty($candidates)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Einfach das erste Ergebnis zurückliefern;
|
||||
// In den PreConditions wurde schon sichergestellt dass es eine gültige Währung ist
|
||||
return $candidates[0]->GetData('text');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Modules\HocrParser\Finder;
|
||||
|
||||
use Xentral\Modules\HocrParser\Data\BoundingBoxCollection;
|
||||
|
||||
interface FinderFacetInterface
|
||||
{
|
||||
public function MatchPreCondition($text);
|
||||
|
||||
public function Select(array $candidates, BoundingBoxCollection $boxes);
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Modules\HocrParser\Finder;
|
||||
|
||||
use Xentral\Modules\HocrParser\Exception\InvalidArgumentException;
|
||||
|
||||
class PatternMatcher
|
||||
{
|
||||
public const PATTERN_DOCUMENT_NUMBER = 'documentnumber';
|
||||
public const PATTERN_MONEY = 'money';
|
||||
public const PATTERN_DATE = 'date';
|
||||
public const PATTERN_DEFAULT = 'default';
|
||||
|
||||
/** @var array $validPatterns */
|
||||
private static $validPatterns = [
|
||||
self::PATTERN_DOCUMENT_NUMBER,
|
||||
self::PATTERN_MONEY,
|
||||
self::PATTERN_DATE,
|
||||
self::PATTERN_DEFAULT,
|
||||
];
|
||||
|
||||
/** @var string $pattern */
|
||||
private $pattern;
|
||||
|
||||
/**
|
||||
* @param string $pattern
|
||||
*/
|
||||
public function __construct($pattern = self::PATTERN_DEFAULT)
|
||||
{
|
||||
if (!in_array($pattern, self::$validPatterns, true)) {
|
||||
throw new InvalidArgumentException(sprintf('Pattern "%s" is not allowed.', $pattern));
|
||||
}
|
||||
|
||||
$this->pattern = $pattern;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $value
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function Match($value)
|
||||
{
|
||||
$value = trim((string)$value);
|
||||
|
||||
if (empty($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch ($this->pattern) {
|
||||
case self::PATTERN_DATE:
|
||||
return $this->IsDateLikeValue($value);
|
||||
break;
|
||||
case self::PATTERN_MONEY:
|
||||
return $this->IsMoneyLikeValue($value);
|
||||
break;
|
||||
case self::PATTERN_DOCUMENT_NUMBER:
|
||||
return $this->IsDocumentNumberLikeValue($value);
|
||||
break;
|
||||
case self::PATTERN_DEFAULT:
|
||||
return $this->IsCandidateValue($value);
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $value
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function IsDateLikeValue($value)
|
||||
{
|
||||
return (bool)preg_match('/\d{1,2}\.\d{1,2}\.\d{2,4}/', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $value
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function IsMoneyLikeValue($value)
|
||||
{
|
||||
// Mit Tausendertrenner: z.B.: 11.111,11 oder 11,111.11
|
||||
$withThousands = (bool)preg_match('/\d+[\.,]\d{3}[\.,]{1}\d{2}$/', $value);
|
||||
if ($withThousands) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ohne Tausendertrenner: z.B.: 1111111,11 oder 1111111.11
|
||||
return (bool)preg_match('/^\d+[\.,]{1}\d{2}$/', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $value
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function IsDocumentNumberLikeValue($value)
|
||||
{
|
||||
// Nur Grossbuchstaben, Zahlen, Minus und Unterstrich sind erlaubt
|
||||
$containsInvalidChars = (bool)preg_match('/[^A-Z0-9\-_]+/', $value);
|
||||
if ($containsInvalidChars) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (bool)preg_match('/\d{4,}/', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $value
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function IsCandidateValue($value)
|
||||
{
|
||||
return $this->IsDateLikeValue($value)
|
||||
|| $this->IsDocumentNumberLikeValue($value)
|
||||
|| $this->IsMoneyLikeValue($value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace Xentral\Modules\HocrParser\Finder;
|
||||
|
||||
use Xentral\Modules\HocrParser\Data\BoundingBox;
|
||||
use Xentral\Modules\HocrParser\Data\BoundingBoxCollection;
|
||||
use Xentral\Modules\HocrParser\Exception\InvalidArgumentException;
|
||||
|
||||
class RelativePositionFinderFacet implements FinderFacetInterface
|
||||
{
|
||||
const DIRECTION_LEFT = 'left';
|
||||
const DIRECTION_RIGHT = 'right';
|
||||
const DIRECTION_ABOVE = 'above';
|
||||
const DIRECTION_BELOW = 'below';
|
||||
|
||||
/** @var array $validDirections */
|
||||
private static $validDirections = [
|
||||
self::DIRECTION_ABOVE,
|
||||
self::DIRECTION_RIGHT,
|
||||
self::DIRECTION_BELOW,
|
||||
self::DIRECTION_LEFT,
|
||||
];
|
||||
|
||||
/** @var string $text */
|
||||
private $text;
|
||||
|
||||
/** @var PatternMatcher $matcher */
|
||||
private $matcher;
|
||||
|
||||
/** @var string $direction */
|
||||
private $direction;
|
||||
|
||||
/**
|
||||
* @param string $searchText
|
||||
* @param string $direction
|
||||
* @param string $pattern
|
||||
*/
|
||||
public function __construct($searchText, $direction, $pattern)
|
||||
{
|
||||
if (!in_array($direction, self::$validDirections, true)) {
|
||||
throw new InvalidArgumentException(sprintf('Direction "%s" is not allowed.', $direction));
|
||||
}
|
||||
|
||||
$this->matcher = new PatternMatcher($pattern);
|
||||
$this->text = trim($searchText);
|
||||
$this->direction = $direction;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function MatchPreCondition($text)
|
||||
{
|
||||
return $text === $this->text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|BoundingBox[] $candidates
|
||||
* @param BoundingBoxCollection $boxes
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
public function Select(array $candidates, BoundingBoxCollection $boxes)
|
||||
{
|
||||
foreach ($candidates as $candidate) {
|
||||
$coords = $candidate->GetCenterPoint();
|
||||
/** @var BoundingBox $nearestBox */
|
||||
$nearestBox = false;
|
||||
|
||||
switch ($this->direction) {
|
||||
case self::DIRECTION_RIGHT:
|
||||
$nearestBox = $boxes->GetNearestBoxRightFromPoint($coords['x'], $coords['y']);
|
||||
break;
|
||||
case self::DIRECTION_LEFT:
|
||||
$nearestBox = $boxes->GetNearestBoxLeftFromPoint($coords['x'], $coords['y']);
|
||||
break;
|
||||
case self::DIRECTION_ABOVE:
|
||||
$nearestBox = $boxes->GetNearestBoxAboveFromPoint($coords['x'], $coords['y']);
|
||||
break;
|
||||
case self::DIRECTION_BELOW:
|
||||
$nearestBox = $boxes->GetNearestBoxBelowFromPoint($coords['x'], $coords['y']);
|
||||
break;
|
||||
}
|
||||
|
||||
if ($nearestBox === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Prüfen ob Wert einem bestimmten Muster folgt
|
||||
$patternMatching = $this->IsPatternMatching($nearestBox->GetData('text'));
|
||||
if ($patternMatching) {
|
||||
return $nearestBox->GetData('text');
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $value
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function IsPatternMatching($value)
|
||||
{
|
||||
return $this->matcher->Match($value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Xentral\Modules\HocrParser\Service;
|
||||
|
||||
use Xentral\Modules\HocrParser\Data\BoundingBoxCollection;
|
||||
use Xentral\Modules\HocrParser\Finder\BoundingBoxFinder;
|
||||
use Xentral\Modules\HocrParser\Finder\CurrencyCodeFinderFacet;
|
||||
use Xentral\Modules\HocrParser\Finder\PatternMatcher;
|
||||
use Xentral\Modules\HocrParser\Finder\RelativePositionFinderFacet;
|
||||
|
||||
class HocrDataExtractor
|
||||
{
|
||||
/** @var array|string[] $validCurrencies */
|
||||
private $validCurrencies;
|
||||
|
||||
/** @var HocrParser $parser */
|
||||
private $parser;
|
||||
|
||||
public function __construct(HocrParser $parser, array $validCurrencies)
|
||||
{
|
||||
$this->validCurrencies = $validCurrencies;
|
||||
$this->parser = $parser;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $document HOCR-Dokument
|
||||
* @param array $settings Einstellungen (pro Lieferant)
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function extractLiabilityDataFromHocrDocument(string $document, array $settings): array
|
||||
{
|
||||
// Suchkriterien zusammenstellen
|
||||
$criteria = [];
|
||||
if(!empty($settings['invoice_number']['term']) && !empty($settings['invoice_number']['direction'])){
|
||||
$criteria['invoice_number'] =
|
||||
new RelativePositionFinderFacet(
|
||||
$settings['invoice_number']['term'],
|
||||
$settings['invoice_number']['direction'],
|
||||
PatternMatcher::PATTERN_DOCUMENT_NUMBER
|
||||
);
|
||||
}
|
||||
if(!empty($settings['invoice_date']['term']) && !empty($settings['invoice_date']['direction'])){
|
||||
$criteria['invoice_date'] =
|
||||
new RelativePositionFinderFacet(
|
||||
$settings['invoice_date']['term'],
|
||||
$settings['invoice_date']['direction'],
|
||||
PatternMatcher::PATTERN_DATE
|
||||
);
|
||||
}
|
||||
if(!empty($settings['total_gross']['term']) && !empty($settings['total_gross']['direction'])){
|
||||
$criteria['total_gross'] =
|
||||
new RelativePositionFinderFacet(
|
||||
$settings['total_gross']['term'],
|
||||
$settings['total_gross']['direction'],
|
||||
PatternMatcher::PATTERN_MONEY
|
||||
);
|
||||
}
|
||||
// Immer nach Währungen suchen
|
||||
$waehrungen = $this->validCurrencies;
|
||||
$criteria['currency'] = new CurrencyCodeFinderFacet($waehrungen);
|
||||
|
||||
// BoundingBoxes aus HOCR extrahieren + Suche anhand der Suchkriterien durchführen
|
||||
$boxes = new BoundingBoxCollection($this->parser->parse($document));
|
||||
$finder = new BoundingBoxFinder($boxes, $criteria);
|
||||
|
||||
return $finder->Find();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Xentral\Modules\HocrParser\Service;
|
||||
|
||||
use DOMDocument;
|
||||
use Xentral\Modules\HocrParser\Data\BoundingBox;
|
||||
|
||||
final class HocrParser
|
||||
{
|
||||
/**
|
||||
* @param string $content HOCR-Dokument
|
||||
*
|
||||
* @return array|BoundingBox[]
|
||||
*/
|
||||
public function parse(string $content): array
|
||||
{
|
||||
$boxes = [];
|
||||
|
||||
$dom = new DOMDocument;
|
||||
$dom->loadXML($content);
|
||||
$spans = $dom->getElementsByTagName('span');
|
||||
|
||||
/** @var DOMElement $span */
|
||||
foreach ($spans as $span) {
|
||||
if ($span->getAttribute('class') === 'ocrx_word') {
|
||||
$title = $span->getAttribute('title');
|
||||
$coords = $this->extractCoordinates($title);
|
||||
$text = trim($span->nodeValue);
|
||||
|
||||
// Boxen ohne Text auslassen; kann vorkommen bei Barcode-Fonts
|
||||
if (empty($text)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Boxen mit Text sammeln
|
||||
$boxes[] = new BoundingBox(
|
||||
$coords['tlx'], $coords['tly'], $coords['brx'], $coords['bry'], ['text' => $text]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $boxes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
*
|
||||
* @return array|bool
|
||||
*/
|
||||
private function extractCoordinates(string $text)
|
||||
{
|
||||
// bbox 599 2737 743 2758; x_wconf 96
|
||||
$parts = explode(' ', $text);
|
||||
if ($parts[0] === 'bbox') {
|
||||
return [
|
||||
'tlx' => (int)$parts[1], // Top left X-Coordinate
|
||||
'tly' => (int)$parts[2], // Top left Y
|
||||
'brx' => (int)$parts[3], // Bottom right X
|
||||
'bry' => (int)$parts[4], // Bottom right Y
|
||||
];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user