From a4d8fc52bfe949f61038945999cd2a1779ecd67f Mon Sep 17 00:00:00 2001 From: David Capello Date: Thu, 18 Oct 2018 15:29:16 -0300 Subject: [PATCH] Manage color profiles (fix #1576) --- INSTALL.md | 8 +- README.md | 9 +- data/pref.xml | 16 +- data/strings/en.ini | 19 +- data/widgets/options.xml | 42 +++- data/widgets/sprite_properties.xml | 12 +- docs/ase-file-specs.md | 22 ++- laf | 2 +- src/CMakeLists.txt | 5 - src/README.md | 23 +-- src/app/CMakeLists.txt | 4 + src/app/app.h | 2 + src/app/cmd/assign_color_profile.cpp | 49 +++++ src/app/cmd/assign_color_profile.h | 42 ++++ src/app/cmd/convert_color_profile.cpp | 104 ++++++++++ src/app/cmd/convert_color_profile.h | 43 +++++ src/app/color_spaces.cpp | 79 ++++++++ src/app/color_spaces.h | 40 ++++ src/app/commands/cmd_options.cpp | 97 ++++++++++ src/app/commands/cmd_sprite_properties.cpp | 79 +++++++- src/app/crash/backup_observer.cpp | 4 +- src/app/crash/read_document.cpp | 23 +++ src/app/crash/write_document.cpp | 19 ++ src/app/doc.cpp | 30 +++ src/app/doc.h | 11 +- src/app/doc_diff.cpp | 5 + src/app/doc_diff.h | 4 +- src/app/doc_observer.h | 3 + src/app/file/ase_format.cpp | 52 +++++ src/app/file/gif_format.cpp | 15 +- src/app/file/jpeg_format.cpp | 213 ++++++++++++++++++--- src/app/file/png_format.cpp | 161 +++++++++++++++- src/app/modules/gfx.cpp | 10 +- src/app/ui/color_selector.cpp | 20 +- src/app/ui/color_selector.h | 6 + src/app/ui/editor/editor.cpp | 22 ++- src/app/ui/editor/editor.h | 3 + src/app/ui/palette_view.cpp | 6 +- src/app/ui/palette_view.h | 4 +- src/dio/LICENSE.txt | 1 + src/dio/README.md | 5 +- src/dio/aseprite_common.h | 8 + src/dio/aseprite_decoder.cpp | 47 +++++ src/dio/aseprite_decoder.h | 2 + src/dio/decoder.cpp | 6 + src/dio/decoder.h | 3 + src/doc/LICENSE.txt | 1 + src/doc/README.md | 3 +- src/doc/image_spec.h | 8 +- src/doc/sprite.cpp | 8 +- src/doc/sprite.h | 3 + 51 files changed, 1303 insertions(+), 100 deletions(-) create mode 100644 src/app/cmd/assign_color_profile.cpp create mode 100644 src/app/cmd/assign_color_profile.h create mode 100644 src/app/cmd/convert_color_profile.cpp create mode 100644 src/app/cmd/convert_color_profile.h create mode 100644 src/app/color_spaces.cpp create mode 100644 src/app/color_spaces.h diff --git a/INSTALL.md b/INSTALL.md index 7fc476b10..97df16fbb 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -204,7 +204,7 @@ Skia. You can always check the [official Skia instructions](https://skia.org/user/build) and select -the OS you are building for. Aseprite uses the `aseprite-m67` Skia +the OS you are building for. Aseprite uses the `aseprite-m71` Skia branch from `https://github.com/aseprite/skia`. ## Skia on Windows @@ -234,7 +234,7 @@ Then: Just ignore it.) cd C:\deps - git clone -b aseprite-m67 https://github.com/aseprite/skia.git + git clone -b aseprite-m71 https://github.com/aseprite/skia.git cd skia python tools/git-sync-deps @@ -265,7 +265,7 @@ several minutes to finish: mkdir $HOME/deps cd $HOME/deps git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git - git clone -b aseprite-m67 https://github.com/aseprite/skia.git + git clone -b aseprite-m71 https://github.com/aseprite/skia.git export PATH="${PWD}/depot_tools:${PATH}" cd skia python tools/git-sync-deps @@ -290,7 +290,7 @@ several minutes to finish: mkdir $HOME/deps cd $HOME/deps git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git - git clone -b aseprite-m67 https://github.com/aseprite/skia.git + git clone -b aseprite-m71 https://github.com/aseprite/skia.git export PATH="${PWD}/depot_tools:${PATH}" cd skia python tools/git-sync-deps diff --git a/README.md b/README.md index c54488072..226e50d4f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ # Aseprite -*Copyright (C) 2001-2018 David Capello* [![Build Status](https://travis-ci.org/aseprite/aseprite.svg)](https://travis-ci.org/aseprite/aseprite) [![Build status](https://ci.appveyor.com/api/projects/status/kdu2gt7ls014i25h?svg=true)](https://ci.appveyor.com/project/dacap/aseprite) @@ -50,8 +49,12 @@ You can ask for help in: ## Authors -* [David Capello](https://davidcapello.com/): Lead developer, bug fixing, new features, designer, and maintainer. -* [Gaspar Capello](https://github.com/Gasparoken): Developer, bug fixing. +[Igara Studio](https://www.igarastudio.com/) is developing Aseprite: + +* [David Capello](https://davidcapello.com/): Lead developer, fixing + issues, new features, and user support. +* [Gaspar Capello](https://github.com/Gasparoken): Developer, fixing + issues and new features. ## Credits diff --git a/data/pref.xml b/data/pref.xml index ba5a9199a..6e2414e15 100644 --- a/data/pref.xml +++ b/data/pref.xml @@ -1,6 +1,7 @@ - + + @@ -97,6 +98,13 @@ + + + + + + + @@ -301,6 +309,12 @@
+
+
diff --git a/data/strings/en.ini b/data/strings/en.ini index b492e0369..515f77461 100644 --- a/data/strings/en.ini +++ b/data/strings/en.ini @@ -1,5 +1,6 @@ # Aseprite -# Copyright (C) 2016-2018 by David Capello +# Copyright (C) 2018 Igara Studio S.A. +# Copyright (C) 2016-2018 David Capello [advanced_mode] title = Warning - Important @@ -565,6 +566,7 @@ dont_show_tooltip = << - + + @@ -8,6 +9,7 @@ + @@ -114,6 +116,38 @@ + + + + + + + + + + + + + + + diff --git a/docs/ase-file-specs.md b/docs/ase-file-specs.md index 8af0f86b2..5dbd69e20 100644 --- a/docs/ase-file-specs.md +++ b/docs/ase-file-specs.md @@ -1,7 +1,5 @@ # Aseprite File Format (.ase/.aseprite) Specifications -> Copyright (C) 2001-2018 by David Capello - 1. [References](#references) 2. [Introduction](#introduction) 3. [Header](#header) @@ -213,6 +211,26 @@ Adds extra information to the latest read cel. FIXED Height of the cel in the sprite BYTE[16] For future use (set to zero) +### Color Profile Chunk (0x2007) + +Color profile for RGB or grayscale values. + + WORD Type + 0 - no color profile (as in old .aseprite files) + 1 - use sRGB + 2 - use the embedded ICC profile + WORD Flags + 1 - use special fixed gamma + FIXED Fixed gamma (1.0 = linear) + Note: The gamma in sRGB is 2.2 in overall but it doesn't use + a this fixed gamma, because sRGB uses different gamma sections + (linear and non-linear). If sRGB is specified with a fixed + gamma = 1.0, it means that this is Linear sRGB. + BYTE[8] Reserved (set to zero] + + If type = ICC: + DWORD ICC profile data length + BYTE[] ICC profile data. More info: http://www.color.org/ICC1V42.pdf + ### Mask Chunk (0x2016) DEPRECATED SHORT X position diff --git a/laf b/laf index ba09f9892..f4a08a23e 160000 --- a/laf +++ b/laf @@ -1 +1 @@ -Subproject commit ba09f989212548d8fc5659cb716398f9407c351a +Subproject commit f4a08a23e8de9a482c8167ae084f4148803673be diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9524962d9..661bb7862 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -16,11 +16,6 @@ if(MSVC) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /LTCG") endif() - # Do not link with libcmt.lib (to avoid duplicated symbols with msvcrtd.lib) - if(CMAKE_BUILD_TYPE STREQUAL Debug) - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /NODEFAULTLIB:LIBCMT") - endif() - add_definitions(-D_SCL_SECURE_NO_WARNINGS) endif(MSVC) diff --git a/src/README.md b/src/README.md index 617232622..d0ff4b8f3 100644 --- a/src/README.md +++ b/src/README.md @@ -28,31 +28,28 @@ because they don't depend on any other component. * [cfg](cfg/) (base): Library to load/save .ini files. * [gen](gen/) (base): Helper utility to generate C++ files from different XMLs. * [net](net/) (base): Networking library to send HTTP requests. + * laf/[os](https://github.com/aseprite/laf/tree/master/os) (base, gfx, wacom): OS input/output. ## Level 2 - * [doc](doc/) (base, fixmath, gfx): Document model library. - * laf/[os](https://github.com/aseprite/laf/tree/master/os) (base, gfx, wacom): OS input/output. - -## Level 3 - - * [filters](filters/) (base, doc, gfx): Effects for images. - * [render](render/) (base, doc, gfx): Library to render documents. + * [doc](doc/) (base, fixmath, gfx, os): Document model library. * [ui](ui/) (base, gfx, os): Portable UI library (buttons, windows, text fields, etc.) * [updater](updater/) (base, cfg, net): Component to check for updates. -## Level 4 +## Level 3 * [dio](dio/) (base, doc, fixmath, flic): Load/save sprites/documents. + * [filters](filters/) (base, doc, gfx): Effects for images. + * [render](render/) (base, doc, gfx): Library to render documents. + +## Level 4 + + * [app](app/) (base, doc, dio, filters, fixmath, flic, gfx, pen, render, scripting, os, ui, undo, updater) + * [desktop](desktop/) (base, doc, dio, render): Integration with the desktop (Windows Explorer, Finder, GNOME, KDE, etc.) ## Level 5 - * [app](app/) (base, doc, dio, filters, fixmath, flic, gfx, pen, render, scripting, os, ui, undo, updater) - -## Level 6 - * [main](main/) (app, base, os, ui) - * [desktop](desktop/) (base, doc, dio, render): Integration with the desktop (Windows Explorer, Finder, GNOME, KDE, etc.) # Debugging Tricks diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 5791b59df..7ddb24f9f 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -1,4 +1,5 @@ # Aseprite +# Copyright (C) 2018 Igara Studio S.A. # Copyright (C) 2001-2018 David Capello # Generate a ui::Widget for each widget in a XML file @@ -431,12 +432,14 @@ add_library(app-lib cmd/add_layer.cpp cmd/add_palette.cpp cmd/add_slice.cpp + cmd/assign_color_profile.cpp cmd/background_from_layer.cpp cmd/clear_cel.cpp cmd/clear_image.cpp cmd/clear_mask.cpp cmd/clear_rect.cpp cmd/configure_background.cpp + cmd/convert_color_profile.cpp cmd/copy_cel.cpp cmd/copy_frame.cpp cmd/copy_rect.cpp @@ -500,6 +503,7 @@ add_library(app-lib cmd_transaction.cpp color.cpp color_picker.cpp + color_spaces.cpp color_utils.cpp commands/cmd_background_from_layer.cpp commands/cmd_cel_opacity.cpp diff --git a/src/app/app.h b/src/app/app.h index 0669ad550..7b692a270 100644 --- a/src/app/app.h +++ b/src/app/app.h @@ -1,4 +1,5 @@ // Aseprite +// Copyright (C) 2018 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -119,6 +120,7 @@ namespace app { // App Signals obs::signal Exit; obs::signal PaletteChange; + obs::signal ColorSpaceChange; private: class CoreModules; diff --git a/src/app/cmd/assign_color_profile.cpp b/src/app/cmd/assign_color_profile.cpp new file mode 100644 index 000000000..ff6e96223 --- /dev/null +++ b/src/app/cmd/assign_color_profile.cpp @@ -0,0 +1,49 @@ +// Aseprite +// Copyright (C) 2018 Igara Studio S.A. +// +// This program is distributed under the terms of +// the End-User License Agreement for Aseprite. + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "app/cmd/assign_color_profile.h" + +#include "app/doc.h" +#include "app/doc_event.h" +#include "doc/sprite.h" + +namespace app { +namespace cmd { + +AssignColorProfile::AssignColorProfile(doc::Sprite* sprite, const gfx::ColorSpacePtr& cs) + : WithSprite(sprite) + , m_oldCS(sprite->colorSpace()) + , m_newCS(cs) +{ +} + +void AssignColorProfile::onExecute() +{ + doc::Sprite* spr = sprite(); + spr->setColorSpace(m_newCS); + spr->incrementVersion(); +} + +void AssignColorProfile::onUndo() +{ + doc::Sprite* spr = sprite(); + spr->setColorSpace(m_oldCS); + spr->incrementVersion(); +} + +void AssignColorProfile::onFireNotifications() +{ + doc::Sprite* sprite = this->sprite(); + Doc* doc = static_cast(sprite->document()); + doc->notifyColorSpaceChanged(); +} + +} // namespace cmd +} // namespace app diff --git a/src/app/cmd/assign_color_profile.h b/src/app/cmd/assign_color_profile.h new file mode 100644 index 000000000..5de5d48db --- /dev/null +++ b/src/app/cmd/assign_color_profile.h @@ -0,0 +1,42 @@ +// Aseprite +// Copyright (C) 2018 Igara Studio S.A. +// +// This program is distributed under the terms of +// the End-User License Agreement for Aseprite. + +#ifndef APP_CMD_ASSIGN_COLOR_PROFILE_H_INCLUDED +#define APP_CMD_ASSIGN_COLOR_PROFILE_H_INCLUDED +#pragma once + +#include "app/cmd.h" +#include "app/cmd/with_sprite.h" +#include "gfx/color_space.h" + +namespace app { +namespace cmd { + + class AssignColorProfile : public Cmd, + public WithSprite { + public: + AssignColorProfile(doc::Sprite* sprite, const gfx::ColorSpacePtr& cs); + + protected: + void onExecute() override; + void onUndo() override; + void onFireNotifications() override; + size_t onMemSize() const override { + return sizeof(*this) + + 2*sizeof(gfx::ColorSpace) + + m_oldCS->iccSize() + + m_newCS->iccSize(); + } + + private: + gfx::ColorSpacePtr m_oldCS; + gfx::ColorSpacePtr m_newCS; + }; + +} // namespace cmd +} // namespace app + +#endif diff --git a/src/app/cmd/convert_color_profile.cpp b/src/app/cmd/convert_color_profile.cpp new file mode 100644 index 000000000..853743bd6 --- /dev/null +++ b/src/app/cmd/convert_color_profile.cpp @@ -0,0 +1,104 @@ +// Aseprite +// Copyright (C) 2018 Igara Studio S.A. +// +// This program is distributed under the terms of +// the End-User License Agreement for Aseprite. + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "app/cmd/convert_color_profile.h" + +#include "app/cmd/assign_color_profile.h" +#include "app/cmd/replace_image.h" +#include "app/cmd/set_palette.h" +#include "app/doc.h" +#include "doc/cels_range.h" +#include "doc/palette.h" +#include "doc/sprite.h" +#include "os/color_space.h" +#include "os/system.h" + +namespace app { +namespace cmd { + +ConvertColorProfile::ConvertColorProfile(doc::Sprite* sprite, const gfx::ColorSpacePtr& newCS) + : WithSprite(sprite) +{ + os::System* system = os::instance(); + + ASSERT(sprite->colorSpace()); + ASSERT(newCS); + + auto srcOCS = system->createColorSpace(sprite->colorSpace()); + auto dstOCS = system->createColorSpace(newCS); + + ASSERT(srcOCS); + ASSERT(dstOCS); + + auto conversion = system->convertBetweenColorSpace(srcOCS, dstOCS); + // Convert images + if (sprite->pixelFormat() == doc::IMAGE_RGB) { + for (Cel* cel : sprite->uniqueCels()) { + ImageRef old_image = cel->imageRef(); + + ImageSpec spec = old_image->spec(); + spec.setColorSpace(newCS); + ImageRef new_image(Image::create(spec)); + + if (conversion) { + for (int y=0; yconvert((uint32_t*)new_image->getPixelAddress(0, y), + (const uint32_t*)old_image->getPixelAddress(0, y), + spec.width()); + } + } + else { + new_image->copy(old_image.get(), gfx::Clip(0, 0, old_image->bounds())); + } + + m_seq.add(new cmd::ReplaceImage(sprite, old_image, new_image)); + } + } + + if (conversion) { + // Convert palette + if (sprite->pixelFormat() != doc::IMAGE_GRAYSCALE) { + for (auto& pal : sprite->getPalettes()) { + Palette newPal(pal->frame(), pal->size()); + + for (int i=0; isize(); ++i) { + color_t oldCol = pal->entry(i); + color_t newCol = pal->entry(i); + conversion->convert((uint32_t*)&newCol, + (const uint32_t*)&oldCol, 1); + newPal.setEntry(i, newCol); + } + + if (*pal != newPal) + m_seq.add(new cmd::SetPalette(sprite, pal->frame(), &newPal)); + } + } + } + + m_seq.add(new cmd::AssignColorProfile(sprite, newCS)); +} + +void ConvertColorProfile::onExecute() +{ + m_seq.execute(context()); +} + +void ConvertColorProfile::onUndo() +{ + m_seq.undo(); +} + +void ConvertColorProfile::onRedo() +{ + m_seq.redo(); +} + +} // namespace cmd +} // namespace app diff --git a/src/app/cmd/convert_color_profile.h b/src/app/cmd/convert_color_profile.h new file mode 100644 index 000000000..0b1867ac4 --- /dev/null +++ b/src/app/cmd/convert_color_profile.h @@ -0,0 +1,43 @@ +// Aseprite +// Copyright (C) 2018 Igara Studio S.A. +// +// This program is distributed under the terms of +// the End-User License Agreement for Aseprite. + +#ifndef APP_CMD_CONVERT_COLOR_PROFILE_H_INCLUDED +#define APP_CMD_CONVERT_COLOR_PROFILE_H_INCLUDED +#pragma once + +#include "app/cmd.h" +#include "app/cmd/with_sprite.h" +#include "app/cmd_sequence.h" +#include "gfx/color_space.h" + +namespace gfx { + class ColorSpace; +} + +namespace app { +namespace cmd { + + class ConvertColorProfile : public Cmd, + public WithSprite { + public: + ConvertColorProfile(doc::Sprite* sprite, const gfx::ColorSpacePtr& newCS); + + protected: + void onExecute() override; + void onUndo() override; + void onRedo() override; + size_t onMemSize() const override { + return sizeof(*this) + m_seq.memSize(); + } + + private: + CmdSequence m_seq; + }; + +} // namespace cmd +} // namespace app + +#endif diff --git a/src/app/color_spaces.cpp b/src/app/color_spaces.cpp new file mode 100644 index 000000000..e50d49654 --- /dev/null +++ b/src/app/color_spaces.cpp @@ -0,0 +1,79 @@ +// Aseprite +// Copyright (C) 2018 Igara Studio S.A. +// +// This program is distributed under the terms of +// the End-User License Agreement for Aseprite. + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "app/color_spaces.h" + +#include "app/doc.h" +#include "app/modules/editors.h" +#include "app/ui/editor/editor.h" +#include "os/display.h" +#include "os/system.h" + +namespace app { + +os::ColorSpacePtr get_screen_color_space() +{ + return os::instance()->defaultDisplay()->colorSpace(); +} + +os::ColorSpacePtr get_current_color_space() +{ + if (current_editor) + return current_editor->document()->osColorSpace(); + else + return get_screen_color_space(); +} + +////////////////////////////////////////////////////////////////////// +// Color conversion + +ConvertCS::ConvertCS() +{ + auto srcCS = get_current_color_space(); + auto dstCS = get_screen_color_space(); + if (srcCS && dstCS) + m_conversion = os::instance()->convertBetweenColorSpace(srcCS, dstCS); +} + +ConvertCS::ConvertCS(const os::ColorSpacePtr& srcCS, + const os::ColorSpacePtr& dstCS) +{ + m_conversion = os::instance()->convertBetweenColorSpace(srcCS, dstCS); +} + +ConvertCS::ConvertCS(ConvertCS&& that) + : m_conversion(std::move(that.m_conversion)) +{ +} + +gfx::Color ConvertCS::operator()(const gfx::Color c) +{ + if (m_conversion) { + gfx::Color out; + m_conversion->convert((uint32_t*)&out, (const uint32_t*)&c, 1); + return out; + } + else { + return c; + } +} + +ConvertCS convert_from_current_to_screen_color_space() +{ + return ConvertCS(); +} + +ConvertCS convert_from_custom_to_srgb(const os::ColorSpacePtr& from) +{ + return ConvertCS(from, + os::instance()->createColorSpace(gfx::ColorSpace::MakeSRGB())); +} + +} // namespace app diff --git a/src/app/color_spaces.h b/src/app/color_spaces.h new file mode 100644 index 000000000..f43968b3a --- /dev/null +++ b/src/app/color_spaces.h @@ -0,0 +1,40 @@ +// Aseprite +// Copyright (C) 2018 Igara Studio S.A. +// +// This program is distributed under the terms of +// the End-User License Agreement for Aseprite. + +#ifndef APP_COLOR_SPACES_H_INCLUDED +#define APP_COLOR_SPACES_H_INCLUDED +#pragma once + +#include "gfx/color.h" +#include "os/color_space.h" + +#include + +namespace app { + + os::ColorSpacePtr get_screen_color_space(); + + // Returns the color space of the current document. + os::ColorSpacePtr get_current_color_space(); + + class ConvertCS { + public: + ConvertCS(); + ConvertCS(const os::ColorSpacePtr& srcCS, + const os::ColorSpacePtr& dstCS); + ConvertCS(ConvertCS&&); + ConvertCS& operator=(const ConvertCS&) = delete; + gfx::Color operator()(const gfx::Color c); + private: + std::unique_ptr m_conversion; + }; + + ConvertCS convert_from_current_to_screen_color_space(); + ConvertCS convert_from_custom_to_srgb(const os::ColorSpacePtr& from); + +} // namespace app + +#endif diff --git a/src/app/commands/cmd_options.cpp b/src/app/commands/cmd_options.cpp index c9074023a..f98a43905 100644 --- a/src/app/commands/cmd_options.cpp +++ b/src/app/commands/cmd_options.cpp @@ -1,4 +1,5 @@ // Aseprite +// Copyright (C) 2018 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -49,10 +50,35 @@ static const char* kSectionExtensionsId = "section_extensions"; static const char* kInfiniteSymbol = "\xE2\x88\x9E"; // Infinite symbol (UTF-8) +static app::gen::ColorProfileBehavior filesWithCsMap[] = { + app::gen::ColorProfileBehavior::DISABLE, + app::gen::ColorProfileBehavior::EMBEDDED, + app::gen::ColorProfileBehavior::CONVERT, + app::gen::ColorProfileBehavior::ASSIGN, + app::gen::ColorProfileBehavior::ASK, +}; + +static app::gen::ColorProfileBehavior missingCsMap[] = { + app::gen::ColorProfileBehavior::DISABLE, + app::gen::ColorProfileBehavior::ASSIGN, + app::gen::ColorProfileBehavior::ASK, +}; + using namespace ui; class OptionsWindow : public app::gen::Options { + class ColorSpaceItem : public ListItem { + public: + ColorSpaceItem(const os::ColorSpacePtr& cs) + : ListItem(cs->gfxColorSpace()->name()), + m_cs(cs) { + } + os::ColorSpacePtr cs() const { return m_cs; } + private: + os::ColorSpacePtr m_cs; + }; + class ThemeItem : public ListItem { public: ThemeItem(const std::string& path, @@ -153,6 +179,21 @@ public: recentFiles()->setValue(m_pref.general.recentItems()); clearRecentFiles()->Click.connect(base::Bind(&OptionsWindow::onClearRecentFiles, this)); + // Color profiles + resetColorManagement()->Click.connect(base::Bind(&OptionsWindow::onResetColorManagement, this)); + colorManagement()->Click.connect(base::Bind(&OptionsWindow::onColorManagement, this)); + { + os::instance()->listColorSpaces(m_colorSpaces); + for (auto& cs : m_colorSpaces) { + if (cs->gfxColorSpace()->type() != gfx::ColorSpace::None) + workingRgbCs()->addItem(new ColorSpaceItem(cs)); + } + updateColorProfileControls(m_pref.color.manage(), + m_pref.color.workingRgbSpace(), + m_pref.color.filesWithProfile(), + m_pref.color.missingProfile()); + } + // Alerts resetAlerts()->Click.connect(base::Bind(&OptionsWindow::onResetAlerts, this)); @@ -444,6 +485,14 @@ public: m_pref.guides.autoGuidesColor(autoGuidesColor()->getColor()); m_pref.slices.defaultColor(defaultSliceColor()->getColor()); + m_pref.color.workingRgbSpace( + workingRgbCs()->getItemText( + workingRgbCs()->getSelectedItemIndex())); + m_pref.color.filesWithProfile( + filesWithCsMap[filesWithCs()->getSelectedItemIndex()]); + m_pref.color.missingProfile( + missingCsMap[missingCs()->getSelectedItemIndex()]); + m_curPref->show.grid(gridVisible()->isSelected()); m_curPref->grid.bounds(gridBounds()); m_curPref->grid.color(gridColor()->getColor()); @@ -627,6 +676,53 @@ private: App::instance()->recentFiles()->clear(); } + void onColorManagement() { + const bool state = colorManagement()->isSelected(); + workingRgbCsLabel()->setEnabled(state); + workingRgbCs()->setEnabled(state); + filesWithCsLabel()->setEnabled(state); + filesWithCs()->setEnabled(state); + missingCsLabel()->setEnabled(state); + missingCs()->setEnabled(state); + } + + void onResetColorManagement() { + updateColorProfileControls(m_pref.color.manage.defaultValue(), + m_pref.color.workingRgbSpace.defaultValue(), + m_pref.color.filesWithProfile.defaultValue(), + m_pref.color.missingProfile.defaultValue()); + } + + void updateColorProfileControls(const bool manage, + const std::string& workingRgbSpace, + const app::gen::ColorProfileBehavior& filesWithProfile, + const app::gen::ColorProfileBehavior& missingProfile) { + colorManagement()->setSelected(manage); + + for (auto child : *workingRgbCs()) { + if (child->text() == workingRgbSpace) { + workingRgbCs()->setSelectedItem(child); + break; + } + } + + for (int i=0; isetSelectedItemIndex(i); + break; + } + } + + for (int i=0; isetSelectedItemIndex(i); + break; + } + } + + onColorManagement(); + } + void onResetAlerts() { fileFormatDoesntSupportAlert()->resetWithDefaultValue(); exportAnimationInSequenceAlert()->resetWithDefaultValue(); @@ -1124,6 +1220,7 @@ private: std::string m_restoreThisTheme; int m_restoreScreenScaling; int m_restoreUIScaling; + std::vector m_colorSpaces; }; class OptionsCommand : public Command { diff --git a/src/app/commands/cmd_sprite_properties.cpp b/src/app/commands/cmd_sprite_properties.cpp index 1b917df2b..c5d12ad5f 100644 --- a/src/app/commands/cmd_sprite_properties.cpp +++ b/src/app/commands/cmd_sprite_properties.cpp @@ -1,4 +1,5 @@ // Aseprite +// Copyright (C) 2018 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -8,12 +9,15 @@ #include "config.h" #endif +#include "app/cmd/assign_color_profile.h" +#include "app/cmd/convert_color_profile.h" #include "app/cmd/set_pixel_ratio.h" #include "app/color.h" #include "app/commands/command.h" #include "app/context_access.h" #include "app/doc_api.h" #include "app/modules/gui.h" +#include "app/pref/preferences.h" #include "app/tx.h" #include "app/ui/color_button.h" #include "app/util/pixel_ratio.h" @@ -22,12 +26,13 @@ #include "doc/image.h" #include "doc/palette.h" #include "doc/sprite.h" +#include "fmt/format.h" +#include "os/color_space.h" +#include "os/system.h" #include "ui/ui.h" #include "sprite_properties.xml.h" -#include - namespace app { using namespace ui; @@ -56,11 +61,15 @@ bool SpritePropertiesCommand::onEnabled(Context* context) void SpritePropertiesCommand::onExecute(Context* context) { std::string imgtype_text; - char buf[256]; - ColorButton* color_button = NULL; + ColorButton* color_button = nullptr; + + // List of available color profiles + std::vector colorSpaces; + os::instance()->listColorSpaces(colorSpaces); // Load the window widget app::gen::SpriteProperties window; + int selectedColorProfile = -1; // Get sprite properties and fill frame fields { @@ -77,8 +86,8 @@ void SpritePropertiesCommand::onExecute(Context* context) imgtype_text = "Grayscale"; break; case IMAGE_INDEXED: - std::sprintf(buf, "Indexed (%d colors)", sprite->palette(0)->size()); - imgtype_text = buf; + imgtype_text = fmt::format("Indexed ({0} colors)", + sprite->palette(0)->size()); break; default: ASSERT(false); @@ -116,6 +125,64 @@ void SpritePropertiesCommand::onExecute(Context* context) // Pixel ratio window.pixelRatio()->setValue( base::convert_to(sprite->pixelRatio())); + + // Color profile + selectedColorProfile = -1; + int i = 0; + for (auto& cs : colorSpaces) { + if (cs->gfxColorSpace()->nearlyEqual(*sprite->colorSpace())) { + selectedColorProfile = i; + break; + } + ++i; + } + if (selectedColorProfile < 0) { + colorSpaces.push_back(os::instance()->createColorSpace(sprite->colorSpace())); + selectedColorProfile = colorSpaces.size()-1; + } + + for (auto& cs : colorSpaces) + window.colorProfile()->addItem(cs->gfxColorSpace()->name()); + window.colorProfile()->setSelectedItemIndex(selectedColorProfile); + + auto updateButtons = + [&] { + bool enabled = (selectedColorProfile != window.colorProfile()->getSelectedItemIndex()); + window.assignColorProfile()->setEnabled(enabled); + window.convertColorProfile()->setEnabled(enabled); + window.ok()->setEnabled(!enabled); + }; + + window.assignColorProfile()->setEnabled(false); + window.convertColorProfile()->setEnabled(false); + window.colorProfile()->Change.connect(updateButtons); + + window.assignColorProfile()->Click.connect( + [&](Event&){ + selectedColorProfile = window.colorProfile()->getSelectedItemIndex(); + + ContextWriter writer(context); + Sprite* sprite(writer.sprite()); + Tx tx(writer.context(), "Assign Color Profile"); + tx(new cmd::AssignColorProfile( + sprite, colorSpaces[selectedColorProfile]->gfxColorSpace())); + tx.commit(); + + updateButtons(); + }); + window.convertColorProfile()->Click.connect( + [&](Event&){ + selectedColorProfile = window.colorProfile()->getSelectedItemIndex(); + + ContextWriter writer(context); + Sprite* sprite(writer.sprite()); + Tx tx(writer.context(), "Convert Color Profile"); + tx(new cmd::ConvertColorProfile( + sprite, colorSpaces[selectedColorProfile]->gfxColorSpace())); + tx.commit(); + + updateButtons(); + }); } window.remapWindow(); diff --git a/src/app/crash/backup_observer.cpp b/src/app/crash/backup_observer.cpp index 3010d50bb..1aef65b1f 100644 --- a/src/app/crash/backup_observer.cpp +++ b/src/app/crash/backup_observer.cpp @@ -140,7 +140,9 @@ void BackupObserver::backgroundThread() diff.frameTags ? "frameTags": "", diff.palettes ? "palettes": "", diff.layers ? "layers": "", - diff.cels ? "cels": ""); + diff.cels ? "cels": "", + diff.images ? "images": "", + diff.colorProfiles ? "colorProfiles": ""); Doc* copyDoc = copy.release(); ui::execute_from_ui_thread( diff --git a/src/app/crash/read_document.cpp b/src/app/crash/read_document.cpp index 41602d469..18cd50296 100644 --- a/src/app/crash/read_document.cpp +++ b/src/app/crash/read_document.cpp @@ -1,4 +1,5 @@ // Aseprite +// Copyright (C) 2018 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -35,6 +36,7 @@ #include "doc/sprite.h" #include "doc/string_io.h" #include "doc/subobjects_io.h" +#include "fixmath/fixmath.h" #include #include @@ -298,9 +300,30 @@ private: } } + // Read color space + gfx::ColorSpacePtr colorSpace = readColorSpace(s); + if (colorSpace) + spr->setColorSpace(colorSpace); + return spr.release(); } + gfx::ColorSpacePtr readColorSpace(std::ifstream& s) { + const gfx::ColorSpace::Type type = (gfx::ColorSpace::Type)read16(s); + const gfx::ColorSpace::Flag flags = (gfx::ColorSpace::Flag)read16(s); + const double gamma = fixmath::fixtof(read32(s)); + const size_t n = read32(s); + std::vector buf(n); + if (n) + s.read((char*)&buf[0], n); + std::string name = read_string(s); + + auto colorSpace = std::make_shared( + type, flags, gamma, std::move(buf)); + colorSpace->setName(name); + return colorSpace; + } + // TODO could we use doc::read_layer() here? Layer* readLayer(std::ifstream& s) { LayerFlags flags = (LayerFlags)read32(s); diff --git a/src/app/crash/write_document.cpp b/src/app/crash/write_document.cpp index bcbad8613..316773d59 100644 --- a/src/app/crash/write_document.cpp +++ b/src/app/crash/write_document.cpp @@ -1,4 +1,5 @@ // Aseprite +// Copyright (C) 2018 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -33,6 +34,7 @@ #include "doc/slice_io.h" #include "doc/sprite.h" #include "doc/string_io.h" +#include "fixmath/fixmath.h" #include #include @@ -165,6 +167,23 @@ private: for (const Slice* slice : spr->slices()) write32(s, slice->id()); + // Color Space + writeColorSpace(s, spr->colorSpace()); + + return true; + } + + bool writeColorSpace(std::ofstream& s, const gfx::ColorSpacePtr& colorSpace) { + write16(s, colorSpace->type()); + write16(s, colorSpace->flags()); + write32(s, fixmath::ftofix(colorSpace->gamma())); + + auto& rawData = colorSpace->rawData(); + write32(s, rawData.size()); + if (rawData.size() > 0) + s.write((const char*)&rawData[0], rawData.size()); + + write_string(s, colorSpace->name()); return true; } diff --git a/src/app/doc.cpp b/src/app/doc.cpp index 8719fc09c..2fd0df8c2 100644 --- a/src/app/doc.cpp +++ b/src/app/doc.cpp @@ -1,4 +1,5 @@ // Aseprite +// Copyright (C) 2018 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -31,6 +32,8 @@ #include "doc/mask_boundaries.h" #include "doc/palette.h" #include "doc/sprite.h" +#include "os/display.h" +#include "os/system.h" #include #include @@ -54,6 +57,8 @@ Doc::Doc(Sprite* sprite) if (sprite) sprites().add(sprite); + + updateOSColorSpace(false); } Doc::~Doc() @@ -111,6 +116,15 @@ void Doc::notifyGeneralUpdate() notify_observers(&DocObserver::onGeneralUpdate, ev); } +void Doc::notifyColorSpaceChanged() +{ + updateOSColorSpace(true); + + DocEvent ev(this); + ev.sprite(sprite()); + notify_observers(&DocObserver::onColorSpaceChanged, ev); +} + void Doc::notifySpritePixelsModified(Sprite* sprite, const gfx::Region& region, frame_t frame) { DocEvent ev(this); @@ -482,6 +496,22 @@ void Doc::removeFromContext() } } +void Doc::updateOSColorSpace(bool appWideSignal) +{ + auto system = os::instance(); + if (system) { + m_osColorSpace = system->createColorSpace(sprite()->colorSpace()); + if (!m_osColorSpace && system->defaultDisplay()) + m_osColorSpace = system->defaultDisplay()->colorSpace(); + } + + if (appWideSignal && + context() && + context()->activeDocument() == this) { + App::instance()->ColorSpaceChange(); + } +} + // static gfx::Point Doc::NoLastDrawingPoint() { diff --git a/src/app/doc.h b/src/app/doc.h index 034755109..bdbc1ae3b 100644 --- a/src/app/doc.h +++ b/src/app/doc.h @@ -1,4 +1,5 @@ // Aseprite +// Copyright (C) 2018 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -23,6 +24,7 @@ #include "doc/pixel_format.h" #include "gfx/rect.h" #include "obs/observable.h" +#include "os/color_space.h" #include @@ -81,10 +83,13 @@ namespace app { color_t bgColor() const; color_t bgColor(Layer* layer) const; + os::ColorSpacePtr osColorSpace() const { return m_osColorSpace; } + ////////////////////////////////////////////////////////////////////// // Notifications void notifyGeneralUpdate(); + void notifyColorSpaceChanged(); void notifySpritePixelsModified(Sprite* sprite, const gfx::Region& region, frame_t frame); void notifyExposeSpritePixels(Sprite* sprite, const gfx::Region& region); void notifyLayerMergedDown(Layer* srcLayer, Layer* targetLayer); @@ -120,7 +125,7 @@ namespace app { // Loaded options from file void setFormatOptions(const base::SharedPtr& format_options); - base::SharedPtr getFormatOptions() { return m_format_options; } + base::SharedPtr getFormatOptions() const { return m_format_options; } ////////////////////////////////////////////////////////////////////// // Boundaries @@ -188,6 +193,7 @@ namespace app { private: void removeFromContext(); + void updateOSColorSpace(bool appWideSignal); Context* m_ctx; int m_flags; @@ -212,6 +218,9 @@ namespace app { gfx::Point m_lastDrawingPoint; + // Last used color space to render a sprite. + os::ColorSpacePtr m_osColorSpace; + DISABLE_COPYING(Doc); }; diff --git a/src/app/doc_diff.cpp b/src/app/doc_diff.cpp index f72d8f765..352f56857 100644 --- a/src/app/doc_diff.cpp +++ b/src/app/doc_diff.cpp @@ -138,6 +138,11 @@ DocDiff compare_docs(const Doc* a, } } + // Compare color spaces + if (!a->sprite()->colorSpace()->nearlyEqual(*b->sprite()->colorSpace())) { + diff.anything = diff.colorProfiles = true; + } + return diff; } diff --git a/src/app/doc_diff.h b/src/app/doc_diff.h index 72af06253..58719bebb 100644 --- a/src/app/doc_diff.h +++ b/src/app/doc_diff.h @@ -21,6 +21,7 @@ namespace app { bool layers : 1; bool cels : 1; bool images : 1; + bool colorProfiles : 1; DocDiff() : anything(false), @@ -31,7 +32,8 @@ namespace app { palettes(false), layers(false), cels(false), - images(false) { + images(false), + colorProfiles(false) { } }; diff --git a/src/app/doc_observer.h b/src/app/doc_observer.h index ccdfa2619..0d249f8a0 100644 --- a/src/app/doc_observer.h +++ b/src/app/doc_observer.h @@ -1,4 +1,5 @@ // Aseprite +// Copyright (C) 2018 Igara Studio S.A. // Copyright (c) 2001-2018 David Capello // // This program is distributed under the terms of @@ -22,6 +23,8 @@ namespace app { // anything in the document could be changed. virtual void onGeneralUpdate(DocEvent& ev) { } + virtual void onColorSpaceChanged(DocEvent& ev) { } + virtual void onPixelFormatChanged(DocEvent& ev) { } virtual void onAddLayer(DocEvent& ev) { } diff --git a/src/app/file/ase_format.cpp b/src/app/file/ase_format.cpp index c06dcc15b..f9d30e925 100644 --- a/src/app/file/ase_format.cpp +++ b/src/app/file/ase_format.cpp @@ -1,4 +1,5 @@ // Aseprite +// Copyright (C) 2018 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -113,6 +114,9 @@ static void ase_file_write_cel_chunk(FILE* f, dio::AsepriteFrameHeader* frame_he const frame_t firstFrame); static void ase_file_write_cel_extra_chunk(FILE* f, dio::AsepriteFrameHeader* frame_header, const Cel* cel); +static void ase_file_write_color_profile(FILE* f, + dio::AsepriteFrameHeader* frame_header, + const doc::Sprite* sprite); #if 0 static void ase_file_write_mask_chunk(FILE* f, dio::AsepriteFrameHeader* frame_header, Mask* mask); #endif @@ -265,6 +269,10 @@ bool AseFormat::onSave(FileOp* fop) // Frame duration frame_header.duration = sprite->frameDuration(frame); + // Save color profile in first frame + if (outputFrame == 0) + ase_file_write_color_profile(f, &frame_header, sprite); + // is the first frame or did the palette change? Palette* pal = sprite->palette(frame); int palFrom = 0, palTo = pal->size()-1; @@ -833,6 +841,50 @@ static void ase_file_write_cel_extra_chunk(FILE* f, ase_file_write_padding(f, 16); } +static void ase_file_write_color_profile(FILE* f, + dio::AsepriteFrameHeader* frame_header, + const doc::Sprite* sprite) +{ + const gfx::ColorSpacePtr& cs = sprite->colorSpace(); + if (!cs) // No color + return; + + int type = ASE_FILE_NO_COLOR_PROFILE; + switch (cs->type()) { + + case gfx::ColorSpace::None: + return; // Without color profile, don't write this chunk. + + case gfx::ColorSpace::sRGB: + type = ASE_FILE_SRGB_COLOR_PROFILE; + break; + case gfx::ColorSpace::ICC: + type = ASE_FILE_ICC_COLOR_PROFILE; + break; + default: + ASSERT(false); // Unknown color profile + return; + } + + ChunkWriter chunk(f, frame_header, ASE_FILE_CHUNK_COLOR_PROFILE); + fputw(type, f); + fputw(cs->hasGamma() ? ASE_COLOR_PROFILE_FLAG_GAMMA: 0, f); + + fixmath::fixed gamma = 0; + if (cs->hasGamma()) + gamma = fixmath::ftofix(cs->gamma()); + fputl(gamma, f); + ase_file_write_padding(f, 8); + + if (cs->type() == gfx::ColorSpace::ICC) { + const size_t size = cs->iccSize(); + const void* data = cs->iccData(); + fputl(size, f); + if (size && data) + fwrite(data, 1, size, f); + } +} + #if 0 static void ase_file_write_mask_chunk(FILE* f, dio::AsepriteFrameHeader* frame_header, Mask* mask) { diff --git a/src/app/file/gif_format.cpp b/src/app/file/gif_format.cpp index 64df17d4c..8e0411e9b 100644 --- a/src/app/file/gif_format.cpp +++ b/src/app/file/gif_format.cpp @@ -1,4 +1,5 @@ // Aseprite +// Copyright (C) 2018 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -8,6 +9,7 @@ #include "config.h" #endif +#include "app/color_spaces.h" #include "app/console.h" #include "app/context.h" #include "app/doc.h" @@ -280,6 +282,9 @@ public: if (m_layer && m_opaque) m_layer->configureAsBackground(); + // sRGB is the default color space for GIF files + m_sprite->setColorSpace(gfx::ColorSpace::MakeSRGB()); + return true; } else @@ -870,6 +875,7 @@ public: GifEncoder(FileOp* fop, GifFileType* gifFile) : m_fop(fop) , m_gifFile(gifFile) + , m_document(fop->document()) , m_sprite(fop->document()->sprite()) , m_spriteBounds(m_sprite->bounds()) , m_hasBackground(m_sprite->backgroundLayer() ? true: false) @@ -1334,10 +1340,14 @@ private: private: - static ColorMapObject* createColorMap(const Palette* palette) { + ColorMapObject* createColorMap(const Palette* palette) { int n = 1 << GifBitSizeLimited(palette->size()); ColorMapObject* colormap = GifMakeMapObject(n, nullptr); + // Color space conversions + ConvertCS convert = convert_from_custom_to_srgb( + m_document->osColorSpace()); + for (int i=0; isize()) @@ -1345,6 +1355,8 @@ private: else color = rgba(0, 0, 0, 255); + color = convert(color); + colormap->Colors[i].Red = rgba_getr(color); colormap->Colors[i].Green = rgba_getg(color); colormap->Colors[i].Blue = rgba_getb(color); @@ -1355,6 +1367,7 @@ private: FileOp* m_fop; GifFileType* m_gifFile; + const Doc* m_document; const Sprite* m_sprite; gfx::Rect m_spriteBounds; bool m_hasBackground; diff --git a/src/app/file/jpeg_format.cpp b/src/app/file/jpeg_format.cpp index da17cb50e..de7e0be32 100644 --- a/src/app/file/jpeg_format.cpp +++ b/src/app/file/jpeg_format.cpp @@ -1,4 +1,5 @@ // Aseprite +// Copyright (C) 2018 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -22,6 +23,7 @@ #include "base/memory.h" #include "doc/doc.h" +#include #include #include #include @@ -66,8 +68,11 @@ class JpegFormat : public FileFormat { } bool onLoad(FileOp* fop) override; + gfx::ColorSpacePtr loadColorSpace(FileOp* fop, jpeg_decompress_struct* dinfo); #ifdef ENABLE_SAVE bool onSave(FileOp* fop) override; + void saveColorSpace(FileOp* fop, jpeg_compress_struct* cinfo, + const gfx::ColorSpace* colorSpace); #endif base::SharedPtr onGetFormatOptions(FileOp* fop) override; @@ -107,9 +112,27 @@ static void output_message(j_common_ptr cinfo) ((struct error_mgr *)cinfo->err)->fop->setError("%s\n", buffer); } +// Some code to read color spaces from jpeg files is from Skia +// (SkJpegCodec.cpp) by Google Inc. +static constexpr uint32_t kMarkerMaxSize = 65533; +static constexpr uint32_t kICCMarker = JPEG_APP0 + 2; +static constexpr uint32_t kICCMarkerHeaderSize = 14; +static constexpr uint32_t kICCAvailDataPerMarker = (kMarkerMaxSize - kICCMarkerHeaderSize); +static constexpr uint8_t kICCSig[] = { 'I', 'C', 'C', '_', 'P', 'R', 'O', 'F', 'I', 'L', 'E', '\0' }; + +static bool is_icc_marker(jpeg_marker_struct* marker) +{ + if (kICCMarker != marker->marker || + marker->data_length < kICCMarkerHeaderSize) { + return false; + } + else + return !memcmp(marker->data, kICCSig, sizeof(kICCSig)); +} + bool JpegFormat::onLoad(FileOp* fop) { - struct jpeg_decompress_struct cinfo; + struct jpeg_decompress_struct dinfo; struct error_mgr jerr; JDIMENSION num_scanlines; JSAMPARRAY buffer; @@ -121,60 +144,65 @@ bool JpegFormat::onLoad(FileOp* fop) // Initialize the JPEG decompression object with error handling. jerr.fop = fop; - cinfo.err = jpeg_std_error(&jerr.head); + dinfo.err = jpeg_std_error(&jerr.head); jerr.head.error_exit = error_exit; jerr.head.output_message = output_message; // Establish the setjmp return context for error_exit to use. if (setjmp(jerr.setjmp_buffer)) { - jpeg_destroy_decompress(&cinfo); + jpeg_destroy_decompress(&dinfo); return false; } - jpeg_create_decompress(&cinfo); + jpeg_create_decompress(&dinfo); // Specify data source for decompression. - jpeg_stdio_src(&cinfo, file); + jpeg_stdio_src(&dinfo, file); + + // Instruct jpeg library to save the markers that we care + // about. Since the color profile will not change, we can skip this + // step on rewinds. + jpeg_save_markers(&dinfo, kICCMarker, 0xFFFF); // Read file header, set default decompression parameters. - jpeg_read_header(&cinfo, true); + jpeg_read_header(&dinfo, true); - if (cinfo.jpeg_color_space == JCS_GRAYSCALE) - cinfo.out_color_space = JCS_GRAYSCALE; + if (dinfo.jpeg_color_space == JCS_GRAYSCALE) + dinfo.out_color_space = JCS_GRAYSCALE; else - cinfo.out_color_space = JCS_RGB; + dinfo.out_color_space = JCS_RGB; // Start decompressor. - jpeg_start_decompress(&cinfo); + jpeg_start_decompress(&dinfo); // Create the image. Image* image = fop->sequenceImage( - (cinfo.out_color_space == JCS_RGB ? IMAGE_RGB: + (dinfo.out_color_space == JCS_RGB ? IMAGE_RGB: IMAGE_GRAYSCALE), - cinfo.output_width, - cinfo.output_height); + dinfo.output_width, + dinfo.output_height); if (!image) { - jpeg_destroy_decompress(&cinfo); + jpeg_destroy_decompress(&dinfo); return false; } // Create the buffer. - buffer_height = cinfo.rec_outbuf_height; + buffer_height = dinfo.rec_outbuf_height; buffer = (JSAMPARRAY)base_malloc(sizeof(JSAMPROW) * buffer_height); if (!buffer) { - jpeg_destroy_decompress(&cinfo); + jpeg_destroy_decompress(&dinfo); return false; } for (c=0; c<(int)buffer_height; c++) { buffer[c] = (JSAMPROW)base_malloc(sizeof(JSAMPLE) * - cinfo.output_width * cinfo.output_components); + dinfo.output_width * dinfo.output_components); if (!buffer[c]) { for (c--; c>=0; c--) base_free(buffer[c]); base_free(buffer); - jpeg_destroy_decompress(&cinfo); + jpeg_destroy_decompress(&dinfo); return false; } } @@ -185,12 +213,8 @@ bool JpegFormat::onLoad(FileOp* fop) fop->sequenceSetColor(c, c, c, c); // Read each scan line. - while (cinfo.output_scanline < cinfo.output_height) { - // TODO -/* if (plugin_want_close()) */ -/* break; */ - - num_scanlines = jpeg_read_scanlines(&cinfo, buffer, buffer_height); + while (dinfo.output_scanline < dinfo.output_height) { + num_scanlines = jpeg_read_scanlines(&dinfo, buffer, buffer_height); // RGB if (image->pixelFormat() == IMAGE_RGB) { @@ -200,7 +224,7 @@ bool JpegFormat::onLoad(FileOp* fop) for (y=0; y<(int)num_scanlines; y++) { src_address = ((uint8_t**)buffer)[y]; - dst_address = (uint32_t*)image->getPixelAddress(0, cinfo.output_scanline-1+y); + dst_address = (uint32_t*)image->getPixelAddress(0, dinfo.output_scanline-1+y); for (x=0; xwidth(); x++) { r = *(src_address++); @@ -218,30 +242,107 @@ bool JpegFormat::onLoad(FileOp* fop) for (y=0; y<(int)num_scanlines; y++) { src_address = ((uint8_t**)buffer)[y]; - dst_address = (uint16_t*)image->getPixelAddress(0, cinfo.output_scanline-1+y); + dst_address = (uint16_t*)image->getPixelAddress(0, dinfo.output_scanline-1+y); for (x=0; xwidth(); x++) *(dst_address++) = graya(*(src_address++), 255); } } - fop->setProgress((float)(cinfo.output_scanline+1) / (float)(cinfo.output_height)); + fop->setProgress((float)(dinfo.output_scanline+1) / (float)(dinfo.output_height)); if (fop->isStop()) break; } - /* destroy all data */ + // Read color space + gfx::ColorSpacePtr colorSpace = loadColorSpace(fop, &dinfo); + if (colorSpace && + fop->document()->sprite()->colorSpace()->type() == gfx::ColorSpace::None) { + fop->document()->sprite()->setColorSpace(colorSpace); + fop->document()->notifyColorSpaceChanged(); + } + for (c=0; c<(int)buffer_height; c++) base_free(buffer[c]); base_free(buffer); - jpeg_finish_decompress(&cinfo); - jpeg_destroy_decompress(&cinfo); + jpeg_finish_decompress(&dinfo); + jpeg_destroy_decompress(&dinfo); return true; } +// ICC profiles may be stored using a sequence of multiple markers. We obtain the ICC profile +// in two steps: +// (1) Discover all ICC profile markers and verify that they are numbered properly. +// (2) Copy the data from each marker into a contiguous ICC profile. +gfx::ColorSpacePtr JpegFormat::loadColorSpace(FileOp* fop, jpeg_decompress_struct* dinfo) +{ + // Note that 256 will be enough storage space since each markerIndex is stored in 8-bits. + jpeg_marker_struct* markerSequence[256]; + memset(markerSequence, 0, sizeof(markerSequence)); + uint8_t numMarkers = 0; + size_t totalBytes = 0; + + // Discover any ICC markers and verify that they are numbered properly. + for (jpeg_marker_struct* marker = dinfo->marker_list; marker; marker = marker->next) { + if (is_icc_marker(marker)) { + // Verify that numMarkers is valid and consistent. + if (0 == numMarkers) { + numMarkers = marker->data[13]; + if (0 == numMarkers) { + fop->setError("ICC Profile Error: numMarkers must be greater than zero.\n"); + return nullptr; + } + } + else if (numMarkers != marker->data[13]) { + fop->setError("ICC Profile Error: numMarkers must be consistent.\n"); + return nullptr; + } + + // Verify that the markerIndex is valid and unique. Note that zero is not + // a valid index. + uint8_t markerIndex = marker->data[12]; + if (markerIndex == 0 || markerIndex > numMarkers) { + fop->setError("ICC Profile Error: markerIndex is invalid.\n"); + return nullptr; + } + if (markerSequence[markerIndex]) { + fop->setError("ICC Profile Error: Duplicate value of markerIndex.\n"); + return nullptr; + } + markerSequence[markerIndex] = marker; + ASSERT(marker->data_length >= kICCMarkerHeaderSize); + totalBytes += marker->data_length - kICCMarkerHeaderSize; + } + } + + if (0 == totalBytes) { + // No non-empty ICC profile markers were found. + return nullptr; + } + + // Combine the ICC marker data into a contiguous profile. + std::vector iccData(totalBytes); + uint8_t* dst = &iccData[0]; + for (uint32_t i = 1; i <= numMarkers; i++) { + jpeg_marker_struct* marker = markerSequence[i]; + if (!marker) { + fop->setError("ICC Profile Error: Missing marker %d of %d.\n", i, numMarkers); + return nullptr; + } + + uint8_t* src = ((uint8_t*)marker->data) + kICCMarkerHeaderSize; + size_t bytes = marker->data_length - kICCMarkerHeaderSize; + memcpy(dst, src, bytes); + dst = dst + bytes; + } + + return gfx::ColorSpace::MakeICC(std::move(iccData)); +} + #ifdef ENABLE_SAVE + bool JpegFormat::onSave(FileOp* fop) { struct jpeg_compress_struct cinfo; @@ -288,6 +389,10 @@ bool JpegFormat::onSave(FileOp* fop) // START compressor. jpeg_start_compress(&cinfo, true); + // Save color space + if (fop->document()->sprite()->colorSpace()) + saveColorSpace(fop, &cinfo, fop->document()->sprite()->colorSpace().get()); + // CREATE the buffer. buffer_height = 1; buffer = (JSAMPARRAY)base_malloc(sizeof(JSAMPROW) * buffer_height); @@ -360,7 +465,53 @@ bool JpegFormat::onSave(FileOp* fop) // All fine. return true; } -#endif + +void JpegFormat::saveColorSpace(FileOp* fop, jpeg_compress_struct* cinfo, + const gfx::ColorSpace* colorSpace) +{ + if (!colorSpace || colorSpace->type() != gfx::ColorSpace::ICC) + return; + + size_t iccSize = colorSpace->iccSize(); + auto iccData = (const uint8_t*)colorSpace->iccData(); + if (!iccSize || !iccData) + return; + + std::vector markerData(kMarkerMaxSize); + int markerIndex = 1; + int numMarkers = + (iccSize / kICCAvailDataPerMarker) + + (iccSize % kICCAvailDataPerMarker > 0 ? 1: 0); + + // ICC profile too big to fit in JPEG markers (64kb*255 ~= 16mb) + if (numMarkers > 255) { + fop->setError("ICC profile is too big to enter in the JPEG file.\n"); + return; + } + + while (iccSize > 0) { + const size_t n = std::min(iccSize, kICCAvailDataPerMarker); + + ASSERT(n > 0); + ASSERT(n < kICCAvailDataPerMarker); + + // Marker Header + std::copy(kICCSig, kICCSig+sizeof(kICCSig), &markerData[0]); + markerData[sizeof(kICCSig) ] = markerIndex; + markerData[sizeof(kICCSig)+1] = numMarkers; + + // Marker Data + std::copy(iccData, iccData+n, &markerData[kICCMarkerHeaderSize]); + + jpeg_write_marker(cinfo, kICCMarker, &markerData[0], kICCMarkerHeaderSize + n); + + ++markerIndex; + iccSize -= n; + iccData += n; + } +} + +#endif // ENABLE_SAVE // Shows the JPEG configuration dialog. base::SharedPtr JpegFormat::onGetFormatOptions(FileOp* fop) diff --git a/src/app/file/png_format.cpp b/src/app/file/png_format.cpp index 101de5696..3cbbb0a14 100644 --- a/src/app/file/png_format.cpp +++ b/src/app/file/png_format.cpp @@ -1,4 +1,5 @@ // Aseprite +// Copyright (C) 2018 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -16,6 +17,7 @@ #include "app/file/png_format.h" #include "base/file_handle.h" #include "doc/doc.h" +#include "gfx/color_space.h" #include #include @@ -53,8 +55,10 @@ class PngFormat : public FileFormat { } bool onLoad(FileOp* fop) override; + gfx::ColorSpacePtr loadColorSpace(png_structp png_ptr, png_infop info_ptr); #ifdef ENABLE_SAVE bool onSave(FileOp* fop) override; + void saveColorSpace(png_structp png_ptr, png_infop info_ptr, const gfx::ColorSpace* colorSpace); #endif }; @@ -68,7 +72,7 @@ static void report_png_error(png_structp png_ptr, png_const_charp error) ((FileOp*)png_get_error_ptr(png_ptr))->setError("libpng: %s\n", error); } -// TODO this should be part of an png encoder instance +// TODO this should be information in FileOp parameter of onSave() static bool fix_one_alpha_pixel = false; PngEncoderOneAlphaPixel::PngEncoderOneAlphaPixel(bool state) @@ -81,12 +85,26 @@ PngEncoderOneAlphaPixel::~PngEncoderOneAlphaPixel() fix_one_alpha_pixel = false; } +// As in png_fixed_point_to_float() in skia/src/codec/SkPngCodec.cpp +static float png_fixtof(png_fixed_point x) +{ + // We multiply by the same factor that libpng used to convert + // fixed point -> double. Since we want floats, we choose to + // do the conversion ourselves rather than convert + // fixed point -> double -> float. + return ((float)x) * 0.00001f; +} + +static png_fixed_point png_ftofix(float x) +{ + return x * 100000.0f; +} + bool PngFormat::onLoad(FileOp* fop) { png_uint_32 width, height, y; unsigned int sig_read = 0; png_structp png_ptr; - png_infop info_ptr; int bit_depth, color_type, interlace_type; int num_palette; png_colorp palette; @@ -110,7 +128,7 @@ bool PngFormat::onLoad(FileOp* fop) } /* Allocate/initialize the memory for image information. */ - info_ptr = png_create_info_struct(png_ptr); + png_infop info_ptr = png_create_info_struct(png_ptr); if (info_ptr == NULL) { fop->setError("png_create_info_struct\n"); png_destroy_read_struct(&png_ptr, NULL, NULL); @@ -126,11 +144,6 @@ bool PngFormat::onLoad(FileOp* fop) return false; } - // Do not check sRGB profile -#ifdef PNG_SKIP_sRGB_CHECK_PROFILE - png_set_option(png_ptr, PNG_SKIP_sRGB_CHECK_PROFILE, 1); -#endif - /* Set up the input control if you are using standard C streams */ png_init_io(png_ptr, fp); @@ -357,12 +370,92 @@ bool PngFormat::onLoad(FileOp* fop) } png_free(png_ptr, rows_pointer); + // Setup the color space. + auto colorSpace = PngFormat::loadColorSpace(png_ptr, info_ptr); + if (colorSpace && + fop->document()->sprite()->colorSpace()->type() == gfx::ColorSpace::None) { + fop->document()->sprite()->setColorSpace(colorSpace); + fop->document()->notifyColorSpaceChanged(); + } + // Clean up after the read, and free any memory allocated png_destroy_read_struct(&png_ptr, &info_ptr, NULL); return true; } +// Returns a colorSpace object that represents any +// color space information in the encoded data. If the encoded data +// contains an invalid/unsupported color space, this will return +// NULL. If there is no color space information, it will guess sRGB +// +// Code to read color spaces from png files from Skia (SkPngCodec.cpp) +// by Google Inc. +gfx::ColorSpacePtr PngFormat::loadColorSpace(png_structp png_ptr, png_infop info_ptr) +{ + // First check for an ICC profile + png_bytep profile; + png_uint_32 length; + // The below variables are unused, however, we need to pass them in anyway or + // png_get_iCCP() will return nothing. + // Could knowing the |name| of the profile ever be interesting? Maybe for debugging? + png_charp name; + // The |compression| is uninteresting since: + // (1) libpng has already decompressed the profile for us. + // (2) "deflate" is the only mode of decompression that libpng supports. + int compression; + if (PNG_INFO_iCCP == png_get_iCCP(png_ptr, info_ptr, + &name, &compression, + &profile, &length)) { + auto colorSpace = gfx::ColorSpace::MakeICC(profile, length); + if (name) + colorSpace->setName(name); + return colorSpace; + } + + // Second, check for sRGB. + if (png_get_valid(png_ptr, info_ptr, PNG_INFO_sRGB)) { + // sRGB chunks also store a rendering intent: Absolute, Relative, + // Perceptual, and Saturation. + return gfx::ColorSpace::MakeSRGB(); + } + + // Next, check for chromaticities. + png_fixed_point wx, wy, rx, ry, gx, gy, bx, by, invGamma; + if (png_get_cHRM_fixed(png_ptr, info_ptr, + &wx, &wy, &rx, &ry, &gx, &gy, &bx, &by)) { + gfx::ColorSpacePrimaries primaries; + primaries.wx = png_fixtof(wx); primaries.wy = png_fixtof(wy); + primaries.rx = png_fixtof(rx); primaries.ry = png_fixtof(ry); + primaries.gx = png_fixtof(gx); primaries.gy = png_fixtof(gy); + primaries.bx = png_fixtof(bx); primaries.by = png_fixtof(by); + + if (PNG_INFO_gAMA == png_get_gAMA_fixed(png_ptr, info_ptr, &invGamma)) { + gfx::ColorSpaceTransferFn fn; + fn.a = 1.0f; + fn.b = fn.c = fn.d = fn.e = fn.f = 0.0f; + fn.g = 1.0f / png_fixtof(invGamma); + + return gfx::ColorSpace::MakeRGB(fn, primaries); + } + + // Default to sRGB gamma if the image has color space information, + // but does not specify gamma. + return gfx::ColorSpace::MakeRGBWithSRGBGamma(primaries); + } + + // Last, check for gamma. + if (PNG_INFO_gAMA == png_get_gAMA_fixed(png_ptr, info_ptr, &invGamma)) { + // Since there is no cHRM, we will guess sRGB gamut. + return gfx::ColorSpace::MakeSRGBWithGamma(1.0f / png_fixtof(invGamma)); + } + + // Report that there is no color space information in the PNG. + // Guess sRGB in this case. + return gfx::ColorSpace::MakeSRGB(); +} + #ifdef ENABLE_SAVE + bool PngFormat::onSave(FileOp* fop) { png_structp png_ptr; @@ -423,6 +516,9 @@ bool PngFormat::onSave(FileOp* fop) png_set_IHDR(png_ptr, info_ptr, width, height, 8, color_type, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE); + if (fop->document()->sprite()->colorSpace()) + saveColorSpace(png_ptr, info_ptr, fop->document()->sprite()->colorSpace().get()); + if (color_type == PNG_COLOR_TYPE_PALETTE) { int c, r, g, b; int pal_size = fop->sequenceGetNColors(); @@ -592,6 +688,53 @@ bool PngFormat::onSave(FileOp* fop) png_destroy_write_struct(&png_ptr, &info_ptr); return true; } -#endif + +void PngFormat::saveColorSpace(png_structp png_ptr, png_infop info_ptr, + const gfx::ColorSpace* colorSpace) +{ + switch (colorSpace->type()) { + + case gfx::ColorSpace::None: + // Do just nothing (png file without profile, like old Aseprite versions) + break; + + case gfx::ColorSpace::sRGB: + // TODO save the original intent + if (!colorSpace->hasGamma()) { + png_set_sRGB(png_ptr, info_ptr, PNG_sRGB_INTENT_PERCEPTUAL); + return; + } + + // Continue to RGB case... + + case gfx::ColorSpace::RGB: { + if (colorSpace->hasPrimaries()) { + const gfx::ColorSpacePrimaries* p = colorSpace->primaries(); + png_set_cHRM_fixed(png_ptr, info_ptr, + png_ftofix(p->wx), png_ftofix(p->wy), + png_ftofix(p->rx), png_ftofix(p->ry), + png_ftofix(p->gx), png_ftofix(p->gy), + png_ftofix(p->bx), png_ftofix(p->by)); + } + if (colorSpace->hasGamma()) { + png_set_gAMA_fixed(png_ptr, info_ptr, + png_ftofix(1.0f / colorSpace->gamma())); + } + break; + } + + case gfx::ColorSpace::ICC: { + png_set_iCCP(png_ptr, info_ptr, + (png_const_charp)colorSpace->name().c_str(), + PNG_COMPRESSION_TYPE_DEFAULT, + (png_const_bytep)colorSpace->iccData(), + (png_uint_32)colorSpace->iccSize()); + break; + } + + } +} + +#endif // ENABLE_SAVE } // namespace app diff --git a/src/app/modules/gfx.cpp b/src/app/modules/gfx.cpp index 76133192d..6b7158c8e 100644 --- a/src/app/modules/gfx.cpp +++ b/src/app/modules/gfx.cpp @@ -1,4 +1,5 @@ // Aseprite +// Copyright (C) 2018 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -11,6 +12,7 @@ #include "app/modules/gfx.h" #include "app/app.h" +#include "app/color_spaces.h" #include "app/color_utils.h" #include "app/console.h" #include "app/modules/gui.h" @@ -81,8 +83,10 @@ void draw_color(ui::Graphics* g, return; app::Color color = _color; + const int alpha = color.getAlpha(); - int alpha = color.getAlpha(); + // Color space conversion + auto convertColor = convert_from_current_to_screen_color_space(); if (alpha < 255) { if (rc.w == rc.h) @@ -102,7 +106,7 @@ void draw_color(ui::Graphics* g, int index = color.getIndex(); if (index >= 0 && index < get_current_palette()->size()) { - g->fillRect(color_utils::color_for_ui(color), rc); + g->fillRect(convertColor(color_utils::color_for_ui(color)), rc); } else { g->fillRect(gfx::rgba(0, 0, 0), rc); @@ -112,7 +116,7 @@ void draw_color(ui::Graphics* g, } } else { - g->fillRect(color_utils::color_for_ui(color), rc); + g->fillRect(convertColor(color_utils::color_for_ui(color)), rc); } } } diff --git a/src/app/ui/color_selector.cpp b/src/app/ui/color_selector.cpp index 373db704c..68389f650 100644 --- a/src/app/ui/color_selector.cpp +++ b/src/app/ui/color_selector.cpp @@ -1,4 +1,5 @@ // Aseprite +// Copyright (C) 2018 Igara Studio S.A. // Copyright (C) 2016-2018 David Capello // // This program is distributed under the terms of @@ -12,6 +13,8 @@ #include "app/ui/color_selector.h" +#include "app/app.h" +#include "app/color_spaces.h" #include "app/color_utils.h" #include "app/modules/gfx.h" #include "app/ui/skin/skin_theme.h" @@ -99,14 +102,17 @@ public: os::Surface* getCanvas(int w, int h, gfx::Color bgColor) { assert_ui_thread(); + auto activeCS = get_current_color_space(); + if (!m_canvas || m_canvas->width() != w || - m_canvas->height() != h) { + m_canvas->height() != h || + m_canvas->colorSpace() != activeCS) { std::unique_lock lock(m_mutex); stopCurrentPainting(lock); auto oldCanvas = m_canvas; - m_canvas = os::instance()->createSurface(w, h); + m_canvas = os::instance()->createSurface(w, h, activeCS); m_canvas->fillRect(bgColor, gfx::Rect(0, 0, w, h)); if (oldCanvas) { m_canvas->drawSurface(oldCanvas, 0, 0); @@ -221,6 +227,10 @@ ColorSelector::ColorSelector() { initTheme(); painter.addRef(); + + m_appConn = App::instance() + ->ColorSpaceChange.connect( + &ColorSelector::updateColorSpace, this); } ColorSelector::~ColorSelector() @@ -499,4 +509,10 @@ gfx::Rect ColorSelector::alphaBarBounds() const return gfx::Rect(); } +void ColorSelector::updateColorSpace() +{ + m_paintFlags |= AllAreasFlag; + invalidate(); +} + } // namespace app diff --git a/src/app/ui/color_selector.h b/src/app/ui/color_selector.h index 30acf06e3..a37c3ec27 100644 --- a/src/app/ui/color_selector.h +++ b/src/app/ui/color_selector.h @@ -1,4 +1,5 @@ // Aseprite +// Copyright (c) 2018 Igara Studio S.A. // Copyright (C) 2016-2018 David Capello // // This program is distributed under the terms of @@ -10,6 +11,7 @@ #include "app/color.h" #include "app/ui/color_source.h" +#include "obs/connection.h" #include "obs/signal.h" #include "os/surface.h" #include "ui/mouse_buttons.h" @@ -91,6 +93,8 @@ namespace app { gfx::Rect bottomBarBounds() const; gfx::Rect alphaBarBounds() const; + void updateColorSpace(); + // Internal flag used to lock the modification of m_color. // E.g. When the user picks a color harmony, we don't want to // change the main color. @@ -104,6 +108,8 @@ namespace app { bool m_capturedInAlpha; ui::Timer m_timer; + + obs::scoped_connection m_appConn; }; } // namespace app diff --git a/src/app/ui/editor/editor.cpp b/src/app/ui/editor/editor.cpp index 2b22806a2..5496faeed 100644 --- a/src/app/ui/editor/editor.cpp +++ b/src/app/ui/editor/editor.cpp @@ -1,4 +1,5 @@ // Aseprite +// Copyright (c) 2018 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -56,6 +57,8 @@ #include "doc/doc.h" #include "doc/mask_boundaries.h" #include "doc/slice.h" +#include "os/color_space.h" +#include "os/display.h" #include "os/surface.h" #include "os/system.h" #include "ui/ui.h" @@ -643,19 +646,27 @@ void Editor::drawOneSpriteUnclippedRect(ui::Graphics* g, const gfx::Rect& sprite if (rendered) { // Convert the render to a os::Surface static os::Surface* tmp = nullptr; // TODO move this to other centralized place - if (!tmp || tmp->width() < rc2.w || tmp->height() < rc2.h) { + + if (!tmp || + tmp->width() < rc2.w || + tmp->height() < rc2.h || + tmp->colorSpace() != m_document->osColorSpace()) { const int maxw = std::max(rc2.w, tmp ? tmp->width(): 0); const int maxh = std::max(rc2.h, tmp ? tmp->height(): 0); if (tmp) tmp->dispose(); - tmp = os::instance()->createSurface(maxw, maxh); + + tmp = os::instance()->createSurface( + maxw, maxh, m_document->osColorSpace()); } + if (tmp->nativeHandle()) { if (newEngine) tmp->clear(); // TODO why we need this? convert_image_to_surface(rendered.get(), m_sprite->palette(m_frame), tmp, 0, 0, 0, 0, rc2.w, rc2.h); + if (newEngine) { g->drawSurface(tmp, gfx::Rect(0, 0, rc2.w, rc2.h), dest); } @@ -1920,6 +1931,13 @@ void Editor::onShowExtrasChange() invalidate(); } +void Editor::onColorSpaceChanged(DocEvent& ev) +{ + // As the document has a new color space, we've to redraw the + // complete canvas again with the new color profile. + invalidate(); +} + void Editor::onExposeSpritePixels(DocEvent& ev) { if (m_state && ev.sprite() == m_sprite) diff --git a/src/app/ui/editor/editor.h b/src/app/ui/editor/editor.h index aeaadecae..7294b62d5 100644 --- a/src/app/ui/editor/editor.h +++ b/src/app/ui/editor/editor.h @@ -1,4 +1,5 @@ // Aseprite +// Copyright (c) 2018 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -26,6 +27,7 @@ #include "filters/tiled_mode.h" #include "gfx/fwd.h" #include "obs/connection.h" +#include "os/color_space.h" #include "render/projection.h" #include "render/zoom.h" #include "ui/base.h" @@ -284,6 +286,7 @@ namespace app { void onShowExtrasChange(); // DocObserver impl + void onColorSpaceChanged(DocEvent& ev) override; void onExposeSpritePixels(DocEvent& ev) override; void onSpritePixelRatioChanged(DocEvent& ev) override; void onBeforeRemoveLayer(DocEvent& ev) override; diff --git a/src/app/ui/palette_view.cpp b/src/app/ui/palette_view.cpp index b5bf0cf19..1eda9e201 100644 --- a/src/app/ui/palette_view.cpp +++ b/src/app/ui/palette_view.cpp @@ -1,4 +1,5 @@ // Aseprite +// Copyright (C) 2018 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -21,6 +22,7 @@ #include "app/ui/skin/skin_theme.h" #include "app/ui/status_bar.h" #include "app/util/clipboard.h" +#include "base/bind.h" #include "base/convert_to.h" #include "doc/image.h" #include "doc/palette.h" @@ -65,7 +67,9 @@ PaletteView::PaletteView(bool editable, PaletteViewStyle style, PaletteViewDeleg setFocusStop(true); setDoubleBuffered(true); - m_conn = App::instance()->PaletteChange.connect(&PaletteView::onAppPaletteChange, this); + m_palConn = App::instance()->PaletteChange.connect(&PaletteView::onAppPaletteChange, this); + m_csConn = App::instance()->ColorSpaceChange.connect( + base::Bind(&PaletteView::invalidate, this)); InitTheme.connect( [this]{ diff --git a/src/app/ui/palette_view.h b/src/app/ui/palette_view.h index 773609e37..4a3643b19 100644 --- a/src/app/ui/palette_view.h +++ b/src/app/ui/palette_view.h @@ -1,4 +1,5 @@ // Aseprite +// Copyright (C) 2018 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -152,7 +153,8 @@ namespace app { int m_rangeAnchor; doc::PalettePicks m_selectedEntries; bool m_isUpdatingColumns; - obs::scoped_connection m_conn; + obs::scoped_connection m_palConn; + obs::scoped_connection m_csConn; Hit m_hot; bool m_copy; }; diff --git a/src/dio/LICENSE.txt b/src/dio/LICENSE.txt index f87aa3ecb..a3236b421 100644 --- a/src/dio/LICENSE.txt +++ b/src/dio/LICENSE.txt @@ -1,3 +1,4 @@ +Copyright (c) 2018 Igara Studio S.A. Copyright (c) 2016-2018 David Capello Permission is hereby granted, free of charge, to any person obtaining diff --git a/src/dio/README.md b/src/dio/README.md index 4f1d5d665..fca144091 100644 --- a/src/dio/README.md +++ b/src/dio/README.md @@ -1,4 +1,7 @@ # Aseprite Document IO Library -*Copyright (C) 2016-2018 David Capello* > Distributed under [MIT license](LICENSE.txt) + +Library to decode `doc::Document` from `.aseprite` files. This +library should support encoding of `.aseprite` files in the near +future. diff --git a/src/dio/aseprite_common.h b/src/dio/aseprite_common.h index 5087cdb07..d0c3f5d2b 100644 --- a/src/dio/aseprite_common.h +++ b/src/dio/aseprite_common.h @@ -1,4 +1,5 @@ // Aseprite Document IO Library +// Copyright (c) 2018 Igara Studio S.A. // Copyright (c) 2001-2018 David Capello // // This file is released under the terms of the MIT license. @@ -18,6 +19,7 @@ #define ASE_FILE_CHUNK_LAYER 0x2004 #define ASE_FILE_CHUNK_CEL 0x2005 #define ASE_FILE_CHUNK_CEL_EXTRA 0x2006 +#define ASE_FILE_CHUNK_COLOR_PROFILE 0x2007 #define ASE_FILE_CHUNK_MASK 0x2016 #define ASE_FILE_CHUNK_PATH 0x2017 #define ASE_FILE_CHUNK_FRAME_TAGS 0x2018 @@ -33,6 +35,12 @@ #define ASE_FILE_LINK_CEL 1 #define ASE_FILE_COMPRESSED_CEL 2 +#define ASE_FILE_NO_COLOR_PROFILE 0 +#define ASE_FILE_SRGB_COLOR_PROFILE 1 +#define ASE_FILE_ICC_COLOR_PROFILE 2 + +#define ASE_COLOR_PROFILE_FLAG_GAMMA 1 + #define ASE_PALETTE_FLAG_HAS_NAME 1 #define ASE_USER_DATA_FLAG_HAS_TEXT 1 diff --git a/src/dio/aseprite_decoder.cpp b/src/dio/aseprite_decoder.cpp index 948be066a..0d7a6e487 100644 --- a/src/dio/aseprite_decoder.cpp +++ b/src/dio/aseprite_decoder.cpp @@ -1,4 +1,5 @@ // Aseprite Document IO Library +// Copyright (c) 2018 Igara Studio S.A. // Copyright (c) 2001-2018 David Capello // // This file is released under the terms of the MIT license. @@ -14,6 +15,7 @@ #include "base/exception.h" #include "base/file_handle.h" #include "base/fs.h" +#include "gfx/color_space.h" #include "dio/aseprite_common.h" #include "dio/decode_delegate.h" #include "dio/file_interface.h" @@ -152,6 +154,11 @@ bool AsepriteDecoder::decode() break; } + case ASE_FILE_CHUNK_COLOR_PROFILE: { + readColorProfile(sprite.get()); + break; + } + case ASE_FILE_CHUNK_MASK: { doc::Mask* mask = readMaskChunk(); if (mask) @@ -730,6 +737,46 @@ void AsepriteDecoder::readCelExtraChunk(doc::Cel* cel) } } +void AsepriteDecoder::readColorProfile(doc::Sprite* sprite) +{ + int type = read16(); + int flags = read16(); + fixmath::fixed gamma = read32(); + readPadding(8); + + // Without color space, like old Aseprite versions + gfx::ColorSpacePtr cs(nullptr); + + switch (type) { + + case ASE_FILE_NO_COLOR_PROFILE: + if (flags & ASE_COLOR_PROFILE_FLAG_GAMMA) + cs = gfx::ColorSpace::MakeSRGBWithGamma(fixmath::fixtof(gamma)); + else + cs = gfx::ColorSpace::MakeNone(); + break; + + case ASE_FILE_SRGB_COLOR_PROFILE: + if (flags & ASE_COLOR_PROFILE_FLAG_GAMMA) + cs = gfx::ColorSpace::MakeSRGBWithGamma(fixmath::fixtof(gamma)); + else + cs = gfx::ColorSpace::MakeSRGB(); + break; + + case ASE_FILE_ICC_COLOR_PROFILE: { + size_t length = read32(); + if (length > 0) { + std::vector data(length); + readBytes(&data[0], length); + cs = gfx::ColorSpace::MakeICC(std::move(data)); + } + break; + } + } + + sprite->setColorSpace(cs); +} + doc::Mask* AsepriteDecoder::readMaskChunk() { int c, u, v, byte; diff --git a/src/dio/aseprite_decoder.h b/src/dio/aseprite_decoder.h index 978cfb760..dfbaae7f4 100644 --- a/src/dio/aseprite_decoder.h +++ b/src/dio/aseprite_decoder.h @@ -1,4 +1,5 @@ // Aseprite Document IO Library +// Copyright (c) 2018 Igara Studio S.A. // Copyright (c) 2017 David Capello // // This file is released under the terms of the MIT license. @@ -52,6 +53,7 @@ private: AsepriteHeader* header, size_t chunk_end); void readCelExtraChunk(doc::Cel* cel); + void readColorProfile(doc::Sprite* sprite); doc::Mask* readMaskChunk(); void readFrameTagsChunk(doc::FrameTags* frameTags); void readSlicesChunk(doc::Slices& slices); diff --git a/src/dio/decoder.cpp b/src/dio/decoder.cpp index 571713184..76821d0cb 100644 --- a/src/dio/decoder.cpp +++ b/src/dio/decoder.cpp @@ -1,4 +1,5 @@ // Aseprite Document IO Library +// Copyright (c) 2018 Igara Studio S.A. // Copyright (c) 2017 David Capello // // This file is released under the terms of the MIT license. @@ -59,4 +60,9 @@ uint32_t Decoder::read32() return 0; } +size_t Decoder::readBytes(uint8_t* buf, size_t n) +{ + return m_f->readBytes(buf, n); +} + } // namespace dio diff --git a/src/dio/decoder.h b/src/dio/decoder.h index 72287b462..37eb9aee7 100644 --- a/src/dio/decoder.h +++ b/src/dio/decoder.h @@ -1,4 +1,5 @@ // Aseprite Document IO Library +// Copyright (c) 2018 Igara Studio S.A. // Copyright (c) 2017 David Capello // // This file is released under the terms of the MIT license. @@ -9,6 +10,7 @@ #pragma once #include +#include namespace doc { class Document; @@ -33,6 +35,7 @@ protected: uint8_t read8(); uint16_t read16(); uint32_t read32(); + size_t readBytes(uint8_t* buf, size_t n); private: DecodeDelegate* m_delegate; diff --git a/src/doc/LICENSE.txt b/src/doc/LICENSE.txt index 3adbd711e..e3cbdab8e 100644 --- a/src/doc/LICENSE.txt +++ b/src/doc/LICENSE.txt @@ -1,3 +1,4 @@ +Copyright (c) 2018 Igara Studio S.A. Copyright (c) 2001-2018 David Capello Permission is hereby granted, free of charge, to any person obtaining diff --git a/src/doc/README.md b/src/doc/README.md index f6c0d14d3..e184b8fdd 100644 --- a/src/doc/README.md +++ b/src/doc/README.md @@ -1,4 +1,5 @@ # Aseprite Document Library -*Copyright (C) 2001-2018 David Capello* > Distributed under [MIT license](LICENSE.txt) + +Library to represent the structure of a sprite on Aseprite. diff --git a/src/doc/image_spec.h b/src/doc/image_spec.h index c73447f5d..1bc0d8aac 100644 --- a/src/doc/image_spec.h +++ b/src/doc/image_spec.h @@ -1,4 +1,5 @@ // Aseprite Document Library +// Copyright (c) 2018 Igara Studio S.A. // Copyright (c) 2016 David Capello // // This file is released under the terms of the MIT license. @@ -11,6 +12,7 @@ #include "base/debug.h" #include "doc/color.h" #include "doc/color_mode.h" +#include "gfx/color_space.h" #include "gfx/rect.h" #include "gfx/size.h" @@ -25,7 +27,8 @@ namespace doc { : m_colorMode(colorMode), m_width(width), m_height(height), - m_maskColor(maskColor) { + m_maskColor(maskColor), + m_colorSpace(gfx::ColorSpace::MakeNone()) { ASSERT(width > 0); ASSERT(height > 0); } @@ -35,6 +38,7 @@ namespace doc { int height() const { return m_height; } gfx::Size size() const { return gfx::Size(m_width, m_height); } gfx::Rect bounds() const { return gfx::Rect(0, 0, m_width, m_height); } + const gfx::ColorSpacePtr& colorSpace() const { return m_colorSpace; } // The transparent color for colored images (0 by default) or just 0 for RGBA and Grayscale color_t maskColor() const { return m_maskColor; } @@ -43,6 +47,7 @@ namespace doc { void setWidth(const int width) { m_width = width; } void setHeight(const int height) { m_height = height; } void setMaskColor(const color_t color) { m_maskColor = color; } + void setColorSpace(const gfx::ColorSpacePtr& cs) { m_colorSpace = cs; } void setSize(const int width, const int height) { m_width = width; @@ -59,6 +64,7 @@ namespace doc { int m_width; int m_height; color_t m_maskColor; + gfx::ColorSpacePtr m_colorSpace; }; } // namespace doc diff --git a/src/doc/sprite.cpp b/src/doc/sprite.cpp index 44ebea342..fb3bc51ed 100644 --- a/src/doc/sprite.cpp +++ b/src/doc/sprite.cpp @@ -1,4 +1,5 @@ // Aseprite Document Library +// Copyright (c) 2018 Igara Studio S.A. // Copyright (c) 2001-2018 David Capello // // This file is released under the terms of the MIT license. @@ -34,7 +35,7 @@ namespace doc { Sprite::Sprite(PixelFormat format, int width, int height, int ncolors) : Object(ObjectType::Sprite) - , m_document(NULL) + , m_document(nullptr) , m_spec((ColorMode)format, width, height, 0) , m_pixelRatio(1, 1) , m_frames(1) @@ -149,6 +150,11 @@ void Sprite::setSize(int width, int height) m_spec.setSize(width, height); } +void Sprite::setColorSpace(const gfx::ColorSpacePtr& colorSpace) +{ + m_spec.setColorSpace(colorSpace); +} + bool Sprite::needAlpha() const { switch (pixelFormat()) { diff --git a/src/doc/sprite.h b/src/doc/sprite.h index c753c5bc2..bdcf69b64 100644 --- a/src/doc/sprite.h +++ b/src/doc/sprite.h @@ -1,4 +1,5 @@ // Aseprite Document Library +// Copyright (c) 2018 Igara Studio S.A. // Copyright (c) 2001-2018 David Capello // // This file is released under the terms of the MIT license. @@ -75,10 +76,12 @@ namespace doc { gfx::Rect bounds() const { return m_spec.bounds(); } int width() const { return m_spec.width(); } int height() const { return m_spec.height(); } + const gfx::ColorSpacePtr& colorSpace() const { return m_spec.colorSpace(); } void setPixelFormat(PixelFormat format); void setPixelRatio(const PixelRatio& pixelRatio); void setSize(int width, int height); + void setColorSpace(const gfx::ColorSpacePtr& colorSpace); // Returns true if the rendered images will contain alpha values less // than 255. Only RGBA and Grayscale images without background needs