Inkscape
Vector Graphics Editor
Loading...
Searching...
No Matches
global-palettes.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-or-later
5/* Authors: PBS <pbs3141@gmail.com>
6 * Copyright (C) 2022 PBS
7 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
8 */
9
10#include "global-palettes.h"
11
12#include <giomm/file.h>
13#include <glibmm/convert.h>
14
15// Using Glib::regex because
16// - std::regex is too slow in debug mode.
17// - boost::regex requires a library not present in the CI image.
18#include <glibmm/i18n.h>
19#include <glibmm/miscutils.h>
20#include <glibmm/regex.h>
21
22#include "colors/manager.h"
23
24#include "colors/spaces/lab.h"
25#include "io/resource.h"
26#include "io/sys.h"
28#include "util/delete-with.h"
29
31using namespace Inkscape::Colors;
32
33namespace {
34
35Glib::ustring get_extension(Glib::ustring const &name) {
36 auto extpos = name.rfind('.');
37 if (extpos != Glib::ustring::npos) {
38 auto ext = name.substr(extpos).casefold();
39 return ext;
40 }
41 return {};
42}
43
44std::vector<uint8_t> read_data(const Glib::RefPtr<Gio::InputStream>& s, size_t len) {
45 std::vector<uint8_t> buf(len, 0);
46 s->read(buf.data(), len);
47 return buf;
48}
49
50std::string read_string(const Glib::RefPtr<Gio::InputStream>& s, size_t len) {
51 std::vector<char> buf(len, 0);
52 s->read(buf.data(), len);
53 return std::string(buf.data(), len);
54}
55
56template<typename T>
57T read_value(const Glib::RefPtr<Gio::InputStream>& s) {
58 uint8_t buf[sizeof(T)];
59 s->read(buf, sizeof(T));
60 T val = 0;
61 for (int i = 0; i < sizeof(T); ++i) {
62 val <<= 8;
63 val |= buf[i];
64 }
65 return val;
66}
67
68float read_float(const Glib::RefPtr<Gio::InputStream>& s) {
69 auto val = read_value<uint32_t>(s);
70 return *reinterpret_cast<float*>(&val);
71}
72
73Glib::ustring read_pstring(const Glib::RefPtr<Gio::InputStream>& s, bool short_string = false) {
74 size_t len = short_string ? read_value<uint16_t>(s) : read_value<uint32_t>(s);
75 if (!len) return {};
76
77 std::vector<uint16_t> buf(len, 0);
78 s->read(buf.data(), 2 * len);
79 for (int i = 0; i < len; ++i) {
80 auto c = buf[i];
81 c = (c & 0xff) << 8 | c >> 8; // std::byteswap()
82 buf[i] = c;
83 }
84 // null terminated string?
85 if (buf[len - 1] == 0) --len;
86
87 auto string = g_utf16_to_utf8(buf.data(), len, nullptr, nullptr, nullptr);
88 if (!string) return {};
89
90 Glib::ustring ret(string);
91 g_free(string);
92 return ret;
93}
94
95void skip(const Glib::RefPtr<Gio::InputStream>& s, size_t bytes) {
96 s->skip(bytes);
97}
98
99using namespace Inkscape::UI::Dialog;
100
101namespace ColorBook {
102
103// Color space codes in ACB color book palettes
104enum Colorspace : uint16_t
105{
106 RgbColorspace = 0,
107 CmykColorspace = 2,
108 LabColorspace = 7,
109 GrayscaleColorspace = 8
110};
111
112} // namespace ColorBook
113
114// Load Adobe ACB color book
115void load_acb_palette(PaletteFileData& palette, std::string const &fname) {
116 auto file = Gio::File::create_for_path(fname);
117 auto stream = file->read();
118 auto magic = read_string(stream, 4);
119 if (magic != "8BCB") throw std::runtime_error(_("ACB file header not recognized."));
120
121 auto version = read_value<uint16_t>(stream);
122 if (version != 1) {
123 g_warning("Unknown ACB palette version in %s", fname.c_str());
124 }
125
126 /* id */ read_value<uint16_t>(stream);
127
128 auto ttl = read_pstring(stream);
129 auto prefix = read_pstring(stream);
130 auto suffix = read_pstring(stream);
131 auto desc = read_pstring(stream);
132 auto extract = [](const Glib::ustring& str) {
133 auto pos = str.find('=');
134 if (pos != Glib::ustring::npos) {
135 return str.substr(pos + 1);
136 }
137 return Glib::ustring();
138 };
139 prefix = extract(prefix);
140 suffix = extract(suffix);
141 ttl = extract(ttl);
142
143 auto color_count = read_value<uint16_t>(stream);
144 palette.columns = read_value<uint16_t>(stream);
145 palette.page_offset = read_value<uint16_t>(stream);
146 auto cs = read_value<uint16_t>(stream);
147
148 auto ext = get_extension(ttl);
149 if (ext == ".acb") {
150 // extension in palette title -> junk name; use file name instead
151 palette.name = Glib::path_get_basename(fname);
152 ext = get_extension(palette.name);
153 if (ext == ".acb") {
154 palette.name = palette.name.substr(0, palette.name.size() - ext.size());
155 }
156 }
157 else {
158 auto r = ttl.find("^R");
159 if (r != Glib::ustring::npos) ttl.replace(r, 2, "®");
160 palette.name = ttl;
161 }
162
163 palette.colors.reserve(color_count);
164
165 auto &cm = Manager::get();
166 std::shared_ptr<Space::AnySpace> space;
167
168 switch (cs) {
169 case ColorBook::RgbColorspace:
170 space = cm.find(Space::Type::RGB);
171 break;
172 case ColorBook::CmykColorspace:
173 space = cm.find(Space::Type::CMYK);
174 break;
175 case ColorBook::LabColorspace:
176 space = cm.find(Space::Type::LAB);
177 break;
178 case ColorBook::GrayscaleColorspace:
179 space = cm.find(Space::Type::Gray);
180 break;
181 default:
182 throw std::runtime_error(_("ACB file color space not supported."));
183 }
184
185 for (int index = 0; index < color_count; ++index) {
186
187 auto name = read_pstring(stream);
188 if (name.substr(0, 3) == "$$$") name = extract(name);
189 auto code = read_string(stream, 6);
190 std::ostringstream ost;
191 ost.precision(3);
192
193 std::vector<double> data;
194 for (auto channel : read_data(stream, space->getComponentCount())) {
195 data.emplace_back(channel / 255.0);
196 }
197 auto color = Color(space, data);
198
199 if (name.empty()) {
200 palette.colors.emplace_back(PaletteFileData::SpacerItem());
201 }
202 else {
203 color.setName(prefix + name + suffix);
204 palette.colors.emplace_back(std::move(color));
205 }
206 }
207}
208
209void load_ase_swatches(PaletteFileData& palette, std::string const &fname) {
210 auto file = Gio::File::create_for_path(fname);
211 auto stream = file->read();
212 auto magic = read_string(stream, 4);
213 if (magic != "ASEF") throw std::runtime_error(_("ASE file header not recognized."));
214
215 auto version_major = read_value<uint16_t>(stream);
216 auto version_minor = read_value<uint16_t>(stream);
217
218 if (version_major > 1) {
219 g_warning("Unknown swatches version %d.%d in %s", (int)version_major, (int)version_minor, fname.c_str());
220 }
221
222 auto block_count = read_value<uint32_t>(stream);
223 auto &cm = Manager::get();
224
225 static std::map<std::string, Space::Type> name_map = {
226 {"RGB ", Space::Type::RGB},
227 {"LAB ", Space::Type::LAB},
228 {"CMYK", Space::Type::CMYK},
229 {"GRAY", Space::Type::Gray}
230 };
231
232 for (uint32_t block = 0; block < block_count; ++block) {
233 auto block_type = read_value<uint16_t>(stream);
234 auto block_length = read_value<uint32_t>(stream);
235 std::ostringstream ost;
236
237 if (block_type == 0xc001) { // group start
238 auto name = read_pstring(stream, true);
239 palette.colors.emplace_back(PaletteFileData::GroupStart{.name = name});
240 }
241 else if (block_type == 0x0001) { // color entry
242 auto color_name = read_pstring(stream, true);
243 auto space_name = read_string(stream, 4);
244
245 auto it = name_map.find(space_name);
246 if (it == name_map.end()) {
247 std::ostringstream ost;
248 ost << _("ASE color mode not recognized:") << " '" << space_name << "'.";
249 throw std::runtime_error(ost.str());
250 }
251 auto space = cm.find(it->second);
252 std::vector<double> data;
253 for (unsigned i = 0; i < space->getComponentCount(); i++) {
254 data.emplace_back(read_float(stream));
255 }
256 auto color = Color(space, data);
257
258 read_value<uint16_t>(stream); // type uint16, ignored for now
259 // auto mode = to_mode(type); Is this used? 0, Global, 1, Spot, else Normal
260
261 color.setName(color_name);
262 palette.colors.emplace_back(color);
263 }
264 else if (block_type == 0xc002) { // group end
265 }
266 else {
267 skip(stream, block_length);
268 }
269 }
270
271 // palette name - file name without extension
272 palette.name = Glib::path_get_basename(fname);
273 auto ext = get_extension(palette.name);
274 if (ext == ".ase") palette.name = palette.name.substr(0, palette.name.size() - ext.size());
275}
276
277// Load GIMP color palette
278void load_gimp_palette(PaletteFileData& palette, std::string const &path)
279{
280 palette.name = Glib::path_get_basename(path);
281 palette.columns = 1;
282
283 auto f = delete_with<std::fclose>(Inkscape::IO::fopen_utf8name(path.c_str(), "r"));
284 if (!f) throw std::runtime_error(_("Failed to open file"));
285
286 char buf[1024];
287 if (!std::fgets(buf, sizeof(buf), f.get())) throw std::runtime_error(_("File is empty"));
288 if (std::strncmp("GIMP Palette", buf, 12) != 0) throw std::runtime_error(_("First line is wrong"));
289
290 static auto const regex_rgb = Glib::Regex::create("\\s*(\\d+)\\s+(\\d+)\\s+(\\d+)\\s*(?:\\s(.*\\S)\\s*)?$", Glib::Regex::CompileFlags::OPTIMIZE | Glib::Regex::CompileFlags::ANCHORED);
291 static auto const regex_name = Glib::Regex::create("\\s*Name:\\s*(.*\\S)", Glib::Regex::CompileFlags::OPTIMIZE | Glib::Regex::CompileFlags::ANCHORED);
292 static auto const regex_cols = Glib::Regex::create("\\s*Columns:\\s*(.*\\S)", Glib::Regex::CompileFlags::OPTIMIZE | Glib::Regex::CompileFlags::ANCHORED);
293 static auto const regex_blank = Glib::Regex::create("\\s*(?:$|#)", Glib::Regex::CompileFlags::OPTIMIZE | Glib::Regex::CompileFlags::ANCHORED);
294
295 auto &cm = Manager::get();
296 auto space = cm.find(Space::Type::RGB);
297
298 while (std::fgets(buf, sizeof(buf), f.get())) {
299 Glib::MatchInfo match;
300 if (regex_rgb->match(buf, match)) {
301 // 8-bit RGB color, followed by an optional name.
302
303 std::vector<double> data;
304 for (unsigned i = 0; i < space->getComponentCount(); i++) {
305 data.emplace_back(std::stoi(match.fetch(i+1)) / 255.0);
306 }
307 auto color = Color(space, data);
308 color.setName(match.fetch(4));
309
310 if (!color.getName().empty()) {
311 // Translate the name if present.
312 color.setName(g_dpgettext2(nullptr, "Palette", color.getName().c_str()));
313 }
314
315 palette.colors.emplace_back(std::move(color));
316 } else if (regex_name->match(buf, match)) {
317 // Header entry for name.
318 palette.name = match.fetch(1);
319 } else if (regex_cols->match(buf, match)) {
320 // Header entry for columns.
321 palette.columns = std::clamp(std::stoi(match.fetch(1)), 1, 1000);
322 } else if (regex_blank->match(buf, match)) {
323 // Comment or blank line.
324 } else {
325 // Unrecognised.
326 throw std::runtime_error(C_("Palette", "Invalid line ") + std::string(buf));
327 }
328 }
329}
330
331} // namespace
332
333namespace Inkscape::UI::Dialog {
334
335PaletteResult load_palette(std::string const &path)
336{
337 auto const utf8path = Glib::filename_to_utf8(path);
338
339 auto compose_error = [&] (char const *what) {
340 return Glib::ustring::compose(_("Error loading palette %1: %2"), utf8path, what);
341 };
342
343 try {
345 p.id = utf8path;
346
347 auto const ext = get_extension(utf8path);
348 if (ext == ".acb") {
349 load_acb_palette(p, path);
350 } else if (ext == ".ase") {
351 load_ase_swatches(p, path);
352 } else {
353 load_gimp_palette(p, path);
354 }
355
356 return {std::move(p), {}};
357
358 } catch (std::runtime_error const &e) {
359 return {{}, compose_error(e.what())};
360 } catch (std::logic_error const &e) {
361 return {{}, compose_error(e.what())};
362 } catch (Glib::Error const &e) {
363 return {{}, compose_error(e.what())};
364 } catch (...) {
365 return {{}, Glib::ustring::compose(_("Unknown error loading palette %1"), utf8path)};
366 }
367}
368
370{
371 // Load the palettes.
372 for (auto const &path : Inkscape::IO::Resource::get_filenames(Inkscape::IO::Resource::PALETTES, {".gpl", ".acb", ".ase"})) {
373 auto res = load_palette(path);
374 if (res.palette) {
375 _palettes.emplace_back(std::move(*res.palette));
376 } else {
377 g_warning("%s", res.error_message.c_str());
378 }
379 }
380
381 // Sort by name.
382 std::sort(_palettes.begin(), _palettes.end(), [] (auto& a, auto& b) {
383 return a.name.compare(b.name) < 0;
384 });
385
386 // First priority for lookup: by id.
387 for (auto& pal : _palettes) {
388 _access.emplace(pal.id.raw(), &pal);
389 }
390
391 // Second priority for lookup: by name.
392 for (auto& pal : _palettes) {
393 if (!pal.name.empty()) {
394 _access.emplace(pal.name.raw(), &pal);
395 }
396 }
397}
398
399const PaletteFileData* GlobalPalettes::find_palette(const Glib::ustring& id) const {
400 auto p = _access.find(id.raw());
401 return p != _access.end() ? p->second : nullptr;
402}
403
404Glib::RefPtr<Gio::File> choose_palette_file(Gtk::Window* window) {
405 static std::string current_folder;
406 static std::vector<std::pair<Glib::ustring, Glib::ustring>> const filters{
407 {_("Gimp Color Palette"), "*.gpl"},
408 {_("Adobe Color Book"), "*.acb"},
409 {_("Adobe Swatch Exchange"), "*.ase"}
410 };
411 return choose_file_open(_("Load color palette"), window, filters, current_folder);
412}
413
415{
416 static GlobalPalettes instance;
417 return instance;
418}
419
420} // namespace Inkscape::UI::Dialog
421
422/*
423 Local Variables:
424 mode:c++
425 c-file-style:"stroustrup"
426 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
427 indent-tabs-mode:nil
428 fill-column:99
429 End:
430*/
431// vim:filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99:
Singleton class that manages the static list of global palettes.
static GlobalPalettes const & get()
std::unordered_map< std::string, PaletteFileData * > _access
std::vector< PaletteFileData > _palettes
const PaletteFileData * find_palette(const Glib::ustring &id) const
double c[8][4]
Ad-hoc smart pointer useful when interfacing with C code.
Global color palette information.
A set of useful color modifying functions which do not fit as generic methods on the color class itse...
Definition profile.cpp:24
std::vector< std::string > get_filenames(Type type, std::vector< const char * > const &extensions, std::vector< const char * > const &exclusions)
Definition resource.cpp:267
FILE * fopen_utf8name(char const *utf8name, char const *mode)
Open a file with g_fopen().
Definition sys.cpp:72
Dialog code.
Definition desktop.h:117
Glib::RefPtr< Gio::File > choose_palette_file(Gtk::Window *window)
PaletteResult load_palette(std::string const &path)
auto delete_with(T *p)
Wrap a raw pointer in a std::unique_ptr with a custom function as the deleter.
Definition delete-with.h:26
unsigned int const version_major
unsigned int const version_minor
Glib::RefPtr< Gio::File > choose_file_open(Glib::ustring const &title, Gtk::Window *parent, Glib::RefPtr< Gio::ListStore< Gtk::FileFilter > > const &filters_model, std::string &current_folder, Glib::ustring const &accept)
Synchronously run a Gtk::FileDialog to open a single file for reading data.
int buf
Inkscape::IO::Resource - simple resource API.
auto len
Definition safe-printf.h:21
static const Point data[]
The data loaded from a palette file.
int columns
The preferred number of columns.
Glib::ustring id
Unique ID of this palette.
std::vector< ColorItem > colors
The list of colors in the palette.
Glib::ustring name
Name of the palette, either specified in the file or taken from the filename.
unsigned int page_offset
Index to a representative color of the color block; starts from 0 for each block.
int index
Glib::ustring name
Definition toolbars.cpp:55