Skip to content
Talk to our solutions team

OCR and text acquisition

The Rules block ingests OCR output. It does not perform OCR.

You run a recogniser yourself, out of band. If it emits hOCR, kis hocr2bbox converts that to a .bbox word-box file; if it already emits word boxes, write the .bbox JSON directly and skip the conversion. Either way the .bbox file is what loads into the document model that extraction, tables and matching operate on.

ComponentRequired forSupplied by
The kis CLIEvery command on this pageThe platform install
The rules pluginkis hocr2bbox, kis ocr, kis bbox, kis rulesA separate plugin install
An OCR engine that emits hOCRProducing the input filesYou — entirely your own installation

Nothing else. No tessdata directory, no language pack, no model download, no locale configuration. Text handling is character substitution only — no dictionary, no language model.

your OCR engine ──▶ page.hocr ──▶ kis hocr2bbox ──▶ page.bbox (JSON word boxes)
page.json (facts) ─────────────┬────┘
├──▶ kis ocr page | kis ocr doc
└──▶ POST /rule (same JSON, sent inline)

kis ocr page and kis ocr doc pair a .bbox file with a same-basename .json facts file and run rules over the pair. Those commands are covered in the CLI reference; how the document reaches a rule is covered in Rules over documents.

The .bbox file contents go under bbox, keyed by the document name the rule passes to bbox.Get, next to the facts:

{
"name": "invoice-checks",
"currency": "EUR",
"bbox": {"page": {"pages": [{"number": 1, "width": 612, "height": 792, "words": []}]}}
}

hOCR is HTML. It is parsed with a real HTML parser, not regex, and the parser is deliberately permissive about which producer emitted the file.

This is the complete accepted vocabulary.

LevelClasses
Pageocr_page
Content area (recursed into)ocr_carea, ocr_photo, ocr_graphic, ocr_separator, ocr_noise
Blockocr_par, ocr_textfloat, ocr_caption, ocr_header, ocr_footer
Lineocr_line, ocrx_line, ocr_textline, ocr_header_line
Wordocrx_word, ocr_word
Characterocrx_cinfo, ocr_cinfo

Any other class beginning with ocr produces an unrecognized_class warning in verbose mode; its text is recovered only by the orphan-text fallback.

<div class="ocr_page" id="page_1" title='image "/tmp/page_1.png"; bbox 0 0 2550 3300; ppageno 0'>
<p class="ocr_par" title="bbox 100 100 400 160">
<span class="ocr_line" title="bbox 100 100 400 160">
<span class="ocrx_word" title="bbox 100 100 400 160; x_wconf 95">HEADER</span>
</span>
</p>
</div>

The page comes out 2550 × 3300; the word lands at {100,100,400,160} with confidence 0.95.

Producers mix structure levels on one page. The parser runs a paragraph pass, then a bare-line pass, then a bare-word pass, each skipping subtrees the earlier pass already consumed, then — only if the page still has zero words — an orphan-text pass. A page containing an ocr_par, a bare ocr_line and a bare ocrx_word side by side yields three words, one paragraph and two lines, with nothing double-counted.

An hOCR title is a ;-separated property list. Splitting honours double-quoted values, and the property name must match as a whole token.

bbox 100 100 200 200 -> {100,100,200,200}
image "/tmp/page_1.png"; bbox 0 0 2550 3300; ppageno 0 -> {0,0,2550,3300}
image "/tmp/scan_bbox.png"; bbox 0 0 2550 3300 -> {0,0,2550,3300}
x_bboxes 10 5 10 10 20 5 30 10; bbox 100 100 200 200 -> {100,100,200,200}
image "a;bbox 9 9 9 9.png"; bbox 1 2 3 4 -> {1,2,3,4}
image "/tmp/page.png"; ppageno 0 -> no bbox
bboxes 1 2 3 4 -> no bbox
bbox a b c d; ppageno 7 -> no bbox

A malformed bbox yields nothing rather than scavenging digits from elsewhere in the title.

PropertyRead asResult
x_wconf NInteger 0–100N / 100
x_conf NFloat; divided by 100 if > 1N or N / 100
Neither present1.0
Word synthesised from fallback text0.0

Every word carries confidence in the 0–1 range whatever the producer.

When a word element has no bbox, the parser synthesises the word using the nearest enclosing element’s box — line, paragraph, content area, or page — and sets confidence to 0.0. If no ancestor has a box either, the word gets a zero-size box at the origin. A cluster of words sharing one identical oversized box, and (0,0,0,0) boxes, are both real possible output.

kis hocr2bbox writes a .bbox file: JSON, indented two spaces, holding the serialised document. This is also the format to emit directly if your engine does not produce hOCR.

{
"pages": [
{
"number": 1,
"width": 612,
"height": 792,
"words": [
{
"text": "Borrower",
"box": {"x0": 72, "y0": 100, "x1": 150, "y1": 115},
"confidence": 0.98,
"page": 1
}
]
}
]
}

words[].line_id is optional. lines and paragraphs arrays are optional and hold index ranges into words and lines respectively — see the document model for what those ranges mean.

The extension is a hint, not a contract. The loader dispatches like this:

ExtensionBehaviour
.jsonJSON loader
.hocrhOCR parser
.html, .htmSniffed for ocr_page, ocrx_word or ocr_word; hOCR if found, else JSON
Anything else, including .bboxJSON first, hOCR second

This is why kis bbox subcommands accept a .hocr file even though their -i help string says “path to bbox file”.

Terminal window
kis hocr2bbox -i scan-page1.hocr

With no -o the output path is the input path with its extension replaced by .bbox.

FlagDefaultPurpose
-i, --inputhOCR file, or a directory of hOCR files
-o, --outputInput locationOutput file (single input) or directory (directory input)
-w, --workers4Parallel workers; directory mode only
-v, --verbosefalseReport content the parser could not place
--cleanfalseApply the default text-cleanup profile
--clean-formfalseApply the form-aware cleanup profile
Terminal window
kis hocr2bbox -i scan-page1.hocr -o out/page1.bbox
kis hocr2bbox -i hocr/ -o bbox/ -w 8 --clean
kis hocr2bbox -i hocr/ -o bbox/ --clean-form
kis hocr2bbox -i hocr/ -v

Directory mode globs *.hocr in the given directory, non-recursively. .html and .htm files are skipped even though the single-file path accepts them, and subdirectories are ignored. Matching nothing is a warning with exit code 0, not an error. Directory mode creates -o if needed; single-file mode does not create a missing parent directory.

Per-file failures in directory mode do not stop the run — every file is attempted, then the first error is returned.

Warnings are the only visibility you have into content the parser could not place.

TypeMeaning
no_structureNo ocr_page found and fallback extraction found no words
missing_bboxA page or word element has no bbox
no_bbox_fallbackNo box available anywhere; a zero box was used
empty_paragraphParagraph has text but no lines or words were extracted
empty_lineLine has text but no words were extracted
empty_wordWord element has no text content
orphan_textText outside any word element; a fallback word was created
unrecognized_classAn OCR class outside the accepted vocabulary
missed_contentPost-hoc diff of source text against extracted word text

missed_content compares all source text against all extracted word text, lowercased and punctuation-stripped, ignoring single characters, and reports the first 10 missed words.

Each warning renders as [type] element: "text" (context: ...), with text trimmed and truncated to 50 characters. Warnings carrying no text drop the : "text" part.

Cleanup is pure text in, text out. It is character-level only — no dictionary, no language model, no language parameter.

OptionEffect
NormalizeUnicodeUnicode NFC
FixQuotes23 quote, prime and guillemet variants — including backtick and acute — to ASCII ' or "
FixDashes11 dash variants to -
FixLigaturesfi/fl/ff/ffi/ffl/st, Æ/Œ/IJ/ꜳ/ꝏ, and symbol expansions such as No., (P), SM, TM, (R), (C)
RemoveGarbageDeletes 56 glyphs — bullets, arrows, box drawing, geometric shapes, stray diacritics, pilcrow, section sign
NormalizeFormIndicatorsCheckbox and radio glyphs to [ ], [x], ( ), (x)
FixMisreadsFullwidth ASCII to ASCII, Greek and Cyrillic homoglyphs to Latin, Roman numerals expanded, common Unicode space variants to a space, zero-width characters deleted
RemoveStrayPunct16 X patterns to a single space
CleanWhitespaceWhitespace collapse
RemoveControlCharsControl characters removed
AggressiveCleanEverything outside printable ASCII becomes a space; newlines and tabs survive

The two profiles compose these options, applied in the order shown. RemoveGarbage reads PreserveFormIndicators to decide whether checkbox and radio glyphs count as garbage.

OptionDefault profileForm-aware profile
NormalizeUnicode, FixQuotes, FixDashes, FixLigaturesonon
RemoveGarbage, FixMisreads, CleanWhitespace, RemoveControlCharsonon
PreserveFormIndicatorsonon
NormalizeFormIndicatorsoffon
RemoveStrayPunctoffoff
AggressiveCleanoffoff

--clean selects the default profile; --clean-form selects the form-aware profile.

Cleanup is applied to each word’s text, then each line’s text, then each paragraph’s text. Three consequences:

  • Boxes are never adjusted. A word whose text is emptied by garbage-stripping stays in the output as an empty-text box.
  • Word text and line text are cleaned independently. A line’s text was joined from the raw words at parse time, so after cleanup a line’s text is not necessarily its cleaned words rejoined.
  • RemoveStrayPunct matches only a punctuation character with a space on both sides, so it is a no-op on word text — a single word has no spaces — but it does fire on line and paragraph text, where words were joined with spaces.

No surface selects an OCR engine: there is no CLI flag, no file format and no request field that names one. Convert your recogniser’s output to hOCR or to word-box JSON yourself, then supply it the way every other document is supplied — a path to kis ocr page, or inline under bbox on a request. These three common shapes map onto word-box JSON, and the notes say what your conversion must handle.

SourceShape it emitsWhat your conversion must do
PaddleOCRFour-corner polygons, per-page result arraysReduce each polygon to an axis-aligned box; drop items with fewer than 4 corners
Tesseract TSVRow records with level, position, size, confKeep only level-5 (word) rows with non-empty text; divide conf by 100
DoclingWord records with bbox [x0,y0,x1,y1] and scoreRequire at least 4 bbox values; score becomes confidence

PaddleOCR and Tesseract report page width and height. Docling does not — carry the page size from your own pipeline, or the page arrives sized 0 × 0.

Page width and height are read from the ocr_page bbox, or from width and height in word-box JSON. If the hOCR has no ocr_page bbox, its page title is malformed, or the JSON page omits the dimensions, both are 0.

Page analysis seeds the statistics from the page declaration, and falls back to the bounding extent of the page’s words when the declaration is zero, flagging that the fallback was used. Percentage-based spatial queries scale by the statistics, not by the raw page record, so they still resolve. The fallback under-estimates the physical page — margins beyond the last word are lost — so percentage regions come out over-scaled. It is a bounded approximation rather than a dead query.

Analysis runs in full for every execution that carries a document, on both surfaces, and caches nothing between executions. Every detector — table, field, checkbox, region — runs on every page, and none of them can be turned off from the CLI or from the request.

Where the two surfaces differ is how many documents are in flight at once, and that is a CLI concern: the service analyses exactly one request’s documents at a time, on the request goroutine.

WhereFlagDefaultEffect
kis hocr2bbox -w-w / --workers4Directory mode only; ignored for a single file
kis ocr page -w-w / --workers1Fully serial; clamped to the number of resolved inputs
kis ocr docNo workers flag; the whole corpus becomes one document
rules.svcOne analysis per request, synchronous
MessageCause
Error: '<command>' is not installed.The rules plugin binary is absent from every search location, or present without an executable bit
failed to stat input path-i does not exist
loading hocr failed from given input fileThe path could not be opened, or the HTML parser failed
failed to create output directoryDirectory mode could not create -o
no hocr files found in directoryDirectory mode matched no *.hocr; warning, exit code 0
writing json to output file path failedPermissions, or a missing parent directory in single-file mode
some files failed to processOne or more files failed in directory mode; the first error is returned

The full table is in Error reference; diagnosis steps are in Troubleshooting.