From a84fa19c566c0b05fb654a4625ad468b7f830a58 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Mon, 10 Aug 2026 13:58:21 +0200 Subject: [PATCH 1/2] fix for crash --- .../system/hedvig/pdfrenderer/PdfDecoder.kt | 56 +++++++++++++------ 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/app/design-system/design-system-hedvig/src/androidMain/kotlin/com/hedvig/android/design/system/hedvig/pdfrenderer/PdfDecoder.kt b/app/design-system/design-system-hedvig/src/androidMain/kotlin/com/hedvig/android/design/system/hedvig/pdfrenderer/PdfDecoder.kt index 3941772dd8..0f19ca3a5c 100644 --- a/app/design-system/design-system-hedvig/src/androidMain/kotlin/com/hedvig/android/design/system/hedvig/pdfrenderer/PdfDecoder.kt +++ b/app/design-system/design-system-hedvig/src/androidMain/kotlin/com/hedvig/android/design/system/hedvig/pdfrenderer/PdfDecoder.kt @@ -1,6 +1,6 @@ package com.hedvig.android.design.system.hedvig.pdfrenderer -import android.graphics.Bitmap +import android.graphics.Color import android.graphics.pdf.PdfRenderer import android.os.ParcelFileDescriptor import androidx.core.graphics.createBitmap @@ -12,30 +12,49 @@ import coil3.decode.Decoder import coil3.decode.ImageSource import coil3.fetch.SourceFetchResult import coil3.request.Options +import kotlin.math.roundToInt +import kotlin.math.sqrt class PdfDecoder( private val source: ImageSource, private val options: Options, ) : Decoder { override suspend fun decode(): DecodeResult { - val context = options.context - val pdfRenderer = PdfRenderer( - ParcelFileDescriptor.open( - source.file().toFile(), - ParcelFileDescriptor.MODE_READ_ONLY, - ), + val fileDescriptor = ParcelFileDescriptor.open( + source.file().toFile(), + ParcelFileDescriptor.MODE_READ_ONLY, ) - val page = pdfRenderer.openPage(0) - - val bitmap = createBitmap(page.width * 2, page.height * 2) - page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY) - page.close() - pdfRenderer.close() + return fileDescriptor.use { descriptor -> + PdfRenderer(descriptor).use { renderer -> + renderer.openPage(0).use { page -> + val scale = page.renderScale() + val bitmap = createBitmap( + width = (page.width * scale).roundToInt().coerceAtLeast(1), + height = (page.height * scale).roundToInt().coerceAtLeast(1), + ) + // `render` blends onto the destination, so unpainted and transparent regions of the page keep the bitmap's + // initial fully transparent pixels unless the background is filled in first. + bitmap.eraseColor(Color.WHITE) + page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY) + DecodeResult( + image = bitmap.toDrawable(options.context.resources).asImage(), + isSampled = scale < RENDER_SCALE, + ) + } + } + } + } - return DecodeResult( - image = bitmap.toDrawable(context.resources).asImage(), - isSampled = false, - ) + /** + * The factor to scale the page's native point size by when rendering it into a bitmap. + * + * [RENDER_SCALE] normally, lowered for pages whose box is large enough that rendering at that scale would exceed + * [MAX_BITMAP_BYTES]. A bitmap above the hardware canvas' 100 MB limit throws from `Canvas#drawBitmap` during the + * draw pass, which no amount of error handling around the image request can recover from. + */ + private fun PdfRenderer.Page.renderScale(): Double { + val bytesAtNativeSize = width.toDouble() * height * BYTES_PER_PIXEL + return RENDER_SCALE.coerceAtMost(sqrt(MAX_BITMAP_BYTES / bytesAtNativeSize)) } class Factory : Decoder.Factory { @@ -49,5 +68,8 @@ class PdfDecoder( companion object { private const val MIME_TYPE_PDF = "application/pdf" + private const val RENDER_SCALE = 2.0 + private const val BYTES_PER_PIXEL = 4 + private const val MAX_BITMAP_BYTES = 32L * 1024 * 1024 } } From 6f44864b14d340aab1df48c8b3f2eaf40eddacf9 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Mon, 10 Aug 2026 14:27:31 +0200 Subject: [PATCH 2/2] early-return for opaque input in pdf with white transform --- .../android/design/system/hedvig/pdfrenderer/PdfDecoder.kt | 3 +++ .../android/feature/imageviewer/ImageViewerDestination.kt | 3 +++ 2 files changed, 6 insertions(+) diff --git a/app/design-system/design-system-hedvig/src/androidMain/kotlin/com/hedvig/android/design/system/hedvig/pdfrenderer/PdfDecoder.kt b/app/design-system/design-system-hedvig/src/androidMain/kotlin/com/hedvig/android/design/system/hedvig/pdfrenderer/PdfDecoder.kt index 0f19ca3a5c..d377ad5495 100644 --- a/app/design-system/design-system-hedvig/src/androidMain/kotlin/com/hedvig/android/design/system/hedvig/pdfrenderer/PdfDecoder.kt +++ b/app/design-system/design-system-hedvig/src/androidMain/kotlin/com/hedvig/android/design/system/hedvig/pdfrenderer/PdfDecoder.kt @@ -36,6 +36,9 @@ class PdfDecoder( // initial fully transparent pixels unless the background is filled in first. bitmap.eraseColor(Color.WHITE) page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY) + // Rendering blends onto the opaque white fill, leaving every pixel opaque. Declaring that lets consumers + // skip compositing the result onto a background of their own. + bitmap.setHasAlpha(false) DecodeResult( image = bitmap.toDrawable(options.context.resources).asImage(), isSampled = scale < RENDER_SCALE, diff --git a/app/feature/feature-image-viewer/src/main/kotlin/com/hedvig/android/feature/imageviewer/ImageViewerDestination.kt b/app/feature/feature-image-viewer/src/main/kotlin/com/hedvig/android/feature/imageviewer/ImageViewerDestination.kt index 781b590c8b..a316d0317f 100644 --- a/app/feature/feature-image-viewer/src/main/kotlin/com/hedvig/android/feature/imageviewer/ImageViewerDestination.kt +++ b/app/feature/feature-image-viewer/src/main/kotlin/com/hedvig/android/feature/imageviewer/ImageViewerDestination.kt @@ -135,6 +135,9 @@ class WhiteBackgroundTransformation : Transformation() { override val cacheKey = "white_background" override suspend fun transform(input: Bitmap, size: Size): Bitmap { + // Without an alpha channel there is no transparency for the white background to show through, so compositing + // would allocate a second full-size copy of a visually identical image. + if (!input.hasAlpha()) return input val output = createBitmap(input.width, input.height) val canvas = AndroidCanvas(output) canvas.drawColor(AndroidColor.WHITE)