mirror of
https://github.com/aseprite/aseprite.git
synced 2025-04-01 01:20:25 +00:00
Load/save custom brushes
This commit is contained in:
parent
718888df91
commit
6fb5258e51
@ -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 <fstream>
|
||||
|
||||
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<RgbTraits> 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<GrayscaleTraits> 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<IndexedTraits> 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<RgbTraits> 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<GrayscaleTraits> 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<IndexedTraits> pixels(image);
|
||||
for (const auto& pixel : pixels) {
|
||||
data.push_back(pixel);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case IMAGE_BITMAP: {
|
||||
// Here we save bitmap format as indexed
|
||||
const LockImageBits<BitmapTraits> 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<int>(std::string(size)): 1),
|
||||
(angle ? base::convert_to<int>(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<int>(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");
|
||||
|
||||
//<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
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
|
||||
|
@ -24,6 +24,7 @@ namespace app {
|
||||
typedef std::vector<BrushSlot> 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<void> 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;
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
35
src/doc/brush_type.cpp
Normal file
35
src/doc/brush_type.cpp
Normal file
@ -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
|
@ -8,6 +8,8 @@
|
||||
#define DOC_BRUSH_TYPE_H_INCLUDED
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
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
|
||||
|
Loading…
x
Reference in New Issue
Block a user