summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJoel Klinghed <the_jk@spawned.biz>2026-01-02 22:42:31 +0100
committerJoel Klinghed <the_jk@spawned.biz>2026-01-02 22:42:31 +0100
commit6ed8f5151719fbc14ec0ac6d28a346d1f74cf2ca (patch)
treeebe7588e89e1aa2ae5376acf85f3a3a7b2ec7e10 /src
Initial commitHEADmain
Diffstat (limited to 'src')
-rw-r--r--src/args.cc389
-rw-r--r--src/args.hh64
-rw-r--r--src/cfg.cc199
-rw-r--r--src/cfg.hh45
-rw-r--r--src/check.hh39
-rw-r--r--src/colour.cc15
-rw-r--r--src/colour.hh20
-rw-r--r--src/config.h.in6
-rw-r--r--src/image.cc15
-rw-r--r--src/image.hh62
-rw-r--r--src/image_loader.hh15
-rw-r--r--src/image_processor.cc947
-rw-r--r--src/image_processor.hh29
-rw-r--r--src/io.cc358
-rw-r--r--src/io.hh109
-rw-r--r--src/line.cc127
-rw-r--r--src/line.hh37
-rw-r--r--src/logger.cc125
-rw-r--r--src/logger.hh41
-rw-r--r--src/main.cc47
-rw-r--r--src/paths.cc100
-rw-r--r--src/paths.hh20
-rw-r--r--src/size.hh30
-rw-r--r--src/spawner.cc236
-rw-r--r--src/spawner.hh49
-rw-r--r--src/str.cc94
-rw-r--r--src/str.hh34
-rw-r--r--src/u.hh21
-rw-r--r--src/u8.hh196
-rw-r--r--src/unique_fd.cc9
-rw-r--r--src/unique_fd.hh36
-rw-r--r--src/xpm/.clang-tidy2
-rw-r--r--src/xpm/color.c882
-rw-r--r--src/xpm/dix-config.h1
-rw-r--r--src/xpm/dix/dix_priv.h14
-rw-r--r--src/xpm/include/dix.h14
36 files changed, 4427 insertions, 0 deletions
diff --git a/src/args.cc b/src/args.cc
new file mode 100644
index 0000000..1794941
--- /dev/null
+++ b/src/args.cc
@@ -0,0 +1,389 @@
+#include "args.hh"
+
+#include <algorithm>
+#include <cassert>
+#include <cstddef>
+#include <cstdint>
+#include <format>
+#include <iostream>
+#include <map>
+#include <memory>
+#include <optional>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+namespace {
+
+std::string kEmpty;
+
+class OptionImpl : public Args::OptionArgument {
+ public:
+ enum Type : uint8_t {
+ NoArgument,
+ RequiredArgument,
+ OptionalArgument,
+ };
+
+ OptionImpl(Type type, char shortname, std::string longname, std::string arg,
+ std::string help)
+ : type(type),
+ shortname(shortname),
+ longname(std::move(longname)),
+ arg(std::move(arg)),
+ help(std::move(help)) {}
+
+ const Type type;
+ const char shortname;
+ const std::string longname;
+ const std::string arg;
+ const std::string help;
+
+ [[nodiscard]] bool is_set() const override { return is_set_; }
+
+ [[nodiscard]] bool has_argument() const override {
+ return value_.has_value();
+ }
+
+ [[nodiscard]] const std::string& argument() const override {
+ if (value_.has_value())
+ return value_.value();
+ assert(false);
+ return kEmpty;
+ }
+
+ void clear() {
+ is_set_ = false;
+ value_.reset();
+ }
+
+ void set_argument(std::string value) {
+ assert(type == Type::RequiredArgument || type == Type::OptionalArgument);
+ is_set_ = true;
+ value_ = std::move(value);
+ }
+
+ void set_no_argument() {
+ assert(type == Type::NoArgument || type == Type::OptionalArgument);
+ is_set_ = true;
+ value_.reset();
+ }
+
+ private:
+ bool is_set_{false};
+ std::optional<std::string> value_;
+};
+
+class ArgsImpl : public Args {
+ public:
+ explicit ArgsImpl(std::string prgname) : prgname_(std::move(prgname)) {}
+
+ std::shared_ptr<Option> option(char shortname, std::string longname,
+ std::string help) override {
+ auto opt = std::make_shared<OptionImpl>(
+ OptionImpl::Type::NoArgument, shortname, std::move(longname),
+ /* arg */ std::string(), std::move(help));
+ add(opt);
+ return opt;
+ }
+
+ std::shared_ptr<OptionArgument> option_argument(char shortname,
+ std::string longname,
+ std::string arg,
+ std::string help,
+ bool required) override {
+ auto opt = std::make_shared<OptionImpl>(
+ required ? OptionImpl::Type::RequiredArgument
+ : OptionImpl::Type::OptionalArgument,
+ shortname, std::move(longname), std::move(arg), std::move(help));
+ add(opt);
+ return opt;
+ }
+
+ bool run(int argc, char** argv,
+ std::vector<std::string_view>* arguments = nullptr) override {
+ last_error_.clear();
+ for (auto& opt : options_) {
+ opt->clear();
+ }
+
+ std::string_view prgname;
+ if (prgname_.empty()) {
+ if (argc > 0)
+ prgname = argv[0];
+ } else {
+ prgname = prgname_;
+ }
+
+ for (int a = 1; a < argc; ++a) {
+ assert(argv[a]);
+ if (argv[a][0] == '-' && argv[a][1] != '\0') {
+ if (argv[a][1] == '-') {
+ // long option
+ size_t eq = 2;
+ while (argv[a][eq] != '=' && argv[a][eq] != '\0')
+ ++eq;
+ size_t end = eq;
+ while (argv[a][end] != '\0')
+ ++end;
+
+ if (end == 2) {
+ // "--", no more options signal
+ if (arguments) {
+ for (++a; a < argc; ++a)
+ arguments->emplace_back(argv[a]);
+ }
+ break;
+ }
+
+ auto name = std::string_view(argv[a] + 2, eq - 2);
+ auto it = long_.find(name);
+ if (it == long_.end()) {
+ last_error_ =
+ std::format("{}: unrecognized option '--{}'", prgname, name);
+ return false;
+ }
+ auto& opt = options_[it->second];
+
+ if (eq < end) {
+ // long option with argument after equal sign
+ switch (opt->type) {
+ case OptionImpl::Type::NoArgument:
+ last_error_ =
+ std::format("{}: option '--{}' doesn't allow an argument",
+ prgname, name);
+ return false;
+ case OptionImpl::Type::RequiredArgument:
+ case OptionImpl::Type::OptionalArgument:
+ opt->set_argument(
+ std::string(argv[a] + eq + 1, end - (eq + 1)));
+ break;
+ }
+ } else {
+ switch (opt->type) {
+ case OptionImpl::Type::NoArgument:
+ case OptionImpl::Type::OptionalArgument:
+ opt->set_no_argument();
+ break;
+ case OptionImpl::Type::RequiredArgument:
+ if (++a >= argc) {
+ last_error_ = std::format(
+ "{}: option '--{}' requires an argument", prgname, name);
+ return false;
+ }
+ opt->set_argument(argv[a]);
+ break;
+ }
+ }
+ } else {
+ // short options
+ char* current = argv[a] + 1;
+ for (; *current; ++current) {
+ auto it = short_.find(*current);
+ if (it == short_.end()) {
+ last_error_ =
+ std::format("{}: invalid option -- '{}'", prgname, *current);
+ return false;
+ }
+
+ auto& opt = options_[it->second];
+ switch (opt->type) {
+ case OptionImpl::Type::NoArgument:
+ case OptionImpl::Type::OptionalArgument:
+ opt->set_no_argument();
+ break;
+ case OptionImpl::Type::RequiredArgument:
+ if (++a >= argc) {
+ last_error_ =
+ std::format("{}: option requires an argument -- '{}'",
+ prgname, *current);
+ return false;
+ }
+ opt->set_argument(argv[a]);
+ break;
+ }
+ }
+ }
+ } else {
+ if (arguments)
+ arguments->emplace_back(argv[a]);
+ }
+ }
+ return true;
+ }
+
+ void print_error(std::ostream& out) const override {
+ if (last_error_.empty())
+ return;
+
+ out << last_error_ << '\n';
+ }
+
+ void print_help(std::ostream& out, uint32_t width = 79) const override {
+ if (options_.empty())
+ return;
+
+ uint32_t indent = 0;
+ const uint32_t max_need = width / 2;
+ std::vector<uint32_t> option_need;
+ for (auto const& opt : options_) {
+ uint32_t need;
+ if (opt->longname.empty()) {
+ need = 4; // -O
+ switch (opt->type) {
+ case OptionImpl::Type::NoArgument:
+ case OptionImpl::Type::OptionalArgument:
+ break;
+ case OptionImpl::Type::RequiredArgument:
+ need += 1 + (opt->arg.empty() ? 3 : opt->arg.size());
+ break;
+ }
+ } else {
+ need = 8 + opt->longname.size(); // -O, --option
+ switch (opt->type) {
+ case OptionImpl::Type::NoArgument:
+ break;
+ case OptionImpl::Type::RequiredArgument:
+ // =ARG
+ need += 1 + (opt->arg.empty() ? 3 : opt->arg.size());
+ break;
+ case OptionImpl::Type::OptionalArgument:
+ // [=ARG]
+ need += 3 + (opt->arg.empty() ? 3 : opt->arg.size());
+ break;
+ }
+ }
+ need += 2; // margin
+
+ option_need.emplace_back(need);
+ if (need <= max_need) {
+ indent = std::max(indent, need);
+ }
+ }
+
+ print_wrap(out, width, /* indent */ 0,
+ "Mandatory arguments to long options"
+ " are mandatory for short options too.");
+ auto need_it = option_need.begin();
+ for (auto const& opt : options_) {
+ if (opt->longname.empty()) {
+ out << " -" << opt->shortname;
+ switch (opt->type) {
+ case OptionImpl::Type::NoArgument:
+ case OptionImpl::Type::OptionalArgument:
+ break;
+ case OptionImpl::Type::RequiredArgument:
+ out << " " << (opt->arg.empty() ? "ARG" : opt->arg);
+ break;
+ }
+ } else {
+ if (opt->shortname != '\0') {
+ out << " -" << opt->shortname << ", --";
+ } else {
+ out << " --";
+ }
+ out << opt->longname;
+ switch (opt->type) {
+ case OptionImpl::Type::NoArgument:
+ break;
+ case OptionImpl::Type::RequiredArgument:
+ out << "=" << (opt->arg.empty() ? "ARG" : opt->arg);
+ break;
+ case OptionImpl::Type::OptionalArgument:
+ out << "=[" << (opt->arg.empty() ? "ARG" : opt->arg) << ']';
+ break;
+ }
+ }
+
+ auto need = *need_it++;
+ if (need > max_need) {
+ out << '\n';
+ if (!opt->help.empty()) {
+ print_wrap(out, width, 0, opt->help);
+ }
+ } else {
+ if (opt->help.empty()) {
+ out << '\n';
+ } else {
+ out << " "; // add margin, already included in need
+ while (need++ < indent)
+ out << ' ';
+ print_wrap(out, width, indent, opt->help);
+ }
+ }
+ }
+ }
+
+ private:
+ void add(std::shared_ptr<OptionImpl> opt) {
+ if (opt->shortname == '\0' && opt->longname.empty()) {
+ assert(false);
+ } else {
+ auto idx = options_.size();
+ if (opt->shortname != '\0')
+ short_.emplace(opt->shortname, idx);
+ if (!opt->longname.empty())
+ long_.emplace(opt->longname, idx);
+ }
+ options_.emplace_back(std::move(opt));
+ }
+
+ static inline bool is_whitespace(char c) { return c == ' ' || c == '\t'; }
+
+ static void print_wrap(std::ostream& out, uint32_t width, uint32_t indent,
+ const std::string& str) {
+ if (indent + str.size() <= width) {
+ out << str << '\n';
+ return;
+ }
+ if (width <= indent || indent + 10 > width) {
+ out << '\n';
+ out << str << '\n';
+ return;
+ }
+ const std::string indent_str(indent, ' ');
+ const uint32_t avail = width - indent;
+ size_t offset = 0;
+ while (offset + avail < str.size()) {
+ uint32_t i = avail;
+ while (i > 0 && !is_whitespace(str[offset + i]))
+ --i;
+ if (i == 0) {
+ out << str.substr(offset, avail - 1);
+ out << "-\n";
+ offset += avail - 1;
+ } else {
+ out << str.substr(offset, i);
+ out << '\n';
+ offset += i;
+ }
+ out << indent_str;
+ }
+ out << str.substr(offset);
+ out << '\n';
+ }
+
+ const std::string prgname_;
+ std::vector<std::shared_ptr<OptionImpl>> options_;
+ std::map<char, size_t> short_;
+ std::map<std::string_view, size_t> long_;
+ std::string last_error_;
+};
+
+} // namespace
+
+std::shared_ptr<Args::Option> Args::option(std::string longname,
+ std::string help) {
+ return option(/* shortname */ '\0', std::move(longname), std::move(help));
+}
+
+std::shared_ptr<Args::OptionArgument> Args::option_argument(
+ std::string longname, std::string arg, std::string help, bool required) {
+ return option_argument(/* shortname */ '\0', std::move(longname),
+ std::move(arg), std::move(help), required);
+}
+
+std::unique_ptr<Args> Args::create(std::string prgname) {
+ return std::make_unique<ArgsImpl>(std::move(prgname));
+}
diff --git a/src/args.hh b/src/args.hh
new file mode 100644
index 0000000..14f3716
--- /dev/null
+++ b/src/args.hh
@@ -0,0 +1,64 @@
+#ifndef ARGS_HH
+#define ARGS_HH
+
+#include <cstdint>
+#include <iosfwd>
+#include <memory>
+#include <string>
+#include <string_view>
+#include <vector>
+
+class Args {
+ public:
+ virtual ~Args() = default;
+
+ class Option {
+ public:
+ virtual ~Option() = default;
+
+ [[nodiscard]] virtual bool is_set() const = 0;
+
+ protected:
+ Option() = default;
+ Option(Option const&) = delete;
+ Option& operator=(Option const&) = delete;
+ };
+
+ class OptionArgument : public Option {
+ public:
+ [[nodiscard]] virtual bool has_argument() const = 0;
+ [[nodiscard]] virtual const std::string& argument() const = 0;
+ };
+
+ static std::unique_ptr<Args> create(std::string prgname = std::string());
+
+ virtual std::shared_ptr<Option> option(char shortname,
+ std::string longname = std::string(),
+ std::string help = std::string()) = 0;
+
+ std::shared_ptr<Option> option(std::string longname,
+ std::string help = std::string());
+
+ virtual std::shared_ptr<OptionArgument> option_argument(
+ char shortname, std::string longname = std::string(),
+ std::string arg = std::string(), std::string help = std::string(),
+ bool required = true) = 0;
+
+ std::shared_ptr<OptionArgument> option_argument(
+ std::string longname, std::string arg = std::string(),
+ std::string help = std::string(), bool required = true);
+
+ virtual bool run(int argc, char** argv,
+ std::vector<std::string_view>* arguments = nullptr) = 0;
+
+ virtual void print_error(std::ostream& out) const = 0;
+
+ virtual void print_help(std::ostream& out, uint32_t width = 79) const = 0;
+
+ protected:
+ Args() = default;
+ Args(Args const&) = delete;
+ Args& operator=(Args const&) = delete;
+};
+
+#endif // ARGS_HH
diff --git a/src/cfg.cc b/src/cfg.cc
new file mode 100644
index 0000000..b65246f
--- /dev/null
+++ b/src/cfg.cc
@@ -0,0 +1,199 @@
+#include "cfg.hh"
+
+#include "io.hh"
+#include "line.hh"
+#include "paths.hh"
+#include "str.hh"
+
+#include <charconv>
+#include <cstdint>
+#include <format>
+#include <functional>
+#include <map>
+#include <memory>
+#include <optional>
+#include <string>
+#include <string_view>
+#include <system_error>
+#include <utility>
+#include <vector>
+
+namespace cfg {
+
+namespace {
+
+inline char ascii_lowercase(char c) {
+ if (c >= 'A' && c <= 'Z') {
+ // NOLINTNEXTLINE(bugprone-narrowing-conversions)
+ return c | 0x20;
+ }
+ return c;
+}
+
+bool ascii_lowercase_eq(std::string_view a, std::string_view b) {
+ if (a.size() != b.size())
+ return false;
+ auto it_a = a.begin();
+ auto it_b = b.begin();
+ for (; it_a != a.end(); ++it_a, ++it_b) {
+ if (ascii_lowercase(*it_a) != *it_b)
+ return false;
+ }
+ return true;
+}
+
+class ConfigSingleImpl : public Config {
+ public:
+ ConfigSingleImpl() = default;
+
+ bool load(std::filesystem::path const& path,
+ std::vector<std::string>& errors) {
+ auto io_reader = io::open(std::string(path));
+ if (!io_reader.has_value()) {
+ errors.push_back(
+ std::format("Unable to open {} for reading", path.string()));
+ return false;
+ }
+ bool all_ok = true;
+ auto line_reader = line::open(std::move(io_reader.value()));
+ while (true) {
+ auto line = line_reader->read();
+ if (line.has_value()) {
+ auto trimmed = str::trim(line.value());
+ if (trimmed.empty() || trimmed.front() == '#')
+ continue;
+ auto eq = trimmed.find('=');
+ if (eq == std::string_view::npos) {
+ errors.push_back(
+ std::format("{}:{}: Invalid line, expected key = value.",
+ path.string(), line_reader->number()));
+ all_ok = false;
+ continue;
+ }
+ auto key = str::trim(trimmed.substr(0, eq));
+ auto value = str::trim(trimmed.substr(eq + 1));
+ auto ret = values_.emplace(key, value);
+ if (!ret.second) {
+ errors.push_back(std::format("{}:{}: Duplicate key {} ignored.",
+ path.string(), line_reader->number(),
+ key));
+ all_ok = false;
+ continue;
+ }
+ } else {
+ switch (line.error()) {
+ case io::ReadError::Eof:
+ break;
+ default:
+ errors.push_back(std::format("{}: Read error", path.string()));
+ all_ok = false;
+ break;
+ }
+ break;
+ }
+ }
+ return all_ok;
+ }
+
+ [[nodiscard]]
+ std::optional<std::string_view> get(std::string_view name) const override {
+ auto it = values_.find(name);
+ if (it == values_.end())
+ return std::nullopt;
+ return it->second;
+ }
+
+ private:
+ std::map<std::string, std::string, std::less<>> values_;
+};
+
+class ConfigXdgImpl : public Config {
+ public:
+ ConfigXdgImpl() = default;
+
+ bool load(std::string_view name, std::vector<std::string>& errors) {
+ bool all_ok = true;
+ for (auto const& dir : paths::config_dirs()) {
+ auto file = dir / name;
+ if (std::filesystem::exists(file)) {
+ auto cfg = std::make_unique<ConfigSingleImpl>();
+ if (!cfg->load(file, errors))
+ all_ok = false;
+ configs_.push_back(std::move(cfg));
+ }
+ }
+ return all_ok;
+ }
+
+ [[nodiscard]]
+ std::optional<std::string_view> get(std::string_view name) const override {
+ for (auto const& config : configs_) {
+ auto ret = config->get(name);
+ if (ret.has_value())
+ return ret;
+ }
+ return std::nullopt;
+ }
+
+ private:
+ std::vector<std::unique_ptr<ConfigSingleImpl>> configs_;
+};
+
+} // namespace
+
+bool Config::has(std::string_view name) const { return get(name).has_value(); }
+
+std::optional<int64_t> Config::get_int64(std::string_view name) const {
+ auto str = get(name);
+ if (str.has_value()) {
+ auto* const end = str->data() + str->size();
+ int64_t ret;
+ // NOLINTNEXTLINE(bugprone-suspicious-stringview-data-usage)
+ auto [ptr, ec] = std::from_chars(str->data(), end, ret);
+ if (ec == std::errc() && ptr == end)
+ return ret;
+ }
+ return std::nullopt;
+}
+
+std::optional<uint64_t> Config::get_uint64(std::string_view name) const {
+ auto str = get(name);
+ if (str.has_value()) {
+ auto* const end = str->data() + str->size();
+ uint64_t ret;
+ // NOLINTNEXTLINE(bugprone-suspicious-stringview-data-usage)
+ auto [ptr, ec] = std::from_chars(str->data(), end, ret);
+ if (ec == std::errc() && ptr == end)
+ return ret;
+ }
+ return std::nullopt;
+}
+
+std::optional<bool> Config::get_bool(std::string_view name) const {
+ auto str = get(name);
+ if (str.has_value()) {
+ if (ascii_lowercase_eq(str.value(), "true") ||
+ ascii_lowercase_eq(str.value(), "yes"))
+ return true;
+ if (ascii_lowercase_eq(str.value(), "false") ||
+ ascii_lowercase_eq(str.value(), "no"))
+ return false;
+ }
+ return std::nullopt;
+}
+
+std::unique_ptr<Config> load_all(std::string_view name,
+ std::vector<std::string>& errors) {
+ auto ret = std::make_unique<ConfigXdgImpl>();
+ ret->load(name, errors);
+ return ret;
+}
+
+std::unique_ptr<Config> load_one(std::filesystem::path const& path,
+ std::vector<std::string>& errors) {
+ auto ret = std::make_unique<ConfigSingleImpl>();
+ ret->load(path, errors);
+ return ret;
+}
+
+} // namespace cfg
diff --git a/src/cfg.hh b/src/cfg.hh
new file mode 100644
index 0000000..3be9a00
--- /dev/null
+++ b/src/cfg.hh
@@ -0,0 +1,45 @@
+#ifndef CFG_HH
+#define CFG_HH
+
+#include <filesystem>
+#include <memory>
+#include <optional>
+#include <string_view>
+#include <vector>
+
+namespace cfg {
+
+class Config {
+ public:
+ virtual ~Config() = default;
+
+ [[nodiscard]]
+ virtual std::optional<std::string_view> get(std::string_view name) const = 0;
+
+ [[nodiscard]]
+ bool has(std::string_view name) const;
+ [[nodiscard]]
+ std::optional<int64_t> get_int64(std::string_view name) const;
+ [[nodiscard]]
+ std::optional<uint64_t> get_uint64(std::string_view name) const;
+ [[nodiscard]]
+ std::optional<bool> get_bool(std::string_view name) const;
+
+ protected:
+ Config() = default;
+
+ Config(Config const&) = delete;
+ Config& operator=(Config const&) = delete;
+};
+
+[[nodiscard]]
+std::unique_ptr<Config> load_all(std::string_view name,
+ std::vector<std::string>& errors);
+
+[[nodiscard]]
+std::unique_ptr<Config> load_one(std::filesystem::path const& path,
+ std::vector<std::string>& errors);
+
+} // namespace cfg
+
+#endif // CFG_HH
diff --git a/src/check.hh b/src/check.hh
new file mode 100644
index 0000000..91c1717
--- /dev/null
+++ b/src/check.hh
@@ -0,0 +1,39 @@
+#ifndef CHECK_HH
+#define CHECK_HH
+
+#include <cstdlib>
+#include <stdckdint.h>
+#include <type_traits>
+
+namespace check {
+
+template <typename T>
+ requires std::is_arithmetic_v<T>
+T add(T a, T b) {
+ T ret;
+ if (ckd_add(&ret, a, b))
+ abort();
+ return ret;
+}
+
+template <typename T>
+ requires std::is_arithmetic_v<T>
+T sub(T a, T b) {
+ T ret;
+ if (ckd_sub(&ret, a, b))
+ abort();
+ return ret;
+}
+
+template <typename T>
+ requires std::is_arithmetic_v<T>
+T mul(T a, T b) {
+ T ret;
+ if (ckd_mul(&ret, a, b))
+ abort();
+ return ret;
+}
+
+} // namespace check
+
+#endif // CHECK_HH
diff --git a/src/colour.cc b/src/colour.cc
new file mode 100644
index 0000000..e9df340
--- /dev/null
+++ b/src/colour.cc
@@ -0,0 +1,15 @@
+#include "colour.hh"
+
+#include <cstdint>
+
+Colour::Colour() : argb(0) {}
+
+Colour::Colour(uint32_t argb) : argb(argb) {}
+
+Colour::Colour(uint8_t r, uint8_t g, uint8_t b)
+ : argb(0xff000000 | (static_cast<uint32_t>(r) << 16) |
+ (static_cast<uint32_t>(g) << 8) | b) {}
+
+Colour::Colour(uint8_t a, uint8_t r, uint8_t g, uint8_t b)
+ : argb((static_cast<uint32_t>(a) << 24) | (static_cast<uint32_t>(r) << 16) |
+ (static_cast<uint32_t>(g) << 8) | b) {}
diff --git a/src/colour.hh b/src/colour.hh
new file mode 100644
index 0000000..8362bb3
--- /dev/null
+++ b/src/colour.hh
@@ -0,0 +1,20 @@
+#ifndef COLOUR_HH
+#define COLOUR_HH
+
+#include <cstdint>
+
+struct Colour {
+ Colour();
+ explicit Colour(uint32_t argb);
+ Colour(uint8_t r, uint8_t g, uint8_t b);
+ Colour(uint8_t a, uint8_t r, uint8_t g, uint8_t b);
+
+ uint8_t alpha() const { return argb >> 24; };
+ uint8_t red() const { return (argb >> 16) & 0xff; };
+ uint8_t green() const { return (argb >> 8) & 0xff; };
+ uint8_t blue() const { return argb & 0xff; };
+
+ uint32_t argb;
+};
+
+#endif // COLOUR_HH
diff --git a/src/config.h.in b/src/config.h.in
new file mode 100644
index 0000000..5280a3d
--- /dev/null
+++ b/src/config.h.in
@@ -0,0 +1,6 @@
+#define VERSION "@version@"
+
+#define HAVE_JPEG @have_jpeg@
+#define HAVE_PNG @have_png@
+#define HAVE_RSVG @have_rsvg@
+#define HAVE_XPM @have_xpm@
diff --git a/src/image.cc b/src/image.cc
new file mode 100644
index 0000000..3e3b485
--- /dev/null
+++ b/src/image.cc
@@ -0,0 +1,15 @@
+#include "image.hh"
+
+#include "size.hh"
+
+#include <cstddef>
+#include <cstdint>
+#include <memory>
+#include <utility>
+
+Image::Image(Format format, Size size, size_t scanline,
+ std::unique_ptr<uint8_t[]> pixels)
+ : format_(format),
+ size_(size),
+ scanline_(scanline),
+ pixels_(std::move(pixels)) {}
diff --git a/src/image.hh b/src/image.hh
new file mode 100644
index 0000000..8191393
--- /dev/null
+++ b/src/image.hh
@@ -0,0 +1,62 @@
+#ifndef IMAGE_HH
+#define IMAGE_HH
+
+#include "size.hh"
+
+#include <cstddef>
+#include <cstdint>
+#include <memory>
+
+class Image {
+ public:
+ enum class Format : uint8_t {
+ RGBA_8888,
+ ARGB_8888,
+ BGRA_8888,
+ ABGR_8888,
+ };
+
+ [[nodiscard]]
+ Size const& size() const {
+ return size_;
+ }
+
+ [[nodiscard]]
+ uint32_t width() const {
+ return size_.width;
+ }
+
+ [[nodiscard]]
+ uint32_t height() const {
+ return size_.height;
+ }
+
+ [[nodiscard]]
+ Format format() const {
+ return format_;
+ }
+
+ [[nodiscard]]
+ size_t scanline() const {
+ return scanline_;
+ }
+
+ [[nodiscard]]
+ uint8_t const* data() const {
+ return pixels_.get();
+ }
+
+ Image(Format format, Size size, size_t scanline,
+ std::unique_ptr<uint8_t[]> pixels);
+
+ private:
+ Image(Image const&) = delete;
+ Image& operator=(Image const&) = delete;
+
+ Format const format_;
+ Size const size_;
+ size_t const scanline_;
+ std::unique_ptr<uint8_t[]> pixels_;
+};
+
+#endif // IMAGE_HH
diff --git a/src/image_loader.hh b/src/image_loader.hh
new file mode 100644
index 0000000..d283624
--- /dev/null
+++ b/src/image_loader.hh
@@ -0,0 +1,15 @@
+#ifndef IMAGE_LOADER_HH
+#define IMAGE_LOADER_HH
+
+#include "image.hh" // IWYU pragma: export
+
+#include <cstdint>
+
+enum class ImageLoadError : uint8_t {
+ kNoSuchFile,
+ kUnsupportedFormat,
+ kProcessError,
+ kError,
+};
+
+#endif // IMAGE_LOADER_HH
diff --git a/src/image_processor.cc b/src/image_processor.cc
new file mode 100644
index 0000000..55e3cac
--- /dev/null
+++ b/src/image_processor.cc
@@ -0,0 +1,947 @@
+#include "image_processor.hh"
+
+#include "colour.hh"
+#include "config.h"
+#include "image.hh"
+#include "image_loader.hh"
+#include "io.hh"
+#include "size.hh"
+#include "spawner.hh"
+
+#include <algorithm>
+#include <bit>
+#include <cassert>
+#include <cctype>
+#include <charconv>
+#include <cstdint>
+#include <cstring>
+#include <expected>
+#include <filesystem>
+#include <memory>
+#include <optional>
+#include <span>
+#include <string>
+#include <system_error>
+#include <utility>
+
+#if HAVE_JPEG || HAVE_PNG
+# include <cerrno>
+# include <csetjmp>
+# include <cstddef>
+# include <cstdio>
+#endif
+
+#if HAVE_JPEG
+# include <jpeglib.h>
+#endif
+
+#if HAVE_PNG
+# include <png.h>
+#endif
+
+#if HAVE_RSVG
+# include <cairo.h>
+# include <cmath>
+# include <gio/gio.h>
+# include <glib-object.h>
+# include <librsvg/rsvg.h>
+#endif
+
+#if HAVE_XPM
+# include <X11/xpm.h>
+extern "C" {
+# include "xpm/include/dix.h"
+}
+#endif
+
+namespace {
+
+struct Request {
+ bool head;
+ Image::Format format;
+ uint32_t max_width;
+ uint32_t max_height;
+ uint32_t background;
+ size_t path_len;
+};
+
+struct Response {
+ uint32_t width;
+ uint32_t height;
+ uint8_t error;
+ size_t scanline;
+};
+
+struct Result {
+ Response response;
+ Image::Format format;
+ std::unique_ptr<uint8_t[]> pixels;
+};
+
+std::expected<void, ImageLoadError> write_request(
+ io::Writer& writer, std::filesystem::path const& path, bool head,
+ Image::Format format, uint32_t max_width, uint32_t max_height,
+ Colour background) {
+ auto path_str = path.native();
+ Request request{.head = head, .format = format, .max_width = max_width,
+ .max_height = max_height, .background = background.argb,
+ .path_len = path_str.size()};
+ auto ret = writer.repeat_write(&request, sizeof(request));
+ if (!ret.has_value() || ret.value() != sizeof(request)) {
+ return std::unexpected(ImageLoadError::kProcessError);
+ }
+ ret = writer.repeat_write(path_str.data(), path_str.size());
+ if (!ret.has_value() || ret.value() != path_str.size()) {
+ return std::unexpected(ImageLoadError::kProcessError);
+ }
+ return {};
+}
+
+std::expected<Response, ImageLoadError> read_response(io::Reader& reader) {
+ Response response;
+ auto ret = reader.repeat_read(&response, sizeof(response));
+ if (!ret.has_value() || ret.value() != sizeof(response)) {
+ return std::unexpected(ImageLoadError::kProcessError);
+ }
+ if (response.width == 0) {
+ return std::unexpected(static_cast<ImageLoadError>(response.error));
+ }
+ return response;
+}
+
+Size rescale(Size const& size, uint32_t max_width, uint32_t max_height) {
+ if (max_width > 0 && size.width > max_height) {
+ if (max_height > 0 && size.height > max_height) {
+ auto sx = static_cast<float>(size.width) / static_cast<float>(max_width);
+ auto sy = static_cast<float>(size.height) / static_cast<float>(max_height);
+ if (sx >= sy) {
+ return Size{max_width, (size.height * max_width) / size.width};
+ }
+ return Size{(size.width * max_height) / size.height, max_height};
+ }
+ return Size{max_width, (size.height * max_width) / size.width};
+ }
+ if (max_height > 0 && size.height > max_height) {
+ return Size{(size.width * max_height) / size.height, max_height};
+ }
+ return size;
+}
+
+void swap_four_bytes(std::span<uint8_t> pixels, uint32_t width, uint32_t height,
+ size_t scanline) {
+ assert(height * scanline <= pixels.size());
+ auto* row = pixels.data();
+ while (height--) {
+ auto* const pixel = reinterpret_cast<uint32_t*>(row);
+ for (uint32_t x = 0; x < width; ++x) {
+ pixel[x] = std::byteswap(pixel[x]);
+ }
+ row += scanline;
+ }
+}
+
+void swap_two_bytes(std::span<uint8_t> pixels, uint32_t width, uint32_t height,
+ size_t scanline, size_t offset) {
+ assert(offset < 4);
+ assert(height * scanline <= pixels.size());
+ auto* row = pixels.data();
+ while (height--) {
+ auto* pixel = row + offset;
+ for (uint32_t x = 0; x < width; ++x, pixel += 4) {
+ std::swap(pixel[0], pixel[2]);
+ }
+ row += scanline;
+ }
+}
+
+void shift_left_bytes(std::span<uint8_t> pixels, uint32_t width,
+ uint32_t height, size_t scanline) {
+ assert(height * scanline <= pixels.size());
+ auto* row = pixels.data();
+ while (height--) {
+ auto* const pixel = reinterpret_cast<uint32_t*>(row);
+ for (uint32_t x = 0; x < width; ++x) {
+ pixel[x] = std::rotl(pixel[x], 8);
+ }
+ row += scanline;
+ }
+}
+
+void shift_right_bytes(std::span<uint8_t> pixels, uint32_t width,
+ uint32_t height, size_t scanline) {
+ assert(height * scanline <= pixels.size());
+ auto* row = pixels.data();
+ while (height--) {
+ auto* const pixel = reinterpret_cast<uint32_t*>(row);
+ for (uint32_t x = 0; x < width; ++x) {
+ pixel[x] = std::rotr(pixel[x], 8);
+ }
+ row += scanline;
+ }
+}
+
+void set_alpha_bytes(std::span<uint8_t> pixels, uint32_t width, uint32_t height,
+ size_t scanline, size_t offset) {
+ assert(height * scanline <= pixels.size());
+ assert(offset < 4);
+ auto* row = pixels.data();
+ while (height--) {
+ auto* pixel = row + offset;
+ for (uint32_t x = 0; x < width; ++x, pixel += 4) {
+ *pixel = 0xff;
+ }
+ row += scanline;
+ }
+}
+
+void insert_alpha_bytes(std::span<uint8_t> pixels, uint32_t width,
+ uint32_t height, size_t scanline, size_t offset) {
+ assert(static_cast<size_t>(width) * 4 <= scanline);
+ assert(height * scanline <= pixels.size());
+
+ if (offset == 0) {
+ auto* row = pixels.data();
+ while (height--) {
+ auto* read_pixel = row + static_cast<size_t>((width - 1) * 3);
+ auto* write_pixel = row + static_cast<size_t>((width - 1) * 4);
+ for (uint32_t x = 0; x < width; ++x, read_pixel -= 3, write_pixel -= 4) {
+ write_pixel[0] = 0xff;
+ std::copy_n(read_pixel, 3, write_pixel + 1);
+ }
+ row += scanline;
+ }
+ } else {
+ assert(offset == 3);
+
+ auto* row = pixels.data();
+ while (height--) {
+ auto* read_pixel = row + static_cast<size_t>((width - 1) * 3);
+ auto* write_pixel = row + static_cast<size_t>((width - 1) * 4);
+ for (uint32_t x = 0; x < width; ++x, read_pixel -= 3, write_pixel -= 4) {
+ std::copy_n(read_pixel, 3, write_pixel);
+ write_pixel[3] = 0xff;
+ }
+ row += scanline;
+ }
+ }
+}
+
+void rearrange_bytes(std::span<uint8_t> pixels, uint32_t width, uint32_t height,
+ size_t scanline, Image::Format source,
+ Image::Format target) {
+ switch (source) {
+ case Image::Format::RGBA_8888:
+ switch (target) {
+ case Image::Format::RGBA_8888:
+ return;
+ case Image::Format::ARGB_8888:
+ shift_right_bytes(pixels, width, height, scanline);
+ return;
+ case Image::Format::BGRA_8888:
+ swap_two_bytes(pixels, width, height, scanline, 0);
+ return;
+ case Image::Format::ABGR_8888:
+ swap_four_bytes(pixels, width, height, scanline);
+ return;
+ }
+ break;
+ case Image::Format::ARGB_8888:
+ switch (target) {
+ case Image::Format::RGBA_8888:
+ shift_left_bytes(pixels, width, height, scanline);
+ return;
+ case Image::Format::ARGB_8888:
+ return;
+ case Image::Format::BGRA_8888:
+ swap_four_bytes(pixels, width, height, scanline);
+ return;
+ case Image::Format::ABGR_8888:
+ swap_two_bytes(pixels, width, height, scanline, 1);
+ return;
+ }
+ break;
+ case Image::Format::BGRA_8888:
+ switch (target) {
+ case Image::Format::RGBA_8888:
+ swap_two_bytes(pixels, width, height, scanline, 0);
+ return;
+ case Image::Format::ARGB_8888:
+ swap_four_bytes(pixels, width, height, scanline);
+ return;
+ case Image::Format::BGRA_8888:
+ return;
+ case Image::Format::ABGR_8888:
+ shift_left_bytes(pixels, width, height, scanline);
+ return;
+ }
+ break;
+ case Image::Format::ABGR_8888:
+ switch (target) {
+ case Image::Format::RGBA_8888:
+ swap_four_bytes(pixels, width, height, scanline);
+ return;
+ case Image::Format::ARGB_8888:
+ swap_two_bytes(pixels, width, height, scanline, 1);
+ return;
+ case Image::Format::BGRA_8888:
+ shift_right_bytes(pixels, width, height, scanline);
+ return;
+ case Image::Format::ABGR_8888:
+ return;
+ }
+ break;
+ }
+}
+
+#if HAVE_JPEG
+struct jpeg_extended_error_mgr {
+ struct jpeg_error_mgr err;
+ jmp_buf jmpbuf;
+};
+
+void jpeg_error_exit(j_common_ptr info) {
+ auto* err = reinterpret_cast<jpeg_extended_error_mgr*>(info->err);
+ jpeg_destroy(info);
+ longjmp(err->jmpbuf, 1);
+}
+
+void jpeg_error_output_nothing(j_common_ptr /* info */) {
+ // be silent, do nothing
+}
+
+# if BITS_IN_JSAMPLE != 8
+# error Unsupported libjpeg setup
+# endif
+
+std::expected<Result, ImageLoadError> load_jpeg(
+ std::filesystem::path const& path, Request const& request) {
+ jpeg_extended_error_mgr err;
+ FILE* fh = nullptr;
+ if (setjmp(err.jmpbuf)) {
+ // This is better than exit() which is the default behavior,
+ // but almost guaranteed to leak some memory here.
+ if (fh)
+ fclose(fh);
+ return std::unexpected(ImageLoadError::kError);
+ }
+
+ fh = fopen(path.c_str(), "rb");
+ if (fh == nullptr) {
+ if (errno == ENOENT) {
+ return std::unexpected(ImageLoadError::kNoSuchFile);
+ }
+ return std::unexpected(ImageLoadError::kError);
+ }
+
+ jpeg_decompress_struct info;
+ info.err = jpeg_std_error(&err.err);
+ info.err->error_exit = jpeg_error_exit;
+ info.err->output_message = jpeg_error_output_nothing;
+
+ jpeg_create_decompress(&info);
+
+ jpeg_stdio_src(&info, fh);
+ if (jpeg_read_header(&info, TRUE) != JPEG_HEADER_OK) {
+ jpeg_destroy_decompress(&info);
+ fclose(fh);
+ return std::unexpected(ImageLoadError::kError);
+ }
+
+ if (request.head) {
+ Response resp{.width = info.image_width, .height = info.image_height, .error = 0, .scanline = 0};
+ jpeg_destroy_decompress(&info);
+ fclose(fh);
+ return Result{.response=resp, .format=Image::Format::RGBA_8888, .pixels=nullptr};
+ }
+
+ enum class Conversion : uint8_t {
+ kNone,
+ kSetAlphaFirst,
+ kSetAlphaLast,
+ kAddAlphaFirst,
+ kAddAlphaLast,
+ } conversion = Conversion::kNone;
+ Image::Format output_format = request.format;
+
+# if JCS_EXTENSIONS
+# if JCS_ALPHA_EXTENSIONS
+ switch (request.format) {
+ case Image::Format::RGBA_8888:
+ info.out_color_space = JCS_EXT_RGBA;
+ break;
+ case Image::Format::BGRA_8888:
+ info.out_color_space = JCS_EXT_BGRA;
+ break;
+ case Image::Format::ABGR_8888:
+ info.out_color_space = JCS_EXT_ABGR;
+ break;
+ case Image::Format::ARGB_8888:
+ info.out_color_space = JCS_EXT_ARGB;
+ break;
+ }
+# else
+ switch (request.format) {
+ case Image::Format::RGBA_8888:
+ info.out_color_space = JCS_EXT_RGBX;
+ conversion = Conversion::kSetAlphaLast;
+ break;
+ case Image::Format::BGRA_8888:
+ info.out_color_space = JCS_EXT_BGRX;
+ conversion = Conversion::kSetAlphaLast;
+ break;
+ case Image::Format::ABGR_8888:
+ info.out_color_space = JCS_EXT_XBGR;
+ conversion = Conversion::kSetAlphaFirst;
+ break;
+ case Image::Format::ARGB_8888:
+ info.out_color_space = JCS_EXT_XRGB;
+ conversion = Conversion::kSetAlphaFirst;
+ break;
+ }
+# endif
+# else
+# if RGB_RED != 0 || RGB_GREEN != 1 || RGB_BLUE != 2 || RGB_PIXELSIZE != 3
+# error Unsupported libjpeg setup
+# endif
+ info.out_color_space = JCS_RGB;
+ switch (request.format) {
+ case Image::Format::RGBA_8888:
+ case Image::Format::BGRA_8888:
+ format = Image::Format::RGBA_8888;
+ conversion = Conversion::kAddAlphaLast;
+ break;
+ case Image::Format::ABGR_8888:
+ case Image::Format::ARGB_8888:
+ format = Image::Format::ARGB_8888;
+ conversion = Conversion::kAddAlphaFirst;
+ break;
+ }
+# endif
+
+ if (request.max_width || request.max_height) {
+ auto new_size = rescale(Size{info.image_width, info.image_height},
+ request.max_width, request.max_height);
+ unsigned int denom = 2;
+ while (info.image_width / denom >= new_size.width &&
+ info.image_height / denom >= new_size.height) {
+ info.scale_denom = denom;
+ denom *= 2;
+ }
+ }
+
+ jpeg_start_decompress(&info);
+
+ size_t scanline = static_cast<size_t>(info.output_width) * 4;
+ auto pixels = std::make_unique<uint8_t[]>(scanline * info.output_height);
+
+ auto buffer = std::make_unique<JSAMPROW[]>(info.rec_outbuf_height);
+
+ while (info.output_scanline < info.output_height) {
+ int buf_height = 1;
+ buffer[0] = reinterpret_cast<JSAMPROW>(pixels.get() +
+ (info.output_scanline * scanline));
+ while (buf_height < info.rec_outbuf_height &&
+ info.output_scanline + buf_height < info.output_height) {
+ buffer[buf_height] = reinterpret_cast<JSAMPROW>(
+ pixels.get() + ((info.output_scanline + buf_height) * scanline));
+ ++buf_height;
+ }
+ jpeg_read_scanlines(&info, buffer.get(), buf_height);
+ }
+
+ switch (conversion) {
+ case Conversion::kNone:
+ break;
+ case Conversion::kAddAlphaFirst:
+ insert_alpha_bytes(std::span{pixels.get(), scanline * info.output_height},
+ info.output_width, info.output_height, scanline, 0);
+ break;
+ case Conversion::kAddAlphaLast:
+ insert_alpha_bytes(std::span{pixels.get(), scanline * info.output_height},
+ info.output_width, info.output_height, scanline, 3);
+ break;
+ case Conversion::kSetAlphaFirst:
+ set_alpha_bytes(std::span{pixels.get(), scanline * info.output_height},
+ info.output_width, info.output_height, scanline, 0);
+ break;
+ case Conversion::kSetAlphaLast:
+ set_alpha_bytes(std::span{pixels.get(), scanline * info.output_height},
+ info.output_width, info.output_height, scanline, 3);
+ break;
+ }
+
+ Response resp{.width = info.output_width, .height = info.output_height, .error = 0, .scanline = scanline};
+ jpeg_destroy_decompress(&info);
+ fclose(fh);
+ return Result{.response = resp, .format = output_format, .pixels = std::move(pixels)};
+}
+#endif
+
+#if HAVE_PNG
+// NOLINTBEGIN(misc-include-cleaner)
+void png_error(png_structp png_ptr, png_const_charp /* message */) {
+ // Don't write anything
+ longjmp(png_jmpbuf(png_ptr), 1);
+}
+
+void png_warning(png_structp /* png_ptr */, png_const_charp /* message */) {
+ // Do nothing
+}
+
+std::expected<Result, ImageLoadError> load_png(
+ std::filesystem::path const& path, Request const& request) {
+ FILE* fh = fopen(path.c_str(), "rb");
+ if (fh == nullptr) {
+ if (errno == ENOENT) {
+ return std::unexpected(ImageLoadError::kNoSuchFile);
+ }
+ return std::unexpected(ImageLoadError::kError);
+ }
+
+ uint8_t header[8];
+ if (fread(header, 1, sizeof(header), fh) != sizeof(header)) {
+ fclose(fh);
+ return std::unexpected(ImageLoadError::kError);
+ }
+
+ if (png_sig_cmp(header, 0, sizeof(header))) {
+ fclose(fh);
+ return std::unexpected(ImageLoadError::kError);
+ }
+
+ png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr,
+ png_error, png_warning);
+
+ if (!png_ptr) {
+ fclose(fh);
+ return std::unexpected(ImageLoadError::kError);
+ }
+
+ png_infop info_ptr = png_create_info_struct(png_ptr);
+ if (!info_ptr) {
+ png_destroy_read_struct(&png_ptr, nullptr, nullptr);
+ fclose(fh);
+ return std::unexpected(ImageLoadError::kError);
+ }
+
+ if (setjmp(png_jmpbuf(png_ptr))) {
+ png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
+ fclose(fh);
+ return std::unexpected(ImageLoadError::kError);
+ }
+
+ png_init_io(png_ptr, fh);
+ png_set_sig_bytes(png_ptr, sizeof(header));
+
+ png_set_alpha_mode(png_ptr, PNG_ALPHA_PNG, PNG_DEFAULT_sRGB);
+
+ png_read_info(png_ptr, info_ptr);
+
+ png_uint_32 width;
+ png_uint_32 height;
+ int bit_depth;
+ int color_type;
+
+ png_get_IHDR(png_ptr, info_ptr, &width, &height, &bit_depth, &color_type,
+ nullptr, nullptr, nullptr);
+
+ if (request.head) {
+ Response resp{.width =width, .height = height, .error = 0, .scanline = 0};
+ png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
+ fclose(fh);
+ return Result{.response = resp, .format = Image::Format::RGBA_8888, .pixels = nullptr};
+ }
+
+ if (request.background != 0) {
+ png_color_16 background_color;
+ Colour colour{request.background};
+ background_color.red = colour.red() << 8 | colour.red();
+ background_color.green = colour.green() << 8 | colour.green();
+ background_color.blue = colour.blue() << 8 | colour.blue();
+ png_set_background(png_ptr, &background_color, PNG_BACKGROUND_GAMMA_SCREEN,
+ 0, 1);
+ } else {
+ png_color_16p background_color;
+ if (png_get_bKGD(png_ptr, info_ptr, &background_color))
+ png_set_background(png_ptr, background_color, PNG_BACKGROUND_GAMMA_FILE,
+ 1, 1);
+ }
+
+ if (color_type == PNG_COLOR_TYPE_PALETTE)
+ png_set_palette_to_rgb(png_ptr);
+
+ if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS))
+ png_set_tRNS_to_alpha(png_ptr);
+
+ if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8)
+ png_set_expand_gray_1_2_4_to_8(png_ptr);
+
+ if (bit_depth == 16)
+ png_set_scale_16(png_ptr);
+
+ if (color_type == PNG_COLOR_TYPE_GRAY ||
+ color_type == PNG_COLOR_TYPE_GRAY_ALPHA)
+ png_set_gray_to_rgb(png_ptr);
+
+ switch (request.format) {
+ case Image::Format::RGBA_8888:
+ png_set_filler(png_ptr, 0xff, PNG_FILLER_AFTER);
+ break;
+ case Image::Format::ARGB_8888:
+ png_set_swap_alpha(png_ptr);
+ png_set_filler(png_ptr, 0xff, PNG_FILLER_BEFORE);
+ break;
+ case Image::Format::BGRA_8888:
+ png_set_bgr(png_ptr);
+ png_set_filler(png_ptr, 0xff, PNG_FILLER_AFTER);
+ break;
+ case Image::Format::ABGR_8888:
+ png_set_bgr(png_ptr);
+ png_set_swap_alpha(png_ptr);
+ png_set_filler(png_ptr, 0xff, PNG_FILLER_BEFORE);
+ break;
+ }
+
+ png_read_update_info(png_ptr, info_ptr);
+
+ width = png_get_image_width(png_ptr, info_ptr);
+ height = png_get_image_height(png_ptr, info_ptr);
+ bit_depth = png_get_bit_depth(png_ptr, info_ptr);
+ color_type = png_get_color_type(png_ptr, info_ptr);
+ size_t scanline = png_get_rowbytes(png_ptr, info_ptr);
+
+ /* Guard against integer overflow */
+ if (height > PNG_SIZE_MAX / scanline)
+ png_error(png_ptr, "image_data buffer would be too large");
+
+ auto pixels = std::make_unique<uint8_t[]>(scanline * height);
+ auto rows = std::make_unique<png_bytep[]>(height);
+
+ for (png_uint_32 y = 0; y < height; ++y)
+ rows[y] = pixels.get() + (y * scanline);
+
+ png_read_image(png_ptr, rows.get());
+
+ png_read_end(png_ptr, nullptr);
+
+ png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
+
+ fclose(fh);
+
+ Response resp{.width = width, .height = height, .error = 0, .scanline = scanline};
+ return Result{.response = resp, .format = request.format, .pixels = std::move(pixels)};
+}
+// NOLINTEND(misc-include-cleaner)
+#endif
+
+#if HAVE_RSVG
+std::expected<Result, ImageLoadError> load_svg(
+ std::filesystem::path const& path, Request const& request) {
+ GFile* file = g_file_new_for_path(path.c_str());
+ if (!file) {
+ return std::unexpected(ImageLoadError::kNoSuchFile);
+ }
+ RsvgHandle* handle =
+ rsvg_handle_new_from_gfile_sync(file, RSVG_HANDLE_FLAGS_NONE, nullptr, nullptr);
+
+ if (!handle) {
+ g_object_unref(file);
+ return std::unexpected(ImageLoadError::kError);
+ }
+ g_object_unref(file);
+
+ // TODO: Get this as part of the request?
+ rsvg_handle_set_dpi(handle, 96.0);
+
+ Size size;
+ {
+ gdouble width_double; // NOLINT(misc-include-cleaner)
+ gdouble height_double; // NOLINT(misc-include-cleaner)
+ if (!rsvg_handle_get_intrinsic_size_in_pixels(handle, &width_double,
+ &height_double)) {
+ g_object_unref(handle);
+ return std::unexpected(ImageLoadError::kError);
+ }
+ size.width = static_cast<uint32_t>(ceil(width_double));
+ size.height = static_cast<uint32_t>(ceil(height_double));
+ }
+
+ if (request.head) {
+ Response resp{.width = size.width, .height = size.height, .error = 0, .scanline = 0};
+ g_object_unref(handle);
+ return Result{.response = resp, .format = Image::Format::RGBA_8888, .pixels = nullptr};
+ }
+
+ if (request.max_width || request.max_height) {
+ size = rescale(size, request.max_width, request.max_height);
+ }
+
+ size_t scanline = static_cast<size_t>(size.width) * 4;
+ auto pixels = std::make_unique<uint8_t[]>(scanline * size.height);
+
+ cairo_surface_t* surface = cairo_image_surface_create_for_data(
+ reinterpret_cast<unsigned char*>(pixels.get()), CAIRO_FORMAT_ARGB32,
+ static_cast<int>(size.width), static_cast<int>(size.height),
+ static_cast<int>(scanline));
+
+ if (!surface) {
+ g_object_unref(handle);
+ return std::unexpected(ImageLoadError::kError);
+ }
+
+ cairo_t* cr = cairo_create(surface);
+
+ if (request.background) {
+ cairo_rectangle(cr, 0, 0, size.width, size.height);
+ Colour colour{request.background};
+ cairo_set_source_rgba(cr, colour.red() / 255.0, colour.green() / 255.0,
+ colour.blue() / 255.0, colour.alpha() / 255.0);
+ cairo_fill(cr);
+ }
+
+ RsvgRectangle viewport = {
+ .x = 0.0,
+ .y = 0.0,
+ .width = static_cast<double>(size.width),
+ .height = static_cast<double>(size.height),
+ };
+
+ if (!rsvg_handle_render_document(handle, cr, &viewport, nullptr)) {
+ cairo_destroy(cr);
+ cairo_surface_destroy(surface);
+ g_object_unref(handle);
+ return std::unexpected(ImageLoadError::kError);
+ }
+
+ cairo_destroy(cr);
+ cairo_surface_destroy(surface);
+ g_object_unref(handle);
+
+ // Cairo uses premultiplied pixels, we don't
+ {
+ uint8_t* row = pixels.get();
+ for (uint32_t y = 0; y < size.height; ++y) {
+ uint8_t* pixel = row;
+ for (uint32_t x = 0; x < size.width; ++x, pixel += 4) {
+ if constexpr (std::endian::native == std::endian::big) {
+ if (pixel[0] != 0xff && pixel[0] > 0) {
+ pixel[1] = (static_cast<uint16_t>(pixel[1]) * 255) / pixel[0];
+ pixel[2] = (static_cast<uint16_t>(pixel[2]) * 255) / pixel[0];
+ pixel[3] = (static_cast<uint16_t>(pixel[3]) * 255) / pixel[0];
+ }
+ } else {
+ if (pixel[3] != 0xff && pixel[3] > 0) {
+ pixel[0] = (static_cast<uint16_t>(pixel[0]) * 255) / pixel[3];
+ pixel[1] = (static_cast<uint16_t>(pixel[1]) * 255) / pixel[3];
+ pixel[2] = (static_cast<uint16_t>(pixel[2]) * 255) / pixel[3];
+ }
+ }
+ }
+ row += scanline;
+ }
+ }
+
+ return Result{
+ .response = Response{.width = size.width, .height = size.height, .error = 0, .scanline = scanline},
+ .format = std::endian::native == std::endian::big ? Image::Format::ARGB_8888
+ : Image::Format::BGRA_8888,
+ .pixels = std::move(pixels)
+ };
+}
+#endif
+
+#if HAVE_XPM
+
+std::optional<uint32_t> xpm_parse_color(XpmColor const& color) {
+ if (color.c_color) {
+ auto const len = strlen(color.c_color);
+ if (color.c_color[0] == '#' && len == 7) {
+ uint32_t rgb;
+ if (std::from_chars(color.c_color + 1, color.c_color + 7, rgb, 16).ec == std::errc()) {
+ return 0xff000000 | rgb;
+ }
+ }
+ if (len == 4 && (strcmp(color.c_color, "None") == 0 ||
+ strcmp(color.c_color, "none") == 0))
+ return 0;
+ unsigned short red;
+ unsigned short green;
+ unsigned short blue;
+ if (dixLookupBuiltinColor(0, color.c_color, len,
+ &red, &green, &blue)) {
+ return 0xff000000 | (static_cast<uint32_t>(red & 0xff) << 16) |
+ (green & 0xff00) | (blue & 0xff);
+ }
+ }
+ if (color.m_color) {
+ if (strcmp(color.m_color, "white") == 0) {
+ return 0xffffffff;
+ }
+ if (strcmp(color.m_color, "black") == 0) {
+ return 0xff000000;
+ }
+ }
+ return std::nullopt;
+}
+
+std::expected<Result, ImageLoadError> load_xpm(
+ std::filesystem::path const& path, Request const& request) {
+ XpmImage image;
+ XpmInfo info;
+ auto ret = XpmReadFileToXpmImage(path.c_str(), &image, &info);
+ if (ret != XpmSuccess) {
+ if (ret == XpmOpenFailed)
+ return std::unexpected(ImageLoadError::kNoSuchFile);
+ return std::unexpected(ImageLoadError::kError);
+ }
+
+ if (request.head) {
+ Response resp{.width = image.width, .height = image.height, .error = 0, .scanline = 0};
+ XpmFreeXpmImage(&image);
+ XpmFreeXpmInfo(&info);
+ return Result{.response = resp, .format = Image::Format::RGBA_8888, .pixels = nullptr};
+ }
+
+ size_t scanline = static_cast<size_t>(image.width) * 4;
+ auto pixels = std::make_unique<uint8_t[]>(scanline * image.height);
+
+ auto colors = std::make_unique<uint32_t[]>(image.ncolors);
+ for (unsigned int i = 0; i < image.ncolors; ++i) {
+ auto ret = xpm_parse_color(image.colorTable[i]);
+ if (ret.has_value()) {
+ colors[i] = ret.value();
+ } else {
+ return std::unexpected(ImageLoadError::kUnsupportedFormat);
+ }
+ }
+
+ auto* out_row = pixels.get();
+ auto* in = image.data;
+ for (unsigned int y = 0; y < image.height; ++y) {
+ auto* out_pixel = reinterpret_cast<uint32_t*>(out_row);
+ for (unsigned int x = 0; x < image.width; ++x) {
+ out_pixel[x] = colors[*in++];
+ }
+ out_row += scanline;
+ }
+
+ XpmFreeXpmImage(&image);
+ XpmFreeXpmInfo(&info);
+
+ return Result{
+ .response = Response{.width = image.width, .height = image.height, .error = 0, .scanline = scanline},
+ .format = std::endian::native == std::endian::big ? Image::Format::ARGB_8888 : Image::Format::BGRA_8888,
+ .pixels = std::move(pixels),
+ };
+}
+#endif
+
+} // namespace
+
+namespace image_processor {
+
+std::expected<Size, ImageLoadError> peek(Process& process,
+ std::filesystem::path const& path) {
+ auto ret = write_request(process.writer(), path, true,
+ Image::Format::ARGB_8888, 0, 0, {});
+ if (!ret) {
+ return std::unexpected(ret.error());
+ }
+ auto ret2 = read_response(process.reader());
+ if (!ret2)
+ return std::unexpected(ret2.error());
+ return Size{ret2->width, ret2->height};
+}
+
+std::expected<std::unique_ptr<Image>, ImageLoadError> load(
+ Process& process, std::filesystem::path const& path, Image::Format format,
+ uint32_t max_width, uint32_t max_height, std::optional<Colour> background) {
+ auto ret = write_request(process.writer(), path, false, format, max_width,
+ max_height, background.value_or({}));
+ if (!ret) {
+ return std::unexpected(ret.error());
+ }
+ auto ret2 = read_response(process.reader());
+ if (!ret2)
+ return std::unexpected(ret2.error());
+ size_t size = static_cast<size_t>(ret2->height) * ret2->scanline;
+ auto pixels = std::make_unique<uint8_t[]>(size);
+ auto ret3 = process.reader().repeat_read(pixels.get(), size);
+ if (!ret3.has_value() || ret3.value() != size) {
+ return std::unexpected(ret.error());
+ }
+ return std::make_unique<Image>(format, Size{ret2->width, ret2->height},
+ ret2->scanline, std::move(pixels));
+}
+
+int run(std::unique_ptr<io::Reader> reader,
+ std::unique_ptr<io::Writer> writer) {
+ while (true) {
+ Request request;
+ auto ret = reader->repeat_read(&request, sizeof(request));
+ if (!ret.has_value() || ret.value() != sizeof(request))
+ break;
+ std::string path_str(request.path_len, ' ');
+ ret = reader->repeat_read(path_str.data(), path_str.size());
+ if (!ret.has_value() || ret.value() != path_str.size())
+ break;
+ std::filesystem::path path(std::move(path_str));
+
+ auto extension = path.extension().native();
+ std::ranges::transform(extension, extension.begin(),
+ [](unsigned char c){ return std::tolower(c); });
+ std::expected<Result, ImageLoadError> result{
+ std::unexpected(ImageLoadError::kUnsupportedFormat)};
+#if HAVE_JPEG
+ if (extension == ".jpeg" || extension == ".jpg" || extension == ".jpe" ||
+ extension == ".jfif" || extension == ".jfi" || extension == ".jif") {
+ result = load_jpeg(path, request);
+ }
+#endif
+#if HAVE_PNG
+ if (path.extension() == ".png") {
+ result = load_png(path, request);
+ }
+#endif
+#if HAVE_RSVG
+ if (path.extension() == ".svg" || path.extension() == ".svgz") {
+ result = load_svg(path, request);
+ }
+#endif
+#if HAVE_XPM
+ if (path.extension() == ".xpm") {
+ result = load_xpm(path, request);
+ }
+#endif
+ if (!request.head && result.has_value() &&
+ result->format != request.format) {
+ auto size = static_cast<size_t>(result->response.height) *
+ result->response.scanline;
+ rearrange_bytes(std::span(result->pixels.get(), size),
+ result->response.width, result->response.height,
+ result->response.scanline, result->format,
+ request.format);
+ }
+ if (result.has_value()) {
+ auto ret2 =
+ writer->repeat_write(&result->response, sizeof(result->response));
+ if (!ret2.has_value() || ret2.value() != sizeof(result->response))
+ break;
+ if (!request.head) {
+ auto size = static_cast<size_t>(result->response.height) *
+ result->response.scanline;
+ ret2 = writer->repeat_write(result->pixels.get(), size);
+ if (!ret2.has_value() || ret2.value() != size)
+ break;
+ }
+ } else {
+ Response response{.width = 0, .height = 0, .error = static_cast<uint8_t>(result.error()), .scanline = 0};
+ auto ret2 = writer->repeat_write(&response, sizeof(response));
+ if (!ret2.has_value() || ret2.value() != sizeof(response))
+ break;
+ }
+ }
+ return -1;
+}
+
+} // namespace image_processor
diff --git a/src/image_processor.hh b/src/image_processor.hh
new file mode 100644
index 0000000..551ac4f
--- /dev/null
+++ b/src/image_processor.hh
@@ -0,0 +1,29 @@
+#ifndef IMAGE_PROCESSOR_HH
+#define IMAGE_PROCESSOR_HH
+
+#include "colour.hh"
+#include "image_loader.hh"
+#include "io.hh"
+#include "spawner.hh"
+
+#include <filesystem>
+#include <memory>
+#include <optional>
+
+namespace image_processor {
+
+[[nodiscard]]
+std::expected<Size, ImageLoadError> peek(Process& process,
+ std::filesystem::path const& path);
+
+[[nodiscard]]
+std::expected<std::unique_ptr<Image>, ImageLoadError> load(
+ Process& process, std::filesystem::path const& path, Image::Format format,
+ uint32_t max_width = 0, uint32_t max_height = 0,
+ std::optional<Colour> background = std::nullopt);
+
+int run(std::unique_ptr<io::Reader> reader, std::unique_ptr<io::Writer> writer);
+
+} // namespace image_processor
+
+#endif // IMAGE_PROCESSOR_HH
diff --git a/src/io.cc b/src/io.cc
new file mode 100644
index 0000000..660c5f7
--- /dev/null
+++ b/src/io.cc
@@ -0,0 +1,358 @@
+#include "io.hh"
+
+#include "unique_fd.hh"
+
+#include <algorithm>
+#include <cerrno>
+#include <cstdio>
+#include <cstring>
+#include <expected>
+#include <fcntl.h>
+#include <limits>
+#include <memory>
+#include <optional>
+#include <string>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <utility>
+
+namespace io {
+
+namespace {
+
+class BasicReader : public Reader {
+ public:
+ explicit BasicReader(unique_fd fd) : fd_(std::move(fd)) {}
+
+ [[nodiscard]]
+ std::expected<size_t, ReadError> read(void* dst, size_t max) override {
+ ssize_t ret = ::read(
+ fd_.get(), dst,
+ std::min(static_cast<size_t>(std::numeric_limits<ssize_t>::max()),
+ max));
+ if (ret < 0) {
+ switch (errno) {
+ case EINTR:
+ return read(dst, max);
+ default:
+ return std::unexpected(ReadError::Error);
+ }
+ } else if (ret == 0 && max > 0) {
+ return std::unexpected(ReadError::Eof);
+ }
+ offset_ += ret;
+ return ret;
+ }
+
+ [[nodiscard]]
+ std::expected<size_t, ReadError> skip(size_t max) override {
+ off_t ret;
+ if (sizeof(size_t) > sizeof(off_t)) {
+ ret = lseek(
+ fd_.get(),
+ // NOLINTNEXTLINE(bugprone-narrowing-conversions)
+ std::min(static_cast<size_t>(std::numeric_limits<off_t>::max()), max),
+ SEEK_CUR);
+ } else {
+ ret = lseek(fd_.get(), static_cast<off_t>(max), SEEK_CUR);
+ }
+ if (ret < 0) {
+ return std::unexpected(ReadError::Error);
+ }
+ // Don't want skip to go past (cached) file end.
+ if (!size_.has_value() || ret >= size_.value()) {
+ // When going past end, double check that it still is the end.
+ off_t ret2 = lseek(fd_.get(), 0, SEEK_END);
+ if (ret2 < 0) {
+ // We're screwed, but try to go back to original position and then
+ // return error.
+ size_.reset();
+ lseek(fd_.get(), offset_, SEEK_SET);
+ return std::unexpected(ReadError::Error);
+ }
+ size_ = ret2;
+ if (ret >= ret2) {
+ auto distance = ret2 - offset_;
+ offset_ = ret2;
+ if (distance == 0 && max > 0)
+ return std::unexpected(ReadError::Eof);
+ return distance;
+ }
+ // Seek back to where we should be
+ if (lseek(fd_.get(), ret, SEEK_SET) < 0) {
+ return std::unexpected(ReadError::Error);
+ }
+ }
+ auto distance = ret - offset_;
+ offset_ = ret;
+ return distance;
+ }
+
+ [[nodiscard]]
+ int raw_fd() const override {
+ return fd_.get();
+ }
+
+ private:
+ unique_fd fd_;
+ off_t offset_{0};
+ std::optional<off_t> size_;
+};
+
+class MemoryReader : public Reader {
+ public:
+ MemoryReader(void* ptr, size_t size) : ptr_(ptr), size_(size) {}
+
+ [[nodiscard]]
+ std::expected<size_t, ReadError> read(void* dst, size_t max) override {
+ size_t avail = size_ - offset_;
+ if (avail == 0 && max > 0)
+ return std::unexpected(io::ReadError::Eof);
+ size_t ret = std::min(max, avail);
+ memcpy(dst, reinterpret_cast<char*>(ptr_) + offset_, ret);
+ offset_ += ret;
+ return ret;
+ }
+
+ [[nodiscard]]
+ std::expected<size_t, ReadError> skip(size_t max) override {
+ size_t avail = size_ - offset_;
+ size_t ret = std::min(max, avail);
+ offset_ += ret;
+ return ret;
+ }
+
+ [[nodiscard]]
+ int raw_fd() const override {
+ return -1;
+ }
+
+ protected:
+ void* ptr_;
+ size_t const size_;
+
+ private:
+ size_t offset_{0};
+};
+
+class MmapReader : public MemoryReader {
+ public:
+ MmapReader(unique_fd fd, void* ptr, size_t size)
+ : MemoryReader(ptr, size), fd_(std::move(fd)) {}
+
+ ~MmapReader() override { munmap(ptr_, size_); }
+
+ [[nodiscard]]
+ int raw_fd() const override {
+ return fd_.get();
+ }
+
+ private:
+ unique_fd fd_;
+};
+
+class StringReader : public MemoryReader {
+ public:
+ explicit StringReader(std::string data)
+ : MemoryReader(nullptr, data.size()), data_(std::move(data)) {
+ ptr_ = data_.data();
+ }
+
+ private:
+ std::string data_;
+};
+
+class BasicWriter : public Writer {
+ public:
+ explicit BasicWriter(unique_fd fd) : fd_(std::move(fd)) {}
+
+ [[nodiscard]]
+ std::expected<size_t, WriteError> write(void const* dst,
+ size_t size) override {
+ ssize_t ret = ::write(
+ fd_.get(), dst,
+ std::min(static_cast<size_t>(std::numeric_limits<ssize_t>::max()),
+ size));
+ if (ret < 0) {
+ switch (errno) {
+ case EINTR:
+ return write(dst, size);
+ default:
+ return std::unexpected(WriteError::Error);
+ }
+ } else if (ret == 0 && size > 0) {
+ return std::unexpected(WriteError::Error);
+ }
+ return ret;
+ }
+
+ [[nodiscard]]
+ std::expected<void, WriteError> close() override {
+ if (::close(fd_.release()) == 0)
+ return {};
+ return std::unexpected(WriteError::Error);
+ }
+
+ [[nodiscard]]
+ int raw_fd() const override {
+ return fd_.get();
+ }
+
+ private:
+ unique_fd fd_;
+};
+
+} // namespace
+
+std::expected<size_t, ReadError> Reader::repeat_read(void* dst, size_t max) {
+ auto ret = read(dst, max);
+ if (!ret.has_value() || ret.value() == max)
+ return ret;
+
+ char* d = reinterpret_cast<char*>(dst);
+ size_t offset = ret.value();
+ while (true) {
+ ret = read(d + offset, max - offset);
+ if (!ret.has_value())
+ break;
+ offset += ret.value();
+ if (offset == max)
+ break;
+ }
+ return offset;
+}
+
+std::expected<size_t, ReadError> Reader::repeat_skip(size_t max) {
+ auto ret = skip(max);
+ if (!ret.has_value() || ret.value() == max)
+ return ret;
+
+ size_t offset = ret.value();
+ while (true) {
+ ret = skip(max - offset);
+ if (!ret.has_value())
+ break;
+ offset += ret.value();
+ if (offset == max)
+ break;
+ }
+ return offset;
+}
+
+std::expected<size_t, WriteError> Writer::repeat_write(void const* dst,
+ size_t size) {
+ auto ret = write(dst, size);
+ if (!ret.has_value() || ret.value() == size)
+ return ret;
+
+ char const* d = reinterpret_cast<char const*>(dst);
+ size_t offset = ret.value();
+ while (true) {
+ ret = write(d + offset, size - offset);
+ if (!ret.has_value())
+ break;
+ offset += ret.value();
+ if (offset == size)
+ break;
+ }
+ return offset;
+}
+
+std::expected<std::unique_ptr<Reader>, OpenError> open(
+ const std::string& file_path) {
+ return openat(AT_FDCWD, file_path);
+}
+
+std::expected<std::unique_ptr<Reader>, OpenError> openat(
+ int dirfd, const std::string& file_path) {
+ unique_fd fd(::openat(dirfd, file_path.c_str(), O_RDONLY));
+ if (fd) {
+ struct stat buf;
+ if (fstat(fd.get(), &buf) == 0) {
+ if (std::cmp_less_equal(buf.st_size,
+ std::numeric_limits<size_t>::max())) {
+ auto size = static_cast<size_t>(buf.st_size);
+ void* ptr = mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd.get(), 0);
+ if (ptr != MAP_FAILED) {
+ return std::make_unique<MmapReader>(std::move(fd), ptr, size);
+ }
+ }
+ }
+ return std::make_unique<BasicReader>(std::move(fd));
+ }
+ OpenError err;
+ switch (errno) {
+ case EINTR:
+ return openat(dirfd, file_path);
+ case EACCES:
+ err = OpenError::NoAccess;
+ break;
+ case ENOENT:
+ err = OpenError::NoSuchFile;
+ break;
+ default:
+ err = OpenError::Error;
+ break;
+ }
+ return std::unexpected(err);
+}
+
+std::unique_ptr<Reader> memory(std::string data) {
+ return std::make_unique<StringReader>(std::move(data));
+}
+
+std::expected<std::unique_ptr<Writer>, CreateError> create(
+ const std::string& file_path, bool replace_existing) {
+ return createat(AT_FDCWD, file_path, replace_existing);
+}
+
+std::expected<std::unique_ptr<Writer>, CreateError> createat(
+ int dirfd, const std::string& file_path, bool replace_existing) {
+ int flags = O_WRONLY | O_CREAT;
+ if (replace_existing) {
+ flags |= O_TRUNC;
+ } else {
+ flags |= O_EXCL;
+ }
+ unique_fd fd(::openat(dirfd, file_path.c_str(), flags, 0666));
+ if (fd) {
+ return std::make_unique<BasicWriter>(std::move(fd));
+ }
+ CreateError err;
+ switch (errno) {
+ case EINTR:
+ return createat(dirfd, file_path, replace_existing);
+ case EACCES:
+ err = CreateError::NoAccess;
+ break;
+ case EEXIST:
+ err = CreateError::Exists;
+ break;
+ default:
+ err = CreateError::Error;
+ break;
+ }
+ return std::unexpected(err);
+}
+
+std::expected<std::pair<std::unique_ptr<Reader>, std::unique_ptr<Writer>>,
+ PipeError>
+pipe() {
+ int fds[2];
+ if (::pipe(fds) == 0) {
+ return std::make_pair(reader_from_raw(fds[0]), writer_from_raw(fds[1]));
+ }
+ return std::unexpected(PipeError::Error);
+}
+
+std::unique_ptr<Reader> reader_from_raw(int fd) {
+ return std::make_unique<BasicReader>(unique_fd{fd});
+}
+
+std::unique_ptr<Writer> writer_from_raw(int fd) {
+ return std::make_unique<BasicWriter>(unique_fd{fd});
+}
+
+} // namespace io
diff --git a/src/io.hh b/src/io.hh
new file mode 100644
index 0000000..90ad52e
--- /dev/null
+++ b/src/io.hh
@@ -0,0 +1,109 @@
+#ifndef IO_HH
+#define IO_HH
+
+#include <cstddef>
+#include <expected>
+#include <memory>
+#include <string>
+#include <utility>
+
+namespace io {
+
+enum class ReadError : uint8_t {
+ Error,
+ Eof,
+ InvalidData, // invalid data read (not used by raw file)
+ MaxTooSmall, // max argument needs to be bigger (not used by raw file)
+};
+
+enum class WriteError : uint8_t {
+ Error,
+};
+
+enum class OpenError : uint8_t {
+ NoSuchFile,
+ NoAccess,
+ Error,
+};
+
+enum class CreateError : uint8_t {
+ Exists,
+ NoAccess,
+ Error,
+};
+
+enum class PipeError : uint8_t {
+ Error,
+};
+
+class Reader {
+ public:
+ virtual ~Reader() = default;
+
+ [[nodiscard]] virtual std::expected<size_t, ReadError> read(void* dst,
+ size_t max) = 0;
+ [[nodiscard]] virtual std::expected<size_t, ReadError> skip(size_t max) = 0;
+
+ [[nodiscard]] std::expected<size_t, ReadError> repeat_read(void* dst,
+ size_t max);
+ [[nodiscard]] std::expected<size_t, ReadError> repeat_skip(size_t max);
+
+ [[nodiscard]]
+ virtual int raw_fd() const = 0;
+
+ protected:
+ Reader() = default;
+
+ Reader(Reader const&) = delete;
+ Reader& operator=(Reader const&) = delete;
+};
+
+class Writer {
+ public:
+ virtual ~Writer() = default;
+
+ [[nodiscard]]
+ virtual std::expected<size_t, WriteError> write(void const* dst,
+ size_t size) = 0;
+
+ // Use this instead of the destructor if you care if the call returned an
+ // error or not. Regardless of returned value, after this method is called
+ // write will always fail.
+ [[nodiscard]]
+ virtual std::expected<void, WriteError> close() = 0;
+
+ [[nodiscard]]
+ virtual int raw_fd() const = 0;
+
+ [[nodiscard]] std::expected<size_t, WriteError> repeat_write(void const* dst,
+ size_t size);
+
+ protected:
+ Writer() = default;
+
+ Writer(Writer const&) = delete;
+ Writer& operator=(Writer const&) = delete;
+};
+
+[[nodiscard]] std::expected<std::unique_ptr<Reader>, OpenError> open(
+ const std::string& file_path);
+[[nodiscard]] std::expected<std::unique_ptr<Reader>, OpenError> openat(
+ int dirfd, const std::string& file_path);
+[[nodiscard]] std::unique_ptr<Reader> memory(std::string data);
+
+[[nodiscard]] std::expected<std::unique_ptr<Writer>, CreateError> create(
+ const std::string& file_path, bool replace_existing);
+[[nodiscard]] std::expected<std::unique_ptr<Writer>, CreateError> createat(
+ int dirfd, const std::string& file_path, bool replace_existing);
+
+[[nodiscard]] std::expected<
+ std::pair<std::unique_ptr<Reader>, std::unique_ptr<Writer>>, PipeError>
+pipe();
+
+// Takes ownership of the fd.
+[[nodiscard]] std::unique_ptr<Reader> reader_from_raw(int fd);
+[[nodiscard]] std::unique_ptr<Writer> writer_from_raw(int fd);
+
+} // namespace io
+
+#endif // IO_HH
diff --git a/src/line.cc b/src/line.cc
new file mode 100644
index 0000000..23370fc
--- /dev/null
+++ b/src/line.cc
@@ -0,0 +1,127 @@
+#include "line.hh"
+
+#include "check.hh"
+
+#include <algorithm>
+#include <cassert>
+#include <cstdint>
+#include <cstring>
+#include <expected>
+#include <memory>
+#include <string_view>
+#include <utility>
+
+namespace line {
+
+namespace {
+
+const char kLineTerminators[] = "\r\n";
+
+class ReaderImpl : public Reader {
+ public:
+ ReaderImpl(std::unique_ptr<io::Reader> reader, size_t max_len)
+ : reader_(std::move(reader)),
+ max_len_(max_len),
+ buffer_(std::make_unique_for_overwrite<char[]>(
+ check::add(max_len, static_cast<size_t>(2)))),
+ rptr_(buffer_.get()),
+ wptr_(buffer_.get()),
+ search_(rptr_),
+ end_(buffer_.get() + check::add(max_len, static_cast<size_t>(2))) {}
+
+ [[nodiscard]] std::expected<std::string_view, io::ReadError> read() override {
+ while (true) {
+ search_ = std::find_first_of(search_, wptr_, kLineTerminators,
+ kLineTerminators + 2);
+ if (search_ < wptr_) {
+ if (std::cmp_greater(search_ - rptr_, max_len_)) {
+ return line(max_len_, 0);
+ }
+
+ size_t tlen;
+ if (*search_ == '\n') {
+ tlen = 1;
+ } else {
+ if (search_ + 1 == wptr_) {
+ make_space_if_needed();
+ auto got = fill();
+ if (!got.has_value()) {
+ if (got.error() == io::ReadError::Eof) {
+ return line(search_ - rptr_, 1);
+ }
+ return std::unexpected(got.error());
+ }
+ }
+ if (search_[1] == '\n') {
+ tlen = 2;
+ } else {
+ tlen = 1;
+ }
+ }
+ return line(search_ - rptr_, tlen);
+ }
+ if (std::cmp_greater_equal(wptr_ - rptr_, max_len_)) {
+ return line(max_len_, 0);
+ }
+
+ make_space_if_needed();
+ auto got = fill();
+ if (!got.has_value()) {
+ if (got.error() == io::ReadError::Eof && rptr_ != wptr_) {
+ return line(wptr_ - rptr_, 0);
+ }
+ return std::unexpected(got.error());
+ }
+ }
+ }
+
+ [[nodiscard]] uint64_t number() const override { return number_; }
+
+ private:
+ std::string_view line(size_t len, size_t terminator_len) {
+ assert(len <= max_len_);
+ auto ret = std::string_view(rptr_, len);
+ rptr_ += len + terminator_len;
+ search_ = rptr_;
+ ++number_;
+ return ret;
+ }
+
+ void make_space_if_needed() {
+ size_t free = rptr_ - buffer_.get();
+ if (free == 0)
+ return;
+ size_t avail = end_ - wptr_;
+ if (avail > 1024)
+ return;
+ memmove(buffer_.get(), rptr_, wptr_ - rptr_);
+ search_ -= free;
+ wptr_ -= free;
+ rptr_ = buffer_.get();
+ }
+
+ std::expected<size_t, io::ReadError> fill() {
+ auto ret = reader_->read(wptr_, end_ - wptr_);
+ if (ret.has_value())
+ wptr_ += ret.value();
+ return ret;
+ }
+
+ std::unique_ptr<io::Reader> reader_;
+ size_t const max_len_;
+ uint64_t number_{0};
+ std::unique_ptr<char[]> buffer_;
+ char* rptr_;
+ char* wptr_;
+ char* search_;
+ char* const end_;
+};
+
+} // namespace
+
+std::unique_ptr<Reader> open(std::unique_ptr<io::Reader> reader,
+ size_t max_len) {
+ return std::make_unique<ReaderImpl>(std::move(reader), max_len);
+}
+
+} // namespace line
diff --git a/src/line.hh b/src/line.hh
new file mode 100644
index 0000000..a8eeea8
--- /dev/null
+++ b/src/line.hh
@@ -0,0 +1,37 @@
+#ifndef LINE_HH
+#define LINE_HH
+
+#include "io.hh" // IWYU pragma: export
+
+#include <cstddef>
+#include <expected>
+#include <memory>
+#include <optional>
+#include <string_view>
+
+namespace line {
+
+class Reader {
+ public:
+ virtual ~Reader() = default;
+
+ // Returned view is only valid until next call to read.
+ [[nodiscard]]
+ virtual std::expected<std::string_view, io::ReadError> read() = 0;
+ // Starts at zero. Returns next line.
+ // So, before first read it is zero, after first read it is one.
+ [[nodiscard]] virtual uint64_t number() const = 0;
+
+ protected:
+ Reader() = default;
+
+ Reader(Reader const&) = delete;
+ Reader& operator=(Reader const&) = delete;
+};
+
+[[nodiscard]] std::unique_ptr<Reader> open(std::unique_ptr<io::Reader> reader,
+ size_t max_len = 8192);
+
+} // namespace line
+
+#endif // LINE_HH
diff --git a/src/logger.cc b/src/logger.cc
new file mode 100644
index 0000000..e6198da
--- /dev/null
+++ b/src/logger.cc
@@ -0,0 +1,125 @@
+#include "logger.hh"
+
+#include <cstdint>
+#include <format>
+#include <iostream>
+#include <memory>
+#include <string>
+#include <string_view>
+#include <utility>
+
+namespace logger {
+
+namespace {
+
+class BaseLogger : public Logger {
+ protected:
+ enum class Level : uint8_t {
+ kError,
+ kWarning,
+ kInfo,
+ kDebug,
+ };
+
+ public:
+ void err(std::string_view message) final { write(Level::kError, message); }
+
+ void warn(std::string_view message) final { write(Level::kWarning, message); }
+
+ void info(std::string_view message) final { write(Level::kInfo, message); }
+
+#ifndef NDEBUG
+ void dbg(std::string_view message) final { write(Level::kDebug, message); }
+#endif
+
+ protected:
+ BaseLogger() = default;
+
+ virtual void write(Level level, std::string_view message) = 0;
+};
+
+class NoopLogger : public BaseLogger {
+ public:
+ NoopLogger() = default;
+
+ protected:
+ void write(Level /* level */, std::string_view /* message */) override {}
+};
+
+class StderrLogger : public BaseLogger {
+ public:
+ explicit StderrLogger(bool verbose) : verbose_(verbose) {}
+
+ protected:
+ void write(Level level, std::string_view message) override {
+ switch (level) {
+ case Level::kError:
+ std::cerr << "Error: ";
+ break;
+ case Level::kWarning:
+ std::cerr << "Warning: ";
+ break;
+ case Level::kInfo:
+ if (!verbose_)
+ return;
+ std::cerr << "Info: ";
+ break;
+ case Level::kDebug:
+ if (!verbose_)
+ return;
+ std::cerr << "Debug: ";
+ break;
+ }
+ std::cerr << message << '\n';
+ }
+
+ private:
+ bool const verbose_;
+};
+
+class PrefixLogger : public Logger {
+ public:
+ PrefixLogger(Logger& logger, std::string prefix)
+ : logger_(logger), prefix_(std::move(prefix)) {}
+
+ void err(std::string_view message) override {
+ logger_.err(std::format("{}: {}", prefix_, message));
+ }
+
+ void warn(std::string_view message) override {
+ logger_.warn(std::format("{}: {}", prefix_, message));
+ }
+
+ void info(std::string_view message) override {
+ logger_.info(std::format("{}: {}", prefix_, message));
+ }
+
+#ifndef NDEBUG
+ void dbg(std::string_view message) override {
+ logger_.dbg(std::format("{}: {}", prefix_, message));
+ }
+#endif
+
+ private:
+ Logger& logger_;
+ std::string prefix_;
+};
+
+} // namespace
+
+[[nodiscard]]
+std::unique_ptr<Logger> noop() {
+ return std::make_unique<NoopLogger>();
+}
+
+[[nodiscard]]
+std::unique_ptr<Logger> stderr(bool verbose) {
+ return std::make_unique<StderrLogger>(verbose);
+}
+
+[[nodiscard]]
+std::unique_ptr<Logger> prefix(Logger& logger, std::string prefix) {
+ return std::make_unique<PrefixLogger>(logger, std::move(prefix));
+}
+
+} // namespace logger
diff --git a/src/logger.hh b/src/logger.hh
new file mode 100644
index 0000000..df6879a
--- /dev/null
+++ b/src/logger.hh
@@ -0,0 +1,41 @@
+#ifndef LOGGER_HH
+#define LOGGER_HH
+
+#include <memory>
+#include <string>
+#include <string_view>
+
+namespace logger {
+
+class Logger {
+ public:
+ virtual ~Logger() = default;
+
+ virtual void err(std::string_view message) = 0;
+ virtual void warn(std::string_view message) = 0;
+ virtual void info(std::string_view message) = 0;
+
+#if defined(NDEBUG)
+ void dbg(std::string_view) {}
+#else
+ virtual void dbg(std::string_view message) = 0;
+#endif
+
+ protected:
+ Logger() = default;
+ Logger(Logger const&) = delete;
+ Logger& operator=(Logger const&) = delete;
+};
+
+[[nodiscard]]
+std::unique_ptr<Logger> noop();
+
+[[nodiscard]]
+std::unique_ptr<Logger> stderr(bool verbose = false);
+
+[[nodiscard]]
+std::unique_ptr<Logger> prefix(Logger& logger, std::string prefix);
+
+} // namespace logger
+
+#endif // LOGGER_HH
diff --git a/src/main.cc b/src/main.cc
new file mode 100644
index 0000000..9756c87
--- /dev/null
+++ b/src/main.cc
@@ -0,0 +1,47 @@
+#include "args.hh"
+#include "config.h"
+#include "image_processor.hh"
+#include "spawner.hh"
+
+#include <stdlib.h> // NOLINT(modernize-deprecated-headers)
+#include <iostream>
+#include <string_view>
+#include <vector>
+
+int main(int argc, char** argv) {
+ auto args = Args::create();
+ auto opt_help = args->option('h', "help", "display this text and exit");
+ auto opt_version = args->option('V', "version", "display version and exit");
+ std::vector<std::string_view> arguments;
+ if (!args->run(argc, argv, &arguments)) {
+ args->print_error(std::cerr);
+ std::cerr << "Try `sawmill --help` for usage.\n";
+ return EXIT_FAILURE;
+ }
+ if (opt_help->is_set()) {
+ std::cout << "Usage: `sawmill [OPTION]...`\n"
+ << "This is a window manager for Wayland.\n"
+ << "\n";
+ args->print_help(std::cout);
+ return EXIT_SUCCESS;
+ }
+ if (opt_version->is_set()) {
+ std::cout << "sawmill " << VERSION << " written by Joel Klinghed.\n";
+ return EXIT_SUCCESS;
+ }
+ auto spawner = Spawner::create();
+ if (spawner.has_value()) {
+ auto processor = spawner.value()->run(Spawner::Exec::kImageProcessor);
+ if (processor.has_value()) {
+ auto ret = image_processor::peek(**processor, "/home/the_jk/Downloads/image2vector.svg");
+ if (ret.has_value()) {
+ std::cout << ret->width << "x" << ret->height << '\n';
+ } else {
+ std::cerr << "Bad file\n";
+ }
+ return EXIT_SUCCESS;
+ }
+ }
+ std::cerr << "Unable to launch spawner.\n";
+ return EXIT_FAILURE;
+}
diff --git a/src/paths.cc b/src/paths.cc
new file mode 100644
index 0000000..091be6e
--- /dev/null
+++ b/src/paths.cc
@@ -0,0 +1,100 @@
+#include "paths.hh"
+
+#include "str.hh"
+
+#include <algorithm>
+#include <cerrno>
+#include <cstddef>
+#include <cstdlib>
+#include <iterator>
+#include <memory>
+#include <pwd.h>
+#include <string_view>
+#include <unistd.h>
+#include <unordered_set>
+#include <vector>
+
+namespace paths {
+
+namespace {
+
+std::vector<std::filesystem::path> xdg_read_dirs(
+ const char* userdir_env_name, std::string_view userdir_home_default,
+ const char* dirs_env_name,
+ std::vector<std::filesystem::path> const& dirs_default_value) {
+ std::vector<std::filesystem::path> ret;
+ std::unordered_set<std::filesystem::path> tmp;
+ auto* env_userdir = getenv(userdir_env_name);
+ if (env_userdir != nullptr && env_userdir[0] != '\0') {
+ ret.emplace_back(env_userdir);
+ } else {
+ ret.emplace_back(home() / userdir_home_default);
+ }
+ tmp.insert(ret.back());
+ auto* env_dirs = getenv(dirs_env_name);
+ if (env_dirs != nullptr && env_dirs[0] != '\0') {
+ for (auto dir : str::split(env_dirs, ':')) {
+ if (tmp.emplace(dir).second) {
+ ret.emplace_back(dir);
+ }
+ }
+ } else {
+ std::ranges::copy_if(
+ dirs_default_value, std::back_inserter(ret),
+ [&tmp](auto const& dir) { return tmp.emplace(dir).second; });
+ }
+ return ret;
+}
+
+} // namespace
+
+std::filesystem::path home() {
+ {
+ auto* str = getenv("HOME");
+ if (str != nullptr && str[0] != '\0')
+ return str;
+ }
+
+ {
+ auto maybe_size = sysconf(_SC_GETPW_R_SIZE_MAX);
+ size_t size = maybe_size > 0 ? static_cast<size_t>(maybe_size) : 1024;
+ auto buffer = std::make_unique<char[]>(size);
+ struct passwd pwd;
+ struct passwd* ret;
+ int err;
+ while (true) {
+ err = getpwuid_r(geteuid(), &pwd, buffer.get(), size, &ret);
+ if (err == 0)
+ break;
+ if (err != ERANGE)
+ break;
+ auto new_size = size * 2;
+ if (new_size < size)
+ break;
+ buffer = std::make_unique<char[]>(new_size);
+ size = new_size;
+ }
+ if (err == 0 && ret) {
+ if (ret->pw_dir != nullptr && ret->pw_dir[0] != '\0') {
+ return ret->pw_dir;
+ }
+ }
+ }
+
+ return "/";
+}
+
+std::vector<std::filesystem::path> config_dirs() {
+ static const std::vector<std::filesystem::path> fallback{"/etc/xdg"};
+ return xdg_read_dirs("XDG_CONFIG_HOME", ".config", "XDG_CONFIG_DIRS",
+ fallback);
+}
+
+std::vector<std::filesystem::path> data_dirs() {
+ static const std::vector<std::filesystem::path> fallback{"/usr/local/share/",
+ "/usr/share/"};
+ return xdg_read_dirs("XDG_DATA_HOME", ".local/share", "XDG_DATA_DIRS",
+ fallback);
+}
+
+} // namespace paths
diff --git a/src/paths.hh b/src/paths.hh
new file mode 100644
index 0000000..8bd4c75
--- /dev/null
+++ b/src/paths.hh
@@ -0,0 +1,20 @@
+#ifndef PATHS_HH
+#define PATHS_HH
+
+#include <filesystem> // IWYU pragma: export
+#include <vector>
+
+namespace paths {
+
+// Return home directory, goes HOME, /etc/passwd entry, / in that order.
+std::filesystem::path home();
+
+// Return config directories for reading, in order of priority.
+std::vector<std::filesystem::path> config_dirs();
+
+// Return data directories for reading, in order of priority.
+std::vector<std::filesystem::path> data_dirs();
+
+} // namespace paths
+
+#endif // PATHS_HH
diff --git a/src/size.hh b/src/size.hh
new file mode 100644
index 0000000..8fcc53f
--- /dev/null
+++ b/src/size.hh
@@ -0,0 +1,30 @@
+#ifndef SIZE_HH
+#define SIZE_HH
+
+#include <compare>
+#include <cstdint>
+
+struct Size final {
+ Size() : width(0), height(0) {}
+ Size(uint32_t width, uint32_t height) : width(width), height(height) {}
+
+ bool empty() const { return width == 0 || height == 0; }
+
+ operator bool() const { return !empty(); }
+
+ std::strong_ordering operator<=>(Size const& other) const {
+ if (empty()) {
+ return other.empty() ? std::strong_ordering::equal
+ : std::strong_ordering::less;
+ }
+ if (other.empty())
+ return std::strong_ordering::greater;
+ auto ret = width <=> other.width;
+ return ret == std::strong_ordering::equal ? height <=> other.height : ret;
+ }
+
+ uint32_t width;
+ uint32_t height;
+};
+
+#endif // SIZE_HH
diff --git a/src/spawner.cc b/src/spawner.cc
new file mode 100644
index 0000000..e49ac34
--- /dev/null
+++ b/src/spawner.cc
@@ -0,0 +1,236 @@
+#include "spawner.hh"
+
+#include "image_processor.hh"
+#include "io.hh"
+#include "unique_fd.hh"
+
+#include <cassert>
+#include <csignal>
+#include <expected>
+#include <linux/prctl.h>
+#include <memory>
+#include <sys/prctl.h>
+#include <sys/syscall.h>
+#include <unistd.h>
+#include <utility>
+
+namespace {
+
+int pidfd_getfd(int pidfd, int targetfd, unsigned int flags) {
+ return static_cast<int>(syscall(SYS_pidfd_getfd, pidfd, targetfd, flags));
+}
+
+// NOLINTNEXTLINE(misc-include-cleaner)
+int pidfd_open(pid_t pid, unsigned int flags) {
+ return static_cast<int>(syscall(SYS_pidfd_open, pid, flags));
+}
+
+class ProcessImpl : public Process {
+ public:
+ ProcessImpl(
+ pid_t pid,
+ std::pair<std::unique_ptr<io::Reader>, std::unique_ptr<io::Writer>> pipe)
+ : pid_(pid), pipe_(std::move(pipe)) {
+ assert(pid_);
+ }
+
+ [[nodiscard]]
+ pid_t pid() const { return pid_; }
+
+ [[nodiscard]]
+ io::Reader& reader() const override { return *pipe_.first; }
+
+ [[nodiscard]]
+ io::Writer& writer() const override { return *pipe_.second; }
+
+ void kill() {
+ if (pid_ != 0) {
+ ::kill(pid_, SIGTERM); // NOLINT(misc-include-cleaner)
+ pid_ = 0;
+ }
+ }
+
+ private:
+ pid_t pid_;
+ std::pair<std::unique_ptr<io::Reader>, std::unique_ptr<io::Writer>> pipe_;
+};
+
+struct Request {
+ Spawner::Exec exec;
+ int reader;
+ int writer;
+};
+
+struct Response {
+ pid_t pid;
+};
+
+void spawner_runner(unique_fd parent, std::unique_ptr<io::Reader> reader,
+ std::unique_ptr<io::Writer> writer) {
+ while (true) {
+ Request request;
+ {
+ auto ret = reader->repeat_read(&request, sizeof(request));
+ if (!ret.has_value() || ret.value() != sizeof(request)) {
+ break;
+ }
+ }
+
+ // Need to not return a response before child has duped fds.
+ // So use a short-lived pipe to sync child start.
+ auto sync_pipe = io::pipe();
+ pid_t child_pid = 0;
+
+ if (sync_pipe.has_value()) {
+ child_pid = fork();
+ if (child_pid == 0) {
+ // Child process
+ sync_pipe->first.reset();
+ reader.reset();
+ writer.reset();
+
+ unique_fd child_reader_fd{pidfd_getfd(parent.get(), request.reader, 0)};
+ unique_fd child_writer_fd{pidfd_getfd(parent.get(), request.writer, 0)};
+
+ parent.reset();
+
+ if (child_reader_fd && child_writer_fd) {
+ char c = 1;
+ if (sync_pipe->second->write(&c, sizeof(c)).has_value()) {
+ auto child_reader = io::reader_from_raw(child_reader_fd.release());
+ auto child_writer = io::writer_from_raw(child_writer_fd.release());
+
+ switch (request.exec) {
+ case Spawner::Exec::kImageProcessor:
+ _exit(image_processor::run(std::move(child_reader),
+ std::move(child_writer)));
+ }
+ }
+ }
+
+ _exit(1);
+ // _exit obviously never returns but to help tools and compilers…
+ return;
+ }
+
+ // Parent process
+ if (child_pid == -1) {
+ // fork() failed, we use zero as error value.
+ child_pid = 0;
+ } else {
+ sync_pipe->second.reset();
+
+ char c = 1;
+ std::ignore = sync_pipe->first->read(&c, sizeof(c));
+ }
+ }
+
+ Response response{child_pid};
+ {
+ auto ret = writer->repeat_write(&response, sizeof(response));
+ if (!ret.has_value() || ret.value() != sizeof(response)) {
+ break;
+ }
+ }
+ }
+}
+
+class SpawnerImpl : public Spawner {
+ public:
+ explicit SpawnerImpl(std::unique_ptr<ProcessImpl> process)
+ : process_(std::move(process)) {}
+
+ ~SpawnerImpl() override {
+ if (process_) {
+ process_->kill();
+ }
+ }
+
+ [[nodiscard]]
+ std::expected<std::unique_ptr<Process>, Error> run(Exec exec) override {
+ if (!process_) {
+ return std::unexpected(Error::kError);
+ }
+
+ auto reader_pipe = io::pipe();
+ auto writer_pipe = io::pipe();
+ if (!reader_pipe.has_value() || !writer_pipe.has_value()) {
+ return std::unexpected(Error::kError);
+ }
+
+ Request req{
+ .exec = exec,
+ .reader = writer_pipe->first->raw_fd(),
+ .writer = reader_pipe->second->raw_fd(),
+ };
+ {
+ auto ret = process_->writer().repeat_write(&req, sizeof(req));
+ if (!ret.has_value() || ret.value() != sizeof(req)) {
+ process_->kill();
+ process_.reset();
+ return std::unexpected(Error::kError);
+ }
+ }
+
+ Response resp;
+ {
+ auto ret = process_->reader().repeat_read(&resp, sizeof(resp));
+ if (!ret.has_value() || ret.value() != sizeof(resp)) {
+ process_->kill();
+ process_.reset();
+ return std::unexpected(Error::kError);
+ }
+ }
+
+ if (resp.pid == 0)
+ return std::unexpected(Error::kError);
+
+ return std::make_unique<ProcessImpl>(
+ resp.pid, std::make_pair(std::move(reader_pipe->first),
+ std::move(writer_pipe->second)));
+ }
+
+ private:
+ std::unique_ptr<ProcessImpl> process_;
+};
+
+} // namespace
+
+std::expected<std::unique_ptr<Spawner>, Spawner::Error> Spawner::create() {
+ auto reader_pipe = io::pipe();
+ auto writer_pipe = io::pipe();
+ if (!reader_pipe.has_value() || !writer_pipe.has_value()) {
+ return std::unexpected(Error::kError);
+ }
+
+ auto pid = getpid();
+ unique_fd pidfd{pidfd_open(pid, 0)};
+ if (!pidfd) {
+ return std::unexpected(Error::kError);
+ }
+
+ // Needed for pidfd_getfd to work in child.
+ if (prctl(PR_SET_PTRACER, pid) != 0) {
+ return std::unexpected(Error::kError);
+ }
+
+ pid = fork();
+ if (pid == 0) {
+ // Child process
+ reader_pipe->first.reset();
+ writer_pipe->second.reset();
+
+ spawner_runner(std::move(pidfd), std::move(writer_pipe->first),
+ std::move(reader_pipe->second));
+ _exit(0);
+ }
+
+ // Parent process
+ if (pid == -1) {
+ return std::unexpected(Error::kError);
+ }
+
+ return std::make_unique<SpawnerImpl>(std::make_unique<ProcessImpl>(
+ pid, std::make_pair(std::move(reader_pipe->first),
+ std::move(writer_pipe->second))));
+}
diff --git a/src/spawner.hh b/src/spawner.hh
new file mode 100644
index 0000000..c4f511b
--- /dev/null
+++ b/src/spawner.hh
@@ -0,0 +1,49 @@
+#ifndef SPAWNER_HH
+#define SPAWNER_HH
+
+#include <cstdint>
+#include <expected>
+#include <memory>
+
+namespace io {
+class Reader;
+class Writer;
+}; // namespace io
+
+class Process {
+ public:
+ virtual ~Process() = default;
+
+ virtual io::Reader& reader() const = 0;
+ virtual io::Writer& writer() const = 0;
+
+ protected:
+ Process() = default;
+ Process(Process const&) = delete;
+ Process& operator=(Process const&) = delete;
+};
+
+class Spawner {
+ public:
+ enum class Exec : uint8_t {
+ kImageProcessor,
+ };
+
+ enum class Error : uint8_t {
+ kError,
+ };
+
+ virtual ~Spawner() = default;
+
+ [[nodiscard]]
+ virtual std::expected<std::unique_ptr<Process>, Error> run(Exec exec) = 0;
+
+ static std::expected<std::unique_ptr<Spawner>, Error> create();
+
+ protected:
+ Spawner() = default;
+ Spawner(Spawner const&) = delete;
+ Spawner& operator=(Spawner const&) = delete;
+};
+
+#endif // SPAWNER_HH
diff --git a/src/str.cc b/src/str.cc
new file mode 100644
index 0000000..129e0f3
--- /dev/null
+++ b/src/str.cc
@@ -0,0 +1,94 @@
+#include "str.hh"
+
+#include <cstddef>
+#include <string_view>
+#include <vector>
+
+namespace str {
+
+namespace {
+
+[[nodiscard]]
+inline bool is_space(char c) {
+ return c == ' ' || c == '\t' || c == '\r' || c == '\n';
+}
+
+} // namespace
+
+void split(std::string_view str, std::vector<std::string_view>& out,
+ char separator, bool keep_empty) {
+ out.clear();
+
+ size_t offset = 0;
+ while (true) {
+ auto next = str.find(separator, offset);
+ if (next == std::string_view::npos) {
+ if (keep_empty || offset < str.size())
+ out.push_back(str.substr(offset));
+ break;
+ }
+ if (keep_empty || offset < next)
+ out.push_back(str.substr(offset, next - offset));
+ offset = next + 1;
+ }
+}
+
+std::vector<std::string_view> split(std::string_view str, char separator,
+ bool keep_empty) {
+ std::vector<std::string_view> vec;
+ split(str, vec, separator, keep_empty);
+ return vec;
+}
+
+void split(std::string_view str, std::vector<std::string_view>& out,
+ std::string_view separator, bool keep_empty) {
+ out.clear();
+
+ size_t offset = 0;
+ while (true) {
+ auto next = str.find(separator, offset);
+ if (next == std::string_view::npos) {
+ if (keep_empty || offset < str.size())
+ out.push_back(str.substr(offset));
+ break;
+ }
+ if (keep_empty || offset < next)
+ out.push_back(str.substr(offset, next - offset));
+ offset = next + separator.size();
+ }
+}
+
+std::vector<std::string_view> split(std::string_view str,
+ std::string_view separator,
+ bool keep_empty) {
+ std::vector<std::string_view> vec;
+ split(str, vec, separator, keep_empty);
+ return vec;
+}
+
+std::string_view trim(std::string_view str) {
+ size_t s = 0;
+ size_t e = str.size();
+ while (s < e && is_space(str[s]))
+ ++s;
+ while (e > s && is_space(str[e - 1]))
+ --e;
+ return str.substr(s, e - s);
+}
+
+std::string_view ltrim(std::string_view str) {
+ size_t s = 0;
+ size_t const e = str.size();
+ while (s < e && is_space(str[s]))
+ ++s;
+ return str.substr(s, e - s);
+}
+
+std::string_view rtrim(std::string_view str) {
+ size_t e = str.size();
+ while (e > 0 && is_space(str[e - 1]))
+ --e;
+ return str.substr(0, e);
+}
+
+} // namespace str
diff --git a/src/str.hh b/src/str.hh
new file mode 100644
index 0000000..c736f4b
--- /dev/null
+++ b/src/str.hh
@@ -0,0 +1,34 @@
+#ifndef STR_HH
+#define STR_HH
+
+#include <string_view>
+#include <vector>
+
+namespace str {
+
+void split(std::string_view str, std::vector<std::string_view>& out,
+ char separator = ' ', bool keep_empty = false);
+
+[[nodiscard]] std::vector<std::string_view> split(std::string_view str,
+ char separator = ' ',
+ bool keep_empty = false);
+
+void split(std::string_view str, std::vector<std::string_view>& out,
+ std::string_view separator, bool keep_empty = false);
+
+[[nodiscard]] std::vector<std::string_view> split(std::string_view str,
+ std::string_view separator,
+ bool keep_empty = false);
+
+[[nodiscard]]
+std::string_view trim(std::string_view str);
+
+[[nodiscard]]
+std::string_view ltrim(std::string_view str);
+
+[[nodiscard]]
+std::string_view rtrim(std::string_view str);
+
+} // namespace str
+
+#endif // STR_HH
diff --git a/src/u.hh b/src/u.hh
new file mode 100644
index 0000000..439d6dc
--- /dev/null
+++ b/src/u.hh
@@ -0,0 +1,21 @@
+#ifndef U_HH
+#define U_HH
+
+#include <cstdint>
+
+namespace u {
+
+enum class ReadError : uint8_t {
+ Invalid, // Invalid sequence
+ End, // At end (it == end)
+ Incomplete, // Too few bytes
+};
+
+enum class ReadErrorReplace : uint8_t {
+ End, // At end (it == end)
+ Incomplete, // Too few bytes
+};
+
+} // namespace u
+
+#endif // U_HH
diff --git a/src/u8.hh b/src/u8.hh
new file mode 100644
index 0000000..d673caa
--- /dev/null
+++ b/src/u8.hh
@@ -0,0 +1,196 @@
+#ifndef U8_HH
+#define U8_HH
+
+#include "u.hh" // IWYU pragma: export
+
+#include <cstdint> // IWYU pragma: export
+#include <expected>
+#include <iterator>
+#include <type_traits>
+#include <utility>
+
+namespace u8 {
+
+template <std::forward_iterator T>
+ requires std::is_same_v<std::iter_value_t<T>, uint8_t>
+std::expected<uint32_t, u::ReadError> read(T& start, T const& end) {
+ if (start == end)
+ return std::unexpected(u::ReadError::End);
+ uint32_t u;
+ switch (*start >> 4) {
+ case 0xf:
+ // 11110uvv 10vvwwww 10xxxxyy 10yyzzzz
+ if (std::distance(start, end) < 4) {
+ return std::unexpected(u::ReadError::Incomplete);
+ }
+ u = (*start & 0x07) << 18;
+ std::advance(start, 1);
+ if ((*start & 0xc0) != 0x80) {
+ std::advance(start, 3);
+ return std::unexpected(u::ReadError::Invalid);
+ }
+ u |= (*start & 0x3f) << 12;
+ std::advance(start, 1);
+ if ((*start & 0xc0) != 0x80) {
+ std::advance(start, 2);
+ return std::unexpected(u::ReadError::Invalid);
+ }
+ u |= (*start & 0x3f) << 6;
+ std::advance(start, 1);
+ if ((*start & 0xc0) != 0x80) {
+ std::advance(start, 1);
+ return std::unexpected(u::ReadError::Invalid);
+ }
+ u |= *start & 0x3f;
+ if (u < 0x10000 || u > 0x10ffff) {
+ std::advance(start, 1);
+ return std::unexpected(u::ReadError::Invalid);
+ }
+ break;
+ case 0xe:
+ // 1110wwww 10xxxxyy 10yyzzzz
+ if (std::distance(start, end) < 3) {
+ return std::unexpected(u::ReadError::Incomplete);
+ }
+ u = (*start & 0x0f) << 12;
+ std::advance(start, 1);
+ if ((*start & 0xc0) != 0x80) {
+ std::advance(start, 2);
+ return std::unexpected(u::ReadError::Invalid);
+ }
+ u |= (*start & 0x3f) << 6;
+ std::advance(start, 1);
+ if ((*start & 0xc0) != 0x80) {
+ std::advance(start, 1);
+ return std::unexpected(u::ReadError::Invalid);
+ }
+ u |= *start & 0x3f;
+ if (u < 0x800 || (u >= 0xd800 && u <= 0xdfff)) {
+ std::advance(start, 1);
+ return std::unexpected(u::ReadError::Invalid);
+ }
+ break;
+ case 0xd:
+ case 0xc:
+ // 110xxxyy 10yyzzzz
+ if (std::distance(start, end) < 2) {
+ return std::unexpected(u::ReadError::Incomplete);
+ }
+ u = (*start & 0x1f) << 6;
+ std::advance(start, 1);
+ if ((*start & 0xc0) != 0x80) {
+ std::advance(start, 1);
+ return std::unexpected(u::ReadError::Invalid);
+ }
+ u |= *start & 0x3f;
+ if (u < 0x80) {
+ std::advance(start, 1);
+ return std::unexpected(u::ReadError::Invalid);
+ }
+ break;
+ case 0xb:
+ case 0xa:
+ case 0x9:
+ case 0x8:
+ std::advance(start, 1);
+ return std::unexpected(u::ReadError::Invalid);
+ default:
+ // 0yyyzzzz
+ u = *start;
+ break;
+ }
+ std::advance(start, 1);
+ return u;
+}
+
+template <std::forward_iterator T>
+ requires std::is_same_v<std::iter_value_t<T>, uint8_t>
+std::expected<uint32_t, u::ReadErrorReplace> read_replace(T& start,
+ T const& end,
+ bool eof) {
+ auto const tmp = start;
+ auto ret = read(start, end);
+ if (ret.has_value())
+ return *ret;
+ switch (ret.error()) {
+ case u::ReadError::Incomplete:
+ if (eof)
+ break;
+ return std::unexpected(u::ReadErrorReplace::Incomplete);
+ case u::ReadError::End:
+ return std::unexpected(u::ReadErrorReplace::End);
+ case u::ReadError::Invalid:
+ break;
+ }
+ start = tmp + 1;
+ return 0xfffd;
+}
+
+template <std::forward_iterator T>
+ requires std::is_same_v<std::iter_value_t<T>, uint8_t>
+bool write(T& start, T const& end, uint32_t code) {
+ if (code < 0x80) {
+ if (start == end)
+ return false;
+ *start = static_cast<uint8_t>(code);
+ } else if (code < 0x800) {
+ if (std::distance(start, end) < 2)
+ return false;
+ *start = 0xc0 | static_cast<uint8_t>(code >> 6);
+ std::advance(start, 1);
+ *start = 0x80 | static_cast<uint8_t>(code & 0x3f);
+ } else if (code < 0x10000) {
+ if (std::distance(start, end) < 3)
+ return false;
+ *start = 0xe0 | static_cast<uint8_t>(code >> 12);
+ std::advance(start, 1);
+ *start = 0x80 | static_cast<uint8_t>((code >> 6) & 0x3f);
+ std::advance(start, 1);
+ *start = 0x80 | static_cast<uint8_t>(code & 0x3f);
+ } else {
+ if (std::distance(start, end) < 4)
+ return false;
+ *start = 0xf0 | static_cast<uint8_t>(code >> 18);
+ std::advance(start, 1);
+ *start = 0x80 | static_cast<uint8_t>((code >> 12) & 0x3f);
+ std::advance(start, 1);
+ *start = 0x80 | static_cast<uint8_t>((code >> 6) & 0x3f);
+ std::advance(start, 1);
+ *start = 0x80 | static_cast<uint8_t>(code & 0x3f);
+ }
+ std::advance(start, 1);
+ return true;
+}
+
+template <std::forward_iterator T>
+ requires std::is_same_v<std::iter_value_t<T>, uint8_t>
+bool skip(T& start, T const& end) {
+ if (start == end)
+ return false;
+ switch (*start >> 4) {
+ case 0xf:
+ if (std::distance(start, end) < 4)
+ return false;
+ std::advance(start, 4);
+ break;
+ case 0xe:
+ if (std::distance(start, end) < 3)
+ return false;
+ std::advance(start, 3);
+ break;
+ case 0xc:
+ case 0xd:
+ if (std::distance(start, end) < 2)
+ return false;
+ std::advance(start, 2);
+ break;
+ default:
+ std::advance(start, 1);
+ break;
+ }
+ return true;
+}
+
+} // namespace u8
+
+#endif // U8_HH
diff --git a/src/unique_fd.cc b/src/unique_fd.cc
new file mode 100644
index 0000000..135a449
--- /dev/null
+++ b/src/unique_fd.cc
@@ -0,0 +1,9 @@
+#include "unique_fd.hh"
+
+#include <unistd.h>
+
+void unique_fd::reset(int fd) {
+ if (fd_ != -1)
+ close(fd_);
+ fd_ = fd;
+}
diff --git a/src/unique_fd.hh b/src/unique_fd.hh
new file mode 100644
index 0000000..2950905
--- /dev/null
+++ b/src/unique_fd.hh
@@ -0,0 +1,36 @@
+#ifndef UNIQUE_FD_HH
+#define UNIQUE_FD_HH
+
+class unique_fd {
+ public:
+ constexpr unique_fd() : fd_(-1) {}
+ explicit constexpr unique_fd(int fd) : fd_(fd) {}
+ unique_fd(unique_fd& fd) = delete;
+ unique_fd& operator=(unique_fd& fd) = delete;
+ unique_fd(unique_fd&& fd) : fd_(fd.release()) {}
+ unique_fd& operator=(unique_fd&& fd) {
+ reset(fd.release());
+ return *this;
+ }
+ ~unique_fd() { reset(); }
+
+ bool operator==(unique_fd const& fd) const { return get() == fd.get(); }
+ bool operator!=(unique_fd const& fd) const { return get() != fd.get(); }
+
+ int get() const { return fd_; }
+ explicit operator bool() const { return fd_ != -1; }
+ int operator*() const { return fd_; }
+
+ int release() {
+ int ret = fd_;
+ fd_ = -1;
+ return ret;
+ }
+
+ void reset(int fd = -1);
+
+ private:
+ int fd_;
+};
+
+#endif // UNIQUE_FD_HH
diff --git a/src/xpm/.clang-tidy b/src/xpm/.clang-tidy
new file mode 100644
index 0000000..8f60b95
--- /dev/null
+++ b/src/xpm/.clang-tidy
@@ -0,0 +1,2 @@
+---
+Checks: ''
diff --git a/src/xpm/color.c b/src/xpm/color.c
new file mode 100644
index 0000000..ead7073
--- /dev/null
+++ b/src/xpm/color.c
@@ -0,0 +1,882 @@
+/***********************************************************
+
+Copyright 1987, 1998 The Open Group
+
+Permission to use, copy, modify, distribute, and sell this software and its
+documentation for any purpose is hereby granted without fee, provided that
+the above copyright notice appear in all copies and that both that
+copyright notice and this permission notice appear in supporting
+documentation.
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+OPEN GROUP BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+Except as contained in this notice, the name of The Open Group shall not be
+used in advertising or otherwise to promote the sale, use or other dealings
+in this Software without prior written authorization from The Open Group.
+
+Copyright 1987 by Digital Equipment Corporation, Maynard, Massachusetts.
+
+ All Rights Reserved
+
+Permission to use, copy, modify, and distribute this software and its
+documentation for any purpose and without fee is hereby granted,
+provided that the above copyright notice appear in all copies and that
+both that copyright notice and this permission notice appear in
+supporting documentation, and that the name of Digital not be
+used in advertising or publicity pertaining to distribution of the
+software without specific, written prior permission.
+
+DIGITAL DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING
+ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL
+DIGITAL BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR
+ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
+WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
+ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
+SOFTWARE.
+
+******************************************************************/
+
+#include <dix-config.h>
+
+#include <X11/keysym.h>
+
+#include "dix/dix_priv.h"
+#include "include/dix.h"
+
+typedef struct _builtinColor {
+ unsigned char red;
+ unsigned char green;
+ unsigned char blue;
+ const char *name;
+} BuiltinColor;
+
+static const BuiltinColor BuiltinColors[] = {
+/* R G B name */
+ { 240, 248, 255, "alice blue" },
+ { 240, 248, 255, "AliceBlue" },
+ { 250, 235, 215, "antique white" },
+ { 250, 235, 215, "AntiqueWhite" },
+ { 255, 239, 219, "AntiqueWhite1" },
+ { 238, 223, 204, "AntiqueWhite2" },
+ { 205, 192, 176, "AntiqueWhite3" },
+ { 139, 131, 120, "AntiqueWhite4" },
+ { 0, 255, 255, "aqua" },
+ { 127, 255, 212, "aquamarine" },
+ { 127, 255, 212, "aquamarine1" },
+ { 118, 238, 198, "aquamarine2" },
+ { 102, 205, 170, "aquamarine3" },
+ { 69, 139, 116, "aquamarine4" },
+ { 240, 255, 255, "azure" },
+ { 240, 255, 255, "azure1" },
+ { 224, 238, 238, "azure2" },
+ { 193, 205, 205, "azure3" },
+ { 131, 139, 139, "azure4" },
+ { 245, 245, 220, "beige" },
+ { 255, 228, 196, "bisque" },
+ { 255, 228, 196, "bisque1" },
+ { 238, 213, 183, "bisque2" },
+ { 205, 183, 158, "bisque3" },
+ { 139, 125, 107, "bisque4" },
+ { 0, 0, 0, "black" },
+ { 255, 235, 205, "blanched almond" },
+ { 255, 235, 205, "BlanchedAlmond" },
+ { 0, 0, 255, "blue" },
+ { 138, 43, 226, "blue violet" },
+ { 0, 0, 255, "blue1" },
+ { 0, 0, 238, "blue2" },
+ { 0, 0, 205, "blue3" },
+ { 0, 0, 139, "blue4" },
+ { 138, 43, 226, "BlueViolet" },
+ { 165, 42, 42, "brown" },
+ { 255, 64, 64, "brown1" },
+ { 238, 59, 59, "brown2" },
+ { 205, 51, 51, "brown3" },
+ { 139, 35, 35, "brown4" },
+ { 222, 184, 135, "burlywood" },
+ { 255, 211, 155, "burlywood1" },
+ { 238, 197, 145, "burlywood2" },
+ { 205, 170, 125, "burlywood3" },
+ { 139, 115, 85, "burlywood4" },
+ { 95, 158, 160, "cadet blue" },
+ { 95, 158, 160, "CadetBlue" },
+ { 152, 245, 255, "CadetBlue1" },
+ { 142, 229, 238, "CadetBlue2" },
+ { 122, 197, 205, "CadetBlue3" },
+ { 83, 134, 139, "CadetBlue4" },
+ { 127, 255, 0, "chartreuse" },
+ { 127, 255, 0, "chartreuse1" },
+ { 118, 238, 0, "chartreuse2" },
+ { 102, 205, 0, "chartreuse3" },
+ { 69, 139, 0, "chartreuse4" },
+ { 210, 105, 30, "chocolate" },
+ { 255, 127, 36, "chocolate1" },
+ { 238, 118, 33, "chocolate2" },
+ { 205, 102, 29, "chocolate3" },
+ { 139, 69, 19, "chocolate4" },
+ { 255, 127, 80, "coral" },
+ { 255, 114, 86, "coral1" },
+ { 238, 106, 80, "coral2" },
+ { 205, 91, 69, "coral3" },
+ { 139, 62, 47, "coral4" },
+ { 100, 149, 237, "cornflower blue" },
+ { 100, 149, 237, "CornflowerBlue" },
+ { 255, 248, 220, "cornsilk" },
+ { 255, 248, 220, "cornsilk1" },
+ { 238, 232, 205, "cornsilk2" },
+ { 205, 200, 177, "cornsilk3" },
+ { 139, 136, 120, "cornsilk4" },
+ { 220, 20, 60, "crimson" },
+ { 0, 255, 255, "cyan" },
+ { 0, 255, 255, "cyan1" },
+ { 0, 238, 238, "cyan2" },
+ { 0, 205, 205, "cyan3" },
+ { 0, 139, 139, "cyan4" },
+ { 0, 0, 139, "dark blue" },
+ { 0, 139, 139, "dark cyan" },
+ { 184, 134, 11, "dark goldenrod" },
+ { 169, 169, 169, "dark gray" },
+ { 0, 100, 0, "dark green" },
+ { 169, 169, 169, "dark grey" },
+ { 189, 183, 107, "dark khaki" },
+ { 139, 0, 139, "dark magenta" },
+ { 85, 107, 47, "dark olive green" },
+ { 255, 140, 0, "dark orange" },
+ { 153, 50, 204, "dark orchid" },
+ { 139, 0, 0, "dark red" },
+ { 233, 150, 122, "dark salmon" },
+ { 143, 188, 143, "dark sea green" },
+ { 72, 61, 139, "dark slate blue" },
+ { 47, 79, 79, "dark slate gray" },
+ { 47, 79, 79, "dark slate grey" },
+ { 0, 206, 209, "dark turquoise" },
+ { 148, 0, 211, "dark violet" },
+ { 0, 0, 139, "DarkBlue" },
+ { 0, 139, 139, "DarkCyan" },
+ { 184, 134, 11, "DarkGoldenrod" },
+ { 255, 185, 15, "DarkGoldenrod1" },
+ { 238, 173, 14, "DarkGoldenrod2" },
+ { 205, 149, 12, "DarkGoldenrod3" },
+ { 139, 101, 8, "DarkGoldenrod4" },
+ { 169, 169, 169, "DarkGray" },
+ { 0, 100, 0, "DarkGreen" },
+ { 169, 169, 169, "DarkGrey" },
+ { 189, 183, 107, "DarkKhaki" },
+ { 139, 0, 139, "DarkMagenta" },
+ { 85, 107, 47, "DarkOliveGreen" },
+ { 202, 255, 112, "DarkOliveGreen1" },
+ { 188, 238, 104, "DarkOliveGreen2" },
+ { 162, 205, 90, "DarkOliveGreen3" },
+ { 110, 139, 61, "DarkOliveGreen4" },
+ { 255, 140, 0, "DarkOrange" },
+ { 255, 127, 0, "DarkOrange1" },
+ { 238, 118, 0, "DarkOrange2" },
+ { 205, 102, 0, "DarkOrange3" },
+ { 139, 69, 0, "DarkOrange4" },
+ { 153, 50, 204, "DarkOrchid" },
+ { 191, 62, 255, "DarkOrchid1" },
+ { 178, 58, 238, "DarkOrchid2" },
+ { 154, 50, 205, "DarkOrchid3" },
+ { 104, 34, 139, "DarkOrchid4" },
+ { 139, 0, 0, "DarkRed" },
+ { 233, 150, 122, "DarkSalmon" },
+ { 143, 188, 143, "DarkSeaGreen" },
+ { 193, 255, 193, "DarkSeaGreen1" },
+ { 180, 238, 180, "DarkSeaGreen2" },
+ { 155, 205, 155, "DarkSeaGreen3" },
+ { 105, 139, 105, "DarkSeaGreen4" },
+ { 72, 61, 139, "DarkSlateBlue" },
+ { 47, 79, 79, "DarkSlateGray" },
+ { 151, 255, 255, "DarkSlateGray1" },
+ { 141, 238, 238, "DarkSlateGray2" },
+ { 121, 205, 205, "DarkSlateGray3" },
+ { 82, 139, 139, "DarkSlateGray4" },
+ { 47, 79, 79, "DarkSlateGrey" },
+ { 0, 206, 209, "DarkTurquoise" },
+ { 148, 0, 211, "DarkViolet" },
+ { 255, 20, 147, "deep pink" },
+ { 0, 191, 255, "deep sky blue" },
+ { 255, 20, 147, "DeepPink" },
+ { 255, 20, 147, "DeepPink1" },
+ { 238, 18, 137, "DeepPink2" },
+ { 205, 16, 118, "DeepPink3" },
+ { 139, 10, 80, "DeepPink4" },
+ { 0, 191, 255, "DeepSkyBlue" },
+ { 0, 191, 255, "DeepSkyBlue1" },
+ { 0, 178, 238, "DeepSkyBlue2" },
+ { 0, 154, 205, "DeepSkyBlue3" },
+ { 0, 104, 139, "DeepSkyBlue4" },
+ { 105, 105, 105, "dim gray" },
+ { 105, 105, 105, "dim grey" },
+ { 105, 105, 105, "DimGray" },
+ { 105, 105, 105, "DimGrey" },
+ { 30, 144, 255, "dodger blue" },
+ { 30, 144, 255, "DodgerBlue" },
+ { 30, 144, 255, "DodgerBlue1" },
+ { 28, 134, 238, "DodgerBlue2" },
+ { 24, 116, 205, "DodgerBlue3" },
+ { 16, 78, 139, "DodgerBlue4" },
+ { 178, 34, 34, "firebrick" },
+ { 255, 48, 48, "firebrick1" },
+ { 238, 44, 44, "firebrick2" },
+ { 205, 38, 38, "firebrick3" },
+ { 139, 26, 26, "firebrick4" },
+ { 255, 250, 240, "floral white" },
+ { 255, 250, 240, "FloralWhite" },
+ { 34, 139, 34, "forest green" },
+ { 34, 139, 34, "ForestGreen" },
+ { 255, 0, 255, "fuchsia" },
+ { 220, 220, 220, "gainsboro" },
+ { 248, 248, 255, "ghost white" },
+ { 248, 248, 255, "GhostWhite" },
+ { 255, 215, 0, "gold" },
+ { 255, 215, 0, "gold1" },
+ { 238, 201, 0, "gold2" },
+ { 205, 173, 0, "gold3" },
+ { 139, 117, 0, "gold4" },
+ { 218, 165, 32, "goldenrod" },
+ { 255, 193, 37, "goldenrod1" },
+ { 238, 180, 34, "goldenrod2" },
+ { 205, 155, 29, "goldenrod3" },
+ { 139, 105, 20, "goldenrod4" },
+ { 190, 190, 190, "gray" },
+ { 0, 0, 0, "gray0" },
+ { 3, 3, 3, "gray1" },
+ { 26, 26, 26, "gray10" },
+ { 255, 255, 255, "gray100" },
+ { 28, 28, 28, "gray11" },
+ { 31, 31, 31, "gray12" },
+ { 33, 33, 33, "gray13" },
+ { 36, 36, 36, "gray14" },
+ { 38, 38, 38, "gray15" },
+ { 41, 41, 41, "gray16" },
+ { 43, 43, 43, "gray17" },
+ { 46, 46, 46, "gray18" },
+ { 48, 48, 48, "gray19" },
+ { 5, 5, 5, "gray2" },
+ { 51, 51, 51, "gray20" },
+ { 54, 54, 54, "gray21" },
+ { 56, 56, 56, "gray22" },
+ { 59, 59, 59, "gray23" },
+ { 61, 61, 61, "gray24" },
+ { 64, 64, 64, "gray25" },
+ { 66, 66, 66, "gray26" },
+ { 69, 69, 69, "gray27" },
+ { 71, 71, 71, "gray28" },
+ { 74, 74, 74, "gray29" },
+ { 8, 8, 8, "gray3" },
+ { 77, 77, 77, "gray30" },
+ { 79, 79, 79, "gray31" },
+ { 82, 82, 82, "gray32" },
+ { 84, 84, 84, "gray33" },
+ { 87, 87, 87, "gray34" },
+ { 89, 89, 89, "gray35" },
+ { 92, 92, 92, "gray36" },
+ { 94, 94, 94, "gray37" },
+ { 97, 97, 97, "gray38" },
+ { 99, 99, 99, "gray39" },
+ { 10, 10, 10, "gray4" },
+ { 102, 102, 102, "gray40" },
+ { 105, 105, 105, "gray41" },
+ { 107, 107, 107, "gray42" },
+ { 110, 110, 110, "gray43" },
+ { 112, 112, 112, "gray44" },
+ { 115, 115, 115, "gray45" },
+ { 117, 117, 117, "gray46" },
+ { 120, 120, 120, "gray47" },
+ { 122, 122, 122, "gray48" },
+ { 125, 125, 125, "gray49" },
+ { 13, 13, 13, "gray5" },
+ { 127, 127, 127, "gray50" },
+ { 130, 130, 130, "gray51" },
+ { 133, 133, 133, "gray52" },
+ { 135, 135, 135, "gray53" },
+ { 138, 138, 138, "gray54" },
+ { 140, 140, 140, "gray55" },
+ { 143, 143, 143, "gray56" },
+ { 145, 145, 145, "gray57" },
+ { 148, 148, 148, "gray58" },
+ { 150, 150, 150, "gray59" },
+ { 15, 15, 15, "gray6" },
+ { 153, 153, 153, "gray60" },
+ { 156, 156, 156, "gray61" },
+ { 158, 158, 158, "gray62" },
+ { 161, 161, 161, "gray63" },
+ { 163, 163, 163, "gray64" },
+ { 166, 166, 166, "gray65" },
+ { 168, 168, 168, "gray66" },
+ { 171, 171, 171, "gray67" },
+ { 173, 173, 173, "gray68" },
+ { 176, 176, 176, "gray69" },
+ { 18, 18, 18, "gray7" },
+ { 179, 179, 179, "gray70" },
+ { 181, 181, 181, "gray71" },
+ { 184, 184, 184, "gray72" },
+ { 186, 186, 186, "gray73" },
+ { 189, 189, 189, "gray74" },
+ { 191, 191, 191, "gray75" },
+ { 194, 194, 194, "gray76" },
+ { 196, 196, 196, "gray77" },
+ { 199, 199, 199, "gray78" },
+ { 201, 201, 201, "gray79" },
+ { 20, 20, 20, "gray8" },
+ { 204, 204, 204, "gray80" },
+ { 207, 207, 207, "gray81" },
+ { 209, 209, 209, "gray82" },
+ { 212, 212, 212, "gray83" },
+ { 214, 214, 214, "gray84" },
+ { 217, 217, 217, "gray85" },
+ { 219, 219, 219, "gray86" },
+ { 222, 222, 222, "gray87" },
+ { 224, 224, 224, "gray88" },
+ { 227, 227, 227, "gray89" },
+ { 23, 23, 23, "gray9" },
+ { 229, 229, 229, "gray90" },
+ { 232, 232, 232, "gray91" },
+ { 235, 235, 235, "gray92" },
+ { 237, 237, 237, "gray93" },
+ { 240, 240, 240, "gray94" },
+ { 242, 242, 242, "gray95" },
+ { 245, 245, 245, "gray96" },
+ { 247, 247, 247, "gray97" },
+ { 250, 250, 250, "gray98" },
+ { 252, 252, 252, "gray99" },
+ { 0, 255, 0, "green" },
+ { 173, 255, 47, "green yellow" },
+ { 0, 255, 0, "green1" },
+ { 0, 238, 0, "green2" },
+ { 0, 205, 0, "green3" },
+ { 0, 139, 0, "green4" },
+ { 173, 255, 47, "GreenYellow" },
+ { 190, 190, 190, "grey" },
+ { 0, 0, 0, "grey0" },
+ { 3, 3, 3, "grey1" },
+ { 26, 26, 26, "grey10" },
+ { 255, 255, 255, "grey100" },
+ { 28, 28, 28, "grey11" },
+ { 31, 31, 31, "grey12" },
+ { 33, 33, 33, "grey13" },
+ { 36, 36, 36, "grey14" },
+ { 38, 38, 38, "grey15" },
+ { 41, 41, 41, "grey16" },
+ { 43, 43, 43, "grey17" },
+ { 46, 46, 46, "grey18" },
+ { 48, 48, 48, "grey19" },
+ { 5, 5, 5, "grey2" },
+ { 51, 51, 51, "grey20" },
+ { 54, 54, 54, "grey21" },
+ { 56, 56, 56, "grey22" },
+ { 59, 59, 59, "grey23" },
+ { 61, 61, 61, "grey24" },
+ { 64, 64, 64, "grey25" },
+ { 66, 66, 66, "grey26" },
+ { 69, 69, 69, "grey27" },
+ { 71, 71, 71, "grey28" },
+ { 74, 74, 74, "grey29" },
+ { 8, 8, 8, "grey3" },
+ { 77, 77, 77, "grey30" },
+ { 79, 79, 79, "grey31" },
+ { 82, 82, 82, "grey32" },
+ { 84, 84, 84, "grey33" },
+ { 87, 87, 87, "grey34" },
+ { 89, 89, 89, "grey35" },
+ { 92, 92, 92, "grey36" },
+ { 94, 94, 94, "grey37" },
+ { 97, 97, 97, "grey38" },
+ { 99, 99, 99, "grey39" },
+ { 10, 10, 10, "grey4" },
+ { 102, 102, 102, "grey40" },
+ { 105, 105, 105, "grey41" },
+ { 107, 107, 107, "grey42" },
+ { 110, 110, 110, "grey43" },
+ { 112, 112, 112, "grey44" },
+ { 115, 115, 115, "grey45" },
+ { 117, 117, 117, "grey46" },
+ { 120, 120, 120, "grey47" },
+ { 122, 122, 122, "grey48" },
+ { 125, 125, 125, "grey49" },
+ { 13, 13, 13, "grey5" },
+ { 127, 127, 127, "grey50" },
+ { 130, 130, 130, "grey51" },
+ { 133, 133, 133, "grey52" },
+ { 135, 135, 135, "grey53" },
+ { 138, 138, 138, "grey54" },
+ { 140, 140, 140, "grey55" },
+ { 143, 143, 143, "grey56" },
+ { 145, 145, 145, "grey57" },
+ { 148, 148, 148, "grey58" },
+ { 150, 150, 150, "grey59" },
+ { 15, 15, 15, "grey6" },
+ { 153, 153, 153, "grey60" },
+ { 156, 156, 156, "grey61" },
+ { 158, 158, 158, "grey62" },
+ { 161, 161, 161, "grey63" },
+ { 163, 163, 163, "grey64" },
+ { 166, 166, 166, "grey65" },
+ { 168, 168, 168, "grey66" },
+ { 171, 171, 171, "grey67" },
+ { 173, 173, 173, "grey68" },
+ { 176, 176, 176, "grey69" },
+ { 18, 18, 18, "grey7" },
+ { 179, 179, 179, "grey70" },
+ { 181, 181, 181, "grey71" },
+ { 184, 184, 184, "grey72" },
+ { 186, 186, 186, "grey73" },
+ { 189, 189, 189, "grey74" },
+ { 191, 191, 191, "grey75" },
+ { 194, 194, 194, "grey76" },
+ { 196, 196, 196, "grey77" },
+ { 199, 199, 199, "grey78" },
+ { 201, 201, 201, "grey79" },
+ { 20, 20, 20, "grey8" },
+ { 204, 204, 204, "grey80" },
+ { 207, 207, 207, "grey81" },
+ { 209, 209, 209, "grey82" },
+ { 212, 212, 212, "grey83" },
+ { 214, 214, 214, "grey84" },
+ { 217, 217, 217, "grey85" },
+ { 219, 219, 219, "grey86" },
+ { 222, 222, 222, "grey87" },
+ { 224, 224, 224, "grey88" },
+ { 227, 227, 227, "grey89" },
+ { 23, 23, 23, "grey9" },
+ { 229, 229, 229, "grey90" },
+ { 232, 232, 232, "grey91" },
+ { 235, 235, 235, "grey92" },
+ { 237, 237, 237, "grey93" },
+ { 240, 240, 240, "grey94" },
+ { 242, 242, 242, "grey95" },
+ { 245, 245, 245, "grey96" },
+ { 247, 247, 247, "grey97" },
+ { 250, 250, 250, "grey98" },
+ { 252, 252, 252, "grey99" },
+ { 240, 255, 240, "honeydew" },
+ { 240, 255, 240, "honeydew1" },
+ { 224, 238, 224, "honeydew2" },
+ { 193, 205, 193, "honeydew3" },
+ { 131, 139, 131, "honeydew4" },
+ { 255, 105, 180, "hot pink" },
+ { 255, 105, 180, "HotPink" },
+ { 255, 110, 180, "HotPink1" },
+ { 238, 106, 167, "HotPink2" },
+ { 205, 96, 144, "HotPink3" },
+ { 139, 58, 98, "HotPink4" },
+ { 205, 92, 92, "indian red" },
+ { 205, 92, 92, "IndianRed" },
+ { 255, 106, 106, "IndianRed1" },
+ { 238, 99, 99, "IndianRed2" },
+ { 205, 85, 85, "IndianRed3" },
+ { 139, 58, 58, "IndianRed4" },
+ { 75, 0, 130, "indigo" },
+ { 255, 255, 240, "ivory" },
+ { 255, 255, 240, "ivory1" },
+ { 238, 238, 224, "ivory2" },
+ { 205, 205, 193, "ivory3" },
+ { 139, 139, 131, "ivory4" },
+ { 240, 230, 140, "khaki" },
+ { 255, 246, 143, "khaki1" },
+ { 238, 230, 133, "khaki2" },
+ { 205, 198, 115, "khaki3" },
+ { 139, 134, 78, "khaki4" },
+ { 230, 230, 250, "lavender" },
+ { 255, 240, 245, "lavender blush" },
+ { 255, 240, 245, "LavenderBlush" },
+ { 255, 240, 245, "LavenderBlush1" },
+ { 238, 224, 229, "LavenderBlush2" },
+ { 205, 193, 197, "LavenderBlush3" },
+ { 139, 131, 134, "LavenderBlush4" },
+ { 124, 252, 0, "lawn green" },
+ { 124, 252, 0, "LawnGreen" },
+ { 255, 250, 205, "lemon chiffon" },
+ { 255, 250, 205, "LemonChiffon" },
+ { 255, 250, 205, "LemonChiffon1" },
+ { 238, 233, 191, "LemonChiffon2" },
+ { 205, 201, 165, "LemonChiffon3" },
+ { 139, 137, 112, "LemonChiffon4" },
+ { 173, 216, 230, "light blue" },
+ { 240, 128, 128, "light coral" },
+ { 224, 255, 255, "light cyan" },
+ { 238, 221, 130, "light goldenrod" },
+ { 250, 250, 210, "light goldenrod yellow" },
+ { 211, 211, 211, "light gray" },
+ { 144, 238, 144, "light green" },
+ { 211, 211, 211, "light grey" },
+ { 255, 182, 193, "light pink" },
+ { 255, 160, 122, "light salmon" },
+ { 32, 178, 170, "light sea green" },
+ { 135, 206, 250, "light sky blue" },
+ { 132, 112, 255, "light slate blue" },
+ { 119, 136, 153, "light slate gray" },
+ { 119, 136, 153, "light slate grey" },
+ { 176, 196, 222, "light steel blue" },
+ { 255, 255, 224, "light yellow" },
+ { 173, 216, 230, "LightBlue" },
+ { 191, 239, 255, "LightBlue1" },
+ { 178, 223, 238, "LightBlue2" },
+ { 154, 192, 205, "LightBlue3" },
+ { 104, 131, 139, "LightBlue4" },
+ { 240, 128, 128, "LightCoral" },
+ { 224, 255, 255, "LightCyan" },
+ { 224, 255, 255, "LightCyan1" },
+ { 209, 238, 238, "LightCyan2" },
+ { 180, 205, 205, "LightCyan3" },
+ { 122, 139, 139, "LightCyan4" },
+ { 238, 221, 130, "LightGoldenrod" },
+ { 255, 236, 139, "LightGoldenrod1" },
+ { 238, 220, 130, "LightGoldenrod2" },
+ { 205, 190, 112, "LightGoldenrod3" },
+ { 139, 129, 76, "LightGoldenrod4" },
+ { 250, 250, 210, "LightGoldenrodYellow" },
+ { 211, 211, 211, "LightGray" },
+ { 144, 238, 144, "LightGreen" },
+ { 211, 211, 211, "LightGrey" },
+ { 255, 182, 193, "LightPink" },
+ { 255, 174, 185, "LightPink1" },
+ { 238, 162, 173, "LightPink2" },
+ { 205, 140, 149, "LightPink3" },
+ { 139, 95, 101, "LightPink4" },
+ { 255, 160, 122, "LightSalmon" },
+ { 255, 160, 122, "LightSalmon1" },
+ { 238, 149, 114, "LightSalmon2" },
+ { 205, 129, 98, "LightSalmon3" },
+ { 139, 87, 66, "LightSalmon4" },
+ { 32, 178, 170, "LightSeaGreen" },
+ { 135, 206, 250, "LightSkyBlue" },
+ { 176, 226, 255, "LightSkyBlue1" },
+ { 164, 211, 238, "LightSkyBlue2" },
+ { 141, 182, 205, "LightSkyBlue3" },
+ { 96, 123, 139, "LightSkyBlue4" },
+ { 132, 112, 255, "LightSlateBlue" },
+ { 119, 136, 153, "LightSlateGray" },
+ { 119, 136, 153, "LightSlateGrey" },
+ { 176, 196, 222, "LightSteelBlue" },
+ { 202, 225, 255, "LightSteelBlue1" },
+ { 188, 210, 238, "LightSteelBlue2" },
+ { 162, 181, 205, "LightSteelBlue3" },
+ { 110, 123, 139, "LightSteelBlue4" },
+ { 255, 255, 224, "LightYellow" },
+ { 255, 255, 224, "LightYellow1" },
+ { 238, 238, 209, "LightYellow2" },
+ { 205, 205, 180, "LightYellow3" },
+ { 139, 139, 122, "LightYellow4" },
+ { 0, 255, 0, "lime" },
+ { 50, 205, 50, "lime green" },
+ { 50, 205, 50, "LimeGreen" },
+ { 250, 240, 230, "linen" },
+ { 255, 0, 255, "magenta" },
+ { 255, 0, 255, "magenta1" },
+ { 238, 0, 238, "magenta2" },
+ { 205, 0, 205, "magenta3" },
+ { 139, 0, 139, "magenta4" },
+ { 176, 48, 96, "maroon" },
+ { 255, 52, 179, "maroon1" },
+ { 238, 48, 167, "maroon2" },
+ { 205, 41, 144, "maroon3" },
+ { 139, 28, 98, "maroon4" },
+ { 102, 205, 170, "medium aquamarine" },
+ { 0, 0, 205, "medium blue" },
+ { 186, 85, 211, "medium orchid" },
+ { 147, 112, 219, "medium purple" },
+ { 60, 179, 113, "medium sea green" },
+ { 123, 104, 238, "medium slate blue" },
+ { 0, 250, 154, "medium spring green" },
+ { 72, 209, 204, "medium turquoise" },
+ { 199, 21, 133, "medium violet red" },
+ { 102, 205, 170, "MediumAquamarine" },
+ { 0, 0, 205, "MediumBlue" },
+ { 186, 85, 211, "MediumOrchid" },
+ { 224, 102, 255, "MediumOrchid1" },
+ { 209, 95, 238, "MediumOrchid2" },
+ { 180, 82, 205, "MediumOrchid3" },
+ { 122, 55, 139, "MediumOrchid4" },
+ { 147, 112, 219, "MediumPurple" },
+ { 171, 130, 255, "MediumPurple1" },
+ { 159, 121, 238, "MediumPurple2" },
+ { 137, 104, 205, "MediumPurple3" },
+ { 93, 71, 139, "MediumPurple4" },
+ { 60, 179, 113, "MediumSeaGreen" },
+ { 123, 104, 238, "MediumSlateBlue" },
+ { 0, 250, 154, "MediumSpringGreen" },
+ { 72, 209, 204, "MediumTurquoise" },
+ { 199, 21, 133, "MediumVioletRed" },
+ { 25, 25, 112, "midnight blue" },
+ { 25, 25, 112, "MidnightBlue" },
+ { 245, 255, 250, "mint cream" },
+ { 245, 255, 250, "MintCream" },
+ { 255, 228, 225, "misty rose" },
+ { 255, 228, 225, "MistyRose" },
+ { 255, 228, 225, "MistyRose1" },
+ { 238, 213, 210, "MistyRose2" },
+ { 205, 183, 181, "MistyRose3" },
+ { 139, 125, 123, "MistyRose4" },
+ { 255, 228, 181, "moccasin" },
+ { 255, 222, 173, "navajo white" },
+ { 255, 222, 173, "NavajoWhite" },
+ { 255, 222, 173, "NavajoWhite1" },
+ { 238, 207, 161, "NavajoWhite2" },
+ { 205, 179, 139, "NavajoWhite3" },
+ { 139, 121, 94, "NavajoWhite4" },
+ { 0, 0, 128, "navy" },
+ { 0, 0, 128, "navy blue" },
+ { 0, 0, 128, "NavyBlue" },
+ { 253, 245, 230, "old lace" },
+ { 253, 245, 230, "OldLace" },
+ { 128, 128, 0, "olive" },
+ { 107, 142, 35, "olive drab" },
+ { 107, 142, 35, "OliveDrab" },
+ { 192, 255, 62, "OliveDrab1" },
+ { 179, 238, 58, "OliveDrab2" },
+ { 154, 205, 50, "OliveDrab3" },
+ { 105, 139, 34, "OliveDrab4" },
+ { 255, 165, 0, "orange" },
+ { 255, 69, 0, "orange red" },
+ { 255, 165, 0, "orange1" },
+ { 238, 154, 0, "orange2" },
+ { 205, 133, 0, "orange3" },
+ { 139, 90, 0, "orange4" },
+ { 255, 69, 0, "OrangeRed" },
+ { 255, 69, 0, "OrangeRed1" },
+ { 238, 64, 0, "OrangeRed2" },
+ { 205, 55, 0, "OrangeRed3" },
+ { 139, 37, 0, "OrangeRed4" },
+ { 218, 112, 214, "orchid" },
+ { 255, 131, 250, "orchid1" },
+ { 238, 122, 233, "orchid2" },
+ { 205, 105, 201, "orchid3" },
+ { 139, 71, 137, "orchid4" },
+ { 238, 232, 170, "pale goldenrod" },
+ { 152, 251, 152, "pale green" },
+ { 175, 238, 238, "pale turquoise" },
+ { 219, 112, 147, "pale violet red" },
+ { 238, 232, 170, "PaleGoldenrod" },
+ { 152, 251, 152, "PaleGreen" },
+ { 154, 255, 154, "PaleGreen1" },
+ { 144, 238, 144, "PaleGreen2" },
+ { 124, 205, 124, "PaleGreen3" },
+ { 84, 139, 84, "PaleGreen4" },
+ { 175, 238, 238, "PaleTurquoise" },
+ { 187, 255, 255, "PaleTurquoise1" },
+ { 174, 238, 238, "PaleTurquoise2" },
+ { 150, 205, 205, "PaleTurquoise3" },
+ { 102, 139, 139, "PaleTurquoise4" },
+ { 219, 112, 147, "PaleVioletRed" },
+ { 255, 130, 171, "PaleVioletRed1" },
+ { 238, 121, 159, "PaleVioletRed2" },
+ { 205, 104, 137, "PaleVioletRed3" },
+ { 139, 71, 93, "PaleVioletRed4" },
+ { 255, 239, 213, "papaya whip" },
+ { 255, 239, 213, "PapayaWhip" },
+ { 255, 218, 185, "peach puff" },
+ { 255, 218, 185, "PeachPuff" },
+ { 255, 218, 185, "PeachPuff1" },
+ { 238, 203, 173, "PeachPuff2" },
+ { 205, 175, 149, "PeachPuff3" },
+ { 139, 119, 101, "PeachPuff4" },
+ { 205, 133, 63, "peru" },
+ { 255, 192, 203, "pink" },
+ { 255, 181, 197, "pink1" },
+ { 238, 169, 184, "pink2" },
+ { 205, 145, 158, "pink3" },
+ { 139, 99, 108, "pink4" },
+ { 221, 160, 221, "plum" },
+ { 255, 187, 255, "plum1" },
+ { 238, 174, 238, "plum2" },
+ { 205, 150, 205, "plum3" },
+ { 139, 102, 139, "plum4" },
+ { 176, 224, 230, "powder blue" },
+ { 176, 224, 230, "PowderBlue" },
+ { 160, 32, 240, "purple" },
+ { 155, 48, 255, "purple1" },
+ { 145, 44, 238, "purple2" },
+ { 125, 38, 205, "purple3" },
+ { 85, 26, 139, "purple4" },
+ { 102, 51, 153, "rebecca purple" },
+ { 102, 51, 153, "RebeccaPurple" },
+ { 255, 0, 0, "red" },
+ { 255, 0, 0, "red1" },
+ { 238, 0, 0, "red2" },
+ { 205, 0, 0, "red3" },
+ { 139, 0, 0, "red4" },
+ { 188, 143, 143, "rosy brown" },
+ { 188, 143, 143, "RosyBrown" },
+ { 255, 193, 193, "RosyBrown1" },
+ { 238, 180, 180, "RosyBrown2" },
+ { 205, 155, 155, "RosyBrown3" },
+ { 139, 105, 105, "RosyBrown4" },
+ { 65, 105, 225, "royal blue" },
+ { 65, 105, 225, "RoyalBlue" },
+ { 72, 118, 255, "RoyalBlue1" },
+ { 67, 110, 238, "RoyalBlue2" },
+ { 58, 95, 205, "RoyalBlue3" },
+ { 39, 64, 139, "RoyalBlue4" },
+ { 139, 69, 19, "saddle brown" },
+ { 139, 69, 19, "SaddleBrown" },
+ { 250, 128, 114, "salmon" },
+ { 255, 140, 105, "salmon1" },
+ { 238, 130, 98, "salmon2" },
+ { 205, 112, 84, "salmon3" },
+ { 139, 76, 57, "salmon4" },
+ { 244, 164, 96, "sandy brown" },
+ { 244, 164, 96, "SandyBrown" },
+ { 46, 139, 87, "sea green" },
+ { 46, 139, 87, "SeaGreen" },
+ { 84, 255, 159, "SeaGreen1" },
+ { 78, 238, 148, "SeaGreen2" },
+ { 67, 205, 128, "SeaGreen3" },
+ { 46, 139, 87, "SeaGreen4" },
+ { 255, 245, 238, "seashell" },
+ { 255, 245, 238, "seashell1" },
+ { 238, 229, 222, "seashell2" },
+ { 205, 197, 191, "seashell3" },
+ { 139, 134, 130, "seashell4" },
+ { 160, 82, 45, "sienna" },
+ { 255, 130, 71, "sienna1" },
+ { 238, 121, 66, "sienna2" },
+ { 205, 104, 57, "sienna3" },
+ { 139, 71, 38, "sienna4" },
+ { 192, 192, 192, "silver" },
+ { 135, 206, 235, "sky blue" },
+ { 135, 206, 235, "SkyBlue" },
+ { 135, 206, 255, "SkyBlue1" },
+ { 126, 192, 238, "SkyBlue2" },
+ { 108, 166, 205, "SkyBlue3" },
+ { 74, 112, 139, "SkyBlue4" },
+ { 106, 90, 205, "slate blue" },
+ { 112, 128, 144, "slate gray" },
+ { 112, 128, 144, "slate grey" },
+ { 106, 90, 205, "SlateBlue" },
+ { 131, 111, 255, "SlateBlue1" },
+ { 122, 103, 238, "SlateBlue2" },
+ { 105, 89, 205, "SlateBlue3" },
+ { 71, 60, 139, "SlateBlue4" },
+ { 112, 128, 144, "SlateGray" },
+ { 198, 226, 255, "SlateGray1" },
+ { 185, 211, 238, "SlateGray2" },
+ { 159, 182, 205, "SlateGray3" },
+ { 108, 123, 139, "SlateGray4" },
+ { 112, 128, 144, "SlateGrey" },
+ { 255, 250, 250, "snow" },
+ { 255, 250, 250, "snow1" },
+ { 238, 233, 233, "snow2" },
+ { 205, 201, 201, "snow3" },
+ { 139, 137, 137, "snow4" },
+ { 0, 255, 127, "spring green" },
+ { 0, 255, 127, "SpringGreen" },
+ { 0, 255, 127, "SpringGreen1" },
+ { 0, 238, 118, "SpringGreen2" },
+ { 0, 205, 102, "SpringGreen3" },
+ { 0, 139, 69, "SpringGreen4" },
+ { 70, 130, 180, "steel blue" },
+ { 70, 130, 180, "SteelBlue" },
+ { 99, 184, 255, "SteelBlue1" },
+ { 92, 172, 238, "SteelBlue2" },
+ { 79, 148, 205, "SteelBlue3" },
+ { 54, 100, 139, "SteelBlue4" },
+ { 210, 180, 140, "tan" },
+ { 255, 165, 79, "tan1" },
+ { 238, 154, 73, "tan2" },
+ { 205, 133, 63, "tan3" },
+ { 139, 90, 43, "tan4" },
+ { 0, 128, 128, "teal" },
+ { 216, 191, 216, "thistle" },
+ { 255, 225, 255, "thistle1" },
+ { 238, 210, 238, "thistle2" },
+ { 205, 181, 205, "thistle3" },
+ { 139, 123, 139, "thistle4" },
+ { 255, 99, 71, "tomato" },
+ { 255, 99, 71, "tomato1" },
+ { 238, 92, 66, "tomato2" },
+ { 205, 79, 57, "tomato3" },
+ { 139, 54, 38, "tomato4" },
+ { 64, 224, 208, "turquoise" },
+ { 0, 245, 255, "turquoise1" },
+ { 0, 229, 238, "turquoise2" },
+ { 0, 197, 205, "turquoise3" },
+ { 0, 134, 139, "turquoise4" },
+ { 238, 130, 238, "violet" },
+ { 208, 32, 144, "violet red" },
+ { 208, 32, 144, "VioletRed" },
+ { 255, 62, 150, "VioletRed1" },
+ { 238, 58, 140, "VioletRed2" },
+ { 205, 50, 120, "VioletRed3" },
+ { 139, 34, 82, "VioletRed4" },
+ { 128, 128, 128, "web gray" },
+ { 0, 128, 0, "web green" },
+ { 128, 128, 128, "web grey" },
+ { 128, 0, 0, "web maroon" },
+ { 128, 0, 128, "web purple" },
+ { 128, 128, 128, "WebGray" },
+ { 0, 128, 0, "WebGreen" },
+ { 128, 128, 128, "WebGrey" },
+ { 128, 0, 0, "WebMaroon" },
+ { 128, 0, 128, "WebPurple" },
+ { 245, 222, 179, "wheat" },
+ { 255, 231, 186, "wheat1" },
+ { 238, 216, 174, "wheat2" },
+ { 205, 186, 150, "wheat3" },
+ { 139, 126, 102, "wheat4" },
+ { 255, 255, 255, "white" },
+ { 245, 245, 245, "white smoke" },
+ { 245, 245, 245, "WhiteSmoke" },
+ { 190, 190, 190, "x11 gray" },
+ { 0, 255, 0, "x11 green" },
+ { 190, 190, 190, "x11 grey" },
+ { 176, 48, 96, "x11 maroon" },
+ { 160, 32, 240, "x11 purple" },
+ { 190, 190, 190, "X11Gray" },
+ { 0, 255, 0, "X11Green" },
+ { 190, 190, 190, "X11Grey" },
+ { 176, 48, 96, "X11Maroon" },
+ { 160, 32, 240, "X11Purple" },
+ { 255, 255, 0, "yellow" },
+ { 154, 205, 50, "yellow green" },
+ { 255, 255, 0, "yellow1" },
+ { 238, 238, 0, "yellow2" },
+ { 205, 205, 0, "yellow3" },
+ { 139, 139, 0, "yellow4" },
+ { 154, 205, 50, "YellowGreen" },
+};
+
+Bool
+dixLookupBuiltinColor(int screen,
+ char *name,
+ unsigned int len,
+ unsigned short *pred,
+ unsigned short *pgreen,
+ unsigned short *pblue)
+{
+ int low = 0;
+ int high = ARRAY_SIZE(BuiltinColors) - 1;
+
+ while (high >= low) {
+ int mid = (low + high) / 2;
+ const BuiltinColor *c = &BuiltinColors[mid];
+ const int currentLen = strlen(c->name);
+ const int r = strncasecmp(c->name, name, min(len, currentLen));
+
+ if (r == 0) {
+ if (len == currentLen) {
+ *pred = c->red * 0x101;
+ *pgreen = c->green * 0x101;
+ *pblue = c->blue * 0x101;
+ return TRUE;
+ } else if (len > currentLen) {
+ low = mid + 1;
+ } else {
+ high = mid - 1;
+ }
+ } else if (r > 0) {
+ high = mid - 1;
+ } else {
+ low = mid + 1;
+ }
+ }
+ return FALSE;
+}
diff --git a/src/xpm/dix-config.h b/src/xpm/dix-config.h
new file mode 100644
index 0000000..5137e8a
--- /dev/null
+++ b/src/xpm/dix-config.h
@@ -0,0 +1 @@
+// Empty file making color.c able to compile without modifications.
diff --git a/src/xpm/dix/dix_priv.h b/src/xpm/dix/dix_priv.h
new file mode 100644
index 0000000..005da83
--- /dev/null
+++ b/src/xpm/dix/dix_priv.h
@@ -0,0 +1,14 @@
+#ifndef DIX_PRIV_H
+#define DIX_PRIV_H
+
+#define TRUE 1
+#define FALSE 0
+
+#define ARRAY_SIZE(a) (sizeof((a)) / sizeof((a)[0]))
+
+#include <string.h>
+#include <strings.h>
+
+#define min(a, b) ((a) < (b) ? (a) : (b))
+
+#endif // DIX_PRIV_H
diff --git a/src/xpm/include/dix.h b/src/xpm/include/dix.h
new file mode 100644
index 0000000..2a7125a
--- /dev/null
+++ b/src/xpm/include/dix.h
@@ -0,0 +1,14 @@
+#ifndef DIX_H
+#define DIX_H
+
+#include <X11/Xlib.h>
+
+Bool
+dixLookupBuiltinColor(int screen,
+ char *name,
+ unsigned int len,
+ unsigned short *pred,
+ unsigned short *pgreen,
+ unsigned short *pblue);
+
+#endif // DIX_H