androidandroid-webviewandroid-file

android pdf printing using PrintedPdfDocument and webview doesn't work as expected


I am trying to achieve generating a pdf file from the rendered html within webview, but it gives me a pdf with single page but page is blank instead of showing the html content.

fun createPdfFromHtml(
    context: Context,
    htmlContent: String,
    onSuccess: (File) -> Unit,
    onFail: (Throwable) -> Unit
) {
    val webView = WebView(context).apply {
        settings.javaScriptEnabled = true
    }

    webView.webViewClient = object : WebViewClient() {
        override fun onPageFinished(view: WebView?, url: String?) {
            val printAttributes = PrintAttributes.Builder()
                .setMediaSize(PrintAttributes.MediaSize.ISO_A4)
                .setResolution(PrintAttributes.Resolution("RES1", "LABEL", 300, 300))
                .setColorMode(PrintAttributes.COLOR_MODE_COLOR)
                .setMinMargins(PrintAttributes.Margins.NO_MARGINS)
                .build()

            val pdfDocument = PrintedPdfDocument(context, printAttributes)
            val pageInfo = PdfDocument.PageInfo.Builder(595, 842, 1).create()
            val page = pdfDocument.startPage(pageInfo)

            webView.draw(page.canvas)
            pdfDocument.finishPage(page)

            // Save the PDF to the Downloads folder
            val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
            val file = File(downloadsDir, "output.pdf")

            try {
                FileOutputStream(file).use { output ->
                    pdfDocument.writeTo(output)
                }
                onSuccess(file)
            } catch (e: Exception) {
                e.printStackTrace()
                onFail(e)
            } finally {
                pdfDocument.close()
            }
        }
    }

    webView.loadDataWithBaseURL(null, htmlContent, "text/html", "UTF-8", null)
}

Solution

  • I ended up achieving the result using print adapter like below using builder pattern (you can skip builder pattern if you don't need)

    class HtmlToPdfConverter private constructor(
        private val webView: WebView,
        private val htmlContent: String,
        private val fileName: String,
        private val pdfMediaSize: MediaSize,
        private val pdfColorMode: Int,
        private val converterListener: ConverterListener? = null
    ) {
    
        companion object {
            private const val FILE_EXTENSION_PDF = "pdf"
            private const val MIME_TYPE_HTML = "text/html"
            private const val ENCODING = "UTF-8"
            private const val PRINT_ATTRIBUTE_RESOLUTION_ID = "PDF_RESOLUTION"
            private const val PRINT_ADAPTER_DOCUMENT_LABEL = "Pdf resolution"
            private const val PRINT_ADAPTER_DEFAULT_DPI = 300
            private const val PRINT_ADAPTER_DOCUMENT_NAME = "Document"
        }
    
        interface ConverterListener {
            fun onSuccess(file: File)
            fun onFail(throwable: Throwable? = null)
        }
    
        class Builder {
    
            private var htmlContent: String = ""
            private var fileName: String = ""
            private var converterListener: ConverterListener? = null
            private var pdfMediaSize: MediaSize = MediaSize.ISO_A4
            private var pdfColorMode: Int = PrintAttributes.COLOR_MODE_COLOR
    
            fun setHtmlContent(htmlContent: String) = apply { this.htmlContent = htmlContent }
    
            fun setFileName(fileName: String) = apply { this.fileName = fileName }
    
            fun setListener(converterListener: ConverterListener) = apply { this.converterListener = converterListener }
    
            fun setPdfMediaSize(mediaSize: MediaSize) = apply { this.pdfMediaSize = mediaSize }
    
            /**
             * @param colorMode A valid color mode or zero.
             * @see PrintAttributes#COLOR_MODE_MONOCHROME
             * @see PrintAttributes#COLOR_MODE_COLOR
             */
            fun setPdfColorMode(colorMode: Int) = apply { this.pdfColorMode = colorMode }
    
            fun build(context: Context) = HtmlToPdfConverter(
                webView = createWebView(context),
                htmlContent = htmlContent,
                fileName = fileName,
                pdfMediaSize = pdfMediaSize,
                pdfColorMode = pdfColorMode,
                converterListener = converterListener
            )
    
            private fun createWebView(context: Context) = WebView(context).apply {
                settings.apply {
                    javaScriptEnabled = true
                    useWideViewPort = true
                    loadWithOverviewMode = true
                }
            }
        }
    
    
        fun convert() {
            webView.webViewClient = object : WebViewClient() {
                override fun onPageFinished(view: WebView?, url: String?) {
                    val printAdapter = webView.createPrintDocumentAdapter(PRINT_ADAPTER_DOCUMENT_NAME)
                    printAdapter.onLayout(
                        null,
                        getPrintAttributes(),
                        null,
                        PrintAdapterLayoutResultCallback(printAdapter),
                        null
                    )
                }
            }
            webView.loadDataWithBaseURL(null, htmlContent, MIME_TYPE_HTML, ENCODING, null)
        }
    
        private inner class PrintAdapterLayoutResultCallback(
            private val printAdapter: PrintDocumentAdapter
        ) : LayoutResultCallback() {
            override fun onLayoutFinished(info: PrintDocumentInfo?, changed: Boolean) {
                super.onLayoutFinished(info, changed)
                val outputFile = File(webView.context.cacheDir, "$fileName.$FILE_EXTENSION_PDF")
                writeContentToOutputFile(printAdapter = printAdapter, file = outputFile)
            }
    
            override fun onLayoutFailed(error: CharSequence?) {
                super.onLayoutFailed(error)
                converterListener?.onFail()
            }
        }
    
    
        private fun writeContentToOutputFile(printAdapter: PrintDocumentAdapter, file: File) {
            try {
                val fileDescriptor = getOutputFile(file)
                val pageRangeOfAllPages = arrayOf(PageRange.ALL_PAGES)
                printAdapter.onWrite(pageRangeOfAllPages, fileDescriptor, null,
                    object : PrintDocumentAdapter.WriteResultCallback() {
                        override fun onWriteFinished(pages: Array<out PageRange>?) {
                            super.onWriteFinished(pages)
                            converterListener?.onSuccess(file)
                        }
    
                        override fun onWriteFailed(error: CharSequence?) {
                            super.onWriteFailed(error)
                            converterListener?.onFail()
                        }
                    })
            } catch (e: Exception) {
                e.printStackTrace()
                converterListener?.onFail(e)
            }
        }
    
        private fun getOutputFile(file: File): ParcelFileDescriptor? {
            val fileDirectory = file.parentFile
            if (fileDirectory != null && !fileDirectory.exists()) {
                fileDirectory.mkdirs()
            }
            return try {
                file.createNewFile()
                ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE)
            } catch (exception: IOException) {
                throw exception
            }
        }
    
        private fun getPrintAttributes(): PrintAttributes {
            return PrintAttributes.Builder()
                .setMediaSize(pdfMediaSize)
                .setResolution(
                    PrintAttributes.Resolution(
                        PRINT_ATTRIBUTE_RESOLUTION_ID,
                        PRINT_ADAPTER_DOCUMENT_LABEL,
                        PRINT_ADAPTER_DEFAULT_DPI,
                        PRINT_ADAPTER_DEFAULT_DPI
                    )
                )
                .setColorMode(pdfColorMode)
                .setMinMargins(PrintAttributes.Margins.NO_MARGINS)
                .build()
        }
    
    }