Próbuję odczytać tabelę dokumentów PDF, ale skieruję problem.

Jeśli regularnie otworzę PDF, pokazuje:

item[tab]item[tab]item[tab]item[tab]item
item[tab]item[tab]item[tab]item[tab]item
item[tab]item[tab]item[tab]item[tab]item

Odniesienie

Konwertuję PDF za pomocą:

StringBuilder result = new StringBuilder();
PdfDocument pdfDoc = new PdfDocument(new PdfReader(SRC));

LocationTextExtractionStrategy strategy = new LocationTextExtractionStrategy();

PdfCanvasProcessor parser = new PdfCanvasProcessor(strategy);
for (int i = 1; i <= pdfDoc.GetNumberOfPages(); i++)
{
    result.AppendLine("INFO_START_PAGE");
    string output = PdfTextExtractor.GetTextFromPage(pdfDoc.GetPage(i));
    /*Note, in the GetTextFromPage i replaced the method to output [tab] instead of a regular space on 
    big spaces*/
    foreach(string data in output.Replace("\r\n", "\n").Replace("\n", "×").Split('×'))
    {
        result.AppendLine(data.Trim().Replace("   ", "[tab]"));
    }

    result.AppendLine("INFO_END_PAGE");
}

pdfDoc.Close();
return result.ToString();

Kiedy próbuję to przeczytać za pomocą PDF do konwersji tekstu w niektórych przypadkach, które brzmi:

item[tab]item[tab]item[tab]item[tab]item
item[tab]item[tab]item[tab]
item[tab]item
item[tab]item[tab]item[tab]item[tab]item

Czy istnieje sposób na naprawienie tego problemu?

1
thekguy 20 styczeń 2020, 12:32

1 odpowiedź

Najlepsza odpowiedź

screenshot provided by the OP

Jest ekstrahowany

Artikelnr. Omschrijving Aantal
Per stuk Kosten
VERHUUR L. GELEVERDE ARBEID PDC 8 € 43,70 € 349,60
VERHUUR O. GELEVERDE ARBEID PDC 3 € 60,95 € 182,85
VERHUUR L.L. GELEVERDE ARBEID EM 24
€ 32,20 € 772,80

Po pierwsze, dlaczego się dzieje

Jak powstają w uwagach do pytania, rzeczywiście ma mały krok pionowy, we wszystkich wierszach pierwsze trzy kolumny są ustawione na tej samej pozycji pionowej, a pozycja pionowa ostatnich dwóch kolumn różni się nieznacznie,

    Row       First columns y   Last columns y
Heading row               536          535.893
First row                 516          516.229
Second row                495          495.478
Third row                 475          474.788

Jednym rozpoznaje, w szczególności, że rzędy złamane przez ekstrakcję tekstu są te, w których dokładne cyfry punktu przed dziesiętne z pozycji Y. (536 VS 535, 475 VS 474), podczas gdy te z równymi wstępnie przecinającymi cyframi punktowymi nie są uszkodzone.

Powodem tego jest to, że klasa TextChunkLocationDefaultImp (co domyślnie służy do przechowywania lokalizacji fragmentów tekstowych i metod porównania takich lokalizacji) przechowuje pozycję Y fragment (w rzeczywistości abstrakcja go również pracuje dla tekstu nie napisany poziomo) W zmiennej całkowitej (private readonly int distPerpendicular) i w metodzie testowej SameLine wymaga równości wartości distPerpendicular.

namespace iText.Kernel.Pdf.Canvas.Parser.Listener {
    internal class TextChunkLocationDefaultImp : ITextChunkLocation {
        ...
        /// <summary>Perpendicular distance to the orientation unit vector (i.e. the Y position in an unrotated coordinate system).
        ///     </summary>
        /// <remarks>
        /// Perpendicular distance to the orientation unit vector (i.e. the Y position in an unrotated coordinate system).
        /// We round to the nearest integer to handle the fuzziness of comparing floats.
        /// </remarks>
        private readonly int distPerpendicular;
        ...
        /// <param name="as">the location to compare to</param>
        /// <returns>true is this location is on the the same line as the other</returns>
        public virtual bool SameLine(ITextChunkLocation @as) {
            ...
            float distPerpendicularDiff = DistPerpendicular() - @as.DistPerpendicular();
            if (distPerpendicularDiff == 0) {
                return true;
            }
            ...
        }
        ...
    }
}

(faktycznie SameLine dalej pozwala na niewielkie odchylenie, jeśli jeden z porównywanych kawałków tekstu ma długość zerowej. Najwyraźniej kawałki o długości zerowej są czasami używane do znaków diakrytycznych, a takie znaki czasami są nakładane na różnych wysokościach. Nie jest to jednak niepokój w swoim przykładowym pliku.)

Jak to naprawić

Jak widzieliśmy powyżej, problem jest spowodowany zachowaniem TextChunkLocationDefaultImp.SameLine. W ten sposób musimy zmienić to zachowanie. Zwykle jednak nie chcemy zmieniać kodu samych klas ITEXT.

Na szczęście LocationTextExtractionStrategy ma konstruktor, który pozwala wstrzyknąć implementację ITextChunkLocationStrategy, tj. Obiekt fabryczny dla instancji ITextChunkLocation.

Tak więc, aby nasze zadanie musimy napisać alternatywę {X0}}, która nie jest tak surowa, a implementacja {x1}}, która generuje instancje naszej implementacji {x2}}.

Niestety jednak TextChunkLocationDefaultImp jest internal do ITEXT i ma liczne zmienne prywatne. W ten sposób nie możemy po prostu wynikać z naszego wdrożenia, ale trzeba kopiować i wkleić go jako całości i zastosować nasze zmiany w tej kopii.

A zatem,

class LaxTextChunkLocationStrategy : LocationTextExtractionStrategy.ITextChunkLocationStrategy
{
    public LaxTextChunkLocationStrategy()
    {
    }

    public virtual ITextChunkLocation CreateLocation(TextRenderInfo renderInfo, LineSegment baseline)
    {
        return new TextChunkLocationLaxImp(baseline.GetStartPoint(), baseline.GetEndPoint(), renderInfo.GetSingleSpaceWidth());
    }
}

class TextChunkLocationLaxImp : ITextChunkLocation
{
    private const float DIACRITICAL_MARKS_ALLOWED_VERTICAL_DEVIATION = 2;
    private readonly Vector startLocation;
    private readonly Vector endLocation;
    private readonly Vector orientationVector;
    private readonly int orientationMagnitude;
    private readonly int distPerpendicular;
    private readonly float distParallelStart;
    private readonly float distParallelEnd;
    private readonly float charSpaceWidth;

    public TextChunkLocationLaxImp(Vector startLocation, Vector endLocation, float charSpaceWidth)
    {
        this.startLocation = startLocation;
        this.endLocation = endLocation;
        this.charSpaceWidth = charSpaceWidth;
        Vector oVector = endLocation.Subtract(startLocation);
        if (oVector.Length() == 0)
        {
            oVector = new Vector(1, 0, 0);
        }
        orientationVector = oVector.Normalize();
        orientationMagnitude = (int)(Math.Atan2(orientationVector.Get(Vector.I2), orientationVector.Get(Vector.I1)) * 1000);
        Vector origin = new Vector(0, 0, 1);
        distPerpendicular = (int)(startLocation.Subtract(origin)).Cross(orientationVector).Get(Vector.I3);
        distParallelStart = orientationVector.Dot(startLocation);
        distParallelEnd = orientationVector.Dot(endLocation);
    }

    public virtual int OrientationMagnitude()
    {
        return orientationMagnitude;
    }

    public virtual int DistPerpendicular()
    {
        return distPerpendicular;
    }

    public virtual float DistParallelStart()
    {
        return distParallelStart;
    }

    public virtual float DistParallelEnd()
    {
        return distParallelEnd;
    }

    public virtual Vector GetStartLocation()
    {
        return startLocation;
    }

    public virtual Vector GetEndLocation()
    {
        return endLocation;
    }

    public virtual float GetCharSpaceWidth()
    {
        return charSpaceWidth;
    }

    public virtual bool SameLine(ITextChunkLocation @as)
    {
        if (OrientationMagnitude() != @as.OrientationMagnitude())
        {
            return false;
        }
        int distPerpendicularDiff = DistPerpendicular() - @as.DistPerpendicular();
        if (Math.Abs(distPerpendicularDiff) < 2)
        {
            return true;
        }
        LineSegment mySegment = new LineSegment(startLocation, endLocation);
        LineSegment otherSegment = new LineSegment(@as.GetStartLocation(), @as.GetEndLocation());
        return Math.Abs(distPerpendicularDiff) <= DIACRITICAL_MARKS_ALLOWED_VERTICAL_DEVIATION && (mySegment.GetLength() == 0 || otherSegment.GetLength() == 0);
    }

    public virtual float DistanceFromEndOf(ITextChunkLocation other)
    {
        return DistParallelStart() - other.DistParallelEnd();
    }

    public virtual bool IsAtWordBoundary(ITextChunkLocation previous)
    {
        if (startLocation.Equals(endLocation) || previous.GetEndLocation().Equals(previous.GetStartLocation()))
        {
            return false;
        }
        float dist = DistanceFromEndOf(previous);
        if (dist < 0)
        {
            dist = previous.DistanceFromEndOf(this);
            //The situation when the chunks intersect. We don't need to add space in this case
            if (dist < 0)
            {
                return false;
            }
        }
        return dist > GetCharSpaceWidth() / 2.0f;
    }

    internal static bool ContainsMark(ITextChunkLocation baseLocation, ITextChunkLocation markLocation)
    {
        return baseLocation.GetStartLocation().Get(Vector.I1) <= markLocation.GetStartLocation().Get(Vector.I1) &&
             baseLocation.GetEndLocation().Get(Vector.I1) >= markLocation.GetEndLocation().Get(Vector.I1) && Math.
            Abs(baseLocation.DistPerpendicular() - markLocation.DistPerpendicular()) <= DIACRITICAL_MARKS_ALLOWED_VERTICAL_DEVIATION;
    }
}

Teraz, aby Twój kod użyć tych klas, wymień

string output = PdfTextExtractor.GetTextFromPage(pdfDoc.GetPage(i));

Przez

LocationTextExtractionStrategy laxStrategy = new LocationTextExtractionStrategy(new LaxTextChunkLocationStrategy());
string output = PdfTextExtractor.GetTextFromPage(pdfDoc.GetPage(i), laxStrategy);

A wynik ekstrakcji tekstu staje się

Artikelnr. Omschrijving Aantal Per stuk Kosten
VERHUUR L. GELEVERDE ARBEID PDC 8 € 43,70 € 349,60
VERHUUR O. GELEVERDE ARBEID PDC 3 € 60,95 € 182,85
VERHUUR L.L. GELEVERDE ARBEID EM 24 € 32,20 € 772,80

Jak było pożądane.

Dodatkowe pytania

Jak zbadać PDF, aby poznać dokładne lokalizacje wierszy

W komentarzu zapytałeś

Czy mogę zapytać, jak wymyśliłeś PDF, aby poznać dokładne lokalizacje wierszy?

Sprawdzałem stronę za pomocą RUPS ITEXT:

screenshot RUPS

W zawartości strumienia wybranego w wystrzeleniu ekranu:

q
...
q
1 0 0 1 60 536 cm
BT
8 0 0 8 0 0 Tm
/F3 1 Tf
(Artikelnr) Tj
8 0 0 8 31.84 0 Tm
(.) Tj
ET
Q
Q
q
...
q
1 0 0 1 147 536 cm
BT
8 0 0 8 0 0 Tm
/F3 1 Tf
(Omschrijving) Tj
ET
Q
Q
q
...
q
1 0 0 1 370 536 cm
BT
8 0 0 8 0 0 Tm
/F3 1 Tf
(Aantal) Tj
ET
Q
Q
q
...
q
1 0 0 1 433.404 535.893 cm
BT
8 0 0 8 0 0 Tm
/F3 1 Tf
(Per stuk) Tj
ET
Q
Q
q
...
q
1 0 0 1 504.878 535.893 cm
BT
8 0 0 8 0 0 Tm
/F3 1 Tf
(Kosten) Tj
ET
Q
Q 

Przed pierwszymi trzema pozycjami, które widzisz

1 0 0 1 XXX 536 cm

Przed ostatnimi dwoma pozycjami, które widzisz

1 0 0 1 XXX 535.893 cm

Ponieważ matryca tekstowa zawsze jest ustawiona z 8 0 0 8 XXX 0 Tm, aby nie mieć części tłumaczenia wzdłuż osi Y, instrukcje cm Powyżej ustawiają układ współrzędnych, dzięki czemu tekst jest narysowany odpowiednio na pozycji Y 536 lub 535.893 .

5
mkl 21 styczeń 2020, 10:57