解码区域
1
,抽象
1,图片
很大时,解码
速度缓慢,占用
内存很高,并且,图片
超过一定尺寸时,无法上传和显示纹理
(这跟GPU
能力有关,一般的GPU
是8192*8192
).这时只好下采样
,但会牺牲图片显示
质量.
2,-对图库
等,需要清晰浏览
图片的应用,不可能设置下采样率
来解决这一问题,因此,Google
加入了解码区域
功能,这样可从原始图片文件
中,解码出一部分完整区域
图片内容.
3,解码区域难点
主要在,定位
像素区域所对应的文件位置
,需要图像编码
有一定的连续性
,所幸,主流图像格式
都是如此.
4,目前解码区域
主要实现了png,jpeg,webp
类型图片的支持.本篇,介绍最常用的jpeg
格式的解码区域
实现.
2
,解码区域总流程
在框架
侧创建BitmapRegionDecoder
时,创建对应类型的SkImageDecoder
来扫描全文件,调用其onBuildTileIndex
方法构建tileIndex
,嵌入在其关联的SkImageDecoder
之中,在后续调用decodeRegion
时,使用SkImageDecoder
的onDecodeSubset
方法来解码区域
.
3
,Jpeg
的解码区域
#ifdef SK_BUILD_FOR_ANDROID
bool SkJPEGImageDecoder::onBuildTileIndex(SkStreamRewindable* stream, int *width, int *height) {SkAutoTDelete<SkJPEGImageIndex> imageIndex(SkNEW_ARGS(SkJPEGImageIndex, (stream, this)));jpeg_decompress_struct* cinfo = imageIndex->cinfo();skjpeg_error_mgr sk_err;set_error_mgr(cinfo, &sk_err);//在调用此`setjmp`前,实例化所有对象,以便在错误时正确清理它们.if (setjmp(sk_err.fJmpBuf)) {return false;}//创建,来创建/构建`HuffmanIndex`的`cinfo`if (!imageIndex->initializeInfoAndReadHeader()) {return false;}if (!imageIndex->buildHuffmanIndex()) {return false;}//析构,来创建/构建霍夫曼索引的`cinfo`imageIndex->destroyInfo();//初化`解码器`到`图像解码`模式if (!imageIndex->initializeInfoAndReadHeader()) {return false;}//`FIXME`:这设置了`cinfo->out_color_space`,稍后可能会根据`onDecodeSubset`中的配置更改它.这应该没问题,因为在(它调用`jinit_color_deconverter`时)更改它后,`jpeg_init_read_tile_scanline`调用再次检查`out_color_space`.(void) this->getBitmapColorType(cinfo);turn_off_visual_optimizations(cinfo);//不是`jpeg_start_decompress()`,而开始平铺解压缩if (!imageIndex->startTileDecompress()) {return false;}SkASSERT(1 == cinfo->scale_num);fImageWidth = cinfo->output_width;fImageHeight = cinfo->output_height;if (width) {*width = fImageWidth;}if (height) {*height = fImageHeight;}SkDELETE(fImageIndex);fImageIndex = imageIndex.detach();return true;
}
bool SkJPEGImageDecoder::onDecodeSubset(SkBitmap* bm, const SkIRect& region) {if (NULL == fImageIndex) {return false;}jpeg_decompress_struct* cinfo = fImageIndex->cinfo();SkIRect rect = SkIRect::MakeWH(fImageWidth, fImageHeight);if (!rect.intersect(region)) {//如果请求区域,完全在`图像`外,则返回`false`return false;}skjpeg_error_mgr errorManager;set_error_mgr(cinfo, &errorManager);if (setjmp(errorManager.fJmpBuf)) {return false;}int requestedSampleSize = this->getSampleSize();cinfo->scale_denom = requestedSampleSize;set_dct_method(*this, cinfo);const SkColorType colorType = this->getBitmapColorType(cinfo);adjust_out_color_space_and_dither(cinfo, colorType, *this);int startX = rect.fLeft;int startY = rect.fTop;int width = rect.width();int height = rect.height();jpeg_init_read_tile_scanline(cinfo, fImageIndex->huffmanIndex(), &startX, &startY, &width, &height);int skiaSampleSize = recompute_sampleSize(requestedSampleSize, *cinfo);int actualSampleSize = skiaSampleSize * (DCTSIZE / cinfo->min_DCT_scaled_size);SkScaledBitmapSampler sampler(width, height, skiaSampleSize);SkBitmap bitmap;//假设`A8`位图不透明,以避免检查每个单独的像素.它不太可能是不透明的,因为不透明的`A8`位图不会很有趣.否则,`jpeg`图像是不透明的.bitmap.setInfo(SkImageInfo::Make(sampler.scaledWidth(), sampler.scaledHeight(), colorType, kAlpha_8_SkColorType == colorType ? kPremul_SkAlphaType : kOpaque_SkAlphaType));//提前检查是否可交换`(dest,src)`.如果是,则坚持使用`AllocPixelRef`,因为交换时它更便宜.如果否,则使用`alloc`来分配像素以避免垃集. int w = rect.width() / actualSampleSize;int h = rect.height() / actualSampleSize;bool swapOnly = (rect == region) && bm->isNull() &&(w == bitmap.width()) && (h == bitmap.height()) &&((startX - rect.x()) / actualSampleSize == 0) &&((startY - rect.y()) / actualSampleSize == 0);if (swapOnly) {if (!this->allocPixelRef(&bitmap, NULL)) {return return_false(*cinfo, bitmap, "allocPixelRef");}} else {if (!bitmap.allocPixels()) {return return_false(*cinfo, bitmap, "allocPixels");}}SkAutoLockPixels alp(bitmap);
#ifdef ANDROID_RGB/*如果可能,请短路`SkScaledBitmapSampler`,因为这会显著提高性能.*/if (skiaSampleSize == 1 &&((kN32_SkColorType == colorType && cinfo->out_color_space == JCS_RGBA_8888) ||(kRGB_565_SkColorType == colorType && cinfo->out_color_space == JCS_RGB_565))){JSAMPLE* rowptr = (JSAMPLE*)bitmap.getPixels();INT32 const bpr = bitmap.rowBytes();int rowTotalCount = 0;while (rowTotalCount < height) {int rowCount = jpeg_read_tile_scanline(cinfo, fImageIndex->huffmanIndex(), &rowptr);//如果`rowCount==0`,则没有得到扫描行,所以中止.`onDecodeSubset()`依赖`onBuildTileIndex()`,它需要完整的映射才能成功. if (0 == rowCount) {return return_false(*cinfo, bitmap, "read_scanlines");}if (this->shouldCancelDecode()) {return return_false(*cinfo, bitmap, "shouldCancelDecode");}rowTotalCount += rowCount;rowptr += bpr;}if (swapOnly) {bm->swap(bitmap);} else {cropBitmap(bm, &bitmap, actualSampleSize, region.x(), region.y(), region.width(), region.height(), startX, startY);}return true;}
#endif//检查支持的格式SkScaledBitmapSampler::SrcConfig sc;int srcBytesPerPixel;if (!get_src_config(*cinfo, &sc, &srcBytesPerPixel)) {return return_false(*cinfo, *bm, "jpeg colorspace");}if (!sampler.begin(&bitmap, sc, *this)) {return return_false(*cinfo, bitmap, "sampler.begin");}SkAutoMalloc srcStorage(width * srcBytesPerPixel);uint8_t* srcRow = (uint8_t*)srcStorage.get();//可能跳过初始行`[sampler.srcY0]`if (!skip_src_rows_tile(cinfo, fImageIndex->huffmanIndex(), srcRow, sampler.srcY0())) {return return_false(*cinfo, bitmap, "skip rows");}//现在遍历扫描行,直到`y==bitmap->height()-1`for (int y = 0;; y++) {JSAMPLE* rowptr = (JSAMPLE*)srcRow;int row_count = jpeg_read_tile_scanline(cinfo, fImageIndex->huffmanIndex(), &rowptr);//如果`row_count==0`,则没有得到扫描行,所以中止.`onDecodeSubset()`依赖`onBuildTileIndex()`,它需要完整的映射才能成功. if (0 == row_count) {return return_false(*cinfo, bitmap, "read_scanlines");}if (this->shouldCancelDecode()) {return return_false(*cinfo, bitmap, "shouldCancelDecode");}if (JCS_CMYK == cinfo->out_color_space) {convert_CMYK_to_RGB(srcRow, width);}sampler.next(srcRow);if (bitmap.height() - 1 == y) {//大功告成break;}if (!skip_src_rows_tile(cinfo, fImageIndex->huffmanIndex(), srcRow, sampler.srcDY() - 1)) {return return_false(*cinfo, bitmap, "skip rows");}}if (swapOnly) {bm->swap(bitmap);} else {cropBitmap(bm, &bitmap, actualSampleSize, region.x(), region.y(),region.width(), region.height(), startX, startY);}return true;
}
#endif
在onBuildTileIndex
时,创建了huffman_index
,其中的内容主要是一系列,记录每个块
对应偏移
的huffman_offset
.
解码
时,先移到对应块
位置,然后解出
像素.
GLOBAL(JDIMENSION)
jpeg_read_tile_scanline (j_decompress_ptr cinfo, huffman_index *index, JSAMPARRAY scanlines)
{//计算`iMCU`的边界int lines_per_iMCU_row = cinfo->max_v_samp_factor * DCTSIZE;int lines_per_iMCU_col = cinfo->max_h_samp_factor * DCTSIZE;int sample_size = DCTSIZE / cinfo->min_DCT_scaled_size;JDIMENSION row_ctr = 0;if (cinfo->progressive_mode) {(*cinfo->main->process_data) (cinfo, scanlines, &row_ctr, 1);} else {if (cinfo->output_scanline % (lines_per_iMCU_row / sample_size) == 0) {//读头设置到下一个`iMCU`行int iMCU_row_offset = cinfo->output_scanline / (lines_per_iMCU_row / sample_size);int offset_data_col_position = cinfo->coef->MCU_column_left_boundary /index->MCU_sample_size;huffman_offset_data offset_data =index->scan[0].offset[iMCU_row_offset][offset_data_col_position];(*cinfo->entropy->configure_huffman_decoder) (cinfo, offset_data);}(*cinfo->main->process_data) (cinfo, scanlines, &row_ctr, 1);}cinfo->output_scanline += row_ctr;return row_ctr;
}