PaintableImageAssetHelper.cpp 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615
  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. #if !defined(Q_MOC_RUN)
  9. #include <Atom/RPI.Edit/Common/AssetUtils.h>
  10. #include <Atom/RPI.Reflect/Image/StreamingImageAsset.h>
  11. #include <AzCore/Asset/AssetCommon.h>
  12. #include <AzCore/IO/Path/Path.h>
  13. #include <AzCore/Preprocessor/EnumReflectUtils.h>
  14. #include <AzCore/Utils/Utils.h>
  15. #include <AzFramework/Application/Application.h>
  16. #include <AzFramework/IO/FileOperations.h>
  17. #include <AzFramework/StringFunc/StringFunc.h>
  18. #include <AzQtComponents/Components/Widgets/FileDialog.h>
  19. #include <AzQtComponents/Components/Widgets/SpinBox.h>
  20. #include <AzToolsFramework/API/EditorAssetSystemAPI.h>
  21. #include <AzToolsFramework/API/ToolsApplicationAPI.h>
  22. #include <AzToolsFramework/UI/UICore/WidgetHelpers.h>
  23. #include <GradientSignal/Editor/EditorGradientImageCreatorRequestBus.h>
  24. #include <GradientSignal/Editor/EditorGradientImageCreatorUtils.h>
  25. #include <GradientSignal/Editor/PaintableImageAssetHelper.h>
  26. #include <QDialog>
  27. #include <QDialogButtonBox>
  28. #include <QFileInfo>
  29. #include <QGridLayout>
  30. #include <QLabel>
  31. #include <QPushButton>
  32. #include <QString>
  33. #include <QVBoxLayout>
  34. #endif
  35. namespace GradientSignal::ImageCreatorUtils
  36. {
  37. //! CreateImageDialog allows the user to specify a set of image creation parameters for use in creating a new image asset.
  38. class CreateImageDialog : public QDialog
  39. {
  40. Q_OBJECT
  41. public:
  42. AZ_CLASS_ALLOCATOR(CreateImageDialog, AZ::SystemAllocator);
  43. CreateImageDialog(QWidget* parent = nullptr)
  44. : QDialog(parent)
  45. {
  46. setModal(true);
  47. setMinimumWidth(300);
  48. resize(300, 100);
  49. setWindowTitle("Create New Image");
  50. // Create the layout for all the widgets to be stacked vertically.
  51. auto verticalLayout = new QVBoxLayout();
  52. // Create the width and height widgets
  53. m_width = new AzQtComponents::SpinBox();
  54. m_width->setRange(MinPixels, MaxPixels);
  55. m_width->setValue(DefaultPixels);
  56. m_height = new AzQtComponents::SpinBox();
  57. m_height->setRange(MinPixels, MaxPixels);
  58. m_height->setValue(DefaultPixels);
  59. QGridLayout* dimensionsLayout = new QGridLayout();
  60. dimensionsLayout->addWidget(new QLabel("Width:"), 0, 0);
  61. dimensionsLayout->addWidget(m_width, 0, 1);
  62. dimensionsLayout->addWidget(new QLabel("Height:"), 0, 2);
  63. dimensionsLayout->addWidget(m_height, 0, 3);
  64. verticalLayout->addLayout(dimensionsLayout);
  65. // Connect ok and cancel buttons and change "ok" to "next".
  66. auto buttonBox = new QDialogButtonBox(this);
  67. buttonBox->setSizePolicy(QSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed));
  68. buttonBox->setOrientation(Qt::Horizontal);
  69. buttonBox->setStandardButtons(QDialogButtonBox::Cancel | QDialogButtonBox::Ok);
  70. QObject::connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
  71. QObject::connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
  72. verticalLayout->addWidget(buttonBox);
  73. // We set this to "Next" instead of "OK" because after the dialog box completes, a standard native file picker dialog
  74. // will appear to select the save location for the created image, so the entire process appears as two steps to the end user.
  75. buttonBox->button(QDialogButtonBox::Ok)->setText("Next");
  76. auto gridLayout = new QGridLayout(this);
  77. gridLayout->addLayout(verticalLayout, 0, 0, 1, 1);
  78. adjustSize();
  79. }
  80. ~CreateImageDialog() = default;
  81. int GetWidth()
  82. {
  83. return m_width->value();
  84. }
  85. int GetHeight()
  86. {
  87. return m_height->value();
  88. }
  89. private:
  90. // Min/max/default values for the image dimensions
  91. static inline constexpr int MinPixels = 1;
  92. static inline constexpr int MaxPixels = 8192;
  93. static inline constexpr int DefaultPixels = 512;
  94. AzQtComponents::SpinBox* m_width = nullptr;
  95. AzQtComponents::SpinBox* m_height = nullptr;
  96. };
  97. AZ_ENUM_DEFINE_REFLECT_UTILITIES(PaintableImageAssetAutoSaveMode);
  98. void PaintableImageAssetHelperBase::Reflect(AZ::ReflectContext* context)
  99. {
  100. // Don't reflect again if we're already reflected this type to the passed-in context.
  101. // (The guard is necessary because every subclass of this base will try to reflect the base class as well)
  102. if (context->IsTypeReflected(azrtti_typeid<PaintableImageAssetHelperBase>()))
  103. {
  104. return;
  105. }
  106. if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
  107. {
  108. PaintableImageAssetAutoSaveModeReflect(*serializeContext);
  109. serializeContext->Class<PaintableImageAssetHelperBase>()
  110. ->Version(0)
  111. ->Field("AutoSaveMode", &PaintableImageAssetHelperBase::m_autoSaveMode)
  112. ->Field("ComponentMode", &PaintableImageAssetHelperBase::m_componentModeDelegate);
  113. if (auto editContext = serializeContext->GetEditContext())
  114. {
  115. editContext->Class<PaintableImageAssetHelperBase>("Paintable Image Asset", "")
  116. ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
  117. ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly)
  118. ->Attribute(AZ::Edit::Attributes::AutoExpand, true)
  119. // Auto-save option when editing an image.
  120. ->DataElement(
  121. AZ::Edit::UIHandlers::Default,
  122. &PaintableImageAssetHelperBase::m_autoSaveMode,
  123. "Auto-Save Mode",
  124. "When editing an image, this selects whether to manually prompt for the save location, auto-save on every "
  125. "edit, "
  126. "or auto-save with incrementing file names on every edit.")
  127. ->EnumAttribute(PaintableImageAssetAutoSaveMode::SaveAs, "Save As...")
  128. ->EnumAttribute(PaintableImageAssetAutoSaveMode::AutoSave, "Auto Save")
  129. ->EnumAttribute(PaintableImageAssetAutoSaveMode::AutoSaveWithIncrementalNames, "Auto Save With Incrementing Names")
  130. // There's no need to ChangeNotify when this property changes, it doesn't affect the behavior of the comopnent,
  131. // it's only queried at the point that an edit is completed.
  132. // Paint controls for editing the image
  133. ->DataElement(
  134. AZ::Edit::UIHandlers::Default,
  135. &PaintableImageAssetHelperBase::m_componentModeDelegate,
  136. "Paint Image",
  137. "Paint into an image asset")
  138. ->Attribute(AZ::Edit::Attributes::ButtonText, "Paint")
  139. ->Attribute(AZ::Edit::Attributes::Visibility, &PaintableImageAssetHelperBase::GetPaintModeVisibility)
  140. ->UIElement(AZ::Edit::UIHandlers::Button, "CreateImage", "Create a new image asset.")
  141. ->Attribute(AZ::Edit::Attributes::NameLabelOverride, "")
  142. ->Attribute(AZ::Edit::Attributes::ButtonText, "Create New Image...")
  143. ->Attribute(AZ::Edit::Attributes::ChangeNotify, &PaintableImageAssetHelperBase::CreateNewImage)
  144. ->Attribute(AZ::Edit::Attributes::ReadOnly, &PaintableImageAssetHelperBase::InComponentMode)
  145. ;
  146. }
  147. }
  148. }
  149. AZ::Crc32 PaintableImageAssetHelperBase::GetPaintModeVisibility() const
  150. {
  151. return ((m_currentImageAssetStatus == AZ::Data::AssetData::AssetStatus::Ready) && !m_currentImageJobsPending)
  152. ? AZ::Edit::PropertyVisibility::ShowChildrenOnly
  153. : AZ::Edit::PropertyVisibility::Hide;
  154. }
  155. bool PaintableImageAssetHelperBase::ImageHasPendingJobs(const AZ::Data::AssetId& assetId)
  156. {
  157. // If it's not a valid asset, it doesn't have any pending jobs.
  158. if (!assetId.IsValid())
  159. {
  160. return false;
  161. }
  162. AZ::Outcome<AzToolsFramework::AssetSystem::JobInfoContainer> jobOutcome = AZ::Failure();
  163. AzToolsFramework::AssetSystemJobRequestBus::BroadcastResult(
  164. jobOutcome, &AzToolsFramework::AssetSystemJobRequestBus::Events::GetAssetJobsInfoByAssetID, assetId, false, false);
  165. if (jobOutcome.IsSuccess())
  166. {
  167. // If there are any jobs that are pending, we'll set our current status based on that instead of
  168. // on the asset loading status.
  169. auto jobInfo = jobOutcome.GetValue();
  170. for (auto& job : jobInfo)
  171. {
  172. switch (job.m_status)
  173. {
  174. case AzToolsFramework::AssetSystem::JobStatus::Queued:
  175. case AzToolsFramework::AssetSystem::JobStatus::InProgress:
  176. return true;
  177. }
  178. }
  179. }
  180. return false;
  181. }
  182. bool PaintableImageAssetHelperBase::RefreshImageAssetStatus(AZ::Data::Asset<AZ::Data::AssetData> imageAsset)
  183. {
  184. bool jobsPending = ImageHasPendingJobs(imageAsset.GetId());
  185. bool statusChanged = (m_currentImageAssetStatus != imageAsset.GetStatus()) || (m_currentImageJobsPending != jobsPending);
  186. m_currentImageAssetStatus = imageAsset.GetStatus();
  187. m_currentImageJobsPending = jobsPending;
  188. return statusChanged;
  189. }
  190. AZStd::string PaintableImageAssetHelperBase::GetImageAssetStatusLabel()
  191. {
  192. if (m_currentImageJobsPending)
  193. {
  194. return m_baseAssetLabel + " (processing)";
  195. }
  196. // No pending asset processing jobs, so just use the current load status of the asset.
  197. switch (m_currentImageAssetStatus)
  198. {
  199. case AZ::Data::AssetData::AssetStatus::NotLoaded:
  200. case AZ::Data::AssetData::AssetStatus::Error:
  201. return m_baseAssetLabel + " (not loaded)";
  202. break;
  203. case AZ::Data::AssetData::AssetStatus::Queued:
  204. case AZ::Data::AssetData::AssetStatus::StreamReady:
  205. case AZ::Data::AssetData::AssetStatus::Loading:
  206. case AZ::Data::AssetData::AssetStatus::LoadedPreReady:
  207. return m_baseAssetLabel + " (loading)";
  208. break;
  209. case AZ::Data::AssetData::AssetStatus::ReadyPreNotify:
  210. case AZ::Data::AssetData::AssetStatus::Ready:
  211. default:
  212. return m_baseAssetLabel;
  213. }
  214. }
  215. void PaintableImageAssetHelperBase::DisableComponentMode()
  216. {
  217. if (!m_componentModeDelegate.IsConnected())
  218. {
  219. return;
  220. }
  221. m_componentModeDelegate.Disconnect();
  222. }
  223. void PaintableImageAssetHelperBase::RefreshComponentModeStatus()
  224. {
  225. const bool paintModeVisible = (GetPaintModeVisibility() != AZ::Edit::PropertyVisibility::Hide);
  226. if (paintModeVisible)
  227. {
  228. EnableComponentMode();
  229. }
  230. else
  231. {
  232. DisableComponentMode();
  233. }
  234. }
  235. bool PaintableImageAssetHelperBase::InComponentMode() const
  236. {
  237. return m_componentModeDelegate.AddedToComponentMode();
  238. }
  239. void PaintableImageAssetHelperBase::Activate(
  240. AZ::EntityComponentIdPair ownerEntityComponentIdPair,
  241. OutputFormat defaultOutputFormat,
  242. AZStd::string baseAssetLabel,
  243. DefaultSaveNameCallback defaultSaveNameCallback,
  244. OnCreateImageCallback onCreateImageCallback)
  245. {
  246. m_ownerEntityComponentIdPair = ownerEntityComponentIdPair;
  247. m_defaultOutputFormat = defaultOutputFormat;
  248. m_baseAssetLabel = baseAssetLabel;
  249. m_defaultSaveNameCallback = defaultSaveNameCallback;
  250. m_onCreateImageCallback = onCreateImageCallback;
  251. }
  252. AZStd::string PaintableImageAssetHelperBase::Refresh(AZ::Data::Asset<AZ::Data::AssetData> imageAsset)
  253. {
  254. RefreshImageAssetStatus(imageAsset);
  255. RefreshComponentModeStatus();
  256. return GetImageAssetStatusLabel();
  257. }
  258. void PaintableImageAssetHelperBase::Deactivate()
  259. {
  260. DisableComponentMode();
  261. m_currentImageAssetStatus = AZ::Data::AssetData::AssetStatus::NotLoaded;
  262. m_currentImageJobsPending = false;
  263. }
  264. AZ::IO::Path PaintableImageAssetHelperBase::GetIncrementingAutoSavePath(const AZ::IO::Path& currentPath) const
  265. {
  266. // Given a path for a source texture, this will return a new path with an incremented version number on the end.
  267. // If the input path doesn't have a version number yet, it will get one added.
  268. // Ex:
  269. // 'Assets/Gradients/MyGradient_gsi.tif' -> 'Assets/Gradients/MyGradient_gsi.0000.tif'
  270. // 'Assets/Gradients/MyGradient_gsi.0005.tif' -> 'Assets/Gradients/MyGradient_gsi.0006.tif'
  271. // We'll use 4 digits as our minimum version number size. We use this to add leading 0s so that alpha-sorting of the
  272. // numbers still puts them in the right order. For example, we'll get 08, 09, 10 instead of 0, 1, 10, 2. This does
  273. // mean that the alpha-sorting will look wrong if we hit 5 digits, but it's a readability tradeoff.
  274. constexpr int NumVersionDigits = 4;
  275. // Store a copy of the filename in a string so that we can modify it below.
  276. AZStd::string baseFileName = currentPath.Stem().Native();
  277. size_t foundDotChar = baseFileName.find_last_of(AZ_FILESYSTEM_EXTENSION_SEPARATOR);
  278. uint32_t versionNumber = 0;
  279. // If the base name ends with '.####' (4 or more digits), then we'll treat that as our auto version number.
  280. // We'll read in the existing number, strip it, and increment it.
  281. if (foundDotChar <= (baseFileName.length() - NumVersionDigits - 1))
  282. {
  283. bool foundVersionNumber = true;
  284. uint32_t tempVersionNumber = 0;
  285. for (size_t digitChar = foundDotChar + 1; digitChar < baseFileName.length(); digitChar++)
  286. {
  287. // If any character after the . isn't a digit, then this isn't a valid version number, so leave it alone.
  288. // (Ex: "image_gsi.o3de")
  289. if (!isdigit(baseFileName.at(digitChar)))
  290. {
  291. foundVersionNumber = false;
  292. break;
  293. }
  294. // Convert the version number that we've found one character at a time.
  295. tempVersionNumber = (tempVersionNumber * 10) + (baseFileName.at(digitChar) - '0');
  296. }
  297. // If we found a valid version number, auto-increment it by one and strip off the previous one.
  298. // We'll re-add the new incremented version number back on at the end.
  299. if (foundVersionNumber)
  300. {
  301. versionNumber = tempVersionNumber + 1;
  302. baseFileName = baseFileName.substr(0, foundDotChar);
  303. }
  304. }
  305. // Create a new string of the form <filename>.####
  306. // For example, "entity1_gsi.tif" should become "entity1_gsi.0000.tif"
  307. AZStd::string newFilename = AZStd::string::format(
  308. AZ_STRING_FORMAT "%c%0*d" AZ_STRING_FORMAT,
  309. AZ_STRING_ARG(baseFileName),
  310. AZ_FILESYSTEM_EXTENSION_SEPARATOR,
  311. NumVersionDigits,
  312. versionNumber,
  313. AZ_STRING_ARG(currentPath.Extension().Native()));
  314. AZ::IO::Path newPath = currentPath;
  315. newPath.ReplaceFilename(AZ::IO::Path(newFilename));
  316. return newPath;
  317. }
  318. AZStd::string PaintableImageAssetHelperBase::GetRelativePathFromAbsolutePath(AZStd::string_view absolutePath)
  319. {
  320. AZStd::string relativePath;
  321. // Turn the absolute path selected in the "Save file" dialog back into a relative path.
  322. // It's a way to verify that our path exists within the project asset search hierarchy,
  323. // and it will get used as an asset hint until the asset is fully processed.
  324. AZStd::string rootFilePath;
  325. bool relativePathFound = false;
  326. AzToolsFramework::AssetSystemRequestBus::BroadcastResult(
  327. relativePathFound,
  328. &AzToolsFramework::AssetSystemRequestBus::Events::GenerateRelativeSourcePath,
  329. absolutePath,
  330. relativePath,
  331. rootFilePath);
  332. if (!relativePathFound)
  333. {
  334. relativePath.clear();
  335. }
  336. return relativePath;
  337. }
  338. bool PaintableImageAssetHelperBase::GetSaveLocation(
  339. AZ::IO::Path& fullPath, AZStd::string& relativePath, PaintableImageAssetAutoSaveMode autoSaveMode)
  340. {
  341. QString absoluteSaveFilePath = QString(fullPath.Native().c_str());
  342. bool promptForSaveName = false;
  343. switch (autoSaveMode)
  344. {
  345. case PaintableImageAssetAutoSaveMode::SaveAs:
  346. promptForSaveName = true;
  347. break;
  348. case PaintableImageAssetAutoSaveMode::AutoSave:
  349. // If the user has never been prompted for a save location during this Editor run, make sure they're prompted at least once.
  350. // If they have been prompted, then skip the prompt and just overwrite the existing location.
  351. promptForSaveName = !m_promptedForSaveLocation;
  352. break;
  353. case PaintableImageAssetAutoSaveMode::AutoSaveWithIncrementalNames:
  354. fullPath = GetIncrementingAutoSavePath(fullPath);
  355. absoluteSaveFilePath = QString(fullPath.Native().c_str());
  356. // Only prompt if our auto-generated name matches an existing file.
  357. promptForSaveName = AZ::IO::SystemFile::Exists(fullPath.Native().c_str());
  358. break;
  359. }
  360. if (promptForSaveName)
  361. {
  362. // Prompt the user for the file name and path.
  363. const QString fileFilter = ImageCreatorUtils::GetSupportedImagesFilter().c_str();
  364. absoluteSaveFilePath = AzQtComponents::FileDialog::GetSaveFileName(nullptr, "Save As...", absoluteSaveFilePath, fileFilter);
  365. }
  366. // User canceled the save dialog, so exit out.
  367. if (absoluteSaveFilePath.isEmpty())
  368. {
  369. return false;
  370. }
  371. // If we prompted for a save name and didn't cancel out with an empty path, track that we've prompted the user so that we don't
  372. // do it again for autosave.
  373. m_promptedForSaveLocation = m_promptedForSaveLocation || promptForSaveName;
  374. const auto absoluteSaveFilePathUtf8 = absoluteSaveFilePath.toUtf8();
  375. const auto absoluteSaveFilePathCStr = absoluteSaveFilePathUtf8.constData();
  376. fullPath.Assign(absoluteSaveFilePathCStr);
  377. fullPath = fullPath.LexicallyNormal();
  378. relativePath = GetRelativePathFromAbsolutePath(fullPath.Native());
  379. if (relativePath.empty())
  380. {
  381. AZ_Error(
  382. "PaintableImageAssetHelper",
  383. false,
  384. "Selected path exists outside of the asset processing directories: %s",
  385. absoluteSaveFilePathCStr);
  386. return false;
  387. }
  388. return true;
  389. }
  390. void PaintableImageAssetHelperBase::CreateNewImage()
  391. {
  392. QWidget* mainWindow = nullptr;
  393. AzToolsFramework::EditorRequests::Bus::BroadcastResult(mainWindow, &AzToolsFramework::EditorRequests::Bus::Events::GetMainWindow);
  394. // Prompt the user for width and height values.
  395. CreateImageDialog dialog(mainWindow);
  396. // If the user pressed "cancel", then return.
  397. if (dialog.exec() != QDialog::Accepted)
  398. {
  399. return;
  400. }
  401. // Get the requested image resolution.
  402. const int imageResolutionX = dialog.GetWidth();
  403. const int imageResolutionY = dialog.GetHeight();
  404. // Call the provided callback to get a default filename to save the created image with.
  405. AZ::IO::Path fileName(m_defaultSaveNameCallback());
  406. // Prompt the user for the save location.
  407. QString absoluteSaveFilePath = AzQtComponents::FileDialog::GetSaveFileName(
  408. mainWindow, "Save As...", QString(fileName.Native().c_str()), QString(ImageCreatorUtils::GetSupportedImagesFilter().c_str()));
  409. // If the user pressed "cancel", then return.
  410. if (absoluteSaveFilePath.isEmpty())
  411. {
  412. return;
  413. }
  414. // The Utf8 version of the save file path is saved into a variable here to ensure that its lifetime of the data is the
  415. // same as fileName.
  416. const auto absoluteSaveFilePathUtf8 = absoluteSaveFilePath.toUtf8();
  417. fileName.Assign(absoluteSaveFilePathUtf8.constData());
  418. fileName = fileName.LexicallyNormal();
  419. // The TGA and EXR formats aren't recognized with only single channel data,
  420. // so need to use RGBA format for them
  421. int channels = GetChannels(m_defaultOutputFormat);
  422. if (fileName.Extension() == ".tga" || fileName.Extension() == ".exr")
  423. {
  424. channels = 4;
  425. }
  426. AZStd::string relativeName = GetRelativePathFromAbsolutePath(fileName.Native());
  427. if (relativeName.empty())
  428. {
  429. AZ_Error(
  430. "PaintableImageAssetHelper",
  431. false,
  432. "Selected path exists outside of the asset processing directories: %s",
  433. fileName.Native().c_str());
  434. return;
  435. }
  436. // Create an blank pixel buffer for our created image.
  437. auto pixelBuffer = ImageCreatorUtils::CreateDefaultImageBuffer(imageResolutionX, imageResolutionY, channels, m_defaultOutputFormat);
  438. // Save the image.
  439. AZStd::string relativePath;
  440. auto createdAsset =
  441. SaveImageInternal(fileName, relativePath, imageResolutionX, imageResolutionY, channels, m_defaultOutputFormat, pixelBuffer);
  442. // Set the active image to the created one and refresh.
  443. if (createdAsset)
  444. {
  445. m_onCreateImageCallback(createdAsset.value());
  446. }
  447. }
  448. AZStd::optional<AZ::Data::Asset<AZ::Data::AssetData>> PaintableImageAssetHelperBase::SaveImage(
  449. int imageResolutionX, int imageResolutionY, OutputFormat format, AZStd::span<const uint8_t> pixelBuffer)
  450. {
  451. AZ::IO::Path fullPath = m_defaultSaveNameCallback();
  452. AZStd::string relativePath;
  453. if (!GetSaveLocation(fullPath, relativePath, m_autoSaveMode))
  454. {
  455. return {};
  456. }
  457. int channels = ImageCreatorUtils::GetChannels(format);
  458. if ((channels == 1) && ((fullPath.Extension() == ".tga" || fullPath.Extension() == ".exr")))
  459. {
  460. AZ_Assert(false, "1-channel TGA / EXR isn't currently supported in this method.");
  461. return {};
  462. }
  463. return SaveImageInternal(fullPath, relativePath, imageResolutionX, imageResolutionY, channels, format, pixelBuffer);
  464. }
  465. AZStd::optional<AZ::Data::Asset<AZ::Data::AssetData>> PaintableImageAssetHelperBase::SaveImageInternal(
  466. AZ::IO::Path& fullPath,
  467. AZStd::string& relativePath,
  468. int imageResolutionX,
  469. int imageResolutionY,
  470. int channels,
  471. OutputFormat format,
  472. AZStd::span<const uint8_t> pixelBuffer)
  473. {
  474. // Try to write out the image
  475. constexpr bool showProgressDialog = true;
  476. if (!ImageCreatorUtils::WriteImage(
  477. fullPath.c_str(), imageResolutionX, imageResolutionY, channels, format, pixelBuffer, showProgressDialog))
  478. {
  479. AZ_Error("PaintableImageAssetHelper", false, "Failed to save image: %s", fullPath.c_str());
  480. return {};
  481. }
  482. // Try to find the source information for the new image in the Asset System.
  483. bool sourceInfoFound = false;
  484. AZ::Data::AssetInfo sourceInfo;
  485. AZStd::string watchFolder;
  486. AzToolsFramework::AssetSystemRequestBus::BroadcastResult(
  487. sourceInfoFound,
  488. &AzToolsFramework::AssetSystemRequestBus::Events::GetSourceInfoBySourcePath,
  489. fullPath.c_str(),
  490. sourceInfo,
  491. watchFolder);
  492. // If this triggers, the flow for handling newly-created images needs to be examined further.
  493. // It's possible that we need to wait for some sort of asset processing event before we can get
  494. // the source asset ID.
  495. AZ_Warning("PaintableImageAssetHelper", sourceInfoFound, "Could not find source info for %s", fullPath.c_str());
  496. // Using the source asset ID, get or create an asset referencing using the expected product asset ID.
  497. // If we're overwriting an existing source asset, this will already exist, but if we're creating a new file,
  498. // the product asset won't exist yet.
  499. auto createdAsset = AZ::Data::AssetManager::Instance().FindOrCreateAsset(
  500. AZ::Data::AssetId(sourceInfo.m_assetId.m_guid, AZ::RPI::StreamingImageAsset::GetImageAssetSubId()),
  501. azrtti_typeid<AZ::RPI::StreamingImageAsset>(),
  502. AZ::Data::AssetLoadBehavior::PreLoad);
  503. // Set the asset hint to the source path so that we can display something reasonably correct in the component while waiting
  504. // for the product asset to get created.
  505. createdAsset.SetHint(relativePath);
  506. // Resync the configurations and refresh the display to hide the "Create" button
  507. // We need to use "Refresh_EntireTree" because "Refresh_AttributesAndValues" isn't enough to refresh the visibility
  508. // settings.
  509. AzToolsFramework::ToolsApplicationNotificationBus::Broadcast(
  510. &AzToolsFramework::ToolsApplicationNotificationBus::Events::InvalidatePropertyDisplayForComponent,
  511. m_ownerEntityComponentIdPair,
  512. AzToolsFramework::Refresh_EntireTree);
  513. return createdAsset;
  514. }
  515. } // namespace GradientSignal::ImageCreatorUtils
  516. #include "PaintableImageAssetHelper.moc"