在 Itext7 中使用自定义字体定位文本有些奇怪

Something strange with positioning text with custom font in Itext7

我正在开发一个程序,它可以创建多个 pdf 文档并将不同的文本放在它们的相同位置。 文本应放置在特定区域,如果宽度不适合它,则应换行。它还具有自定义字体,并且可能在该区域以不同方式对齐。它应该与顶部垂直对齐,因为当该区域布局为三行而我只有一行时,它应该出现在顶部。最后,我需要在字体大小级别上保持领先。

准确定位文本很重要(例如,我需要 "Hello world" 中 "H" 的左上角明确出现在 0、100 处)。

现在,我正在使用

canvas.showTextAligned(paragraph, 0, 300,
                    TextAlignment.valueOf(alignment),
                    VerticalAlignment.TOP);

然而,当我尝试用不同的字体实现它时,它与所需的 y = 300 有不同的偏移量。此外,偏移量因字体而异。对于 Helvetica(所有使用 50 fontSize 的地方)偏移量约为 13 px,对于 Oswald 约为 17 px,而对于 SedgwickAveDisplay 则为 90 px。 为了调试目的,我给段落添加了边框,事情变得更奇怪了。

黑体字: SedgwickAve 显示:

我创建 pdf 的完整代码片段如下:

public byte[] createBadgeInMemory(int i) throws IOException {
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    PdfDocument newPdf = new PdfDocument(new PdfWriter(out));
    srcPdf.copyPagesTo(1,1,newPdf);
    PdfPage page = newPdf.getFirstPage();
    PdfCanvas pdfCanvas = new PdfCanvas(page);
    Canvas canvas = new Canvas(pdfCanvas, newPdf, pageSize);

    File defaultFont = new File("src/main/resources/fonts/Helvetica.otf");
    PdfFont font  = PdfFontFactory
            .createFont(fontPath == null ? defaultFont.getAbsolutePath() : fontPath,
                    PdfEncodings.IDENTITY_H, true);

    String value = "Example word";
    Paragraph paragraph = new Paragraph(value);
    float textWidth = font.getWidth("Example", 50);
    paragraph.setWidth(textWidth);
    switch (alignment) {
        case("CENTER"):
            textWidth /= 2;
            break;
        case("RIGHT"):
            break;
        default:
            textWidth = 0;
            break;
    }

    paragraph.setFont(font)
            .setFontSize(fontSize)
            .setFixedLeading(fontSize)
            .setFontColor(new DeviceRgb(red, green, blue))
            .setMargin(0)
            .setPadding(0);
    paragraph.setBorderTop(new DashedBorder(Color.BLACK, 0.5f))
            .setBorderBottom(new DashedBorder(Color.BLACK, 0.5f))
            .setBorderRight(new DashedBorder(Color.BLACK, 0.5f));
    paragraph.setHyphenation(new HyphenationConfig(0,
            "Example".length()));

    canvas.showTextAligned(paragraph,
            0 + textWidth,
            300,
            TextAlignment.valueOf(alignment),
            VerticalAlignment.TOP);

    newPdf.close();

    return out.toByteArray();
}

我也尝试了 here 的变体,但出于某种原因,矩形内的文本在某些时候被切掉了(例如,如果我的区域宽度为 100px 并且我输入的文本片段我知道正好占据 100 px(在 font.getWidth(value) 的帮助下),我将我的文本剪切了将近 80 px)。 我也没有找到对齐矩形内文本的方法。 这是矩形的结果。实线边框是矩形边框。如您所见,它在 "Redundant" 中剪切了字母 "t"。它还应该在第二行包含 "word",但实际上没有。 我从一个例子中复制了代码。

我需要你的帮助。我做错了什么或者可能有另一种方法可以使用对齐方式和字体在特定区域布局文本? 先感谢您。

更新 21.09.17 还使用 SedgwickAveDisplay 尝试了 的变体:

paragraph.setHorizontalAlignment(HorizontalAlignment.LEFT);
            paragraph.setVerticalAlignment(VerticalAlignment.TOP);
            paragraph.setFixedPosition( 0, 300 - textHeight, "Example".length());
            doc.add(paragraph);

结果与第二张截图相同。

这是一个特定于字体的问题。 iText 猜测有关字体字形的信息,即不正确的 bboxes。

有一种快速而简单的方法可以调整此行为。您可以为文本创建自定义渲染器并调整其中的计算位置。这种 class 的示例如下:

class CustomTextRenderer extends TextRenderer {
    private CustomTextRenderer(Text text) {
        super(text);
    }

    @Override
    public LayoutResult layout(LayoutContext layoutContext) {
        LayoutResult result = super.layout(layoutContext);
        Rectangle oldBbox = this.occupiedArea.getBBox().clone();
        // you can also make the height more than font size or less if needed
        this.occupiedArea.setBBox(oldBbox.moveUp(oldBbox.getHeight() - fontSize).setHeight(fontSize)); 
        yLineOffset = fontSize * 0.8f; // here you config the proportion of the ascender
        return result;
    }

    @Override
    public IRenderer getNextRenderer() {
        return new CustomTextRenderer((Text) modelElement);
    }
}

为了应用新的呈现逻辑,您必须按以下方式使用它:

Text text = new Text(value);
text.setNextRenderer(new CustomTextRenderer(text));
Paragraph paragraph = new Paragraph(text);

请注意,您必须非常小心这种低级布局,注意您正在做的事情并尽可能少使用它。

最后,我创建了一个适合我的变体。

pdfCanvas.beginText()
                .setFontAndSize(font, fontSize)
                .setLeading(fontSize)
                .moveText(0, 300);

        numberOfLines = 0;
        sumOfShifts = 0;
        float maxWidth = computeStringWidth("Exaxple");
        String[] words = value.split("\s");
        StringBuilder line = new StringBuilder();
        line.append(words[0]);
        float spaceWidth = computeStringWidth(" ") ;
        float lineWidth;
        for (int index = 1; index < words.length; index++) {
            String word = words[index];
            float wordWidth = computeStringWidth(word) ;
            lineWidth = computeStringWidth(line.toString()) ;
            if (lineWidth + spaceWidth + wordWidth <= maxWidth) {
                line.append(" ").append(word);
            } else {
                showTextAligned(alignment, pdfCanvas, line.toString(), lineWidth, maxWidth);
                line.delete(0, line.length());
                line.append(word);
            }
        }
        if(line.length() != 0) {
            lineWidth = computeStringWidth(line.toString()) ;
            showTextAligned(alignment, pdfCanvas, line.toString(), lineWidth, maxWidth);
        }

        pdfCanvas.endText();

作为computeStringWidth(String str)我用了

Toolkit.getToolkit().getFontLoader().computeStringWidth(String str, Font font);

来自 import com.sun.javafx.tk.Toolkit,字体来自 javafx.scene.text.Font。我选择它是因为我在我的应用程序的其他部分使用它。

showTextAligned(...) 是我自己的方法,看起来是这样的:

private void showTextAligned(String alignment,
                                  PdfCanvas pdfCanvas,
                                  String line,
                                  float lineWidth,
                                  float maxWidth) {
        switch (alignment) {
            case "CENTER": {
                float shift = (maxWidth - lineWidth) / 2 - sumOfShifts;
                pdfCanvas.moveText(shift, 0);
                if(numberOfLines == 0) pdfCanvas.showText(line);
                else pdfCanvas.newlineShowText(line);
                numberOfLines++;
                sumOfShifts += shift;
                break;
            }
            case "RIGHT": {
                float shift = maxWidth - lineWidth - sumOfShifts;
                pdfCanvas.moveText(shift, 0);
                if(numberOfLines == 0) pdfCanvas.showText(line);
                else pdfCanvas.newlineShowText(line);
                numberOfLines++;
                sumOfShifts += shift;
                break;
            }
            default:
                pdfCanvas.moveText(0, 0);
                if(numberOfLines == 0) pdfCanvas.showText(line);
                else pdfCanvas.newlineShowText(line);
                numberOfLines++;
                break;
        }
    }

在我的项目中,我使用了我的变体,因为它让我有机会更深入地使用连字符(例如,我可以在未来添加功能以避免将介词作为行中的最后一个词)。