@june24/expo-pdf-reader 0.1.0
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.
- package/README.md +50 -0
- package/android/build.gradle +23 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/expo/modules/pdfreader/ExpoPdfReaderModule.kt +202 -0
- package/android/src/main/java/expo/modules/pdfreader/ExpoPdfReaderView.kt +934 -0
- package/build/ExpoPdfReader.types.d.ts +85 -0
- package/build/ExpoPdfReader.types.d.ts.map +1 -0
- package/build/ExpoPdfReader.types.js +2 -0
- package/build/ExpoPdfReader.types.js.map +1 -0
- package/build/ExpoPdfReaderView.d.ts +28 -0
- package/build/ExpoPdfReaderView.d.ts.map +1 -0
- package/build/ExpoPdfReaderView.js +107 -0
- package/build/ExpoPdfReaderView.js.map +1 -0
- package/build/index.d.ts +4 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +3 -0
- package/build/index.js.map +1 -0
- package/expo-module.config.json +16 -0
- package/ios/ExpoPdfReader.podspec +27 -0
- package/ios/ExpoPdfReaderModule.swift +170 -0
- package/ios/ExpoPdfReaderView.swift +675 -0
- package/package.json +37 -0
- package/src/ExpoPdfReader.types.ts +99 -0
- package/src/ExpoPdfReaderView.tsx +137 -0
- package/src/index.ts +4 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import PDFKit
|
|
3
|
+
|
|
4
|
+
class ExpoPdfReaderView: ExpoView {
|
|
5
|
+
let pdfView = PDFView()
|
|
6
|
+
var currentTool: String = "none"
|
|
7
|
+
var currentColor: UIColor = .black
|
|
8
|
+
var currentFontSize: CGFloat = 16.0
|
|
9
|
+
var currentText: String = "Text"
|
|
10
|
+
var currentStrokeWidth: CGFloat = 2.0
|
|
11
|
+
var initialPage: Int = 0
|
|
12
|
+
var minZoom: CGFloat = 1.0
|
|
13
|
+
var maxZoom: CGFloat = 4.0
|
|
14
|
+
|
|
15
|
+
// Events
|
|
16
|
+
let onAnnotationChange = EventDispatcher()
|
|
17
|
+
let onPageChange = EventDispatcher()
|
|
18
|
+
let onScroll = EventDispatcher()
|
|
19
|
+
|
|
20
|
+
// Gesture recognizers for drawing
|
|
21
|
+
private var panGesture: UIPanGestureRecognizer?
|
|
22
|
+
private var tapGesture: UITapGestureRecognizer?
|
|
23
|
+
private var currentAnnotation: PDFAnnotation?
|
|
24
|
+
private var currentPath: UIBezierPath?
|
|
25
|
+
|
|
26
|
+
// Undo/Redo stacks
|
|
27
|
+
private var undoStack: [PDFAnnotation] = []
|
|
28
|
+
private var redoStack: [PDFAnnotation] = []
|
|
29
|
+
|
|
30
|
+
required init(appContext: AppContext? = nil) {
|
|
31
|
+
super.init(appContext: appContext)
|
|
32
|
+
clipsToBounds = true
|
|
33
|
+
addSubview(pdfView)
|
|
34
|
+
|
|
35
|
+
pdfView.autoScales = true
|
|
36
|
+
pdfView.displayMode = .singlePageContinuous
|
|
37
|
+
pdfView.displayDirection = .vertical
|
|
38
|
+
pdfView.minScaleFactor = minZoom
|
|
39
|
+
pdfView.maxScaleFactor = maxZoom
|
|
40
|
+
|
|
41
|
+
// Setup gestures
|
|
42
|
+
panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
|
|
43
|
+
tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
|
|
44
|
+
|
|
45
|
+
// Initially disabled
|
|
46
|
+
panGesture?.isEnabled = false
|
|
47
|
+
tapGesture?.isEnabled = false
|
|
48
|
+
|
|
49
|
+
pdfView.addGestureRecognizer(panGesture!)
|
|
50
|
+
pdfView.addGestureRecognizer(tapGesture!)
|
|
51
|
+
|
|
52
|
+
// Listen for page changes
|
|
53
|
+
NotificationCenter.default.addObserver(self, selector: #selector(handlePageChange), name: .PDFViewPageChanged, object: pdfView)
|
|
54
|
+
|
|
55
|
+
// Setup scroll observer
|
|
56
|
+
setupScrollObserver()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private func setupScrollObserver() {
|
|
60
|
+
// Use KVO to observe scroll position changes
|
|
61
|
+
pdfView.addObserver(self, forKeyPath: "bounds", options: [.new, .old], context: nil)
|
|
62
|
+
|
|
63
|
+
// Also observe content offset if available
|
|
64
|
+
if let scrollView = pdfView.subviews.first(where: { $0 is UIScrollView }) as? UIScrollView {
|
|
65
|
+
scrollView.addObserver(self, forKeyPath: "contentOffset", options: [.new], context: nil)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
|
70
|
+
if keyPath == "bounds" || keyPath == "contentOffset" {
|
|
71
|
+
notifyScroll()
|
|
72
|
+
} else {
|
|
73
|
+
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private func notifyScroll() {
|
|
78
|
+
let bounds = pdfView.bounds
|
|
79
|
+
let contentSize = pdfView.document?.bounds(for: .mediaBox).size ?? .zero
|
|
80
|
+
|
|
81
|
+
// Get scroll position from PDFView
|
|
82
|
+
let visibleRect = pdfView.visiblePageRect
|
|
83
|
+
let scrollX = visibleRect.origin.x
|
|
84
|
+
let scrollY = visibleRect.origin.y
|
|
85
|
+
|
|
86
|
+
onScroll([
|
|
87
|
+
"x": scrollX,
|
|
88
|
+
"y": scrollY,
|
|
89
|
+
"contentWidth": contentSize.width,
|
|
90
|
+
"contentHeight": contentSize.height,
|
|
91
|
+
"layoutWidth": bounds.width,
|
|
92
|
+
"layoutHeight": bounds.height
|
|
93
|
+
])
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
deinit {
|
|
97
|
+
NotificationCenter.default.removeObserver(self)
|
|
98
|
+
|
|
99
|
+
// Remove KVO observers
|
|
100
|
+
pdfView.removeObserver(self, forKeyPath: "bounds")
|
|
101
|
+
if let scrollView = pdfView.subviews.first(where: { $0 is UIScrollView }) as? UIScrollView {
|
|
102
|
+
scrollView.removeObserver(self, forKeyPath: "contentOffset")
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
override func layoutSubviews() {
|
|
107
|
+
pdfView.frame = bounds
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
func load(url: URL) {
|
|
111
|
+
if url.scheme == "http" || url.scheme == "https" {
|
|
112
|
+
DispatchQueue.global().async {
|
|
113
|
+
if let data = try? Data(contentsOf: url), let document = PDFDocument(data: data) {
|
|
114
|
+
DispatchQueue.main.async {
|
|
115
|
+
self.pdfView.document = document
|
|
116
|
+
self.applyInitialPageIfNeeded()
|
|
117
|
+
self.notifyPageChange()
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
if let document = PDFDocument(url: url) {
|
|
123
|
+
pdfView.document = document
|
|
124
|
+
applyInitialPageIfNeeded()
|
|
125
|
+
notifyPageChange()
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@objc func handlePageChange() {
|
|
131
|
+
notifyPageChange()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
func notifyPageChange() {
|
|
135
|
+
guard let document = pdfView.document, let currentPage = pdfView.currentPage else { return }
|
|
136
|
+
let pageIndex = document.index(for: currentPage)
|
|
137
|
+
|
|
138
|
+
onPageChange([
|
|
139
|
+
"page": pageIndex,
|
|
140
|
+
"total": document.pageCount
|
|
141
|
+
])
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
func setDisplayMode(_ mode: String) {
|
|
145
|
+
switch mode {
|
|
146
|
+
case "single":
|
|
147
|
+
pdfView.displayMode = .singlePage
|
|
148
|
+
case "continuous":
|
|
149
|
+
pdfView.displayMode = .singlePageContinuous
|
|
150
|
+
case "twoUp":
|
|
151
|
+
pdfView.displayMode = .twoUp
|
|
152
|
+
case "twoUpContinuous":
|
|
153
|
+
pdfView.displayMode = .twoUpContinuous
|
|
154
|
+
default:
|
|
155
|
+
pdfView.displayMode = .singlePageContinuous
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
func setInitialPage(_ page: Int) {
|
|
160
|
+
self.initialPage = max(0, page)
|
|
161
|
+
applyInitialPageIfNeeded()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private func applyInitialPageIfNeeded() {
|
|
165
|
+
guard let document = pdfView.document else { return }
|
|
166
|
+
let index = max(0, min(initialPage, document.pageCount - 1))
|
|
167
|
+
if let page = document.page(at: index) {
|
|
168
|
+
pdfView.go(to: page)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
func zoomIn() -> Bool {
|
|
173
|
+
guard pdfView.document != nil else { return false }
|
|
174
|
+
let newFactor = min(pdfView.scaleFactor * 1.25, maxZoom)
|
|
175
|
+
if newFactor == pdfView.scaleFactor { return false }
|
|
176
|
+
pdfView.scaleFactor = newFactor
|
|
177
|
+
return true
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
func zoomOut() -> Bool {
|
|
181
|
+
guard pdfView.document != nil else { return false }
|
|
182
|
+
let newFactor = max(pdfView.scaleFactor / 1.25, minZoom)
|
|
183
|
+
if newFactor == pdfView.scaleFactor { return false }
|
|
184
|
+
pdfView.scaleFactor = newFactor
|
|
185
|
+
return true
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
func setAnnotationTool(_ tool: String) {
|
|
189
|
+
self.currentTool = tool
|
|
190
|
+
|
|
191
|
+
// Enable/disable gestures based on tool
|
|
192
|
+
let isDrawing = tool == "pen" || tool == "highlighter"
|
|
193
|
+
let isEraser = tool == "eraser"
|
|
194
|
+
panGesture?.isEnabled = isDrawing || isEraser
|
|
195
|
+
pdfView.isUserInteractionEnabled = !isDrawing && !isEraser // Disable PDF interaction when drawing/erasing to prevent scrolling
|
|
196
|
+
|
|
197
|
+
// Re-enable scrolling if not drawing or erasing
|
|
198
|
+
if !isDrawing && !isEraser {
|
|
199
|
+
pdfView.isUserInteractionEnabled = true
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if tool == "text" || tool == "note" {
|
|
203
|
+
tapGesture?.isEnabled = true
|
|
204
|
+
} else {
|
|
205
|
+
tapGesture?.isEnabled = false
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
func setAnnotationColor(_ colorHex: String?) {
|
|
210
|
+
if let hex = colorHex {
|
|
211
|
+
self.currentColor = UIColor(hex: hex) ?? .black
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
func setAnnotationFontSize(_ size: Double) {
|
|
216
|
+
self.currentFontSize = CGFloat(size)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
func setAnnotationText(_ text: String) {
|
|
220
|
+
self.currentText = text
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
func setAnnotationStrokeWidth(_ width: Double) {
|
|
224
|
+
// Avoid zero / negative
|
|
225
|
+
let w = CGFloat(width)
|
|
226
|
+
self.currentStrokeWidth = w > 0 ? w : 1.0
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
func setMinZoom(_ value: Double) {
|
|
230
|
+
let v = CGFloat(value)
|
|
231
|
+
minZoom = v > 0 ? v : 0.5
|
|
232
|
+
pdfView.minScaleFactor = minZoom
|
|
233
|
+
if pdfView.scaleFactor < minZoom {
|
|
234
|
+
pdfView.scaleFactor = minZoom
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
func setMaxZoom(_ value: Double) {
|
|
239
|
+
let v = CGFloat(value)
|
|
240
|
+
maxZoom = v > minZoom ? v : minZoom
|
|
241
|
+
pdfView.maxScaleFactor = maxZoom
|
|
242
|
+
if pdfView.scaleFactor > maxZoom {
|
|
243
|
+
pdfView.scaleFactor = maxZoom
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
func searchText(_ text: String) -> [[String: Any]] {
|
|
248
|
+
guard let document = pdfView.document else { return [] }
|
|
249
|
+
|
|
250
|
+
let selections = document.findString(text, withOptions: .caseInsensitive)
|
|
251
|
+
var results: [[String: Any]] = []
|
|
252
|
+
|
|
253
|
+
for selection in selections {
|
|
254
|
+
guard let page = selection.pages.first else { continue }
|
|
255
|
+
let pageIndex = document.index(for: page)
|
|
256
|
+
let bounds = selection.bounds(for: page)
|
|
257
|
+
|
|
258
|
+
let highlight = PDFAnnotation(bounds: bounds, forType: .highlight, withProperties: nil)
|
|
259
|
+
highlight.color = .yellow.withAlphaComponent(0.5)
|
|
260
|
+
page.addAnnotation(highlight)
|
|
261
|
+
|
|
262
|
+
results.append([
|
|
263
|
+
"page": pageIndex,
|
|
264
|
+
"x": bounds.origin.x,
|
|
265
|
+
"y": bounds.origin.y,
|
|
266
|
+
"width": bounds.size.width,
|
|
267
|
+
"height": bounds.size.height,
|
|
268
|
+
"textSnippet": selection.string ?? ""
|
|
269
|
+
])
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return results
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
func goToPage(_ pageIndex: Int) {
|
|
276
|
+
if let document = pdfView.document,
|
|
277
|
+
let page = document.page(at: pageIndex) {
|
|
278
|
+
pdfView.go(to: page)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
func undo() -> Bool {
|
|
283
|
+
guard let document = pdfView.document else { return false }
|
|
284
|
+
|
|
285
|
+
// Find the last annotation across all pages
|
|
286
|
+
var lastAnnotation: PDFAnnotation?
|
|
287
|
+
var lastPage: PDFPage?
|
|
288
|
+
|
|
289
|
+
for i in (0..<document.pageCount).reversed() {
|
|
290
|
+
guard let page = document.page(at: i) else { continue }
|
|
291
|
+
if let annotation = page.annotations.last {
|
|
292
|
+
lastAnnotation = annotation
|
|
293
|
+
lastPage = page
|
|
294
|
+
break
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
guard let annotation = lastAnnotation, let page = lastPage else { return false }
|
|
299
|
+
|
|
300
|
+
// Remove from page and add to undo stack
|
|
301
|
+
page.removeAnnotation(annotation)
|
|
302
|
+
undoStack.append(annotation)
|
|
303
|
+
redoStack.removeAll() // Clear redo when new undo is performed
|
|
304
|
+
|
|
305
|
+
// Emit event
|
|
306
|
+
onAnnotationChange([
|
|
307
|
+
"annotations": getAnnotations()
|
|
308
|
+
])
|
|
309
|
+
|
|
310
|
+
return true
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
func redo() -> Bool {
|
|
314
|
+
guard undoStack.isEmpty == false else { return false }
|
|
315
|
+
guard let document = pdfView.document else { return false }
|
|
316
|
+
|
|
317
|
+
// Get the last undone annotation
|
|
318
|
+
let annotation = undoStack.removeLast()
|
|
319
|
+
|
|
320
|
+
// Find which page it belongs to (we'll need to track this better in a real implementation)
|
|
321
|
+
// For now, try to add it to the current page
|
|
322
|
+
if let currentPage = pdfView.currentPage {
|
|
323
|
+
currentPage.addAnnotation(annotation)
|
|
324
|
+
} else if let firstPage = document.page(at: 0) {
|
|
325
|
+
firstPage.addAnnotation(annotation)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Emit event
|
|
329
|
+
onAnnotationChange([
|
|
330
|
+
"annotations": getAnnotations()
|
|
331
|
+
])
|
|
332
|
+
|
|
333
|
+
return true
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
func renderThumbnail(pageIndex: Int, width: Double) throws -> String {
|
|
337
|
+
guard let document = pdfView.document else {
|
|
338
|
+
throw NSError(domain: "ExpoPdfReader", code: 3, userInfo: [NSLocalizedDescriptionKey: "No document loaded"])
|
|
339
|
+
}
|
|
340
|
+
guard let page = document.page(at: pageIndex) else {
|
|
341
|
+
throw NSError(domain: "ExpoPdfReader", code: 4, userInfo: [NSLocalizedDescriptionKey: "Invalid page index"])
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
let pageRect = page.bounds(for: .mediaBox)
|
|
345
|
+
let targetWidth = CGFloat(width)
|
|
346
|
+
let scale = targetWidth / pageRect.width
|
|
347
|
+
let size = CGSize(width: targetWidth, height: pageRect.height * scale)
|
|
348
|
+
|
|
349
|
+
let image = page.thumbnail(of: size, for: .mediaBox)
|
|
350
|
+
guard let data = image.pngData() else {
|
|
351
|
+
throw NSError(domain: "ExpoPdfReader", code: 5, userInfo: [NSLocalizedDescriptionKey: "Failed to create PNG data"])
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
let fileName = "thumb_\(pageIndex)_\(Int(Date().timeIntervalSince1970)).png"
|
|
355
|
+
let url = FileManager.default.temporaryDirectory.appendingPathComponent(fileName)
|
|
356
|
+
try data.write(to: url)
|
|
357
|
+
return url.absoluteString
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
func savePdf() throws -> String {
|
|
361
|
+
guard let document = pdfView.document else {
|
|
362
|
+
throw NSError(domain: "ExpoPdfReader", code: 1, userInfo: [NSLocalizedDescriptionKey: "No document loaded"])
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
let fileName = "saved_pdf_\(Int(Date().timeIntervalSince1970)).pdf"
|
|
366
|
+
let fileManager = FileManager.default
|
|
367
|
+
let tempDir = fileManager.temporaryDirectory
|
|
368
|
+
let fileURL = tempDir.appendingPathComponent(fileName)
|
|
369
|
+
|
|
370
|
+
if document.write(to: fileURL) {
|
|
371
|
+
return fileURL.absoluteString
|
|
372
|
+
} else {
|
|
373
|
+
throw NSError(domain: "ExpoPdfReader", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to write PDF to file"])
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
func getAnnotations() -> [[String: Any]] {
|
|
378
|
+
guard let document = pdfView.document else { return [] }
|
|
379
|
+
var annotationsData: [[String: Any]] = []
|
|
380
|
+
|
|
381
|
+
for i in 0..<document.pageCount {
|
|
382
|
+
guard let page = document.page(at: i) else { continue }
|
|
383
|
+
for annotation in page.annotations {
|
|
384
|
+
if annotation.type == "Ink" {
|
|
385
|
+
var points: [[String: Double]] = []
|
|
386
|
+
|
|
387
|
+
// Simplified extraction logic
|
|
388
|
+
|
|
389
|
+
var colorHex = "#000000"
|
|
390
|
+
if let color = annotation.color {
|
|
391
|
+
colorHex = color.toHex() ?? "#000000"
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
var type = "pen"
|
|
395
|
+
if let color = annotation.color {
|
|
396
|
+
var a: CGFloat = 0
|
|
397
|
+
color.getWhite(nil, alpha: &a)
|
|
398
|
+
if a < 0.5 {
|
|
399
|
+
type = "highlighter"
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
annotationsData.append([
|
|
404
|
+
"type": type,
|
|
405
|
+
"color": colorHex,
|
|
406
|
+
"page": i,
|
|
407
|
+
"points": [points],
|
|
408
|
+
"width": annotation.border?.lineWidth ?? 10.0
|
|
409
|
+
])
|
|
410
|
+
} else if annotation.type == "FreeText" {
|
|
411
|
+
var colorHex = "#000000"
|
|
412
|
+
if let color = annotation.fontColor {
|
|
413
|
+
colorHex = color.toHex() ?? "#000000"
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
annotationsData.append([
|
|
417
|
+
"type": "text",
|
|
418
|
+
"color": colorHex,
|
|
419
|
+
"page": i,
|
|
420
|
+
"text": annotation.contents ?? "",
|
|
421
|
+
"fontSize": annotation.font?.pointSize ?? 16.0,
|
|
422
|
+
"x": annotation.bounds.origin.x,
|
|
423
|
+
"y": annotation.bounds.origin.y
|
|
424
|
+
])
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return annotationsData
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
func setAnnotations(_ annotations: [[String: Any]]) {
|
|
432
|
+
guard let document = pdfView.document else { return }
|
|
433
|
+
|
|
434
|
+
// Clear undo/redo stacks when setting annotations
|
|
435
|
+
undoStack.removeAll()
|
|
436
|
+
redoStack.removeAll()
|
|
437
|
+
|
|
438
|
+
for data in annotations {
|
|
439
|
+
guard let pageIndex = data["page"] as? Int,
|
|
440
|
+
let page = document.page(at: pageIndex),
|
|
441
|
+
let type = data["type"] as? String,
|
|
442
|
+
let colorHex = data["color"] as? String else { continue }
|
|
443
|
+
|
|
444
|
+
let color = UIColor(hex: colorHex) ?? .black
|
|
445
|
+
|
|
446
|
+
if type == "text" {
|
|
447
|
+
guard let text = data["text"] as? String,
|
|
448
|
+
let fontSize = data["fontSize"] as? Double,
|
|
449
|
+
let x = data["x"] as? Double,
|
|
450
|
+
let y = data["y"] as? Double else { continue }
|
|
451
|
+
|
|
452
|
+
let annotation = PDFAnnotation(bounds: CGRect(x: x, y: y, width: 200, height: 50), forType: .freeText, withProperties: nil)
|
|
453
|
+
annotation.contents = text
|
|
454
|
+
annotation.font = UIFont.systemFont(ofSize: CGFloat(fontSize))
|
|
455
|
+
annotation.fontColor = color
|
|
456
|
+
annotation.color = .clear
|
|
457
|
+
page.addAnnotation(annotation)
|
|
458
|
+
|
|
459
|
+
} else {
|
|
460
|
+
guard let pointsArray = data["points"] as? [[[String: Double]]] else { continue }
|
|
461
|
+
|
|
462
|
+
for stroke in pointsArray {
|
|
463
|
+
if stroke.isEmpty { continue }
|
|
464
|
+
|
|
465
|
+
let path = UIBezierPath()
|
|
466
|
+
let start = stroke[0]
|
|
467
|
+
path.move(to: CGPoint(x: start["x"]!, y: start["y"]!))
|
|
468
|
+
|
|
469
|
+
for i in 1..<stroke.count {
|
|
470
|
+
let pt = stroke[i]
|
|
471
|
+
path.addLine(to: CGPoint(x: pt["x"]!, y: pt["y"]!))
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
let bounds = page.bounds(for: pdfView.displayBox)
|
|
475
|
+
|
|
476
|
+
let annotation = PDFAnnotation(bounds: bounds, forType: .ink, withProperties: nil)
|
|
477
|
+
annotation.add(path)
|
|
478
|
+
|
|
479
|
+
if type == "highlighter" {
|
|
480
|
+
annotation.color = color.withAlphaComponent(0.3)
|
|
481
|
+
} else {
|
|
482
|
+
annotation.color = color
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
page.addAnnotation(annotation)
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
@objc func handlePan(_ gesture: UIPanGestureRecognizer) {
|
|
492
|
+
guard let page = pdfView.currentPage else { return }
|
|
493
|
+
let location = gesture.location(in: pdfView)
|
|
494
|
+
let convertedPoint = pdfView.convert(location, to: page)
|
|
495
|
+
|
|
496
|
+
// Handle eraser
|
|
497
|
+
if currentTool == "eraser" {
|
|
498
|
+
switch gesture.state {
|
|
499
|
+
case .began, .changed:
|
|
500
|
+
// Find annotation at touch point
|
|
501
|
+
let tolerance: CGFloat = 20.0
|
|
502
|
+
for annotation in page.annotations {
|
|
503
|
+
let bounds = annotation.bounds
|
|
504
|
+
let expandedBounds = bounds.insetBy(dx: -tolerance, dy: -tolerance)
|
|
505
|
+
|
|
506
|
+
if expandedBounds.contains(convertedPoint) {
|
|
507
|
+
// Check if point is actually on the annotation path
|
|
508
|
+
if let paths = annotation.paths {
|
|
509
|
+
for path in paths {
|
|
510
|
+
let cgPath = path.cgPath
|
|
511
|
+
if cgPath.contains(convertedPoint) || cgPath.boundingBox.contains(convertedPoint) {
|
|
512
|
+
page.removeAnnotation(annotation)
|
|
513
|
+
undoStack.append(annotation)
|
|
514
|
+
redoStack.removeAll()
|
|
515
|
+
|
|
516
|
+
// Emit event
|
|
517
|
+
onAnnotationChange([
|
|
518
|
+
"annotations": getAnnotations()
|
|
519
|
+
])
|
|
520
|
+
return
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
} else {
|
|
524
|
+
// For non-path annotations (like text), just check bounds
|
|
525
|
+
page.removeAnnotation(annotation)
|
|
526
|
+
undoStack.append(annotation)
|
|
527
|
+
redoStack.removeAll()
|
|
528
|
+
|
|
529
|
+
// Emit event
|
|
530
|
+
onAnnotationChange([
|
|
531
|
+
"annotations": getAnnotations()
|
|
532
|
+
])
|
|
533
|
+
return
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
default:
|
|
538
|
+
break
|
|
539
|
+
}
|
|
540
|
+
return
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
switch gesture.state {
|
|
544
|
+
case .began:
|
|
545
|
+
currentPath = UIBezierPath()
|
|
546
|
+
currentPath?.move(to: convertedPoint)
|
|
547
|
+
|
|
548
|
+
// Create annotation
|
|
549
|
+
let bounds = page.bounds(for: pdfView.displayBox)
|
|
550
|
+
|
|
551
|
+
if currentTool == "pen" {
|
|
552
|
+
let annotation = PDFAnnotation(bounds: bounds, forType: .ink, withProperties: nil)
|
|
553
|
+
annotation.color = currentColor
|
|
554
|
+
let border = PDFBorder()
|
|
555
|
+
border.lineWidth = currentStrokeWidth
|
|
556
|
+
annotation.border = border
|
|
557
|
+
annotation.add(currentPath!)
|
|
558
|
+
page.addAnnotation(annotation)
|
|
559
|
+
currentAnnotation = annotation
|
|
560
|
+
}
|
|
561
|
+
else if currentTool == "highlighter" {
|
|
562
|
+
let annotation = PDFAnnotation(bounds: bounds, forType: .ink, withProperties: nil)
|
|
563
|
+
annotation.color = currentColor.withAlphaComponent(0.3)
|
|
564
|
+
let border = PDFBorder()
|
|
565
|
+
border.lineWidth = currentStrokeWidth * 3.0
|
|
566
|
+
annotation.border = border
|
|
567
|
+
annotation.add(currentPath!)
|
|
568
|
+
page.addAnnotation(annotation)
|
|
569
|
+
currentAnnotation = annotation
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
case .changed:
|
|
573
|
+
guard let path = currentPath, let annotation = currentAnnotation else { return }
|
|
574
|
+
path.addLine(to: convertedPoint)
|
|
575
|
+
annotation.add(path) // Update path
|
|
576
|
+
|
|
577
|
+
case .ended, .cancelled:
|
|
578
|
+
// Clear redo stack when new annotation is added
|
|
579
|
+
redoStack.removeAll()
|
|
580
|
+
|
|
581
|
+
currentPath = nil
|
|
582
|
+
currentAnnotation = nil
|
|
583
|
+
|
|
584
|
+
// Emit event
|
|
585
|
+
onAnnotationChange([
|
|
586
|
+
"annotations": getAnnotations()
|
|
587
|
+
])
|
|
588
|
+
|
|
589
|
+
default:
|
|
590
|
+
break
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
@objc func handleTap(_ gesture: UITapGestureRecognizer) {
|
|
595
|
+
guard let page = pdfView.currentPage else { return }
|
|
596
|
+
let location = gesture.location(in: pdfView)
|
|
597
|
+
let convertedPoint = pdfView.convert(location, to: page)
|
|
598
|
+
|
|
599
|
+
if currentTool == "text" {
|
|
600
|
+
let annotation = PDFAnnotation(bounds: CGRect(x: convertedPoint.x, y: convertedPoint.y, width: 200, height: 50), forType: .freeText, withProperties: nil)
|
|
601
|
+
annotation.contents = currentText
|
|
602
|
+
annotation.font = UIFont.systemFont(ofSize: currentFontSize)
|
|
603
|
+
annotation.fontColor = currentColor
|
|
604
|
+
annotation.color = .clear // Background
|
|
605
|
+
page.addAnnotation(annotation)
|
|
606
|
+
|
|
607
|
+
// Clear redo stack when new annotation is added
|
|
608
|
+
redoStack.removeAll()
|
|
609
|
+
|
|
610
|
+
onAnnotationChange([
|
|
611
|
+
"annotations": getAnnotations()
|
|
612
|
+
])
|
|
613
|
+
|
|
614
|
+
} else if currentTool == "note" {
|
|
615
|
+
let annotation = PDFAnnotation(bounds: CGRect(x: convertedPoint.x, y: convertedPoint.y, width: 20, height: 20), forType: .text, withProperties: nil)
|
|
616
|
+
annotation.iconType = .comment
|
|
617
|
+
annotation.color = currentColor
|
|
618
|
+
page.addAnnotation(annotation)
|
|
619
|
+
|
|
620
|
+
// Clear redo stack when new annotation is added
|
|
621
|
+
redoStack.removeAll()
|
|
622
|
+
|
|
623
|
+
onAnnotationChange([
|
|
624
|
+
"annotations": getAnnotations()
|
|
625
|
+
])
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
extension UIColor {
|
|
631
|
+
convenience init?(hex: String) {
|
|
632
|
+
var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
633
|
+
hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "")
|
|
634
|
+
|
|
635
|
+
var rgb: UInt64 = 0
|
|
636
|
+
|
|
637
|
+
var r: CGFloat = 0.0
|
|
638
|
+
var g: CGFloat = 0.0
|
|
639
|
+
var b: CGFloat = 0.0
|
|
640
|
+
var a: CGFloat = 1.0
|
|
641
|
+
|
|
642
|
+
let length = hexSanitized.count
|
|
643
|
+
|
|
644
|
+
guard Scanner(string: hexSanitized).scanHexInt64(&rgb) else { return nil }
|
|
645
|
+
|
|
646
|
+
if length == 6 {
|
|
647
|
+
r = CGFloat((rgb & 0xFF0000) >> 16) / 255.0
|
|
648
|
+
g = CGFloat((rgb & 0x00FF00) >> 8) / 255.0
|
|
649
|
+
b = CGFloat(rgb & 0x0000FF) / 255.0
|
|
650
|
+
|
|
651
|
+
} else if length == 8 {
|
|
652
|
+
r = CGFloat((rgb & 0xFF000000) >> 24) / 255.0
|
|
653
|
+
g = CGFloat((rgb & 0x00FF0000) >> 16) / 255.0
|
|
654
|
+
b = CGFloat((rgb & 0x0000FF00) >> 8) / 255.0
|
|
655
|
+
a = CGFloat(rgb & 0x000000FF) / 255.0
|
|
656
|
+
|
|
657
|
+
} else {
|
|
658
|
+
return nil
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
self.init(red: r, green: g, blue: b, alpha: a)
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
func toHex() -> String? {
|
|
665
|
+
var r: CGFloat = 0
|
|
666
|
+
var g: CGFloat = 0
|
|
667
|
+
var b: CGFloat = 0
|
|
668
|
+
var a: CGFloat = 0
|
|
669
|
+
|
|
670
|
+
guard getRed(&r, green: &g, blue: &b, alpha: &a) else { return nil }
|
|
671
|
+
|
|
672
|
+
let rgb: Int = (Int)(r*255)<<16 | (Int)(g*255)<<8 | (Int)(b*255)<<0
|
|
673
|
+
return String(format: "#%06x", rgb)
|
|
674
|
+
}
|
|
675
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@june24/expo-pdf-reader",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A PDF reader for Expo",
|
|
5
|
+
"main": "build/index.js",
|
|
6
|
+
"types": "build/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "expo-module build",
|
|
9
|
+
"clean": "expo-module clean",
|
|
10
|
+
"lint": "expo-module lint",
|
|
11
|
+
"test": "expo-module test",
|
|
12
|
+
"prepublishOnly": "expo-module prepublishOnly"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"react-native",
|
|
16
|
+
"expo",
|
|
17
|
+
"expo-module",
|
|
18
|
+
"pdf"
|
|
19
|
+
],
|
|
20
|
+
"author": "User",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"expo-modules-core": "^3.0.29"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"expo-module-scripts": "^5.0.8",
|
|
27
|
+
"typescript": "^5.9.3",
|
|
28
|
+
"@types/react": "^19.2.9",
|
|
29
|
+
"react": "19.2.3",
|
|
30
|
+
"react-native": "0.83.1"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"expo": "*",
|
|
34
|
+
"react": "*",
|
|
35
|
+
"react-native": "*"
|
|
36
|
+
}
|
|
37
|
+
}
|