iText7 считывает строки в неправильном порядке - PullRequest
1 голос
/ 20 января 2020

Я пытаюсь зачитать таблицу документов в формате pdf, но столкнулся с проблемой.

Если я регулярно открываю PDF-файл, он отображается как:

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

Ссылка

Я конвертирую PDF, используя:

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();

Когда я пытаюсь прочитать это с помощью преобразования PDF в текст, в некоторых случаях это выглядит как:

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

Is Есть ли способ решить эту проблему?

1 Ответ

5 голосов
/ 20 января 2020

screenshot provided by the OP

извлекается как

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

Прежде всего, почему это происходит

Как и предполагалось в комментариях к вопрос, действительно, есть небольшой вертикальный шаг, во всех строках первые три столбца установлены в одинаковом вертикальном положении, а вертикальное положение последних двух столбцов немного отличается,

    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

В частности, признается, что строки, разбитые при извлечении текста, - это те строки, в которых цифры до и десятичной точки в позициях y различаются (536 против 535, 475 против 474), а строки с одинаковыми цифрами до-десятичной точки не разбиваются.

Причина этого заключается в том, что класс TextChunkLocationDefaultImp (который по умолчанию используется для хранения местоположений текстовых чанков и методов для сравнения таких расположений) хранит y-позицию чанка (фактически его абстракция также работает для текста, не написанного горизонтально) в целочисленная переменная (private readonly int distPerpendicular) и в методе теста SameLine требуется равенство значений 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;
            }
            ...
        }
        ...
    }
}

(на самом деле * 102 4 * далее допускает небольшое отклонение, если один из сравниваемых фрагментов текста имеет нулевую длину. Очевидно, куски с нулевой длиной иногда используются для диакритических знаков, и такие отметки иногда применяются на разных высотах. Это не относится к вашему файлу примера.)

Как это исправить

Как мы видели выше, проблема связана с поведением TextChunkLocationDefaultImp.SameLine , Таким образом, мы должны изменить это поведение. Однако обычно мы не хотим изменять код самих классов iText.

К счастью, LocationTextExtractionStrategy имеет конструктор, который позволяет внедрить реализацию ITextChunkLocationStrategy, то есть объект фабрики для ITextChunkLocation instance.

Таким образом, для нашей задачи мы должны написать альтернативную реализацию ITextChunkLocation, которая не является столь строгой, и реализацию ITextChunkLocationStrategy, которая генерирует экземпляры нашей реализации ITextChunkLocation.

К сожалению, TextChunkLocationDefaultImp равен internal для iText и имеет множество закрытых переменных. Таким образом, мы не можем просто извлечь нашу реализацию из нее, но должны скопировать и вставить ее целиком и применить наши изменения к этой копии.

Таким образом,

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;
    }
}

Теперь, чтобы сделать ваш код используйте эти классы, замените

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

на

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

, и результат извлечения текста станет

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

, как было необходимо.

Дополнительно вопросы

Как изучить PDF-файл, чтобы узнать точное расположение строк

В комментарии, который вы задали

Могу ли я спросить, как вы открыли PDF-файл, чтобы узнать точное расположение строк?

Я проверил страницу, используя iText RUPS :

screenshot RUPS

В содержимом потока, выбранного на снимке экрана, я нашел:

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 

Перед первыми тремя заголовками вы видите

1 0 0 1 XXX 536 cm

, а перед двумя последними заголовками вы видите

1 0 0 1 XXX 535.893 cm

Поскольку текстовая матрица всегда устанавливается с 8 0 0 8 XXX 0 Tm, чтобы не иметь переводной части вдоль оси y, см * 10 91 * Приведенные выше инструкции устанавливают систему координат так, чтобы текст выводился в позиции y 536 или 535.893 соответственно.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...