From f1056ff9d1064e3beb4b268a79ec1ee37039bddb Mon Sep 17 00:00:00 2001 From: "Emilien Huet (Malt)" Date: Mon, 22 Dec 2025 06:37:20 -0800 Subject: [PATCH] Grid Browser with file thumbnails --- lib/GfxRenderer/GfxRenderer.cpp | 51 +++++ lib/GfxRenderer/GfxRenderer.h | 1 + src/activities/home/GridBrowserActivity.cpp | 237 ++++++++++++++++++++ src/activities/home/GridBrowserActivity.h | 51 +++++ src/activities/reader/ReaderActivity.cpp | 5 +- src/images/FolderIcon.h | 58 +++++ 6 files changed, 401 insertions(+), 2 deletions(-) create mode 100644 src/activities/home/GridBrowserActivity.cpp create mode 100644 src/activities/home/GridBrowserActivity.h create mode 100644 src/images/FolderIcon.h diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index a4b9369..d70f558 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -80,6 +80,57 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha } } +void GfxRenderer::drawTextInBox(const int fontId, const int x, const int y, const int w, const int h, const char* text, const bool black, const EpdFontStyle style) const { + const int lineHeight = getLineHeight(fontId); + const int spaceWidth = getSpaceWidth(fontId); + int xpos = x; + int ypos = y + lineHeight; + + // cannot draw a NULL / empty string + if (text == nullptr || *text == '\0') { + return; + } + + if (fontMap.count(fontId) == 0) { + Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId); + return; + } + const auto font = fontMap.at(fontId); + + // no printable characters + if (!font.hasPrintableChars(text, style)) { + return; + } + + uint32_t cp; + while ((cp = utf8NextCodepoint(reinterpret_cast(&text)))) { + // Handle space character for word wrapping + if (cp == ' ') { + if (xpos + spaceWidth > x + w) { + xpos = x; + ypos += lineHeight; + if (h > 0 && ypos - y > h) { + break; // Exceeded box height + } + } else { + xpos += spaceWidth; + } + continue; + } + + const int charWidth = getTextWidth(fontId, reinterpret_cast(&cp), style); + if (xpos + charWidth > x + w) { + xpos = x; + ypos += lineHeight; + if (h > 0 && ypos - y > h) { + break; // Exceeded box height + } + } + + renderChar(font, cp, &xpos, &ypos, black, style); + } +} + void GfxRenderer::drawLine(int x1, int y1, int x2, int y2, const bool state) const { if (x1 == x2) { if (y2 < y1) { diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index 838e018..1e1d988 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -54,6 +54,7 @@ class GfxRenderer { int getTextWidth(int fontId, const char* text, EpdFontStyle style = REGULAR) const; void drawCenteredText(int fontId, int y, const char* text, bool black = true, EpdFontStyle style = REGULAR) const; void drawText(int fontId, int x, int y, const char* text, bool black = true, EpdFontStyle style = REGULAR) const; + void drawTextInBox(int fontId, int x, int y, int w, int h, const char* text, bool black = true, EpdFontStyle style = REGULAR) const; int getSpaceWidth(int fontId) const; int getLineHeight(int fontId) const; diff --git a/src/activities/home/GridBrowserActivity.cpp b/src/activities/home/GridBrowserActivity.cpp new file mode 100644 index 0000000..79d24e2 --- /dev/null +++ b/src/activities/home/GridBrowserActivity.cpp @@ -0,0 +1,237 @@ +#include "GridBrowserActivity.h" + +#include +#include + +#include "config.h" +#include "../../images/FolderIcon.h" + +namespace { +constexpr int PAGE_ITEMS = 12; +constexpr int SKIP_PAGE_MS = 700; +constexpr int TILE_W = 135; +constexpr int TILE_H = 200; +constexpr int TILE_PADDING = 5; +constexpr int THUMB_W = 90; +constexpr int THUMB_H = 120; +constexpr int TILE_TEXT_H = 60; +} // namespace + +inline int min(const int a, const int b) { return a < b ? a : b; } + +void GridBrowserActivity::sortFileList(std::vector& strs) { + std::sort(begin(strs), end(strs), [](const FileInfo& f1, const FileInfo& f2) { + if (f1.type == F_DIRECTORY && f2.type != F_DIRECTORY) return true; + if (f1.type != F_DIRECTORY && f2.type == F_DIRECTORY) return false; + return lexicographical_compare( + begin(f1.name), end(f1.name), begin(f2.name), end(f2.name), + [](const char& char1, const char& char2) { return tolower(char1) < tolower(char2); }); + }); +} + +void GridBrowserActivity::taskTrampoline(void* param) { + auto* self = static_cast(param); + self->displayTaskLoop(); +} + +void GridBrowserActivity::loadFiles() { + files.clear(); + selectorIndex = 0; + auto root = SD.open(basepath.c_str()); + for (File file = root.openNextFile(); file; file = root.openNextFile()) { + const std::string filename = std::string(file.name()); + if (filename.empty() || filename[0] == '.') { + file.close(); + continue; + } + + if (file.isDirectory()) { + files.emplace_back(FileInfo{ filename, filename, F_DIRECTORY }); + } else { + FileType type = F_FILE; + size_t dot = filename.find_last_of('.'); + std::string basename = filename; + if (dot != std::string::npos) { + std::string ext = filename.substr(dot); + basename = filename.substr(0, dot); + // lowercase ext for case-insensitive compare + for (char &c : ext) c = (char)tolower(c); + if (ext == ".epub") { + type = F_EPUB; + } else if (ext == ".bmp") { + type = F_BMP; + } + } + if (type != F_FILE) { + files.emplace_back(FileInfo{ filename, basename, type }); + } + } + file.close(); + } + root.close(); + Serial.printf("Files loaded\n"); + GridBrowserActivity::sortFileList(files); + Serial.printf("Files sorted\n"); +} + +void GridBrowserActivity::onEnter() { + Serial.printf("Enter grid\n"); + renderingMutex = xSemaphoreCreateMutex(); + + basepath = "/Dev/Thumbs"; + loadFiles(); + selectorIndex = 0; + + // Trigger first update + updateRequired = true; + + xTaskCreate(&GridBrowserActivity::taskTrampoline, "GridFileBrowserTask", + 8192, // Stack size + this, // Parameters + 1, // Priority + &displayTaskHandle // Task handle + ); +} + +void GridBrowserActivity::onExit() { + Activity::onExit(); + + // Wait until not rendering to delete task to avoid killing mid-instruction to EPD + xSemaphoreTake(renderingMutex, portMAX_DELAY); + if (displayTaskHandle) { + vTaskDelete(displayTaskHandle); + displayTaskHandle = nullptr; + } + vSemaphoreDelete(renderingMutex); + renderingMutex = nullptr; + files.clear(); +} + +void GridBrowserActivity::loop() { + const bool prevReleased = inputManager.wasReleased(InputManager::BTN_UP) || inputManager.wasReleased(InputManager::BTN_LEFT); + const bool nextReleased = inputManager.wasReleased(InputManager::BTN_DOWN) || inputManager.wasReleased(InputManager::BTN_RIGHT); + const bool skipPage = inputManager.getHeldTime() > SKIP_PAGE_MS; + + if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) { + if (files.empty()) { + return; + } + + if (basepath.back() != '/') { + basepath += "/"; + } + if (files[selectorIndex].type == F_DIRECTORY) { + // open subfolder + basepath += files[selectorIndex].name; + loadFiles(); + updateRequired = true; + } else { + onSelect(basepath + files[selectorIndex].name); + } + } else if (inputManager.wasPressed(InputManager::BTN_BACK)) { + if (basepath != "/") { + basepath = basepath.substr(0, basepath.rfind('/')); + if (basepath.empty()) basepath = "/"; + loadFiles(); + updateRequired = true; + } else { + // At root level, go back home + onGoHome(); + } + } else if (prevReleased) { + if (skipPage) { + selectorIndex = ((selectorIndex / PAGE_ITEMS - 1) * PAGE_ITEMS + files.size()) % files.size(); + } else { + selectorIndex = (selectorIndex + files.size() - 1) % files.size(); + } + updateRequired = true; + } else if (nextReleased) { + if (skipPage) { + selectorIndex = ((selectorIndex / PAGE_ITEMS + 1) * PAGE_ITEMS) % files.size(); + } else { + selectorIndex = (selectorIndex + 1) % files.size(); + } + updateRequired = true; + } +} + +void GridBrowserActivity::displayTaskLoop() { + while (true) { + if (updateRequired) { + updateRequired = false; + xSemaphoreTake(renderingMutex, portMAX_DELAY); + render(); + xSemaphoreGive(renderingMutex); + } + vTaskDelay(10 / portTICK_PERIOD_MS); + } +} + +void GridBrowserActivity::render() const { + const auto pageWidth = renderer.getScreenWidth(); + const auto pageHeight = renderer.getScreenHeight(); + renderer.clearScreen(); + bool hasGeyscaleBitmaps = false; + + if (!files.empty()) { + for (int pass = 0; pass < 3; pass++) { + if (pass > 0) { + renderer.clearScreen(0x00); + if (pass == 1) { + renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB); + } else if (pass == 2) { + renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB); + } + } + + const int16_t iconOffsetX = (TILE_W - FOLDERICON_WIDTH) / 2; + const int16_t iconOffsetY = (TILE_H - TILE_TEXT_H - FOLDERICON_HEIGHT) / 2; + const int16_t thumbOffsetX = (TILE_W - THUMB_W) / 2; + const int16_t thumbOffsetY = (TILE_H - TILE_TEXT_H - THUMB_H) / 2; + for (size_t i = 0; i < min(PAGE_ITEMS, files.size()); i++) { + const auto file = files[i]; + + const int16_t tileX = 45 + i % 3 * TILE_W; + const int16_t tileY = 115 + i / 3 * TILE_H; + + if (pass == 0) { + Serial.printf("Rendering file %s at (%d, %d)\n", file.name.c_str(), tileX, tileY); + if (file.type == F_DIRECTORY) { + renderer.drawImage(FolderIcon, tileX + iconOffsetX, tileY + iconOffsetY, FOLDERICON_WIDTH, FOLDERICON_HEIGHT); + } + } + + if (file.type == F_BMP) { + File bmpFile = SD.open((basepath + "/" + file.name).c_str()); + if (bmpFile) { + Bitmap bitmap(bmpFile); + if (bitmap.parseHeaders() == BmpReaderError::Ok) { + if (bitmap.hasGreyscale()) { + hasGeyscaleBitmaps = true; + } + renderer.drawBitmap(bitmap, tileX + thumbOffsetX, tileY + thumbOffsetY, THUMB_W, THUMB_H); + } + } + } + + if (pass == 0) { + renderer.drawTextInBox(UI_FONT_ID, tileX + TILE_PADDING, tileY + TILE_H - TILE_TEXT_H, TILE_W - 2 * TILE_PADDING, TILE_TEXT_H, file.basename.c_str(), 1); // i != selectorIndex + } + } + + if (pass == 0) { + renderer.displayBuffer(EInkDisplay::HALF_REFRESH); + if (!hasGeyscaleBitmaps) { + // we can skip grayscale passes if no bitmaps use it + break; + } + } else if (pass == 1) { + renderer.copyGrayscaleLsbBuffers(); + } else if (pass == 2) { + renderer.copyGrayscaleMsbBuffers(); + renderer.displayGrayBuffer(); + renderer.setRenderMode(GfxRenderer::BW); + } + } + } +} \ No newline at end of file diff --git a/src/activities/home/GridBrowserActivity.h b/src/activities/home/GridBrowserActivity.h new file mode 100644 index 0000000..762eda2 --- /dev/null +++ b/src/activities/home/GridBrowserActivity.h @@ -0,0 +1,51 @@ +#pragma once +#include +#include +#include + +#include +#include +#include + +#include "../Activity.h" + +enum FileType { + F_DIRECTORY = 0, + F_EPUB, + F_TXT, + F_BMP, + F_FILE +}; + +struct FileInfo { + std::string name; + std::string basename; + FileType type; +}; + +class GridBrowserActivity final : public Activity { + TaskHandle_t displayTaskHandle = nullptr; + SemaphoreHandle_t renderingMutex = nullptr; + std::string basepath = "/"; + std::vector files; + int selectorIndex = 0; + bool updateRequired = false; + const std::function onSelect; + const std::function onGoHome; + + static void taskTrampoline(void* param); + [[noreturn]] void displayTaskLoop(); + void render() const; + void loadFiles(); + + public: + explicit GridBrowserActivity(GfxRenderer& renderer, InputManager& inputManager, + const std::function& onSelect, + const std::function& onGoHome) + : Activity("FileSelection", renderer, inputManager), onSelect(onSelect), onGoHome(onGoHome) {} + void onEnter() override; + void onExit() override; + void loop() override; + private: + static void sortFileList(std::vector& strs); +}; diff --git a/src/activities/reader/ReaderActivity.cpp b/src/activities/reader/ReaderActivity.cpp index d888fb6..5203213 100644 --- a/src/activities/reader/ReaderActivity.cpp +++ b/src/activities/reader/ReaderActivity.cpp @@ -4,7 +4,8 @@ #include "Epub.h" #include "EpubReaderActivity.h" -#include "FileSelectionActivity.h" +// #include "FileSelectionActivity.h" +#include "../home/GridBrowserActivity.h" #include "activities/util/FullScreenMessageActivity.h" std::unique_ptr ReaderActivity::loadEpub(const std::string& path) { @@ -40,7 +41,7 @@ void ReaderActivity::onSelectEpubFile(const std::string& path) { void ReaderActivity::onGoToFileSelection() { exitActivity(); - enterNewActivity(new FileSelectionActivity( + enterNewActivity(new GridBrowserActivity( renderer, inputManager, [this](const std::string& path) { onSelectEpubFile(path); }, onGoBack)); } diff --git a/src/images/FolderIcon.h b/src/images/FolderIcon.h new file mode 100644 index 0000000..a4e6ef0 --- /dev/null +++ b/src/images/FolderIcon.h @@ -0,0 +1,58 @@ +#pragma once +#include + +#define FOLDERICON_WIDTH 80 +#define FOLDERICON_HEIGHT 80 + +static const uint8_t FolderIcon[] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, + 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, + 0xFF, 0xFF, 0xF8, 0xFF, 0x3F, 0xFF, 0xFF, 0xFE, 0x01, 0xFF, 0xFF, 0xFF, 0xF1, 0xFF, 0x3F, 0xFF, + 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xF3, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, + 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, + 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, + 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, + 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, + 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, + 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, + 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, + 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, + 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, + 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, + 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, + 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, + 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, + 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, + 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, + 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, + 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, + 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, + 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, + 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xCF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, + 0x8F, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0x1F, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, + 0xC0, 0xFF, 0xFF, 0xFE, 0x1F, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xF8, 0x7F, 0xFF, + 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xF0, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, + 0xFF, 0xE3, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xC7, 0xFF, 0xFF, 0x3F, 0xFF, + 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xCF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0x9F, + 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0x9F, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, + 0xC0, 0xFF, 0xFF, 0x9F, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0x9F, 0xFF, 0xFF, + 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0x9F, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, + 0xFF, 0x9F, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0x9F, 0xFF, 0xFF, 0x3F, 0xFF, + 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0x9F, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0x9F, + 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0x9F, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, + 0xC0, 0xFF, 0xFF, 0x9F, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0x9F, 0xFF, 0xFF, + 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0x9F, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, + 0xFF, 0x9F, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0x9F, 0xFF, 0xFF, 0x3F, 0xFF, + 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0x9F, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0x9F, + 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x81, 0xFF, 0xFF, 0x9F, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, + 0x81, 0xFF, 0xFF, 0x8F, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x83, 0xFF, 0xFF, 0xC7, 0xFF, 0xFF, + 0x3F, 0xFF, 0xFF, 0xFF, 0x07, 0xFF, 0xFF, 0xE3, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFE, 0x0F, 0xFF, + 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF +};