Reconstruct PDF from hOCR
A scanned PDF is a picture of a document: no text objects, so nothing to extract, search or select. hOCR gives you every word along with the box it occupied, which is enough to build a page of real text objects at the same coordinates.
This is the one OCR page that involves the rest of UniPDF. The extractor package
gets the images out, the ocr package posts them to the service, and creator writes
the words back. See OCR Service for starting ocrserver, and
Get hOCR output for the shape of the hOCR itself.
The pipeline
Four steps, each of which can be swapped out:
| Step | Call | Notes |
|---|---|---|
| Get the page image | extractor.New(page) then ExtractPageImages(nil) | Returns PageImages.Images, a slice of ImageMark. |
| Turn it into a file | ImageMark.Image.ToGoImage() then jpeg.Encode | The service wants an encoded image, not raw samples. |
| Recognize | ExtractText with FormFields{"format": "hocr"} | Response is bytes; parsing is yours. |
| Place the words | creator.StyledParagraph with SetPos | One paragraph per word. |
Doing it
Once the hOCR is parsed, the placement is the interesting part:
c := creator.New()
c.SetPageSize(creator.PageSize{float64(pageBBox.X1), float64(pageBBox.Y1)})
c.NewPage()
for _, word := range words {
sp := c.NewStyledParagraph()
sp.SetPos(float64(word.BBox.X0), float64(word.BBox.Y1)) // absolute, origin top left
sp.SetFontSize(line.XSize) // line height, not word height
sp.SetText(word.Text)
div.Add(sp)
}
c.Draw(div)hOCR boxes have their origin at the top left of the image with y increasing downward,
and creator’s absolute coordinates use the same convention, so pixel values can go
straight into SetPos with no flip. That is only true because the page size is set
from the image bounds; a real page size means scaling both axes and inverting y.
Font size comes from the line’s x_size, not from the word’s box height. A word box
is only as tall as its own glyphs, so sizing from it makes a page of ed. and is
render smaller than the words around them.
Limitations
ExtractPageImages returns whatever image XObjects the page draws, not a rendering of
the page. A scan is normally one full-page image, which is why the pipeline works, but
a page with three photographs gives you three images and no way to reassemble the
layout. Text already in the page is not included, so a hybrid page loses its real text
unless you keep the original page too. Inline stencil masks are skipped by default;
&extractor.ImageExtractOptions{IncludeInlineStencilMasks: true} includes them.
ImageMark carries X, Y, Width and Height in PDF coordinates, describing where
the image was drawn and at what size. The example ignores those and builds the page
from the hOCR bounding box instead, so the output page is as many points as the image
was pixels. A 300 DPI letter scan comes out roughly 2550 by 3300 points, four times
too large. Divide by the scan’s DPI and multiply by 72 to get the original size.
page.Rotate is a *int64 and is nil on any page without a /Rotate entry, so
dereferencing it panics. page.GetRotate() returns the value and walks up the page
tree for an inherited one, which is what to use.
The reconstructed page holds text only. Nothing copies the scan into the output, so
what you get is a text reproduction, not a searchable version of the original. For a
searchable scan, draw the image first with creator.Image and set the words'
TextStyle.RenderingMode to creator.TextRenderingModeInvisible so they sit on top
without showing.
Absolutely positioned components ignore their margins. SetPos switches a
StyledParagraph to absolute mode, after which SetMargins on it does nothing. Note
also that Division.SetMargins and StyledParagraph.SetMargins both take
(left, right, top, bottom), which is not the CSS order.
Nothing here understands reading order. Every word becomes its own paragraph, so
extracting text from the result gives you words in hOCR document order with no
guarantee of spacing between them. x_wconf is available per word and is worth
filtering on before drawing.
Run the example
loadImages extracts and rotates the images for every page, processImage posts one
image and unmarshals the hOCR into OCRPage, and writeContentAsPDF walks areas,
paragraphs, lines and words to draw them. ParseTitleAttributes is the regex-based
reader for the title attributes.
Output goes to output/page_N.pdf, one file per input page, so a page holding more
than one image has each result overwrite the last.
git clone https://github.com/unidoc/unipdf-examples.git
cd unipdf-examples/ocr
go run reconstruct_pdf_from_hocr.go scanned.pdfIf this is your first time using UniPDF, follow the getting started guide to create an API key and set up your development environment.