资讯动态

itextpdf Paragraph使用手册:排版、字体与常见坑

发布时间:2026/10/21 15:36:34 来源:尧图企业网站定制
做过Java后端的人应该都有过这种经历项目跑了一段时间业务方突然甩过来一个需求“把订单数据导成PDF”或者“结算单生成一个带格式的附件”然后你打开搜索引擎输入“itextpdf paragraph使用”结果发现教程七零八落版本还五花八门。实际上itextpdf里Paragraph这个类几乎就是PDF中一切文本块的基础。你可以把它理解成HTML里的P标签负责把一段文字变成一个能参与文档流布局的块级元素block-level element。跟直接操作坐标画canvas完全不一样Paragraph能自动参与换行、分页、对齐、缩进这些排版逻辑所以几乎所有动态导出需求都绕不开它。下面我会结合自己在itext5和itext7两个版本里的实际使用经验把Paragraph的构造方式、排版参数、样式定制、常见问题一次性说透。这篇不是翻译官方文档而是把那些文档里不会写、但真实开发中一定会碰到的坑和数据都摊开给你看。1. 先弄清楚Paragraph在itextpdf里的定位1.1 版本差异itext5和itext7的类路径不是一回事很多开发者翻教程时最容易迷糊的地方是itext5和itext7里Paragraph的包名完全不同。itext5中你要写com.itextpdf.text.Paragraph而itext7里则是com.itextpdf.layout.element.Paragraph。别小看这个差异项目里如果混用了代码可能连编译都过不了。itext7是整个itext5的一次大重构它把内核拆分成了kernel、io、layout等模块。Paragraph被放到了layout包意味着它的角色更偏向“布局”而不是单纯的数据容器。itext5里Paragraph继承自Phrase天然带了一堆块级属性itext7里Paragraph实现了IBlockElement接口在Document.add时被当成块级元素统一处理。这种变化带来的直接影响是同样的“加一段文字”逻辑在两个版本里的写法和对API的依赖完全不同。我的建议是新项目直接上itext7老项目如果已经在用itext5不要硬迁把知识点映射到对应版本即可。因为itext7在支持Unicode字体、渲染性能、布局稳定性上都比itext5有明显改善尤其是中文字体的处理方式itext7要好太多。1.2 Paragraph的本质文本容器兼块级布局单元Paragraph的核心作用是维护“一段文本”这段文本内部可以只有一个字符串也可以由多个不同样式的Text对象组成。它同时负责“块级”行为比如在Document渲染时会自动换行、自动分页、自动应用对齐规则。理解这个本质后面很多问题就顺了。比如你往Paragraph里add一个很长的字符串它不会无限向右延伸而是在当前页面的可用宽度内做折行当段落高度超过页面底部可用空间Document会把它推入下一页。这些行为是“段落”这个抽象层级带来的不是字体或文本本身具备的。所以在文档模型里文本的最小编号是Textitext5里的Chunk而Paragraph是这些文本的“容器排版器”。你不需要手动计算每个字应该放在哪里Paragraph会替你处理。1.3 新手最容易忽略的点Paragraph不负责字体具体渲染很多人以为设置了Paragraph里的文字整个段落字体就统一了。其实Paragraph的字体设置是“默认值”段落内部的每个Text都可以独立覆盖它。换句话说Paragraph在布局中提供了一套基础样式Text可以在这个基础上做差异化渲染。一个段落里红色加粗的关键词就是通过Paragraph默认字体 单个Text特殊样式组合出来的。这也是我见过不少人在项目里反复磨蹭的地方给Paragraph设了字体但局部文字颜色没生效最后发现是内部某个Text又单独设置了字体参数覆盖掉了外面Paragraph的默认值。所以记住一个原则Paragraph的字体是底线Text的字体是个性化谁在后谁优先。2. Paragraph的基础构造与文本添加方式2.1 构造器的选择直接new还是带参数Paragraph提供了很丰富的构造方式但真实项目里常用的就是下面这几种构造方式适用场景new Paragraph()先创建空段落后面通过add方法逐步填充内容new Paragraph(String text)直接放一段纯文本简单快捷new Paragraph(Text text)需要用差异化样式比如局部加粗、局部变色new Paragraph(String text, Font font)文本和字体都要显式指定时你可能会问空构造器有什么用实际上这是最灵活的方式。比如你在循环里根据数据动态拼接内容前面一部分是普通文本后面一部分需要加粗红色这时候先new Paragraph()再逐步往里面添加Text对象比一开始确定好整个段落的内容更贴合真实业务。我自己的习惯是如果文本内容是纯展示且没有样式区分直接new Paragraph(string)就够了只要出现多颜色、多字体、行内图片这类需求就用空构造器配合add方法。2.2 添加文本的核心APIadd方法的玄机Paragraph真正强大的是它的add方法。这个方法可以用字符串、字体、Text、图片等不同对象进行重载。最常见的组合方式有两种Paragraph paragraph new Paragraph(); paragraph.add(普通文本); Text redText new Text(红色加粗文本); redText.setFontColor(ColorConstants.RED); redText.setBold(); paragraph.add(redText);字符串最终会被封装成一个Text对象挂到段落内部所以纯文本添加是“快速通道”适合简单场景如果需要对特定部分的宽度、字色、背景做精确控制还是建议先用Text封装。有一个思维转换要提一下Paragraph内部并不是简单地把这些Text拼成一个字符串而是维护了一个元素列表。布局时每个Text都会参与独立的渲染计算。这样可以实现局部UV样式但也会带来额外的布局开销。所以如果一段文字完全没有差异化样式就尽量用普通字符串别把一个整段文章拆成成百个小小的Text对象否则内存和性能都会很吃亏。2.3 中文字体配置的关键步骤绝大多数人第一次用itextpdf做导出遇到的最大问题就是中文变成“方块”或者干脆不显示。原因很简单itextpdf默认的字体Helvetica不支持中文而Paragraph在默认情况下又不会自己去找系统中文字体。在itext7中通常的做法是显式创建支持中文的字体PdfFont chineseFont PdfFontFactory.createFont( STSongStd-Light, GB-EUC-H, false ); Paragraph paragraph new Paragraph(中文段落).setFont(chineseFont);这里的STSongStd-Light是itextpdf内置的亚洲字体集合之一加上GB-EUC-H编码可以保证简体中文正常渲染。另一个更稳妥的方案是把操作系统中文字体文件直接注册给itextpdfPdfFont sysFont PdfFontFactory.createFont( C:/Windows/Fonts/simsun.ttc,0, PdfEncodings.IDENTITY_H, true );simsun.ttc,0后面的,0是字体索引因为TTC字体集合里可能存在多个字号如果你不写这个索引某些环境下会抛出找不到字体的异常。itext5时代的中文字体处理更痛苦需要BaseFont配合BaseFont bf BaseFont.createFont( STSongStd-Light, UniGB-UCS2-H, BaseFont.EMBEDDED ); com.itextpdf.text.Font font new com.itextpdf.text.Font(bf, 12); Paragraph paragraph new Paragraph(中文内容, font);在itext5里如果使用内置中文字体往往还需要引一个额外的亚洲字体包itext-asian到classpath里否则运行时会报找不到字体定义。这个问题在itext7中已经友好很多但我仍然建议在初始化阶段就把字体注册好并把PdfFont缓存下来复用而不是每次创建段落都重新加载一次字体文件。3. 对齐、缩进、间距与行距这组排版参数如何配合3.1 对齐方式从老版常量到新版枚举文本对齐是Paragraph最常用的排版能力之一。itext5里对齐是通过Element.ALIGN_LEFT这类常量设置的// itext5 paragraph.setAlignment(Element.ALIGN_CENTER);itext7里面对齐参数被设计成了枚举类型TextAlignment// itext7 paragraph.setTextAlignment(TextAlignment.CENTER);在实际项目中左对齐、居中、右对齐、两端对齐都有用武之地。尤其是两端对齐JUSTIFIED在生成合同、正式公函这类文档时非常显专业整个段落左右边界都整齐视觉上明显不简陋。但要注意如果段落包含换行符或者中间有超长英文单词两端对齐的效果可能不太理想局部会出现比较大的字符间距。自己权衡着用。3.2 缩进参数不止是在左边加空格很多新手觉得段落缩进就是前面加几个空格但itextpdf里空格在换行和排版时并不可靠而且不同字体对空格的宽度渲染也不一样。正确做法是使用Paragraph自带的首行缩进和左右缩进paragraph.setFirstLineIndent(24f); paragraph.setIndentationLeft(10f); paragraph.setIndentationRight(10f);setFirstLineIndent是首行缩进单位默认是“用户空间单位”在绝大多数PDF渲染场景中就是“点”point。中文Word标准里首行缩进两个字符大约是28点看你实际字号。如果你用的是12pt字号首行缩进24~28pt视觉上都可接受。setIndentationLeft和setIndentationRight则是整段整体缩进常用于嵌套引用、说明性文字。举个例子生成一份包含公司简介的PDF附件公司的企业简介正文通常不缩进而“免责声明”“注意事项”这类的次级文本往往会左右同时缩进一块视觉上形成层级对比。这里有一个容易踩的坑缩进值过小或为负会导致段落边距超出页面边界。itextpdf并不会拦截这种事它会按数值直接布局结果就是文本被排到页面可打印区域外面看起来像是“内容丢失”。所以当你发现导出的PDF文字莫名被裁断先检查是不是缩进设置出了问题。3.3 行距固定行距和倍数行距怎么选行距控制是Paragraph另一个高频配置点。itext里术语叫Leading概念可以参考Word里的“行距”。itext7中明确区分了setFixedLeading和setMultipliedLeading两种模式。setFixedLeading是固定行距意思是不管文字多大每行之间的垂直距离都是固定值paragraph.setFixedLeading(20f);用固定行距时如果行距值小于字体实际高度文字行之间会重叠拥挤感非常明显。一般固定值设置在字体大小的1.2~1.5倍之间比较稳妥。setMultipliedLeading则是按照当前字体大小按比例放大paragraph.setMultipliedLeading(1.5f);这种模式更符合常见的“1.5倍行距”概念。比如字体大小是12pt计算后实际行距就是18pt。它比固定行距更省心因为你不需要在业务代码里根据字号动态换算像素值。我个人的经验是动态报表推荐用倍距模式模板类正式文档推荐用固定模式这样排版和设计稿完全对齐。itext5中对应的API是setLeading(fixed, multiplied)使用逻辑可以互相参照。还有一个细节值得注意Paragraph的行距是作用在整段上的即使某一行内部出现了一个特别大的字体比如行内图片或特大号文字行距只会按预设模式计算不保证一定不被撑开。如果需要行内组件不影响整段行距那就要借助图片缩放或独立的Text高度控制。4. 样式定制背景、边框、下划线与内边距4.1 给段落加上背景色和边框Paragraph虽然主要是文本容器但它也支持背景色和边框。这在生成表格、高亮提示、重点信息标注时很常用。itext7里的背景色和边框是这样配置的Paragraph highlighted new Paragraph(重要提示内容); highlighted.setBackgroundColor(ColorConstants.YELLOW); highlighted.setBorder(new SolidBorder(1f));setBackgroundColor给文字后方的整块区域上色背景范围是段落的矩形内容区域不是文字笔画的边界。这个区别很关键。如果你想要的是“文字笔画黄色”的效果那应该给Text对象设置背景色而不是给Paragraph设置。SolidBorder是最简单的单线边框线段宽度通过构造器第一个参数指定。除了实线还可以用DashedBorder、DottedBorder做出虚线、点线的效果。不过做PDF导出时我极少在Paragraph上用边框更多是在表格单元格里用。因为段落加边框之后视觉上会有一点“文本框”的味道只有在合同提示语、审核意见里用起来比较合适。4.2 文字特效下划线、删除线与颜色Paragraph层面没有直接的下划线API但可以通过为Text设置属性来实现。这是很多初学者的认知空白点你可能会去Paragraph里找setUnderline()然而它并不存在于行级接口上。正确的做法是Text underlineText new Text(这是带下划线的文字); underlineText.setUnderline(); Paragraph paragraph new Paragraph(); paragraph.add(underlineText);如果需要对下划线颜色、线宽做更精细的控制可以调用setUnderline(ColorConstants.BLACK, 0.5f, 0f, 0f, -1f)这类带参数的重载方法参数分别对应颜色、线宽、线型偏移等。实际开发中用默认的setUnderline()就够用了官方API默认值会按照当前字体基线去绘制。删除线也是同样的思路在Text对象上调用setStrikeThrough()即可。加粗和斜体可以直接用setBold()、setItalic()但这些方法本质上是设置字级内部会自动切换字体样式。如果你在自定义字体千万记得检查这个字体是否有对应的粗体和斜体变体否则Text对象再怎么标粗也只是普通样式的渲染。4.3 内边距和外边距的细节差异Paragraph还有一组padding相关的方法比如setPaddingLeft、setPaddingRight、setPaddingTop、setPaddingBottom。背景色和边框会作用在整个“内容内边距”的矩形区域上所以如果你想做“背景色不能贴死文字”的效果就一定要加上padding否则文字边缘紧挨着边框视觉上很丑。外边距margin和段落间距spacing实际是同一个概念Paragraph里一般用setSpacingBefore和setSpacingAfter控制段落前后的空隙。这一点和HTML里的margin-top/margin-bottom对位。真实项目中两个连续段落之间如果设置了过多的setSpacingAfter再加上下一页顶部也有默认间距就会出现多出很多空白的情况。我踩过的一个坑是在循环里逐条生成段落并add到Document后每个段落分别设置了setSpacingBefore(10f)和setSpacingAfter(10f)结果页面顶部那些剧情空白叠加格外刺眼。后来我改成统一的“段后距策略”前一个段落定下后距后一个段落不再单独设置前距问题立刻消失。5. 列表、图片与复合编排让Paragraph真正可扩展5.1 List与ListItemParagraph的兄弟结构报表里经常需要展示无序列表或有序列表比如“服务条款”“功能特性”。itextpdf中这类需求不是用Paragraph一层层拼而是用List配合ListItem来实现。ListItem本质上是BlockElement内部往往又包含一个Paragraph去承载文本渲染。List list new List(); list.add(new ListItem(第一项内容)); list.add(new ListItem(第二项内容)); list.setListSymbol(•);在itext7中直接向ListItem传入字符串它默认会创建Paragraph作为内容载体。这意味着之前讲的Paragraph排版能力在ListItem里依然有效。你甚至可以向ListItem里再添加一个带样式的Paragraph实现多行列表项。很多人会纠结要不要自己在Paragraph前面加“• ”符号然后控制缩进。我建议别这么做直接用List体系它内部处理好转弯换行和悬挂缩进效果比我手拼字符串稳定得多。5.2 在段落里嵌入图片的操作方式简历、产品说明、发票头部经常需要“文字里嵌个Logo”这种效果。itext7里Paragraph可以直接添加图片对象Image companyLogo new Image(ImageDataFactory.create(logo.png)); companyLogo.scaleToFit(100f, 50f); Paragraph headerParagraph new Paragraph(); headerParagraph.add(companyLogo); headerParagraph.add(有限公司名称);图片在这里被当做一个“内联元素”参与段落行内排版。默认情况下图片会和文字叠加在基线上实际显示效果可能需要手动调整图片偏移。常见做法是设置setFixedPosition或通过Text的rise属性来微调垂直位置不过这个操作在itext7里有点繁琐我建议在图片对象上处理好缩放高度尽量减少文字和图片错位带来的视觉问题。itext5里的方式则是通过Chunk包装图片Chunk imageChunk new Chunk(image, 0f, -10f); paragraph.add(imageChunk);Chunk构造器的第二个参数是图片宽度偏移量第三个是高度偏移量。itext5的这组参数对初学者不那么友好很多人在这一块试错很久。如果你还在用itext5做“文字图片”混排时要有一点耐心多调几次偏移量直到视觉效果满意。5.3 复合段落把多个不同样式Text拼成一个逻辑块这是Paragraph最有价值的使用场景一个段落里不同位置的文字穿着不同的“衣服”但它们在文档流里属于同一行、同一段。比如合同条款里的关键金额加粗、颜色变红比如消息通知里的用户命名特殊样式、链接下划线。Text normal new Text(这是一段普通的说明); Text keyword new Text(重要关键词); keyword.setBold(); keyword.setFontColor(ColorConstants.RED); Text hint new Text(点击查看详情); hint.setFontColor(ColorConstants.BLUE); hint.setUnderline(); Paragraph paragraph new Paragraph(); paragraph.add(normal); paragraph.add(keyword); paragraph.add(hint);这种复合段落的好处在于它不用你去手动算坐标也不会因为某个局部文字较长而破坏整段的结构。itextpdf在布局阶段会把Paragraph内所有文字元素按顺序逐个排列遇到边界自动换行整体始终保持在同一个段落框架里。需要注意的是这样的复合结构比单纯字符串添加多了不少内部对象开销。如果只是“整段文字用同一字体颜色”用new Paragraph(plainText)最快只有在真正需要差异化时才值得把字符串拆开包装成多个Text对象。这条原则能直接关系到性能表现。6. 常见问题与排查技巧这些坑我都踩过6.1 中文变成方块的根因和解决办法这是邮件组和社区里出现频率最高的问题。表现形式是英文和数字正常所有中文显示为一堆小方块或者干脆消失。根因几乎都是字体没有正确配置或者配置的字体不支持中文字符集。在itext7里如果你不注册中文字体就用new Paragraph(中文)实际上Paragraph内部拿到的还是默认字体Helvetica它连汉字的字形都没有自然只能渲染方块。解决步骤如下注册一个系统中文字体文件或者使用itext内置的亚洲字体集合确保在Paragraph上通过setFont应用该字体或者在Document创建时就设置默认字体如果字体文件路径里有中文目录某些服务器环境会有编码问题建议把字体文件放到项目资源目录下用相对路径读取。PdfFont font PdfFontFactory.createFont(STSongStd-Light, GB-EUC-H, false); Paragraph paragraph new Paragraph(中文内容).setFont(font);第三步特别容易被忽略。很多项目在本地Windows跑得好好的部署到Linux服务器就中文乱码多半是字体文件路径找不到或者系统里根本没有对应字体。通用做法是直接把中文字体文件打jar包里通过InputStream注册避免依赖服务器本地字体环境。6.2 段落被硬生生切断分页与跨页处理长段落跨越页面边界时itextpdf默认行为是直接从页尾截断然后剩余部分从下一页继续。这个行为有时候可以接受但更多时候阅读体验很差尤其是一个表格内的整块说明文字被切成两半。itext里提供了一组专门的“分页行为控制”属性paragraph.setKeepTogether(true); paragraph.setKeepWithNext(true);setKeepTogether(true)表示尽可能保持整个段落完整不会在段落中间分页如果当前页放不下整个段落就会整体挪到下一页。setKeepWithNext(true)则让这个段落与下一个段落保持在同一页面这个在某些合同场景里特别有用可以避免“标题”留在页尾而“正文”跑到下一页的尴尬情况。这两个属性并不是100%保证如果段落本身比一整页还长仍然会被切断但从工程角度看已经能覆盖绝大多数业务。我一般在生成动态说明类内容时对所有Paragraph统一设置setKeepTogether(true)效果立竿见影。要精确控制段落是否溢出还可以借助ParagraphRenderer去拿布局结果但这套机制对新手过于复杂。我建议先用简单的属性门控等确实遇到更极端的场景再深入渲染层。6.3 性能与内存上升的排查方向长时间批量生成大量PDF时Paragraph本身不是性能瓶颈常见的性能隐患往往出在周边设计上重复加载字体文件每次创建Paragraph时都重新createFont不仅耗时还会占内存。字体对象应当全局缓存整个应用只创建一次。大量细粒度Text对象一个长文章如果拆成几万个Text渲染和布局开销会非常可观。合理做法是尽量合并为完整字符串只在关键位置用Text。图片未做缩放往Paragraph里塞图片时如果不调用scaleToFit或按目标尺寸缩放高分辨率大图会直接撑爆PDF体积同时降低渲染速度。段落尺寸无谓重复渲染如果可以在Document层面复用模板属性就先用空Paragraph设置好通用样式再复制或构造新段落避免每个段落重复设置大量绝对值。我在一个批量生成几千份报价单的项目里曾把字体加载从每次创建改成静态单例耗时直接下降了一个数量级。那个项目生成1500份PDF从原先的将近10分钟降到2分钟以内。所以如果你正在开发批量导出功能字体复用绝对是首要优化点。还有一个隐藏问题是itext版本不同带来的内存模型不同itext7对PdfFont和Page对象的释放依赖比较重。如果你发现内存持续上涨多半是没做PdfWriter的显式close或者维护了一堆本可以被GC的Paragraph引用。该释放的对象要及时释放避免并发场景下堆内积压。7. 不同文本密度场景下的Paragraph实际策略7.1 固定模板与动态数据流的处理差别在固定模板场景如合同、证书、发票里Paragraph的用法通常是“预定义好格式再填充数据”。我的做法是维护一个模板方法在方法内部构建统一的基础Paragraph对象private Paragraph baseParagraph() { Paragraph p new Paragraph(); p.setFont(chineseFont); p.setFontSize(12f); p.setMultipliedLeading(1.5f); p.setSpacingAfter(6f); return p; }然后根据不同业务字段往里添加内容。这个“基座Paragraph”思路非常省事所有段落的默认风格都集中在同一个地方。在动态数据流场景如日志导出、用户生成内容的报表里Paragraph更像一个“随时生长的容器”。这个时候不要过度预定义样式因为不同数据段落的长度和结构都不一样。先创建空Paragraph再按数据顺序添加Text最后根据内容长度或数据属性设对齐和间距。这种流式操作是Paragraph设计的初衷代码也更好维护。7.2 动态行高与内容测量的经验Paragraph在渲染时有一个“布局”过程。itext7中你可以利用ParagraphRenderer提前完成一段文本的尺寸测量但更简单的做法是先估算一下内容长度float estimatedHeight paragraph.getRenderer() .getCurrentArea().getBBox().getHeight();这个用法依赖渲染器已经在Document上下文中运行如果你在添加Paragraph之前就调用很可能拿到的还是默认值。因此我建议除非有特别精确的高度控制需求否则不要过度依赖测量值。让Document的自动布局去处理段落位置是最稳妥的因为Paragraph内部对边界、缩进、换行的计算非常成熟硬编码反而容易出问题。7.3 一个模板化输出的参考思路实际做项目时我常用一个简单的“模板段落构建器”来减少重复代码。比如把合同正文拆成段落数组每个元素记录文本、字体大小、对齐方式、是否加粗然后用统一方法逐段构建并加入DocumentListTextBlock blocks prepareBlocks(); Document document initDocument(); for (TextBlock block : blocks) { Paragraph paragraph new Paragraph(); paragraph.setFont(sharedChineseFont); paragraph.setFontSize(block.getFontSize()); paragraph.setTextAlignment(block.getAlignment()); paragraph.setSpacingAfter(block.getSpacingAfter()); document.add(paragraph); }这个方法既保持了数据与表现分离又避免了每个段落写一遍相似代码。如果你的业务系统需要对接多种PDF模板把内容结构抽象成“块”再交给Paragraph去渲染比直接在业务逻辑里大量new Paragraph要清晰很多。遇到规则复杂的情况甚至可以再封装一层“段落工厂”但还是那句话别过度设计能把样式集中在基座Paragraph上你已经比大部分人做得专业了。我在实际使用中的体会是Paragraph的API并不难难的是对它“块级布局”角色的理解。很多排版问题不是代码写得不对而是没有按照Paragraph的脾气来写。摸清楚字体、缩进、行距、分页这几件事你的PDF导出功能基本就赢了一半。下次再有人问itextpdf paragraph使用你可以直接把这篇文章甩给他至少能帮他少踩几个我当年踩过的坑。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑