diff --git a/Package.swift b/Package.swift
index 8c003da..66aed80 100644
--- a/Package.swift
+++ b/Package.swift
@@ -25,4 +25,4 @@ let package = Package(
dependencies: ["PrinterPlugin"],
path: "ios/Tests/PrinterPluginTests")
]
-)
\ No newline at end of file
+)
diff --git a/README.md b/README.md
index 4bcddc4..a3d522f 100644
--- a/README.md
+++ b/README.md
@@ -13,19 +13,74 @@ npx cap sync
-* [`printWebView()`](#printwebview)
+* [`printFile(...)`](#printfile)
+* [`printWebView(...)`](#printwebview)
+* [Interfaces](#interfaces)
+* [Type Aliases](#type-aliases)
-### printWebView()
+### printFile(...)
```typescript
-printWebView() => Promise
+printFile(options: PrintFileOptions) => Promise
```
+Present the printing user interface to print a file.
+
+The promise settles after the operating system no longer needs the source
+file, so the file can be safely deleted in a `finally` block.
+
+Only available on Android and iOS.
+
+| Param | Type |
+| ------------- | ------------------------------------------------------------- |
+| **`options`** | PrintFileOptions |
+
+--------------------
+
+
+### printWebView(...)
+
+```typescript
+printWebView(options?: PrintOptions | undefined) => Promise
+```
+
+Present the printing user interface to print the web view content.
+
+| Param | Type |
+| ------------- | ----------------------------------------------------- |
+| **`options`** | PrintOptions |
+
--------------------
+
+### Interfaces
+
+
+#### PrintFileOptions
+
+| Prop | Type | Description |
+| -------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
+| **`path`** | string | The path to the file. Android supports file paths, `file://` URLs, and `content://` URLs. iOS supports file paths and local `file://` URLs. |
+| **`mimeType`** | string | The MIME type of the file. Only used on Android. |
+
+
+#### PrintOptions
+
+| Prop | Type | Description | Default |
+| ---------- | ------------------- | -------------------------- | ----------------------- |
+| **`name`** | string | The name of the print job. | 'Document' |
+
+
+### Type Aliases
+
+
+#### PrintWebViewOptions
+
+PrintOptions
+
diff --git a/android/build.gradle b/android/build.gradle
index 69f29b8..6ce7d29 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -3,6 +3,7 @@ ext {
androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.1'
androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.3.0'
androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.7.0'
+ androidxPrintVersion = project.hasProperty('androidxPrintVersion') ? rootProject.ext.androidxPrintVersion : '1.1.0'
}
buildscript {
@@ -52,6 +53,7 @@ dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':capacitor-android')
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
+ implementation "androidx.print:print:$androidxPrintVersion"
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
diff --git a/android/src/main/java/jp/rdlabo/capacitor/plugin/printer/PrinterPlugin.java b/android/src/main/java/jp/rdlabo/capacitor/plugin/printer/PrinterPlugin.java
index 5af31a2..08a50c4 100644
--- a/android/src/main/java/jp/rdlabo/capacitor/plugin/printer/PrinterPlugin.java
+++ b/android/src/main/java/jp/rdlabo/capacitor/plugin/printer/PrinterPlugin.java
@@ -1,18 +1,60 @@
package jp.rdlabo.capacitor.plugin.printer;
import android.content.Context;
+import android.net.Uri;
+import android.os.CancellationSignal;
+import android.os.ParcelFileDescriptor;
import android.print.PrintAttributes;
import android.print.PrintDocumentAdapter;
+import android.print.PrintDocumentInfo;
import android.print.PrintManager;
import android.webkit.WebView;
+import androidx.print.PrintHelper;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.CapacitorPlugin;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Locale;
@CapacitorPlugin(name = "Printer")
public class PrinterPlugin extends Plugin {
+ private static final String DEFAULT_JOB_NAME = "Document";
+
+ @PluginMethod
+ public void printFile(PluginCall call) {
+ String path = call.getString("path");
+ String mimeType = call.getString("mimeType");
+ if (path == null || path.trim().isEmpty()) {
+ call.reject("path must be provided");
+ return;
+ }
+ if (mimeType == null || mimeType.trim().isEmpty()) {
+ call.reject("mimeType must be provided");
+ return;
+ }
+
+ Uri uri = toUri(path);
+ String jobName = getFileName(uri);
+ String normalizedMimeType = mimeType.split(";", 2)[0].trim().toLowerCase(Locale.ROOT);
+ getActivity().runOnUiThread(() -> {
+ if (isSupportedImageMimeType(normalizedMimeType)) {
+ printImage(call, uri, jobName);
+ } else if ("application/pdf".equals(normalizedMimeType)) {
+ printPdf(call, uri, jobName);
+ } else {
+ call.reject("Unsupported MIME type: " + mimeType);
+ }
+ });
+ }
+
@PluginMethod
public void printWebView(PluginCall call) {
getActivity().runOnUiThread(() -> {
@@ -28,11 +70,146 @@ public void printWebView(PluginCall call) {
return;
}
- String jobName = "WebView Print";
+ String jobName = normalizeJobName(call.getString("name"));
PrintDocumentAdapter printAdapter = webView.createPrintDocumentAdapter(jobName);
printManager.print(jobName, printAdapter, new PrintAttributes.Builder().build());
call.resolve();
});
}
+
+ private void printImage(PluginCall call, Uri uri, String jobName) {
+ try {
+ PrintHelper printHelper = new PrintHelper(getContext());
+ printHelper.setScaleMode(PrintHelper.SCALE_MODE_FIT);
+ printHelper.printBitmap(jobName, uri, () -> call.resolve());
+ } catch (FileNotFoundException | SecurityException exception) {
+ call.reject("Unable to read file: " + exception.getLocalizedMessage(), exception);
+ }
+ }
+
+ private void printPdf(PluginCall call, Uri uri, String jobName) {
+ PrintManager printManager = (PrintManager) getContext().getSystemService(Context.PRINT_SERVICE);
+ if (printManager == null) {
+ call.reject("Print service not available");
+ return;
+ }
+
+ try (InputStream ignored = openInputStream(uri)) {
+ // Validate access before opening the asynchronous print job.
+ } catch (IOException | SecurityException exception) {
+ call.reject("Unable to read file: " + exception.getLocalizedMessage(), exception);
+ return;
+ }
+
+ printManager.print(jobName, new PdfPrintDocumentAdapter(uri, jobName, call), new PrintAttributes.Builder().build());
+ }
+
+ private InputStream openInputStream(Uri uri) throws FileNotFoundException {
+ if ("file".equalsIgnoreCase(uri.getScheme())) {
+ return new FileInputStream(new File(uri.getPath()));
+ }
+ InputStream input = getContext().getContentResolver().openInputStream(uri);
+ if (input == null) {
+ throw new FileNotFoundException("Unable to open " + uri);
+ }
+ return input;
+ }
+
+ private static Uri toUri(String path) {
+ Uri uri = Uri.parse(path);
+ return uri.getScheme() == null ? Uri.fromFile(new File(path)) : uri;
+ }
+
+ private static String getFileName(Uri uri) {
+ String fileName = uri.getLastPathSegment();
+ return fileName == null || fileName.trim().isEmpty() ? DEFAULT_JOB_NAME : fileName;
+ }
+
+ private static String normalizeJobName(String name) {
+ return name == null || name.trim().isEmpty() ? DEFAULT_JOB_NAME : name.trim();
+ }
+
+ private static boolean isSupportedImageMimeType(String mimeType) {
+ return switch (mimeType) {
+ case "image/gif", "image/heic", "image/heif", "image/jpeg", "image/png" -> true;
+ default -> false;
+ };
+ }
+
+ private final class PdfPrintDocumentAdapter extends PrintDocumentAdapter {
+
+ private final Uri uri;
+ private final String name;
+ private final PluginCall call;
+ private volatile String failureMessage;
+
+ private PdfPrintDocumentAdapter(Uri uri, String name, PluginCall call) {
+ this.uri = uri;
+ this.name = name;
+ this.call = call;
+ }
+
+ @Override
+ public void onLayout(
+ PrintAttributes oldAttributes,
+ PrintAttributes newAttributes,
+ CancellationSignal cancellationSignal,
+ LayoutResultCallback callback,
+ android.os.Bundle extras
+ ) {
+ if (cancellationSignal.isCanceled()) {
+ callback.onLayoutCancelled();
+ return;
+ }
+ PrintDocumentInfo info = new PrintDocumentInfo.Builder(name).setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT).build();
+ callback.onLayoutFinished(info, !newAttributes.equals(oldAttributes));
+ }
+
+ @Override
+ public void onWrite(
+ android.print.PageRange[] pages,
+ ParcelFileDescriptor destination,
+ CancellationSignal cancellationSignal,
+ WriteResultCallback callback
+ ) {
+ failureMessage = null;
+ new Thread(() -> {
+ boolean cancelled = false;
+ try (
+ InputStream input = openInputStream(uri);
+ OutputStream output = new FileOutputStream(destination.getFileDescriptor())
+ ) {
+ byte[] buffer = new byte[8192];
+ int length;
+ while ((length = input.read(buffer)) != -1) {
+ if (cancellationSignal.isCanceled()) {
+ cancelled = true;
+ break;
+ }
+ output.write(buffer, 0, length);
+ }
+ } catch (IOException | SecurityException exception) {
+ failureMessage = exception.getLocalizedMessage();
+ callback.onWriteFailed(exception.getLocalizedMessage());
+ return;
+ }
+ if (cancelled) {
+ callback.onWriteCancelled();
+ } else {
+ callback.onWriteFinished(new android.print.PageRange[] { android.print.PageRange.ALL_PAGES });
+ }
+ })
+ .start();
+ }
+
+ @Override
+ public void onFinish() {
+ if (failureMessage == null) {
+ call.resolve();
+ } else {
+ call.reject("Unable to print file: " + failureMessage);
+ }
+ }
+ }
}
diff --git a/ios/Sources/PrinterPlugin/PrinterPlugin.swift b/ios/Sources/PrinterPlugin/PrinterPlugin.swift
index cda1aef..9c82805 100644
--- a/ios/Sources/PrinterPlugin/PrinterPlugin.swift
+++ b/ios/Sources/PrinterPlugin/PrinterPlugin.swift
@@ -11,30 +11,73 @@ public class PrinterPlugin: CAPPlugin, CAPBridgedPlugin {
public let identifier = "PrinterPlugin"
public let jsName = "Printer"
public let pluginMethods: [CAPPluginMethod] = [
+ CAPPluginMethod(name: "printFile", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "printWebView", returnType: CAPPluginReturnPromise)
]
+ @objc func printFile(_ call: CAPPluginCall) {
+ guard let path = call.getString("path"), !path.isEmpty else {
+ call.reject("path must be provided")
+ return
+ }
+
+ let fileURL: URL
+ if let url = URL(string: path), url.isFileURL {
+ fileURL = url
+ } else {
+ fileURL = URL(fileURLWithPath: path)
+ }
+
+ guard FileManager.default.fileExists(atPath: fileURL.path) else {
+ call.reject("File not found")
+ return
+ }
+ guard UIPrintInteractionController.canPrint(fileURL) else {
+ call.reject("File type is not printable")
+ return
+ }
+
+ DispatchQueue.main.async {
+ let printController = UIPrintInteractionController.shared
+ let printInfo = UIPrintInfo(dictionary: nil)
+ printInfo.outputType = .general
+ printInfo.jobName = fileURL.lastPathComponent.isEmpty ? "Document" : fileURL.lastPathComponent
+ printController.printInfo = printInfo
+ printController.printingItem = fileURL
+ self.present(printController, call: call)
+ }
+ }
+
@objc func printWebView(_ call: CAPPluginCall) {
DispatchQueue.main.async {
guard let webView = self.webView else {
call.reject("WebView not available")
return
}
-
+
let printController = UIPrintInteractionController.shared
let printInfo = UIPrintInfo(dictionary: nil)
printInfo.outputType = .general
+ let requestedName = call.getString("name")?.trimmingCharacters(in: .whitespacesAndNewlines)
+ if let requestedName, !requestedName.isEmpty {
+ printInfo.jobName = requestedName
+ } else {
+ printInfo.jobName = "Document"
+ }
printController.printInfo = printInfo
printController.printFormatter = webView.viewPrintFormatter()
-
- printController.present(animated: true) { _, completed, error in
- if let error = error {
- call.reject("Print failed: \(error.localizedDescription)")
- } else if completed {
- call.resolve()
- } else {
- call.reject("Print cancelled")
- }
+ self.present(printController, call: call)
+ }
+ }
+
+ private func present(_ printController: UIPrintInteractionController, call: CAPPluginCall) {
+ printController.present(animated: true) { _, completed, error in
+ if let error = error {
+ call.reject("Print failed: \(error.localizedDescription)")
+ } else if completed {
+ call.resolve()
+ } else {
+ call.reject("Print cancelled")
}
}
}
diff --git a/src/definitions.ts b/src/definitions.ts
index 2d054f3..9f5ef4e 100644
--- a/src/definitions.ts
+++ b/src/definitions.ts
@@ -1,3 +1,40 @@
export interface PrinterPlugin {
- printWebView(): Promise;
+ /**
+ * Present the printing user interface to print a file.
+ *
+ * The promise settles after the operating system no longer needs the source
+ * file, so the file can be safely deleted in a `finally` block.
+ *
+ * Only available on Android and iOS.
+ */
+ printFile(options: PrintFileOptions): Promise;
+
+ /**
+ * Present the printing user interface to print the web view content.
+ */
+ printWebView(options?: PrintWebViewOptions): Promise;
}
+
+export interface PrintFileOptions {
+ /**
+ * The path to the file. Android supports file paths, `file://` URLs, and
+ * `content://` URLs. iOS supports file paths and local `file://` URLs.
+ */
+ path: string;
+
+ /**
+ * The MIME type of the file. Only used on Android.
+ */
+ mimeType: string;
+}
+
+export interface PrintOptions {
+ /**
+ * The name of the print job.
+ *
+ * @default 'Document'
+ */
+ name?: string;
+}
+
+export type PrintWebViewOptions = PrintOptions;
diff --git a/src/web.ts b/src/web.ts
index e80ec3a..4d03e59 100644
--- a/src/web.ts
+++ b/src/web.ts
@@ -1,9 +1,15 @@
import { WebPlugin } from '@capacitor/core';
-import type { PrinterPlugin } from './definitions';
+import type { PrinterPlugin, PrintFileOptions, PrintWebViewOptions } from './definitions';
export class PrinterWeb extends WebPlugin implements PrinterPlugin {
- async printWebView(): Promise {
- console.log('Printing web view...');
+ async printFile(options: PrintFileOptions): Promise {
+ void options;
+ throw this.unavailable('printFile is not available on the web.');
+ }
+
+ async printWebView(options?: PrintWebViewOptions): Promise {
+ void options;
+ window.print();
}
}