EditorGradientImageCreatorUtils.cpp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include <GradientSignal/Editor/EditorGradientImageCreatorUtils.h>
  9. #include <Atom/RPI.Edit/Common/AssetUtils.h>
  10. #include <AzCore/Asset/AssetCommon.h>
  11. #include <AzCore/Settings/SettingsRegistryMergeUtils.h>
  12. #include <AzFramework/IO/FileOperations.h>
  13. #include <AzToolsFramework/API/EditorAssetSystemAPI.h>
  14. #include <AzToolsFramework/API/ToolsApplicationAPI.h>
  15. #include <AzToolsFramework/UI/UICore/WidgetHelpers.h>
  16. #if !defined(Q_MOC_RUN)
  17. #include <QApplication>
  18. #include <QMessageBox>
  19. #include <QProgressDialog>
  20. #endif
  21. AZ_PUSH_DISABLE_WARNING(4777, "-Wunknown-warning-option")
  22. // Clang20 on Windows complains about the expression "(begin - &*context_.begin())" in OpenImageIo/detail/fmt/core.h:2716 not being a
  23. // constant expression, but other compilers dont. To fix this without needing to update the library, define an empty FMT_CONSTEVAL macro,
  24. // which disables constexpr format strings for the library entirely
  25. #define FMT_CONSTEVAL
  26. #include <OpenImageIO/imageio.h>
  27. #undef FMT_CONSTEVAL
  28. AZ_POP_DISABLE_WARNING
  29. namespace GradientSignal::ImageCreatorUtils
  30. {
  31. AZStd::string GetSupportedImagesFilter()
  32. {
  33. // Build filter for supported streaming image formats that will be used on the
  34. // native file dialog when creating/picking an output file for the painted image.
  35. // ImageProcessingAtom::s_SupportedImageExtensions actually has more formats
  36. // that will produce streaming image assets, but not all of them support
  37. // all of the bit depths we care about (8/16/32), so we've reduced the list
  38. // to the image formats that do.
  39. return "Images (*.png *.tif *.tiff *.tga *.exr)";
  40. }
  41. AZStd::vector<AZ::Edit::EnumConstant<OutputFormat>> SupportedOutputFormatOptions()
  42. {
  43. return { AZ::Edit::EnumConstant<OutputFormat>(OutputFormat::R8, "R8 (8-bit)"),
  44. AZ::Edit::EnumConstant<OutputFormat>(OutputFormat::R16, "R16 (16-bit)"),
  45. AZ::Edit::EnumConstant<OutputFormat>(OutputFormat::R32, "R32 (32-bit)") };
  46. }
  47. int GetChannels(OutputFormat format)
  48. {
  49. switch (format)
  50. {
  51. case OutputFormat::R8:
  52. return 1;
  53. case OutputFormat::R16:
  54. return 1;
  55. case OutputFormat::R32:
  56. return 1;
  57. case OutputFormat::R8G8B8A8:
  58. return 4;
  59. default:
  60. AZ_Assert(false, "Unsupported output image format (%d)", format);
  61. return 0;
  62. }
  63. }
  64. int GetBytesPerChannel(OutputFormat format)
  65. {
  66. switch (format)
  67. {
  68. case OutputFormat::R8:
  69. return 1;
  70. case OutputFormat::R16:
  71. return 2;
  72. case OutputFormat::R32:
  73. return 4;
  74. case OutputFormat::R8G8B8A8:
  75. return 1;
  76. default:
  77. AZ_Assert(false, "Unsupported output image format (%d)", format);
  78. return 0;
  79. }
  80. }
  81. AZStd::vector<AZ::u8> CreateDefaultImageBuffer(int imageResolutionX, int imageResolutionY, int channels, OutputFormat format)
  82. {
  83. // Fill in our image buffer. Default all values to 0 (black)
  84. int bytesPerChannel = ImageCreatorUtils::GetBytesPerChannel(format);
  85. const size_t imageSize = imageResolutionX * imageResolutionY * channels * bytesPerChannel;
  86. AZStd::vector<AZ::u8> pixels(imageSize, 0);
  87. // If we're saving a 4-channel image, loop through and set the Alpha channel to opaque.
  88. if (channels == 4)
  89. {
  90. for (size_t alphaIndex = (channels - 1); alphaIndex < (imageResolutionX * imageResolutionY * channels); alphaIndex += channels)
  91. {
  92. switch (format)
  93. {
  94. case OutputFormat::R8:
  95. {
  96. pixels[alphaIndex] = std::numeric_limits<AZ::u8>::max();
  97. break;
  98. }
  99. case OutputFormat::R16:
  100. {
  101. auto actualMem = reinterpret_cast<AZ::u16*>(pixels.data());
  102. actualMem[alphaIndex] = std::numeric_limits<AZ::u16>::max();
  103. break;
  104. }
  105. case OutputFormat::R32:
  106. {
  107. auto actualMem = reinterpret_cast<float*>(pixels.data());
  108. actualMem[alphaIndex] = 1.0f;
  109. break;
  110. }
  111. case OutputFormat::R8G8B8A8:
  112. {
  113. pixels[alphaIndex] = std::numeric_limits<AZ::u8>::max();
  114. break;
  115. }
  116. }
  117. }
  118. }
  119. return pixels;
  120. }
  121. bool WriteImage(
  122. const AZStd::string& absoluteFileName,
  123. int imageResolutionX,
  124. int imageResolutionY,
  125. int channels,
  126. OutputFormat format,
  127. const AZStd::span<const AZ::u8>& pixelBuffer,
  128. bool showProgressDialog)
  129. {
  130. OIIO::TypeDesc pixelFormat = OIIO::TypeDesc::UINT8;
  131. switch (format)
  132. {
  133. case OutputFormat::R8:
  134. pixelFormat = OIIO::TypeDesc::UINT8;
  135. break;
  136. case OutputFormat::R16:
  137. pixelFormat = OIIO::TypeDesc::UINT16;
  138. break;
  139. case OutputFormat::R32:
  140. pixelFormat = OIIO::TypeDesc::FLOAT;
  141. break;
  142. case OutputFormat::R8G8B8A8:
  143. pixelFormat = OIIO::TypeDesc::UINT8;
  144. break;
  145. default:
  146. AZ_Assert(false, "Unsupported output image format (%d)", format);
  147. return false;
  148. }
  149. // We *could* declare this as a local variable and just never show it, but then calling WriteImage would always
  150. // require Qt to be started. This way, we can call WriteImage from unit tests without starting Qt as long as we
  151. // set showProgressDialog = false.
  152. QProgressDialog* saveDialog = nullptr;
  153. // Show a dialog box letting the user know the image is being written out.
  154. // For large image sizes, it can take 15+ seconds to create and write out the image.
  155. if (showProgressDialog)
  156. {
  157. saveDialog = new QProgressDialog(AzToolsFramework::GetActiveWindow());
  158. saveDialog->setWindowFlags(saveDialog->windowFlags() & ~Qt::WindowCloseButtonHint);
  159. saveDialog->setLabelText("Saving image...");
  160. saveDialog->setWindowModality(Qt::WindowModal);
  161. saveDialog->setMaximumSize(QSize(256, 96));
  162. saveDialog->setMinimum(0);
  163. saveDialog->setMaximum(100);
  164. saveDialog->setMinimumDuration(0);
  165. saveDialog->setAutoClose(false);
  166. saveDialog->setCancelButton(nullptr);
  167. saveDialog->show();
  168. QApplication::processEvents();
  169. }
  170. AZ::IO::Path fullPathIO(absoluteFileName);
  171. AZ::IO::Path absolutePath = fullPathIO.LexicallyNormal();
  172. // Give our progress dialog another chance to update so we don't look frozen.
  173. if (showProgressDialog)
  174. {
  175. saveDialog->setValue(1);
  176. QApplication::processEvents();
  177. }
  178. // check out the file in source control if source control exists.
  179. bool checkedOutSuccessfully = true;
  180. AzToolsFramework::ToolsApplicationRequestBus::BroadcastResult(
  181. checkedOutSuccessfully,
  182. &AzToolsFramework::ToolsApplicationRequestBus::Events::RequestEditForFileBlocking,
  183. absolutePath.c_str(),
  184. "Checking out for edit...",
  185. AzToolsFramework::ToolsApplicationRequestBus::Events::RequestEditProgressCallback());
  186. if (!checkedOutSuccessfully)
  187. {
  188. AZ_Error("EditorImageGradientComponent", false, "Failed to check out file from source control: %s", absolutePath.c_str());
  189. delete saveDialog;
  190. return false;
  191. }
  192. // Create and save the image on disk. We initially save it to a temporary name so that the Asset Processor won't start
  193. // processing it, and then we'll move it to the correct name at the end.
  194. AZStd::string tempSavePath;
  195. AZ::IO::CreateTempFileName(absolutePath.c_str(), tempSavePath);
  196. std::unique_ptr<OIIO::ImageOutput> outputImage = OIIO::ImageOutput::create(tempSavePath.c_str());
  197. if (!outputImage)
  198. {
  199. AZ_Error("EditorImageGradientComponent", false, "Failed to create image at path: %s", tempSavePath.c_str());
  200. delete saveDialog;
  201. return false;
  202. }
  203. OIIO::ImageSpec spec(imageResolutionX, imageResolutionY, channels, pixelFormat);
  204. outputImage->open(tempSavePath.c_str(), spec);
  205. // Callback to upgrade our progress dialog during image saving.
  206. auto WriteProgressCallback = [](void* opaqueData, float percentDone) -> bool
  207. {
  208. QProgressDialog* saveDialog = reinterpret_cast<QProgressDialog*>(opaqueData);
  209. if (saveDialog && saveDialog->isVisible())
  210. {
  211. saveDialog->setValue(aznumeric_cast<int>(percentDone * 100.0f));
  212. QApplication::processEvents();
  213. }
  214. return false;
  215. };
  216. bool writeResult = outputImage->write_image(
  217. pixelFormat, pixelBuffer.data(), OIIO::AutoStride, OIIO::AutoStride, OIIO::AutoStride, WriteProgressCallback, saveDialog);
  218. if (!writeResult)
  219. {
  220. AZ_Error("EditorImageGradientComponent", writeResult, "Failed to write out gradient image to path: %s", tempSavePath.c_str());
  221. }
  222. outputImage->close();
  223. // Now that we're done saving the temporary image, rename it to the correct file name.
  224. auto moveResult = AZ::IO::SmartMove(tempSavePath.c_str(), absolutePath.c_str());
  225. AZ_Error("EditorImageGradientComponent", moveResult,
  226. "Failed to rename temporary image asset %s to %s", tempSavePath.c_str(), absolutePath.c_str());
  227. delete saveDialog;
  228. return writeResult && moveResult;
  229. }
  230. AZStd::string GetDefaultImageSourcePath(const AZ::Data::AssetId& imageAssetId, const AZStd::string& defaultFileName)
  231. {
  232. // If the image asset ID is valid, try getting the source asset path to use as the default source path.
  233. // Otherwise, create a new name.
  234. if (imageAssetId.IsValid())
  235. {
  236. AZStd::string sourcePath;
  237. bool sourceFileFound = false;
  238. AZ::Data::AssetInfo assetInfo;
  239. AZStd::string watchFolder;
  240. AzToolsFramework::AssetSystemRequestBus::BroadcastResult(
  241. sourceFileFound,
  242. &AzToolsFramework::AssetSystem::AssetSystemRequest::GetSourceInfoBySourceUUID,
  243. imageAssetId.m_guid,
  244. assetInfo,
  245. watchFolder);
  246. if (sourceFileFound)
  247. {
  248. bool success =
  249. AzFramework::StringFunc::Path::ConstructFull(watchFolder.c_str(), assetInfo.m_relativePath.c_str(), sourcePath, true);
  250. if (success)
  251. {
  252. return sourcePath;
  253. }
  254. }
  255. }
  256. // Invalid image asset or failed path creation, try creating a new name.
  257. AZ::IO::Path defaultPath;
  258. if (auto settingsRegistry = AZ::SettingsRegistry::Get(); settingsRegistry != nullptr)
  259. {
  260. settingsRegistry->Get(defaultPath.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_ProjectPath);
  261. }
  262. defaultPath /= AZ::IO::FixedMaxPathString(AZ::RPI::AssetUtils::SanitizeFileName(defaultFileName));
  263. return defaultPath.Native();
  264. }
  265. }