Как определить начало таблицы в itextSharp?

Я пытаюсь преобразовать pdf в файл csv. pdf файл имеет данные в табличном формате с первой строкой в ​​качестве заголовка. Я достиг уровня, на котором я могу извлечь текст из ячейки, сравнить базовую линию текста в таблице и определить новую строку, но мне нужно сравнить границы таблицы, чтобы определить начало таблицы. Я не знаю, как обнаружить и сравнить строки в PDF. Может кто-нибудь помочь мне?

Спасибо!!!


person NetDeveloper    schedule 02.04.2013    source источник


Ответы (2)


Как вы видели (надеюсь), в PDF-файлах нет концепции таблиц, только текст, размещенный в определенных местах, и линии, нарисованные вокруг них. Между текстом и строками нет внутренней связи. Это очень важно понять.

Зная это, если все ячейки имеют достаточно отступов, вы можете искать пробелы между символами, которые достаточно велики, например, шириной 3 или более пробелов. Если в ячейках недостаточно места, это, к сожалению, может сломаться.

Вы также можете просмотреть каждую строку в PDF-файле и попытаться выяснить, что представляет собой ваши «табличные» строки. См. этот ответ о том, как пройти каждый токен на странице, чтобы увидеть, что рисуется.

person Chris Haas    schedule 02.04.2013
comment
+1; Присоединяюсь к общему описанию. Что касается способа проверки содержимого страницы как есть, я бы предложил использовать пакет синтаксического анализатора iText вместо ручной проверки содержимого страницы. - person mkl; 02.04.2013
comment
Есть ли анализатор фигур/линий в iText? - person Chris Haas; 02.04.2013
comment
Пока нет, но классы пакета синтаксического анализатора достаточно легко расширить, чтобы они также предоставляли информацию о путях. Что еще более важно, классы парсера предоставляют RenderListeners довольно точную информацию о ширине и позициях текста. Чтобы получить ту же информацию вручную, необходимо было бы принять во внимание несколько возможных операторов. - person mkl; 03.04.2013
comment
@mkl, не могли бы вы сообщить мне, какие классы я могу расширить, чтобы получить строки и текстовые позиции? - person NetDeveloper; 05.04.2013
comment
@Jignesh По сути, вы должны расширить функциональность PdfContentStreamProcessor, создав новые реализации ContentOperator, которые обрабатывают необходимые вам команды пути, и зарегистрировав их в своем PdfContentStreamProcessor, используя его метод registerContentOperator. Чтобы эти новые классы операторов содержимого могли в общем сигнализировать о командах пути, вы должны расширить интерфейс RenderListener, чтобы он также содержал обратные вызовы для операций пути. - person mkl; 06.04.2013
comment
@ChrisHaas Мне удалось преобразовать в файл csv, но единственная проблема, с которой я столкнулся, заключается в том, что в таблице есть строки, которые не представляют фактические строки в таблице. Поэтому мне трудно получить ячейку с несколькими строками текста. Но я могу различать строки по цвету фона. Я прочитал ваш пост, и вы сказали, что невозможно получить цвет фона за текстом. Это все еще так? - person NetDeveloper; 11.04.2013
comment
Цвет фона — это просто форма или набор линий, нарисованных с помощью набора цветов заливки. Вам просто нужно будет пройтись по прямоугольнику в поисках этих заливок и вычислить, где они на самом деле находятся по отношению к вашей ячейке. Вполне возможно, что вы найдете перекрывающиеся формы, которые могут еще больше запутать ситуацию. - person Chris Haas; 12.04.2013
comment
@ Крис Я понял!! В моем случае это простая таблица отчетов, поэтому нет перекрывающихся или сложных форм. Спасибо!!! - person NetDeveloper; 19.04.2013

Я также искал ответ на аналогичный вопрос, но, к сожалению, не нашел его, поэтому сделал это самостоятельно.

Такая страница PDFвведите здесь описание изображения

Выдаст результат в виде введите здесь описание изображения

Вот ссылка github для консольного приложения dotnet, которое я сделал. https://github.com/Justabhi96/Detect_And_Extract_Table_From_Pdf

Это приложение обнаруживает таблицу на определенной странице PDF и печатает их в формате таблицы на консоли. Вот код, который я использовал для создания этого приложения.

Прежде всего, я взял текст из PDF вместе с их координатами, используя класс, который расширяет класс iTextSharp.text.pdf.parser.LocationTextExtractionStrategy класса iTextSharp. Кодекс выглядит следующим образом:

Это класс, который будет хранить фрагменты с координатами и текстом.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace itextPdfTextCoordinates
{
    public class RectAndText
    {
        public iTextSharp.text.Rectangle Rect;
        public String Text;
        public RectAndText(iTextSharp.text.Rectangle rect, String text)
        {
            this.Rect = rect;
            this.Text = text;
        }

    }
}

И это класс, который расширяет класс LocationTextExtractionStrategy.

using iTextSharp.text.pdf.parser;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace itextPdfTextCoordinates
{
    public class MyLocationTextExtractionStrategy : LocationTextExtractionStrategy
    {
        public List<RectAndText> myPoints = new List<RectAndText>();

        //Automatically called for each chunk of text in the PDF
        public override void RenderText(TextRenderInfo renderInfo)
        {
            base.RenderText(renderInfo);

            //Get the bounding box for the chunk of text
            var bottomLeft = renderInfo.GetDescentLine().GetStartPoint();
            var topRight = renderInfo.GetAscentLine().GetEndPoint();

            //Create a rectangle from it
            var rect = new iTextSharp.text.Rectangle(
                                                    bottomLeft[Vector.I1],
                                                    bottomLeft[Vector.I2],
                                                    topRight[Vector.I1],
                                                    topRight[Vector.I2]
                                                    );

            //Add this to our main collection
            this.myPoints.Add(new RectAndText(rect, renderInfo.GetText()));
        }
    }
}

Этот класс переопределяет метод RenderText класса LocationTextExtractionStrategy, который будет вызываться каждый раз, когда вы извлекаете фрагменты из страницы PDF с помощью метода PdfTextExtractor.GetTextFromPage().

using itextPdfTextCoordinates;
using iTextSharp.text.pdf;
//Create an instance of our strategy
var t = new MyLocationTextExtractionStrategy();
var path = "F:\\sample-data.pdf";
//Parse page 1 of the document above
using (var r = new PdfReader(path))
{
    for (var i = 1; i <= r.NumberOfPages; i++)
    {
        // Calling this function adds all the chunks with their coordinates to the 
        // 'myPoints' variable of 'MyLocationTextExtractionStrategy' Class
        var ex = iTextSharp.text.pdf.parser.PdfTextExtractor.GetTextFromPage(r, i, t);
    }
}
//Here you can loop over the chunks of PDF
foreach(chunk in t.myPoints){
    Console.WriteLine("character {0} is at {1}*{2}",i.Text,i.Rect.Left,i.Rect.Top);
}

Теперь для определения начала и конца таблицы вы можете использовать координаты фрагментов, извлеченных из PDF. Например, если в конкретной строке нет таблицы, тогда не будет скачков по правой координате текущего фрагмента и по левой координате следующего фрагмента. Но строки с таблицей будут иметь скачки координат не менее чем на 3 точки.

Например, для строк, имеющих таблицу, координаты фрагментов будут примерно такими:

правая координата текущего чанка -> 12.75pts
левая координата следующего чанка -> 20.30pts

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

using itextPdfTextCoordinates;
using iTextSharp.text.pdf;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class LineUsingCoordinates
    {
        public static List<List<string>> getLineText(string path, int page, float[] coord)
        {
            //Create an instance of our strategy
            var t = new MyLocationTextExtractionStrategy();

            //Parse page 1 of the document above
            using (var r = new PdfReader(path))
            {
                // Calling this function adds all the chunks with their coordinates to the 
                // 'myPoints' variable of 'MyLocationTextExtractionStrategy' Class
                var ex = iTextSharp.text.pdf.parser.PdfTextExtractor.GetTextFromPage(r, page, t);
            }
            // List of columns in one line
            List<string> lineWord = new List<string>();
            // temporary list for working around appending the <List<List<string>>
            List<string> tempWord;
            // List of rows. rows are list of string
            List<List<string>> lineText = new List<List<string>>();
            // List consisting list of chunks related to each line
            List<List<RectAndText>> lineChunksList = new List<List<RectAndText>>();
            //List consisting the chunks for whole page;
            List<RectAndText> chunksList;
            // List consisting the list of Bottom coord of the lines present in the page 
            List<float> bottomPointList = new List<float>();

            //Getting List of Coordinates of Lines in the page no matter it's a table or not
            foreach (var i in t.myPoints)
            {
                Console.WriteLine("character {0} is at {1}*{2}", i.Text, i.Rect.Left, i.Rect.Top);
                // If the coords passed to the function is not null then process the part in the 
                // given coords of the page otherwise process the whole page
                if (coord != null)
                {
                    if (i.Rect.Left >= coord[0] &&
                        i.Rect.Bottom >= coord[1] &&
                        i.Rect.Right <= coord[2] &&
                        i.Rect.Top <= coord[3])
                    {
                        float bottom = i.Rect.Bottom;
                        if (bottomPointList.Count == 0)
                        {
                            bottomPointList.Add(bottom);
                        }
                        else if (Math.Abs(bottomPointList.Last() - bottom) > 3)
                        {
                            bottomPointList.Add(bottom);
                        }
                    }
                }
                // else process the whole page
                else
                {
                    float bottom = i.Rect.Bottom;
                    if (bottomPointList.Count == 0)
                    {
                        bottomPointList.Add(bottom);
                    }
                    else if (Math.Abs(bottomPointList.Last() - bottom) > 3)
                    {
                        bottomPointList.Add(bottom);
                    }
                }
            }

            // Sometimes the above List will be having some elements which are from the same line but are
            // having different coordinates due to some characters like " ",".",etc.
            // And these coordinates will be having the difference of at most 4 points between 
            // their bottom coordinates. 

            //so to remove those elements we create two new lists which we need to remove from the original list 

            //This list will be having the elements which are having different but a little difference in coordinates 
            List<float> removeList = new List<float>();
            // This list is having the elements which are having the same coordinates
            List<float> sameList = new List<float>();

            // Here we are adding the elements in those two lists to remove the elements
            // from the original list later
            for (var i = 0; i < bottomPointList.Count; i++)
            {
                var basePoint = bottomPointList[i];
                for (var j = i+1; j < bottomPointList.Count; j++)
                {
                    var comparePoint = bottomPointList[j];
                    //here we are getting the elements with same coordinates
                    if (Math.Abs(comparePoint - basePoint) == 0)
                    {
                        sameList.Add(comparePoint);
                    }
                    // here ae are getting the elements which are having different but the diference
                    // of less than 4 points
                    else if (Math.Abs(comparePoint - basePoint) < 4)
                    {
                        removeList.Add(comparePoint);
                    }
                }
            }

            // Here we are removing the matching elements of remove list from the original list 
            bottomPointList = bottomPointList.Where(item => !removeList.Contains(item)).ToList();

            //Here we are removing the first matching element of same list from the original list
            foreach (var r in sameList)
            {
                bottomPointList.Remove(r);
            }

            // Here we are getting the characters of the same line in a List 'chunkList'.
            foreach (var bottomPoint in bottomPointList)
            {
                chunksList = new List<RectAndText>();
                for (int i = 0; i < t.myPoints.Count; i++)
                {
                    // If the character is having same bottom coord then add it to chunkList
                    if (bottomPoint == t.myPoints[i].Rect.Bottom)
                    {
                        chunksList.Add(t.myPoints[i]);
                    }
                    // If character is having a difference of less than 3 in the bottom coord then also
                    // add it to chunkList because the coord of the next line will differ at least 10 points
                    // from the coord of current line
                    else if (Math.Abs(t.myPoints[i].Rect.Bottom - bottomPoint) < 3)
                    {
                        chunksList.Add(t.myPoints[i]);
                    }
                }
                // Here we are adding the chunkList related to each line
                lineChunksList.Add(chunksList);
            }
            bool sameLine = false;

            //Here we are looping through the lines consisting the chunks related to each line 
            foreach(var linechunk in lineChunksList)
            {
                var text = "";
                // Here we are looping through the chunks of the specific line to put the texts
                // that are having a cord jump in their left coordinates.
                // because only the line having table will be having the coord jumps in their 
                // left coord not the line having texts
                for (var i = 0; i< linechunk.Count-1; i++)
                {
                    // If the coord is having a jump of less than 3 points then it will be in the same
                    // column otherwise the next chunk belongs to different column
                    if (Math.Abs(linechunk[i].Rect.Right - linechunk[i + 1].Rect.Left) < 3)
                    {
                        if (i == linechunk.Count - 2)
                        {
                            text += linechunk[i].Text + linechunk[i+1].Text ;
                        }
                        else
                        {
                            text += linechunk[i].Text;
                        }
                    }
                    else
                    {
                        if (i == linechunk.Count - 2)
                        {
                            // add the text to the column and set the value of next column to ""
                            text += linechunk[i].Text;
                            // this is the list of columns in other word its the row
                            lineWord.Add(text);
                            text = "";
                            text += linechunk[i + 1].Text;
                            lineWord.Add(text);
                            text = "";
                        }
                        else
                        {
                            text += linechunk[i].Text;
                            lineWord.Add(text);
                            text = "";
                        }
                    }                        
                }
                if(text.Trim() != "")
                {
                    lineWord.Add(text);
                }
                // creating a temporary list of strings for the List<List<string>> manipulation
                tempWord = new List<string>();
                tempWord.AddRange(lineWord);
                // "lineText" is the type of List<List<string>>
                // this is our list of rows. and rows are List of strings
                // here we are adding the row to the list of rows
                lineText.Add(tempWord);
                lineWord.Clear();
            }

            return lineText;
        }
    }
}

Вы можете вызвать метод getLineText() вышеуказанного класса и запустить следующий цикл, чтобы просмотреть вывод в структуре таблицы на консоли.

    var testFile = "F:\\sample-data.pdf";
    float[] limitCoordinates = { 52, 671, 357, 728 };//{LowerLeftX,LowerLeftY,UpperRightX,UpperRightY}

    // This line gives the lists of rows consisting of one or more columns
    //if you pass the third parameter as null the it returns the content for whole page
    // but if you pass the coordinates then it returns the content for that coords only
    var lineText = LineUsingCoordinates.getLineText(testFile, 1, null);
    //var lineText = LineUsingCoordinates.getLineText(testFile, 1, limitCoordinates);
    // For detecting the table we are using the fact that the 'lineText' item which length is 
    // less than two is surely not the part of the table and the item which is having more than
    // 2 elements is the part of table
    foreach (var row in lineText)
    {
        if (row.Count > 1)
        {
            for (var col = 0; col < row.Count; col++)
            {
                string trimmedValue = row[col].Trim();
                if (trimmedValue != "")
                {
                    Console.Write("|" + trimmedValue + "|");
                }
            }
            Console.WriteLine("");
        }
    }
    Console.ReadLine();
person Abhishek    schedule 15.09.2018