android pdf框架-14,mupdf重排 - 详解

news/2025/9/27 8:44:29/文章来源:https://www.cnblogs.com/ljbguanli/p/19114721

android pdf框架-14,mupdf重排 - 详解

前面的文章主要在应用端.本文主要是针对文本重排,从mupdf的导出文本到应用端对这个文本再重排.尽量保持原文的格式方面作些说明.

文本重排,对于扫描版,目前不考虑,因为图片的ocr准确度不好,而且要ocr,当前的主流机型一页消耗时间太长.所以只考虑非扫描版,可以用mupdf直接导出文本的.

文本重排有两个阶段

一个是mupdf的导出.一个是针对导出的再次重排.

pdfium也有导出文本,我看了现有的库提供的接口就是直接导出文本,没有任何样式.图片也忽略.

mupdf如果是导出文本,有几类可以选择的.

导出文本

libmupdf/source/fitz/stext-output.c这是导出的核心类

enum {
FZ_FORMAT_TEXT,
FZ_FORMAT_HTML,
FZ_FORMAT_XHTML,
FZ_FORMAT_STEXT_XML,
FZ_FORMAT_STEXT_JSON,
};

它支持这些导出格式,text与json是差不多的,样式就没有了.

剩下的是一类,有样式的.

我的阅读器,目前用的是text的导出,但作了修改,增加了image的部分.所以重排的时候,元素都展现了,但样式就没办法恢复.

导出text后,再对text作一些行合并,现在的规则,合并上大概50%成功率,不算高.

具体是:libmupdf/platform/java/jni/page.c

这个类,添加一个导出的方法

JNIEXPORT jbyteArray JNICALL
FUN(Page_textAsText)(JNIEnv *env, jobject self, jstring joptions)
{
fz_context *ctx = get_context(env);
fz_page *page = from_Page(env, self);
fz_stext_page *text = NULL;
fz_device *dev = NULL;
fz_matrix ctm;
jbyteArray arr = NULL;
fz_buffer *buf = NULL;
fz_output *out = NULL;
unsigned char *data;
size_t len;
const char *options= NULL;
fz_stext_options opts;
if (!ctx || !page) return NULL;
if (joptions)
{
options = (*env)->GetStringUTFChars(env, joptions, NULL);
if (!options) return NULL;
}
fz_try(ctx)
{
fz_parse_stext_options(ctx, &opts, options);
}
fz_catch(ctx)
{
}
fz_var(text);
fz_var(dev);
fz_var(buf);
fz_var(out);
fz_try(ctx)
{
ctm = fz_identity;
text = fz_new_stext_page(ctx, fz_bound_page(ctx, page));
dev = fz_new_stext_device(ctx, text, &opts);
fz_run_page(ctx, page, dev, ctm, NULL);
fz_close_device(ctx, dev);
buf = fz_new_buffer(ctx, 256);
out = fz_new_output_with_buffer(ctx, buf);
fz_print_stext_page_as_text(ctx, out, text);
fz_close_output(ctx, out);
len = fz_buffer_storage(ctx, buf, &data);
arr = (*env)->NewByteArray(env, (jsize)len);
if ((*env)->ExceptionCheck(env))
fz_throw_java(ctx, env);
if (!arr)
fz_throw(ctx, FZ_ERROR_GENERIC, "cannot create byte array");
(*env)->SetByteArrayRegion(env, arr, 0, (jsize)len, (jbyte *)data);
if ((*env)->ExceptionCheck(env))
fz_throw_java(ctx, env);
}
fz_always(ctx)
{
if (options)
(*env)->ReleaseStringUTFChars(env, joptions, options);
fz_drop_output(ctx, out);
fz_drop_buffer(ctx, buf);
fz_drop_device(ctx, dev);
fz_drop_stext_page(ctx, text);
}
fz_catch(ctx)
jni_rethrow(env, ctx);
return arr;
}

从Page_textAsHtml复制来的,去除了html头尾.

fz_parse_stext_options 这个参数比较重要.它决定了导出的时候有没有带图片等.

内容导出是fz_print_stext_page_as_text,它加到上面的stext-output.c.最后是do_as_text这个导出文本,

在它的switch里面加上图片的导出

switch (block->type)
{
case FZ_STEXT_BLOCK_IMAGE:
fz_print_stext_image_as_html(ctx, out, block);
break;
case FZ_STEXT_BLOCK_TEXT:
for (line = block->u.t.first_line; line; line = line->next)
{
int break_line = 1;
for (ch = line->first_char; ch; ch = ch->next)
{
if (ch->next == NULL && (line->flags & FZ_STEXT_LINE_FLAGS_JOINED) != 0)
{
break_line = 0;
continue;
}
n = fz_runetochar(utf, ch->c);
for (i = 0; i u.s.down != NULL)
do_as_text(ctx, out, block->u.s.down->first_block);
break;
}

fz_print_stext_image_as_html这些功能都是现有的.我把最后一句修改了fz_write_string(ctx, out, "\"></p>\n");这样可以<p><img /></p>这样的标签,直接得到图片,是一个base64的,可以解析它.

作了这些修改后,一个带图片的文本就出来了,但是样式没有了.

合并行

pdf的页面渲染时,与重排后的显示,它的宽可能不一样,所以要合并行,这时要转到android显示上的处理了

定义一个TxtParser来解析文本,合并行.解析图片等操作.

定义数据来存储这两类

data class ReflowBean(
var data: String?,
var type: Int = TYPE_STRING,
var page: String? = null
) {
override fun toString(): String {
return "ReflowBean(page=$page, data=$data)"
}
companion object {
@JvmField
public val TYPE_STRING = 0;
@JvmField
public val TYPE_IMAGE = 1;
}
}

一个page产生的所有行,先去除空格,形成一个行的列表.然后针对每一行作出处理.

fun parseAsList(content: String, pageIndex: Int): List {
//Logcat.d("parse:==>" + content)
val sb = StringBuilder()
val list = ArrayList()
var aChar: Char
val rs = content.replace(SINGLE_WORD_FIX_REGEX, "")
for (i in 0 until rs.length) {
aChar = rs[i]
if (aChar == '\n') {
list.add(sb.toString())
sb.setLength(0)
} else {
sb.append(aChar)
}
}
//Logcat.d("result=>>" + result)
return parseList(list, pageIndex)
}

这部分纯体力活,判断是不是图片,图片的话单独处理.有点像解析xml.

private fun parseList(lists: List, pageIndex: Int): List {
val sb = StringBuilder()
var isImage = false
val reflowBeans = ArrayList()
var reflowBean: ReflowBean? = null
var maxNumberCharOfLine = 20
var hasImage = false
for (s in lists) {  //图片第一行会有很多字符
if (s.startsWith(IMAGE_START_MARK)) {
hasImage = true
}
if (s.length > maxNumberCharOfLine && !hasImage) {
maxNumberCharOfLine = s.length
}
}
var lastLine: Line? = null
for (s in lists) {
val ss = s.trim()
if (!TextUtils.isEmpty(ss)) {
//if (Logcat.loggable) {
//    Logcat.longLog("text", ss)
//}
if (ss.startsWith(IMAGE_START_MARK)) {
isImage = true
sb.setLength(0)
reflowBean = ReflowBean(null, ReflowBean.TYPE_STRING, pageIndex.toString())
reflowBean.type = ReflowBean.TYPE_IMAGE
reflowBeans.add(reflowBean)
}
if (!isImage) {
if (null == reflowBean) {
reflowBean =
ReflowBean(null, ReflowBean.TYPE_STRING, pageIndex.toString())
reflowBeans.add(reflowBean)
}
lastLine = parseLine(ss, sb, pageIndex, lastLine, maxNumberCharOfLine - 5)
reflowBean.data = sb.toString()
} else {
sb.append(ss)
}
if (ss.endsWith(IMAGE_END_MARK)) {
isImage = false
reflowBean?.data = sb.toString()
reflowBean = null
sb.setLength(0)
}
}
}
return reflowBeans
}

关键在于如何处理一行的数据

/**
* 重排的数据是按行获取的,只有纯文本,要把行合并起来.合并需要区分是否这一行就是结束.
* 如果这行是开始标志
*     则判断上一行是否有结束.没有则添加结束标志.
*     追加本行
* 如果这行有结束标志
*     上行没有结束符
*         行字数小于标准字数
*             加结束符
*     追加本行内容,加结束符
* 如果这行没有结束标志
*     上行有结束符
*         追加本行内容
*     上行没有结束符
*         上行小于标准字数
*             本行字数小于标准字数
*                 上行添加结束符
*                 追加本行内容,加结束符
*             本行字数大于标准字数
*                 上行添加结束符
*                 追加本行内容
*         上行大于标准字数
*             本行字数小于标准字数
*                 追加本行内容,加结束符
*             本行字数大于标准字数
*                 追加本行内容
* @param ss source
* @param sb parsed string
* @param pageIndex
* @param lastBreak wethere last line has a break char.
*/
private fun parseLine(
ss: String,
sb: StringBuilder,
pageIndex: Int,
lastLine: Line?,
maxNumberCharOfLine: Int
): Line {
val line = StringBuilder()
val thisLine = Line(ss.length  6) {
lineLength = 6
}
val start = ss.substring(0, lineLength)
var isStartLine = START_MARK.matcher(start).find()
//Logcat.d("find:$find")
if (!isStartLine) {
if (ss.startsWith("“|\"|'")) {
isStartLine = true
}
}
if (!isStartLine) {
if (START_MARK2.matcher(start).find()) {
isStartLine = true
}
}
if (isStartLine) {
Logcat.d("step3.line break,length:${ss.length}")
//如果是开始行,上行如果没有结束符,则添加上.
lastLine?.run {
if (!this.isEnd) {
line.append(LINE_END)
}
}
line.append(ss)
if (isEnd) {
line.append(LINE_END)
}
thisLine.isEnd = isEnd
thisLine.text = line.toString()
sb.append(line)
if (Logcat.loggable) {
Logcat.d("count:${maxNumberCharOfLine} :$line")
}
return thisLine
}
//4.如果这行有结束标志
if (isEnd) {
lastLine?.run {
//上行没有结束符,行字数小于标准字数,加结束符
if (!this.isEnd && lastLine.isNotALine) {
line.append(LINE_END)
}
}
line.append(ss)
line.append(LINE_END)
thisLine.isEnd = true
thisLine.text = line.toString()
sb.append(line)
if (Logcat.loggable) {
Logcat.d("count1:${maxNumberCharOfLine} :$line")
}
return thisLine
} else {
//5.如果这行没有结束标志
val lastLineIsEnd = (lastLine == null || lastLine.isEnd)
//上行有结束符
if (lastLineIsEnd) {
line.append(ss)
thisLine.isEnd = false
} else { //上行没有结束符
if (lastLine.isNotALine) { //上行小于标准字数
if (ss.length < maxNumberCharOfLine) {//本行字数小于标准字数
line.append(LINE_END)
line.append(ss)
line.append(LINE_END)
thisLine.isEnd = true
} else {  //本行字数大于标准字数
line.append(LINE_END)
line.append(ss)
}
} else {    //上行大于标准字数
if (ss.length < maxNumberCharOfLine) {//本行字数小于标准字数
//追加本行内容,加结束符
line.append(ss)
line.append(LINE_END)
thisLine.isEnd = true
} else {  //本行字数大于标准字数
line.append(ss)
}
}
}
}
if (isLetterDigitOrChinese(end)) {
Logcat.d("isLetterDigitOrChinese:$end")
line.append(LINE_END)
}
thisLine.text = line.toString()
sb.append(line)
if (Logcat.loggable) {
Logcat.d("count2:${maxNumberCharOfLine} :$line")
}
return thisLine
}

注释上,已经写清楚了我的判断规则.这个规则目前来说,有点简单了.

针对一个普通文本,没有样式,我没有找到更好的办法.如果是有样式的html,效果会好一些.

其中一些变量

/**
* 段落的开始字符可能是以下的:
* 第1章,第四章.
* 总结,小结,●,■,(2),(3)
* //|var|val|let|这是程序的注释.需要换行,或者是程序的开头.
*/
internal val START_MARK =
Pattern.compile("(第\\w*[^章]章)|总结|小结|○|●|■|—|//|var|val|let|fun|public|private|static|abstract|protected|import|export|pack|overri|open|class|void|for|while")
internal val START_MARK2 = Pattern.compile("\\d+\\.")
/**
* 段落的结束字符可能是以下.
*/
internal const val END_MARK = ".!?.!?。!?::」?” ——"
/**
* 如果遇到的是代码,通常是以这些结尾
*/
internal const val PROGRAM_MARK = ";,]>){}"
/**
* 解析pdf得到的文本,取出其中的图片
*/
internal const val IMAGE_START_MARK = ""

这些规则主要是我拿一些书的示例来作的.针对中文.

这种方式的重排效果一般了,但好处就是有图片,样式经过调整后,还勉强可以看.

进一步优化

如果想要更好的效果,导出的时候应该是html,然后针对html再进行重排.这个目前在做,已经实现,只是效果还没达到预期,比纯文本肯定是好不少了.

目前mupdf的导出标签少,这是优点,那么在修改导出html是可控的因素就少了,然后针对html再合并.在webview上显示效果还行,在textview上效果不好.因为处理标签是不一样的.

优化后再写一篇关于html的合并,重排.已经接近原来的文档70%的水平,比纯文本提升20%左右吧.

如果有人做过类似的排版,有好的排版引擎,欢迎介绍给我.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/919258.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

深入解析:UE5GAS GameAbility源码解析 CommitAbility

深入解析:UE5GAS GameAbility源码解析 CommitAbility2025-09-27 08:43 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; di…

详细介绍:基于物联网的智能衣柜系统的设计(论文+源码)

详细介绍:基于物联网的智能衣柜系统的设计(论文+源码)pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas…

电子商务平台 网站 建设方式博达网站建设怎么建立下载

成员函数和成员变量是分开存储的 1. 非静态成员变量&#xff0c;属于类的一部分&#xff0c;sizeof(类名)的时候会包括进去 2. 静态成员变量&#xff0c;不属于类的一部分&#xff0c;不会影响类的大小 3. 成员函数&#xff0c;都不属于类的一部分 4. 空类大小为1B

上海做网站好的公司有哪些安卓优化大师旧版本

Acrobat Pro DC 2023是一款功能强大的PDF编辑和管理软件&#xff0c;它可以帮助用户在创建、编辑、转换和共享PDF文档方面达到前所未有的高度。这款软件提供了丰富的编辑功能&#xff0c;使用户能够轻松添加注释、高亮、下划线、插入文本等&#xff0c;自由地编辑PDF文档。除了…

确定Ceph集群中OSD组件与具体物理磁盘的关联

在Ceph分布式存储系统中,Object Storage Daemons(OSDs)是主要的存储守护进程,负责存储数据、数据复制、恢复、再平衡以及提供信息给集群的监控模块以便于其他组件的决策。为了维护数据的完整性和高可用性,Ceph自动…

深入解析:Jenkins+Tomcat持续集成教程

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

JavaScript加解密实践

加密算法设计 首先,我们选用对称加密,其中加密和解密使用同一把密钥。考虑到执行效率和实现复杂度,本例使用修改版的凯撒密码作为基础。凯撒密码是最简单的替换密码类型之一,通过将字母表中的每个字母移动固定数目…

维护一个网站难吗制作视频的网站软件

即时编译&#xff08;Just-In-Time Compilation&#xff0c;JIT&#xff09;是一种将程序在运行时动态地编译成机器代码的编译技术。相对于传统的静态编译&#xff0c;即时编译将编译过程延迟到程序执行的时候进行&#xff0c;而不是在程序执行之前。这种方法允许编译器根据程序…

手表网站建设策划新乡网站建设求职简历

直方图 何为直方图&#xff1f;没那么高大上&#xff0c;其实就是二维统计图。每个照片都是有像素点所组成&#xff0c;当然也是[0,255]&#xff0c;直方图就是统计每个值所对应的像素点有几个。 直方图横坐标表示0-255这些像素点值&#xff1b;纵坐标表示对应像素点值的个数有…

重庆定制网站建设地址深圳松岗最新消息今天

https://vijos.org/p/1629 描述 八是个很有趣的数字啊。八发&#xff0c;八八爸爸&#xff0c;88拜拜。当然最有趣的还是8用二进制表示是1000。怎么样&#xff0c;有趣吧。当然题目和这些都没有关系。 某个人很无聊&#xff0c;他想找出[a,b]中能被8整除却不能被其他一些数整除…

Linux系统中使用df命令详解磁盘使用情况

df(disk free)是一款Linux系统中用于检查文件系统磁盘空间占用情况的命令行工具。通过这个命令,用户能够查看已挂载的磁盘分区的总空间、已使用空间、可用空间以及挂载点。以下是对 df命令功能的详细阐述。 基本用法…

读人形机器人24岗位替代

读人形机器人24岗位替代1. 评估失业风险 1.1. 人形机器人的兴起给全球社会带来了兴奋感和担忧 1.2. 机器具有无与伦比的机动性、灵活性和适应性,它们能够执行各行各业的复杂任务 1.3. 最容易受自动化影响的行业1.3.1.…

Palantir解密:从企业数字化能力构成说起,“本体”如何破解现代企业数据应用难题? - 指南

Palantir解密:从企业数字化能力构成说起,“本体”如何破解现代企业数据应用难题? - 指南pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !import…

在Ubuntu 18.04/20.04 LTS设置静态DNS服务器

要在Ubuntu 18.04/20.04 LTS上设置静态DNS服务器,需遵循特定的配置过程,这个过程因为Ubuntu系统中网络管理组件的不同而有所区别。 Ubuntu 18.04 (使用netplan) Ubuntu 18.04 默认使用netplan来代替之前的 /etc/netw…

做旅行网站好网络建设与运维初级

队列概念队列(Queue)是限定只能在一端插入、另一端删除的线性表。允许删除的一端叫做队头(front),允许插入的一端叫做队尾(rear)&#xff0c;没有元素的队列称为“空队列”。队列具有先进先出(FIFO)的特性。普通顺序队列存在的问题在普通顺序队列中,入队的操作就是先将尾指针re…

杭州市城乡建设网站做字典网站开发

一、误差思维&#xff08;只要有估计&#xff0c;就会有误差&#xff09;1.误差永远存在&#xff0c;并且不可避免。2.多次测量用平均值的统计方式取得结果&#xff0c;尽量避免偶然因素的影响。二、置信区间&#xff08;误差范围&#xff09;1.统计学最核心思想&#xff1a;用…

wordpress下划线 代码东莞网站关键词优化怎么做

乘车人表结构 分库分表策略 乘车人的数据严重依赖于用户数据。每个用户至少需要有一个对应的乘车人&#xff0c;即自己本人。当然&#xff0c;也有可能是其他人&#xff0c;因为允许用户注册账号后为他人购票的情况。这种关联确保了用户和乘车人之间的正确映射&#xff0c;使系…

杭州手机网站黄埭网站建设

一、RAID的介绍 1.什么是raid "RAID"一词是由David Patterson, Garth A. Gibson, Randy Katz 于1987年在加州大学伯克利分校发明的。在1988年6月SIGMOD会议上提交的论文"A Case for Redundant Arrays of Inexpensive Disks”"中提出&#xff0c;当时性能最…

中国设计师个人网站手机网站建设制作公司

一.安装和配置网络设备在安装linux时,如果你有网卡,安装程序将会提示你给出tcp/ip网络的配置参数,如本机的ip地址,缺省网关的ip地址,DNS的ip地址等等.根据这些配置参数,安装程序将会自动把网卡(linux系统首先要支持)驱动程序编译到内核中去.但是我们一定要了解加载网卡驱动程序…

提高工作效率的软件seo顾问 工资

DeepSeek R1-7B 医疗大模型微调实战全流程指南 目录 环境配置与硬件优化医疗数据工程微调策略详解训练监控与评估模型部署与安全持续优化与迭代多模态扩展伦理与合规体系故障排除与调试行业应用案例进阶调优技巧版本管理与迭代法律风险规避成本控制方案文档与知识传承1. 环境配…