/* * Copyright 2014 Google Inc. * * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #include "SkColorPriv.h" #include "SkImageDecoder.h" #include "SkPixelRef.h" #include "SkScaledBitmapSampler.h" #include "SkStream.h" #include "SkStreamPriv.h" #include "SkTypes.h" #include "ktx.h" #include "etc1.h" ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // KTX Image decoder // --- // KTX is a general texture data storage file format ratified by the Khronos Group. As an // overview, a KTX file contains all of the appropriate values needed to fully specify a // texture in an OpenGL application, including the use of compressed data. // // This decoder is meant to be used with an SkDiscardablePixelRef so that GPU backends // can sniff the data before creating a texture. If they encounter a compressed format // that they understand, they can then upload the data directly to the GPU. Otherwise, // they will decode the data into a format that Skia supports. class SkKTXImageDecoder : public SkImageDecoder { public: SkKTXImageDecoder() { } virtual Format getFormat() const SK_OVERRIDE { return kKTX_Format; } protected: virtual bool onDecode(SkStream* stream, SkBitmap* bm, Mode) SK_OVERRIDE; private: typedef SkImageDecoder INHERITED; }; bool SkKTXImageDecoder::onDecode(SkStream* stream, SkBitmap* bm, Mode mode) { // TODO: Implement SkStream::copyToData() that's cheap for memory and file streams SkAutoDataUnref data(SkCopyStreamToData(stream)); if (NULL == data) { return false; } SkKTXFile ktxFile(data); if (!ktxFile.valid()) { return false; } const unsigned short width = ktxFile.width(); const unsigned short height = ktxFile.height(); #ifdef SK_SUPPORT_LEGACY_IMAGEDECODER_CHOOSER // should we allow the Chooser (if present) to pick a config for us??? if (!this->chooseFromOneChoice(kN32_SkColorType, width, height)) { return false; } #endif // Set a flag if our source is premultiplied alpha const SkString premulKey("KTXPremultipliedAlpha"); const bool bSrcIsPremul = ktxFile.getValueForKey(premulKey) == SkString("True"); // Setup the sampler... SkScaledBitmapSampler sampler(width, height, this->getSampleSize()); // Determine the alpha of the bitmap... SkAlphaType alphaType = kOpaque_SkAlphaType; if (ktxFile.isRGBA8()) { if (this->getRequireUnpremultipliedColors()) { alphaType = kUnpremul_SkAlphaType; // If the client wants unpremul colors and we only have // premul, then we cannot honor their wish. if (bSrcIsPremul) { return false; } } else { alphaType = kPremul_SkAlphaType; } } // Search through the compressed formats to see if the KTX file is holding // compressed data bool ktxIsCompressed = false; SkTextureCompressor::Format ktxCompressedFormat; for (int i = 0; i < SkTextureCompressor::kFormatCnt; ++i) { SkTextureCompressor::Format fmt = static_cast(i); if (ktxFile.isCompressedFormat(fmt)) { ktxIsCompressed = true; ktxCompressedFormat = fmt; break; } } // If the compressed format is a grayscale image, then setup the bitmap properly... bool isCompressedAlpha = ktxIsCompressed && ((SkTextureCompressor::kLATC_Format == ktxCompressedFormat) || (SkTextureCompressor::kR11_EAC_Format == ktxCompressedFormat)); // Set the image dimensions and underlying pixel type. if (isCompressedAlpha) { const int w = sampler.scaledWidth(); const int h = sampler.scaledHeight(); bm->setInfo(SkImageInfo::MakeA8(w, h)); } else { const int w = sampler.scaledWidth(); const int h = sampler.scaledHeight(); bm->setInfo(SkImageInfo::MakeN32(w, h, alphaType)); } if (SkImageDecoder::kDecodeBounds_Mode == mode) { return true; } // If we've made it this far, then we know how to grok the data. if (!this->allocPixelRef(bm, NULL)) { return false; } // Lock the pixels, since we're about to write to them... SkAutoLockPixels alp(*bm); if (isCompressedAlpha) { if (!sampler.begin(bm, SkScaledBitmapSampler::kGray, *this)) { return false; } // Alpha data is only a single byte per pixel. int nPixels = width * height; SkAutoMalloc outRGBData(nPixels); uint8_t *outRGBDataPtr = reinterpret_cast(outRGBData.get()); // Decode the compressed format const uint8_t *buf = reinterpret_cast(ktxFile.pixelData()); if (!SkTextureCompressor::DecompressBufferFromFormat( outRGBDataPtr, width, buf, width, height, ktxCompressedFormat)) { return false; } // Set each of the pixels... const int srcRowBytes = width; const int dstHeight = sampler.scaledHeight(); const uint8_t *srcRow = reinterpret_cast(outRGBDataPtr); srcRow += sampler.srcY0() * srcRowBytes; for (int y = 0; y < dstHeight; ++y) { sampler.next(srcRow); srcRow += sampler.srcDY() * srcRowBytes; } return true; } else if (ktxFile.isCompressedFormat(SkTextureCompressor::kETC1_Format)) { if (!sampler.begin(bm, SkScaledBitmapSampler::kRGB, *this)) { return false; } // ETC1 Data is encoded as RGB pixels, so we should extract it as such int nPixels = width * height; SkAutoMalloc outRGBData(nPixels * 3); uint8_t *outRGBDataPtr = reinterpret_cast(outRGBData.get()); // Decode ETC1 const uint8_t *buf = reinterpret_cast(ktxFile.pixelData()); if (!SkTextureCompressor::DecompressBufferFromFormat( outRGBDataPtr, width*3, buf, width, height, SkTextureCompressor::kETC1_Format)) { return false; } // Set each of the pixels... const int srcRowBytes = width * 3; const int dstHeight = sampler.scaledHeight(); const uint8_t *srcRow = reinterpret_cast(outRGBDataPtr); srcRow += sampler.srcY0() * srcRowBytes; for (int y = 0; y < dstHeight; ++y) { sampler.next(srcRow); srcRow += sampler.srcDY() * srcRowBytes; } return true; } else if (ktxFile.isRGB8()) { // Uncompressed RGB data (without alpha) if (!sampler.begin(bm, SkScaledBitmapSampler::kRGB, *this)) { return false; } // Just need to read RGB pixels const int srcRowBytes = width * 3; const int dstHeight = sampler.scaledHeight(); const uint8_t *srcRow = reinterpret_cast(ktxFile.pixelData()); srcRow += sampler.srcY0() * srcRowBytes; for (int y = 0; y < dstHeight; ++y) { sampler.next(srcRow); srcRow += sampler.srcDY() * srcRowBytes; } return true; } else if (ktxFile.isRGBA8()) { // Uncompressed RGBA data // If we know that the image contains premultiplied alpha, then // we need to turn off the premultiplier SkScaledBitmapSampler::Options opts (*this); if (bSrcIsPremul) { SkASSERT(bm->alphaType() == kPremul_SkAlphaType); SkASSERT(!this->getRequireUnpremultipliedColors()); opts.fPremultiplyAlpha = false; } if (!sampler.begin(bm, SkScaledBitmapSampler::kRGBA, opts)) { return false; } // Just need to read RGBA pixels const int srcRowBytes = width * 4; const int dstHeight = sampler.scaledHeight(); const uint8_t *srcRow = reinterpret_cast(ktxFile.pixelData()); srcRow += sampler.srcY0() * srcRowBytes; for (int y = 0; y < dstHeight; ++y) { sampler.next(srcRow); srcRow += sampler.srcDY() * srcRowBytes; } return true; } return false; } /////////////////////////////////////////////////////////////////////////////// // KTX Image Encoder // // This encoder takes a best guess at how to encode the bitmap passed to it. If // there is an installed discardable pixel ref with existing PKM data, then we // will repurpose the existing ETC1 data into a KTX file. If the data contains // KTX data, then we simply return a copy of the same data. For all other files, // the underlying KTX library tries to do its best to encode the appropriate // data specified by the bitmap based on the config. (i.e. kAlpha8_Config will // be represented as a full resolution 8-bit image dump with the appropriate // OpenGL defines in the header). class SkKTXImageEncoder : public SkImageEncoder { protected: virtual bool onEncode(SkWStream* stream, const SkBitmap& bm, int quality) SK_OVERRIDE; private: virtual bool encodePKM(SkWStream* stream, const SkData *data); typedef SkImageEncoder INHERITED; }; bool SkKTXImageEncoder::onEncode(SkWStream* stream, const SkBitmap& bitmap, int) { if (!bitmap.pixelRef()) { return false; } SkAutoDataUnref data(bitmap.pixelRef()->refEncodedData()); // Is this even encoded data? if (NULL != data) { const uint8_t *bytes = data->bytes(); if (etc1_pkm_is_valid(bytes)) { return this->encodePKM(stream, data); } // Is it a KTX file?? if (SkKTXFile::is_ktx(bytes)) { return stream->write(bytes, data->size()); } // If it's neither a KTX nor a PKM, then we need to // get at the actual pixels, so fall through and decompress... } return SkKTXFile::WriteBitmapToKTX(stream, bitmap); } bool SkKTXImageEncoder::encodePKM(SkWStream* stream, const SkData *data) { const uint8_t* bytes = data->bytes(); SkASSERT(etc1_pkm_is_valid(bytes)); etc1_uint32 width = etc1_pkm_get_width(bytes); etc1_uint32 height = etc1_pkm_get_height(bytes); // ETC1 Data is stored as compressed 4x4 pixel blocks, so we must make sure // that our dimensions are valid. if (width == 0 || (width & 3) != 0 || height == 0 || (height & 3) != 0) { return false; } // Advance pointer to etc1 data. bytes += ETC_PKM_HEADER_SIZE; return SkKTXFile::WriteETC1ToKTX(stream, bytes, width, height); } ///////////////////////////////////////////////////////////////////////////////////////// DEFINE_DECODER_CREATOR(KTXImageDecoder); DEFINE_ENCODER_CREATOR(KTXImageEncoder); ///////////////////////////////////////////////////////////////////////////////////////// static SkImageDecoder* sk_libktx_dfactory(SkStreamRewindable* stream) { if (SkKTXFile::is_ktx(stream)) { return SkNEW(SkKTXImageDecoder); } return NULL; } static SkImageDecoder::Format get_format_ktx(SkStreamRewindable* stream) { if (SkKTXFile::is_ktx(stream)) { return SkImageDecoder::kKTX_Format; } return SkImageDecoder::kUnknown_Format; } SkImageEncoder* sk_libktx_efactory(SkImageEncoder::Type t) { return (SkImageEncoder::kKTX_Type == t) ? SkNEW(SkKTXImageEncoder) : NULL; } static SkImageDecoder_DecodeReg gReg(sk_libktx_dfactory); static SkImageDecoder_FormatReg gFormatReg(get_format_ktx); static SkImageEncoder_EncodeReg gEReg(sk_libktx_efactory);