@webority-technologies/mobile-ui 0.0.7 → 0.0.8
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/RNMobileUiPdfRasterizer.podspec +25 -0
- package/android/build.gradle +64 -0
- package/android/src/newarch/java/com/webority/mobileui/pdfrasterizer/PdfRasterizerModule.kt +211 -0
- package/android/src/newarch/java/com/webority/mobileui/pdfrasterizer/PdfRasterizerPackage.kt +33 -0
- package/ios/PdfRasterizer.h +13 -0
- package/ios/PdfRasterizer.mm +292 -0
- package/lib/commonjs/components/DocumentViewer/DocumentViewer.js +185 -24
- package/lib/commonjs/specs/NativePdfRasterizer.js +9 -0
- package/lib/module/components/DocumentViewer/DocumentViewer.js +187 -26
- package/lib/module/specs/NativePdfRasterizer.js +5 -0
- package/lib/typescript/commonjs/components/DocumentViewer/DocumentViewer.d.ts +5 -4
- package/lib/typescript/commonjs/specs/NativePdfRasterizer.d.ts +40 -0
- package/lib/typescript/module/components/DocumentViewer/DocumentViewer.d.ts +5 -4
- package/lib/typescript/module/specs/NativePdfRasterizer.d.ts +40 -0
- package/package.json +13 -4
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "RNMobileUiPdfRasterizer"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.license = package["license"]
|
|
9
|
+
s.summary = "Native PDF rasterizer for @webority-technologies/mobile-ui's DocumentViewer (PDFKit-backed)."
|
|
10
|
+
s.author = package["author"]
|
|
11
|
+
s.homepage = package["homepage"]
|
|
12
|
+
|
|
13
|
+
s.platforms = { :ios => "15.1" }
|
|
14
|
+
s.requires_arc = true
|
|
15
|
+
s.swift_version = "5.9"
|
|
16
|
+
|
|
17
|
+
s.source = { :git => "https://www.webority.com", :tag => s.version }
|
|
18
|
+
s.source_files = "ios/*.{h,m,mm,swift}"
|
|
19
|
+
|
|
20
|
+
if ENV['RCT_NEW_ARCH_ENABLED'] == "1" then
|
|
21
|
+
install_modules_dependencies(s)
|
|
22
|
+
else
|
|
23
|
+
s.dependency "React-Core"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.safeExtGet = { prop, fallback ->
|
|
3
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
4
|
+
}
|
|
5
|
+
repositories {
|
|
6
|
+
google()
|
|
7
|
+
gradlePluginPortal()
|
|
8
|
+
}
|
|
9
|
+
dependencies {
|
|
10
|
+
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${safeExtGet("kotlinVersion", "2.1.20")}")
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
def isNewArchitectureEnabled() {
|
|
15
|
+
return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
apply plugin: "com.android.library"
|
|
19
|
+
apply plugin: "kotlin-android"
|
|
20
|
+
|
|
21
|
+
if (isNewArchitectureEnabled()) {
|
|
22
|
+
apply plugin: "com.facebook.react"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
android {
|
|
26
|
+
namespace "com.webority.mobileui.pdfrasterizer"
|
|
27
|
+
|
|
28
|
+
buildToolsVersion safeExtGet("buildToolsVersion", "36.0.0")
|
|
29
|
+
compileSdkVersion safeExtGet("compileSdkVersion", 36)
|
|
30
|
+
|
|
31
|
+
buildFeatures {
|
|
32
|
+
buildConfig true
|
|
33
|
+
}
|
|
34
|
+
defaultConfig {
|
|
35
|
+
buildConfigField("boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString())
|
|
36
|
+
minSdkVersion safeExtGet("minSdkVersion", 24)
|
|
37
|
+
targetSdkVersion safeExtGet("targetSdkVersion", 36)
|
|
38
|
+
}
|
|
39
|
+
lintOptions {
|
|
40
|
+
abortOnError false
|
|
41
|
+
}
|
|
42
|
+
sourceSets {
|
|
43
|
+
main {
|
|
44
|
+
if (isNewArchitectureEnabled()) {
|
|
45
|
+
java.srcDirs += ["src/newarch"]
|
|
46
|
+
} else {
|
|
47
|
+
java.srcDirs += ["src/oldarch"]
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
repositories {
|
|
54
|
+
maven {
|
|
55
|
+
url("$rootDir/../node_modules/react-native/android")
|
|
56
|
+
}
|
|
57
|
+
google()
|
|
58
|
+
mavenCentral()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
dependencies {
|
|
62
|
+
//noinspection GradleDynamicVersion
|
|
63
|
+
implementation "com.facebook.react:react-native:+"
|
|
64
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
package com.webority.mobileui.pdfrasterizer
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap
|
|
4
|
+
import android.graphics.Color
|
|
5
|
+
import android.graphics.pdf.PdfRenderer
|
|
6
|
+
import android.os.ParcelFileDescriptor
|
|
7
|
+
import com.facebook.react.bridge.Promise
|
|
8
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
9
|
+
import com.facebook.react.bridge.WritableMap
|
|
10
|
+
import com.facebook.react.bridge.Arguments
|
|
11
|
+
import java.io.File
|
|
12
|
+
import java.io.FileOutputStream
|
|
13
|
+
import java.io.IOException
|
|
14
|
+
import java.util.UUID
|
|
15
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
16
|
+
import java.util.concurrent.ExecutorService
|
|
17
|
+
import java.util.concurrent.Executors
|
|
18
|
+
import kotlin.math.roundToInt
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Android implementation of the PdfRasterizer TurboModule, backed by the platform
|
|
22
|
+
* `android.graphics.pdf.PdfRenderer` (API 21+) — no third-party PDF library.
|
|
23
|
+
*
|
|
24
|
+
* `PdfRenderer` allows only ONE open page per document at a time, so every
|
|
25
|
+
* getPageInfo/renderPage call for a given handle is serialized on that handle's
|
|
26
|
+
* renderer instance via `synchronized`.
|
|
27
|
+
*/
|
|
28
|
+
class PdfRasterizerModule(reactContext: ReactApplicationContext) :
|
|
29
|
+
NativePdfRasterizerSpec(reactContext) {
|
|
30
|
+
|
|
31
|
+
private class OpenDocument(val renderer: PdfRenderer, val fd: ParcelFileDescriptor)
|
|
32
|
+
|
|
33
|
+
private val openDocuments = ConcurrentHashMap<String, OpenDocument>()
|
|
34
|
+
private val executor: ExecutorService = Executors.newCachedThreadPool()
|
|
35
|
+
|
|
36
|
+
companion object {
|
|
37
|
+
const val NAME = "PdfRasterizer"
|
|
38
|
+
|
|
39
|
+
private const val ERR_FILE_NOT_FOUND = "E_FILE_NOT_FOUND"
|
|
40
|
+
private const val ERR_INVALID_PDF = "E_INVALID_PDF"
|
|
41
|
+
private const val ERR_PASSWORD_PROTECTED = "E_PASSWORD_PROTECTED"
|
|
42
|
+
private const val ERR_UNKNOWN_HANDLE = "E_UNKNOWN_HANDLE"
|
|
43
|
+
private const val ERR_PAGE_OUT_OF_RANGE = "E_PAGE_OUT_OF_RANGE"
|
|
44
|
+
|
|
45
|
+
// Clamp the larger render dimension to avoid an OOM on a pathological scale value.
|
|
46
|
+
private const val MAX_RENDER_DIMENSION_PX = 4096
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
override fun getName(): String = NAME
|
|
50
|
+
|
|
51
|
+
override fun open(path: String, promise: Promise) {
|
|
52
|
+
executor.execute {
|
|
53
|
+
var fd: ParcelFileDescriptor? = null
|
|
54
|
+
try {
|
|
55
|
+
val barePath = normalizePath(path)
|
|
56
|
+
val file = File(barePath)
|
|
57
|
+
if (!file.exists()) {
|
|
58
|
+
promise.reject(ERR_FILE_NOT_FOUND, "PDF file not found at path: $path")
|
|
59
|
+
return@execute
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
fd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
|
|
63
|
+
val renderer = PdfRenderer(fd)
|
|
64
|
+
|
|
65
|
+
val handle = UUID.randomUUID().toString()
|
|
66
|
+
openDocuments[handle] = OpenDocument(renderer, fd)
|
|
67
|
+
|
|
68
|
+
val page = renderer.openPage(0)
|
|
69
|
+
val result: WritableMap = Arguments.createMap()
|
|
70
|
+
result.putString("handle", handle)
|
|
71
|
+
result.putInt("pageCount", renderer.pageCount)
|
|
72
|
+
// PdfRenderer reports page size in points (1/72 in) — return as-is.
|
|
73
|
+
result.putDouble("pageWidth", page.width.toDouble())
|
|
74
|
+
result.putDouble("pageHeight", page.height.toDouble())
|
|
75
|
+
page.close()
|
|
76
|
+
|
|
77
|
+
promise.resolve(result)
|
|
78
|
+
} catch (e: SecurityException) {
|
|
79
|
+
// PdfRenderer throws SecurityException specifically for password-protected PDFs.
|
|
80
|
+
fd?.close()
|
|
81
|
+
promise.reject(ERR_PASSWORD_PROTECTED, "PDF is password-protected: $path", e)
|
|
82
|
+
} catch (e: IOException) {
|
|
83
|
+
fd?.close()
|
|
84
|
+
promise.reject(ERR_INVALID_PDF, "PDF file is missing or corrupt: $path", e)
|
|
85
|
+
} catch (e: Exception) {
|
|
86
|
+
fd?.close()
|
|
87
|
+
promise.reject(ERR_INVALID_PDF, "Failed to open PDF: ${e.message}", e)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
override fun getPageInfo(handle: String, pageIndex: Double, promise: Promise) {
|
|
93
|
+
executor.execute {
|
|
94
|
+
val doc = openDocuments[handle]
|
|
95
|
+
if (doc == null) {
|
|
96
|
+
promise.reject(ERR_UNKNOWN_HANDLE, "Unknown PDF handle: $handle")
|
|
97
|
+
return@execute
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
val index = pageIndex.toInt()
|
|
101
|
+
synchronized(doc.renderer) {
|
|
102
|
+
if (index < 0 || index >= doc.renderer.pageCount) {
|
|
103
|
+
promise.reject(
|
|
104
|
+
ERR_PAGE_OUT_OF_RANGE,
|
|
105
|
+
"Page index $index out of range [0, ${doc.renderer.pageCount})")
|
|
106
|
+
return@synchronized
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
val page = doc.renderer.openPage(index)
|
|
111
|
+
val result: WritableMap = Arguments.createMap()
|
|
112
|
+
result.putDouble("width", page.width.toDouble())
|
|
113
|
+
result.putDouble("height", page.height.toDouble())
|
|
114
|
+
page.close()
|
|
115
|
+
promise.resolve(result)
|
|
116
|
+
} catch (e: Exception) {
|
|
117
|
+
promise.reject(ERR_INVALID_PDF, "Failed to read page $index: ${e.message}", e)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
override fun renderPage(handle: String, pageIndex: Double, scale: Double, promise: Promise) {
|
|
124
|
+
executor.execute {
|
|
125
|
+
val doc = openDocuments[handle]
|
|
126
|
+
if (doc == null) {
|
|
127
|
+
promise.reject(ERR_UNKNOWN_HANDLE, "Unknown PDF handle: $handle")
|
|
128
|
+
return@execute
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
val index = pageIndex.toInt()
|
|
132
|
+
|
|
133
|
+
synchronized(doc.renderer) {
|
|
134
|
+
if (index < 0 || index >= doc.renderer.pageCount) {
|
|
135
|
+
promise.reject(
|
|
136
|
+
ERR_PAGE_OUT_OF_RANGE,
|
|
137
|
+
"Page index $index out of range [0, ${doc.renderer.pageCount})")
|
|
138
|
+
return@synchronized
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
val cacheDir = File(reactApplicationContext.cacheDir, "pdf-rasterizer-cache/$handle")
|
|
143
|
+
if (!cacheDir.exists()) {
|
|
144
|
+
cacheDir.mkdirs()
|
|
145
|
+
}
|
|
146
|
+
val outputFile = File(cacheDir, "$index-$scale.png")
|
|
147
|
+
|
|
148
|
+
if (outputFile.exists()) {
|
|
149
|
+
promise.resolve("file://${outputFile.absolutePath}")
|
|
150
|
+
return@synchronized
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
val page = doc.renderer.openPage(index)
|
|
154
|
+
val targetWidth = clampDimension((page.width * scale).roundToInt())
|
|
155
|
+
val targetHeight = clampDimension((page.height * scale).roundToInt())
|
|
156
|
+
|
|
157
|
+
val bitmap = Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.ARGB_8888)
|
|
158
|
+
// Fill white first — PDF transparency renders as black on some devices otherwise.
|
|
159
|
+
bitmap.eraseColor(Color.WHITE)
|
|
160
|
+
page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
|
|
161
|
+
page.close()
|
|
162
|
+
|
|
163
|
+
FileOutputStream(outputFile).use { out ->
|
|
164
|
+
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
|
|
165
|
+
}
|
|
166
|
+
bitmap.recycle()
|
|
167
|
+
|
|
168
|
+
promise.resolve("file://${outputFile.absolutePath}")
|
|
169
|
+
} catch (e: Exception) {
|
|
170
|
+
promise.reject(ERR_INVALID_PDF, "Failed to render page $index: ${e.message}", e)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
override fun close(handle: String, promise: Promise) {
|
|
177
|
+
executor.execute {
|
|
178
|
+
val doc = openDocuments.remove(handle)
|
|
179
|
+
if (doc != null) {
|
|
180
|
+
try {
|
|
181
|
+
doc.renderer.close()
|
|
182
|
+
} catch (_: Exception) {
|
|
183
|
+
// Best-effort.
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
doc.fd.close()
|
|
187
|
+
} catch (_: Exception) {
|
|
188
|
+
// Best-effort.
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
File(reactApplicationContext.cacheDir, "pdf-rasterizer-cache/$handle").deleteRecursively()
|
|
194
|
+
} catch (_: Exception) {
|
|
195
|
+
// Best-effort — cache eviction failure must not fail an otherwise-idempotent close.
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Idempotent: resolve even if the handle was already unknown or already closed.
|
|
199
|
+
promise.resolve(null)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private fun clampDimension(dimension: Int): Int {
|
|
204
|
+
val positive = if (dimension < 1) 1 else dimension
|
|
205
|
+
return if (positive > MAX_RENDER_DIMENSION_PX) MAX_RENDER_DIMENSION_PX else positive
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private fun normalizePath(path: String): String {
|
|
209
|
+
return if (path.startsWith("file://")) path.removePrefix("file://") else path
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
package com.webority.mobileui.pdfrasterizer
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.TurboReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfo
|
|
7
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
8
|
+
|
|
9
|
+
class PdfRasterizerPackage : TurboReactPackage() {
|
|
10
|
+
|
|
11
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
|
|
12
|
+
return if (name == PdfRasterizerModule.NAME) {
|
|
13
|
+
PdfRasterizerModule(reactContext)
|
|
14
|
+
} else {
|
|
15
|
+
null
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
|
|
20
|
+
return ReactModuleInfoProvider {
|
|
21
|
+
mapOf(
|
|
22
|
+
PdfRasterizerModule.NAME to
|
|
23
|
+
ReactModuleInfo(
|
|
24
|
+
PdfRasterizerModule.NAME,
|
|
25
|
+
PdfRasterizerModule.NAME,
|
|
26
|
+
false, // canOverrideExistingModule
|
|
27
|
+
false, // needsEagerInit
|
|
28
|
+
false, // isCxxModule
|
|
29
|
+
true // isTurboModule
|
|
30
|
+
))
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#import <Foundation/Foundation.h>
|
|
2
|
+
|
|
3
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
4
|
+
#import <RNMobileUiPdfRasterizerSpec/RNMobileUiPdfRasterizerSpec.h>
|
|
5
|
+
|
|
6
|
+
@interface PdfRasterizer : NSObject <NativePdfRasterizerSpec>
|
|
7
|
+
#else
|
|
8
|
+
#import <React/RCTBridgeModule.h>
|
|
9
|
+
|
|
10
|
+
@interface PdfRasterizer : NSObject <RCTBridgeModule>
|
|
11
|
+
#endif
|
|
12
|
+
|
|
13
|
+
@end
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
#import "PdfRasterizer.h"
|
|
2
|
+
|
|
3
|
+
#import <CoreGraphics/CoreGraphics.h>
|
|
4
|
+
#import <UIKit/UIKit.h>
|
|
5
|
+
|
|
6
|
+
static NSString *const kCacheDirName = @"pdf-rasterizer-cache";
|
|
7
|
+
|
|
8
|
+
// Wraps a CGPDFDocumentRef so ARC can own its lifetime inside an NSMutableDictionary —
|
|
9
|
+
// CGPDFDocumentRef is a plain CF type with no toll-free-bridged Foundation counterpart.
|
|
10
|
+
@interface PdfRasterizerDocumentBox : NSObject
|
|
11
|
+
@property (nonatomic, assign, readonly) CGPDFDocumentRef document;
|
|
12
|
+
- (instancetype)initWithDocument:(CGPDFDocumentRef)document;
|
|
13
|
+
@end
|
|
14
|
+
|
|
15
|
+
@implementation PdfRasterizerDocumentBox
|
|
16
|
+
- (instancetype)initWithDocument:(CGPDFDocumentRef)document {
|
|
17
|
+
if (self = [super init]) {
|
|
18
|
+
_document = CGPDFDocumentRetain(document);
|
|
19
|
+
}
|
|
20
|
+
return self;
|
|
21
|
+
}
|
|
22
|
+
- (void)dealloc {
|
|
23
|
+
if (_document != NULL) {
|
|
24
|
+
CGPDFDocumentRelease(_document);
|
|
25
|
+
_document = NULL;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
@end
|
|
29
|
+
|
|
30
|
+
@interface PdfRasterizer ()
|
|
31
|
+
@property (nonatomic, strong) NSMutableDictionary<NSString *, PdfRasterizerDocumentBox *> *documents;
|
|
32
|
+
@property (nonatomic, strong) dispatch_queue_t queue;
|
|
33
|
+
@end
|
|
34
|
+
|
|
35
|
+
@implementation PdfRasterizer
|
|
36
|
+
|
|
37
|
+
RCT_EXPORT_MODULE(PdfRasterizer)
|
|
38
|
+
|
|
39
|
+
- (instancetype)init {
|
|
40
|
+
if (self = [super init]) {
|
|
41
|
+
_documents = [NSMutableDictionary new];
|
|
42
|
+
// Serial: guards the handle->document dictionary AND serializes renders,
|
|
43
|
+
// since CGPDFDocument/CGPDFPage are not safe under concurrent access.
|
|
44
|
+
_queue = dispatch_queue_create("com.webority.mobileui.pdfrasterizer", DISPATCH_QUEUE_SERIAL);
|
|
45
|
+
}
|
|
46
|
+
return self;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
+ (BOOL)requiresMainQueueSetup {
|
|
50
|
+
return NO;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#pragma mark - Helpers
|
|
54
|
+
|
|
55
|
+
+ (NSString *)normalizedPathFromPath:(NSString *)path {
|
|
56
|
+
NSString *scheme = @"file://";
|
|
57
|
+
if ([path hasPrefix:scheme]) {
|
|
58
|
+
return [path substringFromIndex:scheme.length];
|
|
59
|
+
}
|
|
60
|
+
return path;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Page size in points, respecting rotation (90/270 swap width & height) — same
|
|
64
|
+
// logic used both for open()'s page-0 read and getPageInfo()'s arbitrary page.
|
|
65
|
+
+ (CGSize)pageSizeForPage:(CGPDFPageRef)page {
|
|
66
|
+
CGRect box = CGPDFPageGetBoxRect(page, kCGPDFMediaBox);
|
|
67
|
+
int rotation = CGPDFPageGetRotationAngle(page);
|
|
68
|
+
CGFloat width = box.size.width;
|
|
69
|
+
CGFloat height = box.size.height;
|
|
70
|
+
if (rotation == 90 || rotation == 270) {
|
|
71
|
+
CGFloat tmp = width;
|
|
72
|
+
width = height;
|
|
73
|
+
height = tmp;
|
|
74
|
+
}
|
|
75
|
+
return CGSizeMake(width, height);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
- (NSString *)cacheDirectoryForHandle:(NSString *)handle {
|
|
79
|
+
return [[NSTemporaryDirectory() stringByAppendingPathComponent:kCacheDirName]
|
|
80
|
+
stringByAppendingPathComponent:handle];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#pragma mark - Spec methods
|
|
84
|
+
|
|
85
|
+
- (void)open:(NSString *)path
|
|
86
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
87
|
+
reject:(RCTPromiseRejectBlock)reject {
|
|
88
|
+
dispatch_async(self.queue, ^{
|
|
89
|
+
NSString *normalizedPath = [PdfRasterizer normalizedPathFromPath:path];
|
|
90
|
+
|
|
91
|
+
if (![[NSFileManager defaultManager] fileExistsAtPath:normalizedPath]) {
|
|
92
|
+
reject(@"E_FILE_NOT_FOUND", [NSString stringWithFormat:@"No file at path: %@", path], nil);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
NSURL *url = [NSURL fileURLWithPath:normalizedPath];
|
|
97
|
+
CGPDFDocumentRef document = CGPDFDocumentCreateWithURL((__bridge CFURLRef)url);
|
|
98
|
+
if (document == NULL) {
|
|
99
|
+
reject(@"E_INVALID_PDF", @"The file is not a valid PDF document.", nil);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (CGPDFDocumentIsEncrypted(document)) {
|
|
104
|
+
// Only ever try the empty password — a real password prompt is a UI
|
|
105
|
+
// concern the caller owns, not this module's job to solve.
|
|
106
|
+
CGPDFDocumentUnlockWithPassword(document, "");
|
|
107
|
+
if (!CGPDFDocumentIsUnlocked(document)) {
|
|
108
|
+
CGPDFDocumentRelease(document);
|
|
109
|
+
reject(@"E_PASSWORD_PROTECTED", @"The PDF is password-protected.", nil);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
size_t pageCount = CGPDFDocumentGetNumberOfPages(document);
|
|
115
|
+
if (pageCount == 0) {
|
|
116
|
+
CGPDFDocumentRelease(document);
|
|
117
|
+
reject(@"E_INVALID_PDF", @"The PDF document has no pages.", nil);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
CGPDFPageRef firstPage = CGPDFDocumentGetPage(document, 1);
|
|
122
|
+
if (firstPage == NULL) {
|
|
123
|
+
CGPDFDocumentRelease(document);
|
|
124
|
+
reject(@"E_INVALID_PDF", @"Unable to read the PDF's first page.", nil);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
CGSize size = [PdfRasterizer pageSizeForPage:firstPage];
|
|
129
|
+
|
|
130
|
+
NSString *handle = [[NSUUID UUID] UUIDString];
|
|
131
|
+
PdfRasterizerDocumentBox *box = [[PdfRasterizerDocumentBox alloc] initWithDocument:document];
|
|
132
|
+
CGPDFDocumentRelease(document); // box retained its own reference above
|
|
133
|
+
self.documents[handle] = box;
|
|
134
|
+
|
|
135
|
+
resolve(@{
|
|
136
|
+
@"handle" : handle,
|
|
137
|
+
@"pageCount" : @(pageCount),
|
|
138
|
+
@"pageWidth" : @(size.width),
|
|
139
|
+
@"pageHeight" : @(size.height),
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
- (void)getPageInfo:(NSString *)handle
|
|
145
|
+
pageIndex:(double)pageIndex
|
|
146
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
147
|
+
reject:(RCTPromiseRejectBlock)reject {
|
|
148
|
+
dispatch_async(self.queue, ^{
|
|
149
|
+
PdfRasterizerDocumentBox *box = self.documents[handle];
|
|
150
|
+
if (box == nil) {
|
|
151
|
+
reject(@"E_UNKNOWN_HANDLE", [NSString stringWithFormat:@"No open document for handle: %@", handle], nil);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
NSInteger index = (NSInteger)pageIndex;
|
|
156
|
+
size_t pageCount = CGPDFDocumentGetNumberOfPages(box.document);
|
|
157
|
+
if (index < 0 || (size_t)index >= pageCount) {
|
|
158
|
+
reject(@"E_PAGE_OUT_OF_RANGE",
|
|
159
|
+
[NSString stringWithFormat:@"Page index %ld out of range (0..%zu).", (long)index, pageCount - 1],
|
|
160
|
+
nil);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
CGPDFPageRef page = CGPDFDocumentGetPage(box.document, (size_t)index + 1);
|
|
165
|
+
if (page == NULL) {
|
|
166
|
+
reject(@"E_PAGE_OUT_OF_RANGE", [NSString stringWithFormat:@"Unable to read page %ld.", (long)index], nil);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
CGSize size = [PdfRasterizer pageSizeForPage:page];
|
|
171
|
+
resolve(@{
|
|
172
|
+
@"width" : @(size.width),
|
|
173
|
+
@"height" : @(size.height),
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
- (void)renderPage:(NSString *)handle
|
|
179
|
+
pageIndex:(double)pageIndex
|
|
180
|
+
scale:(double)scale
|
|
181
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
182
|
+
reject:(RCTPromiseRejectBlock)reject {
|
|
183
|
+
dispatch_async(self.queue, ^{
|
|
184
|
+
PdfRasterizerDocumentBox *box = self.documents[handle];
|
|
185
|
+
if (box == nil) {
|
|
186
|
+
reject(@"E_UNKNOWN_HANDLE", [NSString stringWithFormat:@"No open document for handle: %@", handle], nil);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
NSInteger index = (NSInteger)pageIndex;
|
|
191
|
+
size_t pageCount = CGPDFDocumentGetNumberOfPages(box.document);
|
|
192
|
+
if (index < 0 || (size_t)index >= pageCount) {
|
|
193
|
+
reject(@"E_PAGE_OUT_OF_RANGE",
|
|
194
|
+
[NSString stringWithFormat:@"Page index %ld out of range (0..%zu).", (long)index, pageCount - 1],
|
|
195
|
+
nil);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
CGPDFPageRef page = CGPDFDocumentGetPage(box.document, (size_t)index + 1);
|
|
200
|
+
if (page == NULL) {
|
|
201
|
+
reject(@"E_PAGE_OUT_OF_RANGE", [NSString stringWithFormat:@"Unable to read page %ld.", (long)index], nil);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
NSString *cacheDir = [self cacheDirectoryForHandle:handle];
|
|
206
|
+
NSError *dirError = nil;
|
|
207
|
+
if (![[NSFileManager defaultManager] createDirectoryAtPath:cacheDir
|
|
208
|
+
withIntermediateDirectories:YES
|
|
209
|
+
attributes:nil
|
|
210
|
+
error:&dirError]) {
|
|
211
|
+
reject(@"E_WRITE_FAILED",
|
|
212
|
+
[NSString stringWithFormat:@"Could not create cache directory: %@", dirError.localizedDescription],
|
|
213
|
+
dirError);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
NSString *fileName = [NSString stringWithFormat:@"%ld-%g.png", (long)index, scale];
|
|
218
|
+
NSString *filePath = [cacheDir stringByAppendingPathComponent:fileName];
|
|
219
|
+
|
|
220
|
+
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
|
|
221
|
+
resolve([[NSURL fileURLWithPath:filePath] absoluteString]);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
CGSize pageSize = [PdfRasterizer pageSizeForPage:page];
|
|
226
|
+
CGSize renderSize = CGSizeMake(pageSize.width * scale, pageSize.height * scale);
|
|
227
|
+
if (renderSize.width <= 0 || renderSize.height <= 0) {
|
|
228
|
+
reject(@"E_RENDER_FAILED", @"Computed render size is empty.", nil);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
UIGraphicsImageRendererFormat *format = [UIGraphicsImageRendererFormat preferredFormat];
|
|
233
|
+
format.opaque = YES;
|
|
234
|
+
format.scale = 1.0; // renderSize already carries the caller's scale factor
|
|
235
|
+
UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:renderSize format:format];
|
|
236
|
+
|
|
237
|
+
UIImage *image = [renderer imageWithActions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext) {
|
|
238
|
+
CGContextRef ctx = rendererContext.CGContext;
|
|
239
|
+
|
|
240
|
+
CGContextSetFillColorWithColor(ctx, [UIColor whiteColor].CGColor);
|
|
241
|
+
CGContextFillRect(ctx, CGRectMake(0, 0, renderSize.width, renderSize.height));
|
|
242
|
+
|
|
243
|
+
CGContextSaveGState(ctx);
|
|
244
|
+
// Flip into PDF's bottom-left-origin space, then let CGPDFPageGetDrawingTransform
|
|
245
|
+
// account for the page's own rotation/media box when mapping into renderSize.
|
|
246
|
+
CGContextTranslateCTM(ctx, 0, renderSize.height);
|
|
247
|
+
CGContextScaleCTM(ctx, 1, -1);
|
|
248
|
+
CGAffineTransform transform = CGPDFPageGetDrawingTransform(
|
|
249
|
+
page, kCGPDFMediaBox, CGRectMake(0, 0, renderSize.width, renderSize.height), 0, true);
|
|
250
|
+
CGContextConcatCTM(ctx, transform);
|
|
251
|
+
CGContextDrawPDFPage(ctx, page);
|
|
252
|
+
CGContextRestoreGState(ctx);
|
|
253
|
+
}];
|
|
254
|
+
|
|
255
|
+
NSData *pngData = UIImagePNGRepresentation(image);
|
|
256
|
+
if (pngData == nil) {
|
|
257
|
+
reject(@"E_RENDER_FAILED", @"Failed to encode the rendered page as PNG.", nil);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
NSError *writeError = nil;
|
|
262
|
+
if (![pngData writeToFile:filePath options:NSDataWritingAtomic error:&writeError]) {
|
|
263
|
+
reject(@"E_WRITE_FAILED",
|
|
264
|
+
[NSString stringWithFormat:@"Could not write rendered page: %@", writeError.localizedDescription],
|
|
265
|
+
writeError);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
resolve([[NSURL fileURLWithPath:filePath] absoluteString]);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
- (void)close:(NSString *)handle resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
|
274
|
+
dispatch_async(self.queue, ^{
|
|
275
|
+
[self.documents removeObjectForKey:handle];
|
|
276
|
+
|
|
277
|
+
NSString *cacheDir = [self cacheDirectoryForHandle:handle];
|
|
278
|
+
// Best-effort cleanup — a stale cache dir on delete failure is not worth
|
|
279
|
+
// failing an otherwise-successful close over.
|
|
280
|
+
[[NSFileManager defaultManager] removeItemAtPath:cacheDir error:nil];
|
|
281
|
+
|
|
282
|
+
resolve(nil);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
287
|
+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
|
|
288
|
+
return std::make_shared<facebook::react::NativePdfRasterizerSpecJSI>(params);
|
|
289
|
+
}
|
|
290
|
+
#endif
|
|
291
|
+
|
|
292
|
+
@end
|
|
@@ -5,21 +5,47 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.default = exports.DocumentViewer = void 0;
|
|
7
7
|
var _mobileCore = require("@webority-technologies/mobile-core");
|
|
8
|
+
var _download = require("@webority-technologies/mobile-core/download");
|
|
8
9
|
var _react = require("react");
|
|
9
10
|
var _reactNative = require("react-native");
|
|
10
|
-
var
|
|
11
|
+
var _NativePdfRasterizer = _interopRequireDefault(require("../../specs/NativePdfRasterizer.js"));
|
|
11
12
|
var _index = require("../Spinner/index.js");
|
|
12
13
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
13
14
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
15
|
+
const HAS_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
|
|
16
|
+
const MAX_RENDER_SCALE = 3;
|
|
14
17
|
const toError = value => value instanceof Error ? value : new Error(String(value));
|
|
15
18
|
|
|
19
|
+
/** The renderPage scale is capped so a very wide view never asks the native
|
|
20
|
+
* rasterizer for an unbounded bitmap (see react-native.md's ARGB_8888 4-bytes/px
|
|
21
|
+
* guidance) — both native sides additionally clamp their own pixel ceiling. */
|
|
22
|
+
const scaleForWidth = (viewWidthPx, pageWidthPt) => {
|
|
23
|
+
if (pageWidthPt <= 0) {
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
26
|
+
const raw = viewWidthPx / pageWidthPt * _reactNative.PixelRatio.get();
|
|
27
|
+
return Math.min(Math.max(raw, 1), MAX_RENDER_SCALE);
|
|
28
|
+
};
|
|
29
|
+
const localPathFromSource = async source => {
|
|
30
|
+
if (!HAS_SCHEME.test(source.uri) || source.uri.startsWith('file://')) {
|
|
31
|
+
return source.uri.replace(/^file:\/\//, '');
|
|
32
|
+
}
|
|
33
|
+
const result = await (0, _download.downloadFile)({
|
|
34
|
+
url: source.uri,
|
|
35
|
+
headers: source.headers,
|
|
36
|
+
authenticated: false,
|
|
37
|
+
overwrite: source.cache === false
|
|
38
|
+
});
|
|
39
|
+
return result.path;
|
|
40
|
+
};
|
|
16
41
|
/**
|
|
17
|
-
* Renders a PDF
|
|
18
|
-
*
|
|
42
|
+
* Renders a PDF via our own native rasterizer (PDFKit on iOS, PdfRenderer on
|
|
43
|
+
* Android) — no third-party PDF dependency. The WebView fallback consumer
|
|
44
|
+
* apps use for non-PDF office documents is a distinct rendering strategy and
|
|
45
|
+
* stays app-side.
|
|
19
46
|
*/
|
|
20
47
|
const DocumentViewer = ({
|
|
21
48
|
source,
|
|
22
|
-
trustAllCerts = false,
|
|
23
49
|
onLoadComplete,
|
|
24
50
|
onPageChanged,
|
|
25
51
|
onError,
|
|
@@ -28,29 +54,158 @@ const DocumentViewer = ({
|
|
|
28
54
|
testID,
|
|
29
55
|
accessibilityLabel
|
|
30
56
|
}) => {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
const
|
|
57
|
+
const {
|
|
58
|
+
width: windowWidth
|
|
59
|
+
} = (0, _reactNative.useWindowDimensions)();
|
|
60
|
+
const [pageCount, setPageCount] = (0, _react.useState)(0);
|
|
61
|
+
const [pageSizePt, setPageSizePt] = (0, _react.useState)({
|
|
62
|
+
width: 612,
|
|
63
|
+
height: 792
|
|
64
|
+
}); // US Letter fallback
|
|
65
|
+
const pageAspect = pageSizePt.height / pageSizePt.width;
|
|
66
|
+
const [renderedPages, setRenderedPages] = (0, _react.useState)({});
|
|
67
|
+
const handleRef = (0, _react.useRef)(null);
|
|
68
|
+
const onErrorRef = (0, _react.useRef)(onError);
|
|
69
|
+
onErrorRef.current = onError;
|
|
70
|
+
const onLoadCompleteRef = (0, _react.useRef)(onLoadComplete);
|
|
71
|
+
onLoadCompleteRef.current = onLoadComplete;
|
|
72
|
+
const documentSource = (0, _react.useMemo)(() => ({
|
|
73
|
+
uri: source.uri,
|
|
74
|
+
headers: source.headers,
|
|
75
|
+
cache: source.cache
|
|
76
|
+
}), [source.uri, source.headers, source.cache]);
|
|
77
|
+
(0, _react.useEffect)(() => {
|
|
78
|
+
let cancelled = false;
|
|
79
|
+
const load = async () => {
|
|
80
|
+
if (!_NativePdfRasterizer.default) {
|
|
81
|
+
onErrorRef.current?.(new Error('[@webority-technologies/mobile-ui] PdfRasterizer native module is not linked. ' + 'Run pod install / rebuild the app after adding @webority-technologies/mobile-ui.'));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const path = await localPathFromSource(documentSource);
|
|
86
|
+
if (cancelled) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const opened = await _NativePdfRasterizer.default.open(path);
|
|
90
|
+
if (cancelled) {
|
|
91
|
+
await _NativePdfRasterizer.default.close(opened.handle).catch(() => undefined);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
handleRef.current = opened.handle;
|
|
95
|
+
setPageCount(opened.pageCount);
|
|
96
|
+
if (opened.pageWidth > 0 && opened.pageHeight > 0) {
|
|
97
|
+
setPageSizePt({
|
|
98
|
+
width: opened.pageWidth,
|
|
99
|
+
height: opened.pageHeight
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
onLoadCompleteRef.current?.(opened.pageCount);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
if (!cancelled) {
|
|
105
|
+
const err = toError(error);
|
|
106
|
+
_mobileCore.Logger.error('[@webority-technologies/mobile-ui] DocumentViewer failed to load.', err);
|
|
107
|
+
onErrorRef.current?.(err);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
void load();
|
|
112
|
+
return () => {
|
|
113
|
+
cancelled = true;
|
|
114
|
+
};
|
|
115
|
+
}, [documentSource]);
|
|
116
|
+
(0, _react.useEffect)(() => () => {
|
|
117
|
+
const openHandle = handleRef.current;
|
|
118
|
+
if (openHandle) {
|
|
119
|
+
_NativePdfRasterizer.default?.close(openHandle).catch(() => undefined);
|
|
120
|
+
}
|
|
121
|
+
}, []);
|
|
122
|
+
const renderPageImage = (0, _react.useCallback)(async pageIndex => {
|
|
123
|
+
const openHandle = handleRef.current;
|
|
124
|
+
if (!openHandle || !_NativePdfRasterizer.default) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
const scale = scaleForWidth(windowWidth, pageSizePt.width);
|
|
129
|
+
const uri = await _NativePdfRasterizer.default.renderPage(openHandle, pageIndex, scale);
|
|
130
|
+
setRenderedPages(prev => prev[pageIndex] ? prev : {
|
|
131
|
+
...prev,
|
|
132
|
+
[pageIndex]: uri
|
|
133
|
+
});
|
|
134
|
+
} catch (error) {
|
|
135
|
+
_mobileCore.Logger.error(`[@webority-technologies/mobile-ui] DocumentViewer failed to render page ${pageIndex}.`, toError(error));
|
|
136
|
+
}
|
|
137
|
+
}, [pageSizePt.width, windowWidth]);
|
|
138
|
+
const data = (0, _react.useMemo)(() => Array.from({
|
|
139
|
+
length: pageCount
|
|
140
|
+
}, (_unused, index) => ({
|
|
141
|
+
index
|
|
142
|
+
})), [pageCount]);
|
|
143
|
+
const onPageChangedRef = (0, _react.useRef)(onPageChanged);
|
|
144
|
+
onPageChangedRef.current = onPageChanged;
|
|
145
|
+
const pageCountRef = (0, _react.useRef)(pageCount);
|
|
146
|
+
pageCountRef.current = pageCount;
|
|
147
|
+
const renderPageImageRef = (0, _react.useRef)(renderPageImage);
|
|
148
|
+
renderPageImageRef.current = renderPageImage;
|
|
149
|
+
|
|
150
|
+
// A stable function identity: FlatList warns if onViewableItemsChanged changes
|
|
151
|
+
// identity across renders, so the current values it needs are read from refs
|
|
152
|
+
// kept up to date every render instead of being closed over here.
|
|
153
|
+
const onViewableItemsChanged = (0, _react.useRef)(({
|
|
154
|
+
viewableItems
|
|
155
|
+
}) => {
|
|
156
|
+
const first = viewableItems[0]?.item;
|
|
157
|
+
if (first) {
|
|
158
|
+
onPageChangedRef.current?.(first.index + 1, pageCountRef.current);
|
|
159
|
+
viewableItems.forEach(v => void renderPageImageRef.current(v.item.index));
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
const renderItem = (0, _react.useCallback)(({
|
|
163
|
+
item
|
|
164
|
+
}) => {
|
|
165
|
+
const uri = renderedPages[item.index];
|
|
166
|
+
const pageWidth = windowWidth;
|
|
167
|
+
const pageHeight = pageWidth * pageAspect;
|
|
168
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
169
|
+
style: [styles.page, {
|
|
170
|
+
width: pageWidth,
|
|
171
|
+
height: pageHeight
|
|
172
|
+
}],
|
|
173
|
+
children: uri ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Image, {
|
|
174
|
+
source: {
|
|
175
|
+
uri
|
|
176
|
+
},
|
|
177
|
+
style: styles.pageImage,
|
|
178
|
+
resizeMode: "contain",
|
|
179
|
+
accessible: false
|
|
180
|
+
}) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
181
|
+
style: styles.pagePlaceholder,
|
|
182
|
+
children: loadingIndicator ? loadingIndicator() : /*#__PURE__*/(0, _jsxRuntime.jsx)(_index.Spinner, {
|
|
183
|
+
size: "lg"
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
});
|
|
187
|
+
}, [loadingIndicator, pageAspect, renderedPages, windowWidth]);
|
|
40
188
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
41
189
|
style: [styles.root, style],
|
|
42
190
|
testID: testID,
|
|
43
191
|
accessible: accessibilityLabel !== undefined,
|
|
44
192
|
accessibilityLabel: accessibilityLabel,
|
|
45
193
|
accessibilityRole: "none",
|
|
46
|
-
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
194
|
+
children: pageCount === 0 ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
195
|
+
style: styles.pagePlaceholder,
|
|
196
|
+
children: loadingIndicator ? loadingIndicator() : /*#__PURE__*/(0, _jsxRuntime.jsx)(_index.Spinner, {
|
|
197
|
+
size: "lg"
|
|
198
|
+
})
|
|
199
|
+
}) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.FlatList, {
|
|
200
|
+
data: data,
|
|
201
|
+
keyExtractor: item => String(item.index),
|
|
202
|
+
renderItem: renderItem,
|
|
203
|
+
onViewableItemsChanged: onViewableItemsChanged.current,
|
|
204
|
+
viewabilityConfig: {
|
|
205
|
+
itemVisiblePercentThreshold: 50
|
|
206
|
+
},
|
|
207
|
+
initialNumToRender: 2,
|
|
208
|
+
windowSize: 3
|
|
54
209
|
})
|
|
55
210
|
});
|
|
56
211
|
};
|
|
@@ -60,10 +215,16 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
60
215
|
root: {
|
|
61
216
|
flex: 1
|
|
62
217
|
},
|
|
63
|
-
|
|
218
|
+
page: {
|
|
219
|
+
alignSelf: 'center'
|
|
220
|
+
},
|
|
221
|
+
pageImage: {
|
|
222
|
+
flex: 1
|
|
223
|
+
},
|
|
224
|
+
pagePlaceholder: {
|
|
64
225
|
flex: 1,
|
|
65
|
-
|
|
66
|
-
|
|
226
|
+
alignItems: 'center',
|
|
227
|
+
justifyContent: 'center'
|
|
67
228
|
}
|
|
68
229
|
});
|
|
69
230
|
var _default = exports.default = DocumentViewer;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = void 0;
|
|
7
|
+
var _reactNative = require("react-native");
|
|
8
|
+
var _default = exports.default = _reactNative.TurboModuleRegistry.get('PdfRasterizer');
|
|
9
|
+
//# sourceMappingURL=NativePdfRasterizer.js.map
|
|
@@ -1,20 +1,46 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
import { Logger } from '@webority-technologies/mobile-core';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
4
|
+
import { downloadFile } from '@webority-technologies/mobile-core/download';
|
|
5
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
6
|
+
import { FlatList, Image, PixelRatio, StyleSheet, useWindowDimensions, View } from 'react-native';
|
|
7
|
+
import PdfRasterizer from "../../specs/NativePdfRasterizer.js";
|
|
7
8
|
import { Spinner } from "../Spinner/index.js";
|
|
8
9
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
10
|
+
const HAS_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
|
|
11
|
+
const MAX_RENDER_SCALE = 3;
|
|
9
12
|
const toError = value => value instanceof Error ? value : new Error(String(value));
|
|
10
13
|
|
|
14
|
+
/** The renderPage scale is capped so a very wide view never asks the native
|
|
15
|
+
* rasterizer for an unbounded bitmap (see react-native.md's ARGB_8888 4-bytes/px
|
|
16
|
+
* guidance) — both native sides additionally clamp their own pixel ceiling. */
|
|
17
|
+
const scaleForWidth = (viewWidthPx, pageWidthPt) => {
|
|
18
|
+
if (pageWidthPt <= 0) {
|
|
19
|
+
return 1;
|
|
20
|
+
}
|
|
21
|
+
const raw = viewWidthPx / pageWidthPt * PixelRatio.get();
|
|
22
|
+
return Math.min(Math.max(raw, 1), MAX_RENDER_SCALE);
|
|
23
|
+
};
|
|
24
|
+
const localPathFromSource = async source => {
|
|
25
|
+
if (!HAS_SCHEME.test(source.uri) || source.uri.startsWith('file://')) {
|
|
26
|
+
return source.uri.replace(/^file:\/\//, '');
|
|
27
|
+
}
|
|
28
|
+
const result = await downloadFile({
|
|
29
|
+
url: source.uri,
|
|
30
|
+
headers: source.headers,
|
|
31
|
+
authenticated: false,
|
|
32
|
+
overwrite: source.cache === false
|
|
33
|
+
});
|
|
34
|
+
return result.path;
|
|
35
|
+
};
|
|
11
36
|
/**
|
|
12
|
-
* Renders a PDF
|
|
13
|
-
*
|
|
37
|
+
* Renders a PDF via our own native rasterizer (PDFKit on iOS, PdfRenderer on
|
|
38
|
+
* Android) — no third-party PDF dependency. The WebView fallback consumer
|
|
39
|
+
* apps use for non-PDF office documents is a distinct rendering strategy and
|
|
40
|
+
* stays app-side.
|
|
14
41
|
*/
|
|
15
42
|
export const DocumentViewer = ({
|
|
16
43
|
source,
|
|
17
|
-
trustAllCerts = false,
|
|
18
44
|
onLoadComplete,
|
|
19
45
|
onPageChanged,
|
|
20
46
|
onError,
|
|
@@ -23,29 +49,158 @@ export const DocumentViewer = ({
|
|
|
23
49
|
testID,
|
|
24
50
|
accessibilityLabel
|
|
25
51
|
}) => {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
const
|
|
52
|
+
const {
|
|
53
|
+
width: windowWidth
|
|
54
|
+
} = useWindowDimensions();
|
|
55
|
+
const [pageCount, setPageCount] = useState(0);
|
|
56
|
+
const [pageSizePt, setPageSizePt] = useState({
|
|
57
|
+
width: 612,
|
|
58
|
+
height: 792
|
|
59
|
+
}); // US Letter fallback
|
|
60
|
+
const pageAspect = pageSizePt.height / pageSizePt.width;
|
|
61
|
+
const [renderedPages, setRenderedPages] = useState({});
|
|
62
|
+
const handleRef = useRef(null);
|
|
63
|
+
const onErrorRef = useRef(onError);
|
|
64
|
+
onErrorRef.current = onError;
|
|
65
|
+
const onLoadCompleteRef = useRef(onLoadComplete);
|
|
66
|
+
onLoadCompleteRef.current = onLoadComplete;
|
|
67
|
+
const documentSource = useMemo(() => ({
|
|
68
|
+
uri: source.uri,
|
|
69
|
+
headers: source.headers,
|
|
70
|
+
cache: source.cache
|
|
71
|
+
}), [source.uri, source.headers, source.cache]);
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
let cancelled = false;
|
|
74
|
+
const load = async () => {
|
|
75
|
+
if (!PdfRasterizer) {
|
|
76
|
+
onErrorRef.current?.(new Error('[@webority-technologies/mobile-ui] PdfRasterizer native module is not linked. ' + 'Run pod install / rebuild the app after adding @webority-technologies/mobile-ui.'));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const path = await localPathFromSource(documentSource);
|
|
81
|
+
if (cancelled) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const opened = await PdfRasterizer.open(path);
|
|
85
|
+
if (cancelled) {
|
|
86
|
+
await PdfRasterizer.close(opened.handle).catch(() => undefined);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
handleRef.current = opened.handle;
|
|
90
|
+
setPageCount(opened.pageCount);
|
|
91
|
+
if (opened.pageWidth > 0 && opened.pageHeight > 0) {
|
|
92
|
+
setPageSizePt({
|
|
93
|
+
width: opened.pageWidth,
|
|
94
|
+
height: opened.pageHeight
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
onLoadCompleteRef.current?.(opened.pageCount);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (!cancelled) {
|
|
100
|
+
const err = toError(error);
|
|
101
|
+
Logger.error('[@webority-technologies/mobile-ui] DocumentViewer failed to load.', err);
|
|
102
|
+
onErrorRef.current?.(err);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
void load();
|
|
107
|
+
return () => {
|
|
108
|
+
cancelled = true;
|
|
109
|
+
};
|
|
110
|
+
}, [documentSource]);
|
|
111
|
+
useEffect(() => () => {
|
|
112
|
+
const openHandle = handleRef.current;
|
|
113
|
+
if (openHandle) {
|
|
114
|
+
PdfRasterizer?.close(openHandle).catch(() => undefined);
|
|
115
|
+
}
|
|
116
|
+
}, []);
|
|
117
|
+
const renderPageImage = useCallback(async pageIndex => {
|
|
118
|
+
const openHandle = handleRef.current;
|
|
119
|
+
if (!openHandle || !PdfRasterizer) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const scale = scaleForWidth(windowWidth, pageSizePt.width);
|
|
124
|
+
const uri = await PdfRasterizer.renderPage(openHandle, pageIndex, scale);
|
|
125
|
+
setRenderedPages(prev => prev[pageIndex] ? prev : {
|
|
126
|
+
...prev,
|
|
127
|
+
[pageIndex]: uri
|
|
128
|
+
});
|
|
129
|
+
} catch (error) {
|
|
130
|
+
Logger.error(`[@webority-technologies/mobile-ui] DocumentViewer failed to render page ${pageIndex}.`, toError(error));
|
|
131
|
+
}
|
|
132
|
+
}, [pageSizePt.width, windowWidth]);
|
|
133
|
+
const data = useMemo(() => Array.from({
|
|
134
|
+
length: pageCount
|
|
135
|
+
}, (_unused, index) => ({
|
|
136
|
+
index
|
|
137
|
+
})), [pageCount]);
|
|
138
|
+
const onPageChangedRef = useRef(onPageChanged);
|
|
139
|
+
onPageChangedRef.current = onPageChanged;
|
|
140
|
+
const pageCountRef = useRef(pageCount);
|
|
141
|
+
pageCountRef.current = pageCount;
|
|
142
|
+
const renderPageImageRef = useRef(renderPageImage);
|
|
143
|
+
renderPageImageRef.current = renderPageImage;
|
|
144
|
+
|
|
145
|
+
// A stable function identity: FlatList warns if onViewableItemsChanged changes
|
|
146
|
+
// identity across renders, so the current values it needs are read from refs
|
|
147
|
+
// kept up to date every render instead of being closed over here.
|
|
148
|
+
const onViewableItemsChanged = useRef(({
|
|
149
|
+
viewableItems
|
|
150
|
+
}) => {
|
|
151
|
+
const first = viewableItems[0]?.item;
|
|
152
|
+
if (first) {
|
|
153
|
+
onPageChangedRef.current?.(first.index + 1, pageCountRef.current);
|
|
154
|
+
viewableItems.forEach(v => void renderPageImageRef.current(v.item.index));
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
const renderItem = useCallback(({
|
|
158
|
+
item
|
|
159
|
+
}) => {
|
|
160
|
+
const uri = renderedPages[item.index];
|
|
161
|
+
const pageWidth = windowWidth;
|
|
162
|
+
const pageHeight = pageWidth * pageAspect;
|
|
163
|
+
return /*#__PURE__*/_jsx(View, {
|
|
164
|
+
style: [styles.page, {
|
|
165
|
+
width: pageWidth,
|
|
166
|
+
height: pageHeight
|
|
167
|
+
}],
|
|
168
|
+
children: uri ? /*#__PURE__*/_jsx(Image, {
|
|
169
|
+
source: {
|
|
170
|
+
uri
|
|
171
|
+
},
|
|
172
|
+
style: styles.pageImage,
|
|
173
|
+
resizeMode: "contain",
|
|
174
|
+
accessible: false
|
|
175
|
+
}) : /*#__PURE__*/_jsx(View, {
|
|
176
|
+
style: styles.pagePlaceholder,
|
|
177
|
+
children: loadingIndicator ? loadingIndicator() : /*#__PURE__*/_jsx(Spinner, {
|
|
178
|
+
size: "lg"
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
});
|
|
182
|
+
}, [loadingIndicator, pageAspect, renderedPages, windowWidth]);
|
|
35
183
|
return /*#__PURE__*/_jsx(View, {
|
|
36
184
|
style: [styles.root, style],
|
|
37
185
|
testID: testID,
|
|
38
186
|
accessible: accessibilityLabel !== undefined,
|
|
39
187
|
accessibilityLabel: accessibilityLabel,
|
|
40
188
|
accessibilityRole: "none",
|
|
41
|
-
children: /*#__PURE__*/_jsx(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
189
|
+
children: pageCount === 0 ? /*#__PURE__*/_jsx(View, {
|
|
190
|
+
style: styles.pagePlaceholder,
|
|
191
|
+
children: loadingIndicator ? loadingIndicator() : /*#__PURE__*/_jsx(Spinner, {
|
|
192
|
+
size: "lg"
|
|
193
|
+
})
|
|
194
|
+
}) : /*#__PURE__*/_jsx(FlatList, {
|
|
195
|
+
data: data,
|
|
196
|
+
keyExtractor: item => String(item.index),
|
|
197
|
+
renderItem: renderItem,
|
|
198
|
+
onViewableItemsChanged: onViewableItemsChanged.current,
|
|
199
|
+
viewabilityConfig: {
|
|
200
|
+
itemVisiblePercentThreshold: 50
|
|
201
|
+
},
|
|
202
|
+
initialNumToRender: 2,
|
|
203
|
+
windowSize: 3
|
|
49
204
|
})
|
|
50
205
|
});
|
|
51
206
|
};
|
|
@@ -54,10 +209,16 @@ const styles = StyleSheet.create({
|
|
|
54
209
|
root: {
|
|
55
210
|
flex: 1
|
|
56
211
|
},
|
|
57
|
-
|
|
212
|
+
page: {
|
|
213
|
+
alignSelf: 'center'
|
|
214
|
+
},
|
|
215
|
+
pageImage: {
|
|
216
|
+
flex: 1
|
|
217
|
+
},
|
|
218
|
+
pagePlaceholder: {
|
|
58
219
|
flex: 1,
|
|
59
|
-
|
|
60
|
-
|
|
220
|
+
alignItems: 'center',
|
|
221
|
+
justifyContent: 'center'
|
|
61
222
|
}
|
|
62
223
|
});
|
|
63
224
|
export default DocumentViewer;
|
|
@@ -3,12 +3,11 @@ import type { StyleProp, ViewStyle } from 'react-native';
|
|
|
3
3
|
export interface DocumentViewerSource {
|
|
4
4
|
uri: string;
|
|
5
5
|
headers?: Record<string, string>;
|
|
6
|
+
/** Reuse a previously downloaded copy of this URL instead of re-fetching. Default true. */
|
|
6
7
|
cache?: boolean;
|
|
7
8
|
}
|
|
8
9
|
export interface DocumentViewerProps {
|
|
9
10
|
source: DocumentViewerSource;
|
|
10
|
-
/** Trust self-signed / invalid certs on the document request. Default false. */
|
|
11
|
-
trustAllCerts?: boolean;
|
|
12
11
|
onLoadComplete?: (numberOfPages: number) => void;
|
|
13
12
|
onPageChanged?: (page: number, numberOfPages: number) => void;
|
|
14
13
|
onError?: (error: Error) => void;
|
|
@@ -19,8 +18,10 @@ export interface DocumentViewerProps {
|
|
|
19
18
|
accessibilityLabel?: string;
|
|
20
19
|
}
|
|
21
20
|
/**
|
|
22
|
-
* Renders a PDF
|
|
23
|
-
*
|
|
21
|
+
* Renders a PDF via our own native rasterizer (PDFKit on iOS, PdfRenderer on
|
|
22
|
+
* Android) — no third-party PDF dependency. The WebView fallback consumer
|
|
23
|
+
* apps use for non-PDF office documents is a distinct rendering strategy and
|
|
24
|
+
* stays app-side.
|
|
24
25
|
*/
|
|
25
26
|
export declare const DocumentViewer: React.FC<DocumentViewerProps>;
|
|
26
27
|
export default DocumentViewer;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { TurboModule } from 'react-native';
|
|
2
|
+
export interface PdfDocumentInfo {
|
|
3
|
+
/** Opaque handle for every subsequent call (getPageInfo/renderPage/close) — NOT the file path. */
|
|
4
|
+
handle: string;
|
|
5
|
+
pageCount: number;
|
|
6
|
+
/** Points (1/72 in), the unrotated page size of page 0 — a per-page size read happens in renderPage. */
|
|
7
|
+
pageWidth: number;
|
|
8
|
+
pageHeight: number;
|
|
9
|
+
}
|
|
10
|
+
export interface PdfPageInfo {
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
}
|
|
14
|
+
export interface Spec extends TurboModule {
|
|
15
|
+
/**
|
|
16
|
+
* Opens a LOCAL pdf file (a file:// path or bare absolute path — no http(s), no
|
|
17
|
+
* headers, no auth: callers download remote documents with mobile-core's
|
|
18
|
+
* `downloadFile` first). Returns a handle used by every other call. Throws
|
|
19
|
+
* (rejects) on a missing file, a corrupt PDF, or a password-protected PDF.
|
|
20
|
+
*/
|
|
21
|
+
open(path: string): Promise<PdfDocumentInfo>;
|
|
22
|
+
/**
|
|
23
|
+
* Reads one page's own size without rasterizing it (pages in a PDF can each
|
|
24
|
+
* have a different size/rotation).
|
|
25
|
+
*/
|
|
26
|
+
getPageInfo(handle: string, pageIndex: number): Promise<PdfPageInfo>;
|
|
27
|
+
/**
|
|
28
|
+
* Rasterizes one page to a PNG file in the cache directory and returns its
|
|
29
|
+
* absolute path. `scale` is a multiplier on the page's own point size (pass
|
|
30
|
+
* the view width / page width ratio, capped by the caller — see
|
|
31
|
+
* react-native.md's "RGBA_8888 is 4 bytes/px" guidance). Renders are
|
|
32
|
+
* serialized per handle on both platforms; call sequentially per document.
|
|
33
|
+
*/
|
|
34
|
+
renderPage(handle: string, pageIndex: number, scale: number): Promise<string>;
|
|
35
|
+
/** Releases the native document and evicts any cached page renders for it. */
|
|
36
|
+
close(handle: string): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
declare const _default: Spec | null;
|
|
39
|
+
export default _default;
|
|
40
|
+
//# sourceMappingURL=NativePdfRasterizer.d.ts.map
|
|
@@ -3,12 +3,11 @@ import type { StyleProp, ViewStyle } from 'react-native';
|
|
|
3
3
|
export interface DocumentViewerSource {
|
|
4
4
|
uri: string;
|
|
5
5
|
headers?: Record<string, string>;
|
|
6
|
+
/** Reuse a previously downloaded copy of this URL instead of re-fetching. Default true. */
|
|
6
7
|
cache?: boolean;
|
|
7
8
|
}
|
|
8
9
|
export interface DocumentViewerProps {
|
|
9
10
|
source: DocumentViewerSource;
|
|
10
|
-
/** Trust self-signed / invalid certs on the document request. Default false. */
|
|
11
|
-
trustAllCerts?: boolean;
|
|
12
11
|
onLoadComplete?: (numberOfPages: number) => void;
|
|
13
12
|
onPageChanged?: (page: number, numberOfPages: number) => void;
|
|
14
13
|
onError?: (error: Error) => void;
|
|
@@ -19,8 +18,10 @@ export interface DocumentViewerProps {
|
|
|
19
18
|
accessibilityLabel?: string;
|
|
20
19
|
}
|
|
21
20
|
/**
|
|
22
|
-
* Renders a PDF
|
|
23
|
-
*
|
|
21
|
+
* Renders a PDF via our own native rasterizer (PDFKit on iOS, PdfRenderer on
|
|
22
|
+
* Android) — no third-party PDF dependency. The WebView fallback consumer
|
|
23
|
+
* apps use for non-PDF office documents is a distinct rendering strategy and
|
|
24
|
+
* stays app-side.
|
|
24
25
|
*/
|
|
25
26
|
export declare const DocumentViewer: React.FC<DocumentViewerProps>;
|
|
26
27
|
export default DocumentViewer;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { TurboModule } from 'react-native';
|
|
2
|
+
export interface PdfDocumentInfo {
|
|
3
|
+
/** Opaque handle for every subsequent call (getPageInfo/renderPage/close) — NOT the file path. */
|
|
4
|
+
handle: string;
|
|
5
|
+
pageCount: number;
|
|
6
|
+
/** Points (1/72 in), the unrotated page size of page 0 — a per-page size read happens in renderPage. */
|
|
7
|
+
pageWidth: number;
|
|
8
|
+
pageHeight: number;
|
|
9
|
+
}
|
|
10
|
+
export interface PdfPageInfo {
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
}
|
|
14
|
+
export interface Spec extends TurboModule {
|
|
15
|
+
/**
|
|
16
|
+
* Opens a LOCAL pdf file (a file:// path or bare absolute path — no http(s), no
|
|
17
|
+
* headers, no auth: callers download remote documents with mobile-core's
|
|
18
|
+
* `downloadFile` first). Returns a handle used by every other call. Throws
|
|
19
|
+
* (rejects) on a missing file, a corrupt PDF, or a password-protected PDF.
|
|
20
|
+
*/
|
|
21
|
+
open(path: string): Promise<PdfDocumentInfo>;
|
|
22
|
+
/**
|
|
23
|
+
* Reads one page's own size without rasterizing it (pages in a PDF can each
|
|
24
|
+
* have a different size/rotation).
|
|
25
|
+
*/
|
|
26
|
+
getPageInfo(handle: string, pageIndex: number): Promise<PdfPageInfo>;
|
|
27
|
+
/**
|
|
28
|
+
* Rasterizes one page to a PNG file in the cache directory and returns its
|
|
29
|
+
* absolute path. `scale` is a multiplier on the page's own point size (pass
|
|
30
|
+
* the view width / page width ratio, capped by the caller — see
|
|
31
|
+
* react-native.md's "RGBA_8888 is 4 bytes/px" guidance). Renders are
|
|
32
|
+
* serialized per handle on both platforms; call sequentially per document.
|
|
33
|
+
*/
|
|
34
|
+
renderPage(handle: string, pageIndex: number, scale: number): Promise<string>;
|
|
35
|
+
/** Releases the native document and evicts any cached page renders for it. */
|
|
36
|
+
close(handle: string): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
declare const _default: Spec | null;
|
|
39
|
+
export default _default;
|
|
40
|
+
//# sourceMappingURL=NativePdfRasterizer.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webority-technologies/mobile-ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "Beautiful, animated, accessible React Native components, theme and form engine for Webority projects.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react-native",
|
|
@@ -47,11 +47,22 @@
|
|
|
47
47
|
"types": "./lib/typescript/commonjs/index.d.ts",
|
|
48
48
|
"files": [
|
|
49
49
|
"lib",
|
|
50
|
+
"ios",
|
|
51
|
+
"android",
|
|
52
|
+
"RNMobileUiPdfRasterizer.podspec",
|
|
50
53
|
"!**/__tests__",
|
|
51
54
|
"!**/*.test.*",
|
|
52
55
|
"!**/*.spec.*",
|
|
53
56
|
"!**/*.map"
|
|
54
57
|
],
|
|
58
|
+
"codegenConfig": {
|
|
59
|
+
"name": "RNMobileUiPdfRasterizerSpec",
|
|
60
|
+
"type": "modules",
|
|
61
|
+
"jsSrcsDir": "./src/specs",
|
|
62
|
+
"android": {
|
|
63
|
+
"javaPackageName": "com.webority.mobileui.pdfrasterizer"
|
|
64
|
+
}
|
|
65
|
+
},
|
|
55
66
|
"scripts": {
|
|
56
67
|
"build": "bob build",
|
|
57
68
|
"clean": "rimraf lib",
|
|
@@ -69,7 +80,6 @@
|
|
|
69
80
|
"react": "^19.1.0",
|
|
70
81
|
"react-native": ">=0.81.0",
|
|
71
82
|
"react-native-gesture-handler": "^2.21.0 || ^3.0.0",
|
|
72
|
-
"react-native-pdf": ">=7.0.4",
|
|
73
83
|
"react-native-reanimated": "^4.0.0",
|
|
74
84
|
"react-native-safe-area-context": ">=5.4.0",
|
|
75
85
|
"react-native-worklets": ">=0.10.0"
|
|
@@ -104,7 +114,6 @@
|
|
|
104
114
|
]
|
|
105
115
|
},
|
|
106
116
|
"devDependencies": {
|
|
107
|
-
"expo-haptics": "^57.0.2"
|
|
108
|
-
"react-native-pdf": "^7.0.4"
|
|
117
|
+
"expo-haptics": "^57.0.2"
|
|
109
118
|
}
|
|
110
119
|
}
|