【问题标题】:How to Detect table start in itextSharp?如何在 itextSharp 中检测表开始?
【发布时间】:2013-03-23 23:22:43
【问题描述】:

我正在尝试将 pdf 转换为 csv 文件。 pdf文件具有表格格式的数据,第一行作为标题。我已经达到了可以从单元格中提取文本、比较表格中文本的基线并检测换行符的水平,但我需要比较表格边框以检测表格的开头。我不知道如何检测和比较 PDF 中的行。谁能帮帮我?

谢谢!!!

【问题讨论】:

    标签: pdf csv itextsharp pdf-reader pdf-parsing


    【解决方案1】:

    如您所见(希望如此),PDF 没有表格的概念,只有放置在特定位置的文本和围绕它们绘制的线条。文本和行之间没有内在联系。理解这一点非常重要。

    知道了这一点,如果所有单元格都有足够的填充,您可以寻找足够大的字符之间的间隙,例如 3 个或更多空格的宽度。如果单元格没有足够的间距,这很可能会损坏。

    您还可以查看 PDF 中的每一行,并尝试找出代表“表格”行的内容。请参阅 this answer for how to walk every token on a page 以查看正在绘制的内容。

    【讨论】:

    • +1;我同意一般描述。关于按原样检查页面内容的方式,我建议使用 iText 解析器包而不是手动检查页面内容。
    • iText 中是否有形状/线条的解析器?
    • 还没有,但是解析器包类很容易扩展,也可以提供有关路径的信息。不过,更重要的是,解析器类为 RenderListener 提供了关于文本宽度和位置的非常准确的信息。要手动检索相同的信息,必须考虑一些可能的运算符。
    • @Jignesh 本质上,您必须通过创建新的ContentOperator 实现来扩展PdfContentStreamProcessor 的功能,该实现处理您需要的路径命令并使用PdfContentStreamProcessor 方法将它们注册到您的PdfContentStreamProcessor 中.为了使这些新的内容操作符类能够通用地向路径命令发出信号,您应该扩展RenderListener 接口以包含路径操作的回调。
    • 背景颜色只是用填充颜色集绘制的形状或线条集。您只需要在矩形中寻找这些填充物并计算它们相对于您的单元格的实际位置。您可能会发现重叠的形状可能会使事情更加混乱。
    【解决方案2】:

    我也在寻找类似问题的答案,但不幸的是我没有找到,所以我自己做了。

    这样的PDF页面

    将输出为

    这是我制作的 dotnet 控制台应用程序的 github 链接。 https://github.com/Justabhi96/Detect_And_Extract_Table_From_Pdf

    此应用程序检测 PDF 特定页面中的表格,并在控制台上以表格格式打印它们。 这是我用来制作这个应用程序的代码。

    首先,我使用扩展了 iTextSharp 的 iTextSharp.text.pdf.parser.LocationTextExtractionStrategy 类的类从 PDF 中取出文本及其坐标。代码如下:

    这是用于存储带有坐标和文本的块的类。

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

    该类覆盖 LocationTextExtractionStrategy 类的 RenderText 方法,每次您使用 PdfTextExtractor.GetTextFromPage() 方法从 PDF 页面提取块时都会调用该方法。

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

    【讨论】:

      猜你喜欢
      • 2014-06-15
      • 1970-01-01
      • 1970-01-01
      • 2010-12-04
      • 2016-01-11
      • 1970-01-01
      • 2017-09-25
      • 1970-01-01
      • 2017-04-08
      相关资源
      最近更新 更多