史上最完整的 itext7 真实世界摘要
1. itext7史上最全实战总结
1.1. 前言
最近有个需求需要我用Java手动写一份PDF报告,经过考察几种pdf开源代码,最终选取了itext7,此版本为7.1.11
,由于发现网上关于该工具的博文比较少,特别是实战博文几乎没有,在我踩完各种坑,最终把PDF成型后,打算把经验分享出来,本文通过摘录解释来说明,内容来自本人GitHub itext-pdf
1.2. 配置文件
项目采用了Spring Cloud config
所以配置在git上,仅仅研究itext7不需要用到数据库等功能,请直接运行PdfMain
类的main
方法,即可生成模拟的PDF报告
1.3. 版本POM
itext7相关pom
<properties>
<itext.version>7.1.11</itext.version>
</properties>
<dependencies>
<!-- itext7 -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>kernel</artifactId>
<version>${itext.version}</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>io</artifactId>
<version>${itext.version}</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>layout</artifactId>
<version>${itext.version}</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>forms</artifactId>
<version>${itext.version}</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>pdfa</artifactId>
<version>${itext.version}</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>pdftest</artifactId>
<version>${itext.version}</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>font-asian</artifactId>
<version>${itext.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.18</version>
</dependency>
<!--itext7 html转pdf用到的包-->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>html2pdf</artifactId>
<version>3.0.0</version>
</dependency>
</dependencies>
1.4. 干货
itext7语义本身和前端css很像,所以有点前端基础还是比较容易掌握的
1.4.1. 添加图片
- 读取项目中图片文件
- 设置边距
- 设置宽高扩大缩小
Image indexImage = new Image(ImageDataFactory.create(GenoReportBuilder.class.getClassLoader().getResource("image/gene.png")));
indexImage.setMargins(-50, -60, -60, -60);
indexImage.scale(1, 1.05f);
1.4.2. 添加指定空白页
- 添加第2页为空白页,立即刷新后再继续添加
pdf.addNewPage(2).flush();
1.4.3. Div、Paragraph
Div div = new Div();
div.setWidth(UnitValue.createPercentValue(100));
div.setHeight(UnitValue.createPercentValue(100));
div.setHorizontalAlignment(HorizontalAlignment.CENTER);
Paragraph p1 = new Paragraph();
p1.setHorizontalAlignment(HorizontalAlignment.CENTER);
p1.setMaxWidth(UnitValue.createPercentValue(75));
p1.setMarginTop(180f);
p1.setCharacterSpacing(0.4f);
Style large = new Style();
large.setFontSize(22);
large.setFontColor(GenoColor.getThemeColor());
p1.add(new Text("尊敬的 ").addStyle(large));
...
Paragraph p2 = new Paragraph();
...
div.add(p1);
div.add(p2);
- 整块的内容用Div包裹,这里整块包裹的好处是什么?一方面排版分明成体系,另一方面若需求是整块的内容必须在同一个版面,你可以对Div设置
div.setKeepTogether(true);
,尽量保证若整块的内容超出了一页,那这块内容会自动整块出现在下一页,上一页剩下的就留白了 - 可以看到
Div
,Paragraph
可以设置很多属性,实际上我们常用的组件除了这两种,还有Table
,Cell
,List
,他们大部分的属性都是一样的,只是部分属性只在部分组件起效果,所以当你设置某个属性没起效果也不用奇怪 -
Paragraph
需要特别注意的一点,想要段落文字居中,不要用setHorizontalAlignment(HorizontalAlignment.CENTER);
这是组件的居中对段落无效,甚至对段落里你放Text
也无效,需要改用setTextAlignment(TextAlignment.CENTER);
-
Paragraph
段落的行距也是个高频问题,这里给出官方我看到的解释,参考https://itextpdf.com/en/resources/books/itext-7-building-blocks/chapter-4-adding-abstractelement-objects-part-1
,搜关键字setFixedLeading
,我的理解该方法设值行高绝对值,官方解释是两行文字中间基线之间的距离 - 如果想了解详细的什么属性哪里能起作用哪里不行,请访问该地址
1.4.4. Table
-
useAllAvailableWidth
表示页面有多宽,我就有多宽 -
table.startNewRow();
表示新起一行,table每画一行都要新起一行 - 同样table内容需要居中,和段落一样,请设置
new Cell().setTextAlignment(TextAlignment.CENTER)
- 每个table中cell都有默认高度,会比实际输入字体高些,此时设置
setHeight
,若更大没有问题,若高度小于或接近字体大小文字可能就消失了,若想让Cell高度更接近文字高度,请设置Cell
的padding
,即cell.setPadding(-2)
,设置负值即可
1.4.5. Tab,\t
-
itext7中如果要表示段落前的空格,不能使用
\t
,但换行可以使用\n
-
若要实现
Tab
效果可以有多个方法-
\u00a0
符号,大概7、8个该符号可表示tab,可能不是很准确
p1.add(new Text("\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0壹基因衷心祝愿您身体健康、享受品质生活!"));
-
p1.setFirstLineIndent(24)
,表示段落前留多少空,需要知道一个字多大,设置成两倍就行 -
Tab
也是集成AbstractElement
的组件,通过以下方式也可实现相同的效果
p2.add(new Tab()); p2.addTabStops(new TabStop(20, TabAlignment.LEFT));
-
1.4.6. 换页
我常用的换页方法为如下,该方法可保证立即换页
doc.add(new AreaBreak(AreaBreakType.NEXT_PAGE));
当然PdfDocument
有addNewPage
其实也可以用,但有时候你没把握好刷新时间可能导致某些混乱
1.4.7. 画图或画文字
能画出多么复杂的图形看是谁画了,在我的PDF中,我画的最复杂的图形如下
该图形由多个弧形区域加线段加文字组成,包括数字上的小箭头也是画出来的,画这个的代码过多,想要了解详细的可以自行下载研究,这里介绍API功能
-
lineTo
画线段 -
roundRectangle
可用来画角是弧形的方形,也可以用来画圆 -
showText
用来画文字
以上几种结合填充即可把三角形,多边形画出来了
PdfPage page = pdf.getPage(pdf.getNumberOfPages());
pageSize = pdf.getDefaultPageSize();
PdfCanvas pdfCanvas = new PdfCanvas(page);
pdfCanvas.saveState().moveTo(pageSize.getWidth() / 2 - 100 + i * 40, yOffset - 203)
.lineTo(pageSize.getWidth() / 2 - 100 + i * 40, yOffset - 208)
.stroke().restoreState();
pdfCanvas.setLineWidth(2);
pdfCanvas.setStrokeColor(color);
pdfCanvas.roundRectangle(pageSize.getWidth() / 2 - 3 + posXOffset, yOffset - 188, 6, 6, 3)
.stroke();
pdfCanvas.beginText()
.setFontAndSize(font, 12)
.moveText(pageSize.getWidth() / 2 - text.length() * 12 / 2, yOffset - 45);
pdfCanvas.showText(text);
pdfCanvas.endText();
1.4.8. Html段落转Pdf段落
我们可能遇到把一段Html文本转换成itext7的段落放进来,此时需要用到它的htmlToPdf模块,该模块对应POM
<!--itext7 html转pdf用到的包-->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>html2pdf</artifactId>
<version>3.0.0</version>
</dependency>
至于使用,设置好配置属性,使用也很简单,通常我们需要支持中文,所有配置如下,字体可以自己换
ConverterProperties proper = new ConverterProperties();
//字体设置,解决中文不显示问题
FontSet fontSet = new FontSet();
fontSet.addFont(GenoReportBuilder.class.getClassLoader().getResource("font/SourceHanSansCN-Regular.ttf").getPath(), PdfEncodings.IDENTITY_H);
FontProvider fontProvider = new FontProvider(fontSet);
proper.setFontProvider(fontProvider);
String content = "html内容";
List<IElement> elements = HtmlConverter.convertToElements(content, proper);
转换的内容是IElement
集合,而IElement
是什么呢?给张图就了解了
也就是说只要你的html内容是<div></div>
包裹的,你直接把元素转成itext7的Div
然后add
到document
就可以实现html内容的添加了,当然你也可以用instanceof
判断不同内容不同处理
如下是我的处理例子供参考,我把输入html内容样式进行了一定修改后转成itext7组件,这里特别提心,html转过来的itext7组件可能会不支持部分样式的修改,所以需要在html中进行css样式的添加,这里我就把字体和高度统一用css设值了
Div overall = new Div();
java.util.List<IElement> iElements = getFixContent(value);
for (IElement iElement : iElements) {
Style style = new Style();
style.setFontSize(10);
style.setCharacterSpacing(0.7f);
if (iElement instanceof Div) {
Div div = (Div) iElement;
java.util.List<IElement> children = div.getChildren();
// 全部段落改成相同样式
this.addParagraphStyleCircle(style, children);
overall.add(div);
} else if (iElement instanceof Paragraph) {
Paragraph element = (Paragraph) iElement;
overall.add(element.addStyle(style));
}
}
doc.add(overall);
- getFixContent
private java.util.List<IElement> getFixContent(String content) {
if (content.startsWith("<div>")) {
content = content.replaceAll("<div>", "<div style='line-height:18pt;font-size:16px;'>");
} else {
content = "<div style='line-height:18pt;font-size:16px;'>" + content + "</div>";
}
return HtmlConverter.convertToElements(content, proper);
}
- addParagraphStyleCircle
private void addParagraphStyleCircle(Style style, java.util.List<IElement> children) {
for (IElement child : children) {
if (child instanceof Paragraph) {
Paragraph element = (Paragraph) child;
element.addStyle(style);
java.util.List<IElement> children1 = element.getChildren();
this.addParagraphStyleCircle(style, children1);
}
if (child instanceof Div) {
Div div = (Div) child;
java.util.List<IElement> children1 = div.getChildren();
this.addParagraphStyleCircle(style, children1);
}
if (child instanceof Text) {
Text text = (Text) child;
text.addStyle(style);
}
}
}
1.4.9. 监听事件
在编写pdf的时候,比如一篇整体的文章,我们需要在页眉位置添加关于这篇文章的固定文本或者图形,类似于打个标签,表示你翻了这么多页一直在看这篇文章,当第二篇文章的时候就换一个,举个例子
- 第一页
- 第二页
这种需求我们如何实现呢?思路分析发现,我们需要知道什么时候文章内容一页写不起了,换了一页的时候我们需要添加一个同样的页眉。这样我们就需要知道页是何时添加的,监听事件就是处理这种问题的
- pdf是
PdfDocument
,可添加的事件有START_PAGE
,INSERT_PAGE
,REMOVE_PAGE
,END_PAGE
共四个,如上需求我们需要监听START_PAGE
事件,在事件处理中做相应的处理,我在事件中使用PdfCanvas
画了头部内容
HeaderTextEvent headerTextEvent = new HeaderTextEvent(title, font);
pdf.addEventHandler(PdfDocumentEvent.START_PAGE, headerTextEvent);
- HeaderTextEvent类,
Painting
仅仅是封装了PdfCanvas
public class HeaderTextEvent implements IEventHandler {
private String text;
private PdfFont font;
public HeaderTextEvent(String text,PdfFont font) {
this.text = text;
this.font = font;
}
@Override
public void handleEvent(Event event) {
PdfDocumentEvent docEvent = (PdfDocumentEvent) event;
PdfDocument pdfDoc = docEvent.getDocument();
Painting painting = new Painting(pdfDoc, font);
painting.drawHeader();
painting.drawHeaderText(text);
painting.close();
}
}
在添加内容前添加相应事件,同时需要记得在不需要的时候移除
// 移除监听器
pdf.removeEventHandler(PdfDocumentEvent.START_PAGE, headerTextEvent);
1.4.10. 添加目录
我没有找到itext7原生是否有目录添加,根据我自己的需求,我用Table
组件来实现了自定义目录,由于我的PDF是用来打印的,所以我并没有给目录添加Link
,也就是页面跳转,不过当你彻底理解了我的项目,我想这个需求实现也不难
- 实现效果如下,随着内容的增长,目录自动增长
先说下遇到的困难,目录顾明思意,必须要有内容才会有目录,所以实际上目录是最后添加的,但如果我们添加内容到最后再跳转到前面的页面来添加目录,有三个问题:
- 目录有几页如何知道?
- 目录有几页不知道,如何知道内容在第几页?
- 由于目录不确定,所以后续内容的页码其实也是不确定的,也就是说页码也不是一页页可以添加过去的
而经过实践你会发现,我们不能够回到前几页去修改已存在的页面,因为会提示你已经flush了,不能修改。
这时我看到了movePage这个方法,也就是可以通过移动页面,把目录在内容之后生成,后再移动到前几页,但是页码还是不能修改,发现脑袋不够想了只能用上屁股,灵光一闪,不能一遍生成为什么不能二次渲染呢?于是研究读取原pdf在原pdf上修改,二次渲染的时候填上页码及移动页面,主要代码如下,包括了读取中间文件,移动目录,添加每页页码
PdfReader reader = null;
PdfWriter writer = null;
String inPath = getInPath();
try {
reader = new PdfReader(new File(inPath));
writer = new PdfWriter(new File(outPath));
} catch (IOException e) {
e.printStackTrace();
}
PdfDocument pdf = new PdfDocument(reader, writer);
Document doc = new Document(pdf);
int startPage = 7;
int numberOfPages = pdf.getNumberOfPages();
for (int i = 0; i < catalogSize; i++) {
pdf.movePage(numberOfPages, startPage);
}
String forbidPage = properties.getProperty("forbidPage");
for (int pageNumber = 1; pageNumber < numberOfPages + 1; pageNumber++) {
if (pageNumber > 6 + catalogSize && pageNumber != 8 + catalogSize) {
if (forbidPage != null && (pageNumber - catalogSize) >= Integer.parseInt(forbidPage)) {
continue;
}
PageSize pageSize = pdf.getDefaultPageSize();
doc.showTextAligned(new Paragraph(String.format("- %d -", pageNumber)), pageSize.getWidth() / 2, 30, pageNumber, TextAlignment.CENTER, VerticalAlignment.MIDDLE, 0);
}
}
1.5. 总结
经过上述总结,我基本上把项目中的大多基本点和难点都概括进去了,初次用itext7写PDF的同学基本会遇到的问题基本都在上述这些,不理解的就把项目下下来运行Main方法慢慢调试,理解透我这个项目,还有其它问题那基本只能翻官网了
项目Github: https://github.com/tzxylao/onegeno-itext-pdf
itext7官网:https://itextpdf.com/
下一篇: 跳台和红黑树
推荐阅读
-
史上最完整的 itext7 真实世界摘要
-
反传销网8月30日发布:视频区块链里的骗子,币里的韭菜,杜子建骂人了!金融大V周召说区块链!——“一小帮骗子玩一大帮小白,被割韭菜,小白还轮流被割,割的就是你!” 什么区块链,统统是骗子 作者:周召(知乎金融领域大V,毕业于上海财经大学,目前任职上海某股权投资基金合伙人) 有人问我,区块链现在这么火,到底是不是骗局? 我的回答是: 是骗局。而且我并不是说数字货币是骗局,而是说所有搞区块链的都是骗局。 -01- 区块链是一种鸡肋技术 人类社会任何技术的发明应用,本质都是为了提高社会的生产效率。而所谓区块链技术本质不过是几种早已成熟的技术的大杂烩,冗余且十分低效,除了提高了洗钱和诈骗的效率以外,对人类社会的进步毫无贡献。 真正意义上的区块链得包含三个要素:分布式系统(包括记账和存储),无法篡改的数据结构,以及共识算法,三者互为基础和因果,就像三体世界一样。看上去挺让人不明觉厉的,而经过几年的瞎折腾,稍微懂点区块链的碰了几次壁后都已经渐渐明白区块链其实并没有什么卵用,区块链技术已经名存实亡,沦为了营销工具和传销组织的画皮。 因为符合上述定义的、以比特币为代表的原教旨区块链技术,是反效率的,从经济学角度来说,不但不是一种帕累托改进,甚至还可以说是一种帕累托倒退。 原教旨区块链技术的效率十分低下,因为要遍历所有节点,只能做非常轻量级的数据应用,一旦涉及到大量的数据传输与更新,区块链就瞎了。 一方面整条链交易速度会极慢,另一方面数据库容量极速膨胀,考虑到人手一份的存储机制,区块链其实是对存储资源和能源的一种极大的浪费。 这里还没有加上为了取得所谓的共识和挖矿消耗的巨大的能源,如果说区块链技术是屎,那么这波区块链投机浪潮可谓人类历史上最大规模的搅屎运动。 区块链也验证不了任何东西。 所谓的智能合约,即不智能,也非合约。我看有人还说,如果有了智能合约,就可以跟老板签一份放区块链上,如果明年销售业绩提升30%,就加薪10%,由于区块链不能篡改,不能抵赖,所以老板必须得执行,说得有板有眼,不懂行的愣一看,好像还真是那么回事。 但仔细一想,问题就来了。首先,在区块链上如何证明你真的达到了30%业绩提升?即便真的达到老板耍赖如何执行? 也就是说,如果区块链真这么厉害,要法院和仲裁干什么。 人类社会真正的符合成本效益原则的是代理制度。之前有人说要用区块链改造注册会计师行业,我不知道他准备怎么设计,我猜想他思路大概是这样的,首先肯定搞去中心化,让所有会计师到链上来,然后一个新人要成为注册会计师就要所有会计师同意并记录在链上。 那我就请问了,我每天上班累死累活,为什么还要花时间去验证一个跟我无关的的人的专业能力?最优做法当然是组织一个委员会,让专门的人来负责,这不就是现在注册会师协会干的事儿吗?区块链的逻辑相当于什么事情都要拿出来公投,这个绝对是扯淡的。 当然这么说都有点抬举区块链了,区块链技术本身根本没有判断是非能力,如果这么高级的人工智能,靠一个无脑分布式记账就能实现的话,我们早就进入共产主义社会了。 虽然EOS等数字货币采用了超级节点,通过再中心化的方式提高效率,有点行业协会的意思,是对区块链原教旨主义的一种修正,但是依然无法突破区块链技术最本质的局限性。有人说,私有链和联盟链是区块链技术的未来,也是扯淡,因为区块链技术没有未来。如果有,说明他是包装成区块链的伪区块链技术。 区块链所涉及的所有底层技术,不管是分布式数据库技术,加密技术,还是点对点传输技术等,基本都是早已存在没什么秘密可言的技术。 比特币系统最重要的特性是封闭性和自洽性,他验证不了任何系统自身以外产生的信息的真实性。 所谓系统自身产生的信息,就是数据库数据的变动信息,有价值的基本上有且只有交易信息。所以说比特币最初不过是中本聪一种炫技的产物,来证明自己对几种技术的掌握,你看我多牛逼,设计出了一个像三体一样的系统。因此,数字货币很有可能是区块链从始至终唯一的杀手应用。 比特币和区块链概念从诞生到今天已经快10年了,很多人说区块链技术在爆发的前夜,但这个前夜好像是不是有点过长了啊朋友,跟三体里的长夜有一拼啊。都说区块链技术像是90年代初的互联网,可是90年代初的互联网在十年发展后,已经出现了一大批伟大的公司,阿里巴巴在99年都成立了,区块链怎么除了币还是币呢? 正规的数字货币未来发展的形式无外乎几种,要么就是论坛币形式,或者类似股票的权益凭证等。问题是论坛币和股票之前,本来也都电子化了,区块链来了到底改变了什么呢? 所有想把TOKEN和应用场景结合起来的人最后都很痛苦,最后他们会发现区块链技术就是脱裤子放屁,自己辛苦搞半天,干嘛不自己作为中心关心门来收钱?最后这些人都产生了价值的虚无感,最终精神崩溃,只能发币疯狂收割韭菜,一边嘴里还说着我是个好人之类的奇怪的话。 因此,之前币圈链圈还泾渭分明,互相瞧不起,但这两年链圈逐渐坐不住了,想着是不是趁着泡沫没彻底破灭之前赶快收割一波,不然可能什么都捞不着了。 前段时间和一个名校毕业的链圈朋友瞎聊天,他说他们“致力于用区块链技术解决数字版权保护问题”,我就问他一个问题,你们如何保证你链的版权所有权声明是真实的,万一盗版者抢先一步把数据放在链上怎么办。他说他们的解决方案是连入国家数字版权保护中心的数据库进行验证…… 所以说区块链技术就是个鸡肋,研究到最后都会落入效率与真实性的黑洞,很多人一头扎进链圈后才发现,真正意义上的区块链技术,其实什么都干不了。 -02- 不是蠢就是坏的区块链媒体 空气币和区块链的造富神话,让区块链自媒体也开始迎风乱扭。一群群根本不知道区块链为何物的妖魔鬼怪纷纷进驻区块链自媒体战场,开始大放厥词胡编乱造。 任何东西,但凡只要和区块,链,分,分布式,记账,加密,验证,可追溯等等这些个关键词沾到哪怕一点点,这些所谓的区块链媒体人就会像狗闻到了屎了一样疯狂地把区块链概念往上套。 这让我想起曾经一度也是热闹非凡的物联网,我曾经去看过江苏一家号称要改变世界的“物联网”企业,过去一看是生产路由器的,我黑人问号脸,对方解释说没有路由器万物怎么互联,我觉得他说得好有道理,竟无言以对。 好,下面让我们进入奇葩共赏析时间,来看看区城链媒体经常有哪些危言耸听的奇谈怪论 区块链(分布式记账)的典型应用是*?? 正如前面所说,真正意义上的区块链分布式记账,不光包括“记”这个动作,还包括分布式存储和共识机制等。而*诞生远远早于区块链这个词的出现,勉强算是“分布式编辑”吧,就被很多区块链媒体拿来强行充当区块链技术应用的典范。 其实事实恰恰相反,*恰恰是去中心化失败的典范,现在如果没有精英和专业人士的编辑和维护,*早就没法看了。 区块链会促进社会分工?? 罗振宇好像就说过类似的话,虽然罗振宇说过很多没有逻辑的话,但这句话绝对是最没逻辑思维的。很多区块链自媒体也常常用这句话来忽悠老百姓,说分工代表效率提高社会进步,而区块链“无疑”会促进分工,他们的理由仅仅是分工和分布式记账都共用一个“分”字,就强行把他们扯到一起。 实际情况恰恰相反,区块链是逆分工的,区块链精神是号召所有人积极地参与到他不擅长也不想掺合的事情里面去。 区块链不能像上帝一样许诺他的子民死后上天国,只能给他们许诺你们是六度人脉中的第一级,我可以赚后面五级人的钱,你处于金字塔的顶端。