#include "paint/stroke_script.h" #include #include #include #include namespace pp::paint { namespace { [[nodiscard]] std::string_view trim(std::string_view text) noexcept { while (!text.empty() && (text.front() == ' ' || text.front() == '\t' || text.front() == '\r')) { text.remove_prefix(1); } while (!text.empty() && (text.back() == ' ' || text.back() == '\t' || text.back() == '\r')) { text.remove_suffix(1); } return text; } [[nodiscard]] std::string_view strip_comment(std::string_view line) noexcept { const auto comment = line.find('#'); if (comment == std::string_view::npos) { return line; } return line.substr(0, comment); } [[nodiscard]] pp::foundation::Result parse_float_token(std::string_view token) noexcept { token = trim(token); if (token.empty() || token.size() >= 64U) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("stroke script numeric token is invalid")); } std::array buffer {}; for (std::size_t i = 0; i < token.size(); ++i) { buffer[i] = token[i]; } char* end = nullptr; errno = 0; const auto value = std::strtof(buffer.data(), &end); if (errno != 0 || end != buffer.data() + static_cast(token.size()) || !std::isfinite(value)) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("stroke script numeric token is invalid")); } return pp::foundation::Result::success(value); } [[nodiscard]] pp::foundation::Result split_tokens( std::string_view line, std::array& tokens) noexcept { std::size_t count = 0; std::size_t offset = 0; while (offset < line.size()) { while (offset < line.size() && (line[offset] == ' ' || line[offset] == '\t')) { ++offset; } if (offset >= line.size()) { break; } const auto token_start = offset; while (offset < line.size() && line[offset] != ' ' && line[offset] != '\t') { ++offset; } if (count >= tokens.size()) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("stroke script line has too many tokens")); } tokens[count] = line.substr(token_start, offset - token_start); ++count; } return pp::foundation::Result::success(count); } [[nodiscard]] pp::foundation::Result parse_stroke_line(std::string_view line) noexcept { std::array tokens {}; const auto token_count = split_tokens(line, tokens); if (!token_count) { return pp::foundation::Result::failure(token_count.status()); } if (token_count.value() != tokens.size() || tokens[0] != "stroke") { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("stroke script line must be 'stroke x1 y1 p1 x2 y2 p2 spacing'")); } const auto x1 = parse_float_token(tokens[1]); const auto y1 = parse_float_token(tokens[2]); const auto p1 = parse_float_token(tokens[3]); const auto x2 = parse_float_token(tokens[4]); const auto y2 = parse_float_token(tokens[5]); const auto p2 = parse_float_token(tokens[6]); const auto spacing = parse_float_token(tokens[7]); if (!x1) { return pp::foundation::Result::failure(x1.status()); } if (!y1) { return pp::foundation::Result::failure(y1.status()); } if (!p1) { return pp::foundation::Result::failure(p1.status()); } if (!x2) { return pp::foundation::Result::failure(x2.status()); } if (!y2) { return pp::foundation::Result::failure(y2.status()); } if (!p2) { return pp::foundation::Result::failure(p2.status()); } if (!spacing) { return pp::foundation::Result::failure(spacing.status()); } if (spacing.value() <= 0.0F) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("stroke script spacing must be greater than zero")); } return pp::foundation::Result::success(StrokeScriptStroke { .start = StrokePoint { .x = x1.value(), .y = y1.value(), .pressure = p1.value(), }, .end = StrokePoint { .x = x2.value(), .y = y2.value(), .pressure = p2.value(), }, .spacing = spacing.value(), }); } } pp::foundation::Result parse_stroke_script(std::string_view text) { if (text.empty()) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("stroke script must not be empty")); } if (text.size() > max_stroke_script_bytes) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("stroke script exceeds the configured size limit")); } StrokeScript script; std::size_t offset = 0; while (offset <= text.size()) { const auto line_start = offset; const auto line_end = text.find('\n', line_start); if (line_end == std::string_view::npos) { offset = text.size() + 1U; } else { offset = line_end + 1U; } auto line = text.substr(line_start, (line_end == std::string_view::npos) ? std::string_view::npos : line_end - line_start); if (line.size() > max_stroke_script_line_length) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("stroke script line exceeds the configured length limit")); } line = trim(strip_comment(line)); if (line.empty()) { continue; } if (script.strokes.size() >= max_stroke_script_strokes) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("stroke script stroke count exceeds the configured limit")); } const auto stroke = parse_stroke_line(line); if (!stroke) { return pp::foundation::Result::failure(stroke.status()); } script.strokes.push_back(stroke.value()); } if (script.strokes.empty()) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("stroke script must contain at least one stroke")); } return pp::foundation::Result::success(script); } }