Tools,

Florence-2 OCR, tested on a real scanned PDF

1,887

of 1,955 scan words, read in order

Get the setup

Ten scanned pages in 158 seconds

158 s for 10 pages, Mac GPU

1.6 GB model download

  1. Run pip install torch transformers and brew install poppler (for pdftoppm).
  2. Save florence_ocr.py below next to book.pdf.
  3. Run python3 florence_ocr.py. It writes book.txt. The first run downloads Florence-2-large.
florence_ocr.py
"""Florence-2 OCR on a PDF: book.txt plus page-NN.txt (brasspage.com/florence-2-ocr/, Nov 12, 2026)."""

import argparse
import glob
import subprocess

import torch
from PIL import Image
from transformers import AutoProcessor, Florence2ForConditionalGeneration

ap = argparse.ArgumentParser()
ap.add_argument("--model", default="florence-community/Florence-2-large")
ap.add_argument("--task", default="<OCR>")  # or <OCR_WITH_REGION>
ap.add_argument("--strips", type=int, default=1)  # cut each page into N bands, top to bottom
a = ap.parse_args()

dev = "mps" if torch.backends.mps.is_available() else "cpu"
model = Florence2ForConditionalGeneration.from_pretrained(a.model, dtype=torch.bfloat16).to(dev)
processor = AutoProcessor.from_pretrained(a.model)
subprocess.run(["pdftoppm", "-r", "200", "-png", "book.pdf", "page"], check=True)


def read(image):
    inputs = processor(text=a.task, images=image, return_tensors="pt").to(dev, torch.bfloat16)
    ids = model.generate(**inputs, max_new_tokens=1024, num_beams=3)
    raw = processor.batch_decode(ids, skip_special_tokens=False)[0]
    out = processor.post_process_generation(raw, task=a.task, image_size=image.size)[a.task]
    return "\n".join(out["labels"]) if isinstance(out, dict) else out


pages = []
for png in sorted(glob.glob("page-*.png")):
    page = Image.open(png).convert("RGB")
    w, h = page.size
    cuts = [round(h * i / a.strips) for i in range(a.strips + 1)]
    text = "\n".join(read(page.crop((0, top, w, bottom))) for top, bottom in zip(cuts, cuts[1:]))
    with open(png.replace(".png", ".txt"), "w") as f:
        f.write(text + "\n")
    pages.append(text)
with open("book.txt", "w") as f:
    f.write("\n\n".join(pages) + "\n")

It is the model card's code, with the <OCR> task, one page at a time.

The base model glues lines together

Scan words in order, of 1,955
Setup Words Time
Large, default 1,887 158 s
--strips 2 1,901 160 s
OCR with region 1,893 195 s
Base model 1,410 123 s

Florence-2-base wrote no line breaks at all. The last word of each line ran into the next: "thename", "oldfellow".

Its model card says the OCR task writes line breaks. Only the large model did.

It rewrites dialect words

The book has dialect. Florence-2 wrote the plain word instead:

On the page Florence-2 wrote
whar what
nary navy
wuth with
anythink anything

19 of the 62 missed places have a double quote mark. A few came out as « or ».

German: words that are not there

On a German two-column page, the true text says:

im Kriegsministerium an den neuen

Florence-2 wrote:

im Kriegsstortrum im Neuen neuen

Every image is resized to 768 pixels square. --strips 3 cuts each page into three bands, so the letters get bigger.

3 German pages: 206 umlauts, 1,887 words
Bands Umlauts Words
1, default 33 341
2 59 807
3 80 989

Other vision models read more, faster

The same 10 scanned pages, scorer and Mac GPU:

Scan words in order, of 1,955
Model Words Time
glm-ocr, Ollama 1,953 104 s
GOT-OCR 2.0 1,936 134 s
Florence-2-large 1,887 158 s
docTR 1,869 13 s

In an 11-page file with a typed cover page, it read all 13 cover words. The scan pages came out byte for byte the same.

Florence-2 and our Actor: 1 win, 4 losses

Our Actor, from a file URL, with no install:

curl -s -X POST "https://api.apify.com/v2/acts/brasspage~pdf-ocr-to-markdown/run-sync-get-dataset-items" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"documentUrls": ["https://api.apify.com/v2/key-value-stores/sTnmNrGoSsi7tLQvs/records/anythingllm-mixed-11-pages.pdf"]}' \
  | jq -r '.[].markdown' > mixed.md
Same files, Florence-2 on the Mac GPU
Test Florence-2 Actor
Scan words in order 1,887 1,874
Typed cover words 13 13
Umlauts 80 192
Two columns, in order 279 954
Time, 11 pages 169 s 133 s
Time, 3 German pages 165 s 59 s

Florence-2 used --strips 3 for German.

The Actor needs no GPU and no model download. It costs $0.006 a page.

MIT license, from Microsoft

The Florence-2 license file is MIT, from Microsoft. I read it and the model cards in full before the download.

I used the transformers port in florence-community. Its cards say mit and link to the same Microsoft license file.

How I tested it
  • Scan: the 10 pages of OCR in n8n, proofread Wikisource book scans, 1,955 true words. The 11-page file adds the 13-word typed cover page of AnythingLLM OCR.
  • German: the 3 pages of Paperless-ngx OCR, 1,887 words, 206 umlauts. One page has two columns, 972 words.
  • Score: true words found in order in book.txt, with the scorer of the tessdata test. German: the scorer of Tesseract German.
  • transformers 5.22.2 and torch 2.15.0, Python 3.12, on an Apple Silicon Mac GPU, at the lowest priority. Model revisions 4271c66 (large) and 00921df (base).
  • Times count the whole script, with the model load. The models were already downloaded. One run of each setup.
  • The glm-ocr, GOT and docTR rows: the same files and scorer, on the same Mac, on November 12. They have their own pages.
  • The Actor: two real runs earlier the same day, build 0.1.65, on the same files.
What I did not test
  • CPU only. The script falls back to it without a Mac GPU.
  • The fine-tuned models (-ft) and the original Microsoft code.
  • Bands that overlap, and more than 3 bands.
  • Windows, Linux and NVIDIA GPUs.
  • Handwriting, receipts, tables and photos of signs.
Sources

Two ways to skip the work

Run it yourself. Our Apify Actor turns scanned PDFs into Markdown, tables included.

Try the Actor

Or send us the scan. Tack, our AI, does the job for $29, up to 50 pages. First page free.

Get one page free

Questions people ask

Is Florence-2 good for OCR?

On clean English book pages, yes: the large model read 1,887 of 1,955 words in order. On German pages, it wrote words that are not on the page.

Can Florence-2 read German?

Not well. By default it wrote 33 of 206 umlauts, and words that are not on the page. Page bands help, but not enough.

Is Florence-2 free for business use?

Yes. The license is MIT, from Microsoft, and the transformers port uses the same license.