@june24/expo-pdf-reader 0.1.17 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -2,11 +2,20 @@ package expo.modules.pdfreader
|
|
|
2
2
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
4
|
import android.content.Context
|
|
5
|
-
import android.graphics
|
|
5
|
+
import android.graphics.Bitmap
|
|
6
|
+
import android.graphics.Canvas
|
|
7
|
+
import android.graphics.Color
|
|
8
|
+
import android.graphics.Paint
|
|
9
|
+
import android.graphics.Path
|
|
10
|
+
import android.graphics.PointF
|
|
11
|
+
import android.graphics.RectF
|
|
12
|
+
import android.graphics.Typeface
|
|
13
|
+
import android.graphics.drawable.BitmapDrawable
|
|
6
14
|
import android.graphics.pdf.PdfRenderer
|
|
7
15
|
import android.os.ParcelFileDescriptor
|
|
8
|
-
import android.
|
|
9
|
-
import android.view
|
|
16
|
+
import android.view.MotionEvent
|
|
17
|
+
import android.view.View
|
|
18
|
+
import android.view.ViewGroup
|
|
10
19
|
import android.widget.FrameLayout
|
|
11
20
|
import android.widget.ImageView
|
|
12
21
|
import android.widget.LinearLayout
|
|
@@ -18,36 +27,47 @@ import java.io.File
|
|
|
18
27
|
import java.net.URL
|
|
19
28
|
import kotlinx.coroutines.*
|
|
20
29
|
|
|
21
|
-
|
|
30
|
+
/** Annotation type string matching iOS. */
|
|
22
31
|
enum class AnnotationTool { PEN, HIGHLIGHTER, TEXT, NOTE, ERASER, LINE }
|
|
23
32
|
|
|
24
|
-
|
|
33
|
+
/** Single annotation (pen/highlighter/line/text/note). */
|
|
25
34
|
data class Annotation(
|
|
26
|
-
val type: String,
|
|
35
|
+
val type: String,
|
|
27
36
|
val bounds: RectF,
|
|
28
37
|
var color: Int,
|
|
29
|
-
var strokeWidth: Float
|
|
30
|
-
val paths: MutableList<MutableList<PointF
|
|
31
|
-
var contents: String
|
|
32
|
-
var fontSize: Float
|
|
33
|
-
var bold: Boolean
|
|
34
|
-
var italic: Boolean
|
|
38
|
+
var strokeWidth: Float,
|
|
39
|
+
val paths: MutableList<MutableList<PointF>>,
|
|
40
|
+
var contents: String,
|
|
41
|
+
var fontSize: Float,
|
|
42
|
+
var bold: Boolean,
|
|
43
|
+
var italic: Boolean
|
|
35
44
|
)
|
|
36
45
|
|
|
37
|
-
|
|
46
|
+
/** Undo/redo entry. */
|
|
47
|
+
data class UndoEntry(val annotation: Annotation, val pageIndex: Int)
|
|
38
48
|
|
|
49
|
+
/** ScrollView that respects currentTool: scroll only when no tool selected. */
|
|
39
50
|
@SuppressLint("ViewConstructor")
|
|
40
|
-
class
|
|
51
|
+
private class ToolAwareScrollView(
|
|
52
|
+
context: Context,
|
|
53
|
+
private val isScrollEnabledProvider: () -> Boolean
|
|
54
|
+
) : ScrollView(context) {
|
|
55
|
+
override fun onTouchEvent(ev: MotionEvent): Boolean {
|
|
56
|
+
if (!isScrollEnabledProvider()) return false
|
|
57
|
+
return super.onTouchEvent(ev)
|
|
58
|
+
}
|
|
41
59
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (!scrollEnabled) return false
|
|
46
|
-
return super.onInterceptTouchEvent(ev)
|
|
47
|
-
}
|
|
60
|
+
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
|
|
61
|
+
if (!isScrollEnabledProvider()) return false
|
|
62
|
+
return super.onInterceptTouchEvent(ev)
|
|
48
63
|
}
|
|
64
|
+
}
|
|
49
65
|
|
|
50
|
-
|
|
66
|
+
/** Android PDF viewer with full annotation support matching iOS. */
|
|
67
|
+
@SuppressLint("ViewConstructor")
|
|
68
|
+
class ExpoPdfReaderView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
|
|
69
|
+
|
|
70
|
+
private val scrollView = ToolAwareScrollView(context) { currentTool == null }
|
|
51
71
|
private val container = LinearLayout(context).apply {
|
|
52
72
|
orientation = LinearLayout.VERTICAL
|
|
53
73
|
}
|
|
@@ -56,9 +76,11 @@ class ExpoPdfReaderView(context: Context, appContext: AppContext) : ExpoView(con
|
|
|
56
76
|
private var fileDescriptor: ParcelFileDescriptor? = null
|
|
57
77
|
private val scope = CoroutineScope(Dispatchers.Main + Job())
|
|
58
78
|
|
|
59
|
-
private var
|
|
60
|
-
private var initialPage: Int = 0
|
|
79
|
+
private var initialPage = 0
|
|
61
80
|
private var pdfUrl: String? = null
|
|
81
|
+
private var renderPdfRetryCount = 0
|
|
82
|
+
private val maxRenderPdfRetries = 30
|
|
83
|
+
private var displayMode = "continuous"
|
|
62
84
|
|
|
63
85
|
private val onPageChange = ViewEvent<Map<String, Any>>("onPageChange", this, null)
|
|
64
86
|
private val onAnnotationChange = ViewEvent<Map<String, Any>>("onAnnotationChange", this, null)
|
|
@@ -66,313 +88,275 @@ class ExpoPdfReaderView(context: Context, appContext: AppContext) : ExpoView(con
|
|
|
66
88
|
private val onNotePress = ViewEvent<Map<String, Any>>("onNotePress", this, null)
|
|
67
89
|
private val onTextPress = ViewEvent<Map<String, Any>>("onTextPress", this, null)
|
|
68
90
|
|
|
69
|
-
// Per-page annotations (same structure as iOS, but we store in memory)
|
|
70
|
-
private var pageAnnotations: MutableList<MutableList<Annotation>> = mutableListOf()
|
|
71
|
-
private var appliedAnnotationsFingerprint: String? = null
|
|
72
|
-
private var pendingAnnotations: List<Map<String, Any>>? = null
|
|
73
|
-
|
|
74
|
-
private var currentTool: AnnotationTool? = null
|
|
75
|
-
private var strokeColor: Int = Color.RED
|
|
76
|
-
private var strokeWidth: Float = 2f
|
|
77
|
-
private var textContent: String = ""
|
|
78
|
-
private var textColor: Int = Color.BLACK
|
|
79
|
-
private var textFontSize: Float = 14f
|
|
80
|
-
private var textBold: Boolean = false
|
|
81
|
-
private var textItalic: Boolean = false
|
|
82
|
-
private var noteColor: Int = Color.YELLOW
|
|
83
|
-
|
|
84
|
-
private val undoStack = mutableListOf<UndoEntry>()
|
|
85
|
-
private val redoStack = mutableListOf<UndoEntry>()
|
|
86
|
-
|
|
87
91
|
private var pageHeights = mutableListOf<Int>()
|
|
88
92
|
private var pageWidthsPdf = mutableListOf<Float>()
|
|
89
93
|
private var pageHeightsPdf = mutableListOf<Float>()
|
|
90
94
|
private var pageViews = mutableListOf<ImageView?>()
|
|
91
95
|
private var overlayViews = mutableListOf<AnnotationOverlayView?>()
|
|
92
|
-
private var renderGeneration = 0
|
|
93
96
|
private var lastEmittedPageIndex = -1
|
|
94
97
|
private var totalPageCount = 0
|
|
95
98
|
|
|
96
|
-
|
|
99
|
+
private val renderWindowBefore = 8
|
|
100
|
+
private val renderWindowAfter = 16
|
|
101
|
+
|
|
102
|
+
// Annotation state
|
|
103
|
+
private val pageAnnotations = mutableListOf<MutableList<Annotation>>()
|
|
104
|
+
private var currentTool: AnnotationTool? = null
|
|
105
|
+
private var strokeColor = Color.RED
|
|
106
|
+
private var strokeWidth = 2f
|
|
107
|
+
private var textContent = ""
|
|
108
|
+
private var textColor = Color.BLACK
|
|
109
|
+
private var textFontSize = 14f
|
|
110
|
+
private var textBold = false
|
|
111
|
+
private var textItalic = false
|
|
112
|
+
private var noteColor = Color.YELLOW
|
|
113
|
+
|
|
97
114
|
private var currentPath: Path? = null
|
|
98
|
-
private
|
|
99
|
-
private var activePageIndex
|
|
115
|
+
private val currentPathPoints = mutableListOf<PointF>()
|
|
116
|
+
private var activePageIndex = -1
|
|
100
117
|
private var startPoint: PointF? = null
|
|
101
118
|
private var draggingAnnotation: Annotation? = null
|
|
102
|
-
private var draggingPageIndex
|
|
119
|
+
private var draggingPageIndex = -1
|
|
103
120
|
private var dragStartBounds: RectF? = null
|
|
104
121
|
private var dragStartTouch: PointF? = null
|
|
105
122
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
123
|
+
private val undoStack = mutableListOf<UndoEntry>()
|
|
124
|
+
private val redoStack = mutableListOf<UndoEntry>()
|
|
125
|
+
private var appliedAnnotationsFingerprint: String? = null
|
|
126
|
+
private var pendingAnnotations: List<Map<String, Any>>? = null
|
|
127
|
+
private var renderJob: Job? = null
|
|
128
|
+
|
|
129
|
+
inner class AnnotationOverlayView(
|
|
130
|
+
context: Context,
|
|
131
|
+
val pageIndex: Int
|
|
132
|
+
) : View(context) {
|
|
133
|
+
|
|
134
|
+
private val pathPaint = Paint().apply {
|
|
135
|
+
isAntiAlias = true
|
|
112
136
|
style = Paint.Style.STROKE
|
|
113
|
-
strokeCap = Paint.Cap.ROUND
|
|
114
137
|
strokeJoin = Paint.Join.ROUND
|
|
138
|
+
strokeCap = Paint.Cap.ROUND
|
|
139
|
+
}
|
|
140
|
+
private val textPaint = Paint().apply {
|
|
141
|
+
isAntiAlias = true
|
|
142
|
+
textAlign = Paint.Align.LEFT
|
|
143
|
+
}
|
|
144
|
+
private val noteFillPaint = Paint().apply {
|
|
145
|
+
style = Paint.Style.FILL
|
|
146
|
+
isAntiAlias = true
|
|
147
|
+
}
|
|
148
|
+
private val noteStrokePaint = Paint().apply {
|
|
149
|
+
style = Paint.Style.STROKE
|
|
115
150
|
isAntiAlias = true
|
|
151
|
+
strokeWidth = 1f
|
|
116
152
|
}
|
|
117
|
-
private val textPaint = TextPaint(Paint.ANTI_ALIAS_FLAG)
|
|
118
|
-
private var previewPath: Path? = null
|
|
119
|
-
private var previewPaint: Paint? = null
|
|
120
153
|
|
|
121
|
-
|
|
122
|
-
val
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
// Drawing preview (current stroke)
|
|
127
|
-
if (activePageIndex == pageIndex && currentPath != null && previewPaint != null) {
|
|
128
|
-
canvas.drawPath(currentPath!!, previewPaint!!)
|
|
129
|
-
}
|
|
154
|
+
fun scale(): Float {
|
|
155
|
+
val pw = pageWidthsPdf.getOrNull(pageIndex) ?: 1f
|
|
156
|
+
if (pw <= 0f) return 1f
|
|
157
|
+
return width / pw
|
|
130
158
|
}
|
|
131
159
|
|
|
132
|
-
|
|
160
|
+
override fun onDraw(canvas: Canvas) {
|
|
161
|
+
super.onDraw(canvas)
|
|
133
162
|
val s = scale()
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
163
|
+
if (s <= 0f) return
|
|
164
|
+
|
|
165
|
+
for (ann in getAnnotationsForPage(pageIndex)) {
|
|
166
|
+
val left = ann.bounds.left * s
|
|
167
|
+
val top = ann.bounds.top * s
|
|
168
|
+
val right = ann.bounds.right * s
|
|
169
|
+
val bottom = ann.bounds.bottom * s
|
|
170
|
+
when (ann.type) {
|
|
171
|
+
"pen", "highlighter", "line" -> {
|
|
172
|
+
pathPaint.color = ann.color
|
|
173
|
+
pathPaint.strokeWidth = ann.strokeWidth * s
|
|
174
|
+
pathPaint.alpha = if (ann.type == "highlighter") 102 else 255
|
|
175
|
+
for (pathPoints in ann.paths) {
|
|
176
|
+
if (pathPoints.size < 2) continue
|
|
177
|
+
val path = Path()
|
|
178
|
+
path.moveTo(pathPoints[0].x * s, pathPoints[0].y * s)
|
|
179
|
+
for (i in 1 until pathPoints.size) {
|
|
180
|
+
path.lineTo(pathPoints[i].x * s, pathPoints[i].y * s)
|
|
181
|
+
}
|
|
182
|
+
canvas.drawPath(path, pathPaint)
|
|
147
183
|
}
|
|
148
|
-
canvas.drawPath(path, pathPaint)
|
|
149
184
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
ann.bold && ann.italic -> Typeface.BOLD_ITALIC
|
|
159
|
-
ann.bold -> Typeface.BOLD
|
|
160
|
-
ann.italic -> Typeface.ITALIC
|
|
161
|
-
else -> Typeface.NORMAL
|
|
185
|
+
"text" -> {
|
|
186
|
+
textPaint.color = ann.color
|
|
187
|
+
textPaint.textSize = ann.fontSize * s
|
|
188
|
+
textPaint.typeface = when {
|
|
189
|
+
ann.bold && ann.italic -> Typeface.create(Typeface.DEFAULT, Typeface.BOLD_ITALIC)
|
|
190
|
+
ann.bold -> Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
|
|
191
|
+
ann.italic -> Typeface.create(Typeface.DEFAULT, Typeface.ITALIC)
|
|
192
|
+
else -> Typeface.DEFAULT
|
|
162
193
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
canvas.drawRect(r, pathPaint)
|
|
178
|
-
pathPaint.style = Paint.Style.STROKE
|
|
179
|
-
textPaint.color = Color.BLACK
|
|
180
|
-
textPaint.textSize = 10f * s
|
|
181
|
-
canvas.drawText(ann.contents.ifEmpty { " " }.take(20), r.left + 4, r.top + 12f * s, textPaint)
|
|
194
|
+
val text = ann.contents.ifEmpty { " " }
|
|
195
|
+
canvas.drawText(text, left + 4f, top + ann.fontSize * s - 2f, textPaint)
|
|
196
|
+
}
|
|
197
|
+
"note" -> {
|
|
198
|
+
noteFillPaint.color = ann.color
|
|
199
|
+
noteStrokePaint.color = Color.BLACK
|
|
200
|
+
canvas.drawRect(left, top, right, bottom, noteFillPaint)
|
|
201
|
+
canvas.drawRect(left, top, right, bottom, noteStrokePaint)
|
|
202
|
+
textPaint.color = Color.BLACK
|
|
203
|
+
textPaint.textSize = 10f * s
|
|
204
|
+
textPaint.typeface = Typeface.DEFAULT
|
|
205
|
+
val text = ann.contents.ifEmpty { " " }
|
|
206
|
+
canvas.drawText(text, left + 4f, top + 12f * s, textPaint)
|
|
207
|
+
}
|
|
182
208
|
}
|
|
183
209
|
}
|
|
184
|
-
}
|
|
185
210
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
211
|
+
// Preview path for current stroke
|
|
212
|
+
if (activePageIndex == pageIndex && currentPath != null && currentPathPoints.isNotEmpty()) {
|
|
213
|
+
pathPaint.color = strokeColor
|
|
214
|
+
pathPaint.strokeWidth = strokeWidth * s
|
|
215
|
+
pathPaint.alpha = if (currentTool == AnnotationTool.HIGHLIGHTER) 102 else 255
|
|
216
|
+
val path = Path()
|
|
217
|
+
path.moveTo(currentPathPoints[0].x * s, currentPathPoints[0].y * s)
|
|
218
|
+
for (i in 1 until currentPathPoints.size) {
|
|
219
|
+
path.lineTo(currentPathPoints[i].x * s, currentPathPoints[i].y * s)
|
|
220
|
+
}
|
|
221
|
+
canvas.drawPath(path, pathPaint)
|
|
222
|
+
}
|
|
190
223
|
}
|
|
191
224
|
|
|
225
|
+
@SuppressLint("ClickableViewAccessibility")
|
|
192
226
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
val
|
|
227
|
+
val s = scale()
|
|
228
|
+
if (s <= 0f) return false
|
|
229
|
+
val pdfX = event.x / s
|
|
230
|
+
val pdfY = event.y / s
|
|
231
|
+
|
|
196
232
|
if (currentTool != null) {
|
|
197
|
-
return handleToolTouch(event,
|
|
233
|
+
return handleToolTouch(event, pdfX, pdfY, s)
|
|
198
234
|
}
|
|
199
|
-
return handleTapAndDrag(event,
|
|
235
|
+
return handleTapAndDrag(event, pdfX, pdfY, s)
|
|
200
236
|
}
|
|
201
237
|
|
|
202
|
-
private fun handleToolTouch(event: MotionEvent,
|
|
203
|
-
|
|
204
|
-
when (event.action) {
|
|
238
|
+
private fun handleToolTouch(event: MotionEvent, pdfX: Float, pdfY: Float, scale: Float): Boolean {
|
|
239
|
+
when (event.actionMasked) {
|
|
205
240
|
MotionEvent.ACTION_DOWN -> {
|
|
241
|
+
val tool = currentTool ?: return false
|
|
242
|
+
activePageIndex = pageIndex
|
|
243
|
+
startPoint = PointF(pdfX, pdfY)
|
|
206
244
|
when (tool) {
|
|
207
245
|
AnnotationTool.PEN, AnnotationTool.HIGHLIGHTER, AnnotationTool.LINE -> {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
213
|
-
style = Paint.Style.STROKE
|
|
214
|
-
strokeCap = Paint.Cap.ROUND
|
|
215
|
-
strokeJoin = Paint.Join.ROUND
|
|
216
|
-
color = strokeColor
|
|
217
|
-
strokeWidth = this@ExpoPdfReaderView.strokeWidth
|
|
218
|
-
if (tool == AnnotationTool.HIGHLIGHTER) alpha = (0.4f * 255).toInt()
|
|
219
|
-
}
|
|
220
|
-
setPreview(currentPath, paint)
|
|
221
|
-
invalidate()
|
|
246
|
+
currentPath = Path()
|
|
247
|
+
currentPathPoints.clear()
|
|
248
|
+
currentPathPoints.add(PointF(pdfX, pdfY))
|
|
249
|
+
currentPath?.moveTo(pdfX, pdfY)
|
|
222
250
|
}
|
|
223
251
|
AnnotationTool.TEXT, AnnotationTool.NOTE -> {
|
|
224
|
-
|
|
225
|
-
addInstantAnnotation(pageIndex, x / s, y / s, tool)
|
|
252
|
+
addInstantAnnotation(pageIndex, pdfX, pdfY, tool)
|
|
226
253
|
}
|
|
227
254
|
AnnotationTool.ERASER -> {
|
|
228
|
-
|
|
229
|
-
eraseAt(pageIndex, x / s, y / s)
|
|
255
|
+
eraseAt(pageIndex, pdfX, pdfY)
|
|
230
256
|
}
|
|
231
257
|
}
|
|
258
|
+
invalidate()
|
|
232
259
|
return true
|
|
233
260
|
}
|
|
234
261
|
MotionEvent.ACTION_MOVE -> {
|
|
262
|
+
val tool = currentTool ?: return true
|
|
235
263
|
when (tool) {
|
|
236
264
|
AnnotationTool.PEN, AnnotationTool.HIGHLIGHTER -> {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
currentPathPoints!!.add(PointF(x, y))
|
|
240
|
-
invalidate()
|
|
241
|
-
}
|
|
265
|
+
currentPathPoints.add(PointF(pdfX, pdfY))
|
|
266
|
+
currentPath?.lineTo(pdfX, pdfY)
|
|
242
267
|
}
|
|
243
268
|
AnnotationTool.LINE -> {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
invalidate()
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
AnnotationTool.ERASER -> {
|
|
255
|
-
val s = scale()
|
|
256
|
-
eraseAt(pageIndex, x / s, y / s)
|
|
269
|
+
val start = startPoint ?: return true
|
|
270
|
+
currentPathPoints.clear()
|
|
271
|
+
currentPathPoints.add(PointF(start.x, start.y))
|
|
272
|
+
currentPathPoints.add(PointF(pdfX, pdfY))
|
|
273
|
+
currentPath?.reset()
|
|
274
|
+
currentPath?.moveTo(start.x, start.y)
|
|
275
|
+
currentPath?.lineTo(pdfX, pdfY)
|
|
257
276
|
}
|
|
277
|
+
AnnotationTool.ERASER -> eraseAt(pageIndex, pdfX, pdfY)
|
|
258
278
|
else -> {}
|
|
259
279
|
}
|
|
280
|
+
invalidate()
|
|
260
281
|
return true
|
|
261
282
|
}
|
|
262
283
|
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
|
263
|
-
when (
|
|
284
|
+
when (currentTool) {
|
|
264
285
|
AnnotationTool.PEN, AnnotationTool.HIGHLIGHTER, AnnotationTool.LINE -> {
|
|
265
|
-
if (
|
|
266
|
-
commitStroke(
|
|
286
|
+
if (currentPathPoints.size >= 2) {
|
|
287
|
+
commitStroke(currentTool!!)
|
|
267
288
|
}
|
|
268
|
-
setPreview(null, null)
|
|
269
289
|
currentPath = null
|
|
270
|
-
currentPathPoints
|
|
271
|
-
activePageIndex = -1
|
|
272
|
-
startPoint = null
|
|
273
|
-
invalidate()
|
|
290
|
+
currentPathPoints.clear()
|
|
274
291
|
}
|
|
275
292
|
else -> {}
|
|
276
293
|
}
|
|
294
|
+
activePageIndex = -1
|
|
295
|
+
startPoint = null
|
|
296
|
+
invalidate()
|
|
277
297
|
return true
|
|
278
298
|
}
|
|
279
299
|
}
|
|
280
300
|
return false
|
|
281
301
|
}
|
|
282
302
|
|
|
283
|
-
private fun
|
|
284
|
-
|
|
285
|
-
if (tool == AnnotationTool.LINE && startPoint != null && pts.size >= 1) {
|
|
286
|
-
pts = mutableListOf(PointF(startPoint!!.x, startPoint!!.y), pts.last())
|
|
287
|
-
}
|
|
288
|
-
if (pts.size < 2) return
|
|
289
|
-
val s = scale()
|
|
290
|
-
val ptsPdf = pts.map { PointF(it.x / s, it.y / s) }.toMutableList()
|
|
291
|
-
val pathBounds = RectF()
|
|
292
|
-
val path = Path().apply {
|
|
293
|
-
moveTo(ptsPdf[0].x, ptsPdf[0].y)
|
|
294
|
-
for (i in 1 until ptsPdf.size) lineTo(ptsPdf[i].x, ptsPdf[i].y)
|
|
295
|
-
}
|
|
296
|
-
path.computeBounds(pathBounds, true)
|
|
297
|
-
pathBounds.inset(-strokeWidth / s, -strokeWidth / s)
|
|
298
|
-
val color = if (tool == AnnotationTool.HIGHLIGHTER) Color.argb(102, Color.red(strokeColor), Color.green(strokeColor), Color.blue(strokeColor)) else strokeColor
|
|
299
|
-
val ann = Annotation(
|
|
300
|
-
type = when (tool) {
|
|
301
|
-
AnnotationTool.PEN -> "pen"
|
|
302
|
-
AnnotationTool.HIGHLIGHTER -> "highlighter"
|
|
303
|
-
AnnotationTool.LINE -> "line"
|
|
304
|
-
else -> "pen"
|
|
305
|
-
},
|
|
306
|
-
bounds = RectF(pathBounds),
|
|
307
|
-
color = color,
|
|
308
|
-
strokeWidth = strokeWidth / s,
|
|
309
|
-
paths = mutableListOf(ptsPdf)
|
|
310
|
-
)
|
|
311
|
-
addAnnotationToPage(pageIndex, ann)
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
private fun handleTapAndDrag(event: MotionEvent, x: Float, y: Float): Boolean {
|
|
315
|
-
val s = scale()
|
|
316
|
-
val pdfX = x / s
|
|
317
|
-
val pdfY = y / s
|
|
318
|
-
val anns = getAnnotationsForPage(pageIndex)
|
|
319
|
-
when (event.action) {
|
|
303
|
+
private fun handleTapAndDrag(event: MotionEvent, pdfX: Float, pdfY: Float, scale: Float): Boolean {
|
|
304
|
+
when (event.actionMasked) {
|
|
320
305
|
MotionEvent.ACTION_DOWN -> {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
"note" -> {
|
|
339
|
-
onNotePress(mapOf(
|
|
340
|
-
"page" to pageIndex,
|
|
341
|
-
"index" to idx,
|
|
342
|
-
"bounds" to mapOf(
|
|
343
|
-
"x" to ann.bounds.left,
|
|
344
|
-
"y" to ann.bounds.top,
|
|
345
|
-
"width" to ann.bounds.width(),
|
|
346
|
-
"height" to ann.bounds.height()
|
|
347
|
-
),
|
|
348
|
-
"contents" to (ann.contents)
|
|
349
|
-
))
|
|
350
|
-
return true
|
|
351
|
-
}
|
|
352
|
-
else -> {}
|
|
353
|
-
}
|
|
354
|
-
draggingAnnotation = ann
|
|
355
|
-
draggingPageIndex = pageIndex
|
|
356
|
-
dragStartBounds = RectF(ann.bounds)
|
|
357
|
-
dragStartTouch = PointF(x, y)
|
|
306
|
+
val list = getAnnotationsForPage(pageIndex)
|
|
307
|
+
for (i in list.indices.reversed()) {
|
|
308
|
+
val ann = list[i]
|
|
309
|
+
if (!ann.bounds.contains(pdfX, pdfY)) continue
|
|
310
|
+
if (ann.type == "text") {
|
|
311
|
+
onTextPress(mapOf(
|
|
312
|
+
"page" to pageIndex,
|
|
313
|
+
"index" to i,
|
|
314
|
+
"bounds" to mapOf(
|
|
315
|
+
"x" to ann.bounds.left,
|
|
316
|
+
"y" to ann.bounds.top,
|
|
317
|
+
"width" to ann.bounds.width(),
|
|
318
|
+
"height" to ann.bounds.height()
|
|
319
|
+
),
|
|
320
|
+
"contents" to (ann.contents.ifEmpty { " " })
|
|
321
|
+
))
|
|
358
322
|
return true
|
|
359
323
|
}
|
|
324
|
+
if (ann.type == "note") {
|
|
325
|
+
onNotePress(mapOf(
|
|
326
|
+
"page" to pageIndex,
|
|
327
|
+
"index" to i,
|
|
328
|
+
"bounds" to mapOf(
|
|
329
|
+
"x" to ann.bounds.left,
|
|
330
|
+
"y" to ann.bounds.top,
|
|
331
|
+
"width" to ann.bounds.width(),
|
|
332
|
+
"height" to ann.bounds.height()
|
|
333
|
+
),
|
|
334
|
+
"contents" to (ann.contents.ifEmpty { " " })
|
|
335
|
+
))
|
|
336
|
+
return true
|
|
337
|
+
}
|
|
338
|
+
draggingAnnotation = ann
|
|
339
|
+
draggingPageIndex = pageIndex
|
|
340
|
+
dragStartBounds = RectF(ann.bounds)
|
|
341
|
+
dragStartTouch = PointF(pdfX, pdfY)
|
|
342
|
+
return true
|
|
360
343
|
}
|
|
344
|
+
return false
|
|
361
345
|
}
|
|
362
346
|
MotionEvent.ACTION_MOVE -> {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
347
|
+
val ann = draggingAnnotation ?: return false
|
|
348
|
+
val start = dragStartTouch ?: return true
|
|
349
|
+
val startB = dragStartBounds ?: return true
|
|
350
|
+
val dx = pdfX - start.x
|
|
351
|
+
val dy = pdfY - start.y
|
|
352
|
+
ann.bounds.set(
|
|
353
|
+
startB.left + dx,
|
|
354
|
+
startB.top + dy,
|
|
355
|
+
startB.right + dx,
|
|
356
|
+
startB.bottom + dy
|
|
357
|
+
)
|
|
358
|
+
invalidate()
|
|
359
|
+
return true
|
|
376
360
|
}
|
|
377
361
|
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
|
378
362
|
if (draggingAnnotation != null) {
|
|
@@ -383,43 +367,65 @@ class ExpoPdfReaderView(context: Context, appContext: AppContext) : ExpoView(con
|
|
|
383
367
|
draggingPageIndex = -1
|
|
384
368
|
dragStartBounds = null
|
|
385
369
|
dragStartTouch = null
|
|
370
|
+
return false
|
|
386
371
|
}
|
|
387
372
|
}
|
|
388
|
-
return
|
|
373
|
+
return false
|
|
389
374
|
}
|
|
390
375
|
}
|
|
391
376
|
|
|
392
377
|
init {
|
|
393
378
|
setBackgroundColor(Color.WHITE)
|
|
394
|
-
scrollView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
|
379
|
+
scrollView.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
|
395
380
|
scrollView.isFillViewport = true
|
|
396
|
-
container.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
|
381
|
+
container.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
|
397
382
|
scrollView.addView(container)
|
|
398
383
|
addView(scrollView)
|
|
399
384
|
scrollView.viewTreeObserver.addOnScrollChangedListener {
|
|
400
385
|
notifyPageChange()
|
|
401
386
|
}
|
|
402
387
|
viewTreeObserver.addOnGlobalLayoutListener {
|
|
403
|
-
if (width
|
|
388
|
+
if (width <= 0) {
|
|
389
|
+
if (pdfUrl != null && pdfRenderer == null) {
|
|
390
|
+
loadPdfFromUrl(pdfUrl!!)
|
|
391
|
+
} else if (pdfRenderer != null && renderPdfRetryCount < maxRenderPdfRetries) {
|
|
392
|
+
renderPdfRetryCount++
|
|
393
|
+
postDelayed({ renderPdf() }, 50)
|
|
394
|
+
}
|
|
395
|
+
return@addOnGlobalLayoutListener
|
|
396
|
+
}
|
|
397
|
+
renderPdfRetryCount = 0
|
|
398
|
+
if (pdfUrl != null && pdfRenderer == null) {
|
|
404
399
|
loadPdfFromUrl(pdfUrl!!)
|
|
400
|
+
} else if (pdfRenderer != null) {
|
|
401
|
+
renderPdf()
|
|
405
402
|
}
|
|
406
403
|
}
|
|
407
404
|
}
|
|
408
405
|
|
|
409
|
-
fun
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
406
|
+
override fun onSizeChanged(w: Int, h: Int, oldW: Int, oldH: Int) {
|
|
407
|
+
super.onSizeChanged(w, h, oldW, oldH)
|
|
408
|
+
if (w > 0 && pdfRenderer != null) {
|
|
409
|
+
if (w != oldW) {
|
|
410
|
+
pageHeights.clear()
|
|
411
|
+
}
|
|
412
|
+
if (pageHeights.isEmpty()) {
|
|
413
|
+
renderPdf()
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
fun setUrl(url: String?) {
|
|
419
|
+
val u = url?.takeIf { it.isNotBlank() } ?: return
|
|
420
|
+
if (pdfUrl == u && pdfRenderer != null) return
|
|
421
|
+
pdfUrl = u
|
|
422
|
+
loadPdfFromUrl(u)
|
|
413
423
|
}
|
|
414
424
|
|
|
415
425
|
fun setDisplayMode(mode: String) {
|
|
416
|
-
displayMode
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
"twoUp" -> "twoUp"
|
|
420
|
-
"twoUpContinuous" -> "twoUpContinuous"
|
|
421
|
-
else -> "continuous"
|
|
422
|
-
}
|
|
426
|
+
if (displayMode == mode) return
|
|
427
|
+
displayMode = mode
|
|
428
|
+
pageHeights.clear()
|
|
423
429
|
pdfRenderer?.let { renderPdf() }
|
|
424
430
|
}
|
|
425
431
|
|
|
@@ -429,26 +435,41 @@ class ExpoPdfReaderView(context: Context, appContext: AppContext) : ExpoView(con
|
|
|
429
435
|
}
|
|
430
436
|
|
|
431
437
|
fun setTool(tool: String?) {
|
|
432
|
-
currentTool =
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
fun
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
fun
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
438
|
+
currentTool = tool?.let { runCatching { AnnotationTool.valueOf(it.uppercase()) }.getOrNull() }
|
|
439
|
+
// ToolAwareScrollView uses isScrollEnabledProvider { currentTool == null } — no property to set
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
fun setStrokeColor(hex: String) {
|
|
443
|
+
strokeColor = parseHexColor(hex)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
fun setStrokeWidth(width: Float) {
|
|
447
|
+
strokeWidth = width
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
fun setTextContent(text: String) {
|
|
451
|
+
textContent = text
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
fun setTextColor(hex: String) {
|
|
455
|
+
textColor = parseHexColor(hex)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
fun setTextFontSize(size: Float) {
|
|
459
|
+
textFontSize = size
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
fun setTextBold(value: Boolean) {
|
|
463
|
+
textBold = value
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
fun setTextItalic(value: Boolean) {
|
|
467
|
+
textItalic = value
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
fun setNoteColor(hex: String) {
|
|
471
|
+
noteColor = parseHexColor(hex)
|
|
472
|
+
}
|
|
452
473
|
|
|
453
474
|
fun setInitialAnnotations(annotations: List<Map<String, Any>>) {
|
|
454
475
|
if (pdfRenderer == null) {
|
|
@@ -458,13 +479,14 @@ class ExpoPdfReaderView(context: Context, appContext: AppContext) : ExpoView(con
|
|
|
458
479
|
val fp = fingerprintAnnotations(annotations)
|
|
459
480
|
if (appliedAnnotationsFingerprint == fp) return
|
|
460
481
|
appliedAnnotationsFingerprint = fp
|
|
461
|
-
|
|
462
|
-
pageAnnotations
|
|
482
|
+
pageAnnotations.clear()
|
|
483
|
+
while (pageAnnotations.size < totalPageCount) {
|
|
484
|
+
pageAnnotations.add(mutableListOf())
|
|
485
|
+
}
|
|
463
486
|
for (data in annotations) {
|
|
464
487
|
val pageIndex = (data["page"] as? Number)?.toInt() ?: continue
|
|
465
|
-
if (pageIndex < 0 || pageIndex >= pageCount) continue
|
|
466
488
|
val typeStr = data["type"] as? String ?: continue
|
|
467
|
-
val b = data["bounds"] as? Map
|
|
489
|
+
val b = data["bounds"] as? Map<*, *> ?: continue
|
|
468
490
|
val x = (b["x"] as? Number)?.toFloat() ?: 0f
|
|
469
491
|
val y = (b["y"] as? Number)?.toFloat() ?: 0f
|
|
470
492
|
val w = (b["width"] as? Number)?.toFloat() ?: 0f
|
|
@@ -472,41 +494,36 @@ class ExpoPdfReaderView(context: Context, appContext: AppContext) : ExpoView(con
|
|
|
472
494
|
val bounds = RectF(x, y, x + w, y + h)
|
|
473
495
|
val color = (data["color"] as? String)?.let { parseHexColor(it) } ?: Color.BLACK
|
|
474
496
|
val strokeW = (data["strokeWidth"] as? Number)?.toFloat() ?: 2f
|
|
475
|
-
val
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
italic = data["italic"] as? Boolean ?: false
|
|
485
|
-
)
|
|
486
|
-
if (typeStr == "pen" || typeStr == "highlighter" || typeStr == "line") {
|
|
487
|
-
@Suppress("UNCHECKED_CAST")
|
|
488
|
-
val paths = data["paths"] as? List<List<Map<String, Any>>> ?: emptyList()
|
|
489
|
-
for (pathData in paths) {
|
|
490
|
-
val points = pathData.map { pt ->
|
|
491
|
-
PointF(
|
|
492
|
-
(pt["x"] as? Number)?.toFloat() ?: 0f,
|
|
493
|
-
(pt["y"] as? Number)?.toFloat() ?: 0f
|
|
494
|
-
)
|
|
495
|
-
}.toMutableList()
|
|
496
|
-
if (points.isNotEmpty()) ann.paths.add(points)
|
|
497
|
+
val paths = mutableListOf<MutableList<PointF>>()
|
|
498
|
+
@Suppress("UNCHECKED_CAST")
|
|
499
|
+
(data["paths"] as? List<*>)?.forEach { pathData ->
|
|
500
|
+
val points = mutableListOf<PointF>()
|
|
501
|
+
(pathData as? List<*>)?.forEach { pt ->
|
|
502
|
+
val m = pt as? Map<*, *> ?: return@forEach
|
|
503
|
+
val px = (m["x"] as? Number)?.toFloat() ?: 0f
|
|
504
|
+
val py = (m["y"] as? Number)?.toFloat() ?: 0f
|
|
505
|
+
points.add(PointF(px, py))
|
|
497
506
|
}
|
|
507
|
+
if (points.isNotEmpty()) paths.add(points)
|
|
508
|
+
}
|
|
509
|
+
val contents = data["contents"] as? String ?: ""
|
|
510
|
+
val fontSize = (data["fontSize"] as? Number)?.toFloat() ?: 14f
|
|
511
|
+
val bold = data["bold"] as? Boolean ?: false
|
|
512
|
+
val italic = data["italic"] as? Boolean ?: false
|
|
513
|
+
val ann = Annotation(typeStr, bounds, color, strokeW, paths, contents, fontSize, bold, italic)
|
|
514
|
+
if (pageIndex in 0 until pageAnnotations.size) {
|
|
515
|
+
pageAnnotations[pageIndex].add(ann)
|
|
498
516
|
}
|
|
499
|
-
pageAnnotations[pageIndex].add(ann)
|
|
500
517
|
}
|
|
501
518
|
overlayViews.forEach { it?.invalidate() }
|
|
519
|
+
notifyAnnotationChange()
|
|
502
520
|
}
|
|
503
521
|
|
|
504
522
|
fun undo() {
|
|
505
523
|
val entry = undoStack.removeLastOrNull() ?: return
|
|
506
524
|
redoStack.add(entry)
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
}
|
|
525
|
+
val list = pageAnnotations.getOrNull(entry.pageIndex) ?: return
|
|
526
|
+
list.remove(entry.annotation)
|
|
510
527
|
overlayViews.getOrNull(entry.pageIndex)?.invalidate()
|
|
511
528
|
notifyAnnotationChange()
|
|
512
529
|
notifyUndoRedoStateChange()
|
|
@@ -515,153 +532,29 @@ class ExpoPdfReaderView(context: Context, appContext: AppContext) : ExpoView(con
|
|
|
515
532
|
fun redo() {
|
|
516
533
|
val entry = redoStack.removeLastOrNull() ?: return
|
|
517
534
|
undoStack.add(entry)
|
|
518
|
-
|
|
519
|
-
pageAnnotations
|
|
535
|
+
while (pageAnnotations.size <= entry.pageIndex) {
|
|
536
|
+
pageAnnotations.add(mutableListOf())
|
|
520
537
|
}
|
|
538
|
+
pageAnnotations[entry.pageIndex].add(entry.annotation)
|
|
521
539
|
overlayViews.getOrNull(entry.pageIndex)?.invalidate()
|
|
522
540
|
notifyAnnotationChange()
|
|
523
541
|
notifyUndoRedoStateChange()
|
|
524
542
|
}
|
|
525
543
|
|
|
526
|
-
fun updateText(page: Int, index: Int, contents: String) {
|
|
527
|
-
if (page !in pageAnnotations.indices || index !in pageAnnotations[page].indices) return
|
|
528
|
-
val ann = pageAnnotations[page][index]
|
|
529
|
-
if (ann.type != "text") return
|
|
530
|
-
ann.contents = if (contents.isEmpty()) " " else contents
|
|
531
|
-
overlayViews.getOrNull(page)?.invalidate()
|
|
532
|
-
notifyAnnotationChange()
|
|
533
|
-
notifyUndoRedoStateChange()
|
|
534
|
-
}
|
|
535
|
-
|
|
536
544
|
fun updateNote(page: Int, index: Int, contents: String) {
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
ann.contents = if (contents.isEmpty()) " " else contents
|
|
545
|
+
val list = pageAnnotations.getOrNull(page) ?: return
|
|
546
|
+
if (index !in list.indices) return
|
|
547
|
+
list[index].contents = contents.ifEmpty { " " }
|
|
541
548
|
overlayViews.getOrNull(page)?.invalidate()
|
|
542
549
|
notifyAnnotationChange()
|
|
543
|
-
notifyUndoRedoStateChange()
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
private fun parseHexColor(hex: String): Int {
|
|
547
|
-
var s = hex.trim().removePrefix("#")
|
|
548
|
-
if (s.length == 6) s = "FF$s"
|
|
549
|
-
return try {
|
|
550
|
-
Color.parseColor("#$s")
|
|
551
|
-
} catch (_: Exception) {
|
|
552
|
-
Color.BLACK
|
|
553
|
-
}
|
|
554
550
|
}
|
|
555
551
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
val x = (b["x"] as? Number)?.toDouble() ?: 0.0
|
|
562
|
-
val y = (b["y"] as? Number)?.toDouble() ?: 0.0
|
|
563
|
-
val w = (b["width"] as? Number)?.toDouble() ?: 0.0
|
|
564
|
-
val h = (b["height"] as? Number)?.toDouble() ?: 0.0
|
|
565
|
-
"$page|$type|$x,$y,$w,$h"
|
|
566
|
-
}.sorted().joinToString("||")
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
private fun getAnnotationsForPage(pageIndex: Int): List<Annotation> {
|
|
570
|
-
if (pageIndex !in pageAnnotations.indices) return emptyList()
|
|
571
|
-
return pageAnnotations[pageIndex]
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
private fun addAnnotationToPage(pageIndex: Int, ann: Annotation) {
|
|
575
|
-
while (pageAnnotations.size <= pageIndex) pageAnnotations.add(mutableListOf())
|
|
576
|
-
pageAnnotations[pageIndex].add(ann)
|
|
577
|
-
undoStack.add(UndoEntry(ann, pageIndex))
|
|
578
|
-
redoStack.clear()
|
|
579
|
-
overlayViews.getOrNull(pageIndex)?.invalidate()
|
|
552
|
+
fun updateText(page: Int, index: Int, contents: String) {
|
|
553
|
+
val list = pageAnnotations.getOrNull(page) ?: return
|
|
554
|
+
if (index !in list.indices) return
|
|
555
|
+
list[index].contents = contents.ifEmpty { " " }
|
|
556
|
+
overlayViews.getOrNull(page)?.invalidate()
|
|
580
557
|
notifyAnnotationChange()
|
|
581
|
-
notifyUndoRedoStateChange()
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
private fun addInstantAnnotation(pageIndex: Int, pdfX: Float, pdfY: Float, tool: AnnotationTool) {
|
|
585
|
-
val isNote = (tool == AnnotationTool.NOTE)
|
|
586
|
-
val bounds = if (isNote) {
|
|
587
|
-
RectF(pdfX, pdfY, pdfX + 32, pdfY + 32)
|
|
588
|
-
} else {
|
|
589
|
-
RectF(pdfX, pdfY, pdfX + 180, pdfY + 40)
|
|
590
|
-
}
|
|
591
|
-
val ann = Annotation(
|
|
592
|
-
type = if (isNote) "note" else "text",
|
|
593
|
-
bounds = bounds,
|
|
594
|
-
color = if (isNote) noteColor else textColor,
|
|
595
|
-
contents = if (textContent.isEmpty()) " " else textContent,
|
|
596
|
-
fontSize = textFontSize,
|
|
597
|
-
bold = textBold,
|
|
598
|
-
italic = textItalic
|
|
599
|
-
)
|
|
600
|
-
addAnnotationToPage(pageIndex, ann)
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
private fun eraseAt(pageIndex: Int, x: Float, y: Float) {
|
|
604
|
-
if (pageIndex !in pageAnnotations.indices) return
|
|
605
|
-
val toRemove = pageAnnotations[pageIndex].filter { it.bounds.contains(x, y) }
|
|
606
|
-
for (ann in toRemove) {
|
|
607
|
-
pageAnnotations[pageIndex].remove(ann)
|
|
608
|
-
val idx = undoStack.indexOfFirst { it.annotation === ann }
|
|
609
|
-
if (idx >= 0) undoStack.removeAt(idx)
|
|
610
|
-
}
|
|
611
|
-
if (toRemove.isNotEmpty()) {
|
|
612
|
-
overlayViews.getOrNull(pageIndex)?.invalidate()
|
|
613
|
-
notifyAnnotationChange()
|
|
614
|
-
notifyUndoRedoStateChange()
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
private fun notifyAnnotationChange() {
|
|
619
|
-
val renderer = pdfRenderer ?: return
|
|
620
|
-
val list = mutableListOf<Map<String, Any>>()
|
|
621
|
-
for (pageIndex in 0 until renderer.pageCount) {
|
|
622
|
-
val anns = getAnnotationsForPage(pageIndex)
|
|
623
|
-
for ((idx, ann) in anns.withIndex()) {
|
|
624
|
-
val map = mutableMapOf<String, Any>(
|
|
625
|
-
"page" to pageIndex,
|
|
626
|
-
"bounds" to mapOf(
|
|
627
|
-
"x" to ann.bounds.left,
|
|
628
|
-
"y" to ann.bounds.top,
|
|
629
|
-
"width" to ann.bounds.width(),
|
|
630
|
-
"height" to ann.bounds.height()
|
|
631
|
-
),
|
|
632
|
-
"type" to ann.type,
|
|
633
|
-
"color" to colorToHex(ann.color),
|
|
634
|
-
"contents" to (ann.contents)
|
|
635
|
-
)
|
|
636
|
-
if (ann.type == "pen" || ann.type == "highlighter" || ann.type == "line") {
|
|
637
|
-
map["paths"] = ann.paths.map { path ->
|
|
638
|
-
path.map { p -> mapOf("x" to p.x, "y" to p.y) }
|
|
639
|
-
}
|
|
640
|
-
map["strokeWidth"] = ann.strokeWidth
|
|
641
|
-
}
|
|
642
|
-
if (ann.type == "text") {
|
|
643
|
-
map["fontSize"] = ann.fontSize
|
|
644
|
-
map["bold"] = ann.bold
|
|
645
|
-
map["italic"] = ann.italic
|
|
646
|
-
}
|
|
647
|
-
list.add(map)
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
onAnnotationChange(mapOf("annotations" to list))
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
private fun colorToHex(color: Int): String {
|
|
654
|
-
val r = Color.red(color)
|
|
655
|
-
val g = Color.green(color)
|
|
656
|
-
val b = Color.blue(color)
|
|
657
|
-
return String.format("#%02x%02x%02x", r, g, b)
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
private fun notifyUndoRedoStateChange() {
|
|
661
|
-
onUndoRedoStateChange(mapOf(
|
|
662
|
-
"canUndo" to undoStack.isNotEmpty(),
|
|
663
|
-
"canRedo" to redoStack.isNotEmpty()
|
|
664
|
-
))
|
|
665
558
|
}
|
|
666
559
|
|
|
667
560
|
private fun scrollToPage(pageIndex: Int) {
|
|
@@ -677,23 +570,24 @@ class ExpoPdfReaderView(context: Context, appContext: AppContext) : ExpoView(con
|
|
|
677
570
|
scope.launch {
|
|
678
571
|
try {
|
|
679
572
|
resetRenderer()
|
|
573
|
+
val decodedUrl = runCatching { java.net.URLDecoder.decode(url, "UTF-8") }.getOrElse { url }
|
|
680
574
|
val file = withContext(Dispatchers.IO) {
|
|
681
575
|
when {
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
else -> downloadFile(
|
|
576
|
+
decodedUrl.startsWith("file://") -> File(decodedUrl.removePrefix("file://"))
|
|
577
|
+
decodedUrl.startsWith("/") -> File(decodedUrl)
|
|
578
|
+
else -> downloadFile(decodedUrl)
|
|
685
579
|
}
|
|
686
580
|
}
|
|
687
581
|
if (file.exists()) {
|
|
688
582
|
fileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
|
|
689
583
|
pdfRenderer = PdfRenderer(fileDescriptor!!)
|
|
690
584
|
totalPageCount = pdfRenderer!!.pageCount
|
|
691
|
-
undoStack.clear()
|
|
692
|
-
redoStack.clear()
|
|
693
|
-
appliedAnnotationsFingerprint = null
|
|
694
|
-
notifyUndoRedoStateChange()
|
|
695
585
|
renderPdf()
|
|
696
|
-
|
|
586
|
+
post { renderPdf() }
|
|
587
|
+
pendingAnnotations?.let { ann ->
|
|
588
|
+
pendingAnnotations = null
|
|
589
|
+
setInitialAnnotations(ann)
|
|
590
|
+
}
|
|
697
591
|
}
|
|
698
592
|
} catch (e: Exception) {
|
|
699
593
|
android.util.Log.e("ExpoPdfReader", "Error loading PDF", e)
|
|
@@ -707,34 +601,72 @@ class ExpoPdfReaderView(context: Context, appContext: AppContext) : ExpoView(con
|
|
|
707
601
|
fileDescriptor?.close()
|
|
708
602
|
fileDescriptor = null
|
|
709
603
|
pageHeights.clear()
|
|
604
|
+
pageWidthsPdf.clear()
|
|
605
|
+
pageHeightsPdf.clear()
|
|
710
606
|
pageViews.clear()
|
|
711
607
|
overlayViews.clear()
|
|
712
|
-
pageAnnotations.clear()
|
|
713
608
|
container.removeAllViews()
|
|
609
|
+
pageAnnotations.clear()
|
|
610
|
+
undoStack.clear()
|
|
611
|
+
redoStack.clear()
|
|
612
|
+
appliedAnnotationsFingerprint = null
|
|
613
|
+
pendingAnnotations = null
|
|
614
|
+
currentPath = null
|
|
615
|
+
currentPathPoints.clear()
|
|
616
|
+
activePageIndex = -1
|
|
617
|
+
startPoint = null
|
|
618
|
+
draggingAnnotation = null
|
|
619
|
+
draggingPageIndex = -1
|
|
620
|
+
dragStartBounds = null
|
|
621
|
+
dragStartTouch = null
|
|
622
|
+
renderJob?.cancel()
|
|
714
623
|
}
|
|
715
624
|
|
|
716
625
|
private fun renderPdf() {
|
|
717
626
|
val renderer = pdfRenderer ?: return
|
|
718
|
-
if (width <= 0)
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
val
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
627
|
+
if (width <= 0) {
|
|
628
|
+
if (renderPdfRetryCount < maxRenderPdfRetries) {
|
|
629
|
+
renderPdfRetryCount++
|
|
630
|
+
post { renderPdf() }
|
|
631
|
+
}
|
|
632
|
+
return
|
|
633
|
+
}
|
|
634
|
+
renderPdfRetryCount = 0
|
|
635
|
+
if (pageHeights.isNotEmpty()) {
|
|
636
|
+
updateVisiblePages()
|
|
637
|
+
return
|
|
638
|
+
}
|
|
639
|
+
renderJob?.cancel()
|
|
640
|
+
renderJob = scope.launch {
|
|
641
|
+
val w = width.coerceAtLeast(1)
|
|
642
|
+
val result = withContext(Dispatchers.Default) {
|
|
643
|
+
(0 until renderer.pageCount).map { i ->
|
|
644
|
+
val page = renderer.openPage(i)
|
|
645
|
+
try {
|
|
646
|
+
Triple(
|
|
647
|
+
i,
|
|
648
|
+
(w.toFloat() * page.height / page.width).toInt(),
|
|
649
|
+
Pair(page.width.toFloat(), page.height.toFloat())
|
|
650
|
+
)
|
|
651
|
+
} finally {
|
|
652
|
+
page.close()
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
if (!isActive) return@launch
|
|
657
|
+
withContext(Dispatchers.Main) {
|
|
658
|
+
pageHeights.clear()
|
|
659
|
+
pageWidthsPdf.clear()
|
|
660
|
+
pageHeightsPdf.clear()
|
|
661
|
+
for ((_, h, pdfSize) in result) {
|
|
662
|
+
pageHeights.add(h)
|
|
663
|
+
pageWidthsPdf.add(pdfSize.first)
|
|
664
|
+
pageHeightsPdf.add(pdfSize.second)
|
|
665
|
+
}
|
|
666
|
+
ensurePlaceholders(renderer)
|
|
667
|
+
scrollToPage(initialPage.coerceIn(0, (renderer.pageCount - 1).coerceAtLeast(0)))
|
|
668
|
+
updateVisiblePages()
|
|
669
|
+
}
|
|
738
670
|
}
|
|
739
671
|
}
|
|
740
672
|
|
|
@@ -743,7 +675,9 @@ class ExpoPdfReaderView(context: Context, appContext: AppContext) : ExpoView(con
|
|
|
743
675
|
val pageCount = renderer.pageCount
|
|
744
676
|
pageViews = MutableList(pageCount) { null }
|
|
745
677
|
overlayViews = MutableList(pageCount) { null }
|
|
746
|
-
while (pageAnnotations.size < pageCount)
|
|
678
|
+
while (pageAnnotations.size < pageCount) {
|
|
679
|
+
pageAnnotations.add(mutableListOf())
|
|
680
|
+
}
|
|
747
681
|
for (i in 0 until pageCount) {
|
|
748
682
|
val frame = FrameLayout(context).apply {
|
|
749
683
|
layoutParams = LinearLayout.LayoutParams(
|
|
@@ -764,25 +698,45 @@ class ExpoPdfReaderView(context: Context, appContext: AppContext) : ExpoView(con
|
|
|
764
698
|
}
|
|
765
699
|
}
|
|
766
700
|
|
|
701
|
+
private fun currentPageFromScroll(): Int {
|
|
702
|
+
if (pageHeights.isEmpty()) return 0
|
|
703
|
+
val scrollY = scrollView.scrollY
|
|
704
|
+
var offset = 0
|
|
705
|
+
for (i in pageHeights.indices) {
|
|
706
|
+
if (scrollY < offset + pageHeights[i] / 2) return i
|
|
707
|
+
offset += pageHeights[i] + 16
|
|
708
|
+
}
|
|
709
|
+
return (pageHeights.size - 1).coerceAtLeast(0)
|
|
710
|
+
}
|
|
711
|
+
|
|
767
712
|
private fun updateVisiblePages() {
|
|
768
713
|
val renderer = pdfRenderer ?: return
|
|
769
|
-
val
|
|
770
|
-
val
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
if (
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
714
|
+
val currentPage = currentPageFromScroll()
|
|
715
|
+
val from = (currentPage - renderWindowBefore).coerceAtLeast(0)
|
|
716
|
+
val to = (currentPage + renderWindowAfter).coerceAtMost(renderer.pageCount - 1)
|
|
717
|
+
|
|
718
|
+
for (i in pageViews.indices) {
|
|
719
|
+
if (i in from..to) continue
|
|
720
|
+
val iv = pageViews.getOrNull(i) ?: continue
|
|
721
|
+
val frame = container.getChildAt(i) as? FrameLayout ?: continue
|
|
722
|
+
val bmp = (iv.drawable as? BitmapDrawable)?.bitmap
|
|
723
|
+
iv.setImageBitmap(null)
|
|
724
|
+
bmp?.recycle()
|
|
725
|
+
frame.removeView(iv)
|
|
726
|
+
pageViews[i] = null
|
|
778
727
|
}
|
|
728
|
+
|
|
729
|
+
val order = (from..to).sortedBy { kotlin.math.abs(it - currentPage) }
|
|
730
|
+
for (i in order) {
|
|
731
|
+
renderPageToView(i)
|
|
732
|
+
}
|
|
733
|
+
container.post { container.invalidate() }
|
|
779
734
|
}
|
|
780
735
|
|
|
781
736
|
private fun renderPageToView(pageIndex: Int) {
|
|
782
737
|
val renderer = pdfRenderer ?: return
|
|
783
|
-
if (pageIndex >= container.childCount) return
|
|
784
738
|
val frame = container.getChildAt(pageIndex) as? FrameLayout ?: return
|
|
785
|
-
|
|
739
|
+
// Frame always has overlay; only skip if we already have an ImageView for this page
|
|
786
740
|
if (pageViews.getOrNull(pageIndex) != null) return
|
|
787
741
|
val page = renderer.openPage(pageIndex)
|
|
788
742
|
val w = width.coerceAtLeast(1)
|
|
@@ -798,11 +752,11 @@ class ExpoPdfReaderView(context: Context, appContext: AppContext) : ExpoView(con
|
|
|
798
752
|
FrameLayout.LayoutParams.MATCH_PARENT
|
|
799
753
|
)
|
|
800
754
|
}
|
|
801
|
-
val overlay = overlayViews[pageIndex]
|
|
802
755
|
frame.addView(imageView, 0)
|
|
803
756
|
pageViews[pageIndex] = imageView
|
|
804
|
-
|
|
805
|
-
|
|
757
|
+
overlayViews.getOrNull(pageIndex)?.bringToFront()
|
|
758
|
+
frame.requestLayout()
|
|
759
|
+
frame.invalidate()
|
|
806
760
|
}
|
|
807
761
|
|
|
808
762
|
private fun notifyPageChange() {
|
|
@@ -828,25 +782,161 @@ class ExpoPdfReaderView(context: Context, appContext: AppContext) : ExpoView(con
|
|
|
828
782
|
updateVisiblePages()
|
|
829
783
|
}
|
|
830
784
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
785
|
+
private fun getAnnotationsForPage(pageIndex: Int): List<Annotation> {
|
|
786
|
+
return pageAnnotations.getOrNull(pageIndex).orEmpty()
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
private fun addAnnotationToPage(pageIndex: Int, ann: Annotation) {
|
|
790
|
+
while (pageAnnotations.size <= pageIndex) {
|
|
791
|
+
pageAnnotations.add(mutableListOf())
|
|
792
|
+
}
|
|
793
|
+
pageAnnotations[pageIndex].add(ann)
|
|
794
|
+
overlayViews.getOrNull(pageIndex)?.invalidate()
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
private fun addInstantAnnotation(pageIndex: Int, pdfX: Float, pdfY: Float, tool: AnnotationTool) {
|
|
798
|
+
val isNote = (tool == AnnotationTool.NOTE)
|
|
799
|
+
val rect = if (isNote) {
|
|
800
|
+
RectF(pdfX, pdfY, pdfX + 32, pdfY + 32)
|
|
801
|
+
} else {
|
|
802
|
+
RectF(pdfX, pdfY, pdfX + 180, pdfY + 40)
|
|
803
|
+
}
|
|
804
|
+
val typeStr = if (isNote) "note" else "text"
|
|
805
|
+
val contents = if (textContent.isEmpty()) " " else textContent
|
|
806
|
+
val ann = Annotation(
|
|
807
|
+
type = typeStr,
|
|
808
|
+
bounds = rect,
|
|
809
|
+
color = if (isNote) noteColor else textColor,
|
|
810
|
+
strokeWidth = 0f,
|
|
811
|
+
paths = mutableListOf(),
|
|
812
|
+
contents = contents,
|
|
813
|
+
fontSize = textFontSize,
|
|
814
|
+
bold = textBold,
|
|
815
|
+
italic = textItalic
|
|
816
|
+
)
|
|
817
|
+
addAnnotationToPage(pageIndex, ann)
|
|
818
|
+
undoStack.add(UndoEntry(ann, pageIndex))
|
|
819
|
+
redoStack.clear()
|
|
820
|
+
notifyAnnotationChange()
|
|
821
|
+
notifyUndoRedoStateChange()
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
private fun eraseAt(pageIndex: Int, pdfX: Float, pdfY: Float) {
|
|
825
|
+
val list = pageAnnotations.getOrNull(pageIndex) ?: return
|
|
826
|
+
var erased = false
|
|
827
|
+
val toRemove = list.filter { it.bounds.contains(pdfX, pdfY) }
|
|
828
|
+
for (ann in toRemove) {
|
|
829
|
+
list.remove(ann)
|
|
830
|
+
undoStack.removeAll { it.annotation == ann }
|
|
831
|
+
erased = true
|
|
832
|
+
}
|
|
833
|
+
if (erased) {
|
|
834
|
+
overlayViews.getOrNull(pageIndex)?.invalidate()
|
|
835
|
+
notifyAnnotationChange()
|
|
836
|
+
notifyUndoRedoStateChange()
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
private fun commitStroke(tool: AnnotationTool) {
|
|
841
|
+
if (currentPathPoints.size < 2) return
|
|
842
|
+
val minX = currentPathPoints.minOf { it.x } - strokeWidth
|
|
843
|
+
val minY = currentPathPoints.minOf { it.y } - strokeWidth
|
|
844
|
+
val maxX = currentPathPoints.maxOf { it.x } + strokeWidth
|
|
845
|
+
val maxY = currentPathPoints.maxOf { it.y } + strokeWidth
|
|
846
|
+
val bounds = RectF(minX, minY, maxX, maxY)
|
|
847
|
+
val paths = mutableListOf<MutableList<PointF>>()
|
|
848
|
+
val points = currentPathPoints.map { PointF(it.x, it.y) }.toMutableList()
|
|
849
|
+
paths.add(points)
|
|
850
|
+
val typeStr = when (tool) {
|
|
851
|
+
AnnotationTool.PEN -> "pen"
|
|
852
|
+
AnnotationTool.HIGHLIGHTER -> "highlighter"
|
|
853
|
+
AnnotationTool.LINE -> "line"
|
|
854
|
+
else -> "pen"
|
|
855
|
+
}
|
|
856
|
+
val color = if (tool == AnnotationTool.HIGHLIGHTER) {
|
|
857
|
+
Color.argb(102, Color.red(strokeColor), Color.green(strokeColor), Color.blue(strokeColor))
|
|
858
|
+
} else {
|
|
859
|
+
strokeColor
|
|
860
|
+
}
|
|
861
|
+
val ann = Annotation(typeStr, bounds, color, strokeWidth, paths, "", 0f, false, false)
|
|
862
|
+
addAnnotationToPage(activePageIndex, ann)
|
|
863
|
+
undoStack.add(UndoEntry(ann, activePageIndex))
|
|
864
|
+
redoStack.clear()
|
|
865
|
+
notifyAnnotationChange()
|
|
866
|
+
notifyUndoRedoStateChange()
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
private fun notifyAnnotationChange() {
|
|
870
|
+
val list = mutableListOf<Map<String, Any>>()
|
|
871
|
+
for (pageIndex in pageAnnotations.indices) {
|
|
872
|
+
for (ann in getAnnotationsForPage(pageIndex)) {
|
|
873
|
+
list.add(mapOf(
|
|
874
|
+
"page" to pageIndex,
|
|
875
|
+
"bounds" to mapOf(
|
|
876
|
+
"x" to ann.bounds.left,
|
|
877
|
+
"y" to ann.bounds.top,
|
|
878
|
+
"width" to ann.bounds.width(),
|
|
879
|
+
"height" to ann.bounds.height()
|
|
880
|
+
),
|
|
881
|
+
"type" to ann.type,
|
|
882
|
+
"color" to colorToHex(ann.color),
|
|
883
|
+
"contents" to ann.contents,
|
|
884
|
+
"paths" to ann.paths.map { path ->
|
|
885
|
+
path.map { p -> mapOf("x" to p.x, "y" to p.y) }
|
|
886
|
+
},
|
|
887
|
+
"strokeWidth" to ann.strokeWidth,
|
|
888
|
+
"fontSize" to ann.fontSize,
|
|
889
|
+
"bold" to ann.bold,
|
|
890
|
+
"italic" to ann.italic
|
|
891
|
+
))
|
|
892
|
+
}
|
|
835
893
|
}
|
|
836
|
-
|
|
894
|
+
onAnnotationChange(mapOf("annotations" to list))
|
|
837
895
|
}
|
|
838
896
|
|
|
839
|
-
private fun
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
897
|
+
private fun notifyUndoRedoStateChange() {
|
|
898
|
+
onUndoRedoStateChange(mapOf(
|
|
899
|
+
"canUndo" to undoStack.isNotEmpty(),
|
|
900
|
+
"canRedo" to redoStack.isNotEmpty()
|
|
901
|
+
))
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
private fun parseHexColor(hex: String): Int {
|
|
905
|
+
var s = hex.trim().uppercase()
|
|
906
|
+
if (s.startsWith("#")) s = s.removePrefix("#")
|
|
907
|
+
val v = s.toLongOrNull(16) ?: 0L
|
|
908
|
+
val r = ((v shr 16) and 0xFF).toInt()
|
|
909
|
+
val g = ((v shr 8) and 0xFF).toInt()
|
|
910
|
+
val b = (v and 0xFF).toInt()
|
|
911
|
+
return Color.rgb(r, g, b)
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
private fun fingerprintAnnotations(annotations: List<Map<String, Any>>): String {
|
|
915
|
+
val items = annotations.mapNotNull { data ->
|
|
916
|
+
val pageIndex = (data["page"] as? Number)?.toInt() ?: return@mapNotNull null
|
|
917
|
+
val typeStr = data["type"] as? String ?: return@mapNotNull null
|
|
918
|
+
val b = data["bounds"] as? Map<*, *> ?: return@mapNotNull null
|
|
919
|
+
val x = (b["x"] as? Number)?.toDouble() ?: 0.0
|
|
920
|
+
val y = (b["y"] as? Number)?.toDouble() ?: 0.0
|
|
921
|
+
val w = (b["width"] as? Number)?.toDouble() ?: 0.0
|
|
922
|
+
val h = (b["height"] as? Number)?.toDouble() ?: 0.0
|
|
923
|
+
val color = data["color"] as? String ?: ""
|
|
924
|
+
val strokeW = data["strokeWidth"] as? Double ?: 0.0
|
|
925
|
+
val contents = data["contents"] as? String ?: ""
|
|
926
|
+
var ptsCount = 0
|
|
927
|
+
(data["paths"] as? List<*>)?.forEach { pathData ->
|
|
928
|
+
(pathData as? List<*>)?.forEach { ptsCount++ }
|
|
847
929
|
}
|
|
930
|
+
"$pageIndex|$typeStr|$x,$y,$w,$h|$color|$strokeW|$contents|pts:$ptsCount"
|
|
848
931
|
}
|
|
849
|
-
return
|
|
932
|
+
return items.sorted().joinToString("||")
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
private fun colorToHex(color: Int): String {
|
|
936
|
+
val r = Color.red(color)
|
|
937
|
+
val g = Color.green(color)
|
|
938
|
+
val b = Color.blue(color)
|
|
939
|
+
return String.format("#%02x%02x%02x", r, g, b)
|
|
850
940
|
}
|
|
851
941
|
|
|
852
942
|
private suspend fun downloadFile(urlStr: String): File = withContext(Dispatchers.IO) {
|