извлекается как
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 :
В содержимом потока, выбранного на снимке экрана, я нашел:
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 соответственно.