PDF Parsing — docling + liteparse + pdfplumber

SkillDocs & knowledge

Choose between docling, liteparse and pdfplumber for PDF extraction, tables, layout, fonts, screenshots, vector graphics.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the PDF Parsing — docling + liteparse + pdfplumber skill

What this skill tells your AI

The instructions your AI receives, as published by jokull/icelandic-data in .agents/skills/pdf-parsing/SKILL.md and read by ahel’s review.

Requires: Tier 0 (core) for pdfplumber; Tier 3 (pdf) for docling/liteparse: uv sync --group pdf.

Three tools for PDF extraction. Each has a sweet spot — use the right one (or combine them).

When to use what

TaskTool
Tables (especially borderless financial)docling — AI layout model (TableFormer) handles ársreikningar with no visible borders
Structured document (headings, sections, reading order)docling — classifies every element with semantic labels
Figure/image extraction as PIL imagesdocling with generate_picture_images=True
Markdown/HTML exportdoclingexport_to_markdown(), export_to_html()
Text with font info (name, size per fragment)liteparse.fontName, .fontSize on each TextItem
Page screenshots at configurable DPIliteparseparser.screenshot()
Quick text extraction with coordinatesliteparse — fast, simple API
Vector graphics detection (rects, curves, lines)pdfplumber — detects infographic vs text pages
Character-level accesspdfplumber
DOCX, PPTX, XLSX, HTMLdocling — same API for all formats

docling (primary tool)

IBM's AI-powered document parser. 97.9% table accuracy on benchmarks. The heavy hitter.

uv pip install docling   # v2.74.0

Basic usage

from docling.document_converter import DocumentConverter, PdfFormatOption
from docling.datamodel.pipeline_options import PdfPipelineOptions
from docling.datamodel.base_models import InputFormat

# Configure — ALWAYS disable OCR for native/digital PDFs
po = PdfPipelineOptions()
po.do_ocr = False                      # most Icelandic public PDFs are native text
po.do_table_structure = True           # TableFormer — the killer feature
po.generate_picture_images = True      # extract figures as PIL images

converter = DocumentConverter(
    format_options={InputFormat.PDF: PdfFormatOption(pipeline_options=po)}
)

result = converter.convert("report.pdf")
doc = result.document

Exports

doc.export_to_markdown()    # clean structured markdown
doc.export_to_text()        # plain text
doc.export_to_html()        # full HTML
doc.export_to_dict()        # JSON-serializable dict

# Save with images
doc.save_as_markdown("out.md", image_mode=ImageRefMode.REFERENCED)
doc.save_as_json("out.json")   # images base64-embedded by default

# Per-page markdown
doc.export_to_markdown(page_no=3)

Tables

for table in doc.tables:
    df = table.export_to_dataframe(doc)   # pandas DataFrame
    md = table.export_to_markdown(doc)    # markdown table
    html = table.export_to_html(doc)      # HTML table

    # Cell-level access
    for row in table.data.grid:
        for cell in row:
            print(cell.text, cell.column_header, cell.row_header)
            print(cell.bbox)  # BoundingBox(l, t, r, b)

docling finds tables that pdfplumber misses entirely — borderless financial tables where alignment is the only structure. Critical for Icelandic ársreikningar.

Figures

for pic in doc.pictures:
    if pic.image:
        pil_img = pic.image.pil_image   # PIL.Image (property, not method)
        pil_img.save(f"figure_{pic.self_ref}.png")
    # Position
    for prov in pic.prov:
        print(f"Page {prov.page_no}, bbox: {prov.bbox}")

Document tree & element types

from docling.datamodel.document import DocItemLabel

for item, level in doc.iterate_items():
    label = item.label  # DocItemLabel enum
    # Labels: text, section_header, title, list_item, table, picture,
    #         caption, footnote, page_header, page_footer, chart, formula, code
    if hasattr(item, 'prov') and item.prov:
        bbox = item.prov[0].bbox   # BoundingBox(l, t, r, b, coord_origin=BOTTOMLEFT)
        page = item.prov[0].page_no

# Heading levels
for item, level in doc.iterate_items():
    if isinstance(item, SectionHeaderItem):
        print(f"H{item.level}: {item.text}")

Page range + batch

# Parse only pages 1-10
result = converter.convert("big.pdf", page_range=(1, 10))

# Batch convert
for result in converter.convert_all(["a.pdf", "b.pdf", "c.pdf"]):
    print(result.document.name, result.status)

Pipeline options reference

OptionDefaultNotes
do_ocrTrueDisable for native PDFs — saves ~3-5s/page
do_table_structureTrueTableFormer (ACCURATE mode). Set mode=TableFormerMode.FAST for speed
generate_picture_imagesFalseExtract PictureItem as PIL images
generate_page_imagesFalseFull-page rasters
do_picture_classificationFalseAI picture type classification
do_picture_descriptionFalseVLM captioning (SmolVLM-256M)
do_chart_extractionFalseChart data extraction

Confidence scores

conf = result.confidence
print(conf.parse_score)    # 1.0 = native PDF parsed cleanly
print(conf.layout_score)   # 0.8–0.9 typical
print(conf.mean_grade)     # QualityGrade.GOOD / EXCELLENT

Icelandic OCR (scanned PDFs only)

from docling.datamodel.pipeline_options import OcrMacOptions  # macOS only (Apple Vision)

po.do_ocr = True
po.ocr_options = OcrMacOptions(lang=["is-IS", "en-US"])  # Apple Vision
# Linux/Windows: use TesseractOcrOptions(lang=["isl"]) instead (needs tesseract + isl.traineddata),
# or EasyOcrOptions(lang=["is"]) which needs no system install.

Most Icelandic public PDFs (skatturinn, sedlabanki, Hagstofan) are native text — do_ocr=False is correct.

Performance (M-series Mac)

Config32 pages
No OCR, no tables~8s
No OCR, with tables~15-25s
First call (model loading)+5-10s overhead

liteparse

LlamaIndex's fast local parser. Best for: font-level detail, page screenshots, quick text+coords.

from liteparse import LiteParse

parser = LiteParse()

# Parse (no OCR for native PDFs)
result = parser.parse("doc.pdf", ocr_enabled=False)

for page in result.pages:
    for item in page.textItems:
        # Per-fragment font info — docling doesn't have this
        print(item.text, item.x, item.y, item.fontName, item.fontSize)

CLI

lit parse doc.pdf --format json --no-ocr -o out.json
lit parse doc.pdf --target-pages "1-5" --ocr-language is
lit screenshot doc.pdf -o ./shots --dpi 200
lit batch-parse ./pdfs ./outputs --recursive

Screenshots (docling can't do this as easily)

result = parser.screenshot("doc.pdf", output_dir="./shots", dpi=200)
for s in result.screenshots:
    print(f"Page {s.page_num}: {s.image_path}")

Data model

  • ParseResult.pages → list of ParsedPage
  • ParsedPage: .pageNum, .width, .height, .text, .textItems, .boundingBoxes
  • TextItem: .text, .x, .y, .width, .height, .fontName, .fontSize, .confidence
  • BoundingBox: .x1, .y1, .x2, .y2

Coordinates: top-left origin, PDF points (1pt = 1/72 inch). A4 = 595 x 842.


pdfplumber

Low-level PDF access. Best for: vector graphics detection, character-level parsing, line/rect/curve geometry.

import pdfplumber

with pdfplumber.open("doc.pdf") as pdf:
    for page in pdf.pages:
        # Vector graphics — detect infographics
        images = page.images or []     # embedded raster images
        rects = page.rects or []       # rectangles
        curves = page.curves or []     # bezier curves (SVG-like vector art)
        lines = page.lines or []       # line segments

        # Page type heuristic
        text = page.extract_text() or ""
        if len(curves) > 10 and not images:
            page_type = "infographic"
        elif images and len(text) < 200:
            page_type = "photo"
        elif images:
            page_type = "mixed"
        else:
            page_type = "text"

pdfplumber's table extraction (page.extract_tables()) requires visible borders — it fails on borderless financial tables. Use docling for those.


Combined extraction pattern

import liteparse
import pdfplumber
from docling.document_converter import DocumentConverter, PdfFormatOption
from docling.datamodel.pipeline_options import PdfPipelineOptions
from docling.datamodel.base_models import InputFormat

def extract_pdf(path):
    """Full extraction: structure (docling) + fonts (liteparse) + graphics (pdfplumber)."""

    # 1. docling — structure, tables, figures
    po = PdfPipelineOptions(do_ocr=False, generate_picture_images=True)
    converter = DocumentConverter(
        format_options={InputFormat.PDF: PdfFormatOption(pipeline_options=po)}
    )
    doc_result = converter.convert(path)
    doc = doc_result.document

    # 2. liteparse — font info + screenshots
    lp = liteparse.LiteParse()
    lp_result = lp.parse(path, ocr_enabled=False)
    screenshots = lp.screenshot(path, output_dir="/tmp/shots", dpi=200)

    # 3. pdfplumber — vector graphics detection
    with pdfplumber.open(path) as pdf:
        for i, page in enumerate(pdf.pages):
            curves = page.curves or []
            if len(curves) > 10:
                print(f"Page {i+1}: infographic ({len(curves)} curves)")

    return {
        "markdown": doc.export_to_markdown(),
        "tables": [t.export_to_dataframe(doc) for t in doc.tables],
        "figures": [p.image.pil_image for p in doc.pictures if p.image],
        "font_map": {
            p.pageNum: [(t.text, t.fontName, t.fontSize) for t in p.textItems]
            for p in lp_result.pages
        },
        "screenshots": {s.page_num: s.image_path for s in screenshots.screenshots},
    }

Caveats

  • Ligature artifacts: Some PDFs encode fff/f_short in font — all three tools pass this through. Not fixable without font-level glyph remapping.
  • Icelandic numbers: Financial PDFs use dot-thousands (1.279.828) and parenthetical negatives (1.279.828). All tools preserve these as strings — post-process with text.replace(".", "").replace("(", "-").replace(")", "").
  • Coordinate systems differ: liteparse uses top-left origin; docling BoundingBox uses bottom-left (coord_origin=BOTTOMLEFT). Convert with top_y = page_height - bbox.b.
  • docling model loading: First call in a process loads TableFormer (~5-10s). Reuse the DocumentConverter instance.
  • liteparse under the hood: Calls npx @llamaindex/liteparse — requires Node.js.

Signals

GitHub stars
54
Forks
4
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
pdf-parsing
Source
github.com/jokull/icelandic-data