java图片处理

  • Post author:
  • Post category:java

一、背景

博主最近项目有个需要,为门店做海报,涉及到图片的压缩,图片固定位置生成二维码、门店信息。
当然,生成海报,需要的规范的模板,才好确定你需要填充二维码、文字的坐标。废话少说,直接上代码吧。

二、图片压缩

海报下载有两种情况,

  • 打印图 很大,一般下载直接贴在门店上,差不多10M左右
  • 网络图 在打印图上面压缩10倍,用于app上的展示
    当然,模板都是打印图,为了生成网络图快速,在上传打印图的时间,异步压缩

其次,无论是网络图,还是打印图,我们下载的时候生成一个zip包,先上工具类的代码吧

import com.alibaba.csp.sentinel.util.StringUtil;
import com.tuhu.intl.promotion.common.base.ZipFileInfo;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import javax.imageio.ImageIO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StopWatch;

@Slf4j
public class FileUtil {
   
  /**
   * 图片压缩
   * @param inputStream  源图片的输入流
   * @param destUrl  压缩后的图片的地址
   */
  public static void compressFile(InputStream inputStream,String destUrl){
    StopWatch stopWatch=new StopWatch();
    stopWatch.start();
    if(new File(destUrl).exists()){
      return;
    }
    log.info("图片缩放开始========");
    try {
      // 1、定义图像文件对象
      // 2、定义图像图像数据的访问的缓冲器
      BufferedImage bufferedImage = ImageIO.read(inputStream);
      // 3、获取图片的原始宽高
      int width = bufferedImage.getWidth();
      int height = bufferedImage.getHeight();
      // 4、获取图片的缩放【宽高都是*了缩放比例的再取整】
      Image scaledInstance = bufferedImage.getScaledInstance(Double.valueOf(width*0.1).intValue(),Double.valueOf(height * 0.1).intValue(),Image.SCALE_DEFAULT);
      // 5、将Image类型转换成BufferedImage对象[BufferedImage.TYPE_INT_ARGB:表示具有8位RGBA颜色成分的整数像素的图像]
      BufferedImage newImage = new BufferedImage(Double.valueOf(width*0.1).intValue(),Double.valueOf(height * 0.1).intValue(),BufferedImage.TYPE_INT_BGR);
      // 一个新的图形上下文,这是这个图形上下文的副本
      Graphics g = newImage.getGraphics();
      // 绘制图片大小
      g.drawImage(scaledInstance, 0, 0, null);
      // 释放文件资源
      g.dispose();
      // 将新的图片文件写入到指定的文件夹中
      String formatName=destUrl.substring(destUrl.lastIndexOf(".")+1);
      ImageIO.write(newImage,formatName,new File(destUrl));
    } catch (Exception e) {
      log.error("图片缩放出错,错误信息{}",e.getMessage());
    }
    stopWatch.stop();
    log.info("图片缩放成功 总耗时:{}",stopWatch.getTotalTimeMillis());
  }

   /**
   * 多个文件打车zip包
   * @param list zip包里面的文件信息集合
   * @param zipFile 压缩后的zip包文件
   */
  public static void zipFiles(List<ZipFileInfo> list, File zipFile) {
    StopWatch stopWatch=new StopWatch();
    stopWatch.start();
    FileOutputStream fileOutputStream = null;
    ZipOutputStream zipOutputStream = null;
    try {
      fileOutputStream = new FileOutputStream(zipFile);
      zipOutputStream = new ZipOutputStream(fileOutputStream);
      // 创建 ZipEntry 对象
      ZipEntry zipEntry;
      // 遍历源文件数组
      for(ZipFileInfo shop:list){
        // 将源文件数组中的当前文件读入 FileInputStream 流中
        // 实例化 ZipEntry 对象,源文件数组中的当前文件
        zipEntry = new ZipEntry(shop.getFileName());
        zipOutputStream.putNextEntry(zipEntry);
        // 该变量记录每次真正读的字节个数
        int len;
        // 定义每次读取的字节数组
        byte[] buffer = new byte[1024];
        while (!((len = shop.getInputStream().read(buffer)) <= 0)) {
          zipOutputStream.write(buffer, 0, len);
        }
      }

    } catch (Exception e) {
      log.error("压缩图片出错,错误信息{}",e.getMessage());
    } finally {
      try {
        if (zipOutputStream != null) {
          zipOutputStream.closeEntry();
          zipOutputStream.close();
        }
      } catch (IOException e) {
        log.error("压缩图片出错,错误信息{}",e.getMessage());
      }
      try {
        if (fileOutputStream != null) {
          fileOutputStream.close();
        }
      } catch (IOException e) {
        log.error("压缩图片出错,错误信息{}",e.getMessage());
      }
    }
    stopWatch.stop();
    log.info("图片压缩zip包耗时:{}",stopWatch.getTotalTimeMillis());
  }

   /**
   * 下载文件
   * @param sourceUrl 源文件的http格式的url路径
   * @param descUrl   下载后的目标文件的地址
   */
  public static void downloadUrlFile(String sourceUrl, String descUrl){
    if(new File(descUrl).exists()){
      return;
    }
    InputStream is=null;
    FileOutputStream os=null;
    try {
      // 构造URL
      URL url = new URL(sourceUrl);
      // 打开连接
      URLConnection con = url.openConnection();
      // 输入流
      is = con.getInputStream();
      // 1K的数据缓冲
      byte[] bs = new byte[1024];
      // 读取到的数据长度
      int len;
      // 输出的文件流
      File file = new File(descUrl);
      os = new FileOutputStream(file, true);
      // 开始读取
      while ((len = is.read(bs)) != -1) {
        os.write(bs, 0, len);
      }
    }catch (Exception e){
      log.error("downloadUrlFile error sourceUrl={},descUrl={}",sourceUrl,descUrl,e);
    }finally {
      // 完毕,关闭所有链接
      try {
        if(os!=null){
          os.close();
        }
      } catch (IOException e) {
        log.error(e.getMessage());
      }
      try {
        if(is!=null){
          is.close();
        }
      } catch (IOException e) {
        log.error(e.getMessage());
      }
    }
  }

  /**
   * 删除文件
   */
  public static void deleteFile(String... urls){
    for(String url:urls){
      try {
        if(StringUtil.isNotBlank(url)){
          Files.deleteIfExists(Paths.get(url));
        }
      } catch (IOException e) {
        log.error("deleteFile error url={} error ",url,e);
      }
    }
  }
}

二维码生成添加依赖

  <!-- 二维码生成 -->
    <!-- https://mvnrepository.com/artifact/com.google.zxing/core -->
    <dependency>
      <groupId>com.google.zxing</groupId>
      <artifactId>core</artifactId>
      <version>3.0.1</version>
    </dependency>

生成二维码图片+背景+文字描述工具类

import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import com.tuhu.intl.promotion.common.base.ImgFIllInfo;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.imageio.ImageIO;
import javax.swing.JLabel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StopWatch;

/**
 * 生成二维码图片+背景+文字描述工具类
 */
@Slf4j
public class QRCodeMaxUtil {


  //文字显示
  private static final int QR_COLOR = 0x201f1f; // 二维码颜色:黑色
  private static final int BG_WHITE = 0xFFFFFF; //二维码背景颜色:白色

  private static final String[] SHOP_KEY = {"shopName", "telephone", "address"};

  private static final String QR_CODE_KEY = "qrCode";

  // 设置QR二维码参数信息
  private static final Map<EncodeHintType, Object> hints = new HashMap<EncodeHintType, Object>() {
    private static final long serialVersionUID = 1L;
    {
      put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);// 设置QR二维码的纠错级别(H为最高级别)
      put(EncodeHintType.CHARACTER_SET, "utf-8");// 设置编码方式
      put(EncodeHintType.MARGIN, 0);// 白边
    }
  };

  public static InputStream getFileInputStream(InputStream inputStream,String formatName,List<ImgFIllInfo> fillList) {
    BufferedImage bufferedImage = creatQRCode(inputStream,fillList);
    return bufferImageToInputStream(bufferedImage, formatName);
  }

  private static InputStream bufferImageToInputStream(BufferedImage bufferedImage,String formatName) {
    log.info("BufferedImage 转换为 InputStream 开始");
    StopWatch stopWatch=new StopWatch();
    stopWatch.start();
    ByteArrayOutputStream os = new ByteArrayOutputStream();
    try {
      ImageIO.write(bufferedImage, formatName, os);
      ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(os.toByteArray());
      stopWatch.stop();
      log.info("BufferedImage 转换为 InputStream 完成,耗时{}",stopWatch.getTotalTimeMillis());
      return byteArrayInputStream;
    } catch (IOException e) {
      log.error("BufferedImage 转换为 InputStream 出错,错误信息{}",e.getMessage());
    }
    return null;
  }

  /**
   * 生成二维码图片+背景+文字描述
   */
  public static BufferedImage creatQRCode(InputStream bgImgFile,List<ImgFIllInfo> contentList) {
    log.info("生成二维码图片+背景+文字描述开始");
    StopWatch stopWatch=new StopWatch();
    stopWatch.start();
    //获取二维码配置
    ImgFIllInfo qrCode = contentList.stream()
        .filter(item -> item.getKey().equals(QR_CODE_KEY)).collect(Collectors.toList()).get(0);
    try {

      MultiFormatWriter multiFormatWriter = new MultiFormatWriter();
      // 参数顺序分别为: 编码内容,编码类型,生成图片宽度,生成图片高度,设置参数
      BitMatrix bm = multiFormatWriter
          .encode(MessageFormat.format(qrCode.getValue(),qrCode.getExtraData()), BarcodeFormat.QR_CODE, qrCode.getMaxWidth(), qrCode.getMaxWidth(), hints);
      BufferedImage image = new BufferedImage(qrCode.getMaxWidth(), qrCode.getMaxWidth(),
          BufferedImage.TYPE_INT_RGB);
      for (int x = 0; x < qrCode.getMaxWidth(); x++) {
        for (int y = 0; y < qrCode.getMaxWidth(); y++) {
          image.setRGB(x, y, bm.get(x, y) ? QR_COLOR //int 0
              : BG_WHITE);
        }
      }

      BufferedImage backgroundImage = ImageIO.read(bgImgFile);
      Graphics2D rng = backgroundImage.createGraphics();
      rng.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP));
      rng.drawImage(image, qrCode.getTextX(), qrCode.getTextY(), qrCode.getMaxWidth(), qrCode.getMaxWidth(), null);

      //文字描述参数设置
      Color textColor = Color.white;
      rng.setColor(textColor);

      List<ImgFIllInfo> shopList = contentList.stream()
          .filter(ImgFIllInfo -> Arrays.asList(SHOP_KEY).contains(ImgFIllInfo.getKey()))
          .collect(Collectors.toList());

      for (ImgFIllInfo item : shopList) {
        ///设置字体类型和大小,颜色(BOLD加粗/ PLAIN平常)
        Font font = new Font(item.getFontName(), item.getFontStyle()==1?Font.BOLD:Font.PLAIN, item.getFontSize());
        rng.setFont(font);
        rng.setColor(Color.black);
        drawString(rng, font, item.getValue(), item.getTextX(),item.getTextY(), item.getMaxWidth());
      }
      rng.dispose();
      image = backgroundImage;
      image.flush();
      stopWatch.stop();
      log.info("生成二维码图片+背景+文字描述开始结束,总耗时:{}",stopWatch.getTotalTimeMillis());
      return image;
    } catch (Exception e) {
      log.error("生成二维码图片+背景+文字描述出错,错误信息{}",e.getMessage());
    }
    return null;
  }


  /***
   * @param text 文本内容
   * @param x    起始点X轴坐标
   * @param y    起始点Y轴坐标
   * @param maxWidth  文字最大长度
   */
  public static void drawString(Graphics2D g, Font font, String text, int x, int y, int maxWidth) {
    JLabel label = new JLabel(text);
    label.setFont(font);
    FontMetrics metrics = label.getFontMetrics(label.getFont());
    int textH = metrics.getHeight();
    int textW = metrics.stringWidth(label.getText()); //字符串的宽
    String tempText = text;
    while (textW > maxWidth) {
      int n = textW / maxWidth;
      int subPos = tempText.length() / n;
      String drawText = tempText.substring(0, subPos);
      int subTxtW = metrics.stringWidth(drawText);
      while (subTxtW > maxWidth) {
        subPos--;
        drawText = tempText.substring(0, subPos);
        subTxtW = metrics.stringWidth(drawText);
      }
      g.drawString(drawText, x, y);
      y += textH;
      textW = textW - subTxtW;
      tempText = tempText.substring(subPos);
    }
    g.drawString(tempText, x, y);
  }
}

压缩zip包的实体类

import java.io.InputStream;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.experimental.Accessors;

@Data
@Accessors
@AllArgsConstructor
public class ZipFileInfo {

  private InputStream inputStream;

  private String fileName;

  private String formatName;
}

生成二维码图片+背景+文字描述实体类

package com.tuhu.intl.promotion.common.base;
import java.util.List;
import lombok.Data;
import lombok.experimental.Accessors;

@Data
@Accessors(chain = true)
public class ImgFIllInfo {

  private String key;

  private String value;

  private int fontSize;

  private int fontStyle;

  private String fontName;

  /**
   * 当key为二维码时,maxWidth就是二维码的宽、高
   */
  private int maxWidth;

  /**
   * 当key为二维码时,textX就是imageX
   */
  private int textX;

  /**
   * 当key为二维码时,textY就是imageY
   */
  private int textY;

  private List<String> extraData;
}

测试

 public static void main(String[] args) throws IOException {
    String bigUrl = "https://ptttor-dev-1309300786.cos.ap-bangkok.myqcloud.com/tech-dev/web/image/bddee7e0_1655805533304.png";
    String bigContent = "[{\"extraData\":[],\"key\":\"qrCode\",\"maxWidth\":2802,\"textX\":2349,\"textY\":5600,\"value\":\"http://www.baidu.com?shopCode={0}\"},{\"key\":\"shopName\",\"fontSize\":320,\"fontStyle\":1,\"fontName\":\"SimSun,Cordia New\",\"maxWidth\":5480,\"textX\":1100,\"textY\":10150,\"value\":\"门店23名称的专กรุงเทพมหานคร属位置\"},{\"key\":\"telephone\",\"fontSize\":240,\"fontStyle\":0,\"fontName\":\"SimSun,Cordia New\",\"maxWidth\":5480,\"textX\":1100,\"textY\":11200,\"value\":\"123-001\"},{\"key\":\"address\",\"fontSize\":240,\"fontStyle\":0,\"fontName\":\"SimSun,Cordia New\",\"maxWidth\":5480,\"textX\":1100,\"textY\":11700,\"value\":\"金45融กรุงเทพมหานคร港站\"}]";

    String smallUrl="http://ptttor-dev-1309300786.cos.ap-bangkok.myqcloud.com/intl-promotion/upload/bddee7e0_1655805533304.png/compress_bddee7e0_1655805533304.png";
    String samllContent="[{\"extraData\":[],\"key\":\"qrCode\",\"maxWidth\":295,\"textX\":230,\"textY\":560,\"value\":\"http://www.baidu.com?shopCode={0}\"},{\"key\":\"shopName\",\"fontSize\":32,\"fontStyle\":1,\"fontName\":\"SimSun,Cordia New\",\"maxWidth\":548,\"textX\":110,\"textY\":1015,\"value\":\"哈哈12门店名称的哈哈专属位置กรุงเทพมหานค超长拉,测试换行没\"},{\"key\":\"telephone\",\"fontSize\":24,\"fontStyle\":0,\"fontName\":\"SimSun,Cordia New\",\"maxWidth\":548,\"textX\":110,\"textY\":1120,\"value\":\"123-001\"},{\"key\":\"address\",\"maxWidth\":548,\"fontSize\":24,\"fontStyle\":0,\"fontName\":\"SimSun,Cordia New\",\"textX\":110,\"textY\":1170,\"value\":\"กรุงเทพมหานคร/เขตพระนคร/แขวงพระบรมมหาราชวัง,好长的地址呀,该换行拉\"}]";

    boolean bigFlag=true;
    List<ImgFIllInfo> fillList = JsonUtils.toList(bigFlag?bigContent:samllContent, ImgFIllInfo.class);
    File zipFile = new File("D:\\temp\\ZipFieeeele.zip");
    List<ZipFileInfo> zipFileList=new ArrayList<>();

    String down="D://temp//down.png";
    FileUtil.downloadUrlFile(bigFlag?bigUrl:smallUrl,down);

    for(int i=0;i<2;i++){
      InputStream inputStream = QRCodeMaxUtil
          .getFileInputStream(new FileInputStream(down), "png",fillList);
      zipFileList.add(new ZipFileInfo(inputStream,i+".png","png"));
    }
    // 调用压缩方法
    FileUtil.zipFiles(zipFileList, zipFile);
  }

三、填充文字动态纵坐标

场景:填充文字内容在文字标题的下一行,有时文字标题占一行或者两行,不定,这是内容的纵坐标就需要随着标题的内容上移或者下移
思路:只需在填充文本内容的地址返回该文本的行数,然后内容的纵坐标在标题的纵坐标+行数*一行的高度就行


  /***
   * @param text 文本内容
   * @param x    起始点X轴坐标
   * @param y    起始点Y轴坐标
   * @param maxWidth  文字最大长度
   */
  public static int drawStringReturnLine(Graphics2D g, Font font, String text, int x, int y, int maxWidth) {
    int lineNum = 0;

    JLabel label = new JLabel(text);
    label.setFont(font);
    FontMetrics metrics = label.getFontMetrics(label.getFont());
    int textH = metrics.getHeight();
    int textW = metrics.stringWidth(label.getText()); //字符串的宽
    String tempText = text;

    while (textW > maxWidth) {
      int n = textW / maxWidth;
      int subPos = tempText.length() / n;
      String drawText = tempText.substring(0, subPos);
      int subTxtW = metrics.stringWidth(drawText);
      while (subTxtW > maxWidth) {
        subPos--;
        drawText = tempText.substring(0, subPos);
        subTxtW = metrics.stringWidth(drawText);
      }
      g.drawString(drawText, x, y);
      y += textH;
      textW = textW - subTxtW;
      tempText = tempText.substring(subPos);
      lineNum++;
    }
    g.drawString(tempText, x, y);
    return lineNum;
  }
 int lineNum = 0;
      for (ImgFIllInfo item : shopList) {
        int textH = item.getLineHeight();
        ///设置字体类型和大小,颜色(BOLD加粗/ PLAIN平常)
        Font font = new Font(item.getFontName(), item.getFontStyle() == 1 ? Font.BOLD : Font.PLAIN, item.getFontSize());
        rng.setFont(font);
        rng.setColor(Color.black);
        int strNum = drawStringReturnLine(rng, font, item.getValue(), item.getTextX(), (item.getTextY() + lineNum * textH), item.getMaxWidth());
        lineNum = lineNum + strNum + 1;
      }

四、问题总结

1 生成二维码白边

根据二维码链接的不同,以及二维码图片、面积的不同,生成的白边面积不同,可以自行去掉白边,但是去掉后会影响预期效果,也可以自行微调二维码宽高及x,y坐标

2 图片填充文字乱码

常见是服务器上没有相应的字体,或者设置了字体,字体的名称填写错误
new Font 第一个参数是设置字体名称,注意字体名称不要写错,当然可以支持多种字体,以逗号分割 SimSun,Cordia New

 Font font = new Font("SimSun,Cordia New", item.getFontStyle()==1?Font.BOLD:Font.PLAIN, item.getFontSize());
        rng.setFont(font);
        rng.setColor(Color.black);

服务器查看已安装的字体
在这里插入图片描述
或者代码查看服务已安装的字体

import java.awt.*;
public class FontTest {
    public static void main(String[] args) {
        Font[] fonts = GraphicsEnvironment
                .getLocalGraphicsEnvironment().getAllFonts();
        for (Font f : fonts) {
            System.out.println("Name:" + f.getFontName());
        }
    }
}

在控制台寻找自己想要的字体的名称,再替换到new Font()的指定位置就可以了

在这里插入图片描述

3 图片压缩后蒙上一层红色

因为使用错误的色彩空间渲染图像,给BufferedImage设置type为BufferedImage.TYPE_INT_RGB,如果不行再设置为TYPE_INT_ARGB,或者TYPE_INT_BGR,因为原本图片的位深度是24,替换的位深度是32或者原本图片的位深度是32,替换的位深度是24这两种都是不行的

   BitMatrix bm = multiFormatWriter .encode(MessageFormat.format(qrCode.getValue(),qrCode.getExtraData()), BarcodeFormat.QR_CODE, qrCode.getMaxWidth(), qrCode.getMaxWidth(), hints);
      BufferedImage image = new BufferedImage(qrCode.getMaxWidth(), qrCode.getMaxWidth(),
          BufferedImage.TYPE_INT_RGB);

4 图片压缩变模糊

博主模板图片压缩10倍后变的模糊,5倍还好,然后向产品反映,经确认,原先图片确实不是很支持压缩10倍,然后UI、产品重新给了一个模板,压缩10倍后很完美。


版权声明:本文为zhanlijuan2015原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。