阅读器完善的差不多.
发现mupdf还有一些工具,放到移动端中也是可以用的.只是没有mutool这么强.
今天主要涉及加密,解密,修改字体等功能
目录
加密与解密
pdf加密与解密,jni已经完成了,但没有明确的使用说明.它像文本重排类似,通过一个字符串传入参数来处理.
加密处理
Kotlin
/**
* 保存时添加密码保护
*/
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")
// 保存加密后的PDF
doc.save(outputFile, options.toString())
println("PDF加密成功,保存到: $outputFile")
return true
} catch (e: java.lang.Exception) {
System.err.println("加密PDF时出错: " + e.message)
}
return false
}
这里有两个密码,实际使用过程我用的是同一个密码.下面是调用示例.
Kotlin
val originalFile = java.io.File(bookPath)
val encryptedFileName = originalFile.nameWithoutExtension + "_encrypted.pdf"
val encryptedFilePath = java.io.File(originalFile.parentFile, encryptedFileName).absolutePath
progressDialog.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()
}
如果想处理的完美,还要对之前有没有密码作判断
解密:
Kotlin
/**
* 移除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"
// 保存解密后的PDF
doc.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大概是这样的:
Kotlin
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;
}
这里有两个要素,一个是字体大小,一个是布局的高宽.
由于我用的是手机,所以直接取手机的屏幕高宽,然后字体根据实际的情况作调整.
Kotlin
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.
Kotlin
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的集合,要用工具导出来.
Kotlin
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).name
val dotIndex = fileName.lastIndexOf('.')
return if (dotIndex > 0) fileName.take(dotIndex) else fileName
}
第一部分是设置字体的family,这样自定义的字体路径加载进来.
第二部分是margin,这样设置可以适配绝大多数了.
这还不够,因为默认它的字体加载没办法加载sdcard中的.
libmupdf/platform/java/jni/android/androidfonts.c 这个还要修改
加载字体的方法添加sdcard
cpp
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里面注册的.
cpp
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
Kotlin
fun createPdfFromImages(pdfPath: String?, imagePaths: List<String>): Boolean {
Log.d("TAG", String.format("imagePaths:%s", imagePaths))
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()
}
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)
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
可以添加图片的页面,也可以添加文本的页面.下面是添加图片的
Kotlin
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,w:%s,h:%s", index, contents, w, h))
return page
}
来一个文本创建文档:
Kotlin
fun createPdfFromText(sourcePath: String, destPath: String): Boolean {
val text = EncodingDetect.readFile(sourcePath)
val mediabox = Rect(0f, 0f, 420f, 594f) //A2
val margin = 10f
val 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: Boolean
do {
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编码的
Kotlin
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 = start
if (startPage < 0) {
startPage = 0
} else if (startPage >= count) {
startPage = 0
}
var endPage = end
if (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.x0
val pageHeight = page.bounds.y1 - page.bounds.y0
var exportWidth = screenWidth
if (exportWidth == -1) {
exportWidth = pageWidth.toInt()
}
val scale = exportWidth / pageWidth
val width = exportWidth
val height = pageHeight * scale
val 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的话,因为我修改了导出的方法:
Kotlin
val content =
String(page.textAsHtml2("preserve-whitespace,inhibit-spaces,preserve-images"))
stringBuilder.append(content)
mupdf默认的导出html方法我不想要.它是通过参数来处理的,preserve-images是要处理图片的,否则图片会被忽略.
删除页面
Kotlin
fun deletePage(page: Int): Boolean {
if (null != mupdfDocument && aPageList.size > page) {
isEdit = true
val pdfDocument = mupdfDocument!!.getDocument() as PDFDocument
pdfDocument.deletePage(page)
aPageList.removeAt(page)
Log.d(
"TAG",
"deletePage.$page, cp:${pdfDocument.countPages()}, size:${aPageList.size}"
)
save()
return true
}
return false
}
删除与添加后,文档会发生变化.要重新加载,否则刚删除一个,然后删除第二个页面的时候,页码会对不上.这是一个问题.
还有不少功能.主要用的是这些.