← Back to list

PDF Parser Showdown: How We Slashed Document Processing Costs by ~90% Switching from Google Vision…

If your startup or investment firm deals with hundreds (or thousands) of financial PDFs every month annual reports, quarterly results…

Priyanshu parashar · 2026-01-29 07:43 · 4 claps · 4.8 min read
#ai #pdf #docling #priyanshu #python
Open on Medium ↗
Wiki topics: AI · AI · General INV · Investing & Markets STP · Startups & Venture ECO · Economy · General

PDF Parser Showdown: How We Slashed Document Processing Costs by ~90% Switching from Google Vision to IBM Docling

If your startup or investment firm deals with hundreds (or thousands) of financial PDFs every month annual reports, quarterly results, investor decks, NSE/BSE filings then you already know the dirty secret:

Extracting clean, usable data from these 500–800 page monsters is surprisingly expensive.

We were burning serious money on Google Cloud Vision (and later Document AI) just to turn scanned PDFs + complex layouts into markdown or structured data. The bill kept climbing, accuracy on charts/tables was inconsistent, and latency started hurting our pipelines.

Then we discovered IBM Docling an open-source PDF-to-markdown powerhouse from IBM Research and everything changed.

In this post I’ll share:

  • Why the big cloud OCR APIs become painfully expensive at scale
  • Why most open-source PDF libraries (PyPDF2, pdfplumber, etc.) collapse on real financial documents
  • Our production-grade hybrid solution using Docling + selective Gemini calls
  • Real numbers: cost savings, speed, and quality we achieved
  • Ready-to-adapt code that’s running in our stack today

If you’re a founder trying to keep burn low, a quant/data engineer tired of broken tables, or a recruiter looking for people who ship cost-effective AI infra keep reading.

The Problem When “Enterprise OCR” Starts Feeling Like a Tax

Our workload in one sentence:

Millions of pages per year from listed & unlisted companies mostly 400–800 page PDFs packed with dense tables, embedded charts, multi-column layouts, footnotes, and occasional scanned pages.

We initially used Google Cloud Vision / Document AI because:

  • It handled OCR well
  • It gave us markdown-like output
  • It could detect some tables and figures

But reality hit hard:

At a few thousand documents per month we were already spending four-to-five figures just on parsing before any LLM inference or storage.

Goal: Cut cost by 80–95%, keep (or improve) table/chart fidelity, and run everything in our own environment.

First Attempts Why DIY + Popular Libraries Failed Us

We tried building our own pipeline (like most teams do at first):

  • PyPDF2 → text only, no layout, no tables
  • pdfplumber / PyMuPDF → good text + basic tables, falls apart on rotated text, merged cells, charts
  • Custom layout detection + Tesseract OCR → slow, hallucinated table boundaries, terrible on non-Latin fonts sometimes present in Indian filings

We even chunked PDFs → sent pages to Gemini 1.5 Flash with vision → asked it to output markdown. Results:

  • Token burn was insane (hundreds of thousands per large report)
  • Latency 120–240 seconds per file
  • Inconsistent table structure
  • Cost still higher than pure Google Vision in many cases

The Hero: IBM Docling (Open Source & Shockingly Good)

Enter **Docling** released by IBM Research.

Key things that made us stop and pay attention:

  • Native advanced PDF layout understanding
  • Excellent TableFormer for table structure recognition (FAST / ACCURATE modes)
  • Built-in image classification (charts, logos, signatures, formulas, code blocks…)
  • Zero cloud dependency → runs on your machines (CPU ok, GPU much faster)
  • Exports clean Markdown, JSON, or Docling document tree
  • Integrates beautifully with RAG / GenAI pipelines
  • Permissive open-source license

Most importantly: no per-page API charges. Ever.

Our Production Architecture Docling + Lightweight Gemini Fallback

We didn’t go 100% Docling-only. Why?

Docling is excellent at layout, tables, reading order but very complex embedded charts / graphs (especially ones with tiny labels or 3D effects) still benefit from a small vision model pass.

So we built this hybrid flow:

  1. Docling parses the full PDF → gives structured document (headings, paragraphs, tables, pictures…)
  2. For every detected PictureItem → check classification
  3. Skip obvious junk: logo, signature, QR, stamp, icon…
  4. For promising images (charts, graphs, data visuals) → resize → send to Gemini → get markdown description/table
  5. Replace the image reference with the generated markdown text
  6. Delete images → export final clean Markdown

→ Result: structured text almost everywhere, very few hallucinations, cost of Gemini used only on ~5–12% of pages (mostly investor presentation charts)

Here is the complete code :)

import io
from PIL import Image
import time
import logging
import google.generativeai as genai
from docling.document_converter import DocumentConverter, PdfFormatOption
from docling.datamodel.pipeline_options import PdfPipelineOptions, TableFormerMode
from docling.datamodel.base_models import InputFormat, DocItemLabel
from docling.datamodel.accelerator_options import AcceleratorDevice, AcceleratorOptions
from docling_core.types.doc import ImageRefMode, PictureItem
from docling.datamodel.base_models import DocumentStream
from django.conf import settings
from functools import lru_cache

apilogs = logging.getLogger("apilog")

@lru_cache(maxsize=1)
def get_docling_converter():
    IMAGE_RESOLUTION_SCALE = 1.0
    accelerator_options = AcceleratorOptions(device=AcceleratorDevice.AUTO)

    pipeline_options = PdfPipelineOptions()
    pipeline_options.accelerator_options = accelerator_options
    pipeline_options.generate_page_images = True
    pipeline_options.images_scale = IMAGE_RESOLUTION_SCALE
    pipeline_options.do_picture_classification = True
    pipeline_options.do_code_enrichment = False
    pipeline_options.do_formula_enrichment = False
    pipeline_options.do_ocr = False  # Docling's layout models usually sufficient
    pipeline_options.table_structure_options.mode = TableFormerMode.FAST

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

class DocumentParser:
    def __init__(self, api_key=None, ai_model=None, ai_provider="gemini", **kwargs):
        self.api_key = api_key
        self.ai_model = ai_model
        self.ai_provider = ai_provider
        self.kwargs = kwargs
        self.MAX_PDF_PAGES = 100
        self.reject_list = ["icon", "logo", "signature", "stamp", "qr_code", "bar_code",
                            "screenshot", "chemistry_molecular_structure", "chemistry_markush_structure", "other"]

        self.ai_usage_dict = {"prompt_token_count": 0, "candidates_token_count": 0, "total_token_count": 0}
        self.gemini_model = None
        self.initialize_parsing_models()

    def gemini(self):
        system_instruction = self.kwargs.get("system_instruction") or """
1. Convert the image to structured Markdown.
2. Extract only data — no observations, analysis, or insights.
3. Focus on charts, graphs, and data elements only.
4. Use Markdown tables and headings where appropriate.
5. Output must be in strict Markdown format only.
"""
        if not self.api_key:
            self.api_key = settings.GEMINI_API_KEY
        if not self.ai_model:
            self.ai_model = settings.GCP_MODEL

        genai.configure(api_key=self.api_key)
        self.gemini_model = genai.GenerativeModel(self.ai_model, system_instruction=system_instruction)

    def send_images_to_ai_model(self, processed_image):
        if not self.gemini_model:
            return ""

        generation_config = genai.GenerationConfig(temperature=0.1)

        result = self.gemini_model.generate_content(processed_image, generation_config=generation_config)

        self.ai_usage_dict["prompt_token_count"] += result.usage_metadata.prompt_token_count
        self.ai_usage_dict["candidates_token_count"] += result.usage_metadata.candidates_token_count
        self.ai_usage_dict["total_token_count"] += result.usage_metadata.total_token_count

        return result.text.strip()

    def initialize_parsing_models(self):
        self.converter = get_docling_converter()
        if self.ai_provider.lower() == "gemini":
            self.gemini()

    def pre_process_images(self, img, max_size=(768, 768), quality=50):
        if img.mode not in ("RGB", "L"):
            img = img.convert("RGB")
        img.thumbnail(max_size, Image.Resampling.LANCZOS)

        buffer = io.BytesIO()
        img.save(buffer, format="JPEG", quality=quality)
        buffer.seek(0)
        return Image.open(buffer)

    def run(self, pdf_file):
        self.pdf_file = pdf_file
        markdown = self.parse_document()
        return {"markdown": markdown, "ai_usage_data": self.ai_usage_dict}

    def parse_document(self):
        exception_list = []
        pdf_bytes = io.BytesIO(self.pdf_file.read())
        stream = DocumentStream(stream=pdf_bytes, name=self.pdf_file.name)

        result = self.converter.convert(stream, max_num_pages=self.MAX_PDF_PAGES)
        doc = result.document

        image_element_list = []
        for element, _ in doc.iterate_items():
            if isinstance(element, PictureItem):
                image_element_list.append(element)
                if not element.annotations:
                    continue

                classification = element.annotations[0]
                best_class = max(classification.predicted_classes, key=lambda c: c.confidence)
                cl_name = best_class.class_name

                if cl_name not in self.reject_list:
                    img = element.get_image(doc)
                    processed_img = self.pre_process_images(img)
                    out = ""
                    retries = 0
                    max_retries = 3

                    while retries < max_retries:
                        try:
                            out = self.send_images_to_ai_model(processed_img)
                            break
                        except Exception as e:
                            retries += 1
                            exception_list.append(f"file {self.pdf_file.name}, attempt {retries}: {e}")
                            if retries == max_retries:
                                break
                            time.sleep(2)

                    if out:
                        doc.insert_text(sibling=element, label=DocItemLabel.PARAGRAPH, text=out)

        doc.delete_items(node_items=image_element_list)

        markdown_str = doc.export_to_markdown(
            page_break_placeholder="-------page break----------",
            image_mode=ImageRefMode.REFERENCED
        )

        if exception_list:
            apilogs.error(f"Image processing failures: {exception_list}")

        return markdown_str

Results That Made Us Smile

After 3 months in production:

  • Cost reduction: ~88–94% on parsing (no more per-page cloud billing)
  • Gemini usage: only 6–15% of pages hit the vision model → token spend dropped dramatically
  • Table fidelity: noticeably better than pure Google Vision on complex Indian annual reports
  • Speed: GPU machine → 2–5× faster end-to-end than cloud API round-trips
  • Self-hosted control: no vendor lock-in, data never leaves our infra unless we choose Gemini

Docling isn’t perfect (very old/handwritten docs can still need extra love), but for modern printed financial PDFs it’s become our default choice.

Have you tried Docling yet? What’s your biggest PDF parsing headache right now cost, tables, charts, or something else?

Drop a comment happy to share more config tweaks or benchmark numbers.

AIinFinance #Docling #OpenSource #CostOptimization #IBM

Resources


메타데이터
post_id
51c345cd7386
slug
pdf-parser-showdown-how-we-slashed-document-processing-costs-by-90-switching-from-google-vision-51c345cd7386
url
https://medium.com/@priyanshuparashar/pdf-parser-showdown-how-we-slashed-document-processing-costs-by-90-switching-from-google-vision-51c345cd7386
canonical_url
https://medium.com/@priyanshuparashar/pdf-parser-showdown-how-we-slashed-document-processing-costs-by-90-switching-from-google-vision-51c345cd7386
author_url
https://medium.com/@priyanshuparashar
status
ok
fetched_at
2026-06-28 14:26:31