@webority-technologies/mobile-ui 0.0.7 → 0.0.9

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