最近呢看着前端的同学使用canvas绘制漂亮的海报,刚好这次又碰到产品说又有了大展身手的机会了,果断报名,后端来完成试试。因为是B端需求,对性能也没过多的要求, 果断报名了。
如果你看了还是真的绘制不出来,也可以主动加我微信交流,可远程帮解决,仅限交流哟 微信号: 413007703.
这张图主要是用于投放或者老师给同学们的商品外链图,用于传播和分享。
上图中除了背景图之外,其他的像顶部的企业log和企业名称,当然也可以理解为头像和昵称都可以的。
还有中间商品的链接图,商品描述文字,二维码的生成,都是后期绘制上去的。也就是接下来所要说的重点。
Graphics2D
和BufferedImage
。IO
流.BufferedImage bufferedImage = imageUtils.getUrlByBufferedImage(commodityImage);
/**
* 通过网络获取图片流
*
* @param url
* @return
*/
public BufferedImage getUrlByBufferedImage(String url) {
try {
URL urlObj = new URL(url);
return ImageIO.read(urlObj);
} catch (Exception e) {
log.error("从网络下载图片资源失败",e);
}
return null;
}
resources
资源文件夹下放置好背景图,我本地的项目路径是这样的:static/image/sassCommodityTag.png
.imgUrl = "static/image/sassCommodityTag.png";
/*初始化一张给定宽高的画布*/
BufferedImage image = new BufferedImage(bWidth, bHight, BufferedImage.TYPE_INT_RGB);
接下来就是设置背景图,把imgUrl先绘制到给定宽高尺寸的画布上。
imageUtils.setBackgroundImgGraphics(image, imgUrl, bWidth, bHight);
工具类的细节如下:
/**
* 设置背景图片
*
* @param image 图片流
* @param imgUrl 背景
* @param imgUrlWide 背景宽度
* @param imgUrlHigh 背景高度
* @throws IOException io异常
*/
public void setBackgroundImgGraphics(BufferedImage image, String imgUrl, int imgUrlWide, int imgUrlHigh) throws IOException {
Graphics2D mainPic = image.createGraphics();
InputStream resourceAsStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(imgUrl);
BufferedImage bufferedImage = ImageIO.read(resourceAsStream);
mainPic.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
mainPic.drawImage(bufferedImage, 0, 0, imgUrlWide, imgUrlHigh, null);
mainPic.dispose();
}
/*设置第一张图片*/
imageUtils.setHeadImageUrlGraphics(image, bufferedImage, fWidth, fHight, bWidth, fPosition, radius);
设置细节如下:
private void drawImage(BufferedImage image, int imgUrlWide, int imgUrlHigh, int backgroundWide, int needHigh, BufferedImage bufferedImage) {
// 获取画笔
Graphics2D graphics = image.createGraphics();
// 计算绘制开始的x轴 居中展示 假如你手机屏幕宽100,展示的字宽50,居中展示x就应该从25开始,可画图辅助看下。
int resultImgx = (backgroundWide - imgUrlWide) / 2;
if (Objects.nonNull(bufferedImage)) {
graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
// 按照给定的尺寸和坐标绘制在画布上
graphics.drawImage(
bufferedImage.getScaledInstance(imgUrlWide,
imgUrlHigh, Image.SCALE_SMOOTH),
resultImgx, needHigh, null);
// 释放资源 避免出现卡死现象
graphics.dispose();
}
}
...
代替。...
代替,我在代码中都做了相应的解决方案。/**
* 图片插入文字根据宽度换行,一行会居中显示
* @param image
* @param str 文字
* @param x 横坐标
* @param y 纵坐标
* @param size 文字大小
* @param color 文字颜色
* @param replace 超出行数替换的字符 如:...
* @param rowSpacing 行间距
* @param widthLength 行宽度
* @param line 行数
*/
public void setTextGraphicsWordWrap(BufferedImage image, String str, int x, int y, int size, Color color, String replace, int rowSpacing, int widthLength,int lineNumber) {
// 获取画笔
Graphics2D mainPic = image.createGraphics();
// 设置画笔颜色
mainPic.setColor(color);
// 设置字体
Font tipFont = new Font("pingfang SC", Font.BOLD, size);
mainPic.setFont(tipFont);
mainPic.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
// 字符串分割 指定行距rowSpacing 这个可以自己调试的时候根据字体找到一个合适的数值
List<String> strArr = getListText( mainPic.getFontMetrics(tipFont),str,widthLength,lineNumber, replace);
if(strArr.size() == 1){
int srcImgWidth = image.getWidth(null);
int stringLength = getStringLength(mainPic,str,tipFont);
mainPic.drawString(str, (srcImgWidth / 2) - (stringLength/2), y);
}else{
for (String s : strArr) {
mainPic.drawString(s, x , y);
y+=rowSpacing;
}
}
mainPic.dispose();
}
多行文字换行与分割:
/**
* 切割字符串
* @param fg
* @param text
* @param widthLength
*/
private List<String> getListText(FontMetrics fg, String text, int widthLength, int line,String replace) {
// 把每一个切割后所占用的字符串存起来
List<String> arr= new ArrayList<>();
for (int i = 1 ; i <= line;i++){
if(text.length() == 0){
break;
}
// 当到最后一行时候 先减去replace所占用的宽度,因为后续最后一个if里面会加上 replace的长度
if(i == line){
widthLength -= fg.charsWidth(replace.toCharArray(), 0, replace.length());
}
String t = text;
boolean b = true;
// 循环切割
while (b) {
if (fg.stringWidth(t) > widthLength) {
t = t.substring(0, t.length()-1);
} else {
text = text.substring(t.length());
b = false;
}
}
if(i == line && text.length() > 0){
t += replace;
}
arr.add(t);
}
return arr;
}
com.google.zxing.client
先通过该工具类把图片生成BufferImage
对象,就可以像步骤3一样绘制了。谷歌二维码工具类,功能也比较完整,可根据需求来定制就行;
首先先引入包文件:
<!-- zxing -->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.2.1</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.2.1</version>
</dependency>
具体的工具类代码如下:
import com.google.zxing.*;
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import org.springframework.stereotype.Component;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Hashtable;
@Component
public class QrCodeUtils {
private static final String CHARSET = "UTF-8";
private static final String FORMAT_NAME = "JPG";
/**
* 二维码尺寸
*/
private static final int QRCODE_SIZE = 300;
/**
* LOGO宽度
*/
private static final int WIDTH = 60;
/**
* LOGO高度
*/
private static final int HEIGHT = 60;
/**
* 创建二维码图片
*
* @param content 二维码内容
* @param logoImgPath Logo
* @param needCompress 是否压缩Logo
* @return 返回二维码图片
* @throws WriterException e
* @throws IOException BufferedImage
*/
public static BufferedImage createImage(String content, String logoImgPath, boolean needCompress) throws WriterException, IOException {
Hashtable<EncodeHintType, Object> hints = new Hashtable<>();
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
hints.put(EncodeHintType.CHARACTER_SET, CHARSET);
hints.put(EncodeHintType.MARGIN, 1);
BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, QRCODE_SIZE, QRCODE_SIZE, hints);
int width = bitMatrix.getWidth();
int height = bitMatrix.getHeight();
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
image.setRGB(x, y, bitMatrix.get(x, y) ? 0xFF000000 : 0xFFFFFFFF);
}
}
if (logoImgPath == null || "".equals(logoImgPath)) {
return image;
}
// 插入图片
QrCodeUtils.insertImage(image, logoImgPath, needCompress);
return image;
}
/**
* 添加Logo
*
* @param source 二维码图片
* @param logoImgPath Logo
* @param needCompress 是否压缩Logo
* @throws IOException void
*/
private static void insertImage(BufferedImage source, String logoImgPath, boolean needCompress) throws IOException {
File file = new File(logoImgPath);
if (!file.exists()) {
return;
}
Image src = ImageIO.read(new File(logoImgPath));
int width = src.getWidth(null);
int height = src.getHeight(null);
// 压缩LOGO
if (needCompress) {
if (width > WIDTH) {
width = WIDTH;
}
if (height > HEIGHT) {
height = HEIGHT;
}
Image image = src.getScaledInstance(width, height, Image.SCALE_SMOOTH);
BufferedImage tag = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics g = tag.getGraphics();
// 绘制缩小后的图
g.drawImage(image, 0, 0, null);
g.dispose();
src = image;
}
// 插入LOGO
Graphics2D graph = source.createGraphics();
int x = (QRCODE_SIZE - width) / 2;
int y = (QRCODE_SIZE - height) / 2;
graph.drawImage(src, x, y, width, height, null);
Shape shape = new RoundRectangle2D.Float(x, y, width, width, 6, 6);
graph.setStroke(new BasicStroke(3f));
graph.draw(shape);
graph.dispose();
}
/**
* 生成带Logo的二维码
*
* @param content 二维码内容
* @param logoImgPath Logo
* @param destPath 二维码输出路径
* @param needCompress 是否压缩Logo
* @throws Exception void
*/
public static void encode(String content, String logoImgPath, String destPath, boolean needCompress) throws Exception {
BufferedImage image = QrCodeUtils.createImage(content, logoImgPath, needCompress);
mkdirs(destPath);
ImageIO.write(image, FORMAT_NAME, new File(destPath));
}
/**
* 生成不带Logo的二维码
*
* @param content 二维码内容
* @param destPath 二维码输出路径
*/
public static void encode(String content, String destPath) throws Exception {
QrCodeUtils.encode(content, null, destPath, false);
}
/**
* 生成带Logo的二维码,并输出到指定的输出流
*
* @param content 二维码内容
* @param logoImgPath Logo
* @param output 输出流
* @param needCompress 是否压缩Logo
*/
public static void encode(String content, String logoImgPath, OutputStream output, boolean needCompress) throws Exception {
BufferedImage image = QrCodeUtils.createImage(content, logoImgPath, needCompress);
ImageIO.write(image, FORMAT_NAME, output);
}
/**
* 生成不带Logo的二维码,并输出到指定的输出流
*
* @param content 二维码内容
* @param output 输出流
* @throws Exception void
*/
public static void encode(String content, OutputStream output) throws Exception {
QrCodeUtils.encode(content, null, output, false);
}
/**
* 二维码解析
*
* @param file 二维码
* @return 返回解析得到的二维码内容
* @throws Exception String
*/
public static String decode(File file) throws Exception {
BufferedImage image;
image = ImageIO.read(file);
if (image == null) {
return null;
}
BufferedImageLuminanceSource source = new BufferedImageLuminanceSource(image);
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
Result result;
Hashtable<DecodeHintType, Object> hints = new Hashtable<DecodeHintType, Object>();
hints.put(DecodeHintType.CHARACTER_SET, CHARSET);
result = new MultiFormatReader().decode(bitmap, hints);
return result.getText();
}
/**
* 二维码解析
*
* @param path 二维码存储位置
* @return 返回解析得到的二维码内容
* @throws Exception String
*/
public static String decode(String path) throws Exception {
return QrCodeUtils.decode(new File(path));
}
/**
* 判断路径是否存在,如果不存在则创建
*
* @param dir 目录
*/
public static void mkdirs(String dir) {
if (dir != null && !"".equals(dir)) {
File file = new File(dir);
if (!file.isDirectory()) {
file.mkdirs();
}
}
}
}
绘制二维码:
/*设置第二张图片二维码*/ 参数是image 画布 qrCodeImage 二维码 宽高和具体位置
imageUtils.setHeadImageUrlGraphics(image, qrCodeImage, sWidth, sHight, bWidth, sPosition, radius);
/**
2. 传入头像url或者logurl
*/
private BufferedImage drawHeadImages(String headUrl) {
BufferedImage avatarImage;
try {
// 获取图片资源
avatarImage = ImageIO.read(new URL(headUrl));
int width = 100;
// 先绘制一个透明底的图片
BufferedImage formatAvatarImage = new BufferedImage(width, width, BufferedImage.TYPE_4BYTE_ABGR);
Graphics2D graphics = formatAvatarImage.createGraphics();
//设置抗锯齿
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// 指定边框为1像素的边
int border = 1;
//绘制一个圆形
Ellipse2D.Double shape = new Ellipse2D.Double(border, border, width - border * 2, width - border * 2);
// 图片裁剪 留下圆形
graphics.setClip(shape);
// 在圆形上绘制头像 标记1 效果图看下文
graphics.drawImage(avatarImage, 1, 1, width - border * 2, width - border * 2, null);
graphics.dispose();
//在圆图外面再画一个圆
//新创建一个graphics,这样画的圆不会有锯齿
graphics = formatAvatarImage.createGraphics();
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
int border1 = 3; // x和y轴坐标
//画笔是5个像素,设置画笔线条 在绘制一个画笔为五个像素点边框的空心圆
Stroke s = new BasicStroke(5F, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
graphics.setStroke(s);
graphics.setColor(Color.WHITE);
// 绘制圆形(椭圆形的变种)宽高相等=圆
graphics.drawOval(border1, border1, width - border1 * 2, width - border1 * 2);
return formatAvatarImage;
} catch (Exception e) {
log.error("绘制sass图片报错", e);
}
return null;
}
public void setTextGraphicsV3(BufferedImage image, String text, int y,int bWidth,int size) {
Graphics2D mainPic = image.createGraphics();
mainPic.setColor(Color.white);
Font tipFont = new Font("pingfang SC", Font.BOLD, size);
mainPic.setFont(tipFont);
mainPic.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
// 计算文字长度,计算居中的x点坐标
FontMetrics fm = mainPic.getFontMetrics(tipFont);
int textWidth = fm.stringWidth(text);
boolean flag = false;
while (textWidth > bWidth) {
text = text.substring(0, text.length() - 1);
textWidth = fm.stringWidth(text);
flag = true;
}
// 为了减小误差确定在削减两个字符
if (StringUtils.isNotBlank(text) && text.length() > 2 && flag) {
text = text.substring(0, text.length() - 2) + "...";
}
int newTextWidth = fm.stringWidth(text);
int widthx = (bWidth - newTextWidth) / 2;
mainPic.drawString(text, widthx < 0 ? 0 : widthx, y);
mainPic.dispose();
}
/**
* 生成图片字节流
*
* @param response 返回体
* @param image 图片字节流
*/
@SuppressWarnings("restriction")
public void createImage(HttpServletResponse response, BufferedImage image,String fileName) throws IOException {
//BufferedImage 转 InputStream
ByteArrayOutputStream byteArrayOutputStream = null;
ImageOutputStream imageOutput = null;
InputStream inputStream = null;
OutputStream outputStream = null;
try {
byteArrayOutputStream = new ByteArrayOutputStream();
imageOutput = ImageIO.createImageOutputStream(byteArrayOutputStream);
ImageIO.write(image, "png", imageOutput);
inputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
//返回字节流
outputStream = response.getOutputStream();
response.setContentType("image/png");
response.setCharacterEncoding("UTF-8");
if (StringUtils.isNotBlank(fileName)) {
response.setHeader("content-type", "application/octet-stream");
response.setContentType("application/x-download;charset=UTF-8");
response.setHeader("Content-Disposition", "attachment;filename="+ URLEncoder.encode(fileName,"UTF-8")+".png;filename*=UTF-8''"+URLEncoder.encode(fileName,"UTF-8")+".png");
}
IOUtils.copy(inputStream, outputStream);
outputStream.flush();
}catch (IOException ex){
throw new RuntimeException("下载失败",ex);
}finally {
if(byteArrayOutputStream != null){
byteArrayOutputStream.close();
}
if(imageOutput != null){
imageOutput.close();
}
if(inputStream != null){
inputStream.close();
}
if(outputStream != null){
outputStream.close();
}
}
}