diff --git a/src/app/app_brushes.cpp b/src/app/app_brushes.cpp index 41372f7e7..32efd115d 100644 --- a/src/app/app_brushes.cpp +++ b/src/app/app_brushes.cpp @@ -10,16 +10,170 @@ #endif #include "app/app_brushes.h" +#include "app/resource_finder.h" +#include "app/tools/ink_type.h" +#include "app/xml_document.h" +#include "app/xml_exception.h" +#include "base/base64.h" +#include "base/convert_to.h" +#include "base/path.h" +#include "base/serialization.h" +#include "doc/brush.h" +#include "doc/color.h" +#include "doc/image.h" +#include "doc/image_impl.h" + +#include namespace app { using namespace doc; +using namespace base::serialization; +using namespace base::serialization::little_endian; + +namespace { + +ImageRef load_xml_image(const TiXmlElement* imageElem) +{ + ImageRef image; + int w, h; + if (!imageElem->QueryIntAttribute("width", &w) == TIXML_SUCCESS || + !imageElem->QueryIntAttribute("height", &h) == TIXML_SUCCESS || + w < 0 || w > 9999 && + h < 0 || h > 9999) + return image; + + auto formatValue = imageElem->Attribute("format"); + if (!formatValue) + return image; + + const char* pixels_base64 = imageElem->GetText(); + if (!pixels_base64) + return image; + + base::buffer data; + base::decode_base64(pixels_base64, data); + auto it = data.begin(), end = data.end(); + + std::string formatStr = formatValue; + if (formatStr == "rgba") { + image.reset(Image::create(IMAGE_RGB, w, h)); + LockImageBits pixels(image.get()); + for (auto& pixel : pixels) { + if ((end - it) < 4) + break; + + int r = *it; ++it; + int g = *it; ++it; + int b = *it; ++it; + int a = *it; ++it; + + pixel = doc::rgba(r, g, b, a); + } + } + else if (formatStr == "grayscale") { + image.reset(Image::create(IMAGE_GRAYSCALE, w, h)); + LockImageBits pixels(image.get()); + for (auto& pixel : pixels) { + if ((end - it) < 2) + break; + + int v = *it; ++it; + int a = *it; ++it; + + pixel = doc::graya(v, a); + } + } + else if (formatStr == "indexed") { + image.reset(Image::create(IMAGE_INDEXED, w, h)); + LockImageBits pixels(image.get()); + for (auto& pixel : pixels) { + if (it == end) + break; + + pixel = *it; + ++it; + } + } + return image; +} + +void save_xml_image(TiXmlElement* imageElem, const Image* image) +{ + int w = image->width(); + int h = image->height(); + imageElem->SetAttribute("width", w); + imageElem->SetAttribute("height", h); + + std::string format; + switch (image->pixelFormat()) { + case IMAGE_RGB: format = "rgba"; break; + case IMAGE_GRAYSCALE: format = "grayscale"; break; + case IMAGE_INDEXED: format = "indexed"; break; + case IMAGE_BITMAP: format = "indexed"; break; // TODO add "bitmap" format + } + ASSERT(!format.empty()); + if (!format.empty()) + imageElem->SetAttribute("format", format.c_str()); + + base::buffer data; + data.reserve(h * image->getRowStrideSize()); + switch (image->pixelFormat()) { + case IMAGE_RGB:{ + const LockImageBits pixels(image); + for (const auto& pixel : pixels) { + data.push_back(doc::rgba_getr(pixel)); + data.push_back(doc::rgba_getg(pixel)); + data.push_back(doc::rgba_getb(pixel)); + data.push_back(doc::rgba_geta(pixel)); + } + break; + } + case IMAGE_GRAYSCALE:{ + const LockImageBits pixels(image); + for (const auto& pixel : pixels) { + data.push_back(doc::graya_getv(pixel)); + data.push_back(doc::graya_geta(pixel)); + } + break; + } + case IMAGE_INDEXED: { + const LockImageBits pixels(image); + for (const auto& pixel : pixels) { + data.push_back(pixel); + } + break; + } + case IMAGE_BITMAP: { + // Here we save bitmap format as indexed + const LockImageBits pixels(image); + for (const auto& pixel : pixels) { + data.push_back(pixel); // TODO save bitmap format as bitmap + } + break; + } + } + + std::string data_base64; + base::encode_base64(data, data_base64); + TiXmlText textElem(data_base64.c_str()); + imageElem->InsertEndChild(textElem); +} + +} // anonymous namespace AppBrushes::AppBrushes() { m_standard.push_back(BrushRef(new Brush(kCircleBrushType, 7, 0))); m_standard.push_back(BrushRef(new Brush(kSquareBrushType, 7, 0))); m_standard.push_back(BrushRef(new Brush(kLineBrushType, 7, 44))); + + load(userBrushesFilename()); +} + +AppBrushes::~AppBrushes() +{ + save(userBrushesFilename()); } AppBrushes::slot_id AppBrushes::addBrushSlot(const BrushSlot& brush) @@ -114,4 +268,211 @@ bool AppBrushes::isBrushSlotLocked(slot_id slot) const return false; } +static const int kBrushFlags = + int(BrushSlot::Flags::BrushType) | + int(BrushSlot::Flags::BrushSize) | + int(BrushSlot::Flags::BrushAngle); + +void AppBrushes::load(const std::string& filename) +{ + XmlDocumentRef doc = app::open_xml(filename); + TiXmlHandle handle(doc.get()); + TiXmlElement* brushElem = handle + .FirstChild("brushes") + .FirstChild("brush").ToElement(); + + while (brushElem) { + // flags + int flags = 0; + BrushRef brush; + app::Color fgColor; + app::Color bgColor; + tools::InkType inkType = tools::InkType::DEFAULT; + int inkOpacity = 255; + Shade shade; + bool pixelPerfect = false; + + // Brush + const char* type = brushElem->Attribute("type"); + const char* size = brushElem->Attribute("size"); + const char* angle = brushElem->Attribute("angle"); + if (type || size || angle) { + if (type) flags |= int(BrushSlot::Flags::BrushType); + if (size) flags |= int(BrushSlot::Flags::BrushSize); + if (angle) flags |= int(BrushSlot::Flags::BrushAngle); + brush.reset( + new Brush( + string_id_to_brush_type(type), + (size ? base::convert_to(std::string(size)): 1), + (angle ? base::convert_to(std::string(angle)): 0))); + } + + // Brush image + if (TiXmlElement* imageElem = brushElem->FirstChildElement("image")) { + ImageRef image = load_xml_image(imageElem); + if (image) { + if (!brush) + brush.reset(new Brush()); + brush->setImage(image.get()); + } + } + + // Colors + if (TiXmlElement* fgcolorElem = brushElem->FirstChildElement("fgcolor")) { + if (auto value = fgcolorElem->Attribute("value")) { + fgColor = app::Color::fromString(value); + flags |= int(BrushSlot::Flags::FgColor); + } + } + + if (TiXmlElement* bgcolorElem = brushElem->FirstChildElement("bgcolor")) { + if (auto value = bgcolorElem->Attribute("value")) { + bgColor = app::Color::fromString(value); + flags |= int(BrushSlot::Flags::BgColor); + } + } + + // Ink + if (TiXmlElement* inkTypeElem = brushElem->FirstChildElement("inktype")) { + if (auto value = inkTypeElem->Attribute("value")) { + inkType = app::tools::string_id_to_ink_type(value); + flags |= int(BrushSlot::Flags::InkType); + } + } + + if (TiXmlElement* inkOpacityElem = brushElem->FirstChildElement("inkopacity")) { + if (auto value = inkOpacityElem->Attribute("value")) { + inkOpacity = base::convert_to(std::string(value)); + flags |= int(BrushSlot::Flags::InkOpacity); + } + } + + // Shade + if (TiXmlElement* shadeElem = brushElem->FirstChildElement("shade")) { + if (auto value = shadeElem->Attribute("value")) { + shade = shade_from_string(value); + flags |= int(BrushSlot::Flags::Shade); + } + } + + // Pixel-perfect + if (TiXmlElement* pixelPerfectElem = brushElem->FirstChildElement("pixelperfect")) { + pixelPerfect = bool_attr_is_true(pixelPerfectElem, "value"); + flags |= int(BrushSlot::Flags::PixelPerfect); + } + + if (flags != 0) + flags |= int(BrushSlot::Flags::Locked); + + BrushSlot brushSlot(BrushSlot::Flags(flags), + brush, fgColor, bgColor, + inkType, inkOpacity, shade, + pixelPerfect); + m_slots.push_back(brushSlot); + + brushElem = brushElem->NextSiblingElement(); + } +} + +void AppBrushes::save(const std::string& filename) const +{ + XmlDocumentRef doc(new TiXmlDocument()); + TiXmlElement brushesElem("brushes"); + + // + + for (const auto& slot : m_slots) { + TiXmlElement brushElem("brush"); + if (slot.locked()) { + // Flags + int flags = int(slot.flags()); + + ASSERT(slot.brush()); + if (!slot.hasBrush()) + flags &= ~kBrushFlags; + + // Brush type + if (slot.hasBrush()) { + ASSERT(slot.brush()); + + if (flags & int(BrushSlot::Flags::BrushType)) { + brushElem.SetAttribute( + "type", brush_type_to_string_id(slot.brush()->type()).c_str()); + } + + if (flags & int(BrushSlot::Flags::BrushSize)) { + brushElem.SetAttribute("size", slot.brush()->size()); + } + + if (flags & int(BrushSlot::Flags::BrushAngle)) { + brushElem.SetAttribute("angle", slot.brush()->angle()); + } + + if (slot.brush()->type() == kImageBrushType && + slot.brush()->image()) { + TiXmlElement elem("image"); + save_xml_image(&elem, slot.brush()->image()); + brushElem.InsertEndChild(elem); + } + } + + // Colors + if (flags & int(BrushSlot::Flags::FgColor)) { + TiXmlElement elem("fgcolor"); + elem.SetAttribute("value", slot.fgColor().toString().c_str()); + brushElem.InsertEndChild(elem); + } + + if (flags & int(BrushSlot::Flags::BgColor)) { + TiXmlElement elem("bgcolor"); + elem.SetAttribute("value", slot.bgColor().toString().c_str()); + brushElem.InsertEndChild(elem); + } + + // Ink + if (flags & int(BrushSlot::Flags::InkType)) { + TiXmlElement elem("inktype"); + elem.SetAttribute( + "value", app::tools::ink_type_to_string_id(slot.inkType()).c_str()); + brushElem.InsertEndChild(elem); + } + + if (flags & int(BrushSlot::Flags::InkOpacity)) { + TiXmlElement elem("inkopacity"); + elem.SetAttribute("value", slot.inkOpacity()); + brushElem.InsertEndChild(elem); + } + + // Shade + if (flags & int(BrushSlot::Flags::Shade)) { + TiXmlElement elem("shade"); + elem.SetAttribute("value", shade_to_string(slot.shade()).c_str()); + brushElem.InsertEndChild(elem); + } + + // Pixel-perfect + if (flags & int(BrushSlot::Flags::PixelPerfect)) { + TiXmlElement elem("pixelperfect"); + elem.SetAttribute("value", slot.pixelPerfect() ? "true": "false"); + brushElem.InsertEndChild(elem); + } + } + + brushesElem.InsertEndChild(brushElem); + } + + TiXmlDeclaration declaration("1.0", "utf-8", ""); + doc->InsertEndChild(declaration); + doc->InsertEndChild(brushesElem); + save_xml(doc, filename); +} + +// static +std::string AppBrushes::userBrushesFilename() +{ + ResourceFinder rf; + rf.includeUserDir("user.aseprite-brushes"); + return rf.getFirstOrCreateDefault(); +} + } // namespace app diff --git a/src/app/app_brushes.h b/src/app/app_brushes.h index dc451c2b4..f8a674138 100644 --- a/src/app/app_brushes.h +++ b/src/app/app_brushes.h @@ -24,6 +24,7 @@ namespace app { typedef std::vector BrushSlots; AppBrushes(); + ~AppBrushes(); // Adds a new brush and returns the slot number where the brush // is now available. @@ -43,6 +44,10 @@ namespace app { base::Signal0 ItemsChange; private: + void load(const std::string& filename); + void save(const std::string& filename) const; + static std::string userBrushesFilename(); + doc::Brushes m_standard; BrushSlots m_slots; }; diff --git a/src/app/tools/ink_type.cpp b/src/app/tools/ink_type.cpp index 577cdabdc..2f5bb809b 100644 --- a/src/app/tools/ink_type.cpp +++ b/src/app/tools/ink_type.cpp @@ -26,5 +26,27 @@ std::string ink_type_to_string(InkType inkType) return "Unknown"; } +std::string ink_type_to_string_id(InkType inkType) +{ + switch (inkType) { + case tools::InkType::SIMPLE: return "simple"; + case tools::InkType::ALPHA_COMPOSITING: return "alpha_compositing"; + case tools::InkType::COPY_COLOR: return "copy_color"; + case tools::InkType::LOCK_ALPHA: return "lock_alpha"; + case tools::InkType::SHADING: return "shading"; + } + return "unknown"; +} + +InkType string_id_to_ink_type(const std::string& s) +{ + if (s == "simple") return tools::InkType::SIMPLE; + if (s == "alpha_compositing") return tools::InkType::ALPHA_COMPOSITING; + if (s == "copy_color") return tools::InkType::COPY_COLOR; + if (s == "lock_alpha") return tools::InkType::LOCK_ALPHA; + if (s == "shading") return tools::InkType::SHADING; + return tools::InkType::DEFAULT; +} + } // namespace tools } // namespace app diff --git a/src/app/tools/ink_type.h b/src/app/tools/ink_type.h index 7d92b4366..592e6261c 100644 --- a/src/app/tools/ink_type.h +++ b/src/app/tools/ink_type.h @@ -29,6 +29,8 @@ namespace tools { } std::string ink_type_to_string(InkType inkType); + std::string ink_type_to_string_id(InkType inkType); + InkType string_id_to_ink_type(const std::string& s); } // namespace tools } // namespace app diff --git a/src/doc/CMakeLists.txt b/src/doc/CMakeLists.txt index 1a57c0607..7dc6a1afe 100644 --- a/src/doc/CMakeLists.txt +++ b/src/doc/CMakeLists.txt @@ -15,6 +15,7 @@ add_library(doc-lib blend_funcs.cpp blend_mode.cpp brush.cpp + brush_type.cpp cel.cpp cel_data.cpp cel_data_io.cpp diff --git a/src/doc/brush_type.cpp b/src/doc/brush_type.cpp new file mode 100644 index 000000000..c6f01ee1e --- /dev/null +++ b/src/doc/brush_type.cpp @@ -0,0 +1,35 @@ +// Aseprite Document Library +// Copyright (c) 2015 David Capello +// +// This file is released under the terms of the MIT license. +// Read LICENSE.txt for more information. + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "doc/brush_type.h" + +namespace doc { + +std::string brush_type_to_string_id(BrushType brushType) +{ + switch (brushType) { + case kCircleBrushType: return "circle"; + case kSquareBrushType: return "square"; + case kLineBrushType: return "line"; + case kImageBrushType: return "image"; + } + return "unknown"; +} + +BrushType string_id_to_brush_type(const std::string& s) +{ + if (s == "circle") return kCircleBrushType; + if (s == "square") return kSquareBrushType; + if (s == "line") return kLineBrushType; + if (s == "image") return kImageBrushType; + return kFirstBrushType; +} + +} // namespace doc diff --git a/src/doc/brush_type.h b/src/doc/brush_type.h index cdb52560c..81955b88d 100644 --- a/src/doc/brush_type.h +++ b/src/doc/brush_type.h @@ -8,6 +8,8 @@ #define DOC_BRUSH_TYPE_H_INCLUDED #pragma once +#include + namespace doc { enum BrushType { @@ -20,6 +22,9 @@ namespace doc { kLastBrushType = kImageBrushType, }; + std::string brush_type_to_string_id(BrushType brushType); + BrushType string_id_to_brush_type(const std::string& s); + } // namespace doc #endif