Android将Html转成PDF文件的几种方式

927 阅读3分钟

生成pdf文件:

1.是本地的html,可以用webview先加载(目前来说必须要预览出来,不能遮挡webview,在获取bitmap的时候才没有问题),在获取webview的bitmap图片,在调用系统的api生成pdf文件

系统API生成pdf文件:
步骤一:在assets包中存放html文件:

image.png

步骤二:调用webview加载本地文件:
webview.loadUrl("file:///android_asset/temp.html")
步骤三:调用
//调用方法
fileName = "temp_${Date().time}.pdf"
PdfUtils.webView2SinglePdf(webview, fileName = fileName)

//**************PdfUtils工具类中方法**************

/**
* webView转pdf文件, 单页
*/
fun webView2SinglePdf(webView: WebView, filePath: String = pdfDefaultPath(webView.context), fileName: String = "temp.pdf") {
    webView.post {
        var pdfFile = File(filePath + File.separator + fileName)
        if (pdfFile.exists()){
            pdfFile.delete()
        }
        val parent = File(pdfFile.parent)
        if (!parent.exists()) {
            parent.mkdirs()
        }
        pdfFile.createNewFile()
        webView.measure(0, 0)
        val measuredHeight: Int = webView.measuredHeight
        val measuredWidth: Int = webView.measuredWidth
        if (measuredWidth <= 0 || measuredHeight <= 0) {
            return@post
        }
        // create a new document
        var document = PdfDocument()

        // crate a page description
        val builder = PdfDocument.PageInfo.Builder(measuredWidth , measuredHeight, 1)
        //设置边距
       //builder.setContentRect(Rect(60, 60, measuredWidth - 60, measuredHeight - 60))
        var pageInfo = builder.create();
        // start a page
        var page = document.startPage(pageInfo)
        // draw something on the page
        webView.capturePicture().draw(page.canvas)

        // finish the page
        document.finishPage(page)
        // write the document content
        val outputStream = FileOutputStream(pdfFile)
        document.writeTo(outputStream);

        //close the document
        document.close()
        outputStream.close()
    }
}

/**
 * pdf文件默认路径
 */
private fun pdfDefaultPath(context: Context): String {
    return context.applicationContext
        .getExternalFilesDir(null)?.absolutePath + File.separator+ "pdf"
}
使用dexmaker生成pdf

直接上代码: 调用方法:

fileName = "test_dex_maker_${Date().time}.pdf"
PdfUtils.webView2Pdf(fileName, webview, {
    Log.e("printPDFFile", "success")
}, {
    Log.e("printPDFFile", "failed")
})

PdfUtils相关方法

fun webView2Pdf(pdfFileName: String, webView: WebView, onSuccess: ()-> Unit, onFail: ()-> Unit) {
    /**
     * android 5.0之后,出于对动态注入字节码安全性德考虑,已经不允许随意指定字节码的保存路径了,需要放在应用自己的包名文件夹下。
     */
    //创建DexMaker缓存目录
    //File dexCacheFile = new File(dexCacheDirPath);
    //if (!dexCacheFile.exists()) {
    //file.mkdir();
    //}
    //新的创建DexMaker缓存目录的方式,直接通过context获取路径
    dexCacheFile = File(pdfDefaultPath(webView.context))
    Log.e("TAG", "printPDFFile: "+  dexCacheFile!!.absolutePath)
    try {
        //创建待写入的PDF文件,pdfFilePath为自行指定的PDF文件路径
        val pdfFile = File(dexCacheFile!!.absolutePath + File.separator+ pdfFileName)
        if (pdfFile.exists()) {
            pdfFile.delete()
        }
        val parent = File(pdfFile.parent)
        if (!parent.exists()) {
            parent.mkdirs()
        }
        pdfFile.createNewFile()
        descriptor =
            ParcelFileDescriptor.open(pdfFile, ParcelFileDescriptor.MODE_READ_WRITE)
        // 设置打印参数
        val attributes = PrintAttributes.Builder()
            .setMediaSize(PrintAttributes.MediaSize.ISO_A4)
            .setResolution(
                PrintAttributes.Resolution(
                    "id",
                    Context.PRINT_SERVICE,
                    300,
                    300
                )
            )
            .setColorMode(PrintAttributes.COLOR_MODE_COLOR)
            .setMinMargins(PrintAttributes.Margins.NO_MARGINS)
            .build()

        // 计算webview打印需要的页数
        val numberOfPages = webView.contentHeight * 240  / PrintAttributes.MediaSize.ISO_A4.heightMils + 1
        ranges = arrayOf(PageRange(0, numberOfPages))
        // 获取需要打印的webview适配器
        printAdapter = webView.createPrintDocumentAdapter(pdfFileName)
        // 开始打印
        printAdapter!!.onStart()
        printAdapter!!.onLayout(
            attributes, attributes, CancellationSignal(), getLayoutResultCallback(
                { proxy, method, args ->
                    if (method.name == "onLayoutFinished") {
                        // 监听到内部调用了onLayoutFinished()方法,即打印成功
                        onLayoutSuccess(onSuccess, onFail)
                    } else {
                        // 监听到打印失败或者取消了打印
                        onFail.invoke()
                    }
                    null
                }, dexCacheFile!!.absoluteFile
            ), Bundle()
        )
    } catch (e: IOException) {
        e.printStackTrace()
    }
}

@Throws(IOException::class)
private fun onLayoutSuccess(onSuccess: ()-> Unit, onFail: ()-> Unit) {
    val callback = getWriteResultCallback({ o, method, objects ->
        if (method.name == "onWriteFinished") {
            // PDF文件写入本地完成,导出成功
            onSuccess.invoke()
        } else {
            // 导出失败
            onFail.invoke()
        }
        null
    }, dexCacheFile!!.absoluteFile)
    printAdapter!!.onWrite(ranges, descriptor, CancellationSignal(), callback)
}

@Throws(IOException::class)
fun getLayoutResultCallback(
    invocationHandler: InvocationHandler?,
    dexCacheDir: File?
): PrintDocumentAdapter.LayoutResultCallback {
    return ProxyBuilder.forClass(PrintDocumentAdapter.LayoutResultCallback::class.java)
        .dexCache(dexCacheDir)
        .handler(invocationHandler)
        .build()
}

@Throws(IOException::class)
fun getWriteResultCallback(
    invocationHandler: InvocationHandler?,
    dexCacheDir: File?
): PrintDocumentAdapter.WriteResultCallback {
    return ProxyBuilder.forClass(PrintDocumentAdapter.WriteResultCallback::class.java)
        .dexCache(dexCacheDir)
        .handler(invocationHandler)
        .build()
}

2.使用三方库(iText,目前生产环境是需要收费的,测试环境免费)直接将html文件转成pdf,和方案一相比不需要webview预览,干净无痕,用户无感知(目前还没有找到免费的替换方案),

⚠️注意:这里不仅可以将html转成pdf文件,还能自己按照官网的Api生成一个pdf文件!!!

官网demo:github.com/PDFTron/pdf…

相关集成文档:docs.apryse.com/documentati… 本人在集成的时候用的是kotlin-stdlib是老版本1.5.1,但是这个用的是com.pdftron:tools的room-runtime用的kotlin-stdlib是高版本,所以这里降低了一下版本

implementation("com.pdftron:tools:10.9.0"){
    //room-runtime中依赖了高版本的kotlin-stdlib,导致编译时报错,这里排除掉
    exclude(group: 'androidx.room', module: 'room-runtime')
}

但是会影响后边的openDocument方法,所以纠结之下还是需要升级了kotlin版本:1.7.10,当然如果只是生成文件是不需要升级版本的,毕竟预览还有其他的方式,最后贴一下调用代码:

//**************调用方法
val html2PDF = HTML2PDF(this.context)
html2PDF.setOutputFolder(this.context.cacheDir)
html2PDF.setHTML2PDFListener(object : HTML2PDF.HTML2PDFListener {
    override fun onConversionFinished(pdfOutput: String, isLocal: Boolean) {
        // Handle callback when conversion finished

        openDocument(pdfOutput)
    }

    override fun onConversionFailed(error: String?) {
        // Handle callback if conversion failed
        Log.e("html2pdf", "$error")
    }
})
//assets目录下的html文件
html2PDF.fromUrl("file:///android_asset/temp.html")


//**************openDocument方法

fun openDocument(filepath: String) {
    val config = ViewerConfig.Builder()
        .build()

    val intent = DocumentActivity.IntentBuilder.fromActivityClass(
        this.context,
        DocumentActivity::class.java
    )
        .withUri(Uri.parse(filepath))
        .usingConfig(config)
        .usingNewUi(true)
        .build()
    startActivity(intent)
}

3.使用三方库aspose(这个库有Api请求限制,目前前150次请求是免费的),走的是网络生成会依赖okhttp3和retrofit,原理应该是先下载到本地,在上传到aspose后端,生成完成之后在下载下来,用户无感

集成和demo地址:products.aspose.cloud/html/androi…

参考demo集成的时候是需要加入:

implementation 'org.threeten:threetenbp:1.3.5'
implementation 'io.gsonfire:gson-fire:1.8.0'
//注意这里的okhttp3版本4.9.3和自己项目中的版本保持一致
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.3'
implementation 'com.squareup.retrofit2:retrofit:2.4.0'
implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
implementation 'com.squareup.retrofit2:converter-scalars:2.4.0'

使用

GlobalScope.launch(Dispatchers.IO) {
    Configuration.setBasePath("https://api.aspose.cloud")
    Configuration.setAuthPath("https://api.aspose.cloud/connect/token")
    Configuration.setUserAgent("WebKit")
    Configuration.setDebug(true)

    //这里需要替换成自己的sid和key
    val api =
        HtmlApi("XXXXX", "XXXX")


//        ApiClient apiClient = api.apiClient
    val inputUrl = "https://stallman.org/articles/anonymous-payments-thru-phones.html"
    fileName = "test_aspose_${Date().time}.pdf"
    val outputFile = PdfUtils.pdfDefaultPath(this@PrinterPage.context) + File.separator + fileName

    val f1 = File(outputFile)
    if (f1.exists()) f1.delete()

    val opt_A5 = PDFConversionOptions()
        //A5纸的宽高英寸值
        //.setWidth(5.8)
        //.setHeight(8.3)
        //A4纸的宽高英寸值
        .setWidth(8.3)
        .setHeight(11.6)
        .setTopMargin(0.5)
        .setBottomMargin(0.5)
        .setLeftMargin(0.5)
        .setRightMargin(0.5)
        .setQuality(95)

    val builder = ConverterBuilder()
        .fromUrl(inputUrl)
        //本地加载html
        //.fromStorageFile("")
        //.fromLocalFile(PdfUtils.pdfDefaultPath(this@PrinterPage.context) + //File.separator + "temp.html")
        .useOptions(opt_A5)
        .saveToLocal(outputFile)

    val result = api.convert(builder)

    val f = result.getFile()

    val dst = File(result.getFile())

    if (dst.exists()) {
        println("Result file is $dst")
    } else {
        println("Error conversion")
    }
}

总结:上边的几种方式,目前来熟没有找到免费的不需要预览就将html转换成Pdf文件的方式,个人觉得要是公司条件可以可以用itext方式生成,没有的话还是用webview的方式,如果小伙伴知道更好的方式欢迎留言!