Capabilities¶
Uncial runs Tesseract on Android and the JVM and Apple Vision on iOS. The three are not interchangeable: they populate different fields, their confidences are not comparable, and their language sets differ. The SDK does not paper over that — it reports it.
val ocr = UncialClient { languages = listOf(OcrLanguage.Ukrainian) }
when (val caps = ocr.capabilities) {
null -> showUnavailable() // no engine, or the one present cannot run here
else -> {
if (!caps.perLineLanguage) hideLanguageColumn()
if (caps.droppedAnyOf(ocr.options.languages)) warnAboutDroppedLanguages()
}
}
UncialClient.capabilities¶
The property is OcrCapabilities?. It is computed on every read from the resolved engine
factory — engineProvider()?.takeIf { it.isAvailable() }?.capabilities(options) — so it
reflects the engine that is registered now, with this client's OcrOptions.
null means one of two things: no engine module is on the classpath, or the engine that is
present cannot run here (a JVM without a system libtesseract, for example). In that state
every extract call fails with OcrError.Unsupported. UncialClient.isAvailable is
exactly capabilities != null.
This is a cheap query, but not a free one
capabilities calls OcrEngineFactory.isAvailable(), and on Android that probe
triggers TessBaseAPI's System.loadLibrary the first time. The result is cached in
the factory, but the first read should not happen on the main thread.
Fields of OcrCapabilities¶
| Field | Type | Meaning |
|---|---|---|
engineId |
String |
Stable identifier — tesseract (TESSERACT_ENGINE_ID) or vision (VISION_ENGINE_ID). |
engineVersion |
String? |
The engine's own version when it reports one. Tesseract returns TessVersion() / TessBaseAPI.version; Vision returns vision-<revision>. null when it could not be read. |
languages |
List<OcrLanguage> |
The languages actually available — the request intersected with what this engine and its data can do. May be smaller than OcrOptions.languages. |
wordLevel |
Boolean |
Per-word boxes are available when OcrOptions.includeWords is set. |
confidence |
Boolean |
Lines and words carry a real Confidence, not Unknown. |
perLineLanguage |
Boolean |
OcrLine.language is populated. |
orientationDetection |
Boolean |
OcrPage.orientation is measured rather than assumed to be Orientation.Up. |
skewDetection |
Boolean |
OcrLine.skewDegrees is measured rather than left at zero. |
OcrCapabilities also carries one helper:
true when one or more requested languages had to be dropped — the single call that answers
"did the user's language choice actually take effect?".
The matrix¶
| words | confidence | per-line language | orientation | skew | |
|---|---|---|---|---|---|
| Tesseract (JVM) | yes | yes | yes | yes | yes |
| Tesseract (Android) | yes | yes | no | no | no |
| Vision (iOS) | yes[^1] | yes | no | no | no |
[^1]: Derived, not native — see below.
Why the JVM column is richer¶
Both Tesseract columns are the same engine reading the same .traineddata. The
difference is the binding:
- The JVM engine talks to libtesseract's C API directly. That API reports the
recognition language, the orientation and the deskew angle per element, so
JvmTesseractRecognizerreadselement.languageCode,element.orientationandelement.skewDegreesand the factory reportsperLineLanguage,orientationDetectionandskewDetectionastrue. - The Android engine goes through the Tesseract4Android Java wrapper, whose
ResultIteratorexposes text, a bounding box and a confidence — and nothing else. The data exists in the native layer; the wrapper does not surface it. So the Android factory reports all three asfalse.
This is a real asymmetry between two bindings of one engine, and it is precisely the kind
of thing OcrCapabilities exists to state rather than hide.
Where perLineLanguage is false, use OcrLine.script instead: it is derived from the
recognized text, so it is always available on every platform.
Vision's word boxes are derived¶
Vision has no word-level results. What it has is
VNRecognizedText.boundingBoxForRange, which maps a substring of the recognized line back
onto the image. VisionRecognizer splits each line on whitespace and queries the box for
every span, which is why iOS reports wordLevel = true and matches the Tesseract engines.
Two consequences worth knowing:
- A span whose box Vision declines to compute keeps its word, with
BoundingBox.Zero. Dropping the word would silently lose text. - Vision reports confidence per line, not per word. Each word repeats its line's
Confidence— more honest than inventing a per-word number or claimingUnknown.
Vision's languages are a per-request answer, not a static list¶
This is the subtlest item on the page.
VNRecognizeTextRequest.supportedRecognitionLanguages is an instance method, and Apple's
own header says a language supported at one recognition level might not be available at
another. Ukrainian is one of those: Vision offers uk-UA at the accurate level only.
So the engine's capability probe builds a throwaway VNRecognizeTextRequest configured
exactly as the real request will be — same recognitionLevel, derived from
OcrOptions.recognitionQuality — and asks that. Probing a differently-configured request
would report uk-UA as available under RecognitionQuality.Fast, and the extraction would
then quietly recognize Latin.
RecognitionQuality.Fast genuinely offers fewer languages on iOS
Switching a client from Accurate to Fast can change capabilities.languages. Read
the capabilities of the client you are about to use, not of a default one.
Two further Vision-specific rules:
- No Ukrainian before iOS 16. Vision gained it in text-recognition revision 3. On
iOS 15 the request does not offer
uk-UA, and a Ukrainian document comes back as Latin nonsense rather than failing.capabilities.languagesis where that shows up. - If the language query itself fails, the engine passes the requested languages
through unchanged (minus any with a
nullbcp47) rather than filtering against an empty set. Filtering against nothing would hand Vision an emptyrecognitionLanguages, which is an undeclared fallback toen-US.
Language availability is engine-specific too¶
OcrLanguage carries both identifiers because the engines disagree: Tesseract keys off a
.traineddata basename (tesseractCode, e.g. ukr), Vision off a BCP-47 tag (bcp47,
e.g. uk-UA). A language with a null bcp47 is skipped by the iOS engine rather than
failing the request.
On the Tesseract side, OcrEngineFactory.capabilities(options) is deliberately
optimistic: it reports what the engine would do with these options, because knowing
which models are really present needs I/O. The authoritative answer arrives after the
recognizer is created — the factory copies the resolved set into the recognizer's own
TextRecognizer.capabilities:
UncialClient.capabilities reads the factory, so treat its languages on
Android/JVM as "requested and plausible", and a NoLanguageData failure at extraction time
as the real verdict. See Language data.
Read them; do not assume uniformity¶
The practical rule the SDK is built around:
- Check
capabilities != nullbefore promising the user an OCR feature at all. - Check
capabilities.languages(ordroppedAnyOf) before promising a language. - Check
perLineLanguage/orientationDetection/skewDetectionbefore rendering a UI column that depends on them — on two of the three platforms they are empty. - Do not compare a confidence produced by Tesseract against one produced by Vision. Both are real; they are not the same scale.