yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
image_import_view.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cmath>
5#include <utility>
6
7#include "absl/strings/str_format.h"
13#include "app/gui/core/icons.h"
15#include "imgui/imgui.h"
16#include "imgui/misc/cpp/imgui_stdlib.h"
17#include "util/file_util.h"
19#include "util/indexed_png.h"
20#include "util/macro.h"
21#include "zelda3/game_data.h"
22
23namespace yaze::editor {
24
25namespace {
26
27constexpr int kSheetWidth = 128;
28constexpr int kSheetHeight = 32;
29constexpr size_t kMaxPreviewPixels = 256 * 256;
30
31ImU32 ToImU32(const std::array<uint8_t, 4>& color) {
32 return IM_COL32(color[0], color[1], color[2], color[3]);
33}
34
35ImVec4 ToImVec4(const gfx::SnesColor& color) {
36 const auto rgb = color.rgb();
37 return ImVec4(rgb.x / 255.0f, rgb.y / 255.0f, rgb.z / 255.0f, 1.0f);
38}
39
40void DrawSwatch(const char* id, const ImVec4& color, const std::string& tip) {
41 ImGui::ColorButton(id, color, ImGuiColorEditFlags_NoTooltip,
42 ImVec2(18.0f, 18.0f));
43 if (ImGui::IsItemHovered()) {
44 ImGui::SetTooltip("%s", tip.c_str());
45 }
46}
47
48} // namespace
49
50void ImageImportView::SetStatus(std::string message, bool is_error) {
51 status_ = std::move(message);
52 status_is_error_ = is_error;
53}
54
55void ImageImportView::Load(absl::StatusOr<std::vector<uint8_t>> png_bytes,
56 const std::string& source) {
57 auto image = png_bytes.ok()
58 ? ImportSheetImagePng(*png_bytes)
59 : absl::StatusOr<ImportedSheetImage>(png_bytes.status());
60 if (!image.ok()) {
61 image_.reset();
62 source_.clear();
63 SetStatus(std::string(image.status().message()), true);
64 return;
65 }
66 image_ = std::move(*image);
67 source_ = source;
68 SetStatus("Imported " + source + ": " + DescribeImportedSheetImage(*image_),
69 false);
70}
71
73 ImGui::TextWrapped(
74 "Imports keep the image's palette order and pixel indices. Use an "
75 "indexed PNG, or an RGB PNG with an 8-pixel palette strip under the "
76 "art (8x8 swatches, swatch k = color k). Colors outside the strip are "
77 "an error, never a nearest match.");
78
79 util::FileDialogOptions png_filter;
80 png_filter.filters.push_back({"PNG image", "png"});
81 ImGui::SetNextItemWidth(320.0f);
82 ImGui::InputTextWithHint("File##ImageImportPath", "empty: choose in a dialog",
83 &path_);
85 "PNG path for Open and the save buttons. Leave it empty to choose the "
86 "file in a dialog.");
87 if (ImGui::Button(ICON_MD_FILE_OPEN " Open PNG")) {
88 const std::string path =
90 : path_;
91 if (!path.empty()) {
92 Load(util::ReadBinaryFile(path), path);
93 }
94 }
95 ImGui::SameLine();
96 const bool clipboard = util::ImageClipboardSupported();
97 ImGui::BeginDisabled(!clipboard);
98 if (ImGui::Button(ICON_MD_CONTENT_PASTE " Paste from clipboard")) {
99 Load(util::GetClipboardPng(), "clipboard");
100 }
101 ImGui::EndDisabled();
103 clipboard
104 ? "Reads the clipboard image. An indexed PNG keeps its indices; a "
105 "TIFF-only image arrives as RGB and needs a palette strip."
106 : "The image clipboard is only available on macOS.");
107
108 if (!status_.empty()) {
109 ImGui::PushTextWrapPos(0.0f);
110 if (status_is_error_) {
111 ImGui::TextColored(gui::GetErrorColor(), "%s", status_.c_str());
112 } else {
113 ImGui::TextDisabled("%s", status_.c_str());
114 }
115 ImGui::PopTextWrapPos();
116 }
117 if (!image_) {
118 return absl::OkStatus();
119 }
120
121 ImGui::Separator();
122 DrawImage();
124 ImGui::Separator();
126 ImGui::Separator();
128 return absl::OkStatus();
129}
130
132 const auto& image = *image_;
133 ImGui::Text("%s", DescribeImportedSheetImage(image).c_str());
134 for (size_t i = 0; i < image.palette.size(); ++i) {
135 const auto& c = image.palette[i];
136 if (i % 16 != 0) {
137 ImGui::SameLine(0.0f, 2.0f);
138 }
139 DrawSwatch(absl::StrFormat("##ImportColor%zu", i).c_str(),
140 ImVec4(c[0] / 255.0f, c[1] / 255.0f, c[2] / 255.0f, 1.0f),
141 absl::StrFormat("%zu: #%02X%02X%02X%s", i, c[0], c[1], c[2],
142 c[3] == 0 ? " (transparent)" : ""));
143 }
144
145 const size_t pixels = static_cast<size_t>(image.width) * image.height;
146 if (pixels > kMaxPreviewPixels) {
147 ImGui::TextDisabled("Image is too large to preview");
148 return;
149 }
150 const float avail = std::max(ImGui::GetContentRegionAvail().x, 64.0f);
151 const float scale = std::clamp(
152 std::floor(avail / static_cast<float>(image.width)), 1.0f, 3.0f);
153 const ImVec2 origin = ImGui::GetCursorScreenPos();
154 auto* draw = ImGui::GetWindowDrawList();
155 for (int y = 0; y < image.height; ++y) {
156 for (int x = 0; x < image.width; ++x) {
157 const uint8_t index =
158 image.indices[static_cast<size_t>(y) * image.width + x];
159 if (index >= image.palette.size()) {
160 continue;
161 }
162 const ImVec2 a(origin.x + x * scale, origin.y + y * scale);
163 draw->AddRectFilled(a, ImVec2(a.x + scale, a.y + scale),
164 ToImU32(image.palette[index]));
165 }
166 }
167 const ImVec2 size(image.width * scale, image.height * scale);
168 draw->AddRect(origin, ImVec2(origin.x + size.x, origin.y + size.y),
169 IM_COL32(128, 128, 128, 255));
170 ImGui::Dummy(size);
171}
172
174 const auto& image = *image_;
175 auto save_path = [this](const std::string& default_name,
176 const std::string& extension) {
178 default_name, extension)
179 : path_;
180 };
181 auto save = [&](absl::StatusOr<std::vector<uint8_t>> bytes,
182 const std::string& default_name,
183 const std::string& extension) {
184 if (!bytes.ok()) {
185 SetStatus(std::string(bytes.status().message()), true);
186 return;
187 }
188 const std::string path = save_path(default_name, extension);
189 if (path.empty()) {
190 return;
191 }
192 const absl::Status status = util::WriteBinaryFile(path, *bytes);
193 SetStatus(status.ok() ? absl::StrFormat("Saved %zu bytes to %s",
194 bytes->size(), path)
195 : std::string(status.message()),
196 !status.ok());
197 };
198
199 if (ImGui::Button(ICON_MD_SAVE " Save 4bpp .bin")) {
200 save(EncodeImportedSheetTiles(image, 4), "import_4bpp", "bin");
201 }
202 HOVER_HINT("SNES 4bpp tiles, 32 bytes per 8x8 tile, tiles row-major.");
203 ImGui::SameLine();
204 ImGui::BeginDisabled(image.max_index > 7);
205 if (ImGui::Button(ICON_MD_SAVE " Save 3bpp .bin")) {
206 save(EncodeImportedSheetTiles(image, 3), "import_3bpp", "bin");
207 }
208 ImGui::EndDisabled();
209 HOVER_HINT(image.max_index > 7
210 ? "The art uses indices above 7, which 3bpp cannot hold."
211 : "SNES 3bpp tiles, 24 bytes per 8x8 tile, tiles row-major.");
212 ImGui::SameLine();
213 if (ImGui::Button(ICON_MD_SAVE " Save indexed PNG")) {
214 save(EncodeImportedSheetPng(image), "import_indexed", "png");
215 }
216 HOVER_HINT("The art without the strip, as an indexed PNG in index order.");
217}
218
220 const auto& image = *image_;
221 const uint16_t sheet_id = state_->current_sheet_id;
222 const bool arena_is_ours =
223 state_->game_data != nullptr &&
225 const bool fits = image.width == kSheetWidth &&
226 image.height == kSheetHeight && image.max_index <= 7;
227 ImGui::BeginDisabled(!arena_is_ours || !fits);
228 const std::string label = absl::StrFormat(
229 ICON_MD_FILE_UPLOAD " Write into sheet 0x%02X###ImageImportWrite",
230 sheet_id);
231 if (ImGui::Button(label.c_str())) {
232 zelda3::SheetPalette palette{};
233 for (size_t i = 0; i < palette.size() && i < image.palette.size(); ++i) {
234 palette[i] = {image.palette[i][0], image.palette[i][1],
235 image.palette[i][2]};
236 }
237 const auto& sheets = gfx::Arena::Get().gfx_sheets();
238 const std::vector<uint8_t> pixels = sheet_id < sheets.size()
239 ? sheets[sheet_id].vector()
240 : std::vector<uint8_t>{};
241 auto png = EncodeImportedSheetPng(image);
242 auto preview = png.ok()
243 ? PreviewSheetPngImport(sheet_id, pixels, *png, palette)
244 : absl::StatusOr<SheetPngImportPreview>(png.status());
245 const absl::Status status =
246 preview.ok() ? ApplySheetPngImport(*preview, *state_, undo_manager_)
247 : preview.status();
248 SetStatus(status.ok() ? "Wrote " + DescribeSheetPngImport(*preview) +
249 "; undo reverts it, Save writes the ROM"
250 : std::string(status.message()),
251 !status.ok());
252 }
253 ImGui::EndDisabled();
255 !arena_is_ours
256 ? "The sheets in memory belong to another open ROM."
257 : (!fits ? "Only 128x32 art with indices 0-7 fits a 3bpp sheet."
258 : "Writes the indices as they are, as one undoable step. "
259 "The palette is not changed."));
260}
261
263 const auto& image = *image_;
264 const uint16_t sheet_id = state_->current_sheet_id;
265 const GraphicsPaletteSelection* selection = state_->PreviewPalette(sheet_id);
267 gfx::PaletteGroup* group =
268 selection != nullptr && data != nullptr
269 ? data->palette_groups.get_group(selection->group)
270 : nullptr;
271 if (selection == nullptr || group == nullptr || selection->palette < 0 ||
272 selection->palette >= static_cast<int>(group->size())) {
273 ImGui::TextDisabled(
274 "To import the image's colors too, pick a preview palette for sheet "
275 "0x%02X in Palette Controls. Colors are never imported "
276 "automatically.",
277 sheet_id);
278 return;
279 }
280
281 // Copied: refreshing the previews below may rewrite the selection map.
282 const GraphicsPaletteSelection chosen = *selection;
283 const auto& target = group->palette_ref(chosen.palette);
284 const int count =
285 std::min<int>(7, static_cast<int>(image.palette.size()) - 1);
286 auto slots =
287 PreviewPaletteSlots(chosen.group, target.size(), chosen.row, count);
288 const std::string label =
289 absl::StrFormat(ICON_MD_PALETTE
290 " Import colors 1-%d into %s palette %d row "
291 "%d...###ImageImportPalette",
292 count, chosen.group, chosen.palette, chosen.row);
293 ImGui::BeginDisabled(!slots.ok() || count < 1);
294 if (ImGui::Button(label.c_str())) {
295 ImGui::OpenPopup("Import palette colors?###ImageImportPaletteConfirm");
296 }
297 ImGui::EndDisabled();
298 if (!slots.ok()) {
299 ImGui::TextColored(gui::GetWarningColor(), "%s",
300 std::string(slots.status().message()).c_str());
301 return;
302 }
304 "Writes the image's colors 1-7 into the sheet's preview palette row, "
305 "after a confirmation. Undo reverts it. Index 0 stays transparent.");
306
307 if (!ImGui::BeginPopupModal(
308 "Import palette colors?###ImageImportPaletteConfirm", nullptr,
309 ImGuiWindowFlags_AlwaysAutoResize)) {
310 return;
311 }
312 ImGui::Text(
313 "%s palette %d, row %d (SNES colors are 15-bit; low bits "
314 "are dropped):",
315 chosen.group.c_str(), chosen.palette, chosen.row);
316 std::vector<gfx::SnesColor> colors;
317 for (int i = 0; i < count; ++i) {
318 const auto& c = image.palette[i + 1];
319 colors.emplace_back(c[0], c[1], c[2]);
320 const int slot = (*slots)[i];
321 DrawSwatch(absl::StrFormat("##Old%d", slot).c_str(), ToImVec4(target[slot]),
322 absl::StrFormat("Slot %d now", slot));
323 ImGui::SameLine();
324 ImGui::TextUnformatted(ICON_MD_ARROW_FORWARD);
325 ImGui::SameLine();
326 DrawSwatch(absl::StrFormat("##New%d", slot).c_str(), ToImVec4(colors[i]),
327 absl::StrFormat("Image color %d: #%02X%02X%02X", i + 1, c[0],
328 c[1], c[2]));
329 ImGui::SameLine();
330 ImGui::Text("slot %d <- image color %d", slot, i + 1);
331 }
332 if (ImGui::Button("Import colors")) {
333 auto& manager = gfx::PaletteManager::Get();
334 absl::Status status = absl::OkStatus();
335 if (!manager.IsManaging(data)) {
336 status = absl::FailedPreconditionError(
337 "Palette editing is not active for this ROM");
338 } else {
339 manager.BeginBatch();
340 for (int i = 0; i < count && status.ok(); ++i) {
341 status = manager.SetColor(chosen.group, chosen.palette, (*slots)[i],
342 colors[i]);
343 }
344 manager.EndBatch();
346 }
347 SetStatus(status.ok() ? absl::StrFormat("Imported %d colors into %s "
348 "palette %d row %d",
349 count, chosen.group, chosen.palette,
350 chosen.row)
351 : std::string(status.message()),
352 !status.ok());
353 ImGui::CloseCurrentPopup();
354 }
355 ImGui::SameLine();
356 if (ImGui::Button("Cancel")) {
357 ImGui::CloseCurrentPopup();
358 }
359 ImGui::EndPopup();
360}
361
362} // namespace yaze::editor
const GraphicsPaletteSelection * PreviewPalette(uint16_t sheet) const
std::optional< ImportedSheetImage > image_
void Load(absl::StatusOr< std::vector< uint8_t > > png_bytes, const std::string &source)
void SetStatus(std::string message, bool is_error)
const void * gfx_sheets_owner() const
Definition arena.h:216
GraphicsSheets & gfx_sheets()
Definition arena.h:178
static Arena & Get()
Definition arena.cc:24
static PaletteManager & Get()
Get the singleton instance.
SNES Color container.
Definition snes_color.h:110
constexpr ImVec4 rgb() const
Get RGB values (WARNING: stored as 0-255 in ImVec4)
Definition snes_color.h:183
static std::string ShowSaveFileDialog(const std::string &default_name="", const std::string &default_extension="")
ShowSaveFileDialog opens a save file dialog and returns the selected filepath. Uses global feature fl...
static std::string ShowOpenFileDialog()
ShowOpenFileDialog opens a file dialog and returns the selected filepath. Uses global feature flag to...
#define ICON_MD_FILE_OPEN
Definition icons.h:747
#define ICON_MD_ARROW_FORWARD
Definition icons.h:184
#define ICON_MD_FILE_UPLOAD
Definition icons.h:749
#define ICON_MD_CONTENT_PASTE
Definition icons.h:467
#define ICON_MD_SAVE
Definition icons.h:1644
#define ICON_MD_PALETTE
Definition icons.h:1370
PNG images on the system clipboard. SDL's clipboard is text-only.
#define HOVER_HINT(string)
Definition macro.h:24
void DrawSwatch(const char *id, const ImVec4 &color, const std::string &tip)
ImU32 ToImU32(const std::array< uint8_t, 4 > &color)
Editors are the view controllers for the application.
std::string DescribeImportedSheetImage(const ImportedSheetImage &image)
"128x32 indexed PNG, 8 colors (strip removed), highest index 7".
std::string DescribeSheetPngImport(const SheetPngImportPreview &preview)
"Sheet 0x20: blocks 0, 3 (5 tiles)" for the preview list.
absl::StatusOr< ImportedSheetImage > ImportSheetImagePng(const std::vector< uint8_t > &png_bytes)
Decodes PNG bytes (file or clipboard) and calls ImportSheetImage.
absl::StatusOr< std::vector< uint8_t > > EncodeImportedSheetTiles(const ImportedSheetImage &image, int bpp)
absl::StatusOr< std::vector< int > > PreviewPaletteSlots(std::string_view group_name, size_t palette_size, int row, int count)
void RefreshGraphicsPreviewPalettes(GraphicsEditorState &state)
absl::StatusOr< std::vector< uint8_t > > EncodeImportedSheetPng(const ImportedSheetImage &image)
Re-encodes the art (without the strip) as an indexed PNG in index order.
absl::StatusOr< SheetPngImportPreview > PreviewSheetPngImport(uint16_t sheet_id, const std::vector< uint8_t > &indexed_pixels, const std::vector< uint8_t > &png_bytes, const zelda3::SheetPalette &palette, int first_block)
absl::Status ApplySheetPngImport(const SheetPngImportPreview &preview, GraphicsEditorState &state, UndoManager *undo_manager)
ImVec4 GetErrorColor()
Definition ui_helpers.cc:59
ImVec4 GetWarningColor()
Definition ui_helpers.cc:54
absl::StatusOr< std::vector< uint8_t > > GetClipboardPng(const std::string &)
absl::Status WriteBinaryFile(const std::string &path, const std::vector< uint8_t > &bytes)
absl::StatusOr< std::vector< uint8_t > > ReadBinaryFile(const std::string &path)
bool ImageClipboardSupported()
std::array< SheetColor, 8 > SheetPalette
Colors for 3bpp indices 0-7. Index 0 is exported as transparent.
PNG export and import for the Graphics editor.
PaletteGroup * get_group(const std::string &group_name)
Represents a group of palettes.
const SnesPalette & palette_ref(int i) const
std::vector< FileDialogFilter > filters
Definition file_util.h:18
gfx::PaletteGroupMap palette_groups
Definition game_data.h:95