问题描述
使用Java代码下载图片,并对图片进行裁减后,图片出现偏色的现象。
图片展示
原图:
裁剪后得到的图片
问题代码
这里隐去公司信息,自己写了个demo,问题代码如下:
@Slf4j
public class FileUtil {
public static void main(String[] args) {
// url
String[] urls = new String[]{
"http://test:18181/g1/M01/00/01/rBQZSl5YrLKAE0xcABCU9yL_rvI049.jpg",
"http://test:18181/g1/M01/00/01/rBQZSl5YvVWAD80KAAyrNKu2dbM553.jpg",
"http://test:18181/g1/M00/00/01/rBQZSl5Xnf-AMoS6AADcVze2Feg316.jpg",
"http://test:18181/g1/M01/00/00/rBQZSl5VDeiAWTzTAAYDKkQlXwM345.jpg"
};
Arrays.stream(urls)
.forEach(url -> {
// 获取图片文件名
String picName = url.substring(url.lastIndexOf("/") + 1);
// 下载图片
byte[] imageData = downloadImage(url);
// 这里对图片进行裁减,并转码获得其base64串
InputStream inputStream = new ByteArrayInputStream(imageData);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] transformImageData = null;
try {
BufferedImage bufferedImage = ImageIO.read(inputStream);
int width = bufferedImage.getWidth();
int height = bufferedImage.getHeight();
int leftX = width / 2;
int leftY = height / 2;
// 进行图片裁减,这里裁减的坐标不是实际裁剪坐标,是随便写的
BufferedImage cutImage = bufferedImage.getSubimage(
leftX,
leftY,
width,
height
);
// 将图片转为ByteArrayOutputStream ,图片格式全部转为jpg
ImageIO.write(cutImage, "jpg", outputStream);
transformImageData = outputStream.toByteArray();
String base64 = Base64.getEncoder().encodeToString(transformImageData);
log.info("base64={}", base64);
} catch (IOException e) {
System.out.println(e.getMessage());
}
});
}
}
问题分析
在网上找了很多文章,发现之前也有网友遇到类似的情况,但文章没有给出具体方案。后来就只能按代码功能一行一行地分析,最后发现对图片进行裁剪的代码都没有问题,问题就出现在ImageIO.write()
这个方法的调用上,下图中的红色框部分。
后来测试发现
- 如果原图格式为jpeg,gif,bmp,强转成jpg,再将图片base64串转换成图片,图片不会出现偏色的现象
- 如果原图格式为png,这里强转成jpg后,再将图片base64串转换成图片,则图片会出现偏色的现象
测试的时候借助了网上的base64图片在线转换工具,感觉很好用,感谢。
解决问题
为了解决上面的问题,就需要根据图片内容来获取图片的格式。
最初的时候考虑根据图片名来获取图片的格式,后来发现这样只可以解决部分图片的问题,假如图片文件名后缀与图片实际的格式不相同,则还是会出现色差。
最后,只能考虑通过图片本身的字节码或图片流来获取图片格式:
-
通过图片流获取图片格式
这种方法在试验的过程中,发现返回的图片格式与图片名的后缀相同,就舍弃了。不知道是巧合,还是图片流的图片类型获取方法就是读取的文件名后缀,当时没有深究,所以不太确定,需要去看源码。
-
通过图片字节码获取
实验发现可行,参考文章Java 读取文件 文件头字节码信息,判断文件类型。
实现代码
- 测试的主类
package test;
import com.google.common.io.Files;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.util.Arrays;
import static test.HttpUtil.downloadImage;
/**
* @author zhang
* @date 2020/2/28 17:17
*/
@Slf4j
public class FileUtil {
public static void main(String[] args) {
// url
String[] urls = new String[]{
"http://test:18181/g1/M01/00/01/rBQZSl5YrLKAE0xcABCU9yL_rvI049.jpg",
"http://test:18181/g1/M01/00/01/rBQZSl5YvVWAD80KAAyrNKu2dbM553.jpg",
"http://test:18181/g1/M00/00/01/rBQZSl5Xnf-AMoS6AADcVze2Feg316.jpg",
"http://test:18181/g1/M01/00/00/rBQZSl5VDeiAWTzTAAYDKkQlXwM345.jpg"
};
Arrays.stream(urls)
.forEach(url -> {
String picName = url.substring(url.lastIndexOf("/") + 1);
byte[] imageData = downloadImage(url);
String picFormat = getFormatName(picName, imageData);
System.out.println("fileName: " + picName + ", fileFormat: " + picFormat);
});
}
/**
* 根据图片名来获取图片格式
* @param imageName 图片名
* @return 图片格式
*/
private static String getFormatName(String imageName) {
String extension = Files.getFileExtension(imageName);
if (StringUtils.isBlank(extension)) {
return FileFormatEnum.JPG.getValue();
}
for (FileFormatEnum fileFormatEnum : FileFormatEnum.values()) {
if (StringUtils.equalsIgnoreCase(fileFormatEnum.getValue(), extension)) {
return fileFormatEnum.getValue();
}
}
return FileFormatEnum.UNKNOWN.getValue();
}
/**
* 获取图片格式
* @param imageName 图片名
* @param imageBytes 图片字节码
* @return 图片格式
*/
private static String getFormatName(String imageName, byte[] imageBytes) {
// 根据图片字节码读取图片类型
FileFormatEnum formatName = getImageFormat(imageBytes);
if (formatName != FileFormatEnum.UNKNOWN) {
return formatName.getValue();
}
// 如果通过字节码没有取到图片类型,则根据文件名来判断
return getFormatName(imageName);
}
/**
* 读取图片格式
* @param imageBytes 图片字节码数组
* @return 图片格式
*/
private static FileFormatEnum getImageFormat(byte[] imageBytes) {
// 读取文件的前几个字节来判断图片格式
byte[] b = Arrays.copyOf(imageBytes, 4);
String type = bytesToHexString(b).toUpperCase();
for (FileFormatEnum FileFormatEnum : FileFormatEnum.values()) {
if (type.contains(FileFormatEnum.getCode())) {
return FileFormatEnum;
}
}
return FileFormatEnum.UNKNOWN;
}
/**
* 将要读取文件头信息的文件的byte数组转换成string类型表示
* @param src 要读取文件头信息的文件的byte数组
* @return 图片类型字符串
*/
private static String bytesToHexString(byte[] src){
StringBuilder stringBuilder = new StringBuilder();
if (src == null || src.length <= 0) {
return null;
}
for (int i = 0; i < src.length; i++) {
// 以十六进制(基数 16)无符号整数形式返回一个整数参数的字符串表示形式
int v = src[i] & 0xFF;
String hv = Integer.toHexString(v);
if (hv.length() < 2) {
stringBuilder.append(0);
}
stringBuilder.append(hv);
}
return stringBuilder.toString();
}
}
输出为
从图可以看出,不管下载的图片文件名后缀是什么,通过字节码都能读到其实际的格式。
再将修改应用到最初存在问题的代码中,裁剪后的图片就没有偏色的现象了。
修改后的部分代码如图所示:
- 文件下载工具类
package test;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* @author zhang
* @date 2020/2/28 17:11
*/
@Slf4j
public class HttpUtil {
public static byte[] downloadImage(String url) {
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet(url);
byte[] dataBuffer = null;
CloseableHttpResponse response = null;
try {
response = httpclient.execute(httpGet);
int status = response.getStatusLine().getStatusCode();
if (status == 200) {
HttpEntity entity = response.getEntity();
InputStream input = entity.getContent();
ByteArrayOutputStream output = new ByteArrayOutputStream();
IOUtils.copy(input, output);
output.flush();
input.close();
dataBuffer = output.toByteArray();
output.close();
EntityUtils.consume(entity);
}
} catch (Exception var17) {
log.warn("download image failed|url:{}", url, var17);
} finally {
if (response != null) {
try {
response.close();
} catch (IOException var16) {
log.warn("close io exception failed", var16);
}
}
}
return dataBuffer;
}
}
- 文件名枚举类
图片只使用了前几个,但这里为了以后方便,所以把知道的文件格式都列出来了。
package test;
/**
* @author zhang
* @date 2020/2/28 16:43
*/
public enum FileFormatEnum {
/**
* 文件格式枚举
*/
JPG("FFD8FF", "jpg"),
// JPEG("FFD8FFE0", "jpeg"),
PNG("89504E47", "png"),
GIF("47494638", "gif"),
BMP("424D", "bmp"),
TIF("49492A00", "tif"),
DWG("41433130", "dwg"),
PSD("38425053", "psd"),
RTF("7B5C727466", "rtf"),
XML("3C3F786D6C", "xml"),
HTML("68746D6C3E", "html"),
EML("44656C69766572792D646174653A", "eml"),
/**
* word or xls
*/
DOC("D0CF11E0", "doc"),
MDB("5374616E64617264204A", "mdb"),
PS("252150532D41646F6265", "ps"),
PDF("255044462D312E", "pdf"),
/**
* word or xlsx
*/
DOCX("504B0304", "docx"),
RAR("52617221", "rar"),
WAV("57415645", "wav"),
AVI("41564920", "avi"),
RM("2E524D46", "rm"),
MPG("000001B", "mpg"),
MOV("6D6F6F76", "mov"),
ASF("3026B2758E66CF11", "asf"),
MID("4D546864", "mid"),
GZ("1F8B08", "gz"),
EXE_DLL("4D5A9000", "exe/dll"),
TXT("75736167", "txt"),
UNKNOWN("000000", "unknown")
;
private String code;
private String value;
FileFormatEnum(String code ,String value){
this.code = code;
this.value = value;
}
public String getValue() {
return this.value;
}
public String getCode() {
return this.code;
}
}