From d55dda08542bde82d024ca052cbedf5d348c0fb1 Mon Sep 17 00:00:00 2001 From: Thomas Pucci Date: Tue, 18 Nov 2025 11:40:11 +0100 Subject: [PATCH 1/4] Update Podfile.lock and Runner.xcscheme for integration_test path and custom LLDB init --- packages/pdfx/example/ios/Podfile.lock | 8 +++++++- .../xcshareddata/xcschemes/Runner.xcscheme | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/pdfx/example/ios/Podfile.lock b/packages/pdfx/example/ios/Podfile.lock index 6844dcd5..e249b168 100644 --- a/packages/pdfx/example/ios/Podfile.lock +++ b/packages/pdfx/example/ios/Podfile.lock @@ -1,22 +1,28 @@ PODS: - Flutter (1.0.0) + - integration_test (0.0.1): + - Flutter - pdfx (1.0.0): - Flutter DEPENDENCIES: - Flutter (from `Flutter`) + - integration_test (from `.symlinks/plugins/integration_test/ios`) - pdfx (from `.symlinks/plugins/pdfx/ios`) EXTERNAL SOURCES: Flutter: :path: Flutter + integration_test: + :path: ".symlinks/plugins/integration_test/ios" pdfx: :path: ".symlinks/plugins/pdfx/ios" SPEC CHECKSUMS: Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e pdfx: 77f4dddc48361fbb01486fa2bdee4532cbb97ef3 PODFILE CHECKSUM: 4305caec6b40dde0ae97be1573c53de1882a07e5 -COCOAPODS: 1.16.2 +COCOAPODS: 1.14.2 diff --git a/packages/pdfx/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/pdfx/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index d795332e..c3fedb29 100644 --- a/packages/pdfx/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/pdfx/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -44,6 +44,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> Date: Tue, 18 Nov 2025 12:15:59 +0100 Subject: [PATCH 2/4] Add password support for opening PDF documents across platforms --- .../src/main/kotlin/io/scer/pdfx/Messages.kt | 28 ++++++---- .../pdfx/ios/Classes/SwiftPdfxPlugin.swift | 44 +++++++++------ .../renderer/io/platform_method_channel.dart | 15 ++++-- packages/pdfx/windows/pdfx.cpp | 16 +++--- packages/pdfx/windows/pdfx.h | 8 +-- packages/pdfx/windows/pdfx_plugin.cpp | 54 ++++++++++++++++--- 6 files changed, 120 insertions(+), 45 deletions(-) diff --git a/packages/pdfx/android/src/main/kotlin/io/scer/pdfx/Messages.kt b/packages/pdfx/android/src/main/kotlin/io/scer/pdfx/Messages.kt index f34afe43..c3c8187a 100644 --- a/packages/pdfx/android/src/main/kotlin/io/scer/pdfx/Messages.kt +++ b/packages/pdfx/android/src/main/kotlin/io/scer/pdfx/Messages.kt @@ -4,9 +4,11 @@ import android.graphics.Bitmap import android.graphics.Color import android.graphics.Matrix import android.graphics.Rect +import android.graphics.pdf.LoadParams import android.graphics.pdf.PdfRenderer import android.os.Build import android.os.ParcelFileDescriptor +import android.os.ext.SdkExtensions import android.util.Log import android.util.SparseArray import android.view.Surface @@ -45,7 +47,7 @@ class Messages(private val binding : FlutterPlugin.FlutterPluginBinding, ) { val resultResponse = Pigeon.OpenReply() try { - val documentRenderer = openDataDocument(message.data!!) + val documentRenderer = openDataDocument(message.data!!, message.password) val document = documents.register(documentRenderer) resultResponse.id = document.id resultResponse.pagesCount = document.pagesCount.toLong() @@ -66,7 +68,7 @@ class Messages(private val binding : FlutterPlugin.FlutterPluginBinding, val resultResponse = Pigeon.OpenReply() try { val path = message.path - val documentRenderer = openFileDocument(File(path!!)) + val documentRenderer = openFileDocument(File(path!!), message.password) val document = documents.register(documentRenderer) resultResponse.id = document.id resultResponse.pagesCount = document.pagesCount.toLong() @@ -91,7 +93,7 @@ class Messages(private val binding : FlutterPlugin.FlutterPluginBinding, val resultResponse = Pigeon.OpenReply() try { val path = message.path - val documentRenderer = openAssetDocument(path!!) + val documentRenderer = openAssetDocument(path!!, message.password) val document = documents.register(documentRenderer) resultResponse.id = document.id resultResponse.pagesCount = document.pagesCount.toLong() @@ -356,16 +358,16 @@ class Messages(private val binding : FlutterPlugin.FlutterPluginBinding, surfaceProducers.remove(id) } - private fun openDataDocument(data: ByteArray): Pair { + private fun openDataDocument(data: ByteArray, password: String?): Pair { val tempDataFile = File(binding.applicationContext.cacheDir, "$randomFilename.pdf") if (!tempDataFile.exists()) { tempDataFile.writeBytes(data) } Log.d("pdf_renderer", "OpenDataDocument. Created file: " + tempDataFile.path) - return openFileDocument(tempDataFile) + return openFileDocument(tempDataFile, password) } - private fun openAssetDocument(assetPath: String): Pair { + private fun openAssetDocument(assetPath: String, password: String?): Pair { val fullAssetPath = binding.flutterAssets.getAssetFilePathByName(assetPath) val tempAssetFile = File(binding.applicationContext.cacheDir, "$randomFilename.pdf") if (!tempAssetFile.exists()) { @@ -374,14 +376,22 @@ class Messages(private val binding : FlutterPlugin.FlutterPluginBinding, inputStream.close() } Log.d("pdf_renderer", "OpenAssetDocument. Created file: " + tempAssetFile.path) - return openFileDocument(tempAssetFile) + return openFileDocument(tempAssetFile, password) } - private fun openFileDocument(file: File): Pair { + private fun openFileDocument(file: File, password: String?): Pair { Log.d("pdf_renderer", "OpenFileDocument. File: " + file.path) val fileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY) return if (fileDescriptor != null) { - val pdfRenderer = PdfRenderer(fileDescriptor) + val pdfRenderer = if (password != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 13) { + PdfRenderer(fileDescriptor, LoadParams.Builder().setPassword(password).build()) + } else { + throw CreateRendererException() + } + } else { + PdfRenderer(fileDescriptor) + } Pair(fileDescriptor, pdfRenderer) } else throw CreateRendererException() } diff --git a/packages/pdfx/ios/Classes/SwiftPdfxPlugin.swift b/packages/pdfx/ios/Classes/SwiftPdfxPlugin.swift index d33be748..fc4f250b 100644 --- a/packages/pdfx/ios/Classes/SwiftPdfxPlugin.swift +++ b/packages/pdfx/ios/Classes/SwiftPdfxPlugin.swift @@ -36,7 +36,7 @@ public class SwiftPdfxPlugin: NSObject, FlutterPlugin, PdfxApi { message: "Arguments not sended", details: nil)) } - guard let renderer = openDataDocument(data: data.data) else { + guard let renderer = openDataDocument(data: data.data, password: message.password) else { return completion(nil, FlutterError(code: "RENDER_ERROR", message: "Invalid PDF format", details: nil)) @@ -56,7 +56,7 @@ public class SwiftPdfxPlugin: NSObject, FlutterPlugin, PdfxApi { message: "Arguments not sended", details: nil)) } - guard let renderer = openFileDocument(pdfFilePath: pdfFilePath) else { + guard let renderer = openFileDocument(pdfFilePath: pdfFilePath, password: message.password) else { return completion(nil, FlutterError(code: "RENDER_ERROR", message: "Invalid PDF format", details: nil)) @@ -76,7 +76,7 @@ public class SwiftPdfxPlugin: NSObject, FlutterPlugin, PdfxApi { message: "Arguments not sended", details: nil)) } - guard let renderer = openAssetDocument(name: name) else { + guard let renderer = openAssetDocument(name: name, password: message.password) else { return completion(nil, FlutterError(code: "RENDER_ERROR", message: "Invalid PDF format", details: nil)) @@ -259,24 +259,38 @@ public class SwiftPdfxPlugin: NSObject, FlutterPlugin, PdfxApi { } - func openDataDocument(data: Data) -> CGPDFDocument? { + func openDataDocument(data: Data, password: String?) -> CGPDFDocument? { guard let datProv = CGDataProvider(data: data as CFData) else { return nil } - let docment = CGPDFDocument(datProv) - if docment?.isUnlocked == false { - return nil + guard let document = CGPDFDocument(datProv) else { return nil } + + if document.isEncrypted { + if let password = password { + document.unlockWithPassword(password) + } + if !document.isUnlocked { + return nil + } } - return docment + + return document } - func openFileDocument(pdfFilePath: String) -> CGPDFDocument? { - let docment = CGPDFDocument(URL(fileURLWithPath: pdfFilePath) as CFURL) - if docment?.isEncrypted == true { - return nil + func openFileDocument(pdfFilePath: String, password: String?) -> CGPDFDocument? { + guard let document = CGPDFDocument(URL(fileURLWithPath: pdfFilePath) as CFURL) else { return nil } + + if document.isEncrypted { + if let password = password { + document.unlockWithPassword(password) + } + if !document.isUnlocked { + return nil + } } - return docment + + return document } - func openAssetDocument(name: String) -> CGPDFDocument? { + func openAssetDocument(name: String, password: String?) -> CGPDFDocument? { #if os(iOS) guard let path = Bundle.main.path(forResource: "Frameworks/App.framework/flutter_assets/" + name, ofType: "") else { return nil @@ -285,7 +299,7 @@ public class SwiftPdfxPlugin: NSObject, FlutterPlugin, PdfxApi { let path = Bundle.main.bundlePath + "/Contents/Frameworks/App.framework/Resources/flutter_assets/" + name; #endif - return openFileDocument(pdfFilePath: path) + return openFileDocument(pdfFilePath: path, password: password) } } diff --git a/packages/pdfx/lib/src/renderer/io/platform_method_channel.dart b/packages/pdfx/lib/src/renderer/io/platform_method_channel.dart index 9ab5bffb..81eaa1bd 100644 --- a/packages/pdfx/lib/src/renderer/io/platform_method_channel.dart +++ b/packages/pdfx/lib/src/renderer/io/platform_method_channel.dart @@ -34,7 +34,10 @@ class PdfxPlatformMethodChannel extends PdfxPlatform { return _open( (await _channel.invokeMethod>( 'open.document.file', - filePath, + { + 'path': filePath, + 'password': password, + }, ))!, 'file:$filePath', ); @@ -45,7 +48,10 @@ class PdfxPlatformMethodChannel extends PdfxPlatform { Future openAsset(String name, {String? password}) async => _open( (await _channel.invokeMethod>( 'open.document.asset', - name, + { + 'name': name, + 'password': password, + }, ))!, 'asset:$name', ); @@ -57,7 +63,10 @@ class PdfxPlatformMethodChannel extends PdfxPlatform { _open( (await _channel.invokeMethod>( 'open.document.data', - await data, + { + 'data': await data, + 'password': password, + }, ))!, 'memory:binary', ); diff --git a/packages/pdfx/windows/pdfx.cpp b/packages/pdfx/windows/pdfx.cpp index e6c933b9..bad61d71 100644 --- a/packages/pdfx/windows/pdfx.cpp +++ b/packages/pdfx/windows/pdfx.cpp @@ -69,7 +69,7 @@ std::unordered_map> document_repository; std::unordered_map> page_repository; int lastId = 0; -std::shared_ptr openDocument(std::vector data) { +std::shared_ptr openDocument(std::vector data, const char* password) { if (document_repository.size() == 0) { FPDF_LIBRARY_CONFIG config; config.version = 2; @@ -82,13 +82,13 @@ std::shared_ptr openDocument(std::vector data) { lastId++; std::string strId = std::to_string(lastId); - std::shared_ptr doc = std::make_shared(data, strId); + std::shared_ptr doc = std::make_shared(data, strId, password); document_repository[strId] = doc; return doc; } -std::shared_ptr openDocument(std::string name) { +std::shared_ptr openDocument(std::string name, const char* password) { if (document_repository.size() == 0) { FPDF_LIBRARY_CONFIG config; config.version = 2; @@ -101,7 +101,7 @@ std::shared_ptr openDocument(std::string name) { lastId++; std::string strId = std::to_string(lastId); - std::shared_ptr doc = std::make_shared(name, strId); + std::shared_ptr doc = std::make_shared(name, strId, password); document_repository[strId] = doc; return doc; @@ -151,17 +151,17 @@ PageRender renderPage(std::string id, int width, int height, ImageFormat format, // -Document::Document(std::vector dataRef, std::string id) : id{id} { +Document::Document(std::vector dataRef, std::string id, const char* password) : id{id} { // Copy data into object to keep it in memory data.swap(dataRef); - document = FPDF_LoadMemDocument64(data.data(), data.size(), nullptr); + document = FPDF_LoadMemDocument64(data.data(), data.size(), password); if (!document) { throw std::invalid_argument("Document failed to open"); } } -Document::Document(std::string file, std::string id) : id{id} { +Document::Document(std::string file, std::string id, const char* password) : id{id} { HANDLE hFile; // If is root path, add \\?\ to support long file names @@ -204,7 +204,7 @@ Document::Document(std::string file, std::string id) : id{id} { CloseHandle(hFile); // Load PDF - document = FPDF_LoadMemDocument64(data.data(), bytesRead, nullptr); + document = FPDF_LoadMemDocument64(data.data(), bytesRead, password); if (!document) { throw std::invalid_argument("Document failed to open"); } diff --git a/packages/pdfx/windows/pdfx.h b/packages/pdfx/windows/pdfx.h index 805c29f9..c88bd70d 100644 --- a/packages/pdfx/windows/pdfx.h +++ b/packages/pdfx/windows/pdfx.h @@ -41,8 +41,8 @@ class Document { std::vector data; public: - Document(std::vector data, std::string id); - Document(std::string file, std::string id); + Document(std::vector data, std::string id, const char* password = nullptr); + Document(std::string file, std::string id, const char* password = nullptr); ~Document(); @@ -67,8 +67,8 @@ class Page { unsigned long background, CropDetails* crop); }; -std::shared_ptr openDocument(std::vector data); -std::shared_ptr openDocument(std::string name); +std::shared_ptr openDocument(std::vector data, const char* password = nullptr); +std::shared_ptr openDocument(std::string name, const char* password = nullptr); void closeDocument(std::string id); std::shared_ptr openPage(std::string docId, int index); diff --git a/packages/pdfx/windows/pdfx_plugin.cpp b/packages/pdfx/windows/pdfx_plugin.cpp index 0e2fbda7..f369c684 100644 --- a/packages/pdfx/windows/pdfx_plugin.cpp +++ b/packages/pdfx/windows/pdfx_plugin.cpp @@ -89,7 +89,21 @@ void PdfxPlugin::HandleMethodCall( /// Open document from flutter asset /// if (method_call.method_name().compare(kOpenDocumentAssetMethod) == 0) { - auto name = std::get(*method_call.arguments()); + const auto* arguments = + std::get_if(method_call.arguments()); + + auto vName = arguments->find(flutter::EncodableValue("name")); + if (vName == arguments->end()) { + result->Error("pdfx_exception", "name is required"); + return; + } + auto name = std::get(vName->second); + + const char* password = nullptr; + auto vPassword = arguments->find(flutter::EncodableValue("password")); + if (vPassword != arguments->end() && !vPassword->second.IsNull()) { + password = std::get(vPassword->second).c_str(); + } // Get .exe base path WCHAR basePath[MAX_PATH]; @@ -105,7 +119,7 @@ void PdfxPlugin::HandleMethodCall( utf8_encode(basePath) + "\\data\\flutter_assets\\" + name; try { - std::shared_ptr doc = openDocument(path); + std::shared_ptr doc = openDocument(path, password); auto mp = flutter::EncodableMap{}; mp[flutter::EncodableValue("id")] = flutter::EncodableValue(doc->id); @@ -121,10 +135,24 @@ void PdfxPlugin::HandleMethodCall( /// Open document from file /// else if (method_call.method_name().compare(kOpenDocumentFileMethod) == 0) { - auto name = std::get(*method_call.arguments()); + const auto* arguments = + std::get_if(method_call.arguments()); + + auto vPath = arguments->find(flutter::EncodableValue("path")); + if (vPath == arguments->end()) { + result->Error("pdfx_exception", "path is required"); + return; + } + auto name = std::get(vPath->second); + + const char* password = nullptr; + auto vPassword = arguments->find(flutter::EncodableValue("password")); + if (vPassword != arguments->end() && !vPassword->second.IsNull()) { + password = std::get(vPassword->second).c_str(); + } try { - std::shared_ptr doc = openDocument(name); + std::shared_ptr doc = openDocument(name, password); auto mp = flutter::EncodableMap{}; mp[flutter::EncodableValue("id")] = flutter::EncodableValue(doc->id); @@ -140,10 +168,24 @@ void PdfxPlugin::HandleMethodCall( /// Open document from data stream /// else if (method_call.method_name().compare(kOpenDocumentDataMethod) == 0) { - auto data = std::get>(*method_call.arguments()); + const auto* arguments = + std::get_if(method_call.arguments()); + + auto vData = arguments->find(flutter::EncodableValue("data")); + if (vData == arguments->end()) { + result->Error("pdfx_exception", "data is required"); + return; + } + auto data = std::get>(vData->second); + + const char* password = nullptr; + auto vPassword = arguments->find(flutter::EncodableValue("password")); + if (vPassword != arguments->end() && !vPassword->second.IsNull()) { + password = std::get(vPassword->second).c_str(); + } try { - std::shared_ptr doc = openDocument(data); + std::shared_ptr doc = openDocument(data, password); auto mp = flutter::EncodableMap{}; mp[flutter::EncodableValue("id")] = flutter::EncodableValue(doc->id); From 1a6fbba9f7a66b3ef7c8b0b0fb758e31e939ab62 Mon Sep 17 00:00:00 2001 From: Thomas Pucci Date: Tue, 18 Nov 2025 12:20:13 +0100 Subject: [PATCH 3/4] Refactor PinchPage to update document loading and title management with a popup menu for asset selection --- packages/pdfx/example/lib/pinch.dart | 68 ++++++++++++++++------------ 1 file changed, 40 insertions(+), 28 deletions(-) diff --git a/packages/pdfx/example/lib/pinch.dart b/packages/pdfx/example/lib/pinch.dart index e8f1f93d..82cc157d 100644 --- a/packages/pdfx/example/lib/pinch.dart +++ b/packages/pdfx/example/lib/pinch.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:internet_file/internet_file.dart'; import 'package:pdfx/pdfx.dart'; -import 'package:universal_platform/universal_platform.dart'; class PinchPage extends StatefulWidget { const PinchPage({Key? key}) : super(key: key); @@ -10,11 +9,9 @@ class PinchPage extends StatefulWidget { State createState() => _PinchPageState(); } -enum DocShown { sample, tutorial, hello, password } - class _PinchPageState extends State { static const int _initialPage = 1; - DocShown _showing = DocShown.sample; + String _title = 'Sample PDF'; late PdfControllerPinch _pdfControllerPinch; @override @@ -42,7 +39,7 @@ class _PinchPageState extends State { return Scaffold( backgroundColor: Colors.grey, appBar: AppBar( - title: const Text('Pdfx example'), + title: Text(_title), actions: [ IconButton( icon: const Icon(Icons.navigate_before), @@ -72,34 +69,49 @@ class _PinchPageState extends State { ); }, ), - IconButton( - icon: const Icon(Icons.refresh), - onPressed: () { - switch (_showing) { - case DocShown.sample: - case DocShown.tutorial: - _pdfControllerPinch.loadDocument( - PdfDocument.openAsset('assets/flutter_tutorial.pdf')); - _showing = DocShown.hello; + PopupMenuButton( + icon: const Icon(Icons.file_open), + onSelected: (String asset) { + String title; + switch (asset) { + case 'assets/hello.pdf': + title = 'Hello PDF'; break; - case DocShown.hello: - _pdfControllerPinch - .loadDocument(PdfDocument.openAsset('assets/hello.pdf')); - _showing = UniversalPlatform.isWeb - ? DocShown.password - : DocShown.tutorial; + case 'assets/flutter_tutorial.pdf': + title = 'Flutter Tutorial'; break; - - case DocShown.password: - _pdfControllerPinch.loadDocument(PdfDocument.openAsset( - 'assets/password.pdf', - password: 'MyPassword', - )); - _showing = DocShown.tutorial; + case 'assets/password.pdf': + title = 'Password Protected PDF'; break; + default: + title = 'Pdfx example'; + } + setState(() { + _title = title; + }); + String? password; + if (asset == 'assets/password.pdf') { + password = 'MyPassword'; } + _pdfControllerPinch.loadDocument( + PdfDocument.openAsset(asset, password: password), + ); }, - ) + itemBuilder: (BuildContext context) => >[ + const PopupMenuItem( + value: 'assets/hello.pdf', + child: Text('Hello PDF'), + ), + const PopupMenuItem( + value: 'assets/flutter_tutorial.pdf', + child: Text('Flutter Tutorial'), + ), + const PopupMenuItem( + value: 'assets/password.pdf', + child: Text('Password Protected'), + ), + ], + ), ], ), body: PdfViewPinch( From 5437732c497ebf55962579543b50aeb6a86c9651 Mon Sep 17 00:00:00 2001 From: Thomas Pucci Date: Tue, 18 Nov 2025 14:24:10 +0100 Subject: [PATCH 4/4] Remove outdated password support notes for file, asset, and data opening methods in PdfDocument --- packages/pdfx/lib/src/renderer/interfaces/document.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/pdfx/lib/src/renderer/interfaces/document.dart b/packages/pdfx/lib/src/renderer/interfaces/document.dart index c8ea66f0..86ac2236 100644 --- a/packages/pdfx/lib/src/renderer/interfaces/document.dart +++ b/packages/pdfx/lib/src/renderer/interfaces/document.dart @@ -34,21 +34,18 @@ abstract class PdfDocument { /// Opening the specified file. /// For Web, [filePath] can be relative path from `index.html` or any /// arbitrary URL but it may be restricted by CORS. - /// `password supported only for web!` static Future openFile(String filePath, {String? password}) { assertHasPdfSupport(); return PdfxPlatform.instance.openFile(filePath, password: password); } /// Opening the specified asset. - /// `password supported only for web!` static Future openAsset(String name, {String? password}) { assertHasPdfSupport(); return PdfxPlatform.instance.openAsset(name, password: password); } /// Opening the PDF on memory. - /// `password supported only for web!` static Future openData(FutureOr data, {String? password}) { assertHasPdfSupport();