@june24/expo-pdf-reader 0.1.17 → 0.1.19
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,60 @@ 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 && pageHeights.isEmpty()) {
|
|
409
|
+
renderPdf()
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
fun setUrl(url: String?) {
|
|
414
|
+
val u = url?.takeIf { it.isNotBlank() } ?: return
|
|
415
|
+
if (pdfUrl == u && pdfRenderer != null) return
|
|
416
|
+
pdfUrl = u
|
|
417
|
+
loadPdfFromUrl(u)
|
|
413
418
|
}
|
|
414
419
|
|
|
415
420
|
fun setDisplayMode(mode: String) {
|
|
416
|
-
displayMode
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
"twoUp" -> "twoUp"
|
|
420
|
-
"twoUpContinuous" -> "twoUpContinuous"
|
|
421
|
-
else -> "continuous"
|
|
422
|
-
}
|
|
421
|
+
if (displayMode == mode) return
|
|
422
|
+
displayMode = mode
|
|
423
|
+
pageHeights.clear()
|
|
423
424
|
pdfRenderer?.let { renderPdf() }
|
|
424
425
|
}
|
|
425
426
|
|
|
@@ -429,26 +430,41 @@ class ExpoPdfReaderView(context: Context, appContext: AppContext) : ExpoView(con
|
|
|
429
430
|
}
|
|
430
431
|
|
|
431
432
|
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
|
-
|
|
433
|
+
currentTool = tool?.let { runCatching { AnnotationTool.valueOf(it.uppercase()) }.getOrNull() }
|
|
434
|
+
// ToolAwareScrollView uses isScrollEnabledProvider { currentTool == null } — no property to set
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
fun setStrokeColor(hex: String) {
|
|
438
|
+
strokeColor = parseHexColor(hex)
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
fun setStrokeWidth(width: Float) {
|
|
442
|
+
strokeWidth = width
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
fun setTextContent(text: String) {
|
|
446
|
+
textContent = text
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
fun setTextColor(hex: String) {
|
|
450
|
+
textColor = parseHexColor(hex)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
fun setTextFontSize(size: Float) {
|
|
454
|
+
textFontSize = size
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
fun setTextBold(value: Boolean) {
|
|
458
|
+
textBold = value
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
fun setTextItalic(value: Boolean) {
|
|
462
|
+
textItalic = value
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
fun setNoteColor(hex: String) {
|
|
466
|
+
noteColor = parseHexColor(hex)
|
|
467
|
+
}
|
|
452
468
|
|
|
453
469
|
fun setInitialAnnotations(annotations: List<Map<String, Any>>) {
|
|
454
470
|
if (pdfRenderer == null) {
|
|
@@ -458,13 +474,14 @@ class ExpoPdfReaderView(context: Context, appContext: AppContext) : ExpoView(con
|
|
|
458
474
|
val fp = fingerprintAnnotations(annotations)
|
|
459
475
|
if (appliedAnnotationsFingerprint == fp) return
|
|
460
476
|
appliedAnnotationsFingerprint = fp
|
|
461
|
-
|
|
462
|
-
pageAnnotations
|
|
477
|
+
pageAnnotations.clear()
|
|
478
|
+
while (pageAnnotations.size < totalPageCount) {
|
|
479
|
+
pageAnnotations.add(mutableListOf())
|
|
480
|
+
}
|
|
463
481
|
for (data in annotations) {
|
|
464
482
|
val pageIndex = (data["page"] as? Number)?.toInt() ?: continue
|
|
465
|
-
if (pageIndex < 0 || pageIndex >= pageCount) continue
|
|
466
483
|
val typeStr = data["type"] as? String ?: continue
|
|
467
|
-
val b = data["bounds"] as? Map
|
|
484
|
+
val b = data["bounds"] as? Map<*, *> ?: continue
|
|
468
485
|
val x = (b["x"] as? Number)?.toFloat() ?: 0f
|
|
469
486
|
val y = (b["y"] as? Number)?.toFloat() ?: 0f
|
|
470
487
|
val w = (b["width"] as? Number)?.toFloat() ?: 0f
|
|
@@ -472,41 +489,36 @@ class ExpoPdfReaderView(context: Context, appContext: AppContext) : ExpoView(con
|
|
|
472
489
|
val bounds = RectF(x, y, x + w, y + h)
|
|
473
490
|
val color = (data["color"] as? String)?.let { parseHexColor(it) } ?: Color.BLACK
|
|
474
491
|
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)
|
|
492
|
+
val paths = mutableListOf<MutableList<PointF>>()
|
|
493
|
+
@Suppress("UNCHECKED_CAST")
|
|
494
|
+
(data["paths"] as? List<*>)?.forEach { pathData ->
|
|
495
|
+
val points = mutableListOf<PointF>()
|
|
496
|
+
(pathData as? List<*>)?.forEach { pt ->
|
|
497
|
+
val m = pt as? Map<*, *> ?: return@forEach
|
|
498
|
+
val px = (m["x"] as? Number)?.toFloat() ?: 0f
|
|
499
|
+
val py = (m["y"] as? Number)?.toFloat() ?: 0f
|
|
500
|
+
points.add(PointF(px, py))
|
|
497
501
|
}
|
|
502
|
+
if (points.isNotEmpty()) paths.add(points)
|
|
503
|
+
}
|
|
504
|
+
val contents = data["contents"] as? String ?: ""
|
|
505
|
+
val fontSize = (data["fontSize"] as? Number)?.toFloat() ?: 14f
|
|
506
|
+
val bold = data["bold"] as? Boolean ?: false
|
|
507
|
+
val italic = data["italic"] as? Boolean ?: false
|
|
508
|
+
val ann = Annotation(typeStr, bounds, color, strokeW, paths, contents, fontSize, bold, italic)
|
|
509
|
+
if (pageIndex in 0 until pageAnnotations.size) {
|
|
510
|
+
pageAnnotations[pageIndex].add(ann)
|
|
498
511
|
}
|
|
499
|
-
pageAnnotations[pageIndex].add(ann)
|
|
500
512
|
}
|
|
501
513
|
overlayViews.forEach { it?.invalidate() }
|
|
514
|
+
notifyAnnotationChange()
|
|
502
515
|
}
|
|
503
516
|
|
|
504
517
|
fun undo() {
|
|
505
518
|
val entry = undoStack.removeLastOrNull() ?: return
|
|
506
519
|
redoStack.add(entry)
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
}
|
|
520
|
+
val list = pageAnnotations.getOrNull(entry.pageIndex) ?: return
|
|
521
|
+
list.remove(entry.annotation)
|
|
510
522
|
overlayViews.getOrNull(entry.pageIndex)?.invalidate()
|
|
511
523
|
notifyAnnotationChange()
|
|
512
524
|
notifyUndoRedoStateChange()
|
|
@@ -515,153 +527,29 @@ class ExpoPdfReaderView(context: Context, appContext: AppContext) : ExpoView(con
|
|
|
515
527
|
fun redo() {
|
|
516
528
|
val entry = redoStack.removeLastOrNull() ?: return
|
|
517
529
|
undoStack.add(entry)
|
|
518
|
-
|
|
519
|
-
pageAnnotations
|
|
530
|
+
while (pageAnnotations.size <= entry.pageIndex) {
|
|
531
|
+
pageAnnotations.add(mutableListOf())
|
|
520
532
|
}
|
|
533
|
+
pageAnnotations[entry.pageIndex].add(entry.annotation)
|
|
521
534
|
overlayViews.getOrNull(entry.pageIndex)?.invalidate()
|
|
522
535
|
notifyAnnotationChange()
|
|
523
536
|
notifyUndoRedoStateChange()
|
|
524
537
|
}
|
|
525
538
|
|
|
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
539
|
fun updateNote(page: Int, index: Int, contents: String) {
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
ann.contents = if (contents.isEmpty()) " " else contents
|
|
540
|
+
val list = pageAnnotations.getOrNull(page) ?: return
|
|
541
|
+
if (index !in list.indices) return
|
|
542
|
+
list[index].contents = contents.ifEmpty { " " }
|
|
541
543
|
overlayViews.getOrNull(page)?.invalidate()
|
|
542
544
|
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
|
-
}
|
|
555
|
-
|
|
556
|
-
private fun fingerprintAnnotations(annotations: List<Map<String, Any>>): String {
|
|
557
|
-
return annotations.mapNotNull { data ->
|
|
558
|
-
val page = data["page"] as? Int ?: return@mapNotNull null
|
|
559
|
-
val type = data["type"] as? String ?: return@mapNotNull null
|
|
560
|
-
val b = data["bounds"] as? Map<*, *> ?: return@mapNotNull null
|
|
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
545
|
}
|
|
568
546
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
return
|
|
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()
|
|
547
|
+
fun updateText(page: Int, index: Int, contents: String) {
|
|
548
|
+
val list = pageAnnotations.getOrNull(page) ?: return
|
|
549
|
+
if (index !in list.indices) return
|
|
550
|
+
list[index].contents = contents.ifEmpty { " " }
|
|
551
|
+
overlayViews.getOrNull(page)?.invalidate()
|
|
580
552
|
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
553
|
}
|
|
666
554
|
|
|
667
555
|
private fun scrollToPage(pageIndex: Int) {
|
|
@@ -677,23 +565,24 @@ class ExpoPdfReaderView(context: Context, appContext: AppContext) : ExpoView(con
|
|
|
677
565
|
scope.launch {
|
|
678
566
|
try {
|
|
679
567
|
resetRenderer()
|
|
568
|
+
val decodedUrl = runCatching { java.net.URLDecoder.decode(url, "UTF-8") }.getOrElse { url }
|
|
680
569
|
val file = withContext(Dispatchers.IO) {
|
|
681
570
|
when {
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
else -> downloadFile(
|
|
571
|
+
decodedUrl.startsWith("file://") -> File(decodedUrl.removePrefix("file://"))
|
|
572
|
+
decodedUrl.startsWith("/") -> File(decodedUrl)
|
|
573
|
+
else -> downloadFile(decodedUrl)
|
|
685
574
|
}
|
|
686
575
|
}
|
|
687
576
|
if (file.exists()) {
|
|
688
577
|
fileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
|
|
689
578
|
pdfRenderer = PdfRenderer(fileDescriptor!!)
|
|
690
579
|
totalPageCount = pdfRenderer!!.pageCount
|
|
691
|
-
undoStack.clear()
|
|
692
|
-
redoStack.clear()
|
|
693
|
-
appliedAnnotationsFingerprint = null
|
|
694
|
-
notifyUndoRedoStateChange()
|
|
695
580
|
renderPdf()
|
|
696
|
-
|
|
581
|
+
post { renderPdf() }
|
|
582
|
+
pendingAnnotations?.let { ann ->
|
|
583
|
+
pendingAnnotations = null
|
|
584
|
+
setInitialAnnotations(ann)
|
|
585
|
+
}
|
|
697
586
|
}
|
|
698
587
|
} catch (e: Exception) {
|
|
699
588
|
android.util.Log.e("ExpoPdfReader", "Error loading PDF", e)
|
|
@@ -707,34 +596,70 @@ class ExpoPdfReaderView(context: Context, appContext: AppContext) : ExpoView(con
|
|
|
707
596
|
fileDescriptor?.close()
|
|
708
597
|
fileDescriptor = null
|
|
709
598
|
pageHeights.clear()
|
|
599
|
+
pageWidthsPdf.clear()
|
|
600
|
+
pageHeightsPdf.clear()
|
|
710
601
|
pageViews.clear()
|
|
711
602
|
overlayViews.clear()
|
|
712
|
-
pageAnnotations.clear()
|
|
713
603
|
container.removeAllViews()
|
|
604
|
+
pageAnnotations.clear()
|
|
605
|
+
undoStack.clear()
|
|
606
|
+
redoStack.clear()
|
|
607
|
+
appliedAnnotationsFingerprint = null
|
|
608
|
+
pendingAnnotations = null
|
|
609
|
+
currentPath = null
|
|
610
|
+
currentPathPoints.clear()
|
|
611
|
+
activePageIndex = -1
|
|
612
|
+
startPoint = null
|
|
613
|
+
draggingAnnotation = null
|
|
614
|
+
draggingPageIndex = -1
|
|
615
|
+
dragStartBounds = null
|
|
616
|
+
dragStartTouch = null
|
|
714
617
|
}
|
|
715
618
|
|
|
716
619
|
private fun renderPdf() {
|
|
717
620
|
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
|
-
|
|
621
|
+
if (width <= 0) {
|
|
622
|
+
if (renderPdfRetryCount < maxRenderPdfRetries) {
|
|
623
|
+
renderPdfRetryCount++
|
|
624
|
+
post { renderPdf() }
|
|
625
|
+
}
|
|
626
|
+
return
|
|
627
|
+
}
|
|
628
|
+
renderPdfRetryCount = 0
|
|
629
|
+
if (pageHeights.isNotEmpty()) {
|
|
630
|
+
updateVisiblePages()
|
|
631
|
+
return
|
|
632
|
+
}
|
|
633
|
+
scope.launch {
|
|
634
|
+
val w = width.coerceAtLeast(1)
|
|
635
|
+
val result = withContext(Dispatchers.Default) {
|
|
636
|
+
(0 until renderer.pageCount).map { i ->
|
|
637
|
+
val page = renderer.openPage(i)
|
|
638
|
+
try {
|
|
639
|
+
Triple(
|
|
640
|
+
i,
|
|
641
|
+
(w.toFloat() * page.height / page.width).toInt(),
|
|
642
|
+
Pair(page.width.toFloat(), page.height.toFloat())
|
|
643
|
+
)
|
|
644
|
+
} finally {
|
|
645
|
+
page.close()
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
if (!isActive) return@launch
|
|
650
|
+
withContext(Dispatchers.Main) {
|
|
651
|
+
pageHeights.clear()
|
|
652
|
+
pageWidthsPdf.clear()
|
|
653
|
+
pageHeightsPdf.clear()
|
|
654
|
+
for ((_, h, pdfSize) in result) {
|
|
655
|
+
pageHeights.add(h)
|
|
656
|
+
pageWidthsPdf.add(pdfSize.first)
|
|
657
|
+
pageHeightsPdf.add(pdfSize.second)
|
|
658
|
+
}
|
|
659
|
+
ensurePlaceholders(renderer)
|
|
660
|
+
scrollToPage(initialPage.coerceIn(0, (renderer.pageCount - 1).coerceAtLeast(0)))
|
|
661
|
+
updateVisiblePages()
|
|
662
|
+
}
|
|
738
663
|
}
|
|
739
664
|
}
|
|
740
665
|
|
|
@@ -743,7 +668,9 @@ class ExpoPdfReaderView(context: Context, appContext: AppContext) : ExpoView(con
|
|
|
743
668
|
val pageCount = renderer.pageCount
|
|
744
669
|
pageViews = MutableList(pageCount) { null }
|
|
745
670
|
overlayViews = MutableList(pageCount) { null }
|
|
746
|
-
while (pageAnnotations.size < pageCount)
|
|
671
|
+
while (pageAnnotations.size < pageCount) {
|
|
672
|
+
pageAnnotations.add(mutableListOf())
|
|
673
|
+
}
|
|
747
674
|
for (i in 0 until pageCount) {
|
|
748
675
|
val frame = FrameLayout(context).apply {
|
|
749
676
|
layoutParams = LinearLayout.LayoutParams(
|
|
@@ -764,25 +691,45 @@ class ExpoPdfReaderView(context: Context, appContext: AppContext) : ExpoView(con
|
|
|
764
691
|
}
|
|
765
692
|
}
|
|
766
693
|
|
|
694
|
+
private fun currentPageFromScroll(): Int {
|
|
695
|
+
if (pageHeights.isEmpty()) return 0
|
|
696
|
+
val scrollY = scrollView.scrollY
|
|
697
|
+
var offset = 0
|
|
698
|
+
for (i in pageHeights.indices) {
|
|
699
|
+
if (scrollY < offset + pageHeights[i] / 2) return i
|
|
700
|
+
offset += pageHeights[i] + 16
|
|
701
|
+
}
|
|
702
|
+
return (pageHeights.size - 1).coerceAtLeast(0)
|
|
703
|
+
}
|
|
704
|
+
|
|
767
705
|
private fun updateVisiblePages() {
|
|
768
706
|
val renderer = pdfRenderer ?: return
|
|
769
|
-
val
|
|
770
|
-
val
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
if (
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
707
|
+
val currentPage = currentPageFromScroll()
|
|
708
|
+
val from = (currentPage - renderWindowBefore).coerceAtLeast(0)
|
|
709
|
+
val to = (currentPage + renderWindowAfter).coerceAtMost(renderer.pageCount - 1)
|
|
710
|
+
|
|
711
|
+
for (i in pageViews.indices) {
|
|
712
|
+
if (i in from..to) continue
|
|
713
|
+
val iv = pageViews.getOrNull(i) ?: continue
|
|
714
|
+
val frame = container.getChildAt(i) as? FrameLayout ?: continue
|
|
715
|
+
val bmp = (iv.drawable as? BitmapDrawable)?.bitmap
|
|
716
|
+
iv.setImageBitmap(null)
|
|
717
|
+
bmp?.recycle()
|
|
718
|
+
frame.removeView(iv)
|
|
719
|
+
pageViews[i] = null
|
|
778
720
|
}
|
|
721
|
+
|
|
722
|
+
val order = (from..to).sortedBy { kotlin.math.abs(it - currentPage) }
|
|
723
|
+
for (i in order) {
|
|
724
|
+
renderPageToView(i)
|
|
725
|
+
}
|
|
726
|
+
container.post { container.invalidate() }
|
|
779
727
|
}
|
|
780
728
|
|
|
781
729
|
private fun renderPageToView(pageIndex: Int) {
|
|
782
730
|
val renderer = pdfRenderer ?: return
|
|
783
|
-
if (pageIndex >= container.childCount) return
|
|
784
731
|
val frame = container.getChildAt(pageIndex) as? FrameLayout ?: return
|
|
785
|
-
|
|
732
|
+
// Frame always has overlay; only skip if we already have an ImageView for this page
|
|
786
733
|
if (pageViews.getOrNull(pageIndex) != null) return
|
|
787
734
|
val page = renderer.openPage(pageIndex)
|
|
788
735
|
val w = width.coerceAtLeast(1)
|
|
@@ -798,11 +745,11 @@ class ExpoPdfReaderView(context: Context, appContext: AppContext) : ExpoView(con
|
|
|
798
745
|
FrameLayout.LayoutParams.MATCH_PARENT
|
|
799
746
|
)
|
|
800
747
|
}
|
|
801
|
-
val overlay = overlayViews[pageIndex]
|
|
802
748
|
frame.addView(imageView, 0)
|
|
803
749
|
pageViews[pageIndex] = imageView
|
|
804
|
-
|
|
805
|
-
|
|
750
|
+
overlayViews.getOrNull(pageIndex)?.bringToFront()
|
|
751
|
+
frame.requestLayout()
|
|
752
|
+
frame.invalidate()
|
|
806
753
|
}
|
|
807
754
|
|
|
808
755
|
private fun notifyPageChange() {
|
|
@@ -828,25 +775,161 @@ class ExpoPdfReaderView(context: Context, appContext: AppContext) : ExpoView(con
|
|
|
828
775
|
updateVisiblePages()
|
|
829
776
|
}
|
|
830
777
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
778
|
+
private fun getAnnotationsForPage(pageIndex: Int): List<Annotation> {
|
|
779
|
+
return pageAnnotations.getOrNull(pageIndex).orEmpty()
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
private fun addAnnotationToPage(pageIndex: Int, ann: Annotation) {
|
|
783
|
+
while (pageAnnotations.size <= pageIndex) {
|
|
784
|
+
pageAnnotations.add(mutableListOf())
|
|
785
|
+
}
|
|
786
|
+
pageAnnotations[pageIndex].add(ann)
|
|
787
|
+
overlayViews.getOrNull(pageIndex)?.invalidate()
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
private fun addInstantAnnotation(pageIndex: Int, pdfX: Float, pdfY: Float, tool: AnnotationTool) {
|
|
791
|
+
val isNote = (tool == AnnotationTool.NOTE)
|
|
792
|
+
val rect = if (isNote) {
|
|
793
|
+
RectF(pdfX, pdfY, pdfX + 32, pdfY + 32)
|
|
794
|
+
} else {
|
|
795
|
+
RectF(pdfX, pdfY, pdfX + 180, pdfY + 40)
|
|
796
|
+
}
|
|
797
|
+
val typeStr = if (isNote) "note" else "text"
|
|
798
|
+
val contents = if (textContent.isEmpty()) " " else textContent
|
|
799
|
+
val ann = Annotation(
|
|
800
|
+
type = typeStr,
|
|
801
|
+
bounds = rect,
|
|
802
|
+
color = if (isNote) noteColor else textColor,
|
|
803
|
+
strokeWidth = 0f,
|
|
804
|
+
paths = mutableListOf(),
|
|
805
|
+
contents = contents,
|
|
806
|
+
fontSize = textFontSize,
|
|
807
|
+
bold = textBold,
|
|
808
|
+
italic = textItalic
|
|
809
|
+
)
|
|
810
|
+
addAnnotationToPage(pageIndex, ann)
|
|
811
|
+
undoStack.add(UndoEntry(ann, pageIndex))
|
|
812
|
+
redoStack.clear()
|
|
813
|
+
notifyAnnotationChange()
|
|
814
|
+
notifyUndoRedoStateChange()
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
private fun eraseAt(pageIndex: Int, pdfX: Float, pdfY: Float) {
|
|
818
|
+
val list = pageAnnotations.getOrNull(pageIndex) ?: return
|
|
819
|
+
var erased = false
|
|
820
|
+
val toRemove = list.filter { it.bounds.contains(pdfX, pdfY) }
|
|
821
|
+
for (ann in toRemove) {
|
|
822
|
+
list.remove(ann)
|
|
823
|
+
undoStack.removeAll { it.annotation == ann }
|
|
824
|
+
erased = true
|
|
825
|
+
}
|
|
826
|
+
if (erased) {
|
|
827
|
+
overlayViews.getOrNull(pageIndex)?.invalidate()
|
|
828
|
+
notifyAnnotationChange()
|
|
829
|
+
notifyUndoRedoStateChange()
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
private fun commitStroke(tool: AnnotationTool) {
|
|
834
|
+
if (currentPathPoints.size < 2) return
|
|
835
|
+
val minX = currentPathPoints.minOf { it.x } - strokeWidth
|
|
836
|
+
val minY = currentPathPoints.minOf { it.y } - strokeWidth
|
|
837
|
+
val maxX = currentPathPoints.maxOf { it.x } + strokeWidth
|
|
838
|
+
val maxY = currentPathPoints.maxOf { it.y } + strokeWidth
|
|
839
|
+
val bounds = RectF(minX, minY, maxX, maxY)
|
|
840
|
+
val paths = mutableListOf<MutableList<PointF>>()
|
|
841
|
+
val points = currentPathPoints.map { PointF(it.x, it.y) }.toMutableList()
|
|
842
|
+
paths.add(points)
|
|
843
|
+
val typeStr = when (tool) {
|
|
844
|
+
AnnotationTool.PEN -> "pen"
|
|
845
|
+
AnnotationTool.HIGHLIGHTER -> "highlighter"
|
|
846
|
+
AnnotationTool.LINE -> "line"
|
|
847
|
+
else -> "pen"
|
|
848
|
+
}
|
|
849
|
+
val color = if (tool == AnnotationTool.HIGHLIGHTER) {
|
|
850
|
+
Color.argb(102, Color.red(strokeColor), Color.green(strokeColor), Color.blue(strokeColor))
|
|
851
|
+
} else {
|
|
852
|
+
strokeColor
|
|
853
|
+
}
|
|
854
|
+
val ann = Annotation(typeStr, bounds, color, strokeWidth, paths, "", 0f, false, false)
|
|
855
|
+
addAnnotationToPage(activePageIndex, ann)
|
|
856
|
+
undoStack.add(UndoEntry(ann, activePageIndex))
|
|
857
|
+
redoStack.clear()
|
|
858
|
+
notifyAnnotationChange()
|
|
859
|
+
notifyUndoRedoStateChange()
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
private fun notifyAnnotationChange() {
|
|
863
|
+
val list = mutableListOf<Map<String, Any>>()
|
|
864
|
+
for (pageIndex in pageAnnotations.indices) {
|
|
865
|
+
for (ann in getAnnotationsForPage(pageIndex)) {
|
|
866
|
+
list.add(mapOf(
|
|
867
|
+
"page" to pageIndex,
|
|
868
|
+
"bounds" to mapOf(
|
|
869
|
+
"x" to ann.bounds.left,
|
|
870
|
+
"y" to ann.bounds.top,
|
|
871
|
+
"width" to ann.bounds.width(),
|
|
872
|
+
"height" to ann.bounds.height()
|
|
873
|
+
),
|
|
874
|
+
"type" to ann.type,
|
|
875
|
+
"color" to colorToHex(ann.color),
|
|
876
|
+
"contents" to ann.contents,
|
|
877
|
+
"paths" to ann.paths.map { path ->
|
|
878
|
+
path.map { p -> mapOf("x" to p.x, "y" to p.y) }
|
|
879
|
+
},
|
|
880
|
+
"strokeWidth" to ann.strokeWidth,
|
|
881
|
+
"fontSize" to ann.fontSize,
|
|
882
|
+
"bold" to ann.bold,
|
|
883
|
+
"italic" to ann.italic
|
|
884
|
+
))
|
|
885
|
+
}
|
|
835
886
|
}
|
|
836
|
-
|
|
887
|
+
onAnnotationChange(mapOf("annotations" to list))
|
|
837
888
|
}
|
|
838
889
|
|
|
839
|
-
private fun
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
890
|
+
private fun notifyUndoRedoStateChange() {
|
|
891
|
+
onUndoRedoStateChange(mapOf(
|
|
892
|
+
"canUndo" to undoStack.isNotEmpty(),
|
|
893
|
+
"canRedo" to redoStack.isNotEmpty()
|
|
894
|
+
))
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
private fun parseHexColor(hex: String): Int {
|
|
898
|
+
var s = hex.trim().uppercase()
|
|
899
|
+
if (s.startsWith("#")) s = s.removePrefix("#")
|
|
900
|
+
val v = s.toLongOrNull(16) ?: 0L
|
|
901
|
+
val r = ((v shr 16) and 0xFF).toInt()
|
|
902
|
+
val g = ((v shr 8) and 0xFF).toInt()
|
|
903
|
+
val b = (v and 0xFF).toInt()
|
|
904
|
+
return Color.rgb(r, g, b)
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
private fun fingerprintAnnotations(annotations: List<Map<String, Any>>): String {
|
|
908
|
+
val items = annotations.mapNotNull { data ->
|
|
909
|
+
val pageIndex = (data["page"] as? Number)?.toInt() ?: return@mapNotNull null
|
|
910
|
+
val typeStr = data["type"] as? String ?: return@mapNotNull null
|
|
911
|
+
val b = data["bounds"] as? Map<*, *> ?: return@mapNotNull null
|
|
912
|
+
val x = (b["x"] as? Number)?.toDouble() ?: 0.0
|
|
913
|
+
val y = (b["y"] as? Number)?.toDouble() ?: 0.0
|
|
914
|
+
val w = (b["width"] as? Number)?.toDouble() ?: 0.0
|
|
915
|
+
val h = (b["height"] as? Number)?.toDouble() ?: 0.0
|
|
916
|
+
val color = data["color"] as? String ?: ""
|
|
917
|
+
val strokeW = data["strokeWidth"] as? Double ?: 0.0
|
|
918
|
+
val contents = data["contents"] as? String ?: ""
|
|
919
|
+
var ptsCount = 0
|
|
920
|
+
(data["paths"] as? List<*>)?.forEach { pathData ->
|
|
921
|
+
(pathData as? List<*>)?.forEach { ptsCount++ }
|
|
847
922
|
}
|
|
923
|
+
"$pageIndex|$typeStr|$x,$y,$w,$h|$color|$strokeW|$contents|pts:$ptsCount"
|
|
848
924
|
}
|
|
849
|
-
return
|
|
925
|
+
return items.sorted().joinToString("||")
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
private fun colorToHex(color: Int): String {
|
|
929
|
+
val r = Color.red(color)
|
|
930
|
+
val g = Color.green(color)
|
|
931
|
+
val b = Color.blue(color)
|
|
932
|
+
return String.format("#%02x%02x%02x", r, g, b)
|
|
850
933
|
}
|
|
851
934
|
|
|
852
935
|
private suspend fun downloadFile(urlStr: String): File = withContext(Dispatchers.IO) {
|