|
|
@@ -0,0 +1,142 @@
|
|
|
+package eu.mjdev.desktop.helpers
|
|
|
+
|
|
|
+import coil3.DrawableImage
|
|
|
+import coil3.Image
|
|
|
+import coil3.ImageLoader
|
|
|
+import coil3.annotation.ExperimentalCoilApi
|
|
|
+import coil3.decode.DecodeResult
|
|
|
+import coil3.decode.DecodeUtils
|
|
|
+import coil3.decode.Decoder
|
|
|
+import coil3.decode.ImageSource
|
|
|
+import coil3.fetch.SourceFetchResult
|
|
|
+import coil3.request.Options
|
|
|
+import coil3.size.Dimension
|
|
|
+import coil3.size.Scale
|
|
|
+import coil3.size.isOriginal
|
|
|
+import coil3.size.pxOrElse
|
|
|
+import org.jetbrains.skia.Canvas
|
|
|
+import org.jetbrains.skia.Data
|
|
|
+import org.jetbrains.skia.Rect
|
|
|
+import org.jetbrains.skia.svg.SVGDOM
|
|
|
+import org.jetbrains.skia.svg.SVGLength
|
|
|
+import org.jetbrains.skia.svg.SVGLengthUnit
|
|
|
+import kotlin.math.roundToInt
|
|
|
+
|
|
|
+// todo from svg
|
|
|
+class PdfDecoder(
|
|
|
+ private val source: ImageSource,
|
|
|
+ private val options: Options,
|
|
|
+ val useViewBoundsAsIntrinsicSize: Boolean,
|
|
|
+) : Decoder {
|
|
|
+ @OptIn(ExperimentalCoilApi::class)
|
|
|
+ override suspend fun decode(): DecodeResult {
|
|
|
+// val context = options.context
|
|
|
+ val bytes = source.source().readByteArray()
|
|
|
+ val pdf = SVGDOM(Data.makeFromBytes(bytes))
|
|
|
+ val pdfWidth: Float
|
|
|
+ val pdfHeight: Float
|
|
|
+ val viewBox: Rect? = pdf.root?.viewBox
|
|
|
+ if (useViewBoundsAsIntrinsicSize && viewBox != null) {
|
|
|
+ pdfWidth = viewBox.width
|
|
|
+ pdfHeight = viewBox.height
|
|
|
+ } else {
|
|
|
+ pdfWidth = pdf.root?.width?.value ?: 0f
|
|
|
+ pdfHeight = pdf.root?.height?.value ?: 0f
|
|
|
+ }
|
|
|
+ val bitmapWidth: Int
|
|
|
+ val bitmapHeight: Int
|
|
|
+ val (dstWidth, dstHeight) = getDstSize(pdfWidth, pdfHeight, options.scale)
|
|
|
+ if (pdfWidth > 0f && pdfHeight > 0f) {
|
|
|
+ val multiplier = DecodeUtils.computeSizeMultiplier(
|
|
|
+ srcWidth = pdfWidth,
|
|
|
+ srcHeight = pdfHeight,
|
|
|
+ dstWidth = dstWidth.toFloat(),
|
|
|
+ dstHeight = dstHeight.toFloat(),
|
|
|
+ scale = options.scale,
|
|
|
+ )
|
|
|
+ bitmapWidth = (multiplier * pdfWidth).toInt()
|
|
|
+ bitmapHeight = (multiplier * pdfHeight).toInt()
|
|
|
+ } else {
|
|
|
+ bitmapWidth = dstWidth
|
|
|
+ bitmapHeight = dstHeight
|
|
|
+ }
|
|
|
+ if (viewBox == null && pdfWidth > 0f && pdfHeight > 0f) {
|
|
|
+ pdf.root?.viewBox = Rect.makeWH(pdfWidth, pdfHeight)
|
|
|
+ }
|
|
|
+ pdf.root?.width = SVGLength(
|
|
|
+ value = 100f,
|
|
|
+ unit = SVGLengthUnit.PERCENTAGE,
|
|
|
+ )
|
|
|
+ pdf.root?.height = SVGLength(
|
|
|
+ value = 100f,
|
|
|
+ unit = SVGLengthUnit.PERCENTAGE,
|
|
|
+ )
|
|
|
+ pdf.setContainerSize(bitmapWidth.toFloat(), bitmapHeight.toFloat())
|
|
|
+ return DecodeResult(
|
|
|
+ image = pdf.asCoilImage(bitmapWidth, bitmapHeight),
|
|
|
+ isSampled = false,
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun getDstSize(srcWidth: Float, srcHeight: Float, scale: Scale): Pair<Int, Int> {
|
|
|
+ if (options.size.isOriginal) {
|
|
|
+ val dstWidth = if (srcWidth > 0) srcWidth.roundToInt() else PDF_DEFAULT_SIZE
|
|
|
+ val dstHeight = if (srcHeight > 0) srcHeight.roundToInt() else PDF_DEFAULT_SIZE
|
|
|
+ return dstWidth to dstHeight
|
|
|
+ } else {
|
|
|
+ val (dstWidth, dstHeight) = options.size
|
|
|
+ return dstWidth.toPx(scale) to dstHeight.toPx(scale)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ class Factory(
|
|
|
+ val useViewBoundsAsIntrinsicSize: Boolean,
|
|
|
+ ) : Decoder.Factory {
|
|
|
+ override fun create(result: SourceFetchResult, options: Options, imageLoader: ImageLoader): Decoder? {
|
|
|
+ if (!isApplicable(result)) return null
|
|
|
+ return PdfDecoder(result.source, options, useViewBoundsAsIntrinsicSize)
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun isApplicable(result: SourceFetchResult): Boolean =
|
|
|
+ result.mimeType == MIME_TYPE_PDF
|
|
|
+ }
|
|
|
+
|
|
|
+ @OptIn(ExperimentalCoilApi::class)
|
|
|
+ private class PdfImage(
|
|
|
+ val pdf: SVGDOM,
|
|
|
+ override val width: Int,
|
|
|
+ override val height: Int,
|
|
|
+ ) : DrawableImage() {
|
|
|
+ override val size: Long
|
|
|
+ get() = 4L * width * height
|
|
|
+
|
|
|
+ override val shareable: Boolean = true
|
|
|
+
|
|
|
+ @ExperimentalCoilApi
|
|
|
+ override fun Canvas.onDraw() {
|
|
|
+ pdf.render(this)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ companion object {
|
|
|
+ private const val PDF_DEFAULT_SIZE = 512
|
|
|
+ private const val MIME_TYPE_PDF = "application/pdf"
|
|
|
+
|
|
|
+ private fun Dimension.toPx(scale: Scale): Int = pxOrElse {
|
|
|
+ when (scale) {
|
|
|
+ Scale.FILL -> Int.MIN_VALUE
|
|
|
+ Scale.FIT -> Int.MAX_VALUE
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @OptIn(ExperimentalCoilApi::class)
|
|
|
+ internal fun SVGDOM.asCoilImage(
|
|
|
+ width: Int,
|
|
|
+ height: Int,
|
|
|
+ ): Image = PdfImage(
|
|
|
+ pdf = this,
|
|
|
+ width = width,
|
|
|
+ height = height,
|
|
|
+ )
|
|
|
+ }
|
|
|
+}
|