EditorGradientImageCreatorUtils.cpp 11 KB

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