Android 圖片壓縮之Luban

前言

目前作App開發總繞不開圖片這個元素。可是隨着手機拍照分辨率的提高,圖片的壓縮成爲一個很重要的問題。單純對圖片進行裁切,壓縮已經有不少文章介紹。可是裁切成多少,壓縮成多少卻很難控制好,裁切過頭圖片過小,質量壓縮過頭則顯示效果太差。java

因而天然想到App巨頭「微信」會是怎麼處理,Luban(魯班)就是經過在微信朋友圈發送近100張不一樣分辨率圖片,對比原圖與微信壓縮後的圖片逆向推算出來的壓縮算法。android

由於有其餘語言也想要實現Luban,因此描述了一遍算法步驟git

由於是逆向推算,效果還無法跟微信如出一轍,可是已經很接近微信朋友圈壓縮後的效果,具體看如下對比!github

效果與對比

對外方法

調用方式

異步調用

Luban內部採用IO線程進行圖片壓縮,外部調用只需設置好結果監聽便可:算法

Luban.with(this)
        .load(photos)
        .ignoreBy(100)
        .setTargetDir(getPath())
        .filter(new CompressionPredicate() {
          @Override
          public boolean apply(String path) {
            return !(TextUtils.isEmpty(path) || path.toLowerCase().endsWith(".gif"));
          }
        })
        .setCompressListener(new OnCompressListener() {
          @Override
          public void onStart() {
            // TODO 壓縮開始前調用,能夠在方法內啓動 loading UI
          }

          @Override
          public void onSuccess(File file) {
            // TODO 壓縮成功後調用,返回壓縮後的圖片文件
          }

          @Override
          public void onError(Throwable e) {
            // TODO 當壓縮過程出現問題時調用
          }
        }).launch();
複製代碼

同步調用

同步方法請儘可能避免在主線程調用以避免阻塞主線程,下面以rxJava調用爲例緩存

Flowable.just(photos)
    .observeOn(Schedulers.io())
    .map(new Function<List<String>, List<File>>() {
      @Override public List<File> apply(@NonNull List<String> list) throws Exception {
        // 同步方法直接返回壓縮後的文件
        return Luban.with(MainActivity.this).load(list).get();
      }
    })
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe();
複製代碼

源碼剖析

目錄結構

Checker.java

/**
   * 用來判斷是不是jpg圖片
   *
   */
  private boolean isJPG(byte[] data) {
    if (data == null || data.length < 3) {
      return false;
    }
    byte[] signatureB = new byte[]{data[0], data[1], data[2]};
    return Arrays.equals(JPEG_SIGNATURE, signatureB);
  }
  
  /**
   * 圖片是否須要壓縮
   *
   */
  boolean needCompress(int leastCompressSize, String path) {
    if (leastCompressSize > 0) {
      File source = new File(path);
      return source.exists() && source.length() > (leastCompressSize << 10);
    }
    return true;
  }
  
  /**
   * android camera源碼 用來獲取圖片的角度
   *
   */
  private int getOrientation(byte[] jpeg) {
    if (jpeg == null) {
      return 0;
    }

    int offset = 0;
    int length = 0;

    // ISO/IEC 10918-1:1993(E)
    while (offset + 3 < jpeg.length && (jpeg[offset++] & 0xFF) == 0xFF) {
      int marker = jpeg[offset] & 0xFF;

      // Check if the marker is a padding.
      if (marker == 0xFF) {
        continue;
      }
      offset++;

      // Check if the marker is SOI or TEM.
      if (marker == 0xD8 || marker == 0x01) {
        continue;
      }
      // Check if the marker is EOI or SOS.
      if (marker == 0xD9 || marker == 0xDA) {
        break;
      }

      // Get the length and check if it is reasonable.
      length = pack(jpeg, offset, 2, false);
      if (length < 2 || offset + length > jpeg.length) {
        Log.e(TAG, "Invalid length");
        return 0;
      }

      // Break if the marker is EXIF in APP1.
      if (marker == 0xE1 && length >= 8
          && pack(jpeg, offset + 2, 4, false) == 0x45786966
          && pack(jpeg, offset + 6, 2, false) == 0) {
        offset += 8;
        length -= 8;
        break;
      }

      // Skip other markers.
      offset += length;
      length = 0;
    }

    // JEITA CP-3451 Exif Version 2.2
    if (length > 8) {
      // Identify the byte order.
      int tag = pack(jpeg, offset, 4, false);
      if (tag != 0x49492A00 && tag != 0x4D4D002A) {
        Log.e(TAG, "Invalid byte order");
        return 0;
      }
      boolean littleEndian = (tag == 0x49492A00);

      // Get the offset and check if it is reasonable.
      int count = pack(jpeg, offset + 4, 4, littleEndian) + 2;
      if (count < 10 || count > length) {
        Log.e(TAG, "Invalid offset");
        return 0;
      }
      offset += count;
      length -= count;

      // Get the count and go through all the elements.
      count = pack(jpeg, offset - 2, 2, littleEndian);
      while (count-- > 0 && length >= 12) {
        // Get the tag and check if it is orientation.
        tag = pack(jpeg, offset, 2, littleEndian);
        if (tag == 0x0112) {
          int orientation = pack(jpeg, offset + 8, 2, littleEndian);
          switch (orientation) {
            case 1:
              return 0;
            case 3:
              return 180;
            case 6:
              return 90;
            case 8:
              return 270;
          }
          Log.e(TAG, "Unsupported orientation");
          return 0;
        }
        offset += 12;
        length -= 12;
      }
    }
    return 0;
  }
複製代碼

CompressionPredicate.java

斷言是否須要壓縮接口bash

public interface CompressionPredicate {

    /**
     * 斷言的路徑是否要壓縮,並返回boolean值
     * @param path input path
     * @return the boolean result
     */
    boolean apply(String path);
}
複製代碼

Engine.java

用於操做,開始壓縮,管理活動,緩存資源微信

/**
  * 構造函數
  *
  */
 Engine(InputStreamProvider srcImg, File tagImg, boolean focusAlpha) throws IOException {
    this.tagImg = tagImg;
    this.srcImg = srcImg;
    this.focusAlpha = focusAlpha;

    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    options.inSampleSize = 1;

    BitmapFactory.decodeStream(srcImg.open(), null, options);
    this.srcWidth = options.outWidth;
    this.srcHeight = options.outHeight;
 }
 
 /**
  * 計算壓縮比例
  *
  */
 private int computeSize() {
    srcWidth = srcWidth % 2 == 1 ? srcWidth + 1 : srcWidth;
    srcHeight = srcHeight % 2 == 1 ? srcHeight + 1 : srcHeight;

    int longSide = Math.max(srcWidth, srcHeight);
    int shortSide = Math.min(srcWidth, srcHeight);

    float scale = ((float) shortSide / longSide);
    if (scale <= 1 && scale > 0.5625) {
      if (longSide < 1664) {
        return 1;
      } else if (longSide < 4990) {
        return 2;
      } else if (longSide > 4990 && longSide < 10240) {
        return 4;
      } else {
        return longSide / 1280 == 0 ? 1 : longSide / 1280;
      }
    } else if (scale <= 0.5625 && scale > 0.5) {
      return longSide / 1280 == 0 ? 1 : longSide / 1280;
    } else {
      return (int) Math.ceil(longSide / (1280.0 / scale));
    }
 }
 
 /**
  * 壓縮
  *
  */
 File compress() throws IOException {
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inSampleSize = computeSize();

    Bitmap tagBitmap = BitmapFactory.decodeStream(srcImg.open(), null, options);
    ByteArrayOutputStream stream = new ByteArrayOutputStream();

    if (Checker.SINGLE.isJPG(srcImg.open())) {
      tagBitmap = rotatingImage(tagBitmap, Checker.SINGLE.getOrientation(srcImg.open()));
    }
    tagBitmap.compress(focusAlpha ? Bitmap.CompressFormat.PNG : Bitmap.CompressFormat.JPEG, 60, stream);
    tagBitmap.recycle();

    FileOutputStream fos = new FileOutputStream(tagImg);
    fos.write(stream.toByteArray());
    fos.flush();
    fos.close();
    stream.close();

    return tagImg;
 }
複製代碼

InputStreamProvider.java

獲取輸入流兼容文件、FileProvider方式獲取圖片接口app

public interface InputStreamProvider {
  InputStream open() throws IOException;
  void close();
  String getPath();
}
複製代碼

InputStreamAdapter.java

InputStreamProvider類的實現異步

@Override
  public InputStream open() throws IOException {
    close();
    inputStream = openInternal();
    return inputStream;
  }

  public abstract InputStream openInternal() throws IOException;

  @Override
  public void close() {
    if (inputStream != null) {
      try {
        inputStream.close();
      } catch (IOException ignore) {
      }finally {
        inputStream = null;
      }
    }
  }
複製代碼

OnCompressListener.java

public interface OnCompressListener {

  /**
   * 開始壓縮
   */
  void onStart();

  /**
   * 壓縮成功
   */
  void onSuccess(File file);

  /**
   * 壓縮異常
   */
  void onError(Throwable e);
}
複製代碼

OnRenameListener.java

public interface OnRenameListener {

  /**
   * 壓縮前調用該方法用於修改壓縮後文件名
   *
   */
  String rename(String filePath);
}
複製代碼

Luban.java

加載圖片的幾種方式

/**
 * 加載圖片
 *
 */
public Builder load(InputStreamProvider inputStreamProvider) {
      mStreamProviders.add(inputStreamProvider);
      return this; 
}

/**
 * 加載圖片
 *
 */
public Builder load(final File file) {
      mStreamProviders.add(new InputStreamAdapter() {
        @Override
        public InputStream openInternal() throws IOException {
          return new FileInputStream(file);
        }

        @Override
        public String getPath() {
          return file.getAbsolutePath();
        }
      });
      return this;
}

/**
 * 加載圖片
 *
 */
public Builder load(final String string) {
      mStreamProviders.add(new InputStreamAdapter() {
        @Override
        public InputStream openInternal() throws IOException {
          return new FileInputStream(string);
        }

        @Override
        public String getPath() {
          return string;
        }
      });
      return this;
}

/**
 * 加載圖片
 *
 */
public Builder load(final Uri uri) {
      mStreamProviders.add(new InputStreamAdapter() {
        @Override
        public InputStream openInternal() throws IOException {
          return context.getContentResolver().openInputStream(uri);
        }

        @Override
        public String getPath() {
          return uri.getPath();
        }
      });
      return this;
}

/**
 * 加載圖片列表
 *
 */
public <T> Builder load(List<T> list) {
      for (T src : list) {
        if (src instanceof String) {
          load((String) src);
        } else if (src instanceof File) {
          load((File) src);
        } else if (src instanceof Uri) {
          load((Uri) src);
        } else {
          throw new IllegalArgumentException("Incoming data type exception, it must be String, File, Uri or Bitmap");
        }
      }
      return this;
}
複製代碼

壓縮可設置的先行條件

/**
 * 壓縮的最小單位值,單位kB,默認100kb
 *
 */
public Builder ignoreBy(int size) {
      this.mLeastCompressSize = size;
      return this;
}

/**
 * 壓縮斷言
 *
 */  
public Builder filter(CompressionPredicate compressionPredicate) {
      this.mCompressionPredicate = compressionPredicate;
      return this;
}
複製代碼

壓縮其餘配置

/**
 * 壓縮後目錄
 *
 */  
public Builder setTargetDir(String targetDir) {
      this.mTargetDir = targetDir;
      return this;
}

/**
 * 是否開啓透明通道,true爲png格式壓縮,false爲jpg格式壓縮
 *
 */  
public Builder setFocusAlpha(boolean focusAlpha) {
      this.focusAlpha = focusAlpha;
      return this;      
}
複製代碼

壓縮啓動

/**
 * 開啓壓縮
 *
 */  
private void launch(final Context context) {
    if (mStreamProviders == null || mStreamProviders.size() == 0 && mCompressListener != null) {
      mCompressListener.onError(new NullPointerException("image file cannot be null"));
    }

    Iterator<InputStreamProvider> iterator = mStreamProviders.iterator();

    while (iterator.hasNext()) {
      final InputStreamProvider path = iterator.next();

      AsyncTask.SERIAL_EXECUTOR.execute(new Runnable() {
        @Override
        public void run() {
          try {
            mHandler.sendMessage(mHandler.obtainMessage(MSG_COMPRESS_START));

            File result = compress(context, path);

            mHandler.sendMessage(mHandler.obtainMessage(MSG_COMPRESS_SUCCESS, result));
          } catch (IOException e) {
            mHandler.sendMessage(mHandler.obtainMessage(MSG_COMPRESS_ERROR, e));
          }
        }
      });

      iterator.remove();
    }
}
複製代碼
相關文章
相關標籤/搜索