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
    }

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

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

相关推荐
张拭心30 分钟前
春节后,有些公司明确要求 AI 经验了
android·前端·人工智能
张拭心40 分钟前
Android 17 来了!新特性介绍与适配建议
android·前端
Kapaseker3 小时前
Compose 进阶—巧用 GraphicsLayer
android·kotlin
黄林晴3 小时前
Android17 为什么重写 MessageQueue
android
阿巴斯甜1 天前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker1 天前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95271 天前
Andorid Google 登录接入文档
android
黄林晴1 天前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab2 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿2 天前
Android MediaPlayer 笔记
android