关于java通过背景图生成图片


最近用到了需要生成图片的开发,作为一个没有接触过这个的后端,实在头秃,记录一下学习历程

对接部分(碎碎念,可跳过)

没错,虽然没有接触过图片生成,但是还要去对接另一个开发组的jar包,虽然过程时常心塞,但不得不说,这给我的学习提供了一条捷径。

引入本地jar包

我是把jar包放在了resources下面
resources/lib/test-aaa-sdk-1.0.jar
然后要在pom里面引入一下这个jar包

<dependency>
	<groupId>com.test</groupId>
	<artifactId>test-aaa-sdk</artifactId>
	<version>1.0</version>
	<scope>system</scope>
	<systemPath>${project.basedir}/src/main/resources/lib/test-aaa-sdk-1.0.jar</systemPath>
</dependency>

可以先在idea手动引入一下这个包去看一下groupId和artifactId,不过一般artifactId和version看包名就能知道。
scope这里使用system表示使用系统路径下的jar包,需要跟systemPath一起用,systemPath里面的${project.basedir}是获取项目的本地系统路径
这里会遇到一个问题,就是本地这个包的引入是没有问题的,可以正常运行,测试调试也正常,但是把项目打成jar包后会无法启动并提示NoClassDefFoundError,因为打包的时候没有把resources/lib下的jar包打包到BOOT-INF/lib下面,需要在pom的plugin下面新增incloudeSystemScope配置使maven打包的时候把scope为system引入的jar包也打包到项目里

<plugins>
	<plugin>
		其他配置
		<configuration>
			<incloudeSystemScope>true</incloudeSystemScope>
		</configuration>
	</plugin>
</plugins>

关于idea引入本地包也记一下吧
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

文件路径

严重怀疑对方根本就没有打包成jar包测试运行过,可能只是在idea或其他开发软件本地调通了。
由于要调用的方法需要传入背景图片的路径,所以给的jar包里的静态资源文件夹下的图片唯一的作用大概就是可以让我复制出来而不必单独再跟对方要了。

错误尝试

起初我是模仿jar中的方式,将背景图片放在了resources/static/image下面,通过ClassPathResource去获取文件路径

ClassPathResource classPathResource = new ClassPathResource("static/image/test.jpg");
String templateImgPath = classPathResource.getFile().getPath();

与对方给我的jar包一样,虽然开发过程中可以本地测试成功,但是打包成jar包之后会报错无法获取到文件地址
class path resource [static/image/test.jpg] cannot be resolved to absolute file path because it does not reside in the file :jar:file:/test/test.jar!/BOOT-INF/classes!/static/image/test.jpg
于是我进行的另一种尝试

String templateImgPath = this.getClass().getClassLoader().getResource("static/image/test.jpg").getFile();

报错信息变成了无法读取文件
Can’t read input file!
但其实我在写自己的模块时获取字体用了相同的方法去获取文件流,是成功的

String fontfilename = "/font/test.ttf"; // /font/test.ttf是在resources下的路径
InputStream is = this.getClass().getClassLoader().getResourceAsStream(fontfilename);

这里加上getClassLoader()就可以读取jar包里的resource资源,如果是本地调用去掉这个即可调用成功

解决方案

将背景图片和字体文件放在了服务器的某个目录下面,然后将路径配置写在了nacos中,这里可以根据自己的配置参数方式进行调整。

@Value("${file.path}")
private String path;

如果是自己写的内容可以很快的调整成文件流去获取,但是等对方改就。。。
是的,最后别无他法,只能固定路径去获取。
由于是k8s部署,需要挂载一个pvc,还去改了部署脚本,心酸。。。

开发部分

获取字体的方法

public Font getFont(int style, int size) { 
	//我用的 0,16 0是正常字体,其他的还有加粗斜体之类的
	String fontfilename = "/font/test.ttf"; //resources下的路径
	//这个打成jar包在linux环境也能正常读取resources下的字体文件,本地运行需要去掉.getClassLoader()
	InputStream is = this.getClass().getClassLoader().getResourceAsStream(fontfilename);
	Font actionJson = null;
	try {
		//创建字体
		actionJson = Font.createFont(Font.TRUETYPE_FONT,is);
	}catch (FontFormatException e){
		throw new RuntimeException(e);
	}catch (IOException e){
		throw new RuntimeException(e);
	}
	//设置字体格式
	Font actionJsonBase = actionJson.deriveFont(style,size);
	return actionJsonBase;
}

关于二维码的生成

业务要求还要加一个可以跳转到指定页面的二维码在图片上,以及没有白边
没有白边这个真的是搞了好久
这里引入的依赖是zxing

<dependency>
	<groupId>com.google.zxing</groupId>
	<artifactId>core</artifactId>
	<version>3.3.3</version>
</dependency>
<dependency>
	<groupId>com.google.zxing</groupId>
	<artifactId>javase</artifactId>
	<version>3.3.3</version>
</dependency>
public class QRCodeUtil {
	public static boolean generateCode(String url,String path,String fileName) throws WriterException,IOException{
		//url是跳转地址,path是二维码保存路径,fileName是二维码保存的文件名
		int width = 50;//根据业务要求而定
		int height = 50;
		// 二维码格式
		String format = "png";
		// 设置编码,防止中文乱码
		Hashtable<EncodeHintType,Object> ht = new Hashtable<EncodeHintType,Object>();
		ht.put(EncodeHintType.CHARACTER_SET,"UTF-8");
		//设置二维码参数
		BitMatrix bitMatrix = new MultiFormatWriter().encode(url,BarcodeFormat.QR_CODE,width,height,ht);
		//二维码保存路径
		File file = new File(path);
		if(!file.exists()){
			//如果路径不存在则新建
			file.mkdir();
		}
		//建立图像缓冲器
		BufferedImage bufferedImage = new BufferedImage(width,height,BufferedImage.TYPE_3BYTE_BGR);
		for (int x=0;x<width;x++){
			for (int y=0;y<height;y++){
				bufferedImage.setRGB(x,y,bitMatrix.get(x,y)?BLACK : WHITE);
			}
		}
		int imageRGB = bufferedImage.getRGB(3,3);
		BufferedImage tmpImg = new BufferedImage(width,height,BufferedImage.TYPE_4BYTE_ABGR);//支持透明
		//复制原图到新图并设置背景为透明
		for(int i=0;i<width;++i){
			for(int j=0;j<width;++j){
				//设置背景为透明
				if(bufferedImage.getRGB(i,j) == imageRGB){
					tmpImg.setRGB(i,j,imageRGB & 0x00ffffff);
				}
				//设置透明度
				else{
					int rgb = tmpImg.getRGB(i,j);
					rgb = ((10*255/10)<<24)|(rgb & 0x00ffffff);
					tmpImg.setRGB(i,j,rgb);
				}
			}
		}
		//保存二维码
		ImageIO.write(tmpImg, format, new File(path+"/"+fileName+"."+format));
		return true;
	}
}

好啦,二维码部分终于完成啦!

关于在背景图上添加内容

首先是把背景图读取成可编辑的类型

public static BufferedImage generate(String[] texts, String templateImgPath,String qrImgPath,Font textFont) throws IOException {
	//texts是要写在图上的文本内容,templateImgPath是背景图地址,qrImgPath是二维码路径,textFont是字体
	BufferedImage srcImg = readImg(templateImgPath);
	//设置文字颜色为黑色
	Color textColor = new Color(38,38,38);
	//写字
	BufferedImage textedImg = addTextWaterMark(srcImg, textColor, textFont, texts);
	if(Objects.equals(qrImgPath,"")){
		return textedImg;
	}else{
		//读取二维码图片
		BufferedImage qrImg = readImg(qrImgPath);
		//叠加图片
		return addImageWaterMark(textedImg,qrImg,new Point(200,300));//写好文字的图片作为背景图,传入二维码图片和要放的坐标
	}
}
protected static BufferedImage readImg(String imgPath) throws IOException {
	BufferedImage srcImg;
	if(imgPath.startsWith("http")){
		URL url = new URL(imgPath);
		srcImg = ImageIO.read(url.openStream());
	}else {
		File srcImgFile = new File(imgPath);
		srcImg = ImageIO.read(srcImgFile);
	}
	return srcImg;
}

关于在背景图上写字

写字真的是实践出真知,一开始以为不就是写个字嘛,设置好字体和大小不就好了?
结果写出来的字好丑好丑的,请看
在这里插入图片描述
好在找到了改善的方法,改善之后是这样子的
在这里插入图片描述

protected static BufferedImage addTextWaterMark(BufferedImage targetImg, Color textColor, Font textFont, String[] texts) throws InvalidObjectException {
	if(texts.length !=1){ //看自己有几行需要写
		throw new InvalidObjectException("需要1个可变参数");
	}else{
		try{
			int width = targetImg.getWidth();
			int height = targetImg.getHeight();
			BufferedImage bufferedImage = new BufferedImage(width,height,BufferedImage.TYPE_INT_ARGB);
			Graphics2D g = bufferedImage.createGraphics();
			//使背景透明,其实最后发现没必要加这个,但是也不影响效果就放着了
			bufferedImage = g.getDeviceConfiguration().createCompatibleImage(width,height,Transparency.TRANSLUCENT);
			g.dispose();
			g = bufferedImage.createGraphics();
			//加载背景图
			g.drawImage(targetImg,0,0,width,height,(ImageObserver)null);
			//设置画笔颜色
			g.setColor(textColor);
			//设置字体
			g.setFont(textFont);
			//提高画图文字质量,也不知道具体是哪个起作用了,都放在这儿了
			g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
			//消除锯齿
			g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
			g.setRenderingHint(RenderingHints.KEY_RENDERING,RenderingHints.VALUE_RENDER_QUALITY);
			g.setRenderingHint(RenderingHints.KEY_RENDERING,RenderingHints.VALUE_RENDER_SPEED);
			//写字 参数:g 画板 texts[0] 要写的字符串 20.0F 写字框的高度 100.0F 写字框的最大宽度 1 最大行数 10.0F 写字框距离左边 200.0F 写字框距离上边
			drawString(g,texts[0],20.0F,100.0F,1,10.0F,200.0F);
			//画完了
			g.dispose();
			return bufferedImage;
		}catch (Exception e){
			e.printStackTrace();
			return null;
		}
	}
}
//写字
private static void drawString(Graphics2D g, String text, float lineHeight, float maxWidth, int maxLine, float left, float top) {
	if(text != null && text.length()!=0) {
		FontMetrics fm = g.getFontMetrics();
		StringBuilder s = new StringBuilder();
		for(int i=0; i<text.length(); ++i){
			char c = text.charAt(i);
			s.append(c);
			//获取要写的这一行的长度
			int stringWidth = fm.stringWidth(s.toString());
			//有换行和超长的情况下写字
			if(c=='\n' || (float)stringWidth > maxWidth) {
				if(c=='\n'){
				//下一行的起始字符位置
					++i;
				}
				if(maxLine>1){
				//如果允许有下一行的话先写当前行,然后递归剩下的并调整为下一行的高度
					g.drawString(text.substring(0,i),left,top);
					drawString(g, text.substring(i),lineHeight,maxWidth,maxLine - 1,left,top+lineHeight);
				}else{
				//如果不允许有下一行的话退回到前一个字符的位置加上省略号画到图上
					g.drawString(text.substring(0,i-1)+"…",left,top);
				}
				return;
			}
		}
		//没有换行和超长的情况下写字
		g.drawString(text,left,top);
	}
}

关于在背景图上叠加图片

其实没有想的那么难,照葫芦画瓢

protected static BufferedImage addImageWaterMark(BufferedImage targetImg, BufferedImage waterImg, Point point){
	try{
		int width = targetImg.getWidth();
		int height = targetImg.getHeight();
		BufferedImage bufferedImage = new BufferedImage(width,height,BufferedImage.TYPE_INT_ARGB);
		Graphics2D g = bufferedImage.createGraphics();
		//使背景透明,其实最后发现没必要加这个,但是也不影响效果就放着了
		bufferedImage = g.getDeviceConfiguration().createCompatibleImage(width,height,Transparency.TRANSLUCENT);
		g.dispose();
		g = bufferedImage.createGraphics();
		//加载背景图
		g.drawImage(targetImg,0,0,width,height,(ImageObserver)null);
		//叠加二维码图片
		g.drawImage(waterImg,point.x,point.y,50,50,(ImageObserver)null);
		g.dispose();
		return bufferedImage;
	}catch(Exception e){
		e.printStackTrace();
		return null;
	}
}

关于保存图片

保存图片这里是个大坑
首先我在本地调试,先按保存在本地走吧

第一次尝试

我的背景图是320k的png,处理之后保存的jpg只有80k,被设计diss处理完的图片太糊了

public static void saveImg(BufferedImage targetImg, String outPath) {
	try {
		int width = targetImg.getWidth();
		int height = targetImg.getHeight();
		//这里需要再重新加载一下,不然图就会蒙着一层红色
		BufferedImage bufferedImage = new BufferedImage(width,height,BufferedImage.TYPE_INT_BGR);
		Graphics2D g = bufferedImage.createGraphics();
		g.drawImage(targetImg,0,0,width,height, (ImageObserver)null);
		g.dispose();
		FileOutputStream outImgStream = new FileOutputStream(outPath);
		ImageIO.write(bufferedImage,"jpg",outImgStream);
		outImgStream.flush();
		outImgStream.close();
	}catch (Exception e){
		e.printStackTrace();
	}
}

第二次尝试

我想,保存类型换成png可能会好一些?
果然,变成了240k,清晰了许多,但设计还是说糊

public static void saveImg(BufferedImage targetImg, String outPath) {
	try {
		int width = targetImg.getWidth();
		int height = targetImg.getHeight();
		//这里需要再重新加载一下,不然图就会蒙着一层红色
		BufferedImage bufferedImage = new BufferedImage(width,height,BufferedImage.TYPE_INT_BGR);
		Graphics2D g = bufferedImage.createGraphics();
		g.drawImage(targetImg,0,0,width,height, (ImageObserver)null);
		g.dispose();
		FileOutputStream outImgStream = new FileOutputStream(outPath);
		ImageIO.write(bufferedImage,"png",outImgStream);
		outImgStream.flush();
		outImgStream.close();
	}catch (Exception e){
		e.printStackTrace();
	}
}

第三次尝试

找了好久,有人说可以设置图片质量参数,开搞
本地生成的图片是308k,设计勉强点头了
但是比较坑的是,打jar包出现了问题
package com.sun.image.codec.jpeg does not exist
原因是我的jdk不包含sun.*的类库,需要手动导入需要的rt.jar
没有死磕,因为还有另一种方法还没尝试

public static void saveImg(BufferedImage targetImg, String outPath) {
	try {
		int width = targetImg.getWidth();
		int height = targetImg.getHeight();
		//这里需要再重新加载一下,不然图就会蒙着一层红色
		BufferedImage bufferedImage = new BufferedImage(width,height,BufferedImage.TYPE_INT_BGR);
		Graphics2D g = bufferedImage.createGraphics();
		g.drawImage(targetImg,0,0,width,height, (ImageObserver)null);
		g.dispose();
		//虽然设置了dpi,但是发现好像并不会起作用
		int dpi = 1000;
		FileOutputStream fos = new FileOutputStream(outPath);
		JPEGImageEncoder jpegEncoder = JPEGCodec.createJPEGEncoder(fos);
		JPEGEncodeParam jpegEncodeParam = jpegEncoder.getDefaultJPEGEncodeParam(bufferedImage);
		jpegEncodeParam.setDensityUnit(JPEGEncodeParam.DENSITY_UNIT_DOTS_INCH);
		//这个才是真正起作用的,质量参数取值范围0.01-1
		jpegEncodeParam.setQuality(1f,false);
		jpegEncodeParam.setXDensity(dpi);
		jpegEncodeParam.setYDensity(dpi);
		jpegEncoder.setJPEGEncodeParam(jpegEncodeParam);
		jpegEncoder.encode(bufferedImage,jpegEncodeParam);
		fos.close();
	}catch (Exception e){
		e.printStackTrace();
	}
}

最终方案

这样就本地和线上jar包都没有问题啦

public static void saveImg(BufferedImage targetImg, String outPath) {
	try {
		int width = targetImg.getWidth();
		int height = targetImg.getHeight();
		//这里需要再重新加载一下,不然图就会蒙着一层红色
		BufferedImage bufferedImage = new BufferedImage(width,height,BufferedImage.TYPE_INT_BGR);
		Graphics2D g = bufferedImage.createGraphics();
		g.drawImage(targetImg,0,0,width,height, (ImageObserver)null);
		g.dispose();
		//这里要用jpg,用png会无法使用质量参数导致输出图片变糊
		Iterator<ImageWriter> iter = ImageIO.getImageWritersByFormatName("jpg");
		if(iter.hasNext()){
			ImageWriter writer = iter.next();
			JPEGImageWriteParam param = new JPEGImageWriteParam(null);
			//也可以通过 ImageWriteParam param = writer.getDefaultWriteParam(); 来获取
			param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
			//设置质量参数
			param.setCompressionQuality(1f);
			FileImageOutputStream out = new FileImageOutputStream(new File(outPath));
			writer.setOutput(out);
			writer.write(null,new IIOImage(bufferedImage,null,null),param);
			out.close();
			writer.dispose();
		}
	}catch (Exception e){
		e.printStackTrace();
	}
}

关于文件读取为MultipartFile类型

因为有专门的文件模块,所以这里需要把文件以MultipartFile的类型传过去。
其实也没有必要,完全可以在保存的时候保存成指定的文件名然后把相关信息保存在文件表里。
但是做都做了,就写一下吧。

File file = new File(filePath);
//在这之前要先保存图片然后再读取保存之后的图片
FileInputStream fileInputStream = new FileInputStream(file);
//这里需要引入一个测试的依赖去模拟一个MultipartFile
MultipartFile multipartFile = new MockMultipartFile(fileName,fileName,"jpg",fileInputStream);
//传参给要调用的接口
fileInputStream.close();

引入的依赖

<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-test</artifactId>
</dependency>

关于BufferedImage转MultipartFile

对接的时候干了一件特别傻的事情,就是没有通过对接jar包里的保存方法去保存图片,而是试图用处理图片返回的BufferedImage转成MultipartFile去传参给保存文件接口
但是做都做了,还是记一下吧

int width = img.getWidth();
int height = img.getHeight();
//这里需要再重新加载一下,不然图就会蒙着一层红色
BufferedImage bufferedImage = new BufferedImage(width,height,BufferedImage.TYPE_INT_BGR);
Graphics2D g = bufferedImage.createGraphics();
g.drawImage(img,0,0,width,height, (ImageObserver)null);
g.dispose();
//开始转换
//outputStream转inputStream
ByteArrayOutputStream imgOutput = new ByteArrayOutputStream();
ImageIO.write(bufferedImage,"jpg",imgOutput);
imgOutput.flush();
ByteArrayInputStream inputStream = new ByteArrayInputStream(imgOutput.toByteArray());
MultipartFile multipartFile = new 
//这里需要引入一个测试的依赖去模拟一个MultipartFile
MockMultipartFile(fileName,fileName,"jpg",inputStream);
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值