/* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "stdafx.h" #include "scripting/scriptscreendrawing.h" #include #include namespace script_screens { namespace { struct sequence { std::size_t length{1}; char32_t first_bits{0}; }; // the length of the UTF-8 sequence starting with the byte and the bits it carries; length 0 for a byte that can't // start one sequence utf8_sequence(std::uint8_t const Lead) { if (Lead < 0x80) { return {1, Lead}; } if (Lead >= 0xC2 && Lead <= 0xDF) { return {2, static_cast(Lead & 0x1F)}; } if (Lead >= 0xE0 && Lead <= 0xEF) { return {3, static_cast(Lead & 0x0F)}; } if (Lead >= 0xF0 && Lead <= 0xF4) { return {4, static_cast(Lead & 0x07)}; } return {0, 0}; } bool continuation(std::uint8_t const Byte) { return (Byte & 0xC0) == 0x80; } // a pixel is lit unless it is opaque white, as led_util compares it with (255, 255, 255) and (255, 255, 255, 255) bool lit_pixel(std::span const Rgba, int const Width, int const X, int const Y) { auto const at{(static_cast(Y) * Width + X) * 4}; return false == (Rgba[at] == 255 && Rgba[at + 1] == 255 && Rgba[at + 2] == 255 && Rgba[at + 3] == 255); } // the columns [First, First + Count) of the image as a glyph; columns past the image's edge are unlit glyph cut_glyph(std::span const Rgba, int const Width, int const Height, int const First, int const Count) { std::vector lit(static_cast(Count) * Height, false); for (auto column = 0; column < Count; ++column) { auto const x{First + column}; if (x < 0 || x >= Width) { continue; } for (auto y = 0; y < Height; ++y) { lit[static_cast(column) * Height + y] = lit_pixel(Rgba, Width, x, y); } } return {Count, Height, std::move(lit)}; } // the lines of the text, without the line ends (\n or \r\n, as python's text files read them) std::vector lines(std::string_view Text) { std::vector result; while (false == Text.empty()) { auto const end{std::min(Text.find('\n'), Text.size())}; auto line{Text.substr(0, end)}; if (false == line.empty() && line.back() == '\r') { line.remove_suffix(1); } result.push_back(line); Text.remove_prefix(std::min(end + 1, Text.size())); } return result; } } // namespace std::u32string decode_utf8(std::string_view const Text) { std::u32string result; std::size_t idx{0}; while (idx < Text.size()) { auto const lead{static_cast(Text[idx])}; auto [length, code]{utf8_sequence(lead)}; auto valid{length > 0 && idx + length <= Text.size()}; for (std::size_t offset = 1; valid && offset < length; ++offset) { auto const byte{static_cast(Text[idx + offset])}; valid = continuation(byte); code = (code << 6) | (byte & 0x3F); } if (false == valid) { result.push_back(lead); ++idx; continue; } result.push_back(code); idx += length; } return result; } std::optional bitmap_font::parse(std::span const Rgba, int const Width, int const Height, std::string_view const Description) { if (Width <= 0 || Height <= 0 || Rgba.size() < static_cast(Width) * Height * 4) { return std::nullopt; } bitmap_font font; font.m_height = Height; auto const all{lines(Description)}; auto x{0}; // the first line is a comment for (std::size_t line = 1; line < all.size(); ++line) { auto const characters{decode_utf8(all[line])}; if (characters.empty()) { continue; } // the character, then its width in digits auto const lead{utf8_sequence(static_cast(all[line][0])).length}; auto digits{all[line].substr(std::max(lead, 1))}; // int() of python lets spaces around the number through while (false == digits.empty() && (digits.front() == ' ' || digits.front() == ' ')) { digits.remove_prefix(1); } while (false == digits.empty() && (digits.back() == ' ' || digits.back() == ' ')) { digits.remove_suffix(1); } auto width{0}; if (std::from_chars(digits.data(), digits.data() + digits.size(), width).ec != std::errc{} || width < 0) { continue; } auto cut{cut_glyph(Rgba, Width, Height, x, width)}; if (font.m_glyphs.empty()) { font.m_fallback = cut; } font.m_glyphs.insert_or_assign(characters.front(), std::move(cut)); x += width + 1; } if (font.m_glyphs.empty()) { return std::nullopt; } return font; } glyph bitmap_font::sprite(std::span const Rgba, int const Width, int const Height) { if (Width <= 0 || Height <= 0 || Rgba.size() < static_cast(Width) * Height * 4) { return {}; } return cut_glyph(Rgba, Width, Height, 0, Width); } glyph const &bitmap_font::find(char32_t const Character) const { if (auto const lookup{m_glyphs.find(Character)}; lookup != m_glyphs.end()) { return lookup->second; } return m_fallback; } int bitmap_font::text_width(std::string_view const Text, int const Spacing) const { auto width{0}; for (auto const character : decode_utf8(Text)) { width += find(character).width() + Spacing; } return width; } canvas::canvas(int const Width, int const Height, rgb const Color) : m_width{std::max(Width, 0)}, m_height{std::max(Height, 0)}, m_pixels(static_cast(m_width) * m_height * 4, 255) { fill(Color); } canvas::canvas(int const Width, int const Height, std::vector Pixels) : m_width{Width}, m_height{Height}, m_pixels{std::move(Pixels)} { m_pixels.resize(static_cast(std::max(Width, 0)) * std::max(Height, 0) * 4, 255); } void canvas::fill(rgb const Color) { for (std::size_t at = 0; at < m_pixels.size(); at += 4) { std::ranges::copy(Color, m_pixels.begin() + static_cast(at)); m_pixels[at + 3] = 255; } } void canvas::set(int const X, int const Y, rgb const Color) { if (X < 0 || Y < 0 || X >= m_width || Y >= m_height) { return; } auto const at{(static_cast(Y) * m_width + X) * 4}; std::ranges::copy(Color, m_pixels.begin() + static_cast(at)); } rgb canvas::get(int const X, int const Y) const { if (X < 0 || Y < 0 || X >= m_width || Y >= m_height) { return {0, 0, 0}; } auto const at{(static_cast(Y) * m_width + X) * 4}; return {m_pixels[at], m_pixels[at + 1], m_pixels[at + 2]}; } void canvas::fill_rect(int const X, int const Y, int const Width, int const Height, rgb const Color) { for (auto y = std::max(Y, 0); y < std::min(Y + Height, m_height); ++y) { for (auto x = std::max(X, 0); x < std::min(X + Width, m_width); ++x) { set(x, y, Color); } } } void canvas::draw_glyph(glyph const &Glyph, int const X, int const Y, rgb const Color) { for (auto column = 0; column < Glyph.width(); ++column) { for (auto row = 0; row < Glyph.height(); ++row) { if (Glyph.lit(column, row)) { set(X + column, Y + row, Color); } } } } int canvas::draw_text(bitmap_font const &Font, int X, int const Y, std::string_view const Text, rgb const Color, int const Spacing) { for (auto const character : decode_utf8(Text)) { auto const &shape{Font.find(character)}; draw_glyph(shape, X, Y, Color); X += shape.width() + Spacing; } return X; } void canvas::draw_image(canvas const &Image, int const X, int const Y) { for (auto y = 0; y < Image.height(); ++y) { for (auto x = 0; x < Image.width(); ++x) { set(X + x, Y + y, Image.get(x, y)); } } } } // namespace script_screens