By using this site, you agree to the Privacy Policy and Terms of Use.
Accept
World of SoftwareWorld of SoftwareWorld of Software
  • News
  • Software
  • Mobile
  • Computing
  • Gaming
  • Videos
  • More
    • Gadget
    • Web Stories
    • Trending
    • Press Release
Search
  • Privacy
  • Terms
  • Advertise
  • Contact
Copyright © All Rights Reserved. World of Software.
Reading: Building LetterLens: An OCR-Powered Android App With Kotlin + ML Kit, and Ktor | HackerNoon
Share
Sign In
Notification Show More
Font ResizerAa
World of SoftwareWorld of Software
Font ResizerAa
  • Software
  • Mobile
  • Computing
  • Gadget
  • Gaming
  • Videos
Search
  • News
  • Software
  • Mobile
  • Computing
  • Gaming
  • Videos
  • More
    • Gadget
    • Web Stories
    • Trending
    • Press Release
Have an existing account? Sign In
Follow US
  • Privacy
  • Terms
  • Advertise
  • Contact
Copyright © All Rights Reserved. World of Software.
World of Software > Computing > Building LetterLens: An OCR-Powered Android App With Kotlin + ML Kit, and Ktor | HackerNoon
Computing

Building LetterLens: An OCR-Powered Android App With Kotlin + ML Kit, and Ktor | HackerNoon

News Room
Last updated: 2025/09/26 at 9:26 AM
News Room Published 26 September 2025
Share
SHARE

The first time I saw my family struggle to interpret the NHS and council letters, I decided to create an application that explains these letters in plain English. Government letters are unstructured data full of dates, instructions, codes, and jargon, but mostly people only need to know three things: what it’s about, when it’s happening, and what to do next. That became the starting point for LetterLens.

Purpose

I aimed to reduce the anxiety people feel when dealing with government paperwork. With LetterLens, the user simply scans the letter, and the system translates it into easy-to-understand English with clear next steps. It highlights the key actions the letter expects, so users know exactly what to do.


:::info
Disclaimer: LetterLens is an educational prototype, not an alternative for legal advice.

:::


Tech Stack (Decision-making)

  • Jetpack Compose: Modern declarative UI, quick prototyping, and easier state handling.
  • CameraX: Lifecycle-aware camera integration, seamless with compose.
  • Ktor: Lightweight Kotlin-native backend to classify and elaborate the letter types.
  • ML Kit: On-device OCR for privacy and low latency.

Under the Hood(Deep Dive)

Camerax: capture and preview

Camerax makes capturing and analyzing images. Here’s the setup to bind a preview and analyzer to the lifecycle.

val cameraProviderFuture = ProcessCameraProvider.getInstance(ctx)
cameraProviderFuture.addListener({
   val provider = cameraProviderFuture.get()
   val preview = Preview.Builder()
       .setTargetAspectRatio(AspectRatio.RATIO_4_3) // keep 4:3
       .build()
       .also { it.setSurfaceProvider(view.surfaceProvider) }
   provider.unbindAll()
   provider.bindToLifecycle(
       lifecycle,
       CameraSelector.DEFAULT_BACK_CAMERA,
       preview,
       imageCapture
   )
}, ContextCompat.getMainExecutor(ctx))

:::tip
Tip: Rotate based on EXIF and crop margins; sharper, upright images improve OCR markedly.

:::

ML Kit OCR: Extract raw text on-device

ML Kit processes the image and extracts raw text and confidence scores.

val img = InputImage.fromFilePath(context, Uri.fromFile(photo))
val rec = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS)
rec.process(img)
   .addOnSuccessListener { onText(it.text) }
   .addOnFailureListener { e -> onError("OCR failed: ${e.message}") }

:::info
Note: I keep work entirely on-device; no letter images leave the phone.

:::

Ktor ‘explains’ endpoint: classify + extract

A small Ktor service classifies text and pulls deadlines/actions.

routing {
   post("/explain") {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;val req = call.receive<ExplainReq>()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;val type = classify(req.text, req.hint)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;val deadline = extractDeadline(req.text, type)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;val (summary, actions, citations) = explainForType(type, req.text)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;call.respond(ExplainRes(type, deadline, summary, actions, citations))
&nbsp;&nbsp;&nbsp;}
}

Keyword heuristics (examples)

  • NHS Appointment: “appointment”, “please attend”, “clinic”, “vaccination”, “CHI number”.

  • Electoral Register / Annual Canvass: “Electoral Registration Office”, “annual canvass”, “unique security code”, “register to vote”.

Data Parsing & Classification

Beyond the /explain endpoint, the core of LetterLens is its classifier. Government letters are messy—mixed fonts, spacing, codes, dates, so I added helpers for normalization, fuzzy matching, and deadline detection.

Normalization helpers:

private fun norm(s: String) = s
&nbsp;&nbsp;&nbsp;.lowercase()
&nbsp;&nbsp;&nbsp;.replace('’', ''')
&nbsp;&nbsp;&nbsp;.replace('–', '-')
&nbsp;&nbsp;&nbsp;.replace(Regex("\s+"), " ")
&nbsp;&nbsp;&nbsp;.trim()

Fuzzy matching (so “N H-S” still matches “NHS”):

private fun fuzzyRegex(token: String): Regex {
&nbsp;&nbsp;&nbsp;val letters = token.lowercase().filter { it.isLetterOrDigit() }
&nbsp;&nbsp;&nbsp;val pattern = letters.joinToString("\W*")
&nbsp;&nbsp;&nbsp;return Regex(pattern, RegexOption.IGNORE_CASE)
}

Classify by domain:

private fun classify(textRaw: String, hint: String?): String {
&nbsp;&nbsp;&nbsp;val n = norm("${hint ?: ""} $textRaw")
&nbsp;&nbsp;&nbsp;if (hasAny(n, "nhs", "appointment", "vaccination")) return "NHS Appointment"
&nbsp;&nbsp;&nbsp;if (hasAny(n, "electoral register", "unique security code")) return "Electoral Register"
&nbsp;&nbsp;&nbsp;if (hasAny(n, "council tax", "arrears")) return "Council Tax"
&nbsp;&nbsp;&nbsp;if (hasAny(n, "hmrc", "self assessment")) return "HMRC"
&nbsp;&nbsp;&nbsp;if (hasAny(n, "dvla", "vehicle tax")) return "DVLA"
&nbsp;&nbsp;&nbsp;if (hasAny(n, "ukvi", "visa", "biometric residence")) return "UKVI"
  &nbsp;return "Unknown"

}

Deadline extraction (supports both 12 Sept 2025 and 12/09/2025 formats):

private fun extractDeadline(raw: String, type: String? = null): String? {
&nbsp;&nbsp;&nbsp;val n = norm(raw)
&nbsp;&nbsp;&nbsp;return DATE_DMY_SLASH.find(n)?.value ?: DATE_DMY_TEXT.find(n)?.value
}

Explain response with summary + actions + citations:

private fun explainForType(type: String, text: String): Triple<String, List<String>, List<String>> {
&nbsp;&nbsp;&nbsp;return when (type) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"Electoral Register" -> Triple(
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"Looks like an Electoral Register annual canvass letter.",
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;listOf("Go to website", "Enter unique security code", "Confirm/update household"),
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;listOf("https://www.gov.uk/register-to-vote")
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"NHS Appointment" -> Triple(
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"An NHS clinic invite (likely vaccination).",
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;listOf("Add to calendar", "Bring Red Book", "Call if reschedule needed"),
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;listOf("https://www.nhs.uk/nhs-services/appointments-and-bookings/")
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;else -> Triple("Generic gov letter", listOf("Read carefully", "Follow instructions"), listOf("https://www.gov.uk"))
&nbsp;&nbsp;&nbsp;}
}

Show an example API call/output:

Sample request:

POST /explain
{
&nbsp;&nbsp;&nbsp;"text": "Your NHS vaccination appointment is on 25 Sept at Glasgow Clinic. Please bring your Red Book."
}

Sample response:

{
&nbsp;&nbsp;&nbsp;"type": "NHS Appointment",
&nbsp;&nbsp;&nbsp;"deadline": "25 Sept 2025",
&nbsp;&nbsp;&nbsp;"summary": "This looks like an NHS appointment invite (e.g., vaccination). When: 25 Sept. Location: Glasgow Clinic.",
&nbsp;&nbsp;&nbsp;"actions": [
&nbsp;&nbsp;&nbsp;"Add the appointment date/time to your calendar.",
&nbsp;&nbsp;&nbsp;"Bring any requested documents (e.g., child Red Book).",
&nbsp;&nbsp;&nbsp;"If you need to reschedule, call the number on the letter."
&nbsp;&nbsp;&nbsp;],
&nbsp;&nbsp;&nbsp;"citations": ["https://www.nhs.uk/nhs-services/appointments-and-bookings/"]
}

Compose UI:

A simple card shows type, deadline, summary, and actions.

ElevatedCard(Modifier.fillMaxWidth()) {
&nbsp;&nbsp;&nbsp;Column(
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Modifier.padding(16.dp),
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;verticalArrangement = Arrangement.spacedBy(8.dp)
&nbsp;&nbsp;&nbsp;) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Text("Type:", fontWeight = FontWeight.SemiBold)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Text(r.type)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Text("Deadline:", fontWeight = FontWeight.SemiBold)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Text(r.deadline ?: "—")
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Divider()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Text("Summary", style = MaterialTheme.typography.titleMedium)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Text(r.summary)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Divider()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Text("Actions", style = MaterialTheme.typography.titleMedium)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;r.actions.forEach { a -> Text("• $a") }
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Divider()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Text("Citations", style = MaterialTheme.typography.titleMedium)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;r.citations.forEach { c -> Text(c) }
&nbsp;&nbsp;&nbsp;}
}

Results

  • A letter that takes ~5 minutes to interpret can be summarized in ~10 seconds.
  • In my tests on NHS/council letters, the app reliably pulled dates, locations, and required items.
  • Clear, low-friction UX reduced the cognitive load for non-technical users.

Lessons Learned

  • ML Kit OCR is surprisingly easy to set up in < 20 lines of Kotlin.
  • On-device AI ensures privacy (no letter leaves the phone).
  • Compose + CameraX makes UI binding smooth.

What’s next

  • IOS version with KMP/Swift.
  • Multi-lingual support.
  • More letter types.

Screenshots

Conclusion

LettersLens demonstrates how small, focused AI tools can make everyday tasks, such as opening a letter, less stressful and more actionable.

Try it

  • Code: LetterLens GitHub Repo

  • Demo video: YouTube Dem

Sign Up For Daily Newsletter

Be keep up! Get the latest breaking news delivered straight to your inbox.
By signing up, you agree to our Terms of Use and acknowledge the data practices in our Privacy Policy. You may unsubscribe at any time.
Share This Article
Facebook Twitter Email Print
Share
What do you think?
Love0
Sad0
Happy0
Sleepy0
Angry0
Dead0
Wink0
Previous Article What to Look for in a Commercial Electrician in Greenville, NC
Next Article It’s Time for Amazon Prime’s Best Early Big Deal Days Discounts on Smartwatches and Fitness Trackers
Leave a comment

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Stay Connected

248.1k Like
69.1k Follow
134k Pin
54.3k Follow

Latest News

China tightens rules for electric vehicle exports
News
1 in 5 regularly get news from TikTok: Analysis
News
From Sci-Fi to Reality: How Close Are We to Calling the Moon Home? | HackerNoon
Computing
Facebook and Instagram to charge UK users £3.99 a month for ad-free version
News

You Might also Like

Computing

From Sci-Fi to Reality: How Close Are We to Calling the Moon Home? | HackerNoon

8 Min Read
Computing

Ubuntu 25.10’s Move To Rust Coreutils Is Causing Major Breakage For Some Executables

3 Min Read
Computing

Xiaomi 17 debuts Snapdragon 8 Elite Gen 5 as CEO Lei Jun details XRING chip and high-end tech strategy · TechNode

5 Min Read
Computing

I’m a visual learner, and this flashcard app is a game-changer

8 Min Read
//

World of Software is your one-stop website for the latest tech news and updates, follow us now to get the news that matters to you.

Quick Link

  • Privacy Policy
  • Terms of use
  • Advertise
  • Contact

Topics

  • Computing
  • Software
  • Press Release
  • Trending

Sign Up for Our Newsletter

Subscribe to our newsletter to get our newest articles instantly!

World of SoftwareWorld of Software
Follow US
Copyright © All Rights Reserved. World of Software.
Welcome Back!

Sign in to your account

Lost your password?