Golang OCR with gosseract, tested on 55 scans
50.8%
of two-column text in order, after SetPageSegMode
The code, run on 55 scans
Install Tesseract first: brew install tesseract on a Mac,
the apt packages libtesseract-dev and
tesseract-ocr-eng on Debian. Then
go get github.com/otiai10/gosseract/v2.
package main
import (
"fmt"
"log"
"os"
"github.com/otiai10/gosseract/v2"
)
func main() {
client := gosseract.NewClient()
defer client.Close()
client.SetVariable("tessedit_pageseg_mode", "3")
client.SetVariable("user_defined_dpi", "300")
for _, path := range os.Args[1:] {
if err := client.SetImage(path); err != nil {
log.Fatal(err)
}
text, err := client.Text()
if err != nil {
log.Fatal(err)
}
fmt.Println(text)
}
}
CGO_CPPFLAGS="-I$(brew --prefix)/include" CGO_LDFLAGS="-L$(brew --prefix)/lib" go build -o ocr .
55 of 55 scans, the command's text
13,858 of 14,693 words found, both
SetPageSegMode did nothing
gosseract runs Tesseract's Init inside the first
Text(). A page mode set before that was lost on every
scan. The API then reads in mode 6, one block, and goes straight
across two columns.
| Set before Text() | Same text |
|---|---|
| Nothing, as the README | 9 of 55 |
SetPageSegMode(PSM_AUTO) |
9 of 55 |
| The variable, page mode 3 | 40 of 55 |
| Mode 3 and dpi 300, as above | 55 of 55 |
30 of 30 two-column scans, same bytes as no call
99.2% of letters in order with the variable
The Apple Silicon build error
A plain go build on an M-series Mac:
tessbridge.cpp:5:10: fatal error: 'leptonica/allheaders.h' file not found
gosseract's cgo lines look in /usr/local. Homebrew on
Apple Silicon lives in /opt/homebrew. The two variables
in the build line fix it. On Debian, no flags were needed.
gosseract is cgo only. With CGO_ENABLED=0:
page/main.go:12:22: undefined: gosseract.NewClient
One client, not one per file
| Way | CPU |
|---|---|
exec.Command per line |
58.0 s |
NewClient per line |
46.1 s |
| One client, as above | 19.2 s |
One client gave the command's text on 259 of 259 lines. Each
NewClient pays for Init again on its first
Text().
PDFs: no error, no text
SetImage on a PDF returned no error. Leptonica printed
this on stderr:
Error in pixReadStream: Pdf reading is not supported
On a new client, Text() then failed with "PixImage is not
set". On a client that read a scan before, it returned empty text and
no error. Render the pages first:
pdftoppm -r 300 -gray scan.pdf page
./ocr page-*.pgm
1,882 of 1,955 words on 10 pages
0 words and no error, on a used client
Debian in Docker: 188 MB
FROM golang:1.25-trixie AS build
RUN apt-get update && apt-get install -y --no-install-recommends libtesseract-dev \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY main.go .
RUN go build -o /ocr .
FROM debian:trixie-slim
RUN apt-get update && apt-get install -y --no-install-recommends libtesseract5 tesseract-ocr-eng \
&& rm -rf /var/lib/apt/lists/*
COPY --from=build /ocr /usr/local/bin/ocr
ENTRYPOINT ["ocr"]
With the code above as main.go, it matched apt's
tesseract (5.5.0) on 5 of 5 scans. The final image is 188
MB, for arm64.
No cgo: call our Actor
For PDFs at a URL: our pdf-ocr Actor on Apify, from plain
net/http. The client below needs no cgo and no Tesseract.
353 of 381 words on 2 pages, in 8.0 s
354 with the gosseract code, same pages
It costs $0.006 a page, and you need an Apify token. For a few scans on your own machine, the gosseract code is free.
The Actor client in Go
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
)
const run = "https://api.apify.com/v2/acts/brasspage~pdf-ocr-to-markdown/run-sync-get-dataset-items"
func main() {
body, _ := json.Marshal(map[string]any{"documentUrls": []string{os.Args[1]}})
req, _ := http.NewRequest("POST", run, bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("APIFY_TOKEN"))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
log.Fatal(resp.Status)
}
var items []struct {
Markdown string `json:"markdown"`
}
if err := json.NewDecoder(resp.Body).Decode(&items); err != nil {
log.Fatal(err)
}
for _, it := range items {
fmt.Println(it.Markdown)
}
}
Build it with CGO_ENABLED=0 go build. Run it with
APIFY_TOKEN set and the PDF's URL as the argument.
How I tested it
- Pages: 55 proofread Wikisource scans from 11 printed books, color JPEG, 1,920 pixels wide, no dpi in the file. The same set as the Tesseract C++ API test.
- Two columns: the 30 encyclopedia scans of the two-column test, letters found in order.
- Score: true words found in order, with the scorer of the tessdata test.
- Apple Silicon Mac, Go 1.25.4, gosseract v2.4.1, Homebrew Tesseract 5.5.4, Leptonica 1.87.0, Poppler 26.03.0, tessdata_fast English.
-
"The command" is
tesseract FILE stdout --dpi 300 --psm 3. CPU time is the whole process, on the Mac's efficiency cores. - PDF: the first 10 scans in one PDF, JPEG bytes kept. Actor: one real run on a 2-page PDF of the first 2 scans.
What I did not test
- Windows, Intel Macs, x86-64 Linux, Alpine.
- Goroutines, hOCR, bounding boxes, and SetImageFromBytes.
- Other Tesseract versions, tessdata_best, and photos.
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
Why does SetPageSegMode do nothing in gosseract?
gosseract runs Init inside the first Text call, and a page mode set before that was lost on 55 of 55 scans. Set it with SetVariable("tessedit_pageseg_mode", "3") instead.
Why does go build say leptonica/allheaders.h not found?
gosseract's cgo flags look in /usr/local, and Homebrew on Apple Silicon lives in /opt/homebrew. Build with CGO_CPPFLAGS="-I$(brew --prefix)/include" and CGO_LDFLAGS="-L$(brew --prefix)/lib".
Can gosseract read a PDF?
No. Leptonica reads images only, and SetImage returned no error on a PDF. Render the pages first, for example with pdftoppm -r 300 -gray, then pass each image.