拓展小知识
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); }
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 strm(CreateJavaOutputStreamAdaptor(env, jstream, jstorage)); if (!strm.get()) { return JNI_FALSE; } std::unique_ptr 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);
简单解释一下:获取图片的比例系数,如果在区间 [1, 0.5625) 中即图片处于 [1:1 ~ 9:16)比例范围内,比例以此类推,如果这个系数小于0.5,那么就给它放到 [1:2 ~ 1:∞)比例范围内。 判断图片最长边是否过边界值。
步骤二:上去一看一脸懵,1664是什么,n是什么,pow又是什么。。。这写的估计只有作者自己能看懂了。其实就是判断图片最长边是否过边界值,此边界值是模仿微信的一个经验值,就是说1664、4990都是经验值,模仿微信的策略。 至于n,是返回的是options.inSampleSize的值,就是采样压缩的系数,是int型,Google建议是2的倍数,所以为了配合这个建议,代码中出现了小于10240返回的是4这种操作。最后说一下pow,其实是(长边/1280), 这个1280也是个经验值,逆向推出来的,解释到这里逻辑也清晰了。真是坑啊啊,哈哈哈 计算压缩图片实际边长值,以第2步计算结果为准,超过某个边界值则:
步骤三:这个感觉没什么用,还是计算压缩图片实际边长值,人家也说了,以第2步计算结果为准,其实就是晃你的,乍一看 ,这么多步骤,哈哈哈哈,唬你呢! 计算压缩图片的实际文件大小,以第2、3步结果为准,图片比例越大则文件越大。 size = (newW * newH) / (width * height) * m;
步骤四:这个感觉也没什么用,这个m应该是压缩比。但整个过程就是验证一下压缩完之后,size的大小,是否超过了你的预期,如果超过了你的预期,将进行重复压缩。 判断第4步的size是否过小。
步骤五:这一步也没啥用,也是为了后面循环压缩使用。这个size就是上面计算出来的,最小 size 对应的值公式为:size = (newW * newH) / (width * height) * m,对应的三个值,就是上面根据图片的比例分成的三组,然后计算出来的。 将前面求到的值压缩图片 width, height, size 传入压缩流程,压缩图片直到满足以上数值。 最后一步也没啥用,看字就知道是为了循环压缩,或许是微信也这样做?既然你已经有了预期,为什么不根据预期直接一步到位呢?但是裁剪的系数和压缩的系数怎么调整会达到最优一个效果,我的项目中已经对此功能进行了增加,目前还在内测,没有开源,后期稳定后会开源给大家使用。
// 计算采样压缩的值,也就是模仿微信的经验值,核心内容 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; }
技术改造方案
小结
a. 1010 b. 1011 c. 1100 d. 1101 e. 1110 我们可以看到,最前面的一位数字是 1,其实是浪费掉了,在定长算法下最优的表达式为:
a. 010 b. 011 c. 100 d. 101 e. 110 这样我们就能做到节省一位的损耗,那哈夫曼算法比起定长算法改进的地方在哪里呢?在哈夫曼算法中我们可以给信息赋予权重,即为信息加权重,假设 a 占据了 60%,b 占据了 20%, c 占据了 20%,d,e 都是 0%:
a:010 (60%) b:011 (20%) c:100 (20%) d:101 (0%) e:110 (0%) 在这种情况下,我们可以使用哈夫曼树算法再次优化为:
a:1 b:01 c:00 所以思路当然就是出现频率高的字母使用短码,对出现频率低的使用长码,不出现的直接就去掉,最后 abcde 的哈夫曼编码就对应:1 01 00 定长编码下的abcde:010 011 100 101 110, 使用 哈夫曼树 加权重后的 编码则为 1 01 00,这就是哈夫曼算法的整体思路(关于算法的详细介绍可以参考哈夫曼树及编码讲解及例题)。 所以这个算法一个很重要的思路是必须知道每一个元素出现的权重,如果我们能够知道每一个元素的权重,那么就能够根据权重动态生成一个最优的哈夫曼表。 但是怎么去获取每一个元素,对于图片就是每一个像素中 argb 的权重呢,只能去循环整个图片的像素信息,这无疑是非常消耗性能的,所以早期 android 就使用了默认的哈夫曼表进行图片压缩。
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
#include #include #include #include #include // 因为头文件都是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); }
审核编辑:汤梓红
全部0条评论
快来发表一下你的评论吧 !