android pdf框架-15,mupdf工具与其它

阅读器完善的差不多.

发现mupdf还有一些工具,放到移动端中也是可以用的.只是没有mutool这么强.

今天主要涉及加密,解密,修改字体等功能

目录

加密与解密

加密处理

解密:

字体与样式修改

字体大小

样式

创建pdf

导出


加密与解密

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
    }

删除与添加后,文档会发生变化.要重新加载,否则刚删除一个,然后删除第二个页面的时候,页码会对不上.这是一个问题.

还有不少功能.主要用的是这些.

相关推荐
.豆鲨包5 小时前
【Android】MVP架构模式
android·架构
代码会说话5 小时前
i2c通讯
android·linux·嵌入式硬件·嵌入式
东风西巷6 小时前
MobiPDF安卓版(PDF阅读编辑工具) 修改版
学习·pdf·电脑·软件需求
qq_205279056 小时前
Unity 项目外部浏览并读取PDF文件在RawImage中显示,使用PDFRender插件
pdf
默|笙8 小时前
【c++】set和map的封装
android·数据库·c++
kaikaile19958 小时前
PHP计算过去一定时间段内日期范围函数
android·开发语言·php
2501_929382659 小时前
电视盒子助手开心电视助手 v8.0 删除电视内置软件 电视远程控制ADB去除电视广告
android·windows·adb·开源软件·电视盒子
太过平凡的小蚂蚁9 小时前
Kotlin 异步数据流三剑客:Flow、Channel、StateFlow 深度解析
android·kotlin
铭哥的编程日记10 小时前
【Linux】库制作与原理
android·linux·运维