前面的文章都是如何展示pdf,这篇关于如何生成pdf文件.
使用图片生成pdf
原来我以为是高宽的问题,所以作了裁剪,后来发现是png的问题,一张png大小1000*1000左右,500k左右,生成的pdf达到5m,png转为jpg后会缩小到400k的图片,但生成的pdf会大大减小到600k左右.
在不使用分割法去加载页面,使用长图片生成的pdf如果不切割,那么读取的时候,解码可能内存溢出.这是使用recyclerview时发生的问题.所以我在代码里面作了切割.超过6000的高就切割分断,当然这样的pdf在其它的app看体验不太好.我把分割线去了,看着就像连续的图片.
使用vudroid查看器,因为它是根据页面分割法去加载,所以没有内存溢出的状况.但是现在自动切边还没有实现.使用recyclerview加载一整张图片,很容易实现自动切边.
使用mupdf生成
对于图片,生成pdf时,如果没有考虑内存的问题就比较容易了,如果考虑到内存可能会溢出,那么只能对图片切割处理,然后再生成页面.
切割后可以尽量以大的区域去展示pdf.
定义一个方法
fun createPdfFromImages(pdfPath: String?, imagePaths: List<String>): Boolean
pdf存储的路径,与图片的路径,要支持多张图片生成pdf
假如生成的页面高宽值是8.3 * 72 * 2与11.7 * 72 * 2,就是8*11英寸的页面.好像是a4
那么生成页面时要处理图片大于这个高宽的情况.
以目前手机普遍在1080*1920的分辨率之上,所以我定义了最大的高为2160.当图片的高大于这个高度,比如微博里面有好多图片是长图,那么就要进行切割.
var mDocument: PDFDocument? = null
try {
mDocument = PDFDocument.openDocument(pdfPath) as PDFDocument
} catch (e: Exception) {
Log.d("TAG", "could not open:$pdfPath")
}
if (mDocument == null) {
mDocument = PDFDocument()
}
pdf的路径可以是新的,可以是旧的,所以先读取,如果没有读取到,可能是新的地址.如果是旧的地址,图片生成新的部分是追加到旧的后面的.
val resultPaths = processLargeImage(imagePaths)
//空白页面必须是-1,否则会崩溃,但插入-1的位置的页面会成为最后一个,所以追加的时候就全部用-1就行了.
var index = -1
for (path in resultPaths) {
val page = addPage(path, mDocument, index++)
mDocument.insertPage(-1, page)
}
mDocument.save(pdfPath, OPTS);
private const val OPTS = "compress-images;compress;incremental;linearize;pretty;compress-fonts"
大体的流程就是这样的.剩下的就是处理图片切割的方式了.
图片切割
private fun processLargeImage(imagePaths: List<String>): List<String> {
val options = BitmapFactory.Options()
//默认值为false,如果设置成true,那么在解码的时候就不会返回bitmap,即bitmap = null。
options.inJustDecodeBounds = true
val maxHeight = PAPER_HEIGHT
val result = arrayListOf<String>()
for (path in imagePaths) {
try {
BitmapFactory.decodeFile(path, options)
if (options.outHeight > maxHeight) {
//split image,maxheight=PAPER_HEIGHT
splitImages(result, path, options.outWidth, options.outHeight)
} else {
//result.add(path)
val bitmapPath = compressImageFitPage(path, options.outWidth, options.outHeight)
result.add(bitmapPath)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
return result
}
这里处理了两种,一种是大于高的图片,肯定切割,另一种是不大于高,但图片可能比较大,比如正方形的1400*1400的这种,如果直接使用它的高宽,生成的pdf会比较大,所以进行了屏幕如1080宽的适配操作.生成的pdf就不那么大了.
splitImages切割就是从图片顶点开始,按高度切割,然后保存在临时目录里面.
var top = 0f
val right = 0 + width
var bottom = PAPER_HEIGHT
while (bottom < height) {
val rect = android.graphics.Rect()
rect.set(0, top.toInt(), right, bottom.toInt())
splitImage(path, rect, result)
top = bottom
bottom += PAPER_HEIGHT
}
if (top < height) {
val rect = android.graphics.Rect()
rect.set(0, top.toInt(), right, height)
splitImage(path, rect, result)
}
切割图片后,与不切割的图片一样,做一次宽度适配
val mDecoder = BitmapRegionDecoder.newInstance(path, true)
val bm: Bitmap = mDecoder.decodeRegion(rect, null)
val file =
File(
PDFUtils.getExternalCacheDir(App.instance).path
//FileUtils.getStorageDirPath() + "/amupdf"
+ File.separator + "create" + File.separator + System.currentTimeMillis() + ".jpg"
)
PDFUtils.saveBitmapToFile(bm, file, Bitmap.CompressFormat.JPEG, 100)
Log.d("TAG", "new file:height:${rect.bottom - rect.top}, path:${file.absolutePath}")
//result.add(file.absolutePath)
//splitPaths.add(file)
val bitmapPath = compressImageFitPage(file.absolutePath, bm.width, bm.height)
result.add(bitmapPath)
这是宽度处理
private fun compressImageFitPage(
path: String,
width: Int,
height: Int,
): String {
val options = BitmapFactory.Options()
options.inJustDecodeBounds = false
options.outWidth = PDF_PAGE_WIDTH.toInt()
options.outHeight = (height * PDF_PAGE_WIDTH / width).toInt()
val bitmap = BitmapFactory.decodeFile(path, options)
val file =
File(
PDFUtils.getExternalCacheDir(App.instance).path
+ File.separator + "create" + File.separator + System.currentTimeMillis() + ".jpg"
)
PDFUtils.saveBitmapToFile(bitmap, file, Bitmap.CompressFormat.JPEG, 100)
Log.d(
"TAG",
"bitmap.width:$width, height:$height,:${options.outWidth},${options.outHeight}, path:${file.absolutePath}"
)
return file.absolutePath
}
到这,切割就完成了,图片的高宽处理也完成了.剩下的就是将这些图片添加到页面上.
添加页面addPage
private fun addPage(
path: String,
mDocument: PDFDocument,
index: Int
): PDFObject? {
val image = Image(path)
val resources = mDocument.newDictionary()
val xobj = mDocument.newDictionary()
val obj = mDocument.addImage(image)
xobj.put("I", obj)
resources.put("XObject", xobj)
val w = image.width
val h = image.height
val mediabox = Rect(0f, 0f, w.toFloat(), h.toFloat())
val contents = "q $w 0 0 $h 0 0 cm /I Do Q\n"
val page = mDocument.addPage(mediabox, 0, resources, contents)
Log.d("TAG", String.format("index:%s,page,%s", index, contents))
return page
}
mupdf的语法都是类似的,js,c也都是这个规则.创建image对象.
contents要注意的点,这里设置了高宽,如果要添加内容,就要额外的操作了.否则这段应该是固定的.
在添加页面时,如果是-1,表示追加,追加时如果是空的文档自然就放到第一页了.
系统sdk也提供了相应的方法,和文本生成类似.原理就是,通过canvas,把view画出来.但这个方法有一个问题,对于大量的图片,会内存溢出,目前没有解决方案.
private fun createImagePage(
context: Context?,
parent: ViewGroup?,
pdfDocument: PdfDocument,
pageWidth: Int,
pageNo: Int,
path: String
) {
val contentView =
LayoutInflater.from(context)
.inflate(R.layout.pdf_image_content, parent, false) as ImageView
val bitmap = BitmapFactory.decodeFile(path)
contentView.setImageBitmap(bitmap)
val pageHeight = bitmap.height * pageWidth / bitmap.width
val pageInfo: PdfDocument.PageInfo = PdfDocument.PageInfo
.Builder(pageWidth, pageHeight, pageNo)
.create()
val page: PdfDocument.Page = pdfDocument.startPage(pageInfo)
val canvas: Canvas = page.getCanvas()
canvas.setLayerType(View.LAYER_TYPE_HARDWARE, null)
val measureWidth = View.MeasureSpec.makeMeasureSpec(pageWidth, View.MeasureSpec.EXACTLY)
val measuredHeight = View.MeasureSpec.makeMeasureSpec(pageHeight, View.MeasureSpec.EXACTLY)
contentView.measure(measureWidth, measuredHeight)
contentView.layout(0, 0, pageWidth, pageHeight)
contentView.draw(canvas)
Log.d(
"TAG",
String.format(
"createImagePage:%s-%s",
pageWidth,
pageHeight,
)
)
BitmapPool.getInstance().release(bitmap)
// finish the page
pdfDocument.finishPage(page)
}
布局就是一个imageview.
在保存时,添加一些参数,document.writeTo(outputStream, PdfDocument.COMPRESSION_MODE_MEDIUM)能压缩一些.
相比mupdf的生成方式比,这个体积要小一些.