Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 40 additions & 29 deletions src/Extraction/Text/TextExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use PrinsFrank\PdfParser\Document\ContentStream\PositionedText\PositionedTextElement;
use PrinsFrank\PdfParser\Document\Object\Decorator\Page;
use PrinsFrank\PdfParser\Exception\PdfParserException;
use PrinsFrank\PdfParser\Extraction\Text\TextGrouping\Clustering\BoundingBoxClusterer;
use PrinsFrank\PdfParser\Extraction\Text\TextGrouping\LineGrouping\TextOverlapStrategy;

class TextExtractor {
Expand All @@ -13,43 +14,53 @@ class TextExtractor {
* @throws PdfParserException
*/
public static function extractText(array $positionedTextElements, Page $page): string {
$lineGroupedElements = TextOverlapStrategy::group($positionedTextElements);

$text = '';
foreach ($lineGroupedElements as $i => $positionedTextElementsForLine) {
$previousTextElementOnLine = null;
$clusters = BoundingBoxClusterer::cluster($positionedTextElements, $page);
foreach ($clusters as $i => $cluster) {
if ($i !== 0) {
$text .= "\n";
$previousCluster = $clusters[$i - 1];
if (min($previousCluster->top, $cluster->top) <= max($previousCluster->bottom, $cluster->bottom)) {
$text .= "\n";
$previousTextElementOnLine = null;
}
}

$previousTextElementOnLine = null;
foreach ($positionedTextElementsForLine as $positionedTextElement) {
$elementText = $positionedTextElement->getText($page);
if ($elementText === '') {
$previousTextElementOnLine = $positionedTextElement;
continue;
foreach (TextOverlapStrategy::group($cluster->elements) as $j => $positionedTextElementsForLine) {
if ($j !== 0) {
$text .= "\n";
$previousTextElementOnLine = null;
}

if ($previousTextElementOnLine !== null) {
$gap = $positionedTextElement->absoluteMatrix->offsetX
- $previousTextElementOnLine->absoluteMatrix->offsetX
- $previousTextElementOnLine->getAdvanceWidth($page);

$wordBreakThreshold = $previousTextElementOnLine->textState->getFontSize()
* $previousTextElementOnLine->absoluteMatrix->scaleX
* ($previousTextElementOnLine->textState->scale / 100)
* PositionedTextElement::WORD_BREAK_THRESHOLD_EM;

if (
$gap >= $wordBreakThreshold
&& str_ends_with($text, ' ') === false
&& str_starts_with($elementText, ' ') === false
) {
$text .= ' ';
foreach ($positionedTextElementsForLine as $positionedTextElement) {
$elementText = $positionedTextElement->getText($page);
if ($elementText === '') {
$previousTextElementOnLine = $positionedTextElement;
continue;
}
}

$text .= $elementText;
$previousTextElementOnLine = $positionedTextElement;
if ($previousTextElementOnLine !== null) {
$gap = $positionedTextElement->absoluteMatrix->offsetX
- $previousTextElementOnLine->absoluteMatrix->offsetX
- $previousTextElementOnLine->getAdvanceWidth($page);

$wordBreakThreshold = $previousTextElementOnLine->textState->getFontSize()
* $previousTextElementOnLine->absoluteMatrix->scaleX
* ($previousTextElementOnLine->textState->scale / 100)
* PositionedTextElement::WORD_BREAK_THRESHOLD_EM;

if (
$gap >= $wordBreakThreshold
&& str_ends_with($text, ' ') === false
&& str_starts_with($elementText, ' ') === false
) {
$text .= ' ';
}
}

$text .= $elementText;
$previousTextElementOnLine = $positionedTextElement;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php declare(strict_types=1);

namespace PrinsFrank\PdfParser\Extraction\Text\TextGrouping\Clustering;

use PrinsFrank\PdfParser\Document\ContentStream\PositionedText\PositionedTextElement;
use PrinsFrank\PdfParser\Document\Object\Decorator\Page;

class BoundingBoxClusterer {
/**
* @param list<PositionedTextElement> $positionedTextElements
* @return list<Cluster>
*/
public static function cluster(array $positionedTextElements, Page $page): array {
usort(
$positionedTextElements,
fn(PositionedTextElement $a, PositionedTextElement $b): int => $b->absoluteMatrix->offsetY <=> $a->absoluteMatrix->offsetY,
);

$processedIndices = $clusters = [];
$nrOfItems = count($positionedTextElements);
for ($i = 0; $i < $nrOfItems; $i++) {
if (isset($processedIndices[$i])) {
continue;
}

$firstClusterElement = $positionedTextElements[$i];
$cluster = [$firstClusterElement];
$processedIndices[$i] = true;

$clusterLeft = $firstClusterElement->absoluteMatrix->offsetX;
$clusterRight = $clusterLeft + $firstClusterElement->getAdvanceWidth($page);
$clusterBottom = $firstClusterElement->absoluteMatrix->offsetY;
$clusterTop = $clusterBottom + $firstClusterElement->getHeight();

$previousPassResultedInNewElements = true;
while ($previousPassResultedInNewElements) {
$previousPassResultedInNewElements = false;

for ($j = $i + 1; $j < $nrOfItems; $j++) {
if (isset($processedIndices[$j])) {
continue;
}

$candidate = $positionedTextElements[$j];
$candidateRight = $candidate->absoluteMatrix->offsetX + $candidate->getAdvanceWidth($page);
$candidateTop = $candidate->absoluteMatrix->offsetY + $candidate->getHeight();
if (max(0, $clusterLeft - $candidateRight, $candidate->absoluteMatrix->offsetX - $clusterRight) <= 5
&& max(0, $clusterBottom - $candidateTop, $candidate->absoluteMatrix->offsetY - $clusterTop) <= 7) {
$cluster[] = $candidate;
$processedIndices[$j] = true;
$previousPassResultedInNewElements = true;

$clusterLeft = min($clusterLeft, $candidate->absoluteMatrix->offsetX);
$clusterRight = max($clusterRight, $candidateRight);
$clusterBottom = min($clusterBottom, $candidate->absoluteMatrix->offsetY);
$clusterTop = max($clusterTop, $candidateTop);
}
}
}

$clusters[] = new Cluster($cluster, $clusterLeft, $clusterRight, $clusterBottom, $clusterTop);
}

usort(
$clusters,
function (Cluster $a, Cluster $b): int {
$aHeight = $a->top - $a->bottom;
$bHeight = $b->top - $b->bottom;
$smallestHeight = min($aHeight, $bHeight);
if ($smallestHeight > 0) {
$overlap = min($a->top, $b->top) - max($a->bottom, $b->bottom);
$overlapPercentage = ($overlap / $smallestHeight) * 100;
if ($overlapPercentage >= 90) {
return $a->left <=> $b->left;
}
}

return $b->top <=> $a->top;
},
);

return $clusters;
}
}
16 changes: 16 additions & 0 deletions src/Extraction/Text/TextGrouping/Clustering/Cluster.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php declare(strict_types=1);

namespace PrinsFrank\PdfParser\Extraction\Text\TextGrouping\Clustering;

use PrinsFrank\PdfParser\Document\ContentStream\PositionedText\PositionedTextElement;

readonly class Cluster {
/** @param list<PositionedTextElement> $elements */
public function __construct(
public array $elements,
public float $left,
public float $right,
public float $bottom,
public float $top,
) { }
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ public static function group(array $positionedTextElements): iterable {
continue;
}

/** @var PositionedTextElement $highestPositionedTextElement */
$highestPositionedTextElement = $positionedTextElements[$i];
$highestPositionedTextElementBottom = $highestPositionedTextElement->absoluteMatrix->offsetY;
$highestPositionedTextElementHeight = $highestPositionedTextElement->getHeight();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,15 @@ pages:
● Ut autem excepturi ut sequi laboriosam est quas debitis et placeat consequatur!
-
text: |-
● Qui autem voluptas eum deserunt dolor.
In nesciunt quia qui asperiores corrupti sed perspiciatis tempora id minus architecto!
Rem beatae quos aut fugit vero.
At adipisci enim id reiciendis officiis.
Sed sapiente quia qui sint dolorem vel impedit magni.
In nesciunt quia qui asperiores corrupti sed perspiciatis tempora id minus architecto!
Rem beatae quos aut fugit vero.
At adipisci enim id reiciendis officiis.
Sed sapiente quia qui sint dolorem vel impedit magni.
Et dicta voluptatem sit fugit saepevel nulla et debitis iure et ipsa voluptas? Est facere harum
et inventore eveniet Aut rerum est molestias quam qui perspiciatis magnam.
Qui eligendi saepe et neque autem A omnis et pariatur rerum est dolore rerum est facere
Expand All @@ -94,16 +98,23 @@ pages:
explicabo. Et maxime assumenda et ipsam itaqueAut deleniti ut excepturi alias et fugit
facere non modi laudantium. Qui sapiente rerum non sunt ipsum Ad maxime est velit
eligendi aut quaerat officia est architecto magnam?
1. Et sequi ipsum et expedita ipsum eum dolore laborum.
2. Id enim unde eos commodi quidem et eius quaerat.
3. Est tempore veritatis sed aliquam Quis.
4. Qui quas tempora ut voluptates doloribus est facilis deserunt 33 distinctio internos.
1.
2.
3.
4. Et sequi ipsum et expedita ipsum eum dolore laborum.
Id enim unde eos commodi quidem et eius quaerat.
Est tempore veritatis sed aliquam Quis.
Qui quas tempora ut voluptates doloribus est facilis deserunt 33 distinctio internos.
markdown: |-
● Qui autem voluptas eum deserunt dolor.
In nesciunt quia qui asperiores corrupti sed perspiciatis tempora id minus architecto!
Rem beatae quos aut fugit vero.
At adipisci enim id reiciendis officiis.
Sed sapiente quia qui sint dolorem vel impedit magni.
In nesciunt quia qui asperiores corrupti sed perspiciatis tempora id minus architecto!
Rem beatae quos aut fugit vero.
At adipisci enim id reiciendis officiis.
Sed sapiente quia qui sint dolorem vel impedit magni.
Et dicta voluptatem sit fugit saepevel nulla et debitis iure et ipsa voluptas? Est facere harum
et inventore eveniet Aut rerum est molestias quam qui perspiciatis magnam.
Qui eligendi saepe et neque autem A omnis et pariatur rerum est dolore rerum est facere
Expand All @@ -114,7 +125,10 @@ pages:
explicabo. Et maxime assumenda et ipsam itaqueAut deleniti ut excepturi alias et fugit
facere non modi laudantium. Qui sapiente rerum non sunt ipsum Ad maxime est velit
eligendi aut quaerat officia est architecto magnam?
1. Et sequi ipsum et expedita ipsum eum dolore laborum.
2. Id enim unde eos commodi quidem et eius quaerat.
3. Est tempore veritatis sed aliquam Quis.
4. Qui quas tempora ut voluptates doloribus est facilis deserunt 33 distinctio internos.
1.
2.
3.
4. Et sequi ipsum et expedita ipsum eum dolore laborum.
Id enim unde eos commodi quidem et eius quaerat.
Est tempore veritatis sed aliquam Quis.
Qui quas tempora ut voluptates doloribus est facilis deserunt 33 distinctio internos.
Loading
Loading