Skip to content
Reconstruct PDF from hOCR

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:

StepCallNotes
Get the page imageextractor.New(page) then ExtractPageImages(nil)Returns PageImages.Images, a slice of ImageMark.
Turn it into a fileImageMark.Image.ToGoImage() then jpeg.EncodeThe service wants an encoded image, not raw samples.
RecognizeExtractText with FormFields{"format": "hocr"}Response is bytes; parsing is yours.
Place the wordscreator.StyledParagraph with SetPosOne 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.pdf

If this is your first time using UniPDF, follow the getting started guide to create an API key and set up your development environment.

View the full source
Last updated on