Graphics2D绘制字符串文本自动换行

一、Graphics2D

Graphics2D继承了Graphics提供了对几何形状,坐标转换,颜色管理和文本布局更为复杂的控制,它是在java平台上呈现二维形状、文本和图像的基础类,相对于整个java体系,AWT变成在web方便应用不是很多,常见的使用场景比如:很古老的验证码图形就是通过随机数字旋转角度,画干扰线来实现的,或者给图片添加一些水印、处理一些图片加工的操作
最近要操作图片,通过logo、分享二维码等一些乱七八糟的信息,合成一张图片,主要涉及文本内容的自动换行实现,相当于画布上画一个文本框,里面的内容自动换行展示,但是发现jdk提供的drawString()并没有自动换行的功能,hutool包里也没有相关的工具,所以自己造轮子实现一下。

二、画图片

比如这里有一张图片,需要放到另一种图片或者画板里面,比如分享图片的logo图片,分享二维码,或者一些生成证书的公章图片之类的,就需要图片操作。

参数为x坐标,y坐标,矩形的宽度,矩形的高度,这里画图片为填满格式,所以这个矩形的宽高就是图片的宽高,

ClassPathResource resource = new ClassPathResource("static/template.jpg");
        BufferedImage template = ImgUtil.read(resource.getInputStream());
        Graphics2D g = template.createGraphics();

        String picUrl = "https://wenxin.baidu.com/younger/file/ERNIE-ViLG/68197b9ea840a0d1ed7c2ef538599bce30";
        BufferedImage image = ImgUtil.read(URLUtil.url(picUrl));
        g.drawImage(image, 0, 0, image.getWidth(), image.getHeight(), null);
        g.dispose();
        File file = new File("/Users/liufuqiang/Downloads/baiduImg/" + DateUtil.current() + "test.jpg");
        ImgUtil.write(template, file);

这里以左上角为坐标原点,将picUrl对应的图片画到另一张图片中

可以看到生成的图片左上角与画布左上角正好重合,常见的操作比如将图片居中展示,只需要将x的坐标稍微处理一些,x = (画布宽度 - 图片宽度) / 2,y坐标同理

int x = (template.getWidth() - image.getWidth()) / 2;
        g.drawImage(image, x, 0, image.getWidth(), image.getHeight(), null);

三、画文字


这里的方法跟图片的类似,需要提供需要绘制的文本,以及x、y坐标。
因为画文字,需要设置文字的字体,如需要还可设置文字的颜色

![](https://img2023.cnblogs.com/blog/1597479/202212/1597479-20221204221935128-1231584936.png)

如果想将文本画在左上角,我们将x,y都设为0试一下,

发现整个文本区域只显示了最下面的一部分,上面部分好像被截取了,这里不同于图片的绘制,图片被当作是一个矩形区域,就是以左上角为坐标,但是文字因为字体的不同,所以x,y的值不是以文字为矩形区域左上角为坐标进行计算的。前戏来啦:
字体排印学中,字体在垂直方向有6个重要的概念,分别是:升部(Ascender)、降部(Descender)、基线(BaseLine)、大写高度(Cap height)、主线(Mean Line)和x字高度(x-height),这几个字在字体中的表现如图:

基线(Baseline)是多数字母排列的基准线,如图,大多数字母都沿着基线排列,不会超过基线。
x字高(x-height)主线与基线之间的距离叫x字高
主线(Mean Line)是决定无升部的小写字母的一条线
降部(Descender)在西文字体排印学中指的是一个字体中,字母向下延伸超过基线的笔画部分,也称为下延部。像图中字母“p”向下延伸,超过了基线,超过的那部分叫降部
升部(Ascender)是指一个字体中向上超过主线笔画的部分,向“h”超过主线的部分叫做升部
大写高度(Cap height)是指一种字体中,大写字母最顶端到基线的高度
所以这里绘制文字的时候x、y坐标是根据基线的位置来定的
如果需要计算基线的位置,可以根据字体的工具来进行计算

FontMetrics metrics = g.getFontMetrics(font);
metrics.getAscent();
metrics.getDescent();

最后经过测试基线位置为metrics.getAscent() - metrics.getDescent() - metrics.getLeading();
我们一般在花文本的时候需要各种计算距离,边距等,所以封装了一个方法,可以自动换行,实现文本框,超过的内容...显示

/**
     * 添加文字
     * <pre>
     *     在某个固定宽度的画板上添加文字,文字超过宽度限制自动换行
     *     其余内容如果超过最大行数,...显示
     * </pre>
     * @param g       画板
     * @param font    文字字体
     * @param text    文字
     * @param width   宽度限制
     * @param marginTop  距离顶部高度
     * @param indentWidth  首行缩进距离
     * @param lineHeight 行高
     * @param maxRow  最大行数
     * @return 返回文字所占用的高度,用于后续操作画板
     */
    private static Dimension drawWrapString(Graphics g, Font font, String text, int width, int marginTop,
                                            int marginLeft, int indentWidth, int lineHeight, int maxRow) {
        String prompt = StrUtil.maxLength(text, 1000);
        FontMetrics metrics = g.getFontMetrics(font);
        int length = metrics.stringWidth(prompt);
        int height = metrics.getAscent() - metrics.getLeading() - metrics.getDescent();
        int row = 1;
        int top = (lineHeight - height) / 2;
        int paddingTop = marginTop;
        int firstRowWidth = width - indentWidth;
        if (length > firstRowWidth) {
            int rowLen = 0;
            int lastIndex = 0;
            for (int i = 0; i < prompt.length(); i++) {
                rowLen += metrics.charWidth(prompt.charAt(i));
                int fontMarginHeight = row == 1 ? height + top : lineHeight + top;
                int rowWidth = row == 1 ? firstRowWidth : width;
                int leftWidth = row == 1 ?  indentWidth + marginLeft : marginLeft;
                if (rowLen >= rowWidth - 15) {
                    if (row == maxRow) {
                        String lastRowTxt = i == prompt.length() - 1 ?
                                prompt.substring(lastIndex, i + 1) : prompt.substring(lastIndex, i) + "...";
                        g.drawString(lastRowTxt, leftWidth, paddingTop += fontMarginHeight);
                        break;
                    }
                    rowLen = 0;
                    g.drawString(prompt.substring(lastIndex, i + 1), leftWidth, paddingTop += fontMarginHeight);
                    row++;
                    lastIndex = i + 1;
                }
                if (i == prompt.length() - 1) {
                    g.drawString(prompt.substring(lastIndex), leftWidth, paddingTop += fontMarginHeight);
                }
            }
        } else {
            int leftWidth = row == 1 ?  indentWidth + marginLeft : marginLeft;
            g.drawString(prompt, leftWidth, paddingTop += top + height);
        }
        int actualWidth = maxRow == 1 ? firstRowWidth : width;
        return new Dimension(length > actualWidth ? actualWidth : length,paddingTop + top - marginTop);
    }

代码未优化,但是实现基本的功能,计算每个字符的宽度,如果足够一行则进行绘制并且开始换行画下一行,只到满足所需要的最大行数展示

比如绘制单行文本:

绘制多行文本,最多四行

绘制多行文本,最多两行展示

绘制多行文本,长度不足三行展示

绘制多行文本,首行缩进100px

绘制多行文本,最大宽度为400,行高改为50

绘制不同内容同行展示

⚠️注意事项
这里绘制文字,一般会根据设计给出的字体,比如我们引用字体可能酱紫:

Font font = new Font("宋体", Font.PLAIN, 51)

写完本地测,上测试环境测,没啥问题,可一到生产上各种报错,一查原来是linux服务器未安装指定的字体,或者服务器过期了,需要迁移到新的服务器,程序迁移过去一运行还是会报错,一个坑会在无意间踩很多次,所以这里将字体文件放入项目文件resources里面,创建字体的方法写入static静态方法类,写入static的目的就是在类类创建的时候执行一次,即字体只创建一次,不用在每次调用方法时在new 一个字体对象。

private static Font PROMPT_FONT;
static {
ClassPathResource resource = new ClassPathResource("static/MiSans-Demibold.ttf");
        try {
            Font font = Font.createFont(Font.TRUETYPE_FONT, resource.getInputStream());
            PROMPT_FONT = font.deriveFont(Font.PLAIN, 51);
        } catch (Exception e) {
            PROMPT_FONT = new Font(null, Font.PLAIN, 51);
            log.error("font1 error", e);
        }
}

像这样不管怎样基本不会出现字体创建字体失败的情况,除非有人手动把字体文件给误删了,经过测试这种创建字体方式除了第一次加载字体会比new Font("")方式创建字体快很多,原因也很简单,后者会扫描系统所有的字体,来找到所需要的字体

参考文档
https://www.uisdc.com/english-font-typesetting-guideline

posted @ 2022-12-04 22:32  木马不是马  阅读(1585)  评论(0编辑  收藏  举报