From e16b04aa6041efb6507546547737e9603fa1606e Mon Sep 17 00:00:00 2001 From: msarett Date: Wed, 15 Apr 2015 07:32:19 -0700 Subject: [PATCH] SkJpegCodec Enables basic decoding for jpegs Includes rewinding 565, YUV, and Jpeg encoding are not yet implemented BUG=skia:3257 Review URL: https://codereview.chromium.org/1076923002 --- dm/DM.cpp | 4 +- dm/DMSrcSink.cpp | 6 + gyp/codec.gyp | 4 + src/codec/SkCodec.cpp | 2 + src/codec/SkCodec_libbmp.cpp | 36 +++-- src/codec/SkJpegCodec.cpp | 347 +++++++++++++++++++++++++++++++++++++++++ src/codec/SkJpegCodec.h | 99 ++++++++++++ src/codec/SkJpegDecoderMgr.cpp | 109 +++++++++++++ src/codec/SkJpegDecoderMgr.h | 77 +++++++++ src/codec/SkJpegUtility.cpp | 89 +++++++++++ src/codec/SkJpegUtility.h | 50 ++++++ src/codec/SkSwizzler.cpp | 46 +++++- src/codec/SkSwizzler.h | 2 + tests/CodexTest.cpp | 50 ++++++ tests/SwizzlerTest.cpp | 25 ++- 15 files changed, 924 insertions(+), 22 deletions(-) create mode 100644 src/codec/SkJpegCodec.cpp create mode 100644 src/codec/SkJpegCodec.h create mode 100644 src/codec/SkJpegDecoderMgr.cpp create mode 100644 src/codec/SkJpegDecoderMgr.h create mode 100644 src/codec/SkJpegUtility.cpp create mode 100644 src/codec/SkJpegUtility.h diff --git a/dm/DM.cpp b/dm/DM.cpp index c6d633c..ff9dc57 100644 --- a/dm/DM.cpp +++ b/dm/DM.cpp @@ -223,8 +223,8 @@ static bool codec_supported(const char* ext) { // FIXME: Once other versions of SkCodec are available, we can add them to this // list (and eventually we can remove this check once they are all supported). static const char* const exts[] = { - "bmp", "gif", "png", "ico", "wbmp", - "BMP", "GIF", "PNG", "ICO", "WBMP" + "bmp", "gif", "jpg", "jpeg", "png", "ico", "wbmp", + "BMP", "GIF", "JPG", "JPEG", "PNG", "ICO", "WBMP" }; for (uint32_t i = 0; i < SK_ARRAY_COUNT(exts); i++) { diff --git a/dm/DMSrcSink.cpp b/dm/DMSrcSink.cpp index e70dd3e..6b3ec97 100644 --- a/dm/DMSrcSink.cpp +++ b/dm/DMSrcSink.cpp @@ -83,7 +83,13 @@ Error CodecSrc::draw(SkCanvas* canvas) const { SkColorType canvasColorType = canvasInfo.colorType(); switch (fDstColorType) { case kIndex8_Always_DstColorType: + decodeInfo = codec->getInfo().makeColorType(kIndex_8_SkColorType); + if (kRGB_565_SkColorType == canvasColorType) { + return Error::Nonfatal("Testing non-565 to 565 is uninteresting."); + } + break; case kGrayscale_Always_DstColorType: + decodeInfo = codec->getInfo().makeColorType(kGray_8_SkColorType); if (kRGB_565_SkColorType == canvasColorType) { return Error::Nonfatal("Testing non-565 to 565 is uninteresting."); } diff --git a/gyp/codec.gyp b/gyp/codec.gyp index 1373773..f3fed41 100644 --- a/gyp/codec.gyp +++ b/gyp/codec.gyp @@ -18,6 +18,7 @@ 'dependencies': [ 'core.gyp:*', 'giflib.gyp:giflib', + 'libjpeg.gyp:libjpeg', ], 'cflags':[ # FIXME: This gets around a longjmp warning. See @@ -37,6 +38,9 @@ '../src/codec/SkCodec_libpng.cpp', '../src/codec/SkCodec_wbmp.cpp', '../src/codec/SkGifInterlaceIter.cpp', + '../src/codec/SkJpegCodec.cpp', + '../src/codec/SkJpegDecoderMgr.cpp', + '../src/codec/SkJpegUtility.cpp', '../src/codec/SkMaskSwizzler.cpp', '../src/codec/SkMasks.cpp', '../src/codec/SkSwizzler.cpp', diff --git a/src/codec/SkCodec.cpp b/src/codec/SkCodec.cpp index 7bc9146..c4e4885 100644 --- a/src/codec/SkCodec.cpp +++ b/src/codec/SkCodec.cpp @@ -13,6 +13,7 @@ #include "SkCodec_libpng.h" #include "SkCodec_wbmp.h" #include "SkCodecPriv.h" +#include "SkJpegCodec.h" #include "SkStream.h" struct DecoderProc { @@ -22,6 +23,7 @@ struct DecoderProc { static const DecoderProc gDecoderProcs[] = { { SkPngCodec::IsPng, SkPngCodec::NewFromStream }, + { SkJpegCodec::IsJpeg, SkJpegCodec::NewFromStream }, { SkGifCodec::IsGif, SkGifCodec::NewFromStream }, { SkIcoCodec::IsIco, SkIcoCodec::NewFromStream }, { SkBmpCodec::IsBmp, SkBmpCodec::NewFromStream }, diff --git a/src/codec/SkCodec_libbmp.cpp b/src/codec/SkCodec_libbmp.cpp index 67be0db..56663f8 100644 --- a/src/codec/SkCodec_libbmp.cpp +++ b/src/codec/SkCodec_libbmp.cpp @@ -97,7 +97,6 @@ enum BitmapCompressionMethod { */ bool SkBmpCodec::IsBmp(SkStream* stream) { // TODO: Support "IC", "PT", "CI", "CP", "BA" - // TODO: ICO files may contain a BMP and need to use this decoder const char bmpSig[] = { 'B', 'M' }; char buffer[sizeof(bmpSig)]; return stream->read(buffer, sizeof(bmpSig)) == sizeof(bmpSig) && @@ -773,12 +772,12 @@ SkCodec::Result SkBmpCodec::decodeMask(const SkImageInfo& dstInfo, if (stream()->read(srcRow, rowBytes) != rowBytes) { SkCodecPrintf("Warning: incomplete input stream.\n"); // Fill the destination image on failure - // By using zero as the fill value, we will fill with transparent - // pixels for non-opaque images and white for opaque images. - // These are arbitrary choices but allow for consistent behavior. - if (kNo_ZeroInitialized == opts.fZeroInitialized) { + SkPMColor fillColor = dstInfo.alphaType() == kOpaque_SkAlphaType ? + SK_ColorBLACK : SK_ColorTRANSPARENT; + if (kNo_ZeroInitialized == opts.fZeroInitialized || 0 != fillColor) { void* dstStart = get_dst_start_row(dst, dstRowBytes, y, fRowOrder); - SkSwizzler::Fill(dstStart, dstInfo, dstRowBytes, dstInfo.height() - y, 0, NULL); + SkSwizzler::Fill(dstStart, dstInfo, dstRowBytes, dstInfo.height() - y, fillColor, + NULL); } return kIncompleteInput; } @@ -1090,29 +1089,42 @@ SkCodec::Result SkBmpCodec::decode(const SkImageInfo& dstInfo, const int height = dstInfo.height(); const size_t rowBytes = SkAlign4(compute_row_bytes(width, fBitsPerPixel)); - // Get swizzler configuration + // Get swizzler configuration and choose the fill value for failures. We will use + // zero as the default palette index, black for opaque images, and transparent for + // non-opaque images. SkSwizzler::SrcConfig config; + uint32_t fillColorOrIndex; + bool zeroFill = true; switch (fBitsPerPixel) { case 1: config = SkSwizzler::kIndex1; + fillColorOrIndex = 0; break; case 2: config = SkSwizzler::kIndex2; + fillColorOrIndex = 0; break; case 4: config = SkSwizzler::kIndex4; + fillColorOrIndex = 0; break; case 8: config = SkSwizzler::kIndex; + fillColorOrIndex = 0; break; case 24: config = SkSwizzler::kBGR; + fillColorOrIndex = SK_ColorBLACK; + zeroFill = false; break; case 32: if (kOpaque_SkAlphaType == dstInfo.alphaType()) { config = SkSwizzler::kBGRX; + fillColorOrIndex = SK_ColorBLACK; + zeroFill = false; } else { config = SkSwizzler::kBGRA; + fillColorOrIndex = SK_ColorTRANSPARENT; } break; default: @@ -1138,14 +1150,10 @@ SkCodec::Result SkBmpCodec::decode(const SkImageInfo& dstInfo, if (stream()->read(srcBuffer.get(), rowBytes) != rowBytes) { SkCodecPrintf("Warning: incomplete input stream.\n"); // Fill the destination image on failure - // By using zero as the fill value, we will fill with the first - // color in the color table for palette images, transparent - // pixels for non-opaque images, and white for opaque images. - // These are arbitrary choices but allow for consistent behavior. - if (kNo_ZeroInitialized == opts.fZeroInitialized) { + if (kNo_ZeroInitialized == opts.fZeroInitialized || !zeroFill) { void* dstStart = get_dst_start_row(dst, dstRowBytes, y, fRowOrder); - SkSwizzler::Fill(dstStart, dstInfo, dstRowBytes, dstInfo.height() - y, 0, - colorPtr); + SkSwizzler::Fill(dstStart, dstInfo, dstRowBytes, dstInfo.height() - y, + fillColorOrIndex, colorPtr); } return kIncompleteInput; } diff --git a/src/codec/SkJpegCodec.cpp b/src/codec/SkJpegCodec.cpp new file mode 100644 index 0000000..6f7af49 --- /dev/null +++ b/src/codec/SkJpegCodec.cpp @@ -0,0 +1,347 @@ +/* + * Copyright 2015 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include "SkCodec.h" +#include "SkJpegCodec.h" +#include "SkJpegDecoderMgr.h" +#include "SkJpegUtility.h" +#include "SkCodecPriv.h" +#include "SkColorPriv.h" +#include "SkStream.h" +#include "SkTemplates.h" +#include "SkTypes.h" + +// stdio is needed for jpeglib +#include + +extern "C" { + #include "jerror.h" + #include "jmorecfg.h" + #include "jpegint.h" + #include "jpeglib.h" +} + +// ANDROID_RGB +// If this is defined in the jpeg headers it indicates that jpeg offers +// support for two additional formats: JCS_RGBA_8888 and JCS_RGB_565. + +/* + * Get the source configuarion for the swizzler + */ +SkSwizzler::SrcConfig get_src_config(const jpeg_decompress_struct& dinfo) { + if (JCS_CMYK == dinfo.out_color_space) { + // We will need to perform a manual conversion + return SkSwizzler::kRGBX; + } + if (3 == dinfo.out_color_components && JCS_RGB == dinfo.out_color_space) { + return SkSwizzler::kRGB; + } +#ifdef ANDROID_RGB + if (JCS_RGBA_8888 == dinfo.out_color_space) { + return SkSwizzler::kRGBX; + } + + if (JCS_RGB_565 == dinfo.out_color_space) { + return SkSwizzler::kRGB_565; + } +#endif + if (1 == dinfo.out_color_components && JCS_GRAYSCALE == dinfo.out_color_space) { + return SkSwizzler::kGray; + } + return SkSwizzler::kUnknown; +} + +/* + * Convert a row of CMYK samples to RGBX in place. + * Note that this method moves the row pointer. + * @param width the number of pixels in the row that is being converted + * CMYK is stored as four bytes per pixel + */ +static void convert_CMYK_to_RGB(uint8_t* row, uint32_t width) { + // We will implement a crude conversion from CMYK -> RGB using formulas + // from easyrgb.com. + // + // CMYK -> CMY + // C = C * (1 - K) + K + // M = M * (1 - K) + K + // Y = Y * (1 - K) + K + // + // libjpeg actually gives us inverted CMYK, so we must subtract the + // original terms from 1. + // CMYK -> CMY + // C = (1 - C) * (1 - (1 - K)) + (1 - K) + // M = (1 - M) * (1 - (1 - K)) + (1 - K) + // Y = (1 - Y) * (1 - (1 - K)) + (1 - K) + // + // Simplifying the above expression. + // CMYK -> CMY + // C = 1 - CK + // M = 1 - MK + // Y = 1 - YK + // + // CMY -> RGB + // R = (1 - C) * 255 + // G = (1 - M) * 255 + // B = (1 - Y) * 255 + // + // Therefore the full conversion is below. This can be verified at + // www.rapidtables.com (assuming inverted CMYK). + // CMYK -> RGB + // R = C * K * 255 + // G = M * K * 255 + // B = Y * K * 255 + // + // As a final note, we have treated the CMYK values as if they were on + // a scale from 0-1, when in fact they are 8-bit ints scaling from 0-255. + // We must divide each CMYK component by 255 to obtain the true conversion + // we should perform. + // CMYK -> RGB + // R = C * K / 255 + // G = M * K / 255 + // B = Y * K / 255 + for (uint32_t x = 0; x < width; x++, row += 4) { + row[0] = SkMulDiv255Round(row[0], row[3]); + row[1] = SkMulDiv255Round(row[1], row[3]); + row[2] = SkMulDiv255Round(row[2], row[3]); + row[3] = 0xFF; + } +} + +bool SkJpegCodec::IsJpeg(SkStream* stream) { + static const uint8_t jpegSig[] = { 0xFF, 0xD8, 0xFF }; + char buffer[sizeof(jpegSig)]; + return stream->read(buffer, sizeof(jpegSig)) == sizeof(jpegSig) && + !memcmp(buffer, jpegSig, sizeof(jpegSig)); +} + +bool SkJpegCodec::ReadHeader(SkStream* stream, SkCodec** codecOut, + JpegDecoderMgr** decoderMgrOut) { + + // Create a JpegDecoderMgr to own all of the decompress information + SkAutoTDelete decoderMgr(SkNEW_ARGS(JpegDecoderMgr, (stream))); + + // libjpeg errors will be caught and reported here + if (setjmp(decoderMgr->getJmpBuf())) { + return decoderMgr->returnFalse("setjmp"); + } + + // Initialize the decompress info and the source manager + decoderMgr->init(); + + // Read the jpeg header + if (JPEG_HEADER_OK != jpeg_read_header(decoderMgr->dinfo(), true)) { + return decoderMgr->returnFalse("read_header"); + } + + if (NULL != codecOut) { + // Recommend the color type to decode to + const SkColorType colorType = decoderMgr->getColorType(); + + // Create image info object and the codec + const SkImageInfo& imageInfo = SkImageInfo::Make(decoderMgr->dinfo()->image_width, + decoderMgr->dinfo()->image_height, colorType, kOpaque_SkAlphaType); + *codecOut = SkNEW_ARGS(SkJpegCodec, (imageInfo, stream, decoderMgr.detach())); + } else { + SkASSERT(NULL != decoderMgrOut); + *decoderMgrOut = decoderMgr.detach(); + } + return true; +} + +SkCodec* SkJpegCodec::NewFromStream(SkStream* stream) { + SkAutoTDelete streamDeleter(stream); + SkCodec* codec = NULL; + if (ReadHeader(stream, &codec, NULL)) { + // Codec has taken ownership of the stream, we do not need to delete it + SkASSERT(codec); + streamDeleter.detach(); + return codec; + } + return NULL; +} + +SkJpegCodec::SkJpegCodec(const SkImageInfo& srcInfo, SkStream* stream, + JpegDecoderMgr* decoderMgr) + : INHERITED(srcInfo, stream) + , fDecoderMgr(decoderMgr) +{} + +/* + * Return a valid set of output dimensions for this decoder, given an input scale + */ +SkISize SkJpegCodec::onGetScaledDimensions(float desiredScale) const { + // libjpeg supports scaling by 1/1, 1/2, 1/4, and 1/8, so we will support these as well + long scale; + if (desiredScale > 0.75f) { + scale = 1; + } else if (desiredScale > 0.375f) { + scale = 2; + } else if (desiredScale > 0.1875f) { + scale = 4; + } else { + scale = 8; + } + + // Set up a fake decompress struct in order to use libjpeg to calculate output dimensions + jpeg_decompress_struct dinfo; + dinfo.image_width = this->getInfo().width(); + dinfo.image_height = this->getInfo().height(); + dinfo.global_state = DSTATE_READY; + dinfo.num_components = 0; + dinfo.scale_num = 1; + dinfo.scale_denom = scale; + jpeg_calc_output_dimensions(&dinfo); + + // Return the calculated output dimensions for the given scale + return SkISize::Make(dinfo.output_width, dinfo.output_height); +} + +/* + * Checks if the conversion between the input image and the requested output + * image has been implemented + */ +static bool conversion_possible(const SkImageInfo& dst, + const SkImageInfo& src) { + // Ensure that the profile type is unchanged + if (dst.profileType() != src.profileType()) { + return false; + } + + // Ensure that the alpha type is opaque + if (kOpaque_SkAlphaType != dst.alphaType()) { + return false; + } + + // Always allow kN32 as the color type + if (kN32_SkColorType == dst.colorType()) { + return true; + } + + // Otherwise require that the destination color type match our recommendation + return dst.colorType() == src.colorType(); +} + +/* + * Performs the jpeg decode + */ +SkCodec::Result SkJpegCodec::onGetPixels(const SkImageInfo& dstInfo, + void* dst, size_t dstRowBytes, + const Options& options, SkPMColor*, int*) { + // Rewind the stream if needed + SkCodec::RewindState rewindState = this->rewindIfNeeded(); + if (rewindState == kCouldNotRewind_RewindState) { + return kCouldNotRewind; + } else if (rewindState == kRewound_RewindState) { + JpegDecoderMgr* decoderMgr = NULL; + if (!ReadHeader(this->stream(), NULL, &decoderMgr)) { + return kCouldNotRewind; + } + SkASSERT(NULL != decoderMgr); + fDecoderMgr.reset(decoderMgr); + } + + // Get a pointer to the decompress info since we will use it quite frequently + jpeg_decompress_struct* dinfo = fDecoderMgr->dinfo(); + + // Set the jump location for libjpeg errors + if (setjmp(fDecoderMgr->getJmpBuf())) { + return fDecoderMgr->returnFailure("setjmp", kInvalidInput); + } + + // Check if we can decode to the requested destination + if (!conversion_possible(dstInfo, this->getInfo())) { + return fDecoderMgr->returnFailure("conversion_possible", kInvalidConversion); + } + // Check if we can scale to the requested dimensions + // libjpeg can scale to 1/1, 1/2, 1/4, and 1/8 + SkASSERT(1 == dinfo->scale_num); + SkASSERT(1 == dinfo->scale_denom); + jpeg_calc_output_dimensions(dinfo); + const uint32_t dstWidth = dstInfo.width(); + const uint32_t dstHeight = dstInfo.height(); + while (dinfo->output_width != dstWidth || dinfo->output_height != dstHeight) { + + // Return a failure if we have tried all of the possible scales + if (8 == dinfo->scale_denom || + dstWidth > dinfo->output_width || + dstHeight > dinfo->output_height) { + return fDecoderMgr->returnFailure("cannot scale to requested dims", kInvalidScale); + } + + // Try the next scale + dinfo->scale_denom *= 2; + jpeg_calc_output_dimensions(dinfo); + } + + // Now, given valid output dimensions, we can start the decompress + if (!jpeg_start_decompress(dinfo)) { + return fDecoderMgr->returnFailure("startDecompress", kInvalidInput); + } + + // Create the swizzler + SkSwizzler::SrcConfig srcConfig = get_src_config(*dinfo); + SkAutoTDelete swizzler(SkSwizzler::CreateSwizzler(srcConfig, NULL, dstInfo, dst, + dstRowBytes, options.fZeroInitialized)); + if (NULL == swizzler) { + return fDecoderMgr->returnFailure("getSwizzler", kInvalidInput); + } + const uint32_t srcBytesPerPixel = SkSwizzler::BytesPerPixel(srcConfig); + + // This is usually 1, but can also be 2 or 4. + // If we wanted to always read one row at a time, we could, but we will save space and time + // by using the recommendation from libjpeg. + const uint32_t rowsPerDecode = dinfo->rec_outbuf_height; + SkASSERT(rowsPerDecode <= 4); + + // Create a buffer to contain decoded rows (libjpeg requires a 2D array) + const uint32_t srcRowBytes = srcBytesPerPixel * dstWidth; + SkAutoTDeleteArray srcBuffer(SkNEW_ARRAY(uint8_t, srcRowBytes * rowsPerDecode)); + JSAMPLE* srcRows[4]; + uint8_t* srcPtr = srcBuffer.get(); + for (uint8_t i = 0; i < rowsPerDecode; i++) { + srcRows[i] = (JSAMPLE*) srcPtr; + srcPtr += srcRowBytes; + } + + // Ensure that we loop enough times to decode all of the rows + // libjpeg will prevent us from reading past the bottom of the image + for (uint32_t y = 0; y < dstHeight + rowsPerDecode - 1; y += rowsPerDecode) { + // Read rows of the image + uint32_t rowsDecoded = jpeg_read_scanlines(dinfo, srcRows, rowsPerDecode); + + // Convert to RGB if necessary + if (JCS_CMYK == dinfo->out_color_space) { + convert_CMYK_to_RGB(srcRows[0], dstWidth * rowsDecoded); + } + + // Swizzle to output destination + for (uint32_t i = 0; i < rowsDecoded; i++) { + swizzler->next(srcRows[i]); + } + + // If we cannot read enough rows, assume the input is incomplete + if (rowsDecoded < rowsPerDecode && y + rowsDecoded < dstHeight) { + // Fill the remainder of the image with black. This error handling + // behavior is unspecified but SkCodec consistently uses black as + // the fill color for opaque images. If the destination is kGray, + // the low 8 bits of SK_ColorBLACK will be used. Conveniently, + // these are zeros, which is the representation for black in kGray. + SkSwizzler::Fill(swizzler->getDstRow(), dstInfo, dstRowBytes, + dstHeight - y - rowsDecoded, SK_ColorBLACK, NULL); + + // Prevent libjpeg from failing on incomplete decode + dinfo->output_scanline = dstHeight; + + // Finish the decode and indicate that the input was incomplete. + jpeg_finish_decompress(dinfo); + return fDecoderMgr->returnFailure("Incomplete image data", kIncompleteInput); + } + } + jpeg_finish_decompress(dinfo); + + return kSuccess; +} diff --git a/src/codec/SkJpegCodec.h b/src/codec/SkJpegCodec.h new file mode 100644 index 0000000..51a741a --- /dev/null +++ b/src/codec/SkJpegCodec.h @@ -0,0 +1,99 @@ +/* + * Copyright 2015 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkJpegCodec_DEFINED +#define SkJpegCodec_DEFINED + +#include "SkCodec.h" +#include "SkImageInfo.h" +#include "SkJpegDecoderMgr.h" +#include "SkJpegUtility.h" +#include "SkStream.h" + +extern "C" { + #include "jpeglib.h" +} + +/* + * + * This class implements the decoding for jpeg images + * + */ +class SkJpegCodec : public SkCodec { +public: + + /* + * Checks the start of the stream to see if the image is a jpeg + * Does not take ownership of the stream + */ + static bool IsJpeg(SkStream*); + + /* + * Assumes IsJpeg was called and returned true + * Creates a jpeg decoder + * Takes ownership of the stream + */ + static SkCodec* NewFromStream(SkStream*); + +protected: + + /* + * Recommend a set of destination dimensions given a requested scale + */ + SkISize onGetScaledDimensions(float desiredScale) const override; + + /* + * Initiates the jpeg decode + */ + Result onGetPixels(const SkImageInfo& dstInfo, void* dst, size_t dstRowBytes, const Options&, + SkPMColor*, int*) override; + + SkEncodedFormat onGetEncodedFormat() const override { + return kJPEG_SkEncodedFormat; + } + +private: + + /* + * Read enough of the stream to initialize the SkJpegCodec. + * Returns a bool representing success or failure. + * + * @param codecOut + * If this returns true, and codecOut was not NULL, + * codecOut will be set to a new SkJpegCodec. + * + * @param decoderMgrOut + * If this returns true, and codecOut was NULL, + * decoderMgrOut must be non-NULL and decoderMgrOut will be set to a new + * JpegDecoderMgr pointer. + * + * @param stream + * Deleted on failure. + * codecOut will take ownership of it in the case where we created a codec. + * Ownership is unchanged when we set decoderMgrOut. + * + */ + static bool ReadHeader(SkStream* stream, SkCodec** codecOut, + JpegDecoderMgr** decoderMgrOut); + + /* + * Creates an instance of the decoder + * Called only by NewFromStream + * + * @param srcInfo contains the source width and height + * @param stream the encoded image data + * @param decoderMgr holds decompress struct, src manager, and error manager + * takes ownership + */ + SkJpegCodec(const SkImageInfo& srcInfo, SkStream* stream, JpegDecoderMgr* decoderMgr); + + SkAutoTDelete fDecoderMgr; + + typedef SkCodec INHERITED; +}; + +#endif diff --git a/src/codec/SkJpegDecoderMgr.cpp b/src/codec/SkJpegDecoderMgr.cpp new file mode 100644 index 0000000..c1f044c --- /dev/null +++ b/src/codec/SkJpegDecoderMgr.cpp @@ -0,0 +1,109 @@ +/* + * Copyright 2015 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include "SkJpegDecoderMgr.h" +#include "SkJpegUtility.h" + +/* + * Print information, warning, and error messages + */ +static void print_message(const j_common_ptr info, const char caller[]) { + char buffer[JMSG_LENGTH_MAX]; + info->err->format_message(info, buffer); + SkCodecPrintf("libjpeg error %d <%s> from %s\n", info->err->msg_code, buffer, caller); +} + +/* + * Reporting functions for libjpeg + */ +static void emit_message(j_common_ptr info, int) { + print_message(info, "emit_message"); +} +static void output_message(j_common_ptr info) { + print_message(info, "output_message"); +} + +/* + * Choose the size of the memory buffer on Android + */ +static void overwrite_mem_buffer_size(jpeg_decompress_struct* dinfo) { +#ifdef SK_BUILD_FOR_ANDROID + +// Use 30 MB for devices with a large amount of system memory and 5MB otherwise +// TODO: (msarett) This matches SkImageDecoder. Why were these values chosen? +#ifdef ANDROID_LARGE_MEMORY_DEVICE + dinfo->mem->max_memory_to_use = 30 * 1024 * 1024; +#else + dinfo->mem->max_memory_to_use = 5 * 1024 * 1024; +#endif + +#endif // SK_BUILD_FOR_ANDROID +} + +bool JpegDecoderMgr::returnFalse(const char caller[]) { + print_message((j_common_ptr) &fDInfo, caller); + return false; +} + +SkCodec::Result JpegDecoderMgr::returnFailure(const char caller[], SkCodec::Result result) { + print_message((j_common_ptr) &fDInfo, caller); + return result; +} + +SkColorType JpegDecoderMgr::getColorType() { + switch (fDInfo.jpeg_color_space) { + case JCS_CMYK: + case JCS_YCCK: + // libjpeg cannot convert from CMYK or YCCK to RGB. + // Here, we ask libjpeg to give us CMYK samples back and + // we will later manually convert them to RGB. + fDInfo.out_color_space = JCS_CMYK; + return kN32_SkColorType; + case JCS_GRAYSCALE: + fDInfo.out_color_space = JCS_GRAYSCALE; + return kGray_8_SkColorType; + default: +#ifdef ANDROID_RGB + fDInfo.out_color_space = JCS_RGBA_8888; +#else + fDInfo.out_color_space = JCS_RGB; +#endif + return kN32_SkColorType; + } +} + +JpegDecoderMgr::JpegDecoderMgr(SkStream* stream) + : fSrcMgr(stream) + , fInit(false) +{ + // Error manager must be set before any calls to libjeg in order to handle failures + fDInfo.err = jpeg_std_error(&fErrorMgr); + fErrorMgr.error_exit = skjpeg_err_exit; +} + +void JpegDecoderMgr::init() { + jpeg_create_decompress(&fDInfo); + fInit = true; + fDInfo.src = &fSrcMgr; + overwrite_mem_buffer_size(&fDInfo); + fDInfo.err->emit_message = &emit_message; + fDInfo.err->output_message = &output_message; +} + +JpegDecoderMgr::~JpegDecoderMgr() { + if (fInit) { + jpeg_destroy_decompress(&fDInfo); + } +} + +jmp_buf& JpegDecoderMgr::getJmpBuf() { + return fErrorMgr.fJmpBuf; +} + +jpeg_decompress_struct* JpegDecoderMgr::dinfo() { + return &fDInfo; +} diff --git a/src/codec/SkJpegDecoderMgr.h b/src/codec/SkJpegDecoderMgr.h new file mode 100644 index 0000000..444e693 --- /dev/null +++ b/src/codec/SkJpegDecoderMgr.h @@ -0,0 +1,77 @@ +/* + * Copyright 2015 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkJpegDecoderMgr_DEFINED +#define SkJpegDecoderMgr_DEFINED + +#include "SkCodec.h" +#include "SkCodecPriv.h" +#include "SkJpegUtility.h" +#include "SkSwizzler.h" +#include "SkTemplates.h" + +// stdio is needed for jpeglib +#include + +extern "C" { + #include "jpeglib.h" +} + +class JpegDecoderMgr : SkNoncopyable { +public: + + /* + * Print a useful error message and return false + */ + bool returnFalse(const char caller[]); + + /* + * Print a useful error message and return a decode failure + */ + SkCodec::Result returnFailure(const char caller[], SkCodec::Result result); + + /* + * Create the decode manager + * Does not take ownership of stream + */ + JpegDecoderMgr(SkStream* stream); + + /* + * Initialize decompress struct + * Initialize the source manager + */ + void init(); + + /* + * Recommend a color type based on the encoded format + */ + SkColorType getColorType(); + + /* + * Free memory used by the decode manager + */ + ~JpegDecoderMgr(); + + /* + * Get the jump buffer in order to set an error return point + */ + jmp_buf& getJmpBuf(); + + /* + * Get function for the decompress info struct + */ + jpeg_decompress_struct* dinfo(); + +private: + + jpeg_decompress_struct fDInfo; + skjpeg_source_mgr fSrcMgr; + skjpeg_error_mgr fErrorMgr; + bool fInit; +}; + +#endif diff --git a/src/codec/SkJpegUtility.cpp b/src/codec/SkJpegUtility.cpp new file mode 100644 index 0000000..5dccf94 --- /dev/null +++ b/src/codec/SkJpegUtility.cpp @@ -0,0 +1,89 @@ +/* + * Copyright 2015 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include "SkCodecPriv.h" +#include "SkJpegUtility.h" + +/* + * Initialize the source manager + */ +static void sk_init_source(j_decompress_ptr dinfo) { + skjpeg_source_mgr* src = (skjpeg_source_mgr*) dinfo->src; + src->next_input_byte = (const JOCTET*) src->fBuffer; + src->bytes_in_buffer = 0; +} + +/* + * Fill the input buffer from the stream + */ +static boolean sk_fill_input_buffer(j_decompress_ptr dinfo) { + skjpeg_source_mgr* src = (skjpeg_source_mgr*) dinfo->src; + size_t bytes = src->fStream->read(src->fBuffer, skjpeg_source_mgr::kBufferSize); + + // libjpeg is still happy with a less than full read, as long as the result is non-zero + if (bytes == 0) { + return false; + } + + src->next_input_byte = (const JOCTET*) src->fBuffer; + src->bytes_in_buffer = bytes; + return true; +} + +/* + * Skip a certain number of bytes in the stream + */ +static void sk_skip_input_data(j_decompress_ptr dinfo, long numBytes) { + skjpeg_source_mgr* src = (skjpeg_source_mgr*) dinfo->src; + size_t bytes = (size_t) numBytes; + + if (bytes > src->bytes_in_buffer) { + size_t bytesToSkip = bytes - src->bytes_in_buffer; + if (bytesToSkip != src->fStream->skip(bytesToSkip)) { + SkCodecPrintf("Failure to skip.\n"); + dinfo->err->error_exit((j_common_ptr) dinfo); + return; + } + + src->next_input_byte = (const JOCTET*) src->fBuffer; + src->bytes_in_buffer = 0; + } else { + src->next_input_byte += numBytes; + src->bytes_in_buffer -= numBytes; + } +} + +/* + * We do not need to do anything to terminate our stream + */ +static void sk_term_source(j_decompress_ptr dinfo) +{} + +/* + * Constructor for the source manager that we provide to libjpeg + * We provide skia implementations of all of the stream processing functions required by libjpeg + */ +skjpeg_source_mgr::skjpeg_source_mgr(SkStream* stream) + : fStream(stream) +{ + init_source = sk_init_source; + fill_input_buffer = sk_fill_input_buffer; + skip_input_data = sk_skip_input_data; + resync_to_restart = jpeg_resync_to_restart; + term_source = sk_term_source; +} + +/* + * Call longjmp to continue execution on an error + */ +void skjpeg_err_exit(j_common_ptr dinfo) { + // Simply return to Skia client code + // JpegDecoderMgr will take care of freeing memory + skjpeg_error_mgr* error = (skjpeg_error_mgr*) dinfo->err; + (*error->output_message) (dinfo); + longjmp(error->fJmpBuf, 1); +} diff --git a/src/codec/SkJpegUtility.h b/src/codec/SkJpegUtility.h new file mode 100644 index 0000000..42cd7af --- /dev/null +++ b/src/codec/SkJpegUtility.h @@ -0,0 +1,50 @@ +/* + * Copyright 2015 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + + +#ifndef SkJpegUtility_DEFINED +#define SkJpegUtility_DEFINED + +#include "SkStream.h" + +#include +// stdio is needed for jpeglib +#include + +extern "C" { + #include "jpeglib.h" + #include "jerror.h" +} + +/* + * Error handling struct + */ +struct skjpeg_error_mgr : jpeg_error_mgr { + jmp_buf fJmpBuf; +}; + +/* + * Error handling function + */ +void skjpeg_err_exit(j_common_ptr cinfo); + +/* + * Source handling struct for that allows libjpeg to use our stream object + */ +struct skjpeg_source_mgr : jpeg_source_mgr { + skjpeg_source_mgr(SkStream* stream); + + SkStream* fStream; // unowned + enum { + // TODO (msarett): Experiment with different buffer sizes. + // This size was chosen because it matches SkImageDecoder. + kBufferSize = 1024 + }; + uint8_t fBuffer[kBufferSize]; +}; + +#endif diff --git a/src/codec/SkSwizzler.cpp b/src/codec/SkSwizzler.cpp index 7913633..294229c 100644 --- a/src/codec/SkSwizzler.cpp +++ b/src/codec/SkSwizzler.cpp @@ -68,6 +68,8 @@ static SkSwizzler::ResultAlpha swizzle_small_index_to_n32( return COMPUTE_RESULT_ALPHA; } +// kIndex + static SkSwizzler::ResultAlpha swizzle_index_to_index( void* SK_RESTRICT dstRow, const uint8_t* SK_RESTRICT src, int width, int bytesPerPixel, int y, const SkPMColor ctable[]) { @@ -84,8 +86,6 @@ static SkSwizzler::ResultAlpha swizzle_index_to_index( return COMPUTE_RESULT_ALPHA; } -// kIndex - static SkSwizzler::ResultAlpha swizzle_index_to_n32( void* SK_RESTRICT dstRow, const uint8_t* SK_RESTRICT src, int width, int bytesPerPixel, int y, const SkPMColor ctable[]) { @@ -118,6 +118,28 @@ static SkSwizzler::ResultAlpha swizzle_index_to_n32_skipZ( #undef A32_MASK_IN_PLACE +// kGray + +static SkSwizzler::ResultAlpha swizzle_gray_to_n32( + void* SK_RESTRICT dstRow, const uint8_t* SK_RESTRICT src, int width, + int bytesPerPixel, int y, const SkPMColor ctable[]) { + + SkPMColor* SK_RESTRICT dst = (SkPMColor*)dstRow; + for (int x = 0; x < width; x++) { + dst[x] = SkPackARGB32NoCheck(0xFF, src[x], src[x], src[x]); + } + return SkSwizzler::kOpaque_ResultAlpha; +} + +static SkSwizzler::ResultAlpha swizzle_gray_to_gray( + void* SK_RESTRICT dstRow, const uint8_t* SK_RESTRICT src, int width, + int bytesPerPixel, int y, const SkPMColor ctable[]) { + memcpy(dstRow, src, width); + return SkSwizzler::kOpaque_ResultAlpha; +} + +// kBGRX + static SkSwizzler::ResultAlpha swizzle_bgrx_to_n32( void* SK_RESTRICT dstRow, const uint8_t* SK_RESTRICT src, int width, int bytesPerPixel, int y, const SkPMColor ctable[]) { @@ -299,6 +321,17 @@ SkSwizzler* SkSwizzler::CreateSwizzler(SkSwizzler::SrcConfig sc, break; } break; + case kGray: + switch (info.colorType()) { + case kN32_SkColorType: + proc = &swizzle_gray_to_n32; + break; + case kGray_8_SkColorType: + proc = &swizzle_gray_to_gray; + default: + break; + } + break; case kBGR: case kBGRX: switch (info.colorType()) { @@ -464,6 +497,15 @@ void SkSwizzler::Fill(void* dstStartRow, const SkImageInfo& dstInfo, size_t dstR SkASSERT(colorOrIndex == (uint8_t) colorOrIndex); memset(dstStartRow, colorOrIndex, bytesToFill); break; + case kGray_8_SkColorType: + // If the destination is kGray, the caller passes in an 8-bit color. + // We will not assert that the high bits of colorOrIndex must be zeroed. + // This allows us to take advantage of the fact that the low 8 bits of an + // SKPMColor may be a valid a grayscale color. For example, the low 8 + // bits of SK_ColorBLACK are identical to the grayscale representation + // for black. + memset(dstStartRow, (uint8_t) colorOrIndex, bytesToFill); + break; default: SkCodecPrintf("Error: Unsupported dst color type for fill(). Doing nothing.\n"); SkASSERT(false); diff --git a/src/codec/SkSwizzler.h b/src/codec/SkSwizzler.h index 8a471e9..e3a38cb 100644 --- a/src/codec/SkSwizzler.h +++ b/src/codec/SkSwizzler.h @@ -155,6 +155,8 @@ public: * the colorTable. i.e. each 4-byte pixel will be set to * colorTable[(uint8_t) colorOrIndex]. * + * If dstInfo.colorType() is kGray, colorOrIndex is always treated as an 8-bit color. + * * Other SkColorTypes are not supported. * */ diff --git a/tests/CodexTest.cpp b/tests/CodexTest.cpp index a771716..d714251 100644 --- a/tests/CodexTest.cpp +++ b/tests/CodexTest.cpp @@ -104,6 +104,13 @@ DEF_TEST(Codec, r) { check(r, "color_wheel.gif", SkISize::Make(128, 128), false); check(r, "randPixels.gif", SkISize::Make(8, 8), false); + // JPG + check(r, "CMYK.jpg", SkISize::Make(642, 516), false); + check(r, "color_wheel.jpg", SkISize::Make(128, 128), false); + check(r, "grayscale.jpg", SkISize::Make(128, 128), false); + check(r, "mandrill_512_q075.jpg", SkISize::Make(512, 512), false); + check(r, "randPixels.jpg", SkISize::Make(8, 8), false); + // PNG check(r, "arrow.png", SkISize::Make(187, 312), true); check(r, "baby_tux.png", SkISize::Make(240, 246), true); @@ -148,3 +155,46 @@ DEF_TEST(Codec_leaks, r) { test_invalid_stream(r, emptyIco, sizeof(emptyIco)); test_invalid_stream(r, emptyGif, sizeof(emptyGif)); } + +static void test_dimensions(skiatest::Reporter* r, const char path[]) { + // Create the codec from the resource file + SkAutoTDelete stream(resource(path)); + if (!stream) { + SkDebugf("Missing resource '%s'\n", path); + return; + } + SkAutoTDelete codec(SkCodec::NewFromStream(stream.detach())); + if (!codec) { + ERRORF(r, "Unable to create codec '%s'", path); + return; + } + + // Check that the decode is successful for a variety of scales + for (float scale = -0.05f; scale < 2.0f; scale += 0.05f) { + // Scale the output dimensions + SkISize scaledDims = codec->getScaledDimensions(scale); + SkImageInfo scaledInfo = codec->getInfo().makeWH(scaledDims.width(), scaledDims.height()); + + // Set up for the decode + size_t rowBytes = scaledDims.width() * sizeof(SkPMColor); + size_t totalBytes = scaledInfo.getSafeSize(rowBytes); + SkAutoTMalloc pixels(totalBytes); + + SkImageGenerator::Result result = + codec->getPixels(scaledInfo, pixels.get(), rowBytes, NULL, NULL, NULL); + REPORTER_ASSERT(r, SkImageGenerator::kSuccess == result); + } +} + +// Ensure that onGetScaledDimensions returns valid image dimensions to use for decodes +DEF_TEST(Codec_Dimensions, r) { + // JPG + test_dimensions(r, "CMYK.jpg"); + test_dimensions(r, "color_wheel.jpg"); + test_dimensions(r, "grayscale.jpg"); + test_dimensions(r, "mandrill_512_q075.jpg"); + test_dimensions(r, "randPixels.jpg"); +} + + + diff --git a/tests/SwizzlerTest.cpp b/tests/SwizzlerTest.cpp index 147dfaa..7ed1c39 100644 --- a/tests/SwizzlerTest.cpp +++ b/tests/SwizzlerTest.cpp @@ -42,13 +42,24 @@ static void check_fill(skiatest::Reporter* r, // Ensure that the pixels are filled properly // The bots should catch any memory corruption uint8_t* indexPtr = imageData + startRow * rowBytes; + uint8_t* grayPtr = indexPtr; uint32_t* colorPtr = (uint32_t*) indexPtr; for (uint32_t y = startRow; y <= endRow; y++) { for (int32_t x = 0; x < imageInfo.width(); x++) { - if (kIndex_8_SkColorType == imageInfo.colorType()) { - REPORTER_ASSERT(r, kFillIndex == indexPtr[x]); - } else { - REPORTER_ASSERT(r, kFillColor == colorPtr[x]); + switch (imageInfo.colorType()) { + case kIndex_8_SkColorType: + REPORTER_ASSERT(r, kFillIndex == indexPtr[x]); + break; + case kN32_SkColorType: + REPORTER_ASSERT(r, kFillColor == colorPtr[x]); + break; + case kGray_8_SkColorType: + // We always fill kGray with black + REPORTER_ASSERT(r, (uint8_t) kFillColor == grayPtr[x]); + break; + default: + REPORTER_ASSERT(r, false); + break; } } indexPtr += rowBytes; @@ -82,6 +93,7 @@ DEF_TEST(SwizzlerFill, r) { const SkImageInfo colorInfo = SkImageInfo::MakeN32(width, height, kUnknown_SkAlphaType); const SkImageInfo indexInfo = colorInfo.makeColorType(kIndex_8_SkColorType); + const SkImageInfo grayInfo = colorInfo.makeColorType(kGray_8_SkColorType); for (uint32_t padding : paddings) { @@ -89,6 +101,7 @@ DEF_TEST(SwizzlerFill, r) { size_t colorRowBytes = SkColorTypeBytesPerPixel(kN32_SkColorType) * width + padding; size_t indexRowBytes = width + padding; + size_t grayRowBytes = indexRowBytes; // If there is padding, we can invent an offset to change the memory alignment for (uint32_t offset = 0; offset <= padding; offset++) { @@ -108,6 +121,10 @@ DEF_TEST(SwizzlerFill, r) { // Fill with an index check_fill(r, indexInfo, startRow, endRow, indexRowBytes, offset, kFillIndex, NULL); + + // Fill a grayscale image + check_fill(r, grayInfo, startRow, endRow, grayRowBytes, offset, + kFillColor, NULL); } } } -- 2.7.4