1// Aseprite
2// Copyright (C) 2019-2022 Igara Studio S.A.
3// Copyright (C) 2001-2017 David Capello
4//
5// This program is distributed under the terms of
6// the End-User License Agreement for Aseprite.
7
8#ifdef HAVE_CONFIG_H
9#include "config.h"
10#endif
11
12#include "app/file/file_data.h"
13
14#include "app/color.h"
15#include "app/xml_document.h"
16#include "base/convert_to.h"
17#include "base/fs.h"
18#include "doc/color.h"
19#include "doc/document.h"
20#include "doc/slice.h"
21#include "gfx/color.h"
22
23#include <cstdlib>
24#include <cstring>
25#include <set>
26
27namespace app {
28
29using namespace base;
30
31namespace {
32
33std::string color_to_hex(doc::color_t color)
34{
35 char buf[256];
36 if (doc::rgba_geta(color) == 255) {
37 std::sprintf(buf, "#%02x%02x%02x",
38 doc::rgba_getr(color),
39 doc::rgba_getg(color),
40 doc::rgba_getb(color));
41 }
42 else {
43 std::sprintf(buf, "#%02x%02x%02x%02x",
44 doc::rgba_getr(color),
45 doc::rgba_getg(color),
46 doc::rgba_getb(color),
47 doc::rgba_geta(color));
48 }
49 return buf;
50}
51
52doc::color_t color_from_hex(const char* str)
53{
54 if (*str == '#')
55 ++str;
56
57 if (std::strlen(str) == 3) { // #fff
58 uint32_t value = std::strtol(str, nullptr, 16);
59 int r = ((value & 0xf00) >> 8);
60 int g = ((value & 0xf0) >> 4);
61 int b = (value & 0xf);
62 return gfx::rgba(
63 r | (r << 4),
64 g | (g << 4),
65 b | (b << 4));
66 }
67 else if (std::strlen(str) == 6) { // #ffffff
68 uint32_t value = std::strtol(str, nullptr, 16);
69 return gfx::rgba(
70 (value & 0xff0000) >> 16,
71 (value & 0xff00) >> 8,
72 (value & 0xff));
73 }
74 else if (std::strlen(str) == 8) { // #ffffffff
75 uint32_t value = std::strtol(str, nullptr, 16);
76 return gfx::rgba(
77 (value & 0xff000000) >> 24,
78 (value & 0xff0000) >> 16,
79 (value & 0xff00) >> 8,
80 (value & 0xff));
81 }
82 else
83 return gfx::rgba(0, 0, 0, 255);
84}
85
86template<typename Container,
87 typename ChildNameGetterFunc,
88 typename UpdateXmlChildFunc>
89void update_xml_collection(const Container& container,
90 TiXmlElement* xmlParent,
91 const char* childElemName,
92 const char* idAttrName,
93 ChildNameGetterFunc childNameGetter,
94 UpdateXmlChildFunc updateXmlChild)
95{
96 if (!xmlParent)
97 return;
98
99 TiXmlElement* xmlNext = nullptr;
100 std::set<std::string> existent;
101
102 // Update existent children
103 for (TiXmlElement* xmlChild=(xmlParent->FirstChild(childElemName) ?
104 xmlParent->FirstChild(childElemName)->ToElement(): nullptr);
105 xmlChild;
106 xmlChild=xmlNext) {
107 xmlNext = xmlChild->NextSiblingElement();
108
109 const char* xmlChildName = xmlChild->Attribute(idAttrName);
110 if (!xmlChildName)
111 continue;
112
113 bool found = false;
114 for (const auto& child : container) {
115 std::string thisChildName = childNameGetter(child);
116 if (thisChildName == xmlChildName) {
117 existent.insert(thisChildName);
118 updateXmlChild(child, xmlChild);
119 found = true;
120 break;
121 }
122 }
123
124 // Delete this <child> element (as the child was removed from the
125 // original container)
126 if (!found)
127 xmlParent->RemoveChild(xmlChild);
128 }
129
130 // Add new children
131 for (const auto& child : container) {
132 std::string thisChildName = childNameGetter(child);
133 if (existent.find(thisChildName) == existent.end()) {
134 TiXmlElement xmlChild(childElemName);
135 xmlChild.SetAttribute(idAttrName, thisChildName.c_str());
136 updateXmlChild(child, &xmlChild);
137
138 xmlParent->InsertEndChild(xmlChild);
139 }
140 }
141}
142
143void update_xml_part_from_slice_key(const doc::SliceKey* key, TiXmlElement* xmlPart)
144{
145 xmlPart->SetAttribute("x", key->bounds().x);
146 xmlPart->SetAttribute("y", key->bounds().y);
147 if (!key->hasCenter()) {
148 xmlPart->SetAttribute("w", key->bounds().w);
149 xmlPart->SetAttribute("h", key->bounds().h);
150 if (xmlPart->Attribute("w1")) xmlPart->RemoveAttribute("w1");
151 if (xmlPart->Attribute("w2")) xmlPart->RemoveAttribute("w2");
152 if (xmlPart->Attribute("w3")) xmlPart->RemoveAttribute("w3");
153 if (xmlPart->Attribute("h1")) xmlPart->RemoveAttribute("h1");
154 if (xmlPart->Attribute("h2")) xmlPart->RemoveAttribute("h2");
155 if (xmlPart->Attribute("h3")) xmlPart->RemoveAttribute("h3");
156 }
157 else {
158 xmlPart->SetAttribute("w1", key->center().x);
159 xmlPart->SetAttribute("w2", key->center().w);
160 xmlPart->SetAttribute("w3", key->bounds().w - key->center().x2());
161 xmlPart->SetAttribute("h1", key->center().y);
162 xmlPart->SetAttribute("h2", key->center().h);
163 xmlPart->SetAttribute("h3", key->bounds().h - key->center().y2());
164 if (xmlPart->Attribute("w")) xmlPart->RemoveAttribute("w");
165 if (xmlPart->Attribute("h")) xmlPart->RemoveAttribute("h");
166 }
167
168 if (key->hasPivot()) {
169 xmlPart->SetAttribute("focusx", key->pivot().x);
170 xmlPart->SetAttribute("focusy", key->pivot().y);
171 }
172 else {
173 if (xmlPart->Attribute("focusx")) xmlPart->RemoveAttribute("focusx");
174 if (xmlPart->Attribute("focusy")) xmlPart->RemoveAttribute("focusy");
175 }
176}
177
178void update_xml_slice(const doc::Slice* slice, TiXmlElement* xmlSlice)
179{
180 if (!slice->userData().text().empty())
181 xmlSlice->SetAttribute("text", slice->userData().text().c_str());
182 else if (xmlSlice->Attribute("text"))
183 xmlSlice->RemoveAttribute("text");
184 xmlSlice->SetAttribute("color", color_to_hex(slice->userData().color()).c_str());
185
186 // Update <key> elements
187 update_xml_collection(
188 *slice,
189 xmlSlice, "key", "frame",
190 [](const Keyframes<SliceKey>::Key& key) -> std::string {
191 return base::convert_to<std::string>(key.frame());
192 },
193 [](const Keyframes<SliceKey>::Key& key, TiXmlElement* xmlKey) {
194 SliceKey* sliceKey = key.value();
195
196 xmlKey->SetAttribute("x", sliceKey->bounds().x);
197 xmlKey->SetAttribute("y", sliceKey->bounds().y);
198 xmlKey->SetAttribute("w", sliceKey->bounds().w);
199 xmlKey->SetAttribute("h", sliceKey->bounds().h);
200
201 if (sliceKey->hasCenter()) {
202 xmlKey->SetAttribute("cx", sliceKey->center().x);
203 xmlKey->SetAttribute("cy", sliceKey->center().y);
204 xmlKey->SetAttribute("cw", sliceKey->center().w);
205 xmlKey->SetAttribute("ch", sliceKey->center().h);
206 }
207 else {
208 if (xmlKey->Attribute("cx")) xmlKey->RemoveAttribute("cx");
209 if (xmlKey->Attribute("cy")) xmlKey->RemoveAttribute("cy");
210 if (xmlKey->Attribute("cw")) xmlKey->RemoveAttribute("cw");
211 if (xmlKey->Attribute("ch")) xmlKey->RemoveAttribute("ch");
212 }
213
214 if (sliceKey->hasPivot()) {
215 xmlKey->SetAttribute("px", sliceKey->pivot().x);
216 xmlKey->SetAttribute("py", sliceKey->pivot().y);
217 }
218 else {
219 if (xmlKey->Attribute("px")) xmlKey->RemoveAttribute("px");
220 if (xmlKey->Attribute("py")) xmlKey->RemoveAttribute("py");
221 }
222 });
223}
224
225} // anonymous namespace
226
227void load_aseprite_data_file(const std::string& dataFilename,
228 doc::Document* doc,
229 app::Color& defaultSliceColor)
230{
231 XmlDocumentRef xmlDoc = open_xml(dataFilename);
232 TiXmlHandle handle(xmlDoc.get());
233
234 TiXmlElement* xmlSlices = handle
235 .FirstChild("sprite")
236 .FirstChild("slices").ToElement();
237
238 // Load slices/parts from theme.xml file
239 if (xmlSlices &&
240 xmlSlices->Attribute("theme")) {
241 std::string themeFileName = xmlSlices->Attribute("theme");
242
243 // Open theme XML file
244 XmlDocumentRef xmlThemeDoc = open_xml(
245 base::join_path(base::get_file_path(dataFilename), themeFileName));
246 TiXmlHandle themeHandle(xmlThemeDoc.get());
247 for (TiXmlElement* xmlPart = themeHandle
248 .FirstChild("theme")
249 .FirstChild("parts")
250 .FirstChild("part").ToElement();
251 xmlPart;
252 xmlPart=xmlPart->NextSiblingElement()) {
253 const char* partId = xmlPart->Attribute("id");
254 if (!partId)
255 continue;
256
257 auto slice = new doc::Slice();
258 slice->setName(partId);
259
260 // Default slice color
261 slice->userData().setColor(
262 doc::rgba(defaultSliceColor.getRed(),
263 defaultSliceColor.getGreen(),
264 defaultSliceColor.getBlue(),
265 defaultSliceColor.getAlpha()));
266
267 doc::SliceKey key;
268
269 int x = std::strtol(xmlPart->Attribute("x"), NULL, 10);
270 int y = std::strtol(xmlPart->Attribute("y"), NULL, 10);
271
272 if (xmlPart->Attribute("w1")) {
273 int w1 = xmlPart->Attribute("w1") ? std::strtol(xmlPart->Attribute("w1"), NULL, 10): 0;
274 int w2 = xmlPart->Attribute("w2") ? std::strtol(xmlPart->Attribute("w2"), NULL, 10): 0;
275 int w3 = xmlPart->Attribute("w3") ? std::strtol(xmlPart->Attribute("w3"), NULL, 10): 0;
276 int h1 = xmlPart->Attribute("h1") ? std::strtol(xmlPart->Attribute("h1"), NULL, 10): 0;
277 int h2 = xmlPart->Attribute("h2") ? std::strtol(xmlPart->Attribute("h2"), NULL, 10): 0;
278 int h3 = xmlPart->Attribute("h3") ? std::strtol(xmlPart->Attribute("h3"), NULL, 10): 0;
279
280 key.setBounds(gfx::Rect(x, y, w1+w2+w3, h1+h2+h3));
281 key.setCenter(gfx::Rect(w1, h1, w2, h2));
282 }
283 else if (xmlPart->Attribute("w")) {
284 int w = xmlPart->Attribute("w") ? std::strtol(xmlPart->Attribute("w"), NULL, 10): 0;
285 int h = xmlPart->Attribute("h") ? std::strtol(xmlPart->Attribute("h"), NULL, 10): 0;
286 key.setBounds(gfx::Rect(x, y, w, h));
287 }
288
289 if (xmlPart->Attribute("focusx")) {
290 int x = xmlPart->Attribute("focusx") ? std::strtol(xmlPart->Attribute("focusx"), NULL, 10): 0;
291 int y = xmlPart->Attribute("focusy") ? std::strtol(xmlPart->Attribute("focusy"), NULL, 10): 0;
292 key.setPivot(gfx::Point(x, y));
293 }
294
295 slice->insert(0, key);
296 doc->sprite()->slices().add(slice);
297 }
298 }
299 // Load slices from <slice> elements
300 else if (xmlSlices) {
301 for (TiXmlElement* xmlSlice=(xmlSlices->FirstChild("slice") ?
302 xmlSlices->FirstChild("slice")->ToElement(): nullptr);
303 xmlSlice;
304 xmlSlice=xmlSlice->NextSiblingElement()) {
305 const char* sliceId = xmlSlice->Attribute("id");
306 if (!sliceId)
307 continue;
308
309 // If the document already contains a slice with this name, use the one from the document
310 if (doc->sprite()->slices().getByName(sliceId))
311 continue;
312
313 auto slice = new doc::Slice();
314 slice->setName(sliceId);
315
316 // Slice text
317 if (xmlSlice->Attribute("text"))
318 slice->userData().setText(xmlSlice->Attribute("text"));
319
320 // Slice color
321 doc::color_t color;
322 if (xmlSlice->Attribute("color")) {
323 color = color_from_hex(xmlSlice->Attribute("color"));
324 }
325 else {
326 color = doc::rgba(defaultSliceColor.getRed(),
327 defaultSliceColor.getGreen(),
328 defaultSliceColor.getBlue(),
329 defaultSliceColor.getAlpha());
330 }
331 slice->userData().setColor(color);
332
333 for (TiXmlElement* xmlKey=(xmlSlice->FirstChild("key") ?
334 xmlSlice->FirstChild("key")->ToElement(): nullptr);
335 xmlKey;
336 xmlKey=xmlKey->NextSiblingElement()) {
337 if (!xmlKey->Attribute("frame"))
338 continue;
339
340 doc::SliceKey key;
341 doc::frame_t frame = std::strtol(xmlKey->Attribute("frame"), nullptr, 10);
342
343 int x = std::strtol(xmlKey->Attribute("x"), nullptr, 10);
344 int y = std::strtol(xmlKey->Attribute("y"), nullptr, 10);
345 int w = std::strtol(xmlKey->Attribute("w"), nullptr, 10);
346 int h = std::strtol(xmlKey->Attribute("h"), nullptr, 10);
347 key.setBounds(gfx::Rect(x, y, w, h));
348
349 if (xmlKey->Attribute("cx")) {
350 int cx = std::strtol(xmlKey->Attribute("cx"), nullptr, 10);
351 int cy = std::strtol(xmlKey->Attribute("cy"), nullptr, 10);
352 int cw = std::strtol(xmlKey->Attribute("cw"), nullptr, 10);
353 int ch = std::strtol(xmlKey->Attribute("ch"), nullptr, 10);
354 key.setCenter(gfx::Rect(cx, cy, cw, ch));
355 }
356
357 if (xmlKey->Attribute("px")) {
358 int px = std::strtol(xmlKey->Attribute("px"), nullptr, 10);
359 int py = std::strtol(xmlKey->Attribute("py"), nullptr, 10);
360 key.setPivot(gfx::Point(px, py));
361 }
362
363 slice->insert(frame, key);
364 }
365
366 doc->sprite()->slices().add(slice);
367 }
368 }
369}
370
371#ifdef ENABLE_SAVE
372void save_aseprite_data_file(const std::string& dataFilename, const doc::Document* doc)
373{
374 XmlDocumentRef xmlDoc = open_xml(dataFilename);
375 TiXmlHandle handle(xmlDoc.get());
376
377 TiXmlElement* xmlSlices = handle
378 .FirstChild("sprite")
379 .FirstChild("slices").ToElement();
380
381 // Update theme.xml file
382 if (xmlSlices &&
383 xmlSlices->Attribute("theme")) {
384 // Open theme XML file
385 std::string themeFileName = base::join_path(
386 base::get_file_path(dataFilename), xmlSlices->Attribute("theme"));
387 XmlDocumentRef xmlThemeDoc = open_xml(themeFileName);
388
389 TiXmlHandle themeHandle(xmlThemeDoc.get());
390 TiXmlElement* xmlParts =
391 themeHandle
392 .FirstChild("theme")
393 .FirstChild("parts").ToElement();
394
395 update_xml_collection(
396 doc->sprite()->slices(),
397 xmlParts, "part", "id",
398 [](const Slice* slice) -> std::string {
399 if (slice->getByFrame(0))
400 return slice->name();
401 else
402 return std::string();
403 },
404 [](Slice* slice, TiXmlElement* xmlSlice) {
405 ASSERT(slice->getByFrame(0));
406 update_xml_part_from_slice_key(slice->getByFrame(0), xmlSlice);
407 });
408
409 // Save theme.xml file
410 save_xml(xmlThemeDoc, themeFileName);
411 }
412 // <slices> without "theme" attribute
413 else if (xmlSlices) {
414 update_xml_collection(
415 doc->sprite()->slices(),
416 xmlSlices, "slice", "id",
417 [](const Slice* slice) -> std::string {
418 return slice->name();
419 },
420 update_xml_slice);
421
422 // Save .aseprite-data file
423 save_xml(xmlDoc, dataFilename);
424 }
425}
426#endif
427
428} // namespace app
429