markdown数据转换,处理html2canvas+jsPDF下载后文字截断问题(记录)

news2025/1/7 18:24:13

声明:此篇文章并不是最优解决办法,下载pdf这一步主要参考睡衣大佬提供的思路和代码,个人在此基础上进行细微修改处理段落文字截断,勉强实现不截断文字效果,但也有诸多限制和不足。
原文引路:https://blog.csdn.net/web2022050903/article/details/127069911)

项目场景:

项目要求前端将es中的markdown数据解析显示在网页上,并要求提供下载功能,将单个文章下载为pdf格式。


一、markdown数据处理

第一次处理markdown转换,就开启了我的踩坑历程(以下碎碎的怨念可以忽略(ˉ▽ˉ;)…):
1、数据格式踩坑
  据说文章格式固定,就让他们提供了一份样例文件,结果,在文章内容那里,他们直接把es字符串拼到了json中,一导入就报错不是json,因为拼进去的文字换行,是敲回车的空白换行,而不是\n,就像这样:

{
	title:"标题1",
	"content":"**这是一段介绍** 
			
			介绍内容介绍内容介绍内容介绍内容介绍内容介绍内容"	 
}

嗯,是换行了,在编译器里也能看到,但是content的格式前端解析不了啊!!!最后沟通多次,终于把返回数据的换行变成\n,然后开始继续踩坑

2、markdown插件选择
markdown转换插件有很多种,都大同小异。

刚开始采用最简单的marked,该插件采用的原理是替换(类似于replace(a,b)),使用后可以帮你把\n换成<br>,相关markdown格式语法都能精准转换,还有配套的美化css,效率高;唯一不足的是,只帮你把对应的语法标签替换,并没有将段落文字处理成block元素包裹。

最后下载pdf时发现文字被无情截断,网上查找诸多办法,可行的思路是计算块级元素的高度,给需要分页的位置加class样式标识。然而,marked转化后,除了原始包裹的div,整体就是一个涵盖全部内容的元素。没有块级元素就没法计算每一段落的高度,又怎么添加分页节点呢!!
然后尝试markdown-it,发现该插件可以将段落文字用<p></p>包裹,只需要简单配置即可(参考markdown-it中文文档),以下是我项目中的配置:

const md = new MarkdownIt({
  html:true,//在源码中启用 HTML 标签(true为启用)
  xhtmlOut:true,// 使用 '/' 来闭合单标签 (比如 <br/>)
  breaks:true,//转换段落里的'\n'到 <br>(true为启用,否则会直接把\n字符串返回)
  linkify:false// 将类似 URL 的文本自动转换为链接。
 });
 //渲染内容到页面
 this.page = md.render(str)//str是接口获取到的文章字符串

html

<div v-html="page" class="markdown-body1" ref="downArea1"></div>

如果字符串被一对\n\n包裹,则会被一个<p></p>包裹


二、生成dom转pdf下载

1、问题
  块级元素基本划分出来了,但是到哪里分页咱不知道,如果是固定的dom结构,咱还能自己加个class循环遍历,或者后端传回有标记的html也行。现在,dom是根据文章json内容自己生成的,多种元素出现位置都不固定,只能计算元素自力更生了。

2、未成功的尝试
对于不固定的结构,尝试过如下方法:
①网上热度比较高的“元素超过一页内容就分页”法,和图片高度分法差不多,最终的效果是:某段落高度 > 该页剩余高度,这个段落被放到下一页,上一页底部留下一片空白;嗯,这样确实不会出现文字截断的问题了,但可能是某元素过大,计算的间隔过大,有时会在中间留下空白页。
②import “@/utils/markdown/bookjs-eazy.min.js”; ,评论下推荐的这个,可能是我使用方式不对,引用时运行卡死。
③利用columns相关设置解决图片和文字被无情截断问题,源码jq编写,原谅我笨/(ㄒoㄒ)/~~,不知道怎么跑起来,作者也没有给出回复。

3、分析
  经历了种种折磨,决定还是从基层入手,这里先对pdf添加图片的参数做一个简单介绍:

//(dom转换成的图片,图片格式,x轴偏移,y轴偏移,放入图片的宽度,放入图片的高度)
PDF.addImage(pageData, 'JPEG', 0, position, imgWidth, imgHeight)

(1)首先,将整个需要转换的dom看成一个大图,有个A4纸大小的木框作为可视区域;以大图左上角为原点,右下为正向区域,y值越小,大图就相当于往上平移拉动,我们即可从木框中将一篇文章从上往下浏览完毕。
(2)imgWidth和imgHeight是生成图片的高度,由计算获取,调整imgHeight并不能控制你截取内容的范围,因为在转换图片时已经将内容固定好了,如果此时减小imgHeight值,只会呈现pdf文字变扁的效果。
(3)综上,还是得在生成图片之前截取适当高度作为一页内容

4、文字截断处理
认真研究了大佬对于分页的理解和方法,对于加页眉页脚,以及首尾间距确实解决的很好(链接在顶部)。以下是个人的理解:
(1)原理分析:生成内容类型
特殊元素:比如图片、富文本、表格等,后两种我没用到。通过假数据模拟测试,图片分页确实很准,不会出现把图片拦腰截断的状况。总结计算原理就是:
①该元素顶部距上一个分页距离直接 > 一页内容,说明这个元素直接在第二页,直接加一页内容高度
②该元素距上一个分页距离 + 该元素高度 > 一页内容应当的高度,说明这张A4放不下,比如图片,为了保持完整图片只能将截取标记放到图片之前

普通元素:比如我文章中会渲染出的段落<p></p>,这个和上面的原理①一样,没检测到分页不会处理,有超出直接计入一页内容的高度,然后就导致横跨两页中间的一段文字容易出现拦腰砍断的现象。

虽然会出现文字截断,但我比较倾向于这种对普通段落文字铺满一页的处理结果,因为如果像2、①中那样,段落高度导致生成pdf后每张纸都大量空白,看上去感觉怪怪的,打印出来还浪费纸张。那么就要重新确定段落分页位置。

(2)解决:修改普通元素转换代码

以我UI给的设计图为例,段落文字的字号为16px,每行高度为28px,这样算下来,一行文字的上下空白距离为6px。为了方便计算元素剩余高度,最好在css中提前将<p>等自带的margin改为内padding间距。

还有,由于markdownit转化后,图片代码有双换行也会将图片用<p>包裹,但段落横跨截取是根据文字整行来计算,容易在外层判断时就把整个图片拦腰截断,所以在判断横跨时加了图片判断。
最后一个判断语句,如果剩余内容不满一行,就直接在这个元素前进行截断,向上稍微挪到上一行字底紧贴文字;如果剩余内容可以放多行,那就截取到整行前,并向上稍微挪到上一行字底紧贴文字。

js修改:

	//添加两行代码
  const fontHeight = rate * 28;//在转换后的行高
  const fontBottom = rate * 6;//转换后行高与文字底部距离是(28-16)/ 2 = 6
  ...
  function updateNomalElPos(eheight,top,elem) {
      let outstrip = top - (pages.length > 0 ? pages[pages.length - 1] : 0) > originalPageHeight;//当前内容超过一页内容的高度
      let notOutstrip = top - (pages.length > 0 ? pages[pages.length - 1] : 0) < originalPageHeight;//当前内容位置不超过一页的高度
      let between = notOutstrip && (top - (pages.length > 0 ? pages[pages.length - 1] : 0) + eheight > originalPageHeight);//当前内容位置在分页之前但内容超过高度导致分页
      if (outstrip) {
        pages.push((pages.length > 0 ? pages[pages.length - 1] : 0) + originalPageHeight - pages[0]);
      }
      if(between){
        let extra = originalPageHeight - (top - pages[0] - (pages.length > 0 ? pages[pages.length - 1] : 0));
        if(elem.childNodes && elem.childNodes[0].tagName == 'IMG'){
          pages.push((pages.length > 0 ? pages[pages.length - 1] : 0) + originalPageHeight - extra)
        }
        if(elem.childNodes && elem.childNodes[0].tagName !== 'IMG'){
          let endDis = extra%fontHeight;
          let cut = parseInt(extra/fontHeight)
          if(cut < 1){
            pages.push(top - fontBottom)
          }else{
            pages.push((pages.length > 0 ? pages[pages.length - 1] : 0) + originalPageHeight - endDis - fontBottom)
          }  
        }
      }
   }

减去pages[0]是为了消除一开始偏移带来的误差:文章页内容每个元素距离父元素的offsetTop固定,但第一个截取不为0坐标的话,相当于所有后续截取都向下偏移了一个距离,那么固定某元素top - 上一个位置分页的结果就会偏小,top-pages[0]可以看作将生成的图片原点按pages的起始纵坐标统一。
或者可以写成:
let extra = originalPageHeight - (top - (pages.length > 0 ? pages[pages.length - 1] : 0)) + pages[0];

理解为计算完剩余高度后补充初始偏移的误差,即得到真实剩余高度。(下面内容会继续解释)


三、文字截断踩过的坑与弊端

1、对于横跨两页的元素,剩余距离的计算原理是:

剩余距离 = 一页盛放内容的高度 - (某元素距顶部距离 - 上一个分页截断位置)

但是不知道为什么,计算的结果和实际可放行数根本对不上,比如parseInt(endDis/fontHeight)=3,但实际打印出的pdf,剩余位置放了7.5行 ,最后一行文字被拦腰切断。
直到我把图片数据弄到分页的位置测试,发现本该分页的位置,图片多打印了一块高度,既然计算原理没错,那就是整体循环监测出现了偏移。偶然间,尝试将后续对于普通元素(也就是段落文字)的判断截取点,都减去第一个分页坐标,也就是pages[0],神奇的是,剩余位置行数对了,末行文字也不腰斩了,激动in~。但是!!这样每页底部都空了一段距离
2、所以,那个偏移位置是怎么来的呢?(这段内容很重要!!!)
代码获取元素顶部的偏移的计算是循环往上直到循环到父级有定位的元素,由markdown-it转后无法自定义,所以我初始就给包裹文章的div加了relative,文章容器上下留间距(设计与美观要求),布局如下:
在这里插入图片描述
包裹页面的祖级容器也用到了定位,所以,按照函数中获取顶部距离的方法,这个循环最终捅到了祖级容器(即上图黑框),而祖级容器由于顶部导航栏某功能样式要求,祖级relative定位不可舍弃。

3、单独设立打印页面隐藏
做出这个决定主要是两点原因:
①以上祖级定位影响实际分页点适配a4定位,但又要兼顾ui设计布局。
②整体项目采用postcss响应适配,放在小屏打印时,文字缩放太小,打印出的文字也是迷你费眼睛,而且因为尺寸过小,计算误差也增大,不可避免继续出现文字腰斩;如果保留原尺寸,未下载前在小屏浏览文章,文章16号字体比rem转换后的网站标题还大,违和感太强。(如果网页没有用适配转换的要求,可以直接将显示页面作为pdf下载打印页面,外部包裹一个无定位容器或修改源码自定义循环终止条件)
4、设立专门的打印页面
首先,取消原先打印文章容器顶部的留白距离,布局改为如下:
在这里插入图片描述
然后,将打印的真正页隐藏在给用户浏览页的背后。由于markdown-it对hidden和unvisible元素不会处理,二者设置都会使pdf下载后打开全是空白,所以使用定位来处理打印页(relative)和显示页(absolute)的层级,并用显示页的背景容器宽度100%+background遮挡。

5、文章标题失踪
文章由自建标题markdown语句和json内容拼接后交给markdown-it处理,在去掉顶部距离后,突然发现打印的标题没了,在改标题文字后发现,单行标题无法显示,两行标题只有第二行可以露出,被隐藏遮挡了一个<h1>文字高度,因此在拼接时添加了一行占位,完整字符串和效果如下:

let str = "# <font color=\"#fff\">一级标题占位,防止打印时第一行标题缺失</font>\n# " +title+"\n<font color=\"#999\">来源:"+from+" </font><font color=\"#999\">  &emsp; &emsp;    "+docDate+"</font>\n\n---\n\n"

并去掉占位<h1>的上下空白

.markdown-body>>h1:first-child{margin-top:0;margin-bottom:0;}

(ps:粉色块为调试时内容部分,方便查看文字截断情况,完成后背景要改为白色)
在这里插入图片描述
6、弊端
(1)需要配合修改常见元素的css样式:
  如果需要展示1~6级标题的上下距离,需要将margin都改为padding,<p></p>同理。最重要的是,文章内容与定位包裹的父级,顶部不要有padding或margin间距
(2)此改法对数据内容有局限性:
  比如在上面最大留取文字行数我用的固定28行高值,是因为我和其他同事沟通过,说在正文讲述部分不会出现h1等大文字情况,而我文章中的标题只设置有下padding,h2级纯文本24px,包括h2以下字号都不会超过28这个范围。所以,这里没有对其他情况做限制,有需要可以自己添加限制条件。

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

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

相关文章

软件测试基础面试题大全(下)

11. 一个输入框&#xff0c;如何编写测试用例&#xff1f; 字符型输入框 1. 字符型输入框&#xff1a;英文全角、英文半角、数字、空或者空格、特殊字符“~&#xff01;#&#xffe5;%……&*&#xff1f;[]{}”特别要注意单引号和&符号。禁止直接输入特殊字符时&…

nodejs安装及配置过程

目录 如何查看自己电脑中有没有下载nodejs 请问应该如何更新nodejs呢&#xff1f; 为什么称nodejs为vue脚手架呢&#xff1f; 如何打开系统变量呢 C:\Users\abner>npm config set registry https: registry.npm.taobao.org npm WARN invalid config registry"http…

Mysql 学习(十 二)查询优化 Explain

什么是Explain&#xff1f; 一条查询语句经过Mysql查询优化器的各种基于成本和规则的优化后生成一个所谓的执行计划&#xff0c;而Explain 语句可以让我们知道执行计划的语法&#xff0c;从而我们有针对性的提升性能举例子&#xff1a;EXPLAIN SELECT 1 由此我们得到了一些参数…

Prompt 指南

https://github.com/OleNet/YouPromptMe/tree/gh-pages/you-prompt-me 这是一份如何调整 Prompt 得到更漂亮的图片的经验性文档。结果和经验都来源于文心 ERNIE-ViLG Demo 和社区的资料。 极乐迪斯科里的猫,故障艺术 呼吁与准则 机器生成图片的最终目的还是便捷地为人类创造…

Cyanine7-Hylauronic菁染料CY7标记透明质酸Cy7-Hylauronic

荧光CY7是一种近红外荧光染料&#xff0c;具有高吸收和高荧光发射强度&#xff0c;适用于生物医学研究中的细胞成像和药物传递等领域。而荧光Cy7-Hylauronic透明质酸则是将荧光CY7与透明质酸结合而成的复合物&#xff0c;可以在细胞内或体内被稳定地释放&#xff0c;具有良好的…

【Python入门】Python循环语句(while循环的嵌套应用)

前言 &#x1f4d5;作者简介&#xff1a;热爱跑步的恒川&#xff0c;致力于C/C、Java、Python等多编程语言&#xff0c;热爱跑步&#xff0c;喜爱音乐的一位博主。 &#x1f4d7;本文收录于Python零基础入门系列&#xff0c;本专栏主要内容为Python基础语法、判断、循环语句、函…

centos7.5离线安装部署TiDB-6.5.0分布式系统

centos7.5离线安装部署TiDB-6.5.0分布式系统 一、需求&#xff0c;为什么要部署TiDB-6.5.0分布式系统 当前绝大部分企业的业务数据都分散在不同的系统中&#xff0c;没有一个统一的汇总&#xff0c;随着业务的发展&#xff0c;企业的决策层需要了解整个公司的业务状况以便及时…

抖音seo源码保姆式服务搭建|定制产品开发分享

抖音seo霸屏&#xff0c;是一种专为抖音视频创作者和传播者打造的视频批量剪辑&#xff0c;批量分发产品。使用抖音seo霸屏软件&#xff0c;可以帮助用户快速高效的制作出高质量的优质视频。 使用方法&#xff1a;1. 了解用户的行为习惯 2. 充分利用自身资源进行开发 3. 不…

蓝桥杯第十四届青少年Python组省赛试题--第4题

提示信息&#xff1a; 杨辉三角就是一个用数排列起来的三角形&#xff08;如下图&#xff09;&#xff0c;杨辉三角规则如下&#xff1a; 1&#xff09;每行第一个数和最后一个数都为1&#xff0c;其它每个数等于它左上方和右上方的两数之和&#xff1b; 2&#xff09;第n行有n…

FiftyOne 系列教程(1)FiftyOne简介及跑通官网的demo案例

文章目录 简介demo效果 简介 FiftyOne 是一个数据集管理和模型分析工具&#xff0c;提供灵活的 API 和直观的应用程序&#xff0c;作为开源“数据集的 IDE”。可以非常方便的做出来自己的算法与baseline之间的对比效果图在更多数据集上获得结果&#xff0c;通常是你以前从未使…

logstash同步数据从kafka到es集群

背景&#xff1a;需求是这样的&#xff0c;原始文件是txt文件&#xff08;每天300个文件&#xff09;&#xff0c;最终想要的结果是每天将txt中的数据加载到es中&#xff0c;开始的想法是通过logstash加载数据到es中&#xff0c;但是对logstash不太熟悉&#xff0c;不知道怎么讲…

数据仓库是什么?什么是列式存储?

事务和分析 在早期的业务数据处理过程中&#xff0c;一次典型的数据库写入通常与一笔 商业交易&#xff08;commercial transaction&#xff09; 相对应&#xff1a;卖个货、向供应商下订单、支付员工工资等等。但随着数据库开始应用到那些不涉及到钱的领域&#xff0c;术语 交…

Liunx 套接字编程(2)TCP接口通信程序

1.TCP通信程序的编写 面向连接、可靠传输、提供字节流传输服务 客户端向服务器发送一个连接建立的请求流程&#xff0c;上图中服务端第三步详细流程 2.TCP接口 socket--创建套接字 int socket(int domain, int type, int protocol); bind---绑定 intbind(int sockfd, struct s…

自动化测试工具 —— selenium介绍及基本使用方法

Selenium是一个开源、免费、简单、灵活&#xff0c;对Web浏览器支持良好的自动化测试工具&#xff0c;在UI自动化、爬虫等场景下是十分实用的&#xff0c;能够熟练掌握并使用Selenium工具可以大大的提高效率。 Selenium简介 Selenium支持多平台、多浏览器、多语言去实现自动化…

声音合成——Foley Sound——DECASE项目——多模态智能感知与应用——项目复现

文章目录 概述项目复现配置环境下载并配置文件运行代码第一阶段&#xff0c;训练提取DTFR特征的模型资料搜集 train_vqvae.py 第二阶段&#xff0c;使用训练好的模型提取声音的DTFR特征torch.cuda.OutOfMemoryError: CUDA out of memory. 第三阶段&#xff0c;基于特征训练合成…

【软件测试】支付模块测试攻略,这些测试方法和注意事项你掌握了么?

对于大部分人而言&#xff0c;支付模块或许是日常生活中最为关注和使用的功能之一&#xff0c;因此&#xff0c;对于支付模块的质量控制也显得尤为重要。 但考虑到支付涉及到金钱流转等敏感信息&#xff0c;一旦出现问题可能带来非常严重后果。因此&#xff0c;在支付模块测试…

FastAPI 的路由介绍及使用

上一篇文章中&#xff0c;我介绍了 FastAPI 框架的安装和 HelloWorld 项目搭建方式。本文将介绍如何使用 Router 路由处理 FastAPI 中的请求。 什么是路由 路由 Router 就像是一个流水线上的线长&#xff0c;协调生产&#xff0c;下达命令给不同的组长进行分工&#xff0c;然…

Springboot——事物管理

文章目录 事务管理一、 Spring事务管理1.1 事务回顾1.2 案例&#xff1a; 解散部门&#xff08;未开启事务&#xff09;1.3 事务管理注解Transactional1.4 事务管理日志开关1.5 rollbackFor 异常回滚属性1.6 propagation 事务传播行为1.7 解散部门并记录操作日志1.7.1 创建数据…

Java 8 腰斩!Java 17 暴涨 430%!!(文末福利)

New Relic 最新发布了一份 “2023 年 Java 生态系统状况报告”&#xff0c;旨在提供有关当今 Java 生态系统状态的背景和见解。该报告基于从数百万个提供性能数据的应用程序中收集的数据&#xff0c;对生产中使用最多的版本、最受欢迎的 JDK 供应商、容器的兴起等多方面进行了调…

AIGC+实时云渲染:开启3D内容生态的黄金时代

AIGC技术革命下&#xff0c;我们的3D内容生态将会迎来怎样的变化格局&#xff1f; 实时云渲染 / Cloud XR技术将在AIGC大潮中扮演什么样的角色&#xff1f; 作为云基础设施厂商&#xff0c;我们有哪些机会可以抓住&#xff1f; 这些问题已在XR产业、3D内容行业以及软件行业内…