chromeocr

package module
v0.0.0-...-5a90d89 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 26, 2026 License: MIT Imports: 29 Imported by: 0

README

chromeocr

A high-performance Go library and CLI for running Google Chrome Screen AI OCR on-device. This package extracts text from images and PDFs with high precision, generates searchable PDFs (with transparent text layers), and provides built-in cleaning pipelines tailored for legal and general documents.


🌟 Key Features

  • 3 Output Formats Simultaneously: Generates Layered PDF (.pdf), Parsed Text (.txt), and Page-by-Page JSON Array (.json) in a single call.
  • Indonesian Legal Document Cleaning: Fixes merged legal terms (DAYAAIRDAYA AIR), normalizes legal preamble triggers (Menimbang, Mengingat, PASAL), and strips diagonal & JDIH portal watermarks.
  • Image & PDF Support: Processes PDF files, image files (.png, .jpg), or in-memory []image.Image slices (e.g. rendered via go-fitz).
  • Silent Native Logs: Suppresses raw C++ glog/TensorFlow Lite log noise at the OS level while offering execution progress metrics (s, ms, ns).
  • Zero Environment Variables: Configured via Go Functional Options (WithModelDir, WithProgress, WithLightMode, etc.).

📥 Installation

go get github.com/fuadarradhi/chromeocr

System Requirement: Requires poppler-utils (pdftoppm) installed on your system to process PDF files.


🚀 Quick Start & Usage

1. Command Line Interface (CLI)

Run directly from your terminal to process PDF files:

# Process PDF and generate 3 output files (_layered.pdf, .txt, .json)
go run ./cmd/chromeocr --input document.pdf

# Specify explicit output paths
go run ./cmd/chromeocr --input document.pdf --output output.pdf --txt-output output.txt --json-output output.json

2. Usage as a Go Library
A. Processing PDFs (PDF → Searchable PDF + Clean TXT + Page JSON)
package main

import (
	"fmt"
	"log"

	"github.com/fuadarradhi/chromeocr"
)

func main() {
	// Initialize Engine
	engine, err := chromeocr.New()
	if err != nil {
		log.Fatal(err)
	}
	defer engine.Close()

	// Process PDF
	result, err := engine.ProcessPDF("document.pdf", "document_layered.pdf")
	if err != nil {
		log.Fatal(err)
	}

	// Access parsed text and export outputs
	fmt.Println(result.FullText)
	_ = result.SaveOutputs("document.txt", "document.json")
}
B. OCR for a Single Image File (.png / .jpg)
engine, err := chromeocr.New()
if err != nil {
    log.Fatal(err)
}
defer engine.Close()

page, err := engine.OCRFile("page.png")
if err != nil {
    log.Fatal(err)
}

fmt.Printf("Image Dimensions: %dx%d px\n", page.Width, page.Height)
fmt.Println(page.Text())
C. In-Memory Image Loop (Single Engine Session e.g. with go-fitz)

Use this method when PDF pages are pre-rendered in memory for maximum performance:

engine, err := chromeocr.New()
if err != nil {
    log.Fatal(err)
}
defer engine.Close() // Single initialization for the entire loop

// inMemoryImages is a slice of image.Image
result, err := engine.ProcessImages(inMemoryImages)
if err != nil {
    log.Fatal(err)
}

_ = result.SaveOutputs("output.txt", "output.json")
D. Dynamic Options & Execution Progress Metrics
engine, err := chromeocr.New(
    chromeocr.WithLightMode(false), // High-accuracy model
    chromeocr.WithProgress(func(p chromeocr.PageProgress) {
        fmt.Printf("Page %d/%d completed in %v\n", p.PageNum, p.TotalPages, p.Duration)
    }),
)

📁 Examples

Check the example/ directory for runnable implementations:


📄 License

MIT License

Documentation

Overview

Package chromeocr calls Google Chrome's on-device "Screen AI" OCR component directly — the same local ML model Chrome uses to make scanned PDFs and images searchable — without needing a running browser.

Index

Constants

This section is empty.

Variables

View Source
var LegalKeywords = []string{
	"MENIMBANG", "MENGINGAT", "MEMPERHATIKAN", "MEMUTUSKAN", "MENETAPKAN",
	"KESATU", "KEDUA", "KETIGA", "KEEMPAT", "KELIMA", "KEENAM", "KETUJUH", "KEDELAPAN", "KESEMBILAN", "KESEPULUH",
	"PASAL", "BAB", "LAMPIRAN", "SALINAN", "KEPADA", "UNTUK", "TENTANG", "NOMOR",
	"UNDANG-UNDANG", "PERATURAN", "KEPUTUSAN", "INSTRUKSI", "QANUN", "EDARAN", "SURAT",
	"PRESIDEN", "PEMERINTAH", "MENTERI", "GUBERNUR", "BUPATI", "WALIKOTA", "WALIKOTA",
	"PROPINSI", "PROVINSI", "KABUPATEN", "KOTA", "DAERAH", "GAMPONG", "DESA",
}

LegalKeywords contains standard Indonesian legal document structural markers (National to Regency/City levels).

View Source
var WatermarkKeywords = []string{
	"DRAFT", "WATERMARK", "COPY", "CONFIDENTIAL", "RAHASIA", "SAMPLE", "CONTOH", "ARSIP",
}

WatermarkKeywords contains common watermark terms found in legal & official documents.

Functions

func AreOnSameLine

func AreOnSameLine(boxA, boxB BoundingBox) bool

AreOnSameLine checks if two bounding boxes are on the same printed line height (vertical overlap >= 30%).

func CleanLeadingNoisePunct

func CleanLeadingNoisePunct(s string) string

CleanLeadingNoisePunct removes leading dots/commas before capital letters (e.g. ".Penetapan" -> "Penetapan").

func CorrectLegalTriggers

func CorrectLegalTriggers(s string) string

func CorrectWithIndonesianDict

func CorrectWithIndonesianDict(s string) string

CorrectWithIndonesianDict scores OCR candidate words against the Indonesian dictionary and safely corrects OCR digit/symbol corruptions (e.g. "Pasa1" -> "Pasal", "Nom0r" -> "Nomor"). Pure alphabetic words (like proper names "Safrizal", "Madjid", "Lhoksukon") are 100% PRESERVED and NEVER modified.

func DownloadComponent

func DownloadComponent(targetDir string) (string, error)

DownloadComponent attempts to download the screen_ai component directly from Google's update servers, without needing Chrome installed or running. It returns the directory the component was extracted into (pass this to WithModelDir).

This may fail with a "noupdate" error — see the package-level comment in download.go for why, and what to do instead.

func FixMergedLegalWords

func FixMergedLegalWords(s string) string

FixMergedLegalWords dynamically splits merged OCR words (e.g. "SumberDaya" -> "Sumber Daya", "PeraturanGubernur" -> "Peraturan Gubernur", "PERATURANGUBERNUR" -> "PERATURAN GUBERNUR", "SUMBERDAYA" -> "SUMBER DAYA") without requiring any manual hardcoded maps.

func GetOriginalStderr

func GetOriginalStderr() io.Writer

GetOriginalStderr returns the saved terminal stderr writer.

func GetOriginalStdout

func GetOriginalStdout() io.Writer

GetOriginalStdout returns the saved terminal stdout writer for clean CLI progress logging.

func InjectTextLayer

func InjectTextLayer(inputPath, outputPath string, pages []overlayPage) error

InjectTextLayer reads inputPath, strips any pre-existing text layer, injects a new invisible OCR text layer for every page, and writes the result to outputPath using a PDF incremental update so the original xref is never touched.

func IsCanonicalLegalTrigger

func IsCanonicalLegalTrigger(w string) bool

IsCanonicalLegalTrigger checks if a word is part of the canonical Indonesian legal document structure (e.g. Menimbang, Mengingat, Memperhatikan, Memutuskan, Menetapkan, KESATU, KEDUA, KETIGA, Pasal, BAB, Lampiran, Nomor, Tentang).

func IsDiagonalWatermark

func IsDiagonalWatermark(ln Line) bool

IsDiagonalWatermark checks if a line represents a background watermark.

func IsKnownIndonesianWord

func IsKnownIndonesianWord(w string) bool

IsKnownIndonesianWord checks if a word exists in our Indonesian dictionary.

func IsLegalMarginLabel

func IsLegalMarginLabel(s string) bool

IsLegalMarginLabel checks if a text line represents a legal margin/header label.

func IsPageNumberOrHeaderFooter

func IsPageNumberOrHeaderFooter(ln Line, imgHeight int32) bool

IsPageNumberOrHeaderFooter checks if a line is a running header, footer, or page number marker.

func LoadCustomDictionaryFile

func LoadCustomDictionaryFile(filePath string) error

LoadCustomDictionaryFile loads a plain text dictionary file (.txt or .dic) containing custom Indonesian words (one word per line), dynamically expanding the Language Model Dictionary Scorer at runtime.

func NormalizeIndonesian

func NormalizeIndonesian(s string) string

func ReadPDFPageSizes

func ReadPDFPageSizes(pdfPath string) ([][2]float64, error)

ReadPDFPageSizes reads MediaBox dimensions for every page in the PDF.

func SilenceNativeOutputs

func SilenceNativeOutputs()

SilenceNativeOutputs redirects OS File Descriptor 1 (stdout) and 2 (stderr) to /dev/null so that native C++ glog / TensorFlow Lite logs are completely hidden, while preserving originalStdout for clean CLI progress logging.

func StripTextOperators

func StripTextOperators(streamBody []byte) []byte

StripTextOperators removes all "BT ... ET" text drawing blocks from a PDF content stream body, preserving all image/vector operators untouched.

Types

type Block

type Block struct {
	Lines []Line
}

Block groups lines that belong together spatially.

func (Block) Text

func (b Block) Text() string

Text joins every line in the block with a newline.

type BoundingBox

type BoundingBox struct {
	X, Y, Width, Height int32
	Angle               float32
}

BoundingBox is a pixel-space rectangle returned by Screen AI. X, Y are the top-left corner in image space (origin top-left, Y downward). Angle is in radians, representing the clockwise rotation of the text element relative to the horizontal — the same field Chrome reads in pdf_accessibility_tree.cc to apply skew corrections.

func DeskewBox

func DeskewBox(box BoundingBox, skew PageSkewInfo, imgW, imgH int) BoundingBox

type ContentType

type ContentType int32

ContentType classifies what kind of content a line represents.

const (
	ContentPrintedText        ContentType = 0
	ContentHandwrittenText    ContentType = 1
	ContentImage              ContentType = 2
	ContentLineDrawing        ContentType = 3
	ContentSeparator          ContentType = 4
	ContentUnreadableText     ContentType = 5
	ContentFormula            ContentType = 6
	ContentHandwrittenFormula ContentType = 7
	ContentSignature          ContentType = 8
)

type Direction

type Direction int32

Direction is the reading direction of a detected text line.

const (
	DirectionUnspecified Direction = 0
	DirectionLeftToRight Direction = 1
	DirectionRightToLeft Direction = 2
	DirectionTopToBottom Direction = 3
)

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

Engine is a loaded, initialized Screen AI OCR session.

func New

func New(opts ...Option) (*Engine, error)

func (*Engine) Close

func (e *Engine) Close() error

func (*Engine) MaxImageDimension

func (e *Engine) MaxImageDimension() int

func (*Engine) OCRFile

func (e *Engine) OCRFile(path string) (Page, error)

func (*Engine) OCRImage

func (e *Engine) OCRImage(img image.Image) (Page, error)

func (*Engine) OCRImageAutoOrient

func (e *Engine) OCRImageAutoOrient(img image.Image) (Page, error)

OCRImageAutoOrient replicates Chrome PDF Searchify behavior by testing page orientations (0°, 90° CW, 270° CW) and picking the rotation that maximizes recognized words for rotated/landscape pages.

func (*Engine) OCRImageTiled

func (e *Engine) OCRImageTiled(img image.Image) (Page, error)

OCRImageTiled performs Tiled Segmented OCR for large high-resolution images (e.g. 300 DPI A4 scans). When an image's height exceeds maxDim, it splits the image into vertical tiles (height <= maxDim) and processes each tile at 100% 1:1 native 300 DPI resolution with ZERO downscaling blur!

func (*Engine) OCRPDF

func (e *Engine) OCRPDF(pdfPath string, firstPage, lastPage int) ([]Page, error)

OCRPDF renders a PDF's pages to images and OCRs each one — the same two-step process Chrome itself performs internally (its built-in PDF renderer, pdfium, rasterizes a page; only then does that bitmap go to PerformOCR). chrome_screen_ai has no concept of "PDF" at all — see OCRImage's doc comment.

This shells out to `pdftoppm` (part of poppler-utils, NOT a Go module dependency — install it with e.g. `apt install poppler-utils` / `dnf install poppler-utils`) rather than pulling in a cgo PDF renderer, to keep this package's go.mod dependency-free. If your project already renders PDF pages another way (pdfium, MuPDF, ghostscript, ...), prefer calling OCRImage directly on those pages instead — that path has one less moving part.

firstPage and lastPage are 1-based and inclusive; pass 0, 0 to OCR every page. Pages are returned in order.

func (*Engine) OCRToSearchablePDF

func (e *Engine) OCRToSearchablePDF(inputPDF, outputPDF string) ([]Page, error)

OCRToSearchablePDF OCRs every page of inputPDF, injects an invisible text layer, and writes the layered PDF to outputPDF.

func (*Engine) ProcessImages

func (e *Engine) ProcessImages(images []image.Image) (*PDFResult, error)

ProcessImages processes a slice of in-memory image.Image (e.g. rendered via go-fitz, image/png, etc.) using the active Engine session without re-initializing or closing Screen AI.

func (*Engine) ProcessPDF

func (e *Engine) ProcessPDF(inputPDF, outputPDF string) (*PDFResult, error)

ProcessPDF processes inputPDF, generates outputPDF (layered PDF text layer) if outputPDF is non-empty, and returns a PDFResult containing clean full text (.txt) and page-by-page JSON array (.json).

func (*Engine) ProcessPDFSmart

func (e *Engine) ProcessPDFSmart(inputPDF, outputPDF string) (*PDFResult, error)

ProcessPDFSmart automatically detects if inputPDF already has a valid text layer. If valid text exists (>= 15 words per page), it restructures the text layer in 2D reading order in milliseconds. If the PDF is a scanned image (0 text layer), it automatically falls back to Screen AI OCR.

func (*Engine) Version

func (e *Engine) Version() (major, minor uint32)

type LibraryNotFoundError

type LibraryNotFoundError struct {
	Searched []string
}

LibraryNotFoundError is returned when the screen_ai component could not be located automatically. It reports where chromeocr looked so the caller can decide whether to install Chrome, visit chrome://components to trigger a download, or pass an explicit path via WithModelDir / WithLibraryPath.

func (*LibraryNotFoundError) Error

func (e *LibraryNotFoundError) Error() string

type Line

type Line struct {
	Text        string
	Language    string
	BlockID     int32
	ParagraphID int32
	Confidence  float32
	Direction   Direction
	ContentType ContentType
	Box         BoundingBox
	Words       []Word
}

Line is a single recognized line of text.

func FilterLegalNoise

func FilterLegalNoise(lines []Line, imgHeight int32) []Line

FilterLegalNoise removes images, watermarks, page numbers, and running footers, and safely merges legal margin labels (Kepada, Untuk, KESATU, KEDUA, KETIGA, etc.) with colon bodies.

func OrganizeLegalLayoutLines

func OrganizeLegalLayoutLines(lines []Line) []Line

OrganizeLegalLayoutLines sorts lines on a page into strict Indonesian legal reading order.

func SafeMergeLegalLabelLines

func SafeMergeLegalLabelLines(lines []Line) []Line

SafeMergeLegalLabelLines merges a standalone legal label (Kepada, Untuk, KESATU, KEDUA, KETIGA, etc.) with its colon body text on the same line.

type LineDetail

type LineDetail struct {
	Text       string       `json:"text"`
	Confidence float32      `json:"confidence"`
	Box        BoundingBox  `json:"box"`
	Words      []WordDetail `json:"words,omitempty"`
}

LineDetail represents a single line's clean text, line confidence score, bounding box, and per-word details.

type LowConfWord

type LowConfWord struct {
	Page       int     `json:"page"`
	Line       string  `json:"line"`
	Word       string  `json:"word"`
	Confidence float32 `json:"confidence"`
}

LowConfWord represents a single word with confidence below threshold, for human review.

type Option

type Option func(*engineConfig)

Option configures New.

func WithAutoDownload

func WithAutoDownload(enabled bool) Option

func WithLibraryPath

func WithLibraryPath(path string) Option

func WithLightMode

func WithLightMode(enabled bool) Option

func WithModelDir

func WithModelDir(dir string) Option

func WithProgress

func WithProgress(fn func(PageProgress)) Option

func WithVerboseLogging

func WithVerboseLogging(enabled bool) Option

type PDFResult

type PDFResult struct {
	Pages        []Page        `json:"pages"`
	FullText     string        `json:"full_text"`
	JSONPages    []PageJSON    `json:"json_pages"`
	LowConfWords []LowConfWord `json:"-"`
}

PDFResult contains the full structured output from processing a PDF file.

func BuildPDFResult

func BuildPDFResult(pages []Page) *PDFResult

ProcessPDF processes inputPDF, generates outputPDF (layered PDF text layer) if outputPDF is non-empty, and returns a PDFResult containing clean full text (.txt) and page-by-page JSON array (.json). BuildPDFResult formats raw OCR pages into a clean PDFResult (.txt & .json page array).

func RestructurePDFTextLayer

func RestructurePDFTextLayer(inputPDF, outputPDF string) (*PDFResult, error)

RestructurePDFTextLayer extracts an existing PDF text layer, re-orders all text in 2D reading order, cleans Indonesian legal preambles/watermarks, and injects the restructured text layer into outputPDF. Takes milliseconds and incurs ZERO OCR errors.

func (*PDFResult) BuildConfidenceReport

func (r *PDFResult) BuildConfidenceReport() string

BuildConfidenceReport returns a human-readable plain-text report of all words with confidence score below 0.85, grouped by page, for manual review.

func (*PDFResult) SaveOutputs

func (r *PDFResult) SaveOutputs(txtPath, jsonPath, confidencePath string) error

SaveOutputs saves TXT, JSON, and optionally confidence report output files to disk.

type Page

type Page struct {
	Width, Height int
	Blocks        []Block
}

Page is the OCR result for a single image or PDF page. Width and Height are the pixel dimensions of the image fed to Screen AI (after any downscaling to respect MaxImageDimension). These dimensions are used by OCRToSearchablePDF to compute the exact scale factor: scaleX = page_width_pts / Width (Chrome's formula).

func ApplyDeskewToPage

func ApplyDeskewToPage(page Page, skew PageSkewInfo) Page

func ExtractPDFTextLayerBBox

func ExtractPDFTextLayerBBox(pdfPath string) ([]Page, error)

ExtractPDFTextLayerBBox extracts all words and 2D bounding boxes from an existing PDF text layer.

func (Page) Text

func (p Page) Text() string

Text joins every block's text in natural reading order with blank lines.

type PageJSON

type PageJSON struct {
	Page  int          `json:"page"`
	Text  string       `json:"text"`
	Lines []LineDetail `json:"lines,omitempty"`
}

PageJSON represents a single page's clean extracted text and line details.

type PageProgress

type PageProgress struct {
	PageNum      int
	TotalPages   int
	Width        int
	Height       int
	IsTiled      bool
	TileCount    int
	Mode         string
	TotalWords   int
	LowConfCount int
	Duration     time.Duration
}

PageProgress provides processing metrics for a single page.

type PageSkewInfo

type PageSkewInfo struct {
	AngleRad    float64
	Significant bool
}

func DetectPageSkew

func DetectPageSkew(lines []Line) PageSkewInfo

type SubImageable

type SubImageable interface {
	SubImage(r image.Rectangle) image.Image
}

type Word

type Word struct {
	Text       string
	Language   string
	Confidence float32
	Box        BoundingBox
}

Word is a single recognized word within a Line.

type WordDetail

type WordDetail struct {
	Text       string      `json:"text"`
	Confidence float32     `json:"confidence"`
	Box        BoundingBox `json:"box"`
}

WordDetail represents a single word's clean text, word-level confidence score, and bounding box.

Directories

Path Synopsis
cmd
chromeocr command
chromeocr-download command
Command chromeocr-download tries to fetch the screen_ai component directly from Google's update servers, without needing Chrome installed.
Command chromeocr-download tries to fetch the screen_ai component directly from Google's update servers, without needing Chrome installed.
example

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL