mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2025-12-18 23:27:44 +03:00
Merge d60378719c into 5cabba7712
This commit is contained in:
commit
a31532cac2
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
.pio
|
.pio
|
||||||
.idea
|
.idea
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.vscode
|
||||||
|
|||||||
174
lib/BmpToMono/BmpToMono.cpp
Normal file
174
lib/BmpToMono/BmpToMono.cpp
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
#include "BmpToMono.h"
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
uint16_t BmpToMono::readLE16(File& f) {
|
||||||
|
const int c0 = f.read();
|
||||||
|
const int c1 = f.read();
|
||||||
|
const uint8_t b0 = (uint8_t)(c0 < 0 ? 0 : c0);
|
||||||
|
const uint8_t b1 = (uint8_t)(c1 < 0 ? 0 : c1);
|
||||||
|
return (uint16_t)b0 | ((uint16_t)b1 << 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t BmpToMono::readLE32(File& f) {
|
||||||
|
const int c0 = f.read();
|
||||||
|
const int c1 = f.read();
|
||||||
|
const int c2 = f.read();
|
||||||
|
const int c3 = f.read();
|
||||||
|
|
||||||
|
const uint8_t b0 = (uint8_t)(c0 < 0 ? 0 : c0);
|
||||||
|
const uint8_t b1 = (uint8_t)(c1 < 0 ? 0 : c1);
|
||||||
|
const uint8_t b2 = (uint8_t)(c2 < 0 ? 0 : c2);
|
||||||
|
const uint8_t b3 = (uint8_t)(c3 < 0 ? 0 : c3);
|
||||||
|
|
||||||
|
return (uint32_t)b0 | ((uint32_t)b1 << 8) | ((uint32_t)b2 << 16) | ((uint32_t)b3 << 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BmpToMono::freeMonoBitmap(MonoBitmap& bmp) {
|
||||||
|
if (bmp.data) {
|
||||||
|
free(bmp.data);
|
||||||
|
bmp.data = nullptr;
|
||||||
|
}
|
||||||
|
bmp.width = 0;
|
||||||
|
bmp.height = 0;
|
||||||
|
bmp.len = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* BmpToMono::errorToString(BmpToMonoError err) {
|
||||||
|
switch (err) {
|
||||||
|
case BmpToMonoError::Ok:
|
||||||
|
return "Ok";
|
||||||
|
case BmpToMonoError::FileInvalid:
|
||||||
|
return "FileInvalid";
|
||||||
|
case BmpToMonoError::SeekStartFailed:
|
||||||
|
return "SeekStartFailed";
|
||||||
|
case BmpToMonoError::NotBMP:
|
||||||
|
return "NotBMP (missing 'BM')";
|
||||||
|
case BmpToMonoError::DIBTooSmall:
|
||||||
|
return "DIBTooSmall (<40 bytes)";
|
||||||
|
case BmpToMonoError::BadPlanes:
|
||||||
|
return "BadPlanes (!= 1)";
|
||||||
|
case BmpToMonoError::UnsupportedBpp:
|
||||||
|
return "UnsupportedBpp (expected 24)";
|
||||||
|
case BmpToMonoError::UnsupportedCompression:
|
||||||
|
return "UnsupportedCompression (expected BI_RGB)";
|
||||||
|
case BmpToMonoError::BadDimensions:
|
||||||
|
return "BadDimensions";
|
||||||
|
case BmpToMonoError::SeekPixelDataFailed:
|
||||||
|
return "SeekPixelDataFailed";
|
||||||
|
case BmpToMonoError::OomOutput:
|
||||||
|
return "OomOutput";
|
||||||
|
case BmpToMonoError::OomRowBuffer:
|
||||||
|
return "OomRowBuffer";
|
||||||
|
case BmpToMonoError::ShortReadRow:
|
||||||
|
return "ShortReadRow";
|
||||||
|
}
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
BmpToMonoError BmpToMono::convert24BitRotate90CCW(File& file, MonoBitmap& out, uint8_t threshold) {
|
||||||
|
return convert24BitImpl(file, out, threshold, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
BmpToMonoError BmpToMono::convert24BitImpl(File& f, MonoBitmap& out, uint8_t threshold, bool rotate90CCW) {
|
||||||
|
freeMonoBitmap(out);
|
||||||
|
|
||||||
|
if (!f) return BmpToMonoError::FileInvalid;
|
||||||
|
if (!f.seek(0)) return BmpToMonoError::SeekStartFailed;
|
||||||
|
|
||||||
|
// --- BMP FILE HEADER ---
|
||||||
|
const uint16_t bfType = readLE16(f);
|
||||||
|
if (bfType != 0x4D42) return BmpToMonoError::NotBMP;
|
||||||
|
|
||||||
|
(void)readLE32(f);
|
||||||
|
(void)readLE16(f);
|
||||||
|
(void)readLE16(f);
|
||||||
|
const uint32_t bfOffBits = readLE32(f);
|
||||||
|
|
||||||
|
// --- DIB HEADER ---
|
||||||
|
const uint32_t biSize = readLE32(f);
|
||||||
|
if (biSize < 40) return BmpToMonoError::DIBTooSmall;
|
||||||
|
|
||||||
|
const int32_t srcW = (int32_t)readLE32(f);
|
||||||
|
int32_t srcHRaw = (int32_t)readLE32(f);
|
||||||
|
const uint16_t planes = readLE16(f);
|
||||||
|
const uint16_t bpp = readLE16(f);
|
||||||
|
const uint32_t comp = readLE32(f);
|
||||||
|
|
||||||
|
if (planes != 1) return BmpToMonoError::BadPlanes;
|
||||||
|
if (bpp != 24) return BmpToMonoError::UnsupportedBpp;
|
||||||
|
if (comp != 0) return BmpToMonoError::UnsupportedCompression;
|
||||||
|
|
||||||
|
(void)readLE32(f);
|
||||||
|
(void)readLE32(f);
|
||||||
|
(void)readLE32(f);
|
||||||
|
(void)readLE32(f);
|
||||||
|
(void)readLE32(f);
|
||||||
|
|
||||||
|
if (srcW <= 0) return BmpToMonoError::BadDimensions;
|
||||||
|
|
||||||
|
const bool topDown = (srcHRaw < 0);
|
||||||
|
const int32_t srcH = topDown ? -srcHRaw : srcHRaw;
|
||||||
|
if (srcH <= 0) return BmpToMonoError::BadDimensions;
|
||||||
|
|
||||||
|
// Output dimensions
|
||||||
|
out.width = rotate90CCW ? (int)srcH : (int)srcW;
|
||||||
|
out.height = rotate90CCW ? (int)srcW : (int)srcH;
|
||||||
|
|
||||||
|
const size_t outBytesPerRow = (size_t)(out.width + 7) / 8;
|
||||||
|
out.len = outBytesPerRow * (size_t)out.height;
|
||||||
|
|
||||||
|
out.data = (uint8_t*)malloc(out.len);
|
||||||
|
if (!out.data) return BmpToMonoError::OomOutput;
|
||||||
|
memset(out.data, 0xFF, out.len);
|
||||||
|
|
||||||
|
// Source row stride (padded to 4 bytes)
|
||||||
|
const uint32_t srcBytesPerRow24 = (uint32_t)srcW * 3u;
|
||||||
|
const uint32_t srcRowStride = (srcBytesPerRow24 + 3u) & ~3u;
|
||||||
|
|
||||||
|
if (!f.seek(bfOffBits)) {
|
||||||
|
freeMonoBitmap(out);
|
||||||
|
return BmpToMonoError::SeekPixelDataFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t* rowBuf = (uint8_t*)malloc(srcRowStride);
|
||||||
|
if (!rowBuf) {
|
||||||
|
freeMonoBitmap(out);
|
||||||
|
return BmpToMonoError::OomRowBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int fileRow = 0; fileRow < (int)srcH; fileRow++) {
|
||||||
|
if (f.read(rowBuf, srcRowStride) != (int)srcRowStride) {
|
||||||
|
free(rowBuf);
|
||||||
|
freeMonoBitmap(out);
|
||||||
|
return BmpToMonoError::ShortReadRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int srcY = topDown ? fileRow : ((int)srcH - 1 - fileRow);
|
||||||
|
|
||||||
|
for (int srcX = 0; srcX < (int)srcW; srcX++) {
|
||||||
|
const uint8_t b = rowBuf[srcX * 3 + 0];
|
||||||
|
const uint8_t g = rowBuf[srcX * 3 + 1];
|
||||||
|
const uint8_t r = rowBuf[srcX * 3 + 2];
|
||||||
|
|
||||||
|
const uint8_t lum = (uint8_t)((77u * r + 150u * g + 29u * b) >> 8);
|
||||||
|
bool isBlack = (lum < threshold);
|
||||||
|
|
||||||
|
int outX, outY;
|
||||||
|
if (!rotate90CCW) {
|
||||||
|
outX = srcX;
|
||||||
|
outY = srcY;
|
||||||
|
} else {
|
||||||
|
// 90° counter-clockwise: (x,y) -> (y, w-1-x)
|
||||||
|
outX = srcY;
|
||||||
|
outY = (int)srcW - 1 - srcX;
|
||||||
|
}
|
||||||
|
|
||||||
|
setMonoPixel(out.data, out.width, outX, outY, isBlack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
free(rowBuf);
|
||||||
|
return BmpToMonoError::Ok;
|
||||||
|
}
|
||||||
58
lib/BmpToMono/BmpToMono.h
Normal file
58
lib/BmpToMono/BmpToMono.h
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <FS.h>
|
||||||
|
|
||||||
|
struct MonoBitmap {
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
size_t len = 0; // bytesPerRow * height
|
||||||
|
uint8_t* data = nullptr; // row-aligned, MSB-first, 1=white 0=black
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class BmpToMonoError : uint8_t {
|
||||||
|
Ok = 0,
|
||||||
|
FileInvalid,
|
||||||
|
SeekStartFailed,
|
||||||
|
|
||||||
|
NotBMP,
|
||||||
|
DIBTooSmall,
|
||||||
|
|
||||||
|
BadPlanes,
|
||||||
|
UnsupportedBpp,
|
||||||
|
UnsupportedCompression,
|
||||||
|
|
||||||
|
BadDimensions,
|
||||||
|
|
||||||
|
SeekPixelDataFailed,
|
||||||
|
OomOutput,
|
||||||
|
OomRowBuffer,
|
||||||
|
ShortReadRow,
|
||||||
|
};
|
||||||
|
|
||||||
|
class BmpToMono {
|
||||||
|
public:
|
||||||
|
// Rotate 90° counter-clockwise: (w,h) -> (h,w)
|
||||||
|
// Used for converting portrait BMP (480x800) into landscape framebuffer (800x480)
|
||||||
|
static BmpToMonoError convert24BitRotate90CCW(File& file, MonoBitmap& out, uint8_t threshold = 160);
|
||||||
|
|
||||||
|
static void freeMonoBitmap(MonoBitmap& bmp);
|
||||||
|
static const char* errorToString(BmpToMonoError err);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static uint16_t readLE16(File& f);
|
||||||
|
static uint32_t readLE32(File& f);
|
||||||
|
|
||||||
|
// Writes a single pixel into a row-aligned 1bpp buffer (MSB-first), 0=black, 1=white
|
||||||
|
static inline void setMonoPixel(uint8_t* buf, int w, int x, int y, bool isBlack) {
|
||||||
|
const size_t bytesPerRow = (size_t)(w + 7) / 8;
|
||||||
|
const size_t idx = (size_t)y * bytesPerRow + (size_t)(x >> 3);
|
||||||
|
const uint8_t mask = (uint8_t)(0x80u >> (x & 7));
|
||||||
|
if (isBlack)
|
||||||
|
buf[idx] &= (uint8_t)~mask;
|
||||||
|
else
|
||||||
|
buf[idx] |= mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
static BmpToMonoError convert24BitImpl(File& file, MonoBitmap& out, uint8_t threshold, bool rotate90CW);
|
||||||
|
};
|
||||||
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
#include <Utf8.h>
|
#include <Utf8.h>
|
||||||
|
|
||||||
|
#include "BmpToMono.h"
|
||||||
|
|
||||||
void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { fontMap.insert({fontId, font}); }
|
void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { fontMap.insert({fontId, font}); }
|
||||||
|
|
||||||
void GfxRenderer::drawPixel(const int x, const int y, const bool state) const {
|
void GfxRenderer::drawPixel(const int x, const int y, const bool state) const {
|
||||||
@ -119,6 +121,37 @@ void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, co
|
|||||||
einkDisplay.drawImage(bitmap, y, x, height, width);
|
einkDisplay.drawImage(bitmap, y, x, height, width);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool GfxRenderer::drawFullScreenBmp(File& file) {
|
||||||
|
if (!file) {
|
||||||
|
Serial.printf("[%lu] [GFX] drawFullScreenBmp: invalid file\n", millis());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
file.seek(0); // Ensure we're at the start of the file
|
||||||
|
|
||||||
|
MonoBitmap bmp;
|
||||||
|
auto err = BmpToMono::convert24BitRotate90CCW(file, bmp);
|
||||||
|
|
||||||
|
if (err != BmpToMonoError::Ok) {
|
||||||
|
Serial.printf("[%lu] [GFX] BMP convert failed: %s\n", millis(), BmpToMono::errorToString(err));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hard requirement: must match panel exactly
|
||||||
|
if (bmp.width != EInkDisplay::DISPLAY_WIDTH || bmp.height != EInkDisplay::DISPLAY_HEIGHT) {
|
||||||
|
Serial.printf("[%lu] [GFX] drawFullScreenBmp: rotated BMP size %dx%d does not match panel %dx%d\n", millis(),
|
||||||
|
bmp.width, bmp.height, EInkDisplay::DISPLAY_WIDTH, EInkDisplay::DISPLAY_HEIGHT);
|
||||||
|
BmpToMono::freeMonoBitmap(bmp);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full-screen blit
|
||||||
|
einkDisplay.drawImage(bmp.data, 0, 0, bmp.width, bmp.height);
|
||||||
|
|
||||||
|
BmpToMono::freeMonoBitmap(bmp);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void GfxRenderer::clearScreen(const uint8_t color) const { einkDisplay.clearScreen(color); }
|
void GfxRenderer::clearScreen(const uint8_t color) const { einkDisplay.clearScreen(color); }
|
||||||
|
|
||||||
void GfxRenderer::invertScreen() const {
|
void GfxRenderer::invertScreen() const {
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <EInkDisplay.h>
|
#include <EInkDisplay.h>
|
||||||
#include <EpdFontFamily.h>
|
#include <EpdFontFamily.h>
|
||||||
|
#include <FS.h>
|
||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
|
|
||||||
@ -36,6 +37,7 @@ class GfxRenderer {
|
|||||||
void drawRect(int x, int y, int width, int height, bool state = true) const;
|
void drawRect(int x, int y, int width, int height, bool state = true) const;
|
||||||
void fillRect(int x, int y, int width, int height, bool state = true) const;
|
void fillRect(int x, int y, int width, int height, bool state = true) const;
|
||||||
void drawImage(const uint8_t bitmap[], int x, int y, int width, int height) const;
|
void drawImage(const uint8_t bitmap[], int x, int y, int width, int height) const;
|
||||||
|
bool drawFullScreenBmp(File& file);
|
||||||
|
|
||||||
// Text
|
// Text
|
||||||
int getTextWidth(int fontId, const char* text, EpdFontStyle style = REGULAR) const;
|
int getTextWidth(int fontId, const char* text, EpdFontStyle style = REGULAR) const;
|
||||||
|
|||||||
@ -199,6 +199,12 @@ void setup() {
|
|||||||
// SD Card Initialization
|
// SD Card Initialization
|
||||||
SD.begin(SD_SPI_CS, SPI, SPI_FQ);
|
SD.begin(SD_SPI_CS, SPI, SPI_FQ);
|
||||||
|
|
||||||
|
// TODO: Remove testing code before merging
|
||||||
|
// exitScreen();
|
||||||
|
// enterNewScreen(new SleepScreen(renderer, inputManager));
|
||||||
|
// waitForPowerRelease();
|
||||||
|
// return;
|
||||||
|
|
||||||
appState.loadFromFile();
|
appState.loadFromFile();
|
||||||
if (!appState.openEpubPath.empty()) {
|
if (!appState.openEpubPath.empty()) {
|
||||||
auto epub = loadEpub(appState.openEpubPath);
|
auto epub = loadEpub(appState.openEpubPath);
|
||||||
|
|||||||
@ -2,10 +2,23 @@
|
|||||||
|
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
|
|
||||||
|
#include "SD.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "images/CrossLarge.h"
|
#include "images/CrossLarge.h"
|
||||||
|
|
||||||
void SleepScreen::onEnter() {
|
void SleepScreen::onEnter() {
|
||||||
|
// Look for sleep.bmp on the root of the sd card to determine if we should
|
||||||
|
// render a custom sleep screen instead of the default.
|
||||||
|
auto file = SD.open("/sleep.bmp");
|
||||||
|
if (file) {
|
||||||
|
renderCustomSleepScreen(file);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderDefaultSleepScreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SleepScreen::renderDefaultSleepScreen() {
|
||||||
const auto pageWidth = GfxRenderer::getScreenWidth();
|
const auto pageWidth = GfxRenderer::getScreenWidth();
|
||||||
const auto pageHeight = GfxRenderer::getScreenHeight();
|
const auto pageHeight = GfxRenderer::getScreenHeight();
|
||||||
|
|
||||||
@ -16,3 +29,15 @@ void SleepScreen::onEnter() {
|
|||||||
renderer.invertScreen();
|
renderer.invertScreen();
|
||||||
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
|
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SleepScreen::renderCustomSleepScreen(File& file) {
|
||||||
|
renderer.clearScreen();
|
||||||
|
bool didImageDrawSuccessfully = renderer.drawFullScreenBmp(file);
|
||||||
|
|
||||||
|
if (!didImageDrawSuccessfully) {
|
||||||
|
renderer.clearScreen();
|
||||||
|
renderer.drawCenteredText(UI_FONT_ID, GfxRenderer::getScreenHeight() / 2, "BAD CUSTOM SLEEP SCREEN", true, BOLD);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
#include "SD.h"
|
||||||
#include "Screen.h"
|
#include "Screen.h"
|
||||||
|
|
||||||
class SleepScreen final : public Screen {
|
class SleepScreen final : public Screen {
|
||||||
public:
|
public:
|
||||||
explicit SleepScreen(GfxRenderer& renderer, InputManager& inputManager) : Screen(renderer, inputManager) {}
|
explicit SleepScreen(GfxRenderer& renderer, InputManager& inputManager) : Screen(renderer, inputManager) {}
|
||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
|
void renderDefaultSleepScreen();
|
||||||
|
void renderCustomSleepScreen(File& file);
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user