From d191cc9a4ed3a1ca1ef9371256193cb4e368eb69 Mon Sep 17 00:00:00 2001 From: rh101 Date: Fri, 26 Jul 2019 18:10:39 +1000 Subject: [PATCH] Added support for saving non-premultiplied alpha images from RenderTexture. --- cocos/2d/CCRenderTexture.cpp | 51 +++++++++++++-- cocos/2d/CCRenderTexture.h | 26 +++++++- cocos/platform/CCImage.cpp | 27 +++++++- cocos/platform/CCImage.h | 4 +- cocos/platform/ios/CCImage-ios.mm | 9 ++- .../RenderTextureTest/RenderTextureTest.cpp | 64 +++++++++++++++++-- .../RenderTextureTest/RenderTextureTest.h | 5 +- 7 files changed, 169 insertions(+), 17 deletions(-) diff --git a/cocos/2d/CCRenderTexture.cpp b/cocos/2d/CCRenderTexture.cpp index 39c137c5b451..c66d1a3ddc8e 100644 --- a/cocos/2d/CCRenderTexture.cpp +++ b/cocos/2d/CCRenderTexture.cpp @@ -354,6 +354,28 @@ void RenderTexture::visit(Renderer *renderer, const Mat4 &parentTransform, uint3 // setOrderOfArrival(0); } +bool RenderTexture::saveToFileAsNonPMA(const std::string& filename, bool isRGBA, std::function callback) +{ + std::string basename(filename); + std::transform(basename.begin(), basename.end(), basename.begin(), ::tolower); + + if (basename.find(".png") != std::string::npos) + { + return saveToFileAsNonPMA(filename, Image::Format::PNG, isRGBA, callback); + } + else if (basename.find(".jpg") != std::string::npos) + { + if (isRGBA) CCLOG("RGBA is not supported for JPG format."); + return saveToFileAsNonPMA(filename, Image::Format::JPG, false, callback); + } + else + { + CCLOG("Only PNG and JPG format are supported now!"); + } + + return saveToFileAsNonPMA(filename, Image::Format::JPG, false, callback); +} + bool RenderTexture::saveToFile(const std::string& filename, bool isRGBA, std::function callback) { std::string basename(filename); @@ -376,6 +398,23 @@ bool RenderTexture::saveToFile(const std::string& filename, bool isRGBA, std::fu return saveToFile(filename, Image::Format::JPG, false, callback); } +bool RenderTexture::saveToFileAsNonPMA(const std::string& fileName, Image::Format format, bool isRGBA, std::function callback) +{ + CCASSERT(format == Image::Format::JPG || format == Image::Format::PNG, + "the image can only be saved as JPG or PNG format"); + if (isRGBA && format == Image::Format::JPG) CCLOG("RGBA is not supported for JPG format"); + + _saveFileCallback = callback; + + std::string fullpath = FileUtils::getInstance()->getWritablePath() + fileName; + _saveToFileCommand.init(_globalZOrder); + _saveToFileCommand.func = CC_CALLBACK_0(RenderTexture::onSaveToFile, this, fullpath, isRGBA, true); + + Director::getInstance()->getRenderer()->addCommand(&_saveToFileCommand); + return true; +} + + bool RenderTexture::saveToFile(const std::string& fileName, Image::Format format, bool isRGBA, std::function callback) { CCASSERT(format == Image::Format::JPG || format == Image::Format::PNG, @@ -386,17 +425,21 @@ bool RenderTexture::saveToFile(const std::string& fileName, Image::Format format std::string fullpath = FileUtils::getInstance()->getWritablePath() + fileName; _saveToFileCommand.init(_globalZOrder); - _saveToFileCommand.func = CC_CALLBACK_0(RenderTexture::onSaveToFile, this, fullpath, isRGBA); + _saveToFileCommand.func = CC_CALLBACK_0(RenderTexture::onSaveToFile, this, fullpath, isRGBA, false); Director::getInstance()->getRenderer()->addCommand(&_saveToFileCommand); return true; } -void RenderTexture::onSaveToFile(const std::string& filename, bool isRGBA) +void RenderTexture::onSaveToFile(const std::string& filename, bool isRGBA, bool forceNonPMA) { - auto callbackFunc = [&, filename, isRGBA](Image* image){ + auto callbackFunc = [&, filename, isRGBA, forceNonPMA](Image* image){ if (image) { + if (forceNonPMA && image->hasPremultipliedAlpha()) + { + image->reversePremultipliedAlpha(); + } image->saveToFile(filename, !isRGBA); } if(_saveFileCallback) @@ -429,7 +472,7 @@ void RenderTexture::newImage(std::function imageCallback, bool fli Image *image = new (std::nothrow) Image(); auto initCallback = [&, savedBufferWidth, savedBufferHeight, imageCallback](Image* image, const unsigned char* tempData){ - image->initWithRawData(tempData, savedBufferWidth * savedBufferHeight * 4, savedBufferWidth, savedBufferHeight, 8); + image->initWithRawData(tempData, savedBufferWidth * savedBufferHeight * 4, savedBufferWidth, savedBufferHeight, 8, true); imageCallback(image); }; auto callback = std::bind(initCallback, image, std::placeholders::_1); diff --git a/cocos/2d/CCRenderTexture.h b/cocos/2d/CCRenderTexture.h index afa77c9ad48f..c8c888793e1b 100644 --- a/cocos/2d/CCRenderTexture.h +++ b/cocos/2d/CCRenderTexture.h @@ -160,6 +160,16 @@ class CC_DLL RenderTexture : public Node */ void newImage(std::function imageCallback, bool flipImage = true); + /** Saves the texture into a file using JPEG format. The file will be saved in the Documents folder. + * Returns true if the operation is successful. + * + * @param filename The file name. + * @param isRGBA The file is RGBA or not. + * @param callback When the file is save finished,it will callback this function. + * @return Returns true if the operation is successful. + */ + bool saveToFileAsNonPMA(const std::string& filename, bool isRGBA = true, std::function callback = nullptr); + /** Saves the texture into a file using JPEG format. The file will be saved in the Documents folder. * Returns true if the operation is successful. * @@ -170,6 +180,20 @@ class CC_DLL RenderTexture : public Node */ bool saveToFile(const std::string& filename, bool isRGBA = true, std::function callback = nullptr); + /** saves the texture into a file in non-PMA. The format could be JPG or PNG. The file will be saved in the Documents folder. + Returns true if the operation is successful. + * Notes: since v3.x, saveToFile will generate a custom command, which will be called in the following render->render(). + * So if this function is called in a event handler, the actual save file will be called in the next frame. If we switch to a different scene, the game will crash. + * To solve this, add Director::getInstance()->getRenderer()->render(); after this function. + * + * @param filename The file name. + * @param format The image format. + * @param isRGBA The file is RGBA or not. + * @param callback When the file is save finished,it will callback this function. + * @return Returns true if the operation is successful. + */ + bool saveToFileAsNonPMA(const std::string& fileName, Image::Format format, bool isRGBA, std::function callback); + /** saves the texture into a file. The format could be JPG or PNG. The file will be saved in the Documents folder. Returns true if the operation is successful. * Notes: since v3.x, saveToFile will generate a custom command, which will be called in the following render->render(). @@ -322,7 +346,7 @@ class CC_DLL RenderTexture : public Node void onEnd(); void clearColorAttachment(); - void onSaveToFile(const std::string& fileName, bool isRGBA = true); + void onSaveToFile(const std::string& fileName, bool isRGBA = true, bool forceNonPMA = false); bool _keepMatrix = false; Rect _rtTextureRect; diff --git a/cocos/platform/CCImage.cpp b/cocos/platform/CCImage.cpp index 9a7dde6ae840..f613974cf5ab 100644 --- a/cocos/platform/CCImage.cpp +++ b/cocos/platform/CCImage.cpp @@ -1048,7 +1048,7 @@ bool Image::initWithPngData(const unsigned char * data, ssize_t dataLen) { if (PNG_PREMULTIPLIED_ALPHA_ENABLED) { - premultipliedAlpha(); + premultiplyAlpha(); } else { @@ -2341,7 +2341,7 @@ bool Image::saveImageToJPG(const std::string& filePath) #endif // CC_USE_JPEG } -void Image::premultipliedAlpha() +void Image::premultiplyAlpha() { #if CC_ENABLE_PREMULTIPLIED_ALPHA == 0 _hasPremultipliedAlpha = false; @@ -2360,6 +2360,29 @@ void Image::premultipliedAlpha() #endif } +static inline unsigned char clamp(int x) { + return (unsigned char)(x >= 0 ? (x < 255 ? x : 255) : 0); +} + +void Image::reversePremultipliedAlpha() +{ + CCASSERT(_pixelFormat == backend::PixelFormat::RGBA8888, "The pixel format should be RGBA8888!"); + + unsigned int* fourBytes = (unsigned int*)_data; + for (int i = 0; i < _width * _height; i++) + { + unsigned char* p = _data + i * 4; + if (p[3] > 0) + { + fourBytes[i] = clamp(int(std::ceil((p[0] * 255.0f) / p[3]))) | + clamp(int(std::ceil((p[1] * 255.0f) / p[3]))) << 8 | + clamp(int(std::ceil((p[2] * 255.0f) / p[3]))) << 16 | + p[3] << 24; + } + } + + _hasPremultipliedAlpha = false; +} void Image::setPVRImagesHavePremultipliedAlpha(bool haveAlphaPremultiplied) { diff --git a/cocos/platform/CCImage.h b/cocos/platform/CCImage.h index a83bae80e1c3..48a3aaca069e 100644 --- a/cocos/platform/CCImage.h +++ b/cocos/platform/CCImage.h @@ -155,6 +155,8 @@ class CC_DLL Image : public Ref @param isToRGB whether the image is saved as RGB format. */ bool saveToFile(const std::string &filename, bool isToRGB = true); + void premultiplyAlpha(); + void reversePremultipliedAlpha(); protected: bool initWithJpgData(const unsigned char * data, ssize_t dataLen); @@ -173,7 +175,7 @@ class CC_DLL Image : public Ref bool saveImageToPNG(const std::string& filePath, bool isToRGB = true); bool saveImageToJPG(const std::string& filePath); - void premultipliedAlpha(); + protected: /** diff --git a/cocos/platform/ios/CCImage-ios.mm b/cocos/platform/ios/CCImage-ios.mm index 0571dc857e8a..f2dd09a247c0 100644 --- a/cocos/platform/ios/CCImage-ios.mm +++ b/cocos/platform/ios/CCImage-ios.mm @@ -91,7 +91,14 @@ of this software and associated documentation files (the "Software"), to deal CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault; if (saveToPNG && hasAlpha() && (! isToRGB)) { - bitmapInfo |= kCGImageAlphaPremultipliedLast; + if (_hasPremultipliedAlpha) + { + bitmapInfo |= kCGImageAlphaPremultipliedLast; + } + else + { + bitmapInfo |= kCGImageAlphaLast; + } } CGDataProviderRef provider = CGDataProviderCreateWithData(nullptr, pixels, myDataLength, nullptr); CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB(); diff --git a/tests/cpp-tests/Classes/RenderTextureTest/RenderTextureTest.cpp b/tests/cpp-tests/Classes/RenderTextureTest/RenderTextureTest.cpp index 2fedc43f0180..4cbcfa31bff2 100644 --- a/tests/cpp-tests/Classes/RenderTextureTest/RenderTextureTest.cpp +++ b/tests/cpp-tests/Classes/RenderTextureTest/RenderTextureTest.cpp @@ -62,12 +62,15 @@ RenderTextureSave::RenderTextureSave() // Save Image menu MenuItemFont::setFontSize(16); - auto item1 = MenuItemFont::create("Save Image", CC_CALLBACK_1(RenderTextureSave::saveImage, this)); - auto item2 = MenuItemFont::create("Clear", CC_CALLBACK_1(RenderTextureSave::clearImage, this)); - auto menu = Menu::create(item1, item2, nullptr); + auto item1 = MenuItemFont::create("Save Image PMA", CC_CALLBACK_1(RenderTextureSave::saveImageWithPremultipliedAlpha, this)); + auto item2 = MenuItemFont::create("Save Image Non-PMA", CC_CALLBACK_1(RenderTextureSave::saveImageWithNonPremultipliedAlpha, this)); + auto item3 = MenuItemFont::create("Add Image", CC_CALLBACK_1(RenderTextureSave::addImage, this)); + auto item4 = MenuItemFont::create("Clear to Random", CC_CALLBACK_1(RenderTextureSave::clearImage, this)); + auto item5 = MenuItemFont::create("Clear to Transparent", CC_CALLBACK_1(RenderTextureSave::clearImageTransparent, this)); + auto menu = Menu::create(item1, item2, item3, item4, item5, nullptr); this->addChild(menu); menu->alignItemsVertically(); - menu->setPosition(Vec2(VisibleRect::rightTop().x - 80, VisibleRect::rightTop().y - 30)); + menu->setPosition(Vec2(VisibleRect::rightTop().x - 80, VisibleRect::rightTop().y - 100)); } std::string RenderTextureSave::title() const @@ -85,12 +88,43 @@ void RenderTextureSave::clearImage(cocos2d::Ref *sender) _target->clear(CCRANDOM_0_1(), CCRANDOM_0_1(), CCRANDOM_0_1(), CCRANDOM_0_1()); } -void RenderTextureSave::saveImage(cocos2d::Ref *sender) +void RenderTextureSave::clearImageTransparent(cocos2d::Ref* sender) +{ + _target->clear(0, 0, 0, 0); +} + +void RenderTextureSave::saveImageWithPremultipliedAlpha(cocos2d::Ref* sender) +{ + static int counter = 0; + + char png[20]; + sprintf(png, "image-pma-%d.png", counter); + + auto callback = [&](RenderTexture* rt, const std::string& path) + { + auto sprite = Sprite::create(path); + addChild(sprite); + sprite->setScale(0.3f); + sprite->setPosition(Vec2(40, 40)); + sprite->setRotation(counter * 3); + _target->release(); + }; + + _target->retain(); + _target->saveToFile(png, Image::Format::PNG, true, callback); + //Add this function to avoid crash if we switch to a new scene. + Director::getInstance()->getRenderer()->render(); + CCLOG("Image saved %s", png); + + counter++; +} + +void RenderTextureSave::saveImageWithNonPremultipliedAlpha(cocos2d::Ref *sender) { static int counter = 0; char png[20]; - sprintf(png, "image-%d.png", counter); + sprintf(png, "image-no-pma-%d.png", counter); auto callback = [&](RenderTexture* rt, const std::string& path) { @@ -103,7 +137,8 @@ void RenderTextureSave::saveImage(cocos2d::Ref *sender) }; _target->retain(); - _target->saveToFile(png, Image::Format::PNG, true, callback); + _target->saveToFileAsNonPMA(png, Image::Format::PNG, true, callback); + //Add this function to avoid crash if we switch to a new scene. Director::getInstance()->getRenderer()->render(); CCLOG("Image saved %s", png); @@ -111,6 +146,21 @@ void RenderTextureSave::saveImage(cocos2d::Ref *sender) counter++; } +void RenderTextureSave::addImage(cocos2d::Ref* sender) +{ + auto s = Director::getInstance()->getWinSize(); + + // begin drawing to the render texture + _target->begin(); + + Sprite* sprite = Sprite::create("Images/test-rgba1.png"); + sprite->setPosition(sprite->getContentSize().width + CCRANDOM_0_1() * (s.width - sprite->getContentSize().width), sprite->getContentSize().height + CCRANDOM_0_1() * (s.height - sprite->getContentSize().height)); + sprite->visit(); + + // finish drawing and return context back to the screen + _target->end(); +} + RenderTextureSave::~RenderTextureSave() { _target->release(); diff --git a/tests/cpp-tests/Classes/RenderTextureTest/RenderTextureTest.h b/tests/cpp-tests/Classes/RenderTextureTest/RenderTextureTest.h index 64a179d5a30e..ebd890460566 100644 --- a/tests/cpp-tests/Classes/RenderTextureTest/RenderTextureTest.h +++ b/tests/cpp-tests/Classes/RenderTextureTest/RenderTextureTest.h @@ -44,7 +44,10 @@ class RenderTextureSave : public RenderTextureTest virtual std::string subtitle() const override; void onTouchesMoved(const std::vector& touches, cocos2d::Event* event); void clearImage(cocos2d::Ref* pSender); - void saveImage(cocos2d::Ref* pSender); + void clearImageTransparent(cocos2d::Ref* sender); + void saveImageWithPremultipliedAlpha(cocos2d::Ref* pSender); + void saveImageWithNonPremultipliedAlpha(cocos2d::Ref* pSender); + void addImage(cocos2d::Ref* sender); private: cocos2d::RenderTexture* _target;