android pdf框架-15,mupdf工具与其它
阅读器完善的差不多.
发现mupdf还有一些工具,放到移动端中也是可以用的.只是没有mutool这么强.
今天主要涉及加密,解密,修改字体等功能
目录
加密与解密
加密处理
解密:
字体与样式修改
字体大小
样式
创建pdf
导出
加密与解密
pdf加密与解密,jni已经完成了,但没有明确的使用说明.它像文本重排类似,通过一个字符串传入参数来处理.
加密处理
/*** 保存时添加密码保护*/fun encryptPDF(inputFile: String?, outputFile: String?,userPassword: String?, ownerPassword: String?): Boolean {try {val doc: Document? = Document.openDocument(inputFile)if (doc !is PDFDocument) {System.err.println("输入文件不是PDF格式")return false}if (doc.needsPassword()) {println("原文档需要密码验证")// 这里需要提供原文档的密码// pdfDoc.authenticatePassword("original_password");}val options = java.lang.StringBuilder()// 设置加密算法 (AES 256位是最安全的)options.append("encrypt=aes-256")// 设置用户密码(打开文档时需要)if (userPassword != null && !userPassword.isEmpty()) {options.append(",user-password=").append(userPassword)}// 设置所有者密码(拥有完整权限)if (ownerPassword != null && !ownerPassword.isEmpty()) {options.append(",owner-password=").append(ownerPassword)}// 设置权限(-1表示所有权限)options.append(",permissions=-1")println("保存选项: $options")// 保存加密后的PDFdoc.save(outputFile, options.toString())println("PDF加密成功,保存到: $outputFile")return true} catch (e: java.lang.Exception) {System.err.println("加密PDF时出错: " + e.message)}return false}
这里有两个密码,实际使用过程我用的是同一个密码.下面是调用示例.
val originalFile = java.io.File(bookPath)val encryptedFileName = originalFile.nameWithoutExtension + "_encrypted.pdf"val encryptedFilePath = java.io.File(originalFile.parentFile, encryptedFileName).absolutePathprogressDialog.show()lifecycleScope.launch {val result = withContext(Dispatchers.IO) {PDFCreaterHelper.encryptPDF(bookPath, encryptedFilePath, password, password)}if (result){val successMsg = getString(R.string.encrypt_decrypt_encrypt_success, encryptedFilePath)Toast.makeText(activity, successMsg, Toast.LENGTH_LONG).show()}else{Toast.makeText(activity, R.string.encrypt_decrypt_encrypt_failed, Toast.LENGTH_LONG).show()}progressDialog.dismiss()}
如果想处理的完美,还要对之前有没有密码作判断
解密:
/*** 移除PDF密码保护(解密)*/fun decryptPDF(inputFile: String?, outputFile: String?, password: String?): Boolean {try {val doc: Document? = Document.openDocument(inputFile)if (doc !is PDFDocument) {System.err.println("输入文件不是PDF格式")return false}// 验证密码(如果需要)if (doc.needsPassword()) {if (!doc.authenticatePassword(password)) {System.err.println("密码验证失败,无法解密")return false}println("密码验证成功")}// 构建保存选项字符串 - 移除加密val options = "encrypt=no,decrypt=yes"// 保存解密后的PDFdoc.save(outputFile, options)println("PDF解密成功,保存到: $outputFile")return true} catch (e: java.lang.Exception) {System.err.println("解密PDF时出错: " + e.message)}return false}
这些比较简单,调用mupdf提供的api就可以了.
字体与样式修改
字体修改主要是针对移动端epub/cbz/mobi/docx等这类的文档.个人主要是epub/mobi的书较多.手机默认的字体有点难看.所以可以替换自己的字体是比较必要的.中文我一般就用宋体,以前用雅黑,后来又觉得不好看了.
字体大小
在加载epub大概是这样的:
Document document = null;try {document = Document.openDocument(fname);int w = Utils.getScreenWidthPixelWithOrientation(App.Companion.getInstance());int h = Utils.getScreenHeightPixelWithOrientation(App.Companion.getInstance());float fontSize = getFontSize(fname);System.out.printf("w:%s, h:%s, font:%s, open:%s%n",w,h, fontSize, fname);document.layout(w, h, fontSize);epubDocument.setDocument(document);} catch (Exception e) {e.printStackTrace();return null;}
这里有两个要素,一个是字体大小,一个是布局的高宽.
由于我用的是手机,所以直接取手机的屏幕高宽,然后字体根据实际的情况作调整.
public static float getDefFontSize() {float fontSize = (8.4f * Utils.getDensityDpi(App.Companion.getInstance()) / 72);return fontSize;}public static float getFontSize(String name) {MMKV mmkv = MMKV.mmkvWithID("epub");var fs = mmkv.decodeFloat("font_" + name.hashCode(), getDefFontSize());if (fs > 90) {fs = 90f;}return fs;}
字体外部可以调整,如果没有设置过大小,就用默认的,是8.4*dpi/72,比如320的dpi这个值大概是36.450的dpi这个大概47号.
字体已经有了,另一个就是调整布局的css
样式
mupdf解析的时候,里面有它定义好的样式.它允许自定义样式,我不修改太多,只修改必要的就行了.
一个是字体的fontfamily,另一个就是margin.
String css = FontCSSGenerator.INSTANCE.generateFontCSS(getFontFace(), "10px");if (!TextUtils.isEmpty(css)) {System.out.println("应用自定义CSS: " + css);}Context.setUserCSS(css);
这样就通过自定义css完成修改.css里面包含了字体路径.
我把字体放到/sdcard/fonts中,目前mupdf支持otf与ttf两种.ttc是ttf的集合,要用工具导出来.
fun generateFontCSS(fontPath: String?, margin: String): String {val buffer = StringBuilder()if (!fontPath.isNullOrEmpty()) {val fontFile = File(fontPath)if (fontFile.exists()) {val fontName = getFontNameFromPath(fontPath)buffer.apply {appendLine("@font-face {")appendLine(" font-family: '$fontName' !important;")appendLine(" src: url('file://$fontPath');")appendLine("}")appendLine("* {")appendLine(" font-family: '$fontName', serif !important;")appendLine("}")}}}buffer.apply {// 忽略mupdf的边距appendLine(" @page { margin:$margin $margin !important; }")appendLine(" p { margin: 20px !important; padding: 0 !important; }")appendLine(" blockquote { margin: 0 !important; padding: 0 !important; }")// 强制所有元素的边距和内边距appendLine("* {")appendLine(" margin: 0 !important;")appendLine(" padding: 0 !important;")appendLine("}")}return buffer.toString()}private fun getFontNameFromPath(fontPath: String): String {val fileName = File(fontPath).nameval dotIndex = fileName.lastIndexOf('.')return if (dotIndex > 0) fileName.take(dotIndex) else fileName}
第一部分是设置字体的family,这样自定义的字体路径加载进来.
第二部分是margin,这样设置可以适配绝大多数了.
这还不够,因为默认它的字体加载没办法加载sdcard中的.
libmupdf/platform/java/jni/android/androidfonts.c 这个还要修改
加载字体的方法添加sdcard
static fz_font *load_noto(fz_context *ctx, const char *a, const char *b, const char *c, int idx)
{char buf[500];fz_font *font = NULL;fz_try(ctx){fz_snprintf(buf, sizeof buf, "/system/fonts/%s%s%s.ttf", a, b, c);if (!fz_file_exists(ctx, buf))fz_snprintf(buf, sizeof buf, "/sdcard/fonts/%s%s%s.ttf", a, b, c);if (!fz_file_exists(ctx, buf))fz_snprintf(buf, sizeof buf, "/system/fonts/%s%s%s.otf", a, b, c);if (!fz_file_exists(ctx, buf))fz_snprintf(buf, sizeof buf, "/system/fonts/%s%s%s.ttc", a, b, c);if (fz_file_exists(ctx, buf))font = fz_new_font_from_file(ctx, NULL, buf, idx, 0);}fz_catch(ctx)return NULL;return font;
}
然后覆盖load_droid_font,这是在context.c里面注册的.
fz_font *load_droid_font(fz_context *ctx, const char *name, int bold, int italic, int needs_exact_metrics)
{char buf[500];fz_font *font = NULL;if (!name){LOGE("load_droid_font: name is NULL");return NULL;}//LOGE("load_droid_font: loading font '%s' (bold=%d, italic=%d)", name, bold, italic);fz_try(ctx){const char *style = "";if (bold && italic)style = "-BoldItalic";else if (bold)style = "-Bold";else if (italic)style = "-Italic";fz_snprintf(buf, sizeof buf, "/sdcard/fonts/%s%s.ttf", name, style);//LOGE("Trying font path: %s", buf);if (fz_file_exists(ctx, buf)){font = fz_new_font_from_file(ctx, NULL, buf, 0, 0);//if (font)// LOGE("Successfully loaded font: %s", buf);//else// LOGE("Failed to load font (fz_new_font_from_file failed): %s", buf);}if (!font){fz_snprintf(buf, sizeof buf, "/sdcard/fonts/%s.ttf", name);//LOGE("Trying font path: %s", buf);if (fz_file_exists(ctx, buf)){font = fz_new_font_from_file(ctx, NULL, buf, 0, 0);//if (font)// LOGE("Successfully loaded font: %s", buf);//else// LOGE("Failed to load font (fz_new_font_from_file failed): %s", buf);}}if (!font){fz_snprintf(buf, sizeof buf, "/sdcard/fonts/%s%s.otf", name, style);//LOGE("Trying font path: %s", buf);if (fz_file_exists(ctx, buf)){font = fz_new_font_from_file(ctx, NULL, buf, 0, 0);//if (font)// LOGE("Successfully loaded font: %s", buf);//else// LOGE("Failed to load font (fz_new_font_from_file failed): %s", buf);}}if (!font){fz_snprintf(buf, sizeof buf, "/sdcard/fonts/%s.otf", name);//LOGE("Trying font path: %s", buf);if (fz_file_exists(ctx, buf)){font = fz_new_font_from_file(ctx, NULL, buf, 0, 0);//if (font)// LOGE("Successfully loaded font: %s", buf);//else// LOGE("Failed to load font (fz_new_font_from_file failed): %s", buf);}}}fz_catch(ctx){LOGE("load_droid_font: exception occurred while loading font '%s'", name);return NULL;}//if (!font)//LOGE("load_droid_font: failed to load font '%s' from /sdcard/fonts", name);return font;
}
第一个方法要不要改,忘了,好像不用.主要是这里的load_droid_font方法,改变了加载字体的位置.
然后就是ui中添加字体.需要字体的全路径名.比如/sdcard/fonts/simsun.ttf
创建pdf
创建也提供了api,是pdfdocument
fun createPdfFromImages(pdfPath: String?, imagePaths: List<String>): Boolean {Log.d("TAG", String.format("imagePaths:%s", imagePaths))var mDocument: PDFDocument? = nulltry {mDocument = PDFDocument.openDocument(pdfPath) as PDFDocument} catch (e: Exception) {Log.d("TAG", "could not open:$pdfPath")}if (mDocument == null) {mDocument = PDFDocument()}val resultPaths = processLargeImage(imagePaths)//空白页面必须是-1,否则会崩溃,但插入-1的位置的页面会成为最后一个,所以追加的时候就全部用-1就行了.var index = -1for (path in resultPaths) {val page = addPage(path, mDocument, index++)mDocument.insertPage(-1, page)}mDocument.save(pdfPath, OPTS)Log.d("TAG", String.format("save,%s,%s", mDocument.toString(), mDocument.countPages()))val cacheDir = FileUtils.getExternalCacheDir(App.instance).path + File.separator + "create"val dir = File(cacheDir)if (dir.isDirectory) {dir.deleteRecursively()}return mDocument.countPages() > 0}
const val OPTS = "compress-images;compress;incremental;linearize;pretty;compress-fonts;garbage"
创建的过程就是,创建一个PDFDocument,然后,创建page,再insertPage添加页面.最后save
可以添加图片的页面,也可以添加文本的页面.下面是添加图片的
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.widthval h = image.heightval 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,w:%s,h:%s", index, contents, w, h))return page}
来一个文本创建文档:
fun createPdfFromText(sourcePath: String, destPath: String): Boolean {val text = EncodingDetect.readFile(sourcePath)val mediabox = Rect(0f, 0f, 420f, 594f) //A2val margin = 10fval writer = DocumentWriter(destPath, "PDF", OPTS)val snark = "<!DOCTYPE html>" +"<style>" +"#body { font-family: \"Noto Sans\", sans-serif; }" +"</style>" +"<body>" +text +"</body></html>"val story = Story(snark, "", 12f)var more: Booleando {val filled = Rect()val where = Rect(mediabox.x0 + margin,mediabox.y0 + margin,mediabox.x1 - margin,mediabox.y1 - margin)val dev: Device = writer.beginPage(mediabox)more = story.place(where, filled)story.draw(dev, Matrix.Identity())writer.endPage()} while (more)writer.close()writer.destroy()story.destroy()return true}
文本创建有一个缺点,就是字体,如果设置了外部字体,则文档非常大.如果不设置,中文显示太糟糕了.
导出
导出图片是常见的操作.还可以导出html,如果有图片是base64编码的
fun extractToImages(context: Context, screenWidth: Int, dir: String, pdfPath: String,start: Int,end: Int): Int {try {Log.d("TAG","extractToImages:$screenWidth, start:$start, end:$end dir:$dir, dst:$pdfPath")val mupdfDocument = MupdfDocument(context)mupdfDocument.newDocument(pdfPath, null)val count: Int = mupdfDocument.countPages()var startPage = startif (startPage < 0) {startPage = 0} else if (startPage >= count) {startPage = 0}var endPage = endif (end > count) {endPage = count} else if (endPage < 0) {endPage = count}for (i in startPage until endPage) {if (!canExtract) {Log.d("TAG", "extractToImages.stop")return i}val page = mupdfDocument.loadPage(i)if (null != page) {val pageWidth = page.bounds.x1 - page.bounds.x0val pageHeight = page.bounds.y1 - page.bounds.y0var exportWidth = screenWidthif (exportWidth == -1) {exportWidth = pageWidth.toInt()}val scale = exportWidth / pageWidthval width = exportWidthval height = pageHeight * scaleval bitmap = BitmapPool.getInstance().acquire(width, height.toInt())val ctm = Matrix(scale)MupdfDocument.render(page, ctm, bitmap, 0, 0, 0)page.destroy()BitmapUtils.saveBitmapToFile(bitmap, File("$dir/${i + 1}.jpg"))BitmapPool.getInstance().release(bitmap)}Log.d("TAG", "extractToImages:page:${i + 1}.jpg")}} catch (e: Exception) {e.printStackTrace()return -2}return 0}
这是一个导出的示例.至于导出的图片分辨率可以自己设置了.
导出html的话,因为我修改了导出的方法:
val content =
String(page.textAsHtml2("preserve-whitespace,inhibit-spaces,preserve-images"))
stringBuilder.append(content)
mupdf默认的导出html方法我不想要.它是通过参数来处理的,preserve-images是要处理图片的,否则图片会被忽略.
删除页面
fun deletePage(page: Int): Boolean {if (null != mupdfDocument && aPageList.size > page) {isEdit = trueval pdfDocument = mupdfDocument!!.getDocument() as PDFDocumentpdfDocument.deletePage(page)aPageList.removeAt(page)Log.d("TAG","deletePage.$page, cp:${pdfDocument.countPages()}, size:${aPageList.size}")save()return true}return false}
删除与添加后,文档会发生变化.要重新加载,否则刚删除一个,然后删除第二个页面的时候,页码会对不上.这是一个问题.
还有不少功能.主要用的是这些.