日期:
来源:code小生收集编辑:Mr.Louis
以下内容来自公众号code小生,关注每日干货及时送达
作者:Mr.Louis
https://blog.csdn.net/weixin_44005563
前言
图片基础知识
该图片在内存中所占大小为:100 * 100 * (32 / 8) Byte 在文件中所占大小为 100 * 100 * ( 24/ 8 ) * 压缩率 Byte
BitmapFactory.java
public static Bitmap decodeResourceStream(Resources res, TypedValue value,InputStream is, Rect pad, Options opts) {
if (opts == null) {
opts = new Options();
}
if (opts.inDensity == 0 && value != null) {
final int density = value.density;
if (density == TypedValue.DENSITY_DEFAULT) {
//inDensity默认为图片所在文件夹对应的密度
opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
} else if (density != TypedValue.DENSITY_NONE) {
opts.inDensity = density;
}
}
if (opts.inTargetDensity == 0 && res != null) {
//inTargetDensity为当前系统密度。
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
return decodeStream(is, pad, opts);
}
BitmapFactory.cpp 此处只列出主要代码。
static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
//初始缩放系数
float scale = 1.0f;
if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
const int density = env->GetIntField(options, gOptions_densityFieldID);
const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
if (density != 0 && targetDensity != 0 && density != screenDensity) {
//缩放系数是当前系数密度/图片所在文件夹对应的密度;
scale = (float) targetDensity / density;
}
}
//原始解码出来的Bitmap;
SkBitmap decodingBitmap;
if (decoder->decode(stream, &decodingBitmap, prefColorType, decodeMode)
!= SkImageDecoder::kSuccess) {
return nullObjectReturn("decoder->decode returned false");
}
//原始解码出来的Bitmap的宽高;
int scaledWidth = decodingBitmap.width();
int scaledHeight = decodingBitmap.height();
//要使用缩放系数进行缩放,缩放后的宽高;
if (willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) {
scaledWidth = int(scaledWidth * scale + 0.5f);
scaledHeight = int(scaledHeight * scale + 0.5f);
}
//源码解释为因为历史原因;sx、sy基本等于scale。
const float sx = scaledWidth / float(decodingBitmap.width());
const float sy = scaledHeight / float(decodingBitmap.height());
canvas.scale(sx, sy);
canvas.drawARGB(0x00, 0x00, 0x00, 0x00);
canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
// now create the java bitmap
return GraphicsJNI::createBitmap(env, javaAllocator.getStorageObjAndReset(),
bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
}
Android中图片压缩的方法介绍
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
//quality 为0~100,0表示最小体积,100表示最高质量,对应体积也是最大
bitmap.compress(Bitmap.CompressFormat.JPEG, quality , outputStream);
//Bitmap.cpp
static jboolean Bitmap_compress(JNIEnv* env, jobject clazz, jlong bitmapHandle,
jint format, jint quality,
jobject jstream, jbyteArray jstorage) {
LocalScopedBitmap bitmap(bitmapHandle);
SkImageEncoder::Type fm;
switch (format) {
case kJPEG_JavaEncodeFormat:
fm = SkImageEncoder::kJPEG_Type;
break;
case kPNG_JavaEncodeFormat:
fm = SkImageEncoder::kPNG_Type;
break;
case kWEBP_JavaEncodeFormat:
fm = SkImageEncoder::kWEBP_Type;
break;
default:
return JNI_FALSE;
}
if (!bitmap.valid()) {
return JNI_FALSE;
}
bool success = false;
std::unique_ptr<SkWStream> strm(CreateJavaOutputStreamAdaptor(env, jstream, jstorage));
if (!strm.get()) {
return JNI_FALSE;
}
std::unique_ptr<SkImageEncoder> encoder(SkImageEncoder::Create(fm));
if (encoder.get()) {
SkBitmap skbitmap;
bitmap->getSkBitmap(&skbitmap);
success = encoder->encodeStream(strm.get(), skbitmap, quality);
}
return success ? JNI_TRUE : JNI_FALSE;
}
BitmapFactory.Options options = new BitmapFactory.Options();
//或者 inDensity 搭配 inTargetDensity 使用,算法和 inSampleSize 一样
options.inSampleSize = 2; //设置图片的缩放比例(宽和高) , google推荐用2的倍数:
Bitmap bitmap = BitmapFactory.decodeFile("xxx.png");
Bitmap compress = BitmapFactory.decodeFile("xxx.png", options);
Bitmap bitmap = BitmapFactory.decodeFile("xxx.png");
Bitmap compress = Bitmap.createScaledBitmap(bitmap, bitmap.getWidth()/2, bitmap.getHeight()/2, true);
或者直接使用 matrix 进行缩放
Bitmap bitmap = BitmapFactory.decodeFile("xxx.png");
Matrix matrix = new Matrix();
matrix.setScale(0.5f, 0.5f);
bm = Bitmap.createBitmap(bitmap, 0, 0, bit.getWidth(), bit.getHeight(), matrix, true);
鲁班压缩的背景
Luban算法解析
[1, 0.5625) 即图片处于 [1:1 ~ 9:16) 比例范围内 [0.5625, 0.5) 即图片处于 [9:16 ~ 1:2) 比例范围内 [0.5, 0) 即图片处于 [1:2 ~ 1:∞) 比例范围内
[1, 0.5625) 边界值为:1664 * n(n=1), 4990 * n(n=2), 1280 * pow(2, n-1)(n≥3) [0.5625, 0.5) 边界值为:1280 * pow(2, n-1)(n≥1) [0.5, 0) 边界值为:1280 * pow(2, n-1)(n≥1)
width / pow(2, n-1) height/ pow(2, n-1)
[1, 0.5625) 则 width & height 对应 1664,4990,1280 * n(n≥3),m 对应 150,300,300; [0.5625, 0.5) 则 width = 1440,height = 2560, m = 200; [0.5, 0) 则 width = 1280,height = 1280 / scale,m = 500;注:scale为比例值
[1, 0.5625) 则最小 size 对应 60,60,100 [0.5625, 0.5) 则最小 size 都为 100 [0.5, 0) 则最小 size 都为 100
// 计算采样压缩的值,也就是模仿微信的经验值,核心内容
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);
// 获取图片的比例系数,如果在区间[1, 0.5625) 中即图片处于 [1:1 ~ 9:16) 比例
float scale = ((float) shortSide / longSide);
// 开始判断图片处于那种比例中,就是上面所说的第一个步骤
if (scale <= 1 && scale > 0.5625) {
// 判断图片最长边是否过边界值,此边界值是模仿微信的一个经验值,就是上面所说的第二个步骤
if (longSide < 1664) {
// 返回的是 options.inSampleSize的值,就是采样压缩的系数,是int型,Google建议是2的倍数
return 1;
} else if (longSide < 4990) {
return 2;
// 这个10240上面的逻辑没有提到,也是经验值,不用去管它,你可以随意调整
} 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));
}
}
// 图片旋转方法
private Bitmap rotatingImage(Bitmap bitmap, int angle) {
Matrix matrix = new Matrix();
// 将传入的bitmap 进行角度旋转
matrix.postRotate(angle);
// 返回一个新的bitmap
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
}
// 压缩方法,返回一个File
File compress() throws IOException {
// 创建一个option对象
BitmapFactory.Options options = new BitmapFactory.Options();
// 获取采样压缩的值
options.inSampleSize = computeSize();
// 把图片进行采样压缩后放入一个bitmap, 参数1是bitmap图片的格式,前面获取的
Bitmap tagBitmap = BitmapFactory.decodeStream(srcImg.open(), null, options);
// 创建一个输出流的对象
ByteArrayOutputStream stream = new ByteArrayOutputStream();
// 判断是否是JPG图片
if (Checker.SINGLE.isJPG(srcImg.open())) {
// Checker.SINGLE.getOrientation这个方法是检测图片是否被旋转过,对图片进行矫正
tagBitmap = rotatingImage(tagBitmap, Checker.SINGLE.getOrientation(srcImg.open()));
}
// 对图片进行质量压缩,参数1:通过是否有透明通道来判断是PNG格式还是JPG格式,
// 参数2:压缩质量固定为60,参数3:压缩完后将bitmap写入到字节流中
tagBitmap.compress(focusAlpha ? Bitmap.CompressFormat.PNG : Bitmap.CompressFormat.JPEG, 60, stream);
// bitmap用完回收掉
tagBitmap.recycle();
// 将图片流写入到File中,然后刷新缓冲区,关闭文件流和Byte流
FileOutputStream fos = new FileOutputStream(tagImg);
fos.write(stream.toByteArray());
fos.flush();
fos.close();
stream.close();
return tagImg;
}
Luban原框架问题分析
解码前没有对内存做出预判 质量压缩写死 60 没有提供图片输出格式选择 不支持多文件合理并行压缩,输出顺序和压缩顺序不能保证一致 检测文件格式和图像的角度多次重复创建InputStream,增加不必要开销,增加OOM风险 可能出现内存泄漏,需要自己合理处理生命周期 图片要是有大小限制,只能进行重复压缩 原框架用的还是RxJava1.0
解码前利用获取的图片宽高对内存占用做出计算,超出内存的使用RGB-565尝试解码 针对质量压缩的时候,提供传入质量系数的接口 对图片输出支持多种格式,不局限于File 利用协程来实现异步压缩和并行压缩任务,可以在合适时机取消协程来终止任务 参考Glide对字节数组的复用,以及InputStream的mark()、reset()来优化重复打开开销 利用LiveData来实现监听,自动注销监听。 压缩前计算好大小,逆向推导出尺寸压缩系数和质量压缩系数 现在已经出了RxJava3和协程,但大多数项目中已经有了线程池,要利用项目中的线程池,而不是导入一个三方库就建一个线程池而造成资源浪费
图像压缩与Huffman算法
a. 1010 b. 1011 c. 1100 d. 1101 e. 1110
a. 010 b. 011 c. 100 d. 101 e. 110
a:010 (60%) b:011 (20%) c:100 (20%) d:101 (0%) e:110 (0%)
a:1 b:01 c:00
libjpeg与optimize_coding
TRUE causes the compressor to compute optimal Huffman coding tables
for the image. This requires an extra pass over the data and
therefore costs a good deal of space and time. The default is
FALSE, which tells the compressor to use the supplied or default
Huffman tables. In most cases optimal tables save only a few percent
of file size compared to the default tables. Note that when this is
TRUE, you need not supply Huffman tables at all, and any you do
supply will be overwritten.
https://cloud.tencent.com/developer/article/1006307
手写JPEG图像处理引擎
#include <jni.h>
#include <string>
#include <android/bitmap.h>
#include <android/log.h>
#include <malloc.h>
// 因为头文件都是c文件,咱们写的是.cpp 是C++文件,这时候就需要混编,所以加入下面关键字
extern "C"
{
#include "jpeglib.h"
}
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
#define LOG_TAG "louis"
#define true 1
typedef uint8_t BYTE;
// 写入图片函数
void writeImg(BYTE *data, const char *path, int w, int h) {
// 信使: java与C沟通的桥梁,jpeg的结构体,保存的比如宽、高、位深、图片格式等信息
struct jpeg_compress_struct jpeg_struct;
// 设置错误处理信息 当读完整个文件的时候就会回调my_error_exit,例如内置卡出错、没权限等
jpeg_error_mgr err;
jpeg_struct.err = jpeg_std_error(&err);
// 给结构体分配内存
jpeg_create_compress(&jpeg_struct);
// 打开输出文件
FILE *file = fopen(path, "wb");
// 设置输出路径
jpeg_stdio_dest(&jpeg_struct, file);
jpeg_struct.image_width = w;
jpeg_struct.image_height = h;
// 初始化 初始化
// 改成FALSE ---》 开启hufuman算法
jpeg_struct.arith_code = FALSE;
// 是否采用哈弗曼表数据计算 品质相差2倍多,官方实测, 吹5-10倍的都是扯淡
jpeg_struct.optimize_coding = TRUE;
// 设置结构体的颜色空间为RGB
jpeg_struct.in_color_space = JCS_RGB;
// 颜色通道数量
jpeg_struct.input_components = 3;
// 其他的设置默认
jpeg_set_defaults(&jpeg_struct);
// 设置质量
jpeg_set_quality(&jpeg_struct, 60, true);
// 开始压缩,(是否写入全部像素)
jpeg_start_compress(&jpeg_struct, TRUE);
JSAMPROW row_pointer[1];
// 一行的rgb
int row_stride = w * 3;
// 一行一行遍历 如果当前的行数小于图片的高度,就进入循环
while (jpeg_struct.next_scanline < h) {
// 得到一行的首地址
row_pointer[0] = &data[jpeg_struct.next_scanline * w * 3];
// 此方法会将jcs.next_scanline加1
jpeg_write_scanlines(&jpeg_struct, row_pointer, 1);//row_pointer就是一行的首地址,1:写入的行数
}
jpeg_finish_compress(&jpeg_struct);
jpeg_destroy_compress(&jpeg_struct);
fclose(file);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_maniu_wechatimagesend_MainActivity_compress(JNIEnv *env,
jobject instance,
jobject bitmap,
jstring path_) {
const char *path = env->GetStringUTFChars(path_, 0);
// 获取Bitmap信息
AndroidBitmapInfo bitmapInfo;
AndroidBitmap_getInfo(env, bitmap, &bitmapInfo);
// 存储ARGB所有像素点
BYTE *pixels;
// 1、读取Bitmap所有像素信息
AndroidBitmap_lockPixels(env, bitmap, (void **) &pixels);
// 获取bitmap的 宽,高,format
int h = bitmapInfo.height;
int w = bitmapInfo.width;
// 存储RGB所有像素点
BYTE *data,*tmpData;
// 2、解析每个像素,去除A通量,取出RGB通量,
// 假如图片的像素是1920*1080,只有RGB三个颜色通道的话,计算公式为 w*h*3
data= (BYTE *) malloc(w * h * 3);
// 存储RGB首地址
tmpData = data;
BYTE r, g, b;
int color;
for (int i = 0; i < h; ++i) {
for (int j = 0; j < w; ++j) {
color = *((int *) pixels);
// 取出R G B
r = ((color & 0x00FF0000) >> 16);
g = ((color & 0x0000FF00) >> 8);
b = ((color & 0x000000FF));
// 赋值
*data = b;
*(data + 1) = g;
*(data + 2) = r;
// 指针后移
data += 3;
pixels += 4;
}
}
// 3、读取像素点完毕 解锁,
AndroidBitmap_unlockPixels(env, bitmap);
// 直接用data写数据
writeImg(tmpData, path, w, h);
env->ReleaseStringUTFChars(path_, path);
}
我是 code小生
,喜欢可以随手点个在看
、转发给你的朋友,谢谢~