Skip to content

Commit 11b5e4a

Browse files
committed
added: text recognition
1 parent c66577e commit 11b5e4a

13 files changed

Lines changed: 351 additions & 67 deletions

File tree

.idea/deploymentTargetSelector.xml

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/misc.xml

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/build.gradle

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ apply plugin: 'kotlin-android'
99
apply plugin: 'kotlin-kapt'
1010

1111
android {
12-
compileSdk 35
12+
compileSdkVersion 36
1313

1414
defaultConfig {
1515
applicationId "com.home.reader"
@@ -78,6 +78,7 @@ dependencies {
7878
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
7979

8080
implementation "com.google.accompanist:accompanist-permissions:0.32.0"
81+
implementation 'com.google.mlkit:text-recognition:16.0.1'
8182

8283
implementation 'androidx.navigation:navigation-compose:2.9.0'
8384
implementation 'androidx.compose.material3:material3:1.3.2'
@@ -101,7 +102,7 @@ dependencies {
101102
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3"
102103

103104
//For REST api
104-
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.14'
105+
implementation 'com.squareup.okhttp3:okhttp:5.1.0'
105106
implementation 'com.google.code.gson:gson:2.11.0'
106107

107108
implementation "org.jetbrains.kotlin:kotlin-stdlib:2.1.20"

app/src/main/AndroidManifest.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@
99
<uses-permission android:name="android.permission.INTERNET" />
1010
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
1111

12+
<queries>
13+
<intent>
14+
<action android:name="android.intent.action.PROCESS_TEXT" />
15+
<data android:mimeType="text/plain" />
16+
</intent>
17+
</queries>
18+
1219
<application
1320
android:name=".ReaderApplication"
1421
android:icon="@mipmap/ic_launcher"

app/src/main/java/com/home/reader/ui/catalogue/component/CatalogueIssueItem.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ fun CatalogueIssueItem(
109109
lineHeight = 14.sp,
110110
modifier = Modifier.width(COVER_WIDTH)
111111
)
112+
112113
}
113114
}
114115
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package com.home.reader.ui.common.component
2+
3+
import androidx.compose.foundation.layout.Box
4+
import androidx.compose.foundation.layout.Column
5+
import androidx.compose.material.icons.Icons
6+
import androidx.compose.material.icons.filled.Warning
7+
import androidx.compose.material3.Button
8+
import androidx.compose.material3.Icon
9+
import androidx.compose.material3.MaterialTheme
10+
import androidx.compose.material3.Text
11+
import androidx.compose.runtime.Composable
12+
import androidx.compose.ui.Alignment
13+
import androidx.compose.ui.Modifier
14+
import androidx.compose.ui.tooling.preview.Preview
15+
16+
@Composable
17+
fun LoadingFailAlert(
18+
onRefreshClick: () -> Unit,
19+
modifier: Modifier
20+
) {
21+
Column(modifier = modifier) {
22+
Icon(
23+
imageVector = Icons.Default.Warning,
24+
contentDescription = "Error",
25+
tint = MaterialTheme.colorScheme.error,
26+
modifier = Modifier.align(Alignment.CenterHorizontally)
27+
)
28+
Button(
29+
onClick = onRefreshClick
30+
) {
31+
Text("Refresh")
32+
}
33+
}
34+
}
35+
36+
@Preview
37+
@Composable
38+
private fun Preview() {
39+
Box {
40+
LoadingFailAlert(
41+
onRefreshClick = {},
42+
modifier = Modifier.align(Alignment.Center)
43+
)
44+
}
45+
}

app/src/main/java/com/home/reader/ui/common/component/ProgressAsyncImage.kt

Lines changed: 75 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,106 @@
11
package com.home.reader.ui.common.component
22

3+
import android.util.Log
34
import androidx.compose.foundation.Image
5+
import androidx.compose.foundation.horizontalScroll
46
import androidx.compose.foundation.layout.Box
5-
import androidx.compose.foundation.layout.Column
6-
import androidx.compose.material.icons.Icons
7-
import androidx.compose.material.icons.filled.Warning
8-
import androidx.compose.material3.Button
7+
import androidx.compose.foundation.rememberScrollState
8+
import androidx.compose.foundation.verticalScroll
99
import androidx.compose.material3.CircularProgressIndicator
10-
import androidx.compose.material3.Icon
11-
import androidx.compose.material3.MaterialTheme
12-
import androidx.compose.material3.Text
1310
import androidx.compose.runtime.Composable
11+
import androidx.compose.runtime.LaunchedEffect
1412
import androidx.compose.runtime.getValue
1513
import androidx.compose.runtime.key
1614
import androidx.compose.runtime.mutableIntStateOf
15+
import androidx.compose.runtime.mutableStateOf
1716
import androidx.compose.runtime.remember
17+
import androidx.compose.runtime.rememberCoroutineScope
1818
import androidx.compose.runtime.setValue
19+
import androidx.compose.runtime.snapshotFlow
1920
import androidx.compose.ui.Alignment
2021
import androidx.compose.ui.Modifier
22+
import androidx.compose.ui.draw.drawWithContent
2123
import androidx.compose.ui.graphics.FilterQuality
24+
import androidx.compose.ui.graphics.layer.drawLayer
25+
import androidx.compose.ui.graphics.rememberGraphicsLayer
2226
import androidx.compose.ui.layout.ContentScale
23-
import androidx.compose.ui.tooling.preview.Preview
27+
import androidx.compose.ui.zIndex
2428
import androidx.core.util.Supplier
2529
import coil.compose.AsyncImagePainter
2630
import coil.compose.rememberAsyncImagePainter
2731
import coil.request.ImageRequest
32+
import com.google.mlkit.vision.text.TextRecognizer
33+
import com.home.reader.utils.RecognizedData
34+
import com.home.reader.utils.process
35+
import kotlinx.coroutines.FlowPreview
36+
import kotlinx.coroutines.flow.debounce
37+
import kotlinx.coroutines.launch
38+
import kotlin.time.Duration.Companion.milliseconds
2839

40+
@OptIn(FlowPreview::class)
2941
@Composable
3042
fun ProgressAsyncImage(
3143
model: Supplier<ImageRequest>,
3244
contentDescription: String?,
3345
contentScale: ContentScale,
46+
recognizer: TextRecognizer,
3447
modifier: Modifier = Modifier
3548
) {
3649
var retryCounter by remember { mutableIntStateOf(0) }
50+
var positions by remember { mutableStateOf(listOf<RecognizedData>()) }
51+
val graphicsLayer = rememberGraphicsLayer()
3752

38-
Box {
39-
key(retryCounter) {
40-
val painter = rememberAsyncImagePainter(
41-
model = model.get(),
42-
filterQuality = FilterQuality.High
43-
)
53+
val scope = rememberCoroutineScope()
54+
55+
val painter = rememberAsyncImagePainter(
56+
model = model.get(),
57+
filterQuality = FilterQuality.High
58+
)
59+
60+
val verticalScrollState = rememberScrollState()
61+
val horizontalScrollState = rememberScrollState()
62+
63+
var recognize by remember { mutableStateOf(false) }
64+
65+
LaunchedEffect(graphicsLayer) {
66+
snapshotFlow { graphicsLayer }
67+
.debounce(300.milliseconds)
68+
.collect { layer ->
69+
70+
scope.launch {
71+
recognizer.process(graphicsLayer) {
72+
positions = it
73+
recognize = false
74+
}
75+
}
76+
}
77+
}
78+
79+
Box(
80+
modifier = Modifier.drawWithContent {
81+
graphicsLayer.record {
82+
this@drawWithContent.drawContent()
83+
}
84+
85+
drawLayer(graphicsLayer)
86+
}
87+
) {
4488

89+
for (position in positions) {
90+
RecognizedText(position)
91+
}
92+
93+
key(retryCounter) {
4594
val state = painter.state
4695

4796
Image(
4897
painter = painter,
4998
contentDescription = contentDescription,
5099
contentScale = contentScale,
51100
modifier = modifier
101+
.zIndex(-1f)
102+
.verticalScroll(verticalScrollState)
103+
.horizontalScroll(horizontalScrollState)
52104
)
53105

54106
if (state is AsyncImagePainter.State.Loading) {
@@ -57,44 +109,18 @@ fun ProgressAsyncImage(
57109
)
58110
}
59111

112+
if (state is AsyncImagePainter.State.Success) {
113+
Log.i("Recognition trigger", "Recognized: $recognize")
114+
recognize = true
115+
}
116+
60117
if (state is AsyncImagePainter.State.Error) {
61-
Column(modifier = Modifier.align(Alignment.Center)) {
62-
Icon(
63-
imageVector = Icons.Default.Warning,
64-
contentDescription = "Error",
65-
tint = MaterialTheme.colorScheme.error,
66-
modifier = Modifier.align(Alignment.CenterHorizontally)
67-
)
68-
Button(
69-
onClick = { retryCounter = retryCounter + 1 }
70-
) {
71-
Text("Refresh")
72-
}
73-
}
118+
LoadingFailAlert(
119+
modifier = Modifier.align(Alignment.Center),
120+
onRefreshClick = { retryCounter = retryCounter + 1 }
121+
)
74122
}
75123
}
76124

77125
}
78126
}
79-
80-
@Preview
81-
@Composable
82-
private fun Preview() {
83-
var retryCounter by remember { mutableIntStateOf(0) }
84-
85-
Box {
86-
Column(modifier = Modifier.align(Alignment.Center)) {
87-
Icon(
88-
imageVector = Icons.Default.Warning,
89-
contentDescription = "Error",
90-
tint = MaterialTheme.colorScheme.error,
91-
modifier = Modifier.align(Alignment.CenterHorizontally)
92-
)
93-
Button(
94-
onClick = { retryCounter = retryCounter + 1 }
95-
) {
96-
Text("Refresh")
97-
}
98-
}
99-
}
100-
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package com.home.reader.ui.common.component
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.layout.height
5+
import androidx.compose.foundation.layout.offset
6+
import androidx.compose.foundation.layout.width
7+
import androidx.compose.foundation.text.selection.SelectionContainer
8+
import androidx.compose.material3.Text
9+
import androidx.compose.runtime.Composable
10+
import androidx.compose.runtime.CompositionLocalProvider
11+
import androidx.compose.ui.Modifier
12+
import androidx.compose.ui.graphics.Color
13+
import androidx.compose.ui.platform.LocalClipboard
14+
import androidx.compose.ui.platform.LocalContext
15+
import androidx.compose.ui.platform.LocalDensity
16+
import androidx.compose.ui.platform.LocalTextToolbar
17+
import androidx.compose.ui.text.TextStyle
18+
import androidx.compose.ui.text.font.FontFamily
19+
import androidx.compose.ui.text.rememberTextMeasurer
20+
import androidx.compose.ui.unit.Dp
21+
import androidx.compose.ui.unit.sp
22+
import com.home.reader.utils.RecognizedData
23+
24+
private data class RelativeRect(
25+
val x: Dp,
26+
val y: Dp,
27+
val width: Dp,
28+
val height: Dp
29+
)
30+
31+
@Composable
32+
fun RecognizedText(data: RecognizedData) {
33+
val ctx = LocalContext.current
34+
val textMeasurer = rememberTextMeasurer()
35+
val baseFontSize = 6
36+
37+
val relativeBounds = with(LocalDensity.current) {
38+
RelativeRect(
39+
x = data.rect.left.toDp(),
40+
y = data.rect.top.toDp(),
41+
width = data.rect.width().toDp(),
42+
height = data.rect.height().toDp()
43+
)
44+
}
45+
46+
CompositionLocalProvider(
47+
LocalTextToolbar provides SelectedTextToolbar(ctx, LocalClipboard.current)
48+
) {
49+
SelectionContainer(
50+
modifier = Modifier
51+
.width(relativeBounds.width)
52+
.height(relativeBounds.height)
53+
.offset(relativeBounds.x, relativeBounds.y)
54+
.background(color = Color.Transparent)
55+
) {
56+
val measureResult = textMeasurer.measure(
57+
text = data.text,
58+
style = TextStyle(
59+
color = Color.Black,
60+
fontSize = baseFontSize.sp,
61+
fontFamily = FontFamily.SansSerif
62+
),
63+
maxLines = data.lines
64+
)
65+
66+
val fontSize = ((data.rect.width() * baseFontSize) / measureResult.size.width).sp
67+
Text(
68+
text = data.text,
69+
lineHeight = fontSize,
70+
fontSize = fontSize,
71+
color = Color.Transparent,
72+
fontFamily = FontFamily.SansSerif,
73+
softWrap = false
74+
)
75+
}
76+
}
77+
78+
}

0 commit comments

Comments
 (0)