Graphics2D - 生成证书图片

需求:用户领取证书功能。起初做法是将证书内容保存至富文本编辑器,再将富文本内容返回,由前端组成证书页面。但是前端因为屏幕尺寸、拉伸等问题,导致展示的效果不尽人意,无法满足业务要求。所以选择了使用Graphics2D在后端生成证书图片,再将图片返回的方式。

目录

  • 一、使用类
    • Graphics2D
    • BufferedImage
    • ByteArrayOutputStream
  • 二、生成证书图片
    • 1.准备工作:
    • 2、工具类
      • (1)引入依赖
      • (2)绘图工具类
      • (3)上传文件工具类
    • 3、绘制证书
      • 结果图展示:
  • 三、保存证书文件
    • 1.上传至服务器
    • 2.上传至minio

一、使用类

Graphics2D

Graphics类,提供了对几何形状、坐标转换、颜色管理和文本布局更为复杂的控制。用于在Java平台上呈现二维形状、文本和图像的基础类。Graphics2D对象相当于一块画布,我们可以使用Graohics2D提供的方法在画布上进行绘制。

BufferedImage

BufferedImage是带缓冲区图像类,主要作用是将图片加载到内存中,用于对图片进行一些绘图操作。

ByteArrayOutputStream

ByteArrayOutputStream是一个输出流,用于将数据输出到字节数组中,可以通过toByteArray()方法获取写日的字节数组,也可以通过toString()方法将字节数组转换为字符串。

二、生成证书图片

1.准备工作:

(1)模板底图(包含固定文字信息)
(2)填入的信息(比如:姓名、编号等),记录底图中需要填写信息位置的坐标轴(x,y)。

[{name:"Java123",x:"374",y:"298"},{name:"小李",x:"570",y:"299"},{name:"Graphics杯",x:"320",y:"351"}]

待写入具体信息的模板底图

2、工具类

(1)引入依赖


<dependency>
	<groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.14</version>
</dependency>

(2)绘图工具类

import cn.hutool.core.codec.Base64;
import cn.hutool.core.img.FontUtil;
import cn.hutool.core.img.GraphicsUtil;
import cn.hutool.core.img.Img;
import cn.hutool.core.img.ImgUtil;
import org.springframework.core.io.ClassPathResource;


import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.math.BigDecimal;
import java.text.AttributedCharacterIterator;
import java.text.AttributedString;

public class GraphicsUtils {

    private static final String prefix = "/graphics.background/";

    private static Font baseFont;
    private static Font normalFont;
    private static Font ltBoldFont;

    static {
        //基础字体
        //每次Font.createFont都会生成一个临时文件,多了磁盘就爆了
        try {
        	// 宋体字资源
            ClassPathResource songFontResource = new ClassPathResource(prefix + "simsun.ttc");
            baseFont = Font.createFont(Font.TRUETYPE_FONT, songFontResource.getInputStream());
            normalFont = Font.createFont(Font.TRUETYPE_FONT, songFontResource.getInputStream());
           	// 黑体字资源
            ClassPathResource ltFontResource = new ClassPathResource(prefix + "Lantinghei.ttc");
            ltBoldFont = Font.createFont(Font.TRUETYPE_FONT, ltFontResource.getInputStream());
        } catch (FontFormatException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 写字
     * @param graphics
     * @param text
     * @param font
     * @param x
     * @param y
     * @param align
     */
    public static void drawString(Graphics2D graphics, String text, Font font, int x, int y, int align)
    {
    	// 获得字体对应字符串的长宽信息
        Dimension dimension = FontUtil.getDimension(graphics.getFontMetrics(font), text);
        int newX = x;
        if(align == 0) {
            newX = (int) (x-dimension.getWidth()/2);
        }
        else if(align == -1){
            newX = x;
        }
        else{
            newX = (int) (x-dimension.getWidth());
        }
        GraphicsUtil.drawString(graphics,text, font,Color.BLACK,new Point(newX, y));
    }

    public static int drawStringWithMultiLine(Graphics2D graphics2D, String text,Font font, int maxWidth, int startX, int startY, int heightSpace,int indent)
    {
        return drawStringWithMultiLine(graphics2D, text, font, maxWidth, startX, startY, heightSpace, indent, true);
    }

    /**
     * 输出整行字符串,超过换行
     * @param graphics2D
     * @param text
     * @param font
     * @param maxWidth
     * @param startX
     * @param startY
     * @param heightSpace
     * @param indent
     * @return
     */
    public static int drawStringWithMultiLine(Graphics2D graphics2D, String text,Font font, int maxWidth, int startX, int startY, int heightSpace,int indent,boolean draw)
    {
        int lastY = startY;
        // 获取指定字体的字体度量。
        FontMetrics fontMetrics = graphics2D.getFontMetrics(font);
        Dimension dimension = FontUtil.getDimension(fontMetrics, text);
        StringBuilder sb = new StringBuilder();
        for(int i=0;i<indent;i++)
        {
            //全角空格
            sb.append('\u3000');
        }
        sb.append(text);

        text = sb.toString();

        int textLength = text.length();
        int totalWidth = (int) dimension.getWidth(); //一行的总长度,用于判断是否超出了范围

        int textHeight = (int) dimension.getHeight(); //计算一行的高度

        int rightOffset = maxWidth - startX;
        if (totalWidth > rightOffset) {
            //总长度超过了整个长度限制
            int alreadyWriteLine = 0; //已经写了多少行
            int nowWidth = 0; //目前一行写的长度
            for (int i = 0; i < textLength; i++) {
                int oneWordWidth = fontMetrics.charWidth(text.charAt(i)); //获取单个字符的长度
                int tempWidth = oneWordWidth + nowWidth; //判断目前的一行加上这个字符的长度是否超出了总长度
                if (tempWidth > rightOffset) {
                    //如果超出了一行的总长度,则要换成下一行
                    nowWidth = 0;
                    alreadyWriteLine++;
                    int writeY = startY + alreadyWriteLine * (textHeight + heightSpace);
                    if(draw)
                    {
                        GraphicsUtil.drawString(graphics2D,text.charAt(i)+"", font,Color.BLACK,new Point(startX + nowWidth, writeY));
                    }

                    nowWidth = oneWordWidth;
                } else {
                    //当前行长度足够,可以直接画
                    int writeY = startY + alreadyWriteLine * (textHeight + heightSpace);
                    if(draw)
                    {
                        GraphicsUtil.drawString(graphics2D,text.charAt(i)+"", font,Color.BLACK,new Point(startX + nowWidth, writeY));
                    }

                    nowWidth = tempWidth;
                }
            }
            lastY = startY + (alreadyWriteLine+1) * (textHeight + heightSpace);
        } else {
            //没有超过限制,直接画
            if(draw)
            {
                GraphicsUtil.drawString(graphics2D,text, font,Color.BLACK,new Point(startX , startY));
            }
            lastY = startY + textHeight + heightSpace;
        }
        return lastY;
    }

    /**
     *
     * @param graphics2D
     * @param text
     * @param font
     * @param maxWidth
     * @param startX
     * @param currentX
     * @param startY
     * @param heightSpace
     * @param indent
     * @return
     */
    public static int[] drawAndAppendString(Graphics2D graphics2D, String text,Font font, int maxWidth, int startX, int currentX, int startY, int heightSpace,int indent)
    {
        return drawAndAppendString(graphics2D, text, font, maxWidth, startX, currentX, startY, heightSpace, indent, 0, true);
    }

    public static int[] drawAndAppendString(Graphics2D graphics2D, String text,Font font, int maxWidth, int startX, int currentX, int startY, int heightSpace,int indent, int nextIndent)
    {
        return drawAndAppendString(graphics2D, text, font, maxWidth, startX, currentX, startY, heightSpace, indent, nextIndent, true);
    }

    public static int[] drawAndAppendString(Graphics2D graphics2D, String text,Font font, int maxWidth, int startX, int currentX, int startY, int heightSpace,int indent, boolean draw)
    {
        return drawAndAppendString(graphics2D, text, font, maxWidth, startX, currentX, startY, heightSpace, indent, 0, draw);
    }

    /**
     * 追加字符串,超过自动换行
     * @param graphics2D 地图对象
     * @param text  文本
     * @param font  字体
     * @param maxWidth  文本最宽x轴
     * @param startX    起始x轴
     * @param currentX  当前x轴(上一个文本的结尾坐标)
     * @param startY    起始y轴
     * @param heightSpace   行间距
     * @param indent 开头空格数
     * @return
     */
    public static int[] drawAndAppendString(Graphics2D graphics2D, String text,Font font, int maxWidth, int startX, int currentX, int startY, int heightSpace,int indent,int nextIndent,boolean draw)
    {
        int lastY = startY;
        int lastX = 0;
        StringBuilder sb = new StringBuilder();
        if(currentX == startX)
        {
            for(int i=0;i<indent;i++)
            {
                //全角空格
                sb.append('\u3000');
            }
        }
        sb.append(text);

        text = sb.toString();

        FontMetrics fontMetrics = graphics2D.getFontMetrics(font);
        Dimension dimension = FontUtil.getDimension(fontMetrics, text);

        int textLength = text.length();
        int totalWidth = (int) dimension.getWidth(); //一行的总长度,用于判断是否超出了范围

        int textHeight = (int) dimension.getHeight(); //计算一行的高度

        int rightOffset = maxWidth - currentX;
        if (totalWidth > rightOffset) {
            //总长度超过了整个长度限制
            int alreadyWriteLine = 0; //已经写了多少行
            int nowWidth = 0; //目前一行写的长度
            for (int i = 0; i < textLength; i++) {
                int nextIndentWidth = nextIndent * fontMetrics.charWidth('\u3000');

                int oneWordWidth = fontMetrics.charWidth(text.charAt(i)); //获取单个字符的长度
                int tempWidth = oneWordWidth + nowWidth; //判断目前的一行加上这个字符的长度是否超出了总长度
                if (tempWidth > rightOffset) {
                    //如果超出了一行的总长度,则要换成下一行
                    nowWidth = 0;
                    alreadyWriteLine++;
                    int writeY = startY + alreadyWriteLine * (textHeight + heightSpace);
                    if(draw)
                    {
                        GraphicsUtil.drawString(graphics2D,text.charAt(i)+"", font,Color.BLACK,new Point(startX + nowWidth + nextIndentWidth, writeY));
                    }
                    nowWidth = oneWordWidth;
                    currentX = startX + nextIndentWidth;
                    rightOffset = maxWidth - currentX;
                } else {
                    //当前行长度足够,可以直接画
                    int writeY = startY + alreadyWriteLine * (textHeight + heightSpace);
                    if(draw)
                    {
                        GraphicsUtil.drawString(graphics2D,text.charAt(i)+"", font,Color.BLACK,new Point(currentX + nowWidth, writeY));
                    }
                    nowWidth = tempWidth;
                }
            }
            lastY = startY + alreadyWriteLine * (textHeight + heightSpace);
            lastX = currentX + nowWidth;
        } else {
            //没有超过限制,直接画
            if(draw)
            {
                GraphicsUtil.drawString(graphics2D,text, font,Color.BLACK,new Point(currentX , startY));
            }
//            lastY = startY + textHeight + heightSpace;
            lastX = currentX + totalWidth;
        }
        return new int[]{lastX, lastY};
    }

    /**
     * 追加字符串,超过自动换行
     * @param graphics2D
     * @param as
     * @param font
     * @param maxWidth
     * @param startX
     * @param currentX
     * @param startY
     * @param heightSpace
     * @param indent
     * @return
     */
    public static int[] drawAndAppendString(Graphics2D graphics2D, AttributedString as,Font font, int maxWidth, int startX, int currentX, int startY, int heightSpace,int indent)
    {
        int lastY = startY;
        int lastX = 0;

        AttributedCharacterIterator iter = as.getIterator();
        int limit = iter.getRunLimit();

        //构造字符串
        StringBuilder textBuilder = new StringBuilder();
        while (iter.getIndex() < limit)
        {
            textBuilder.append(iter.current());
            iter.next();
        }

        String text = textBuilder.toString();

        StringBuilder sb = new StringBuilder();
        if(currentX == startX)
        {
            for(int i=0;i<indent;i++)
            {
                //全角空格
                sb.append('\u3000');
            }
        }
        sb.append(text);

        text = sb.toString();

        FontMetrics fontMetrics = graphics2D.getFontMetrics(font);
        Dimension dimension = FontUtil.getDimension(fontMetrics, text);

        int textLength = text.length();
        int totalWidth = (int) dimension.getWidth(); //一行的总长度,用于判断是否超出了范围

        int textHeight = (int) dimension.getHeight(); //计算一行的高度

        //距离右边
        int rightOffset = maxWidth - currentX;
        //从头遍历
        iter.setIndex(0);
        if (totalWidth > rightOffset) {
            //总长度超过了整个长度限制
            int alreadyWriteLine = 0; //已经写了多少行
            int nowWidth = 0; //目前一行写的长度

            //遍历字符
            while (iter.getIndex() < limit)
            {
                //获取单个富文本
                AttributedString tempAs = new AttributedString(String.valueOf(iter.current()));
                AttributedCharacterIterator tempIter = tempAs.getIterator();
                tempAs.addAttributes(iter.getAttributes(), tempIter.getBeginIndex(), tempIter.getEndIndex());

                int oneWordWidth = fontMetrics.charWidth(iter.current()); //获取单个字符的长度
                int tempWidth = oneWordWidth + nowWidth; //判断目前的一行加上这个字符的长度是否超出了总长度
                if (tempWidth > rightOffset) {
                    //如果超出了一行的总长度,则要换成下一行
                    nowWidth = 0;
                    alreadyWriteLine++;
                    int writeY = startY + alreadyWriteLine * (textHeight + heightSpace);
//                    GraphicsUtil.drawString(graphics2D,text.charAt(i)+"", font,Color.BLACK,new Point(startX + nowWidth, writeY));

                    graphics2D.drawString(tempAs.getIterator(), startX + nowWidth, writeY);
                    nowWidth = oneWordWidth;
                    currentX = startX;
                    rightOffset = maxWidth;
                } else {
                    //当前行长度足够,可以直接画
                    int writeY = startY + alreadyWriteLine * (textHeight + heightSpace);
//                    GraphicsUtil.drawString(graphics2D,text.charAt(i)+"", font,Color.BLACK, new Point(currentX + nowWidth, writeY));
                    graphics2D.drawString(tempAs.getIterator(), currentX + nowWidth, writeY);
                    nowWidth = tempWidth;
                }
                iter.next();
            }
            lastY = startY + alreadyWriteLine * (textHeight + heightSpace);
            lastX = currentX + nowWidth;
        } else {
            //没有超过限制,直接画
            graphics2D.drawString(iter, currentX, startY);
            lastX = currentX + totalWidth;
        }
        return new int[]{lastX, lastY};
    }

    /**
     * 计算
     * @param graphics2D
     * @param text
     * @param font
     * @param maxWidth
     * @param currentX
     * @return
     */
    public static double calculatePercentOfMargin(Graphics2D graphics2D, String text, Font font, int maxWidth, int currentX)
    {
        FontMetrics fontMetrics = graphics2D.getFontMetrics(font);
        Dimension dimension = FontUtil.getDimension(fontMetrics, text);
        int totalWidth = (int) dimension.getWidth();
        int rightOffset = maxWidth - currentX;
        BigDecimal bigDecimal = new BigDecimal((float)totalWidth / rightOffset).setScale(2, BigDecimal.ROUND_DOWN);
        return bigDecimal.doubleValue();
    }

    /**
     * 生成证书base64
     *
     */
    public static String createCertificateImageBase64(BufferedImage bufferedImage) {

        if (null == bufferedImage) {
            return "";
        }
        String result = "";
        try {
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            ImageIO.write(bufferedImage, "png", outputStream);
            result = Base64.encode(outputStream.toByteArray());

            outputStream.flush();
            outputStream.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }
}

(3)上传文件工具类

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Paths;
import java.util.Objects;
import java.util.UUID;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.springframework.web.multipart.MultipartFile;


/**
 * 文件上传工具类
 *
 * @author ruoyi
 */
public class FileUploadUtils
{
    /**
     * 默认大小 100M
     */
    public static final long DEFAULT_MAX_SIZE = 100 * 1024 * 1024;

    /**
     * 默认的文件名最大长度 100
     */
    public static final int DEFAULT_FILE_NAME_LENGTH = 100;

    /**
     * 默认上传的地址
     */
    private static String defaultBaseDir = RuoYiConfig.getProfile();

    public static void setDefaultBaseDir(String defaultBaseDir)
    {
        FileUploadUtils.defaultBaseDir = defaultBaseDir;
    }

    public static String getDefaultBaseDir()
    {
        return defaultBaseDir;
    }

    /**
     * 以默认配置进行文件上传
     *
     * @param file 上传的文件
     * @return 文件名称
     * @throws Exception
     */
    public static final String upload(MultipartFile file) throws IOException
    {
        try
        {
            return upload(getDefaultBaseDir(), file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION);
        }
        catch (Exception e)
        {
            throw new IOException(e.getMessage(), e);
        }
    }

    /**
     * 根据文件路径上传
     *
     * @param baseDir 相对应用的基目录
     * @param file 上传的文件
     * @return 文件名称
     * @throws IOException
     */
    public static final String upload(String baseDir, MultipartFile file) throws IOException
    {
        try
        {
            return upload(baseDir, file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION);
        }
        catch (Exception e)
        {
            throw new IOException(e.getMessage(), e);
        }
    }
    /**
     * 二维码专用
     *
     * @param baseDir 相对应用的基目录
     * @param file 上传的文件
     * @return 文件名称
     * @throws IOException
     */
    public static final String upCodeload(String baseDir, MultipartFile file) throws IOException
    {
        try
        {
            return uploadCode(baseDir, file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION);
        }
        catch (Exception e)
        {
            throw new IOException(e.getMessage(), e);
        }
    }
    public static final String uploadCode(String baseDir, MultipartFile file, String[] allowedExtension)
            throws FileSizeLimitExceededException, IOException, FileNameLengthLimitExceededException,
            InvalidExtensionException
    {
        int fileNamelength = Objects.requireNonNull(file.getOriginalFilename()).length();
        if (fileNamelength > FileUploadUtils.DEFAULT_FILE_NAME_LENGTH)
        {
            throw new FileNameLengthLimitExceededException(FileUploadUtils.DEFAULT_FILE_NAME_LENGTH);
        }

        assertAllowed(file, allowedExtension);

        String fileName = file.getOriginalFilename();

        String absPath = getAbsoluteFile(baseDir, fileName).getAbsolutePath();
        file.transferTo(Paths.get(absPath));
        return getPathFileName(baseDir, fileName);
    }

    /**
     * 文件上传
     *
     * @param baseDir 相对应用的基目录
     * @param file 上传的文件
     * @param allowedExtension 上传文件类型
     * @return 返回上传成功的文件名
     * @throws FileSizeLimitExceededException 如果超出最大大小
     * @throws FileNameLengthLimitExceededException 文件名太长
     * @throws IOException 比如读写文件出错时
     * @throws InvalidExtensionException 文件校验异常
     */
    public static final String upload(String baseDir, MultipartFile file, String[] allowedExtension)
            throws FileSizeLimitExceededException, IOException, FileNameLengthLimitExceededException,
            InvalidExtensionException
    {
        int fileNamelength = Objects.requireNonNull(file.getOriginalFilename()).length();
        if (fileNamelength > FileUploadUtils.DEFAULT_FILE_NAME_LENGTH)
        {
            throw new FileNameLengthLimitExceededException(FileUploadUtils.DEFAULT_FILE_NAME_LENGTH);
        }

        assertAllowed(file, allowedExtension);

        String fileName = extractFilename(file);

        String absPath = getAbsoluteFile(baseDir, fileName).getAbsolutePath();
        file.transferTo(Paths.get(absPath));
        return getPathFileName(baseDir, fileName);
    }

    /**
     * 编码文件名
     */
    public static final String extractFilename(MultipartFile file)
    {
        return StringUtils.format("{}/{}_{}.{}", DateUtils.datePath(),
                FilenameUtils.getBaseName(UUID.randomUUID().toString()), Seq.getId(Seq.uploadSeqType), getExtension(file));
//        return StringUtils.format("{}/{}_{}.{}", DateUtils.datePath(),
//                FilenameUtils.getBaseName(file.getOriginalFilename()), Seq.getId(Seq.uploadSeqType), getExtension(file));
    }

    public static final File getAbsoluteFile(String uploadDir, String fileName) throws IOException
    {
        File desc = new File(uploadDir + File.separator + fileName);

        if (!desc.exists())
        {
            if (!desc.getParentFile().exists())
            {
                desc.getParentFile().mkdirs();
            }
        }
        return desc;
    }

    public static final String getPathFileName(String uploadDir, String fileName) throws IOException
    {
        int dirLastIndex = RuoYiConfig.getProfile().length() + 1;
        String currentDir = StringUtils.substring(uploadDir, dirLastIndex);
        return Constants.RESOURCE_PREFIX + "/" + currentDir + "/" + fileName;
    }

    /**
     * 文件大小校验
     *
     * @param file 上传的文件
     * @return
     * @throws FileSizeLimitExceededException 如果超出最大大小
     * @throws InvalidExtensionException
     */
    public static final void assertAllowed(MultipartFile file, String[] allowedExtension)
            throws FileSizeLimitExceededException, InvalidExtensionException
    {
        long size = file.getSize();
        if (size > DEFAULT_MAX_SIZE)
        {
            throw new FileSizeLimitExceededException(DEFAULT_MAX_SIZE / 1024 / 1024);
        }

        String fileName = file.getOriginalFilename();
        String extension = getExtension(file);
        if (allowedExtension != null && !isAllowedExtension(extension, allowedExtension))
        {
            if (allowedExtension == MimeTypeUtils.IMAGE_EXTENSION)
            {
                throw new InvalidExtensionException.InvalidImageExtensionException(allowedExtension, extension,
                        fileName);
            }
            else if (allowedExtension == MimeTypeUtils.FLASH_EXTENSION)
            {
                throw new InvalidExtensionException.InvalidFlashExtensionException(allowedExtension, extension,
                        fileName);
            }
            else if (allowedExtension == MimeTypeUtils.MEDIA_EXTENSION)
            {
                throw new InvalidExtensionException.InvalidMediaExtensionException(allowedExtension, extension,
                        fileName);
            }
            else if (allowedExtension == MimeTypeUtils.VIDEO_EXTENSION)
            {
                throw new InvalidExtensionException.InvalidVideoExtensionException(allowedExtension, extension,
                        fileName);
            }
            else
            {
                throw new InvalidExtensionException(allowedExtension, extension, fileName);
            }
        }

    }

    /**
     * 判断MIME类型是否是允许的MIME类型
     *
     * @param extension
     * @param allowedExtension
     * @return
     */
    public static final boolean isAllowedExtension(String extension, String[] allowedExtension)
    {
        for (String str : allowedExtension)
        {
            if (str.equalsIgnoreCase(extension))
            {
                return true;
            }
        }
        return false;
    }

    /**
     * 获取文件名的后缀
     *
     * @param file 表单文件
     * @return 后缀名
     */
    public static final String getExtension(MultipartFile file)
    {
        String extension = FilenameUtils.getExtension(file.getOriginalFilename());
        if (StringUtils.isEmpty(extension))
        {
            extension = MimeTypeUtils.getExtension(Objects.requireNonNull(file.getContentType()));
        }
        return extension;
    }

    /**
     * 根据url下载文件到本地
     * @param url
     * @param localFilePath 含文件名
     * @return 1:成功 0:失败
     */
    public static int downloadFileFromUrl(String url, String localFilePath) {

        try {
            URL httpUrl = new URL(url);
            File f = new File(localFilePath);
            FileUtils.copyURLToFile(httpUrl, f);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
        return 1;
    }

    public static int fileExists(String path)
    {
        File file = new File(path);
        if(file.isFile()) {
            return 1; // 是文件
        } else if(file.isDirectory()) {
            if(Objects.requireNonNull(file.list()).length>0)
                return 2;   // 非空文件夹
            else
                return 3;   // 空文件夹
        }
        return 0; // 路径不存在
    }

    /**
     * 根据url下载文件到本地
     * @param url
     * @param localFilePath 含文件名
     * @return 1:成功 0:失败
     */
    public static int downloadFileFromUrl2(String url, String localFilePath) {
        int res = 0;
        int fileExists = FileUploadUtils.fileExists(localFilePath);
        // 下载文件,先查询文件是否已存在
        if (StringUtils.isNotBlank(url) && fileExists == 0) {
            res = FileUploadUtils.downloadFileFromUrl(url, localFilePath);
        }
        return res;
    }
}

3、绘制证书

    public static void main(String args[]) {

        final int alignRight = 1;   // 对齐右侧
        final int alignCenter = 0;  // 居中

        // 背景图输入流
        InputStream backgroundInputStream = null;
        try {
            // 获取背景
            String path = "C:\\Users\\Dell\\Desktop\\图片\\证书模板.jpg";

            // 构造背景
            backgroundInputStream = new FileInputStream(path);

			// 按需压缩
            int backgroundWidth = 1000;     //宽度像素
            Img backGround = Img.from(backgroundInputStream);
            Img backgroudScale = backGround.scale(backgroundWidth, 700);// 按照目标宽度、高度缩放图像
            BufferedImage backgroudBuffer = ImgUtil.copyImage(backgroudScale.getImg(), BufferedImage.TYPE_INT_ARGB);    // 表示包含8位RGBA颜色组件的图像整数像素

            // 以背景图作为画布
            Graphics2D graphics = GraphicsUtil.createGraphics(backgroudBuffer, null);

            // 设置字体大小
            baseFont = baseFont.deriveFont(20f);
            float originNormalFontSize = 20f;
            normalFont = normalFont.deriveFont(originNormalFontSize);   // 正文字体
            ltBoldFont = ltBoldFont.deriveFont(originNormalFontSize);   //加粗字体

            graphics.setFont(baseFont);

            String variableCoordinates = "[{name:\"Java123\",x:\"374\",y:\"298\"},{name:\"小李\",x:\"570\",y:\"299\"},{name:\"Graphics杯\",x:\"320\",y:\"351\"}]";
            // 遍历把字全部填到地图上
            List<VariableCoordinatesVo> variableCoordinatesList = JSONUtil.toList(JSONUtil.toJsonStr(variableCoordinates), VariableCoordinatesVo.class);
            for (VariableCoordinatesVo variableCoordinatesVo : variableCoordinatesList) {

                int align = alignCenter;   // 居中写
                Integer x = variableCoordinatesVo.getX();
                Integer y = variableCoordinatesVo.getY();
                String name = variableCoordinatesVo.getName();
                // 根据x,y轴居中写
                HuGraphics.drawString(graphics, name, ltBoldFont, x, y, align);

            }
            // 收尾
            graphics.dispose();

			// 比例压缩到一半
            backgroudBuffer = Thumbnails.of(backgroudBuffer).scale(0.5f).asBufferedImage();
			
			// 图片base64码
    		String baseImg = createCertificateImageBase64(backgroudBuffer);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

结果图展示:

压缩绘制后的图片

三、保存证书文件

生成结束后,可以一次性将图片的base64返回给前端,但是绘制图片是一个比较占用内存的操作,当并发量较大时应该减少绘制的频率。可以将生成的证书图片转为文件保存至服务器,再返回文件的url给到前端。

public static void main(String args[]) {
	// ... 
	
	// 比例压缩到一半
	backgroudBuffer = Thumbnails.of(backgroudBuffer).scale(0.5f).asBufferedImage();

	// 1.图片base64码
    String baseImg = createCertificateImageBase64(backgroudBuffer);
	
	// 2.将图片写入ByteArrayOutputStream流,转为字节数组
	ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
	ImageIO.write(backgroudBuffer, "png", byteArrayOutputStream);
	byte[] bytes = byteArrayOutputStream.toByteArray();
}

1.上传至服务器

使用图片base64作为入参

String localFilePath = "C:\\Users\\Dell\\Desktop\\图片\\生成证书.png";
FileUploadUtils.downloadFileFromUrl2(URLDecoder.decode(srcUrl), localFilePath);

2.上传至minio

使用字节数组作为入参

/**
     * 将输出流转为输入流,上传至minio
     *
     * @param bytes
     * @param bizPath 文件夹路径
     * @param fileName 文件名
     */
    public static String outPutStreamUpload(byte[] bytes, String bizPath, String customBucket, String fileName) throws IOException, InvalidKeyException, NoSuchAlgorithmException, InsufficientDataException, InternalException, NoResponseException, InvalidBucketNameException, XmlPullParserException, ErrorResponseException, RegionConflictException, InvalidArgumentException {

        // 创建inputStream流
        InputStream inputStream = new ByteArrayInputStream(bytes);//将inputStream流写成文件(查看文件内容是否正确)

        // 上传至minio
        String fileUrl = "";
        String newBucket = bucketName;
        if (oConvertUtils.isNotEmpty(customBucket)) {
            newBucket = customBucket;
        }
        // 初始化minio
        initMinio(minioUrl, minioName, minioPass);
        if (!minioClient.bucketExists(bucketName)) {
            // 创建一个名为ota的存储桶
            minioClient.makeBucket(bucketName);
        }
        String objectName = bizPath + "/" + fileName;
        minioClient.putObject(newBucket, bizPath + "/" + fileName, inputStream, inputStream.available(), "application/octet-stream");
        inputStream.close();
        fileUrl = minioUrl + newBucket + "/" + objectName;

        // 获取文件大小
        ObjectStat objectStat = minioClient.statObject(newBucket, objectName);
        long fileSize = objectStat.length();
//        log.info("读取minio文件:桶名:{},文件名:{},获取文件大小:{}", minioBucketName, relativePath, fileSize);

        String ofileName = fileName;
        String suffix = "";
        if (StringUtils.isNotEmpty(ofileName)) {
            suffix = ofileName.substring(ofileName.lastIndexOf(".") + 1);
            ofileName = ofileName.substring(0, ofileName.lastIndexOf("."));
        }

        return fileUrl;
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/324138.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【栈】Leetcode 496 下一个更大元素I

【栈】Leetcode 496 下一个更大元素I 解法1 两个单调栈解法2 ---------------&#x1f388;&#x1f388;题目链接&#x1f388;&#x1f388;------------------- 解法1 两个单调栈 两个栈进行操作&#xff0c;一个栈用来遍历寻找&#xff0c;一个栈用来保留 将nums2中的元素…

电商平台api接口对接须知

随着互联网的发展和普及&#xff0c;电商平台已成为人们日常生活中不可或缺的一部分。而为了保证电商平台的正常运行&#xff0c;平台与开发者之间需要进行数据交互&#xff0c;这便涉及到了电商平台API接口对接的问题。本文将详细介绍电商平台API接口对接的须知事项。 一、了…

去他的“不喝不给面子”,我只要“酒文化”,不要“酒桌文化”!

&#xff08;图源网络&#xff0c;侵删&#xff09; 文 | 琥珀酒研社 作者 | 五画 这是我最讨厌的环节。 前段时间&#xff0c;公司对客户安排个答谢宴&#xff0c;我知道我要去餐厅陪酒了&#xff0c;即便我算是个爱喝酒的人&#xff0c;但也感到十分头疼。 进到包厢里我…

数据结构学习 jz53_1 在排序数组中查找数字1 0 ~ n - 1 中缺失的数字

关键词&#xff1a;查找算法 二分法 映射 位运算 题目一&#xff1a;统计目标成绩的出现次数 方法一&#xff1a;我自己写的。[ 用时: 13 m 3 s ] 二分法线性扫描 方法二&#xff1a;看了题解 方法一&#xff1a; 二分法线性查找 思路&#xff1a; 先二分查找找到和targe…

FPGA开发设计

一、概述 FPGA是可编程逻辑器件的一种&#xff0c;本质上是一种高密度可编程逻辑器件。 FPGA的灵活性高、开发周期短、并行性高、具备可重构特性&#xff0c;是一种广泛应用的半定制电路。 FPGA的原理 采用基于SRAM工艺的查位表结构&#xff08;LUT&#xff09;&#xff0c;…

15 万奖金!开放原子开源大赛 OpenAnolis -云原生赛题报名开始

开放原子开源基金会牵头发起的首届“开放原子开源大赛”&#xff0c;旨在联合开源组织、企事业单位、高等院校、科研院所、行业组织、投融资机构等多方资源&#xff0c;充分发挥产业链生态上下游的协同能力&#xff0c;基于开源共享、共建共治的原则共同举办。大赛搭建面向全球…

LaTeX 章节的使用

目录 1、介绍 2、章节的等级 3、取消编号章节 4、章节引用 1、介绍 命令\section{}标志着一个新章的开始&#xff0c;大括号内的文字为章的标题。章的编号是自动生成的&#xff0c;你也可以使用没有编号的章。 \documentclass[]{article}\begin{document}\section{Introd…

Kubernetes (十三) 存储——持久卷-动静态分配

一. 简介 二. NFS持久化存储步骤&#xff08;静态分配&#xff09; 1. 集群外…

python中小数据池和编码

嗨喽&#xff0c;大家好呀~这里是爱看美女的茜茜呐 ⼀. 小数据池 在说小数据池之前. 我们先看⼀个概念. 什么是代码块: 根据提示我们从官⽅⽂档找到了这样的说法&#xff1a; A Python program is constructed from code blocks. A block is a piece of Python program text…

腾讯云服务器租用价格表_2024新版报价

腾讯云服务器租用价格表&#xff1a;轻量应用服务器2核2G3M价格62元一年、2核2G4M价格118元一年&#xff0c;540元三年、2核4G5M带宽218元一年&#xff0c;2核4G5M带宽756元三年、轻量4核8G12M服务器446元一年、646元15个月&#xff0c;云服务器CVM S5实例2核2G配置280.8元一年…

2024年腾讯云新用户优惠云服务器价格多少?

腾讯云服务器租用价格表&#xff1a;轻量应用服务器2核2G3M价格62元一年、2核2G4M价格118元一年&#xff0c;540元三年、2核4G5M带宽218元一年&#xff0c;2核4G5M带宽756元三年、轻量4核8G12M服务器446元一年、646元15个月&#xff0c;云服务器CVM S5实例2核2G配置280.8元一年…

Ubuntu20.04安装配置OpenCV-Python库并首次执行读图

一、选择三方提供的预编译包安装&#xff1a; 可以从官网下载 OpenCV 的安装包&#xff0c;编译后使用&#xff1b;也可以直接使用第三方提供的预编译包 安装。显然后者不需要执行编译步骤&#xff0c;更便捷。选择由 PyPI 提供的 OpenCV 安装包&#xff0c;可以在 https://py…

排序——计数排序

文章目录 概念思路绝对映射&#xff1a;相对映射 代码实现特性结果演示 概念 计数排序是一个非基于比较的排序算法&#xff0c;该算法于1954年由 Harold H. Seward 提出。它的优势在于在对一定范围内的整数排序时&#xff0c;它的复杂度为Ο(nk)&#xff08;其中k是整数的范围…

基于springboot书籍学习平台源码和论文

首先,论文一开始便是清楚的论述了平台的研究内容。其次,剖析平台需求分析,弄明白“做什么”,分析包括业务分析和业务流程的分析以及用例分析,更进一步明确平台的需求。然后在明白了平台的需求基础上需要进一步地设计平台,主要包罗软件架构模式、整体功能模块、数据库设计。本项…

2024年腾讯云服务器多少钱1年?超便宜62元一年

腾讯云服务器租用价格表&#xff1a;轻量应用服务器2核2G3M价格62元一年、2核2G4M价格118元一年&#xff0c;540元三年、2核4G5M带宽218元一年&#xff0c;2核4G5M带宽756元三年、轻量4核8G12M服务器446元一年、646元15个月&#xff0c;云服务器CVM S5实例2核2G配置280.8元一年…

Zung氏抑郁自评量表SDS

抑郁症是常见的心理障碍&#xff0c;其症状表现为&#xff1a;心境低落、思维迟缓、意志活动减退、认知功能损害、躯体症状等。在生活中常有悲观消沉&#xff0c;灰心丧气&#xff0c;对所有事情都提不起兴趣&#xff0c;严重的还会出现肢体僵硬和耳鸣等症状。 部分人有明显的…

Lede(OpenWrt)安装和双宽带叠加

文章目录 一、Lede介绍1. 简介2. 相关网站 二、Lede安装1. 编译环境2. SHELL编译步骤3. 腾讯云自动化助手 三、Lede配置1. 电信接口配置2. 联通接口配置3. 多线多播配置4. 网速测试效果 一、Lede介绍 1. 简介 LEDE是一个专为路由器和嵌入式设备设计的自由和开源的操作系统。 …

哈希(hash)

目录 一、什么是哈希 二、哈希冲突 三、哈希函数 3.1、哈希函数设计原则 3.2、常见的哈希函数 四、哈希冲突解决 4.1、闭散列 4.2、开散列 五、哈希表的模拟实现 5.1、哈希表的功能模拟实现 5.2、测试模拟实现&#xff1a; 一、什么是哈希 如果构造一种存储结构&…

启动Vue项目,报错:‘vue-cli-service‘ 不是内部或外部命令,也不是可运行的程序

前言&#xff1a; 最近在打开一个Vue项目的时候&#xff0c;打开之后输入命令行&#xff1a;npm run serve之后发现&#xff0c;报错&#xff1a;vue-cli-service 不是内部或外部命令&#xff0c;也不是可运行的程序&#xff0c;以下是解决方案&#xff1a; 报错图片截图&…

Day30 78子集 90子集II 491非递减子序列

78 子集 给定一组不含重复元素的整数数组 nums&#xff0c;返回该数组所有可能的子集&#xff08;幂集&#xff09;。 说明&#xff1a;解集不能包含重复的子集。 示例: 输入: nums [1,2,3] 输出: [ [3], [1], [2], [1,2,3], [1,3], [2,3], [1,2], [] ] 注意…