一、背景
博主最近项目有个需要,为门店做海报,涉及到图片的压缩,图片固定位置生成二维码、门店信息。
当然,生成海报,需要的规范的模板,才好确定你需要填充二维码、文字的坐标。废话少说,直接上代码吧。
二、图片压缩
海报下载有两种情况,
- 打印图 很大,一般下载直接贴在门店上,差不多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倍后很完美。