spacy-layout

repository·main·Indexed 21 days ago

https://github.com/explosion/spacy-layout

A spaCy plugin that integrates Docling to provide structured processing of PDFs, Word documents, and other formats. It converts documents into spaCy Doc objects enriched with layout information such as sections, headings, pages, and tables represented as pandas DataFrames.

Tokens
2.8K
Snippets
10
Records
10
Agent score
25%

What's inside spacy-layout

  1. Extract and customize table data

    main

    Tables are identified as layout spans with the label "table" and are accessible via the Doc._.tables shortcut. Each table span provides a pandas.DataFrame of its contents through the Span._.data attribute.

    By default, the text in doc.text for a table is a placeholder "TABLE". You can customize this by providing a display_table callback to spaCyLayout. This callback receives the pandas.DataFrame and should return a string representation of the table.

    import pandas as pd
    
    # Customizing table rendering in doc.text
    def display_table(df: pd.DataFrame) -> str:
        return f"Table with columns: {', '.join(df.columns.tolist())}"
    
    layout = spaCyLayout(nlp, display_table=display_table)
    doc = layout("./starcraft.pdf")
    
    # Accessing table data
    for table in doc._.tables:
        # Token position and bounding box
        print(table.start, table.end, table._.layout)
        # pandas.DataFrame of contents
        print(table._.data)
  2. Process a document with spaCyLayout

    main

    To process a document, initialize spaCyLayout with a spaCy nlp object. You can then call the layout object directly on a file path, bytes, or a DoclingDocument to produce a spaCy Doc object containing structured layout information.

    import spacy
    from spacy_layout import spaCyLayout
    
    nlp = spacy.blank("en")
    layout = spaCyLayout(nlp)
    
    # Process a document and create a spaCy Doc object
    doc = layout("./starcraft.pdf")
  3. Serialize and deserialize processed Docs

    main

    To avoid re-running expensive document conversions, you can serialize the processed Doc objects using spaCy's DocBin.

    Important: When deserializing, you must re-initialize spaCyLayout with your nlp object so that the custom extension attributes (like Doc._.layout) are correctly registered and repopulated.

    from spacy.tokens import DocBin
    
    # Serialization
    docs = layout.pipe(["one.pdf", "two.pdf"])
    doc_bin = DocBin(docs=docs, store_user_data=True)
    doc_bin.to_disk("./file.spacy")
    
    # Deserialization
    # Note: layout must be initialized first to register extensions
    layout = spaCyLayout(nlp)
    doc_bin = DocBin(store_user_data=True).from_disk("./file.spacy")
    docs = list(doc_bin.get_docs(nlp.vocab))
  4. Visualize document layout and bounding boxes with matplotlib

    main

    You can visualize the extracted layout by overlaying bounding boxes and section labels onto the original document image using matplotlib.

    To do this:

    1. Render the document page to a numpy array (e.g., using pypdfium2).
    2. Process the document with spaCyLayout to get the doc object.
    3. Iterate through the sections in a specific page.
    4. Use section._.layout.x, section._.layout.y, section._.layout.width, and section._.layout.height to draw Rectangle patches.
    5. Use section.label_ to annotate the bounding boxes with the section type (e.g., 'title', 'text').
    import pypdfium2 as pdfium
    import matplotlib.pyplot as plt
    from matplotlib.patches import Rectangle
    import spacy
    from spacy_layout import spaCyLayout
    
    DOCUMENT_PATH = "./document.pdf"
    
    # Load and convert the PDF page to an image
    pdf = pdfium.PdfDocument(DOCUMENT_PATH)
    page_image = pdf[2].render(scale=1)  # get page 3 (index 2)
    numpy_array = page_image.to_numpy()
    
    # Process document with spaCy
    nlp = spacy.blank("en")
    layout = spaCyLayout(nlp)
    doc = layout(DOCUMENT_PATH)
    
    # Get page 3 layout and sections
    page = doc._.pages[2]
    page_layout = doc._.layout.pages[2]
    
    # Create figure and axis with page dimensions
    fig, ax = plt.subplots(figsize=(12, 16))
    
    # Display the PDF image
    ax.imshow(numpy_array)
    
    # Add rectangles for each section's bounding box
    for section in page[1]:
        # Create rectangle patch
        rect = Rectangle(
            (section._.layout.x, section._.layout.y),
            section._.layout.width,
            section._.layout.height,
            fill=False,
            color="blue",
            linewidth=1,
            alpha=0.5
        )
        ax.add_patch(rect)
        # Add text label at top of box
        ax.text(
            section._.layout.x,
            section._.layout.y,
            section.label_,
            fontsize=8,
            color="red",
            verticalalignment="bottom"
        )
    
    ax.axis("off")  # hide axes
    plt.show()
  5. Iterate through layout spans (sections, headings, etc.)

    main

    The Doc.spans["layout"] attribute contains a SpanGroup of all extracted layout sections (e.g., "text", "title", "section_header"). Each span provides metadata about its position and type.

    for span in doc.spans["layout"]:
        # Section type (e.g., "text", "title", "section_header")
        print(span.label_)
        
        # Document section and token/character offsets
        print(span.text, span.start, span.end, span.start_char, span.end_char)
        
        # Layout features (bounding box, page number)
        print(span._.layout)
        
        # Closest heading to the span
        print(span._.heading)
  6. Process multiple documents at scale with pipe()

    main

    For high-volume processing, use the pipe method. It accepts an iterable of paths or bytes and yields Doc objects. If as_tuples=True is passed, it accepts an iterable of (source, context) tuples and yields (doc, context) tuples, following the standard spaCy Language.pipe pattern.

    paths = ["one.pdf", "two.pdf", "three.pdf", ...]
    for doc in layout.pipe(paths):
        print(doc._.layout)
    
    # Using as_tuples for context preservation
    sources = [("one.pdf", {"id": 1}), ("two.pdf", {"id": 2})]
    for doc, context in layout.pipe(sources, as_tuples=True):
        print(doc._.layout, context)
  7. Access document layout and markdown content

    main

    Once a document is processed, you can access several layout-related attributes via spaCy extension attributes on the Doc object:

    # The text-based contents of the document
    print(doc.text)
    
    # Document layout including pages and page sizes
    print(doc._.layout)
    
    # Markdown representation of the document
    print(doc._.markdown)
    
    # Pages in the document and the spans they contain
    print(doc._.pages)
  8. Reference: spaCyLayout Initialization Arguments

    main

    The spaCyLayout constructor accepts the following arguments:

    layout = spaCyLayout(
        nlp,                                # spacy.language.Language
        separator="\n\n",                  # str: Token used to separate sections. Defaults to "\n\n".
        attrs={},                           # dict[str, str]: Override custom spaCy attributes.
        headings=["section_header", "page_header", "title"], # list[str]: Labels for heading detection.
        display_table="TABLE",             # Callable or str: Function to render tables in text.
        docling_options={}                  # dict: Options passed to Docling's DocumentConverter.
    )
  9. Reference: Layout Data Structures

    main

    The following dataclasses define the layout information available via Doc._.layout, Doc._.pages, and Span._.layout:

    # PageLayout
    # - page_no: int (1-indexed)
    # - width: float (pixels)
    # - height: float (pixels)
    
    # DocLayout
    # - pages: list[PageLayout]
    
    # SpanLayout
    # - x: float (pixels)
    # - y: float (pixels)
    # - width: float (pixels)
    # - height: float (pixels)
    # - page_no: int