Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -12,30 +12,52 @@ 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)
// 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,
)
}
}
}
}

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 {
Expand All @@ -49,5 +71,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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading