pdfHTML/iText 7: 在 <tfoot> 中打印 <table> start/end 行数

pdfHTML/iText 7: Print <table> start/end row count in <tfoot>

对于一个项目,我目前正在编写一个使用 pdfHTML 3.0.3 和 iText 7.1.14 的文档生成器。该文档包含一个显示 'items' 的 table。这些项目行可能永远不会真正适合一页,并且在大多数情况下会跨越许多页。

此table的第一列有一个项目编号,可能缺少项目编号(由于项目无效)。

我希望 table 在 <table><tfoot> 中显示第一个和最后一个项目编号,在理想的解决方案中,这个第一个和最后一个项目将被动态确定基于当前布局页面上打印的内容。

示例:https://i.imgur.com/X4cQ4HB.png(FROM 应显示数字 1,TO 应显示数字 5)。

似乎单独使用 HTML 和 CSS 是不可能的,因为它们不支持任何使用页面作为上下文的计数器(CSS 计数器似乎使用全局上下文,不是页面上下文)。

我认为可以基于TableRenderer编写一个渲染器,但我不知道从哪里开始。 iText 文档确实展示了如何创建您自己的渲染器的示例,但我似乎找不到与此问题相关的示例。

代码将在 Java 但移植到 C# 应该是将一些方法名称更改为以大写字母开头的问题。我的回答基于以下 HTML table:

的可视化表示

HTML代码:

<!DOCTYPE html>
<html>

<head>
  <style>
    table {
      border-collapse: collapse;
    }
    td {
      border: solid 1px black;
      page-break-inside: avoid;
    }
  </style>
</head>

<body>

<table>
  <colgroup>
    <col width="30%">
    <col width="70%">
  </colgroup>
  <tbody>
  <tr>
    <td>1</td>
    <td>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Varius quam quisque id diam. Malesuada proin libero nunc consequat interdum varius. Tristique sollicitudin nibh sit amet commodo nulla. Ac tortor dignissim convallis aenean et tortor at risus. Odio ut sem nulla pharetra diam sit amet nisl. Purus faucibus ornare suspendisse sed nisi lacus. Interdum posuere lorem ipsum dolor sit amet consectetur. Elementum facilisis leo vel fringilla est ullamcorper eget. Ac turpis egestas sed tempus urna et pharetra. Urna porttitor rhoncus dolor purus non enim praesent. Mattis ullamcorper velit sed ullamcorper morbi tincidunt ornare. Ipsum consequat nisl vel pretium lectus quam id. Eget nunc scelerisque viverra mauris in aliquam sem fringilla. At urna condimentum mattis pellentesque.

    </td>
  </tr>
  <tr>
    <td>2</td>
    <td>Iaculis at erat pellentesque adipiscing commodo. Sollicitudin ac orci phasellus egestas tellus rutrum. Posuere sollicitudin aliquam ultrices sagittis orci a scelerisque purus semper. A iaculis at erat pellentesque adipiscing commodo elit at. Nisl rhoncus mattis rhoncus urna neque viverra. Urna cursus eget nunc scelerisque viverra mauris in. Nunc aliquet bibendum enim facilisis gravida. Malesuada bibendum arcu vitae elementum curabitur vitae nunc. Elementum facilisis leo vel fringilla est ullamcorper eget nulla. Quis hendrerit dolor magna eget est lorem.

    </td>
  </tr>
  <tr>
    <td>3</td>
    <td>Eget velit aliquet sagittis id consectetur purus ut faucibus. Tortor condimentum lacinia quis vel eros. Elementum nibh tellus molestie nunc non blandit. Magna eget est lorem ipsum dolor sit amet. Gravida arcu ac tortor dignissim. Commodo viverra maecenas accumsan lacus vel. Vel fringilla est ullamcorper eget nulla facilisi etiam. Tellus in hac habitasse platea dictumst vestibulum. Lectus urna duis convallis convallis. Tincidunt ornare massa eget egestas purus viverra accumsan in nisl. Elementum tempus egestas sed sed risus pretium quam. Aenean pharetra magna ac placerat vestibulum lectus mauris ultrices. Ultrices vitae auctor eu augue ut lectus arcu. Placerat duis ultricies lacus sed turpis tincidunt. Tellus cras adipiscing enim eu turpis egestas pretium aenean. Tincidunt arcu non sodales neque sodales. Posuere ac ut consequat semper viverra nam libero justo laoreet. Turpis egestas integer eget aliquet nibh praesent tristique. Facilisis leo vel fringilla est ullamcorper eget nulla facilisi.

    </td>
  </tr>
  <tr>
    <td>4</td>
    <td>Sem integer vitae justo eget magna fermentum iaculis eu. Dolor sit amet consectetur adipiscing elit ut aliquam purus. Erat imperdiet sed euismod nisi. Scelerisque fermentum dui faucibus in ornare quam. Ipsum dolor sit amet consectetur adipiscing elit duis tristique sollicitudin. Semper quis lectus nulla at. Netus et malesuada fames ac turpis. Ornare suspendisse sed nisi lacus sed viverra tellus in. At urna condimentum mattis pellentesque id. Sit amet justo donec enim diam vulputate ut pharetra sit. Eget egestas purus viverra accumsan. In metus vulputate eu scelerisque felis imperdiet proin fermentum. Fermentum leo vel orci porta non pulvinar. Ut enim blandit volutpat maecenas. Ac tortor vitae purus faucibus ornare suspendisse sed nisi lacus. Bibendum ut tristique et egestas. In massa tempor nec feugiat nisl pretium fusce. Vitae ultricies leo integer malesuada nunc vel. Porttitor massa id neque aliquam. Elementum curabitur vitae nunc sed velit dignissim sodales ut.

      </td>
  </tr>
  <tr>
    <td>5</td>
    <td>Risus ultricies tristique nulla aliquet enim tortor. Bibendum enim facilisis gravida neque convallis a cras semper. Sit amet consectetur adipiscing elit ut aliquam purus sit amet. Nec nam aliquam sem et. Nullam eget felis eget nunc lobortis mattis. In tellus integer feugiat scelerisque varius morbi enim nunc faucibus. Vitae tempus quam pellentesque nec nam. Elit sed vulputate mi sit. Scelerisque fermentum dui faucibus in ornare quam. Lacus viverra vitae congue eu consequat ac felis donec. Sed velit dignissim sodales ut eu sem integer vitae justo. Enim nunc faucibus a pellentesque sit amet porttitor eget. Est ultricies integer quis auctor elit. Massa sed elementum tempus egestas sed sed risus pretium quam. Lectus magna fringilla urna porttitor rhoncus dolor. Viverra maecenas accumsan lacus vel facilisis volutpat est. Tristique et egestas quis ipsum suspendisse ultrices gravida dictum fusce. Eget lorem dolor sed viverra ipsum nunc. Eget arcu dictum varius duis at consectetur lorem donec. A diam sollicitudin tempor id eu nisl.

    </td>
  </tr>
  <tr>
    <td>6</td>
    <td>Lacinia at quis risus sed vulputate odio ut enim blandit. Tincidunt lobortis feugiat vivamus at augue eget. Duis convallis convallis tellus id interdum velit laoreet. Vitae turpis massa sed elementum. Quam vulputate dignissim suspendisse in est. Id faucibus nisl tincidunt eget nullam non nisi est sit. Enim neque volutpat ac tincidunt vitae semper quis lectus nulla. Purus in mollis nunc sed id semper risus in hendrerit. Faucibus nisl tincidunt eget nullam. Non enim praesent elementum facilisis leo vel fringilla. Nec ultrices dui sapien eget. Eleifend mi in nulla posuere sollicitudin aliquam ultrices sagittis. Quis enim lobortis scelerisque fermentum dui faucibus in ornare quam. Est velit egestas dui id ornare. Vehicula ipsum a arcu cursus vitae congue mauris rhoncus. Congue eu consequat ac felis donec.

    </td>
  </tr>
  <tr>
    <td>7</td>
    <td>Pulvinar sapien et ligula ullamcorper malesuada proin. Ac turpis egestas sed tempus urna. Nunc faucibus a pellentesque sit. Elit ut aliquam purus sit amet luctus. Etiam sit amet nisl purus in mollis nunc sed. At volutpat diam ut venenatis tellus in. Non pulvinar neque laoreet suspendisse interdum consectetur. Quam nulla porttitor massa id neque aliquam vestibulum morbi. Id volutpat lacus laoreet non curabitur gravida arcu ac. Facilisis sed odio morbi quis commodo odio aenean. Donec pretium vulputate sapien nec sagittis aliquam malesuada bibendum. Placerat orci nulla pellentesque dignissim. Sit amet mattis vulputate enim. Neque ornare aenean euismod elementum nisi quis. Proin libero nunc consequat interdum varius sit amet mattis vulputate. Eget egestas purus viverra accumsan in nisl nisi scelerisque eu. Penatibus et magnis dis parturient montes nascetur.

    </td>
  </tr>
  <tr>
    <td>8</td>
    <td>Tristique et egestas quis ipsum suspendisse ultrices gravida dictum. Consectetur lorem donec massa sapien faucibus et molestie. Cursus mattis molestie a iaculis at erat pellentesque adipiscing. Dui nunc mattis enim ut tellus elementum sagittis. Congue eu consequat ac felis donec et odio. Purus viverra accumsan in nisl nisi scelerisque eu. Lacus laoreet non curabitur gravida arcu ac tortor dignissim convallis. Tempor nec feugiat nisl pretium fusce id velit ut tortor. Ut faucibus pulvinar elementum integer enim. Egestas quis ipsum suspendisse ultrices gravida dictum fusce. Eu tincidunt tortor aliquam nulla facilisi cras fermentum. Rutrum tellus pellentesque eu tincidunt. Scelerisque eleifend donec pretium vulputate sapien nec.

    </td>
  </tr>
  <tr>
    <td>9</td>
    <td>Integer feugiat scelerisque varius morbi. Posuere sollicitudin aliquam ultrices sagittis orci a. Habitant morbi tristique senectus et netus et malesuada fames ac. Sed faucibus turpis in eu mi bibendum neque. Tortor id aliquet lectus proin. Enim sit amet venenatis urna cursus eget nunc scelerisque. Cras adipiscing enim eu turpis egestas pretium aenean pharetra. Volutpat diam ut venenatis tellus in metus vulputate. Senectus et netus et malesuada. Gravida cum sociis natoque penatibus et. Ut tristique et egestas quis ipsum suspendisse. At tempor commodo ullamcorper a. Mauris pharetra et ultrices neque ornare aenean euismod elementum. Massa ultricies mi quis hendrerit dolor magna.

    </td>
  </tr>
  <tr>
    <td>10</td>
    <td>In eu mi bibendum neque egestas congue quisque egestas diam. Hendrerit gravida rutrum quisque non tellus. Posuere morbi leo urna molestie at. Turpis egestas pretium aenean pharetra magna ac placerat. Vel pharetra vel turpis nunc eget lorem dolor. Lorem sed risus ultricies tristique nulla aliquet enim tortor. Purus ut faucibus pulvinar elementum integer enim neque volutpat ac. Consectetur adipiscing elit pellentesque habitant morbi tristique senectus et. Proin libero nunc consequat interdum varius sit amet mattis vulputate. Elementum pulvinar etiam non quam lacus. Egestas egestas fringilla phasellus faucibus. Vel pretium lectus quam id leo in vitae turpis. Bibendum ut tristique et egestas. Morbi non arcu risus quis varius. Morbi tristique senectus et netus. Sed id semper risus in hendrerit gravida rutrum quisque. Luctus venenatis lectus magna fringilla urna. Sed turpis tincidunt id aliquet.

    </td>
  </tr>
  <tr>
    <td>11</td>
    <td>Integer feugiat scelerisque varius morbi. Posuere sollicitudin aliquam ultrices sagittis orci a. Habitant morbi tristique senectus et netus et malesuada fames ac. Sed faucibus turpis in eu mi bibendum neque. Tortor id aliquet lectus proin. Enim sit amet venenatis urna cursus eget nunc scelerisque. Cras adipiscing enim eu turpis egestas pretium aenean pharetra. Volutpat diam ut venenatis tellus in metus vulputate. Senectus et netus et malesuada. Gravida cum sociis natoque penatibus et. Ut tristique et egestas quis ipsum suspendisse. At tempor commodo ullamcorper a. Mauris pharetra et ultrices neque ornare aenean euismod elementum. Massa ultricies mi quis hendrerit dolor magna.

    </td>
  </tr>
  <tr>
    <td>12</td>
    <td>Tristique et egestas quis ipsum suspendisse ultrices gravida dictum. Consectetur lorem donec massa sapien faucibus et molestie. Cursus mattis molestie a iaculis at erat pellentesque adipiscing. Dui nunc mattis enim ut tellus elementum sagittis. Congue eu consequat ac felis donec et odio. Purus viverra accumsan in nisl nisi scelerisque eu. Lacus laoreet non curabitur gravida arcu ac tortor dignissim convallis. Tempor nec feugiat nisl pretium fusce id velit ut tortor. Ut faucibus pulvinar elementum integer enim. Egestas quis ipsum suspendisse ultrices gravida dictum fusce. Eu tincidunt tortor aliquam nulla facilisi cras fermentum. Rutrum tellus pellentesque eu tincidunt. Scelerisque eleifend donec pretium vulputate sapien nec.

    </td>
  </tr>
  </tbody>
  <tfoot>
  <tr><td colspan="2">from <span id="from">dummy</span> to <span id="to">dummy</span></td></tr>
  </tfoot>
</table>

</body>
</html>

请注意,我们在这里使用 page-break-inside: avoid; CSS 声明是为了使单元格不被分页(这会使我们更难确定要在页脚)。

另请注意,我们的 <tfoot> 元素中有一个特殊结构:

  <tfoot>
  <tr><td colspan="2">from <span id="from">dummy</span> to <span id="to">dummy</span></td></tr>
  </tfoot>

我们标记了 <span> 占位符,这些占位符将用我们自定义的 id 填充范围编号。如果您不能完全控制用于生成 PDF 的 HTML,那么您可以执行一些预处理步骤以将所需的 <tfoot> 内容注入您的 HTML.

我们需要在 HTML 到 PDF 的转换中自定义一些渲染行为,起点是自定义传递给 ConverterProperties:

的标签工作者工厂
ConverterProperties converterProperties = new ConverterProperties();
converterProperties.setTagWorkerFactory(new CustomTagWorkerFactory());
HtmlConverter.convertToPdf(new File(sourceHtml), new File(targetPDF), converterProperties);

在我们的标签工作者工厂中,我们需要对上述 <span> 占位符以及通用 <table> 元素进行自定义处理(请注意,如果您在其中有多个 table你的 HTML 那么你可能想要区分你在标签工作器中处理的 table 并且只为你需要在页脚中添加范围的 table 添加自定义)。

private static final class CustomTagWorkerFactory extends DefaultTagWorkerFactory {
    @Override
    public ITagWorker getCustomTagWorker(IElementNode tag, ProcessorContext context) {
        if (tag.name().equals("span") && ("from".equals(tag.getAttribute("id")) || "to".equals(tag.getAttribute("id")))) {
            return new ElementCounterTagWorker(tag, context);
        } else if (tag.name().equals("table")) {
            return new CustomTableTagWorker(tag, context);
        }
        return super.getCustomTagWorker(tag, context);
    }
}

自定义 table 标记工作器非常基本 - 它只是将自定义渲染器设置为 table 的页脚(渲染器本身将在下面定义):

private static class CustomTableTagWorker extends TableTagWorker {
    public CustomTableTagWorker(IElementNode element, ProcessorContext context) {
        super(element, context);
    }

    @Override
    public IPropertyContainer getElementResult() {
        IPropertyContainer table = super.getElementResult();
        ((Table)table).getFooter().setNextRenderer(new CustomTableFooterRenderer(((Table) table).getFooter()));
        return table;
    }
}

<span> 元素的标记工作器也旨在为我们的 <span> 占位符定义自定义渲染器,但由于 HTML 的映射方式,实现起来有点困难元素到 iText 布局元素执行。请注意,我们还将占位符的类型(来自 id 属性)保存到自定义渲染器:

private static class ElementCounterTagWorker extends SpanTagWorker {
    private IElementNode element;
    public ElementCounterTagWorker(IElementNode element,
            ProcessorContext context) {
        super(element, context);
        this.element = element;
    }

    @Override
    public List<IPropertyContainer> getAllElements() {
        List<IPropertyContainer> elements = super.getAllElements();
        for (IPropertyContainer elem : elements) {
            if (elem instanceof Text) {
                ((Text) elem).setNextRenderer(new TextCounterRenderer((Text) elem, element.getAttribute("id")));
            }
        }
        return elements;
    }
}

现在最有趣的部分是 table 和我们的 span 占位符的渲染器定义。这个想法是,在 span 元素的布局期间,该元素在 table 页脚渲染器(它是链中的父元素之一)中注册自己。然后 table 页脚渲染器将在绘制内容之前替换 span 元素的内容,此时已经定义了确切的布局位置。

这里是我们 <span> 占位符中文本的自定义呈现器的定义方式(请注意,您必须同时覆盖 getNextRenderer()createCopy():

private static class TextCounterRenderer extends TextRenderer {
    String type;

    public TextCounterRenderer(Text textElement, String type) {
        super(textElement);
        this.type = type;
    }

    protected TextCounterRenderer(TextCounterRenderer other) {
        super(other);
        this.type = other.type;
    }

    @Override
    public LayoutResult layout(LayoutContext layoutContext) {
        IRenderer tableRenderer = parent;
        while (!(tableRenderer instanceof TableRenderer)) {
            tableRenderer = tableRenderer.getParent();
        }
        ((CustomTableFooterRenderer)tableRenderer).register(this, layoutContext);
        return super.layout(layoutContext);
    }

    @Override
    public IRenderer getNextRenderer() {
        return new TextCounterRenderer((Text) getModelElement(), type);
    }

    @Override
    protected TextRenderer createCopy(GlyphLine gl, PdfFont font) {
        TextCounterRenderer copy = new TextCounterRenderer(this);
        copy.setProcessedGlyphLineAndFont(gl, font);
        return copy;
    }
}

最后,实现我们的 table 页脚渲染器。我们的想法是跟踪所有已注册的文本渲染器(我们用于插入范围的占位符),然后在我们即将绘制 table 页脚之前,分析我们 table 的内容即将绘制以找到将在此页面上绘制的行的范围并将这些范围注入我们的占位符。

private static class CustomTableFooterRenderer extends TableRenderer {
    private Set<Pair<TextCounterRenderer, LayoutContext>> textCounters = new HashSet<>();

    public CustomTableFooterRenderer(Table modelElement) {
        super((Table)modelElement);
    }

    public CustomTableFooterRenderer(Table modelElement, RowRange rowRange) {
        super(modelElement, rowRange);
    }

    public void register(TextCounterRenderer renderer, LayoutContext context) {
        Pair<TextCounterRenderer, LayoutContext> textNode = new Pair<>(renderer, context);
        if (!textCounters.contains(textNode)) {
            textCounters.add(textNode);
        }
    }

    @Override
    public void draw(DrawContext drawContext) {
        for (Pair<TextCounterRenderer, LayoutContext> counter : textCounters) {
            IRenderer counterParent = counter.getKey().getParent();
            while (counterParent != null && counterParent != this) {
                counterParent = counterParent.getParent();
            }
            if (counterParent == this) {
                TableRenderer tableRenderer = (TableRenderer) this.getParent();
                int columnCount = 2;
                IRenderer firstRowFirstCellRenderer = tableRenderer.getChildRenderers().get(0);
                IRenderer lastRowFirstCellRenderer = tableRenderer.getChildRenderers().get(tableRenderer.getChildRenderers().size() - columnCount);
                if ("from".equals(((TextCounterRenderer)counter.getKey()).type)) {
                    counter.getKey().setText(firstRowFirstCellRenderer.toString());
                } else {
                    counter.getKey().setText(lastRowFirstCellRenderer.toString());
                }
                counter.getKey().layout(counter.getValue());
            }
        }
        super.draw(drawContext);
    }

    @Override
    public IRenderer getNextRenderer() {
        return new CustomTableFooterRenderer((Table) modelElement, rowRange);
    }
}

总而言之,我们在 PDF 中得到以下结果:

请注意,不能 100% 保证此解决方案适用于所有未来的 iText 版本,因为它取决于一些实现细节,但它应该适用于 iText 7.1.15 行,并且它给出了一个很好的主意在哪里在 iText 中寻找通用布局自定义,包括 HTML 到 PDF 转换上下文和纯布局模块用法。