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
28namespace app {
29
30using namespace doc;
31using namespace base::serialization;
32using namespace base::serialization::little_endian;
33
34namespace {
35
36ImageRef 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
112void 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
176AppBrushes::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
192AppBrushes::~AppBrushes()
193{
194 if (!m_userBrushesFilename.empty())
195 save(m_userBrushesFilename);
196}
197
198AppBrushes::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
213void 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
228void AppBrushes::removeAllBrushSlots()
229{
230 while (!m_slots.empty())
231 m_slots.erase(--m_slots.end());
232
233 ItemsChange();
234}
235
236bool 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
243BrushSlot 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
252void 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
261void 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
270void 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
279bool 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
290static const int kBrushFlags =
291 int(BrushSlot::Flags::BrushType) |
292 int(BrushSlot::Flags::BrushSize) |
293 int(BrushSlot::Flags::BrushAngle);
294
295void 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
404void 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
510std::string AppBrushes::userBrushesFilename()
511{
512 ResourceFinder rf;
513 rf.includeUserDir("user.aseprite-brushes");
514 return rf.getFirstOrCreateDefault();
515}
516
517} // namespace app
518