当前位置: 首页 > news >正文

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

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.hskmm.com/?act=detail&tid=18709

相关文章:

  • 详细介绍:基于物联网的智能衣柜系统的设计(论文+源码)
  • 确定Ceph集群中OSD组件与具体物理磁盘的关联
  • JavaScript加解密实践
  • Linux系统中使用df命令详解磁盘使用情况
  • 读人形机器人24岗位替代
  • 在Ubuntu 18.04/20.04 LTS设置静态DNS服务器
  • 分布式 ID 生成方案实战指南:从选型到落地的全场景避坑手册(三) - 实践
  • 队列+宽搜(BFS)-662.二叉树最大宽度-力扣(LeetCode) - 指南
  • JWT攻防实战:混淆、破解与红队利用技术详解
  • “中国英伟达”投资人,赚翻了
  • The 3rd UCUP Stage 29: Metropolis(QOJ contest 1913) 总结
  • 空白金兰契的多维解构与实践路径:从价值表征困境到人机共生伦理
  • 2025中国制造企业500强榜单发布
  • 读 WPF 源代码 了解获取 GlyphTypeface 的 CharacterToGlyphMap 的数量耗时原因
  • 张江,首个万亿市值巨头诞生!
  • Java 与智慧交通:车联网与自动驾驶支持
  • 9月26号
  • 初衷的澄明:空白金兰契的深意
  • Aidoku - 专为iOS/iPadOS打造的免费开源漫画阅读器
  • windos的hyper-v安装的宝塔面板,在面板里面点击重启服务器后再也无法启动面板。
  • Obsidia Git同步方法(偏安卓)
  • 什么是 FullGC
  • Unity渲染时的排序规则
  • AI智慧的三重跃升:从「数理魔兽」到「悬荡悟空」的文明协作者
  • 新学期每日总结(第 5天)
  • codeforces round 1054(e.f)
  • 【SimpleFOC-小项目】驱动电机正转3周
  • 联合体union的基本用法
  • 弱结构光三维扫描重建
  • 9.27 git与pycharm