iText7高级教程之构建基础块——4.使用AbstractElement对象(part 1)

Posted CuteXiaoKe

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iText7高级教程之构建基础块——4.使用AbstractElement对象(part 1)相关的知识,希望对你有一定的参考价值。

  在之前的章节中,我们讨论了实现AbstractElement类的5个类。在章节2中,我们讨论了AreaBreak类。在章节3中我们讨论了实现ILeafElement类的4个类——TabLinkTextImage类。在这章节,我们会着重于其他一系列实现AbstractElement的类。如用于分组元素的Div类和用于在元素之间划线的LineSeparator。我们在之前的章节中已经多次使用过Paragraph类,但是在本章中我们将会重新讨论它。最后我们将讨论List类和ListItem类,而TableCell类将会在下一章节中讲述。

1. 使用Div类分组元素

  Div类是BlockElement类的实现,可以用来分组不同的元素。在图4.1中,我们可以看到Jekyll and Hyde系列电影的概览。其中每个条目包含最多三种元素:

  • 一个Paragraph显示电影的标题;
  • 一个Paragraph显示导演、国家和年份;
  • 一个Image显示电影海报(如果有的话);

  我们将这个三个元素组合在一个Div中,并为这个Div定义了左边框、左填充和下边距。

图4.1 在一个div中组合元素

  整体代码如下:

public void createPdf(String dest) throws IOException 
    PdfDocument pdf = new PdfDocument(new PdfWriter(dest));
    Document document = new Document(pdf);
    List> resultSet = CsvTo2DList.convert(SRC, "|");
    resultSet.remove(0);
    for (List record : resultSet) 
        Div div = new Div()
            .setBorderLeft(new SolidBorder(2))
            .setPaddingLeft(3)
            .setMarginBottom(10);
        String url = String.format(
            "https://www.imdb.com/title/tt%s", record.get(0));
        Link movie = new Link(record.get(2), PdfAction.createURI(url));
        div.add(new Paragraph(movie.setFontSize(14)))
            .add(new Paragraph(String.format(
                "Directed by %s (%s, %s)",
                record.get(3), record.get(4), record.get(1))));
        File file = new File(String.format(
            "src/main/resources/img/%s.jpg", record.get(0)));
        if (file.exists()) 
            Image img = new Image(
                ImageDataFactory.create(file.getPath()));
            img.scaleToFit(10000, 120);
            div.add(img);
        
        document.add(div);
    
    document.close();

  和往常一样,我们创建一个PdfDocument 和一个Document实例(第2-3行)。其中CSV文件我们重用了前一章节介绍的CSV文件,并遍历这个文件,列出所有电影,不包括标题行(行4-6)。我们创建另一个新的Div对象(行7),将它的左边框定义为2个用户单位的实心边框(行8),并将做填充设置为3个用户单位(行9),下边距设置为10个用户单位(行10)。紧接着我们电影标题Paragraph(行14)和其他信息(行15-17)添加到这个Div中。如果我们可以找到到电影海报,我们会添加Image对象(行24)。最后添加每一个DivDocument中并且关闭这个Document

  我们把目光放在第一页的底部和第二页的顶部,也就是电影标题是Dr. Jekyll and Mr. Hyde,导演是John S. RobersonDiv分布到两页上。电影海报在第一页上放不下,所以被放到了第二页。当然这可能不是我们所希望,也许我们希望添加到同一个Div的元素保持在一起,如图4.2所示:

图4.2 让一个div保持在一个页面

  我们只需要用一个额外的方法来做到如此,代码如下:

Div div = new Div()
    .setKeepTogether(true)
    .setBorderLeft(new SolidBorder(2))
    .setPaddingLeft(3)
    .setMarginBottom(10);

  通过添加setKeepTogether(true),告诉iText尝试将Div的内容保持在同一页面上。如果该Div的内容适合在下一页,则该Div中所有的元素将被转发到下一页。在图4.2中就是这种情况,上述我们提到Dr. Jekyll and Mr. Hyde电影的标题和信息被转发到下一页。

  如果Div的内容不适合下一页,那么此方法将会不起作用。在这种情况下,元素分部在当前页面和后续页面上,就好像未使用 setKeepTogether() 方法一样。 如果真的想将一个元素与下一个元素保持在同一页面上,有一种解决方法。 在我们讨论了 LineSeparator对象之后,我们将看一个演示此解决方法的示例。

2. 使用 LineSeparator对象绘制水平线

  相信大家也看到了哈,iText里面的构建块与html里面的标签很类似。Text大致对应 (空标签),Paragraph对应<p>标签,Div对应<div>标签,以此类推。同理LineSeparator最好的对应标签为<hr>图 4.3 显示了一条由红线组成的水平线,1 个用户单位粗,占可用宽度的 50%,为此定义了 5 个用户单位的上边距。

图4.3 添加分割线

  代码如下:

SolidLine line = new SolidLine(1f);
line.setColor(Color.RED);
LineSeparator ls = new LineSeparator(line);
ls.setWidthPercent(50);
ls.setMarginTop(5);

  首先我们传递了厚度参数来创建了一个SolidLine对象。在这里我们回顾一下,在上一章中,SolidLineILineDrawer 接口的实现之一。 然后将其颜色设置为红色,并使用此 ILineDrawer 创建 LineSeparator 实例。 接着使用 setWidthPercent() 方法定义线条的宽度。 当然我们也可以使用 setWidth() 方法来定义以用户单位表示的绝对宽度。 最后,我们将上边距设置为 5 个用户单位。

  我们将 ls 对象添加到包含电影信息的Div元素中。

div.add(ls);

  关于LineSeparator基本上就是这些内容。我们只需要记住我们应该使用正确的方法来设置属性。例如:在LineSeparator层面不能改变线的颜色,需要在ILineDrawer层面设置。显得宽度也是如此。我们可以查验附录B来看LineSeparator 类实现了哪些 AbstractElement 方法,以及忽略了哪些方法。

3. 让内容保持在一起

  在前面的示例中,我们已经多次使用 Paragraph 类。 例如:在第 2 章中,我们使用 Paragraph 类将文本文件转换为 PDF,方法是为文本文件中的每一行创建一个 Paragraph 对象,并将所有这些 Paragraph 对象添加到 Document 实例中 . 前几章的屏幕截图表明,我们可以制作一些非常好的 PDF 文档,但总有改进的余地。

  如图4.4中展示了我们需要修复的一个缺陷:在第3也上面有个标题,但是那这章的内容却在第4也上面。

图4.4 一个孤独的标题

  这种问题我们应该尽量避免,让标题和内容在同一页面上,首先让我们做第一次尝试,采用的代码如下:

public void createPdf(String dest) throws IOException 
    PdfDocument pdf = new PdfDocument(new PdfWriter(dest));
    Document document = new Document(pdf);
    PdfFont font = PdfFontFactory.createFont(FontConstants.TIMES_ROMAN);
    PdfFont bold = PdfFontFactory.createFont(FontConstants.HELVETICA_BOLD);
    document.setTextAlignment(TextAlignment.JUSTIFIED)
        .setHyphenation(new HyphenationConfig("en", "uk", 3, 3));
    BufferedReader br = new BufferedReader(new FileReader(SRC));
    String line;
    Div div = new Div();
    while ((line = br.readLine()) != null) 
        Paragraph title = new Paragraph(line)
            .setFont(bold).setFontSize(12)
            .setMarginBottom(0);
        div = new Div()
            .add(title)
            .setFont(font).setFontSize(11)
            .setMarginBottom(18);
        while ((line = br.readLine()) != null) 
            div.add(
                new Paragraph(line)
                    .setMarginBottom(0)
                    .setFirstLineIndent(36)
            );
            if (line.isEmpty()) 
                document.add(div);
                break;
            
        
    
    document.add(div);
    document.close();

  上面代码和我们第2章写的很相似,最主要的区别就是我们不再直接把Paragrah添加到Document中。相反,我们把Paragrah添加到Div中,然后在每个章节的末尾把Div添加到Paragrah中。但是最终我们发现并能做到我们想要的结果。

  我们可以在第 15 行和第 16 行之间尝试添加 .setKeepTogether(true) ,但这不会产生任何影响,因为 Div 里面全部内容在单个页面里面放不下。 这个也提到过了, setKeepTogether() 方法会被忽略。 iText 上就如何解决这个问题进行了长时间的研究, 最终定避免寡居对象的最优雅方法是引入 setKeepWithNext() 方法。
  setKeepWithNext() 方法是在 iText 7.0.1 中引入的。 第一个 iText 7 版本中找不到这个方法。 iText正在研究是否可以支持嵌套对象的方法, 但是不愿意这样做,因为这可能会对库的整体性能产生重大的负面影响。

  如下代码展示了如何使用:

BufferedReader br = new BufferedReader(new FileReader(SRC));
String line;
Div div = new Div();
while ((line = br.readLine()) != null) 
    document.add(new Paragraph(line)
        .setFont(bold).setFontSize(12)
        .setMarginBottom(0)
        .setKeepWithNext(true));
    div = new Div()
        .setFont(font).setFontSize(11)
        .setMarginBottom(18);
    while ((line = br.readLine()) != null) 
        div.add(
            new Paragraph(line)
                .setMarginBottom(0)
                .setFirstLineIndent(36)
        );
        if (line.isEmpty()) 
            document.add(div);
            break;
        
    

document.add(div);

  首先把一个Paragraph直接添加到Document(行5);然后创建一个Div来容纳章节剩余内容(行9)。通过添加 setKeepWithNext(true) 来表明 Paragraph 需要与 Div 的(第一部分)保持在同一页面上。 结果如图 4.5所示。 与图 4.4相比,标题“SEARCH FOR MR. HYDE”现在被转发到下一页。

图4.5 让标题和文本保持在一起

  setKeepWithNext() 方法可以与除 Cell 之外的所有其他 AbstractElement 的实现类一起使用。 该方法仅适用于直接添加到 Document 实例的元素。 它不适用于嵌套对象,例如始终添加到表格而不直接添加到文档的Cell。 在上面示例中,如果标题 Paragraph 被添加到 Div 而不是 Document,它将不起作用。

4. 更改段落行距

  Paragraph 类除了实现 AbstractElement 级别定义的方法之上还有一些额外的方法。 我们已经使用了上一章中涉及 TabStops 的方法(制表符/制表位)。 上面的例子里面还引用了 setFirstLineIndent() 方法(改变缩进)。 现在我们将目光放在如何研究行距。
  行距-英文leading这个词发音为ledding,它来自铅(金属)这个词。 当为印刷机手动设置字体时,铅条被放置在字体行之间以增加空间。 这个词最初指的是放置在线条之间的这些铅条的厚度。 PDF 标准将前导重新定义为“相邻文本行的基线之间的垂直距离”(ISO-32000-1,第 9.3.5 节)。

  有两种方法可以改变段落的行距:

  • setFixedLeading() —— 将行距更改为绝对值。 例如:如果定义 18 的固定行距,则两行文本的基线之间的距离将为 18 个用户单位。
  • setMultipliedLeading ——将行距更改为相对于字体大小的值。 例如,如果定义乘以 1.5f 的行距并且字体为 12 pt,那么前导将为 18 个用户单位(即 1.5 乘以 12)。

  这两个方法是相互排斥的。 如果在同一个段落中同时使用这两种方法,则以最后调用的方法为准。 图 4.6 展示了读取这个故事的另一种转换: 总页数较低,因为我们通过添加 .setMultipliedLeading(1.2f) 更改了行之间的距离。

图4.6 改变缩进和行距

  实现上述的代码除了以下代码片段以外,和之前的例子几乎一样:

div.add(
    new Paragraph(line)
        .setMarginBottom(0)
        .setFirstLineIndent(36)
        .setMultipliedLeading(1.2f)
);

  当我们直接或间接(例如通过Div)将对象添加到 Document 时,iText 使用适当的 IRenderer 将此对象呈现为 PDF。 在本系列第0章的图 4 里面展示了不同渲染器的概览。一般情况下我们使用 iText 几乎不需要创建自定义渲染器,但我们将看一个示例,在这个示例中我们创建了一个继承(extends)默认 ParagraphRendererMyParagraphRenderer

5. 创建自定义渲染器

  看如图4.7所示,有两个不同背景的Paragraphs。对于第一个Paragraph,我们使用了.setBackgroundColor()方法,这个方法基于段落的位置画了一个矩形,对于第二个Paragraph,我想要画一个圆角矩形。但是iText 7里面没有方法来做到,做到这我们需要写一个自定义ParagraphRenderer类。

图4.7 段落的默认和自定义背景

  让我们看看实现上述效果的代码片段,首先是第一个Paragraph,代码如此:

Paragraph p1 = new Paragraph(
    "The Strange Case of Dr. Jekyll and Mr. Hyde");
p1.setBackgroundColor(Color.ORANGE);
document.add(p1);

  然后是第二个Paragraph,代码如此:

Paragraph p2 = new Paragraph(
    "The Strange Case of Dr. Jekyll and Mr. Hyde");
p2.setBackgroundColor(Color.ORANGE);
p2.setNextRenderer(new MyParagraphRenderer(p2));
document.add(p2);

  其中用到的MyParagraphRenderer的定义如下:

class MyParagraphRenderer extends ParagraphRenderer 
    public MyParagraphRenderer(Paragraph modelElement) 
        super(modelElement);
    
    @Override
    public void drawBackground(DrawContext drawContext) 
        Background background =
            this.<Background>getProperty(Property.BACKGROUND);
        if (background != null) 
            Rectangle bBox = getOccupiedAreaBBox();
            boolean isTagged =
                drawContext.isTaggingEnabled()
                && getModelElement() instanceof IAccessibleElement;
            if (isTagged) 
                drawContext.getCanvas().openTag(new CanvasArtifact());
            
            Rectangle bgArea = applyMargins(bBox, false);
            if (bgArea.getWidth() <= 0 || bgArea.getHeight() <= 0) 
                return;
            
            drawContext.getCanvas().saveState()
                .setFillColor(background.getColor())
                .roundRectangle(
                (double)bgArea.getX() - background.getExtraLeft(),
                (double)bgArea.getY() - background.getExtraBottom(),
                (double)bgArea.getWidth()
                    + background.getExtraLeft() + background.getExtraRight(),
                (double)bgArea.getHeight()
                    + background.getExtraTop() + background.getExtraBottom(),
                5)
                .fill().restoreState();
            if (isTagged) 
                drawContext.getCanvas().closeTag();
            
        
    

  我们继承了现有的类ParagraphRenderer并且值覆写了一个方法。从AbstractRenderer类中copy了原始的drawBackground()方法,并且使用了roundRectangle()(行23)代替了rectangle()方法。行24-29里面我们注意到矩形的尺寸可以通过左侧、右侧、顶部和底部的额外空间进行微调。这些额外空间可以通过不同参数的setBackgroundColor()方法来设置(具体为带有4个额外浮点数的变量:extraLeft, extraTop, extraRight, 和extraBottom)。

  最后让我们看看ListListItem的例子并结束本篇章。

5. 列表和列表符号

  如图4.8显示了默认可用的不同类型的列表。其中有编号列表(罗马数字和阿拉伯数字)、带有字母的列表(小写、大写、拉丁、希腊)等等。

图4.8 不同类型的列表

  下面代码是前面三种列表如何添加:

List list = new List();
list.add("Dr. Jekyll");
list.add("Mr. Hyde");
document.add(list);
list = new List(ListNumberingType.DECIMAL);
list.add("Dr. Jekyll");
list.add("Mr. Hyde");
document.add(list);
list = new List(ListNumberingType.ENGLISH_LOWER);
list.add("Dr. Jekyll");
list.add("Mr. Hyde");
document.add(list);

  行1我们首先声明了没有类型的List。默认情况下,这将生成一个以连字符作(-)为列表符号的列表。 然后在第 2-3 行快速 添加了2个列表项; 然后我们将List添加到 Document(第 4 行)。之后就是多次重复这四行,例如定义一个十进制数字列表(第 5 行),定义一个带有小写字母的字母列表(第 9 行)。

  创建不同类型的列表的参数是一个枚举类:ListNumberingType,这个类有以下几种值:

  • DECIMAL——列表符号是阿拉伯数字:1、2、3、4、5、…
  • ROMAN_LOWER——列表符号是小写罗马数字:i、ii、iii、iv、v、…
  • ROMAN_UPPER——列表符号为大写罗马数字:I、II、III、IV、V、…
  • ENGLISH_LOWER —— 列表符号是小写字母(使用英文字母):a、b、c、d、e、…
  • ENGLISH_UPPER ——列表符号是大写字母(使用英文字母):A、B、C、D、E、…
  • GREEK_LOWER ——列表符号为小写希腊字母:α, β, γ, δ, ε,…
  • GREEK_UPPER ——列表符号为大写希腊字母:Α、Β、Γ、Δ、Ε、…
  • ZAPF_DINGBATS_1 ——列表符号是来自 Zapfdingbats 字体的项目符号,更具体地说是 [172; 181]范围内的字符。
  • ZAPF_DINGBATS_2——列表符号是 Zapfdingbats 字体的项目符号,更具体地说是 [182;191]范围内的字符。
  • ZAPF_DINGBATS_3 ——列表符号是来自 Zapfdingbats 字体的项目符号,更具体地说是 [192; 201]范围内的字符。
  • ZAPF_DINGBATS_4 ——列表符号是来自 Zapfdingbats 字体的项目符号,更具体地说是 [202; 221]范围内的字符。

  显然,我们也可以定义自己的自定义列表符号,或者我们可以使用默认列表符号(例如数字)的组合并将它们与前缀或后缀组合。 效果如图 4.9 所示。

图4.9 自定义列表符号

  首先我们看一下如何引入一个简单的项目符号作为列表符号,而不是默认的连字符。代码片段如下:

List list = new List();
list.setListSymbol("\\u2022");
list.add("Dr. Jekyll");
list.add("Mr. Hyde");
document.add(list);

  我们创建一个 List 并使用 setListSymbol() 方法来更改列表符号,其中可以使用任何String作为列表符号。在我们的例子中,我们想要一个像子弹一样的原点,对应的Unicode值为/u2022。 但是注意到子弹符号与列表项的内容非常接近。 做到这样可以通过使用 setSymbolIndent() 方法定义缩进来改变这一点,就像下面例子里面一样:

list = new List();
PdfFont font = PdfFontFactory.createFont(FontConstants.ZAPFDINGBATS);
list.setListSymbol(new Text("*").setFont(font).以上是关于iText7高级教程之构建基础块——4.使用AbstractElement对象(part 1)的主要内容,如果未能解决你的问题,请参考以下文章

iText7高级教程之构建基础块——5.使用AbstractElement对象(part 2)

iText7高级教程之构建基础块——5.使用AbstractElement对象(part 2)

iText7高级教程之构建基础块——3.使用ILeafElement实现类

iText7高级教程之构建基础块——3.使用ILeafElement实现类

iText7高级教程之构建基础块——2.添加内容到Canvas或Document

iText7高级教程之构建基础块——7.处理事件,设置阅读器首选项和打印属性