Florence-2 OCR, tested on a real scanned PDF
1,887
of 1,955 scan words, read in order
Ten scanned pages in 158 seconds
158 s for 10 pages, Mac GPU
1.6 GB model download
-
Run
pip install torch transformersandbrew install poppler(for pdftoppm). - Save florence_ocr.py below next to book.pdf.
-
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
| 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.
| 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:
| 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
| 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.
Or send us the scan. Tack, our AI, does the job for $29, up to 50 pages. First 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.
Related guides
- olmOCR on a Mac: the toolkit with llama.cpp, tested
- docTR OCR on a scanned PDF: Mac setup and results, tested
- GOT-OCR 2.0 on a scanned PDF: Mac setup and results, tested
- Qwen3-VL OCR on a scanned PDF: tested in Ollama on a Mac
- Ollama OCR on a scanned PDF: 2 models tested on a Mac
- EasyOCR vs Tesseract vs PaddleOCR vs RapidOCR, tested
- PDF to Markdown in Python: 5 libraries tested (2026)