Initial commit: PaperScan — lokale Android documentscanner voor Paperless-ngx

Native Kotlin/Compose app met CameraX + OpenCV randdetectie/perspectiefcorrectie,
multi-page PDF-export, en Paperless-ngx upload (of Android deelmenu). Alles lokaal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-12 22:43:09 +02:00
commit 037550afa9
37 changed files with 2051 additions and 0 deletions

View File

@@ -0,0 +1,60 @@
package eu.geyskens.pdfscan.pdf
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.pdf.PdfDocument
import java.io.File
import java.io.FileOutputStream
/** How each PDF page is sized relative to its scanned image. */
enum class PageSizeMode { A4, ORIGINAL }
object PdfBuilder {
// A4 in PostScript points (1/72"): 210 x 297 mm.
private const val A4_WIDTH_PT = 595
private const val A4_HEIGHT_PT = 842
/**
* Writes [pages] into a single multi-page PDF at [output].
* With [PageSizeMode.A4] each image is centered and scaled to fit an A4 page
* (portrait or landscape chosen per image); with ORIGINAL the page matches the image.
*/
fun build(pages: List<Bitmap>, output: File, mode: PageSizeMode = PageSizeMode.A4) {
require(pages.isNotEmpty()) { "Cannot build a PDF with no pages." }
val doc = PdfDocument()
val paint = Paint(Paint.FILTER_BITMAP_FLAG or Paint.ANTI_ALIAS_FLAG)
try {
pages.forEachIndexed { index, bitmap ->
val (pageW, pageH) = when (mode) {
PageSizeMode.ORIGINAL -> bitmap.width to bitmap.height
PageSizeMode.A4 ->
if (bitmap.width >= bitmap.height) A4_HEIGHT_PT to A4_WIDTH_PT // landscape
else A4_WIDTH_PT to A4_HEIGHT_PT
}
val info = PdfDocument.PageInfo.Builder(pageW, pageH, index + 1).create()
val page = doc.startPage(info)
val canvas = page.canvas
canvas.drawColor(Color.WHITE)
canvas.drawBitmap(bitmap, null, fitRect(bitmap, pageW, pageH), paint)
doc.finishPage(page)
}
FileOutputStream(output).use { doc.writeTo(it) }
} finally {
doc.close()
}
}
/** Largest centered rect inside the page that preserves the bitmap's aspect ratio. */
private fun fitRect(bitmap: Bitmap, pageW: Int, pageH: Int): Rect {
val scale = minOf(pageW.toFloat() / bitmap.width, pageH.toFloat() / bitmap.height)
val w = (bitmap.width * scale).toInt()
val h = (bitmap.height * scale).toInt()
val left = (pageW - w) / 2
val top = (pageH - h) / 2
return Rect(left, top, left + w, top + h)
}
}