@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.
@@ -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 _reactNativePdf = _interopRequireDefault(require("react-native-pdf"));
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 only the WebView fallback consumer apps use for non-PDF
18
- * office documents is a distinct rendering strategy and stays app-side.
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 renderActivityIndicator = (0, _react.useCallback)(() => loadingIndicator ? loadingIndicator() : /*#__PURE__*/(0, _jsxRuntime.jsx)(_index.Spinner, {
32
- size: "lg"
33
- }), [loadingIndicator]);
34
- const handleError = (0, _react.useCallback)(error => {
35
- const err = toError(error);
36
- _mobileCore.Logger.error('[@webority-technologies/mobile-ui] DocumentViewer failed to load.', err);
37
- onError?.(err);
38
- }, [onError]);
39
- const handleLoadComplete = (0, _react.useCallback)(numberOfPages => onLoadComplete?.(numberOfPages), [onLoadComplete]);
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)(_reactNativePdf.default, {
47
- source: source,
48
- trustAllCerts: trustAllCerts,
49
- style: styles.pdf,
50
- renderActivityIndicator: renderActivityIndicator,
51
- onLoadComplete: handleLoadComplete,
52
- onPageChanged: onPageChanged,
53
- onError: handleError
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
- pdf: {
218
+ page: {
219
+ alignSelf: 'center'
220
+ },
221
+ pageImage: {
222
+ flex: 1
223
+ },
224
+ pagePlaceholder: {
64
225
  flex: 1,
65
- width: '100%',
66
- height: '100%'
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 { useCallback } from 'react';
5
- import { StyleSheet, View } from 'react-native';
6
- import Pdf from 'react-native-pdf';
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 only the WebView fallback consumer apps use for non-PDF
13
- * office documents is a distinct rendering strategy and stays app-side.
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 renderActivityIndicator = useCallback(() => loadingIndicator ? loadingIndicator() : /*#__PURE__*/_jsx(Spinner, {
27
- size: "lg"
28
- }), [loadingIndicator]);
29
- const handleError = useCallback(error => {
30
- const err = toError(error);
31
- Logger.error('[@webority-technologies/mobile-ui] DocumentViewer failed to load.', err);
32
- onError?.(err);
33
- }, [onError]);
34
- const handleLoadComplete = useCallback(numberOfPages => onLoadComplete?.(numberOfPages), [onLoadComplete]);
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(Pdf, {
42
- source: source,
43
- trustAllCerts: trustAllCerts,
44
- style: styles.pdf,
45
- renderActivityIndicator: renderActivityIndicator,
46
- onLoadComplete: handleLoadComplete,
47
- onPageChanged: onPageChanged,
48
- onError: handleError
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
- pdf: {
212
+ page: {
213
+ alignSelf: 'center'
214
+ },
215
+ pageImage: {
216
+ flex: 1
217
+ },
218
+ pagePlaceholder: {
58
219
  flex: 1,
59
- width: '100%',
60
- height: '100%'
220
+ alignItems: 'center',
221
+ justifyContent: 'center'
61
222
  }
62
223
  });
63
224
  export default DocumentViewer;
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+
3
+ import { TurboModuleRegistry } from 'react-native';
4
+ export default TurboModuleRegistry.get('PdfRasterizer');
5
+ //# sourceMappingURL=NativePdfRasterizer.js.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 only the WebView fallback consumer apps use for non-PDF
23
- * office documents is a distinct rendering strategy and stays app-side.
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 only the WebView fallback consumer apps use for non-PDF
23
- * office documents is a distinct rendering strategy and stays app-side.
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.7",
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
  }