背景
我们在日常开发的过程中,经常会使用到各类图标,前端开发,如果用的是矢量图那基本上没什么问题。但是在实际开发的过程中,很多地方需要用到ico图片,比如网站图标(favicon.ico);比如Electron打包成exe(mac下需要icns,也是icop组合的)的时候,需要使用ico图片,并且需要不同尺寸的图片。
现在网上也有很多在线网站,提供png格式转ico格式的功能,但是局限性也比较大,有的转完以后有水印,有的转完以后失真,有的不同格式需要一次次转换,不能批量转换等等。虽然通过不同网站一通操作,勉强可以达到要求,但是实在太繁琐了。
需求
- 支持png格式转换成ico
- 一键生成各种尺寸的图片(包括ico和png的,尺寸从
16*16
到1024*1024
) - 图片放大缩小过程中不能太失真。
技术介绍
每次上传一张图片,会自动生成 7中尺寸x2中格式,也就是14张图片。
后端
图片格式转换和尺寸转换,都是通过后台Java去处理的。
/**
图片转换,如果已经转换过的,就不需要在重新转换了。
图片最终也是通过oss上传的
*/
public List<String> createImage(MultipartFile[] multipartFiles, RsaRequestEntity rsaRequestEntity) {
List<ImageChangeEntity> imageChangeEntityList = new ArrayList<>();
List<String> lstError = new ArrayList<>();
for (MultipartFile file : multipartFiles) {
try {
String name = file.getOriginalFilename();
logger.info("开始转换文件:" + name);
String md5 = FileUtils.getMd5(file);
if (imageChangeDao.getMd5Record(rsaRequestEntity.getUser(), md5) > 0) {
lstError.add(name + "文件已上传过,无需重复上传!");
continue;
}
//图片转换成ICON
InputStream inputStreamIco = convertImageToIco(file.getInputStream());
//这里InputStream转换成ByteArrayInputStream,可以重复读取
InputStream inputStreamPng = convertImageToPng(file.getInputStream());
for (int k : SIZE_LIST) {
//ico超过256显示就有问题了
if (k <= 256) {
uploadOss(k,k, name, rsaRequestEntity.getUser(), inputStreamIco, imageChangeEntityList, "ico",md5);
}
}
for (int k : SIZE_LIST) {
uploadOss(k,k, name, rsaRequestEntity.getUser(), inputStreamPng, imageChangeEntityList, "png",md5);
}
} catch (Exception er) {
er.printStackTrace();
}
}
if(imageChangeEntityList.size()>0){
imageChangeDao.insertImageBatch(imageChangeEntityList);
}
return lstError;
}
/**
* 文件上传到OSS,并记录到数据库
* @param width 图片宽度
* @param height 图片高度
* @param name 图片名称
* @param user 当前客户端用户
* @param inputStream 文件流
* @param imageChangeEntityList 文件对象集合
* @param type 文件类型
* @param md5 文件md5值
*/
private void uploadOss(int width,int height,String name,String user,
InputStream inputStream,List<ImageChangeEntity> imageChangeEntityList, String type,String md5) throws IOException {
InputStream oss ;
if(ICO_IMAGE_TYPE.equals(type)){
oss = resizeImageIco(width,height, inputStream);
}
else{
oss = resizeImage(width,height,inputStream);
}
assert name != null;
String tempName = name.substring(0, name.lastIndexOf(".")) + "("+width +"x"+height+")"+"." + type;
String url = OssUtils.upload("assassin-tool-"+ type , tempName,oss);
//上传记录存储,便于下次直接打开
inputStream.reset();
}
/**
图片转换成ico,以及尺寸管理
*/
public static InputStream resizeImageIco(int width, int height, InputStream inputStream) throws IOException {
List<BufferedImage> bufferedImageList = IcoDecoder.read(inputStream);
BufferedImage bufferedImage = bufferedImageList.get(0);
BufferedImage to = resizeImageBase(bufferedImage,width,height);
List<BufferedImage> images = new ArrayList<>(1);
images.add(to);
ByteArrayOutputStream byteArrayInputStream = new ByteArrayOutputStream();
IcoEncoder.write(images, byteArrayInputStream);
return new ByteArrayInputStream(byteArrayInputStream.toByteArray());
}
/**
图片转换成png,以及尺寸管理
*/
public static InputStream resizeImage(int width, int height, InputStream inputStream) throws IOException {
BufferedImage bufferedImage = ImageIO.read(inputStream);
BufferedImage to = resizeImageBase(bufferedImage,width,height);
ByteArrayOutputStream byteArrayInputStream = new ByteArrayOutputStream();
ImageIO.write(to, "png", byteArrayInputStream);
return new ByteArrayInputStream(byteArrayInputStream.toByteArray());
}
//IcoDecoder.read
public static List<BufferedImage> read(InputStream is)
throws IOException {
List<IcoImage> list = readExt(is);
List<BufferedImage> ret = new ArrayList<>(
list.size());
for (IcoImage icoImage : list) {
BufferedImage image = icoImage.getImage();
ret.add(image);
}
return ret;
}
public static List<IcoImage> readExt(InputStream is)
throws IOException {
LittleEndianInputStream in = new LittleEndianInputStream(
new CountingInputStream(is));
// Reserved 2 byte =0
short sReserved = in.readShortLe();
// Type 2 byte =1
short sType = in.readShortLe();
// Count 2 byte Number of Icons in this file
short sCount = in.readShortLe();
// Entries Count * 16 list of icons
IconEntry[] entries = new IconEntry[sCount];
for (short s = 0; s < sCount; s++) {
entries[s] = new IconEntry(in);
}
int i = 0;
// images list of bitmap structures in BMP/PNG format
List<IcoImage> ret = new ArrayList<>(sCount);
try {
for (i = 0; i < sCount; i++) {
// Make sure we're at the right file offset!
int fileOffset = in.getCount();
if (fileOffset != entries[i].iFileOffset) {
throw new IOException("Cannot read image #" + i
+ " starting at unexpected file offset.");
}
int info = in.readIntLe();
if (info == 40) {
InfoHeader infoHeader = BmpDecoder.readInfoHeader(in, info);
InfoHeader andHeader = new InfoHeader(infoHeader);
andHeader.iHeight = infoHeader.iHeight / 2;
InfoHeader xorHeader = new InfoHeader(infoHeader);
xorHeader.iHeight = andHeader.iHeight;
andHeader.sBitCount = 1;
andHeader.iNumColors = 2;
BufferedImage xor = BmpDecoder.read(xorHeader, in);
BufferedImage img = new BufferedImage(xorHeader.iWidth,
xorHeader.iHeight, BufferedImage.TYPE_INT_ARGB);
ColorEntry[] andColorTable = new ColorEntry[] {
new ColorEntry(255, 255, 255, 255),
new ColorEntry(0, 0, 0, 0) };
if (infoHeader.sBitCount == 32) {
int size = entries[i].iSizeInBytes;
int infoHeaderSize = infoHeader.iSize;
// data size = w * h * 4
int dataSize = xorHeader.iWidth * xorHeader.iHeight * 4;
int skip = size - infoHeaderSize - dataSize;
int skip2 = entries[i].iFileOffset + size
- in.getCount();
if (in.skip(skip, false) < skip && i < sCount - 1) {
throw new EOFException("Unexpected end of input");
}
WritableRaster srgb = xor.getRaster();
WritableRaster salpha = xor.getAlphaRaster();
WritableRaster rgb = img.getRaster();
WritableRaster alpha = img.getAlphaRaster();
for (int y = xorHeader.iHeight - 1; y >= 0; y--) {
for (int x = 0; x < xorHeader.iWidth; x++) {
int r = srgb.getSample(x, y, 0);
int g = srgb.getSample(x, y, 1);
int b = srgb.getSample(x, y, 2);
int a = salpha.getSample(x, y, 0);
rgb.setSample(x, y, 0, r);
rgb.setSample(x, y, 1, g);
rgb.setSample(x, y, 2, b);
alpha.setSample(x, y, 0, a);
}
}
} else {
BufferedImage and = BmpDecoder.read(andHeader, in,
andColorTable);
// copy rgb
WritableRaster srgb = xor.getRaster();
WritableRaster rgb = img.getRaster();
// copy alpha
WritableRaster alpha = img.getAlphaRaster();
WritableRaster salpha = and.getRaster();
for (int y = 0; y < xorHeader.iHeight; y++) {
for (int x = 0; x < xorHeader.iWidth; x++) {
int r;
int g;
int b;
int c = xor.getRGB(x, y);
r = (c >> 16) & 0xFF;
g = (c >> 8) & 0xFF;
b = (c) & 0xFF;
// red
rgb.setSample(x, y, 0, r);
// green
rgb.setSample(x, y, 1, g);
// blue
rgb.setSample(x, y, 2, b);
int a = and.getRGB(x, y);
alpha.setSample(x, y, 0, a);
}
}
}
// create ICOImage
IconEntry iconEntry = entries[i];
IcoImage icoImage = new IcoImage(img, infoHeader, iconEntry);
icoImage.setPngCompressed(false);
icoImage.setIconIndex(i);
ret.add(icoImage);
}
else if (info == PNG_MAGIC_LE) {
int info2 = in.readIntLe();
if (info2 != PNG_MAGIC2_LE) {
throw new IOException(
"Unrecognized icon format for image #" + i);
}
IconEntry e = entries[i];
int size = e.iSizeInBytes - 8;
byte[] pngData = new byte[size];
in.readFully(pngData);
ByteArrayOutputStream bout = new ByteArrayOutputStream();
DataOutputStream dataOutputStream = new DataOutputStream(
bout);
dataOutputStream.writeInt(PNG_MAGIC);
dataOutputStream.writeInt(PNG_MAGIC2);
dataOutputStream.write(pngData);
byte[] pngData2 = bout.toByteArray();
ByteArrayInputStream bin = new ByteArrayInputStream(
pngData2);
javax.imageio.stream.ImageInputStream input = ImageIO
.createImageInputStream(bin);
javax.imageio.ImageReader reader = getPngImageReader();
reader.setInput(input);
BufferedImage img = reader.read(0);
// create ICOImage
IconEntry iconEntry = entries[i];
IcoImage icoImage = new IcoImage(img, null, iconEntry);
icoImage.setPngCompressed(true);
icoImage.setIconIndex(i);
ret.add(icoImage);
} else {
throw new IOException(
"Unrecognized icon format for image #" + i);
}
}
} catch (IOException ex) {
throw new IOException("Failed to read image # " + i, ex);
}
return ret;
}
private static javax.imageio.ImageReader getPngImageReader() {
javax.imageio.ImageReader ret = null;
Iterator<javax.imageio.ImageReader> itr = ImageIO
.getImageReadersByFormatName("png");
if (itr.hasNext()) {
ret = itr.next();
}
return ret;
}
public static void write(List<BufferedImage> images, int[] bpp, boolean[] compress, OutputStream os) throws IOException {
LittleEndianOutputStream out = new LittleEndianOutputStream(os);
int count = images.size();
//file header 6
writeFileHeader(count, IcoConstants.TYPE_ICON, out);
//file offset where images start
int fileOffset = 6 + count * 16;
List<InfoHeader> infoHeaders = new java.util.ArrayList<>(count);
List<BufferedImage> converted = new java.util.ArrayList<>(count);
List<byte[]> compressedImages = null;
if (compress != null) {
compressedImages = new java.util.ArrayList<>(count);
}
ImageWriter pngWriter = null;
//icon entries 16 * count
for (int i = 0; i < count; i++) {
BufferedImage img = images.get(i);
int b = bpp == null ? -1 : bpp[i];
//convert image
BufferedImage imgc = b == -1 ? img : convert(img, b);
converted.add(imgc);
//create info header
InfoHeader ih = BmpEncoder.createInfoHeader(imgc);
//create icon entry
IconEntry e = createIconEntry(ih);
if (compress != null) {
if (compress[i]) {
if (pngWriter == null) {
pngWriter = getPngImageWriter();
}
byte[] compressedImage = encodePng(pngWriter, imgc);
compressedImages.add(compressedImage);
e.iSizeInBytes = compressedImage.length;
} else {
compressedImages.add(null);
}
}
ih.iHeight *= 2;
e.iFileOffset = fileOffset;
fileOffset += e.iSizeInBytes;
e.write(out);
infoHeaders.add(ih);
}
//images
for (int i = 0; i < count; i++) {
BufferedImage img = images.get(i);
BufferedImage imgc = converted.get(i);
if (compress == null || !compress[i]) {
//info header
InfoHeader ih = infoHeaders.get(i);
ih.write(out);
//color map
if (ih.sBitCount <= 8) {
IndexColorModel icm = (IndexColorModel) imgc.getColorModel();
BmpEncoder.writeColorMap(icm, out);
}
//xor bitmap
writeXorBitmap(imgc, ih, out);
//and bitmap
writeAndBitmap(img, out);
}
else {
byte[] compressedImage = compressedImages.get(i);
out.write(compressedImage);
}
}
}
前端
<template>
<div>
<a-spin :spinning="spinning">
<div>
<a-button type="primary" @click="$refs.ImageTypeChangeAdd.show()">上传</a-button>
<span style="margin-left: 20px;font-size: 14px;color: red">注:512 * 512规格开始,不提供ICO模式</span>
</div>
<a-tabs tab-position="top" default-active-key="1" @change="tabChange" style="margin-right: 20px">
<!--suppress JSUnusedLocalSymbols -->
<a-tab-pane v-for="item in tabList" :key="item" :tab="getTabTitle(item)">
<a-tabs tab-position="bottom" :default-active-key="currentKey>5?'png':'ico'" v-if="imageList.length>0">
<a-tab-pane key="ico" tab="ico" v-if="imageList.filter(p=>p.type==='ico').length>0">
<ImageTypeChangeShow :ref="'ImageTypeChangeShow'+item" :imageList="imageList.filter(p=>p.type==='ico')" :size="getSize(item)" @deleteImageList="deleteImageList"></ImageTypeChangeShow>
</a-tab-pane>
<a-tab-pane key="png" tab="png" v-if="imageList.filter(p=>p.type==='png').length>0">
<ImageTypeChangeShow :ref="'ImageTypeChangeShow'+item" :imageList="imageList.filter(p=>p.type==='png')" :size="getSize(item)" @deleteImageList="deleteImageList"></ImageTypeChangeShow>
</a-tab-pane>
</a-tabs>
</a-tab-pane>
</a-tabs>
<ImageTypeChangeAdd ref="ImageTypeChangeAdd" @uploadImageList="uploadImageList"></ImageTypeChangeAdd>
</a-spin>
</div>
</template>
<script>
import {getImageList} from "../../../api/other";
import ImageTypeChangeAdd from "./modules/ImageTypeChangeAdd";
import ImageTypeChangeShow from "./modules/ImageTypeChangeShow";
export default {
name: "upload",
data() {
return {
fileList: [],
spinning: false,
urlType: "markdown",
imageList: [],
currentKey: 1,
tabList: [1,2,3,4,5,6,7]
}
},components:{
ImageTypeChangeAdd,ImageTypeChangeShow
}, mounted() {
this.getImageList();
}, methods: {
deleteImageList(id){
const index = this.imageList.findIndex(p=>p.id===id);
this.imageList.splice(index,1);
},
tabChange(activeKey){
this.currentKey = activeKey;
this.getImageList();
},
getTabTitle(key){
let size = Math.pow(2, parseInt(key) + 3);
return `${size}x${size}`
},
getSize(key){
return Math.pow(2, parseInt(key) + 3);
},
getImageList(){
this.imageList = [];
let size = Math.pow(2, parseInt(this.currentKey) + 3);
getImageList({
user: this.$configInfo.user,
size: size
}).then(data=>{
this.imageList = data;
}).catch(()=>{
})
},
uploadImageList(){
this.getImageList()
}
}
}
</script>
<style scoped>
</style>
总结
图片转换使用场景,相对不是很多,但是确实很方便,就像我本身这个Electron的工具各种图片,就是通过这个功能自己去生成,基本就是一键式的。