1 | // Aseprite |
2 | // Copyright (C) 2020-2022 Igara Studio S.A. |
3 | // Copyright (C) 2001-2016 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/app_brushes.h" |
13 | #include "app/resource_finder.h" |
14 | #include "app/tools/ink_type.h" |
15 | #include "app/xml_document.h" |
16 | #include "app/xml_exception.h" |
17 | #include "base/base64.h" |
18 | #include "base/convert_to.h" |
19 | #include "base/fs.h" |
20 | #include "base/serialization.h" |
21 | #include "doc/brush.h" |
22 | #include "doc/color.h" |
23 | #include "doc/image.h" |
24 | #include "doc/image_impl.h" |
25 | |
26 | #include <fstream> |
27 | |
28 | namespace app { |
29 | |
30 | using namespace doc; |
31 | using namespace base::serialization; |
32 | using namespace base::serialization::little_endian; |
33 | |
34 | namespace { |
35 | |
36 | ImageRef load_xml_image(const TiXmlElement* imageElem) |
37 | { |
38 | ImageRef image; |
39 | int w, h; |
40 | if (imageElem->QueryIntAttribute("width" , &w) != TIXML_SUCCESS || |
41 | imageElem->QueryIntAttribute("height" , &h) != TIXML_SUCCESS || |
42 | w < 0 || w > 9999 || |
43 | h < 0 || h > 9999) |
44 | return image; |
45 | |
46 | auto formatValue = imageElem->Attribute("format" ); |
47 | if (!formatValue) |
48 | return image; |
49 | |
50 | const char* pixels_base64 = imageElem->GetText(); |
51 | if (!pixels_base64) |
52 | return image; |
53 | |
54 | base::buffer data; |
55 | base::decode_base64(pixels_base64, data); |
56 | auto it = data.begin(), end = data.end(); |
57 | |
58 | std::string formatStr = formatValue; |
59 | if (formatStr == "rgba" ) { |
60 | image.reset(Image::create(IMAGE_RGB, w, h)); |
61 | LockImageBits<RgbTraits> pixels(image.get()); |
62 | for (auto& pixel : pixels) { |
63 | if ((end - it) < 4) |
64 | break; |
65 | |
66 | int r = *it; ++it; |
67 | int g = *it; ++it; |
68 | int b = *it; ++it; |
69 | int a = *it; ++it; |
70 | |
71 | pixel = doc::rgba(r, g, b, a); |
72 | } |
73 | } |
74 | else if (formatStr == "grayscale" ) { |
75 | image.reset(Image::create(IMAGE_GRAYSCALE, w, h)); |
76 | LockImageBits<GrayscaleTraits> pixels(image.get()); |
77 | for (auto& pixel : pixels) { |
78 | if ((end - it) < 2) |
79 | break; |
80 | |
81 | int v = *it; ++it; |
82 | int a = *it; ++it; |
83 | |
84 | pixel = doc::graya(v, a); |
85 | } |
86 | } |
87 | else if (formatStr == "indexed" ) { |
88 | image.reset(Image::create(IMAGE_INDEXED, w, h)); |
89 | LockImageBits<IndexedTraits> pixels(image.get()); |
90 | for (auto& pixel : pixels) { |
91 | if (it == end) |
92 | break; |
93 | |
94 | pixel = *it; |
95 | ++it; |
96 | } |
97 | } |
98 | else if (formatStr == "bitmap" ) { |
99 | image.reset(Image::create(IMAGE_BITMAP, w, h)); |
100 | LockImageBits<BitmapTraits> pixels(image.get()); |
101 | for (auto& pixel : pixels) { |
102 | if (it == end) |
103 | break; |
104 | |
105 | pixel = *it; |
106 | ++it; |
107 | } |
108 | } |
109 | return image; |
110 | } |
111 | |
112 | void save_xml_image(TiXmlElement* imageElem, const Image* image) |
113 | { |
114 | int w = image->width(); |
115 | int h = image->height(); |
116 | imageElem->SetAttribute("width" , w); |
117 | imageElem->SetAttribute("height" , h); |
118 | |
119 | std::string format; |
120 | switch (image->pixelFormat()) { |
121 | case IMAGE_RGB: format = "rgba" ; break; |
122 | case IMAGE_GRAYSCALE: format = "grayscale" ; break; |
123 | case IMAGE_INDEXED: format = "indexed" ; break; |
124 | case IMAGE_BITMAP: format = "bitmap" ; break; // TODO add "bitmap" format |
125 | } |
126 | ASSERT(!format.empty()); |
127 | if (!format.empty()) |
128 | imageElem->SetAttribute("format" , format.c_str()); |
129 | |
130 | base::buffer data; |
131 | data.reserve(h * image->getRowStrideSize()); |
132 | switch (image->pixelFormat()) { |
133 | case IMAGE_RGB:{ |
134 | const LockImageBits<RgbTraits> pixels(image); |
135 | for (const auto& pixel : pixels) { |
136 | data.push_back(doc::rgba_getr(pixel)); |
137 | data.push_back(doc::rgba_getg(pixel)); |
138 | data.push_back(doc::rgba_getb(pixel)); |
139 | data.push_back(doc::rgba_geta(pixel)); |
140 | } |
141 | break; |
142 | } |
143 | case IMAGE_GRAYSCALE:{ |
144 | const LockImageBits<GrayscaleTraits> pixels(image); |
145 | for (const auto& pixel : pixels) { |
146 | data.push_back(doc::graya_getv(pixel)); |
147 | data.push_back(doc::graya_geta(pixel)); |
148 | } |
149 | break; |
150 | } |
151 | case IMAGE_INDEXED: { |
152 | const LockImageBits<IndexedTraits> pixels(image); |
153 | for (const auto& pixel : pixels) { |
154 | data.push_back(pixel); |
155 | } |
156 | break; |
157 | } |
158 | case IMAGE_BITMAP: { |
159 | // Here we save bitmap format as indexed |
160 | const LockImageBits<BitmapTraits> pixels(image); |
161 | for (const auto& pixel : pixels) { |
162 | data.push_back(pixel); // TODO save bitmap format as bitmap |
163 | } |
164 | break; |
165 | } |
166 | } |
167 | |
168 | std::string data_base64; |
169 | base::encode_base64(data, data_base64); |
170 | TiXmlText textElem(data_base64.c_str()); |
171 | imageElem->InsertEndChild(textElem); |
172 | } |
173 | |
174 | } // anonymous namespace |
175 | |
176 | AppBrushes::AppBrushes() |
177 | { |
178 | m_standard.push_back(BrushRef(new Brush(kCircleBrushType, 7, 0))); |
179 | m_standard.push_back(BrushRef(new Brush(kSquareBrushType, 7, 0))); |
180 | m_standard.push_back(BrushRef(new Brush(kLineBrushType, 7, 44))); |
181 | |
182 | try { |
183 | std::string fn = m_userBrushesFilename = userBrushesFilename(); |
184 | if (base::is_file(fn)) |
185 | load(fn); |
186 | } |
187 | catch (const std::exception& ex) { |
188 | LOG(ERROR, "BRSH: Error loading user brushes: %s\n" , ex.what()); |
189 | } |
190 | } |
191 | |
192 | AppBrushes::~AppBrushes() |
193 | { |
194 | if (!m_userBrushesFilename.empty()) |
195 | save(m_userBrushesFilename); |
196 | } |
197 | |
198 | AppBrushes::slot_id AppBrushes::addBrushSlot(const BrushSlot& brush) |
199 | { |
200 | // Use an empty slot |
201 | for (size_t i=0; i<m_slots.size(); ++i) { |
202 | if (!m_slots[i].locked() || m_slots[i].isEmpty()) { |
203 | m_slots[i] = brush; |
204 | return i+1; |
205 | } |
206 | } |
207 | |
208 | m_slots.push_back(brush); |
209 | ItemsChange(); |
210 | return slot_id(m_slots.size()); // Returns the slot |
211 | } |
212 | |
213 | void AppBrushes::removeBrushSlot(slot_id slot) |
214 | { |
215 | --slot; |
216 | if (slot >= 0 && slot < (int)m_slots.size()) { |
217 | m_slots[slot] = BrushSlot(); |
218 | |
219 | // Erase empty trailing slots |
220 | while (!m_slots.empty() && |
221 | m_slots[m_slots.size()-1].isEmpty()) |
222 | m_slots.erase(--m_slots.end()); |
223 | |
224 | ItemsChange(); |
225 | } |
226 | } |
227 | |
228 | void AppBrushes::removeAllBrushSlots() |
229 | { |
230 | while (!m_slots.empty()) |
231 | m_slots.erase(--m_slots.end()); |
232 | |
233 | ItemsChange(); |
234 | } |
235 | |
236 | bool AppBrushes::hasBrushSlot(slot_id slot) const |
237 | { |
238 | --slot; |
239 | return (slot >= 0 && slot < (int)m_slots.size() && |
240 | !m_slots[slot].isEmpty()); |
241 | } |
242 | |
243 | BrushSlot AppBrushes::getBrushSlot(slot_id slot) const |
244 | { |
245 | --slot; |
246 | if (slot >= 0 && slot < (int)m_slots.size()) |
247 | return m_slots[slot]; |
248 | else |
249 | return BrushSlot(); |
250 | } |
251 | |
252 | void AppBrushes::setBrushSlot(slot_id slot, const BrushSlot& brush) |
253 | { |
254 | --slot; |
255 | if (slot >= 0 && slot < (int)m_slots.size()) { |
256 | m_slots[slot] = brush; |
257 | ItemsChange(); |
258 | } |
259 | } |
260 | |
261 | void AppBrushes::lockBrushSlot(slot_id slot) |
262 | { |
263 | --slot; |
264 | if (slot >= 0 && slot < (int)m_slots.size() && |
265 | !m_slots[slot].isEmpty()) { |
266 | m_slots[slot].setLocked(true); |
267 | } |
268 | } |
269 | |
270 | void AppBrushes::unlockBrushSlot(slot_id slot) |
271 | { |
272 | --slot; |
273 | if (slot >= 0 && slot < (int)m_slots.size() && |
274 | !m_slots[slot].isEmpty()) { |
275 | m_slots[slot].setLocked(false); |
276 | } |
277 | } |
278 | |
279 | bool AppBrushes::isBrushSlotLocked(slot_id slot) const |
280 | { |
281 | --slot; |
282 | if (slot >= 0 && slot < (int)m_slots.size() && |
283 | !m_slots[slot].isEmpty()) { |
284 | return m_slots[slot].locked(); |
285 | } |
286 | else |
287 | return false; |
288 | } |
289 | |
290 | static const int kBrushFlags = |
291 | int(BrushSlot::Flags::BrushType) | |
292 | int(BrushSlot::Flags::BrushSize) | |
293 | int(BrushSlot::Flags::BrushAngle); |
294 | |
295 | void AppBrushes::load(const std::string& filename) |
296 | { |
297 | XmlDocumentRef doc = app::open_xml(filename); |
298 | TiXmlHandle handle(doc.get()); |
299 | TiXmlElement* brushElem = handle |
300 | .FirstChild("brushes" ) |
301 | .FirstChild("brush" ).ToElement(); |
302 | |
303 | while (brushElem) { |
304 | // flags |
305 | int flags = 0; |
306 | BrushRef brush; |
307 | app::Color fgColor; |
308 | app::Color bgColor; |
309 | tools::InkType inkType = tools::InkType::DEFAULT; |
310 | int inkOpacity = 255; |
311 | Shade shade; |
312 | bool pixelPerfect = false; |
313 | |
314 | // Brush |
315 | const char* type = brushElem->Attribute("type" ); |
316 | const char* size = brushElem->Attribute("size" ); |
317 | const char* angle = brushElem->Attribute("angle" ); |
318 | if (type || size || angle) { |
319 | if (type) flags |= int(BrushSlot::Flags::BrushType); |
320 | if (size) flags |= int(BrushSlot::Flags::BrushSize); |
321 | if (angle) flags |= int(BrushSlot::Flags::BrushAngle); |
322 | brush.reset( |
323 | new Brush( |
324 | (type ? string_id_to_brush_type(type): kFirstBrushType), |
325 | (size ? base::convert_to<int>(std::string(size)): 1), |
326 | (angle ? base::convert_to<int>(std::string(angle)): 0))); |
327 | } |
328 | |
329 | // Brush image |
330 | ImageRef image, mask; |
331 | if (TiXmlElement* imageElem = brushElem->FirstChildElement("image" )) |
332 | image = load_xml_image(imageElem); |
333 | if (TiXmlElement* maskElem = brushElem->FirstChildElement("mask" )) |
334 | mask = load_xml_image(maskElem); |
335 | |
336 | if (image) { |
337 | if (!brush) |
338 | brush.reset(new Brush()); |
339 | brush->setImage(image.get(), mask.get()); |
340 | } |
341 | |
342 | // Colors |
343 | if (TiXmlElement* fgcolorElem = brushElem->FirstChildElement("fgcolor" )) { |
344 | if (auto value = fgcolorElem->Attribute("value" )) { |
345 | fgColor = app::Color::fromString(value); |
346 | flags |= int(BrushSlot::Flags::FgColor); |
347 | } |
348 | } |
349 | |
350 | if (TiXmlElement* bgcolorElem = brushElem->FirstChildElement("bgcolor" )) { |
351 | if (auto value = bgcolorElem->Attribute("value" )) { |
352 | bgColor = app::Color::fromString(value); |
353 | flags |= int(BrushSlot::Flags::BgColor); |
354 | } |
355 | } |
356 | |
357 | // Ink |
358 | if (TiXmlElement* inkTypeElem = brushElem->FirstChildElement("inktype" )) { |
359 | if (auto value = inkTypeElem->Attribute("value" )) { |
360 | inkType = app::tools::string_id_to_ink_type(value); |
361 | flags |= int(BrushSlot::Flags::InkType); |
362 | } |
363 | } |
364 | |
365 | if (TiXmlElement* inkOpacityElem = brushElem->FirstChildElement("inkopacity" )) { |
366 | if (auto value = inkOpacityElem->Attribute("value" )) { |
367 | inkOpacity = base::convert_to<int>(std::string(value)); |
368 | flags |= int(BrushSlot::Flags::InkOpacity); |
369 | } |
370 | } |
371 | |
372 | // Shade |
373 | if (TiXmlElement* shadeElem = brushElem->FirstChildElement("shade" )) { |
374 | if (auto value = shadeElem->Attribute("value" )) { |
375 | shade = shade_from_string(value); |
376 | flags |= int(BrushSlot::Flags::Shade); |
377 | } |
378 | } |
379 | |
380 | // Pixel-perfect |
381 | if (TiXmlElement* pixelPerfectElem = brushElem->FirstChildElement("pixelperfect" )) { |
382 | pixelPerfect = bool_attr(pixelPerfectElem, "value" , false); |
383 | flags |= int(BrushSlot::Flags::PixelPerfect); |
384 | } |
385 | |
386 | // Image color (enabled by default for backward compatibility) |
387 | if (!brushElem->Attribute("imagecolor" ) || |
388 | bool_attr(brushElem, "imagecolor" , false)) |
389 | flags |= int(BrushSlot::Flags::ImageColor); |
390 | |
391 | if (flags != 0) |
392 | flags |= int(BrushSlot::Flags::Locked); |
393 | |
394 | BrushSlot brushSlot(BrushSlot::Flags(flags), |
395 | brush, fgColor, bgColor, |
396 | inkType, inkOpacity, shade, |
397 | pixelPerfect); |
398 | m_slots.push_back(brushSlot); |
399 | |
400 | brushElem = brushElem->NextSiblingElement(); |
401 | } |
402 | } |
403 | |
404 | void AppBrushes::save(const std::string& filename) const |
405 | { |
406 | XmlDocumentRef doc(new TiXmlDocument()); |
407 | TiXmlElement brushesElem("brushes" ); |
408 | |
409 | //<?xml version="1.0" encoding="utf-8"?> |
410 | |
411 | for (const auto& slot : m_slots) { |
412 | TiXmlElement brushElem("brush" ); |
413 | if (slot.locked()) { |
414 | // Flags |
415 | int flags = int(slot.flags()); |
416 | |
417 | // This slot might not have a brush. (E.g. a slot that changes |
418 | // the pixel perfect mode only.) |
419 | if (!slot.hasBrush()) |
420 | flags &= ~kBrushFlags; |
421 | |
422 | // Brush type |
423 | if (slot.hasBrush()) { |
424 | ASSERT(slot.brush()); |
425 | |
426 | if (flags & int(BrushSlot::Flags::BrushType)) { |
427 | brushElem.SetAttribute( |
428 | "type" , brush_type_to_string_id(slot.brush()->type()).c_str()); |
429 | } |
430 | |
431 | if (flags & int(BrushSlot::Flags::BrushSize)) { |
432 | brushElem.SetAttribute("size" , slot.brush()->size()); |
433 | } |
434 | |
435 | if (flags & int(BrushSlot::Flags::BrushAngle)) { |
436 | brushElem.SetAttribute("angle" , slot.brush()->angle()); |
437 | } |
438 | |
439 | if (slot.brush()->type() == kImageBrushType && |
440 | slot.brush()->originalImage()) { |
441 | TiXmlElement elem("image" ); |
442 | save_xml_image(&elem, slot.brush()->originalImage()); |
443 | brushElem.InsertEndChild(elem); |
444 | |
445 | if (slot.brush()->maskBitmap()) { |
446 | TiXmlElement maskElem("mask" ); |
447 | save_xml_image(&maskElem, slot.brush()->maskBitmap()); |
448 | brushElem.InsertEndChild(maskElem); |
449 | } |
450 | |
451 | // Image color |
452 | brushElem.SetAttribute( |
453 | "imagecolor" , |
454 | (flags & int(BrushSlot::Flags::ImageColor)) ? "true" : "false" ); |
455 | } |
456 | } |
457 | |
458 | // Colors |
459 | if (flags & int(BrushSlot::Flags::FgColor)) { |
460 | TiXmlElement elem("fgcolor" ); |
461 | elem.SetAttribute("value" , slot.fgColor().toString().c_str()); |
462 | brushElem.InsertEndChild(elem); |
463 | } |
464 | |
465 | if (flags & int(BrushSlot::Flags::BgColor)) { |
466 | TiXmlElement elem("bgcolor" ); |
467 | elem.SetAttribute("value" , slot.bgColor().toString().c_str()); |
468 | brushElem.InsertEndChild(elem); |
469 | } |
470 | |
471 | // Ink |
472 | if (flags & int(BrushSlot::Flags::InkType)) { |
473 | TiXmlElement elem("inktype" ); |
474 | elem.SetAttribute( |
475 | "value" , app::tools::ink_type_to_string_id(slot.inkType()).c_str()); |
476 | brushElem.InsertEndChild(elem); |
477 | } |
478 | |
479 | if (flags & int(BrushSlot::Flags::InkOpacity)) { |
480 | TiXmlElement elem("inkopacity" ); |
481 | elem.SetAttribute("value" , slot.inkOpacity()); |
482 | brushElem.InsertEndChild(elem); |
483 | } |
484 | |
485 | // Shade |
486 | if (flags & int(BrushSlot::Flags::Shade)) { |
487 | TiXmlElement elem("shade" ); |
488 | elem.SetAttribute("value" , shade_to_string(slot.shade()).c_str()); |
489 | brushElem.InsertEndChild(elem); |
490 | } |
491 | |
492 | // Pixel-perfect |
493 | if (flags & int(BrushSlot::Flags::PixelPerfect)) { |
494 | TiXmlElement elem("pixelperfect" ); |
495 | elem.SetAttribute("value" , slot.pixelPerfect() ? "true" : "false" ); |
496 | brushElem.InsertEndChild(elem); |
497 | } |
498 | } |
499 | |
500 | brushesElem.InsertEndChild(brushElem); |
501 | } |
502 | |
503 | TiXmlDeclaration declaration("1.0" , "utf-8" , "" ); |
504 | doc->InsertEndChild(declaration); |
505 | doc->InsertEndChild(brushesElem); |
506 | save_xml(doc, filename); |
507 | } |
508 | |
509 | // static |
510 | std::string AppBrushes::userBrushesFilename() |
511 | { |
512 | ResourceFinder rf; |
513 | rf.includeUserDir("user.aseprite-brushes" ); |
514 | return rf.getFirstOrCreateDefault(); |
515 | } |
516 | |
517 | } // namespace app |
518 | |