Storage: view pictures, PNG, JPEG, BMP and GIF (#45)
CI / build (pull_request) Successful in 1m56s
Site / build (pull_request) Successful in 10s

Enter on a picture shows it: shrunk to fit the screen, or at its own size
with Enter again and the arrows to move. Dithered to the screen's 256
colours; a colour the screen has exactly is left alone, so screenshots are
shown as they are.

The picture is decoded once, straight into the screen's buffer, and kept
there (App::retainsContent): no copy in memory. Decoding runs on the
storage task, so the keys keep working and a 12 megapixel photograph
appears as it comes instead of tripping the watchdog.

PNG, BMP and GIF are read by decoders of our own, host-tested against files
made by Pillow; the PNG one needs 32 KB where the display library's needed
44 KB in one block, which the device often doesn't have. JPEG uses the
library's TJpgDec.

Also corrects two sentences that still gave 16 KB as the editing limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
2026-10-07 21:04:39 +02:00
co-authored by Claude Opus 5.5
parent ae25cf0be2
commit 2c18762614
22 changed files with 2074 additions and 9 deletions
+15 -2
View File
@@ -162,11 +162,15 @@ void FileViewer::open(const std::string& path, uint32_t size) {
file_ = std::make_shared<fs::File>();
std::string name = files::baseName(path);
files::FileKind kind = files::kindOf(name);
if (kind == files::FileKind::Unknown) { // what do its first bytes look like?
files::ImageKind picture = files::imageKindOfName(name);
if (kind == files::FileKind::Unknown && picture == files::ImageKind::None) { // what do its first bytes look like?
uint8_t head[256];
size_t n = readAt(0, head, sizeof head);
picture = files::imageKindOfBytes(head, n);
kind = files::looksLikeText(head, n) ? files::FileKind::Text : files::FileKind::Unknown;
}
std::string notShown;
if (picture != files::ImageKind::None) notShown = image_.open(path, size);
switch (kind) {
case files::FileKind::Text: base_ = Mode::Text; break;
case files::FileKind::Gpx: base_ = Mode::Gpx; break;
@@ -174,11 +178,13 @@ void FileViewer::open(const std::string& path, uint32_t size) {
case files::FileKind::Ota: base_ = Mode::Ota; break;
default: base_ = Mode::Hex; break;
}
if (picture != files::ImageKind::None && notShown.empty()) base_ = Mode::Image;
pager_.reset(new files::TextPager([this](uint32_t offset, uint8_t* into, size_t len) { return readAt(offset, into, len); }, size_,
kCols, kRows));
if (files::opensAtEnd(name)) pager_->toEnd();
hexTop_ = 0;
show(base_);
if (!notShown.empty()) say(notShown); // a picture that can't be shown: its bytes, and why
if (base_ == Mode::Gpx || base_ == Mode::Pcap || base_ == Mode::Ota) startScan(base_);
}
@@ -203,6 +209,7 @@ void FileViewer::close() {
});
}
file_.reset();
image_.close();
pager_.reset();
confirm_.reset();
message_.clear();
@@ -214,11 +221,13 @@ void FileViewer::close() {
void FileViewer::show(Mode mode) {
mode_ = mode;
if (mode == Mode::Image) image_.lost(); // another view was drawn where it was
stale_ = true;
rowsFrom_ = -1;
}
void FileViewer::say(const std::string& text) {
if (mode_ == Mode::Image) return image_.say(text);
message_ = text;
messageMs_ = millis();
}
@@ -275,6 +284,7 @@ void FileViewer::help(std::vector<KeyHelp>& out) const {
case Mode::Packet: keys::add(out, keys::kViewerPacket); break;
case Mode::Gpx: keys::add(out, keys::kViewerGpx); break;
case Mode::Ota: keys::add(out, keys::kViewerOta); break;
case Mode::Image: image_.help(out); break;
}
}
@@ -299,6 +309,7 @@ bool FileViewer::onKey(const KeyEvent& e) {
else show(base_ == Mode::Hex || base_ == Mode::Gpx ? Mode::Text : Mode::Hex);
return true;
}
if (mode_ == Mode::Image) return image_.onKey(e), true;
uint32_t ch = e.key == Key::Char ? e.ch : 0;
switch (mode_) {
case Mode::Text:
@@ -332,7 +343,8 @@ bool FileViewer::onKey(const KeyEvent& e) {
return true;
}
bool FileViewer::update(uint32_t) {
bool FileViewer::update(uint32_t nowMs) {
if (mode_ == Mode::Image) return image_.update(nowMs);
if (!message_.empty() && millis() - messageMs_ >= 4000) {
message_.clear();
return true;
@@ -389,6 +401,7 @@ void FileViewer::refresh() {
}
void FileViewer::draw(Canvas& c) {
if (mode_ == Mode::Image) return image_.draw(c);
const auto& area = theme::kContent;
refresh();
c.setTextDatum(top_left);