Java如何绘制一张漂亮的海报(附源码)

需求说明

最近呢看着前端的同学使用canvas绘制漂亮的海报,刚好这次又碰到产品说又有了大展身手的机会了,果断报名,后端来完成试试。因为是B端需求,对性能也没过多的要求, 果断报名了。

如果你看了还是真的绘制不出来,也可以主动加我微信交流,可远程帮解决,仅限交流哟 微信号: 413007703.

效果图如下

在这里插入图片描述

在这里插入图片描述

这张图主要是用于投放或者老师给同学们的商品外链图,用于传播和分享。
上图中除了背景图之外,其他的像顶部的企业log和企业名称,当然也可以理解为头像和昵称都可以的。

还有中间商品的链接图,商品描述文字,二维码的生成,都是后期绘制上去的。也就是接下来所要说的重点。

核心技术点

  • Java画板和画笔Graphics2DBufferedImage
  • Java相关的IO流.
  • 还有对坐标系的理解和熟练程度。
  • 图片上文字换行处理方式。

绘制流程

  1. 把一个URL链接下载并转成相应的文件流。
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;
    }
  1. 获取并初始化背景图
    首先在项目的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();
    }
  1. 第三步就可以绘制商品海报图了
/*设置第一张图片*/
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();
        }
    }
  1. 绘制文字
    以此类推的是,文字的绘制方式跟画图一样,把指定的文字描述绘制在画布上即可,但是有一个细节点需要注意,如果是单行文字,超出画布的宽根据产品要求用...代替。
    有些时候产品可能需要文字换行,指定展示多少行后在用...代替,我在代码中都做了相应的解决方案。
/**
     * 图片插入文字根据宽度换行,一行会居中显示
     * @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;
    }
  1. 绘制二维码
    这里用到了谷歌的二维码生成工具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);
  1. 绘制顶部白底头像logo和企业昵称(可单独摘出来用)
    1. 绘制圆形logo
/**
 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;
    }

代码中标记1出效果图:也就是执行完上述代码22行效果
在这里插入图片描述

  1. 绘制昵称或者说企业名称(文字占一行情况)
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();
    }
  1. 最后将图片信息流转成浏览器能下载的格式即可
/**
     * 生成图片字节流
     *
     * @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();
            }
        }

    }
  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值