Java如何实现长图文生成的示例代码
很久很久以前,就觉得微博的长图文实现得非常有意思,将排版直接以最终的图片输出,收藏查看分享都很方便,现在则自己动手实现一个简单版本的
目标
首先定义下我们预期达到的目标:根据文字 + 图片生成长图文
目标拆解
- 支持大段文字生成图片
- 支持插入图片
- 支持上下左右边距设置
- 支持字体选择
- 支持字体颜色
- 支持左对齐,居中,右对齐
预期结果
我们将通过spring-boot搭建一个生成长图文的http接口,通过传入参数来指定各种配置信息,下面是一个最终调用的示意图
设计&实现
长图文的生成,采用awt进行文字绘制和图片绘制
1. 参数选项 imgcreateoptions
根据我们的预期目标,设定配置参数,基本上会包含以下参数
@getter @setter @tostring public class imgcreateoptions { /** * 绘制的背景图 */ private bufferedimage bgimg; /** * 生成图片的宽 */ private integer imgw; private font font = new font("宋体", font.plain, 18); /** * 字体色 */ private color fontcolor = color.black; /** * 两边边距 */ private int leftpadding; /** * 上边距 */ private int toppadding; /** * 底边距 */ private int bottompadding; /** * 行距 */ private int linepadding; private alignstyle alignstyle; /** * 对齐方式 */ public enum alignstyle { left, center, right; private static map<string, alignstyle> map = new hashmap<>(); static { for(alignstyle style: alignstyle.values()) { map.put(style.name(), style); } } public static alignstyle getstyle(string name) { name = name.touppercase(); if (map.containskey(name)) { return map.get(name); } return left; } } }
2. 封装类 imagecreatewrapper
封装配置参数的设置,绘制文本,绘制图片的操作方式,输出样式等接口
public class imgcreatewrapper { public static builder build() { return new builder(); } public static class builder { /** * 生成的图片创建参数 */ private imgcreateoptions options = new imgcreateoptions(); /** * 输出的结果 */ private bufferedimage result; private final int addh = 1000; /** * 实际填充的内容高度 */ private int contenth; private color bgcolor; public builder setbgcolor(int color) { return setbgcolor(colorutil.int2color(color)); } /** * 设置背景图 * * @param bgcolor * @return */ public builder setbgcolor(color bgcolor) { this.bgcolor = bgcolor; return this; } public builder setbgimg(bufferedimage bgimg) { options.setbgimg(bgimg); return this; } public builder setimgw(int w) { options.setimgw(w); return this; } public builder setfont(font font) { options.setfont(font); return this; } public builder setfontname(string fontname) { font font = options.getfont(); options.setfont(new font(fontname, font.getstyle(), font.getsize())); return this; } public builder setfontcolor(int fontcolor) { return setfontcolor(colorutil.int2color(fontcolor)); } public builder setfontcolor(color fontcolor) { options.setfontcolor(fontcolor); return this; } public builder setfontsize(integer fontsize) { font font = options.getfont(); options.setfont(new font(font.getname(), font.getstyle(), fontsize)); return this; } public builder setleftpadding(int leftpadding) { options.setleftpadding(leftpadding); return this; } public builder settoppadding(int toppadding) { options.settoppadding(toppadding); contenth = toppadding; return this; } public builder setbottompadding(int bottompadding) { options.setbottompadding(bottompadding); return this; } public builder setlinepadding(int linepadding) { options.setlinepadding(linepadding); return this; } public builder setalignstyle(string style) { return setalignstyle(imgcreateoptions.alignstyle.getstyle(style)); } public builder setalignstyle(imgcreateoptions.alignstyle alignstyle) { options.setalignstyle(alignstyle); return this; } public builder drawcontent(string content) { // xxx return this; } public builder drawimage(string img) { bufferedimage bfimg; try { bfimg = imageutil.getimagebypath(img); } catch (ioexception e) { log.error("load draw img error! img: {}, e:{}", img, e); throw new illegalstateexception("load draw img error! img: " + img, e); } return drawimage(bfimg); } public builder drawimage(bufferedimage bufferedimage) { // xxx return this; } public bufferedimage asimage() { int realh = contenth + options.getbottompadding(); bufferedimage bf = new bufferedimage(options.getimgw(), realh, bufferedimage.type_int_argb); graphics2d g2d = bf.creategraphics(); if (options.getbgimg() == null) { g2d.setcolor(bgcolor == null ? color.white : bgcolor); g2d.fillrect(0, 0, options.getimgw(), realh); } else { g2d.drawimage(options.getbgimg(), 0, 0, options.getimgw(), realh, null); } g2d.drawimage(result, 0, 0, null); g2d.dispose(); return bf; } public string asstring() throws ioexception { bufferedimage img = asimage(); return base64util.encode(img, "png"); } }
上面具体的文本和图片绘制实现没有,后面详细讲解,这里主要关注的是一个参数 contenth, 表示实际绘制的内容高度(包括上边距),因此最终生成图片的高度应该是
int realh = contenth + options.getbottompadding();
其次简单说一下上面的图片输出方法:com.hust.hui.quickmedia.common.image.imgcreatewrapper.builder#asimage
- 计算最终生成图片的高度(宽度由输入参数指定)
- 绘制背景(如果没有背景图片,则用纯色填充)
- 绘制实体内容(即绘制的文本,图片)
3. 内容填充 graphicutil
具体的内容填充,区分为文本绘制和图片绘制
设计
考虑到在填充的过程中,可以*设置字体,颜色等,所以在我们的绘制方法中,直接实现掉内容的绘制填充,即 drawxxx 方法真正的实现了内容填充,执行完之后,内容已经填充到画布上了
图片绘制,考虑到图片本身大小和最终结果的大小可能有冲突,采用下面的规则
- 绘制图片宽度 <=(指定生成图片宽 - 边距),全部填充
- 绘制图片宽度 >(指定生成图片宽 - 边距),等比例缩放绘制图片
文本绘制,换行的问题
- 每一行允许的文本长度有限,超过时,需要自动换行处理
文本绘制
考虑基本的文本绘制,流程如下
1、创建bufferimage对象
2、获取graphic2d对象,操作绘制
3、设置基本配置信息
4、文本按换行进行拆分为字符串数组, 循环绘制单行内容
- 计算当行字符串,实际绘制的行数,然后进行拆分
- 依次绘制文本(需要注意y坐标的变化)
下面是具体的实现
public static int drawcontent(graphics2d g2d, string content, int y, imgcreateoptions options) { int w = options.getimgw(); int leftpadding = options.getleftpadding(); int linepadding = options.getlinepadding(); font font = options.getfont(); // 一行容纳的字符个数 int linenum = (int) math.floor((w - (leftpadding << 1)) / (double) font.getsize()); // 对长串字符串进行分割成多行进行绘制 string[] strs = splitstr(content, linenum); g2d.setfont(font); g2d.setcolor(options.getfontcolor()); int index = 0; int x; for (string tmp : strs) { x = caloffsetx(leftpadding, w, tmp.length() * font.getsize(), options.getalignstyle()); g2d.drawstring(tmp, x, y + (linepadding + font.getsize()) * index); index++; } return y + (linepadding + font.getsize()) * (index); } /** * 计算不同对其方式时,对应的x坐标 * * @param padding 左右边距 * @param width 图片总宽 * @param strsize 字符串总长 * @param style 对其方式 * @return 返回计算后的x坐标 */ private static int caloffsetx(int padding, int width, int strsize, imgcreateoptions.alignstyle style) { if (style == imgcreateoptions.alignstyle.left) { return padding; } else if (style == imgcreateoptions.alignstyle.right) { return width - padding - strsize; } else { return (width - strsize) >> 1; } } /** * 按照长度对字符串进行分割 * <p> * fixme 包含emoj表情时,兼容一把 * * @param str 原始字符串 * @param splitlen 分割的长度 * @return */ public static string[] splitstr(string str, int splitlen) { int len = str.length(); int size = (int) math.ceil(len / (float) splitlen); string[] ans = new string[size]; int start = 0; int end = splitlen; for (int i = 0; i < size; i++) { ans[i] = str.substring(start, end > len ? len : end); start = end; end += splitlen; } return ans; }
上面的实现比较清晰了,图片的绘制则更加简单
图片绘制
只需要重新计算下待绘制图片的宽高即可,具体实现如下
/** * 在原图上绘制图片 * * @param source 原图 * @param dest 待绘制图片 * @param y 待绘制的y坐标 * @param options * @return 绘制图片的高度 */ public static int drawimage(bufferedimage source, bufferedimage dest, int y, imgcreateoptions options) { graphics2d g2d = getg2d(source); int w = math.min(dest.getwidth(), options.getimgw() - (options.getleftpadding() << 1)); int h = w * dest.getheight() / dest.getwidth(); int x = caloffsetx(options.getleftpadding(), options.getimgw(), w, options.getalignstyle()); // 绘制图片 g2d.drawimage(dest, x, y + options.getlinepadding(), w, h, null); g2d.dispose(); return h; } public static graphics2d getg2d(bufferedimage bf) { graphics2d g2d = bf.creategraphics(); g2d.setrenderinghint(renderinghints.key_alpha_interpolation, renderinghints.value_alpha_interpolation_quality); g2d.setrenderinghint(renderinghints.key_antialiasing, renderinghints.value_antialias_on); g2d.setrenderinghint(renderinghints.key_color_rendering, renderinghints.value_color_render_quality); g2d.setrenderinghint(renderinghints.key_dithering, renderinghints.value_dither_enable); g2d.setrenderinghint(renderinghints.key_fractionalmetrics, renderinghints.value_fractionalmetrics_on); g2d.setrenderinghint(renderinghints.key_interpolation, renderinghints.value_interpolation_bilinear); g2d.setrenderinghint(renderinghints.key_rendering, renderinghints.value_render_quality); g2d.setrenderinghint(renderinghints.key_stroke_control, renderinghints.value_stroke_pure); return g2d; }
4. 内容渲染
前面只是给出了单块内容(如一段文字,一张图片)的渲染,存在一些问题
- 绘制的内容超过画布的高度如何处理
- 文本绘制要求传入的文本没有换行符,否则换行不生效
- 交叉绘制的场景,如何重新计算y坐标
解决这些问题则是在 imgcreatewrapper 的具体绘制中进行了实现,先看文本的绘制
根据换行符对字符串进行拆分
计算绘制内容最终转换为图片时,所占用的高度
重新生成画布 bufferedimage result
- 如果result为空,则直接生成
- 如果最终生成的高度,超过已有画布的高度,则生成一个更高的画布,并将原来的内容绘制上去
迭代绘制单行内容
public builder drawcontent(string content) { string[] strs = stringutils.split(content, "\n"); if (strs.length == 0) { // empty line strs = new string[1]; strs[0] = " "; } int fontsize = options.getfont().getsize(); int linenum = callinenum(strs, options.getimgw(), options.getleftpadding(), fontsize); // 填写内容需要占用的高度 int height = linenum * (fontsize + options.getlinepadding()); if (result == null) { result = graphicutil.createimg(options.getimgw(), math.max(height + options.gettoppadding() + options.getbottompadding(), base_add_h), null); } else if (result.getheight() < contenth + height + options.getbottompadding()) { // 超过原来图片高度的上限, 则需要扩充图片长度 result = graphicutil.createimg(options.getimgw(), result.getheight() + math.max(height + options.getbottompadding(), base_add_h), result); } // 绘制文字 graphics2d g2d = graphicutil.getg2d(result); int index = 0; for (string str : strs) { graphicutil.drawcontent(g2d, str, contenth + (fontsize + options.getlinepadding()) * (++index) , options); } g2d.dispose(); contenth += height; return this; } /** * 计算总行数 * * @param strs 字符串列表 * @param w 生成图片的宽 * @param padding 渲染内容的左右边距 * @param fontsize 字体大小 * @return */ private int callinenum(string[] strs, int w, int padding, int fontsize) { // 每行的字符数 double linefontlen = math.floor((w - (padding << 1)) / (double) fontsize); int totalline = 0; for (string str : strs) { totalline += math.ceil(str.length() / linefontlen); } return totalline; }
上面需要注意的是画布的生成规则,特别是高度超过上限之后,重新计算图片高度时,需要额外注意新增的高度,应该为基本的增量与(绘制内容高度+下边距)的较大值
int realaddh = math.max(bufferedimage.getheight() + options.getbottompadding() + options.gettoppadding(), base_add_h)
重新生成画布实现 com.hust.hui.quickmedia.common.util.graphicutil#createimg
public static bufferedimage createimg(int w, int h, bufferedimage img) { bufferedimage bf = new bufferedimage(w, h, bufferedimage.type_int_argb); graphics2d g2d = bf.creategraphics(); if (img != null) { g2d.setcomposite(alphacomposite.src); g2d.setrenderinghint(renderinghints.key_antialiasing, renderinghints.value_antialias_on); g2d.drawimage(img, 0, 0, null); } g2d.dispose(); return bf; }
上面理解之后,绘制图片就比较简单了,基本上行没什么差别
public builder drawimage(string img) { bufferedimage bfimg; try { bfimg = imageutil.getimagebypath(img); } catch (ioexception e) { log.error("load draw img error! img: {}, e:{}", img, e); throw new illegalstateexception("load draw img error! img: " + img, e); } return drawimage(bfimg); } public builder drawimage(bufferedimage bufferedimage) { if (result == null) { result = graphicutil.createimg(options.getimgw(), math.max(bufferedimage.getheight() + options.getbottompadding() + options.gettoppadding(), base_add_h), null); } else if (result.getheight() < contenth + bufferedimage.getheight() + options.getbottompadding()) { // 超过阀值 result = graphicutil.createimg(options.getimgw(), result.getheight() + math.max(bufferedimage.getheight() + options.getbottompadding() + options.gettoppadding(), base_add_h), result); } // 更新实际高度 int h = graphicutil.drawimage(result, bufferedimage, contenth, options); contenth += h + options.getlinepadding(); return this; }
5. http接口
上面实现的生成图片的公共方法,在 quick-media 工程中,利用spring-boot搭建了一个web服务,提供了一个http接口,用于生成长图文,最终的成果就是我们开头的那个gif图的效果,相关代码就没啥好说的,有兴趣的可以直接查看工程源码,链接看最后
测试验证
上面基本上完成了我们预期的目标,接下来则是进行验证,测试代码比较简单,先准备一段文本,这里拉了一首诗
招魂酹翁宾旸
郑起
君之在世帝敕下,君之谢世帝敕回。
魂之为变性原返,气之为物情本开。
於戏龙兮凤兮神气盛,噫嘻鬼兮归兮大块埃。
身可朽名不可朽,骨可灰神不可灰。
采石捉月李白非醉,耒阳避水子美非灾。
长孙王吉命不夭,玉川老子诗不徘。
新城罗隐在奇特,钱塘潘阆终崔嵬。
阴兮魄兮曷往,阳兮魄兮曷来。
君其归来,故交寥落更散漫。
君来归来,帝城绚烂可徘徊。
君其归来,东西南北不可去。
君其归来。
春秋霜露令人哀。
花之明吾无与笑,叶之陨吾实若摧。
晓猿啸吾闻泪堕,宵鹤立吾见心猜。
玉泉其清可鉴,西湖其甘可杯。
孤山暖梅香可嗅,花翁葬荐菊之隈。
君其归来,可伴逋仙之梅,去此又奚之哉。
测试代码
@test public void testgenimg() throws ioexception { int w = 400; int leftpadding = 10; int toppadding = 40; int bottompadding = 40; int linepadding = 10; font font = new font("宋体", font.plain, 18); imgcreatewrapper.builder build = imgcreatewrapper.build() .setimgw(w) .setleftpadding(leftpadding) .settoppadding(toppadding) .setbottompadding(bottompadding) .setlinepadding(linepadding) .setfont(font) .setalignstyle(imgcreateoptions.alignstyle.center) // .setbgimg(imageutil.getimagebypath("qrbg.jpg")) .setbgcolor(0xfff7eed6) ; bufferedreader reader = filereadutil.createlineread("text/poem.txt"); string line; int index = 0; while ((line = reader.readline()) != null) { build.drawcontent(line); if (++index == 5) { build.drawimage(imageutil.getimagebypath("https://static.oschina.net/uploads/img/201708/12175633_sofz.png")); } if (index == 7) { build.setfontsize(25); } if (index == 10) { build.setfontsize(20); build.setfontcolor(color.red); } } bufferedimage img = build.asimage(); string out = base64util.encode(img, "png"); system.out.println("<img src=\"data:image/png;base64," + out + "\" />"); }
输出图片
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
上一篇: 编辑器Ueditor和SpringBoot 的整合方法
下一篇: java正则表达式使用示例