diff --git a/gyp/ktx.gyp b/gyp/ktx.gyp index 2eaa941b0d..141eba1fd1 100644 --- a/gyp/ktx.gyp +++ b/gyp/ktx.gyp @@ -14,7 +14,8 @@ '../third_party/ktx/ktx.cpp', ], 'dependencies': [ - 'core.gyp:*' + 'core.gyp:*', + 'etc1.gyp:libetc1', ], 'direct_dependent_settings': { 'include_dirs': [ diff --git a/gyp/tests.gypi b/gyp/tests.gypi index 46d40fc640..7a32da29e1 100644 --- a/gyp/tests.gypi +++ b/gyp/tests.gypi @@ -104,6 +104,7 @@ '../tests/ImageFilterTest.cpp', '../tests/InfRectTest.cpp', '../tests/JpegTest.cpp', + '../tests/KtxTest.cpp', '../tests/LListTest.cpp', '../tests/LayerDrawLooperTest.cpp', '../tests/LayerRasterizerTest.cpp', diff --git a/include/core/SkImageEncoder.h b/include/core/SkImageEncoder.h index 5622eee7e0..754d5bbbc3 100644 --- a/include/core/SkImageEncoder.h +++ b/include/core/SkImageEncoder.h @@ -26,6 +26,7 @@ public: kPNG_Type, kWBMP_Type, kWEBP_Type, + kKTX_Type, }; static SkImageEncoder* Create(Type); @@ -96,6 +97,7 @@ protected: DECLARE_ENCODER_CREATOR(ARGBImageEncoder); DECLARE_ENCODER_CREATOR(JPEGImageEncoder); DECLARE_ENCODER_CREATOR(PNGImageEncoder); +DECLARE_ENCODER_CREATOR(KTXImageEncoder); DECLARE_ENCODER_CREATOR(WEBPImageEncoder); // Typedef to make registering encoder callback easier diff --git a/src/images/SkImageDecoder_ktx.cpp b/src/images/SkImageDecoder_ktx.cpp index bf3445e985..6ff2459540 100644 --- a/src/images/SkImageDecoder_ktx.cpp +++ b/src/images/SkImageDecoder_ktx.cpp @@ -7,6 +7,7 @@ #include "SkColorPriv.h" #include "SkImageDecoder.h" +#include "SkPixelRef.h" #include "SkScaledBitmapSampler.h" #include "SkStream.h" #include "SkStreamHelpers.h" @@ -73,7 +74,7 @@ bool SkKTXImageDecoder::onDecode(SkStream* stream, SkBitmap* bm, Mode mode) { bm->setConfig(SkBitmap::kARGB_8888_Config, sampler.scaledWidth(), sampler.scaledHeight(), 0, - ktxFile.isRGBA8()? kUnpremul_SkAlphaType : kOpaque_SkAlphaType); + ktxFile.isRGBA8()? kPremul_SkAlphaType : kOpaque_SkAlphaType); if (SkImageDecoder::kDecodeBounds_Mode == mode) { return true; } @@ -135,6 +136,15 @@ bool SkKTXImageDecoder::onDecode(SkStream* stream, SkBitmap* bm, Mode mode) { } else if (ktxFile.isRGBA8()) { + // If we know that the image contains premultiplied alpha, then + // don't premultiply it upon decoding. + bool setRequireUnpremul = false; + const SkString premulKey("KTXPremultipliedAlpha"); + if (ktxFile.getValueForKey(premulKey) == SkString("True")) { + this->setRequireUnpremultipliedColors(true); + setRequireUnpremul = true; + } + // Uncompressed RGBA data if (!sampler.begin(bm, SkScaledBitmapSampler::kRGBA, *this)) { return false; @@ -150,14 +160,83 @@ bool SkKTXImageDecoder::onDecode(SkStream* stream, SkBitmap* bm, Mode mode) { srcRow += sampler.srcDY() * srcRowBytes; } + // Reset this in case the decoder needs to be used again. + if (setRequireUnpremul) { + this->setRequireUnpremultipliedColors(false); + } + 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) { + 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) { @@ -167,8 +246,6 @@ static SkImageDecoder* sk_libktx_dfactory(SkStreamRewindable* stream) { return NULL; } -static SkImageDecoder_DecodeReg gReg(sk_libktx_dfactory); - static SkImageDecoder::Format get_format_ktx(SkStreamRewindable* stream) { if (SkKTXFile::is_ktx(stream)) { return SkImageDecoder::kKTX_Format; @@ -176,4 +253,10 @@ static SkImageDecoder::Format get_format_ktx(SkStreamRewindable* stream) { 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); diff --git a/tests/KtxTest.cpp b/tests/KtxTest.cpp new file mode 100644 index 0000000000..ee6a17dabd --- /dev/null +++ b/tests/KtxTest.cpp @@ -0,0 +1,168 @@ +/* + * 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 "SkBitmap.h" +#include "SkData.h" +#include "SkDecodingImageGenerator.h" +#include "SkForceLinking.h" +#include "SkImageDecoder.h" +#include "SkOSFile.h" +#include "SkRandom.h" +#include "SkStream.h" +#include "Test.h" + +__SK_FORCE_IMAGE_DECODER_LINKING; + +/** + * First, make sure that writing an 8-bit RGBA KTX file and then + * reading it produces the same bitmap. + */ +DEF_TEST(KtxReadWrite, reporter) { + + // Random number generator with explicit seed for reproducibility + SkRandom rand(0x1005cbad); + + SkBitmap bm8888; + bm8888.setConfig(SkBitmap::kARGB_8888_Config, 128, 128); + + bool pixelsAllocated = bm8888.allocPixels(); + REPORTER_ASSERT(reporter, pixelsAllocated); + + uint8_t *pixels = reinterpret_cast(bm8888.getPixels()); + REPORTER_ASSERT(reporter, NULL != pixels); + + if (NULL == pixels) { + return; + } + + uint8_t *row = pixels; + for (int y = 0; y < bm8888.height(); ++y) { + for (int x = 0; x < bm8888.width(); ++x) { + uint8_t a = rand.nextRangeU(0, 255); + uint8_t r = rand.nextRangeU(0, 255); + uint8_t g = rand.nextRangeU(0, 255); + uint8_t b = rand.nextRangeU(0, 255); + + SkPMColor &pixel = *(reinterpret_cast(row + x*sizeof(SkPMColor))); + pixel = SkPreMultiplyARGB(a, r, g, b); + } + row += bm8888.rowBytes(); + } + REPORTER_ASSERT(reporter, !(bm8888.empty())); + + SkAutoDataUnref encodedData(SkImageEncoder::EncodeData(bm8888, SkImageEncoder::kKTX_Type, 0)); + REPORTER_ASSERT(reporter, NULL != encodedData); + + SkAutoTUnref stream(SkNEW_ARGS(SkMemoryStream, (encodedData))); + REPORTER_ASSERT(reporter, NULL != stream); + + SkBitmap decodedBitmap; + bool imageDecodeSuccess = SkImageDecoder::DecodeStream(stream, &decodedBitmap); + REPORTER_ASSERT(reporter, imageDecodeSuccess); + + REPORTER_ASSERT(reporter, decodedBitmap.config() == bm8888.config()); + REPORTER_ASSERT(reporter, decodedBitmap.alphaType() == bm8888.alphaType()); + REPORTER_ASSERT(reporter, decodedBitmap.width() == bm8888.width()); + REPORTER_ASSERT(reporter, decodedBitmap.height() == bm8888.height()); + REPORTER_ASSERT(reporter, !(decodedBitmap.empty())); + + uint8_t *decodedPixels = reinterpret_cast(decodedBitmap.getPixels()); + REPORTER_ASSERT(reporter, NULL != decodedPixels); + REPORTER_ASSERT(reporter, decodedBitmap.getSize() == bm8888.getSize()); + + if (NULL == decodedPixels) { + return; + } + + REPORTER_ASSERT(reporter, memcmp(decodedPixels, pixels, decodedBitmap.getSize()) == 0); +} + +/** + * Next test is to see whether or not reading an unpremultiplied KTX file accurately + * creates a premultiplied buffer... + */ +DEF_TEST(KtxReadUnpremul, reporter) { + + static const uint8_t kHalfWhiteKTX[] = { + 0xAB, 0x4B, 0x54, 0x58, 0x20, 0x31, // First twelve bytes is magic + 0x31, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A, // KTX identifier string + 0x01, 0x02, 0x03, 0x04, // Then magic endian specifier + 0x01, 0x14, 0x00, 0x00, // uint32_t fGLType; + 0x01, 0x00, 0x00, 0x00, // uint32_t fGLTypeSize; + 0x08, 0x19, 0x00, 0x00, // uint32_t fGLFormat; + 0x58, 0x80, 0x00, 0x00, // uint32_t fGLInternalFormat; + 0x08, 0x19, 0x00, 0x00, // uint32_t fGLBaseInternalFormat; + 0x02, 0x00, 0x00, 0x00, // uint32_t fPixelWidth; + 0x02, 0x00, 0x00, 0x00, // uint32_t fPixelHeight; + 0x00, 0x00, 0x00, 0x00, // uint32_t fPixelDepth; + 0x00, 0x00, 0x00, 0x00, // uint32_t fNumberOfArrayElements; + 0x01, 0x00, 0x00, 0x00, // uint32_t fNumberOfFaces; + 0x01, 0x00, 0x00, 0x00, // uint32_t fNumberOfMipmapLevels; + 0x00, 0x00, 0x00, 0x00, // uint32_t fBytesOfKeyValueData; + 0x10, 0x00, 0x00, 0x00, // image size: 2x2 image of RGBA = 4 * 4 = 16 bytes + 0xFF, 0xFF, 0xFF, 0x80, // Pixel 1 + 0xFF, 0xFF, 0xFF, 0x80, // Pixel 2 + 0xFF, 0xFF, 0xFF, 0x80, // Pixel 3 + 0xFF, 0xFF, 0xFF, 0x80};// Pixel 4 + + SkAutoTUnref stream( + SkNEW_ARGS(SkMemoryStream, (kHalfWhiteKTX, sizeof(kHalfWhiteKTX)))); + REPORTER_ASSERT(reporter, NULL != stream); + + SkBitmap decodedBitmap; + bool imageDecodeSuccess = SkImageDecoder::DecodeStream(stream, &decodedBitmap); + REPORTER_ASSERT(reporter, imageDecodeSuccess); + + REPORTER_ASSERT(reporter, decodedBitmap.config() == SkBitmap::kARGB_8888_Config); + REPORTER_ASSERT(reporter, decodedBitmap.alphaType() == kPremul_SkAlphaType); + REPORTER_ASSERT(reporter, decodedBitmap.width() == 2); + REPORTER_ASSERT(reporter, decodedBitmap.height() == 2); + REPORTER_ASSERT(reporter, !(decodedBitmap.empty())); + + uint8_t *decodedPixels = reinterpret_cast(decodedBitmap.getPixels()); + REPORTER_ASSERT(reporter, NULL != decodedPixels); + + uint8_t *row = decodedPixels; + for (int j = 0; j < decodedBitmap.height(); ++j) { + for (int i = 0; i < decodedBitmap.width(); ++i) { + SkPMColor pixel = *(reinterpret_cast(row + i*sizeof(SkPMColor))); + REPORTER_ASSERT(reporter, SkPreMultiplyARGB(0x80, 0xFF, 0xFF, 0xFF) == pixel); + } + row += decodedBitmap.rowBytes(); + } +} + +/** + * Finally, make sure that if we get ETC1 data from a PKM file that we can then + * accurately write it out into a KTX file (i.e. transferring the ETC1 data from + * the PKM to the KTX should produce an identical KTX to the one we have on file) + */ +DEF_TEST(KtxReexportPKM, reporter) { + SkString resourcePath = skiatest::Test::GetResourcePath(); + SkString filename = SkOSPath::SkPathJoin(resourcePath.c_str(), "mandrill_128.pkm"); + + // Load PKM file into a bitmap + SkBitmap etcBitmap; + SkAutoTUnref fileData(SkData::NewFromFileName(filename.c_str())); + REPORTER_ASSERT(reporter, NULL != fileData); + + bool installDiscardablePixelRefSuccess = + SkInstallDiscardablePixelRef( + SkDecodingImageGenerator::Create( + fileData, SkDecodingImageGenerator::Options()), &etcBitmap); + REPORTER_ASSERT(reporter, installDiscardablePixelRefSuccess); + + // Write the bitmap out to a KTX file. + SkData *ktxDataPtr = SkImageEncoder::EncodeData(etcBitmap, SkImageEncoder::kKTX_Type, 0); + SkAutoDataUnref newKtxData(ktxDataPtr); + REPORTER_ASSERT(reporter, NULL != ktxDataPtr); + + // See is this data is identical to data in existing ktx file. + SkString ktxFilename = SkOSPath::SkPathJoin(resourcePath.c_str(), "mandrill_128.ktx"); + SkAutoDataUnref oldKtxData(SkData::NewFromFileName(ktxFilename.c_str())); + REPORTER_ASSERT(reporter, oldKtxData->equals(newKtxData)); +} diff --git a/third_party/ktx/ktx.cpp b/third_party/ktx/ktx.cpp index 15c44fcd35..5eaadef3bd 100644 --- a/third_party/ktx/ktx.cpp +++ b/third_party/ktx/ktx.cpp @@ -7,11 +7,14 @@ */ #include "ktx.h" +#include "SkBitmap.h" #include "SkStream.h" #include "SkEndian.h" #include "gl/GrGLDefines.h" +#include "etc1.h" + #define KTX_FILE_IDENTIFIER_SIZE 12 static const uint8_t KTX_FILE_IDENTIFIER[KTX_FILE_IDENTIFIER_SIZE] = { 0xAB, 0x4B, 0x54, 0x58, 0x20, 0x31, 0x31, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A @@ -39,9 +42,11 @@ bool SkKTXFile::KeyValue::readKeyAndValue(const uint8_t* data) { ++value; size_t bytesLeft = this->fDataSz - bytesRead; - this->fKey.set(key, bytesRead); + + // We ignore the null terminator when setting the string value. + this->fKey.set(key, bytesRead - 1); if (bytesLeft > 0) { - this->fValue.set(value, bytesLeft); + this->fValue.set(value, bytesLeft - 1); } else { return false; } @@ -49,6 +54,42 @@ bool SkKTXFile::KeyValue::readKeyAndValue(const uint8_t* data) { return true; } +bool SkKTXFile::KeyValue::writeKeyAndValueForKTX(SkWStream* strm) { + size_t bytesWritten = 0; + if (!strm->write(&(this->fDataSz), 4)) { + return false; + } + + bytesWritten += 4; + + // Here we know that C-strings must end with a null terminating + // character, so when we get a c_str(), it will have as many + // bytes of data as size() returns plus a zero, so we just + // write size() + 1 bytes into the stream. + + size_t keySize = this->fKey.size() + 1; + if (!strm->write(this->fKey.c_str(), keySize)) { + return false; + } + + bytesWritten += keySize; + + size_t valueSize = this->fValue.size() + 1; + if (!strm->write(this->fValue.c_str(), valueSize)) { + return false; + } + + bytesWritten += valueSize; + + size_t bytesWrittenPadFour = (bytesWritten + 3) & ~3; + uint8_t nullBuf[4] = { 0, 0, 0, 0 }; + + size_t padding = bytesWrittenPadFour - bytesWritten; + SkASSERT(padding < 4); + + return strm->write(nullBuf, padding); +} + uint32_t SkKTXFile::readInt(const uint8_t** buf, size_t* bytesLeft) const { SkASSERT(NULL != buf && NULL != bytesLeft); @@ -71,6 +112,17 @@ uint32_t SkKTXFile::readInt(const uint8_t** buf, size_t* bytesLeft) const { return result; } +SkString SkKTXFile::getValueForKey(const SkString& key) const { + const KeyValue *begin = this->fKeyValuePairs.begin(); + const KeyValue *end = this->fKeyValuePairs.end(); + for (const KeyValue *kv = begin; kv != end; ++kv) { + if (kv->key() == key) { + return kv->value(); + } + } + return SkString(); +} + bool SkKTXFile::isETC1() const { return this->valid() && GR_GL_COMPRESSED_RGB8_ETC1 == fHeader.fGLInternalFormat; } @@ -240,3 +292,208 @@ bool SkKTXFile::is_ktx(SkStreamRewindable* stream) { } return is_ktx(buf); } + +SkKTXFile::KeyValue SkKTXFile::CreateKeyValue(const char *cstrKey, const char *cstrValue) { + SkString key(cstrKey); + SkString value(cstrValue); + + // Size of buffer is length of string plus the null terminators... + size_t size = key.size() + 1 + value.size() + 1; + + SkAutoSMalloc<256> buf(size); + uint8_t* kvBuf = reinterpret_cast(buf.get()); + memcpy(kvBuf, key.c_str(), key.size() + 1); + memcpy(kvBuf + key.size() + 1, value.c_str(), value.size() + 1); + + KeyValue kv(size); + SkAssertResult(kv.readKeyAndValue(kvBuf)); + return kv; +} + +bool SkKTXFile::WriteETC1ToKTX(SkWStream* stream, const uint8_t *etc1Data, + uint32_t width, uint32_t height) { + // First thing's first, write out the magic identifier and endianness... + if (!stream->write(KTX_FILE_IDENTIFIER, KTX_FILE_IDENTIFIER_SIZE)) { + return false; + } + + if (!stream->write(&kKTX_ENDIANNESS_CODE, 4)) { + return false; + } + + Header hdr; + hdr.fGLType = 0; + hdr.fGLTypeSize = 1; + hdr.fGLFormat = 0; + hdr.fGLInternalFormat = GR_GL_COMPRESSED_RGB8_ETC1; + hdr.fGLBaseInternalFormat = GR_GL_RGB; + hdr.fPixelWidth = width; + hdr.fPixelHeight = height; + hdr.fNumberOfArrayElements = 0; + hdr.fNumberOfFaces = 1; + hdr.fNumberOfMipmapLevels = 1; + + // !FIXME! The spec suggests that we put KTXOrientation as a + // key value pair in the header, but that means that we'd have to + // pipe through the bitmap's orientation to properly do that. + hdr.fBytesOfKeyValueData = 0; + + // Write the header + if (!stream->write(&hdr, sizeof(hdr))) { + return false; + } + + // Write the size of the image data + etc1_uint32 dataSize = etc1_get_encoded_data_size(width, height); + if (!stream->write(&dataSize, 4)) { + return false; + } + + // Write the actual image data + if (!stream->write(etc1Data, dataSize)) { + return false; + } + + return true; +} + +bool SkKTXFile::WriteBitmapToKTX(SkWStream* stream, const SkBitmap& bitmap) { + const SkBitmap::Config config = bitmap.config(); + SkAutoLockPixels alp(bitmap); + + const int width = bitmap.width(); + const int height = bitmap.width(); + const uint8_t* src = reinterpret_cast(bitmap.getPixels()); + if (NULL == bitmap.getPixels()) { + return false; + } + + // First thing's first, write out the magic identifier and endianness... + if (!stream->write(KTX_FILE_IDENTIFIER, KTX_FILE_IDENTIFIER_SIZE) || + !stream->write(&kKTX_ENDIANNESS_CODE, 4)) { + return false; + } + + // Collect our key/value pairs... + SkTArray kvPairs; + + // Next, write the header based on the bitmap's config. + Header hdr; + switch (config) { + case SkBitmap::kIndex8_Config: + // There is a compressed format for this, but we don't support it yet. + SkDebugf("Writing indexed bitmap to KTX unsupported.\n"); + // VVV fall through VVV + default: + case SkBitmap::kNo_Config: + // Bitmap hasn't been configured. + return false; + + case SkBitmap::kA8_Config: + hdr.fGLType = GR_GL_UNSIGNED_BYTE; + hdr.fGLTypeSize = 1; + hdr.fGLFormat = GR_GL_RED; + hdr.fGLInternalFormat = GR_GL_R8; + hdr.fGLBaseInternalFormat = GR_GL_RED; + break; + + case SkBitmap::kRGB_565_Config: + hdr.fGLType = GR_GL_UNSIGNED_SHORT_5_6_5; + hdr.fGLTypeSize = 2; + hdr.fGLFormat = GR_GL_RGB; + hdr.fGLInternalFormat = GR_GL_RGB; + hdr.fGLBaseInternalFormat = GR_GL_RGB; + break; + + case SkBitmap::kARGB_4444_Config: + hdr.fGLType = GR_GL_UNSIGNED_SHORT_4_4_4_4; + hdr.fGLTypeSize = 2; + hdr.fGLFormat = GR_GL_RGBA; + hdr.fGLInternalFormat = GR_GL_RGBA4; + hdr.fGLBaseInternalFormat = GR_GL_RGBA; + kvPairs.push_back(CreateKeyValue("KTXPremultipliedAlpha", "True")); + break; + + case SkBitmap::kARGB_8888_Config: + hdr.fGLType = GR_GL_UNSIGNED_BYTE; + hdr.fGLTypeSize = 1; + hdr.fGLFormat = GR_GL_RGBA; + hdr.fGLInternalFormat = GR_GL_RGBA8; + hdr.fGLBaseInternalFormat = GR_GL_RGBA; + kvPairs.push_back(CreateKeyValue("KTXPremultipliedAlpha", "True")); + break; + } + + // Everything else in the header is shared. + hdr.fPixelWidth = width; + hdr.fPixelHeight = height; + hdr.fNumberOfArrayElements = 0; + hdr.fNumberOfFaces = 1; + hdr.fNumberOfMipmapLevels = 1; + + // Calculate the key value data size + hdr.fBytesOfKeyValueData = 0; + for (KeyValue *kv = kvPairs.begin(); kv != kvPairs.end(); ++kv) { + // Key value size is the size of the key value data, + // four bytes for saying how big the key value size is + // and then additional bytes for padding to four byte boundary + size_t kvsize = kv->size(); + kvsize += 4; + kvsize = (kvsize + 3) & ~3; + hdr.fBytesOfKeyValueData += kvsize; + } + + // Write the header + if (!stream->write(&hdr, sizeof(hdr))) { + return false; + } + + // Write out each key value pair + for (KeyValue *kv = kvPairs.begin(); kv != kvPairs.end(); ++kv) { + if (!kv->writeKeyAndValueForKTX(stream)) { + return false; + } + } + + // Calculate the size of the data + int bpp = bitmap.bytesPerPixel(); + uint32_t dataSz = bpp * width * height; + + if (0 >= bpp) { + return false; + } + + // Write it into the buffer + if (!stream->write(&dataSz, 4)) { + return false; + } + + // Write the pixel data... + const uint8_t* rowPtr = src; + if (SkBitmap::kARGB_8888_Config == config) { + for (int j = 0; j < height; ++j) { + const uint32_t* pixelsPtr = reinterpret_cast(rowPtr); + for (int i = 0; i < width; ++i) { + uint32_t pixel = pixelsPtr[i]; + uint8_t dstPixel[4]; + dstPixel[0] = pixel >> SK_R32_SHIFT; + dstPixel[1] = pixel >> SK_G32_SHIFT; + dstPixel[2] = pixel >> SK_B32_SHIFT; + dstPixel[3] = pixel >> SK_A32_SHIFT; + if (!stream->write(dstPixel, 4)) { + return false; + } + } + rowPtr += bitmap.rowBytes(); + } + } else { + for (int i = 0; i < height; ++i) { + if (!stream->write(rowPtr, bpp*width)) { + return false; + } + rowPtr += bitmap.rowBytes(); + } + } + + return true; +} diff --git a/third_party/ktx/ktx.h b/third_party/ktx/ktx.h index 0e4ed9b990..2f445a817a 100644 --- a/third_party/ktx/ktx.h +++ b/third_party/ktx/ktx.h @@ -16,7 +16,9 @@ #include "SkString.h" #include "SkRefCnt.h" +class SkBitmap; class SkStreamRewindable; +class SkWStream; // KTX Image File // --- @@ -49,6 +51,11 @@ public: return this->valid() ? fPixelData[mipmap].data() : NULL; } + // If the decoded KTX file has the following key, then it will + // return the associated value. If not found, the empty string + // is returned. + SkString getValueForKey(const SkString& key) const; + int numMipmaps() const { return static_cast(fHeader.fNumberOfMipmapLevels); } bool isETC1() const; @@ -58,6 +65,9 @@ public: static bool is_ktx(const uint8_t *data); static bool is_ktx(SkStreamRewindable* stream); + static bool WriteETC1ToKTX(SkWStream* stream, const uint8_t *etc1Data, + uint32_t width, uint32_t height); + static bool WriteBitmapToKTX(SkWStream* stream, const SkBitmap& bitmap); private: // The blob holding the file data. @@ -88,13 +98,18 @@ private: public: KeyValue(size_t size) : fDataSz(size) { } bool readKeyAndValue(const uint8_t *data); - + size_t size() const { return fDataSz; } + const SkString& key() const { return fKey; } + const SkString& value() const { return fValue; } + bool writeKeyAndValueForKTX(SkWStream* strm); private: const size_t fDataSz; - SkString fKey; - SkString fValue; + SkString fKey; + SkString fValue; }; + static KeyValue CreateKeyValue(const char *key, const char *value); + // The pixel data for a single mipmap level in an image. Based on how // the rest of the data is stored, this may be compressed, a cubemap, etc. // The header will describe the format of this data.