效率提升-ICON制作

427 阅读3分钟

背景

  我们在日常开发的过程中,经常会使用到各类图标,前端开发,如果用的是矢量图那基本上没什么问题。但是在实际开发的过程中,很多地方需要用到ico图片,比如网站图标(favicon.ico);比如Electron打包成exe(mac下需要icns,也是icop组合的)的时候,需要使用ico图片,并且需要不同尺寸的图片。

  现在网上也有很多在线网站,提供png格式转ico格式的功能,但是局限性也比较大,有的转完以后有水印,有的转完以后失真,有的不同格式需要一次次转换,不能批量转换等等。虽然通过不同网站一通操作,勉强可以达到要求,但是实在太繁琐了。

需求

  1. 支持png格式转换成ico
  2. 一键生成各种尺寸的图片(包括ico和png的,尺寸从16*161024*1024)
  3. 图片放大缩小过程中不能太失真。

技术介绍

每次上传一张图片,会自动生成 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的工具各种图片,就是通过这个功能自己去生成,基本就是一键式的。