jsvine / pdfplumber

Plumb a PDF for detailed information about each char, rectangle, line, et cetera — and easily extract text and tables.
MIT License
6.1k stars 625 forks source link

PSLiteral object using non_stroking_color from a rectangle #828

Open luanmota opened 1 year ago

luanmota commented 1 year ago

Describe the bug

In my code I check if an obj is a rect and do some filters using the non_stroking_color property. But in one pdf the non_stroking_color is a PSLiteral obj and not a float. And if a change my code to check if the non_stroking_color is a float, the text is extracted with triple letters in each word.

Code to reproduce the problem

def get_plumber_table(page):
    tables = []
    tables = page.filter(keep_visible_lines).find_tables(
        table_settings={
            "vertical_strategy":
            "lines",
            "horizontal_strategy":
            "lines",
            "explicit_vertical_lines":
            page.filter(keep_visible_lines).curves +
            page.filter(keep_visible_lines).edges,
            "explicit_horizontal_lines":
            page.filter(keep_visible_lines).curves +
            page.filter(keep_visible_lines).edges,
        })
    return tables

def keep_visible_lines(obj):
    if obj['object_type'] == 'rect' or obj['object_type'] == 'edge':
        height = obj['height']
        width = obj['width']
        if width < 1.0 and height < 1.0:
            return False
        non_stroking_color = obj['non_stroking_color']
        if type(non_stroking_color) is tuple:
            if non_stroking_color == (0, 0, 0):
                return True
            else:
                return False
        if type(non_stroking_color) is list:
            non_stroking_color = min(non_stroking_color)
        if non_stroking_color is not None and non_stroking_color > 0.6:
            return False
    return True

with pdfplumber.open(pdf_path, laparams={}) as pdf:
        for pn in range(0, len(pdf.pages)):
            tables = get_plumber_table(page)

PDF file

Edital053_Assinado.pdf

Environment

samkit-jain commented 1 year ago

Hi @luanmota Appreciate your interest in the library. The non_stroking_color in pdfplumber comes from the pdfminer.six' PDFGraphicState.ncolor. My recommendation would be to also open an issue (or start a discussion) on the pdfminer.six repo.

jsvine commented 1 year ago

Hi @luanmota, a couple of additional notes:

luanmota commented 1 year ago

Hey @jsvine thanks for your time and help!

Sorry for the "noob" question, but what exactly pdfplumber.utils.resolve_and_decode(...) does? I didn't find this function in the documentation.

Thanks!

jsvine commented 1 year ago

@luanmota, no apology necessary! That's a utility method that's mainly used internally (and thus not listed in the core documentation), but might be useful here for your edge-case. It resolves any indirect object references (not an issue for you) and converts any PSLiterals into standard text (your issue). You can see its implementation here: https://github.com/jsvine/pdfplumber/blob/ee48b26099a614b9e97465963a5ff46aa2b04e46/pdfplumber/utils/pdfinternals.py#L19-L34

luanmota commented 1 year ago

@jsvine I tested the resolve_and_decode and in some cases non_stroking_color is a list with this inside: /'Pattern1' Do you jave any ideia what it can be? I tried to find in the pdfminer.six but nothing there.

I think we can close this issue if you don't have any ideia how I can undertand what is this return. I find another problem with this PDF with duplicate chars and resolve with dedupe_chars function. Pdfplumber is a really great tool!!! Thanks again for the help :)

jsvine commented 1 year ago

Thanks for the kind words @luanmota, and thanks for the very interesting example. The PDF specification has a section ("4.6 Patterns") on patterns, and it seems like this is what the non-stroking-color value is trying to use. Per the example of p. 296–297, it seems that this approach is valid. (My mistake on thinking it was invalid earlier.)

Accessing details about the pattern is possible, using page.page_obj.resources to access the raw resource information gathered by pdfminer.six. E.g., for your example:

page = pdf.pages[33]
p1 = page.page_obj.resources["Pattern"]["Pattern1"]
print(pdfplumber.utils.resolve_and_decode(p1))

... which gives you:

{'Matrix': [0.75, 0, 0, -0.75, 0, 841.92004],
 'PatternType': 2,
 'Shading': {'ColorSpace': 'DeviceRGB',
  'Coords': [0, 152.48, 0, 153.75999],
  'Extend': [True, True],
  'Function': {'Bounds': [0.5, 0.5],
   'Domain': [0, 1],
   'Encode': [0, 1, 0, 1, 0, 1],
   'FunctionType': 3,
   'Functions': [{'C0': [0, 0, 0.03922],
     'C1': [0, 0, 0.03922],
     'Domain': [0, 1],
     'FunctionType': 2,
     'N': 1},
    {'C0': [0, 0, 0.03922],
     'C1': [0, 0, 0],
     'Domain': [0, 1],
     'FunctionType': 2,
     'N': 1},
    {'C0': [0, 0, 0],
     'C1': [0, 0, 0],
     'Domain': [0, 1],
     'FunctionType': 2,
     'N': 1}]},
  'ShadingType': 2},
 'Type': 'Pattern'}