Beta 1.0 Release #11
@@ -29,7 +29,7 @@ class AppContainer(context: Context) {
|
||||
|
||||
private val photoStorage: PhotoStorage by lazy { InternalPhotoStorage(context) }
|
||||
private val signatureFileStorage: SignatureFileStorage by lazy { InternalSignatureFileStorage(context) }
|
||||
private val pdfExporter: DocumentationPdfExporter by lazy { AndroidPdfDocumentExporter(context) }
|
||||
private val pdfExporter: DocumentationPdfExporter by lazy { AndroidPdfDocumentExporter(context, photoStorage) }
|
||||
|
||||
val patientRepository: PatientRepository by lazy {
|
||||
PatientRepository(patientDao = database.patientDao())
|
||||
|
||||
@@ -9,6 +9,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* Verwaltet die physischen Bilddateien im internen App-Speicher, organisiert pro Dokumentation:
|
||||
@@ -86,6 +87,27 @@ interface PhotoStorage {
|
||||
* wenn eine Datei nachträglich außerhalb der App entfernt wurde).
|
||||
*/
|
||||
suspend fun decodeThumbnail(documentationId: String, fileName: String, maxDimensionPx: Int): Bitmap?
|
||||
|
||||
/**
|
||||
* Decodiert die gespeicherte Datei proportional eingepasst in eine Box von höchstens
|
||||
* [maxWidthPx] × [maxHeightPx] Pixeln, OHNE Hochskalieren über die native Auflösung hinaus
|
||||
* (PDF-004: Fotoanhang). Läuft zweistufig:
|
||||
*
|
||||
* 1. Grobe, speicherschonende Decodierung über `BitmapFactory.Options.inSampleSize`
|
||||
* (Zweierpotenz-Downsampling direkt im Decoder) - liefert ein Bitmap, das mindestens so
|
||||
* groß wie die exakte Zielgröße ist, aber wegen der Zweierpotenz-Beschränkung von
|
||||
* `inSampleSize` bei Quellbildern nahe der Zielgröße (z. B. nahe dem Import-Limit
|
||||
* [ImageDownsampler.MAX_STORED_DIMENSION_PX]) oft noch keine ausreichende Reduktion
|
||||
* bewirkt.
|
||||
* 2. Exakte Skalierung dieses groben Zwischenergebnisses auf die tatsächliche Zielgröße
|
||||
* (`Bitmap.createScaledBitmap`). Das Zwischenergebnis aus Schritt 1 wird direkt danach
|
||||
* freigegeben (`recycle()`) - das ist unproblematisch, da es zu diesem Zeitpunkt noch
|
||||
* nirgends (insbesondere nicht an `PdfDocument`) weitergereicht wurde. Nur das Ergebnis
|
||||
* aus Schritt 2 wird von der aufrufenden Stelle tatsächlich in die PDF gezeichnet.
|
||||
*
|
||||
* Liefert `null`, wenn die Datei fehlt oder nicht decodiert werden kann (kein Absturz).
|
||||
*/
|
||||
suspend fun decodeFitted(documentationId: String, fileName: String, maxWidthPx: Int, maxHeightPx: Int): Bitmap?
|
||||
}
|
||||
|
||||
class InternalPhotoStorage(private val context: Context) : PhotoStorage {
|
||||
@@ -212,4 +234,59 @@ class InternalPhotoStorage(private val context: Context) : PhotoStorage {
|
||||
)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
override suspend fun decodeFitted(
|
||||
documentationId: String,
|
||||
fileName: String,
|
||||
maxWidthPx: Int,
|
||||
maxHeightPx: Int,
|
||||
): Bitmap? = withContext(Dispatchers.IO) {
|
||||
val file = resolve(documentationId, fileName)
|
||||
if (!file.exists()) return@withContext null
|
||||
|
||||
runCatching {
|
||||
val boundsOptions = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||
BitmapFactory.decodeFile(file.absolutePath, boundsOptions)
|
||||
val sourceWidth = boundsOptions.outWidth
|
||||
val sourceHeight = boundsOptions.outHeight
|
||||
if (sourceWidth <= 0 || sourceHeight <= 0) return@runCatching null
|
||||
|
||||
// Zielgröße: proportionale Einpassung in (maxWidthPx x maxHeightPx), Faktor auf
|
||||
// maximal 1f begrenzt - kein Hochskalieren über die native Auflösung hinaus.
|
||||
val scale = minOf(
|
||||
maxWidthPx.toFloat() / sourceWidth,
|
||||
maxHeightPx.toFloat() / sourceHeight,
|
||||
1f,
|
||||
)
|
||||
val targetWidth = (sourceWidth * scale).roundToInt().coerceAtLeast(1)
|
||||
val targetHeight = (sourceHeight * scale).roundToInt().coerceAtLeast(1)
|
||||
|
||||
// Schritt 1: grobe, speicherschonende Decodierung über inSampleSize - liefert ein
|
||||
// Bitmap, das mindestens so groß wie (targetWidth, targetHeight) ist.
|
||||
val inSampleSize = ImageDownsampler.computeInSampleSize(
|
||||
sourceWidth,
|
||||
sourceHeight,
|
||||
maxOf(targetWidth, targetHeight),
|
||||
)
|
||||
val decodeOptions = BitmapFactory.Options().apply {
|
||||
this.inSampleSize = inSampleSize
|
||||
inPreferredConfig = Bitmap.Config.ARGB_8888
|
||||
}
|
||||
val coarseBitmap = BitmapFactory.decodeFile(file.absolutePath, decodeOptions)
|
||||
?: return@runCatching null
|
||||
|
||||
// Schritt 2: exakte Zielgröße. Nur nötig, wenn inSampleSize (Zweierpotenz) nicht
|
||||
// bereits exakt getroffen hat - insbesondere bei Quellbildern nahe dem
|
||||
// Import-Limit oft der Fall, da dort inSampleSize = 1 bleibt.
|
||||
if (coarseBitmap.width == targetWidth && coarseBitmap.height == targetHeight) {
|
||||
coarseBitmap
|
||||
} else {
|
||||
val precise = Bitmap.createScaledBitmap(coarseBitmap, targetWidth, targetHeight, true)
|
||||
// Das grobe Zwischenergebnis wurde noch nirgends (insbesondere nicht an
|
||||
// PdfDocument) weitergereicht - Freigabe an dieser Stelle ist unproblematisch.
|
||||
if (precise !== coarseBitmap) coarseBitmap.recycle()
|
||||
precise
|
||||
}
|
||||
}.getOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package de.medapp.wunddoku.pdf
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.pdf.PdfDocument
|
||||
import android.text.TextPaint
|
||||
import de.medapp.wunddoku.media.PhotoStorage
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
@@ -22,11 +24,30 @@ import java.time.format.FormatStyle
|
||||
* reproduzierbar, unabhängig davon, welche Systemschrift auf dem jeweiligen Gerät installiert
|
||||
* ist bzw. welche Unicode-Symbole sie abdeckt.
|
||||
*
|
||||
* PDF-004 (Fotoanhang) - wichtige, geprüfte Speicher-Einschränkung von `PdfDocument`: Jede
|
||||
* `Page` wird intern über ein `SkPicture` AUFGEZEICHNET (verifiziert anhand des AOSP-
|
||||
* Quellcodes, `frameworks/base/.../PdfDocument.cpp`: `startPage()` -> `mPicture->beginRecording()`,
|
||||
* `finishPage()` -> nur `endRecording()`, tatsächliches PDF-Rendering inkl. Bild-Einbettung
|
||||
* erst in `write()`/`writeTo()` über `canvas->drawPicture(*page->mPicture)`). Das bedeutet: JEDE
|
||||
* über `Canvas.drawBitmap(...)` gezeichnete Bitmap muss bis zum Abschluss von `writeTo()`
|
||||
* gültig (nicht recycelt) bleiben - ein `Bitmap.recycle()` direkt nach dem Zeichnen einer
|
||||
* einzelnen Fotoseite ist NICHT sicher. Deshalb werden alle tatsächlich gezeichneten
|
||||
* Foto-Bitmaps in [export] gesammelt und erst NACH `writeTo()` (im `finally`-Block)
|
||||
* freigegeben - siehe [retainedPhotoBitmaps].
|
||||
*
|
||||
* Der einzige verbleibende Hebel gegen hohen Speicherverbrauch ist deshalb die Zielauflösung
|
||||
* pro Foto, nicht der Recycling-Zeitpunkt: Zwischenergebnisse, die selbst nie an
|
||||
* `PdfDocument`/`Canvas.drawBitmap` übergeben werden (grobe `inSampleSize`-Decodierung vor der
|
||||
* exakten Zielgrößen-Skalierung, s. [de.medapp.wunddoku.media.PhotoStorage.decodeFitted]),
|
||||
* dürfen dagegen sofort freigegeben werden.
|
||||
*
|
||||
* Speicherort: `filesDir/pdf/<documentationId>/<Dateiname>.pdf` - ausschließlich intern, keine
|
||||
* MediaStore-Nutzung, kein öffentlicher Speicher (PDF-005/006 mit FileProvider/Sharesheet folgen
|
||||
* später).
|
||||
* MediaStore-Nutzung, kein öffentlicher Speicher.
|
||||
*/
|
||||
class AndroidPdfDocumentExporter(private val context: Context) : DocumentationPdfExporter {
|
||||
class AndroidPdfDocumentExporter(
|
||||
private val context: Context,
|
||||
private val photoStorage: PhotoStorage,
|
||||
) : DocumentationPdfExporter {
|
||||
|
||||
companion object {
|
||||
// A4 bei 72pt/Zoll (Standard-PDF-Punkt-Konvention), entspricht 210 x 297 mm.
|
||||
@@ -47,6 +68,32 @@ class AndroidPdfDocumentExporter(private val context: Context) : DocumentationPd
|
||||
private const val CHECKBOX_GAP = 6f
|
||||
private const val CHECKBOX_INDENT = CHECKBOX_SIZE + CHECKBOX_GAP
|
||||
|
||||
/**
|
||||
* Zielauflösung für den Fotoanhang, bewusst als Kompromiss zwischen Bildqualität und
|
||||
* Speicherverbrauch gewählt (freigegeben nach expliziter Prüfung der `PdfDocument`-
|
||||
* Speichersemantik, s. Klassendokumentation): untere Grenze des ursprünglich
|
||||
* vorgeschlagenen 150-200-DPI-Bereichs, da ALLE Foto-Bitmaps bis `writeTo()` gleichzeitig
|
||||
* im Speicher gehalten werden müssen (keine echte "ein Bitmap zur Zeit"-Verarbeitung mit
|
||||
* dieser PDF-Engine möglich) und `ARGB_8888` statt `RGB_565` verwendet wird (keine
|
||||
* Farbtiefenreduktion bei medizinischen Wundfotos).
|
||||
*/
|
||||
private const val PHOTO_TARGET_DPI = 150
|
||||
|
||||
// Verfügbare Bildfläche in Punkten (Seite abzüglich Rand) - siehe drawText()-nahe
|
||||
// Verwendung von contentWidth/remainingHeight für die tatsächliche Platzierung; diese
|
||||
// Konstanten bestimmen ausschließlich die DECODE-Zielgröße (wie groß ein Foto maximal
|
||||
// eingelesen wird), nicht die exakte Zeichenposition (die hängt zusätzlich von der
|
||||
// jeweils bereits gezeichneten Bildunterschrift ab und wird von PdfPageCursor selbst
|
||||
// anhand des tatsächlich verbleibenden Platzes bestimmt).
|
||||
private const val PHOTO_AREA_WIDTH_PT = PAGE_WIDTH - 2 * MARGIN
|
||||
private const val PHOTO_AREA_HEIGHT_PT = PAGE_HEIGHT - 2 * MARGIN
|
||||
|
||||
/** Maximale Decode-Breite in Pixeln bei [PHOTO_TARGET_DPI] (≈ 1073px bei 515pt/150dpi). */
|
||||
private val PHOTO_MAX_WIDTH_PX = (PHOTO_AREA_WIDTH_PT / 72f * PHOTO_TARGET_DPI).toInt()
|
||||
|
||||
/** Maximale Decode-Höhe in Pixeln bei [PHOTO_TARGET_DPI] (≈ 1587px bei 762pt/150dpi). */
|
||||
private val PHOTO_MAX_HEIGHT_PX = (PHOTO_AREA_HEIGHT_PT / 72f * PHOTO_TARGET_DPI).toInt()
|
||||
|
||||
private val HEADER_DATE_FORMATTER: DateTimeFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
|
||||
}
|
||||
|
||||
@@ -63,6 +110,12 @@ class AndroidPdfDocumentExporter(private val context: Context) : DocumentationPd
|
||||
val tempFile = File(targetDir, "${model.fileName}.tmp")
|
||||
|
||||
val pdfDocument = PdfDocument()
|
||||
// Sammelt AUSSCHLIESSLICH die Bitmaps, die tatsächlich per Canvas.drawBitmap() gezeichnet
|
||||
// wurden (nicht die groben inSampleSize-Zwischenergebnisse, die bereits innerhalb von
|
||||
// PhotoStorage.decodeFitted() freigegeben werden). Diese müssen laut der in der
|
||||
// Klassendokumentation belegten PdfDocument-Speichersemantik bis NACH writeTo() gültig
|
||||
// bleiben und werden deshalb erst im finally-Block unten recycelt.
|
||||
val retainedPhotoBitmaps = mutableListOf<Bitmap>()
|
||||
try {
|
||||
val paints = ExportPaints()
|
||||
val cursor = PdfPageCursor(
|
||||
@@ -79,6 +132,9 @@ class AndroidPdfDocumentExporter(private val context: Context) : DocumentationPd
|
||||
for (block in model.blocks) {
|
||||
drawBlock(cursor, block, paints)
|
||||
}
|
||||
for ((index, photo) in model.photos.withIndex()) {
|
||||
drawPhotoPage(cursor, model, photo, index, paints, retainedPhotoBitmaps)
|
||||
}
|
||||
cursor.finish()
|
||||
|
||||
FileOutputStream(tempFile).use { out -> pdfDocument.writeTo(out) }
|
||||
@@ -99,6 +155,10 @@ class AndroidPdfDocumentExporter(private val context: Context) : DocumentationPd
|
||||
)
|
||||
} finally {
|
||||
pdfDocument.close()
|
||||
// Erst JETZT sicher, da writeTo() (im try-Block oben) zu diesem Zeitpunkt entweder
|
||||
// bereits erfolgreich abgeschlossen ist, oder der Export ohnehin fehlgeschlagen ist -
|
||||
// in beiden Fällen werden diese Bitmaps nicht mehr gebraucht.
|
||||
retainedPhotoBitmaps.forEach { runCatching { it.recycle() } }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,6 +211,13 @@ class AndroidPdfDocumentExporter(private val context: Context) : DocumentationPd
|
||||
strokeCap = Paint.Cap.ROUND
|
||||
color = Color.BLACK
|
||||
}
|
||||
|
||||
/** Für das Zeichnen der Fotoanhang-Bitmaps (PDF-004). `isFilterBitmap` sorgt für
|
||||
* weiche statt treppenartige Kanten beim Canvas-seitigen Skalieren. */
|
||||
val photo = Paint().apply {
|
||||
isAntiAlias = true
|
||||
isFilterBitmap = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawHeader(cursor: PdfPageCursor, model: PdfExportModel, paints: ExportPaints) {
|
||||
@@ -233,4 +300,49 @@ class AndroidPdfDocumentExporter(private val context: Context) : DocumentationPd
|
||||
canvas.drawLine(boxRight - inset, boxTop + inset, left + inset, boxBottom - inset, paints.checkboxMark)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeichnet den Fotoanhang: jedes Foto auf einer eigenen, neu begonnenen Seite (PDF-004).
|
||||
* Vor dem ersten Foto wird eine kurze Überschrift "Fotoanhang" ergänzt, jedes Foto bekommt
|
||||
* zusätzlich eine neutrale Nummerierung ("Foto N von M") - keine Patientendaten oder
|
||||
* ursprünglichen Dateinamen als Beschriftung.
|
||||
*
|
||||
* Erfolgreich decodierte Bitmaps werden in [retainedPhotoBitmaps] gesammelt (s. Begründung
|
||||
* in der Klassendokumentation zur PdfDocument-Speichersemantik) - der Aufrufer ist dafür
|
||||
* verantwortlich, diese erst NACH `writeTo()` freizugeben.
|
||||
*/
|
||||
private suspend fun drawPhotoPage(
|
||||
cursor: PdfPageCursor,
|
||||
model: PdfExportModel,
|
||||
photo: PdfExportPhoto,
|
||||
index: Int,
|
||||
paints: ExportPaints,
|
||||
retainedPhotoBitmaps: MutableList<Bitmap>,
|
||||
) {
|
||||
cursor.forceNewPage()
|
||||
if (index == 0) {
|
||||
cursor.drawText("Fotoanhang", paints.blockTitle, spacingAfter = 8f)
|
||||
}
|
||||
cursor.drawText("Foto ${index + 1} von ${model.photos.size}", paints.headerInfo, spacingAfter = 8f)
|
||||
|
||||
val bitmap = runCatching {
|
||||
photoStorage.decodeFitted(
|
||||
documentationId = model.documentationId,
|
||||
fileName = photo.fileName,
|
||||
maxWidthPx = PHOTO_MAX_WIDTH_PX,
|
||||
maxHeightPx = PHOTO_MAX_HEIGHT_PX,
|
||||
)
|
||||
}.getOrNull()
|
||||
|
||||
if (bitmap != null) {
|
||||
retainedPhotoBitmaps.add(bitmap)
|
||||
cursor.drawBitmapFitted(bitmap, paints.photo)
|
||||
} else {
|
||||
// Fehlende/beschädigte Datei, IOException, ungültige Abmessungen o. Ä. - Option B:
|
||||
// neutrale Hinweisseite statt stillschweigendem Überspringen, damit nachvollziehbar
|
||||
// bleibt, dass an dieser Stelle ursprünglich ein Foto zur Dokumentation gehörte.
|
||||
// Bewusst OHNE Dateipfad/technischen Fehlertext.
|
||||
cursor.drawText("Foto konnte nicht geladen werden.", paints.item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,14 @@ object DocumentationToPdfExportModelMapper {
|
||||
birthDate = snapshot.birthDate,
|
||||
)
|
||||
|
||||
// Explizit erneut nach orderIndex sortiert (nicht implizit auf die bereits im
|
||||
// Domain-Modell vorhandene Reihenfolge verlassen) - dieselbe defensive Konsistenz wie
|
||||
// bei blocks/items oben, gemäß Vorgabe "nicht Dateiname/UUID/createdAt/Dateisystem-
|
||||
// Reihenfolge verwenden".
|
||||
val photos = documentation.photos
|
||||
.sortedBy { it.orderIndex }
|
||||
.map { photo -> PdfExportPhoto(fileName = photo.fileName) }
|
||||
|
||||
return PdfExportModel(
|
||||
documentationId = documentation.id,
|
||||
fileName = fileName,
|
||||
@@ -56,6 +64,7 @@ object DocumentationToPdfExportModelMapper {
|
||||
patientDisplayAddress = snapshot.displayAddress,
|
||||
createdAt = createdAtDate,
|
||||
blocks = editedBlocks,
|
||||
photos = photos,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ data class PdfExportModel(
|
||||
val createdAt: LocalDate,
|
||||
/** Bereits gefiltert (nur bearbeitete Blöcke, s. BlockEditedPolicy) und sortiert (blockOrder). */
|
||||
val blocks: List<PdfExportBlock>,
|
||||
/** Bereits sortiert nach orderIndex (PDF-004: Fotoanhang). */
|
||||
val photos: List<PdfExportPhoto>,
|
||||
)
|
||||
|
||||
data class PdfExportBlock(
|
||||
@@ -34,3 +36,12 @@ data class PdfExportItem(
|
||||
val isFreeText: Boolean,
|
||||
val freeTextValue: String,
|
||||
)
|
||||
|
||||
/**
|
||||
* Minimale, für den Fotoanhang benötigte Referenz. Enthält bewusst nur [fileName] -
|
||||
* `documentationId` steht bereits am [PdfExportModel] selbst zur Verfügung (nötig, um über
|
||||
* [de.medapp.wunddoku.media.PhotoStorage] die intern gespeicherte Datei aufzulösen).
|
||||
*/
|
||||
data class PdfExportPhoto(
|
||||
val fileName: String,
|
||||
)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package de.medapp.wunddoku.pdf
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.RectF
|
||||
import android.graphics.pdf.PdfDocument
|
||||
import android.text.Layout
|
||||
import android.text.StaticLayout
|
||||
@@ -51,6 +53,14 @@ internal class PdfPageCursor(
|
||||
val contentWidth: Int
|
||||
get() = (pageWidth - marginLeft - marginRight).toInt()
|
||||
|
||||
/** Verbleibende Höhe auf der aktuellen Seite ab der aktuellen Cursor-Position (PDF-004:
|
||||
* Grundlage für die Einpassung des Fotos nach einer bereits gezeichneten Bildunterschrift). */
|
||||
val remainingHeight: Float
|
||||
get() {
|
||||
startIfNeeded()
|
||||
return (contentBottom() - cursorY).coerceAtLeast(0f)
|
||||
}
|
||||
|
||||
private fun contentBottom(): Float = pageHeight - marginBottom
|
||||
|
||||
private fun startIfNeeded() {
|
||||
@@ -77,6 +87,42 @@ internal class PdfPageCursor(
|
||||
canvas = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzwingt den Beginn einer neuen Seite, unabhängig davon, wie viel Platz auf der aktuellen
|
||||
* Seite noch verfügbar wäre (PDF-004: jedes Foto beginnt zwingend auf einer eigenen neuen
|
||||
* Seite, nie im verbleibenden Platz einer Dokumentations- oder vorherigen Fotoseite). Die
|
||||
* Seitennummerierung läuft dabei fortlaufend weiter, da [startNewPage] intern lediglich
|
||||
* `pageNumber` erhöht, nie zurücksetzt.
|
||||
*/
|
||||
fun forceNewPage() {
|
||||
startNewPage()
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeichnet [bitmap] proportional eingepasst (kein Zuschneiden, keine Verzerrung) horizontal
|
||||
* über die volle Inhaltsbreite und vertikal zentriert im aktuell verbleibenden Platz der
|
||||
* Seite. Setzt voraus, dass die Seite bereits begonnen wurde (i. d. R. direkt nach
|
||||
* [forceNewPage] plus einer optionalen Bildunterschrift über [drawText]) - diese Methode
|
||||
* selbst löst KEINEN Seitenumbruch aus, da ein Foto nicht über zwei Seiten verteilt werden
|
||||
* darf; reicht der verbleibende Platz nicht aus, wird das Bild in den vorhandenen Rest
|
||||
* hineinskaliert (kleiner, nie abgeschnitten).
|
||||
*/
|
||||
fun drawBitmapFitted(bitmap: Bitmap, paint: Paint) {
|
||||
startIfNeeded()
|
||||
val availableWidth = contentWidth.toFloat()
|
||||
val availableHeight = remainingHeight
|
||||
if (availableWidth <= 0f || availableHeight <= 0f || bitmap.width <= 0 || bitmap.height <= 0) return
|
||||
|
||||
val scale = minOf(availableWidth / bitmap.width, availableHeight / bitmap.height)
|
||||
val drawWidth = bitmap.width * scale
|
||||
val drawHeight = bitmap.height * scale
|
||||
val left = marginLeft + (availableWidth - drawWidth) / 2f
|
||||
val top = cursorY + (availableHeight - drawHeight) / 2f
|
||||
|
||||
canvas!!.drawBitmap(bitmap, null, RectF(left, top, left + drawWidth, top + drawHeight), paint)
|
||||
cursorY = contentBottom()
|
||||
}
|
||||
|
||||
/** Schließt die aktuell offene Seite ab. Muss am Ende des gesamten Exports genau einmal
|
||||
* aufgerufen werden. */
|
||||
fun finish() {
|
||||
|
||||
Reference in New Issue
Block a user