Skip to content

Let TextRecognizer read non-Latin scripts - #5562

Merged
shai-almog merged 3 commits into
masterfrom
text-recognizer-script-selection
Aug 18, 2026
Merged

Let TextRecognizer read non-Latin scripts#5562
shai-almog merged 3 commits into
masterfrom
text-recognizer-script-selection

Conversation

@shai-almog

Copy link
Copy Markdown
Collaborator

Closes the RFE in discussion #5555: com.codename1.ai.vision.TextRecognizer had no way to say what writing system to read, so it was Latin-only. Android hard-coded ML Kit's Latin TextRecognizerOptions.DEFAULT_OPTIONS, and the Apple backend left VNRecognizeTextRequest on its default languages. Both frameworks can do better — ML Kit ships a recognizer per script, Apple Vision takes recognitionLanguages — the portable API just never exposed the choice.

The API

TextRecognizer recognizer = new TextRecognizer(
        new VisionOptions().textScript(TextScript.japanese()));

New TextScript with latin() / chinese() / devanagari() / japanese() / korean(), plus VisionOptions.textScript(...). It names a script, not a language: that is what the underlying recognizers are organized by, and it is the one shape both backends can honour. Each script model also reads the Latin text mixed into the same page.

No existing constructor, signature, or default changes — an app that never names a script behaves exactly as before.

Per platform

  • Android — each script maps to its own ML Kit recognizer.
  • Apple Vision (the iOS default) — sets recognitionLanguages, asking supportedRecognitionLanguagesAndReturnError: what the OS actually supports rather than hard-coding a per-release list. When the OS supports none of them the analysis fails with an explicit error naming VisionBackends.mlKitTextRecognition() as the way out. Leaving the request on its defaults would have run Latin OCR over a Japanese page and returned confident nonsense.
  • iOS ML Kit — resolves the script options class through NSClassFromString, so CN1Vision.m still compiles whichever pods a given build linked.

Dependency selection

Like VisionBackends, each selector call doubles as a build-time dependency marker, so a build carries only the models it asked for — a Japanese app does not ship the Korean or Devanagari models.

The Android AI adapters compile inside the generated app, not in this repo, so each script gets its own adapter source: one import per file is exactly what lets pruneOptionalAiSources delete the models the app never selects. PlatformFeatureCatalog.Entry gained support for several required methods (it held one), so the iOS script pod lands only when the ML Kit backend and that script are both selected — Apple Vision reads these scripts itself.

Verification

Vendor facts were checked against the real artifacts, not the docs alone:

  • the four Java options classes and TextRecognition.getClient(TextRecognizerOptionsInterface) read out of the published AARs with javap
  • the four pod subspec names out of the GoogleMLKit podspec
  • the ObjC options class names out of Google's iOS reference
  • the Vision selectors and their availability out of the Xcode SDK headers

Green locally: core-unittests (3 new), catalog tests (2 new), builder tests (5 new), SpotBugs at zero across ios/plugin/core-unittests, the CI quality-report script, check-cast-semantics.sh, and check-native-signatures.sh — the native gained a String argument, so its C name changed. CN1Vision.m was syntax-checked against the iOS 26.2 SDK both with and without INCLUDE_CN1_VISION, and the Android adapters compiled against stubbed ML Kit APIs.

A note on the new parity test. The Android AI adapters are excluded from every build in this repo, so a class-name typo there is not a compile error anywhere — CI stays green and the feature ships inert. AndroidTextScriptParityTest ties the chain together instead: selector method → retained adapter source → class the port loads → ML Kit artifact the catalog adds.

Not verified locally

Both need a device build, and are worth confirming before release:

  • the iOS ML Kit branch (needs the pods linked)
  • end-to-end recognition of real non-Latin text on device

Deliberate tradeoff

The Latin model ships in every OCR app, including a Japanese-only one: the base adapter and the no-arg constructor both need it, and the catalog cannot express "unless a script was selected". Documented in the developer guide.

🤖 Generated with Claude Code

The built-in OCR API had no way to say what writing system to read, so it
was Latin-only: Android hard-coded ML Kit's Latin DEFAULT_OPTIONS and the
Apple backend left VNRecognizeTextRequest on its default languages. Both
frameworks can do better -- ML Kit ships a recognizer per script and Apple
Vision takes recognitionLanguages -- the portable API just never exposed
the choice. Reported in discussion #5555 for Japanese OCR.

Adds com.codename1.ai.vision.TextScript with latin/chinese/devanagari/
japanese/korean selectors and VisionOptions.textScript(...):

    new TextRecognizer(new VisionOptions().textScript(TextScript.japanese()))

The selector names a script rather than a language because that is what
the recognizers are organized by, and it is the one shape both backends
can honour. No existing constructor or default changes.

Like VisionBackends, each selector call is also a build-time dependency
marker, so a build carries only the models it asked for. On Android the
adapters are compiled inside the generated app, so each script gets its
own adapter source: one import per file is what lets the builder delete
the models the app never selects. PlatformFeatureCatalog.Entry gained
support for several required methods, so the iOS script pod lands only
when the ML Kit backend AND that script are both selected.

Apple Vision asks the OS which recognition languages it supports instead
of hard-coding a per-release list, and fails the analysis with an explicit
error when the script is not among them. Leaving the request on its
defaults would have run Latin OCR over the page and returned confident
nonsense; the error names mlKitTextRecognition() as the way out.

Verified against the vendor artifacts rather than the docs alone: the four
Java options classes and TextRecognition.getClient(TextRecognizerOptions-
Interface) read out of the published AARs with javap, the pod subspec
names out of the GoogleMLKit podspec, and the Vision selectors and their
availability out of the Xcode SDK headers.

The Android AI adapters are excluded from every build in this repo (they
need ML Kit, which only the generated app has), so a new parity test ties
the chain together instead: selector method -> retained adapter source ->
class the port loads -> ML Kit artifact the catalog adds. A break there is
otherwise silent, leaving CI green and the feature inert.

Not exercised locally, both needing a device build: the iOS ML Kit branch
and end-to-end recognition of real non-Latin text.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ff50115486

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Ports/iOSPort/nativeSources/CN1Vision.m Outdated
@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Developer Guide build artifacts are available for download from this workflow run:

Developer Guide quality checks:

  • AsciiDoc linter: No issues found (report)
  • Vale: No alerts found (report)
  • Paragraph capitalization: No paragraph capitalization issues (report)
  • LanguageTool: No grammar matches (report)
  • Image references: No unused images detected (report)

@github-actions

Copy link
Copy Markdown
Contributor

Cloudflare Preview

@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

✅ Continuous Quality Report

Test & Coverage

Static Analysis

  • SpotBugs [Report archive]
    • ByteCodeTranslator: 0 findings (no issues)
    • android: 0 findings (no issues)
    • codenameone-maven-plugin: 0 findings (no issues)
    • core-unittests: 0 findings (no issues)
    • ios: 0 findings (no issues)
  • PMD: 0 findings (no issues) [Report archive]
  • Checkstyle: 0 findings (no issues) [Report archive]

Generated automatically by the PR CI workflow.

@shai-almog

shai-almog commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 151 screenshots: 151 matched.

Native Android coverage

  • 📊 Line coverage: 8.09% (7868/97198 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.07% (41636/515701), branch 2.88% (1402/48723), complexity 3.18% (1663/52272), method 4.90% (1355/27642), class 9.97% (367/3680)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6367 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.ClassReader – 0.00% (0/1524 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1187 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.MethodWriter – 0.00% (0/922 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/736 lines covered)
      • com.google.common.cache.com.google.common.cache.LocalCache$Segment – 0.00% (0/726 lines covered)
      • okio.okio.Buffer – 0.00% (0/687 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/625 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.Frame – 0.00% (0/570 lines covered)

✅ Native Android screenshot tests passed.

Native Android coverage

  • 📊 Line coverage: 8.09% (7868/97198 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.07% (41636/515701), branch 2.88% (1402/48723), complexity 3.18% (1663/52272), method 4.90% (1355/27642), class 9.97% (367/3680)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6367 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.ClassReader – 0.00% (0/1524 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1187 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.MethodWriter – 0.00% (0/922 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/736 lines covered)
      • com.google.common.cache.com.google.common.cache.LocalCache$Segment – 0.00% (0/726 lines covered)
      • okio.okio.Buffer – 0.00% (0/687 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/625 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.Frame – 0.00% (0/570 lines covered)

Benchmark Results

Detailed Performance Metrics

Metric Duration
SIMD kernel backend scalar fallback (no native SIMD)
SIMD int-add (64K x300) java 160ms / native 239ms = 0.6x speedup
SIMD float-mul (64K x300) java 178ms / native 70ms = 2.5x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path gated to scalar (CPU autovectorizes scalar; explicit SIMD not beneficial here)
Base64 CN1 encode 77.000 ms
Base64 CN1 decode 86.000 ms
Base64 native encode 402.000 ms
Base64 encode ratio (CN1/native) 0.192x (80.8% faster)
Base64 native decode 295.000 ms
Base64 decode ratio (CN1/native) 0.292x (70.8% faster)
Image encode benchmark status skipped (SIMD unsupported)

@shai-almog

shai-almog commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 181 screenshots: 181 matched.
✅ JavaScript-port screenshot tests passed.

Both new capability failures -- Apple Vision having no recognition
language for the selected script, and the ML Kit script model not being
linked -- were reported as plain JSON errors, which IOSVisionImpl.parse
classified as VisionException.BACKEND_ERROR. Android raises UNSUPPORTED
for the same condition, so a caller could not use the portable code to
decide whether the ML Kit fallback was worth offering and would have had
to pattern-match on the English message.

The native side now tags those two payloads with a symbolic errorCode and
parse() maps it, so the condition is UNSUPPORTED on both platforms. An
untagged failure still means a genuine backend error.

Also fixes the three developer-guide Vale findings that turned CI red.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@shai-almog

shai-almog commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 149 screenshots: 149 matched.
✅ Native iOS Metal screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 1153 seconds

Build and Run Timing

Metric Duration
Simulator Boot 76000 ms
Simulator Boot (Run) 1000 ms
App Install 13000 ms
App Launch 2000 ms
Test Execution 384000 ms

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 62ms / native 3ms = 20.6x speedup
SIMD float-mul (64K x300) java 57ms / native 4ms = 14.2x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path active (NEON-accelerated)
Base64 CN1 encode 163.000 ms
Base64 CN1 decode 134.000 ms
Base64 native encode 534.000 ms
Base64 encode ratio (CN1/native) 0.305x (69.5% faster)
Base64 native decode 421.000 ms
Base64 decode ratio (CN1/native) 0.318x (68.2% faster)
Base64 SIMD encode 51.000 ms
Base64 encode ratio (SIMD/CN1) 0.313x (68.7% faster)
Base64 SIMD decode 46.000 ms
Base64 decode ratio (SIMD/CN1) 0.343x (65.7% faster)
Base64 encode ratio (SIMD/native) 0.096x (90.4% faster)
Base64 decode ratio (SIMD/native) 0.109x (89.1% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 10.000 ms
Image createMask (SIMD on) 4.000 ms
Image createMask ratio (SIMD on/off) 0.400x (60.0% faster)
Image applyMask (SIMD off) 122.000 ms
Image applyMask (SIMD on) 354.000 ms
Image applyMask ratio (SIMD on/off) 2.902x (190.2% slower)
Image modifyAlpha (SIMD off) 202.000 ms
Image modifyAlpha (SIMD on) 148.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.733x (26.7% faster)
Image modifyAlpha removeColor (SIMD off) 92.000 ms
Image modifyAlpha removeColor (SIMD on) 257.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 2.793x (179.3% slower)

@shai-almog

shai-almog commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 148 screenshots: 148 matched.
✅ Native Mac screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 280 seconds

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 89ms / native 3ms = 29.6x speedup
SIMD float-mul (64K x300) java 84ms / native 6ms = 14.0x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path active (NEON-accelerated)
Base64 CN1 encode 236.000 ms
Base64 CN1 decode 146.000 ms
Base64 native encode 792.000 ms
Base64 encode ratio (CN1/native) 0.298x (70.2% faster)
Base64 native decode 385.000 ms
Base64 decode ratio (CN1/native) 0.379x (62.1% faster)
Base64 SIMD encode 81.000 ms
Base64 encode ratio (SIMD/CN1) 0.343x (65.7% faster)
Base64 SIMD decode 61.000 ms
Base64 decode ratio (SIMD/CN1) 0.418x (58.2% faster)
Base64 encode ratio (SIMD/native) 0.102x (89.8% faster)
Base64 decode ratio (SIMD/native) 0.158x (84.2% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 13.000 ms
Image createMask (SIMD on) 5.000 ms
Image createMask ratio (SIMD on/off) 0.385x (61.5% faster)
Image applyMask (SIMD off) 84.000 ms
Image applyMask (SIMD on) 56.000 ms
Image applyMask ratio (SIMD on/off) 0.667x (33.3% faster)
Image modifyAlpha (SIMD off) 68.000 ms
Image modifyAlpha (SIMD on) 61.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.897x (10.3% faster)
Image modifyAlpha removeColor (SIMD off) 57.000 ms
Image modifyAlpha removeColor (SIMD on) 49.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.860x (14.0% faster)

@shai-almog

shai-almog commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 217 screenshots: 217 matched.
✅ Native Apple Watch (watchOS, Core Graphics) screenshot tests passed.

The iOS ML Kit path reached the per-script options class through
NSClassFromString so that CN1Vision.m would keep compiling for a build
that linked only some of the pods. That is the wrong way to buy it, and
it would have shipped the feature inert.

CocoaPods links these pods as static frameworks, and IPhoneBuilder passes
-ObjC only for ads builds or the ios.objC hint. A class that no symbol
references is therefore dropped at link time, so NSClassFromString would
have returned nil -- and the analyzer reported "not linked into this
build" -- on a build whose Podfile did include the model. The failure is
silent in exactly the way a wrong ParparVM native name is: green build,
dead feature.

__has_include gives the same "compiles without the pod" property with a
real symbol reference, which is what the neighbouring MLKitTextRecognition
and MLKitBarcodeScanning blocks already do. It also gets the class names
spell-checked by the compiler rather than at runtime on a device.

Verified by compiling CN1Vision.m against the actual ML Kit frameworks
pulled from their podspecs, in three configurations: all four script pods
present, only the Japanese pod present (the realistic case, where the
other three branches must compile out), and no ML Kit at all. A separate
probe with #error in the else branch confirms all four header guards
really fire rather than silently skipping, and that each options class
and its init type-check against MLKCommonTextRecognizerOptions. The
umbrella header and class names were read out of the downloaded
frameworks, not assumed from the naming convention.

No builder change is needed for any of this: the script pods reach the
Podfile through the catalog's generic iosPods() loop, and INCLUDE_CN1_VISION
keys off the analyzer classes, which any TextScript caller also references.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@shai-almog

shai-almog commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 144 screenshots: 144 matched.
✅ Native Apple TV (tvOS, Metal) screenshot tests passed.

@shai-almog

shai-almog commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 143 screenshots: 143 matched.
✅ Native iOS screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 1156 seconds

Build and Run Timing

Metric Duration
Simulator Boot 100000 ms
Simulator Boot (Run) 1000 ms
App Install 14000 ms
App Launch 1000 ms
Test Execution 455000 ms

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 100ms / native 2ms = 50.0x speedup
SIMD float-mul (64K x300) java 82ms / native 4ms = 20.5x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path active (NEON-accelerated)
Base64 CN1 encode 443.000 ms
Base64 CN1 decode 204.000 ms
Base64 native encode 451.000 ms
Base64 encode ratio (CN1/native) 0.982x (1.8% faster)
Base64 native decode 392.000 ms
Base64 decode ratio (CN1/native) 0.520x (48.0% faster)
Base64 SIMD encode 114.000 ms
Base64 encode ratio (SIMD/CN1) 0.257x (74.3% faster)
Base64 SIMD decode 123.000 ms
Base64 decode ratio (SIMD/CN1) 0.603x (39.7% faster)
Base64 encode ratio (SIMD/native) 0.253x (74.7% faster)
Base64 decode ratio (SIMD/native) 0.314x (68.6% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 9.000 ms
Image createMask (SIMD on) 2.000 ms
Image createMask ratio (SIMD on/off) 0.222x (77.8% faster)
Image applyMask (SIMD off) 76.000 ms
Image applyMask (SIMD on) 33.000 ms
Image applyMask ratio (SIMD on/off) 0.434x (56.6% faster)
Image modifyAlpha (SIMD off) 127.000 ms
Image modifyAlpha (SIMD on) 369.000 ms
Image modifyAlpha ratio (SIMD on/off) 2.906x (190.6% slower)
Image modifyAlpha removeColor (SIMD off) 221.000 ms
Image modifyAlpha removeColor (SIMD on) 156.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.706x (29.4% faster)

@shai-almog
shai-almog merged commit dc13785 into master Aug 18, 2026
45 of 49 checks passed
@shai-almog
shai-almog deleted the text-recognizer-script-selection branch August 18, 2026 01:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant