5#include <boost/spirit/home/x3.hpp>
12namespace x3 = boost::spirit::x3;
22std::string lower(std::string_view value) {
23 std::string out(value);
24 std::ranges::transform(out, out.begin(), [](
unsigned char c) {
25 return static_cast<char>(std::tolower(c));
30bool isGeneratorPrefix(std::string_view prefix) {
31 const auto key = lower(prefix);
35std::optional<Format> knownFormat(std::string_view name) {
38 if (format ==
Format::RAW && !lower(name).starts_with(
"raw"))
return std::nullopt;
40 }
catch (
const std::exception&) {
45std::optional<PrefixSpec> parsePrefixedSpec(std::string_view token) {
47 auto first = token.begin();
48 const auto last = token.end();
50 const auto prefixRule = x3::lexeme[+(x3::char_ -
':' -
'/' -
'\\')];
51 const auto valueRule = x3::lexeme[*x3::char_];
52 if (!x3::parse(first, last, prefixRule, spec.prefix))
return std::nullopt;
53 if (!x3::parse(first, last, x3::lit(
':')))
return std::nullopt;
54 if (!x3::parse(first, last, valueRule, spec.value) || first != last)
return std::nullopt;
56 if (spec.prefix.empty() || spec.value.empty())
return spec;
57 if (!knownFormat(spec.prefix) && !isGeneratorPrefix(spec.prefix))
return std::nullopt;
61Format formatFromPathOrRaw(std::string_view path) {
64 }
catch (
const std::exception&) {
69void setError(
Command& command, std::string message, std::string token = {}) {
70 command.error.message = std::move(message);
71 command.error.token = std::move(token);
74bool isOption(std::string_view token) {
75 return token.starts_with(
'-') && token !=
"-";
78std::string unescapeDoubleQuoted(std::string_view text) {
80 out.reserve(text.size());
81 for (
size_t i = 0; i < text.size(); i++) {
82 if (text[i] ==
'\\' && i + 1 < text.size()) {
83 out.push_back(text[++i]);
85 out.push_back(text[i]);
91std::string normalizeLexeme(std::string text) {
92 if (text ==
"\\(")
return "(";
93 if (text ==
"\\)")
return ")";
94 if (text.size() >= 2 && text.front() ==
'\'' && text.back() ==
'\'') {
95 return text.substr(1, text.size() - 2);
97 if (text.size() >= 2 && text.front() ==
'"' && text.back() ==
'"') {
98 return unescapeDoubleQuoted(std::string_view(text).substr(1, text.size() - 2));
103TokenType generatorType(std::string_view prefix) {
104 const auto key = lower(prefix);
109 constexpr size_t maxTokens = 32;
110 if (command.tokens.size() >= maxTokens) {
111 setError(command,
"too many convert tokens", token.value);
114 command.tokens.push_back(std::move(token));
118bool tokenizeGenerator(
Command& command,
const PrefixSpec& spec, std::string sizeArg, std::string token) {
119 if (command.hasInput) {
120 setError(command,
"multiple convert inputs are not supported yet", token);
124 command.hasInput =
true;
125 command.hasGenerator =
true;
126 return addToken(command,
Token{.type = generatorType(spec.prefix), .value = spec.value, .arg = std::move(sizeArg)});
129bool tokenizeInput(
Command& command, std::string token) {
130 if (command.hasInput) {
131 setError(command,
"multiple convert inputs are not supported yet", token);
135 command.hasInput =
true;
136 command.inputPath = token;
137 command.inputFormat = formatFromPathOrRaw(token);
139 if (
const auto spec = parsePrefixedSpec(token); spec && knownFormat(spec->prefix)) {
140 if (spec->value.empty()) {
141 setError(command,
"missing path after format prefix", token);
144 command.inputPath = spec->value;
145 command.inputFormatName = spec->prefix;
146 command.inputFormat = *knownFormat(spec->prefix);
149 return addToken(command,
Token{.type =
TokenType::InputFile, .value = command.inputPath, .arg = command.inputFormatName, .format = command.inputFormat});
152bool tokenizeOutput(
Command& command, std::string token) {
153 command.outputPath = token;
155 if (
const auto spec = parsePrefixedSpec(token); spec && knownFormat(spec->prefix)) {
156 if (spec->value.empty()) {
157 setError(command,
"missing path after format prefix", token);
160 command.outputPath = spec->value;
161 command.outputFormatName = spec->prefix;
162 command.outputFormat = *knownFormat(spec->prefix);
163 }
else if (command.outputFormatName.empty()) {
164 command.outputFormat = formatFromPathOrRaw(token);
167 command.outputIsStdout = command.outputPath ==
"-";
169 return addToken(command,
Token{.type =
TokenType::OutputFile, .value = command.outputPath, .arg = command.outputFormatName, .format = command.outputFormat});
172bool tokenizeSource(
Command& command, std::string token, std::string pendingSize) {
173 if (
const auto spec = parsePrefixedSpec(token); spec && isGeneratorPrefix(spec->prefix)) {
174 return tokenizeGenerator(command, *spec, std::move(pendingSize), std::move(token));
182 if (!pendingSize.empty()) {
183 setError(command,
"-size is the size a generator is created at; use -resize to scale a picture",
188 return tokenizeInput(command, std::move(token));
198std::optional<std::string> valueOf(
Command& command,
const Lexer::Word& word,
size_t& i,
199 std::span<const std::string> args) {
200 if (
word.hasValue)
return std::string(
word.value);
201 if (
word.option->args == 0)
return std::string{};
202 if (i + 1 >= args.size()) {
203 setError(command,
"missing convert option argument", std::string(
word.spelling));
209bool addCrop(
Command& command, std::string value,
bool required) {
210 std::optional<Rect> rect;
211 if (!value.empty()) {
214 setError(command,
"invalid crop geometry", value);
218 }
else if (required) {
219 setError(command,
"missing convert option argument",
"-crop");
226bool addResize(
Command& command, std::string value, std::string option) {
235 setError(command,
"invalid resize size, which is either WxH or a share such as 30% or 30pct",
236 value.empty() ? option : value);
243std::optional<size_t> countOf(
const std::string& value) {
246 if (number <= 0)
return std::nullopt;
247 return static_cast<size_t>(number);
248 }
catch (
const std::exception&) {
253bool addRadius(
Command& command, std::string value) {
257 }
catch (
const std::exception&) {
258 setError(command,
"invalid radius", value);
264bool addFormat(
Command& command, std::string value) {
265 const auto format = knownFormat(value);
267 setError(command,
"unknown output format", value);
270 command.outputFormat = *format;
271 command.outputFormatName = value;
280bool addPixelFormat(
Command& command, std::string value) {
283 setError(command,
"unknown pixel format, which is a colour such as rgba8888, bgr888, rgb565 or yuv420", value);
287 command.outputColor = *color;
288 command.outputColorName = value;
292bool addFx(
Command& command, std::string value) {
300bool addFilter(
Command& command, std::string value, std::string option) {
302 command.error =
ParseError{
"filter takes nearest, bayer, floyd or strict", value};
305 if (command.pendingFilter) {
306 command.error =
ParseError{
"this filter replaces one nothing has used yet",
307 command.pendingFilter->value};
310 command.pendingFilter =
Command::Pending{std::move(option), std::move(value)};
315bool addLimit(
Command& command, std::string value, std::string option) {
325 const auto colon = std::string_view{value}.find(
':');
326 if (colon == std::string_view::npos) {
327 command.error =
ParseError{
"limit needs to say which colours to keep, "
328 "as in -limit spread:16 or -limit sort:16", value};
332 const auto named =
Haio::limitNamed(std::string_view{value}.substr(0, colon));
334 command.error =
ParseError{
"limit takes sort or spread before its colon", value};
338 const auto most = countOf(std::string(std::string_view{value}.substr(colon + 1)));
340 command.error =
ParseError{
"limit takes a number of colours, counted from one", value};
343 if (command.pendingLimit) {
344 command.error =
ParseError{
"this limit replaces one nothing has used yet",
345 command.pendingLimit->value};
349 command.pendingLimitHow = *named;
350 command.pendingLimit =
Command::Pending{std::move(option), std::move(value)};
355bool addPalette(
Command& command, std::string value) {
356 if (!command.pendingFilter) {
357 command.error =
ParseError{
"putting a picture into a palette needs a filter first, "
358 "as in -filter bayer before -palete", value};
362 command.pendingFilter.reset();
366 if (command.pendingLimit) {
367 const auto spelled = std::string_view{command.pendingLimit->value};
368 most = *countOf(std::string(spelled.substr(spelled.find(
':') + 1)));
369 limitHow = command.pendingLimitHow;
370 command.pendingLimit.reset();
375 .limit = most, .limitHow = limitHow});
387bool readCrop(
Command& command,
const Lexer::Word& word,
size_t& i, std::span<const std::string> args) {
388 std::string value{
word.value};
390 if (!
word.hasValue && i + 1 < args.size() && !isOption(args[i + 1])) {
392 const std::string_view next = args[i + 1];
395 }
else if (i + 2 < args.size()) {
396 setError(command,
"invalid crop geometry", std::string(next));
400 return addCrop(command, std::move(value),
false);
411 std::span<const std::string> args, std::string& pendingSize) {
418 setError(command,
"-size is the size a generator is created at; use -resize to scale a picture",
425 const auto value = valueOf(command, word, i, args);
426 if (!value)
return false;
428 const auto option = std::string(
word.spelling);
429 switch (
word.option->kind) {
446Command parseTokens(std::span<const std::string> args) {
448 if (args.size() <= 2) {
449 setError(command,
"missing convert input");
453 std::string pendingSize;
454 bool hasOutput =
false;
456 for (
size_t i = 1; i < args.size(); i++) {
457 const std::string_view token = args[i];
460 if (!readOption(command, *word, i, args, pendingSize))
return command;
464 if (isOption(token)) {
465 setError(command,
"unknown convert option", std::string(token));
469 if (!command.hasInput) {
470 if (!tokenizeSource(command, std::string(token), std::move(pendingSize)))
return command;
475 if (!pendingSize.empty()) {
476 setError(command,
"-size is the size a generator is created at; use -resize to scale a picture",
482 setError(command,
"multiple convert outputs are not supported yet", std::string(token));
486 if (!tokenizeOutput(command, std::string(token)))
return command;
491 if (!pendingSize.empty()) {
492 setError(command,
"nothing used this -size; it has to come before a generator such as xc:",
497 if (!command.hasInput) {
498 setError(command,
"missing convert input");
503 setError(command,
"missing convert output");
512ParseError::operator bool() const noexcept {
529 std::vector<std::string> args;
530 auto first = text.begin();
531 const auto last = text.end();
532 if (!x3::parse(first, last,
Lexer::cmdline, args) || first != last) {
533 throw std::runtime_error(
"invalid convert command line");
536 std::ranges::transform(args, args.begin(), normalizeLexeme);
545 std::vector<std::string> args;
546 args.reserve(
static_cast<size_t>(std::max(argc, 0)));
547 for (
int i = 0; i < argc; i++) {
548 args.emplace_back(argv[i]);
551 auto command = parseTokens(args);
552 if (command.error)
return command;
561 for (
const auto* waiting : {&command.pendingFilter, &command.pendingLimit}) {
562 if (!*waiting)
continue;
563 command.error =
ParseError{
"nothing used this " + (*waiting)->option
564 +
"; it has to come before what it applies to",
constexpr std::string_view generatorKinds[]
constexpr std::optional< Word > optionOf(std::string_view token)
bool parseSizeToken(std::string_view text, Size &out)
bool parseCropGeometryToken(std::string_view text, Rect &out)
Command parseCommandLine(std::string_view text)
std::vector< std::string > lexCommandLine(std::string_view text)
Command parseArgs(int argc, char *argv[])
bool parseRectToken(std::string_view text, Rect &out)
bool tryGetRect(std::string_view value, Rect &out)
bool tryGetSize(std::string_view value, Size &out)
int getPercent(std::string_view value)
int getInt(std::string_view value)
bool tryGetCropGeometry(std::string_view value, Rect &out)
Format formatFromExtension(std::string_view path)
constexpr Format formatFromName(std::string_view name)
constexpr std::optional< Color > colorNamed(std::string_view name)
std::optional< Dither > ditherNamed(std::string_view name)
std::optional< Limit > limitNamed(std::string_view name)