Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions PRINTING_FEATURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# AirPrint/IPP Everywhere Printing Feature Implementation

## Overview
This implementation adds printing functionality to ScanBridge using Android's Print Framework, which automatically supports AirPrint and IPP Everywhere printers through the system's print services.

## Changes Made

### 1. UI Changes
- **ExportSettingsPopup.kt**: Added a third print button (🖨️) alongside existing PDF and Archive export buttons
- **Strings.xml**: Added "Print" text in English and German translations

### 2. Core Functionality
- **ScanningScreen.kt**:
- Added `doPrint()` function that uses Android PrintManager
- Created `ScanDocumentPrintAdapter` class that converts scanned images to PDF for printing
- Integrated print callback into export options popup

### 3. Key Features
- **AirPrint/IPP Everywhere Support**: Uses Android Print Framework which automatically discovers and supports AirPrint and IPP Everywhere printers
- **Multi-page Printing**: Supports printing multiple scanned pages as a single print job
- **Page Range Selection**: Users can select which pages to print through the system print dialog
- **Error Handling**: Same validation as other export functions (no scans, job running, etc.)
- **PDF Generation**: Converts scanned images to PDF format for printing using existing iText library

### 4. User Experience
When users tap the share/export button in the scanning screen, they now see three options:
1. 📄 PDF Export
2. 🖼️ Archive Export
3. 🖨️ Print (NEW)

Selecting print will:
1. Open the Android system print dialog
2. Show available printers (including AirPrint/IPP printers)
3. Allow users to select printer settings, page ranges, etc.
4. Send the scanned documents to the selected printer

## Technical Implementation
- Uses `PrintManager.print()` with a custom `PrintDocumentAdapter`
- Converts scanned images to PDF format using iText library
- Handles cancellation and error scenarios
- Supports page range printing
- Automatic scaling to fit page size (A4 with 1-inch margins)

## Compatibility
- **Minimum Android Version**: Works with existing app requirements (Android 28+)
- **Printer Support**: Any printer supported by Android Print Framework, including:
- AirPrint printers (Apple ecosystem)
- IPP Everywhere printers
- Manufacturer-specific print services
- Cloud printing services

## Testing
- Code compiles successfully
- Maintains compatibility with existing export functionality
- Ready for manual testing with actual printers

The implementation follows the existing app patterns and provides a seamless user experience for printing scanned documents.
144 changes: 144 additions & 0 deletions app/src/main/java/io/github/chrisimx/scanbridge/ScanningScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ import android.content.Context.MODE_PRIVATE
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Bundle
import android.os.CancellationSignal
import android.os.ParcelFileDescriptor
import android.print.PageRange
import android.print.PrintAttributes
import android.print.PrintDocumentAdapter
import android.print.PrintDocumentInfo
import android.print.PrintManager
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
Expand Down Expand Up @@ -352,6 +360,126 @@ fun doPdfExport(scanningViewModel: ScanningScreenViewModel, context: Context, on
context.startActivity(share)
}

/**
* Print scanned documents using Android Print Framework (supports AirPrint/IPP Everywhere)
*/
fun doPrint(scanningViewModel: ScanningScreenViewModel, context: Context, onError: (String) -> Unit) {
if (scanningViewModel.scanningScreenData.currentScansState.isEmpty()) {
onError(context.getString(R.string.no_scans_yet))
return
}
if (scanningViewModel.scanningScreenData.scanJobRunning) {
onError(context.getString(R.string.job_still_running))
return
}

val printManager = context.getSystemService(Context.PRINT_SERVICE) as PrintManager
val printAdapter = ScanDocumentPrintAdapter(
context,
scanningViewModel.scanningScreenData.currentScansState.map { File(it.first) }
)

val jobName = "ScanBridge - ${
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
}"

printManager.print(jobName, printAdapter, null)
}

/**
* PrintDocumentAdapter for printing scanned documents
*/
class ScanDocumentPrintAdapter(
private val context: Context,
private val imageFiles: List<File>
) : PrintDocumentAdapter() {

override fun onLayout(
oldAttributes: PrintAttributes?,
newAttributes: PrintAttributes,
cancellationSignal: CancellationSignal?,
callback: LayoutResultCallback,
extras: Bundle?
) {
if (cancellationSignal?.isCanceled == true) {
callback.onLayoutCancelled()
return
}

val info = PrintDocumentInfo.Builder("ScanBridge_Print")
.setContentType(PrintDocumentInfo.CONTENT_TYPE_PHOTO)
.setPageCount(imageFiles.size)
.build()

callback.onLayoutFinished(info, true)
}

override fun onWrite(
pages: Array<out PageRange>?,
destination: ParcelFileDescriptor,
cancellationSignal: CancellationSignal?,
callback: WriteResultCallback
) {
try {
if (cancellationSignal?.isCanceled == true) {
callback.onWriteCancelled()
return
}

// Create a temporary file for the PDF
val tempFile = File.createTempFile("print_document", ".pdf")
val pdfWriter = PdfWriter(tempFile)
val pdfDocument = PdfDocument(pdfWriter)
val document = Document(pdfDocument)

imageFiles.forEachIndexed { index, imageFile ->
if (cancellationSignal?.isCanceled == true) {
document.close()
callback.onWriteCancelled()
return
}

// Check if this page should be printed based on the page ranges
val shouldPrintPage = pages?.any { range ->
index >= range.start && index <= range.end
} ?: true

if (shouldPrintPage) {
val imageData = ImageDataFactory.create(imageFile.absolutePath)
val image = Image(imageData)

// Scale image to fit page
val pageSize = PageSize.A4
image.scaleToFit(pageSize.width - 72, pageSize.height - 72) // 1 inch margins
image.setFixedPosition(36f, 36f) // Center on page

if (index > 0) {
document.add(com.itextpdf.layout.element.AreaBreak())
}
document.add(image)
}
}

document.close()

// Copy the temporary file to the destination
tempFile.inputStream().use { input ->
ParcelFileDescriptor.AutoCloseOutputStream(destination).use { output ->
input.copyTo(output)
}
}

// Clean up temporary file
tempFile.delete()

callback.onWriteFinished(arrayOf(PageRange.ALL_PAGES))

} catch (e: Exception) {
callback.onWriteFailed(e.message)
}
}
}

@OptIn(ExperimentalUuidApi::class)
fun doScan(
esclRequestClient: ESCLRequestClient,
Expand Down Expand Up @@ -834,6 +962,22 @@ fun ScanningScreen(
}
)
}
},
onPrint = {
scanningViewModel.setShowExportOptionsPopup(false)
doPrint(
scanningViewModel,
context,
{ error ->
snackBarError(
error,
scope,
context,
snackbarHostState,
false
)
}
)
}
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ fun ExportSettingsPopup(
onDismiss: () -> Unit,
updateWidth: (Int) -> Unit,
onExportPdf: () -> Unit,
onExportArchive: () -> Unit
onExportArchive: () -> Unit,
onPrint: () -> Unit
) {
Popup(
alignment = Alignment.TopStart,
Expand Down Expand Up @@ -58,6 +59,12 @@ fun ExportSettingsPopup(
contentDescription = stringResource(R.string.export_as_archive)
)
}
IconButton(onClick = { onPrint() }) {
Icon(
painterResource(R.drawable.round_print_36),
contentDescription = stringResource(R.string.print)
)
}
}
}
}
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values-de/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<string name="swap_with_next_page">Nach rechts verschieben</string>
<string name="page_x_of_y">Seite %1$s von %2$s</string>
<string name="export">Exportieren</string>
<string name="print">Drucken</string>
<string name="retrieving_page">Seite wird abgerufen</string>
<string name="exporting">Wird exportiert…</string>
<string name="no_scans_yet">Bisher keine Seiten. Klicke den Scanbutton :)</string>
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<string name="swap_with_next_page">Swap with next</string>
<string name="page_x_of_y">Page %1$s of %2$s</string>
<string name="export">Export</string>
<string name="print">Print</string>
<string name="retrieving_page">Retrieving page</string>
<string name="exporting">Exporting…</string>
<string name="no_scans_yet">No pages were scanned yet. Click the scan button :)</string>
Expand Down