生成pdf文件:
1.是本地的html,可以用webview先加载(目前来说必须要预览出来,不能遮挡webview,在获取bitmap的时候才没有问题),在获取webview的bitmap图片,在调用系统的api生成pdf文件
系统API生成pdf文件:
步骤一:在assets包中存放html文件:
步骤二:调用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的方式,如果小伙伴知道更好的方式欢迎留言!