@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.
- 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 +199 -24
- package/lib/commonjs/specs/NativePdfRasterizer.js +9 -0
- package/lib/module/components/DocumentViewer/DocumentViewer.js +201 -26
- package/lib/module/specs/NativePdfRasterizer.js +5 -0
- package/lib/typescript/commonjs/components/DocumentViewer/DocumentViewer.d.ts +12 -4
- package/lib/typescript/commonjs/specs/NativePdfRasterizer.d.ts +40 -0
- package/lib/typescript/module/components/DocumentViewer/DocumentViewer.d.ts +12 -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
|