diff --git a/ACKNOWLEDGEMENTS.md b/ACKNOWLEDGEMENTS.md index 54c664a3..49826c6f 100644 --- a/ACKNOWLEDGEMENTS.md +++ b/ACKNOWLEDGEMENTS.md @@ -3815,6 +3815,32 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +-------------------------------------------------------------------------------- +## swift-perception (https://github.com/pointfreeco/swift-perception) + +MIT License + +Copyright (c) 2023 Point-Free + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + -------------------------------------------------------------------------------- ## Tesseract (https://tesseract-ocr.github.io/) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbc750f4..c0b8ef4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ ## Newest Release +### 2.12.0 - 01 Aug 2024 + +- Adds APIs belonging to the `PDFDocument` interface, moving them away from the global namespace. (J#HYB-406) +- Adds support for using `React.RefObject` as `PSPDFKitView` ref property. (J#HYB-444) +- Updates for PSPDFKit 2024.3.1 for Android. +- Updates for PSPDFKit 13.8.0 for iOS. +- Fixes an issue where the `PSPDFKitView` sometimes failed to load the document on React Native Android. (J#HYB-397) +- Fixes an issue where Instant JSON containing widgets was not applied using the `addAnnotations` API on iOS. (J#HYB-413) +- Fixes an issue where password protected documents could not be saved after annotation changes were made. (J#HYB-454) +- Fixes an issue where the `onDocumentLoaded` callback was not called reliably on iOS. (J#HYB-480) + +## Previous Releases + ### 2.11.0 - 07 Jun 2024 - Adds the ability to clear the document cache. (J#HYB-347) @@ -9,8 +22,6 @@ - Fixes an issue where the annotation `uuid` isn't included in `onAnnotationTapped` callbacks. (J#HYB-374) - Fixes an issue where Instant configuration wasn't applied when using the `presentInstant` API on iOS. (J#HYB-375) -## Previous Releases - ### 2.10.0 - 06 May 2024 - Adds the ability to define annotation behavior using flags. (J#HYB-283) diff --git a/android/.settings/org.eclipse.buildship.core.prefs b/android/.settings/org.eclipse.buildship.core.prefs index f12fb09c..6d3acf55 100644 --- a/android/.settings/org.eclipse.buildship.core.prefs +++ b/android/.settings/org.eclipse.buildship.core.prefs @@ -1,4 +1,4 @@ -arguments=--init-script /var/folders/3v/qy3ssjxs2m7d97yc60nrl2l00000gn/T/d146c9752a26f79b52047fb6dc6ed385d064e120494f96f08ca63a317c41f94c.gradle --init-script /var/folders/3v/qy3ssjxs2m7d97yc60nrl2l00000gn/T/52cde0cfcf3e28b8b7510e992210d9614505e0911af0c190bd590d7158574963.gradle +arguments=--init-script /var/folders/3v/qy3ssjxs2m7d97yc60nrl2l00000gn/T/db3b08fc4a9ef609cb16b96b200fa13e563f396e9bb1ed0905fdab7bc3bc513b.gradle --init-script /var/folders/3v/qy3ssjxs2m7d97yc60nrl2l00000gn/T/52cde0cfcf3e28b8b7510e992210d9614505e0911af0c190bd590d7158574963.gradle auto.sync=false build.scans.enabled=false connection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(8.1.1)) diff --git a/android/build.gradle b/android/build.gradle index f57a5014..eb0681ae 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -15,7 +15,7 @@ * Contains gradle configuration constants */ ext { - PSPDFKIT_VERSION = '2024.2.1' + PSPDFKIT_VERSION = '2024.3.1' } buildscript { diff --git a/android/src/main/java/com/pspdfkit/react/PDFDocumentModule.kt b/android/src/main/java/com/pspdfkit/react/PDFDocumentModule.kt index 21f6972f..e508ee1e 100644 --- a/android/src/main/java/com/pspdfkit/react/PDFDocumentModule.kt +++ b/android/src/main/java/com/pspdfkit/react/PDFDocumentModule.kt @@ -1,16 +1,37 @@ package com.pspdfkit.react +import android.net.Uri +import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableArray +import com.facebook.react.bridge.ReadableMap import com.facebook.react.module.annotations.ReactModule +import com.pspdfkit.annotations.Annotation +import com.pspdfkit.annotations.AnnotationProvider.ALL_ANNOTATION_TYPES +import com.pspdfkit.annotations.AnnotationType import com.pspdfkit.document.PdfDocument +import com.pspdfkit.document.formatters.DocumentJsonFormatter +import com.pspdfkit.document.formatters.XfdfFormatter +import com.pspdfkit.document.providers.ContentResolverDataProvider +import com.pspdfkit.document.providers.DataProvider +import com.pspdfkit.internal.model.ImageDocumentImpl +import com.pspdfkit.react.helper.ConversionHelpers.getAnnotationTypeFromString +import com.pspdfkit.react.helper.DocumentJsonDataProvider +import com.pspdfkit.react.helper.JsonUtilities +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.schedulers.Schedulers +import org.json.JSONObject +import java.io.ByteArrayOutputStream +import java.util.EnumSet @ReactModule(name = PDFDocumentModule.NAME) class PDFDocumentModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { private var documents = mutableMapOf() + private var documentConfigurations = mutableMapOf>() override fun getName(): String { return NAME @@ -20,14 +41,22 @@ class PDFDocumentModule(reactContext: ReactApplicationContext) : ReactContextBas return this.documents[reference] } + private fun getDocumentConfiguration(reference: Int): MutableMap? { + return this.documentConfigurations[reference] + } + fun setDocument(document: PdfDocument, reference: Int) { this.documents[reference] = document } + fun updateDocumentConfiguration(key: String, value: Any, reference: Int) { + val currentConfiguration = documentConfigurations[reference] + currentConfiguration?.set(key, value) + } + @ReactMethod fun getDocumentId(reference: Int, promise: Promise) { try { - // Using uid here until Android exposes the documentId property. - promise.resolve(this.getDocument(reference)?.uid) + promise.resolve(this.getDocument(reference)?.documentIdString) } catch (e: Throwable) { promise.reject("getDocumentId error", e) } @@ -51,6 +80,242 @@ class PDFDocumentModule(reactContext: ReactApplicationContext) : ReactContextBas } } + @ReactMethod fun save(reference: Int, promise: Promise) { + try { + this.getDocument(reference)?.let { + if (it is ImageDocumentImpl.ImagePdfDocumentWrapper) { + val metadata = this.getDocumentConfiguration(reference)?.get("imageSaveMode")?.equals("flattenAndEmbed") == true + if (it.imageDocument.saveIfModified(metadata)) { + promise.resolve(true) + } + } else { + it.saveIfModified() + promise.resolve(true) + } + } + promise.reject("save error", RuntimeException("Could not save document")) + } catch (e: Throwable) { + promise.reject("save error", e) + } + } + + @ReactMethod fun getAllUnsavedAnnotations(reference: Int, promise: Promise) { + try { + this.getDocument(reference)?.let { + val outputStream = ByteArrayOutputStream() + DocumentJsonFormatter.exportDocumentJsonAsync(it, outputStream) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + val json = JSONObject(outputStream.toString()) + val jsonMap = JsonUtilities.jsonObjectToMap(json) + val nativeMap = Arguments.makeNativeMap(jsonMap) + promise.resolve(nativeMap) + }, { e -> + promise.reject(RuntimeException(e)) + } + ) + } + } catch (e: Throwable) { + promise.reject("getAllUnsavedAnnotations error", e) + } + } + + @ReactMethod fun getAnnotations(reference: Int, type: String?, promise: Promise) { + try { + this.getDocument(reference)?.let { + it.annotationProvider.getAllAnnotationsOfTypeAsync(if (type == null) ALL_ANNOTATION_TYPES else getAnnotationTypeFromString(type)) + .toList() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { annotations -> + var annotationsSerialized: ArrayList> = ArrayList() + for (annotation in annotations) { + if (annotation.type == AnnotationType.POPUP) { + continue + } + val annotationInstantJSON = JSONObject(annotation.toInstantJson()) + val annotationMap = JsonUtilities.jsonObjectToMap(annotationInstantJSON) + annotationMap["uuid"] = annotation.uuid + annotationsSerialized.add(annotationMap) + } + val nativeList = Arguments.makeNativeArray(annotationsSerialized) + promise.resolve(nativeList) + }, { e -> + promise.reject(RuntimeException(e)) + } + ) + } + } catch (e: Throwable) { + promise.reject("getAnnotations error", e) + } + } + + @ReactMethod fun getAnnotationsForPage(reference: Int, pageIndex: Int, type: String?, promise: Promise) { + try { + this.getDocument(reference)?.let { + + if (pageIndex > it.pageCount-1) { + promise.reject(RuntimeException("Specified page index is out of bounds")) + return + } + + it.annotationProvider.getAllAnnotationsOfTypeAsync(if (type == null) EnumSet.allOf(AnnotationType::class.java) else + getAnnotationTypeFromString(type), pageIndex, 1) + .toList() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { annotations -> + var annotationsSerialized: ArrayList> = ArrayList() + for (annotation in annotations) { + if (annotation.type == AnnotationType.POPUP) { + continue + } + val annotationInstantJSON = JSONObject(annotation.toInstantJson()) + val annotationMap = JsonUtilities.jsonObjectToMap(annotationInstantJSON) + annotationMap["uuid"] = annotation.uuid + annotationsSerialized.add(annotationMap) + } + val nativeList = Arguments.makeNativeArray(annotationsSerialized) + promise.resolve(nativeList) + }, { e -> + promise.reject(RuntimeException(e)) + } + ) + } + } catch (e: Throwable) { + promise.reject("getAnnotationsForPage error", e) + } + } + + @ReactMethod fun removeAnnotations(reference: Int, instantJSON: ReadableArray, promise: Promise) { + try { + this.getDocument(reference)?.let { + + val instantJSONArray: List> = instantJSON.toArrayList().filterIsInstance>() + var annotationsToDelete: ArrayList = ArrayList() + it.annotationProvider.getAllAnnotationsOfTypeAsync(ALL_ANNOTATION_TYPES) + .toList() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { annotations -> + for (annotation in annotations) { + for (instantJSONAnnotation in instantJSONArray) { + if (annotation.name == instantJSONAnnotation["name"] || + annotation.uuid == instantJSONAnnotation["uuid"]) { + annotationsToDelete.add(annotation) + } + } + } + + for (annotation in annotationsToDelete) { + it.annotationProvider.removeAnnotationFromPage(annotation) + } + promise.resolve(true) + }, { e -> + promise.reject(RuntimeException(e)) + } + ) + } + } catch (e: Throwable) { + promise.reject("removeAnnotations error", e) + } + } + + @ReactMethod fun addAnnotations(reference: Int, instantJSON: ReadableMap, promise: Promise) { + try { + this.getDocument(reference)?.let { + val json = JSONObject(instantJSON.toHashMap()) + val dataProvider: DataProvider = DocumentJsonDataProvider(json) + DocumentJsonFormatter.importDocumentJsonAsync(it, dataProvider) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ + promise.resolve(true) + }, { e -> + promise.reject(RuntimeException(e)) + }) + } + } catch (e: Throwable) { + promise.reject("addAnnotations error", e) + } + } + + @ReactMethod fun importXFDF(reference: Int, filePath: String, promise: Promise) { + try { + this.getDocument(reference)?.let { + var importPath = filePath; + if (Uri.parse(importPath).scheme == null) { + importPath = "file:///$filePath"; + } + + XfdfFormatter.parseXfdfAsync(it, ContentResolverDataProvider((Uri.parse(importPath)))) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { annotations -> + for (annotation in annotations) { + it.annotationProvider.addAnnotationToPage(annotation) + } + val result = JSONObject() + result.put("success", true) + val jsonMap = JsonUtilities.jsonObjectToMap(result) + val nativeMap = Arguments.makeNativeMap(jsonMap) + promise.resolve(nativeMap) + }, { e -> + promise.reject("importXFDF error", e) + }) + } + } catch (e: Throwable) { + promise.reject("importXFDF error", e) + } + } + + @ReactMethod fun exportXFDF(reference: Int, filePath: String, promise: Promise) { + try { + this.getDocument(reference)?.let { + var exportPath = filePath; + if (Uri.parse(exportPath).scheme == null) { + exportPath = "file:///$filePath"; + } + + val outputStream = reactApplicationContext.contentResolver.openOutputStream(Uri.parse(exportPath)) + if (outputStream == null) { + promise.reject("exportXFDF error", RuntimeException("Could not write to supplied file path error")) + return + } + + val allAnnotations = it.annotationProvider.getAllAnnotationsOfType(ALL_ANNOTATION_TYPES) + val allFormFields = it.formProvider.formFields + + XfdfFormatter.writeXfdfAsync(it, allAnnotations, allFormFields, outputStream) + XfdfFormatter.parseXfdfAsync(it, ContentResolverDataProvider((Uri.parse(exportPath)))) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { annotations -> + for (annotation in annotations) { + it.annotationProvider.addAnnotationToPage(annotation) + } + val result = JSONObject() + result.put("success", true) + result.put("filePath", filePath) + val jsonMap = JsonUtilities.jsonObjectToMap(result) + val nativeMap = Arguments.makeNativeMap(jsonMap) + promise.resolve(nativeMap) + }, { e -> + promise.reject("exportXFDF error", e) + }) + } + } catch (e: Throwable) { + promise.reject("exportXFDF error", e) + } + } + companion object { const val NAME = "PDFDocumentManager" } diff --git a/android/src/main/java/com/pspdfkit/react/PSPDFKitModule.java b/android/src/main/java/com/pspdfkit/react/PSPDFKitModule.java index 866829b1..9e865422 100644 --- a/android/src/main/java/com/pspdfkit/react/PSPDFKitModule.java +++ b/android/src/main/java/com/pspdfkit/react/PSPDFKitModule.java @@ -178,7 +178,19 @@ public void presentInstant(@NonNull ReadableMap documentData, @NonNull ReadableM ConfigurationAdapter configurationAdapter = new ConfigurationAdapter(getCurrentActivity(), configuration); lastPresentPromise = promise; - RNInstantPdfActivity.showInstantDocument(getCurrentActivity(), serverUrl, jwt, configurationAdapter.build()); + + Handler mainHandler = new Handler(getReactApplicationContext().getMainLooper()); + Runnable myRunnable = new Runnable() { + @Override + public void run() { + try { + RNInstantPdfActivity.showInstantDocument(getCurrentActivity(), serverUrl, jwt, configurationAdapter.build()); + } catch (Exception e) { + // Could not start instant + } + } + }; + mainHandler.post(myRunnable); } } diff --git a/android/src/main/java/com/pspdfkit/react/helper/MeasurementsHelper.kt b/android/src/main/java/com/pspdfkit/react/helper/MeasurementsHelper.kt index 65ef0b4d..9a840876 100644 --- a/android/src/main/java/com/pspdfkit/react/helper/MeasurementsHelper.kt +++ b/android/src/main/java/com/pspdfkit/react/helper/MeasurementsHelper.kt @@ -34,6 +34,7 @@ class MeasurementsHelper { */ @JvmStatic fun removeMeasurementConfiguration(pdfFragment: PdfFragment, configurations :Map) { + @Suppress("UNCHECKED_CAST") val measurementValueConfiguration = convertMeasurementConfiguration(configurations["configuration"] as Map) val deleteAssociatedAnnotations = configurations["deleteAssociatedAnnotations"] as Boolean val addToUndoStack = configurations["addToUndo"] as Boolean @@ -57,6 +58,7 @@ class MeasurementsHelper { * @param pdfFragment The [PdfFragment] in which the measurement configuration should be modified. * @param args The arguments for the modification. */ + @Suppress("UNCHECKED_CAST") @JvmStatic fun modifyMeasurementConfiguration(pdfFragment: PdfFragment, args : Map) { val newMeasurementValueConfiguration = convertMeasurementConfiguration(args["newConfiguration"] as Map?) @@ -73,6 +75,7 @@ class MeasurementsHelper { * @param measurementConfigurations The map representation of the measurement configuration. * @return The [MeasurementValueConfiguration] representation of the measurement configuration. */ + @Suppress("UNCHECKED_CAST") @JvmStatic fun convertMeasurementConfiguration(measurementConfigurations: Map?): MeasurementValueConfiguration { val scale = diff --git a/android/src/main/java/com/pspdfkit/react/helper/RemoteDocumentDownloader.kt b/android/src/main/java/com/pspdfkit/react/helper/RemoteDocumentDownloader.kt index de4892fa..a5358099 100644 --- a/android/src/main/java/com/pspdfkit/react/helper/RemoteDocumentDownloader.kt +++ b/android/src/main/java/com/pspdfkit/react/helper/RemoteDocumentDownloader.kt @@ -33,9 +33,11 @@ class RemoteDocumentDownloader(private val remoteURL: String, } if (overwriteExisting && destinationFileURL != null) { - val delete = File(destinationFileURL) - if (delete.exists()) { - delete.delete() + val delete = destinationFileURL?.let { File(it) } + if (delete != null) { + if (delete.exists()) { + delete.delete() + } } } @@ -43,7 +45,7 @@ class RemoteDocumentDownloader(private val remoteURL: String, .source(source) .outputFile(if (destinationFileURL == null) File(context.getDir("documents", Context.MODE_PRIVATE), "temp.pdf") else - File(destinationFileURL)) + destinationFileURL?.let { File(it) }) .overwriteExisting(overwriteExisting) .useTemporaryOutputFile(false) .build() diff --git a/android/src/main/java/com/pspdfkit/views/PdfView.java b/android/src/main/java/com/pspdfkit/views/PdfView.java index 949d8022..3b42c913 100644 --- a/android/src/main/java/com/pspdfkit/views/PdfView.java +++ b/android/src/main/java/com/pspdfkit/views/PdfView.java @@ -22,10 +22,12 @@ import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; +import android.os.Handler; import android.util.AttributeSet; import android.util.Log; import android.util.Pair; import android.view.Choreographer; +import android.view.LayoutInflater; import android.view.View; import android.view.ViewTreeObserver; import android.widget.FrameLayout; @@ -34,6 +36,7 @@ import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.DrawableCompat; +import androidx.fragment.app.FragmentContainerView; import androidx.fragment.app.FragmentManager; import com.facebook.react.bridge.ReactApplicationContext; @@ -202,6 +205,10 @@ public class PdfView extends FrameLayout { @Nullable private ReadableArray measurementValueConfigurations; + private FragmentContainerView fragmentContainerView; + + private ReactApplicationContext reactApplicationContext; + public PdfView(@NonNull Context context) { super(context); init(); @@ -223,6 +230,7 @@ public PdfView(@NonNull Context context, @Nullable AttributeSet attrs, int defSt } private void init() { + fragmentContainerView = (FragmentContainerView) LayoutInflater.from(getContext()).inflate(R.layout.pspdf__fragment_container, null); pdfViewModeController = new PdfViewModeController(this); Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() { @@ -317,6 +325,8 @@ public void setDocument(@Nullable String documentPath, ReactApplicationContext r return; } + this.reactApplicationContext = reactApplicationContext; + if (Uri.parse(documentPath).getScheme() == null) { // If there is no scheme it might be a raw path. try { @@ -330,7 +340,6 @@ public void setDocument(@Nullable String documentPath, ReactApplicationContext r documentOpeningDisposable.dispose(); } this.documentPath = documentPath; - updateState(); if (Uri.parse(documentPath).getScheme().toLowerCase(Locale.getDefault()).contains("http")) { String outputFilePath = this.remoteDocumentConfiguration != null && @@ -351,6 +360,7 @@ public void setDocument(@Nullable String documentPath, ReactApplicationContext r .subscribe(pdfDocument -> { PdfView.this.document = pdfDocument; reactApplicationContext.getNativeModule(PDFDocumentModule.class).setDocument(pdfDocument, this.getId()); + reactApplicationContext.getNativeModule(PDFDocumentModule.class).updateDocumentConfiguration("imageSaveMode", imageSaveMode, this.getId()); setupFragment(false); }, throwable -> { // The Android SDK will present password UI, do not emit an error. @@ -371,6 +381,7 @@ public void setDocument(@Nullable String documentPath, ReactApplicationContext r .subscribe(imageDocument -> { PdfView.this.document = imageDocument.getDocument(); reactApplicationContext.getNativeModule(PDFDocumentModule.class).setDocument(imageDocument.getDocument(), this.getId()); + reactApplicationContext.getNativeModule(PDFDocumentModule.class).updateDocumentConfiguration("imageSaveMode", imageSaveMode, this.getId()); setupFragment(false); }, throwable -> { PdfView.this.document = null; @@ -384,6 +395,7 @@ public void setDocument(@Nullable String documentPath, ReactApplicationContext r .subscribe(pdfDocument -> { PdfView.this.document = pdfDocument; reactApplicationContext.getNativeModule(PDFDocumentModule.class).setDocument(pdfDocument, this.getId()); + reactApplicationContext.getNativeModule(PDFDocumentModule.class).updateDocumentConfiguration("imageSaveMode", imageSaveMode, this.getId()); setupFragment(false); }, throwable -> { // The Android SDK will present password UI, do not emit an error. @@ -560,50 +572,70 @@ private void setupFragment(boolean recreate) { prepareFragment(pdfFragment, true); } } + fragment = pdfFragment; + } + } - if (pdfFragment.getDocument() != null) { - if (pageIndex <= document.getPageCount()-1) { - pdfFragment.setPageIndex(pageIndex, true); - } + private void postFragmentSetup(PdfUiFragment pdfFragment) { + updateState(); + attachPdfFragmentListeners(pdfFragment); + updateAnnotationConfiguration(); + if (pdfFragment.getDocument() != null) { + if (pageIndex <= document.getPageCount()-1) { + pdfFragment.setPageIndex(pageIndex, true); } - - fragment = pdfFragment; - pdfUiFragmentGetter.onNext(Collections.singletonList(pdfFragment)); } + pdfUiFragmentGetter.onNext(Collections.singletonList(pdfFragment)); } private void prepareFragment(final PdfUiFragment pdfUiFragment, final boolean attachFragment) { if (attachFragment) { + fragmentContainerView.addOnAttachStateChangeListener(new OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(@NonNull View view) { + Handler mainHandler = new Handler(getContext().getMainLooper()); + Runnable myRunnable = new Runnable() { + @Override + public void run() { + try { + fragmentManager + .beginTransaction() + .add(R.id.pspdf__fragment_container + , pdfUiFragment) + .commitNowAllowingStateLoss(); + postFragmentSetup(pdfUiFragment); + } catch (Exception e) { + // Could not add fragment + } + } + }; + mainHandler.post(myRunnable); + } - getRootView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override - public void onGlobalLayout() { - // Reattach the fragment if running on API >= 34, there is a compatibility issue with React Native StackScreen fragments. - if (Build.VERSION.SDK_INT >= 34) { - fragmentManager.beginTransaction() - .detach(pdfUiFragment) - .commitNow(); - - fragmentManager.beginTransaction() - .attach(pdfUiFragment) - .commitNow(); - - addView(pdfUiFragment.getView(), LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); - attachPdfFragmentListeners(pdfUiFragment); - updateAnnotationConfiguration(); - } - getRootView().getViewTreeObserver().removeOnGlobalLayoutListener(this); + public void onViewDetachedFromWindow(@NonNull View view) { + Handler mainHandler = new Handler(getContext().getMainLooper()); + Runnable myRunnable = new Runnable() { + @Override + public void run() { + try { + fragmentManager + .beginTransaction() + .remove(pdfUiFragment) + .commitNowAllowingStateLoss(); + } catch (Exception e) { + // Could not remove fragment + } + } + }; + mainHandler.post(myRunnable); } }); - - fragmentManager.beginTransaction() - .add(pdfUiFragment, fragmentTag) - .commitNow(); - - View fragmentView = pdfUiFragment.getView(); - addView(fragmentView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + removeAllViews(); + addView(fragmentContainerView); + } else { + attachPdfFragmentListeners(pdfUiFragment); } - attachPdfFragmentListeners(pdfUiFragment); } private void attachPdfFragmentListeners(final PdfUiFragment pdfUiFragment) { @@ -648,6 +680,9 @@ private void preparePdfFragment(@NonNull PdfFragment pdfFragment) { pdfFragment.addDocumentListener(new SimpleDocumentListener() { @Override public void onDocumentLoaded(@NonNull PdfDocument document) { + if (reactApplicationContext != null) { + reactApplicationContext.getNativeModule(PDFDocumentModule.class).setDocument(document, getId()); + } manuallyLayoutChildren(); if (pageIndex <= document.getPageCount()-1) { pdfFragment.setPageIndex(pageIndex, false); @@ -758,16 +793,16 @@ public void exitCurrentlyActiveMode() { public boolean saveCurrentDocument() throws Exception { if (fragment != null) { try { - if (document instanceof ImageDocumentImpl.ImagePdfDocumentWrapper) { + if (fragment.getDocument() instanceof ImageDocumentImpl.ImagePdfDocumentWrapper) { boolean metadata = this.imageSaveMode.equals("flattenAndEmbed") ? true : false; - if (((ImageDocumentImpl.ImagePdfDocumentWrapper) document).getImageDocument().saveIfModified(metadata)) { + if (((ImageDocumentImpl.ImagePdfDocumentWrapper) fragment.getDocument()).getImageDocument().saveIfModified(metadata)) { // Since the document listeners won't be called when manually saving we also dispatch this event here. eventDispatcher.dispatchEvent(new PdfViewDocumentSavedEvent(getId())); return true; } } else { - if (document.saveIfModified()) { + if (fragment.getDocument().saveIfModified()) { // Since the document listeners won't be called when manually saving we also dispatch this event here. eventDispatcher.dispatchEvent(new PdfViewDocumentSavedEvent(getId())); return true; diff --git a/android/src/main/res/layout/pspdf__fragment_container.xml b/android/src/main/res/layout/pspdf__fragment_container.xml new file mode 100644 index 00000000..0a9fc59b --- /dev/null +++ b/android/src/main/res/layout/pspdf__fragment_container.xml @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/index.js b/index.js index aef1c475..76e87b85 100644 --- a/index.js +++ b/index.js @@ -46,6 +46,10 @@ class PSPDFKitView extends React.Component { * @ignore */ _pdfDocument = null; + /** + * @ignore + */ + _componentRef = React.createRef(this); render() { if (Platform.OS === 'ios' || Platform.OS === 'android') { @@ -56,7 +60,7 @@ class PSPDFKitView extends React.Component { : null; return ( { + * // Document load failed event. + * }} + */ + onDocumentLoadFailed: PropTypes.func, /** * Callback that’s called when the document is saved. * @type {function} diff --git a/ios/RCTPSPDFKit/Helpers/SessionStorage.swift b/ios/RCTPSPDFKit/Helpers/SessionStorage.swift index cce43ced..b94f00fd 100644 --- a/ios/RCTPSPDFKit/Helpers/SessionStorage.swift +++ b/ios/RCTPSPDFKit/Helpers/SessionStorage.swift @@ -14,14 +14,20 @@ import Foundation @objc public class SessionStorage: NSObject { + @objc public enum CallbackType: Int { + case onDocumentLoaded + } + var annotationContextualMenuItems: NSDictionary! var barButtonItems: NSMutableDictionary! var closeButtonAttributes: NSDictionary! + var pendingCallbacks: NSMutableArray! override public init() { annotationContextualMenuItems = [:] barButtonItems = [:] closeButtonAttributes = [:] + pendingCallbacks = [] super .init() } @@ -48,4 +54,16 @@ import Foundation @objc public func getCloseButtonAttributes() -> NSDictionary { return closeButtonAttributes } + + @objc public func addPendingCallback(_ type: CallbackType) { + pendingCallbacks.add(type.rawValue) + } + + @objc public func removePendingCallback(_ type: CallbackType) { + pendingCallbacks.remove(type.rawValue) + } + + @objc public func getPendingCallbacks() -> NSArray { + return pendingCallbacks + } } diff --git a/ios/RCTPSPDFKit/PDFDocumentManager.m b/ios/RCTPSPDFKit/PDFDocumentManager.m index 95f8c88d..583bfb93 100644 --- a/ios/RCTPSPDFKit/PDFDocumentManager.m +++ b/ios/RCTPSPDFKit/PDFDocumentManager.m @@ -15,6 +15,25 @@ @interface RCT_EXTERN_MODULE(PDFDocumentManager, NSObject) RCT_EXTERN_METHOD(getDocumentId:(NSNumber _Nonnull)reference onSuccess:(RCTPromiseResolveBlock)resolve onError:(RCTPromiseRejectBlock)reject); + RCT_EXTERN_METHOD(invalidateCache:(NSNumber _Nonnull)reference onSuccess:(RCTPromiseResolveBlock)resolve onError:(RCTPromiseRejectBlock)reject); + RCT_EXTERN_METHOD(invalidateCacheForPage:(NSNumber _Nonnull)reference pageIndex:(NSInteger)pageIndex onSuccess:(RCTPromiseResolveBlock)resolve onError:(RCTPromiseRejectBlock)reject); + +RCT_EXTERN_METHOD(save:(NSNumber _Nonnull)reference onSuccess:(RCTPromiseResolveBlock)resolve onError:(RCTPromiseRejectBlock)reject); + +RCT_EXTERN_METHOD(getAllUnsavedAnnotations:(NSNumber _Nonnull)reference onSuccess:(RCTPromiseResolveBlock)resolve onError:(RCTPromiseRejectBlock)reject); + +RCT_EXTERN_METHOD(getAnnotations:(NSNumber _Nonnull)reference type:(NSString *)type onSuccess:(RCTPromiseResolveBlock)resolve onError:(RCTPromiseRejectBlock)reject); + +RCT_EXTERN_METHOD(getAnnotationsForPage:(NSNumber _Nonnull)reference pageIndex:(NSInteger)pageIndex type:(NSString *)type onSuccess:(RCTPromiseResolveBlock)resolve onError:(RCTPromiseRejectBlock)reject); + +RCT_EXTERN_METHOD(removeAnnotations:(NSNumber _Nonnull)reference instantJSON:(NSArray *)instantJSON onSuccess:(RCTPromiseResolveBlock)resolve onError:(RCTPromiseRejectBlock)reject); + +RCT_EXTERN_METHOD(addAnnotations:(NSNumber _Nonnull)reference instantJSON:(NSDictionary *)instantJSON onSuccess:(RCTPromiseResolveBlock)resolve onError:(RCTPromiseRejectBlock)reject); + +RCT_EXTERN_METHOD(importXFDF:(NSNumber _Nonnull)reference filePath:(NSString *)filePath onSuccess:(RCTPromiseResolveBlock)resolve onError:(RCTPromiseRejectBlock)reject); + +RCT_EXTERN_METHOD(exportXFDF:(NSNumber _Nonnull)reference filePath:(NSString *)filePath onSuccess:(RCTPromiseResolveBlock)resolve onError:(RCTPromiseRejectBlock)reject); + @end diff --git a/ios/RCTPSPDFKit/PDFDocumentManager.swift b/ios/RCTPSPDFKit/PDFDocumentManager.swift index ff8e8a17..146eed0a 100644 --- a/ios/RCTPSPDFKit/PDFDocumentManager.swift +++ b/ios/RCTPSPDFKit/PDFDocumentManager.swift @@ -11,9 +11,15 @@ import Foundation import React import PSPDFKit +@objc public protocol PDFDocumentManagerDelegate { + @objc optional func didGenerateCallbackEvent(name: String, data: Dictionary) + @objc optional func reloadControllerData() +} + @objc(PDFDocumentManager) public class PDFDocumentManager: NSObject { var documents = [NSNumber:Document]() + @objc public var delegate: PDFDocumentManagerDelegate? private func getDocument(_ reference: NSNumber) -> Document? { return documents[reference] @@ -49,7 +55,218 @@ import PSPDFKit onError("invalidateCache", "Document is nil", nil) return } - SDK.shared.cache.remove(for: document) + document.clearCache() onSuccess(true) } + + @objc func save(_ reference: NSNumber, onSuccess: @escaping RCTPromiseResolveBlock, onError: @escaping RCTPromiseRejectBlock) -> Void { + guard let document = getDocument(reference) else { + onError("save", "Document is nil", nil) + return + } + + document.save { result in + switch result { + case .success: + onSuccess(true) + return + case .failure(let error): + onError("save", error.localizedDescription, nil) + return + } + } + } + + @objc func getAllUnsavedAnnotations(_ reference: NSNumber, onSuccess: @escaping RCTPromiseResolveBlock, onError: @escaping RCTPromiseRejectBlock) -> Void { + guard let document = getDocument(reference) else { + onError("getAllUnsavedAnnotations", "Document is nil", nil) + return + } + guard let documentProvider = document.documentProviders.first else { + onError("getAllUnsavedAnnotations", "DocumentProvider is nil", nil) + return + } + guard let data = try? document.generateInstantJSON(from: documentProvider) else { + onError("getAllUnsavedAnnotations", "Could not export annotations", nil) + return + } + if let json = try? JSONSerialization.jsonObject(with: data) { + onSuccess(json) + } else { + onError("getAllUnsavedAnnotations", "Could not export annotations", nil) + } + } + + @objc func getAnnotations(_ reference: NSNumber, type: String?, onSuccess: @escaping RCTPromiseResolveBlock, onError: @escaping RCTPromiseRejectBlock) -> Void { + guard let document = getDocument(reference) else { + onError("getAnnotations", "Document is nil", nil) + return + } + + let allAnnotations = document.allAnnotations(of: type == nil ? + .all : RCTConvert.annotationType(fromInstantJSONType: type)) + .flatMap({ $0.value }) + + if let allAnnotationsJSON = try? RCTConvert.instantJSON(from: allAnnotations) { + onSuccess(allAnnotationsJSON) + } else { + onError("getAnnotations", "Could not export annotations", nil) + } + } + + @objc func getAnnotationsForPage(_ reference: NSNumber, pageIndex: Int, type: String?, onSuccess: @escaping RCTPromiseResolveBlock, onError: @escaping RCTPromiseRejectBlock) -> Void { + guard let document = getDocument(reference) else { + onError("getAnnotationsForPage", "Document is nil", nil) + return + } + + let allAnnotations = document.annotationsForPage(at: PageIndex(pageIndex), type: type == nil ? .all : RCTConvert.annotationType(fromInstantJSONType: type)) + + if let allAnnotationsJSON = try? RCTConvert.instantJSON(from: allAnnotations) { + onSuccess(allAnnotationsJSON) + } else { + onError("getAnnotationsForPage", "Could not export annotations", nil) + } + } + + @objc func removeAnnotations(_ reference: NSNumber, instantJSON: Array>, onSuccess: @escaping RCTPromiseResolveBlock, onError: @escaping RCTPromiseRejectBlock) -> Void { + guard let document = getDocument(reference) else { + onError("removeAnnotations", "Document is nil", nil) + return + } + var annotationsToDelete = Array() + let allAnnotations = document.allAnnotations(of: .all).flatMap({ $0.value }) + for annotation in allAnnotations { + for instantJSONAnnotation in instantJSON { + if let instantJSONName = instantJSONAnnotation["name"] as? String { + if (annotation.name == instantJSONName) { + annotationsToDelete.append(annotation) + } + } + else if let instantJSONUUID = instantJSONAnnotation["uuid"] as? String { + if (annotation.uuid == instantJSONUUID) { + annotationsToDelete.append(annotation) + } + } + } + } + + if (annotationsToDelete.isEmpty) { + onError("removeAnnotations", "No annotations found to delete", nil) + return + } + let result = document.remove(annotations: annotationsToDelete) + onSuccess(result) + } + + @objc func addAnnotations(_ reference: NSNumber, instantJSON: Dictionary, onSuccess: @escaping RCTPromiseResolveBlock, onError: @escaping RCTPromiseRejectBlock) -> Void { + guard let document = getDocument(reference) else { + onError("addAnnotations", "Document is nil", nil) + return + } + + do { + let data = try JSONSerialization.data(withJSONObject: instantJSON) + let dataContainerProvider = DataContainerProvider(data: data) + + guard let documentProvider = document.documentProviders.first else { + onError("addAnnotations", "DocumentProvider is nil", nil) + return + } + + do { + try document.applyInstantJSON(fromDataProvider: dataContainerProvider, to: documentProvider, lenient: false) + + // Calculate the diff to get the full Annotation data (most importantly the uuid) before calling onAnnotationsChanged + var annotationArray = Array() + let allAnnotations = document.allAnnotations(of: .all).flatMap({ $0.value }) + for annotation in allAnnotations { + if let annotations = instantJSON["annotations"] as? Array> { + for addedAnnotation in annotations { + if let instantJSONName = addedAnnotation["name"] as? String { + if (annotation.name == instantJSONName) { + annotationArray.append(annotation) + break + } + } + } + } + } + + // Delegate to RCTPSPDFKitView to reload controller data + delegate?.reloadControllerData?() + onSuccess(true) + // Emit the onAnnotationsChanged event since document.applyInstantJSON doesn't trigger the event on iOS + if let annotationInstantJSONArray = try? RCTConvert.instantJSON(from: annotationArray) { + delegate?.didGenerateCallbackEvent?(name: "onAnnotationsChanged", data: ["change" : "added", "annotations" : annotationInstantJSONArray]) + } + return + } + catch { + onError("addAnnotations", error.localizedDescription, nil) + return + } + } catch { + onError("addAnnotations", "Cannot serialize instantJSON data", nil) + return + } + } + + @objc func importXFDF(_ reference: NSNumber, filePath: String, onSuccess: @escaping RCTPromiseResolveBlock, onError: @escaping RCTPromiseRejectBlock) -> Void { + guard let document = getDocument(reference) else { + onError("importXFDF", "Document is nil", nil) + return + } + + guard let externalAnnotationsFile = RCTConvert.parseURL(filePath) else { + onError("importXFDF", "Could not find XFDF file", nil) + return + } + + guard let documentProvider = document.documentProviders.first else { + onError("importXFDF", "DocumentProvider is nil", nil) + return + } + + let dataProvider = FileDataProvider(fileURL: externalAnnotationsFile) + let parser = XFDFParser(dataProvider: dataProvider, documentProvider: documentProvider) + guard let annotations = try? parser.parse() else { + onError("importXFDF", "Could not parse XFDF annotations", nil) + return + } + + let result = document.add(annotations: annotations) + let response = ["success" : result] + onSuccess(response) + } + + @objc func exportXFDF(_ reference: NSNumber, filePath: String, onSuccess: @escaping RCTPromiseResolveBlock, onError: @escaping RCTPromiseRejectBlock) -> Void { + guard let document = getDocument(reference) else { + onError("exportXFDF", "Document is nil", nil) + return + } + + guard let externalAnnotationsFile = RCTConvert.parseURL(filePath) else { + onError("exportXFDF", "Could not find XFDF file", nil) + return + } + + guard let documentProvider = document.documentProviders.first else { + onError("exportXFDF", "DocumentProvider is nil", nil) + return + } + + let allAnnotations = document.allAnnotations(of: .all).flatMap({ $0.value }) + guard let dataSink = try? FileDataSink(fileURL: externalAnnotationsFile) else { + onError("exportXFDF", "Could open XFDF file", nil) + return + } + + guard let result = try? XFDFWriter().write(allAnnotations, to: dataSink, documentProvider: documentProvider) else { + onError("exportXFDF", "Could not write to XFDF file", nil) + return + } + let response = ["success" : true, "filePath" : filePath] as [String : Any] + onSuccess(response) + } } diff --git a/ios/RCTPSPDFKit/RCTPSPDFKitView.h b/ios/RCTPSPDFKit/RCTPSPDFKitView.h index bcdacadd..6f0d271e 100644 --- a/ios/RCTPSPDFKit/RCTPSPDFKitView.h +++ b/ios/RCTPSPDFKit/RCTPSPDFKitView.h @@ -38,6 +38,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, copy, nullable) NSArray *availableFontNames; @property (nonatomic, copy, nullable) NSString *selectedFontName; @property (nonatomic) BOOL showDownloadableFonts; +@property (nonatomic) id configurationJSON; /// Annotation Toolbar - (BOOL)enterAnnotationCreationMode; diff --git a/ios/RCTPSPDFKit/RCTPSPDFKitView.m b/ios/RCTPSPDFKit/RCTPSPDFKitView.m index 223a824d..dc89bb01 100644 --- a/ios/RCTPSPDFKit/RCTPSPDFKitView.m +++ b/ios/RCTPSPDFKit/RCTPSPDFKitView.m @@ -13,6 +13,7 @@ #import "RCTConvert+PSPDFViewMode.h" #import "RCTConvert+UIBarButtonItem.h" #import "RCTConvert+PSPDFDocument.h" +#import "RCTConvert+PSPDFConfiguration.h" #if __has_include("PSPDFKitReactNativeiOS-Swift.h") #import "PSPDFKitReactNativeiOS-Swift.h" #else @@ -24,10 +25,11 @@ @interface RCTPSPDFKitViewController : PSPDFViewController @end -@interface RCTPSPDFKitView () +@interface RCTPSPDFKitView () @property (nonatomic, nullable) UIViewController *topController; @property (nonatomic, strong) SessionStorage *sessionStorage; +@property (nonatomic) BOOL isPropsSet; @end @@ -53,11 +55,17 @@ - (instancetype)initWithFrame:(CGRect)frame { [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(annotationChangedNotification:) name:PSPDFAnnotationsRemovedNotification object:nil]; [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(spreadIndexDidChange:) name:PSPDFDocumentViewControllerSpreadIndexDidChangeNotification object:nil]; + + [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(documentDidFinishRendering) name:PSPDFDocumentViewControllerDidConfigureSpreadViewNotification object:nil]; } return self; } +- (void)setDocument:(PSPDFDocument *)document { + self.pdfController.document = document; +} + - (void)removeFromSuperview { // When the React Native `PSPDFKitView` in unmounted, we need to dismiss the `PSPDFViewController` to avoid orphan popovers. // See https://github.com/PSPDFKit/react-native/issues/277 @@ -104,6 +112,7 @@ - (void)destroyViewControllerRelationship { [self.topController willMoveToParentViewController:nil]; [self.topController removeFromParentViewController]; } + self.pdfController.document = nil; } - (void)closeButtonPressed:(nullable id)sender { @@ -336,6 +345,7 @@ - (BOOL)addAnnotations:(id)jsonAnnotations error:(NSError *_Nullable *)error { PSPDFDataContainerProvider *dataContainerProvider = [[PSPDFDataContainerProvider alloc] initWithData:data]; PSPDFDocument *document = self.pdfController.document; VALIDATE_DOCUMENT(document, NO) + [document clearCache]; PSPDFDocumentProvider *documentProvider = document.documentProviders.firstObject; BOOL success = [document applyInstantJSONFromDataProvider:dataContainerProvider toDocumentProvider:documentProvider lenient:NO error:error]; if (!success) { @@ -521,6 +531,20 @@ - (void)spreadIndexDidChange:(NSNotification *)notification { [self onStateChangedForPDFViewController:self.pdfController pageView:pageView pageAtIndex:pageIndex]; } +- (void)documentDidFinishRendering { + // Remove observer after the initial notification + [NSNotificationCenter.defaultCenter removeObserver:self + name:PSPDFDocumentViewControllerDidConfigureSpreadViewNotification + object:nil]; + if ([self isPropsSet] == YES) { + if (self.onDocumentLoaded) { + self.onDocumentLoaded(@{}); + } + } else { + [_sessionStorage addPendingCallback:CallbackTypeOnDocumentLoaded]; + } +} + // MARK: - Customize the Toolbar - (void)setLeftBarButtonItems:(nullable NSArray *)items forViewMode:(nullable NSString *) viewMode animated:(BOOL)animated { @@ -630,6 +654,71 @@ - (void)setAnnotationContextualMenuItems:(NSDictionary *)items { // MARK: - Helpers +- (void)didSetProps:(NSArray *)changedProps { + [super didSetProps:changedProps]; + [self setIsPropsSet:YES]; + NSArray *pending = [_sessionStorage getPendingCallbacks]; + [self processPendingCallbacks:pending]; + // Only apply config once all React props have been loaded + if (_configurationJSON != nil) { + [self applyDocumentConfiguration:_configurationJSON]; + } +} + +- (void)processPendingCallbacks:(NSArray *)pending { + for (int i = 0; i < pending.count; i++) { + CallbackType callback = [pending[i] integerValue]; + switch (callback) { + case CallbackTypeOnDocumentLoaded: + if (self.onDocumentLoaded) { + self.onDocumentLoaded(@{}); + } + break; + + default: + break; + } + [_sessionStorage removePendingCallback:callback]; + } +} + +- (void)applyDocumentConfiguration:(id)configuration { + [self.pdfController updateConfigurationWithBuilder:^(PSPDFConfigurationBuilder *builder) { + [builder setupFromJSON:configuration]; + }]; + + [self postProcessConfigurationOptionsWithJSON:configuration forPDFViewController:self.pdfController]; +} + +// These options are configuration options in Android, but not on iOS, so we apply them +// manually after we update the document configuration. +- (void)postProcessConfigurationOptionsWithJSON:(id)json forPDFViewController:(PSPDFViewController *)controller { + if (json) { + NSDictionary *dictionary = [RCTConvert processConfigurationOptionsDictionaryForPrefix:[RCTConvert NSDictionary:json]]; + if (dictionary[@"toolbarTitle"]) { + NSString *title = [RCTConvert NSString:dictionary[@"toolbarTitle"]]; + controller.title = title; + } + if (dictionary[@"invertColors"]) { + BOOL shouldInvertColors = [RCTConvert BOOL:dictionary[@"invertColors"]]; + controller.appearanceModeManager.appearanceMode = shouldInvertColors ? PSPDFAppearanceModeNight : PSPDFAppearanceModeDefault; + } + + if ([dictionary objectForKey:@"documentPassword"]) { + [controller.document unlockWithPassword:[dictionary objectForKey:@"documentPassword"]]; + } + + // Apply any measurementValueConfigurations once the document is loaded + if ([dictionary objectForKey:@"measurementValueConfigurations"]) { + NSArray *configs = [dictionary objectForKey:@"measurementValueConfigurations"]; + for (NSDictionary *config in configs) { + [PspdfkitMeasurementConvertor addMeasurementValueConfigurationWithDocument:controller.document + configuration:config]; + } + } + } +} + - (void)onStateChangedForPDFViewController:(PSPDFViewController *)pdfController pageView:(PSPDFPageView *)pageView pageAtIndex:(NSInteger)pageIndex { if (self.onStateChanged) { BOOL isDocumentLoaded = [pdfController.document isValid]; @@ -705,6 +794,22 @@ - (void)handleCustomAnnotationContextualMenuItemEvent:(id)sender { } } +// MARK - Delegates + +- (void)didGenerateCallbackEventWithName:(NSString *)name data:(NSDictionary *)data { + if ([name isEqualToString:@"onAnnotationsChanged"]) { + if (self.onAnnotationsChanged) { + self.onAnnotationsChanged(data); + } + } +} + +- (void)reloadControllerData { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.pdfController reloadData]; + }); +} + @end @implementation RCTPSPDFKitViewController diff --git a/ios/RCTPSPDFKit/RCTPSPDFKitViewManager.h b/ios/RCTPSPDFKit/RCTPSPDFKitViewManager.h index 5fd0467b..e2da7a42 100644 --- a/ios/RCTPSPDFKit/RCTPSPDFKitViewManager.h +++ b/ios/RCTPSPDFKit/RCTPSPDFKitViewManager.h @@ -11,6 +11,4 @@ @interface RCTPSPDFKitViewManager : RCTViewManager -@property (nonatomic, strong) id configuration; - @end diff --git a/ios/RCTPSPDFKit/RCTPSPDFKitViewManager.m b/ios/RCTPSPDFKit/RCTPSPDFKitViewManager.m index 7d3ac0b2..2dead845 100644 --- a/ios/RCTPSPDFKit/RCTPSPDFKitViewManager.m +++ b/ios/RCTPSPDFKit/RCTPSPDFKitViewManager.m @@ -46,41 +46,30 @@ @implementation RCTPSPDFKitViewManager RCT_CUSTOM_VIEW_PROPERTY(document, PSPDFDocument, RCTPSPDFKitView) { if (json) { - view.pdfController.document = [RCTConvert PSPDFDocument:json - remoteDocumentConfig:[_configuration objectForKey:@"remoteDocumentConfiguration"]]; + view.pdfController.document = [RCTConvert PSPDFDocument:json + remoteDocumentConfig:[view.configurationJSON objectForKey:@"remoteDocumentConfiguration"]]; view.pdfController.document.delegate = (id)view; PDFDocumentManager *documentManager = [self.bridge moduleForClass:[PDFDocumentManager class]]; [documentManager setDocument:view.pdfController.document reference:view.reactTag]; + [documentManager setDelegate:(id)view]; // The following properties need to be set after the document is set. // We set them again here when we're certain the document exists. if (view.annotationAuthorName) { view.pdfController.document.defaultAnnotationUsername = view.annotationAuthorName; } - + view.pdfController.pageIndex = view.pageIndex; - if ([_configuration objectForKey:@"documentPassword"]) { - [view.pdfController.document unlockWithPassword:[_configuration objectForKey:@"documentPassword"]]; - } if ([view.pdfController.document isKindOfClass:[PSPDFImageDocument class]]) { PSPDFImageDocument *imageDocument = (PSPDFImageDocument *)view.pdfController.document; imageDocument.imageSaveMode = view.imageSaveMode; } - - // Apply any measurementValueConfigurations once the document is loaded - if ([_configuration objectForKey:@"measurementValueConfigurations"]) { - NSArray *configs = [_configuration objectForKey:@"measurementValueConfigurations"]; - for (NSDictionary *config in configs) { - [PspdfkitMeasurementConvertor addMeasurementValueConfigurationWithDocument:view.pdfController.document - configuration:config]; - } - } - _configuration = nil; - if(view.onDocumentLoaded) { - view.onDocumentLoaded(@{}); - } + + [view.pdfController updateConfigurationWithBuilder:^(PSPDFConfigurationBuilder *builder) { + [builder overrideClass:PSPDFFontPickerViewController.class withClass:CustomFontPickerViewController.class]; + }]; } } @@ -94,13 +83,7 @@ @implementation RCTPSPDFKitViewManager RCT_CUSTOM_VIEW_PROPERTY(configuration, PSPDFConfiguration, RCTPSPDFKitView) { if (json) { - [view.pdfController updateConfigurationWithBuilder:^(PSPDFConfigurationBuilder *builder) { - [builder overrideClass:PSPDFFontPickerViewController.class withClass:CustomFontPickerViewController.class]; - [builder setupFromJSON:json]; - _configuration = json; - }]; - - [self postProcessConfigurationOptionsWithJSON: json forPDFViewController: view.pdfController]; + view.configurationJSON = json; } } @@ -111,22 +94,6 @@ @implementation RCTPSPDFKitViewManager } } -// These options are configuration options in Android, but not on iOS, so we apply them -// after we update the actual configuration. -- (void)postProcessConfigurationOptionsWithJSON:(id)json forPDFViewController:(PSPDFViewController *)controller { - if (json) { - NSDictionary *dictionary = [RCTConvert processConfigurationOptionsDictionaryForPrefix:[RCTConvert NSDictionary:json]]; - if (dictionary[@"toolbarTitle"]) { - NSString *title = [RCTConvert NSString:dictionary[@"toolbarTitle"]]; - controller.title = title; - } - if (dictionary[@"invertColors"]) { - BOOL shouldInvertColors = [RCTConvert BOOL:dictionary[@"invertColors"]]; - controller.appearanceModeManager.appearanceMode = shouldInvertColors ? PSPDFAppearanceModeNight : PSPDFAppearanceModeDefault; - } - } -} - RCT_CUSTOM_VIEW_PROPERTY(annotationAuthorName, NSString, RCTPSPDFKitView) { if (json) { view.pdfController.document.defaultAnnotationUsername = json; diff --git a/lib/document/PDFDocument.js b/lib/document/PDFDocument.js index 5b736503..93015054 100644 --- a/lib/document/PDFDocument.js +++ b/lib/document/PDFDocument.js @@ -19,9 +19,10 @@ var PDFDocument = /** @class */ (function () { * @memberof PDFDocument * @description Returns a document identifier (inferred from a document provider if possible). * A permanent identifier based on the contents of the file at the time it was originally created. - * If a document identifier is not available, generated UID value is returned. + * If a document identifier is not available, a generated UID value is returned. * @example * const documentId = await this.pdfRef.current?.getDocument()?.getDocumentId(); + * @returns { Promise } A promise containing the document identifier. */ PDFDocument.prototype.getDocumentId = function () { return react_native_1.NativeModules.PDFDocumentManager.getDocumentId((0, react_native_1.findNodeHandle)(this.pdfViewRef)); @@ -33,7 +34,8 @@ var PDFDocument = /** @class */ (function () { * @description Invalidates the rendered cache of the given pageIndex for this document. * Use this method if a single page of the document is not updated after a change, or changed externally, and needs to be re-rendered. * @example - * const result = await this.pdfRef.current?.getDocument().invalidateCacheForPage(0); + * const result = await this.pdfRef.current?.getDocument()?.invalidateCacheForPage(0); + * @returns { Promise } A promise containing the result of the operation. ```true``` if the cache was invalidated, ```false``` otherwise. */ PDFDocument.prototype.invalidateCacheForPage = function (pageIndex) { return react_native_1.NativeModules.PDFDocumentManager.invalidateCacheForPage((0, react_native_1.findNodeHandle)(this.pdfViewRef), pageIndex); @@ -44,11 +46,107 @@ var PDFDocument = /** @class */ (function () { * @description Invalidates the rendered cache of all the pages for this document. * Use this method if the document is not updated after a change, or changed externally, and needs to be re-rendered. * @example - * const result = await this.pdfRef.current?.getDocument().invalidateCache(); + * const result = await this.pdfRef.current?.getDocument()?.invalidateCache(); + * @returns { Promise } A promise containing the result of the operation. ```true``` if the cache was invalidated, ```false``` otherwise. */ PDFDocument.prototype.invalidateCache = function () { return react_native_1.NativeModules.PDFDocumentManager.invalidateCache((0, react_native_1.findNodeHandle)(this.pdfViewRef)); }; + /** + * @method save + * @memberof PDFDocument + * @description Saves the document asynchronously. + * @example + * const result = await this.pdfRef.current?.getDocument()?.save(); + * @returns { Promise } A promise containing the result of the operation. ```true``` if the document was saved, ```false``` otherwise. + */ + PDFDocument.prototype.save = function () { + return react_native_1.NativeModules.PDFDocumentManager.save((0, react_native_1.findNodeHandle)(this.pdfViewRef)); + }; + /** + * @method getAllUnsavedAnnotations + * @memberof PDFDocument + * @description Gets all the unsaved changes to annotations in the document. + * @example + * const result = await this.pdfRef.current?.getDocument()?.getAllUnsavedAnnotations(); + * @returns { Promise> } A promise containing the unsaved annotations as an array, wrapped in a Map with the mandatory 'annotations' key. + */ + PDFDocument.prototype.getAllUnsavedAnnotations = function () { + return react_native_1.NativeModules.PDFDocumentManager.getAllUnsavedAnnotations((0, react_native_1.findNodeHandle)(this.pdfViewRef)); + }; + /** + * @method getAnnotations + * @memberof PDFDocument + * @param {string} [type] The type of annotation to get. If not specified, all annotation types are returned. + * @description Gets all the annotations in the document for a specified type. + * @example + * const result = await this.pdfRef.current?.getDocument()?.getAnnotations("pspdfkit/ink"); + * @returns { Promise> } A promise containing the annotations of the document as an array. + */ + PDFDocument.prototype.getAnnotations = function (type) { + return react_native_1.NativeModules.PDFDocumentManager.getAnnotations((0, react_native_1.findNodeHandle)(this.pdfViewRef), type); + }; + /** + * @method getAnnotationsForPage + * @memberof PDFDocument + * @param {number} pageIndex The page index to get annotations for. Starts at 0. + * @param {string} [type] The type of annotation to get. If not specified, all annotation types are returned. + * @description Gets all the annotations in the document for a specified type. + * @example + * const result = await this.pdfRef.current?.getDocument()?.getAnnotationsForPage(0, "pspdfkit/ink"); + * @returns { Promise> } A promise containing the annotations for the specified page of the document as an array. + */ + PDFDocument.prototype.getAnnotationsForPage = function (pageIndex, type) { + return react_native_1.NativeModules.PDFDocumentManager.getAnnotationsForPage((0, react_native_1.findNodeHandle)(this.pdfViewRef), pageIndex, type); + }; + /** + * @method removeAnnotations + * @memberof PDFDocument + * @param {Array} instantJSON An array of the annotations to remove in InstantJSON format. Should not include the "annotations" key as used in the ```addAnnotations``` API. + * @description Removes all the specified annotations from the document. + * @example + * const result = await this.pdfRef.current?.getDocument()?.removeAnnotations(annotations); + * @returns { Promise } A promise containing the result of the operation. ```true``` if the annotations were removed, ```false``` otherwise. + */ + PDFDocument.prototype.removeAnnotations = function (instantJSON) { + return react_native_1.NativeModules.PDFDocumentManager.removeAnnotations((0, react_native_1.findNodeHandle)(this.pdfViewRef), instantJSON); + }; + /** + * @method addAnnotations + * @memberof PDFDocument + * @param {Record} instantJSON The annotations to add to the document in InstantJSON format. Ensure that the "annotations" key is included with an array of all the annotations as value. + * @description Adds all the specified annotations in the document. + * @example + * const result = await this.pdfRef.current?.getDocument()?.addAnnotations(annotations); + * @returns { Promise } A promise containing the result of the operation. ```true``` if the annotations were added, ```false``` otherwise. + */ + PDFDocument.prototype.addAnnotations = function (instantJSON) { + return react_native_1.NativeModules.PDFDocumentManager.addAnnotations((0, react_native_1.findNodeHandle)(this.pdfViewRef), instantJSON); + }; + /** + * @method importXFDF + * @memberof PDFDocument + * @param {string} filePath The path to the XFDF file to import. + * @description Imports the supplied XFDF file into the current document. + * @example + * const result = await this.pdfRef.current?.getDocument()?.importXFDF('path/to/XFDF.xfdf'); + * @returns { Promise } A promise containing an object with the result. ```true``` if the xfdf file was imported successfully, and ```false``` if an error occurred. + */ + PDFDocument.prototype.importXFDF = function (filePath) { + return react_native_1.NativeModules.PDFDocumentManager.importXFDF((0, react_native_1.findNodeHandle)(this.pdfViewRef), filePath); + }; + /** + * @method exportXFDF + * @memberof PDFDocument + * @param {string} filePath The path where the XFDF file should be exported to. + * @description Exports the annotations from the current document to a XFDF file. + * @example + * const result = await this.pdfRef.current?.getDocument()?.exportXFDF('path/to/XFDF.xfdf'); + * @returns { Promise } A promise containing an object with the exported file path and result. ```true``` if the xfdf file was exported successfully, and ```false``` if an error occurred. + */ + PDFDocument.prototype.exportXFDF = function (filePath) { + return react_native_1.NativeModules.PDFDocumentManager.exportXFDF((0, react_native_1.findNodeHandle)(this.pdfViewRef), filePath); + }; return PDFDocument; }()); exports.PDFDocument = PDFDocument; diff --git a/package.json b/package.json index 649db87c..d3550e3f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-pspdfkit", - "version": "2.11.0", + "version": "2.12.0", "description": "React Native PDF Library by PSPDFKit", "keywords": [ "react native", diff --git a/react-native-pspdfkit.podspec b/react-native-pspdfkit.podspec index 28e22560..81414939 100644 --- a/react-native-pspdfkit.podspec +++ b/react-native-pspdfkit.podspec @@ -20,7 +20,7 @@ Pod::Spec.new do |s| s.source = { git: "https://github.com/PSPDFKit/react-native" } s.source_files = "ios/*.{xcodeproj}", "ios/RCTPSPDFKit/*.{h,m,swift}", "ios/RCTPSPDFKit/Converters/*.{h,m,swift}", "ios/RCTPSPDFKit/Helpers/*.{h,m,swift}" s.dependency("React") - s.dependency("PSPDFKit", "13.5.0") - s.dependency("Instant", "13.5.0") + s.dependency("PSPDFKit", "13.8.0") + s.dependency("Instant", "13.8.0") s.frameworks = "UIKit" end diff --git a/samples/Catalog/ExamplesNavigationMenu.tsx b/samples/Catalog/ExamplesNavigationMenu.tsx index 73946dcd..744821a8 100644 --- a/samples/Catalog/ExamplesNavigationMenu.tsx +++ b/samples/Catalog/ExamplesNavigationMenu.tsx @@ -4,7 +4,6 @@ import { exampleDocumentName, exampleDocumentPath, exampleImagePath, - exampleReportName, exampleXFDFName, tiffImagePath, } from './configuration/Constants'; diff --git a/samples/Catalog/android/app/build.gradle b/samples/Catalog/android/app/build.gradle index 8edd994c..51b988d2 100644 --- a/samples/Catalog/android/app/build.gradle +++ b/samples/Catalog/android/app/build.gradle @@ -150,7 +150,6 @@ android { dependencies { // The version of react-native is set by the React Native Gradle Plugin implementation("com.facebook.react:react-android") - implementation("com.facebook.react:flipper-integration") if (hermesEnabled.toBoolean()) { implementation("com.facebook.react:hermes-android") diff --git a/samples/Catalog/android/app/src/main/java/com/pspdfkit/rn/MainApplication.java b/samples/Catalog/android/app/src/main/java/com/pspdfkit/rn/MainApplication.java index 990e9ab1..9a04e03f 100644 --- a/samples/Catalog/android/app/src/main/java/com/pspdfkit/rn/MainApplication.java +++ b/samples/Catalog/android/app/src/main/java/com/pspdfkit/rn/MainApplication.java @@ -11,7 +11,6 @@ import com.facebook.react.shell.MainReactPackage; import com.facebook.soloader.SoLoader; import com.rnfs.RNFSPackage; -import com.facebook.react.flipper.ReactNativeFlipper; import java.util.Arrays; import java.util.List; @@ -70,6 +69,5 @@ public void onCreate() { // If you opted-in for the New Architecture, we load the native entry point for this app. DefaultNewArchitectureEntryPoint.load(); } - ReactNativeFlipper.initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); } } diff --git a/samples/Catalog/android/build.gradle b/samples/Catalog/android/build.gradle index e1ecabdf..2e2a724c 100644 --- a/samples/Catalog/android/build.gradle +++ b/samples/Catalog/android/build.gradle @@ -3,13 +3,12 @@ buildscript { ext { buildToolsVersion = "34.0.0" - minSdkVersion = 21 + minSdkVersion = 23 compileSdkVersion = 34 targetSdkVersion = 34 - // We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP. - ndkVersion = "23.1.7779620" - kotlin_version = '1.8.10' + ndkVersion = "26.1.10909125" + kotlinVersion = "1.9.22" } repositories { google() @@ -20,9 +19,9 @@ buildscript { } dependencies { - classpath('com.android.tools.build:gradle:8.1.0') + classpath("com.android.tools.build:gradle:8.1.0") classpath("com.facebook.react:react-native-gradle-plugin") - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/samples/Catalog/android/gradle.properties b/samples/Catalog/android/gradle.properties index a3b2fa12..a46a5b90 100644 --- a/samples/Catalog/android/gradle.properties +++ b/samples/Catalog/android/gradle.properties @@ -24,9 +24,6 @@ android.useAndroidX=true # Automatically convert third-party libraries to use AndroidX android.enableJetifier=true -# Version of flipper SDK to use with React Native -FLIPPER_VERSION=0.182.0 - # Use this property to specify which architecture you want to build. # You can also override it from the CLI using # ./gradlew -PreactNativeArchitectures=x86_64 diff --git a/samples/Catalog/android/gradle/wrapper/gradle-wrapper.properties b/samples/Catalog/android/gradle/wrapper/gradle-wrapper.properties index 0c85a1f7..a21c6ebe 100644 --- a/samples/Catalog/android/gradle/wrapper/gradle-wrapper.properties +++ b/samples/Catalog/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-all.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/samples/Catalog/examples/ManualSave.tsx b/samples/Catalog/examples/ManualSave.tsx index 2d85588d..ee7e3657 100644 --- a/samples/Catalog/examples/ManualSave.tsx +++ b/samples/Catalog/examples/ManualSave.tsx @@ -42,8 +42,7 @@ export class ManualSave extends BaseExampleAutoHidingHeaderComponent { testID={'Save Button'} onPress={() => { // Manual Save - this.pdfRef?.current - ?.saveCurrentDocument() + this.pdfRef?.current?.getDocument().save() .then(saved => { if (saved) { Alert.alert( diff --git a/samples/Catalog/examples/OpenImageDocument.tsx b/samples/Catalog/examples/OpenImageDocument.tsx index bccb62ff..b9786af1 100644 --- a/samples/Catalog/examples/OpenImageDocument.tsx +++ b/samples/Catalog/examples/OpenImageDocument.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { processColor, View } from 'react-native'; +import { Alert, Button, processColor, View } from 'react-native'; import PSPDFKitView from 'react-native-pspdfkit'; import { pspdfkitColor, tiffImagePath } from '../configuration/Constants'; @@ -35,6 +35,22 @@ export class OpenImageDocument extends BaseExampleAutoHidingHeaderComponent { onNavigationButtonClicked={() => navigation.goBack()} style={styles.pdfColor} /> + + +