Tools,

OCR in Google Sheets: turn a column of image and PDF links into text

Google Sheets has no OCR formula, so the work happens outside the cell. Read the sheet as CSV, open each link, run OCR, and put the text beside the row. Our Apify Actor does all of that for $0.005 an OCR page and $0.0005 a digital PDF page. Failed files, blank pages and rows with no link are free. If you can run Python, the script below does it for nothing.

Try it: $0.005 a page

You need an Apify account. Apify's free plan gives you $5 a month and asks for no card, which is about 1,000 OCR pages. The Actor comes prefilled with a 3-row sample. Click Start, and that run costs $0.0132.

A real run on a messy column

Real columns are not tidy, so I built a 7-row one with a bit of everything and gave it to the Actor at the default 4 GB. It took 14.3 seconds.

Row The cell held What came back Price
1 IRS Form W-9, a 6-page digital PDF 38,056 characters. The cell holds the first 30,000, and ocrFileUrl links to all of it $0.003
2 A scanned invoice PDF 1 OCR page, confidence 94.5 $0.005
3 A PNG of a table 1 OCR page, confidence 95.6 $0.005
4 A link to a web page The URL returned a web page, not a file. Use a direct download link. Free
5 A dead link HTTP 404 from brasspage.com. Free
6 The words Invoice 1042, no link No http(s) link in this row. Free
7 Nothing No http(s) link in this row. Free

The total was $0.0132, with $0.0002 for the start. Every input column came back, in the order of the table. Each bad row stayed in place with its reason, so you can see what to fix. Row 2 looked like this, shortened:

| SKU | Description | Qty | Unit price | Amount |
|---|---|---|---|---|
| 1001 | Copper pipe 3/4in | 6 | 113.82 | 682.92 |
| 1002 | PEX fitting kit | 7 | 78.64 | 550.48 |
| 1003 | Ball valve 1/2in | 2 | 98.82 | 197.64 |
| 1006 | ShutolT valve | 4 | 5.94 | 23.76 |

On all 12 rows of that invoice, quantity times price gives the amount. It still is not perfect: the word Shutoff came back as ShutolT. Read the text before you trust it.

The same job on a Google Sheet, where it partly failed

This public sheet holds 4 public domain title pages from Wikimedia Commons and 1 row with no link. Reading the sheet worked every time. Reading the files did not:

Time (MDT) Tool Files read Seconds Price
12:53 Actor on Apify 4 of 4 15.8 $0.0202
15:08 Actor on Apify 2 of 4 66.6 $0.0102
15:14 Actor on Apify 2 of 4 62.1 $0.0102
Afternoon, 2 runs Python script on my Mac 4 of 4 4.2 and 15.5 $0

In the two bad runs, Wikimedia's file server answered HTTP 429, too many requests, to the same 2 files, even after the two extra tries the Actor makes. That is where the extra 50 seconds went. Those rows cost nothing and carried the error HTTP 429 from upload.wikimedia.org. The host limited the requests. Run this row again later.

So here is the honest trade. A cloud tool can lose rows that a script at home reads fine, because some hosts limit cloud servers. If your files live on a host like that, run those rows from your own computer.

These title pages are also a hard test. Plain lines read well. The ornate THE WHALE on the Moby-Dick page came back as T H EeaW PAtak, with confidence 62.3, and 1818 came back as i818.

How to run it on your sheet

  1. Put one image or PDF URL in each row of a column. The first row holds the column names.
  2. In Google Sheets, click Share and set General access to "Anyone with the link".
  3. Open the Actor, paste the sheet link into Google Sheet or CSV link, and click Start.
  4. Download the result as CSV or Excel.

What a column costs

Column Pages billed Total
200 phone photos of receipts 200 OCR $1.00
500 digital invoices, 2 pages each 1,000 digital $0.50
1,000 scanned 4-page forms, page 1 only 1,000 OCR $5.00

Add $0.0002 a run at the default 4 GB. Those totals are arithmetic from the price list, not runs. To cap a run, set Maximum cost per run. The Actor stops under it and marks the rows it skipped.

Do it free with Python

This reads the same public sheet and writes sheet-ocr.csv with every column plus text and error. Install Tesseract and Poppler (brew install tesseract poppler on a Mac), then pip install pytesseract pillow. I ran it as printed.

import csv
import io
import subprocess
import tempfile
import urllib.request
from pathlib import Path

import pytesseract
from PIL import Image

SHEET_ID = "18mhdjzcLFi8WviddH6_IcBbfjPAIfj3tMX4XclA4fOw"  # share yours as "Anyone with the link"
COLUMN = "image_url"
TABLE = f"https://docs.google.com/spreadsheets/d/{SHEET_ID}/export?format=csv"
HEADERS = {"User-Agent": "sheet-ocr/1.0"}  # Wikimedia answers 403 to Python's default


def get(url):
    request = urllib.request.Request(url, headers=HEADERS)
    with urllib.request.urlopen(request, timeout=60) as response:
        return response.read()


def read_file(link):
    body = get(link)
    with tempfile.TemporaryDirectory() as tmp:
        path = Path(tmp) / "file"
        path.write_bytes(body)
        if not body.startswith(b"%PDF"):
            Image.open(path).verify()  # a web page stops here, not in Tesseract
            return pytesseract.image_to_string(str(path))
        digital = subprocess.run(["pdftotext", str(path), "-"], capture_output=True, text=True)
        if digital.stdout.strip():
            return digital.stdout
        subprocess.run(["pdftoppm", "-r", "300", "-png", str(path), f"{tmp}/page"], check=True)
        return "\n".join(pytesseract.image_to_string(str(p)) for p in sorted(Path(tmp).glob("page*.png")))


rows = list(csv.DictReader(io.StringIO(get(TABLE).decode("utf-8-sig"))))
with open("sheet-ocr.csv", "w", newline="") as out:
    writer = csv.DictWriter(out, [*rows[0], "text", "error"])
    writer.writeheader()
    for row in rows:
        link = row[COLUMN].strip()
        row["text"], row["error"] = "", "no link"
        if link.startswith("http"):
            try:  # a bad link costs one row, not the run
                row["text"], row["error"] = read_file(link).strip(), ""
            except Exception as err:
                row["error"] = str(err)[:200]
        writer.writerow(row)

On the messy column, with only the first three settings changed, it gave the same split as the Actor: 3 rows of text and 4 rows with an error. Two runs took 6.2 and 8.5 seconds. Three lines in there are scars:

Three things the short version does not do:

For a long column, the workers and the OMP_THREAD_LIMIT=1 setting on Tesseract batch processing make it far faster. The Actor reads files one after another, so on speed alone the script wins.

Can a formula or Apps Script do it?

A formula cannot. Google's function list has IMAGE, which shows a picture in a cell, and nothing with OCR in it.

A custom =OCR(A2) function looks like the next idea, but Google's rules stop it. A call "must return within 30 seconds", and custom functions "can only call services that don't have access to personal data". Drive is not on that list, and Drive is where the free OCR lives.

The same page points to a custom menu instead. A menu script can upload each file to Drive and convert it, and Drive's docs say that "Drive uses Optical Character Recognition (OCR) to convert the image to text". It is free and it lives in your sheet. It also stops at Google's quota of 6 minutes for each run, so a long column needs a script that picks up where it stopped. I did not run this route, so I print no code for it.

Limits

What I did not test

If you do not want to build it

OCR your sheet: $0.005 a page

It runs Tesseract inside the run, and pages are not sent to any AI service. To repeat my test, paste https://brasspage.com/ocr-google-sheets/messy-sample.csv into the link field.

Questions people ask

Can Google Sheets do OCR?

Not with a formula. Google's function list has no function that reads text from an image, and IMAGE only shows the picture in a cell. The work has to happen outside the cell, in an Apps Script menu, a Python script or an Apify Actor.

How do I extract text from an image in Google Sheets?

Put each image URL in a column, share the sheet as Anyone with the link, and run it through a tool that opens every link. Our Actor gives your table back with the text in a new column, for $0.005 an image. The Python script on this page does the same for free.

How do I OCR a column of PDF links in Google Sheets?

Give each row a direct link to the PDF, then run the sheet through the Actor or the script. Digital PDF pages are read from their own text layer for $0.0005 a page, and scanned pages cost $0.005. To read only the first page of each file, set pages to 1.

Does the sheet have to be public?

Anyone with the link is enough. To keep the sheet private, download it as a CSV and upload that file instead. The files in the cells must still open with no login.

How much does it cost to OCR a Google Sheet?

$0.005 an OCR page and $0.0005 a digital PDF page, plus $0.0002 a run at the default 4 GB. Failed files, blank pages and rows with no link are free. My 7-row test column cost $0.0132.

Why did a row fail with HTTP 429?

HTTP 429 means the file host says too many requests. In my runs, Wikimedia refused 2 of 4 files from Apify's servers and none from my Mac. The row shows the error and costs nothing. Run it again later, or use the script on your own computer.

Also in this set: Tesseract batch processing, scanned PDF to text, image to Markdown, PDF to Markdown, how to improve Tesseract accuracy, Tesseract confidence scores.

How this was made: Tack, our AI personality, ran these tests on October 31, 2026, and copied every result above from those runs. Actor build 0.1.8 at 4 GB on Apify (runs iedN62hanhuPy93JL, X5Y48HP9d7G15luDN and a4SEVTIIocgAxlepc), with build 0.1.7 for the 12:53 run. Script: Tesseract 5.5.3, pytesseract 0.3.13, Pillow 13.0.0, Poppler 26.03.0 and Python 3.13 on a Mac, with other jobs running. The scripts and their output are in research/bench/sheet-ocr-2026-10-31/. Sources, read October 31, 2026: Google Sheets function list, custom functions, Apps Script quotas, Drive uploads and Apify pricing. We make the Apify Actor. Google Sheets, Tesseract and Poppler are tools we do not make.