diff --git a/firmware/application/CMakeLists.txt b/firmware/application/CMakeLists.txt index 4e67af38..ebd5e670 100644 --- a/firmware/application/CMakeLists.txt +++ b/firmware/application/CMakeLists.txt @@ -178,6 +178,7 @@ set(CPPSRC ${COMMON}/wm8731.cpp ${COMMON}/ads1110.cpp ${COMMON}/performance_counter.cpp + ${COMMON}/bmpfile.cpp app_settings.cpp audio.cpp baseband_api.cpp @@ -261,6 +262,7 @@ set(CPPSRC ui/ui_textentry.cpp ui/ui_tone_key.cpp ui/ui_transmitter.cpp + ui/ui_bmpview.cpp apps/acars_app.cpp apps/ais_app.cpp apps/analog_audio_app.cpp @@ -283,6 +285,7 @@ set(CPPSRC apps/ui_aprs_rx.cpp apps/ui_aprs_tx.cpp apps/ui_bht_tx.cpp + apps/ui_bmp_file_viewer.cpp apps/ui_btle_rx.cpp # apps/ui_coasterp.cpp apps/ui_debug.cpp diff --git a/firmware/application/apps/ui_bmp_file_viewer.cpp b/firmware/application/apps/ui_bmp_file_viewer.cpp new file mode 100644 index 00000000..2f20c819 --- /dev/null +++ b/firmware/application/apps/ui_bmp_file_viewer.cpp @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2024 HTotoo + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui_bmp_file_viewer.hpp" + +extern ui::SystemView* system_view_ptr; + +using namespace portapack; +namespace fs = std::filesystem; + +namespace ui { + +BMPFileViewer::BMPFileViewer( + NavigationView& nav, + const std::filesystem::path& path) + : nav_{nav}, + path_{path} { + add_children( + {&bmp}); + bmp.set_enter_pass(true); // pass the enter key to me, so i can exit. this will disable zoom + pos reset + set_focusable(true); + system_view_ptr->set_app_fullscreen(true); +} + +BMPFileViewer::~BMPFileViewer() { + system_view_ptr->set_app_fullscreen(false); +} + +void BMPFileViewer::focus() { + bmp.focus(); +} + +bool BMPFileViewer::on_key(KeyEvent k) { + if (k == KeyEvent::Select) { + nav_.pop(); + return true; + } + return false; +} + +void BMPFileViewer::paint(Painter&) { + bmp.load_bmp(path_); +} + +} // namespace ui diff --git a/firmware/application/apps/ui_bmp_file_viewer.hpp b/firmware/application/apps/ui_bmp_file_viewer.hpp new file mode 100644 index 00000000..5f991470 --- /dev/null +++ b/firmware/application/apps/ui_bmp_file_viewer.hpp @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 HTotoo + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __UI_BMP_FILE_VIEWER_H__ +#define __UI_BMP_FILE_VIEWER_H__ + +#include "ui.hpp" +#include "ui_navigation.hpp" +#include "ui_painter.hpp" +#include "ui_styles.hpp" +#include "ui_widget.hpp" +#include "file.hpp" +#include "ui_bmpview.hpp" + +namespace ui { + +class BMPFileViewer : public View { + public: + BMPFileViewer(NavigationView& nav, const std::filesystem::path& path); + ~BMPFileViewer(); + bool on_key(KeyEvent key) override; + void paint(Painter& painter) override; + void focus() override; + + private: + NavigationView& nav_; + std::filesystem::path path_{}; + BMPViewer bmp{{0, 0, 240, 320}}; +}; + +} // namespace ui + +#endif // __UI_BMP_FILE_VIEWER_H__ diff --git a/firmware/application/apps/ui_fileman.cpp b/firmware/application/apps/ui_fileman.cpp index 2ad716ad..a03a614c 100644 --- a/firmware/application/apps/ui_fileman.cpp +++ b/firmware/application/apps/ui_fileman.cpp @@ -29,11 +29,13 @@ #include "ui_playlist.hpp" #include "ui_remote.hpp" #include "ui_ss_viewer.hpp" +#include "ui_bmp_file_viewer.hpp" #include "ui_text_editor.hpp" #include "ui_iq_trim.hpp" #include "string_format.hpp" #include "portapack.hpp" #include "event_m0.hpp" +#include "file_path.hpp" using namespace portapack; namespace fs = std::filesystem; @@ -694,7 +696,12 @@ bool FileManagerView::handle_file_open() { nav_.push(path); return true; } else if (path_iequal(bmp_ext, ext)) { - nav_.push(path); + if (path_iequal(current_path, u"/" + splash_dir)) { + nav_.push(path); // splash, so load that viewer + } else { + nav_.push(path); // any other bmp + } + reload_current(false); return true; } else if (path_iequal(rem_ext, ext)) { diff --git a/firmware/application/file.cpp b/firmware/application/file.cpp index 0964b0da..dae33f01 100644 --- a/firmware/application/file.cpp +++ b/firmware/application/file.cpp @@ -70,6 +70,10 @@ File::~File() { f_close(&f); } +void File::close() { + f_close(&f); +} + File::Result File::read(void* data, Size bytes_to_read) { UINT bytes_read = 0; const auto result = f_read(&f, data, bytes_to_read, &bytes_read); @@ -98,6 +102,10 @@ File::Offset File::tell() const { return f_tell(&f); } +File::Result File::eof() { + return f_eof(&f); +} + File::Result File::seek(Offset new_position) { /* NOTE: Returns *old* position, not new position */ const auto old_position = tell(); diff --git a/firmware/application/file.hpp b/firmware/application/file.hpp index ae602f48..3a9e1d52 100644 --- a/firmware/application/file.hpp +++ b/firmware/application/file.hpp @@ -332,6 +332,7 @@ class File { // TODO: Return Result<>. Optional open(const std::filesystem::path& filename, bool read_only = true, bool create = false); + void close(); Optional append(const std::filesystem::path& filename); Optional create(const std::filesystem::path& filename); @@ -342,6 +343,7 @@ class File { Result seek(uint64_t Offset); Result truncate(); Size size() const; + Result eof(); template Result write(const std::array& data) { diff --git a/firmware/application/string_format.cpp b/firmware/application/string_format.cpp index 381cb75f..764b7f7e 100644 --- a/firmware/application/string_format.cpp +++ b/firmware/application/string_format.cpp @@ -188,7 +188,7 @@ std::string to_string_decimal_padding(float decimal, int8_t precision, const int result = to_string_dec_int(integer_part) + "." + to_string_dec_uint(fractional_part, precision, '0'); // Add padding with spaces to meet the length requirement - if (result.length() < l) { + if (result.length() < (uint32_t)l) { int padding_length = l - result.length(); std::string padding(padding_length, ' '); result = padding + result; diff --git a/firmware/application/ui/ui_bmpview.cpp b/firmware/application/ui/ui_bmpview.cpp new file mode 100644 index 00000000..1d1fdd7c --- /dev/null +++ b/firmware/application/ui/ui_bmpview.cpp @@ -0,0 +1,223 @@ +#include "ui_bmpview.hpp" +#include "usb_serial_asyncmsg.hpp" +#include "portapack.hpp" + +bool BMPViewer::load_bmp(const std::filesystem::path& file) { + if (!bmp.open(file, true)) return false; + // calc default zoom level to fit screen, and min / max zoom too + auto rect = screen_rect(); + auto d_height = rect.height(); + auto d_width = rect.width(); + auto b_width = bmp.get_width(); + auto b_height = bmp.get_real_height(); + // aspects + // if image is smaller then our vp + auto x_w = d_width / b_width; + auto x_h = d_height / b_height; + if (x_w < 1 && x_h < 1) { + // not zoom in, but zoom out + x_w = b_width / d_width; + x_h = b_height / d_height; + x_w = (127 < x_w) ? 127 : x_w; + x_h = (127 < x_h) ? 127 : x_h; + zoom_fit = (x_h > x_w) ? -1 * x_h : -1 * x_w; + } else { + x_w = (127 < x_w) ? 127 : x_w; + x_h = (127 < x_h) ? 127 : x_h; + zoom_fit = (x_h > x_w) ? x_h : x_w; + } + if (zoom_fit > max_zoom) zoom_fit = max_zoom; + min_zoom = zoom_fit - 3; + + reset_pos(); + return true; +} + +bool BMPViewer::move_pos(int32_t delta_x, int32_t delta_y) { + if (!bmp.is_loaded()) return false; + + auto rect = screen_rect(); + auto d_height = rect.height(); + auto d_width = rect.width(); + auto ocx = cx; // save old pos + auto ocy = cy; + // top left protection + if (delta_x < 0 && cx <= (uint32_t)(-1 * delta_x)) + cx = 0; + else + cx += delta_x; + + if (delta_y < 0 && cy <= (uint32_t)(-1 * delta_y)) + cy = 0; + else + cy += delta_y; + // right bottom protection + float zt = zoom < 0 ? -1.0f / (float)zoom : (float)zoom; + if (zt == 0) zt = 1; + if (cy + (uint32_t)(d_height / zt) > bmp.get_real_height()) { + cy = (bmp.get_real_height() < (uint32_t)(d_height / zt)) ? 0 : bmp.get_real_height() - (uint32_t)(d_height / zt); + } + if (cx + (uint32_t)(d_width / zt) > bmp.get_width()) { + cx = (bmp.get_width() < (uint32_t)(d_width / zt)) ? 0 : bmp.get_width() - (uint32_t)(d_width / zt); + } + bool ret = !(cx == ocx && ocy == cy); // was any change? + if (ret) set_dirty(); + return ret; +} + +void BMPViewer::set_zoom(int8_t new_zoom) { + if (!bmp.is_loaded()) return; + if (new_zoom > max_zoom) new_zoom = max_zoom; + if (new_zoom < min_zoom) new_zoom = min_zoom; + if (new_zoom == 0) new_zoom = 1; + if (new_zoom == -1) new_zoom = 1; + zoom = new_zoom; + auto rect = screen_rect(); + auto d_height = rect.height(); + auto d_width = rect.width(); + if (zoom < 0) { + mvx = d_width / 3 * (-1.0 * zoom); + mvy = d_height / 3 * (-1.0 * zoom); + } else { + mvx = d_width / zoom / 3; + mvy = d_height / zoom / 3; + } + move_pos(0, 0); // fix based on zoom, without real move (if not edge case) + set_dirty(); +} + +// reads a lint from the bmp's bx, by coordinate to the line that's size is cnt. according to zoom +void BMPViewer::get_line(ui::Color* line, uint32_t bx, uint32_t by, uint32_t cnt) { + if (!bmp.is_loaded()) return; + uint32_t last_targetx = 65534; + for (uint32_t x = 0; x < cnt; x++) { + uint32_t targetx = (zoom < 0) ? bx + x * -1 * zoom : bx + x / zoom; // on zoom out could probably avg the pixels, or apply some smoothing, but this is way faster. + if (last_targetx == targetx) { + line[x] = line[x - 1]; + continue; + } + last_targetx = targetx; + if (!bmp.seek(targetx, by)) { + line[x] = Color::white(); // can't seek there + } else { + bmp.read_next_px(line[x], false); + } + } +} + +void BMPViewer::paint(Painter& painter) { + if (!bmp.is_loaded()) { + painter.draw_string({48, 24}, ui::Styles::white, "Can't load BMP"); + return; + } + // get where i can paint + auto rect = screen_rect(); + auto d_height = rect.height(); + auto d_width = rect.width(); + + uint32_t by = cy; // we start to read from there + uint32_t last_by = 65534; + ui::Color* line = new ui::Color[d_width]; + for (int32_t y = 0; y < d_height; y++) { + by = cy + ((zoom < 0) ? y * -1 * zoom : y / (int32_t)zoom); + if (by != last_by) get_line(line, cx, by, d_width); + last_by = by; + portapack::display.draw_pixels({rect.left(), rect.top() + y, d_width, 1}, line, d_width); + } + delete line; +} + +int8_t BMPViewer::get_zoom() { + return zoom; +} + +BMPViewer::BMPViewer(Rect parent_rect) + : Widget{parent_rect} { + set_focusable(true); +} + +BMPViewer::BMPViewer(Rect parent_rect, const std::filesystem::path& file) + : Widget{parent_rect} { + set_focusable(true); + load_bmp(file); +} + +void BMPViewer::on_focus() { + set_highlighted(true); +} + +void BMPViewer::on_blur() { + set_dirty(); +} + +void BMPViewer::reset_pos() { + if (!bmp.is_loaded()) return; + cx = 0; + cy = 0; + set_zoom(zoom_fit); + set_dirty(); +} + +bool BMPViewer::on_key(const KeyEvent key) { + if (!bmp.is_loaded()) return false; + if (key == KeyEvent::Up) { + return move_pos(0, -1 * mvy); + } + if (key == KeyEvent::Down) { + return move_pos(0, mvy); + } + if (key == KeyEvent::Left) { + return move_pos(-1 * mvx, 0); + } + if (key == KeyEvent::Right) { + return move_pos(mvx, 0); + } + if (key == KeyEvent::Select) { + if (!enter_pass) { + reset_pos(); + return true; + } + } + return false; +} + +bool BMPViewer::on_encoder(EncoderEvent delta) { + if (!bmp.is_loaded()) return false; + if (delta > 0) { + set_zoom(zoom + delta); // 0 handled in set_zoom + return true; + } + if (delta < 0) { + if (zoom == 1) // not 0, but -1 + set_zoom(-2); + else + set_zoom(zoom + delta); // decrease + return true; + } + return false; +} + +// sets if the enter key should be passed to parent or handled. true = pass it, false = handle as reset pos+zoom +void BMPViewer::set_enter_pass(bool pass) { + enter_pass = pass; +} + +bool BMPViewer::get_enter_pass() { + return enter_pass; +} + +bool BMPViewer::on_keyboard(const KeyboardEvent event) { + if (!bmp.is_loaded()) return false; + if (event == '+') { + set_zoom(zoom + 1); + return true; + } + if (event == '-') { + if (zoom == 1) // not 0, but -1 + set_zoom(-2); + else + set_zoom(zoom - 1); // decrease + return true; + } + return false; +} \ No newline at end of file diff --git a/firmware/application/ui/ui_bmpview.hpp b/firmware/application/ui/ui_bmpview.hpp new file mode 100644 index 00000000..ec4a349d --- /dev/null +++ b/firmware/application/ui/ui_bmpview.hpp @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2024 HTotoo + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __UIBMPVIEW_H__ +#define __UIBMPVIEW_H__ + +#include "ui.hpp" +#include "ui_widget.hpp" +#include "bmpfile.hpp" +#include "ui_styles.hpp" + +class BMPViewer : public Widget { + public: + BMPViewer(Rect parent_rect); + BMPViewer(Rect parent_rect, const std::filesystem::path& file); + BMPViewer(const BMPViewer& other) = delete; + BMPViewer& operator=(const BMPViewer& other) = delete; + + bool load_bmp(const std::filesystem::path& file); + + void paint(Painter& painter) override; + void on_focus() override; + void on_blur() override; + bool on_key(const KeyEvent key) override; + bool on_encoder(EncoderEvent delta) override; + bool on_keyboard(const KeyboardEvent event) override; + + void reset_pos(); + void set_zoom(int8_t new_zoom); + int8_t get_zoom(); + + void set_enter_pass(bool pass); + bool get_enter_pass(); + + private: + void get_line(ui::Color* line, uint32_t bx, uint32_t by, uint32_t cnt); + bool move_pos(int32_t delta_x, int32_t delta_y); + BMPFile bmp{}; + int8_t zoom = 1; // positive = zoom in, negative = zoom out 0-invalid 1- no zoom + int8_t zoom_fit = 1; // if this value is set, the image will fit the screen the most + int8_t max_zoom = 10; + int8_t min_zoom = -20; // will be calculated on load + uint32_t cx = 0; // current top-left coordinate + uint32_t cy = 0; + uint32_t mvx = 1; // how much to move on key + uint32_t mvy = 1; + bool enter_pass = false; +}; + +#endif \ No newline at end of file diff --git a/firmware/application/ui_navigation.cpp b/firmware/application/ui_navigation.cpp index 2f36d98d..2773e500 100644 --- a/firmware/application/ui_navigation.cpp +++ b/firmware/application/ui_navigation.cpp @@ -945,6 +945,15 @@ void SystemView::paint_overlay() { } } +void SystemView::set_app_fullscreen(bool fullscreen) { + auto parent_rect = screen_rect(); + Dim status_view_height = (fullscreen) ? 0 : 16; + status_view.hidden(fullscreen); + navigation_view.set_parent_rect( + {{0, status_view_height}, + {parent_rect.width(), static_cast(parent_rect.height() - status_view_height)}}); +} + /* ***********************************************************************/ void BMPView::focus() { diff --git a/firmware/application/ui_navigation.hpp b/firmware/application/ui_navigation.hpp index 801e2a35..0340da11 100644 --- a/firmware/application/ui_navigation.hpp +++ b/firmware/application/ui_navigation.hpp @@ -377,6 +377,7 @@ class SystemView : public View { Context& context() const override; void toggle_overlay(); void paint_overlay(); + void set_app_fullscreen(bool fullscreen); NavigationView* get_navigation_view(); SystemStatusView* get_status_view(); diff --git a/firmware/application/usb_serial_asyncmsg.hpp b/firmware/application/usb_serial_asyncmsg.hpp index 17b22f84..14330fe0 100644 --- a/firmware/application/usb_serial_asyncmsg.hpp +++ b/firmware/application/usb_serial_asyncmsg.hpp @@ -120,6 +120,14 @@ void UsbSerialAsyncmsg::asyncmsg(const uint64_t& data) { chprintf((BaseSequentialStream*)&SUSBD1, "%s\r\n", to_string_dec_int(data).c_str()); } +template <> +void UsbSerialAsyncmsg::asyncmsg(const float& data) { + if (!portapack::async_tx_enabled) { + return; + } + chprintf((BaseSequentialStream*)&SUSBD1, "%s\r\n", to_string_decimal(data, 7).c_str()); +} + /// fs things template <> diff --git a/firmware/common/bmp.hpp b/firmware/common/bmp.hpp index 1b6a3fbc..dd032fb6 100644 --- a/firmware/common/bmp.hpp +++ b/firmware/common/bmp.hpp @@ -29,7 +29,7 @@ struct bmp_header_t { uint32_t image_data; uint32_t BIH_size; uint32_t width; - uint32_t height; + int32_t height; // can be negative, to signal the bottom-up or reserve status uint16_t planes; uint16_t bpp; uint32_t compression; diff --git a/firmware/common/bmpfile.cpp b/firmware/common/bmpfile.cpp new file mode 100644 index 00000000..46e74340 --- /dev/null +++ b/firmware/common/bmpfile.cpp @@ -0,0 +1,285 @@ +/* + * Copyright (C) 2024 HTotoo + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "bmpfile.hpp" + +bool BMPFile::is_loaded() { + return is_opened; +} + +// fix height info +uint32_t BMPFile::get_real_height() { + if (!is_opened) return 0; + return bmp_header.height >= 0 ? (uint32_t)bmp_header.height : (uint32_t)(-1 * bmp_header.height); +} + +// get bmp width +uint32_t BMPFile::get_width() { + if (!is_opened) return 0; + return bmp_header.width; +} + +// get if the rows are bottom up (for most bmp), or up to bottom (negative height, we use it for write) +bool BMPFile::is_bottomup() { + return (bmp_header.height >= 0); +} + +BMPFile::~BMPFile() { + close(); +} + +// closes file +void BMPFile::close() { + is_opened = false; + bmpimage.close(); +} + +// creates a new bmp file. for now, hardcoded to 3 byte colour depth +bool BMPFile::create(const std::filesystem::path& file, uint32_t x, uint32_t y) { + is_opened = false; + is_read_ony = true; + bmpimage.close(); // if already open, close before open a new + if (file_exists(file)) { + delete_file(file); // overwrite + } + auto result = bmpimage.open(file, false, true); + if (!result.value().ok()) return false; + file_pos = 0; + byte_per_row = (x * 3 % 4 == 0) ? x * 3 : (x * 3 + (4 - ((x * 3) % 4))); // with padding + bmpimage.seek(file_pos); + bmp_header.signature = 0x4D42; + bmp_header.planes = 1; + bmp_header.compression = 0; + bmp_header.bpp = 24; // 3 byte depth + bmp_header.width = x; + bmp_header.height = 0; // for now, will expand + bmp_header.image_data = 0x36; + bmp_header.BIH_size = 0x28; + bmp_header.h_res = 100; + bmp_header.v_res = 100; + byte_per_px = 3; + type = 1; + bmp_header.size = sizeof(bmp_header) + get_real_height() * byte_per_row; // with padding! --will update later with expand + bmp_header.data_size = bmp_header.size - sizeof(bmp_header_t); + bmp_header.colors_count = 0; + bmp_header.icolors_count = 0; + + bmpimage.write(&bmp_header, sizeof(bmp_header_t)); + file_pos = bmp_header.image_data; + is_opened = true; + is_read_ony = false; + if (!expand_y(y)) return false; // will fill with 0, and update header data + seek(0, 0); + return true; +} + +// opens the file and parses header data. return true on success +bool BMPFile::open(const std::filesystem::path& file, bool readonly) { + is_opened = false; + is_read_ony = true; + bmpimage.close(); // if already open, close before open a new + + auto result = bmpimage.open(file, readonly, false); + if (!result.value().ok()) return false; + file_pos = 0; + bmpimage.seek(file_pos); + auto read_size = bmpimage.read(&bmp_header, sizeof(bmp_header_t)); + if (!((bmp_header.signature == 0x4D42) && // "BM" Signature + (bmp_header.planes == 1) && // Seems always to be 1 + (bmp_header.compression == 0 || bmp_header.compression == 3))) { // No compression + return false; + } + char buffer[257]; + switch (bmp_header.bpp) { + case 16: + file_pos = 0x36; + memset(buffer, 0, 16); + bmpimage.read(buffer, 16); + byte_per_px = 2; + if (buffer[1] == 0x7C) + type = 3; // A1R5G5B5 + else + type = 0; // R5G6B5 + break; + case 24: + type = 1; + byte_per_px = 3; + break; + case 32: + type = 2; + byte_per_px = 4; + break; + default: + // not supported + return false; + break; + } + byte_per_row = (bmp_header.width * byte_per_px % 4 == 0) ? bmp_header.width * byte_per_px : (bmp_header.width * byte_per_px + (4 - ((bmp_header.width * byte_per_px) % 4))); + file_pos = bmp_header.image_data; + is_opened = true; + is_read_ony = readonly; + currx = 0; + curry = 0; + return true; +} + +// jumps to next pixel. false on the end +bool BMPFile::advance_curr_px(uint32_t num = 1) { + if (curry >= get_real_height()) return false; + uint32_t rowsToAdvance = (currx + num) / bmp_header.width; + uint32_t nx = (currx + num) % bmp_header.width; + uint32_t ny = curry + rowsToAdvance; + if (ny >= get_real_height()) { + return false; + } + seek(nx, ny); + return true; +} + +// reads next px, then advance the pos (and seek). return false on error +bool BMPFile::read_next_px(ui::Color& px, bool seek = true) { + if (!is_opened) return false; + uint8_t buffer[4]; + auto res = bmpimage.read(buffer, byte_per_px); + if (res.is_error()) return false; + switch (type) { + case 0: // R5G6B5 + case 3: // A1R5G5B5 + if (!type) + px = ui::Color((uint16_t)buffer[0] | ((uint16_t)buffer[1] << 8)); + else + px = ui::Color(((uint16_t)buffer[0] & 0x1F) | ((uint16_t)buffer[0] & 0xE0) << 1 | ((uint16_t)buffer[1] & 0x7F) << 9); + break; + case 1: // 24 + default: + px = ui::Color(buffer[2], buffer[1], buffer[0]); + + break; + case 2: // 32 + px = ui::Color(buffer[2], buffer[1], buffer[0]); + break; + } + if (seek) advance_curr_px(); + return true; +} + +// if you set this, then the expanded part (or the newly created) will be filled with this color. but the expansion or the creation will be slower. +void BMPFile::set_bg_color(ui::Color background) { + bg = background; + use_bg = true; +} + +// delete bg color. default. creation or expansion will be fast, but the file will contain random garbage. no problem if you write all pixels later. +void BMPFile::delete_db_color() { + use_bg = false; +} + +// writes a color data to the current position, and advances 1 px. true on success, false on error +bool BMPFile::write_next_px(ui::Color& px) { + if (!is_opened) return false; + if (is_read_ony) return false; + uint8_t buffer[4]; + switch (type) { + case 0: // R5G6B5 + case 3: // A1R5G5B5 + if (!type) { + buffer[0] = (px.r() << 3) | (px.g() >> 3); // todo test in future + buffer[1] = (px.g() << 5) | px.b(); + } else { + buffer[0] = (1 << 7) | (px.r() << 2) | (px.g() >> 3); // todo test in future + buffer[1] = (px.g() << 5) | px.b(); + } + break; + case 1: // 24 + default: + buffer[2] = px.r(); + buffer[1] = px.g(); + buffer[0] = px.b(); + break; + case 2: // 32 + buffer[2] = px.r(); + buffer[1] = px.g(); + buffer[0] = px.b(); + buffer[3] = 255; + break; + } + auto res = bmpimage.write(buffer, byte_per_px); + if (res.is_error()) return false; + advance_curr_px(); + return true; +} + +// positions in the file to the given pixel. 0 based indexing +bool BMPFile::seek(uint32_t x, uint32_t y) { + if (!is_opened) return false; + if (x >= bmp_header.width) return false; + if (y >= get_real_height()) return false; + if (!BMPFile::is_bottomup()) { + file_pos = bmp_header.image_data; // nav to start pos. + file_pos += y * byte_per_row; + file_pos += x * byte_per_px; + bmpimage.seek(file_pos); + currx = x; + curry = y; + } else { + file_pos = bmp_header.image_data; // nav to start pos. + file_pos += (get_real_height() - y - 1) * byte_per_row; + file_pos += x * byte_per_px; + bmpimage.seek(file_pos); + currx = x; + curry = y; + } + return true; +} + +// expands the image with a delta (y). also seek's t it's begining. in bottumup format, it should be used carefully! +bool BMPFile::expand_y_delta(uint32_t delta_y) { + return expand_y(get_real_height() + delta_y); +} + +// expands the image to a new y size. also seek's t it's begining. in bottumup format, it should be used carefully! +bool BMPFile::expand_y(uint32_t new_y) { + if (!is_opened) return false; // not yet opened + uint32_t old_height = get_real_height(); + if (new_y < old_height) return true; // already bigger + if (is_read_ony) return false; // can't expand + uint32_t delta = (new_y - old_height) * byte_per_row; + bmp_header.size += delta; + bmp_header.data_size += delta; + bmp_header.height = -1 * new_y; //-1*, so no bottom-up structure needed. easier to expand. + bmpimage.seek(0); + bmpimage.write(&bmp_header, sizeof(bmp_header)); // overwrite header + bmpimage.seek(bmp_header.size); // seek to new end to expand + // fill with bg color if needed + if (use_bg) { + seek(0, old_height); // to the new begin + size_t newpxcount = ((new_y - old_height) * bmp_header.width); + for (size_t i = 0; i < newpxcount; ++i) + write_next_px(bg); + } + + if (is_bottomup()) { + seek(0, new_y - old_height); // seek to the new chunk begin + } else { + seek(0, curry + 1); // seek to the begin of the new chunk + } + return true; +} \ No newline at end of file diff --git a/firmware/common/bmpfile.hpp b/firmware/common/bmpfile.hpp new file mode 100644 index 00000000..efaf1381 --- /dev/null +++ b/firmware/common/bmpfile.hpp @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2024 HTotoo + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __BMPFILE__H +#define __BMPFILE__H + +#include +#include + +#include "file.hpp" +#include "bmp.hpp" +#include "ui.hpp" + +class BMPFile { + public: + ~BMPFile(); + bool open(const std::filesystem::path& file, bool readonly); + bool create(const std::filesystem::path& file, uint32_t x, uint32_t y); + void close(); + bool is_loaded(); + bool seek(uint32_t x, uint32_t y); + bool expand_y(uint32_t new_y); + bool expand_y_delta(uint32_t delta_y); + uint32_t getbpr() { return byte_per_row; }; + + bool read_next_px(ui::Color& px, bool seek); + bool write_next_px(ui::Color& px); + uint32_t get_real_height(); + uint32_t get_width(); + bool is_bottomup(); + void set_bg_color(ui::Color background); + void delete_db_color(); + + private: + bool advance_curr_px(uint32_t num); + bool is_opened = false; + bool is_read_ony = true; + + File bmpimage{}; + size_t file_pos = 0; + bmp_header_t bmp_header{}; + uint8_t type = 0; + uint8_t byte_per_px = 1; + uint32_t byte_per_row = 0; + + uint32_t currx = 0; + uint32_t curry = 0; + ui::Color bg{}; + bool use_bg = false; +}; + +#endif \ No newline at end of file