diff --git a/cocos/native-binding/impl.ts b/cocos/native-binding/impl.ts index 1b95baf61d9..290976ec130 100644 --- a/cocos/native-binding/impl.ts +++ b/cocos/native-binding/impl.ts @@ -124,6 +124,18 @@ if( NATIVE ){ globalJsb.__JsbBridgeWrapper = value; }, }); + const originSaveImageData = globalJsb.saveImageData; + globalJsb.saveImageData = (data: Uint8Array, width: number, height: number, filePath: string) => { + return new Promise((resolve, reject) => { + originSaveImageData(data, width, height, filePath, (isSuccess) => { + if (isSuccess) { + resolve(); + } else { + reject(); + } + }) + }); + } } export const native = { @@ -140,4 +152,5 @@ export const native = { AssetsManager: globalJsb.AssetsManager, EventAssetsManager: globalJsb.EventAssetsManager, Manifest: globalJsb.Manifest, + saveImageData: globalJsb.saveImageData, }; diff --git a/cocos/native-binding/index.ts b/cocos/native-binding/index.ts index 1daf1590250..e162d4a357d 100644 --- a/cocos/native-binding/index.ts +++ b/cocos/native-binding/index.ts @@ -1316,4 +1316,37 @@ export declare namespace native { */ export function removeAllListeners(); } + /** + * @en Save the image to the path indicated. + * @zh 保存图片到指定路径。 + * @param data : @en the image data, should be raw data array with uint8 @zh 图片数据, 应为原始数据数组,uint8 格式。 + * @param path : @en the path to save @zh 保存路径 + * @param width : @en the width of the image @zh 图片宽度 + * @param height : @en the height of the image @zh 图片高度 + * @param filePath : @en the file path of the image @zh 图片文件路径 + * @param callback : @en the callback function @zh 回调函数 + * @example + * ```ts + let renderTexture = new RenderTexture(); + let renderWindowInfo = { + width: this._width, + height: this._height + }; + renderTexture.reset(renderWindowInfo); + cameras.forEach((camera: any) => { + camera.targetTexture = renderTexture; + }); + await this.waitForNextFrame(); + cameras.forEach((camera: any) => { + camera.targetTexture = null; + }); + let pixelData = renderTexture.readPixels(); + jsb.saveImageData(pixelData, path, width, height, filePath, (isSuccess) => { + if (isSuccess) { + console.log('save image success'); + } else { + console.log('save image failed'); + })); + */ + export function saveImageData(data: Uint8Array, width: number, height: number, filePath: string): Promise; } diff --git a/native/cocos/bindings/manual/jsb_global.cpp b/native/cocos/bindings/manual/jsb_global.cpp index 1de97edfa2e..8f57a533c96 100644 --- a/native/cocos/bindings/manual/jsb_global.cpp +++ b/native/cocos/bindings/manual/jsb_global.cpp @@ -25,6 +25,7 @@ #include "jsb_global.h" #include "application/ApplicationManager.h" +#include "base/Data.h" #include "base/DeferredReleasePool.h" #include "base/Scheduler.h" #include "base/ThreadPool.h" @@ -659,7 +660,67 @@ static bool js_loadImage(se::State &s) { // NOLINT return false; } SE_BIND_FUNC(js_loadImage) +//pixels(RGBA), width, height, fullFilePath(*.png/*.jpg) +static bool js_saveImageData(se::State& s) { // NOLINT + const auto& args = s.args(); + size_t argc = args.size(); + bool ok = true;// NOLINT(readability-identifier-length) + if (argc == 4 || argc == 5) { + auto *uint8ArrayObj = args[0].toObject(); + uint8_t *uint8ArrayData {nullptr}; + size_t length = 0; + uint8ArrayObj->root(); + uint8ArrayObj->incRef(); + uint8ArrayObj->getTypedArrayData(&uint8ArrayData, &length); + uint32_t width; + uint32_t height; + ok &= sevalue_to_native(args[1], &width); + ok &= sevalue_to_native(args[2], &height); + + std::string filePath; + ok &= sevalue_to_native(args[3], &filePath); + SE_PRECONDITION2(ok, false, "js_saveImageData : Error processing arguments"); + + se::Value callbackVal = argc == 5 ? args[4] : se::Value::Null; + se::Object *callbackObj {nullptr}; + if (!callbackVal.isNull()) { + CC_ASSERT(callbackVal.isObject()); + CC_ASSERT(callbackVal.toObject()->isFunction()); + callbackObj = callbackVal.toObject(); + callbackObj->root(); + callbackObj->incRef(); + } + + gThreadPool->pushTask([=](int /*tid*/) { + // isToRGB = false, to keep alpha channel + auto *img = ccnew Image(); + // A conversion from size_t to uint32_t might lose integer precision + img->initWithRawData(uint8ArrayData, static_cast(length), width, height, 32 /*Unused*/); + bool isSuccess = img->saveToFile(filePath, false/*isToRGB*/); + CC_CURRENT_ENGINE()->getScheduler()->performFunctionInCocosThread([=]() { + se::AutoHandleScope hs; + se::ValueArray seArgs; + + se::Value isSuccessVal; + nativevalue_to_se(isSuccess, isSuccessVal); + seArgs.push_back(isSuccessVal); + if (callbackObj) { + callbackObj->call(seArgs, nullptr); + callbackObj->unroot(); + callbackObj->decRef(); + } + uint8ArrayObj->unroot(); + uint8ArrayObj->decRef(); + delete img; + }); + }); + return true; + } + SE_REPORT_ERROR("wrong number of arguments: %d, was expecting %d or %d", (int)argc, 4, 5); + return false; +} +SE_BIND_FUNC(js_saveImageData) static bool js_destroyImage(se::State &s) { // NOLINT const auto &args = s.args(); size_t argc = args.size(); @@ -1313,6 +1374,7 @@ bool jsb_register_global_variables(se::Object *global) { // NOLINT __jsbObj->defineFunction("dumpNativePtrToSeObjectMap", _SE(jsc_dumpNativePtrToSeObjectMap)); __jsbObj->defineFunction("loadImage", _SE(js_loadImage)); + __jsbObj->defineFunction("saveImageData", _SE(js_saveImageData)); __jsbObj->defineFunction("openURL", _SE(JSB_openURL)); __jsbObj->defineFunction("copyTextToClipboard", _SE(JSB_copyTextToClipboard)); __jsbObj->defineFunction("setPreferredFramesPerSecond", _SE(JSB_setPreferredFramesPerSecond)); diff --git a/native/cocos/platform/Image.cpp b/native/cocos/platform/Image.cpp index 31948993203..b33c9a5c36d 100644 --- a/native/cocos/platform/Image.cpp +++ b/native/cocos/platform/Image.cpp @@ -30,6 +30,7 @@ #include #include "base/Config.h" // CC_USE_JPEG, CC_USE_WEBP #include "base/std/container/string.h" +#include "gfx-base/GFXDef-common.h" #if CC_USE_JPEG #include "jpeg/jpeglib.h" @@ -288,7 +289,6 @@ bool Image::initWithImageFile(const ccstd::string &path) { bool Image::initWithImageData(const unsigned char *data, uint32_t dataLen) { bool ret = false; - do { CC_BREAK_IF(!data || dataLen <= 0); @@ -944,4 +944,188 @@ bool Image::initWithRawData(const unsigned char *data, uint32_t /*dataLen*/, int return ret; } +bool Image::saveToFile(const std::string& filename, bool isToRGB) { + //only support for Image::PixelFormat::RGB888 or Image::PixelFormat::RGBA8888 uncompressed data + if (isCompressed() || (_renderFormat != gfx::Format::RGB8 && _renderFormat != gfx::Format::RGBA8)) { + CC_LOG_DEBUG("saveToFile: Image: saveToFile is only support for gfx::Format::RGB8 or gfx::Format::RGBA8 uncompressed data for now"); + return false; + } + + std::string fileExtension = FileUtils::getInstance()->getFileExtension(filename); + + if (fileExtension == ".png") { + return saveImageToPNG(filename, isToRGB); + } + if (fileExtension == ".jpg") { + return saveImageToJPG(filename); + } + CC_LOG_DEBUG("saveToFile: Image: saveToFile no support file extension(only .png or .jpg) for file: %s", filename.c_str()); + return false; +} + + +bool Image::saveImageToPNG(const std::string& filePath, bool isToRGB) { + bool ret = false; + + FILE *fp{nullptr}; + png_structp pngPtr{nullptr}; + png_infop infoPtr{nullptr}; + png_colorp palette{nullptr}; + png_bytep *rowPointers{nullptr}; + bool hasAlpha = gfx::GFX_FORMAT_INFOS[static_cast(_renderFormat)].hasAlpha; + do { + // Init png structure and png ptr + pngPtr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + CC_BREAK_IF(!pngPtr); + if (setjmp(png_jmpbuf(pngPtr))) { + break; + } + infoPtr = png_create_info_struct(pngPtr); + CC_BREAK_IF(!infoPtr); + // Start open file + fp = fopen(FileUtils::getInstance()->getSuitableFOpen(filePath).c_str(), "wb"); + CC_BREAK_IF(!fp); + png_init_io(pngPtr, fp); + auto mask = (!isToRGB && hasAlpha) ? PNG_COLOR_TYPE_RGB_ALPHA : PNG_COLOR_TYPE_RGB; + png_set_IHDR(pngPtr, infoPtr, _width, _height, 8, mask, + PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE); + + palette = static_cast(png_malloc(pngPtr, PNG_MAX_PALETTE_LENGTH * sizeof (png_color))); + CC_BREAK_IF(!palette); + png_set_PLTE(pngPtr, infoPtr, palette, PNG_MAX_PALETTE_LENGTH); + + png_write_info(pngPtr, infoPtr); + + png_set_packing(pngPtr); + + //row_pointers = (png_bytep *)png_malloc(png_ptr, _height * sizeof(png_bytep)); + rowPointers = static_cast(CC_MALLOC(_height * sizeof(png_bytep))); + CC_BREAK_IF(!rowPointers); + + if (!hasAlpha) { + for (int i = 0; i < _height; i++) { + rowPointers[i] = static_cast(_data) + i * _width * 3; + } + png_write_image(pngPtr, rowPointers); + } else { + if (isToRGB) { + auto *tempData = static_cast(CC_MALLOC(_width * _height * 3 * sizeof(unsigned char))); + if (nullptr == tempData) { + break; + } + auto *dst = tempData; + auto *src = _data; + for (int t = 0; t < _width * _height; t++) { + memcpy(dst, src, 3); + dst += 3; + src += 4; + } + + for (int i = 0; i < _height; i++) { + rowPointers[i] = static_cast(tempData) + i * _width * 3; + } + if (tempData != nullptr) { + CC_FREE(tempData); + } + png_write_image(pngPtr, rowPointers); + } else { + for (int i = 0; i < _height; i++) { + rowPointers[i] = static_cast(_data) + i * _width * 4 /*Bytes per pixel*/; + } + png_write_image(pngPtr, rowPointers); + } + } + + png_write_end(pngPtr, infoPtr); + ret = true; + } while (false); + + /*Later free for all functions*/ + if (rowPointers) { + free(rowPointers); + rowPointers = nullptr; + } + if (palette) { + png_free(pngPtr, palette); + } + if (infoPtr) { + png_destroy_write_struct(&pngPtr, &infoPtr); + } + if (fp) { + fclose(fp); + } + if (pngPtr) { + png_destroy_write_struct(&pngPtr, nullptr); + } + return ret; +} + +bool Image::saveImageToJPG(const std::string& filePath) { + bool ret = false; + do { + struct jpeg_compress_struct cinfo; + struct jpeg_error_mgr jerr; + FILE * outfile; /* target file */ + JSAMPROW rowPointer[1]; /* pointer to JSAMPLE row[s] */ + int rowStride; /* physical row width in image buffer */ + + cinfo.err = jpeg_std_error(&jerr); + /* Now we can initialize the JPEG compression object. */ + jpeg_create_compress(&cinfo); + + CC_BREAK_IF((outfile = fopen(FileUtils::getInstance()->getSuitableFOpen(filePath).c_str(), "wb")) == nullptr); + + jpeg_stdio_dest(&cinfo, outfile); + + cinfo.image_width = _width; /* image width and height, in pixels */ + cinfo.image_height = _height; + cinfo.input_components = 3; /* # of color components per pixel */ + cinfo.in_color_space = JCS_RGB; /* colorspace of input image */ + + jpeg_set_defaults(&cinfo); + jpeg_set_quality(&cinfo, 90, TRUE); + + jpeg_start_compress(&cinfo, TRUE); + + rowStride = _width * 3; /* JSAMPLEs per row in image_buffer */ + bool hasAlpha = gfx::GFX_FORMAT_INFOS[static_cast(_renderFormat)].hasAlpha; + + if (hasAlpha) { + auto *tempData = static_cast(CC_MALLOC(_width * _height * 3 * sizeof(unsigned char))); + if (nullptr == tempData) { + jpeg_finish_compress(&cinfo); + jpeg_destroy_compress(&cinfo); + fclose(outfile); + break; + } + auto *dst = tempData; + auto *src = _data; + for (int t = 0; t < _width * _height; t++) { + memcpy(dst, src, 3); + dst += 3; + src += 4; + } + while (cinfo.next_scanline < cinfo.image_height) { + rowPointer[0] = & tempData[cinfo.next_scanline * rowStride]; + (void) jpeg_write_scanlines(&cinfo, rowPointer, 1); + } + if (tempData != nullptr) { + CC_FREE(tempData); + } + } else { + while (cinfo.next_scanline < cinfo.image_height) { + rowPointer[0] = & _data[cinfo.next_scanline * rowStride]; + (void) jpeg_write_scanlines(&cinfo, rowPointer, 1); + } + } + + jpeg_finish_compress(&cinfo); + fclose(outfile); + jpeg_destroy_compress(&cinfo); + + ret = true; + } while (false); + return ret; +} + } // namespace cc diff --git a/native/cocos/platform/Image.h b/native/cocos/platform/Image.h index 497f5f6f558..8b0605e0e45 100644 --- a/native/cocos/platform/Image.h +++ b/native/cocos/platform/Image.h @@ -80,9 +80,16 @@ class Image : public RefCounted { inline int getWidth() const { return _width; } inline int getHeight() const { return _height; } inline ccstd::string getFilePath() const { return _filePath; } - inline bool isCompressed() const { return _isCompressed; } + /** + @brief Save Image data to the specified file, with specified format. + @param filename the file's absolute path, including file suffix. + @param isToRGB whether the image is saved as RGB format. + */ + bool saveToFile(const std::string &filename, bool isToRGB = true); + + protected: bool initWithJpgData(const unsigned char *data, uint32_t dataLen); bool initWithPngData(const unsigned char *data, uint32_t dataLen); @@ -96,6 +103,9 @@ class Image : public RefCounted { bool initWithETC2Data(const unsigned char *data, uint32_t dataLen); bool initWithASTCData(const unsigned char *data, uint32_t dataLen); + bool saveImageToPNG(const std::string& filePath, bool isToRGB = true); + bool saveImageToJPG(const std::string& filePath); + unsigned char *_data = nullptr; uint32_t _dataLen = 0; int _width = 0; diff --git a/native/cocos/platform/win32/modules/System.cpp b/native/cocos/platform/win32/modules/System.cpp index e82bb498889..ed9f8036383 100644 --- a/native/cocos/platform/win32/modules/System.cpp +++ b/native/cocos/platform/win32/modules/System.cpp @@ -112,10 +112,10 @@ ccstd::string System::getCurrentLanguageCode() const { const LCID locale_id = MAKELCID(lid, SORT_DEFAULT); int length = GetLocaleInfoA(locale_id, LOCALE_SISO639LANGNAME, nullptr, 0); - char *tempCode = ccnew char[length]; + char *tempCode = reinterpret_cast(CC_MALLOC(length)); GetLocaleInfoA(locale_id, LOCALE_SISO639LANGNAME, tempCode, length); - ccstd::string code = tempCode; - delete tempCode; + ccstd::string code(tempCode); + CC_FREE(tempCode); return code; }